Changeset 3783407
- Timestamp:
- 11/04/2026 08:28:53 AM (6 hours ago)
- Location:
- frontman-agentic-ai-editor
- Files:
-
- 398 added
- 17 edited
-
tags/5.6.0 (added)
-
tags/5.6.0/LICENSE (added)
-
tags/5.6.0/assets (added)
-
tags/5.6.0/assets/bridge-licenses.txt (added)
-
tags/5.6.0/assets/bridge.js (added)
-
tags/5.6.0/assets/frontman-admin-menu.css (added)
-
tags/5.6.0/assets/frontman-logo.svg (added)
-
tags/5.6.0/assets/frontman-menu-icon.svg (added)
-
tags/5.6.0/assets/frontman-page.css (added)
-
tags/5.6.0/assets/frontman-page.js (added)
-
tags/5.6.0/frontman.php (added)
-
tags/5.6.0/includes (added)
-
tags/5.6.0/includes/class-frontman-auth.php (added)
-
tags/5.6.0/includes/class-frontman-elementor-data.php (added)
-
tags/5.6.0/includes/class-frontman-plugin-dependencies.php (added)
-
tags/5.6.0/includes/class-frontman-router.php (added)
-
tags/5.6.0/includes/class-frontman-sentry.php (added)
-
tags/5.6.0/includes/class-frontman-settings.php (added)
-
tags/5.6.0/includes/class-frontman-tools.php (added)
-
tags/5.6.0/includes/class-frontman-ui.php (added)
-
tags/5.6.0/readme.txt (added)
-
tags/5.6.0/tools (added)
-
tags/5.6.0/tools/class-tool-301-redirects.php (added)
-
tags/5.6.0/tools/class-tool-angie.php (added)
-
tags/5.6.0/tools/class-tool-blocks.php (added)
-
tags/5.6.0/tools/class-tool-cache.php (added)
-
tags/5.6.0/tools/class-tool-directorist.php (added)
-
tags/5.6.0/tools/class-tool-elementor.php (added)
-
tags/5.6.0/tools/class-tool-media.php (added)
-
tags/5.6.0/tools/class-tool-menus.php (added)
-
tags/5.6.0/tools/class-tool-options.php (added)
-
tags/5.6.0/tools/class-tool-polylang.php (added)
-
tags/5.6.0/tools/class-tool-posts.php (added)
-
tags/5.6.0/tools/class-tool-public-files.php (added)
-
tags/5.6.0/tools/class-tool-redirection.php (added)
-
tags/5.6.0/tools/class-tool-seo.php (added)
-
tags/5.6.0/tools/class-tool-templates.php (added)
-
tags/5.6.0/tools/class-tool-users.php (added)
-
tags/5.6.0/tools/class-tool-widgets.php (added)
-
tags/5.6.0/tools/class-tool-woocommerce.php (added)
-
tags/5.6.0/uninstall.php (added)
-
tags/5.6.0/vendor (added)
-
tags/5.6.0/vendor/autoload.php (added)
-
tags/5.6.0/vendor/composer (added)
-
tags/5.6.0/vendor/composer/ClassLoader.php (added)
-
tags/5.6.0/vendor/composer/InstalledVersions.php (added)
-
tags/5.6.0/vendor/composer/LICENSE (added)
-
tags/5.6.0/vendor/composer/autoload_classmap.php (added)
-
tags/5.6.0/vendor/composer/autoload_files.php (added)
-
tags/5.6.0/vendor/composer/autoload_namespaces.php (added)
-
tags/5.6.0/vendor/composer/autoload_psr4.php (added)
-
tags/5.6.0/vendor/composer/autoload_real.php (added)
-
tags/5.6.0/vendor/composer/autoload_static.php (added)
-
tags/5.6.0/vendor/composer/installed.json (added)
-
tags/5.6.0/vendor/composer/installed.php (added)
-
tags/5.6.0/vendor/composer/platform_check.php (added)
-
tags/5.6.0/vendor/guzzlehttp (added)
-
tags/5.6.0/vendor/guzzlehttp/psr7 (added)
-
tags/5.6.0/vendor/guzzlehttp/psr7/LICENSE (added)
-
tags/5.6.0/vendor/guzzlehttp/psr7/composer.json (added)
-
tags/5.6.0/vendor/guzzlehttp/psr7/src (added)
-
tags/5.6.0/vendor/guzzlehttp/psr7/src/AppendStream.php (added)
-
tags/5.6.0/vendor/guzzlehttp/psr7/src/BufferStream.php (added)
-
tags/5.6.0/vendor/guzzlehttp/psr7/src/CachingStream.php (added)
-
tags/5.6.0/vendor/guzzlehttp/psr7/src/DiagnosticValue.php (added)
-
tags/5.6.0/vendor/guzzlehttp/psr7/src/DroppingStream.php (added)
-
tags/5.6.0/vendor/guzzlehttp/psr7/src/Exception (added)
-
tags/5.6.0/vendor/guzzlehttp/psr7/src/Exception/MalformedUriException.php (added)
-
tags/5.6.0/vendor/guzzlehttp/psr7/src/Exception/TimeoutException.php (added)
-
tags/5.6.0/vendor/guzzlehttp/psr7/src/FnStream.php (added)
-
tags/5.6.0/vendor/guzzlehttp/psr7/src/Header.php (added)
-
tags/5.6.0/vendor/guzzlehttp/psr7/src/HttpFactory.php (added)
-
tags/5.6.0/vendor/guzzlehttp/psr7/src/InflateStream.php (added)
-
tags/5.6.0/vendor/guzzlehttp/psr7/src/Integers.php (added)
-
tags/5.6.0/vendor/guzzlehttp/psr7/src/LazyOpenStream.php (added)
-
tags/5.6.0/vendor/guzzlehttp/psr7/src/LimitStream.php (added)
-
tags/5.6.0/vendor/guzzlehttp/psr7/src/Message.php (added)
-
tags/5.6.0/vendor/guzzlehttp/psr7/src/MessageParser.php (added)
-
tags/5.6.0/vendor/guzzlehttp/psr7/src/MessageTrait.php (added)
-
tags/5.6.0/vendor/guzzlehttp/psr7/src/MimeType.php (added)
-
tags/5.6.0/vendor/guzzlehttp/psr7/src/MultipartStream.php (added)
-
tags/5.6.0/vendor/guzzlehttp/psr7/src/NoSeekStream.php (added)
-
tags/5.6.0/vendor/guzzlehttp/psr7/src/NonSerializableStreamTrait.php (added)
-
tags/5.6.0/vendor/guzzlehttp/psr7/src/PumpStream.php (added)
-
tags/5.6.0/vendor/guzzlehttp/psr7/src/Query.php (added)
-
tags/5.6.0/vendor/guzzlehttp/psr7/src/Request.php (added)
-
tags/5.6.0/vendor/guzzlehttp/psr7/src/Response.php (added)
-
tags/5.6.0/vendor/guzzlehttp/psr7/src/Rfc3986.php (added)
-
tags/5.6.0/vendor/guzzlehttp/psr7/src/Rfc9110.php (added)
-
tags/5.6.0/vendor/guzzlehttp/psr7/src/Rfc9112.php (added)
-
tags/5.6.0/vendor/guzzlehttp/psr7/src/ServerRequest.php (added)
-
tags/5.6.0/vendor/guzzlehttp/psr7/src/ServerRequestGlobalsFactory.php (added)
-
tags/5.6.0/vendor/guzzlehttp/psr7/src/Stream.php (added)
-
tags/5.6.0/vendor/guzzlehttp/psr7/src/StreamDecoratorTrait.php (added)
-
tags/5.6.0/vendor/guzzlehttp/psr7/src/StreamTimeout.php (added)
-
tags/5.6.0/vendor/guzzlehttp/psr7/src/StreamWrapper.php (added)
-
tags/5.6.0/vendor/guzzlehttp/psr7/src/UploadedFile.php (added)
-
tags/5.6.0/vendor/guzzlehttp/psr7/src/UploadedFileNormalizer.php (added)
-
tags/5.6.0/vendor/guzzlehttp/psr7/src/Uri.php (added)
-
tags/5.6.0/vendor/guzzlehttp/psr7/src/UriComparator.php (added)
-
tags/5.6.0/vendor/guzzlehttp/psr7/src/UriNormalizer.php (added)
-
tags/5.6.0/vendor/guzzlehttp/psr7/src/UriParser.php (added)
-
tags/5.6.0/vendor/guzzlehttp/psr7/src/UriResolver.php (added)
-
tags/5.6.0/vendor/guzzlehttp/psr7/src/Utils.php (added)
-
tags/5.6.0/vendor/jean85 (added)
-
tags/5.6.0/vendor/jean85/pretty-package-versions (added)
-
tags/5.6.0/vendor/jean85/pretty-package-versions/LICENSE (added)
-
tags/5.6.0/vendor/jean85/pretty-package-versions/composer.json (added)
-
tags/5.6.0/vendor/jean85/pretty-package-versions/rector.php (added)
-
tags/5.6.0/vendor/jean85/pretty-package-versions/src (added)
-
tags/5.6.0/vendor/jean85/pretty-package-versions/src/Exception (added)
-
tags/5.6.0/vendor/jean85/pretty-package-versions/src/Exception/ProvidedPackageException.php (added)
-
tags/5.6.0/vendor/jean85/pretty-package-versions/src/Exception/ReplacedPackageException.php (added)
-
tags/5.6.0/vendor/jean85/pretty-package-versions/src/Exception/VersionMissingExceptionInterface.php (added)
-
tags/5.6.0/vendor/jean85/pretty-package-versions/src/PrettyVersions.php (added)
-
tags/5.6.0/vendor/jean85/pretty-package-versions/src/Version.php (added)
-
tags/5.6.0/vendor/psr (added)
-
tags/5.6.0/vendor/psr/http-factory (added)
-
tags/5.6.0/vendor/psr/http-factory/LICENSE (added)
-
tags/5.6.0/vendor/psr/http-factory/composer.json (added)
-
tags/5.6.0/vendor/psr/http-factory/src (added)
-
tags/5.6.0/vendor/psr/http-factory/src/RequestFactoryInterface.php (added)
-
tags/5.6.0/vendor/psr/http-factory/src/ResponseFactoryInterface.php (added)
-
tags/5.6.0/vendor/psr/http-factory/src/ServerRequestFactoryInterface.php (added)
-
tags/5.6.0/vendor/psr/http-factory/src/StreamFactoryInterface.php (added)
-
tags/5.6.0/vendor/psr/http-factory/src/UploadedFileFactoryInterface.php (added)
-
tags/5.6.0/vendor/psr/http-factory/src/UriFactoryInterface.php (added)
-
tags/5.6.0/vendor/psr/http-message (added)
-
tags/5.6.0/vendor/psr/http-message/LICENSE (added)
-
tags/5.6.0/vendor/psr/http-message/composer.json (added)
-
tags/5.6.0/vendor/psr/http-message/src (added)
-
tags/5.6.0/vendor/psr/http-message/src/MessageInterface.php (added)
-
tags/5.6.0/vendor/psr/http-message/src/RequestInterface.php (added)
-
tags/5.6.0/vendor/psr/http-message/src/ResponseInterface.php (added)
-
tags/5.6.0/vendor/psr/http-message/src/ServerRequestInterface.php (added)
-
tags/5.6.0/vendor/psr/http-message/src/StreamInterface.php (added)
-
tags/5.6.0/vendor/psr/http-message/src/UploadedFileInterface.php (added)
-
tags/5.6.0/vendor/psr/http-message/src/UriInterface.php (added)
-
tags/5.6.0/vendor/psr/log (added)
-
tags/5.6.0/vendor/psr/log/LICENSE (added)
-
tags/5.6.0/vendor/psr/log/Psr (added)
-
tags/5.6.0/vendor/psr/log/Psr/Log (added)
-
tags/5.6.0/vendor/psr/log/Psr/Log/AbstractLogger.php (added)
-
tags/5.6.0/vendor/psr/log/Psr/Log/InvalidArgumentException.php (added)
-
tags/5.6.0/vendor/psr/log/Psr/Log/LogLevel.php (added)
-
tags/5.6.0/vendor/psr/log/Psr/Log/LoggerAwareInterface.php (added)
-
tags/5.6.0/vendor/psr/log/Psr/Log/LoggerAwareTrait.php (added)
-
tags/5.6.0/vendor/psr/log/Psr/Log/LoggerInterface.php (added)
-
tags/5.6.0/vendor/psr/log/Psr/Log/LoggerTrait.php (added)
-
tags/5.6.0/vendor/psr/log/Psr/Log/NullLogger.php (added)
-
tags/5.6.0/vendor/psr/log/Psr/Log/Test (added)
-
tags/5.6.0/vendor/psr/log/Psr/Log/Test/DummyTest.php (added)
-
tags/5.6.0/vendor/psr/log/Psr/Log/Test/LoggerInterfaceTest.php (added)
-
tags/5.6.0/vendor/psr/log/Psr/Log/Test/TestLogger.php (added)
-
tags/5.6.0/vendor/psr/log/composer.json (added)
-
tags/5.6.0/vendor/sentry (added)
-
tags/5.6.0/vendor/sentry/sentry (added)
-
tags/5.6.0/vendor/sentry/sentry/AUTHORS (added)
-
tags/5.6.0/vendor/sentry/sentry/LICENSE (added)
-
tags/5.6.0/vendor/sentry/sentry/composer.json (added)
-
tags/5.6.0/vendor/sentry/sentry/src (added)
-
tags/5.6.0/vendor/sentry/sentry/src/Agent (added)
-
tags/5.6.0/vendor/sentry/sentry/src/Agent/Transport (added)
-
tags/5.6.0/vendor/sentry/sentry/src/Agent/Transport/AgentClient.php (added)
-
tags/5.6.0/vendor/sentry/sentry/src/Agent/Transport/AgentClientBuilder.php (added)
-
tags/5.6.0/vendor/sentry/sentry/src/Attributes (added)
-
tags/5.6.0/vendor/sentry/sentry/src/Attributes/Attribute.php (added)
-
tags/5.6.0/vendor/sentry/sentry/src/Attributes/AttributeBag.php (added)
-
tags/5.6.0/vendor/sentry/sentry/src/Breadcrumb.php (added)
-
tags/5.6.0/vendor/sentry/sentry/src/CheckIn.php (added)
-
tags/5.6.0/vendor/sentry/sentry/src/CheckInStatus.php (added)
-
tags/5.6.0/vendor/sentry/sentry/src/Client.php (added)
-
tags/5.6.0/vendor/sentry/sentry/src/ClientBuilder.php (added)
-
tags/5.6.0/vendor/sentry/sentry/src/ClientInterface.php (added)
-
tags/5.6.0/vendor/sentry/sentry/src/ClientReport (added)
-
tags/5.6.0/vendor/sentry/sentry/src/ClientReport/ClientReportAggregator.php (added)
-
tags/5.6.0/vendor/sentry/sentry/src/ClientReport/DiscardedEvent.php (added)
-
tags/5.6.0/vendor/sentry/sentry/src/ClientReport/Reason.php (added)
-
tags/5.6.0/vendor/sentry/sentry/src/Context (added)
-
tags/5.6.0/vendor/sentry/sentry/src/Context/OsContext.php (added)
-
tags/5.6.0/vendor/sentry/sentry/src/Context/RuntimeContext.php (added)
-
tags/5.6.0/vendor/sentry/sentry/src/Dsn.php (added)
-
tags/5.6.0/vendor/sentry/sentry/src/ErrorHandler.php (added)
-
tags/5.6.0/vendor/sentry/sentry/src/Event.php (added)
-
tags/5.6.0/vendor/sentry/sentry/src/EventHint.php (added)
-
tags/5.6.0/vendor/sentry/sentry/src/EventId.php (added)
-
tags/5.6.0/vendor/sentry/sentry/src/EventType.php (added)
-
tags/5.6.0/vendor/sentry/sentry/src/Exception (added)
-
tags/5.6.0/vendor/sentry/sentry/src/Exception/EventCreationException.php (added)
-
tags/5.6.0/vendor/sentry/sentry/src/Exception/FatalErrorException.php (added)
-
tags/5.6.0/vendor/sentry/sentry/src/Exception/JsonException.php (added)
-
tags/5.6.0/vendor/sentry/sentry/src/Exception/SilencedErrorException.php (added)
-
tags/5.6.0/vendor/sentry/sentry/src/ExceptionDataBag.php (added)
-
tags/5.6.0/vendor/sentry/sentry/src/ExceptionMechanism.php (added)
-
tags/5.6.0/vendor/sentry/sentry/src/Frame.php (added)
-
tags/5.6.0/vendor/sentry/sentry/src/FrameBuilder.php (added)
-
tags/5.6.0/vendor/sentry/sentry/src/HttpClient (added)
-
tags/5.6.0/vendor/sentry/sentry/src/HttpClient/HttpClient.php (added)
-
tags/5.6.0/vendor/sentry/sentry/src/HttpClient/HttpClientInterface.php (added)
-
tags/5.6.0/vendor/sentry/sentry/src/HttpClient/Request.php (added)
-
tags/5.6.0/vendor/sentry/sentry/src/HttpClient/Response.php (added)
-
tags/5.6.0/vendor/sentry/sentry/src/Integration (added)
-
tags/5.6.0/vendor/sentry/sentry/src/Integration/AbstractErrorListenerIntegration.php (added)
-
tags/5.6.0/vendor/sentry/sentry/src/Integration/EnvironmentIntegration.php (added)
-
tags/5.6.0/vendor/sentry/sentry/src/Integration/ErrorListenerIntegration.php (added)
-
tags/5.6.0/vendor/sentry/sentry/src/Integration/ExceptionListenerIntegration.php (added)
-
tags/5.6.0/vendor/sentry/sentry/src/Integration/FatalErrorListenerIntegration.php (added)
-
tags/5.6.0/vendor/sentry/sentry/src/Integration/FrameContextifierIntegration.php (added)
-
tags/5.6.0/vendor/sentry/sentry/src/Integration/IntegrationInterface.php (added)
-
tags/5.6.0/vendor/sentry/sentry/src/Integration/IntegrationRegistry.php (added)
-
tags/5.6.0/vendor/sentry/sentry/src/Integration/ModulesIntegration.php (added)
-
tags/5.6.0/vendor/sentry/sentry/src/Integration/OTLPIntegration.php (added)
-
tags/5.6.0/vendor/sentry/sentry/src/Integration/OptionAwareIntegrationInterface.php (added)
-
tags/5.6.0/vendor/sentry/sentry/src/Integration/RequestFetcher.php (added)
-
tags/5.6.0/vendor/sentry/sentry/src/Integration/RequestFetcherInterface.php (added)
-
tags/5.6.0/vendor/sentry/sentry/src/Integration/RequestIntegration.php (added)
-
tags/5.6.0/vendor/sentry/sentry/src/Integration/TransactionIntegration.php (added)
-
tags/5.6.0/vendor/sentry/sentry/src/Logger (added)
-
tags/5.6.0/vendor/sentry/sentry/src/Logger/DebugFileLogger.php (added)
-
tags/5.6.0/vendor/sentry/sentry/src/Logger/DebugLogger.php (added)
-
tags/5.6.0/vendor/sentry/sentry/src/Logger/DebugStdOutLogger.php (added)
-
tags/5.6.0/vendor/sentry/sentry/src/Logs (added)
-
tags/5.6.0/vendor/sentry/sentry/src/Logs/Log.php (added)
-
tags/5.6.0/vendor/sentry/sentry/src/Logs/LogLevel.php (added)
-
tags/5.6.0/vendor/sentry/sentry/src/Logs/Logs.php (added)
-
tags/5.6.0/vendor/sentry/sentry/src/Logs/LogsAggregator.php (added)
-
tags/5.6.0/vendor/sentry/sentry/src/Metrics (added)
-
tags/5.6.0/vendor/sentry/sentry/src/Metrics/Metrics.php (added)
-
tags/5.6.0/vendor/sentry/sentry/src/Metrics/MetricsAggregator.php (added)
-
tags/5.6.0/vendor/sentry/sentry/src/Metrics/TraceMetrics.php (added)
-
tags/5.6.0/vendor/sentry/sentry/src/Metrics/Types (added)
-
tags/5.6.0/vendor/sentry/sentry/src/Metrics/Types/CounterMetric.php (added)
-
tags/5.6.0/vendor/sentry/sentry/src/Metrics/Types/DistributionMetric.php (added)
-
tags/5.6.0/vendor/sentry/sentry/src/Metrics/Types/GaugeMetric.php (added)
-
tags/5.6.0/vendor/sentry/sentry/src/Metrics/Types/Metric.php (added)
-
tags/5.6.0/vendor/sentry/sentry/src/MonitorConfig.php (added)
-
tags/5.6.0/vendor/sentry/sentry/src/MonitorSchedule.php (added)
-
tags/5.6.0/vendor/sentry/sentry/src/MonitorScheduleUnit.php (added)
-
tags/5.6.0/vendor/sentry/sentry/src/Monolog (added)
-
tags/5.6.0/vendor/sentry/sentry/src/Monolog/BreadcrumbHandler.php (added)
-
tags/5.6.0/vendor/sentry/sentry/src/Monolog/CompatibilityLogLevelTrait.php (added)
-
tags/5.6.0/vendor/sentry/sentry/src/Monolog/CompatibilityProcessingHandlerTrait.php (added)
-
tags/5.6.0/vendor/sentry/sentry/src/Monolog/ExceptionToSentryIssueHandler.php (added)
-
tags/5.6.0/vendor/sentry/sentry/src/Monolog/Handler.php (added)
-
tags/5.6.0/vendor/sentry/sentry/src/Monolog/LogToSentryIssueHandler.php (added)
-
tags/5.6.0/vendor/sentry/sentry/src/Monolog/LogsHandler.php (added)
-
tags/5.6.0/vendor/sentry/sentry/src/OpenTelemetry (added)
-
tags/5.6.0/vendor/sentry/sentry/src/OpenTelemetry/Propagation (added)
-
tags/5.6.0/vendor/sentry/sentry/src/OpenTelemetry/Propagation/SentryPropagator.php (added)
-
tags/5.6.0/vendor/sentry/sentry/src/Options.php (added)
-
tags/5.6.0/vendor/sentry/sentry/src/OptionsResolver.php (added)
-
tags/5.6.0/vendor/sentry/sentry/src/Profiling (added)
-
tags/5.6.0/vendor/sentry/sentry/src/Profiling/Profile.php (added)
-
tags/5.6.0/vendor/sentry/sentry/src/Profiling/Profiler.php (added)
-
tags/5.6.0/vendor/sentry/sentry/src/SentrySdk.php (added)
-
tags/5.6.0/vendor/sentry/sentry/src/Serializer (added)
-
tags/5.6.0/vendor/sentry/sentry/src/Serializer/AbstractSerializer.php (added)
-
tags/5.6.0/vendor/sentry/sentry/src/Serializer/EnvelopItems (added)
-
tags/5.6.0/vendor/sentry/sentry/src/Serializer/EnvelopItems/CheckInItem.php (added)
-
tags/5.6.0/vendor/sentry/sentry/src/Serializer/EnvelopItems/ClientReportItem.php (added)
-
tags/5.6.0/vendor/sentry/sentry/src/Serializer/EnvelopItems/EnvelopeItemInterface.php (added)
-
tags/5.6.0/vendor/sentry/sentry/src/Serializer/EnvelopItems/EventItem.php (added)
-
tags/5.6.0/vendor/sentry/sentry/src/Serializer/EnvelopItems/LogsItem.php (added)
-
tags/5.6.0/vendor/sentry/sentry/src/Serializer/EnvelopItems/MetricsItem.php (added)
-
tags/5.6.0/vendor/sentry/sentry/src/Serializer/EnvelopItems/ProfileItem.php (added)
-
tags/5.6.0/vendor/sentry/sentry/src/Serializer/EnvelopItems/TransactionItem.php (added)
-
tags/5.6.0/vendor/sentry/sentry/src/Serializer/PayloadSerializer.php (added)
-
tags/5.6.0/vendor/sentry/sentry/src/Serializer/PayloadSerializerInterface.php (added)
-
tags/5.6.0/vendor/sentry/sentry/src/Serializer/RepresentationSerializer.php (added)
-
tags/5.6.0/vendor/sentry/sentry/src/Serializer/RepresentationSerializerInterface.php (added)
-
tags/5.6.0/vendor/sentry/sentry/src/Serializer/SerializableInterface.php (added)
-
tags/5.6.0/vendor/sentry/sentry/src/Serializer/Serializer.php (added)
-
tags/5.6.0/vendor/sentry/sentry/src/Serializer/SerializerInterface.php (added)
-
tags/5.6.0/vendor/sentry/sentry/src/Serializer/Traits (added)
-
tags/5.6.0/vendor/sentry/sentry/src/Serializer/Traits/BreadcrumbSeralizerTrait.php (added)
-
tags/5.6.0/vendor/sentry/sentry/src/Serializer/Traits/StacktraceFrameSeralizerTrait.php (added)
-
tags/5.6.0/vendor/sentry/sentry/src/Severity.php (added)
-
tags/5.6.0/vendor/sentry/sentry/src/Spotlight (added)
-
tags/5.6.0/vendor/sentry/sentry/src/Spotlight/SpotlightClient.php (added)
-
tags/5.6.0/vendor/sentry/sentry/src/Stacktrace.php (added)
-
tags/5.6.0/vendor/sentry/sentry/src/StacktraceBuilder.php (added)
-
tags/5.6.0/vendor/sentry/sentry/src/State (added)
-
tags/5.6.0/vendor/sentry/sentry/src/State/Hub.php (added)
-
tags/5.6.0/vendor/sentry/sentry/src/State/HubAdapter.php (added)
-
tags/5.6.0/vendor/sentry/sentry/src/State/HubInterface.php (added)
-
tags/5.6.0/vendor/sentry/sentry/src/State/Layer.php (added)
-
tags/5.6.0/vendor/sentry/sentry/src/State/RuntimeContext.php (added)
-
tags/5.6.0/vendor/sentry/sentry/src/State/RuntimeContextManager.php (added)
-
tags/5.6.0/vendor/sentry/sentry/src/State/Scope.php (added)
-
tags/5.6.0/vendor/sentry/sentry/src/Tracing (added)
-
tags/5.6.0/vendor/sentry/sentry/src/Tracing/DynamicSamplingContext.php (added)
-
tags/5.6.0/vendor/sentry/sentry/src/Tracing/GuzzleTracingMiddleware.php (added)
-
tags/5.6.0/vendor/sentry/sentry/src/Tracing/PropagationContext.php (added)
-
tags/5.6.0/vendor/sentry/sentry/src/Tracing/SamplingContext.php (added)
-
tags/5.6.0/vendor/sentry/sentry/src/Tracing/Span.php (added)
-
tags/5.6.0/vendor/sentry/sentry/src/Tracing/SpanContext.php (added)
-
tags/5.6.0/vendor/sentry/sentry/src/Tracing/SpanId.php (added)
-
tags/5.6.0/vendor/sentry/sentry/src/Tracing/SpanRecorder.php (added)
-
tags/5.6.0/vendor/sentry/sentry/src/Tracing/SpanStatus.php (added)
-
tags/5.6.0/vendor/sentry/sentry/src/Tracing/TraceId.php (added)
-
tags/5.6.0/vendor/sentry/sentry/src/Tracing/Traits (added)
-
tags/5.6.0/vendor/sentry/sentry/src/Tracing/Traits/TraceHeaderParserTrait.php (added)
-
tags/5.6.0/vendor/sentry/sentry/src/Tracing/Transaction.php (added)
-
tags/5.6.0/vendor/sentry/sentry/src/Tracing/TransactionContext.php (added)
-
tags/5.6.0/vendor/sentry/sentry/src/Tracing/TransactionMetadata.php (added)
-
tags/5.6.0/vendor/sentry/sentry/src/Tracing/TransactionSource.php (added)
-
tags/5.6.0/vendor/sentry/sentry/src/Transport (added)
-
tags/5.6.0/vendor/sentry/sentry/src/Transport/DataCategory.php (added)
-
tags/5.6.0/vendor/sentry/sentry/src/Transport/HttpTransport.php (added)
-
tags/5.6.0/vendor/sentry/sentry/src/Transport/RateLimiter.php (added)
-
tags/5.6.0/vendor/sentry/sentry/src/Transport/Result.php (added)
-
tags/5.6.0/vendor/sentry/sentry/src/Transport/ResultStatus.php (added)
-
tags/5.6.0/vendor/sentry/sentry/src/Transport/TransportInterface.php (added)
-
tags/5.6.0/vendor/sentry/sentry/src/Unit.php (added)
-
tags/5.6.0/vendor/sentry/sentry/src/UserDataBag.php (added)
-
tags/5.6.0/vendor/sentry/sentry/src/Util (added)
-
tags/5.6.0/vendor/sentry/sentry/src/Util/Arr.php (added)
-
tags/5.6.0/vendor/sentry/sentry/src/Util/CodeLocationResolver.php (added)
-
tags/5.6.0/vendor/sentry/sentry/src/Util/Http.php (added)
-
tags/5.6.0/vendor/sentry/sentry/src/Util/JSON.php (added)
-
tags/5.6.0/vendor/sentry/sentry/src/Util/PHPConfiguration.php (added)
-
tags/5.6.0/vendor/sentry/sentry/src/Util/PHPVersion.php (added)
-
tags/5.6.0/vendor/sentry/sentry/src/Util/PrefixStripper.php (added)
-
tags/5.6.0/vendor/sentry/sentry/src/Util/RingBuffer.php (added)
-
tags/5.6.0/vendor/sentry/sentry/src/Util/SentryUid.php (added)
-
tags/5.6.0/vendor/sentry/sentry/src/Util/Str.php (added)
-
tags/5.6.0/vendor/sentry/sentry/src/Util/TelemetryStorage.php (added)
-
tags/5.6.0/vendor/sentry/sentry/src/functions.php (added)
-
tags/5.6.0/vendor/symfony (added)
-
tags/5.6.0/vendor/symfony/deprecation-contracts (added)
-
tags/5.6.0/vendor/symfony/deprecation-contracts/LICENSE (added)
-
tags/5.6.0/vendor/symfony/deprecation-contracts/composer.json (added)
-
tags/5.6.0/vendor/symfony/deprecation-contracts/function.php (added)
-
tags/5.6.0/vendor/symfony/options-resolver (added)
-
tags/5.6.0/vendor/symfony/options-resolver/Debug (added)
-
tags/5.6.0/vendor/symfony/options-resolver/Debug/OptionsResolverIntrospector.php (added)
-
tags/5.6.0/vendor/symfony/options-resolver/Exception (added)
-
tags/5.6.0/vendor/symfony/options-resolver/Exception/AccessException.php (added)
-
tags/5.6.0/vendor/symfony/options-resolver/Exception/ExceptionInterface.php (added)
-
tags/5.6.0/vendor/symfony/options-resolver/Exception/InvalidArgumentException.php (added)
-
tags/5.6.0/vendor/symfony/options-resolver/Exception/InvalidOptionsException.php (added)
-
tags/5.6.0/vendor/symfony/options-resolver/Exception/MissingOptionsException.php (added)
-
tags/5.6.0/vendor/symfony/options-resolver/Exception/NoConfigurationException.php (added)
-
tags/5.6.0/vendor/symfony/options-resolver/Exception/NoSuchOptionException.php (added)
-
tags/5.6.0/vendor/symfony/options-resolver/Exception/OptionDefinitionException.php (added)
-
tags/5.6.0/vendor/symfony/options-resolver/Exception/UndefinedOptionsException.php (added)
-
tags/5.6.0/vendor/symfony/options-resolver/LICENSE (added)
-
tags/5.6.0/vendor/symfony/options-resolver/OptionConfigurator.php (added)
-
tags/5.6.0/vendor/symfony/options-resolver/Options.php (added)
-
tags/5.6.0/vendor/symfony/options-resolver/OptionsResolver.php (added)
-
tags/5.6.0/vendor/symfony/options-resolver/composer.json (added)
-
tags/5.6.0/vendor/symfony/polyfill-php73 (added)
-
tags/5.6.0/vendor/symfony/polyfill-php73/LICENSE (added)
-
tags/5.6.0/vendor/symfony/polyfill-php73/Php73.php (added)
-
tags/5.6.0/vendor/symfony/polyfill-php73/Resources (added)
-
tags/5.6.0/vendor/symfony/polyfill-php73/Resources/stubs (added)
-
tags/5.6.0/vendor/symfony/polyfill-php73/Resources/stubs/JsonException.php (added)
-
tags/5.6.0/vendor/symfony/polyfill-php73/bootstrap.php (added)
-
tags/5.6.0/vendor/symfony/polyfill-php73/composer.json (added)
-
tags/5.6.0/vendor/symfony/polyfill-php80 (added)
-
tags/5.6.0/vendor/symfony/polyfill-php80/LICENSE (added)
-
tags/5.6.0/vendor/symfony/polyfill-php80/Php80.php (added)
-
tags/5.6.0/vendor/symfony/polyfill-php80/PhpToken.php (added)
-
tags/5.6.0/vendor/symfony/polyfill-php80/Resources (added)
-
tags/5.6.0/vendor/symfony/polyfill-php80/Resources/stubs (added)
-
tags/5.6.0/vendor/symfony/polyfill-php80/Resources/stubs/Attribute.php (added)
-
tags/5.6.0/vendor/symfony/polyfill-php80/Resources/stubs/PhpToken.php (added)
-
tags/5.6.0/vendor/symfony/polyfill-php80/Resources/stubs/Stringable.php (added)
-
tags/5.6.0/vendor/symfony/polyfill-php80/Resources/stubs/UnhandledMatchError.php (added)
-
tags/5.6.0/vendor/symfony/polyfill-php80/Resources/stubs/ValueError.php (added)
-
tags/5.6.0/vendor/symfony/polyfill-php80/bootstrap.php (added)
-
tags/5.6.0/vendor/symfony/polyfill-php80/composer.json (added)
-
tags/5.6.0/vendor/symfony/polyfill-php82 (added)
-
tags/5.6.0/vendor/symfony/polyfill-php82/LICENSE (added)
-
tags/5.6.0/vendor/symfony/polyfill-php82/NoDynamicProperties.php (added)
-
tags/5.6.0/vendor/symfony/polyfill-php82/Php82.php (added)
-
tags/5.6.0/vendor/symfony/polyfill-php82/Random (added)
-
tags/5.6.0/vendor/symfony/polyfill-php82/Random/Engine (added)
-
tags/5.6.0/vendor/symfony/polyfill-php82/Random/Engine/Secure.php (added)
-
tags/5.6.0/vendor/symfony/polyfill-php82/Resources (added)
-
tags/5.6.0/vendor/symfony/polyfill-php82/Resources/stubs (added)
-
tags/5.6.0/vendor/symfony/polyfill-php82/Resources/stubs/AllowDynamicProperties.php (added)
-
tags/5.6.0/vendor/symfony/polyfill-php82/Resources/stubs/Random (added)
-
tags/5.6.0/vendor/symfony/polyfill-php82/Resources/stubs/Random/BrokenRandomEngineError.php (added)
-
tags/5.6.0/vendor/symfony/polyfill-php82/Resources/stubs/Random/CryptoSafeEngine.php (added)
-
tags/5.6.0/vendor/symfony/polyfill-php82/Resources/stubs/Random/Engine (added)
-
tags/5.6.0/vendor/symfony/polyfill-php82/Resources/stubs/Random/Engine.php (added)
-
tags/5.6.0/vendor/symfony/polyfill-php82/Resources/stubs/Random/Engine/Secure.php (added)
-
tags/5.6.0/vendor/symfony/polyfill-php82/Resources/stubs/Random/RandomError.php (added)
-
tags/5.6.0/vendor/symfony/polyfill-php82/Resources/stubs/Random/RandomException.php (added)
-
tags/5.6.0/vendor/symfony/polyfill-php82/Resources/stubs/SensitiveParameter.php (added)
-
tags/5.6.0/vendor/symfony/polyfill-php82/Resources/stubs/SensitiveParameterValue.php (added)
-
tags/5.6.0/vendor/symfony/polyfill-php82/SensitiveParameterValue.php (added)
-
tags/5.6.0/vendor/symfony/polyfill-php82/bootstrap.php (added)
-
tags/5.6.0/vendor/symfony/polyfill-php82/composer.json (added)
-
trunk/frontman.php (modified) (8 diffs)
-
trunk/includes/class-frontman-elementor-data.php (modified) (1 diff)
-
trunk/includes/class-frontman-router.php (modified) (5 diffs)
-
trunk/includes/class-frontman-sentry.php (modified) (1 diff)
-
trunk/includes/class-frontman-settings.php (added)
-
trunk/includes/class-frontman-tools.php (modified) (9 diffs)
-
trunk/includes/class-frontman-ui.php (modified) (2 diffs)
-
trunk/readme.txt (modified) (3 diffs)
-
trunk/tools/class-tool-cache.php (modified) (1 diff)
-
trunk/tools/class-tool-directorist.php (added)
-
trunk/tools/class-tool-elementor.php (modified) (9 diffs)
-
trunk/tools/class-tool-options.php (modified) (4 diffs)
-
trunk/tools/class-tool-polylang.php (added)
-
trunk/tools/class-tool-posts.php (modified) (7 diffs)
-
trunk/tools/class-tool-seo.php (modified) (11 diffs)
-
trunk/tools/class-tool-templates.php (modified) (1 diff)
-
trunk/tools/class-tool-users.php (modified) (1 diff)
-
trunk/tools/class-tool-widgets.php (modified) (8 diffs)
-
trunk/tools/class-tool-woocommerce.php (modified) (3 diffs)
-
trunk/vendor/composer/installed.php (modified) (1 diff)
Legend:
- Unmodified
- Added
- Removed
-
frontman-agentic-ai-editor/trunk/frontman.php
r3767236 r3783407 4 4 * Plugin URI: https://frontman.sh 5 5 * Description: Edit WordPress with AI beside a live preview. Update pages, posts, Elementor layouts, WooCommerce data, menus, and settings faster. 6 * Version: 5. 5.06 * Version: 5.6.0 7 7 * Requires at least: 6.0 8 8 * Requires PHP: 7.4 … … 44 44 } 45 45 46 define( 'FRONTMAN_VERSION', '5. 5.0' );46 define( 'FRONTMAN_VERSION', '5.6.0' ); 47 47 define( 'FRONTMAN_PLUGIN_DIR', frontman_plugin_dir_path( __FILE__ ) ); 48 48 define( 'FRONTMAN_PLUGIN_URL', frontman_plugin_dir_url( __FILE__ ) ); … … 54 54 require_once FRONTMAN_PLUGIN_DIR . 'includes/class-frontman-tools.php'; 55 55 require_once FRONTMAN_PLUGIN_DIR . 'includes/class-frontman-sentry.php'; 56 require_once FRONTMAN_PLUGIN_DIR . 'includes/class-frontman-settings.php'; 56 57 require_once FRONTMAN_PLUGIN_DIR . 'includes/class-frontman-router.php'; 57 58 require_once FRONTMAN_PLUGIN_DIR . 'includes/class-frontman-ui.php'; … … 65 66 require_once FRONTMAN_PLUGIN_DIR . 'tools/class-tool-templates.php'; 66 67 require_once FRONTMAN_PLUGIN_DIR . 'tools/class-tool-widgets.php'; 68 add_action( 'widgets_init', static function(): void { 69 add_filter( 'widget_display_callback', [ 'Frontman_Tool_Widgets', 'filter_visibility' ], 10, 3 ); 70 } ); 67 71 require_once FRONTMAN_PLUGIN_DIR . 'tools/class-tool-cache.php'; 72 require_once FRONTMAN_PLUGIN_DIR . 'tools/class-tool-polylang.php'; 68 73 require_once FRONTMAN_PLUGIN_DIR . 'tools/class-tool-public-files.php'; 69 74 require_once FRONTMAN_PLUGIN_DIR . 'tools/class-tool-seo.php'; … … 71 76 require_once FRONTMAN_PLUGIN_DIR . 'tools/class-tool-301-redirects.php'; 72 77 require_once FRONTMAN_PLUGIN_DIR . 'tools/class-tool-angie.php'; 78 require_once FRONTMAN_PLUGIN_DIR . 'tools/class-tool-directorist.php'; 73 79 74 80 /** … … 93 99 ( new Frontman_Tool_Widgets() )->register( $tools ); 94 100 ( new Frontman_Tool_Cache() )->register( $tools ); 101 ( new Frontman_Tool_Polylang() )->register( $tools ); 95 102 ( new Frontman_Tool_Public_Files() )->register( $tools ); 96 103 97 104 if ( Frontman_Tool_Angie::is_available() ) { 98 105 ( new Frontman_Tool_Angie() )->register( $tools ); 106 } 107 108 if ( Frontman_Tool_Directorist::is_available() ) { 109 ( new Frontman_Tool_Directorist() )->register( $tools ); 99 110 } 100 111 … … 107 118 } 108 119 109 if ( null === Frontman_Tool_Seo::unavailable_reason() ) { 110 ( new Frontman_Tool_Seo() )->register( $tools ); 111 } 120 ( new Frontman_Tool_Seo() )->register( $tools ); 112 121 113 122 if ( Frontman_Plugin_Dependencies::is_available( 'elementor/elementor.php', '\Elementor\Plugin' ) ) { … … 127 136 $router->register(); 128 137 $ui->register(); 129 Frontman_Se ntry::register();138 Frontman_Settings::register(); 130 139 } 131 140 add_action( 'init', 'frontman_init' ); -
frontman-agentic-ai-editor/trunk/includes/class-frontman-elementor-data.php
r3767236 r3783407 662 662 } 663 663 664 public static function get_element_context( array $elements, string $element_id ): ?array { 665 $matches = self::element_contexts( $elements, $element_id ); 666 if ( count( $matches ) > 1 ) { 667 throw new Frontman_Tool_Error( 'Context is ambiguous for element: ' . $element_id ); 668 } 669 return $matches[0] ?? null; 670 } 671 672 public static function wrap_at_context( array &$elements, array $context, array $wrapper ): bool { 673 $id = (string) $context['element']['id']; 674 $current = self::get_element_context( $elements, $id ); 675 if ( null === $current || $current !== $context ) { 676 return false; 677 } 678 return self::replace_element_at_context( $elements, $id, $context['parent_id'], (int) $context['position'], $wrapper ); 679 } 680 664 681 private static function element_contexts( array $elements, string $element_id, ?string $parent_id = null, ?array $parent = null ): array { 665 682 $matches = []; -
frontman-agentic-ai-editor/trunk/includes/class-frontman-router.php
r3767236 r3783407 45 45 public function register(): void { 46 46 add_action( 'parse_request', [ $this, 'intercept' ], 1 ); 47 add_action( 'rest_api_init', static function(): void { 48 $permission = static function() { 49 wp_get_current_user(); 50 global $wp_rest_application_password_uuid; 51 if ( empty( $wp_rest_application_password_uuid ) || ! current_user_can( 'manage_options' ) 52 || ! Frontman_Sentry::sanitize_enabled( get_option( 'frontman_mcp_enabled', false ) ) ) { 53 return new WP_Error( 'frontman_mcp_forbidden', 'MCP access denied.', [ 'status' => 403 ] ); 54 } 55 return true; 56 }; 57 register_rest_route( 'frontman/v1', '/tools', [ 58 'methods' => 'GET', 59 'permission_callback' => $permission, 60 'callback' => static function() { 61 return [ 'tools' => Frontman_Tools::instance()->all_definitions( true ) ]; 62 }, 63 ] ); 64 register_rest_route( 'frontman/v1', '/tools/call', [ 65 'methods' => 'POST', 66 'permission_callback' => $permission, 67 'callback' => static function( WP_REST_Request $request ) { 68 $tools = Frontman_Tools::instance(); 69 $name = $request->get_param( 'name' ); 70 $tool = is_string( $name ) ? $tools->get( $name ) : null; 71 if ( ! $tool || null === $tool->remote_validator ) { 72 return new WP_Error( 'frontman_mcp_forbidden', 'MCP access denied.', [ 'status' => 403 ] ); 73 } 74 $body = json_decode( $request->get_body() ); 75 if ( ! is_object( $body ) || ! isset( $body->arguments ) || ! is_object( $body->arguments ) 76 || ! ( $tool->remote_validator )( $body->arguments ) ) { 77 return new WP_Error( 'frontman_mcp_arguments', 'Invalid tool arguments.', [ 'status' => 400 ] ); 78 } 79 try { 80 return $tools->call( $name, $tools->sanitize_input( $name, $body->arguments, true ) ); 81 } catch ( Frontman_Tool_Error $e ) { 82 return new WP_Error( 'frontman_mcp_arguments', $e->getMessage(), [ 'status' => 400 ] ); 83 } 84 }, 85 ] ); 86 } ); 47 87 } 48 88 … … 279 319 } 280 320 } 281 if ( is_array( $data ) && in_array( sanitize_key( $data['name'] ?? '' ), [ 'wp_create_post', 'wp_update_post' ], true )) {321 if ( is_array( $data ) ) { 282 322 $typed = json_decode( $raw ); 283 323 foreach ( [ 'arguments', 'input' ] as $key ) { 284 if ( isset( $typed->$key ) && is_object( $typed->$key ) && isset( $data[ $key ] ) && is_array( $data[ $key ] ) ) { 285 foreach ( [ 'categories', 'tags' ] as $field ) { 286 if ( property_exists( $typed->$key, $field ) ) { 287 $data[ $key ][ $field ] = $typed->$key->$field; 288 } 289 } 324 if ( $typed instanceof \stdClass && property_exists( $typed, $key ) ) { 325 $data[ $key ] = $typed->$key; 290 326 } 291 327 } … … 302 338 ]; 303 339 304 if ( isset( $data['arguments'] ) && is_array( $data['arguments']) ) {340 if ( array_key_exists( 'arguments', $data ) ) { 305 341 $body['arguments'] = $data['arguments']; 306 342 } 307 343 308 if ( isset( $data['input'] ) && is_array( $data['input']) ) {344 if ( array_key_exists( 'input', $data ) ) { 309 345 $body['input'] = $data['input']; 310 346 } … … 360 396 private function handle_tool_call( array $body ): void { 361 397 $name = sanitize_key( $body['name'] ?? '' ); 362 $raw_input = $body['arguments'] ?? $body['input'] ?? [];398 $raw_input = array_key_exists( 'arguments', $body ) ? $body['arguments'] : ( array_key_exists( 'input', $body ) ? $body['input'] : (object) [] ); 363 399 364 400 if ( empty( $name ) ) { … … 369 405 if ( $this->tools->is_wp_tool( $name ) ) { 370 406 try { 371 $input = is_array( $raw_input ) ? $this->tools->sanitize_input( $name, $raw_input ) : [];407 $input = $this->tools->sanitize_input( $name, $raw_input, true ); 372 408 $result = $this->tools->call( $name, $input ); 373 409 $this->send_sse_tool_result( $result ); -
frontman-agentic-ai-editor/trunk/includes/class-frontman-sentry.php
r3767236 r3783407 30 30 } 31 31 32 public static function register(): void {33 add_action( 'admin_init', [ self::class, 'register_setting' ] );34 add_action( 'admin_menu', static function (): void {35 self::boundary( 'diagnostics_admin_menu', static function (): void {36 add_options_page( 'Frontman', 'Frontman', 'manage_options', 'frontman-diagnostics', [ self::class, 'render_settings' ] );37 } );38 } );39 }40 41 32 public static function sanitize_enabled( $value ): bool { 42 33 return in_array( $value, [ true, 1, '1' ], true ); 43 }44 45 public static function register_setting(): void {46 self::boundary( 'admin_init', [ self::class, 'register_diagnostic_settings' ] );47 }48 49 private static function register_diagnostic_settings(): void {50 register_setting( 'frontman_diagnostics', self::OPTION, [51 'type' => 'boolean',52 'default' => false,53 'sanitize_callback' => [ self::class, 'sanitize_enabled' ],54 ] );55 register_setting( 'frontman_diagnostics', 'frontman_auth_diagnostics', [56 'type' => 'boolean',57 'default' => false,58 'sanitize_callback' => [ self::class, 'sanitize_enabled' ],59 ] );60 }61 62 public static function render_settings(): void {63 if ( ! current_user_can( 'manage_options' ) ) {64 wp_die( esc_html__( 'Administrator access required.', 'frontman-agentic-ai-editor' ) );65 return;66 }67 self::boundary( 'diagnostics_settings', [ self::class, 'render_settings_form' ] );68 }69 70 private static function render_settings_form(): void {71 ?>72 <div class="wrap">73 <h1><?php esc_html_e( 'Frontman PHP Diagnostics', 'frontman-agentic-ai-editor' ); ?></h1>74 <form action="options.php" method="post">75 <?php settings_fields( 'frontman_diagnostics' ); ?>76 <input type="hidden" name="frontman_php_diagnostics" value="0">77 <label>78 <input type="checkbox" name="frontman_php_diagnostics" value="1" <?php checked( self::sanitize_enabled( get_option( self::OPTION, false ) ) ); ?>>79 <?php esc_html_e( 'Send unexpected Frontman PHP tool errors to Sentry.', 'frontman-agentic-ai-editor' ); ?>80 </label>81 <p><?php esc_html_e( 'Reports include exception types, sanitized stack traces, tool names, and software versions. They exclude exception messages, tool arguments, source code, site URLs, and user data. Sentry receives the server IP address when delivering reports. You can disable reporting here at any time.', 'frontman-agentic-ai-editor' ); ?></p>82 <p><a href="https://frontman.sh/privacy/" target="_blank" rel="noopener noreferrer"><?php esc_html_e( 'Frontman Privacy Policy', 'frontman-agentic-ai-editor' ); ?></a> · <a href="https://sentry.io/privacy/" target="_blank" rel="noopener noreferrer"><?php esc_html_e( 'Sentry Privacy Policy', 'frontman-agentic-ai-editor' ); ?></a></p>83 <input type="hidden" name="frontman_auth_diagnostics" value="0">84 <label>85 <input type="checkbox" name="frontman_auth_diagnostics" value="1" <?php checked( self::sanitize_enabled( get_option( 'frontman_auth_diagnostics', false ) ) ); ?>>86 <?php esc_html_e( 'Enable authentication diagnostics in the server PHP error log.', 'frontman-agentic-ai-editor' ); ?>87 </label>88 <p><?php esc_html_e( 'Records contain only fixed authentication failure categories and WordPress, PHP, and plugin versions. They exclude credentials, request content, URLs, and user data. Each category is logged at most once every five minutes per site. These records are not sent to Sentry. You can disable logging here at any time.', 'frontman-agentic-ai-editor' ); ?></p>89 <?php submit_button(); ?>90 </form>91 </div>92 <?php93 34 } 94 35 -
frontman-agentic-ai-editor/trunk/includes/class-frontman-tools.php
r3767236 r3783407 35 35 /** @var callable(array): array */ 36 36 public $handler; 37 /** @var null|callable(\stdClass): bool Remote opt-in; validate native JSON before sanitization. */ 38 public $remote_validator; 37 39 38 40 /** … … 44 46 * @param bool $visible_to_agent Whether the agent can see this tool. 45 47 * @param bool $preserve_input_strings Whether schema sanitization should preserve raw string values for downstream API validation. 48 * @param callable|null $remote_validator Remote opt-in. Return true for valid decoded JSON arguments, false for HTTP 400. 46 49 */ 47 50 public function __construct( … … 52 55 ?string $access = null, 53 56 bool $visible_to_agent = true, 54 bool $preserve_input_strings = false 57 bool $preserve_input_strings = false, 58 ?callable $remote_validator = null 55 59 ) { 56 60 $this->name = $name; … … 61 65 $this->visible_to_agent = $visible_to_agent; 62 66 $this->preserve_input_strings = $preserve_input_strings; 67 $this->remote_validator = $remote_validator; 63 68 } 64 69 … … 138 143 139 144 /** 140 * Return all tool definitions as serializable arrays.145 * Return serializable tool definitions, optionally limited to remote contracts. 141 146 * 142 147 * @return array[] 143 148 */ 144 public function all_definitions( ): array {149 public function all_definitions( bool $remote_only = false ): array { 145 150 return array_values( 146 151 array_map( 147 152 function( Frontman_Tool_Definition $t ) { return $t->to_array(); }, 148 $this->tools153 array_filter( $this->tools, static fn( Frontman_Tool_Definition $t ) => ! $remote_only || null !== $t->remote_validator ) 149 154 ) 150 155 ); … … 154 159 * Sanitize tool input against the registered JSON schema before dispatch. 155 160 */ 156 public function sanitize_input( string $name, array $input): array {161 public function sanitize_input( string $name, $input, bool $json = false ): array { 157 162 $tool = $this->get( $name ); 158 163 if ( ! $tool ) { 159 return $this->sanitize_untyped_array( $input, $name ); 164 return $this->sanitize_untyped_array( (array) $input, $name ); 165 } 166 167 $this->validate_types( $input, $tool->input_schema, 'arguments', $json ); 168 $schema = json_decode( wp_json_encode( $tool->input_schema, JSON_PRESERVE_ZERO_FRACTION ), true ); 169 $valid = rest_validate_value_from_schema( $input, $schema, 'arguments' ); 170 if ( is_wp_error( $valid ) ) { 171 throw new Frontman_Tool_Error( $valid->get_error_message() ); 172 } 173 if ( $input instanceof \stdClass ) { 174 $input = json_decode( wp_json_encode( $input, JSON_PRESERVE_ZERO_FRACTION ), true ); 160 175 } 161 176 162 177 if ( class_exists( 'Frontman_Tool_Seo' ) && in_array( $name, [ 'wp_read_seo', 'wp_update_seo' ], true ) ) { 163 178 return Frontman_Tool_Seo::validate_input( $name, $input ); 179 } 180 if ( class_exists( 'Frontman_Tool_WooCommerce' ) && in_array( $name, [ 'wc_get_catalog_display', 'wc_update_catalog_display' ], true ) ) { 181 Frontman_Tool_WooCommerce::validate_catalog_display_input( $input, 'wc_update_catalog_display' === $name ); 182 return array_intersect_key( $input, array_flip( [ 'name', 'value', 'confirm' ] ) ); 164 183 } 165 184 … … 181 200 } 182 201 183 if ( class_exists( 'Frontman_Tool_Elementor' ) && in_array( $name, [ 'wp_elementor_add_element', 'wp_elementor_generate_element' ], true ) ) {202 if ( class_exists( 'Frontman_Tool_Elementor' ) && in_array( $name, [ 'wp_elementor_add_element', 'wp_elementor_generate_element', 'wp_elementor_wrap_element' ], true ) ) { 184 203 Frontman_Tool_Elementor::validate_creation_input( $name, $input ); 185 204 } … … 188 207 $sanitized = $this->sanitize_value_for_schema( $input, $tool->input_schema, $name, '', $preserve_strings ); 189 208 return is_array( $sanitized ) ? $sanitized : []; 209 } 210 211 /** 212 * Reject coercions before WordPress schema validation. PHP callers use arrays 213 * for objects; HTTP callers retain native JSON types until validation ends. 214 */ 215 private function validate_types( $value, array $schema, string $path, bool $json ): void { 216 $types = (array) ( $schema['type'] ?? [] ); 217 $matches = [ 218 'object' => $value instanceof \stdClass || ( ! $json && is_array( $value ) ), 219 'array' => is_array( $value ), 220 'string' => is_string( $value ), 221 'integer' => is_int( $value ), 222 'number' => is_int( $value ) || is_float( $value ), 223 'boolean' => is_bool( $value ), 224 'null' => null === $value, 225 ]; 226 if ( $types && ! array_filter( $types, static fn( $type ) => $matches[ $type ] ?? false ) ) { 227 throw new Frontman_Tool_Error( $path . ' must be of type ' . implode( ' or ', $types ) . '.' ); 228 } 229 if ( $value instanceof \stdClass || is_array( $value ) ) { 230 foreach ( $value as $key => $item ) { 231 $child = ( $value instanceof \stdClass || ( ! $json && in_array( 'object', $types, true ) ) ) 232 ? ( (array) ( $schema['properties'] ?? [] ) )[ $key ] ?? [] 233 : ( $schema['items'] ?? [] ); 234 if ( is_array( $child ) && $child ) { 235 $this->validate_types( $item, $child, $path . '[' . $key . ']', $json ); 236 } 237 } 238 } 190 239 } 191 240 … … 304 353 return $value; 305 354 } 306 if ( in_array( $field_name, [ 'slug', 'content', 'template' ], true ) && in_array( $tool_name, [ 'wp_create_post', 'wp_update_post' ], true ) ) {355 if ( in_array( $field_name, [ 'slug', 'content', 'template' ], true ) && in_array( $tool_name, [ 'wp_create_post', 'wp_update_post', 'wp_save_post_preview' ], true ) ) { 307 356 return $value; 308 357 } -
frontman-agentic-ai-editor/trunk/includes/class-frontman-ui.php
r3767236 r3783407 113 113 * 114 114 * Called directly by the router — this outputs a complete HTML document 115 * (no wp-admin chrome). The client is loaded fromthe production CDN.115 * (no wp-admin chrome). The client defaults to the production CDN. 116 116 * 117 117 * @param string|null $preview_path Path to load in the web preview iframe. … … 121 121 public function render_page( ?string $preview_path = null ): void { 122 122 $logo_url = FRONTMAN_PLUGIN_URL . 'assets/frontman-logo.svg'; 123 $host = 'api.frontman.sh';124 $base_js_url = 'https://app.frontman.sh/frontman.es.js';125 $client_css = 'https://app.frontman.sh/frontman.css';123 $host = defined( 'FRONTMAN_HOST' ) ? FRONTMAN_HOST : 'api.frontman.sh'; 124 $base_js_url = defined( 'FRONTMAN_CLIENT_URL' ) ? FRONTMAN_CLIENT_URL : 'https://app.frontman.sh/frontman.es.js'; 125 $client_css = defined( 'FRONTMAN_CLIENT_CSS' ) ? FRONTMAN_CLIENT_CSS : 'https://app.frontman.sh/frontman.css'; 126 126 127 127 $client_url = add_query_arg( -
frontman-agentic-ai-editor/trunk/readme.txt
r3767236 r3783407 5 5 Tested up to: 7.0.2 6 6 Requires PHP: 7.4 7 Stable tag: 5. 5.07 Stable tag: 5.6.0 8 8 License: GPL-2.0-or-later 9 9 License URI: https://www.gnu.org/licenses/gpl-2.0.html … … 87 87 == Optional SEO Tools == 88 88 89 `wp_read_seo` reads stored SEO title and description overrides. `wp_update_seo` updates one or both overrides. Eligibility depends on Yoast SEO Free runtime capabilities, not an exact version list. Frontman does not install Yoast.89 `wp_read_seo` reads stored SEO overrides from Yoast SEO Free or SureRank. `wp_update_seo` supports Yoast SEO Free only. Frontman does not install either plugin. 90 90 91 91 Both tools also support one `focus_keyphrase`, stored through Yoast Free as `focuskw`. A focus-only update preserves title and description. Omission preserves the keyphrase; an empty string clears it. Reads and mutation snapshots include it. This is an analysis input, not a rendered keywords tag. Secondary keywords, synonyms, related keyphrases, and Premium support are not included. 92 92 93 The route requires WordPress 6.9 or later, initialized Yoast metadata methods, and registered metadata sanitizers. The integration-test matrix includes Yoast 28.6 on WordPress 7.0.2 with PHP 8.4, Yoast 28.4 on WordPress 7.0.2 with PHP 7.4 and 8.4, and Yoast 19.9 on WordPress 6.9.9 with PHP 8.2. These pins define test coverage, not production eligibility. Future Yoast versions must pass the same runtime contract. Premium and active Rank Math, All in One SEO, or The SEO Framework disable these tools. Frontman cannot detect every custom SEO owner. 94 95 `wp_get_site_info` returns `seo_unavailable_reason`. A nonempty reason explains why Frontman does not register the SEO tools. A null reason means the compatibility checks pass. Each post still requires edit permissions. 93 SureRank support is read-only. The reader returns stored `page_title`, `page_description`, and `focus_keyword` values through SureRank's raw metadata accessor. Missing values return empty strings, not inherited templates or generated text. Reads do not initialize or update metadata. SureRank 1.10.2 defines the integration-test contract. Runtime eligibility requires the active, loaded plugin and its raw metadata reader. Canonical URLs and social overrides are outside this tool's field set. 94 95 Both providers require WordPress 6.9 or later. Yoast reads require its initialized metadata reader. Yoast updates also require its setters, deleter, and registered metadata sanitizers. The integration-test matrix includes Yoast 28.6 on WordPress 7.0.2 with PHP 8.4, Yoast 28.4 on WordPress 7.0.2 with PHP 7.4 and 8.4, and Yoast 19.9 on WordPress 6.9.9 with PHP 8.2. These pins define test coverage, not production eligibility. Future Yoast versions must pass the same runtime contract. Premium and active Rank Math, All in One SEO, or The SEO Framework disable these tools. Frontman cannot detect every custom SEO owner. 96 97 `wp_get_site_info.seo_unavailable_reason` retains its existing meaning: the reason that SEO updates are unavailable. The additive `seo_capabilities` object reports `provider`, `read`, `write`, `supported_fields`, and separate read/write reasons. For SureRank, reads are available but writes are not. Each post still requires post and metadata edit permissions. Simultaneously active Yoast and SureRank disable both SEO tools because ownership is ambiguous. 96 98 97 99 Supported objects are posts, pages, and editable non-attachment custom post types with an admin interface, including authorized drafts and private content. Revisions, autosaves, attachments, and internal WordPress object types are not supported. … … 218 220 == Changelog == 219 221 222 = 5.6.0 = 223 * Sync the Frontman plugin release with Frontman v5.6.0 224 * See the GitHub release notes for the full cross-product changelog 225 220 226 = 5.5.0 = 221 227 * Sync the Frontman plugin release with Frontman v5.5.0 -
frontman-agentic-ai-editor/trunk/tools/class-tool-cache.php
r3642353 r3783407 121 121 case 'wp-super-cache': 122 122 if ( function_exists( 'wp_cache_clear_cache' ) ) { 123 wp_cache_clear_cache( '', true);123 wp_cache_clear_cache( is_multisite() ? get_current_blog_id() : 0 ); 124 124 $cleared[] = $plugin['slug']; 125 125 } -
frontman-agentic-ai-editor/trunk/tools/class-tool-elementor.php
r3767236 r3783407 129 129 $tools->add( new Frontman_Tool_Definition( 130 130 'wp_elementor_add_element', 131 'Adds a new Elementor element at the root or inside a parent container after saving the previous page tree as a private rollback snapshot. Prefer wp_elementor_generate_element and pass its returned object as element. Do not call withelement: {}.',131 'Adds a new Elementor subtree in one save with a private rollback. Use after_element_id: "book-demo" to insert immediately after a unique sibling, excluding any explicit parent_id or position. Alternatively use parent_id and position. Prefer wp_elementor_generate_element; do not pass element: {}.', 132 132 [ 133 133 'type' => 'object', … … 135 135 'properties' => [ 136 136 'post_id' => [ 'type' => 'integer' ], 137 'element' => [ 138 'type' => 'object', 139 'description' => 'Complete Elementor subtree. IDs can be omitted at every depth and are generated automatically. Prefer wp_elementor_generate_element; never pass an empty object.', 140 'additionalProperties' => true, 141 'properties' => [ 142 'elType' => [ 'type' => 'string', 'enum' => [ 'container', 'section', 'column', 'widget' ], 'description' => 'Elementor element type. Use widget for widgets and include widgetType.' ], 143 'widgetType' => [ 'type' => 'string', 'description' => 'Required when elType is widget.' ], 144 'settings' => [ 'type' => 'object', 'additionalProperties' => true, 'properties' => new \stdClass() ], 145 'elements' => [ 'type' => 'array', 'items' => [ 'type' => 'object', 'additionalProperties' => true, 'properties' => new \stdClass() ] ], 146 ], 147 'required' => [ 'elType' ], 148 ], 137 'element' => $this->new_element_schema(), 138 'after_element_id' => [ 'type' => 'string' ], 149 139 'parent_id' => [ 'type' => 'string' ], 150 'position' => [ 'type' => 'integer' , 'default' => -1],140 'position' => [ 'type' => 'integer' ], 151 141 ], 152 142 'required' => [ 'post_id', 'element' ], 153 143 ], 154 144 [ $this, 'add_element' ] 145 ) ); 146 147 $tools->add( new Frontman_Tool_Definition( 148 'wp_elementor_wrap_element', 149 'Wraps a modern widget or container at its exact position in a new row, optionally adding element_after. Supports root or container parents only, not legacy layouts. Preserves the original subtree IDs; saves one rollback and one final page tree. settings override row defaults. Check rendered desktop/mobile layout after wrapping.', 150 [ 151 'type' => 'object', 152 'additionalProperties' => false, 153 'properties' => [ 154 'post_id' => [ 'type' => 'integer' ], 155 'element_id' => [ 'type' => 'string' ], 156 'settings' => [ 'type' => 'object', 'additionalProperties' => true, 'properties' => new \stdClass() ], 157 'element_after' => $this->new_element_schema(), 158 ], 159 'required' => [ 'post_id', 'element_id' ], 160 ], 161 [ $this, 'wrap_element' ], 162 'read-write' 155 163 ) ); 156 164 … … 197 205 $tools->add( new Frontman_Tool_Definition( 198 206 'wp_elementor_duplicate_element', 199 'Duplicates an Elementor element next to the original after saving the previous page tree as a private rollback snapshot, assigning new IDs to the clone and its children.', 200 $this->element_id_schema(), 207 'Duplicates an Elementor element next to the original in one save with a private rollback. Optional settings override only the cloned root with a shallow merge: settings: {text: "Try It Now", link: {url: "/try/"}} replaces the complete link object. HTML widget settings.html overrides are forbidden.', 208 array_replace_recursive( $this->element_id_schema(), [ 209 'properties' => [ 210 'settings' => [ 'type' => 'object', 'additionalProperties' => true, 'properties' => new \stdClass(), 'minProperties' => 1 ], 211 ], 212 ] ), 201 213 [ $this, 'duplicate_element' ] 202 214 ) ); … … 684 696 } 685 697 698 private function new_element_schema(): array { 699 return [ 700 'type' => 'object', 701 'description' => 'Elementor subtree: elType is required, and widgets require widgetType. IDs can be omitted at every depth and are generated automatically.', 702 'additionalProperties' => true, 703 'properties' => [ 704 'elType' => [ 'type' => 'string', 'enum' => [ 'container', 'section', 'column', 'widget' ] ], 705 'widgetType' => [ 'type' => 'string' ], 706 'settings' => [ 'type' => 'object', 'additionalProperties' => true, 'properties' => new \stdClass() ], 707 'elements' => [ 'type' => 'array', 'items' => [ 'type' => 'object', 'additionalProperties' => true, 'properties' => new \stdClass() ] ], 708 ], 709 'required' => [ 'elType' ], 710 ]; 711 } 712 713 public function wrap_element( array $input ): array { 714 self::validate_creation_input( 'wp_elementor_wrap_element', $input ); 715 $post_id = $this->require_post_id( $input ); 716 $element_id = $this->require_element_id( $input ); 717 $data = $this->require_page_data( $post_id ); 718 $before = $data; 719 $context = Frontman_Elementor_Data::get_element_context( $data, $element_id ); 720 if ( null === $context ) { 721 throw new Frontman_Tool_Error( 'Element not found: ' . $element_id ); 722 } 723 if ( ! in_array( $context['element']['elType'] ?? '', [ 'widget', 'container' ], true ) || 724 ( null !== $context['parent_id'] && 'container' !== $context['parent_el_type'] ) ) { 725 throw new Frontman_Tool_Error( 'Wrap supports widget/container targets with root/container parents only.' ); 726 } 727 $companion = null; 728 $reserved = $data; 729 if ( array_key_exists( 'element_after', $input ) ) { 730 $companion = $this->prepare_new_subtree( $input['element_after'], $data ); 731 $reserved[] = $companion; 732 } 733 $wrapper = Frontman_Elementor_Data::generate_element( [ 'type' => 'row', 'settings' => $input['settings'] ?? [] ] ); 734 unset( $wrapper['id'] ); 735 $wrapper = $this->prepare_new_subtree( $wrapper, $reserved ); 736 $wrapper['isInner'] = null !== $context['parent_id']; 737 $wrapper['elements'] = [ $context['element'] ]; 738 if ( null !== $companion ) { 739 $wrapper['elements'][] = $companion; 740 } 741 if ( ! Frontman_Elementor_Data::wrap_at_context( $data, $context, $wrapper ) ) { 742 throw new Frontman_Tool_Error( 'Wrap target no longer occupies its resolved position.' ); 743 } 744 $this->validate_full_element_tree( $data ); 745 $rollback = Frontman_Elementor_Data::make_page_rollback( 'wrapped_element', $before ); 746 Frontman_Elementor_Data::save_rollback( $post_id, $rollback ); 747 $save_result = $this->save_elementor_data( $post_id, $data ); 748 return $this->append_save_result( [ 749 'success' => true, 'post_id' => $post_id, 'element_id' => $element_id, 750 'wrapper_id' => $wrapper['id'], 'added_element_id' => $companion['id'] ?? null, 751 'parent_id' => $context['parent_id'], 'position' => $context['position'], 752 'rollback_id' => $rollback['rollback_id'], 753 ], $save_result ); 754 } 755 686 756 public function add_element( array $input ): array { 687 757 self::validate_creation_input( 'wp_elementor_add_element', $input ); … … 700 770 $parent_id = isset( $input['parent_id'] ) ? sanitize_text_field( $input['parent_id'] ) : null; 701 771 $position = (int) ( $input['position'] ?? -1 ); 772 if ( array_key_exists( 'after_element_id', $input ) ) { 773 if ( array_key_exists( 'parent_id', $input ) || array_key_exists( 'position', $input ) ) { 774 throw new Frontman_Tool_Error( 'after_element_id excludes explicit parent_id and position.' ); 775 } 776 $anchor = Frontman_Elementor_Data::get_element_context( $data, (string) $input['after_element_id'] ); 777 if ( null === $anchor ) { 778 throw new Frontman_Tool_Error( 'Anchor element not found.' ); 779 } 780 $parent_id = $anchor['parent_id']; 781 $position = $anchor['position'] + 1; 782 } 702 783 if ( ! Frontman_Elementor_Data::insert_element( $data, $element, $parent_id, $position ) ) { 703 784 throw new Frontman_Tool_Error( 'Parent element not found: ' . $parent_id ); … … 707 788 Frontman_Elementor_Data::save_rollback( $post_id, $rollback ); 708 789 $save_result = $this->save_elementor_data( $post_id, $data ); 709 return $this->append_save_result( [ 'success' => true, 'post_id' => $post_id, 'element_id' => $element['id'] ?? '', 'rollback_id' => $rollback['rollback_id'] ], $save_result ); 790 $inserted = Frontman_Elementor_Data::get_element_context( $data, $element['id'] ); 791 return $this->append_save_result( [ 'success' => true, 'post_id' => $post_id, 'element_id' => $element['id'] ?? '', 'parent_id' => $inserted['parent_id'], 'position' => $inserted['position'], 'rollback_id' => $rollback['rollback_id'] ], $save_result ); 710 792 } 711 793 … … 762 844 $data = $this->require_page_data( $post_id ); 763 845 $rollback = Frontman_Elementor_Data::make_page_rollback( 'duplicated_element', $data ); 846 $context = Frontman_Elementor_Data::get_element_context( $data, $element_id ); 847 if ( null === $context ) { 848 throw new Frontman_Tool_Error( 'Element not found: ' . $element_id ); 849 } 850 if ( array_key_exists( 'settings', $input ) ) { 851 if ( ! is_array( $input['settings'] ) || [] === $input['settings'] || array_values( $input['settings'] ) === $input['settings'] ) { 852 throw new Frontman_Tool_Error( 'settings must be a nonempty object.' ); 853 } 854 if ( 'html' === ( $context['element']['widgetType'] ?? '' ) && array_key_exists( 'html', $input['settings'] ) ) { 855 throw new Frontman_Tool_Error( 'Use old_html and new_html on wp_elementor_update_element to edit an Elementor HTML widget settings.html fragment.' ); 856 } 857 } 764 858 $new_id = Frontman_Elementor_Data::duplicate_element( $data, $element_id ); 765 859 if ( null === $new_id ) { 766 860 throw new Frontman_Tool_Error( 'Element not found: ' . $element_id ); 767 861 } 862 if ( isset( $input['settings'] ) ) { 863 Frontman_Elementor_Data::update_element_settings( $data, $new_id, $input['settings'] ); 864 } 865 $this->validate_full_element_tree( $data ); 768 866 769 867 Frontman_Elementor_Data::save_rollback( $post_id, $rollback ); … … 792 890 $element = Frontman_Elementor_Data::generate_element( $input ); 793 891 unset( $element['id'] ); 794 return $this->prepare_new_subtree( $element ); 892 $element = $this->prepare_new_subtree( $element ); 893 $wire_element = static function ( array $node ) use ( &$wire_element ): array { 894 if ( isset( $node['settings'] ) && [] === $node['settings'] ) { 895 $node['settings'] = (object) []; 896 } 897 foreach ( $node['elements'] ?? [] as $index => $child ) { 898 $node['elements'][ $index ] = $wire_element( $child ); 899 } 900 return $node; 901 }; 902 return $wire_element( $element ); 795 903 } 796 904 … … 803 911 public static function validate_creation_input( string $name, array $input ): void { 804 912 $seen = []; 913 if ( 'wp_elementor_wrap_element' === $name ) { 914 if ( array_key_exists( 'settings', $input ) ) { 915 self::validate_settings_object( $input['settings'], 'settings' ); 916 } 917 if ( array_key_exists( 'element_after', $input ) ) { 918 self::validate_element_tree( [ $input['element_after'] ], 'element_after', $seen, true, true ); 919 if ( ! in_array( $input['element_after']['elType'], [ 'widget', 'container' ], true ) ) { 920 throw new Frontman_Tool_Error( 'element_after must be a widget or container.' ); 921 } 922 } 923 return; 924 } 805 925 if ( 'wp_elementor_add_element' === $name ) { 806 926 self::validate_element_tree( [ $input['element'] ?? null ], 'element', $seen, true, true ); -
frontman-agentic-ai-editor/trunk/tools/class-tool-options.php
r3767236 r3783407 215 215 $tools->add( new Frontman_Tool_Definition( 216 216 'wp_list_theme_mods', 217 'Lists active theme mods/customizer settings. Use this to inspect theme-rendered source state such as header images, page title options, and layout settingsbefore choosing a mutation path.',217 'Lists stored active theme mods, not every Customizer setting. For native Astra Header Builder or other registered settings missing here, use wp_list_customizer_settings. Inspect theme-rendered source state before choosing a mutation path.', 218 218 [ 219 219 'type' => 'object', … … 226 226 $tools->add( new Frontman_Tool_Definition( 227 227 'wp_get_theme_mod', 228 'Reads one active theme mod /customizer setting by name. Use this before changing theme-rendered elements.',228 'Reads one active theme mod by name, not option-backed Customizer settings. A null value does not mean an option-backed setting is absent. Use wp_list_customizer_settings and wp_get_customizer_setting for native Astra Header Builder settings.', 229 229 [ 230 230 'type' => 'object', … … 257 257 true 258 258 ) ); 259 260 $tools->add( new Frontman_Tool_Definition( 261 'wp_list_customizer_settings', 262 'Discover registered, accessible active-theme Customizer settings before using CSS to override native theme layout. For Astra Header Builder, search for header-desktop-items or header-mobile-items. Supports top-level theme mods and indexed astra-settings options when Astra is the active parent theme, not arbitrary plugin options. Results are bounded and contain IDs, not values; use wp_get_customizer_setting before editing.', 263 [ 264 'type' => 'object', 'additionalProperties' => false, 265 'properties' => [ 266 'search' => [ 'type' => 'string', 'description' => 'Optional case-insensitive substring of the setting ID.' ], 267 'offset' => [ 'type' => 'integer', 'minimum' => 0 ], 268 'limit' => [ 'type' => 'integer', 'minimum' => 1, 'maximum' => 100 ], 269 ], 270 ], 271 [ $this, 'list_customizer_settings' ], null, true, true 272 ) ); 273 $tools->add( new Frontman_Tool_Definition( 274 'wp_get_customizer_setting', 275 'Read one supported registered Customizer setting, including its stored value or registered default, storage presence, and fingerprint. Preserve existing layout components, logo and menu assignments unless the user requests changes. Use the exact ID and fingerprint for wp_update_customizer_setting. Filters and Additional CSS may affect rendering; inspect them separately.', 276 [ 277 'type' => 'object', 'additionalProperties' => false, 278 'properties' => [ 'id' => [ 'type' => 'string', 'description' => 'Exact ID from wp_list_customizer_settings.' ] ], 279 'required' => [ 'id' ], 280 ], 281 [ $this, 'get_customizer_setting' ], null, true, true 282 ) ); 283 $tools->add( new Frontman_Tool_Definition( 284 'wp_update_customizer_setting', 285 'Save one confirmed supported registered Customizer setting using native validation, sanitization and save. Read it first and supply its fingerprint and active stylesheet. Conflict detection is best-effort, not atomic. Reread after any failure or lost response; never retry automatically. Verify the rendered site separately.', 286 [ 287 'type' => 'object', 'additionalProperties' => false, 288 'properties' => [ 289 'id' => [ 'type' => 'string' ], 290 'value' => [ 'type' => [ 'string', 'number', 'boolean', 'array', 'object' ] ], 291 'stylesheet' => [ 'type' => 'string', 'description' => 'Active stylesheet from the read.' ], 292 'expected_fingerprint' => [ 'type' => 'string', 'pattern' => '^[a-f0-9]{64}$' ], 293 'confirm' => [ 'type' => 'boolean', 'description' => 'Must be true after user approval.' ], 294 ], 295 'required' => [ 'id', 'value', 'stylesheet', 'expected_fingerprint', 'confirm' ], 296 ], 297 [ $this, 'update_customizer_setting' ], null, true, true 298 ) ); 259 299 } 260 300 … … 620 660 } 621 661 622 global $wp_customize; 623 if ( ! ( $wp_customize instanceof \WP_Customize_Manager ) ) { 624 if ( ! class_exists( '\WP_Customize_Manager' ) ) { 625 require_once ABSPATH . WPINC . '/class-wp-customize-manager.php'; 626 } 627 $wp_customize = new \WP_Customize_Manager( [ 'settings_previewed' => false ] ); 628 remove_action( 'customize_register', [ $wp_customize, 'register_controls' ] ); 629 $wp_customize->register_controls(); 630 do_action( 'customize_register', $wp_customize ); 631 } 662 $wp_customize = $this->customizer_manager(); 632 663 $setting = $wp_customize->get_setting( $current['name'] ); 633 664 if ( ! $setting || 'theme_mod' !== $setting->type || ! empty( $setting->id_data()['keys'] ) ) { 634 throw new Frontman_Tool_Error( 'Only registered top-level Customizer theme mods can be updated. ' );665 throw new Frontman_Tool_Error( 'Only registered top-level Customizer theme mods can be updated. For supported indexed Astra settings use wp_list_customizer_settings and wp_update_customizer_setting.' ); 635 666 } 636 667 if ( ! $setting->check_capabilities() ) { 637 668 throw new Frontman_Tool_Error( 'You cannot edit this Customizer setting.' ); 638 669 } 639 $valid = $setting->validate( $input['value'] ); 640 if ( is_wp_error( $valid ) ) { 641 throw new Frontman_Tool_Error( $valid->get_error_message() ); 642 } 643 $value = $setting->sanitize( $input['value'] ); 644 if ( null === $value || is_wp_error( $value ) ) { 645 throw new Frontman_Tool_Error( 'Customizer setting rejected the value.' ); 646 } 647 $mods = get_theme_mods(); 648 $before = $mods[ $current['name'] ] ?? null; 649 $wp_customize->set_post_value( $current['name'], $input['value'] ); 650 $filter = 'pre_set_theme_mod_' . $current['name']; 651 $observe = static function( $filtered ) use ( &$value ) { 652 $value = $filtered; 653 return $filtered; 654 }; 655 add_filter( $filter, $observe, PHP_INT_MAX ); 656 try { 657 do_action( 'customize_save', $wp_customize ); 658 if ( false === $setting->save() ) { 659 throw new Frontman_Tool_Error( 'Customizer setting could not be saved. Read current state before retrying.' ); 660 } 661 do_action( 'customize_save_after', $wp_customize ); 662 } finally { 663 remove_filter( $filter, $observe, PHP_INT_MAX ); 664 } 665 $mods = get_theme_mods(); 666 $after = $mods[ $current['name'] ] ?? null; 667 if ( $after !== $value ) { 668 throw new Frontman_Tool_Error( 'Theme mod write could not be verified. Read current state before retrying.' ); 669 } 670 $state = $this->customizer_state( $setting ); 671 $result = $this->save_customizer_setting( $wp_customize, $setting, $input['value'], $current['stylesheet'], $state ); 670 672 671 673 return [ 672 674 'name' => $current['name'], 673 675 'stylesheet' => $current['stylesheet'], 674 'updated' => $before !== $after, 675 'before' => $before, 676 'after' => $after, 677 ]; 676 'updated' => ( $state['present'] ? $result['before'] : null ) !== $result['after'], 677 'before' => $state['present'] ? $result['before'] : null, 678 'after' => $result['after'], 679 ]; 680 } 681 682 /** 683 * Initialize the same registration hooks used by the Customizer. 684 * 685 * Astra skips two registration hooks on frontend requests. Its public 686 * callbacks provide the configuration and register the actual settings. 687 */ 688 private function customizer_manager() { 689 if ( ! current_user_can( 'edit_theme_options' ) ) { 690 throw new Frontman_Tool_Error( 'You cannot edit theme options.' ); 691 } 692 global $wp_customize; 693 if ( $wp_customize instanceof \WP_Customize_Manager ) { 694 if ( $wp_customize->settings_previewed() || $wp_customize->is_preview() ) { 695 throw new Frontman_Tool_Error( 'Customizer preview managers are not supported; use a non-preview request.' ); 696 } 697 if ( $this->is_astra_theme() && ( ! class_exists( 'Astra_Customizer' ) || false === has_filter( 'customize_dynamic_setting_args', [ \Astra_Customizer::get_instance(), 'filter_dynamic_setting_args' ] ) ) ) { 698 throw new Frontman_Tool_Error( 'Astra Customizer was initialized without its native settings. Use a fresh request to discover settings.' ); 699 } 700 $this->register_astra_dynamic_settings( $wp_customize ); 701 return $wp_customize; 702 } 703 if ( ! class_exists( '\WP_Customize_Manager' ) ) { 704 require_once ABSPATH . WPINC . '/class-wp-customize-manager.php'; 705 } 706 $wp_customize = new \WP_Customize_Manager( [ 'settings_previewed' => false ] ); 707 if ( $this->is_astra_theme() && class_exists( 'Astra_Customizer' ) ) { 708 $astra = \Astra_Customizer::get_instance(); 709 foreach ( [ 'include_configurations' => 1, 'prepare_customizer_javascript_configs' => 10, 'prepare_group_configs' => 9 ] as $method => $priority ) { 710 if ( is_callable( [ $astra, $method ] ) && false === has_action( 'customize_register', [ $astra, $method ] ) ) { 711 add_action( 'customize_register', [ $astra, $method ], $priority ); 712 } 713 } 714 if ( is_callable( [ $astra, 'filter_dynamic_setting_args' ] ) && false === has_filter( 'customize_dynamic_setting_args', [ $astra, 'filter_dynamic_setting_args' ] ) ) { 715 add_filter( 'customize_dynamic_setting_args', [ $astra, 'filter_dynamic_setting_args' ], 10, 2 ); 716 } 717 } 718 remove_action( 'customize_register', [ $wp_customize, 'register_controls' ] ); 719 $wp_customize->register_controls(); 720 do_action( 'customize_register', $wp_customize ); 721 $this->register_astra_dynamic_settings( $wp_customize ); 722 return $wp_customize; 723 } 724 725 private function register_astra_dynamic_settings( $manager ): void { 726 if ( ! $this->is_astra_theme() || ! class_exists( 'Astra_Customizer' ) ) { 727 return; 728 } 729 $cache = get_option( 'astra_partials_config_cache', [] ); 730 $entries = is_array( $cache ) && isset( $cache['settings'] ) && is_array( $cache['settings'] ) ? $cache['settings'] : []; 731 $ids = []; 732 foreach ( array_keys( $entries ) as $id ) { 733 if ( is_string( $id ) && preg_match( '/\Aastra-settings\[[^\[\]\/\\\\\x00-\x1f]+\]\z/', $id ) ) { 734 $ids[] = $id; 735 } 736 } 737 if ( count( $ids ) > 2000 ) { 738 throw new Frontman_Tool_Error( 'Astra Customizer registration exceeds the supported discovery bound.' ); 739 } 740 $manager->add_dynamic_settings( $ids ); 741 } 742 743 private function is_astra_theme(): bool { 744 return function_exists( 'get_template' ) && 'astra' === get_template(); 745 } 746 747 private function supported_customizer_setting( $setting ): bool { 748 if ( ! $setting || ! $setting->check_capabilities() ) { 749 return false; 750 } 751 $data = $setting->id_data(); 752 if ( 'theme_mod' === $setting->type ) { 753 return empty( $data['keys'] ); 754 } 755 return 'option' === $setting->type && $this->is_astra_theme() 756 && 'astra-settings' === $data['base'] && 1 === count( $data['keys'] ) 757 && is_string( $data['keys'][0] ) && '' !== $data['keys'][0]; 758 } 759 760 private function customizer_setting( $manager, $id ) { 761 if ( ! is_string( $id ) || '' === $id || strlen( $id ) > 256 || $this->has_control_or_path_separator( $id ) ) { 762 throw new Frontman_Tool_Error( 'id must be a valid registered Customizer setting ID.' ); 763 } 764 $setting = $manager->get_setting( $id ); 765 if ( ! $this->supported_customizer_setting( $setting ) ) { 766 throw new Frontman_Tool_Error( 'Setting is not registered, accessible, or supported. Use wp_list_customizer_settings to discover supported IDs.' ); 767 } 768 return $setting; 769 } 770 771 private function customizer_state( $setting ): array { 772 global $wpdb; 773 $data = $setting->id_data(); 774 $name = 'option' === $setting->type ? $data['base'] : 'theme_mods_' . $this->active_stylesheet(); 775 $row = $wpdb->get_row( $wpdb->prepare( "SELECT option_value FROM {$wpdb->options} WHERE option_name = %s", $name ) ); 776 if ( '' !== $wpdb->last_error ) { 777 throw new Frontman_Tool_Error( 'Could not read persisted Customizer state. Do not retry a save automatically.' ); 778 } 779 $root = $row ? maybe_unserialize( $row->option_value ) : null; 780 $key = 'option' === $setting->type ? $data['keys'][0] : $data['base']; 781 $present = is_array( $root ) && array_key_exists( $key, $root ); 782 $stored = $present ? $root[ $key ] : null; 783 $value = $present ? $stored : $setting->default; 784 return [ 785 'value' => $value, 786 'present' => $present, 787 'fingerprint' => hash( 'sha256', serialize( [ $this->active_stylesheet(), $setting->id, $present, $stored ] ) ), 788 ]; 789 } 790 791 private function assert_astra_aggregate_current( $setting ): void { 792 if ( 'option' !== $setting->type ) { 793 return; 794 } 795 global $wpdb; 796 $row = $wpdb->get_row( $wpdb->prepare( "SELECT option_value FROM {$wpdb->options} WHERE option_name = %s", 'astra-settings' ) ); 797 if ( '' !== $wpdb->last_error ) { 798 throw new \RuntimeException( 'Could not read the Astra option root.' ); 799 } 800 $root = $row ? maybe_unserialize( $row->option_value ) : []; 801 $property = new \ReflectionProperty( \WP_Customize_Setting::class, 'aggregated_multidimensionals' ); 802 $property->setAccessible( true ); 803 $aggregates = $property->getValue(); 804 if ( ! isset( $aggregates['option']['astra-settings']['root_value'] ) || $root !== $aggregates['option']['astra-settings']['root_value'] ) { 805 throw new \RuntimeException( 'Astra option root changed since Customizer registration; use a fresh request and reread settings.' ); 806 } 807 } 808 809 public function list_customizer_settings( array $input ): array { 810 $search = $input['search'] ?? ''; 811 $offset = $input['offset'] ?? 0; 812 $limit = $input['limit'] ?? 50; 813 if ( ! is_string( $search ) || strlen( $search ) > 256 || ! is_int( $offset ) || $offset < 0 || ! is_int( $limit ) || $limit < 1 || $limit > 100 ) { 814 throw new Frontman_Tool_Error( 'Invalid search, offset, or limit (1–100).' ); 815 } 816 $manager = $this->customizer_manager(); 817 $settings = []; 818 foreach ( $manager->settings() as $setting ) { 819 if ( $this->supported_customizer_setting( $setting ) && ( '' === $search || false !== stripos( $setting->id, $search ) ) ) { 820 $settings[] = [ 'id' => $setting->id, 'type' => $setting->type ]; 821 } 822 } 823 usort( $settings, static function( $a, $b ) { return strcmp( $a['id'], $b['id'] ); } ); 824 return [ 825 'stylesheet' => $this->active_stylesheet(), 826 'settings' => array_slice( $settings, $offset, $limit ), 827 'next_offset' => $offset + $limit < count( $settings ) ? $offset + $limit : null, 828 ]; 829 } 830 831 public function get_customizer_setting( array $input ): array { 832 $manager = $this->customizer_manager(); 833 $setting = $this->customizer_setting( $manager, $input['id'] ?? null ); 834 return array_merge( 835 [ 'stylesheet' => $this->active_stylesheet(), 'id' => $setting->id, 'type' => $setting->type ], 836 $this->customizer_state( $setting ) 837 ); 838 } 839 840 public function update_customizer_setting( array $input ): array { 841 if ( true !== ( $input['confirm'] ?? false ) ) { 842 throw new Frontman_Tool_Error( 'Customizer update requires confirm=true after user approval.' ); 843 } 844 $manager = $this->customizer_manager(); 845 $stylesheet = $this->active_stylesheet(); 846 if ( '' === $stylesheet || ( $input['stylesheet'] ?? null ) !== $stylesheet ) { 847 throw new Frontman_Tool_Error( 'stylesheet must match the active theme. Read the setting again.' ); 848 } 849 if ( ! array_key_exists( 'value', $input ) || null === $input['value'] || is_object( $input['value'] ) ) { 850 throw new Frontman_Tool_Error( 'value is required and must be a JSON scalar, array or object decoded as an array.' ); 851 } 852 $setting = $this->customizer_setting( $manager, $input['id'] ?? null ); 853 $before = $this->customizer_state( $setting ); 854 $expected = $input['expected_fingerprint'] ?? null; 855 if ( ! is_string( $expected ) || ! preg_match( '/\A[a-f0-9]{64}\z/', $expected ) || ! hash_equals( $before['fingerprint'], $expected ) ) { 856 throw new Frontman_Tool_Error( "Setting {$setting->id} changed or has an invalid fingerprint. Read it again; no writes were attempted." ); 857 } 858 return $this->save_customizer_setting( $manager, $setting, $input['value'], $stylesheet, $before ); 859 } 860 861 private function save_customizer_setting( $manager, $setting, $value, string $stylesheet, array $before ): array { 862 $manager->set_post_value( $setting->id, $value ); 863 try { 864 $valid = $setting->validate( $value ); 865 if ( is_wp_error( $valid ) ) { 866 throw new Frontman_Tool_Error( $valid->get_error_message() ); 867 } 868 $expected = $setting->sanitize( $value ); 869 if ( null === $expected || is_wp_error( $expected ) ) { 870 throw new Frontman_Tool_Error( 'Customizer setting rejected the value; no writes were attempted.' ); 871 } 872 if ( $stylesheet !== $this->active_stylesheet() || $before['fingerprint'] !== $this->customizer_state( $setting )['fingerprint'] ) { 873 throw new Frontman_Tool_Error( 'Theme or setting changed during validation; no writes were attempted.' ); 874 } 875 try { 876 $this->assert_astra_aggregate_current( $setting ); 877 } catch ( \RuntimeException $e ) { 878 throw new Frontman_Tool_Error( $e->getMessage() ); 879 } 880 881 $data = $setting->id_data(); 882 $filter = 'pre_set_theme_mod_' . $data['base']; 883 $observe = static function( $filtered ) use ( &$expected ) { 884 $expected = $filtered; 885 return $filtered; 886 }; 887 if ( 'theme_mod' === $setting->type ) { 888 add_filter( $filter, $observe, PHP_INT_MAX ); 889 } 890 try { 891 do_action( 'customize_save', $manager ); 892 if ( $stylesheet !== $this->active_stylesheet() ) { 893 throw new \RuntimeException( 'Active stylesheet changed during save.' ); 894 } 895 $this->assert_astra_aggregate_current( $setting ); 896 if ( false === $setting->save() ) { 897 throw new \RuntimeException( 'Customizer setting save failed.' ); 898 } 899 do_action( 'customize_save_after', $manager ); 900 $after = $this->customizer_state( $setting ); 901 if ( $stylesheet !== $this->active_stylesheet() || ! $after['present'] || $after['value'] !== $expected ) { 902 throw new \RuntimeException( 'Persisted Customizer value could not be verified.' ); 903 } 904 } catch ( \Throwable $e ) { 905 throw new Frontman_Tool_Error( $e->getMessage() . ' The setting may have been saved. Reread current state; do not retry automatically.' ); 906 } finally { 907 if ( 'theme_mod' === $setting->type ) { 908 remove_filter( $filter, $observe, PHP_INT_MAX ); 909 } 910 } 911 return [ 912 'id' => $setting->id, 913 'stylesheet' => $stylesheet, 914 'before' => $before['value'], 915 'after' => $after['value'], 916 'updated' => $before['value'] !== $after['value'], 917 ]; 918 } finally { 919 $manager->set_post_value( $setting->id, null ); 920 } 678 921 } 679 922 -
frontman-agentic-ai-editor/trunk/tools/class-tool-posts.php
r3767236 r3783407 49 49 'default' => 'post', 50 50 ], 51 'lang' => [ 52 'type' => 'string', 53 'description' => 'Optional Polylang language slug to filter posts. Requires active Polylang and a configured language.', 54 ], 51 55 'status' => [ 52 56 'type' => 'string', … … 95 99 'id' => [ 96 100 'type' => 'integer', 101 'minimum' => 1, 97 102 'description' => 'The post ID to read.', 98 103 ], … … 100 105 'required' => [ 'id' ], 101 106 ], 102 [ $this, 'read_post' ] 107 [ $this, 'read_post' ], 108 null, 109 true, 110 false, 111 static fn( \stdClass $input ): bool => true 103 112 ) ); 104 113 … … 234 243 235 244 $tools->add( new Frontman_Tool_Definition( 245 'wp_save_post_preview', 246 'Saves proposed content as a native autosave for a published post or page without changing the published content. Open the returned preview_url while signed in as the same user. Existing differing autosaves are preserved and cause an error. After approval use wp_update_post to publish; preview URLs are temporary.', 247 [ 248 'type' => 'object', 249 'additionalProperties' => false, 250 'properties' => [ 251 'id' => [ 'type' => 'integer', 'minimum' => 1, 'description' => 'Published post or page ID.' ], 252 'content' => [ 'type' => 'string', 'description' => 'Complete proposed HTML or block markup.' ], 253 ], 254 'required' => [ 'id', 'content' ], 255 ], 256 [ $this, 'save_post_preview' ], 257 'read-write', 258 true, 259 true 260 ) ); 261 262 $tools->add( new Frontman_Tool_Definition( 236 263 'wp_delete_post', 237 264 'Deletes a post or page. Ask the user for confirmation first and only call this tool with confirm=true after they approve. By default moves to trash; set force=true to permanently delete.', … … 258 285 [ $this, 'delete_post' ] 259 286 ) ); 287 } 288 289 /** Save only a revision: core's preview handler reads the latest autosave across authors. */ 290 public function save_post_preview( array $input ): array { 291 if ( ! isset( $input['id'] ) || ! is_int( $input['id'] ) || $input['id'] <= 0 ) { 292 throw new Frontman_Tool_Error( 'id must be a positive integer.' ); 293 } 294 if ( ! array_key_exists( 'content', $input ) ) { 295 throw new Frontman_Tool_Error( 'content is required and must be a string.' ); 296 } 297 $post = get_post( $input['id'] ); 298 if ( ! $post ) { 299 throw new Frontman_Tool_Error( 'Post not found.' ); 300 } 301 $this->check_write_permissions( [], $post->post_type, $post ); 302 $type = get_post_type_object( $post->post_type ); 303 if ( 'publish' !== $post->post_status || ! $type || ! is_post_type_viewable( $type ) || ! post_type_supports( $post->post_type, 'revisions' ) ) { 304 throw new Frontman_Tool_Error( 'Preview requires a published, publicly viewable post type with revision support.' ); 305 } 306 $this->check_content( $input ); 307 if ( ( class_exists( 'Frontman_Elementor_Data' ) && Frontman_Elementor_Data::post_uses_elementor( $post->ID ) ) 308 || get_post_meta( $post->ID, '_et_pb_use_builder', true ) === 'on' 309 || get_post_meta( $post->ID, '_fl_builder_enabled', true ) 310 || get_post_meta( $post->ID, '_bricks_page_content_2', true ) ) { 311 throw new Frontman_Tool_Error( 'Builder-managed content cannot be previewed with this tool.' ); 312 } 313 314 $autosaves = get_posts( [ 315 'post_type' => 'revision', 'post_status' => 'inherit', 'post_parent' => $post->ID, 316 'name' => $post->ID . '-autosave-v1', 'posts_per_page' => -1, 317 ] ); 318 foreach ( $autosaves as $autosave ) { 319 if ( $autosave->post_content !== $input['content'] || $autosave->post_title !== $post->post_title || $autosave->post_excerpt !== $post->post_excerpt ) { 320 throw new Frontman_Tool_Error( 'A differing editor autosave exists. It was preserved; resolve it in WordPress before previewing.' ); 321 } 322 } 323 $autosave = wp_get_post_autosave( $post->ID ); 324 if ( ! $autosave ) { 325 $data = get_post( $post->ID, ARRAY_A ); 326 $data['post_content'] = $input['content']; 327 $id = _wp_put_post_revision( $data, true ); 328 if ( is_wp_error( $id ) || ! $id ) { 329 throw new Frontman_Tool_Error( is_wp_error( $id ) ? $id->get_error_message() : 'Could not save the autosave.' ); 330 } 331 $autosave = get_post( $id ); 332 if ( ! $autosave || $autosave->post_content !== $input['content'] || $autosave->post_title !== $post->post_title || $autosave->post_excerpt !== $post->post_excerpt ) { 333 throw new Frontman_Tool_Error( "Autosave {$id} was saved, but readback differs. It was not rolled back; inspect it in WordPress." ); 334 } 335 } 336 $autosaves = get_posts( [ 337 'post_type' => 'revision', 'post_status' => 'inherit', 'post_parent' => $post->ID, 338 'name' => $post->ID . '-autosave-v1', 'posts_per_page' => -1, 339 ] ); 340 $selected = wp_get_post_autosave( $post->ID ); 341 if ( ! $selected ) { 342 throw new Frontman_Tool_Error( 'The preview autosave is no longer available; inspect autosaves in WordPress.' ); 343 } 344 $autosaves[] = $selected; 345 foreach ( $autosaves as $candidate ) { 346 if ( $candidate->post_content !== $input['content'] || $candidate->post_title !== $post->post_title || $candidate->post_excerpt !== $post->post_excerpt ) { 347 throw new Frontman_Tool_Error( 'A differing editor autosave exists. It was preserved; a new autosave may also have been created. Resolve autosaves in WordPress before previewing.' ); 348 } 349 } 350 $autosave = $selected; 351 return [ 352 'parent_id' => $post->ID, 353 'autosave_id' => $autosave->ID, 354 'preview_url' => get_preview_post_link( $post, [ 355 'preview_id' => $post->ID, 356 'preview_nonce' => wp_create_nonce( 'post_preview_' . $post->ID ), 357 ] ), 358 ]; 260 359 } 261 360 … … 273 372 ]; 274 373 374 if ( array_key_exists( 'lang', $input ) ) { 375 if ( ! function_exists( 'pll_languages_list' ) || ! in_array( $input['lang'], pll_languages_list( [ 'hide_empty' => false, 'fields' => 'slug' ] ), true ) ) { 376 throw new Frontman_Tool_Error( 'lang must be a configured Polylang language slug; Polylang must be active.' ); 377 } 378 if ( ! function_exists( 'pll_is_translated_post_type' ) || ! pll_is_translated_post_type( $args['post_type'] ) ) { 379 throw new Frontman_Tool_Error( 'Polylang does not manage the requested post type; language filtering is unavailable.' ); 380 } 381 $args['lang'] = $input['lang']; 382 } 275 383 if ( ! empty( $input['search'] ) ) { 276 384 $args['s'] = sanitize_text_field( $input['search'] ); … … 290 398 'modified' => $post->post_modified, 291 399 'excerpt' => wp_trim_words( $post->post_content, 30 ), 400 'lang' => function_exists( 'pll_get_post_language' ) ? ( pll_get_post_language( $post->ID, 'slug' ) ?: null ) : null, 292 401 ]; 293 402 } -
frontman-agentic-ai-editor/trunk/tools/class-tool-seo.php
r3767236 r3783407 1 1 <?php 2 2 /** 3 * Provider-neutral SEO tools for the runtime-validated Yoast route.3 * Stored SEO inspection and runtime-validated Yoast updates. 4 4 * @package Frontman 5 5 */ … … 10 10 class Frontman_Tool_Seo { 11 11 private const YOAST = 'wordpress-seo/wp-seo.php'; 12 private const SURERANK = 'surerank/surerank.php'; 12 13 private const META = [ 'title' => 'title', 'description' => 'metadesc', 'focus_keyphrase' => 'focuskw' ]; 14 private const SURERANK_META = [ 15 'title' => 'surerank_settings_page_title', 16 'description' => 'surerank_settings_page_description', 17 'focus_keyphrase' => 'surerank_settings_focus_keyword', 18 ]; 13 19 public function register( Frontman_Tools $tools ): void { 20 if ( null !== self::read_unavailable_reason() ) { 21 return; 22 } 14 23 $schema = [ 15 24 'type' => 'object', … … 20 29 $tools->add( new Frontman_Tool_Definition( 21 30 'wp_read_seo', 22 'Reads stored SEO title, description, and focus keyphrase for an editable post .',31 'Reads stored SEO title, description, and focus keyphrase for an editable post, not computed tags.', 23 32 $schema, 24 33 [ $this, 'read_seo' ], 25 34 'read' 26 35 ) ); 36 if ( null !== self::unavailable_reason() ) { 37 return; 38 } 27 39 $schema['properties'] += [ 28 40 'title' => [ 'type' => 'string', 'description' => 'SEO title override.' ], … … 30 42 'focus_keyphrase' => [ 'type' => 'string', 'description' => 'Single Yoast focus keyphrase, not secondary keywords.' ], 31 43 ]; 32 $schema['anyOf'] = array_map( static fn( string $field ): array => [ 'required' => [ $field ] ], array_keys( self::META ) );33 44 $tools->add( new Frontman_Tool_Definition( 34 45 'wp_update_seo', 35 'Updates SEO overrides or the focus keyphrase. Omitted fields stay unchanged; empty strings restore provider defaults.',46 'Updates SEO overrides or the focus keyphrase. Provide at least one of title, description, or focus_keyphrase. Omitted fields stay unchanged; empty strings restore provider defaults.', 36 47 $schema, 37 48 [ $this, 'update_seo' ], … … 39 50 ) ); 40 51 } 52 public static function read_unavailable_reason(): ?string { 53 return self::compatibility_reason( false ); 54 } 41 55 public static function unavailable_reason(): ?string { 56 return self::compatibility_reason( true ); 57 } 58 public static function capabilities(): array { 59 $read_reason = self::read_unavailable_reason(); 60 $write_reason = self::unavailable_reason(); 61 return [ 62 'provider' => null === $read_reason ? self::provider() : null, 63 'read' => null === $read_reason, 64 'write' => null === $write_reason, 65 'supported_fields' => null === $read_reason ? array_keys( self::META ) : [], 66 'read_unavailable_reason' => $read_reason, 67 'write_unavailable_reason' => $write_reason, 68 ]; 69 } 70 private static function provider(): string { 71 return Frontman_Plugin_Dependencies::is_available( self::SURERANK ) ? 'surerank' : 'yoast'; 72 } 73 private static function compatibility_reason( bool $write ): ?string { 42 74 global $wp_version; 43 if ( ! Frontman_Plugin_Dependencies::is_available( self::YOAST ) || ! defined( 'WPSEO_VERSION' ) ) { 44 return __( 'Yoast SEO Free is absent or not loaded.', 'frontman-agentic-ai-editor' ); 45 } 46 if ( Frontman_Plugin_Dependencies::is_available( 'wordpress-seo-premium/wp-seo-premium.php' ) 75 $yoast = Frontman_Plugin_Dependencies::is_available( self::YOAST ); 76 $surerank = Frontman_Plugin_Dependencies::is_available( self::SURERANK ); 77 if ( ! $yoast && ! $surerank ) { 78 return __( 'Yoast SEO Free and SureRank are absent or not loaded.', 'frontman-agentic-ai-editor' ); 79 } 80 if ( ( $yoast && $surerank ) 81 || Frontman_Plugin_Dependencies::is_available( 'wordpress-seo-premium/wp-seo-premium.php' ) 47 82 || Frontman_Plugin_Dependencies::is_available( 'seo-by-rank-math/rank-math.php', 'RankMath' ) 48 83 || Frontman_Plugin_Dependencies::is_available( 'all-in-one-seo-pack/all_in_one_seo_pack.php' ) || function_exists( 'aioseo' ) … … 54 89 return __( 'SEO tools require WordPress 6.9 or later.', 'frontman-agentic-ai-editor' ); 55 90 } 91 if ( $surerank ) { 92 return $write 93 ? __( 'SureRank SEO support is read-only; updates are not available.', 'frontman-agentic-ai-editor' ) 94 : self::surerank_unavailable_reason(); 95 } 96 if ( ! defined( 'WPSEO_VERSION' ) ) { 97 return __( 'Yoast SEO Free is absent or not loaded.', 'frontman-agentic-ai-editor' ); 98 } 56 99 $runtime_reason = __( 'Yoast SEO runtime methods, initialization, or metadata sanitizers are unavailable.', 'frontman-agentic-ai-editor' ); 57 if ( did_action( 'wpseo_loaded' ) < 1 || ! function_exists( 'sanitize_meta') ) {100 if ( did_action( 'wpseo_loaded' ) < 1 || ( $write && ! function_exists( 'sanitize_meta' ) ) ) { 58 101 return $runtime_reason; 59 102 } 60 foreach ( [ 'get_value', 'set_value', 'delete', 'sanitize_post_meta' ] as $method ) {103 foreach ( $write ? [ 'get_value', 'set_value', 'delete', 'sanitize_post_meta' ] : [ 'get_value' ] as $method ) { 61 104 if ( ! is_callable( [ 'WPSEO_Meta', $method ] ) ) { 62 105 return $runtime_reason; 63 106 } 107 } 108 if ( ! $write ) { 109 return null; 64 110 } 65 111 $registered = get_registered_meta_keys( 'post' ); … … 73 119 return null; 74 120 } 121 private static function surerank_unavailable_reason(): ?string { 122 if ( ! defined( 'SURERANK_VERSION' ) 123 || ! is_callable( [ '\SureRank\Inc\Functions\Get', 'post_meta' ] ) ) { 124 return __( 'SureRank SEO runtime metadata reader is unavailable.', 'frontman-agentic-ai-editor' ); 125 } 126 return null; 127 } 128 private function surerank_snapshot( int $id ): array { 129 $values = []; 130 try { 131 foreach ( self::SURERANK_META as $field => $key ) { 132 $values[ $field ] = \SureRank\Inc\Functions\Get::post_meta( $id, $key, true ); 133 } 134 } catch ( \Throwable $error ) { 135 throw new Frontman_Tool_Error( __( 'SureRank could not read the stored SEO overrides.', 'frontman-agentic-ai-editor' ) ); 136 } 137 foreach ( $values as $value ) { 138 if ( ! is_string( $value ) || 1 !== preg_match( '//u', $value ) ) { 139 throw new Frontman_Tool_Error( __( 'SureRank returned invalid SEO overrides.', 'frontman-agentic-ai-editor' ) ); 140 } 141 } 142 return $values; 143 } 75 144 public static function validate_input( string $tool, array $input ): array { 76 145 $allowed = 'wp_read_seo' === $tool ? [ 'id' ] : array_merge( [ 'id' ], array_keys( self::META ) ); … … 93 162 public function read_seo( array $input ): array { 94 163 $input = self::validate_input( 'wp_read_seo', $input ); 95 $this->guard( $input['id'] );96 return [ 'id' => $input['id'], 'provider' => 'yoast'] + $this->snapshot( $input['id'] );164 $this->guard( $input['id'], false ); 165 return [ 'id' => $input['id'], 'provider' => self::provider() ] + $this->snapshot( $input['id'] ); 97 166 } 98 167 public function update_seo( array $input ): array { … … 146 215 ]; 147 216 } 148 private function guard( int $id ): string {149 $reason = self::unavailable_reason();217 private function guard( int $id, bool $write = true ): string { 218 $reason = $write ? self::unavailable_reason() : self::read_unavailable_reason(); 150 219 if ( null !== $reason ) { 151 220 throw new Frontman_Tool_Error( $reason ); … … 161 230 throw new Frontman_Tool_Error( __( 'You cannot edit this post.', 'frontman-agentic-ai-editor' ) ); 162 231 } 163 foreach ( self::META as $key ) { 164 if ( ! current_user_can( 'edit_post_meta', $id, '_yoast_wpseo_' . $key ) ) { 232 $keys = 'surerank' === self::provider() ? self::SURERANK_META : array_map( static fn( string $key ): string => '_yoast_wpseo_' . $key, self::META ); 233 foreach ( $keys as $key ) { 234 if ( ! current_user_can( 'edit_post_meta', $id, $key ) ) { 165 235 throw new Frontman_Tool_Error( __( 'You cannot edit this post SEO metadata.', 'frontman-agentic-ai-editor' ) ); 166 236 } … … 169 239 } 170 240 private function snapshot( int $id ): array { 241 if ( 'surerank' === self::provider() ) { 242 return $this->surerank_snapshot( $id ); 243 } 171 244 $values = []; 172 245 try { -
frontman-agentic-ai-editor/trunk/tools/class-tool-templates.php
r3767236 r3783407 225 225 ], 226 226 'seo_unavailable_reason' => Frontman_Tool_Seo::unavailable_reason(), 227 'seo_capabilities' => Frontman_Tool_Seo::capabilities(), 227 228 'plugins' => $plugin_info, 228 229 'post_types' => $pt_list, -
frontman-agentic-ai-editor/trunk/tools/class-tool-users.php
r3686780 r3783407 20 20 'properties' => [ 21 21 'search' => [ 22 'type' => 'string', 'minLength' => 1, 'maxLength' => 100,22 'type' => 'string', 'minLength' => 1, 23 23 'description' => 'Trimmed nonempty literal substring, at most 100 Unicode characters. Must not start or end with * after whitespace trimming (including asterisk-only input). Interior *, %, _, and quotes are literal. Case sensitivity depends on database collation.', 24 24 ], -
frontman-agentic-ai-editor/trunk/tools/class-tool-widgets.php
r3758369 r3783407 24 24 public function register( Frontman_Tools $tools ): void { 25 25 $tools->add( new Frontman_Tool_Definition( 26 'wp_set_widget_visibility', 27 'Sets a per-widget WooCommerce visibility rule: all clears the rule; exclude_checkout preserves other routes; cart_only shows only on cart, never checkout. Uses semantic WooCommerce predicates, not URLs. Read back and verify the storefront.', 28 [ 29 'type' => 'object', 30 'additionalProperties' => false, 31 'properties' => [ 32 'widget_id' => [ 'type' => 'string' ], 33 'visibility' => [ 'type' => 'string', 'enum' => [ 'all', 'exclude_checkout', 'cart_only' ] ], 34 ], 35 'required' => [ 'widget_id', 'visibility' ], 36 ], 37 [ $this, 'set_widget_visibility' ] 38 ) ); 39 $tools->add( new Frontman_Tool_Definition( 26 40 'wp_list_widget_areas', 27 41 'Lists all registered widget areas (sidebars) and their active widget IDs.', … … 70 84 $tools->add( new Frontman_Tool_Definition( 71 85 'wp_update_widget', 72 'Updates a text, custom_html, or block widget without moving it. Text accepts title/text strings and filter/visual booleans; custom_html accepts title/content strings; block requires content containing the complete replacement markup, including all blocks. Omitted settings and plugin metadata are preserved. Read back and verify the rendered page.',86 'Updates text, custom_html, block, or an existing woocommerce_recently_viewed_products widget without moving it. Recently viewed accepts title string and number integer (1–15); empty browsing history produces no output. Text accepts title/text strings and filter/visual booleans; custom_html accepts title/content strings; block requires complete replacement content. Omitted settings and metadata are preserved. Verify the rendered page in the same browsing session.', 73 87 [ 74 88 'type' => 'object', … … 176 190 177 191 private function sanitized_settings( string $widget_base, $raw ): array { 178 $this->assert_mutation_supported( $widget_base ); 192 if ( 'woocommerce_recently_viewed_products' !== $widget_base ) { 193 $this->assert_mutation_supported( $widget_base ); 194 } 179 195 $settings = is_string( $raw ) ? json_decode( $raw ) : null; 180 196 if ( ! $settings instanceof \stdClass ) { … … 182 198 } 183 199 $settings = get_object_vars( $settings ); 200 if ( 'woocommerce_recently_viewed_products' === $widget_base ) { 201 foreach ( $settings as $key => $value ) { 202 if ( 'title' === $key && is_string( $value ) ) { 203 $settings[ $key ] = sanitize_text_field( $value ); 204 } elseif ( 'number' !== $key || ! is_int( $value ) || $value < 1 || $value > 15 ) { 205 throw new Frontman_Tool_Error( 'Recently viewed accepts only title (string) and number (integer, 1–15).' ); 206 } 207 } 208 return $settings; 209 } 184 210 switch ( $widget_base ) { 185 211 case 'text': … … 263 289 'sidebar_id' => $map[ $widget_id ]['sidebar_id'] ?? null, 264 290 'position' => $map[ $widget_id ]['position'] ?? null, 291 'visibility'=> get_option( 'frontman_widget_visibility', [] )[ $widget_id ] ?? 'all', 265 292 ]; 266 293 } … … 273 300 $sidebar_id = sanitize_key( $input['sidebar_id'] ?? '' ); 274 301 $widget_base = sanitize_key( $input['widget_base'] ?? '' ); 302 $this->assert_mutation_supported( $widget_base ); 275 303 $settings = $this->sanitized_settings( $widget_base, $input['settings'] ?? null ); 276 304 $before = $this->sidebar_snapshot( $sidebar_id ); … … 331 359 $all_settings = get_option( $option, [] ); 332 360 $all_settings[ $parts['number'] ] = array_merge( $widget['settings'], $settings ); 361 if ( 'woocommerce_recently_viewed_products' === $parts['base'] ) { 362 global $wp_widget_factory; 363 $native = $wp_widget_factory->widgets['WC_Widget_Recently_Viewed'] ?? null; 364 if ( ! $native instanceof \WC_Widget_Recently_Viewed ) { 365 throw new Frontman_Tool_Error( 'The native WooCommerce recently viewed widget is not available.' ); 366 } 367 $native->_set( $parts['number'] ); 368 $new_instance = array_merge( [ 'title' => '', 'number' => 10 ], $all_settings[ $parts['number'] ] ); 369 $instance = $native->update( $new_instance, $widget['settings'] ); 370 $instance = apply_filters( 'widget_update_callback', $instance, $new_instance, $widget['settings'], $native ); 371 if ( ! is_array( $instance ) ) { 372 throw new Frontman_Tool_Error( 'The native widget update was rejected.' ); 373 } 374 $all_settings[ $parts['number'] ] = $instance; 375 } 333 376 $this->save_settings( $option, $all_settings ); 334 377 … … 339 382 'settings' => $this->read_widget( [ 'widget_id' => $widget_id ] )['settings'], 340 383 ]; 384 } 385 386 /** 387 * Persist a bounded, reversible rule independently of native widget settings. 388 */ 389 public function set_widget_visibility( array $input ): array { 390 $this->assert_can_edit_widgets(); 391 $widget = $this->read_widget( $input ); 392 $rule = $input['visibility'] ?? null; 393 if ( ! in_array( $rule, [ 'all', 'exclude_checkout', 'cart_only' ], true ) ) { 394 throw new Frontman_Tool_Error( 'Invalid widget visibility rule.' ); 395 } 396 if ( 'all' !== $rule && ( ! function_exists( 'is_cart' ) || ! function_exists( 'is_checkout' ) ) ) { 397 throw new Frontman_Tool_Error( 'WooCommerce is required to set this visibility rule.' ); 398 } 399 $rules = get_option( 'frontman_widget_visibility', [] ); 400 unset( $rules[ $widget['widget_id'] ] ); 401 if ( 'all' !== $rule ) { 402 $rules[ $widget['widget_id'] ] = $rule; 403 } 404 $this->save_settings( 'frontman_widget_visibility', $rules ); 405 return [ 'before' => $widget['visibility'], 'widget' => $this->read_widget( $input ) ]; 406 } 407 408 /** 409 * Registered at plugin load, not during tool execution. Never resurrect a 410 * widget suppressed by another filter. Without WooCommerce, cart-only 411 * stays hidden and checkout exclusion leaves unrelated pages alone. 412 */ 413 public static function filter_visibility( $instance, $widget, $args ) { 414 if ( false === $instance ) { 415 return false; 416 } 417 $rule = get_option( 'frontman_widget_visibility', [] )[ $args['widget_id'] ?? $widget->id ] ?? 'all'; 418 $checkout = function_exists( 'is_checkout' ) && is_checkout(); 419 if ( ( 'exclude_checkout' === $rule && $checkout ) || 420 ( 'cart_only' === $rule && ( $checkout || ! function_exists( 'is_cart' ) || ! is_cart() ) ) ) { 421 return false; 422 } 423 return $instance; 341 424 } 342 425 -
frontman-agentic-ai-editor/trunk/tools/class-tool-woocommerce.php
r3642353 r3783407 213 213 214 214 private function register_settings( Frontman_Tools $tools ): void { 215 $catalog = $this->object_schema( [ 216 'name' => [ 217 'type' => 'string', 218 'description' => 'The shop page or product category archive display setting.', 219 'enum' => [ 'woocommerce_shop_page_display', 'woocommerce_category_archive_display' ], 220 ], 221 ], [ 'name' ] ); 222 $this->add_custom_tool( $tools, 'get_catalog_display', 'Reads a WooCommerce catalog display setting. Empty string means products; subcategories means categories; both means categories and products.', $catalog, 223 function( array $input ) { return $this->catalog_display( $input, false ); }, false 224 ); 225 $catalog['properties']['value'] = [ 226 'type' => 'string', 227 'description' => 'Empty string shows products directly; subcategories shows categories; both shows both.', 228 'enum' => [ '', 'subcategories', 'both' ], 229 ]; 230 $catalog['required'][] = 'value'; 231 $this->add_custom_tool( $tools, 'update_catalog_display', 'Updates only a WooCommerce catalog display setting through its native Customizer save path. Ask the user for confirmation before calling with confirm=true.', $this->with_confirm( $catalog ), 232 function( array $input ) { return $this->catalog_display( $input, true ); }, false 233 ); 215 234 $this->add_endpoint_tool( $tools, 'get_settings', 'Retrieves WooCommerce setting groups.', 'GET', '/settings', $this->empty_schema() ); 216 235 $this->add_endpoint_tool( $tools, 'get_setting_options', 'Retrieves options in one WooCommerce setting group.', 'GET', '/settings/{group}', $this->object_schema( [ 'group' => $this->string_prop( 'The WooCommerce setting group ID.' ) ], [ 'group' ] ) ); 217 236 $this->add_endpoint_tool( $tools, 'update_setting_option', 'Updates one WooCommerce setting option.', 'PUT', '/settings/{group}/{id}', $this->data_schema( 'settingData', 'Setting fields to update.', [ 'group', 'id', 'settingData' ], [ 'group' => $this->string_prop( 'The WooCommerce setting group ID.' ), 'id' => $this->string_prop( 'The WooCommerce setting ID.' ) ] ), [ 'body' => 'settingData' ] ); 237 } 238 239 public static function validate_catalog_display_input( array $input, bool $write ): void { 240 $name = $input['name'] ?? null; 241 if ( ! in_array( $name, [ 'woocommerce_shop_page_display', 'woocommerce_category_archive_display' ], true ) ) { 242 throw new Frontman_Tool_Error( 'Unsupported WooCommerce catalog display setting.' ); 243 } 244 if ( $write && true !== ( $input['confirm'] ?? false ) ) { 245 throw new Frontman_Tool_Error( 'Catalog display update requires confirm=true after user approval.' ); 246 } 247 if ( $write && ! in_array( $input['value'] ?? null, [ '', 'subcategories', 'both' ], true ) ) { 248 throw new Frontman_Tool_Error( 'value must be an empty string, subcategories, or both.' ); 249 } 250 } 251 252 private function catalog_display( array $input, bool $write ): array { 253 self::validate_catalog_display_input( $input, $write ); 254 $name = $input['name']; 255 if ( ! class_exists( 'WooCommerce' ) || ! class_exists( 'WC_Shop_Customizer' ) ) { 256 throw new Frontman_Tool_Error( 'WooCommerce is not active.' ); 257 } 258 if ( ! current_user_can( 'manage_woocommerce' ) || ! current_user_can( 'edit_theme_options' ) ) { 259 throw new Frontman_Tool_Error( 'You cannot manage WooCommerce catalog display settings.' ); 260 } 261 262 global $wp_customize; 263 if ( ! ( $wp_customize instanceof \WP_Customize_Manager ) ) { 264 require_once ABSPATH . WPINC . '/class-wp-customize-manager.php'; 265 $wp_customize = new \WP_Customize_Manager( [ 'settings_previewed' => false ] ); 266 remove_action( 'customize_register', [ $wp_customize, 'register_controls' ] ); 267 $wp_customize->register_controls(); 268 } 269 if ( ! $wp_customize->get_setting( $name ) ) { 270 $customizer = new \WC_Shop_Customizer(); 271 $customizer->add_product_catalog_section( $wp_customize ); 272 } 273 $setting = $wp_customize->get_setting( $name ); 274 if ( ! $setting || 'option' !== $setting->type || ! empty( $setting->id_data()['keys'] ) || ! $setting->check_capabilities() ) { 275 throw new Frontman_Tool_Error( 'Catalog display Customizer setting is unavailable or not permitted.' ); 276 } 277 $before = get_option( $name, $setting->default ); 278 if ( ! $write ) { 279 return [ 'name' => $name, 'value' => $before, 'supported_values' => [ '', 'subcategories', 'both' ] ]; 280 } 281 $valid = $setting->validate( $input['value'] ); 282 if ( is_wp_error( $valid ) ) { 283 throw new Frontman_Tool_Error( $valid->get_error_message() ); 284 } 285 $value = $setting->sanitize( $input['value'] ); 286 if ( ! in_array( $value, [ '', 'subcategories', 'both' ], true ) ) { 287 throw new Frontman_Tool_Error( 'Catalog display Customizer sanitizer rejected the value.' ); 288 } 289 $wp_customize->set_post_value( $name, $input['value'] ); 290 do_action( 'customize_save', $wp_customize ); 291 if ( false === $setting->save() ) { 292 throw new Frontman_Tool_Error( 'Catalog display setting could not be saved.' ); 293 } 294 do_action( 'customize_save_after', $wp_customize ); 295 $after = get_option( $name, $setting->default ); 296 if ( $after !== $value ) { 297 throw new Frontman_Tool_Error( 'Catalog display write could not be verified. Read current state before retrying.' ); 298 } 299 return [ 'name' => $name, 'before' => $before, 'after' => $after, 'updated' => $before !== $after ]; 218 300 } 219 301 … … 252 334 } 253 335 254 private function add_custom_tool( Frontman_Tools $tools, string $source_method, string $description, array $schema, callable $handler ): void {336 private function add_custom_tool( Frontman_Tools $tools, string $source_method, string $description, array $schema, callable $handler, bool $mirrors_rest = true ): void { 255 337 $tools->add( new Frontman_Tool_Definition( 256 338 'wc_' . $source_method, 257 $description . ' Mirrors woocommerce-mcp-server method `' . $source_method . '`using local WordPress authentication; no WooCommerce REST API keys are required.',339 $description . ( $mirrors_rest ? ' Mirrors woocommerce-mcp-server method `' . $source_method . '`' : ' Uses the native WooCommerce Customizer setting' ) . ' using local WordPress authentication; no WooCommerce REST API keys are required.', 258 340 $schema, 259 341 $handler, … … 281 363 $operation . '_' . $entity . '_meta', 282 364 $entity_label . ' metadata upsert. Creates the key when missing and updates the first matching key when present. Ask the user for confirmation before calling with confirm=true.', 283 $this->with_confirm( $this->object_schema( array_merge( $id_prop, [ 'metaKey' => $this->string_prop( 'The metadata key.' ), 'metaValue' => [ ' description' => 'The metadata value. Can be a scalar, object, array, or null.' ] ] ), [ $id_key, 'metaKey', 'metaValue' ] ) ),365 $this->with_confirm( $this->object_schema( array_merge( $id_prop, [ 'metaKey' => $this->string_prop( 'The metadata key.' ), 'metaValue' => [ 'type' => [ 'string', 'number', 'boolean', 'object', 'array', 'null' ], 'properties' => (object) [], 'description' => 'The metadata value. Can be a scalar, object, array, or null.' ] ] ), [ $id_key, 'metaKey', 'metaValue' ] ) ), 284 366 function( array $input ) use ( $path_template ) { return $this->upsert_meta( $path_template, $input ); } 285 367 ); -
frontman-agentic-ai-editor/trunk/vendor/composer/installed.php
r3767236 r3783407 3 3 namespace FrontmanVendor; 4 4 5 return array('root' => array('name' => 'frontman-ai/wordpress-diagnostics', 'pretty_version' => ' v5.5.0', 'version' => '5.5.0.0', 'reference' => '095f90276bb7e7f29365860049793a8700a204ec', 'type' => 'library', 'install_path' => __DIR__ . '/../../', 'aliases' => array(), 'dev' => \false), 'versions' => array('frontman-ai/wordpress-diagnostics' => array('pretty_version' => 'v5.5.0', 'version' => '5.5.0.0', 'reference' => '095f90276bb7e7f29365860049793a8700a204ec', 'type' => 'library', 'install_path' => __DIR__ . '/../../', 'aliases' => array(), 'dev_requirement' => \false), 'guzzlehttp/psr7' => array('pretty_version' => '3.1.0', 'version' => '3.1.0.0', 'reference' => 'a3059ba1a84c9139c4ae03cf0f45bea276c97c74', 'type' => 'library', 'install_path' => __DIR__ . '/../guzzlehttp/psr7', 'aliases' => array(), 'dev_requirement' => \false), 'jean85/pretty-package-versions' => array('pretty_version' => '2.1.1', 'version' => '2.1.1.0', 'reference' => '4d7aa5dab42e2a76d99559706022885de0e18e1a', 'type' => 'library', 'install_path' => __DIR__ . '/../jean85/pretty-package-versions', 'aliases' => array(), 'dev_requirement' => \false), 'psr/http-factory' => array('pretty_version' => '1.1.0', 'version' => '1.1.0.0', 'reference' => '2b4765fddfe3b508ac62f829e852b1501d3f6e8a', 'type' => 'library', 'install_path' => __DIR__ . '/../psr/http-factory', 'aliases' => array(), 'dev_requirement' => \false), 'psr/http-factory-implementation' => array('dev_requirement' => \false, 'provided' => array(0 => '1.1')), 'psr/http-message' => array('pretty_version' => '2.0', 'version' => '2.0.0.0', 'reference' => '402d35bcb92c70c026d1a6a9883f06b2ead23d71', 'type' => 'library', 'install_path' => __DIR__ . '/../psr/http-message', 'aliases' => array(), 'dev_requirement' => \false), 'psr/http-message-implementation' => array('dev_requirement' => \false, 'provided' => array(0 => '2.0')), 'psr/log' => array('pretty_version' => '1.1.4', 'version' => '1.1.4.0', 'reference' => 'd49695b909c3b7628b6289db5479a1c204601f11', 'type' => 'library', 'install_path' => __DIR__ . '/../psr/log', 'aliases' => array(), 'dev_requirement' => \false), 'sentry/sentry' => array('pretty_version' => '4.31.0', 'version' => '4.31.0.0', 'reference' => '39d9f86bd56c6cc6ae7b64c2b849f7f046d38394', 'type' => 'library', 'install_path' => __DIR__ . '/../sentry/sentry', 'aliases' => array(), 'dev_requirement' => \false), 'symfony/deprecation-contracts' => array('pretty_version' => 'v2.5.4', 'version' => '2.5.4.0', 'reference' => '605389f2a7e5625f273b53960dc46aeaf9c62918', 'type' => 'library', 'install_path' => __DIR__ . '/../symfony/deprecation-contracts', 'aliases' => array(), 'dev_requirement' => \false), 'symfony/options-resolver' => array('pretty_version' => 'v5.4.45', 'version' => '5.4.45.0', 'reference' => '74e5b6f0db3e8589e6cfd5efb317a1fc2bb52fb6', 'type' => 'library', 'install_path' => __DIR__ . '/../symfony/options-resolver', 'aliases' => array(), 'dev_requirement' => \false), 'symfony/polyfill-php73' => array('pretty_version' => 'v1.37.0', 'version' => '1.37.0.0', 'reference' => '0f68c03565dcaaf25a890667542e8bd75fe7e5bb', 'type' => 'library', 'install_path' => __DIR__ . '/../symfony/polyfill-php73', 'aliases' => array(), 'dev_requirement' => \false), 'symfony/polyfill-php80' => array('pretty_version' => 'v1.37.0', 'version' => '1.37.0.0', 'reference' => 'dfb55726c3a76ea3b6459fcfda1ec2d80a682411', 'type' => 'library', 'install_path' => __DIR__ . '/../symfony/polyfill-php80', 'aliases' => array(), 'dev_requirement' => \false), 'symfony/polyfill-php82' => array('pretty_version' => 'v1.38.1', 'version' => '1.38.1.0', 'reference' => '002dc0cfe5fd4ed6033d48f27d4f19a486c4b04b', 'type' => 'library', 'install_path' => __DIR__ . '/../symfony/polyfill-php82', 'aliases' => array(), 'dev_requirement' => \false)));5 return array('root' => array('name' => 'frontman-ai/wordpress-diagnostics', 'pretty_version' => 'dev-main', 'version' => 'dev-main', 'reference' => '30e52ed22bde0643d9a005704c12d90fa1f185c9', 'type' => 'library', 'install_path' => __DIR__ . '/../../', 'aliases' => array(), 'dev' => \false), 'versions' => array('frontman-ai/wordpress-diagnostics' => array('pretty_version' => 'dev-main', 'version' => 'dev-main', 'reference' => '30e52ed22bde0643d9a005704c12d90fa1f185c9', 'type' => 'library', 'install_path' => __DIR__ . '/../../', 'aliases' => array(), 'dev_requirement' => \false), 'guzzlehttp/psr7' => array('pretty_version' => '3.1.0', 'version' => '3.1.0.0', 'reference' => 'a3059ba1a84c9139c4ae03cf0f45bea276c97c74', 'type' => 'library', 'install_path' => __DIR__ . '/../guzzlehttp/psr7', 'aliases' => array(), 'dev_requirement' => \false), 'jean85/pretty-package-versions' => array('pretty_version' => '2.1.1', 'version' => '2.1.1.0', 'reference' => '4d7aa5dab42e2a76d99559706022885de0e18e1a', 'type' => 'library', 'install_path' => __DIR__ . '/../jean85/pretty-package-versions', 'aliases' => array(), 'dev_requirement' => \false), 'psr/http-factory' => array('pretty_version' => '1.1.0', 'version' => '1.1.0.0', 'reference' => '2b4765fddfe3b508ac62f829e852b1501d3f6e8a', 'type' => 'library', 'install_path' => __DIR__ . '/../psr/http-factory', 'aliases' => array(), 'dev_requirement' => \false), 'psr/http-factory-implementation' => array('dev_requirement' => \false, 'provided' => array(0 => '1.1')), 'psr/http-message' => array('pretty_version' => '2.0', 'version' => '2.0.0.0', 'reference' => '402d35bcb92c70c026d1a6a9883f06b2ead23d71', 'type' => 'library', 'install_path' => __DIR__ . '/../psr/http-message', 'aliases' => array(), 'dev_requirement' => \false), 'psr/http-message-implementation' => array('dev_requirement' => \false, 'provided' => array(0 => '2.0')), 'psr/log' => array('pretty_version' => '1.1.4', 'version' => '1.1.4.0', 'reference' => 'd49695b909c3b7628b6289db5479a1c204601f11', 'type' => 'library', 'install_path' => __DIR__ . '/../psr/log', 'aliases' => array(), 'dev_requirement' => \false), 'sentry/sentry' => array('pretty_version' => '4.31.0', 'version' => '4.31.0.0', 'reference' => '39d9f86bd56c6cc6ae7b64c2b849f7f046d38394', 'type' => 'library', 'install_path' => __DIR__ . '/../sentry/sentry', 'aliases' => array(), 'dev_requirement' => \false), 'symfony/deprecation-contracts' => array('pretty_version' => 'v2.5.4', 'version' => '2.5.4.0', 'reference' => '605389f2a7e5625f273b53960dc46aeaf9c62918', 'type' => 'library', 'install_path' => __DIR__ . '/../symfony/deprecation-contracts', 'aliases' => array(), 'dev_requirement' => \false), 'symfony/options-resolver' => array('pretty_version' => 'v5.4.45', 'version' => '5.4.45.0', 'reference' => '74e5b6f0db3e8589e6cfd5efb317a1fc2bb52fb6', 'type' => 'library', 'install_path' => __DIR__ . '/../symfony/options-resolver', 'aliases' => array(), 'dev_requirement' => \false), 'symfony/polyfill-php73' => array('pretty_version' => 'v1.37.0', 'version' => '1.37.0.0', 'reference' => '0f68c03565dcaaf25a890667542e8bd75fe7e5bb', 'type' => 'library', 'install_path' => __DIR__ . '/../symfony/polyfill-php73', 'aliases' => array(), 'dev_requirement' => \false), 'symfony/polyfill-php80' => array('pretty_version' => 'v1.37.0', 'version' => '1.37.0.0', 'reference' => 'dfb55726c3a76ea3b6459fcfda1ec2d80a682411', 'type' => 'library', 'install_path' => __DIR__ . '/../symfony/polyfill-php80', 'aliases' => array(), 'dev_requirement' => \false), 'symfony/polyfill-php82' => array('pretty_version' => 'v1.38.1', 'version' => '1.38.1.0', 'reference' => '002dc0cfe5fd4ed6033d48f27d4f19a486c4b04b', 'type' => 'library', 'install_path' => __DIR__ . '/../symfony/polyfill-php82', 'aliases' => array(), 'dev_requirement' => \false)));
Note:
See TracChangeset
for help on using the changeset viewer.
![(please configure the [header_logo] section in trac.ini)](/chrome/site/your_project_logo.png)