Below is a differential between the unpatched vulnerable code and the patched update, for reference.
--- a/kivicare-clinic-management-system/app/abstracts/KCAbstractPaymentGateway.php
+++ b/kivicare-clinic-management-system/app/abstracts/KCAbstractPaymentGateway.php
@@ -140,6 +140,30 @@
public function get_settings(){
return $this->settings;
}
+
+ /**
+ * Public/mobile-safe gateway configuration.
+ *
+ * Gateway implementations may override this to expose SDK-safe values such
+ * as public keys, environment, and client-side API URLs. Secret credentials
+ * must never be returned from this method.
+ *
+ * @return array
+ */
+ public function get_public_config(): array
+ {
+ return ['paymentMethod' => $this->get_public_payment_method_id()];
+ }
+
+ /**
+ * Client-facing payment method identifier.
+ *
+ * @return string
+ */
+ protected function get_public_payment_method_id(): string
+ {
+ return (string) $this->gateway_id;
+ }
/**
* Log gateway activity
--- a/kivicare-clinic-management-system/app/admin/KCDashboardPermalinkHandler.php
+++ b/kivicare-clinic-management-system/app/admin/KCDashboardPermalinkHandler.php
@@ -109,6 +109,10 @@
return $redirect;
}
+ if ($role === 'administrator') {
+ return $redirect;
+ }
+
$login_redirects = KCOption::get('login_redirect', []);
if (!empty($login_redirects[$role])) {
@@ -525,7 +529,12 @@
*/
public function redirect_to_user_dashboard()
{
- $user_role = KCBase::get_instance()->KCGetRoles();
+ $user_role = KCBase::get_instance()->getLoginUserRole();
+ if ($user_role === 'administrator') {
+ wp_safe_redirect(admin_url());
+ exit;
+ }
+
$dashboard_url = $this->get_dashboard_url($user_role);
if ($dashboard_url) {
wp_safe_redirect($dashboard_url);
--- a/kivicare-clinic-management-system/app/baseClasses/KCApp.php
+++ b/kivicare-clinic-management-system/app/baseClasses/KCApp.php
@@ -144,6 +144,10 @@
return $redirect_to;
}
+ if ($role === 'administrator') {
+ return $redirect_to;
+ }
+
apply_filters('kc_login_redirect_role', $role, $user, KCDashboardPermalinkHandler::instance()->get_dashboard_url($role));
// Check if a custom redirect is set for this role
if (!empty($login_redirects[$role])) {
--- a/kivicare-clinic-management-system/app/baseClasses/KCPaymentGatewayFactory.php
+++ b/kivicare-clinic-management-system/app/baseClasses/KCPaymentGatewayFactory.php
@@ -153,6 +153,35 @@
}
/**
+ * Get enabled payment method configuration safe for frontend/mobile clients.
+ *
+ * @return array<int, array<string, mixed>>
+ */
+ public static function get_payment_methods_config(): array
+ {
+ $methods = [];
+
+ foreach (self::get_available_gateways(false) as $gateway_data) {
+ $gateway = $gateway_data['instance'] ?? null;
+
+ if (!$gateway instanceof KCAbstractPaymentGateway || !$gateway->is_enabled()) {
+ continue;
+ }
+
+ $gateway_methods = $gateway->get_public_config();
+
+ if (!isset($gateway_methods['paymentMethod'])) {
+ $methods = array_merge($methods, array_values($gateway_methods));
+ continue;
+ }
+
+ $methods[] = $gateway_methods;
+ }
+
+ return apply_filters('kc_payment_methods_config', $methods);
+ }
+
+ /**
* Get a gateway instance by ID.
*
* @param string $gateway_id Gateway identifier
@@ -187,4 +216,4 @@
return $gateway;
}
-}
No newline at end of file
+}
--- a/kivicare-clinic-management-system/app/controllers/api/AppointmentsController.php
+++ b/kivicare-clinic-management-system/app/controllers/api/AppointmentsController.php
@@ -20,6 +20,7 @@
use AppmodelsKCPaymentsAppointmentMapping;
use AppmodelsKCUserMeta;
use AppmodelsKCUser;
+use AppmodelsKCReceptionistClinicMapping;
use AppmodelsKCPatientMedicalReport;
use AppservicesKCAppointmentDataService;
use AppservicesKCAppointmentPaymentService;
@@ -1311,9 +1312,13 @@
return $this->checkResourceAccess('appointment', 'view');
}
+ if (strpos($route, '/print-report/') !== false) {
+ return $this->checkResourceAccess('appointment', 'view');
+ }
+
// For single appointment view
if (isset($request['id'])) {
- return $this->checkResourceAccess('appointment', 'view');
+ return $this->checkResourceAccess('appointment', 'view') && $this->canAccessAppointmentId((int) $request['id']);
}
// For appointment list - check appointment_list capability
@@ -1344,6 +1349,57 @@
}
}
+ private function canAccessAppointmentId(int $appointmentId): bool
+ {
+ if ($appointmentId <= 0) {
+ return false;
+ }
+
+ $appointment = KCAppointment::find($appointmentId);
+ if (!$appointment) {
+ return false;
+ }
+
+ $role = $this->kcbase->getLoginUserRole();
+ $currentUserId = get_current_user_id();
+
+ if ($role === 'administrator') {
+ return true;
+ }
+
+ if ($role === $this->kcbase->getPatientRole()) {
+ return (int) $appointment->patientId === (int) $currentUserId;
+ }
+
+ if ($role === $this->kcbase->getDoctorRole()) {
+ return (int) $appointment->doctorId === (int) $currentUserId;
+ }
+
+ if ($role === $this->kcbase->getReceptionistRole()) {
+ return in_array((int) $appointment->clinicId, $this->getReceptionistClinicIds($currentUserId), true);
+ }
+
+ if ($role === $this->kcbase->getClinicAdminRole()) {
+ $clinic = KCClinic::find((int) $appointment->clinicId);
+ return $clinic && (int) $clinic->clinicAdminId === (int) $currentUserId;
+ }
+
+ return false;
+ }
+
+ /**
+ * @return int[]
+ */
+ private function getReceptionistClinicIds(int $receptionistId): array
+ {
+ return KCReceptionistClinicMapping::query()
+ ->where('receptionistId', $receptionistId)
+ ->select(['clinic_id'])
+ ->get()
+ ->map(fn($row) => (int) $row->clinicId)
+ ->toArray();
+ }
+
/**
* Check if user has permission to create an appointment
*
--- a/kivicare-clinic-management-system/app/controllers/api/AuthController.php
+++ b/kivicare-clinic-management-system/app/controllers/api/AuthController.php
@@ -601,21 +601,22 @@
*/
public function checkRegistrationPermission($request) :bool|WP_Error
{
- KCErrorLogger::instance()->error("AuthController: checkRegistrationPermission called");
-
- // Allow registration if WordPress registration is enabled OR if KiviCare registration is specifically enabled
- if (get_option('users_can_register')) {
- KCErrorLogger::instance()->error("AuthController: WordPress registration enabled");
- return true;
- }
-
- // Determine user role
$user_role = $request->get_param('user_role') ?? $this->kcbase->getPatientRole();
- KCErrorLogger::instance()->error("Requested user role: " . $user_role);
- // Validate if the selected role is enabled in settings
+ return $this->validatePublicRegistrationRole($user_role);
+ }
+
+ private function validatePublicRegistrationRole(string $user_role): bool|WP_Error
+ {
$role_settings = KCOption::get('user_registration_shortcode_role_setting') ?? [];
- if (isset($role_settings[$user_role]) && $role_settings[$user_role] !== 'on') {
+ $role_enabled = (($role_settings[$user_role] ?? 'off') === 'on');
+
+ $staff_roles = [
+ $this->kcbase->getDoctorRole(),
+ $this->kcbase->getReceptionistRole(),
+ ];
+
+ if (in_array($user_role, $staff_roles, true) && !$role_enabled) {
return new WP_Error(
'role_not_allowed',
__('Selected user role is not enabled for registration', 'kivicare-clinic-management-system'),
@@ -623,32 +624,39 @@
);
}
- // Block receptionist registration if the receptionist module is disabled
- $receptionist_role = $this->kcbase->getReceptionistRole();
- if ($user_role === $receptionist_role || $user_role === 'receptionist') {
- $modules_data = kcGetModules();
- $module_config = $modules_data['module_config'] ?? [];
- $receptionist_on = false;
- foreach ($module_config as $mod) {
- if (($mod['name'] ?? '') === 'receptionist') {
- $receptionist_on = ($mod['status'] === '1' || $mod['status'] === 1 || $mod['status'] === true);
- break;
- }
- }
- if (!$receptionist_on) {
- return new WP_Error(
- 'role_not_allowed',
- __('Receptionist registration is currently disabled.', 'kivicare-clinic-management-system'),
- ['status' => 403]
- );
- }
+ if ($user_role === $this->kcbase->getPatientRole() && !get_option('users_can_register') && !$role_enabled) {
+ return new WP_Error(
+ 'role_not_allowed',
+ __('Selected user role is not enabled for registration', 'kivicare-clinic-management-system'),
+ ['status' => 400]
+ );
+ }
+
+ if ($user_role === $this->kcbase->getReceptionistRole() && !$this->isReceptionistModuleEnabled()) {
+ return new WP_Error(
+ 'role_not_allowed',
+ __('Receptionist registration is currently disabled.', 'kivicare-clinic-management-system'),
+ ['status' => 403]
+ );
}
- // Default to allowing registration for KiviCare (since this is a medical system)
- KCErrorLogger::instance()->error("AuthController: Allowing registration by default");
return true;
}
+ private function isReceptionistModuleEnabled(): bool
+ {
+ $modules_data = kcGetModules();
+ $module_config = $modules_data['module_config'] ?? [];
+
+ foreach ($module_config as $module) {
+ if (($module['name'] ?? '') === 'receptionist') {
+ return in_array($module['status'] ?? null, ['1', 1, true], true);
+ }
+ }
+
+ return false;
+ }
+
public function checkLogoutPermission($request)
{
return is_user_logged_in();
@@ -1277,14 +1285,14 @@
// Determine user role
$user_role = $params['user_role'] ?? $this->kcbase->getPatientRole();
- // Validate if the selected role is enabled in settings
- $role_settings = KCOption::get('user_registration_shortcode_role_setting') ?? [];
- if (isset($role_settings[$user_role]) && $role_settings[$user_role] !== 'on') {
+ $role_validation = $this->validatePublicRegistrationRole($user_role);
+ if (is_wp_error($role_validation)) {
+ $error_data = $role_validation->get_error_data();
return $this->response(
null,
- __('Selected user role is not enabled for registration', 'kivicare-clinic-management-system'),
+ $role_validation->get_error_message(),
false,
- 400
+ $error_data['status'] ?? 400
);
}
@@ -1977,4 +1985,4 @@
}
}
-}
No newline at end of file
+}
--- a/kivicare-clinic-management-system/app/controllers/api/BillController.php
+++ b/kivicare-clinic-management-system/app/controllers/api/BillController.php
@@ -15,6 +15,7 @@
use AppmodelsKCPaymentsAppointmentMapping;
use AppmodelsKCServiceDoctorMapping;
use AppmodelsKCAppointmentServiceMapping;
+use AppmodelsKCReceptionistClinicMapping;
use WP_REST_Request;
use WP_REST_Response;
use KCProAppcontrollersapiGoogleCalendarIntegration;
@@ -210,13 +211,95 @@
];
}
- public function checkViewPermission()
+ public function checkViewPermission($request = null)
{
if (!$this->isModuleEnabled('billing')) {
return false;
}
- // Check if user has permission to list
- return $this->checkResourceAccess('patient_bill', 'view');
+
+ if (!$this->checkResourceAccess('patient_bill', 'view')) {
+ return false;
+ }
+
+ if ($request instanceof WP_REST_Request) {
+ if ($request->get_param('id')) {
+ return $this->canAccessBillId((int) $request->get_param('id'));
+ }
+
+ if ($request->get_param('encounter_id')) {
+ return $this->canAccessEncounterId((int) $request->get_param('encounter_id'));
+ }
+ }
+
+ return true;
+ }
+
+ private function canAccessBillId(int $billId): bool
+ {
+ if ($billId <= 0) {
+ return false;
+ }
+
+ $bill = KCBill::find($billId);
+ return $bill && $this->canAccessEncounterId((int) $bill->encounterId);
+ }
+
+ private function canAccessEncounterId(int $encounterId): bool
+ {
+ if ($encounterId <= 0) {
+ return false;
+ }
+
+ $encounter = KCPatientEncounter::find($encounterId);
+ if (!$encounter) {
+ return false;
+ }
+
+ $role = $this->kcbase->getLoginUserRole();
+ $currentUserId = get_current_user_id();
+ $clinicId = (int) $encounter->clinicId;
+
+ return match ($role) {
+ 'administrator' => true,
+ $this->kcbase->getPatientRole() => (int) $encounter->patientId === (int) $currentUserId,
+ $this->kcbase->getDoctorRole() => (int) $encounter->doctorId === (int) $currentUserId,
+ $this->kcbase->getReceptionistRole() => $this->receptionistCanAccessClinic($currentUserId, $clinicId),
+ $this->kcbase->getClinicAdminRole() => $this->clinicAdminCanAccessClinic($currentUserId, $clinicId),
+ default => false,
+ };
+ }
+
+ private function receptionistCanAccessClinic(int $receptionistId, int $clinicId): bool
+ {
+ if ($receptionistId <= 0 || $clinicId <= 0) {
+ return false;
+ }
+
+ return in_array($clinicId, $this->getReceptionistClinicIds($receptionistId), true);
+ }
+
+ private function clinicAdminCanAccessClinic(int $clinicAdminId, int $clinicId): bool
+ {
+ if ($clinicAdminId <= 0 || $clinicId <= 0) {
+ return false;
+ }
+
+ $clinic = KCClinic::find($clinicId);
+
+ return $clinic && (int) $clinic->clinicAdminId === (int) $clinicAdminId;
+ }
+
+ /**
+ * @return int[]
+ */
+ private function getReceptionistClinicIds(int $receptionistId): array
+ {
+ return KCReceptionistClinicMapping::query()
+ ->where('receptionist_id', $receptionistId)
+ ->select(['clinic_id'])
+ ->get()
+ ->map(fn($row) => (int) $row->clinicId)
+ ->toArray();
}
public function checkPermission($request)
--- a/kivicare-clinic-management-system/app/controllers/api/ConfigController.php
+++ b/kivicare-clinic-management-system/app/controllers/api/ConfigController.php
@@ -2,9 +2,11 @@
namespace Appcontrollersapi;
+use AppbaseClassesKCActivate;
use AppbaseClassesKCBase;
use AppbaseClassesKCBaseController;
use AppbaseClassesKCPaymentGatewayFactory;
+use AppbaseClassesKCPermissions;
use AppmodelsKCClinic;
use AppmodelsKCOption;
use KCProAppcontrollersapiKCProPermissionSetting;
@@ -218,7 +220,7 @@
// Lazy generation: activation hook may have been missed on existing installs
if (!$public_key) {
- AppbaseClassesKCActivate::generate_server_key_pair();
+ KCActivate::generate_server_key_pair();
$public_key = get_option('kc_server_public_key');
}
@@ -492,7 +494,7 @@
/**
* Payment methods with detailed configuration.
*/
- $response['payment_methods'] = $this->getPaymentMethodsArray();
+ $response['payment_methods'] = KCPaymentGatewayFactory::get_payment_methods_config();
// Define the sub-module configurations to process
$response['encounter_modules'] = apply_filters('kcpro_get_encounter_list',[]);
@@ -506,7 +508,7 @@
if ($user_id && is_user_logged_in()) {
$userObj = new WP_User($user_id);
$final_capabilities = $userObj->allcaps;
- $user_role_key = AppbaseClassesKCPermissions::get_user_role($user_id);
+ $user_role_key = KCPermissions::get_user_role($user_id);
if (function_exists('isKiviCareProActive') && isKiviCareProActive()) {
$lookup_key = ($user_role_key === 'admin') ? 'administrator' : str_replace(KIVI_CARE_PREFIX, '', $user_role_key);
@@ -1039,145 +1041,6 @@
}
}
- /**
- * Get payment methods array with detailed configuration
- * Only returns enabled payment methods
- *
- * @return array
- */
- private function getPaymentMethodsArray(): array
- {
- $payment_methods = [];
- $gateways = AppbaseClassesKCPaymentGatewayFactory::get_available_gateways();
-
- // Map gateway IDs to user's expected format
- $gateway_id_mapping = [
- 'razorpay' => 'razorPay',
- 'stripe' => 'stripe',
- 'stripepay' => 'stripe',
- 'woocommerce' => 'wooCommerce',
- 'manual' => 'offline',
- 'paypal' => 'paypal'
- ];
-
- foreach ($gateways as $gateway_id => $gateway_data) {
- $instance = $gateway_data['instance'] ?? null;
-
- if (!$instance) {
- continue;
- }
-
- // Only include enabled payment methods
- $is_enabled = $instance->is_enabled() ?? false;
- if (!$is_enabled) {
- continue;
- }
-
- $mapped_id = $gateway_id_mapping[$gateway_id] ?? $gateway_id;
- // Use get_settings() method to properly retrieve settings
- $settings = method_exists($instance, 'get_settings') ? $instance->get_settings() : ($instance->settings ?? []);
-
- // Build payment method object
- $payment_method = [
- 'paymentMethod' => $mapped_id,
- ];
-
- // For offline/manual payment, only include paymentMethod
- if ($gateway_id === 'manual') {
- unset($payment_method['paymentURL']); // Remove URL for offline to match desired output
- $payment_methods[] = $payment_method;
- continue;
- }
- // Same for woocommerce? User JSON had "paymentMethod": "wooCommerce" and nothing else.
- if ($gateway_id === 'woocommerce') {
- // $checkout_page_id = get_option('woocommerce_checkout_page_id');
- // $payment_method['paymentURL'] = $checkout_page_id ? get_permalink($checkout_page_id) : site_url();
- $payment_methods[] = $payment_method;
- continue;
- }
-
- // For gateways that support environment/mode
- $environment = 'live';
- $secret_key = '';
- $public_key = '';
- $payment_url = '';
-
- switch ($gateway_id) {
- case 'razorpay':
- // Razorpay - get settings from instance first, then fallback to option
- $razorpay_settings = $settings;
-
- // If settings are empty or missing credentials, try getting directly from option
- if (empty($razorpay_settings) || empty($razorpay_settings['key_id']) || empty($razorpay_settings['key_secret'])) {
- $option_settings = get_option(KIVI_CARE_PREFIX . 'razorpay_setting', []);
- if (is_string($option_settings)) {
- $option_settings = json_decode($option_settings, true) ?? [];
- }
- // Merge option settings with instance settings (option takes precedence)
- if (!empty($option_settings)) {
- $razorpay_settings = array_merge($razorpay_settings, $option_settings);
- }
- }
-
- $mode = $razorpay_settings['mode'] ?? 'sandbox';
- $environment = ($mode === 'sandbox') ? 'test' : 'live';
- $secret_key = $razorpay_settings['key_secret'] ?? '';
- $public_key = $razorpay_settings['key_id'] ?? '';
- $payment_url = 'https://api.razorpay.com/v1';
- break;
-
- case 'paypal':
- // PayPal uses 'mode' with id 0 (sandbox) or 1 (live)
- $mode = $settings['mode'] ?? [];
- if (is_array($mode) && isset($mode['id'])) {
- $environment = ($mode['id'] == '0' || $mode['id'] === 0) ? 'test' : 'live';
- } else {
- $environment = ($mode == '0' || $mode === 0) ? 'test' : 'live';
- }
- $secret_key = $settings['client_secret'] ?? '';
- $public_key = $settings['client_id'] ?? '';
- $payment_url = ($environment === 'test')
- ? 'https://api.sandbox.paypal.com'
- : 'https://api.paypal.com';
- break;
-
- case 'stripe':
- case 'stripepay':
- // Stripe - get settings from instance or directly from options
- $stripe_settings = $settings;
-
- // Fallback to option if empty
- if (empty($stripe_settings) || (empty($stripe_settings['api_key']) && empty($stripe_settings['publishable_key']))) {
- $option_key = defined('KIVI_CARE_PREFIX') ? KIVI_CARE_PREFIX . 'stripepay_setting' : 'kiviCare_stripepay_setting';
- $option_settings = get_option($option_key, []);
- if (is_string($option_settings)) {
- $option_settings = json_decode($option_settings, true) ?? [];
- }
- if (!empty($option_settings)) {
- $stripe_settings = array_merge($stripe_settings, $option_settings);
- }
- }
-
- $mode = $stripe_settings['mode'] ?? 'sandbox';
- $environment = ($mode === 'sandbox' || $mode === 'test') ? 'test' : 'live';
- // Stripe addon uses 'api_key' for secret, 'publishable_key' for public
- $secret_key = $stripe_settings['api_key'] ?? $stripe_settings['secret_key'] ?? $stripe_settings['secretKey'] ?? '';
- $public_key = $stripe_settings['publishable_key'] ?? $stripe_settings['publishableKey'] ?? $stripe_settings['public_key'] ?? '';
- $payment_url = 'https://api.stripe.com/v1';
- break;
- }
-
- // Populate fields - always include structure if it's one of the processed gateways
- $payment_method['environment'] = $environment;
- $payment_method['secretKey'] = $secret_key;
- $payment_method['publicKey'] = $public_key;
- $payment_method['paymentURL'] = $payment_url;
-
- $payment_methods[] = $payment_method;
- }
-
- return $payment_methods;
- }
/**
* Get general configuration
--- a/kivicare-clinic-management-system/app/controllers/api/KCPrintInvoiceController.php
+++ b/kivicare-clinic-management-system/app/controllers/api/KCPrintInvoiceController.php
@@ -11,6 +11,7 @@
use AppmodelsKCPaymentsAppointmentMapping;
use AppmodelsKCPatientEncounter;
use AppmodelsKCBill;
+use AppmodelsKCReceptionistClinicMapping;
use WP_REST_Request;
use WP_REST_Response;
use ApputilsKCPdfGenerator;
@@ -37,6 +38,73 @@
]);
}
+ public function checkPermission($request)
+ {
+ if (!$this->checkCapability('read') || !$this->checkResourceAccess('appointment', 'view')) {
+ return false;
+ }
+
+ return $this->canAccessAppointmentId((int) $request->get_param('id'));
+ }
+
+ private function canAccessAppointmentId(int $appointmentId): bool
+ {
+ if ($appointmentId <= 0) {
+ return false;
+ }
+
+ $appointment = KCAppointment::find($appointmentId);
+ if (!$appointment) {
+ return false;
+ }
+
+ $role = $this->kcbase->getLoginUserRole();
+ $currentUserId = get_current_user_id();
+ $clinicId = (int) $appointment->clinicId;
+
+ return match ($role) {
+ 'administrator' => true,
+ $this->kcbase->getPatientRole() => (int) $appointment->patientId === (int) $currentUserId,
+ $this->kcbase->getDoctorRole() => (int) $appointment->doctorId === (int) $currentUserId,
+ $this->kcbase->getReceptionistRole() => $this->receptionistCanAccessClinic($currentUserId, $clinicId),
+ $this->kcbase->getClinicAdminRole() => $this->clinicAdminCanAccessClinic($currentUserId, $clinicId),
+ default => false,
+ };
+ }
+
+ private function receptionistCanAccessClinic(int $receptionistId, int $clinicId): bool
+ {
+ if ($receptionistId <= 0 || $clinicId <= 0) {
+ return false;
+ }
+
+ return in_array($clinicId, $this->getReceptionistClinicIds($receptionistId), true);
+ }
+
+ private function clinicAdminCanAccessClinic(int $clinicAdminId, int $clinicId): bool
+ {
+ if ($clinicAdminId <= 0 || $clinicId <= 0) {
+ return false;
+ }
+
+ $clinic = KCClinic::find($clinicId);
+
+ return $clinic && (int) $clinic->clinicAdminId === (int) $clinicAdminId;
+ }
+
+ /**
+ * @return int[]
+ */
+ private function getReceptionistClinicIds(int $receptionistId): array
+ {
+ return KCReceptionistClinicMapping::query()
+ ->where('receptionist_id', $receptionistId)
+ ->select(['clinic_id'])
+ ->get()
+ ->map(fn($row) => (int) $row->clinicId)
+ ->toArray();
+ }
+
public function print(WP_REST_Request $request)
{
try {
@@ -242,4 +310,4 @@
return KIVI_CARE_DIR . 'templates/KCInvoicePrintTemplate.php';
}
-}
No newline at end of file
+}
--- a/kivicare-clinic-management-system/app/controllers/api/SettingsController/ListingData.php
+++ b/kivicare-clinic-management-system/app/controllers/api/SettingsController/ListingData.php
@@ -264,11 +264,11 @@
$query->where(function ($q) use ($searchTerm, $like) {
if (is_numeric($searchTerm)) {
$q->where('id', $searchTerm)
- ->orWhereRaw("LOWER(type) LIKE '{$like}'")
- ->orWhereRaw("LOWER(value) LIKE '{$like}'");
+ ->orWhereRaw('LOWER(type) LIKE %s', [$like])
+ ->orWhereRaw('LOWER(value) LIKE %s', [$like]);
} else {
- $q->whereRaw("LOWER(type) LIKE '{$like}'")
- ->orWhereRaw("LOWER(value) LIKE '{$like}'");
+ $q->whereRaw('LOWER(type) LIKE %s', [$like])
+ ->orWhereRaw('LOWER(value) LIKE %s', [$like]);
}
});
}
@@ -453,11 +453,11 @@
$query->where(function ($q) use ($searchTerm, $like) {
if (is_numeric($searchTerm)) {
$q->where('id', $searchTerm)
- ->orWhereRaw("LOWER(type) LIKE '{$like}'")
- ->orWhereRaw("LOWER(value) LIKE '{$like}'");
+ ->orWhereRaw('LOWER(type) LIKE %s', [$like])
+ ->orWhereRaw('LOWER(value) LIKE %s', [$like]);
} else {
- $q->whereRaw("LOWER(type) LIKE '{$like}'")
- ->orWhereRaw("LOWER(value) LIKE '{$like}'");
+ $q->whereRaw('LOWER(type) LIKE %s', [$like])
+ ->orWhereRaw('LOWER(value) LIKE %s', [$like]);
}
});
}
--- a/kivicare-clinic-management-system/app/controllers/api/StaticDataController.php
+++ b/kivicare-clinic-management-system/app/controllers/api/StaticDataController.php
@@ -200,13 +200,30 @@
'description' => __('Whether the request is from the mobile app (allows inactive clinics in some cases)', 'kivicare-clinic-management-system'),
],
],
- 'permission_callback' => function (WP_REST_Request $request) {
- // For now, allowing all requests - can be customized later
- return true;
- },
+ 'permission_callback' => [$this, 'checkStaticDataPermission'],
]);
}
+ public function checkStaticDataPermission(WP_REST_Request $request)
+ {
+ $isPatientListRequest = $request->get_param('dataType') === 'patientList'
+ || ($request->get_param('dataType') === 'staticData' && $request->get_param('staticDataType') === 'patients');
+
+ if (!$isPatientListRequest) {
+ return true;
+ }
+
+ if (!is_user_logged_in() || !$this->checkCapability('read')) {
+ return false;
+ }
+
+ if ($this->kcbase->getLoginUserRole() === $this->kcbase->getPatientRole()) {
+ return false;
+ }
+
+ return $this->checkResourceAccess('patient', 'view');
+ }
+
public function getStaticData(WP_REST_Request $request): WP_REST_Response
{
try {
@@ -645,7 +662,7 @@
/**
* Get clinic list
*/
- private function getClinicList(?WP_REST_Request $request = null): array
+ private function getClinicList(WP_REST_Request $request): array
{
try {
$currentUserRole = $this->kcbase->getLoginUserRole();
@@ -710,8 +727,22 @@
$clinicsQuery->where('status', 1);
}
+ $clinicId = $request->get_param('clinic_id');
+ $clinicIds = $request->get_param('clinic_ids');
+
+ if (!empty($clinicId)) {
+ $clinicsQuery->where('id', absint($clinicId));
+ } elseif (!empty($clinicIds)) {
+ $clinicIdsArray = is_array($clinicIds) ? $clinicIds : explode(',', (string) $clinicIds);
+ $clinicIdsArray = array_filter(array_map('absint', $clinicIdsArray));
+
+ if (!empty($clinicIdsArray)) {
+ $clinicsQuery->whereIn('id', $clinicIdsArray);
+ }
+ }
+
// Handle search
- if ($request instanceof WP_REST_Request && !empty($request->get_param('search'))) {
+ if (!empty($request->get_param('search'))) {
$search = $request->get_param('search');
if ($search !== 'undefined' && $search !== 'null') {
$clinicsQuery->where('name', 'LIKE', '%' . $search . '%');
@@ -719,72 +750,68 @@
}
// Filter by service_id
- if ($request instanceof WP_REST_Request) {
- $serviceId = $request->get_param('service_id');
- if (!empty($serviceId)) {
- $serviceClinicIds = KCServiceDoctorMapping::query()
- ->where('service_id', absint($serviceId))
- ->where('status', 1)
- ->select(['clinic_id'])
- ->groupBy('clinic_id')
- ->get()
- ->map(fn($row) => $row->clinicId)
- ->toArray();
+ $serviceId = $request->get_param('service_id');
+ if (!empty($serviceId)) {
+ $serviceClinicIds = KCServiceDoctorMapping::query()
+ ->where('service_id', absint($serviceId))
+ ->where('status', 1)
+ ->select(['clinic_id'])
+ ->groupBy('clinic_id')
+ ->get()
+ ->map(fn($row) => $row->clinicId)
+ ->toArray();
- $serviceClinicIds = array_filter($serviceClinicIds);
+ $serviceClinicIds = array_filter($serviceClinicIds);
- if (!empty($serviceClinicIds)) {
- $clinicsQuery->whereIn('id', $serviceClinicIds);
- } else {
- // Service not found in any clinic
- $clinicsQuery->whereRaw('0=1');
- }
+ if (!empty($serviceClinicIds)) {
+ $clinicsQuery->whereIn('id', $serviceClinicIds);
+ } else {
+ // Service not found in any clinic
+ $clinicsQuery->whereRaw('0=1');
}
}
// Filter by doctor_id and exclude clinics with existing sessions
- if ($request instanceof WP_REST_Request) {
- $doctorId = $request->get_param('doctor_id');
- $excludeWithSessions = $request->get_param('exclude_with_sessions');
-
- if (!empty($doctorId)) {
-
- // First, filter to only clinics where this doctor works
- $doctorClinicIds = KCDoctorClinicMapping::query()
- ->where('doctor_id', absint($doctorId))
- ->select(['clinic_id'])
- ->get()
- ->map(fn($row) => $row->clinicId)
- ->toArray();
-
- $doctorClinicIds = array_filter($doctorClinicIds);
-
+ $doctorId = $request->get_param('doctor_id');
+ $excludeWithSessions = $request->get_param('exclude_with_sessions');
- if (!empty($doctorClinicIds)) {
- $clinicsQuery->whereIn('id', $doctorClinicIds);
+ if (!empty($doctorId)) {
- // If exclude_with_sessions is true, exclude clinics where doctor has sessions
- if ($excludeWithSessions === true) {
- // Get clinic IDs where this doctor already has sessions
- $sessionClinicIds = AppmodelsKCClinicSession::query()
- ->where('doctorId', absint($doctorId))
- ->select(['clinic_id'])
- ->groupBy('clinic_id')
- ->get()
- ->map(fn($row) => $row->clinicId)
- ->toArray();
-
- $sessionClinicIds = array_filter($sessionClinicIds);
-
- if (!empty($sessionClinicIds)) {
- // Exclude clinics where doctor already has sessions
- $clinicsQuery->whereNotIn('id', $sessionClinicIds);
- }
+ // First, filter to only clinics where this doctor works
+ $doctorClinicIds = KCDoctorClinicMapping::query()
+ ->where('doctor_id', absint($doctorId))
+ ->select(['clinic_id'])
+ ->get()
+ ->map(fn($row) => $row->clinicId)
+ ->toArray();
+
+ $doctorClinicIds = array_filter($doctorClinicIds);
+
+
+ if (!empty($doctorClinicIds)) {
+ $clinicsQuery->whereIn('id', $doctorClinicIds);
+
+ // If exclude_with_sessions is true, exclude clinics where doctor has sessions
+ if ($excludeWithSessions === true) {
+ // Get clinic IDs where this doctor already has sessions
+ $sessionClinicIds = AppmodelsKCClinicSession::query()
+ ->where('doctorId', absint($doctorId))
+ ->select(['clinic_id'])
+ ->groupBy('clinic_id')
+ ->get()
+ ->map(fn($row) => $row->clinicId)
+ ->toArray();
+
+ $sessionClinicIds = array_filter($sessionClinicIds);
+
+ if (!empty($sessionClinicIds)) {
+ // Exclude clinics where doctor already has sessions
+ $clinicsQuery->whereNotIn('id', $sessionClinicIds);
}
- } else {
- // Doctor not associated with any clinic
- $clinicsQuery->whereRaw('0=1');
}
+ } else {
+ // Doctor not associated with any clinic
+ $clinicsQuery->whereRaw('0=1');
}
}
@@ -792,13 +819,11 @@
$totalCount = $clinicsQuery->count();
// Handle pagination
- if ($request instanceof WP_REST_Request) {
- $page = $request->get_param('page') ?: 1;
- $perPage = $request->get_param('per_page') ?: 10;
- $offset = ($page - 1) * $perPage;
- if ($page !== -1) {
- $clinicsQuery->limit($perPage)->offset($offset);
- }
+ $page = $request->get_param('page') ?: 1;
+ $perPage = $request->get_param('per_page') ?: 10;
+ $offset = ($page - 1) * $perPage;
+ if ($page !== -1) {
+ $clinicsQuery->limit($perPage)->offset($offset);
}
$clinics = $clinicsQuery->get();
@@ -842,30 +867,20 @@
];
})->toArray();
- // Add pagination metadata if request provided
- if ($request instanceof WP_REST_Request) {
- $page = $request->get_param('page') ?: 1;
- $perPage = $request->get_param('per_page') ?: 10;
- $totalPages = ceil($totalCount / $perPage);
-
- return [
- 'data' => $result,
- 'pagination' => [
- 'total' => $totalCount,
- 'per_page' => $perPage,
- 'current_page' => $page,
- 'total_pages' => $totalPages,
- 'has_more' => $page < $totalPages,
- ]
- ];
- }
+ $totalPages = ceil($totalCount / $perPage);
- return $result;
+ return [
+ 'data' => $result,
+ 'pagination' => [
+ 'total' => $totalCount,
+ 'per_page' => $perPage,
+ 'current_page' => $page,
+ 'total_pages' => $totalPages,
+ 'has_more' => $page < $totalPages,
+ ]
+ ];
} catch (Exception $e) {
- if ($request instanceof WP_REST_Request) {
- return ['data' => [], 'pagination' => ['total' => 0, 'per_page' => 10, 'current_page' => 1, 'total_pages' => 0, 'has_more' => false]];
- }
- return [];
+ return ['data' => [], 'pagination' => ['total' => 0, 'per_page' => 10, 'current_page' => 1, 'total_pages' => 0, 'has_more' => false]];
}
}
--- a/kivicare-clinic-management-system/app/models/KCBill.php
+++ b/kivicare-clinic-management-system/app/models/KCBill.php
@@ -176,11 +176,13 @@
$prefix = $currency_format['prefix'] ?? '';
$postfix = $currency_format['postfix'] ?? '';
+ $total_revenue = is_object($total) ? ($total->total_revenue ?? 0) : (is_numeric($total) ? $total : 0);
+
// Format the total with number_format for proper thousand separators
- $formatted_total = $prefix . number_format($total->total_revenue) . $postfix;
+ $formatted_total = $prefix . number_format((float) $total_revenue) . $postfix;
return [
- 'count' => $total->total_revenue ?? 0,
+ 'count' => $total_revenue,
'formatted_count' => $formatted_total
];
}
-}
No newline at end of file
+}
--- a/kivicare-clinic-management-system/app/paymentGateways/KCKnitPay.php
+++ b/kivicare-clinic-management-system/app/paymentGateways/KCKnitPay.php
@@ -145,6 +145,31 @@
}
/**
+ * Public/mobile-safe Knit Pay payment methods.
+ *
+ * Knit Pay can expose multiple configured payment methods, so this returns
+ * a list of payment method configs instead of a single gateway config.
+ *
+ * @return array<int, array<string, string>>
+ */
+ public function get_public_config(): array
+ {
+ $methods = [];
+
+ foreach ($this->get_enabled_configs() as $config) {
+ if (empty($config['id'])) {
+ continue;
+ }
+
+ $methods[] = [
+ 'paymentMethod' => 'knit_pay_' . $config['id'],
+ ];
+ }
+
+ return $methods;
+ }
+
+ /**
* Filter Redirect URL
*
* Intercepts the Knit Pay redirect URL to ensure it hits the KiviCare REST API endpoint.
@@ -459,4 +484,4 @@
{
return $this->create_payment_response('success', 'Webhooks for Knit Pay are handled by the Pronamic plugin.', []);
}
-}
No newline at end of file
+}
--- a/kivicare-clinic-management-system/app/paymentGateways/KCPayLater.php
+++ b/kivicare-clinic-management-system/app/paymentGateways/KCPayLater.php
@@ -79,7 +79,7 @@
'Appointment booked successfully. Payment due later (manual).'
);
- } catch (Exception $e) {
+ } catch (Exception $e) {
$this->log("Pay Later processing error: " . $e->getMessage(), 'error');
return $this->create_payment_response(
'failed',
@@ -128,9 +128,19 @@
*/
public function get_settings() {
$value = $this->settings ?? 'off';
- $this->settings = [];
- $this->settings['enablePayLater'] = ($value === 'on');
- return $this->settings;
+ return [
+ 'enablePayLater' => ($value === 'on'),
+ ];
+ }
+
+ /**
+ * Public/mobile-safe payment config.
+ */
+ public function get_public_config(): array
+ {
+ return [
+ 'paymentMethod' => 'offline',
+ ];
}
/**
@@ -149,4 +159,4 @@
return $this->get_settings();
}
-}
No newline at end of file
+}
--- a/kivicare-clinic-management-system/app/paymentGateways/KCPaypal.php
+++ b/kivicare-clinic-management-system/app/paymentGateways/KCPaypal.php
@@ -666,6 +666,24 @@
return $settings;
}
+ /**
+ * Public/mobile-safe PayPal configuration.
+ */
+ public function get_public_config(): array
+ {
+ $settings = $this->get_settings();
+ $environment = ((string) ($settings['mode'] ?? '0') === '0') ? 'test' : 'live';
+
+ return [
+ 'paymentMethod' => 'paypal',
+ 'environment' => $environment,
+ 'publicKey' => $settings['client_id'] ?? '',
+ 'paymentURL' => $environment === 'test'
+ ? 'https://api.sandbox.paypal.com'
+ : 'https://api.paypal.com',
+ ];
+ }
+
public function update_settings($settings)
{
$processed_settings = $this->settings;
@@ -759,4 +777,4 @@
return $this->get_settings();
}
-}
No newline at end of file
+}
--- a/kivicare-clinic-management-system/app/paymentGateways/KCWooCommerce.php
+++ b/kivicare-clinic-management-system/app/paymentGateways/KCWooCommerce.php
@@ -668,6 +668,16 @@
return $this->settings;
}
+ /**
+ * Public/mobile-safe WooCommerce configuration.
+ */
+ public function get_public_config(): array
+ {
+ return [
+ 'paymentMethod' => 'wooCommerce',
+ ];
+ }
+
public function update_settings($settings)
{
$value = $settings['enableWooCommerce'] ?? 'off';
--- a/kivicare-clinic-management-system/kivicare-clinic-management-system.php
+++ b/kivicare-clinic-management-system/kivicare-clinic-management-system.php
@@ -3,7 +3,7 @@
* Plugin Name: KiviCare - Clinic & Patient Management System (EHR)
* Plugin URI: https://kivicare.io
* Description: KiviCare is an impressive clinic and patient management plugin (EHR). It comes with powerful shortcodes for appointment booking and patient registration.
- * Version: 4.5.1
+ * Version: 4.5.2
* Author: iqonic design
* Text Domain: kivicare-clinic-management-system
* Domain Path: /languages
@@ -46,7 +46,7 @@
}
if (!defined('KIVI_CARE_VERSION')) {
- define('KIVI_CARE_VERSION', "4.5.1");
+ define('KIVI_CARE_VERSION', "4.5.2");
}
if (!defined('KIVI_CARE_API_VERSION')) {