403Webshell
Server IP : 52.25.153.185  /  Your IP : 216.73.217.59
Web Server : Apache
System : Linux ip-172-26-6-158 5.10.0-45-cloud-amd64 #1 SMP Debian 5.10.259-1 (2026-07-02) x86_64
User : daemon ( 1)
PHP Version : 8.1.10
Disable Function : NONE
MySQL : OFF  |  cURL : ON  |  WGET : ON  |  Perl : ON  |  Python : OFF  |  Sudo : ON  |  Pkexec : OFF
Directory :  /bitnami/wordpress/wp-content/plugins/fluentformpro/src/Reports/

Upload File :
current_dir [ Writeable ] document_root [ Writeable ]

 

Command :


[ Back ]     

Current File : /bitnami/wordpress/wp-content/plugins/fluentformpro/src/Reports/McpReportTools.php
<?php

namespace FluentFormPro\Reports;

defined('ABSPATH') || exit;

use FluentForm\App\Modules\Acl\Acl;
use FluentForm\App\Modules\MCP\Support\ErrorCodes;
use FluentForm\App\Modules\MCP\Support\FormAccess;
use FluentForm\App\Modules\MCP\Support\MCPHelper;
use FluentForm\App\Modules\MCP\Support\PermissionGate;
use FluentFormPro\Reports\ReportHelperPro;

/**
 * Pro Advanced Reporting MCP tools, injected via the fluentform/mcp_tool_definitions filter.
 *
 * ReportHelperPro scopes only by an optional $formId (0 = all forms, no filter), so
 * the all-forms path is re-gated here to the admin ReportPolicy contract: a
 * specific-forms manager is refused the cross-form aggregate rather than leaking it.
 */
class McpReportTools
{
    const CAP_PAYMENTS = 'fluentform_view_payments';

    public static function defineTools($defs)
    {
        if (!is_array($defs)) {
            $defs = [];
        }

        return array_merge($defs, self::definitions());
    }

    public static function definitions()
    {
        return [
            'fluentform/get-revenue-analysis' => [
                'label'       => __('Get Revenue Analysis (Advanced/Pro)', 'fluentformpro'),
                'group'       => __('Reports', 'fluentformpro'),
                'description' => __('Advanced (Pro) NET revenue analytics — the same figures as the admin Reports → Revenue page. NET means paid minus refunded (contrast the free get-payment-summary tool, which reports GROSS per-status buckets). Breaks revenue down by form, payment method, or payment type (one-time vs subscription), each row carrying paid/pending/refunded/net amounts in whole currency units. At most the top 100 groups by net are returned (see data.truncated / data.total); net is also reported per currency under data.by_currency. Scope to one form with form_id, or omit it to cover every form you can access.', 'fluentformpro'),
                'input_schema' => [
                    'type'       => 'object',
                    'properties' => [
                        'form_id'   => ['type' => 'integer', 'description' => 'Optional. Omit to aggregate across all forms in your access scope.'],
                        'group_by'  => ['type' => 'string', 'enum' => ['forms', 'payment_method', 'payment_type'], 'description' => 'How to break down revenue. Defaults to "forms".'],
                        'date_from' => ['type' => 'string', 'description' => 'YYYY-MM-DD (site timezone). Defaults to 30 days ago. The window may span at most 366 days.'],
                        'date_to'   => ['type' => 'string', 'description' => 'YYYY-MM-DD (site timezone). Defaults to today.'],
                    ],
                ],
                'execute_callback' => [self::class, 'getRevenueAnalysis'],
                'capability'       => self::CAP_PAYMENTS,
                'annotations'      => ['readonly' => true],
                'pro'              => true,
            ],

            'fluentform/get-completion-rate' => [
                'label'       => __('Get Completion Rate (Advanced/Pro)', 'fluentformpro'),
                'group'       => __('Reports', 'fluentformpro'),
                'description' => __('Advanced (Pro) form completion / funnel figures — the same numbers as the admin Reports → Completion Rate page. Returns completed submissions, incomplete (draft/abandoned) attempts, total attempts, and the completion_rate percentage (completed ÷ total attempts). Scope to one form with form_id, or omit it to cover every form you can access.', 'fluentformpro'),
                'input_schema' => [
                    'type'       => 'object',
                    'properties' => [
                        'form_id'   => ['type' => 'integer', 'description' => 'Optional. Omit to aggregate across all forms in your access scope.'],
                        'date_from' => ['type' => 'string', 'description' => 'YYYY-MM-DD (site timezone). Defaults to 30 days ago. The window may span at most 366 days.'],
                        'date_to'   => ['type' => 'string', 'description' => 'YYYY-MM-DD (site timezone). Defaults to today.'],
                    ],
                ],
                'execute_callback'    => [self::class, 'getCompletionRate'],
                // Both caps required (AND), matching ReportPolicy::canAccessOptionalFormScopedReport;
                // the registrar's `capability` array is any-of (OR), which would be too permissive.
                'permission_callback' => [self::class, 'canAccessCompletionRate'],
                'annotations'         => ['readonly' => true],
                'pro'                 => true,
            ],

            'fluentform/get-subscription-report' => [
                'label'       => __('Get Subscription Report (Advanced/Pro)', 'fluentformpro'),
                'group'       => __('Reports', 'fluentformpro'),
                'description' => __('Advanced (Pro) recurring-revenue report — the same figures as the admin Reports → Subscriptions page. Returns total recurring amount, active subscription count, period-over-period growth percentage, and the top recurring plans by amount. Amounts are in whole currency units. Scope to one form with form_id, or omit it to cover every form you can access.', 'fluentformpro'),
                'input_schema' => [
                    'type'       => 'object',
                    'properties' => [
                        'form_id'   => ['type' => 'integer', 'description' => 'Optional. Omit to aggregate across all forms in your access scope.'],
                        'date_from' => ['type' => 'string', 'description' => 'YYYY-MM-DD (site timezone). Defaults to 30 days ago. The window may span at most 366 days.'],
                        'date_to'   => ['type' => 'string', 'description' => 'YYYY-MM-DD (site timezone). Defaults to today.'],
                    ],
                ],
                'execute_callback' => [self::class, 'getSubscriptionReport'],
                'capability'       => self::CAP_PAYMENTS,
                'annotations'      => ['readonly' => true],
                'pro'              => true,
            ],
        ];
    }

    public static function getRevenueAnalysis($params = [])
    {
        $formId = self::resolveScopedFormId($params);
        if (is_wp_error($formId)) {
            return $formId;
        }

        $disabled = self::paymentModuleGuard();
        if (is_wp_error($disabled)) {
            return $disabled;
        }

        $window = self::window($params);
        if (is_wp_error($window)) {
            return $window;
        }
        list($from, $to, $start, $end) = $window;

        $groupBy = isset($params['group_by']) ? sanitize_text_field($params['group_by']) : 'forms';
        if (!in_array($groupBy, ['forms', 'payment_method', 'payment_type'], true)) {
            return MCPHelper::error(ErrorCodes::INVALID_PARAM, __('group_by must be one of: forms, payment_method, payment_type.', 'fluentformpro'), ['fields' => ['group_by']]);
        }

        switch ($groupBy) {
            case 'payment_method':
                $result = ReportHelperPro::getNetRevenueByPaymentMethod($start, $end, $formId, self::MAX_ROWS, 1);
                break;
            case 'payment_type':
                $result = ReportHelperPro::getNetRevenueByPaymentType($start, $end, $formId, self::MAX_ROWS, 1);
                break;
            default:
                $result = ReportHelperPro::getNetRevenueByForms($start, $end, $formId, self::MAX_ROWS, 1);
        }

        $totals = isset($result['totals']) ? $result['totals'] : [];
        $net    = isset($totals['net']) ? (float) $totals['net'] : 0;

        // Grouped rows/totals sum payment_total across currencies. Report NET per
        // currency so a multi-currency window is never a single unlabelled figure.
        $byCurrency    = ReportHelperPro::getNetRevenueByCurrency($start, $end, $formId);
        $multiCurrency = count($byCurrency) > 1;

        $rows      = isset($result['data']) ? $result['data'] : [];
        $total     = isset($result['total']) ? (int) $result['total'] : 0;
        $truncated = $total > self::MAX_ROWS;

        $data = [
            'form_id'        => $formId ? $formId : null,
            'from'           => $from,
            'to'             => $to,
            'group_by'       => $groupBy,
            'basis'          => 'net',
            'rows'           => $rows,
            'totals'         => $totals,
            'total'          => $total,
            'returned'       => count($rows),
            'truncated'      => $truncated,
            'by_currency'    => $byCurrency,
            'multi_currency' => $multiCurrency,
        ];

        // Rows are capped at MAX_ROWS with no paging input; say so rather than let the
        // agent read a partial breakdown as complete.
        $notes = [];
        if ($multiCurrency) {
            $notes[] = __('The window spans multiple currencies; per-currency NET is under "by_currency". The grouped rows and totals are cross-currency sums and must not be read as a single amount.', 'fluentformpro');
        }
        if ($truncated) {
            $notes[] = sprintf(
                /* translators: 1: max rows returned, 2: total groups matched */
                __('Only the top %1$d groups by net revenue are returned (%2$d matched). Narrow the form scope or date range to see the rest.', 'fluentformpro'),
                self::MAX_ROWS,
                $total
            );
        }
        if ($notes) {
            $data['note'] = implode(' ', $notes);
        }

        if ($multiCurrency) {
            $parts = [];
            foreach ($byCurrency as $c) {
                $parts[] = number_format($c['net'], 2) . ' ' . $c['currency'];
            }

            return MCPHelper::envelope(
                sprintf(
                    /* translators: 1: per-currency net list, 2: group_by dimension, 3: start date, 4: end date */
                    __('Net revenue by currency, grouped by %2$s, between %3$s and %4$s: %1$s.', 'fluentformpro'),
                    implode('; ', $parts),
                    $groupBy,
                    $from,
                    $to
                ),
                $data
            );
        }

        return MCPHelper::envelope(
            sprintf(
                /* translators: 1: net revenue, 2: group_by dimension, 3: start date, 4: end date */
                __('Net revenue %1$s (paid − refunded), grouped by %2$s, between %3$s and %4$s.', 'fluentformpro'),
                number_format($net, 2),
                $groupBy,
                $from,
                $to
            ),
            $data
        );
    }

    public static function getCompletionRate($params = [])
    {
        $formId = self::resolveScopedFormId($params);
        if (is_wp_error($formId)) {
            return $formId;
        }

        $window = self::window($params);
        if (is_wp_error($window)) {
            return $window;
        }
        list($from, $to, $start, $end) = $window;

        $result = ReportHelperPro::getCompletionRateData($start, $end, $formId);

        $rate = isset($result['completion_rate']) ? (float) $result['completion_rate'] : 0;

        return MCPHelper::envelope(
            sprintf(
                /* translators: 1: completion rate percent, 2: completed count, 3: total attempts, 4: start date, 5: end date */
                __('%1$s%% completion rate: %2$d of %3$d attempts completed between %4$s and %5$s.', 'fluentformpro'),
                number_format($rate, 1),
                isset($result['total_submissions']) ? (int) $result['total_submissions'] : 0,
                isset($result['total_attempts']) ? (int) $result['total_attempts'] : 0,
                $from,
                $to
            ),
            [
                'form_id'                => $formId ? $formId : null,
                'from'                   => $from,
                'to'                     => $to,
                'completion_rate_pct'    => $rate,
                'completed_submissions'  => isset($result['total_submissions']) ? (int) $result['total_submissions'] : 0,
                'incomplete_submissions' => isset($result['incomplete_submissions']) ? (int) $result['incomplete_submissions'] : 0,
                'total_attempts'         => isset($result['total_attempts']) ? (int) $result['total_attempts'] : 0,
            ]
        );
    }

    public static function getSubscriptionReport($params = [])
    {
        $formId = self::resolveScopedFormId($params);
        if (is_wp_error($formId)) {
            return $formId;
        }

        $disabled = self::paymentModuleGuard();
        if (is_wp_error($disabled)) {
            return $disabled;
        }

        $window = self::window($params);
        if (is_wp_error($window)) {
            return $window;
        }
        list($from, $to, $start, $end) = $window;

        $result     = ReportHelperPro::getSubscriptions($start, $end, $formId);
        $byCurrency = (is_array($result) && isset($result['by_currency'])) ? $result['by_currency'] : [];

        // Per currency, never blended across currencies (a cross-currency SUM is
        // meaningless); highest recurring first. Recurring totals + growth only — a
        // plan ranking is not surfaced to the agent (it would either blend currencies
        // or need a per-currency query loop), so no misleading top-plans amount ships.
        $currencies = array_values($byCurrency);
        usort($currencies, function ($a, $b) {
            return $b['total_recurring'] <=> $a['total_recurring'];
        });

        $data = [
            'form_id'        => $formId ? $formId : null,
            'from'           => $from,
            'to'             => $to,
            'multi_currency' => count($currencies) > 1,
            'currencies'     => $currencies,
        ];

        if (empty($currencies)) {
            $data['total_recurring']    = 0;
            $data['subscription_count'] = 0;
            $data['currency_symbol']    = '';

            return MCPHelper::envelope(
                sprintf(
                    /* translators: 1: start date, 2: end date */
                    __('No active subscriptions between %1$s and %2$s.', 'fluentformpro'),
                    $from,
                    $to
                ),
                $data
            );
        }

        // Single currency (the common case): mirror it at the top level too.
        if (1 === count($currencies)) {
            $only                       = $currencies[0];
            $data['currency']           = $only['currency'];
            $data['currency_symbol']    = $only['currency_symbol'];
            $data['total_recurring']    = $only['total_recurring'];
            $data['subscription_count'] = $only['subscription_count'];
            $data['growth_percentage']  = $only['growth_percentage'];

            return MCPHelper::envelope(
                sprintf(
                    /* translators: 1: subscription count, 2: currency symbol, 3: recurring amount, 4: start date, 5: end date */
                    __('%1$d subscriptions totalling %2$s%3$s recurring between %4$s and %5$s.', 'fluentformpro'),
                    $only['subscription_count'],
                    $only['currency_symbol'],
                    number_format($only['total_recurring'], 2),
                    $from,
                    $to
                ),
                $data
            );
        }

        $data['note'] = __('Amounts span multiple currencies and are reported separately under "currencies"; they are not summed.', 'fluentformpro');

        $parts = [];
        foreach ($currencies as $c) {
            $parts[] = $c['currency_symbol'] . number_format($c['total_recurring'], 2) . ' ' . $c['currency'];
        }

        return MCPHelper::envelope(
            sprintf(
                /* translators: 1: comma-separated per-currency recurring totals, 2: start date, 3: end date */
                __('Recurring by currency between %2$s and %3$s: %1$s.', 'fluentformpro'),
                implode('; ', $parts),
                $from,
                $to
            ),
            $data
        );
    }

    const MAX_ROWS = 100;

    const MAX_SPAN_DAYS = 366;

    /** @return int|\WP_Error */
    private static function resolveScopedFormId($params)
    {
        if (!empty($params['form_id'])) {
            $form = FormAccess::resolveForm($params);
            if (is_wp_error($form)) {
                return $form;
            }

            return (int) $form->id;
        }

        $scope = PermissionGate::formScope();
        if (false !== $scope) {
            return MCPHelper::error(
                ErrorCodes::FORBIDDEN,
                __('You may only view advanced reports for a specific form. Pass a form_id you have access to.', 'fluentformpro')
            );
        }

        return 0;
    }

    public static function canAccessCompletionRate()
    {
        return Acl::hasPermission('fluentform_dashboard_access')
            && Acl::hasPermission('fluentform_entries_viewer');
    }

    /** @return true|\WP_Error */
    private static function paymentModuleGuard()
    {
        $settings = get_option('__fluentform_payment_module_settings');
        // Disabled is persisted as the string 'no' (empty('no') is false), so match the
        // canonical enabled value the admin ReportHelper checks: status === 'yes'.
        if (!$settings || 'yes' !== ($settings['status'] ?? '')) {
            return MCPHelper::error(ErrorCodes::FEATURE_DISABLED, __('The payment module is disabled, so there is no revenue or subscription data to report.', 'fluentformpro'));
        }

        return true;
    }

    /**
     * Returns [fromYmd, toYmd, startDateTime, endDateTime]. Full-day bounds
     * (00:00:00 / 23:59:59) match the admin Reports filters so figures line up.
     *
     * @return array|\WP_Error
     */
    private static function window($params)
    {
        $to = !empty($params['date_to']) ? sanitize_text_field($params['date_to']) : gmdate('Y-m-d', current_time('timestamp'));
        if (!MCPHelper::isYmd($to)) {
            return MCPHelper::error(ErrorCodes::INVALID_PARAM, __('date_to must be a valid date in YYYY-MM-DD format.', 'fluentformpro'), ['fields' => ['date_to']]);
        }

        $from = !empty($params['date_from']) ? sanitize_text_field($params['date_from']) : gmdate('Y-m-d', strtotime('-30 days', strtotime($to)));
        if (!MCPHelper::isYmd($from)) {
            return MCPHelper::error(ErrorCodes::INVALID_PARAM, __('date_from must be a valid date in YYYY-MM-DD format.', 'fluentformpro'), ['fields' => ['date_from']]);
        }

        if ($from > $to) {
            return MCPHelper::error(ErrorCodes::INVALID_PARAM, __('date_from must be on or before date_to.', 'fluentformpro'), ['fields' => ['date_from', 'date_to']]);
        }

        // Bound the window: subscription/revenue queries materialize rows, so an
        // unbounded span is unbounded work/memory. Agents chunk longer ranges.
        $spanDays = (int) (new \DateTime($from))->diff(new \DateTime($to))->days;
        if ($spanDays > self::MAX_SPAN_DAYS) {
            return MCPHelper::error(
                ErrorCodes::INVALID_PARAM,
                sprintf(
                    /* translators: %d: maximum number of days */
                    __('The date range is too large; it may span at most %d days. Narrow date_from/date_to.', 'fluentformpro'),
                    self::MAX_SPAN_DAYS
                ),
                ['fields' => ['date_from', 'date_to']]
            );
        }

        return [$from, $to, $from . ' 00:00:00', $to . ' 23:59:59'];
    }
}

Youez - 2016 - github.com/yon3zu
LinuXploit