From 77037e7e84367688d82a41b0d550a311680d03df Mon Sep 17 00:00:00 2001 From: emmymayo Date: Tue, 4 Feb 2025 23:06:08 +0100 Subject: [PATCH] init --- .DS_Store | Bin 0 -> 6148 bytes .gitignore | 6 + README.md | 97 + accessListing.php | 96 + accesslog-model.php | 39 + cal.php | 93 + calendar-model.php | 49 + calendar-routes.php | 6 + calendar.php | 271 + calendar_functions.php | 545 + campaign-model.php | 132 + campaignAdd.php | 260 + campaignEdit.php | 254 + campaignListing.php | 166 + campaignView copy.php | 1040 ++ campaignView.php | 1276 ++ client-login.php | 50 + client-profile.php | 93 + client-routes.php | 1021 ++ core.php | 124 + cronjob.php | 605 + db.sql | 137 + get_all_v1_api_images.php | 67 + get_v1_api_image_get.php | 38 + index.php | 1930 +++ layout/.DS_Store | Bin 0 -> 6148 bytes layout/footer/Adminnone_footer.php | 8 + layout/footer/Clientnone_footer.php | 8 + layout/header/Adminleft_sidebar.php | 170 + layout/header/Clientleft_sidebar.php | 170 + lib/ghl/calendar.php | 84 + lib/ghl/oauth2.php | 138 + lib/google/drive.php | 185 + lib/google/oauth2.php | 207 + lib/redbean/rb-mysql.php | 17094 +++++++++++++++++++++++++ license-model.php | 47 + licenseAdd.php | 19 + licenseEdit.php | 34 + licenseListing.php | 101 + location-model.php | 49 + locationAdd.php | 27 + locationEdit.php | 29 + locationListing.php | 103 + login.php | 50 + migration.sql | 233 + mysql-adapter.php | 48 + mysql-database-service.php | 658 + mysql.php | 872 ++ oauth-routes.php | 80 + post_v1_api_image_add.php | 52 + privacy-policy.php | 103 + project-model.php | 61 + project.js | 86 + projectAdd.php | 514 + projectEdit.php | 555 + projectEditMulti.php | 558 + projectListing.php | 354 + put_v1_api_image_edit.php | 66 + query-service.php | 154 + report-model.php | 152 + reportListing.php | 163 + report_cronjob.php | 436 + sample.tsv | 118 + schema.sql | 170 + style.css | 81 + team_followup_2023-10-16.sql | 87 + terms.php | 273 + test.html | 409 + tt.php | 86 + user-model.php | 43 + userAdd.php | 31 + userEdit.php | 39 + userListing.php | 83 + validation-service.php | 90 + 74 files changed, 33573 insertions(+) create mode 100644 .DS_Store create mode 100644 .gitignore create mode 100644 README.md create mode 100644 accessListing.php create mode 100644 accesslog-model.php create mode 100644 cal.php create mode 100644 calendar-model.php create mode 100644 calendar-routes.php create mode 100644 calendar.php create mode 100644 calendar_functions.php create mode 100644 campaign-model.php create mode 100644 campaignAdd.php create mode 100644 campaignEdit.php create mode 100644 campaignListing.php create mode 100644 campaignView copy.php create mode 100644 campaignView.php create mode 100644 client-login.php create mode 100644 client-profile.php create mode 100644 client-routes.php create mode 100644 core.php create mode 100644 cronjob.php create mode 100644 db.sql create mode 100644 get_all_v1_api_images.php create mode 100644 get_v1_api_image_get.php create mode 100644 index.php create mode 100644 layout/.DS_Store create mode 100644 layout/footer/Adminnone_footer.php create mode 100644 layout/footer/Clientnone_footer.php create mode 100644 layout/header/Adminleft_sidebar.php create mode 100644 layout/header/Clientleft_sidebar.php create mode 100644 lib/ghl/calendar.php create mode 100644 lib/ghl/oauth2.php create mode 100644 lib/google/drive.php create mode 100644 lib/google/oauth2.php create mode 100644 lib/redbean/rb-mysql.php create mode 100644 license-model.php create mode 100644 licenseAdd.php create mode 100644 licenseEdit.php create mode 100644 licenseListing.php create mode 100644 location-model.php create mode 100644 locationAdd.php create mode 100644 locationEdit.php create mode 100644 locationListing.php create mode 100644 login.php create mode 100644 migration.sql create mode 100644 mysql-adapter.php create mode 100644 mysql-database-service.php create mode 100644 mysql.php create mode 100644 oauth-routes.php create mode 100644 post_v1_api_image_add.php create mode 100644 privacy-policy.php create mode 100644 project-model.php create mode 100644 project.js create mode 100644 projectAdd.php create mode 100644 projectEdit.php create mode 100644 projectEditMulti.php create mode 100644 projectListing.php create mode 100644 put_v1_api_image_edit.php create mode 100644 query-service.php create mode 100644 report-model.php create mode 100644 reportListing.php create mode 100644 report_cronjob.php create mode 100644 sample.tsv create mode 100644 schema.sql create mode 100644 style.css create mode 100644 team_followup_2023-10-16.sql create mode 100644 terms.php create mode 100644 test.html create mode 100644 tt.php create mode 100644 user-model.php create mode 100644 userAdd.php create mode 100644 userEdit.php create mode 100644 userListing.php create mode 100644 validation-service.php diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..785859f901215dc292dc9b1b020cc7c3412d8b3f GIT binary patch literal 6148 zcmeHK%}T>S5T30;B3=q!Jgx_CQs@gvEb%IZdh#SqqzZ{!{OLJY#e+}cD|pdo@iqKr zXQ^3ZJ&KeWnEf{MvzdG;nGO+|(S122>Jw24W$bNX`9avv+L2zcaGv2as@ZT-xO(1+ zRKtH|fZtt01Deql-No;3F=+d0m*>;UO<_wvb{<~Gr|(Cxzx>rdzij7Be+b##b9|2E; KF3P~4GVlqX7))9K literal 0 HcmV?d00001 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ef71748 --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +vendor +config.php +jobs/node_modules +jobs/locations/* +jobs/*.png +jobs \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..5d0fb49 --- /dev/null +++ b/README.md @@ -0,0 +1,97 @@ + + +# Module 3 - TFU + +A PHP-based web application for managing team campaigns and followups. + +## Getting Started + +### Prerequisites + +- PHP 7.4 or higher +- MySQL/MariaDB + +### Installation + +1. Clone this repository to your local machine +2. Configure your database connection in `config.php` +3. Import the database schema using the provided `db.sql` file + +### Running the Application + +Start the development server using: + +```bash + +php -S localhost:9000 + +``` + + +Then visit `http://localhost:9000` in your web browser. + +Admin Login `http://localhost:9000/admin/login` + +admin@manaknight.com +a123456 + +Client Login `http://localhost:9000/client/login` + +emmy@manaknight.com +a123456 + +## Features + +- Campaign management +- Team followup tracking +- Add and edit campaign details +- Database-driven application + +## Core Project Files + +├── README.md + +├── config.php # Database configuration + +├── db.sql # Database schema + +├── migration.sql # Database seed data + +├── index.php # Main entry point + +## Tasks + +### Miscs + +1. On admin login redirect to Access Log + +2. Client login is broken + +3. Add bulk delete functionality to the Admin License list + +4. Change the pagination type from offset to cursor pagination on Admin License list (https://www.merge.dev/blog/cursor-pagination) + +### Filter functionality + +5. Add a fuzzy email search to the Admin license list + +6. Add a fuzzy search for the project & webhook fields to the Admin Availability Checker list + +7. Add a fuzzy search filter for the project field in Admin Reports + +8. Add a date range filter for the date field in Admin Reports + +### Integrations + +9. User should be able to Add Campign without connecting drive. On the Add Campign screen when the user selects the Select Google sheet button, display the google picker modal to select spreadsheets only. + +https://developers.google.com/drive/picker/guides/overview + +Google console creds: +Client id: 356934742115-c707dqbhct9b7gj64eo9rqfdfi47rb8o.apps.googleusercontent.com +API key: AIzaSyB7JhbYloABBC-jZebyjHoiXUiM-s_7sBA + + + + + diff --git a/accessListing.php b/accessListing.php new file mode 100644 index 0000000..a2f7f74 --- /dev/null +++ b/accessListing.php @@ -0,0 +1,96 @@ + + +
+

Access Log

+
+
+
+
+ + + + +
+
+
+
+ + +
+ + + + + + + + + + + $value) { + echo ' '; + echo ' '; + echo ' '; + echo ' '; + echo ' '; + echo ''; + } + ?> + +
IDRelationship #IPDate
' . $value->id . '
delete
' . $value->relationship_num . ' ' . $value->ip . ' ' . $value->created_at . '
+
+ + 0 ? ($currentPage - $range) : 1; +$endPage = ($currentPage + $range) < $totalPages ? ($currentPage + $range) : $totalPages; + +if ($total > 0) { +?> + + + +
diff --git a/accesslog-model.php b/accesslog-model.php new file mode 100644 index 0000000..98996bd --- /dev/null +++ b/accesslog-model.php @@ -0,0 +1,39 @@ + 'Project' + ]; + // $config = MkdConfig::get_instance()->get_config(); + // $apikey = $config['gohighlevel_key']; + // $cid = $_POST['calendar_id']; + $pid = (int)$_POST['project_id']; + $projectModel = new ProjectModel(); + + try { + $model = $projectModel->get((int)$pid); + $apikey = $model->location; + $curl = curl_init(); + + curl_setopt_array($curl, [ + CURLOPT_URL => "https://rest.gohighlevel.com/v1/calendars/services", + CURLOPT_RETURNTRANSFER => true, + CURLOPT_ENCODING => "", + CURLOPT_MAXREDIRS => 10, + CURLOPT_TIMEOUT => 30, + CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1, + CURLOPT_CUSTOMREQUEST => "GET", + CURLOPT_HTTPHEADER => [ + "Accept: application/json", + "Authorization: Bearer " . $apikey, + "Version: 2021-04-15" + ], + ]); + + $response = curl_exec($curl); + $err = curl_error($curl); + $status_code = curl_getinfo($curl, CURLINFO_HTTP_CODE); + + curl_close($curl); + + // print_r($response); + + if ($status_code != 200) { + echo "Something went wrong."; + exit; + } + + $cid = $model->calendar; + $data = json_decode($response, true); + + // Search for the service with the specified ID + $searchedService = null; + if (!empty($data['services']) && count($data['services'] ) > 0) { + foreach ($data['services'] as $service) { + if ($service['id'] === $cid) { + $searchedService = $service; + break; + } + } + } + + if ($searchedService == null) { + echo "Calendar Service with ID '" . $cid . "' not found."; + exit; + } + + $slots = json_decode($model->slot); + + echo checkServiceInSlot($searchedService["availability"]["officeHours"], + $slots, + $model, + $searchedService["availability"]["eventTiming"], + $searchedService["availability"]["schedule"], + $searchedService["id"]); + } catch (\Throwable $th) { + echo "Something went wrong."; + exit; + } + + + exit; + + + }, 'post'); + + + diff --git a/calendar-model.php b/calendar-model.php new file mode 100644 index 0000000..d15aab0 --- /dev/null +++ b/calendar-model.php @@ -0,0 +1,49 @@ + +
+

Available Time Slots

+
    +
    --> + + +
    + + + + + + + + \ No newline at end of file diff --git a/calendar_functions.php b/calendar_functions.php new file mode 100644 index 0000000..f34a9fd --- /dev/null +++ b/calendar_functions.php @@ -0,0 +1,545 @@ += $start && $slotTime <= $end; + }); + + return array_values($filteredSlots); // Reset array keys +} + + +function extractRanges($slotStart, $slotEnd, $serviceStart, $serviceEnd, $interval, $date) +{ + //echo function args + echo "\n Function Args \n "; + print_r(func_get_args()); + // Convert time strings to DateTime objects for easier comparison + $slotStartTime = new DateTime($date . ' ' . $slotStart); + $slotEndTime = new DateTime($date . ' ' . $slotEnd); + $serviceStartTime = new DateTime($date . ' ' . $serviceStart); + $serviceEndTime = new DateTime($date . ' ' . $serviceEnd); + + if ($slotEndTime < $slotStartTime) { + $slotEndTime->modify('+1 day'); + } + if ($serviceEndTime < $serviceStartTime) { + $serviceEndTime->modify('+1 day'); + } + + // Check if service time falls within the slot range + if ($serviceStartTime >= $slotStartTime && $serviceStartTime < $slotEndTime) { + // if ($serviceStartTime >= $slotStartTime && $serviceEndTime <= $slotEndTime) { + $ranges = array(); + + // Calculate ranges based on the interval + $currentRangeStart = $serviceStartTime; + if ($currentRangeStart < $slotEndTime) { + array_push($ranges, $currentRangeStart->format('H:i:s')); + } + while ($currentRangeStart < $serviceEndTime) { + $currentRangeEnd = clone $currentRangeStart; + $currentRangeEnd->add(new DateInterval("PT" . $interval . "M")); + + // Check if the calculated range end is within the slot range + if ($currentRangeEnd < $slotEndTime) { + // array_push( + // $ranges, + // $currentRangeStart->format('H:i:s') + // ); + array_push( + $ranges, + $currentRangeEnd->format('H:i:s') + ); + } else { + // Add the end time within the slot range + // array_push($ranges, $slotEndTime->format('H:i:s')); + // Break the loop if the range end exceeds the slot end time + break; + } + + $currentRangeStart = $currentRangeEnd; + } + + return $ranges; + } elseif ($serviceStartTime < $slotStartTime && $serviceEndTime <= $slotEndTime) { + // if ($serviceStartTime >= $slotStartTime && $serviceEndTime <= $slotEndTime) { + $ranges = array(); + + // Calculate ranges based on the interval + $currentRangeStart = $slotStartTime; + if ($currentRangeStart < $serviceEndTime) { + array_push($ranges, $currentRangeStart->format('H:i:s')); + } + // array_push($ranges, $currentRangeStart->format('H:i:s')); + while ($currentRangeStart < $serviceEndTime) { + $currentRangeEnd = clone $currentRangeStart; + $currentRangeEnd->add(new DateInterval("PT" . $interval . "M")); + + // Check if the calculated range end is within the slot range + if ($currentRangeEnd < $slotEndTime) { + // array_push( + // $ranges, + // $currentRangeStart->format('H:i:s') + // ); + array_push( + $ranges, + $currentRangeEnd->format('H:i:s') + ); + } else { + // Add the end time within the slot range + // array_push($ranges, $slotEndTime->format('H:i:s')); + // Break the loop if the range end exceeds the slot end time + break; + } + + $currentRangeStart = $currentRangeEnd; + } + + return $ranges; + } elseif ($serviceStartTime < $slotStartTime && $serviceEndTime > $slotEndTime) { + // if ($serviceStartTime >= $slotStartTime && $serviceEndTime <= $slotEndTime) { + $ranges = array(); + + // Calculate ranges based on the interval + $currentRangeStart = $slotStartTime; + if ($currentRangeStart < $serviceEndTime) { + array_push($ranges, $currentRangeStart->format('H:i:s')); + } + // array_push($ranges, $currentRangeStart->format('H:i:s')); + while ($currentRangeStart < $serviceEndTime) { + $currentRangeEnd = clone $currentRangeStart; + $currentRangeEnd->add(new DateInterval("PT" . $interval . "M")); + + // Check if the calculated range end is within the slot range + if ($currentRangeEnd < $slotEndTime) { + // array_push( + // $ranges, + // $currentRangeStart->format('H:i:s') + // ); + array_push( + $ranges, + $currentRangeEnd->format('H:i:s') + ); + } else { + // Add the end time within the slot range + // array_push($ranges, $slotEndTime->format('H:i:s')); + // Break the loop if the range end exceeds the slot end time + break; + } + + $currentRangeStart = $currentRangeEnd; + } + + return $ranges; + } elseif ($serviceStartTime >= $slotStartTime && $serviceEndTime <= $slotEndTime) { + $ranges = array(); + + // Calculate ranges based on the interval + $currentRangeStart = $serviceStartTime; + if ($currentRangeStart < $serviceEndTime) { + array_push($ranges, $currentRangeStart->format('H:i:s')); + } + // array_push($ranges, $currentRangeStart->format('H:i:s')); + while ($currentRangeStart <= $serviceEndTime) { + $currentRangeEnd = clone $currentRangeStart; + $currentRangeEnd->add(new DateInterval("PT" . $interval . "M")); + + // Check if the calculated range end is within the slot range + if ($currentRangeEnd <= $slotEndTime) { + // array_push( + // $ranges, + // $currentRangeStart->format('H:i:s') + // ); + array_push( + $ranges, + $currentRangeEnd->format('H:i:s') + ); + } else { + // Add the end time within the slot range + // array_push($ranges, $slotEndTime->format('H:i:s')); + // Break the loop if the range end exceeds the slot end time + break; + } + + $currentRangeStart = $currentRangeEnd; + } + + return $ranges; + } else { + // Service time is outside the slot range + return false; + } +} + +function convertToDays($value, $unit) +{ + switch ($unit) { + case 'weeks': + return $value * 7; + case 'months': + // Assuming a month is considered as 30 days for simplicity + return $value * 30; + case 'hours': + return 0; // Set to 0 days if the unit is hours + default: + return $value; + } +} + +function checkServiceInSlot($service, $slot, $mod, $eventTiming, $schedule, $ser_id) +{ + + $slotpoint = 0; + // Get the current date + $currentDate = new DateTime('now'); + + + // return $slotpoint; + + // if ($mod->alert == 'On') { + $slotpoint = calculateTotalPoints((int)$mod->days, $slot, $service, $eventTiming, $schedule, $mod, $ser_id); + if (is_array($slotpoint)) { + return $slotpoint['message']; + } + if ($slotpoint < (int)$mod->score_threshold && $slotpoint != 0) { + + $data = [ + 'actual_score' => $slotpoint, + ]; + // Update points + $projectModel = new ProjectModel(); + $projectModel->edit($data, $mod->id); + + if ($mod->alert != 'On') { + return 'No alert set for project ' . $mod->project_name . ' with ID ' . $mod->id; + } + + // Retrieve webhook URL and payload + $webhookUrl = $mod->webhook; + if (!filter_var($webhookUrl, FILTER_VALIDATE_URL)) { + // error_log('Webhook for project ' . $mod->project_name . ' with ID ' . $mod->id . ' is not a URL '); + return 'Invalid Webhook URL'; + } + + + $jsonData = json_decode($mod->payload); + + + // Set up cURL options + $curlOptions = [ + CURLOPT_URL => $webhookUrl, + CURLOPT_POST => 1, + CURLOPT_POSTFIELDS => json_encode($jsonData), // Send encoded JSON + CURLOPT_HTTPHEADER => ['Content-Type: application/json'], + CURLOPT_RETURNTRANSFER => true, + ]; + + // Initialize cURL session + $ch = curl_init(); + + // Set cURL options + curl_setopt_array($ch, $curlOptions); + + // Execute cURL session and get the result + $response = curl_exec($ch); + $status_code = curl_getinfo($ch, CURLINFO_HTTP_CODE); + + if ($status_code != 200) { + // error_log('Curl error for project ' . $mod->project_name . ' with ID ' . $mod->id . ': ' . curl_error($ch)); + return 'Something went wrong while sending webhook payload'; + } + + + + // Log the webhook response + curl_close($ch); + // error_log('Webhook response for project ' . $mod->project_name . ' with ID ' . $mod->id . ': ' . $response); + return 'Accepted'; + + + // Close cURL session + + } else { + + $data = [ + 'actual_score' => $slotpoint, + ]; + + + // Insert data into the database using LicenseModel + $projectModel = new ProjectModel(); + $projectModel->edit($data, $mod->id); + $result = "Point is higher than mininum point set"; // Initialize result variable + + return $result; // Return the accumulated result + + } + // } else { + // // Log that no alert is set for the project + // // error_log('No alert set for project ' . $mod->project_name . ' with ID ' . $mod->id); + // return 'No alert set for project ' . $mod->project_name . ' with ID ' . $mod->id; + // } +} + +function generateTimeIntervals($startTime, $endTime, $intervalMinutes, $date) +{ + + $start = new DateTime($date . ' ' . $startTime); + $end = new DateTime($date . ' ' . $endTime); + + if ($end < $start) { + $end->modify('+1 day'); + } + $intervalObj = new DateInterval("PT{$intervalMinutes}M"); // PT stands for 'Period Time' + + $period = new DatePeriod($start, $intervalObj, $end); + + $intervals = array(); + foreach ($period as $dt) { + $intervals[] = $dt->format('H:i:s'); + } + + return $intervals; +} + +function calculateTotalPoints($daysToCheck, $slot, $service, $eventTiming, $schedule, $project, $ser_id) +{ + // return json_encode($schedule); + + + // Convert allowBookingFor to days if the unit is months or weeks + if ($schedule['allowBookingForUnit'] === 'months' || $schedule['allowBookingForUnit'] === 'weeks') { + $schedule['allowBookingFor'] = convertToDays($schedule['allowBookingFor'], $schedule['allowBookingForUnit']); + $schedule['allowBookingForUnit'] = 'days'; + } + if ($schedule['allowBookingAfterUnit'] === 'months' || $schedule['allowBookingAfterUnit'] === 'weeks' || $schedule['allowBookingAfterUnit'] === 'hours') { + $schedule['allowBookingAfter'] = convertToDays($schedule['allowBookingAfter'], $schedule['allowBookingAfterUnit']); + $schedule['allowBookingAfterUnit'] = 'days'; + } + // return "well"; + + $currentDate = new DateTime(); + $currentDayIndex = $currentDate->format('w'); // 0 for Sunday, 1 for Monday, ..., 6 for Saturday + + // Get the next two days to check + // $daysToCheckIndices = []; + // for ($i = 0; $i < $daysToCheck; $i++) { + // $nextDayIndex = ($currentDayIndex + $i + 1) % 7; + // $daysToCheckIndices[] = $nextDayIndex; + // } + + + + $daysToCheckData = []; + if (empty($schedule['allowBookingFor'])) { + $counter = $daysToCheck; + } else { + $counter = (int)$schedule['allowBookingFor']; + } + if (empty($schedule['allowBookingAfter'])) { + $skip = 0; + } else { + $skip = (int)$schedule['allowBookingAfter']; + } + // Don't check beyond 30 days + if ($counter > 30) { + $counter = 30; + } + for ($i = $skip; $i < $counter; $i++) { + $nextDayIndex = ($currentDayIndex + $i) % 7; + $nextDate = clone $currentDate; + $nextDate->modify("+$i day"); + + + $daysToCheckData[] = [ + 'index' => $nextDayIndex, + 'date' => $nextDate->format('Y-m-d'), + ]; + } + + + + // return json_encode($daysToCheckData); + $totalPoints = 0; + + // Loop through the selected days and calculate total points + $dayNames = ['sunday', 'monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday']; + + // GHL V2 token refresh + // access token expires in a day + // refresh token last for a year if you don't use it. + // after refreshing you get a new refresh token and access token but previous refresh token expires. + $canRefresh = false; + if ($project->refresh_token != "") { + $canRefresh = true; + } + + if ($canRefresh) { + $calendar = new GHLCalendar($project->id, $project->access_token, $project->refresh_token); + $result = $calendar->refreshToken(); + + if ($result['code'] != 200) { + return $result; + } + } + // foreach ($daysToCheckIndices as $dayIndex) { + foreach ($daysToCheckData as $dayData) { + // $dayName = $dayNames[$dayIndex]; + // // error_log("day" . $dayName); + + + + $dayIndex = $dayData['index']; + $dayName = $dayNames[$dayIndex]; + $date = $dayData['date']; + + + + // print_r($service); + if (isset($slot->$dayName)) { + if (!array_key_exists($dayName, $service)) { + continue; // Day not found in the service schedule + } + $der = []; + + + foreach ($service[$dayName] as $ser) { + $serviceStart = strtotime(sprintf("%02d:%02d", $ser["openHour"], $ser["openMinute"])); + $serviceEnd = strtotime(sprintf("%02d:%02d", $ser["closeHour"], $ser["closeMinute"])); + + + + + + + + // $start = strtotime($dayData['date'] . ' ' . sprintf("%02d:%02d", $ser["openHour"], $ser["openMinute"])); + // $end = strtotime($dayData['date'] . ' ' . sprintf("%02d:%02d", $ser["closeHour"], $ser["closeMinute"])); + // $start = $start * 1000; + // $end = $end * 1000; + + // Set start time to beginning of day (00:00:00) + $start = strtotime($dayData['date'] . ' 00:00:00'); + // Set end time to end of day (23:59:59) + $end = strtotime($dayData['date'] . ' 23:59:59'); + + $start = $start * 1000; + $end = $end * 1000; + $apikey2 = $project->location; + $curl = curl_init(); + + + + curl_setopt_array($curl, [ + CURLOPT_URL => "https://rest.gohighlevel.com/v1/appointments/slots?calendarId=$ser_id&startDate=$start&endDate=$end", + CURLOPT_RETURNTRANSFER => true, + CURLOPT_ENCODING => "", + CURLOPT_MAXREDIRS => 10, + CURLOPT_TIMEOUT => 30, + CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1, + CURLOPT_CUSTOMREQUEST => "GET", + CURLOPT_HTTPHEADER => [ + "Accept: application/json", + "Authorization: Bearer " . $apikey2, + "Version: 2021-04-15" + ], + ]); + + $response = curl_exec($curl); + $err = curl_error($curl); + $status_code = curl_getinfo($curl, CURLINFO_HTTP_CODE); + + + curl_close($curl); + + + if ($status_code != 200 && !$canRefresh) { + return ["code" => 422, "message" => "Please authorize the app to fetch free slots"]; + } + + if ($status_code != 200) { + + $calendar = new GHLCalendar($project->id, $project->access_token, $project->refresh_token); + $result = $calendar->getFreeSlots( $ser_id, $start, $end); + sleep(3); // consider rate limit + + if ($result['code'] != 200) { + // $result['message'] = "Something went wrong while fetching free slots"; + return $result; + continue; + } + $data = $result['data']; + // continue; + } + + + + $data = empty($data) ? json_decode($response, true) : $data; + + + // return json_encode($data); + + + $stepSize = $eventTiming["slotInterval"]; + + $rslt = []; + + if (!empty($data[$dayData['date']]["slots"])) { + + foreach ($data[$dayData['date']]["slots"] as $time) { + $dateTime = new DateTime($time); + $dateTime->modify('+5 hours'); // Convert from UTC-5 to UTC/GMT + + $rslt[] = $dateTime->format('H:i:s'); + } + + + + foreach ($slot->$dayName as $timeSlot) { + + + $slotStart = strtotime($timeSlot->from); + $slotEnd = strtotime($timeSlot->to); + + $filteredSlots = filterTimeSlots($rslt, date("H:i:s", $slotStart), date("H:i:s", $slotEnd)); + + + $totalPoints += (int)$timeSlot->point * count($filteredSlots); + + /* + $range = extractRanges(date("H:i:s", $slotStart), date("H:i:s", $slotEnd), date("H:i:s", $serviceStart), date("H:i:s", $serviceEnd), $stepSize, $dayData['date']); + + + if (!$range) { + continue; + } + $intersection = array_intersect($rslt, $range); + if (empty($intersection)) { + continue; + } + foreach ($intersection as $intersect) { + $totalPoints += (int)$timeSlot->point; + // return $totalPoints; + } + */ + } + } + + + + // $totalPoints += intval($timeSlot->point) ?: 0; + } + } + + // return json_encode(["derrr" => $der]); + } + + return $totalPoints; +} \ No newline at end of file diff --git a/campaign-model.php b/campaign-model.php new file mode 100644 index 0000000..afed568 --- /dev/null +++ b/campaign-model.php @@ -0,0 +1,132 @@ +get_config(); + $userModel = new UserModel(); + $user = $userModel->get($_SESSION['user']); + + $oauth = new \Lib\Google\GoogleOAuth2( + $config['google_client_id'], + $config['google_client_secret'], + $config['google_redirect_uri'] + ); + + $oauth->setRefreshToken($user->drive_refresh_token); + $oauth->refreshAccessToken(); + + $drive = new \Lib\Google\GoogleDrive($oauth); + + // Download and parse the file + $content = $drive->downloadFile($campaign->file_id, 'text/csv'); + $data = $this->csvToObject($content); + + // Apply filters + $validRows = range(0, count($data['date']) - 1); + + if ($filters['campaign_name']) { + $validRows = array_filter($validRows, function($i) use ($data, $filters) { + return $data['campaign_name'][$i] === $filters['campaign_name']; + }); + } + + if ($filters['ad_set_name']) { + $validRows = array_filter($validRows, function($i) use ($data, $filters) { + return $data['ad_set_name'][$i] === $filters['ad_set_name']; + }); + } + + if ($filters['ad_name']) { + $validRows = array_filter($validRows, function($i) use ($data, $filters) { + return $data['ad_name'][$i] === $filters['ad_name']; + }); + } + + // Filter the data + $filteredData = []; + foreach ($data as $column => $values) { + $filteredData[$column] = array_intersect_key($values, array_flip($validRows)); + } + + return $filteredData; + } + +} diff --git a/campaignAdd.php b/campaignAdd.php new file mode 100644 index 0000000..ec1c200 --- /dev/null +++ b/campaignAdd.php @@ -0,0 +1,260 @@ +
    +

    Add Campaign Data

    + + + + +
    +
    + + +
    +
    + +
    +
    +
    + +
    + +
    +
    +
    + + +
    +
    + + + + \ No newline at end of file diff --git a/campaignEdit.php b/campaignEdit.php new file mode 100644 index 0000000..8b5b62d --- /dev/null +++ b/campaignEdit.php @@ -0,0 +1,254 @@ +
    +

    Edit Campaign

    + +
    +
    + + +
    + +
    + +
    +
    +
    + +
    + +
    +
    +
    + + + Cancel +
    +
    + + + + \ No newline at end of file diff --git a/campaignListing.php b/campaignListing.php new file mode 100644 index 0000000..28e3f8a --- /dev/null +++ b/campaignListing.php @@ -0,0 +1,166 @@ + + +
    +

    Campaigns

    + + get($_SESSION['user']); + + if (!$user->drive_refresh_token): ?> +
    + Connect your Google Drive to create campaigns + + Connect Drive + +
    + +
    + + Add Campaign + + +
    + +
    +
    + + +
    +
    +
    +
    + + + + +
    +
    +
    +
    + +
    + + + + + + + + + + + + + + + + + + + +
    IDNameDateActions
    id; ?>name); ?>created_at; ?> + View + Edit + Delete +
    +
    + + + 0 ? ($currentPage - $range) : 1; + $endPage = ($currentPage + $range) < $totalPages ? ($currentPage + $range) : $totalPages; + + ?> + + +
    + + + \ No newline at end of file diff --git a/campaignView copy.php b/campaignView copy.php new file mode 100644 index 0000000..583818d --- /dev/null +++ b/campaignView copy.php @@ -0,0 +1,1040 @@ + + +
    +

    Campaign View

    + + +
    +
    + + +
    + +
    + + +
    + +
    + + +
    + +
    + +
    + + +
    +
    +
    + + +
    +
    + +
    +
    + + +
    +
    + Showing 0 entries +
    +
    + +
    +
    + + +
    + + + + + + + + $value) { + if (!in_array($key, ['date', 'campaign_name', 'ad_set_name', 'ad_name'])) { + echo ''; + } + } + } + ?> + + + + + +
    DateCampaign NameAd Set NameAd Name' . ucwords(str_replace('_', ' ', $key)) . '
    +
    +
    + + +
    +
    +
    Column Visibility
    + +
    +
    +
    + + +
    +
    + + +
    +
    + + +
    +
    + + +
    + $value) { + if (!in_array($key, ['date', 'campaign_name', 'ad_set_name', 'ad_name'])) { + $columnId = str_replace('_', '', $key) . 'Column'; + echo '
    '; + echo ''; + echo ''; + echo '
    '; + } + } + } + ?> +
    +
    + + + + \ No newline at end of file diff --git a/campaignView.php b/campaignView.php new file mode 100644 index 0000000..c58643f --- /dev/null +++ b/campaignView.php @@ -0,0 +1,1276 @@ + + +
    +

    Campaign View

    + + +
    + + + +
    + + +
    +
    + + +
    + +
    + + +
    + +
    + + +
    + +
    + +
    + + +
    +
    +
    + + +
    +
    + +
    +
    + + +
    +
    + Showing 0 entries +
    +
    + +
    +
    + + +
    + + + + + + + + $value) { + if (!in_array($key, ['date', 'campaign_name', 'ad_set_name', 'ad_name'])) { + echo ''; + } + } + } + ?> + + + + + +
    DateCampaign NameAd Set NameAd Name' . ucwords(str_replace('_', ' ', $key)) . '
    +
    +
    + + +
    +
    +
    Column Visibility
    + +
    +
    + + $value) { + if (!in_array($key, ['date', 'campaign_name', 'ad_set_name', 'ad_name'])) { + $columnId = str_replace('_', '', $key) . 'Column'; + echo '
    '; + echo ''; + echo ''; + echo '
    '; + } + } + } + ?> +
    +
    + + + + \ No newline at end of file diff --git a/client-login.php b/client-login.php new file mode 100644 index 0000000..dc2ba06 --- /dev/null +++ b/client-login.php @@ -0,0 +1,50 @@ + + + + + + + Login Page + + + + + + +
    +
    +
    +
    +
    +
    Login
    + + + +
    +
    + + +
    +
    + + +
    +
    + +
    +
    +
    +
    +
    +
    +
    + + + + + + + + \ No newline at end of file diff --git a/client-profile.php b/client-profile.php new file mode 100644 index 0000000..0f7363a --- /dev/null +++ b/client-profile.php @@ -0,0 +1,93 @@ +
    +

    Profile

    + + + +
    +
    + + +
    Please enter a valid email address
    +
    +
    + + +
    +
    + + +
    Passwords do not match!
    +
    + + + +
    +
    + + \ No newline at end of file diff --git a/client-routes.php b/client-routes.php new file mode 100644 index 0000000..201359e --- /dev/null +++ b/client-routes.php @@ -0,0 +1,1021 @@ + password_hash($raw_password, PASSWORD_BCRYPT), + 'email' => $email, + ]; + + // Insert data into the database using LicenseModel + $userModel = new UserModel(); + $result = $userModel->get_by_field('id', $email); + // var_dump($result);exit; + if ($result) { + if (password_verify($raw_password, $result['password']) && + $result['status'] == 'active' && + $result['role'] == 'client') { + $_SESSION['is_logged_in'] = true; + $_SESSION['role'] = $result['role']; + $_SESSION['user'] = $result['id']; + header('Location: /client/report'); + } else { + + $error = true; + // include_once __DIR__ . '/layout/header/Clientleft_sidebar.php'; + include_once __DIR__ . '/client-login.php'; + // include_once __DIR__ . '/layout/footer/Clientnone_footer.php'; + exit; + } + } + $error = true; + include_once __DIR__ . '/client-login.php'; + } +}, 'post'); + +Route::add('/client/report', function () { + check_login(); + $format = isset($_GET['format']) ? $_GET['format'] : 'json'; + $page = isset($_GET['page']) ? intval($_GET['page']) : 1; + $id = isset($_GET['id']) ? intval($_GET['id']) : 0; + $per_page = isset($_GET['size']) ? intval($_GET['size']) : 10; + $sort = isset($_GET['sort']) ? $_GET['sort'] : 'id'; + $direction = isset($_GET['direction']) ? $_GET['direction'] : 'ASC'; + $date = isset($_GET['date']) ? $_GET['date'] : ''; + + $reportModel = new ReportModel(); + + $data = [ + 'page_title' => 'Report', + 'date' => $date + ]; + + $user = $_SESSION['user']; + $where = []; + $where[] = "location_id IN (SELECT id from location WHERE user_id = {$user} )"; + + if ($date != '') { + $where['date'] = '"' . $date . '"'; + } + + + + $result = $reportModel->get_paginated($page, $per_page, $where, 'id', 'DESC'); + // echo json_encode($result); + if ($result) { + if ($format == 'json') { + $data = array_merge($data, $result); + } + } + include_once __DIR__ . '/layout/header/Clientleft_sidebar.php'; + include_once __DIR__ . '/reportListing.php'; + include_once __DIR__ . '/layout/footer/Clientnone_footer.php'; +}, 'get'); + + +Route::add('/client/location', function () { + check_login(); + $format = isset($_GET['format']) ? $_GET['format'] : 'json'; + $page = isset($_GET['page']) ? intval($_GET['page']) : 1; + $id = isset($_GET['id']) ? intval($_GET['id']) : 0; + $per_page = isset($_GET['size']) ? intval($_GET['size']) : 10; + $sort = isset($_GET['sort']) ? $_GET['sort'] : 'id'; + $direction = isset($_GET['direction']) ? $_GET['direction'] : 'ASC'; + $name = isset($_GET['name']) ? $_GET['name'] : ''; + + + $locationModel = new LocationModel(); + + $data = [ + 'page_title' => 'Location', + 'name' => $name + ]; + + $where = [ + 'user_id' => $_SESSION['user'] + ]; + + if ($name != '') { + $where['name'] = '"' . $name . '"'; + } + + $result = $locationModel->get_paginated($page, $per_page, $where, $sort, $direction); + + if ($result) { + if ($format == 'json') { + $data = array_merge($data, $result); + } + } + include_once __DIR__ . '/layout/header/Clientleft_sidebar.php'; + include_once __DIR__ . '/locationListing.php'; + include_once __DIR__ . '/layout/footer/Clientnone_footer.php'; +}, 'get'); +Route::add('/client/location/add', function () { + check_login(); + $error = false; + + $data = [ + 'page_title' => 'Location' + ]; + + include_once __DIR__ . '/layout/header/Clientleft_sidebar.php'; + include_once __DIR__ . '/locationAdd.php'; + include_once __DIR__ . '/layout/footer/Clientnone_footer.php'; +}, 'get'); + +Route::add('/client/location/add', function () { + check_login(); + $error = false; + + $data = [ + 'page_title' => 'Location' + ]; + + if (empty($_POST['name']) || empty($_POST['apikey']) || empty($_POST['location_id'])) { + $error = true; + include_once __DIR__ . '/layout/header/Clientleft_sidebar.php'; + include_once __DIR__ . '/locationAdd.php'; + include_once __DIR__ . '/layout/footer/Clientnone_footer.php'; + } else { + // Collect form data + $name = $_POST['name']; + $apikey = $_POST['apikey']; + $webhook = $_POST['webhook']; + $location_id = $_POST['location_id']; + + // Prepare data array + $data = [ + 'name' => $name, + 'apikey' => $apikey, + 'webhook' => $webhook, + 'location_id' => $location_id, + 'created_at' => $current_date, + 'user_id' => $_SESSION['user'] + ]; + + // Insert data into the database using LicenseModel + $locationModel = new LocationModel(); + $locationModel->create($data); + header('Location: /client/location'); + } +}, 'post'); + +Route::add('/client/location/edit/([0-9]+)', function ($id) { + check_login(); + $error = false; + + $locationModel = new LocationModel(); + $model = $locationModel->get($id); + + if (!$model) { + header('Location: /admin/location'); + exit; + } + + $data = [ + 'page_title' => 'Location', + 'id' => $id + ]; + + if (empty($_POST['name']) || empty($_POST['apikey']) || empty($_POST['location_id']) ) { + $error = true; + include_once __DIR__ . '/layout/header/Clientleft_sidebar.php'; + include_once __DIR__ . '/locationEdit.php'; + include_once __DIR__ . '/layout/footer/Clientnone_footer.php'; + } else { + // Collect form data + $name = $_POST['name']; + $apikey = $_POST['apikey']; + $webhook = $_POST['webhook']; + $location_id = $_POST['location_id']; + + + + // Generate apikey + $current_date = date('Y-m-d H:i:s'); + + // Prepare data array + $data = [ + 'name' => $name, + 'apikey' => $apikey, + 'webhook' => $webhook, + 'location_id' => $location_id, + ]; + + $locationModel = new LocationModel(); + $locationModel->edit($data, $id); + header('Location: /client/location'); + } +}, 'post'); + + +Route::add('/client/location/edit/([0-9]+)', function ($id) { + check_login(); + $error = false; + $locationModel = new LocationModel(); + $model = $locationModel->get($id); + + if (!$model) { + header('Location: /client/location'); + exit; + } + + $data = [ + 'page_title' => 'Location', + 'model' => $model + ]; + + include_once __DIR__ . '/layout/header/Clientleft_sidebar.php'; + include_once __DIR__ . '/locationEdit.php'; + include_once __DIR__ . '/layout/footer/Clientnone_footer.php'; +}, 'get'); + +Route::add('/client/location/delete/([0-9]+)', function ($id) { + check_login(); + $locationModel = new LocationModel(); + $locationModel->real_delete($id); + header('Location: /client/location'); +}, 'get'); + + +Route::add('/client/profile', function () { + check_login(); + $error = false; + + $userModel = new UserModel(); + $model = $userModel->get($_SESSION['user']); + + $data = [ + 'page_title' => 'Profile', + 'model' => $model + ]; + + include_once __DIR__ . '/layout/header/Clientleft_sidebar.php'; + include_once __DIR__ . '/client-profile.php'; + include_once __DIR__ . '/layout/footer/Clientnone_footer.php'; +}, 'get'); + + +Route::add('/client/profile/edit/([0-9]+)', function ($id) { + check_login(); + $error = false; + + $userModel = new UserModel(); + $model = $userModel->get($id); + + if (!$model) { + header('Location: /client/profile'); + exit; + } + + $data = [ + 'page_title' => 'Profile', + 'id' => $id + ]; + + if (empty($_POST['email']) || empty($_POST['status'])) { + $error = true; + include_once __DIR__ . '/layout/header/Clientleft_sidebar.php'; + include_once __DIR__ . '/client-profile.php'; + include_once __DIR__ . '/layout/footer/Clientnone_footer.php'; + } else { + // Collect form data + $password = isset($_POST['password']) ? $_POST['password'] : ''; + $email = $_POST['email']; + $status = isset($_POST['status']) ? $_POST['status'] : 'active'; + // Prepare data array + $data = [ + 'email' => $email, + 'status' => $status + ]; + + if (strlen($password) > 0) { + $data['password'] = password_hash($password, PASSWORD_BCRYPT); + } + + // Insert data into the database using LicenseModel + $userModel = new UserModel(); + $userModel->edit($data, $id); + header('Location: /client/profile'); + } +}, 'post'); + + +Route::add('/client/project', function () { + check_login(); + $format = isset($_GET['format']) ? $_GET['format'] : 'json'; + $page = isset($_GET['page']) ? intval($_GET['page']) : 1; + $id = isset($_GET['id']) ? intval($_GET['id']) : 0; + $per_page = isset($_GET['size']) ? intval($_GET['size']) : 10; + $sort = isset($_GET['sort']) ? $_GET['sort'] : 'id'; + $direction = isset($_GET['direction']) ? $_GET['direction'] : 'ASC'; + // $relationship_num = isset($_GET['relationship_num']) ? $_GET['relationship_num'] : ''; + + $projectModel = new ProjectModel(); + + $data = [ + 'page_title' => 'Project', + ]; + + $where = [ + "user_id" => $_SESSION['user'] + ]; + + // if ($relationship_num != '') { + // $where['relationship_num'] = '"' . $relationship_num . '"'; + // } + + $result = $projectModel->get_paginated($page, $per_page, $where, 'id', 'DESC'); + // echo json_encode($result); + if ($result) { + if ($format == 'json') { + $data = array_merge($data, $result); + } + } + include_once __DIR__ . '/layout/header/Clientleft_sidebar.php'; + include_once __DIR__ . '/projectListing.php'; + include_once __DIR__ . '/layout/footer/Clientnone_footer.php'; +}, 'get'); + + +Route::add('/client/project/add', function () { + check_login(); + $error = false; + + $data = [ + 'page_title' => 'Project' + ]; + + include_once __DIR__ . '/layout/header/Clientleft_sidebar.php'; + include_once __DIR__ . '/projectAdd.php'; + include_once __DIR__ . '/layout/footer/Clientnone_footer.php'; +}, 'get'); + +Route::add('/client/project/edit/([0-9]+)', function ($id) { + check_login(); + $error = false; + $projectModel = new ProjectModel(); + $model = $projectModel->get($id); + + if (!$model) { + header('Location: /client/project'); + exit; + } + + $data = [ + 'page_title' => 'Project', + 'model' => $model + ]; + + include_once __DIR__ . '/layout/header/Clientleft_sidebar.php'; + include_once __DIR__ . '/projectEdit.php'; + include_once __DIR__ . '/layout/footer/Clientnone_footer.php'; +}, 'get'); + +Route::add('/client/project/add', function () { + check_login(); + $error = false; + + $data = [ + 'page_title' => 'Project' + ]; + + if (empty($_POST['project_name']) || empty($_POST['slot']) || empty($_POST['days']) || empty($_POST['score_threshold']) || empty($_POST['actual_score']) || empty($_POST['webhook']) || empty($_POST['calendar_id']) || empty($_POST['location'])) { + $error = true; + include_once __DIR__ . '/layout/header/Clientleft_sidebar.php'; + include_once __DIR__ . '/projectAdd.php'; + include_once __DIR__ . '/layout/footer/Clientnone_footer.php'; + } else { + // Collect form data + $project_name = $_POST['project_name']; + $slot = $_POST['slot']; + $days = $_POST['days']; + // $alert = $_POST['alert']; + $score_threshold = $_POST['score_threshold']; + $actual_score = $_POST['actual_score']; + $webhook = $_POST['webhook']; + // $webhook_payload = $_POST['webhook_payload']; + $calendar_id = $_POST['calendar_id']; + $location = $_POST['location']; + $current_date = date('Y-m-d H:i:s'); + + + $webhook_payload = array( + "project_name" => $project_name, + ); + $webhook_payload = json_encode($webhook_payload); + // echo $webhook_payload; + // exit; + // function create_calendar_id() + // { + // $dt = microtime(true) * 1000; // Get current time in milliseconds + // $uuid = preg_replace_callback('/[xy]/', function ($matches) use ($dt) { + // $r = ($dt + mt_rand() * 16) % 16 | 0; + // $dt = floor($dt / 16); + // return ($matches[0] == 'x' ? dechex($r) : (dechex($r & 0x3 | 0x8))); + // }, 'xxxxxxxxxx'); + + // return $uuid; + // } + + // function create_calendar_id() + // { + // $base = uniqid(); // Use uniqid as a base + // $uuid = preg_replace_callback('/[a-f0-9]/', function ($matches) { + // return dechex(mt_rand(0, 15)); + // }, $base); + + // return $uuid; + // } + // $config = MkdConfig::get_instance()->get_config(); + // $calendar = $config['domain-name'] . "/admin/calendar/"; + // $calendars = create_calendar_id(); + // Prepare data array + // $calendar_data = [ + // 'slot' => $slot, + // 'days' => $days, + // 'calendar' => $calendars, + // 'created_at' => $current_date + // ]; + + // $calendarModel = new CalendarModel(); + // $calendarModel->create($calendar_data); + // echo $test; + // exit; + // if ($score_threshold < $actual_score) { + // $alert = "Yes"; + // } else { + $alert = "Off"; + // } + $data = [ + 'project_name' => $project_name, + 'slot' => $slot, + 'days' => $days, + 'alert' => $alert, + 'score_threshold' => $score_threshold, + 'actual_score' => $actual_score, + 'webhook' => $webhook, + 'payload' => $webhook_payload, + 'calendar' => $calendar_id, + 'location' => $location, + 'created_at' => $current_date, + 'user_id' => $_SESSION["user"] + ]; + + + // Insert data into the database using LicenseModel + $projectModel = new ProjectModel(); + $projectModel->create($data); + echo 'Project Added'; + // header('Location: /admin/project'); + } +}, 'post'); + + +Route::add('/client/project/edit/([0-9]+)', function ($id) { + check_login(); + $error = false; + + $projectModel = new ProjectModel(); + $model = $projectModel->get($id); + + if (!$model) { + header('Location: /client/project'); + exit; + } + + $data = [ + 'page_title' => 'Project', + 'id' => $id + ]; + + if (empty($_POST['project_name']) || empty($_POST['slot']) || empty($_POST['days']) || empty($_POST['score_threshold']) || empty($_POST['actual_score']) || empty($_POST['webhook']) || empty($_POST['webhook_payload']) || empty($_POST['calendar_id']) || empty($_POST['location'])) { + $error = true; + include_once __DIR__ . '/layout/header/Clientleft_sidebar.php'; + include_once __DIR__ . '/projectEdit.php'; + include_once __DIR__ . '/layout/footer/Clientnone_footer.php'; + } else { + // Collect form data + $project_name = $_POST['project_name']; + $slot = $_POST['slot']; + $days = $_POST['days']; + // $alert = $_POST['alert']; + $score_threshold = $_POST['score_threshold']; + $actual_score = $_POST['actual_score']; + $webhook = $_POST['webhook']; + $webhook_payload = $_POST['webhook_payload']; + $calendar_id = $_POST['calendar_id']; + $location = $_POST['location']; + // $calendar_id = $_POST['calendar_id']; + $current_date = date('Y-m-d H:i:s'); + + + + + $data = [ + 'project_name' => $project_name, + 'slot' => $slot, + 'days' => $days, + // 'alert' => $alert, + 'score_threshold' => $score_threshold, + 'actual_score' => $actual_score, + 'webhook' => $webhook, + 'calendar' => $calendar_id, + 'location' => $location, + 'payload' => $webhook_payload, + ]; + + + + + // Insert data into the database using LicenseModel + $projectModel = new ProjectModel(); + $projectModel->edit($data, $id); + // header('Location: /admin/project'); + // echo 'done'; + } +}, 'post'); + +Route::add('/client/duplicate', function () { + check_login(); + $error = false; + + // $data = [ + // 'page_title' => 'Calendar' + // ]; + + + + // if (empty($_POST['project_name']) || empty($_POST['slot'])) { + // $error = true; + // include_once __DIR__ . '/layout/header/Adminleft_sidebar.php'; + // include_once __DIR__ . '/projectAdd.php'; + // include_once __DIR__ . '/layout/footer/Adminnone_footer.php'; + // } else { + // Collect form data + $id = $_POST['project_id']; + $calendar_id = $_POST['calendar_id']; + $current_date = date('Y-m-d H:i:s'); + + $projectModel = new ProjectModel(); + + $model = $projectModel->get($id); + // echo $model; + // exit; + + + + + // Use regular expression to check if the variable ends with a number within brackets + if (preg_match('/\((\d+)\)$/', $model->project_name, $matches)) { + // Extract the number and increment it + $number = $matches[1] + 1; + + // Replace the old number with the incremented number + $modifiedVariable = preg_replace('/\(\d+\)$/', "($number)", $model->project_name); + + // echo $modifiedVariable; + // Remove content within parentheses + $modifiedVariable2 = preg_replace('/\(\d+\)/', '', $model->project_name); + } else { + // If no number within brackets at the end, append "(1)" + $modifiedVariable2 = $model->project_name; + + // echo $modifiedVariable; + } + + + $model2 = $projectModel->get_like('project_name', $modifiedVariable2); + if (!empty($model2)) { + foreach ($model2 as $mod) { + // Use regular expression to check if the variable ends with a number within brackets + if (preg_match('/\((\d+)\)$/', $mod->project_name, $matches)) { + // Extract the number and increment it + $number = $matches[1] + 1; + + // Replace the old number with the incremented number + $modifiedVariable = preg_replace('/\(\d+\)$/', "($number)", $mod->project_name); + + // echo $modifiedVariable; + } else { + // If no number within brackets at the end, append "(1)" + $modifiedVariable = $mod->project_name . "(1)"; + + // echo $modifiedVariable; + } + // $modifiedVariable = $mod->project_name; + } + } + // echo json_encode($model2); + // exit; + $data = [ + 'project_name' => $modifiedVariable, + 'slot' => $model->slot, + 'days' => $model->days, + 'alert' => $model->alert, + 'score_threshold' => $model->score_threshold, + 'actual_score' => $model->actual_score, + 'webhook' => $model->webhook, + 'location' => $model->location, + 'payload' => $model->payload, + 'calendar' => $calendar_id, + 'created_at' => $current_date, + 'user_id' => $_SESSION["user"] + ]; + + + // Insert data into the database using LicenseModel + + $projectModel->create($data); + echo 'Project Duplicated'; + + // } +}, 'post'); + +Route::add('/client/project/delete/([0-9]+)', function ($id) { + check_login(); + $projectModel = new ProjectModel(); + $projectModel->real_delete($id); + header('Location: /client/project'); +}, 'get'); + + +Route::add('/client/project/list/multiselect', function () { + check_login(); + $error = false; + $projectModel = new ProjectModel(); + + + if (isset($_POST['delete'])) { + $ids = implode(', ', array_map('intval', $_POST['selected'])); + $projectModel->real_delete_by_fields([ + "id IN ($ids)" + ]); + header('Location: /client/project'); + + } + + if (isset($_POST['edit'])) { + $ids = implode(',', array_map('intval', $_POST['selected'])); + $data = [ + 'page_title' => 'Project', + 'ids' => "$ids" + ]; + + include_once __DIR__ . '/layout/header/Clientleft_sidebar.php'; + include_once __DIR__ . '/projectEditMulti.php'; + include_once __DIR__ . '/layout/footer/Clientnone_footer.php'; + exit; + } + + if (isset($_POST['multiedit'])) { + if (empty($_POST['project_name']) || empty($_POST['slot']) || empty($_POST['days']) || empty($_POST['score_threshold']) || empty($_POST['actual_score']) || empty($_POST['webhook']) || empty($_POST['webhook_payload']) || empty($_POST['calendar_id']) || empty($_POST['location'])) { + $error = true; + $ids = implode(',', array_map('intval', $_POST['selected'])); + $data = [ + 'page_title' => 'Project', + 'ids' => "$ids" + ]; + + include_once __DIR__ . '/layout/header/Clientleft_sidebar.php'; + include_once __DIR__ . '/projectEditMulti.php'; + include_once __DIR__ . '/layout/footer/Clientnone_footer.php'; + exit; + } else { + // Collect form data + $project_name = $_POST['project_name']; + $slot = $_POST['slot']; + $days = $_POST['days']; + // $alert = $_POST['alert']; + $score_threshold = $_POST['score_threshold']; + $actual_score = $_POST['actual_score']; + $webhook = $_POST['webhook']; + $webhook_payload = $_POST['webhook_payload']; + $calendar_id = $_POST['calendar_id']; + $location = $_POST['location']; + // $calendar_id = $_POST['calendar_id']; + $current_date = date('Y-m-d H:i:s'); + + + + + $data = [ + 'project_name' => $project_name, + 'slot' => $slot, + 'days' => $days, + // 'alert' => $alert, + 'score_threshold' => $score_threshold, + 'actual_score' => $actual_score, + 'webhook' => $webhook, + 'calendar' => $calendar_id, + 'location' => $location, + 'payload' => $webhook_payload, + ]; + + + $edit_ids = explode(",", $_POST['ids']); + + foreach($edit_ids as $id) { + $projectModel = new ProjectModel(); + $projectModel->edit($data, $id); + } + + header('Location: /client/project'); + exit; + } + } + +}, 'post'); + + + + +Route::add('/client/campaign', function () { + check_login(); + $format = isset($_GET['format']) ? $_GET['format'] : 'json'; + $page = isset($_GET['page']) ? intval($_GET['page']) : 1; + $id = isset($_GET['id']) ? intval($_GET['id']) : 0; + $per_page = isset($_GET['size']) ? intval($_GET['size']) : 10; + $sort = isset($_GET['sort']) ? $_GET['sort'] : 'id'; + $direction = isset($_GET['direction']) ? $_GET['direction'] : 'ASC'; + // $relationship_num = isset($_GET['relationship_num']) ? $_GET['relationship_num'] : ''; + + $campaignModel = new CampaignModel(); + + $data = [ + 'page_title' => 'Campaign', + ]; + + $where = [ + "user_id" => $_SESSION['user'] + ]; + + // if ($relationship_num != '') { + // $where['relationship_num'] = '"' . $relationship_num . '"'; + // } + + $result = $campaignModel->get_paginated($page, $per_page, $where, 'id', 'DESC'); + // echo json_encode($result); + if ($result) { + if ($format == 'json') { + $data = array_merge($data, $result); + } + } + include_once __DIR__ . '/layout/header/Clientleft_sidebar.php'; + include_once __DIR__ . '/campaignListing.php'; + include_once __DIR__ . '/layout/footer/Clientnone_footer.php'; +}, 'get'); + + +Route::add('/client/campaign/add', function () { + check_login(); + $error = false; + + $data = [ + 'page_title' => 'Campaign' + ]; + + include_once __DIR__ . '/layout/header/Clientleft_sidebar.php'; + include_once __DIR__ . '/campaignAdd.php'; + include_once __DIR__ . '/layout/footer/Clientnone_footer.php'; +}, 'get'); + +Route::add('/client/campaign/add', function () { + check_login(); + $error = false; + + $data = [ + 'page_title' => 'Campaign' + ]; + + if (empty($_POST['name']) || empty($_POST['file_id'])) { + $error = true; + include_once __DIR__ . '/layout/header/Clientleft_sidebar.php'; + include_once __DIR__ . '/campaignAdd.php'; + include_once __DIR__ . '/layout/footer/Clientnone_footer.php'; + } else { + // Collect form data + $name = $_POST['name']; + $file_id = $_POST['file_id']; + $current_date = date('Y-m-d H:i:s'); + + + // } + $data = [ + 'name' => $name, + 'file_id' => $file_id, + 'user_id' => $_SESSION["user"], + 'created_at' => $current_date, + ]; + + + // Insert data into the database using LicenseModel + $campaignModel = new CampaignModel(); + $campaignModel->create($data); + // echo 'Campaign Added'; + header('Location: /client/campaign'); + } +}, 'post'); + +Route::add('/client/campaign/view/([0-9]+)', function ($id) { + check_login(); + + $campaignModel = new CampaignModel(); + $campaign = $campaignModel->get($id); + + if (!$campaign) { + header('Location: /client/campaign'); + exit; + } + + $config = MkdConfig::get_instance()->get_config(); + $userModel = new UserModel(); + $user = $userModel->get($_SESSION['user']); + + if (!$user->drive_refresh_token) { + header('Location: /client/campaign?error=drive_not_connected'); + exit; + } + + $oauth = new \Lib\Google\GoogleOAuth2( + $config['google_client_id'], + $config['google_client_secret'], + $config['google_redirect_uri'] + ); + + $oauth->setRefreshToken($user->drive_refresh_token); + $oauth->refreshAccessToken(); + + $drive = new \Lib\Google\GoogleDrive($oauth); + + try { + // Download as CSV + $content = $drive->downloadFile( + $campaign->file_id, + 'text/csv' + ); + + // Convert TSV/CSV to array of objects + $rows = array_map('str_getcsv', explode("\n", $content)); + $headers = array_map(function($header) { + return str_replace(' ', '_', trim(strtolower($header))); + }, array_shift($rows)); + + $campaignData = array_map(function($row) use ($headers) { + return array_combine($headers, $row); + }, array_filter($rows)); + + $data = [ + 'page_title' => 'View Campaign', + 'campaign' => $campaign, + 'campaign_data' => $campaignData + ]; + + include_once __DIR__ . '/layout/header/Clientleft_sidebar.php'; + include_once __DIR__ . '/campaignView.php'; + include_once __DIR__ . '/layout/footer/Clientnone_footer.php'; + + } catch (\Exception $e) { + print_r($e); + exit; + header('Location: /client/campaign?error=file_load_failed'); + exit; + } +}, 'get'); + +// Add route to handle filter updates via AJAX +Route::add('/client/campaign/filter', function() { + check_login(); + + if (!isset($_POST['campaign_id'])) { + http_response_code(400); + echo json_encode(['error' => 'Missing campaign ID']); + exit; + } + + $campaignModel = new CampaignModel(); + $campaign = $campaignModel->get($_POST['campaign_id']); + + if (!$campaign) { + http_response_code(404); + echo json_encode(['error' => 'Campaign not found']); + exit; + } + + // Get the current filters + $filters = [ + 'campaign_name' => $_POST['campaign_name'] ?? null, + 'ad_set_name' => $_POST['ad_set_name'] ?? null, + 'ad_name' => $_POST['ad_name'] ?? null + ]; + + // Get filtered data + $filteredData = $campaignModel->getFilteredData($campaign, $filters); + + echo json_encode([ + 'data' => $filteredData + ]); + +}, 'post'); + +// Add edit route +Route::add('/client/campaign/edit/([0-9]+)', function ($id) { + check_login(); + + $campaignModel = new CampaignModel(); + $campaign = $campaignModel->get($id); + + if (!$campaign) { + header('Location: /client/campaign'); + exit; + } + + $data = [ + 'page_title' => 'Edit Campaign', + 'campaign' => $campaign + ]; + + include_once __DIR__ . '/layout/header/Clientleft_sidebar.php'; + include_once __DIR__ . '/campaignEdit.php'; + include_once __DIR__ . '/layout/footer/Clientnone_footer.php'; +}, 'get'); + +Route::add('/client/campaign/edit/([0-9]+)', function ($id) { + check_login(); + + if (empty($_POST['name']) || empty($_POST['file_id'])) { + header('Location: /client/campaign/edit/' . $id); + exit; + } + + $campaignModel = new CampaignModel(); + $campaign = $campaignModel->get($id); + + if (!$campaign) { + header('Location: /client/campaign'); + exit; + } + + $data = [ + 'name' => $_POST['name'], + 'file_id' => $_POST['file_id'] + ]; + + $campaignModel->edit($data, $id); + header('Location: /client/campaign'); +}, 'post'); + +// Add delete route +Route::add('/client/campaign/delete/([0-9]+)', function ($id) { + check_login(); + + $campaignModel = new CampaignModel(); + $campaignModel->real_delete($id); + + header('Location: /client/campaign'); +}, 'get'); + +Route::add('/drive/disconnect', function() { + check_login(); + + $userModel = new UserModel(); + $userModel->edit([ + 'drive_refresh_token' => null, + 'drive_access_token' => null + ], $_SESSION['user']); + + header('Location: /' . $_SESSION['role'] . '/campaign'); + exit; +}, 'post'); diff --git a/core.php b/core.php new file mode 100644 index 0000000..c628f96 --- /dev/null +++ b/core.php @@ -0,0 +1,124 @@ + $expression, + 'function' => $function, + 'method' => $method + ]); + } + + public static function pathNotFound($function) + { + self::$pathNotFound = $function; + } + + public static function methodNotAllowed($function) + { + self::$methodNotAllowed = $function; + } + + public static function run($basepath = '/') + { + + // Parse current url + $parsed_url = parse_url($_SERVER['REQUEST_URI']); //Parse Uri + if (isset($parsed_url['path'])) + { + $path = $parsed_url['path']; + } + else + { + $path = '/'; + } + + // Get current request method + $method = $_SERVER['REQUEST_METHOD']; + + $path_match_found = false; + + $route_match_found = false; + + foreach (self::$routes as $route) + { + + // If the method matches check the path + // Add basepath to matching string + if ($basepath != '' && $basepath != '/') + { + $route['expression'] = '(' . $basepath . ')' . $route['expression']; + } + + // Add 'find string start' automatically + $route['expression'] = '^' . $route['expression']; + + // Add 'find string end' automatically + $route['expression'] = $route['expression'] . '$'; + + // echo $route['expression'].'
    '; + // Check path match + if (preg_match('#' . $route['expression'] . '#', $path, $matches)) + { + + $path_match_found = true; + + // Check method match + if (strtolower($method) == strtolower($route['method'])) + { + + array_shift($matches); // Always remove first element. This contains the whole string + if ($basepath != '' && $basepath != '/') + { + array_shift($matches); // Remove basepath + + } + + call_user_func_array($route['function'], $matches); + + $route_match_found = true; + + // Do not check other routes + break; + } + } + } + + // No matching route was found + if (!$route_match_found) + { + + // But a matching path exists + if ($path_match_found) + { + header("HTTP/1.0 405 Method Not Allowed"); + if (self::$methodNotAllowed) + { + call_user_func_array(self::$methodNotAllowed, Array( + $path, + $method + )); + } + } + else + { + header("HTTP/1.0 404 Not Found"); + if (self::$pathNotFound) + { + call_user_func_array(self::$pathNotFound, Array( + $path + )); + } + } + + } + + } + +} diff --git a/cronjob.php b/cronjob.php new file mode 100644 index 0000000..bab75e9 --- /dev/null +++ b/cronjob.php @@ -0,0 +1,605 @@ + +get_all(); + +// Log the start of the script +error_log('Cron job started: ' . date('Y-m-d H:i:s')); +// Function to convert weeks or months to days +function convertToDays($value, $unit) +{ + switch ($unit) { + case 'weeks': + return $value * 7; + case 'months': + // Assuming a month is considered as 30 days for simplicity + return $value * 30; + case 'hours': + return 0; // Set to 0 days if the unit is hours + default: + return $value; + } +} + +function extractRanges($slotStart, $slotEnd, $serviceStart, $serviceEnd, $interval, $date) +{ + // Convert time strings to DateTime objects for easier comparison + $slotStartTime = new DateTime($date . ' ' . $slotStart); + $slotEndTime = new DateTime($date . ' ' . $slotEnd); + $serviceStartTime = new DateTime($date . ' ' . $serviceStart); + $serviceEndTime = new DateTime($date . ' ' . $serviceEnd); + + if ($slotEndTime < $slotStartTime) { + $slotEndTime->modify('+1 day'); + } + if ($serviceEndTime < $serviceStartTime) { + $serviceEndTime->modify('+1 day'); + } + + // Check if service time falls within the slot range + if ($serviceStartTime >= $slotStartTime && $serviceStartTime < $slotEndTime) { + // if ($serviceStartTime >= $slotStartTime && $serviceEndTime <= $slotEndTime) { + $ranges = array(); + + // Calculate ranges based on the interval + $currentRangeStart = $serviceStartTime; + if ($currentRangeStart < $slotEndTime) { + array_push($ranges, $currentRangeStart->format('H:i:s')); + } + while ($currentRangeStart < $serviceEndTime) { + $currentRangeEnd = clone $currentRangeStart; + $currentRangeEnd->add(new DateInterval("PT" . $interval . "M")); + + // Check if the calculated range end is within the slot range + if ($currentRangeEnd < $slotEndTime) { + // array_push( + // $ranges, + // $currentRangeStart->format('H:i:s') + // ); + array_push( + $ranges, + $currentRangeEnd->format('H:i:s') + ); + } else { + // Add the end time within the slot range + // array_push($ranges, $slotEndTime->format('H:i:s')); + // Break the loop if the range end exceeds the slot end time + break; + } + + $currentRangeStart = $currentRangeEnd; + } + + return $ranges; + } elseif ($serviceStartTime < $slotStartTime && $serviceEndTime <= $slotEndTime) { + // if ($serviceStartTime >= $slotStartTime && $serviceEndTime <= $slotEndTime) { + $ranges = array(); + + // Calculate ranges based on the interval + $currentRangeStart = $slotStartTime; + if ($currentRangeStart < $serviceEndTime) { + array_push($ranges, $currentRangeStart->format('H:i:s')); + } + // array_push($ranges, $currentRangeStart->format('H:i:s')); + while ($currentRangeStart < $serviceEndTime) { + $currentRangeEnd = clone $currentRangeStart; + $currentRangeEnd->add(new DateInterval("PT" . $interval . "M")); + + // Check if the calculated range end is within the slot range + if ($currentRangeEnd < $slotEndTime) { + // array_push( + // $ranges, + // $currentRangeStart->format('H:i:s') + // ); + array_push( + $ranges, + $currentRangeEnd->format('H:i:s') + ); + } else { + // Add the end time within the slot range + // array_push($ranges, $slotEndTime->format('H:i:s')); + // Break the loop if the range end exceeds the slot end time + break; + } + + $currentRangeStart = $currentRangeEnd; + } + + return $ranges; + } elseif ($serviceStartTime < $slotStartTime && $serviceEndTime > $slotEndTime) { + // if ($serviceStartTime >= $slotStartTime && $serviceEndTime <= $slotEndTime) { + $ranges = array(); + + // Calculate ranges based on the interval + $currentRangeStart = $slotStartTime; + if ($currentRangeStart < $serviceEndTime) { + array_push($ranges, $currentRangeStart->format('H:i:s')); + } + // array_push($ranges, $currentRangeStart->format('H:i:s')); + while ($currentRangeStart < $serviceEndTime) { + $currentRangeEnd = clone $currentRangeStart; + $currentRangeEnd->add(new DateInterval("PT" . $interval . "M")); + + // Check if the calculated range end is within the slot range + if ($currentRangeEnd < $slotEndTime) { + // array_push( + // $ranges, + // $currentRangeStart->format('H:i:s') + // ); + array_push( + $ranges, + $currentRangeEnd->format('H:i:s') + ); + } else { + // Add the end time within the slot range + // array_push($ranges, $slotEndTime->format('H:i:s')); + // Break the loop if the range end exceeds the slot end time + break; + } + + $currentRangeStart = $currentRangeEnd; + } + + return $ranges; + } elseif ($serviceStartTime >= $slotStartTime && $serviceEndTime <= $slotEndTime) { + $ranges = array(); + + // Calculate ranges based on the interval + $currentRangeStart = $serviceStartTime; + if ($currentRangeStart < $serviceEndTime) { + array_push($ranges, $currentRangeStart->format('H:i:s')); + } + // array_push($ranges, $currentRangeStart->format('H:i:s')); + while ($currentRangeStart <= $serviceEndTime) { + $currentRangeEnd = clone $currentRangeStart; + $currentRangeEnd->add(new DateInterval("PT" . $interval . "M")); + + // Check if the calculated range end is within the slot range + if ($currentRangeEnd <= $slotEndTime) { + // array_push( + // $ranges, + // $currentRangeStart->format('H:i:s') + // ); + array_push( + $ranges, + $currentRangeEnd->format('H:i:s') + ); + } else { + // Add the end time within the slot range + // array_push($ranges, $slotEndTime->format('H:i:s')); + // Break the loop if the range end exceeds the slot end time + break; + } + + $currentRangeStart = $currentRangeEnd; + } + + return $ranges; + } else { + // Service time is outside the slot range + return false; + } +} +function calculateTotalPoints($daysToCheck, $slot, $service, $eventTiming, $schedule, $project, $ser_id) +{ + + + + // Convert allowBookingFor to days if the unit is months or weeks + if ($schedule['allowBookingForUnit'] === 'months' || $schedule['allowBookingForUnit'] === 'weeks') { + $schedule['allowBookingFor'] = convertToDays($schedule['allowBookingFor'], $schedule['allowBookingForUnit']); + $schedule['allowBookingForUnit'] = 'days'; + } + if ($schedule['allowBookingAfterUnit'] === 'months' || $schedule['allowBookingAfterUnit'] === 'weeks' || $schedule['allowBookingAfterUnit'] === 'hours') { + $schedule['allowBookingAfter'] = convertToDays($schedule['allowBookingAfter'], $schedule['allowBookingAfterUnit']); + $schedule['allowBookingAfterUnit'] = 'days'; + } + $currentDate = new DateTime(); + $currentDayIndex = $currentDate->format('w'); // 0 for Sunday, 1 for Monday, ..., 6 for Saturday + + // Get the next two days to check + // $daysToCheckIndices = []; + // for ($i = 0; $i < $daysToCheck; $i++) { + // $nextDayIndex = ($currentDayIndex + $i + 1) % 7; + // $daysToCheckIndices[] = $nextDayIndex; + // } + + $daysToCheckData = []; + if (empty($schedule['allowBookingFor'])) { + $counter = $daysToCheck; + } else { + $counter = (int)$schedule['allowBookingFor']; + } + if (empty($schedule['allowBookingAfter'])) { + $skip = 0; + } else { + $skip = (int)$schedule['allowBookingAfter']; + } + // error_log("counter" . json_encode($counter)); + // error_log("skip" . json_encode($skip)); + + for ($i = $skip; $i < $counter; $i++) { + $nextDayIndex = ($currentDayIndex + $i) % 7; + $nextDate = clone $currentDate; + $nextDate->modify("+$i day"); + + $daysToCheckData[] = [ + 'index' => $nextDayIndex, + 'date' => $nextDate->format('Y-m-d'), + ]; + } + // error_log("daysToCheckIndices" . json_encode($daysToCheckIndices)); + // error_log("daysToCheckIndices" . json_encode($daysToCheckData)); + + $totalPoints = 0; + + // Loop through the selected days and calculate total points + $dayNames = ['sunday', 'monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday']; + // foreach ($daysToCheckIndices as $dayIndex) { + foreach ($daysToCheckData as $dayData) { + // $dayName = $dayNames[$dayIndex]; + // error_log("day" . $dayName); + + + $dayIndex = $dayData['index']; + $dayName = $dayNames[$dayIndex]; + $date = $dayData['date']; + + error_log("day: $dayName, date: $date"); + + if (isset($slot->$dayName)) { + if (!array_key_exists($dayName, $service)) { + continue; // Day not found in the service schedule + } + + + foreach ($service[$dayName] as $ser) { + + $serviceStart = strtotime(sprintf("%02d:%02d", $ser["openHour"], $ser["openMinute"])); + $serviceEnd = strtotime(sprintf("%02d:%02d", $ser["closeHour"], $ser["closeMinute"])); + + // Comparing service hours with slot hours + // if ($serviceStart >= $slotStart && $serviceEnd <= $slotEnd) { + // error_log("id" . $project->id); + // error_log("slotStart" . date("H:i:s", $slotStart)); + + $start = strtotime($dayData['date'] . ' ' . sprintf("%02d:%02d", $ser["openHour"], $ser["openMinute"])); + $end = strtotime($dayData['date'] . ' ' . sprintf("%02d:%02d", $ser["closeHour"], $ser["closeMinute"])); + $start = $start * 1000; + $end = $end * 1000; + $apikey2 = $project->location; + $curl = curl_init(); + + curl_setopt_array($curl, [ + CURLOPT_URL => "https://rest.gohighlevel.com/v1/appointments/slots?calendarId=$ser_id&startDate=$start&endDate=$end", + CURLOPT_RETURNTRANSFER => true, + CURLOPT_ENCODING => "", + CURLOPT_MAXREDIRS => 10, + CURLOPT_TIMEOUT => 30, + CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1, + CURLOPT_CUSTOMREQUEST => "GET", + CURLOPT_HTTPHEADER => [ + "Accept: application/json", + "Authorization: Bearer " . $apikey2, + "Version: 2021-04-15" + ], + ]); + + $response = curl_exec($curl); + $err = curl_error($curl); + + + curl_close($curl); + + if ($err) { + echo "cURL Error #:" . $err; + } else { + + // error_log($response); + $data = json_decode($response, true); + $stepSize = $eventTiming["slotInterval"]; + error_log("stepSize" . json_encode($stepSize)); + + $rslt = []; + if (!empty($data[$dayData['date']]["slots"])) { + + foreach ($data[$dayData['date']]["slots"] as $time) { + $dateTime = new DateTime($time); + // while ($dateTime <= new DateTime($data[$dayData['date']]["slots"][1])) { + $rslt[] = $dateTime->format('H:i:s'); + // $dateTime->modify("+$stepSize minutes"); + // } + } + foreach ($slot->$dayName as $timeSlot) { + + // Convert the day to a DateTime object for comparison + // $currentDay = new DateTime($dayName); + + // Check if the day is after or equal to today. + // if ($currentDay > $currentDate) { + + + // error_log("currentDay" . $currentDay->format('Y-m-d H:i:s')); + + // foreach ($timeSlots as $timeSlot) { + $slotStart = strtotime($timeSlot->from); + $slotEnd = strtotime($timeSlot->to); + + $range = extractRanges(date("H:i:s", $slotStart), date("H:i:s", $slotEnd), date("H:i:s", $serviceStart), date("H:i:s", $serviceEnd), $stepSize, $dayData['date']); + + // $intervals = generateTimeIntervals(date("H:i:s", $serviceStart), date("H:i:s", $serviceEnd), $eventTiming["slotInterval"], $dayData['date']); + // $intervals2 = generateTimeIntervals(date("H:i:s", $slotStart), date("H:i:s", $slotEnd), $eventTiming["slotInterval"], $dayData['date']); + + error_log("slotStart " . date("H:i:s", $slotStart)); + + error_log("slotEnd " . date("H:i:s", $slotEnd)); + + error_log("serviceStart " . date("H:i:s", $serviceStart)); + error_log("serviceEnd " . date("H:i:s", $serviceEnd)); + // error_log("intervals " . json_encode($intervals)); + // error_log("intervals2 " . json_encode($intervals2)); + // error_log("slots " . json_encode($rslt)); + error_log("range " . json_encode($range)); + + // $intersections = array_intersect($intervals, $intervals2); + if (!$range) { + continue; + } + $intersection = array_intersect($rslt, $range); + if (empty($intersection)) { + continue; + } + // error_log("intersections " . json_encode($intersections)); + error_log("intersection " . json_encode($intersection)); + + foreach ($intersection as $intersect) { + $totalPoints += (int)$timeSlot->point; + } + } + error_log($totalPoints); + } + // error_log("start " . $start * 1000); + // error_log("slotEnd" . date("Y-m-d H:i:s", $start)); + // error_log("end " . $end * 1000); + // error_log("slotStart " . $slotStart * 1000); + // // error_log("slotEnd" . date("H:i:s", $slotEnd)); + // error_log("slotEnd " . $slotEnd * 1000); + // // error_log("serviceStart" . date("H:i:s", $serviceStart)); + // error_log("serviceStart " . $serviceStart * 1000); + // // error_log("serviceEnd" . date("H:i:s", $serviceEnd)); + // error_log("serviceEnd " . $serviceEnd * 1000); + // foreach ($intersections as $intersection) { + // $totalPoints += (int)$timeSlot->point; + // } + // } + } + // } + // } + + // $totalPoints += intval($timeSlot->point) ?: 0; + } + } + } + + return $totalPoints; +} + +function generateTimeIntervals($startTime, $endTime, $intervalMinutes, $date) +{ + // $start = new DateTime($startTime); + // $end = new DateTime($endTime); + // $start = $date . ' ' . date("H:i:s", $startTime); + // $end = $date . ' ' . date("H:i:s", $endTime); + $start = new DateTime($date . ' ' . $startTime); + $end = new DateTime($date . ' ' . $endTime); + + // error_log("start " . $start); + // error_log("end " . $end); + + if ($end < $start) { + $end->modify('+1 day'); + } + $intervalObj = new DateInterval("PT{$intervalMinutes}M"); // PT stands for 'Period Time' + // Adjust end time to include it in the intervals + // $end->add($intervalObj); + $period = new DatePeriod($start, $intervalObj, $end); + + $intervals = array(); + foreach ($period as $dt) { + $intervals[] = $dt->format('H:i:s'); + } + + return $intervals; +} + +function checkServiceInSlot($service, $slot, $project, $eventTiming, $schedule, $ser_id) +{ + $slotpoint = 0; + // Get the current date + $currentDate = new DateTime('now'); + error_log("currentDate" . $currentDate->format('Y-m-d H:i:s')); + + + + if ($project->alert == 'On') { + $slotpoint = calculateTotalPoints((int)$project->days, $slot, $service, $eventTiming, $schedule, $project, $ser_id); + + if ($slotpoint < (int)$project->score_threshold && $slotpoint != 0) { + // Retrieve webhook URL and payload + $webhookUrl = $project->webhook; + // $webhookUrl = "https://hook.eu1.make.com/tcdowbyvjswiw6xnhxu8derehhyczfb4"; + // $webhookUrl = "https://hook.eu1.make.com/otaauwkih0ojxa3bezs4gdg6dr8w7bpd"; + if (filter_var($webhookUrl, FILTER_VALIDATE_URL)) { + + // echo json_validate($project->payload); + $jsonData = json_decode($project->payload); + // echo json_encode($project->payload); + // echo gettype($project->payload); + // echo gettype($jsonData); + // error_log(gettype($jsonData)); + + + // Check if decoding was successful + // if (json_last_error() !== JSON_ERROR_NONE) { + // echo json_last_error_msg(); + // error_log('Error decoding JSON for project with ID ' . $project->id); + // continue; + // } + + // Set up cURL options + $curlOptions = [ + CURLOPT_URL => $webhookUrl, + CURLOPT_POST => 1, + CURLOPT_POSTFIELDS => json_encode($jsonData), // Send encoded JSON + CURLOPT_HTTPHEADER => ['Content-Type: application/json'], + CURLOPT_RETURNTRANSFER => true, + ]; + + // Initialize cURL session + $ch = curl_init(); + + // Set cURL options + curl_setopt_array($ch, $curlOptions); + + // Execute cURL session and get the result + $response = curl_exec($ch); + + // Check for cURL errors + if (curl_errno($ch)) { + error_log('Curl error for project with ID ' . $project->id . ': ' . curl_error($ch)); + } else { + // Log the webhook response + $data = [ + 'actual_score' => $slotpoint, + ]; + + + + + // Insert data into the database using LicenseModel + $projectModel = new ProjectModel(); + $projectModel->edit($data, $project->id); + error_log('Webhook response for project with ID ' . $project->id . ': ' . $response); + } + + // Close cURL session + curl_close($ch); + } else { + error_log('Webhook for project with ID ' . $project->id . ' is not a URL '); + } + } else { + $data = [ + 'actual_score' => $slotpoint, + ]; + + + + + // Insert data into the database using LicenseModel + $projectModel = new ProjectModel(); + $projectModel->edit($data, $project->id); + error_log("Point is higher than mininum point set"); + } + } else { + // Log that no alert is set for the project + error_log('No alert set for project with ID ' . $project->id); + } +} + + +// $config = MkdConfig::get_instance()->get_config(); +// $apikey = $config['gohighlevel_key']; +// $cid = $_POST['calendar_id']; +// $pid = (int)$_POST['project_id']; + + + +// Iterate through projects +foreach ($projects as $project) { + if (!empty($project->location)) { + $apikey = $project->location; + + $curl = curl_init(); + + curl_setopt_array($curl, [ + CURLOPT_URL => "https://rest.gohighlevel.com/v1/calendars/services", + CURLOPT_RETURNTRANSFER => true, + CURLOPT_ENCODING => "", + CURLOPT_MAXREDIRS => 10, + CURLOPT_TIMEOUT => 30, + CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1, + CURLOPT_CUSTOMREQUEST => "GET", + CURLOPT_HTTPHEADER => [ + "Accept: application/json", + "Authorization: Bearer " . $apikey, + "Version: 2021-04-15" + ], + ]); + + $response = curl_exec($curl); + $err = curl_error($curl); + // error_log($response); + + curl_close($curl); + + if ($err) { + echo "cURL Error #:" . $err; + } else { + + // Check if the project has an alert set to 'Yes' + $slots = json_decode($project->slot); + + // error_log($slotpoint); + // error_log((int)$project->score_threshold); + error_log(' '); + // exit; + + + + $cid = $project->calendar; + // Decode JSON string to PHP array + $data = json_decode($response, true); + // Search for the service with the specified ID + $searchedService = null; + foreach ($data['services'] as $service) { + if ($service['id'] === $cid) { + $searchedService = $service; + break; + } + } + + // Output the result + if ($searchedService !== null) { + + // echo json_encode($searchedService["availability"]["officeHours"]); + + + $slots = json_decode($project->slot); + + // echo json_encode($slots); + + + + + echo checkServiceInSlot($searchedService["availability"]["officeHours"], $slots, $project, $searchedService["availability"]["eventTiming"], $searchedService["availability"]["schedule"], $searchedService["id"]); + } else { + error_log("Service with ID '" . $cid . "' not found."); + } + } + } else { + error_log('Empty location api key'); + } + // Log the end of the script +} +error_log('Cron job completed: ' . date('Y-m-d H:i:s')); diff --git a/db.sql b/db.sql new file mode 100644 index 0000000..0d1e85f --- /dev/null +++ b/db.sql @@ -0,0 +1,137 @@ +-- Adminer 4.8.1 MySQL 8.0.39-0ubuntu0.22.04.1 dump + +SET NAMES utf8; +SET time_zone = '+00:00'; +SET foreign_key_checks = 0; +SET sql_mode = 'NO_AUTO_VALUE_ON_ZERO'; + +DROP TABLE IF EXISTS `accesslog`; +CREATE TABLE `accesslog` ( + `id` int NOT NULL AUTO_INCREMENT, + `relationship_num` varchar(255) CHARACTER SET utf8mb3 COLLATE utf8mb3_unicode_ci NOT NULL, + `ip` varchar(255) CHARACTER SET utf8mb3 COLLATE utf8mb3_unicode_ci NOT NULL, + `created_at` datetime NOT NULL, + `updated_at` varchar(191) CHARACTER SET utf8mb3 COLLATE utf8mb3_unicode_ci DEFAULT NULL, + PRIMARY KEY (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_unicode_ci; + + +SET NAMES utf8mb4; + +DROP TABLE IF EXISTS `calendar`; +CREATE TABLE `calendar` ( + `id` int unsigned NOT NULL AUTO_INCREMENT, + `slot` varchar(191) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL, + `days` int unsigned DEFAULT NULL, + `calendar` varchar(191) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL, + `created_at` datetime DEFAULT NULL, + `updated_at` datetime DEFAULT NULL, + PRIMARY KEY (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci; + + +DROP TABLE IF EXISTS `campaign`; +CREATE TABLE `campaign` ( + `id` int NOT NULL AUTO_INCREMENT, + `name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL, + `file_id` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL, + `data` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci, + `user_id` int unsigned NOT NULL, + `created_at` date DEFAULT NULL, + `updated_at` datetime DEFAULT NULL, + PRIMARY KEY (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci; + + +DROP TABLE IF EXISTS `license`; +CREATE TABLE `license` ( + `id` int NOT NULL AUTO_INCREMENT, + `relationship_num` varchar(255) CHARACTER SET utf8mb3 COLLATE utf8mb3_unicode_ci NOT NULL, + `email` varchar(255) CHARACTER SET utf8mb3 COLLATE utf8mb3_unicode_ci NOT NULL, + `apikey` varchar(255) CHARACTER SET utf8mb3 COLLATE utf8mb3_unicode_ci NOT NULL, + `ip` varchar(255) CHARACTER SET utf8mb3 COLLATE utf8mb3_unicode_ci NOT NULL, + `status` varchar(255) CHARACTER SET utf8mb3 COLLATE utf8mb3_unicode_ci NOT NULL, + `created_at` date NOT NULL, + `updated_at` datetime NOT NULL, + PRIMARY KEY (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_unicode_ci; + + +DROP TABLE IF EXISTS `location`; +CREATE TABLE `location` ( + `id` int unsigned NOT NULL AUTO_INCREMENT, + `user_id` int unsigned DEFAULT NULL, + `name` varchar(191) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `apikey` text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci, + `location_id` text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci, + `webhook` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci, + `created_at` varchar(191) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `updated_at` varchar(191) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL, + PRIMARY KEY (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + + +DROP TABLE IF EXISTS `project`; +CREATE TABLE `project` ( + `id` int NOT NULL AUTO_INCREMENT, + `project_name` varchar(191) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL, + `user_id` int DEFAULT NULL, + `slot` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci, + `days` int DEFAULT NULL, + `alert` varchar(191) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL, + `score_threshold` int DEFAULT NULL, + `actual_score` int DEFAULT NULL, + `webhook` varchar(191) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL, + `calendar` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL, + `created_at` datetime DEFAULT NULL, + `updated_at` datetime DEFAULT NULL, + `payload` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL, + `location` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci, + `access_token` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci, + `refresh_token` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci, + `token_expiry` int DEFAULT NULL, + PRIMARY KEY (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci; + + +DROP TABLE IF EXISTS `report`; +CREATE TABLE `report` ( + `id` int unsigned NOT NULL AUTO_INCREMENT, + `project` varchar(191) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `location_id` int unsigned DEFAULT NULL, + `type` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `report` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci, + `new_lead` int unsigned DEFAULT NULL, + `outbound_dial` int unsigned DEFAULT NULL, + `pickup` int unsigned DEFAULT NULL, + `conversation` int unsigned DEFAULT NULL, + `booked_appointment` int unsigned DEFAULT NULL, + `callback_request` int unsigned DEFAULT NULL, + `created_at` datetime DEFAULT NULL, + `status` int DEFAULT '0', + `updated_at` datetime DEFAULT NULL, + `date` date DEFAULT NULL, + `webhook_sent` int DEFAULT '0', + PRIMARY KEY (`id`), + KEY `location_id` (`location_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + + +DROP TABLE IF EXISTS `user`; +CREATE TABLE `user` ( + `id` int unsigned NOT NULL AUTO_INCREMENT, + `email` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL, + `password` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL, + `status` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL, + `role` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT 'admin', + `company` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT 'Team Follow Up', + `drive_access_token` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci, + `drive_refresh_token` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci, + `created_at` date DEFAULT NULL, + `updated_at` datetime DEFAULT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `email` (`email`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci; + + +-- 2025-02-04 21:37:09 \ No newline at end of file diff --git a/get_all_v1_api_images.php b/get_all_v1_api_images.php new file mode 100644 index 0000000..5612d0b --- /dev/null +++ b/get_all_v1_api_images.php @@ -0,0 +1,67 @@ + 'integer', + 'url' => 'string', + 'user_id' => 'integer', + 'caption' => 'string' + ]; + + $allow_fields = ['id', 'url', 'user_id', 'caption']; + $allow_column_fields = ['ID', 'URL', 'User ID', 'Caption']; + $format = isset($_GET['format']) ? $_GET['format'] : 'json'; + $page = isset($_GET['page']) ? intval($_GET['page']) : 1; + $id = isset($_GET['id']) ? intval($_GET['id']) : 0; + $per_page = isset($_GET['size']) ? intval($_GET['size']) : 25; + $sort = isset($_GET['sort']) ? $_GET['sort'] : 'id'; + $direction = isset($_GET['direction']) ? $_GET['direction'] : 'ASC'; + //TODO: default sort/direction from cms builder + + $query_service = new QueryService(); + $format_service = new FormatService(); + $custom_where = $query_service->create_where( + $type_array, + Flight::request()->query['field'] ?? [], + Flight::request()->query['operator'] ?? [], + Flight::request()->query['value'] ?? []); + + $result = $model->get_paginated($page, $per_page, $custom_where, $sort, $direction); + // $result = $model->get_cursor_paginated($page, $per_page, $custom_where, $sort, $direction, $id); + + if ($result) + { + $result['code'] = 200; + $result['error'] = false; + $result['query'] = $custom_where; + + if ($format == 'json') + { + $result['data'] = $format_service->to_json($result['data'], $model->get_mapping(), $allow_column_fields, $allow_fields); + echo json_encode($result); + exit; + } + + if ($format == 'csv') + { + header('Content-Type: text/csv'); + header('Content-Disposition: attachment; filename="export.csv"'); + + echo implode("\n", $format_service->to_csv($result['list'], $model->get_mapping(), $allow_column_fields), $allow_fields); + exit(); + } + } + else + { + echo json_encode([ + 'code' => 409, + 'error' => false + ]); + http_response_code(409); + exit; + } +}); \ No newline at end of file diff --git a/get_v1_api_image_get.php b/get_v1_api_image_get.php new file mode 100644 index 0000000..91d2067 --- /dev/null +++ b/get_v1_api_image_get.php @@ -0,0 +1,38 @@ +get($id); + $output = []; + if ($result) + { + $allow_fields = ['id', 'url', 'user_id', 'caption']; + + foreach ($result as $key => &$value) + { + if (in_array($key, $allow_fields)) + { + $output[$key] = $value; + } + } + + echo json_encode([ + 'code' => 200, + 'error' => false, + 'model' => $output + ]); + http_response_code(200); + exit; + } + else + { + echo json_encode([ + 'code' => 404, + 'error' => true + ]); + http_response_code(404); + exit; + } +}); \ No newline at end of file diff --git a/index.php b/index.php new file mode 100644 index 0000000..4ab8ebb --- /dev/null +++ b/index.php @@ -0,0 +1,1930 @@ + true, 'message' => 'Invalid API key']); + return; + } + $apikey = $_GET['apikey']; + + // Check if relationship_num is set in the GET parameters and validate it + if (!isset($_GET['relationship_num']) || empty($_GET['relationship_num'])) { + http_response_code(403); + echo json_encode(['error' => true, 'message' => 'Invalid relationship_num']); + return; + } + + $relationship_num = $_GET['relationship_num']; + + // Get the user IP address + $user_ip = $_SERVER['REMOTE_ADDR']; + + // Instantiate the LicenseModel + $licenseModel = new LicenseModel(); + + // Call the get_by_fields($where) function on the LicenseModel + $where = ['apikey' => $apikey]; + + $licenseModel = new LicenseModel(); + $license = $licenseModel->get_one_by_fields($where); + + // If the 'apikey' in the license array does not match the provided $apikey, print a JSON error with HTTP code 403 + if ($license->apikey !== $apikey) { + http_response_code(403); + echo json_encode(['error' => true, 'message' => 'Invalid API key']); + exit; + } + + if ($license->status !== 'active') { + http_response_code(401); + echo json_encode(['error' => true, 'message' => 'Suspended']); + exit; + } + + // If the 'relationship_num' in the license array does not match the provided $relationship_num, print a JSON error with HTTP code 403 + if ($license->relationship_num !== $relationship_num) { + http_response_code(403); + echo json_encode(['error' => true, 'message' => 'Invalid relationship number']); + exit; + } + + // Instantiate the AccesslogModel + $accesslogModel = new AccesslogModel(); + + // Prepare the data array for the create() function + $data = [ + 'relationship_num' => $relationship_num, + 'ip' => $user_ip, + 'created_at' => date('Y-m-d H:i:s') // Current date and time + ]; + + // Call the create($data) function on the AccesslogModel + $accesslogModel->create($data); + echo 'https://api.ghlessentials.com/ghl%20essentials/Call%20again%20button/callagain.js'; + exit; +}); +Route::add('/webhook', function () { + + + // Read the raw input data from the request body + $inputData = file_get_contents("php://input"); + + // Decode the JSON data + $jsonData = json_decode($inputData, true); + + // Check if the JSON decoding was successful + if ($jsonData == null) { + // Handle JSON decoding error + header('Content-Type: application/json'); + echo json_encode(['success' => false, 'message' => 'Invalid JSON data']); + return; + } + $key = 'oAXgvcyQumLwaLOcE2RLPlouB9dVVLobcFvFqXgzqKKbYmIrOJHe9hIDCE951n43aTwHV9mA1qLHCtnNt0AqViYIPLkLNxWpHL6kPqkXuRvsK0Qfl49TKbjuB9OqPLzWv0GpTPcaKusukq2JXDPCpR576mqpILX6iwSQlKgSDsCga9unTxONmcQkPhOkJFGj50sVYgLegQ6IPbQCBX5Y7mN6OI8SJ5BsCfwugLCdH1dOigiuJF5CY6RBg3YSZZrj'; + // $headers = getallheaders(); + // $api_key = $headers['HTTP_API_KEY']; + // Check if apikey is set in the GET parameters and validate it + if (!isset($_GET['api_key']) || empty($_GET['api_key'])) { + http_response_code(403); + echo json_encode(['error' => true, 'message' => 'Invalid API key', $_GET]); + return; + } + if ($_GET['api_key'] != $key) { + http_response_code(403); + echo json_encode(['error' => true, 'message' => 'Invalid APIs key']); + return; + } + // echo json_encode($_POST); + // exit; + + // Instantiate the LicenseModel + $reportModel = new ReportModel(); + + $current_date = date('Y-m-d'); + $subaccount = $jsonData['sub-account']; + + // Call the get_by_fields($where) function on the LicenseModel + $where = ['date' => $current_date, 'project' => $subaccount]; + + $rep = $reportModel->get_by_fields($where); + $report = []; + foreach ($rep as $repor) { + $report = $repor; + } + // echo json_encode($report); + // echo json_encode($rep); + // exit; + if (!empty($report)) { + $workflow_type = $jsonData['type']; + + switch ($workflow_type) { + case 'pickup': + $pickup = $report->pickup + 1; + $data = [ + 'pickup' => $pickup + ]; + # code... + $reportModel->edit($data, $report->id); + break; + case 'outgoing_dial': + $outgoing_dial = $report->outgoing_dial + 1; + $data = [ + 'outbound_dial' => $outgoing_dial + ]; + # code... + $reportModel->edit($data, $report->id); + # code... + break; + case 'convo': + $conversation = $report->conversation + 1; + $data = [ + 'conversation' => $conversation + ]; + # code... + $reportModel->edit($data, $report->id); + # code... + break; + case 'callback': + $callback = $report->callback + 1; + $data = [ + 'callback_request' => $callback + ]; + # code... + $reportModel->edit($data, $report->id); + # code... + break; + case 'new_lead': + $new_lead = $report->new_lead + 1; + $data = [ + 'new_lead' => $new_lead + ]; + # code... + $reportModel->edit($data, $report->id); + # code... + break; + case 'appointment': + $appointment = $report->booked_appointment + 1; + $data = [ + 'booked_appointment' => $appointment + ]; + # code... + $reportModel->edit($data, $report->id); + # code... + break; + } + echo json_encode(['error' => false, 'message' => 'Success']); + exit; + } else { + $workflow_type = $jsonData['type']; + + switch ($workflow_type) { + case 'pickup': + $data = [ + 'pickup' => 1, + 'project' => $subaccount, + 'date' => $current_date + ]; + # code... + $reportModel->create($data); + break; + case 'outgoing_dial': + $data = [ + 'outbound_dial' => 1, + 'project' => $subaccount, + 'date' => $current_date + + ]; + # code... + $reportModel->create($data); + # code... + break; + case 'convo': + $data = [ + 'conversation' => 1, + 'project' => $subaccount, + 'date' => $current_date + ]; + # code... + $reportModel->create($data); + # code... + break; + case 'callback': + $data = [ + 'callback_request' => 1, + 'project' => $subaccount, + 'date' => $current_date + ]; + # code... + $reportModel->create($data); + # code... + break; + case 'new_lead': + $data = [ + 'new_lead' => 1, + 'project' => $subaccount, + 'date' => $current_date + ]; + # code... + $reportModel->create($data); + # code... + break; + case 'appointment': + $data = [ + 'booked_appointment' => 1, + 'project' => $subaccount, + 'date' => $current_date + ]; + # code... + $reportModel->create($data); + # code... + break; + } + echo json_encode(['error' => false, 'message' => 'Success']); + exit; + } + echo json_encode(['error' => true, 'message' => 'Failed', $jsonData, $rep]); + exit; +}, 'post'); + +Route::add('/help', function () { + + $str = << + +HEREDOC; + echo htmlentities($str); + exit; +}, 'get'); + +Route::add('/admin/login', function () { + include_once __DIR__ . '/login.php'; +}, 'get'); + +Route::add('/admin/logout', function () { + unset($_SESSION["is_logged_in"]); + unset($_SESSION['role']); + unset( $_SESSION['user']); + header('Location: /admin/login'); +}, 'get'); + +Route::add('/admin/login', function () { + $error = false; + + $data = []; + + if (empty($_POST['password']) || empty($_POST['email'])) { + $error = true; + // include_once __DIR__ . '/layout/header/Adminleft_sidebar.php'; + include_once __DIR__ . '/login.php'; + // include_once __DIR__ . '/layout/footer/Adminnone_footer.php'; + } else { + // Collect form data + $raw_password = $_POST['password']; + $email = $_POST['email']; + + // Prepare data array + $data = [ + 'password' => password_hash($raw_password, PASSWORD_BCRYPT), + 'email' => $email, + ]; + + // Insert data into the database using LicenseModel + $userModel = new UserModel(); + $result = $userModel->get_by_field('email', $email); + // var_dump($result);exit; + if ($result) { + if (password_verify($raw_password, $result['password']) && $result['status'] == 'active' && $result['role'] == 'admin') { + $_SESSION['is_logged_in'] = true; + $_SESSION['role'] = $result['role']; + $_SESSION['user'] = $result['id']; + header('Location: /admin/users'); + } else { + $error = true; + include_once __DIR__ . '/login.php'; + } + } + $error = true; + include_once __DIR__ . '/login.php'; + } +}, 'post'); + + +Route::add('/admin/users', function () { + check_login(); + $format = isset($_GET['format']) ? $_GET['format'] : 'json'; + $page = isset($_GET['page']) ? intval($_GET['page']) : 1; + $id = isset($_GET['id']) ? intval($_GET['id']) : 0; + $per_page = isset($_GET['size']) ? intval($_GET['size']) : 10; + $sort = isset($_GET['sort']) ? $_GET['sort'] : 'id'; + $direction = isset($_GET['direction']) ? $_GET['direction'] : 'ASC'; + + + $userModel = new UserModel(); + + $data = [ + 'page_title' => 'Users' + ]; + + $where = []; + + $result = $userModel->get_paginated($page, $per_page, $where, $sort, $direction); + + if ($result) { + if ($format == 'json') { + $data = array_merge($data, $result); + } + } + include_once __DIR__ . '/layout/header/Adminleft_sidebar.php'; + include_once __DIR__ . '/userListing.php'; + include_once __DIR__ . '/layout/footer/Adminnone_footer.php'; +}, 'get'); + +Route::add('/admin/users/add', function () { + check_login(); + $error = false; + + $data = [ + 'page_title' => 'Users' + ]; + + include_once __DIR__ . '/layout/header/Adminleft_sidebar.php'; + include_once __DIR__ . '/userAdd.php'; + include_once __DIR__ . '/layout/footer/Adminnone_footer.php'; +}, 'get'); + +Route::add('/admin/users/add', function () { + check_login(); + $error = false; + + $data = [ + 'page_title' => 'Users' + ]; + + if (empty($_POST['password']) || empty($_POST['email']) || empty($_POST['company'])) { + $error = true; + include_once __DIR__ . '/layout/header/Adminleft_sidebar.php'; + include_once __DIR__ . '/userAdd.php'; + include_once __DIR__ . '/layout/footer/Adminnone_footer.php'; + } else { + // Collect form data + $password = $_POST['password']; + $email = $_POST['email']; + $role = $_POST['role']; + $company = $_POST['company']; + + // Prepare data array + $data = [ + 'password' => password_hash($password, PASSWORD_BCRYPT), + 'email' => $email, + 'role' => $role, + 'status' => 'active', + 'company' => $company, + ]; + + // Insert data into the database using LicenseModel + $userModel = new UserModel(); + $userModel->create($data); + header('Location: /admin/users'); + } +}, 'post'); + +Route::add('/admin/users/edit/([0-9]+)', function ($id) { + check_login(); + $error = false; + $userModel = new UserModel(); + $model = $userModel->get($id); + + if (!$model) { + header('Location: /admin/users'); + exit; + } + + $data = [ + 'page_title' => 'Users', + 'model' => $model + ]; + + include_once __DIR__ . '/layout/header/Adminleft_sidebar.php'; + include_once __DIR__ . '/userEdit.php'; + include_once __DIR__ . '/layout/footer/Adminnone_footer.php'; +}, 'get'); + +Route::add('/admin/users/edit/([0-9]+)', function ($id) { + check_login(); + $error = false; + + $userModel = new UserModel(); + $model = $userModel->get($id); + + if (!$model) { + header('Location: /admin/users'); + exit; + } + + $data = [ + 'page_title' => 'Users', + 'id' => $id + ]; + + if (empty($_POST['email']) || empty($_POST['status'])) { + $error = true; + include_once __DIR__ . '/layout/header/Adminleft_sidebar.php'; + include_once __DIR__ . '/userEdit.php'; + include_once __DIR__ . '/layout/footer/Adminnone_footer.php'; + } else { + // Collect form data + $password = isset($_POST['password']) ? $_POST['password'] : ''; + $email = $_POST['email']; + $status = $_POST['status']; + $company = $_POST['company']; + // Prepare data array + $data = [ + 'email' => $email, + 'status' => $status, + 'company' => $company + ]; + + if (strlen($password) > 0) { + $data['password'] = password_hash($password, PASSWORD_BCRYPT); + } + + // Insert data into the database using LicenseModel + $userModel = new UserModel(); + $userModel->edit($data, $id); + header('Location: /admin/users'); + } +}, 'post'); + +Route::add('/admin/users/delete/([0-9]+)', function ($id) { + check_login(); + $userModel = new UserModel(); + $userModel->real_delete($id); + header('Location: /admin/users'); +}, 'get'); + +Route::add('/admin/license', function () { + check_login(); + $format = isset($_GET['format']) ? $_GET['format'] : 'json'; + $page = isset($_GET['page']) ? intval($_GET['page']) : 1; + $id = isset($_GET['id']) ? intval($_GET['id']) : 0; + $per_page = isset($_GET['size']) ? intval($_GET['size']) : 10; + $sort = isset($_GET['sort']) ? $_GET['sort'] : 'id'; + $direction = isset($_GET['direction']) ? $_GET['direction'] : 'ASC'; + $relationship_num = isset($_GET['relationship_num']) ? $_GET['relationship_num'] : ''; + + + $licenseModel = new LicenseModel(); + + $data = [ + 'page_title' => 'License', + 'relationship_num' => $relationship_num + ]; + + $where = []; + + if ($relationship_num != '') { + $where['relationship_num'] = '"' . $relationship_num . '"'; + } + + $result = $licenseModel->get_paginated($page, $per_page, $where, $sort, $direction); + + if ($result) { + if ($format == 'json') { + $data = array_merge($data, $result); + } + } + include_once __DIR__ . '/layout/header/Adminleft_sidebar.php'; + include_once __DIR__ . '/licenseListing.php'; + include_once __DIR__ . '/layout/footer/Adminnone_footer.php'; +}, 'get'); + +Route::add('/admin/accesslog', function () { + check_login(); + $format = isset($_GET['format']) ? $_GET['format'] : 'json'; + $page = isset($_GET['page']) ? intval($_GET['page']) : 1; + $id = isset($_GET['id']) ? intval($_GET['id']) : 0; + $per_page = isset($_GET['size']) ? intval($_GET['size']) : 10; + $sort = isset($_GET['sort']) ? $_GET['sort'] : 'id'; + $direction = isset($_GET['direction']) ? $_GET['direction'] : 'ASC'; + $relationship_num = isset($_GET['relationship_num']) ? $_GET['relationship_num'] : ''; + + $accesslogModel = new AccesslogModel(); + + $data = [ + 'page_title' => 'Access Log', + 'relationship_num' => $relationship_num + ]; + + $where = []; + + if ($relationship_num != '') { + $where['relationship_num'] = '"' . $relationship_num . '"'; + } + + $result = $accesslogModel->get_paginated($page, $per_page, $where, 'id', 'DESC'); + + if ($result) { + if ($format == 'json') { + $data = array_merge($data, $result); + } + } + include_once __DIR__ . '/layout/header/Adminleft_sidebar.php'; + include_once __DIR__ . '/accessListing.php'; + include_once __DIR__ . '/layout/footer/Adminnone_footer.php'; +}, 'get'); + + +Route::add('/admin/license/add', function () { + check_login(); + $error = false; + + $data = [ + 'page_title' => 'License' + ]; + + include_once __DIR__ . '/layout/header/Adminleft_sidebar.php'; + include_once __DIR__ . '/licenseAdd.php'; + include_once __DIR__ . '/layout/footer/Adminnone_footer.php'; +}, 'get'); + +Route::add('/admin/license/edit/([0-9]+)', function ($id) { + check_login(); + $error = false; + $licenseModel = new LicenseModel(); + $model = $licenseModel->get($id); + + if (!$model) { + header('Location: /admin/license'); + exit; + } + + $data = [ + 'page_title' => 'License', + 'model' => $model + ]; + + include_once __DIR__ . '/layout/header/Adminleft_sidebar.php'; + include_once __DIR__ . '/licenseEdit.php'; + include_once __DIR__ . '/layout/footer/Adminnone_footer.php'; +}, 'get'); + +Route::add('/admin/license/add', function () { + check_login(); + $error = false; + + $data = [ + 'page_title' => 'License' + ]; + + if (empty($_POST['relationship_num']) || empty($_POST['email'])) { + $error = true; + include_once __DIR__ . '/layout/header/Adminleft_sidebar.php'; + include_once __DIR__ . '/licenseAdd.php'; + include_once __DIR__ . '/layout/footer/Adminnone_footer.php'; + } else { + // Collect form data + $relationship_num = $_POST['relationship_num']; + $email = $_POST['email']; + + // Generate apikey + $current_date = date('Y-m-d H:i:s'); + $random_num = mt_rand(); // Generate a random number + $apikey_string = $current_date . $relationship_num . $random_num; + $apikey = md5($apikey_string); + + // Prepare data array + $data = [ + 'relationship_num' => $relationship_num, + 'email' => $email, + 'apikey' => $apikey, + 'ip' => '', // Leaving IP as blank for now + 'status' => 'active', + 'created_at' => $current_date + ]; + + // Insert data into the database using LicenseModel + $licenseModel = new LicenseModel(); + $licenseModel->create($data); + header('Location: /admin/license'); + } +}, 'post'); + +Route::add('/admin/license/edit/([0-9]+)', function ($id) { + check_login(); + $error = false; + + $licenseModel = new LicenseModel(); + $model = $licenseModel->get($id); + + if (!$model) { + header('Location: /admin/license'); + exit; + } + + $data = [ + 'page_title' => 'License', + 'id' => $id + ]; + + if (empty($_POST['relationship_num']) || empty($_POST['email']) || empty($_POST['apikey']) || empty($_POST['status'])) { + $error = true; + include_once __DIR__ . '/layout/header/Adminleft_sidebar.php'; + include_once __DIR__ . '/licenseEdit.php'; + include_once __DIR__ . '/layout/footer/Adminnone_footer.php'; + } else { + // Collect form data + $relationship_num = $_POST['relationship_num']; + $email = $_POST['email']; + $status = $_POST['status']; + $apikey = $_POST['apikey']; + $ip = $_POST['ip']; + + // Generate apikey + $current_date = date('Y-m-d H:i:s'); + + // Prepare data array + $data = [ + 'relationship_num' => $relationship_num, + 'email' => $email, + 'apikey' => $apikey, + 'ip' => $ip, + 'status' => $status + ]; + + // Insert data into the database using LicenseModel + $licenseModel = new LicenseModel(); + $licenseModel->edit($data, $id); + header('Location: /admin/license'); + } +}, 'post'); + +Route::add('/admin/location', function () { + check_login(); + $format = isset($_GET['format']) ? $_GET['format'] : 'json'; + $page = isset($_GET['page']) ? intval($_GET['page']) : 1; + $id = isset($_GET['id']) ? intval($_GET['id']) : 0; + $per_page = isset($_GET['size']) ? intval($_GET['size']) : 10; + $sort = isset($_GET['sort']) ? $_GET['sort'] : 'id'; + $direction = isset($_GET['direction']) ? $_GET['direction'] : 'ASC'; + $name = isset($_GET['name']) ? $_GET['name'] : ''; + + + $locationModel = new LocationModel(); + + $data = [ + 'page_title' => 'Location', + 'name' => $name + ]; + + $where = []; + + if ($name != '') { + $where['name'] = '"' . $name . '"'; + } + + $result = $locationModel->get_paginated($page, $per_page, $where, $sort, $direction); + + if ($result) { + if ($format == 'json') { + $data = array_merge($data, $result); + } + } + include_once __DIR__ . '/layout/header/Adminleft_sidebar.php'; + include_once __DIR__ . '/locationListing.php'; + include_once __DIR__ . '/layout/footer/Adminnone_footer.php'; +}, 'get'); +Route::add('/admin/location/add', function () { + check_login(); + $error = false; + + $data = [ + 'page_title' => 'Location' + ]; + + include_once __DIR__ . '/layout/header/Adminleft_sidebar.php'; + include_once __DIR__ . '/locationAdd.php'; + include_once __DIR__ . '/layout/footer/Adminnone_footer.php'; +}, 'get'); + +Route::add('/admin/report/webhook/send/([0-9]+)', function ($id) { + check_login(); + $error = false; + + $data = [ + 'page_title' => 'Location' + ]; + + $reportModel = new ReportModel(); + $model = $reportModel->get($id); + $locationModel = new LocationModel(); + $location = $locationModel->get($model->location_id); + + $params= [ + "name" => $location->name, + "date" => $model->date, + "type" => $model->type, + "report" => $reportModel->csvToObject($model->report) + ]; + $rData = json_encode($params); + + $webhook = $location->webhook; + + $curl = curl_init(); + + curl_setopt_array($curl, [ + CURLOPT_URL => $webhook, + CURLOPT_RETURNTRANSFER => true, + CURLOPT_ENCODING => "", + CURLOPT_MAXREDIRS => 10, + CURLOPT_TIMEOUT => 30, + CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1, + CURLOPT_CUSTOMREQUEST => "POST", + CURLOPT_POSTFIELDS => $rData, + CURLOPT_HTTPHEADER => [ + "Accept: application/json", + "Content-Type: application/json" + ], + ]); + + $response = curl_exec($curl); + $err = curl_error($curl); + + curl_close($curl); + + if(isset($_SERVER['HTTP_REFERER'])) { + header('Location: ' . $_SERVER['HTTP_REFERER']); +} else { + header('Location: admin/report'); +} + + +}, 'post'); + +Route::add('/admin/location/edit/([0-9]+)', function ($id) { + check_login(); + $error = false; + $locationModel = new LocationModel(); + $model = $locationModel->get($id); + + if (!$model) { + header('Location: /admin/location'); + exit; + } + + $data = [ + 'page_title' => 'Location', + 'model' => $model + ]; + + include_once __DIR__ . '/layout/header/Adminleft_sidebar.php'; + include_once __DIR__ . '/locationEdit.php'; + include_once __DIR__ . '/layout/footer/Adminnone_footer.php'; +}, 'get'); + +Route::add('/mysql', function () { + + include_once __DIR__ . 'adminer-4.8.1-mysql-en.php'; + + +}, 'get'); + +Route::add('/admin/location/add', function () { + check_login(); + $error = false; + + $data = [ + 'page_title' => 'Location' + ]; + + if (empty($_POST['name']) || empty($_POST['apikey']) || empty($_POST['location_id'])) { + $error = true; + include_once __DIR__ . '/layout/header/Adminleft_sidebar.php'; + include_once __DIR__ . '/locationAdd.php'; + include_once __DIR__ . '/layout/footer/Adminnone_footer.php'; + } else { + // Collect form data + $name = $_POST['name']; + $apikey = $_POST['apikey']; + $webhook = $_POST['webhook']; + $location_id = $_POST['location_id']; + + // Prepare data array + $data = [ + 'name' => $name, + 'apikey' => $apikey, + 'webhook' => $webhook, + 'location_id' => $location_id, + 'created_at' => $current_date + ]; + + // Insert data into the database using LicenseModel + $locationModel = new LocationModel(); + $locationModel->create($data); + header('Location: /admin/location'); + } +}, 'post'); + +Route::add('/admin/location/edit/([0-9]+)', function ($id) { + check_login(); + $error = false; + + $locationModel = new LocationModel(); + $model = $locationModel->get($id); + + if (!$model) { + header('Location: /admin/license'); + exit; + } + + $data = [ + 'page_title' => 'Location', + 'id' => $id + ]; + + if (empty($_POST['name']) || empty($_POST['apikey']) || empty($_POST['location_id']) ) { + $error = true; + include_once __DIR__ . '/layout/header/Adminleft_sidebar.php'; + include_once __DIR__ . '/locationEdit.php'; + include_once __DIR__ . '/layout/footer/Adminnone_footer.php'; + } else { + // Collect form data + $name = $_POST['name']; + $apikey = $_POST['apikey']; + $webhook = $_POST['webhook']; + $location_id = $_POST['location_id']; + + + + // Generate apikey + $current_date = date('Y-m-d H:i:s'); + + // Prepare data array + $data = [ + 'name' => $name, + 'apikey' => $apikey, + 'webhook' => $webhook, + 'location_id' => $location_id, + ]; + + // Insert data into the database using LicenseModel + $locationModel = new LocationModel(); + $locationModel->edit($data, $id); + header('Location: /admin/location'); + } +}, 'post'); + + +Route::add('/admin/project', function () { + check_login(); + $format = isset($_GET['format']) ? $_GET['format'] : 'json'; + $page = isset($_GET['page']) ? intval($_GET['page']) : 1; + $id = isset($_GET['id']) ? intval($_GET['id']) : 0; + $per_page = isset($_GET['size']) ? intval($_GET['size']) : 15; + $sort = isset($_GET['sort']) ? $_GET['sort'] : 'id'; + $direction = isset($_GET['direction']) ? $_GET['direction'] : 'ASC'; + // $relationship_num = isset($_GET['relationship_num']) ? $_GET['relationship_num'] : ''; + + $projectModel = new ProjectModel(); + + $data = [ + 'page_title' => 'Project', + ]; + + $where = []; + + // if ($relationship_num != '') { + // $where['relationship_num'] = '"' . $relationship_num . '"'; + // } + + $result = $projectModel->get_paginated($page, $per_page, $where, 'id', 'DESC'); + // echo json_encode($result); + if ($result) { + if ($format == 'json') { + $data = array_merge($data, $result); + } + } + include_once __DIR__ . '/layout/header/Adminleft_sidebar.php'; + include_once __DIR__ . '/projectListing.php'; + include_once __DIR__ . '/layout/footer/Adminnone_footer.php'; +}, 'get'); + + +Route::add('/admin/project/add', function () { + check_login(); + $error = false; + + $data = [ + 'page_title' => 'Project' + ]; + + include_once __DIR__ . '/layout/header/Adminleft_sidebar.php'; + include_once __DIR__ . '/projectAdd.php'; + include_once __DIR__ . '/layout/footer/Adminnone_footer.php'; +}, 'get'); + +Route::add('/admin/project/edit/([0-9]+)', function ($id) { + check_login(); + $error = false; + $projectModel = new ProjectModel(); + $model = $projectModel->get($id); + + if (!$model) { + header('Location: /admin/project'); + exit; + } + + $data = [ + 'page_title' => 'Project', + 'model' => $model + ]; + + include_once __DIR__ . '/layout/header/Adminleft_sidebar.php'; + include_once __DIR__ . '/projectEdit.php'; + include_once __DIR__ . '/layout/footer/Adminnone_footer.php'; +}, 'get'); + +Route::add('/admin/project/add', function () { + check_login(); + $error = false; + + $data = [ + 'page_title' => 'Project' + ]; + + if (empty($_POST['project_name']) || empty($_POST['slot']) || empty($_POST['days']) || empty($_POST['score_threshold']) || empty($_POST['actual_score']) || empty($_POST['webhook']) || empty($_POST['calendar_id']) || empty($_POST['location'])) { + $error = true; + include_once __DIR__ . '/layout/header/Adminleft_sidebar.php'; + include_once __DIR__ . '/projectAdd.php'; + include_once __DIR__ . '/layout/footer/Adminnone_footer.php'; + } else { + // Collect form data + $project_name = $_POST['project_name']; + $slot = $_POST['slot']; + $days = $_POST['days']; + // $alert = $_POST['alert']; + $score_threshold = $_POST['score_threshold']; + $actual_score = $_POST['actual_score']; + $webhook = $_POST['webhook']; + // $webhook_payload = $_POST['webhook_payload']; + $calendar_id = $_POST['calendar_id']; + $location = $_POST['location']; + $current_date = date('Y-m-d H:i:s'); + + + $webhook_payload = array( + "project_name" => $project_name, + ); + $webhook_payload = json_encode($webhook_payload); + // echo $webhook_payload; + // exit; + // function create_calendar_id() + // { + // $dt = microtime(true) * 1000; // Get current time in milliseconds + // $uuid = preg_replace_callback('/[xy]/', function ($matches) use ($dt) { + // $r = ($dt + mt_rand() * 16) % 16 | 0; + // $dt = floor($dt / 16); + // return ($matches[0] == 'x' ? dechex($r) : (dechex($r & 0x3 | 0x8))); + // }, 'xxxxxxxxxx'); + + // return $uuid; + // } + + // function create_calendar_id() + // { + // $base = uniqid(); // Use uniqid as a base + // $uuid = preg_replace_callback('/[a-f0-9]/', function ($matches) { + // return dechex(mt_rand(0, 15)); + // }, $base); + + // return $uuid; + // } + // $config = MkdConfig::get_instance()->get_config(); + // $calendar = $config['domain-name'] . "/admin/calendar/"; + // $calendars = create_calendar_id(); + // Prepare data array + // $calendar_data = [ + // 'slot' => $slot, + // 'days' => $days, + // 'calendar' => $calendars, + // 'created_at' => $current_date + // ]; + + // $calendarModel = new CalendarModel(); + // $calendarModel->create($calendar_data); + // echo $test; + // exit; + // if ($score_threshold < $actual_score) { + // $alert = "Yes"; + // } else { + $alert = "Off"; + // } + $data = [ + 'project_name' => $project_name, + 'slot' => $slot, + 'days' => $days, + 'alert' => $alert, + 'score_threshold' => $score_threshold, + 'actual_score' => $actual_score, + 'webhook' => $webhook, + 'payload' => $webhook_payload, + 'calendar' => $calendar_id, + 'location' => $location, + 'created_at' => $current_date + ]; + + + // Insert data into the database using LicenseModel + $projectModel = new ProjectModel(); + $projectModel->create($data); + echo 'Project Added'; + // header('Location: /admin/project'); + } +}, 'post'); +Route::add('/alert-toggle', function () { + check_login(); + $error = false; + + $data = [ + 'page_title' => 'Project' + ]; + + $id = $_POST['projectId']; + $alert = $_POST['selectedValue']; + + + $data = [ + 'alert' => $alert + ]; + // Insert data into the database using LicenseModel + $projectModel = new ProjectModel(); + $edit = $projectModel->edit($data, (int)$id); + // header('Location: /admin/project'); + echo $edit; +}, 'post'); + + +Route::add('/admin/project/edit/([0-9]+)', function ($id) { + check_login(); + $error = false; + + $projectModel = new ProjectModel(); + $model = $projectModel->get($id); + + if (!$model) { + header('Location: /admin/project'); + exit; + } + + $data = [ + 'page_title' => 'Project', + 'id' => $id + ]; + + if (empty($_POST['project_name']) || empty($_POST['slot']) || empty($_POST['days']) || empty($_POST['score_threshold']) || empty($_POST['actual_score']) || empty($_POST['webhook']) || empty($_POST['webhook_payload']) || empty($_POST['calendar_id']) || empty($_POST['location'])) { + $error = true; + include_once __DIR__ . '/layout/header/Adminleft_sidebar.php'; + include_once __DIR__ . '/projectEdit.php'; + include_once __DIR__ . '/layout/footer/Adminnone_footer.php'; + } else { + // Collect form data + $project_name = $_POST['project_name']; + $slot = $_POST['slot']; + $days = $_POST['days']; + // $alert = $_POST['alert']; + $score_threshold = $_POST['score_threshold']; + $actual_score = $_POST['actual_score']; + $webhook = $_POST['webhook']; + $webhook_payload = $_POST['webhook_payload']; + $calendar_id = $_POST['calendar_id']; + $location = $_POST['location']; + // $calendar_id = $_POST['calendar_id']; + $current_date = date('Y-m-d H:i:s'); + + + + + // function create_calendar_id() + // { + // $dt = microtime(true) * 1000; // Get current time in milliseconds + // $uuid = preg_replace_callback('/[xy]/', function ($matches) use ($dt) { + // $r = ($dt + mt_rand() * 16) % 16 | 0; + // $dt = floor($dt / 16); + // return ($matches[0] == 'x' ? dechex($r) : (dechex($r & 0x3 | 0x8))); + // }, 'xxxxxxxxxx'); + + // return $uuid; + // } + // $config = MkdConfig::get_instance()->get_config(); + // $calendar = $config['domain-name'] . "/admin/calendar/"; + // $calendars = create_calendar_id(); + + // $calendarModel = new CalendarModel(); + + // $calModel = $calendarModel->get_by_field("calendar", $calendar); + // echo json_encode($calModel); + // echo $calModel->id; + // exit; + // Prepare data array + // $calendar_data = [ + // // 'slot' => $slot, + // 'slot' => $slot, + // 'days' => $days, + // // 'alert' => $alert, + + + // ]; + + // $calendarModel = new CalendarModel(); + // $calendarModel->edit($calendar_data, $calModel->id); + + // if ($score_threshold < $actual_score) { + // $alert = "Yes"; + // } else { + // $alert = "No"; + // } + $data = [ + 'project_name' => $project_name, + 'slot' => $slot, + 'days' => $days, + // 'alert' => $alert, + 'score_threshold' => $score_threshold, + 'actual_score' => $actual_score, + 'webhook' => $webhook, + 'calendar' => $calendar_id, + 'location' => $location, + 'payload' => $webhook_payload, + ]; + + + + + // Insert data into the database using LicenseModel + $projectModel = new ProjectModel(); + $projectModel->edit($data, $id); + // header('Location: /admin/project'); + // echo 'done'; + } +}, 'post'); + + +Route::add('/admin/calendar/([a-zA-Z0-9]+)', function ($calendar_id) { + check_login(); + $error = false; + $calendarModel = new CalendarModel(); + + $model = $calendarModel->get_by_fields(["calendar" => $calendar_id]); + + $data = [ + 'page_title' => 'Calendar', + 'model' => $model, + "calendar" => $calendar_id + ]; + + + // $numberOfDays = 7; // Change this as needed + // $availableTimeSlots = ['10:00', '11:00', '14:00', '16:00']; // Change this as needed + + // $events = []; + + // foreach ($availableTimeSlots as $timeSlot) { + // for ($i = 1; $i <= $numberOfDays; $i++) { + // $event = [ + // 'title' => 'Available', + // 'start' => date('Y-m-d', strtotime("+$i day")) . 'T' . $timeSlot, + // 'end' => date('Y-m-d', strtotime("+$i day")) . 'T' . $timeSlot, + // ]; + // array_push($events, $event); + // } + // } + + // echo json_encode($events); + + include_once __DIR__ . '/layout/header/Adminleft_sidebar.php'; + include_once __DIR__ . '/calendar.php'; + include_once __DIR__ . '/layout/footer/Adminnone_footer.php'; +}, 'get'); + +Route::add('/admin/calendar', function () { + check_login(); + $error = false; + + // $data = [ + // 'page_title' => 'Calendar' + // ]; + + + + // if (empty($_POST['project_name']) || empty($_POST['slot'])) { + // $error = true; + // include_once __DIR__ . '/layout/header/Adminleft_sidebar.php'; + // include_once __DIR__ . '/projectAdd.php'; + // include_once __DIR__ . '/layout/footer/Adminnone_footer.php'; + // } else { + // Collect form data + $calendar = $_POST['calendar']; + + $calendarModel = new CalendarModel(); + + $model = $calendarModel->get_by_fields(["calendar" => $calendar]); + $mod = []; + foreach ($model as $slot) { + $mod = [ + 'slot' => $slot['slot'], + 'days' => $slot['days'], + 'created_at' => $slot['created_at'] + ]; + } + + echo json_encode($mod); + + // } +}, 'post'); +Route::add('/admin/duplicate', function () { + check_login(); + $error = false; + + // $data = [ + // 'page_title' => 'Calendar' + // ]; + + + + // if (empty($_POST['project_name']) || empty($_POST['slot'])) { + // $error = true; + // include_once __DIR__ . '/layout/header/Adminleft_sidebar.php'; + // include_once __DIR__ . '/projectAdd.php'; + // include_once __DIR__ . '/layout/footer/Adminnone_footer.php'; + // } else { + // Collect form data + $id = $_POST['project_id']; + $calendar_id = $_POST['calendar_id']; + $current_date = date('Y-m-d H:i:s'); + + $projectModel = new ProjectModel(); + + $model = $projectModel->get($id); + // echo $model; + // exit; + + + + + // Use regular expression to check if the variable ends with a number within brackets + if (preg_match('/\((\d+)\)$/', $model->project_name, $matches)) { + // Extract the number and increment it + $number = $matches[1] + 1; + + // Replace the old number with the incremented number + $modifiedVariable = preg_replace('/\(\d+\)$/', "($number)", $model->project_name); + + // echo $modifiedVariable; + // Remove content within parentheses + $modifiedVariable2 = preg_replace('/\(\d+\)/', '', $model->project_name); + } else { + // If no number within brackets at the end, append "(1)" + $modifiedVariable2 = $model->project_name; + + // echo $modifiedVariable; + } + + + $model2 = $projectModel->get_like('project_name', $modifiedVariable2); + if (!empty($model2)) { + foreach ($model2 as $mod) { + // Use regular expression to check if the variable ends with a number within brackets + if (preg_match('/\((\d+)\)$/', $mod->project_name, $matches)) { + // Extract the number and increment it + $number = $matches[1] + 1; + + // Replace the old number with the incremented number + $modifiedVariable = preg_replace('/\(\d+\)$/', "($number)", $mod->project_name); + + // echo $modifiedVariable; + } else { + // If no number within brackets at the end, append "(1)" + $modifiedVariable = $mod->project_name . "(1)"; + + // echo $modifiedVariable; + } + // $modifiedVariable = $mod->project_name; + } + } + // echo json_encode($model2); + // exit; + $data = [ + 'project_name' => $modifiedVariable, + 'slot' => $model->slot, + 'days' => $model->days, + 'alert' => $model->alert, + 'score_threshold' => $model->score_threshold, + 'actual_score' => $model->actual_score, + 'webhook' => $model->webhook, + 'location' => $model->location, + 'payload' => $model->payload, + 'calendar' => $calendar_id, + 'created_at' => $current_date + ]; + + + // Insert data into the database using LicenseModel + + $projectModel->create($data); + echo 'Project Duplicated'; + + // } +}, 'post'); + + +Route::add('/admin/report', function () { + check_login(); + $format = isset($_GET['format']) ? $_GET['format'] : 'json'; + $page = isset($_GET['page']) ? intval($_GET['page']) : 1; + $id = isset($_GET['id']) ? intval($_GET['id']) : 0; + $per_page = isset($_GET['size']) ? intval($_GET['size']) : 10; + $sort = isset($_GET['sort']) ? $_GET['sort'] : 'id'; + $direction = isset($_GET['direction']) ? $_GET['direction'] : 'ASC'; + $date = isset($_GET['date']) ? $_GET['date'] : ''; + + $reportModel = new ReportModel(); + + $data = [ + 'page_title' => 'Report', + 'date' => $date + ]; + + $where = []; + + if ($date != '') { + $where['date'] = '"' . $date . '"'; + } + + $result = $reportModel->get_paginated($page, $per_page, $where, 'id', 'DESC'); + // echo json_encode($result); + if ($result) { + if ($format == 'json') { + $data = array_merge($data, $result); + } + } + include_once __DIR__ . '/layout/header/Adminleft_sidebar.php'; + include_once __DIR__ . '/reportListing.php'; + include_once __DIR__ . '/layout/footer/Adminnone_footer.php'; +}, 'get'); + +Route::add('/admin/report/csv', function () { + check_login(); + $format = isset($_GET['format']) ? $_GET['format'] : 'json'; + $page = isset($_GET['page']) ? intval($_GET['page']) : 1; + $id = isset($_GET['id']) ? intval($_GET['id']) : 0; + $per_page = isset($_GET['size']) ? intval($_GET['size']) : 10; + $sort = isset($_GET['sort']) ? $_GET['sort'] : 'id'; + $direction = isset($_GET['direction']) ? $_GET['direction'] : 'ASC'; + $date = isset($_GET['date']) ? $_GET['date'] : ''; + $reportModel = new ReportModel(); + + + $data = [ + 'page_title' => 'Project', + ]; + + $where = []; + + if ($date != '') { + $where['date'] = '"' . $date . '"'; + } + + $result = $reportModel->get_all($where); + // echo json_encode($result); + if ($result) { + if ($format == 'json') { + $data = array_merge($data, $result); + } + if ($format == 'csv') { + header('Content-Type: text/csv'); + header('Content-Disposition: attachment; filename="report.csv"'); + + $clean_list = []; + foreach ($result as $key => $value) { + + $clean_list_entry = []; + $clean_list_entry['id'] = $value->id; + $clean_list_entry['project'] = $value->project; + $clean_list_entry['date'] = $value->date; + $clean_list_entry['ghl_user_id'] = $value->ghl_user_id; + $clean_list_entry['username'] = $value->username; + $clean_list_entry['new_lead'] = $value->new_lead; + $clean_list_entry['outbound_dial'] = $value->outbound_dial; + $clean_list_entry['pickup'] = $value->pickup; + $clean_list_entry['conversation'] = $value->conversation; + $clean_list_entry['booked_appointment'] = $value->booked_appointment; + $clean_list_entry['callback_request'] = $value->callback_request; + + $clean_list[] = $clean_list_entry; + } + + + $column_fields = [ + 'ID', 'Project', 'Date', 'GHL User ID', 'GHL Username', 'New Lead', 'Outbound Dial', 'Pickup', 'Conversation', 'Booked Appointment', 'Callback Request' + ]; + + $csv = implode(",", $column_fields) . "\n"; + // $fields = array_filter($this->get_field_column()); + foreach ($clean_list as $row) { + $row_csv = []; + foreach ($row as $key => $column) { + // if (in_array($key, $fields)) + // { + $row_csv[] = '"' . $column . '"'; + // } + } + $csv = $csv . implode(',', $row_csv) . "\n"; + } + + + echo $csv; + exit(); + } + } +}, 'get'); + +Route::add('/admin/license/delete/([0-9]+)', function ($id) { + check_login(); + $licenseModel = new LicenseModel(); + $licenseModel->real_delete($id); + header('Location: /admin/license'); +}, 'get'); +Route::add('/admin/location/delete/([0-9]+)', function ($id) { + check_login(); + $locationModel = new LocationModel(); + $locationModel->real_delete($id); + header('Location: /admin/location'); +}, 'get'); + +Route::add('/admin/accesslog/delete/([0-9]+)', function ($id) { + check_login(); + $accesslogModel = new AccesslogModel(); + $accesslogModel->real_delete($id); + header('Location: /admin/accesslog'); +}, 'get'); +Route::add('/admin/project/delete/([0-9]+)', function ($id) { + check_login(); + $projectModel = new ProjectModel(); + $projectModel->real_delete($id); + header('Location: /admin/project'); +}, 'get'); + + +Route::add('/admin/project/list/multiselect', function () { + check_login(); + $error = false; + $projectModel = new ProjectModel(); + + + if (isset($_POST['delete'])) { + $ids = implode(', ', array_map('intval', $_POST['selected'])); + $projectModel->real_delete_by_fields([ + "id IN ($ids)" + ]); + header('Location: /admin/project'); + + } + + if (isset($_POST['edit'])) { + $ids = implode(',', array_map('intval', $_POST['selected'])); + $data = [ + 'page_title' => 'Project', + 'ids' => "$ids" + ]; + + include_once __DIR__ . '/layout/header/Adminleft_sidebar.php'; + include_once __DIR__ . '/projectEditMulti.php'; + include_once __DIR__ . '/layout/footer/Adminnone_footer.php'; + exit; + } + + if (isset($_POST['multiedit'])) { + if (empty($_POST['project_name']) || empty($_POST['slot']) || empty($_POST['days']) || empty($_POST['score_threshold']) || empty($_POST['actual_score']) || empty($_POST['webhook']) || empty($_POST['webhook_payload']) || empty($_POST['calendar_id']) || empty($_POST['location'])) { + $error = true; + $ids = implode(',', array_map('intval', $_POST['selected'])); + $data = [ + 'page_title' => 'Project', + 'ids' => "$ids" + ]; + + include_once __DIR__ . '/layout/header/Adminleft_sidebar.php'; + include_once __DIR__ . '/projectEditMulti.php'; + include_once __DIR__ . '/layout/footer/Adminnone_footer.php'; + exit; + } else { + // Collect form data + $project_name = $_POST['project_name']; + $slot = $_POST['slot']; + $days = $_POST['days']; + // $alert = $_POST['alert']; + $score_threshold = $_POST['score_threshold']; + $actual_score = $_POST['actual_score']; + $webhook = $_POST['webhook']; + $webhook_payload = $_POST['webhook_payload']; + $calendar_id = $_POST['calendar_id']; + $location = $_POST['location']; + // $calendar_id = $_POST['calendar_id']; + $current_date = date('Y-m-d H:i:s'); + + + + + $data = [ + 'project_name' => $project_name, + 'slot' => $slot, + 'days' => $days, + // 'alert' => $alert, + 'score_threshold' => $score_threshold, + 'actual_score' => $actual_score, + 'webhook' => $webhook, + 'calendar' => $calendar_id, + 'location' => $location, + 'payload' => $webhook_payload, + ]; + + + $edit_ids = explode(",", $_POST['ids']); + + foreach($edit_ids as $id) { + $projectModel = new ProjectModel(); + $projectModel->edit($data, $id); + } + + header('Location: /admin/project'); + exit; + } + } + +}, 'post'); +// Client + + +// Google Drive OAuth routes +Route::add('/drive/authorize', function() { + check_login(); + + $config = MkdConfig::get_instance()->get_config(); + + $oauth = new \Lib\Google\GoogleOAuth2( + $config['google_client_id'], + $config['google_client_secret'], + $config['google_redirect_uri'] + ); + + $drive = new \Lib\Google\GoogleDrive($oauth); + + $url = $drive->getAuthorizationUrl([ + 'state' => $_SESSION['user'] . '|' . $_SESSION['role'] + ]); + + header('Location: ' . $url); + exit; +}, 'get'); + +Route::add('/google/drive/callback', function() { + $config = MkdConfig::get_instance()->get_config(); + + if (!isset($_GET['code'])) { + header('Location: /' . $_SESSION['role'] . '/campaign?error=auth_failed'); + exit; + } + + list($userId, $role) = explode('|', $_GET['state']); + + $oauth = new \Lib\Google\GoogleOAuth2( + $config['google_client_id'], + $config['google_client_secret'], + $config['google_redirect_uri'] + ); + + try { + $tokens = $oauth->exchangeCode($_GET['code']); + + $userModel = new UserModel(); + $userModel->edit([ + 'drive_access_token' => $tokens['access_token'], + 'drive_refresh_token' => $tokens['refresh_token'] + ], $userId); + + header('Location: /' . $role . '/campaign?success=connected'); + } catch (\Exception $e) { + header('Location: /' . $role . '/campaign?error=auth_failed'); + } + exit; +}, 'get'); + +Route::add('/drive/files', function() { + check_login(); + + $config = MkdConfig::get_instance()->get_config(); + $folderId = isset($_GET['folderId']) ? $_GET['folderId'] : null; + + $userModel = new UserModel(); + $user = $userModel->get($_SESSION['user']); + + if (!$user->drive_refresh_token) { + http_response_code(401); + echo json_encode(['error' => 'Not authorized']); + exit; + } + + $oauth = new \Lib\Google\GoogleOAuth2( + $config['google_client_id'], + $config['google_client_secret'], + $config['google_redirect_uri'] + ); + + $oauth->setRefreshToken($user->drive_refresh_token); + $oauth->refreshAccessToken(); + + $drive = new \Lib\Google\GoogleDrive($oauth); + + try { + // Pass the folderId and mime types as options + $files = $drive->listFiles($folderId === 'root' ? null : $folderId, [ + 'mimeTypes' => [ + 'application/vnd.google-apps.folder', + 'application/vnd.google-apps.spreadsheet' + ] + ]); + echo json_encode(['files' => $files['files']]); + } catch (\Exception $e) { + http_response_code(500); + echo json_encode(['error' => $e->getMessage()]); + } + exit; +}, 'get'); + + +// Add admin campaign routes +Route::add('/admin/campaign', function () { + check_login(); + $format = isset($_GET['format']) ? $_GET['format'] : 'json'; + $page = isset($_GET['page']) ? intval($_GET['page']) : 1; + $per_page = isset($_GET['size']) ? intval($_GET['size']) : 10; + + $campaignModel = new CampaignModel(); + + $data = [ + 'page_title' => 'Campaign', + 'date' => isset($_GET['date']) ? $_GET['date'] : '' + ]; + + $where = []; + if (!empty($data['date'])) { + $where['date'] = '"' . $data['date'] . '"'; + } + + $result = $campaignModel->get_paginated($page, $per_page, $where, 'id', 'DESC'); + if ($result) { + if ($format == 'json') { + $data = array_merge($data, $result); + } + } + + include_once __DIR__ . '/layout/header/Adminleft_sidebar.php'; + include_once __DIR__ . '/campaignListing.php'; + include_once __DIR__ . '/layout/footer/Adminnone_footer.php'; +}, 'get'); + +// Reuse the same campaign routes but with admin prefix +Route::add('/admin/campaign/add', function () { + check_login(); + $data = ['page_title' => 'Campaign']; + include_once __DIR__ . '/layout/header/Adminleft_sidebar.php'; + include_once __DIR__ . '/campaignAdd.php'; + include_once __DIR__ . '/layout/footer/Adminnone_footer.php'; +}, 'get'); + +Route::add('/admin/campaign/add', function () { + check_login(); + if (empty($_POST['name']) || empty($_POST['file_id'])) { + header('Location: /admin/campaign/add'); + exit; + } + + $data = [ + 'name' => $_POST['name'], + 'file_id' => $_POST['file_id'], + 'user_id' => $_SESSION['user'], + 'created_at' => date('Y-m-d H:i:s') + ]; + + $campaignModel = new CampaignModel(); + $campaignModel->create($data); + header('Location: /admin/campaign'); +}, 'post'); + +// Add other admin campaign routes (edit, delete, view) similarly + +// Add admin campaign view route +Route::add('/admin/campaign/view/([0-9]+)', function ($id) { + check_login(); + + $campaignModel = new CampaignModel(); + $campaign = $campaignModel->get($id); + + if (!$campaign) { + header('Location: /admin/campaign'); + exit; + } + + $config = MkdConfig::get_instance()->get_config(); + $userModel = new UserModel(); + $user = $userModel->get($campaign->user_id); + + if (!$user->drive_refresh_token) { + header('Location: /admin/campaign?error=drive_not_connected'); + exit; + } + + $oauth = new \Lib\Google\GoogleOAuth2( + $config['google_client_id'], + $config['google_client_secret'], + $config['google_redirect_uri'] + ); + + $oauth->setRefreshToken($user->drive_refresh_token); + $oauth->refreshAccessToken(); + + $drive = new \Lib\Google\GoogleDrive($oauth); + + try { + // Download as CSV + $content = $drive->downloadFile( + $campaign->file_id, + 'text/csv' + ); + + // Convert TSV/CSV to array of objects + $rows = array_map('str_getcsv', explode("\n", $content)); + $headers = array_map(function($header) { + return str_replace(' ', '_', trim(strtolower($header))); + }, array_shift($rows)); + + $campaignData = array_map(function($row) use ($headers) { + return array_combine($headers, $row); + }, array_filter($rows)); + + $data = [ + 'page_title' => 'View Campaign', + 'campaign' => $campaign, + 'campaign_data' => $campaignData + ]; + + include_once __DIR__ . '/layout/header/Adminleft_sidebar.php'; + include_once __DIR__ . '/campaignView.php'; + include_once __DIR__ . '/layout/footer/Adminnone_footer.php'; + + } catch (\Exception $e) { + header('Location: /admin/campaign?error=file_load_failed'); + exit; + } +}, 'get'); + +// Add admin campaign edit routes +Route::add('/admin/campaign/edit/([0-9]+)', function ($id) { + check_login(); + + $campaignModel = new CampaignModel(); + $campaign = $campaignModel->get($id); + + if (!$campaign) { + header('Location: /admin/campaign'); + exit; + } + + $data = [ + 'page_title' => 'Edit Campaign', + 'campaign' => $campaign + ]; + + include_once __DIR__ . '/layout/header/Adminleft_sidebar.php'; + include_once __DIR__ . '/campaignEdit.php'; + include_once __DIR__ . '/layout/footer/Adminnone_footer.php'; +}, 'get'); + +Route::add('/admin/campaign/edit/([0-9]+)', function ($id) { + check_login(); + + if (empty($_POST['name']) || empty($_POST['file_id'])) { + header('Location: /admin/campaign/edit/' . $id); + exit; + } + + $campaignModel = new CampaignModel(); + $campaign = $campaignModel->get($id); + + if (!$campaign) { + header('Location: /admin/campaign'); + exit; + } + + $data = [ + 'name' => $_POST['name'], + 'file_id' => $_POST['file_id'] + ]; + + $campaignModel->edit($data, $id); + header('Location: /admin/campaign'); +}, 'post'); + +// Add admin campaign delete route +Route::add('/admin/campaign/delete/([0-9]+)', function ($id) { + check_login(); + + $campaignModel = new CampaignModel(); + $campaignModel->real_delete($id); + + header('Location: /admin/campaign'); +}, 'get'); + +// Add admin campaign filter route +Route::add('/admin/campaign/filter', function() { + check_login(); + + if (!isset($_POST['campaign_id'])) { + http_response_code(400); + echo json_encode(['error' => 'Missing campaign ID']); + exit; + } + + $campaignModel = new CampaignModel(); + $campaign = $campaignModel->get($_POST['campaign_id']); + + if (!$campaign) { + http_response_code(404); + echo json_encode(['error' => 'Campaign not found']); + exit; + } + + // Get the current filters + $filters = [ + 'campaign_name' => $_POST['campaign_name'] ?? null, + 'ad_set_name' => $_POST['ad_set_name'] ?? null, + 'ad_name' => $_POST['ad_name'] ?? null + ]; + + // Get filtered data + $filteredData = $campaignModel->getFilteredData($campaign, $filters); + + echo json_encode([ + 'data' => $filteredData + ]); + +}, 'post'); + +Route::add('/privacy-policy', function () { + include_once __DIR__ . '/privacy-policy.php'; +}, 'get'); + +Route::add('/terms', function () { + include_once __DIR__ . '/terms.php'; +}, 'get'); + + + +include_once 'client-routes.php'; +include_once 'cal.php'; +include_once 'oauth-routes.php'; + +Route::run('/'); + + diff --git a/layout/.DS_Store b/layout/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..a3f920448bd0edd79f4fc6b73fe8125834373997 GIT binary patch literal 6148 zcmeHKu};G<5WOoE3WcE~qko|rV_JoU38_B-X`mevxuUS=3-}klfr-!HFBo~}vx?KS zVncxLqVsdkclP-?vST9R*;6$k8WT|l6&#$P*dx*|+LJ-FjDQ^axTA+^v#3k&>z=n6 zej@|2cE@y29?v5s>xTvHHq^_>eBqn5&Vt`|+Z$gLW$jDYvyY4C^6KSv+V`ox^;xgd zapc299xJ4FOI1I?q@kPO+52u_n&9;`NigZ7@m~oZ^4XK1Bxzy{7z4(@-(f(ucu01t zXr(b=3>X7j24sJ5P{BN6s~ENp6yXX09Ksv~_3+CEbYcMJ5nDxAAgrN44dr;nU=4@e zMZY{^tEl0`VPy0ZN9K4#;V?SvF1QotidGr}#z2>WBYhpq`G0x%{@)F!3GK5%Fskw;`y6qZqz&6rV$b!0x01%p + +
    + +
    + + + \ No newline at end of file diff --git a/layout/footer/Clientnone_footer.php b/layout/footer/Clientnone_footer.php new file mode 100644 index 0000000..7ea3eeb --- /dev/null +++ b/layout/footer/Clientnone_footer.php @@ -0,0 +1,8 @@ + + +
    + +
    + + + \ No newline at end of file diff --git a/layout/header/Adminleft_sidebar.php b/layout/header/Adminleft_sidebar.php new file mode 100644 index 0000000..6519f84 --- /dev/null +++ b/layout/header/Adminleft_sidebar.php @@ -0,0 +1,170 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    + + + + +
    + +
    \ No newline at end of file diff --git a/layout/header/Clientleft_sidebar.php b/layout/header/Clientleft_sidebar.php new file mode 100644 index 0000000..846b88b --- /dev/null +++ b/layout/header/Clientleft_sidebar.php @@ -0,0 +1,170 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    + + + + +
    + +
    \ No newline at end of file diff --git a/lib/ghl/calendar.php b/lib/ghl/calendar.php new file mode 100644 index 0000000..56ae1a5 --- /dev/null +++ b/lib/ghl/calendar.php @@ -0,0 +1,84 @@ +get_config(); + +$oauth = new GHLOAuth2($config); + +class GHLCalendar { + private $access_token; + private $refresh_token; + private $token_expiry; + private $oauth; + private $project_id; + + public function __construct($project_id, $access_token, $refresh_token) { + $this->project_id = $project_id; + $this->access_token = $access_token; + $this->refresh_token = $refresh_token; + $this->oauth = $GLOBALS['oauth']; + } + + public function refreshToken() { + $result = $this->oauth->refreshToken($this->refresh_token); + + + if ($result['success']) { + $this->access_token = $result['data']['access_token']; + $this->refresh_token = $result['data']['refresh_token'] ?? $this->refresh_token; + + $projectModel = new ProjectModel(); + $projectModel->edit([ + 'access_token' => $this->access_token, + 'refresh_token' => $this->refresh_token + ], $this->project_id ); + + return [ + 'code' => 200, + 'message' => 'Access token refreshed successfully', + 'data' => $result['data'] + ]; + } else { + return [ + 'code' => 400, + 'message' => 'Error: ' . $result['error'] . '. Please authorize', + 'data' => null + ]; + } + } + + public function getFreeSlots($calendar_id, $start_date, $end_date) { + + + $url = "https://services.leadconnectorhq.com/calendars/$calendar_id/free-slots?startDate=$start_date&endDate=$end_date"; + $headers = [ + "Accept: application/json", + "Authorization: Bearer " . $this->access_token, + "Version: 2021-04-15" + ]; + + // Initialize cURL session + $ch = curl_init($url); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + curl_setopt($ch, CURLOPT_HTTPHEADER, $headers); + + // Execute cURL request + $response = curl_exec($ch); + $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); // Get the HTTP response code + curl_close($ch); + + // Decode the response + $responseData = json_decode($response, true); + + // Prepare the return array + return [ + 'code' => $httpCode, + 'message' => $responseData['message'] ?? 'Success', // Default message if not present + 'data' => $responseData + ]; + } +} \ No newline at end of file diff --git a/lib/ghl/oauth2.php b/lib/ghl/oauth2.php new file mode 100644 index 0000000..066e559 --- /dev/null +++ b/lib/ghl/oauth2.php @@ -0,0 +1,138 @@ + +config = $config; + } + + public function getAuthorizationUrl() { + $params = [ + 'response_type' => 'code', + 'redirect_uri' => $this->config['gohighlevel_redirect_uri'] ?? $this->redirectUri, + 'client_id' => $this->config['gohighlevel_client_id'] ?? $this->clientId, + 'scope' => implode(' ', $this->scopes), + 'user_type' => 'Location' + ]; + + return $this->authorizeUrl . '?' . http_build_query($params); + } + + /* + Gohighlevel returns the code in the redirect uri ?code=xxxx + */ + + public function getAccessToken($code, $redirectUri = null) { + $data = [ + 'client_id' => $this->config['gohighlevel_client_id'] ?? $this->clientId, + 'client_secret' => $this->config['gohighlevel_client_secret'] ?? $this->clientSecret, + 'grant_type' => 'authorization_code', + 'code' => $code, + 'user_type' => 'Location' + ]; + + if ($redirectUri) { + $data['redirect_uri'] = $redirectUri; + } + + return $this->makeTokenRequest($data); + } + + public function refreshToken($refreshToken) { + if (empty($refreshToken)) { + throw new Exception('Refresh token is required'); + } + + + $data = [ + 'client_id' => $this->config['gohighlevel_client_id'] ?? $this->clientId, + 'client_secret' => $this->config['gohighlevel_client_secret'] ?? $this->clientSecret, + 'grant_type' => 'refresh_token', + 'refresh_token' => $refreshToken, + 'user_type' => 'Location' + ]; + + return $this->makeTokenRequest($data); + } + + private function makeTokenRequest($data) { + $curl = curl_init(); + + $options = [ + CURLOPT_URL => $this->baseUrl . '/oauth/token', + CURLOPT_RETURNTRANSFER => true, + CURLOPT_ENCODING => '', + CURLOPT_MAXREDIRS => 10, + CURLOPT_TIMEOUT => 30, + CURLOPT_FOLLOWLOCATION => true, + CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1, + CURLOPT_CUSTOMREQUEST => 'POST', + CURLOPT_POSTFIELDS => http_build_query($data), + CURLOPT_HTTPHEADER => [ + 'Accept: application/json', + 'Content-Type: application/x-www-form-urlencoded' + ], + ]; + + curl_setopt_array($curl, $options); + + $response = curl_exec($curl); + $httpCode = curl_getinfo($curl, CURLINFO_HTTP_CODE); + $error = curl_error($curl); + + curl_close($curl); + + if ($error) { + return [ + 'success' => false, + 'error' => 'Curl error: ' . $error + ]; + } + + $responseData = json_decode($response, true); + if ($responseData === null) { + return [ + 'success' => false, + 'error' => 'Error decoding JSON response' + ]; + } + + + + if ($httpCode !== 200) { + return [ + 'success' => false, + 'error' => $responseData['message'] ?? 'Unknown error occurred', + 'code' => $httpCode, + 'raw_response' => $responseData + ]; + } + + // Ensure we always return refresh_token if it exists in the response + return [ + 'success' => true, + 'data' => [ + 'access_token' => $responseData['access_token'] ?? null, + 'refresh_token' => $responseData['refresh_token'] ?? null, + 'expires_in' => $responseData['expires_in'] ?? null, + 'locationId' => $responseData['locationId'] ?? null, + 'companyId' => $responseData['companyId'] ?? null, + 'userId' => $responseData['userId'] ?? null, + 'scope' => $responseData['scope'] ?? null, + 'token_type' => $responseData['token_type'] ?? null + ], + 'raw_response' => $responseData + ]; + } +} diff --git a/lib/google/drive.php b/lib/google/drive.php new file mode 100644 index 0000000..f6f708f --- /dev/null +++ b/lib/google/drive.php @@ -0,0 +1,185 @@ +oauth = $oauth; + $this->oauth->addScopes(self::DRIVE_SCOPES); + } + + /** + * List files and folders in Google Drive + * @param string|null $folderId Parent folder ID (null for root) + * @param array $options Additional query parameters + * @return array + * @throws \Exception + */ + public function listFiles(?string $folderId = null, array $options = []): array { + try { + $query = []; + + if ($folderId) { + $query[] = "'{$folderId}' in parents"; + } else { + $query[] = "'root' in parents"; + } + + // Add trashed=false to exclude deleted files + $query[] = "trashed=false"; + + // Handle mime type filtering through options + if (isset($options['mimeTypes'])) { + $mimeTypes = array_map(function($type) { + return "mimeType='" . $type . "'"; + }, $options['mimeTypes']); + $query[] = '(' . implode(' or ', $mimeTypes) . ')'; + unset($options['mimeTypes']); // Remove from options to avoid duplication in params + } + + $params = array_merge([ + 'fields' => 'files(id, name, mimeType, size, modifiedTime, parents)', + 'orderBy' => 'folder,name', // Sort folders first, then by name + 'q' => implode(' and ', $query) + ], $options); + + $response = $this->oauth->makeAuthenticatedRequest( + self::DRIVE_API_BASE . '/files?' . http_build_query($params) + ); + + return $response; + } catch (\Exception $e) { + if ($this->isAuthenticationError($e)) { + throw new \Exception('Authentication required. Use getAuthorizationUrl() to start the auth flow.', 401); + } + throw $e; + } + } + + /** + * Download a file by its ID + * @param string $fileId + * @param string|null $mimeType Optional mime type for export (useful for Google Docs) + * @return string File content + * @throws \Exception + */ + public function downloadFile(string $fileId, ?string $mimeType = null): string { + try { + // First, get file metadata to determine if it's a Google Workspace file + $file = $this->getFile($fileId); + $isGoogleWorkspaceFile = substr($file['mimeType'], 0, 23) === 'application/vnd.google-apps'; + + // For Google Sheets, always use export + if ($file['mimeType'] === 'application/vnd.google-apps.spreadsheet') { + $endpoint = self::DRIVE_API_BASE . '/files/' . $fileId . '/export'; + $endpoint .= '?mimeType=' . urlencode('text/csv'); + + return $this->oauth->makeAuthenticatedRequest( + $endpoint, + 'GET', + ['Accept: text/csv'], + null, + true + ); + } + + // For other files, use regular download + $endpoint = self::DRIVE_API_BASE . '/files/' . $fileId; + if ($isGoogleWorkspaceFile && $mimeType) { + $endpoint .= '/export'; + $endpoint .= '?mimeType=' . urlencode($mimeType); + } else { + $endpoint .= '?alt=media'; + } + + return $this->oauth->makeAuthenticatedRequest( + $endpoint, + 'GET', + ['Accept: */*'], + null, + true + ); + } catch (\Exception $e) { + if ($this->isAuthenticationError($e)) { + throw new \Exception('Authentication required. Use getAuthorizationUrl() to start the auth flow.', 401); + } + throw $e; + } + } + + /** + * Get file metadata + * @param string $fileId + * @return array + * @throws \Exception + */ + public function getFile(string $fileId): array { + try { + return $this->oauth->makeAuthenticatedRequest( + self::DRIVE_API_BASE . '/files/' . $fileId . '?fields=id,name,mimeType,size,modifiedTime,parents' + ); + } catch (\Exception $e) { + if ($this->isAuthenticationError($e)) { + throw new \Exception('Authentication required. Use getAuthorizationUrl() to start the auth flow.', 401); + } + throw $e; + } + } + + /** + * Get authorization URL to start OAuth flow + * @param array $additionalParams + * @return string + */ + public function getAuthorizationUrl(array $additionalParams = []): string { + return $this->oauth->getAuthorizationUrl($additionalParams); + } + + /** + * Check if the error is related to authentication + * @param \Exception $e + * @return bool + */ + private function isAuthenticationError(\Exception $e): bool { + return $e->getCode() === 401 || $e->getCode() === 403; + } + + /** + * Get common MIME types for Google Workspace files + * @return array + */ + public static function getCommonExportMimeTypes(): array { + return [ + 'application/vnd.google-apps.spreadsheet' => [ + 'xlsx' => 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', + 'csv' => 'text/csv', + 'pdf' => 'application/pdf' + ], + 'application/vnd.google-apps.document' => [ + 'docx' => 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', + 'pdf' => 'application/pdf', + 'txt' => 'text/plain' + ], + 'application/vnd.google-apps.presentation' => [ + 'pptx' => 'application/vnd.openxmlformats-officedocument.presentationml.presentation', + 'pdf' => 'application/pdf' + ] + ]; + } +} + +// Initialize the service +// $oauth = new GoogleOAuth2($clientId, $clientSecret, $redirectUri); +// $drive = new GoogleDrive($oauth); + diff --git a/lib/google/oauth2.php b/lib/google/oauth2.php new file mode 100644 index 0000000..69d1d76 --- /dev/null +++ b/lib/google/oauth2.php @@ -0,0 +1,207 @@ +clientId = $clientId; + $this->clientSecret = $clientSecret; + $this->redirectUri = $redirectUri; + } + + /** + * Add scopes for the OAuth2 request + * @param array $scopes Array of Google OAuth2 scopes + * @return self + */ + public function addScopes(array $scopes): self { + $this->scopes = array_merge($this->scopes, $scopes); + return $this; + } + + /** + * Generate the authorization URL + * @param array $additionalParams Additional URL parameters + * @return string + */ + public function getAuthorizationUrl(array $additionalParams = []): string { + $params = array_merge([ + 'client_id' => $this->clientId, + 'redirect_uri' => $this->redirectUri, + 'response_type' => 'code', + 'access_type' => 'offline', + 'prompt' => 'consent', + 'scope' => implode(' ', array_unique($this->scopes)) + ], $additionalParams); + + return self::AUTH_URL . '?' . http_build_query($params); + } + + /** + * Exchange authorization code for tokens + * @param string $code Authorization code + * @return array Token response + * @throws \Exception + */ + public function exchangeCode(string $code): array { + $response = $this->makeTokenRequest([ + 'grant_type' => 'authorization_code', + 'code' => $code, + 'redirect_uri' => $this->redirectUri + ]); + + $this->setTokens($response); + return $response; + } + + /** + * Refresh the access token + * @throws \Exception + */ + public function refreshAccessToken(): array { + if (!$this->refreshToken) { + throw new \Exception('No refresh token available'); + } + + $response = $this->makeTokenRequest([ + 'grant_type' => 'refresh_token', + 'refresh_token' => $this->refreshToken + ]); + + $this->setTokens($response); + return $response; + } + + /** + * Make an authenticated request to Google APIs + * @param string $url + * @param string $method + * @param array $headers + * @param mixed $body + * @param bool $rawResponse Whether to return raw response instead of JSON + * @return array|string + * @throws \Exception + */ + public function makeAuthenticatedRequest( + string $url, + string $method = 'GET', + array $headers = [], + $body = null, + bool $rawResponse = false + ){ + if ($this->isTokenExpired()) { + $this->refreshAccessToken(); + } + + $headers = array_merge([ + 'Authorization: Bearer ' . $this->accessToken, + 'Accept: application/json' + ], $headers); + + $ch = curl_init(); + curl_setopt_array($ch, [ + CURLOPT_URL => $url, + CURLOPT_RETURNTRANSFER => true, + CURLOPT_HTTPHEADER => $headers, + CURLOPT_CUSTOMREQUEST => $method + ]); + + if ($body && in_array($method, ['POST', 'PUT', 'PATCH'])) { + curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($body)); + } + + $response = curl_exec($ch); + $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); + curl_close($ch); + + if ($httpCode >= 400) { + throw new \Exception('Request failed with status ' . $httpCode . ': ' . $response); + } + + return $rawResponse ? $response : json_decode($response, true); + } + + /** + * Set tokens from OAuth2 response + * @param array $response + */ + private function setTokens(array $response): void { + $this->accessToken = $response['access_token']; + if (isset($response['refresh_token'])) { + $this->refreshToken = $response['refresh_token']; + } + $this->tokenExpires = time() + ($response['expires_in'] ?? 3600); + } + + /** + * Check if the current access token is expired + * @return bool + */ + private function isTokenExpired(): bool { + return !$this->accessToken || !$this->tokenExpires || time() >= $this->tokenExpires; + } + + /** + * Make a token request to Google's OAuth2 server + * @param array $params + * @return array + * @throws \Exception + */ + private function makeTokenRequest(array $params): array { + $params = array_merge([ + 'client_id' => $this->clientId, + 'client_secret' => $this->clientSecret + ], $params); + + $ch = curl_init(); + curl_setopt_array($ch, [ + CURLOPT_URL => self::TOKEN_URL, + CURLOPT_RETURNTRANSFER => true, + CURLOPT_POST => true, + CURLOPT_POSTFIELDS => http_build_query($params), + CURLOPT_HTTPHEADER => ['Content-Type: application/x-www-form-urlencoded'] + ]); + + $response = curl_exec($ch); + $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); + curl_close($ch); + + if ($httpCode !== 200) { + throw new \Exception('Token request failed with status ' . $httpCode . ': ' . $response); + } + + return json_decode($response, true); + } + + // Getters for tokens and expiration + public function getAccessToken(): ?string { + return $this->accessToken; + } + + public function setAccessToken(string $accessToken): void { + $this->accessToken = $accessToken; + } + + public function getRefreshToken(): ?string { + return $this->refreshToken; + } + + public function setRefreshToken(string $refreshToken): void { + $this->refreshToken = $refreshToken; + } + + public function getTokenExpires(): ?int { + return $this->tokenExpires; + } +} diff --git a/lib/redbean/rb-mysql.php b/lib/redbean/rb-mysql.php new file mode 100644 index 0000000..566c96b --- /dev/null +++ b/lib/redbean/rb-mysql.php @@ -0,0 +1,17094 @@ +mode === self::C_LOGGER_ECHO ) { + echo $log; + } else { + $this->logs[] = $log; + } + } else { + if ( $this->mode === self::C_LOGGER_ECHO ) { + echo $argument; + } else { + $this->logs[] = $argument; + } + } + + if ( $this->mode === self::C_LOGGER_ECHO ) echo "
    " . PHP_EOL; + } + } + + /** + * Returns the internal log array. + * The internal log array is where all log messages are stored. + * + * @return array + */ + public function getLogs() + { + return $this->logs; + } + + /** + * Clears the internal log array, removing all + * previously stored entries. + * + * @return self + */ + public function clear() + { + $this->logs = array(); + return $this; + } + + /** + * Selects a logging mode. + * There are several options available. + * + * * C_LOGGER_ARRAY - log silently, stores entries in internal log array only + * * C_LOGGER_ECHO - also forward log messages directly to STDOUT + * + * @param integer $mode mode of operation for logging object + * + * @return self + */ + public function setMode( $mode ) + { + if ($mode !== self::C_LOGGER_ARRAY && $mode !== self::C_LOGGER_ECHO ) { + throw new RedException( 'Invalid mode selected for logger, use C_LOGGER_ARRAY or C_LOGGER_ECHO.' ); + } + $this->mode = $mode; + return $this; + } + + /** + * Searches for all log entries in internal log array + * for $needle and returns those entries. + * This method will return an array containing all matches for your + * search query. + * + * @param string $needle phrase to look for in internal log array + * + * @return array + */ + public function grep( $needle ) + { + $found = array(); + foreach( $this->logs as $logEntry ) { + if ( strpos( $logEntry, $needle ) !== FALSE ) $found[] = $logEntry; + } + return $found; + } +} +} + +namespace RedBeanPHP\Logger\RDefault { + +use RedBeanPHP\Logger as Logger; +use RedBeanPHP\Logger\RDefault as RDefault; +use RedBeanPHP\RedException as RedException; + +/** + * Debug logger. + * A special logger for debugging purposes. + * Provides debugging logging functions for RedBeanPHP. + * + * @file RedBeanPHP/Logger/RDefault/Debug.php + * @author Gabor de Mooij and the RedBeanPHP Community + * @license BSD/GPLv2 + * + * @copyright + * copyright (c) G.J.G.T. (Gabor) de Mooij + * This source file is subject to the BSD/GPLv2 License that is bundled + * with this source code in the file license.txt. + */ +class Debug extends RDefault implements Logger +{ + /** + * @var integer + */ + protected $strLen = 40; + + /** + * @var boolean + */ + protected static $noCLI = FALSE; + + /** + * @var boolean + */ + protected $flagUseStringOnlyBinding = FALSE; + + /** + * Toggles CLI override. By default debugging functions will + * output differently based on PHP_SAPI values. This function + * allows you to override the PHP_SAPI setting. If you set + * this to TRUE, CLI output will be supressed in favour of + * HTML output. So, to get HTML on the command line use + * setOverrideCLIOutput( TRUE ). + * + * @param boolean $yesNo CLI-override setting flag + * + * @return void + */ + public static function setOverrideCLIOutput( $yesNo ) + { + self::$noCLI = $yesNo; + } + + /** + * Writes a query for logging with all bindings / params filled + * in. + * + * @param string $newSql the query + * @param array $newBindings the bindings to process (key-value pairs) + * + * @return string + */ + protected function writeQuery( $newSql, $newBindings ) + { + //avoid str_replace collisions: slot1 and slot10 (issue 407). + uksort( $newBindings, function( $a, $b ) { + return ( strlen( $b ) - strlen( $a ) ); + } ); + + $newStr = $newSql; + foreach( $newBindings as $slot => $value ) { + if ( strpos( $slot, ':' ) === 0 ) { + $newStr = str_replace( $slot, $this->fillInValue( $value ), $newStr ); + } + } + return $newStr; + } + + /** + * Fills in a value of a binding and truncates the + * resulting string if necessary. + * + * @param mixed $value bound value + * + * @return string + */ + protected function fillInValue( $value ) + { + if ( is_array( $value ) && count( $value ) == 2 ) { + $paramType = end( $value ); + $value = reset( $value ); + } else { + $paramType = NULL; + } + + if ( is_null( $value ) ) $value = 'NULL'; + + if ( $this->flagUseStringOnlyBinding ) $paramType = \PDO::PARAM_STR; + + if ( $paramType != \PDO::PARAM_INT && $paramType != \PDO::PARAM_STR ) { + if ( \RedBeanPHP\QueryWriter\AQueryWriter::canBeTreatedAsInt( $value ) || $value === 'NULL') { + $paramType = \PDO::PARAM_INT; + } else { + $paramType = \PDO::PARAM_STR; + } + } + + if ( strlen( $value ) > ( $this->strLen ) ) { + $value = substr( $value, 0, ( $this->strLen ) ).'... '; + } + + if ($paramType === \PDO::PARAM_STR) { + $value = '\''.$value.'\''; + } + + return $value; + } + + /** + * Dependending on the current mode of operation, + * this method will either log and output to STDIN or + * just log. + * + * Depending on the value of constant PHP_SAPI this function + * will format output for console or HTML. + * + * @param string $str string to log or output and log + * + * @return void + */ + protected function output( $str ) + { + $this->logs[] = $str; + if ( !$this->mode ) { + $highlight = FALSE; + /* just a quick heuritsic to highlight schema changes */ + if ( strpos( $str, 'CREATE' ) === 0 + || strpos( $str, 'ALTER' ) === 0 + || strpos( $str, 'DROP' ) === 0) { + $highlight = TRUE; + } + if (PHP_SAPI === 'cli' && !self::$noCLI) { + if ($highlight) echo "\e[91m"; + echo $str, PHP_EOL; + echo "\e[39m"; + } else { + if ($highlight) { + echo "{$str}"; + } else { + echo $str; + } + echo '
    '; + } + } + } + + /** + * Normalizes the slots in an SQL string. + * Replaces question mark slots with :slot1 :slot2 etc. + * + * @param string $sql sql to normalize + * + * @return string + */ + protected function normalizeSlots( $sql ) + { + $newSql = $sql; + $i = 0; + while(strpos($newSql, '?') !== FALSE ){ + $pos = strpos( $newSql, '?' ); + $slot = ':slot'.$i; + $begin = substr( $newSql, 0, $pos ); + $end = substr( $newSql, $pos+1 ); + if (PHP_SAPI === 'cli' && !self::$noCLI) { + $newSql = "{$begin}\e[32m{$slot}\e[39m{$end}"; + } else { + $newSql = "{$begin}$slot{$end}"; + } + $i ++; + } + return $newSql; + } + + /** + * Normalizes the bindings. + * Replaces numeric binding keys with :slot1 :slot2 etc. + * + * @param array $bindings bindings to normalize + * + * @return array + */ + protected function normalizeBindings( $bindings ) + { + $i = 0; + $newBindings = array(); + foreach( $bindings as $key => $value ) { + if ( is_numeric($key) ) { + $newKey = ':slot'.$i; + $newBindings[$newKey] = $value; + $i++; + } else { + $newBindings[$key] = $value; + } + } + return $newBindings; + } + + /** + * Logger method. + * + * Takes a number of arguments tries to create + * a proper debug log based on the available data. + * + * @return void + */ + public function log() + { + if ( func_num_args() < 1 ) return; + + $sql = func_get_arg( 0 ); + + if ( func_num_args() < 2) { + $bindings = array(); + } else { + $bindings = func_get_arg( 1 ); + } + + if ( !is_array( $bindings ) ) { + return $this->output( $sql ); + } + + $newSql = $this->normalizeSlots( $sql ); + $newBindings = $this->normalizeBindings( $bindings ); + $newStr = $this->writeQuery( $newSql, $newBindings ); + $this->output( $newStr ); + } + + /** + * Sets the max string length for the parameter output in + * SQL queries. Set this value to a reasonable number to + * keep you SQL queries readable. + * + * @param integer $len string length + * + * @return self + */ + public function setParamStringLength( $len = 20 ) + { + $this->strLen = max(0, $len); + return $this; + } + + /** + * Whether to bind all parameters as strings. + * If set to TRUE this will cause all integers to be bound as STRINGS. + * This will NOT affect NULL values. + * + * @param boolean $yesNo pass TRUE to bind all parameters as strings. + * + * @return self + */ + public function setUseStringOnlyBinding( $yesNo = false ) + { + $this->flagUseStringOnlyBinding = (boolean) $yesNo; + return $this; + } +} +} + +namespace RedBeanPHP { + +/** + * Interface for database drivers. + * The Driver API conforms to the ADODB pseudo standard + * for database drivers. + * + * @file RedBeanPHP/Driver.php + * @author Gabor de Mooij and the RedBeanPHP Community + * @license BSD/GPLv2 + * + * @copyright + * copyright (c) G.J.G.T. (Gabor) de Mooij and the RedBeanPHP Community + * This source file is subject to the BSD/GPLv2 License that is bundled + * with this source code in the file license.txt. + */ +interface Driver +{ + /** + * Runs a query and fetches results as a multi dimensional array. + * + * @param string $sql SQL query to execute + * @param array $bindings list of values to bind to SQL snippet + * + * @return array + */ + public function GetAll( $sql, $bindings = array() ); + + /** + * Runs a query and fetches results as a column. + * + * @param string $sql SQL query to execute + * @param array $bindings list of values to bind to SQL snippet + * + * @return array + */ + public function GetCol( $sql, $bindings = array() ); + + /** + * Runs a query and returns results as a single cell. + * + * @param string $sql SQL query to execute + * @param array $bindings list of values to bind to SQL snippet + * + * @return mixed + */ + public function GetOne( $sql, $bindings = array() ); + + /** + * Runs a query and returns results as an associative array + * indexed by the first column. + * + * @param string $sql SQL query to execute + * @param array $bindings list of values to bind to SQL snippet + * + * @return mixed + */ + public function GetAssocRow( $sql, $bindings = array() ); + + /** + * Runs a query and returns a flat array containing the values of + * one row. + * + * @param string $sql SQL query to execute + * @param array $bindings list of values to bind to SQL snippet + * + * @return array + */ + public function GetRow( $sql, $bindings = array() ); + + /** + * Executes SQL code and allows key-value binding. + * This function allows you to provide an array with values to bind + * to query parameters. For instance you can bind values to question + * marks in the query. Each value in the array corresponds to the + * question mark in the query that matches the position of the value in the + * array. You can also bind values using explicit keys, for instance + * array(":key"=>123) will bind the integer 123 to the key :key in the + * SQL. This method has no return value. + * + * @param string $sql SQL query to execute + * @param array $bindings list of values to bind to SQL snippet + * + * @return array Affected Rows + */ + public function Execute( $sql, $bindings = array() ); + + /** + * Returns the latest insert ID if driver does support this + * feature. + * + * @return integer + */ + public function GetInsertID(); + + /** + * Returns the number of rows affected by the most recent query + * if the currently selected driver driver supports this feature. + * + * @return integer + */ + public function Affected_Rows(); + + /** + * Returns a cursor-like object from the database. + * + * @param string $sql SQL query to execute + * @param array $bindings list of values to bind to SQL snippet + * + * @return mixed + */ + public function GetCursor( $sql, $bindings = array() ); + + /** + * Toggles debug mode. In debug mode the driver will print all + * SQL to the screen together with some information about the + * results. + * + * This method is for more fine-grained control. Normally + * you should use the facade to start the query debugger for + * you. The facade will manage the object wirings necessary + * to use the debugging functionality. + * + * Usage (through facade): + * + * + * R::debug( TRUE ); + * ...rest of program... + * R::debug( FALSE ); + * + * + * The example above illustrates how to use the RedBeanPHP + * query debugger through the facade. + * + * @param boolean $trueFalse turn on/off + * @param Logger $logger logger instance + * + * @return void + */ + public function setDebugMode( $tf, $customLogger ); + + /** + * Starts a transaction. + * + * @return void + */ + public function CommitTrans(); + + /** + * Commits a transaction. + * + * @return void + */ + public function StartTrans(); + + /** + * Rolls back a transaction. + * + * @return void + */ + public function FailTrans(); + + /** + * Resets the internal Query Counter. + * + * @return self + */ + public function resetCounter(); + + /** + * Returns the number of SQL queries processed. + * + * @return integer + */ + public function getQueryCount(); + + /** + * Sets initialization code for connection. + * + * @param callable $code code + * + * @return void + */ + public function setInitCode( $code ); + + /** + * Returns the version string from the database server. + * + * @return string + */ + public function DatabaseServerVersion(); +} +} + +namespace RedBeanPHP\Driver { + +use RedBeanPHP\Driver as Driver; +use RedBeanPHP\Logger as Logger; +use RedBeanPHP\QueryWriter\AQueryWriter as AQueryWriter; +use RedBeanPHP\RedException as RedException; +use RedBeanPHP\RedException\SQL as SQL; +use RedBeanPHP\Logger\RDefault as RDefault; +use RedBeanPHP\PDOCompatible as PDOCompatible; +use RedBeanPHP\Cursor\PDOCursor as PDOCursor; + +/** + * PDO Driver + * This Driver implements the RedBean Driver API. + * for RedBeanPHP. This is the standard / default database driver + * for RedBeanPHP. + * + * @file RedBeanPHP/PDO.php + * @author Gabor de Mooij and the RedBeanPHP Community, Desfrenes + * @license BSD/GPLv2 + * + * @copyright + * copyright (c) Desfrenes & Gabor de Mooij and the RedBeanPHP community + * This source file is subject to the BSD/GPLv2 License that is bundled + * with this source code in the file license.txt. + */ +class RPDO implements Driver +{ + /** + * @var integer + */ + protected $max; + + /** + * @var string + */ + protected $dsn; + + /** + * @var boolean + */ + protected $loggingEnabled = FALSE; + + /** + * @var Logger + */ + protected $logger = NULL; + + /** + * @var PDO + */ + protected $pdo; + + /** + * @var integer + */ + protected $affectedRows; + + /** + * @var integer + */ + protected $resultArray; + + /** + * @var array + */ + protected $connectInfo = array(); + + /** + * @var boolean + */ + protected $isConnected = FALSE; + + /** + * @var bool + */ + protected $flagUseStringOnlyBinding = FALSE; + + /** + * @var integer + */ + protected $queryCounter = 0; + + /** + * @var string + */ + protected $mysqlCharset = ''; + + /** + * @var string + */ + protected $mysqlCollate = ''; + + /** + * @var boolean + */ + protected $stringifyFetches = TRUE; + + /** + * @var string + */ + protected $initSQL = NULL; + + /** + * @var callable + */ + protected $initCode = NULL; + + /** + * Binds parameters. This method binds parameters to a PDOStatement for + * Query Execution. This method binds parameters as NULL, INTEGER or STRING + * and supports both named keys and question mark keys. + * + * @param PDOStatement $statement PDO Statement instance + * @param array $bindings values that need to get bound to the statement + * + * @return void + */ + protected function bindParams( $statement, $bindings ) + { + foreach ( $bindings as $key => &$value ) { + $k = is_integer( $key ) ? $key + 1 : $key; + + if ( is_array( $value ) && count( $value ) == 2 ) { + $paramType = end( $value ); + $value = reset( $value ); + } else { + $paramType = NULL; + } + + if ( is_null( $value ) ) { + $statement->bindValue( $k, NULL, \PDO::PARAM_NULL ); + continue; + } + + if ( $paramType != \PDO::PARAM_INT && $paramType != \PDO::PARAM_STR ) { + if ( !$this->flagUseStringOnlyBinding && AQueryWriter::canBeTreatedAsInt( $value ) && abs( $value ) <= $this->max ) { + $paramType = \PDO::PARAM_INT; + } else { + $paramType = \PDO::PARAM_STR; + } + } + + $statement->bindParam( $k, $value, $paramType ); + } + } + + /** + * This method runs the actual SQL query and binds a list of parameters to the query. + * slots. The result of the query will be stored in the protected property + * $rs (always array). The number of rows affected (result of rowcount, if supported by database) + * is stored in protected property $affectedRows. If the debug flag is set + * this function will send debugging output to screen buffer. + * + * @param string $sql the SQL string to be send to database server + * @param array $bindings the values that need to get bound to the query slots + * @param array $options + * + * @return mixed + * @throws SQL + */ + protected function runQuery( $sql, $bindings, $options = array() ) + { + $this->connect(); + if ( $this->loggingEnabled && $this->logger ) { + $this->logger->log( $sql, $bindings ); + } + try { + if ( strpos( 'pgsql', $this->dsn ) === 0 ) { + if (defined('\\PDO::PGSQL_ATTR_DISABLE_NATIVE_PREPARED_STATEMENT')) { + $statement = @$this->pdo->prepare($sql, array(\PDO::PGSQL_ATTR_DISABLE_NATIVE_PREPARED_STATEMENT => TRUE)); + } else { + $statement = $this->pdo->prepare($sql); + } + } else { + $statement = $this->pdo->prepare( $sql ); + } + $this->bindParams( $statement, $bindings ); + $statement->execute(); + $this->queryCounter ++; + $this->affectedRows = $statement->rowCount(); + if ( $statement->columnCount() ) { + $fetchStyle = ( isset( $options['fetchStyle'] ) ) ? $options['fetchStyle'] : NULL; + if ( isset( $options['noFetch'] ) && $options['noFetch'] ) { + $this->resultArray = array(); + return $statement; + } + $this->resultArray = $statement->fetchAll( $fetchStyle ); + if ( $this->loggingEnabled && $this->logger ) { + $this->logger->log( 'resultset: ' . count( $this->resultArray ) . ' rows' ); + } + } else { + $this->resultArray = array(); + } + } catch ( \PDOException $e ) { + //Unfortunately the code field is supposed to be int by default (php) + //So we need a property to convey the SQL State code. + $err = $e->getMessage(); + if ( $this->loggingEnabled && $this->logger ) $this->logger->log( 'An error occurred: ' . $err ); + $exception = new SQL( $err, 0, $e ); + $exception->setSQLState( $e->getCode() ); + $exception->setDriverDetails( $e->errorInfo ); + throw $exception; + } + } + + /** + * Try to fix MySQL character encoding problems. + * MySQL < 5.5.3 does not support proper 4 byte unicode but they + * seem to have added it with version 5.5.3 under a different label: utf8mb4. + * We try to select the best possible charset based on your version data. + * + * @return void + */ + protected function setEncoding() + { + $driver = $this->pdo->getAttribute( \PDO::ATTR_DRIVER_NAME ); + if ($driver === 'mysql') { + $charset = $this->hasCap( 'utf8mb4' ) ? 'utf8mb4' : 'utf8'; + $collate = $this->hasCap( 'utf8mb4_520' ) ? '_unicode_520_ci' : '_unicode_ci'; + $this->pdo->setAttribute(\PDO::MYSQL_ATTR_INIT_COMMAND, 'SET NAMES '. $charset ); //on every re-connect + /* #624 removed space before SET NAMES because it causes trouble with ProxySQL */ + $this->pdo->exec('SET NAMES '. $charset); //also for current connection + $this->mysqlCharset = $charset; + $this->mysqlCollate = $charset . $collate; + } + } + + /** + * Determine if a database supports a particular feature. + * Currently this function can be used to detect the following features: + * + * - utf8mb4 + * - utf8mb4 520 + * + * Usage: + * + * + * $this->hasCap( 'utf8mb4_520' ); + * + * + * By default, RedBeanPHP uses this method under the hood to make sure + * you use the latest UTF8 encoding possible for your database. + * + * @param $db_cap identifier of database capability + * + * @return int|false Whether the database feature is supported, FALSE otherwise. + **/ + protected function hasCap( $db_cap ) + { + $compare = FALSE; + $version = $this->pdo->getAttribute( \PDO::ATTR_SERVER_VERSION ); + switch ( strtolower( $db_cap ) ) { + case 'utf8mb4': + //oneliner, to boost code coverage (coverage does not span versions) + if ( version_compare( $version, '5.5.3', '<' ) ) { return FALSE; } + $client_version = $this->pdo->getAttribute(\PDO::ATTR_CLIENT_VERSION ); + /* + * libmysql has supported utf8mb4 since 5.5.3, same as the MySQL server. + * mysqlnd has supported utf8mb4 since 5.0.9. + */ + if ( strpos( $client_version, 'mysqlnd' ) !== FALSE ) { + $client_version = preg_replace( '/^\D+([\d.]+).*/', '$1', $client_version ); + $compare = version_compare( $client_version, '5.0.9', '>=' ); + } else { + $compare = version_compare( $client_version, '5.5.3', '>=' ); + } + break; + case 'utf8mb4_520': + $compare = version_compare( $version, '5.6', '>=' ); + break; + } + + return $compare; + } + + /** + * Constructor. You may either specify dsn, user and password or + * just give an existing PDO connection. + * + * Usage: + * + * + * $driver = new RPDO( $dsn, $user, $password ); + * + * + * The example above illustrates how to create a driver + * instance from a database connection string (dsn), a username + * and a password. It's also possible to pass a PDO object. + * + * Usage: + * + * + * $driver = new RPDO( $existingConnection ); + * + * + * The second example shows how to create an RPDO instance + * from an existing PDO object. + * + * @param string|object $dsn database connection string + * @param string $user optional, usename to sign in + * @param string $pass optional, password for connection login + * + * @return void + */ + public function __construct( $dsn, $user = NULL, $pass = NULL, $options = array() ) + { + if ( is_object( $dsn ) ) { + $this->pdo = $dsn; + $this->isConnected = TRUE; + $this->setEncoding(); + $this->pdo->setAttribute( \PDO::ATTR_ERRMODE,\PDO::ERRMODE_EXCEPTION ); + $this->pdo->setAttribute( \PDO::ATTR_DEFAULT_FETCH_MODE,\PDO::FETCH_ASSOC ); + // make sure that the dsn at least contains the type + $this->dsn = $this->getDatabaseType(); + } else { + $this->dsn = $dsn; + $this->connectInfo = array( 'pass' => $pass, 'user' => $user ); + if (is_array($options)) $this->connectInfo['options'] = $options; + } + + //PHP 5.3 PDO SQLite has a bug with large numbers: + if ( ( strpos( $this->dsn, 'sqlite' ) === 0 && PHP_MAJOR_VERSION === 5 && PHP_MINOR_VERSION === 3 ) || defined('HHVM_VERSION') || $this->dsn === 'test-sqlite-53' ) { + $this->max = 2147483647; //otherwise you get -2147483648 ?! demonstrated in build #603 on Travis. + } elseif ( strpos( $this->dsn, 'cubrid' ) === 0 ) { + $this->max = 2147483647; //bindParam in pdo_cubrid also fails... + } else { + $this->max = PHP_INT_MAX; //the normal value of course (makes it possible to use large numbers in LIMIT clause) + } + } + + /** + * Sets PDO in stringify fetch mode. + * If set to TRUE, this method will make sure all data retrieved from + * the database will be fetched as a string. Default: TRUE. + * + * To set it to FALSE... + * + * Usage: + * + * + * R::getDatabaseAdapter()->getDatabase()->stringifyFetches( FALSE ); + * + * + * Important! + * Note, this method only works if you set the value BEFORE the connection + * has been establish. Also, this setting ONLY works with SOME drivers. + * It's up to the driver to honour this setting. + * + * @param boolean $bool + */ + public function stringifyFetches( $bool ) { + $this->stringifyFetches = $bool; + } + + /** + * Returns the best possible encoding for MySQL based on version data. + * This method can be used to obtain the best character set parameters + * possible for your database when constructing a table creation query + * containing clauses like: CHARSET=... COLLATE=... + * This is a MySQL-specific method and not part of the driver interface. + * + * Usage: + * + * + * $charset_collate = $this->adapter->getDatabase()->getMysqlEncoding( TRUE ); + * + * + * @param boolean $retCol pass TRUE to return both charset/collate + * + * @return string|array + */ + public function getMysqlEncoding( $retCol = FALSE ) + { + if( $retCol ) + return array( 'charset' => $this->mysqlCharset, 'collate' => $this->mysqlCollate ); + return $this->mysqlCharset; + } + + /** + * Whether to bind all parameters as strings. + * If set to TRUE this will cause all integers to be bound as STRINGS. + * This will NOT affect NULL values. + * + * @param boolean $yesNo pass TRUE to bind all parameters as strings. + * + * @return void + */ + public function setUseStringOnlyBinding( $yesNo ) + { + $this->flagUseStringOnlyBinding = (boolean) $yesNo; + if ( $this->loggingEnabled && $this->logger && method_exists($this->logger,'setUseStringOnlyBinding')) { + $this->logger->setUseStringOnlyBinding( $this->flagUseStringOnlyBinding ); + } + } + + /** + * Sets the maximum value to be bound as integer, normally + * this value equals PHP's MAX INT constant, however sometimes + * PDO driver bindings cannot bind large integers as integers. + * This method allows you to manually set the max integer binding + * value to manage portability/compatibility issues among different + * PHP builds. This method will return the old value. + * + * @param integer $max maximum value for integer bindings + * + * @return integer + */ + public function setMaxIntBind( $max ) + { + if ( !is_integer( $max ) ) throw new RedException( 'Parameter has to be integer.' ); + $oldMax = $this->max; + $this->max = $max; + return $oldMax; + } + + /** + * Sets initialization code to execute upon connecting. + * + * @param callable $code + * + * @return void + */ + public function setInitCode($code) + { + $this->initCode= $code; + } + + /** + * Establishes a connection to the database using PHP\PDO + * functionality. If a connection has already been established this + * method will simply return directly. This method also turns on + * UTF8 for the database and PDO-ERRMODE-EXCEPTION as well as + * PDO-FETCH-ASSOC. + * + * @return void + */ + public function connect() + { + if ( $this->isConnected ) return; + try { + $user = $this->connectInfo['user']; + $pass = $this->connectInfo['pass']; + $options = array(); + if (isset($this->connectInfo['options']) && is_array($this->connectInfo['options'])) { + $options = $this->connectInfo['options']; + } + $this->pdo = new \PDO( $this->dsn, $user, $pass, $options ); + $this->setEncoding(); + $this->pdo->setAttribute( \PDO::ATTR_STRINGIFY_FETCHES, $this->stringifyFetches ); + //cant pass these as argument to constructor, CUBRID driver does not understand... + $this->pdo->setAttribute( \PDO::ATTR_ERRMODE, \PDO::ERRMODE_EXCEPTION ); + $this->pdo->setAttribute( \PDO::ATTR_DEFAULT_FETCH_MODE, \PDO::FETCH_ASSOC ); + $this->isConnected = TRUE; + /* run initialisation query if any */ + if ( $this->initSQL !== NULL ) { + $this->Execute( $this->initSQL ); + $this->initSQL = NULL; + } + if ( $this->initCode !== NULL ) { + $code = $this->initCode; + $code( $this->pdo->getAttribute( \PDO::ATTR_SERVER_VERSION ) ); + } + } catch ( \PDOException $exception ) { + $matches = array(); + $dbname = ( preg_match( '/dbname=(\w+)/', $this->dsn, $matches ) ) ? $matches[1] : '?'; + throw new \PDOException( 'Could not connect to database (' . $dbname . ').', $exception->getCode() ); + } + } + + /** + * Directly sets PDO instance into driver. + * This method might improve performance, however since the driver does + * not configure this instance terrible things may happen... only use + * this method if you are an expert on RedBeanPHP, PDO and UTF8 connections and + * you know your database server VERY WELL. + * + * - connected TRUE|FALSE (treat this instance as connected, default: TRUE) + * - setEncoding TRUE|FALSE (let RedBeanPHP set encoding for you, default: TRUE) + * - setAttributes TRUE|FALSE (let RedBeanPHP set attributes for you, default: TRUE)* + * - setDSNString TRUE|FALSE (extract DSN string from PDO instance, default: TRUE) + * - stringFetch TRUE|FALSE (whether you want to stringify fetches or not, default: TRUE) + * - runInitCode TRUE|FALSE (run init code if any, default: TRUE) + * + * *attributes: + * - RedBeanPHP will ask database driver to throw Exceptions on errors (recommended for compatibility) + * - RedBeanPHP will ask database driver to use associative arrays when fetching (recommended for compatibility) + * + * @param PDO $pdo PDO instance + * @param array $options Options to apply + * + * @return void + */ + public function setPDO( \PDO $pdo, $options = array() ) { + $this->pdo = $pdo; + + $connected = TRUE; + $setEncoding = TRUE; + $setAttributes = TRUE; + $setDSNString = TRUE; + $runInitCode = TRUE; + $stringFetch = TRUE; + + if ( isset($options['connected']) ) $connected = $options['connected']; + if ( isset($options['setEncoding']) ) $setEncoding = $options['setEncoding']; + if ( isset($options['setAttributes']) ) $setAttributes = $options['setAttributes']; + if ( isset($options['setDSNString']) ) $setDSNString = $options['setDSNString']; + if ( isset($options['runInitCode']) ) $runInitCode = $options['runInitCode']; + if ( isset($options['stringFetch']) ) $stringFetch = $options['stringFetch']; + + if ($connected) $this->connected = $connected; + if ($setEncoding) $this->setEncoding(); + if ($setAttributes) { + $this->pdo->setAttribute( \PDO::ATTR_ERRMODE,\PDO::ERRMODE_EXCEPTION ); + $this->pdo->setAttribute( \PDO::ATTR_DEFAULT_FETCH_MODE,\PDO::FETCH_ASSOC ); + $this->pdo->setAttribute( \PDO::ATTR_STRINGIFY_FETCHES, $stringFetch ); + } + if ($runInitCode) { + /* run initialisation query if any */ + if ( $this->initSQL !== NULL ) { + $this->Execute( $this->initSQL ); + $this->initSQL = NULL; + } + if ( $this->initCode !== NULL ) { + $code = $this->initCode; + $code( $this->pdo->getAttribute( \PDO::ATTR_SERVER_VERSION ) ); + } + } + if ($setDSNString) $this->dsn = $this->getDatabaseType(); + } + + /** + * @see Driver::GetAll + */ + public function GetAll( $sql, $bindings = array() ) + { + $this->runQuery( $sql, $bindings ); + return $this->resultArray; + } + + /** + * @see Driver::GetAssocRow + */ + public function GetAssocRow( $sql, $bindings = array() ) + { + $this->runQuery( $sql, $bindings, array( + 'fetchStyle' => \PDO::FETCH_ASSOC + ) + ); + return $this->resultArray; + } + + /** + * @see Driver::GetCol + */ + public function GetCol( $sql, $bindings = array() ) + { + $rows = $this->GetAll( $sql, $bindings ); + + if ( empty( $rows ) || !is_array( $rows ) ) { + return array(); + } + + $cols = array(); + foreach ( $rows as $row ) { + $cols[] = reset( $row ); + } + + return $cols; + } + + /** + * @see Driver::GetOne + */ + public function GetOne( $sql, $bindings = array() ) + { + $arr = $this->GetAll( $sql, $bindings ); + + if ( empty( $arr[0] ) || !is_array( $arr[0] ) ) { + return NULL; + } + + return reset( $arr[0] ); + } + + /** + * Alias for getOne(). + * Backward compatibility. + * + * @param string $sql SQL + * @param array $bindings bindings + * + * @return mixed + */ + public function GetCell( $sql, $bindings = array() ) + { + return $this->GetOne( $sql, $bindings ); + } + + /** + * @see Driver::GetRow + */ + public function GetRow( $sql, $bindings = array() ) + { + $arr = $this->GetAll( $sql, $bindings ); + + if ( is_array( $arr ) && count( $arr ) ) { + return reset( $arr ); + } + + return array(); + } + + /** + * @see Driver::Excecute + */ + public function Execute( $sql, $bindings = array() ) + { + $this->runQuery( $sql, $bindings ); + return $this->affectedRows; + } + + /** + * @see Driver::GetInsertID + */ + public function GetInsertID() + { + $this->connect(); + + return (int) $this->pdo->lastInsertId(); + } + + /** + * @see Driver::GetCursor + */ + public function GetCursor( $sql, $bindings = array() ) + { + $statement = $this->runQuery( $sql, $bindings, array( 'noFetch' => TRUE ) ); + $cursor = new PDOCursor( $statement, \PDO::FETCH_ASSOC ); + return $cursor; + } + + /** + * @see Driver::Affected_Rows + */ + public function Affected_Rows() + { + $this->connect(); + return (int) $this->affectedRows; + } + + /** + * @see Driver::setDebugMode + */ + public function setDebugMode( $tf, $logger = NULL ) + { + $this->connect(); + $this->loggingEnabled = (bool) $tf; + if ( $this->loggingEnabled and !$logger ) { + $logger = new RDefault(); + } + $this->setLogger( $logger ); + } + + /** + * Injects Logger object. + * Sets the logger instance you wish to use. + * + * This method is for more fine-grained control. Normally + * you should use the facade to start the query debugger for + * you. The facade will manage the object wirings necessary + * to use the debugging functionality. + * + * Usage (through facade): + * + * + * R::debug( TRUE ); + * ...rest of program... + * R::debug( FALSE ); + * + * + * The example above illustrates how to use the RedBeanPHP + * query debugger through the facade. + * + * @param Logger $logger the logger instance to be used for logging + * + * @return self + */ + public function setLogger( Logger $logger ) + { + $this->logger = $logger; + return $this; + } + + /** + * Gets Logger object. + * Returns the currently active Logger instance. + * + * @return Logger + */ + public function getLogger() + { + return $this->logger; + } + + /** + * @see Driver::StartTrans + */ + public function StartTrans() + { + $this->connect(); + $this->pdo->beginTransaction(); + } + + /** + * @see Driver::CommitTrans + */ + public function CommitTrans() + { + $this->connect(); + $this->pdo->commit(); + } + + /** + * @see Driver::FailTrans + */ + public function FailTrans() + { + $this->connect(); + $this->pdo->rollback(); + } + + /** + * Returns the name of database driver for PDO. + * Uses the PDO attribute DRIVER NAME to obtain the name of the + * PDO driver. Use this method to identify the current PDO driver + * used to provide access to the database. Example of a database + * driver string: + * + * + * mysql + * + * + * Usage: + * + * + * echo R::getDatabaseAdapter()->getDatabase()->getDatabaseType(); + * + * + * The example above prints the current database driver string to + * stdout. + * + * Note that this is a driver-specific method, not part of the + * driver interface. This method might not be available in other + * drivers since it relies on PDO. + * + * @return string + */ + public function getDatabaseType() + { + $this->connect(); + return $this->pdo->getAttribute(\PDO::ATTR_DRIVER_NAME ); + } + + /** + * Returns the version identifier string of the database client. + * This method can be used to identify the currently installed + * database client. Note that this method will also establish a connection + * (because this is required to obtain the version information). + * + * Example of a version string: + * + * + * mysqlnd 5.0.12-dev - 20150407 - $Id: b5c5906d452ec590732a93b051f3827e02749b83 $ + * + * + * Usage: + * + * + * echo R::getDatabaseAdapter()->getDatabase()->getDatabaseVersion(); + * + * + * The example above will print the version string to stdout. + * + * Note that this is a driver-specific method, not part of the + * driver interface. This method might not be available in other + * drivers since it relies on PDO. + * + * To obtain the database server version, use getDatabaseServerVersion() + * instead. + * + * @return mixed + */ + public function getDatabaseVersion() + { + $this->connect(); + return $this->pdo->getAttribute(\PDO::ATTR_CLIENT_VERSION ); + } + + /** + * Returns the underlying PHP PDO instance. + * For some low-level database operations you'll need access to the PDO + * object. Not that this method is only available in RPDO and other + * PDO based database drivers for RedBeanPHP. Other drivers may not have + * a method like this. The following example demonstrates how to obtain + * a reference to the PDO instance from the facade: + * + * Usage: + * + * + * $pdo = R::getDatabaseAdapter()->getDatabase()->getPDO(); + * + * + * @return PDO + */ + public function getPDO() + { + $this->connect(); + return $this->pdo; + } + + /** + * Closes the database connection. + * While database connections are closed automatically at the end of the PHP script, + * closing database connections is generally recommended to improve performance. + * Closing a database connection will immediately return the resources to PHP. + * + * Usage: + * + * + * R::setup( ... ); + * ... do stuff ... + * R::close(); + * + * + * @return void + */ + public function close() + { + $this->pdo = NULL; + $this->isConnected = FALSE; + } + + /** + * Returns TRUE if the current PDO instance is connected. + * + * @return boolean + */ + public function isConnected() + { + return $this->isConnected && $this->pdo; + } + + /** + * Toggles logging, enables or disables logging. + * + * @param boolean $enable TRUE to enable logging + * + * @return self + */ + public function setEnableLogging( $enable ) + { + $this->loggingEnabled = (boolean) $enable; + return $this; + } + + /** + * Resets the query counter. + * The query counter can be used to monitor the number + * of database queries that have + * been processed according to the database driver. You can use this + * to monitor the number of queries required to render a page. + * + * Usage: + * + * + * R::resetQueryCount(); + * echo R::getQueryCount() . ' queries processed.'; + * + * + * @return self + */ + public function resetCounter() + { + $this->queryCounter = 0; + return $this; + } + + /** + * Returns the number of SQL queries processed. + * This method returns the number of database queries that have + * been processed according to the database driver. You can use this + * to monitor the number of queries required to render a page. + * + * Usage: + * + * + * echo R::getQueryCount() . ' queries processed.'; + * + * + * @return integer + */ + public function getQueryCount() + { + return $this->queryCounter; + } + + /** + * Returns the maximum value treated as integer parameter + * binding. + * + * This method is mainly for testing purposes but it can help + * you solve some issues relating to integer bindings. + * + * @return integer + */ + public function getIntegerBindingMax() + { + return $this->max; + } + + /** + * Sets a query to be executed upon connecting to the database. + * This method provides an opportunity to configure the connection + * to a database through an SQL-based interface. Objects can provide + * an SQL string to be executed upon establishing a connection to + * the database. This has been used to solve issues with default + * foreign key settings in SQLite3 for instance, see Github issues: + * #545 and #548. + * + * @param string $sql SQL query to run upon connecting to database + * + * @return self + */ + public function setInitQuery( $sql ) { + $this->initSQL = $sql; + return $this; + } + + /** + * Returns the version string from the database server. + * + * @return string + */ + public function DatabaseServerVersion() { + return trim( strval( $this->pdo->getAttribute(\PDO::ATTR_SERVER_VERSION) ) ); + } +} +} + +namespace RedBeanPHP { + +use RedBeanPHP\QueryWriter\AQueryWriter as AQueryWriter; +use RedBeanPHP\BeanHelper as BeanHelper; +use RedBeanPHP\RedException as RedException; + +/** + * PHP 5.3 compatibility + * We extend JsonSerializable to avoid namespace conflicts, + * can't define interface with special namespace in PHP + */ +if (interface_exists('\JsonSerializable')) { interface Jsonable extends \JsonSerializable {}; } else { interface Jsonable {}; } + +/** + * OODBBean (Object Oriented DataBase Bean). + * + * to exchange information with the database. A bean represents + * a single table row and offers generic services for interaction + * with databases systems as well as some meta-data. + * + * @file RedBeanPHP/OODBBean.php + * @author Gabor de Mooij and the RedBeanPHP community + * @license BSD/GPLv2 + * @desc OODBBean represents a bean. RedBeanPHP uses beans + * + * @copyright + * copyright (c) G.J.G.T. (Gabor) de Mooij and the RedBeanPHP Community. + * This source file is subject to the BSD/GPLv2 License that is bundled + * with this source code in the file license.txt. + */ +class OODBBean implements \IteratorAggregate,\ArrayAccess,\Countable,Jsonable +{ + /** + * FUSE error modes. + */ + const C_ERR_IGNORE = FALSE; + const C_ERR_LOG = 1; + const C_ERR_NOTICE = 2; + const C_ERR_WARN = 3; + const C_ERR_EXCEPTION = 4; + const C_ERR_FUNC = 5; + const C_ERR_FATAL = 6; + + /** + * @var boolean + */ + protected static $useFluidCount = FALSE; + + /** + * @var boolean + */ + protected static $convertArraysToJSON = FALSE; + + /** + * @var boolean + */ + protected static $errorHandlingFUSE = FALSE; + + /** + * @var callable|NULL + */ + protected static $errorHandler = NULL; + + /** + * @var array + */ + protected static $aliases = array(); + + /** + * If this is set to TRUE, the __toString function will + * encode all properties as UTF-8 to repair invalid UTF-8 + * encodings and prevent exceptions (which are uncatchable from within + * a __toString-function). + * + * @var boolean + */ + protected static $enforceUTF8encoding = FALSE; + + /** + * This is where the real properties of the bean live. They are stored and retrieved + * by the magic getter and setter (__get and __set). + * + * @var array $properties + */ + protected $properties = array(); + + /** + * Here we keep the meta data of a bean. + * + * @var array + */ + protected $__info = array(); + + /** + * The BeanHelper allows the bean to access the toolbox objects to implement + * rich functionality, otherwise you would have to do everything with R or + * external objects. + * + * @var BeanHelper + */ + protected $beanHelper = NULL; + + /** + * @var null + */ + protected $fetchType = NULL; + + /** + * @var string + */ + protected $withSql = ''; + + /** + * @var array + */ + protected $withParams = array(); + + /** + * @var string + */ + protected $aliasName = NULL; + + /** + * @var string + */ + protected $via = NULL; + + /** + * @var boolean + */ + protected $noLoad = FALSE; + + /** + * @var boolean + */ + protected $all = FALSE; + + /** + * If fluid count is set to TRUE then $bean->ownCount() will + * return 0 if the table does not exists. + * Only for backward compatibility. + * Returns previouds value. + * + * @param boolean $toggle toggle + * + * @return boolean + */ + public static function useFluidCount( $toggle ) + { + $old = self::$useFluidCount; + self::$useFluidCount = $toggle; + return $old; + } + + /** + * If this is set to TRUE, the __toString function will + * encode all properties as UTF-8 to repair invalid UTF-8 + * encodings and prevent exceptions (which are uncatchable from within + * a __toString-function). + * + * @param boolean $toggle TRUE to enforce UTF-8 encoding (slower) + * + * @return void + */ + public static function setEnforceUTF8encoding( $toggle ) + { + self::$enforceUTF8encoding = (boolean) $toggle; + } + + /** + * Sets the error mode for FUSE. + * What to do if a FUSE model method does not exist? + * You can set the following options: + * + * * OODBBean::C_ERR_IGNORE (default), ignores the call, returns NULL + * * OODBBean::C_ERR_LOG, logs the incident using error_log + * * OODBBean::C_ERR_NOTICE, triggers a E_USER_NOTICE + * * OODBBean::C_ERR_WARN, triggers a E_USER_WARNING + * * OODBBean::C_ERR_EXCEPTION, throws an exception + * * OODBBean::C_ERR_FUNC, allows you to specify a custom handler (function) + * * OODBBean::C_ERR_FATAL, triggers a E_USER_ERROR + * + * + * Custom handler method signature: handler( array ( + * 'message' => string + * 'bean' => OODBBean + * 'method' => string + * ) ) + * + * + * This method returns the old mode and handler as an array. + * + * @param integer $mode error handling mode + * @param callable|NULL $func custom handler + * + * @return array + */ + public static function setErrorHandlingFUSE($mode, $func = NULL) { + if ( + $mode !== self::C_ERR_IGNORE + && $mode !== self::C_ERR_LOG + && $mode !== self::C_ERR_NOTICE + && $mode !== self::C_ERR_WARN + && $mode !== self::C_ERR_EXCEPTION + && $mode !== self::C_ERR_FUNC + && $mode !== self::C_ERR_FATAL + ) throw new \Exception( 'Invalid error mode selected' ); + + if ( $mode === self::C_ERR_FUNC && !is_callable( $func ) ) { + throw new \Exception( 'Invalid error handler' ); + } + + $old = array( self::$errorHandlingFUSE, self::$errorHandler ); + self::$errorHandlingFUSE = $mode; + if ( is_callable( $func ) ) { + self::$errorHandler = $func; + } else { + self::$errorHandler = NULL; + } + return $old; + } + + /** + * Toggles array to JSON conversion. If set to TRUE any array + * set to a bean property that's not a list will be turned into + * a JSON string. Used together with AQueryWriter::useJSONColumns this + * extends the data type support for JSON columns. Returns the previous + * value of the flag. + * + * @param boolean $flag flag + * + * @return boolean + */ + public static function convertArraysToJSON( $flag ) + { + $old = self::$convertArraysToJSON; + self::$convertArraysToJSON = $flag; + return $old; + } + + /** + * Sets global aliases. + * Registers a batch of aliases in one go. This works the same as + * fetchAs and setAutoResolve but explicitly. For instance if you register + * the alias 'cover' for 'page' a property containing a reference to a + * page bean called 'cover' will correctly return the page bean and not + * a (non-existant) cover bean. + * + * + * R::aliases( array( 'cover' => 'page' ) ); + * $book = R::dispense( 'book' ); + * $page = R::dispense( 'page' ); + * $book->cover = $page; + * R::store( $book ); + * $book = $book->fresh(); + * $cover = $book->cover; + * echo $cover->getMeta( 'type' ); //page + * + * + * The format of the aliases registration array is: + * + * {alias} => {actual type} + * + * In the example above we use: + * + * cover => page + * + * From that point on, every bean reference to a cover + * will return a 'page' bean. Note that with autoResolve this + * feature along with fetchAs() is no longer very important, although + * relying on explicit aliases can be a bit faster. + * + * @param array $list list of global aliases to use + * + * @return void + */ + public static function aliases( $list ) + { + self::$aliases = $list; + } + + /** + * Return list of global aliases + * + * @return array + */ + public static function getAliases() + { + return self::$aliases; + } + + /** + * Sets a meta property for all beans. This is a quicker way to set + * the meta properties for a collection of beans because this method + * can directly access the property arrays of the beans. + * This method returns the beans. + * + * @param array $beans beans to set the meta property of + * @param string $property property to set + * @param mixed $value value + * + * @return array + */ + public static function setMetaAll( $beans, $property, $value ) + { + foreach( $beans as $bean ) { + if ( $bean instanceof OODBBean ) $bean->__info[ $property ] = $value; + if ( $property == 'type' && !empty($bean->beanHelper)) { + $bean->__info['model'] = $bean->beanHelper->getModelForBean( $bean ); + } + } + return $beans; + } + + /** + * Accesses the shared list of a bean. + * To access beans that have been associated with the current bean + * using a many-to-many relationship use sharedXList where + * X is the type of beans in the list. + * + * Usage: + * + * + * $person = R::load( 'person', $id ); + * $friends = $person->sharedFriendList; + * + * + * The code snippet above demonstrates how to obtain all beans of + * type 'friend' that have associated using an N-M relation. + * This is a private method used by the magic getter / accessor. + * The example illustrates usage through these accessors. + * + * @param string $type the name of the list you want to retrieve + * @param OODB $redbean instance of the RedBeanPHP OODB class + * @param ToolBox $toolbox instance of ToolBox (to get access to core objects) + * + * @return array + */ + private function getSharedList( $type, $redbean, $toolbox ) + { + $writer = $toolbox->getWriter(); + if ( $this->via ) { + $oldName = $writer->getAssocTable( array( $this->__info['type'], $type ) ); + if ( $oldName !== $this->via ) { + //set the new renaming rule + $writer->renameAssocTable( $oldName, $this->via ); + } + $this->via = NULL; + } + $beans = array(); + if ($this->getID()) { + $type = $this->beau( $type ); + $assocManager = $redbean->getAssociationManager(); + $beans = $assocManager->related( $this, $type, $this->withSql, $this->withParams ); + } + return $beans; + } + + /** + * Accesses the ownList. The 'own' list contains beans + * associated using a one-to-many relation. The own-lists can + * be accessed through the magic getter/setter property + * ownXList where X is the type of beans in that list. + * + * Usage: + * + * + * $book = R::load( 'book', $id ); + * $pages = $book->ownPageList; + * + * + * The example above demonstrates how to access the + * pages associated with the book. Since this is a private method + * meant to be used by the magic accessors, the example uses the + * magic getter instead. + * + * @param string $type name of the list you want to retrieve + * @param OODB $oodb The RB OODB object database instance + * + * @return array + */ + private function getOwnList( $type, $redbean ) + { + $type = $this->beau( $type ); + if ( $this->aliasName ) { + $parentField = $this->aliasName; + $myFieldLink = $parentField . '_id'; + + $this->__info['sys.alias.' . $type] = $this->aliasName; + + $this->aliasName = NULL; + } else { + $parentField = $this->__info['type']; + $myFieldLink = $parentField . '_id'; + } + $beans = array(); + if ( $this->getID() ) { + reset( $this->withParams ); + $firstKey = count( $this->withParams ) > 0 + ? key( $this->withParams ) + : 0; + if ( is_int( $firstKey ) ) { + $sql = "{$myFieldLink} = ? {$this->withSql}"; + $bindings = array_merge( array( $this->getID() ), $this->withParams ); + } else { + $sql = "{$myFieldLink} = :slot0 {$this->withSql}"; + $bindings = $this->withParams; + $bindings[':slot0'] = $this->getID(); + } + $beans = $redbean->find( $type, array(), $sql, $bindings ); + } + foreach ( $beans as $beanFromList ) { + $beanFromList->__info['sys.parentcache.' . $parentField] = $this; + } + return $beans; + } + + /** + * Initializes a bean. Used by OODB for dispensing beans. + * It is not recommended to use this method to initialize beans. Instead + * use the OODB object to dispense new beans. You can use this method + * if you build your own bean dispensing mechanism. + * This is not recommended. + * + * Unless you know what you are doing, do NOT use this method. + * This is for advanced users only! + * + * @param string $type type of the new bean + * @param BeanHelper $beanhelper bean helper to obtain a toolbox and a model + * + * @return void + */ + public function initializeForDispense( $type, $beanhelper = NULL ) + { + $this->beanHelper = $beanhelper; + $this->__info['type'] = $type; + $this->__info['sys.id'] = 'id'; + $this->__info['sys.orig'] = array( 'id' => 0 ); + $this->__info['tainted'] = TRUE; + $this->__info['changed'] = TRUE; + $this->__info['changelist'] = array(); + if ( $beanhelper ) { + $this->__info['model'] = $this->beanHelper->getModelForBean( $this ); + } + $this->properties['id'] = 0; + } + + /** + * Sets the Bean Helper. Normally the Bean Helper is set by OODB. + * Here you can change the Bean Helper. The Bean Helper is an object + * providing access to a toolbox for the bean necessary to retrieve + * nested beans (bean lists: ownBean, sharedBean) without the need to + * rely on static calls to the facade (or make this class dep. on OODB). + * + * @param BeanHelper $helper helper to use for this bean + * + * @return void + */ + public function setBeanHelper( BeanHelper $helper ) + { + $this->beanHelper = $helper; + } + + /** + * Returns an ArrayIterator so you can treat the bean like + * an array with the properties container as its contents. + * This method is meant for PHP and allows you to access beans as if + * they were arrays, i.e. using array notation: + * + * + * $bean[$key] = $value; + * + * + * Note that not all PHP functions work with the array interface. + * + * @return ArrayIterator + */ + public function getIterator() + { + return new \ArrayIterator( $this->properties ); + } + + /** + * Imports all values from an associative array $array. Chainable. + * This method imports the values in the first argument as bean + * propery and value pairs. Use the second parameter to provide a + * selection. If a selection array is passed, only the entries + * having keys mentioned in the selection array will be imported. + * Set the third parameter to TRUE to preserve spaces in selection keys. + * + * @param array $array what you want to import + * @param string|array $selection selection of values + * @param boolean $notrim if TRUE selection keys will NOT be trimmed + * + * @return OODBBean + */ + public function import( $array, $selection = FALSE, $notrim = FALSE ) + { + if ( is_string( $selection ) ) { + $selection = explode( ',', $selection ); + } + if ( is_array( $selection ) ) { + if ( $notrim ) { + $selected = array_flip($selection); + } else { + $selected = array(); + foreach ( $selection as $key => $select ) { + $selected[trim( $select )] = TRUE; + } + } + } else { + $selected = FALSE; + } + foreach ( $array as $key => $value ) { + if ( $key != '__info' ) { + if ( !$selected || isset( $selected[$key] ) ) { + if ( is_array($value ) ) { + if ( isset( $value['_type'] ) ) { + $bean = $this->beanHelper->getToolbox()->getRedBean()->dispense( $value['_type'] ); + unset( $value['_type'] ); + $bean->import($value); + $this->$key = $bean; + } else { + $listBeans = array(); + foreach( $value as $listKey => $listItem ) { + $bean = $this->beanHelper->getToolbox()->getRedBean()->dispense( $listItem['_type'] ); + unset( $listItem['_type'] ); + $bean->import($listItem); + $list = &$this->$key; + $list[ $listKey ] = $bean; + } + } + } else { + $this->$key = $value; + } + } + } + } + return $this; + } + + /** + * Imports an associative array directly into the + * internal property array of the bean as well as the + * meta property sys.orig and sets the changed flag to FALSE. + * This is used by the repository objects to inject database rows + * into the beans. It is not recommended to use this method outside + * of a bean repository. + * + * @param array $row a database row + * + * @return self + */ + public function importRow( $row ) + { + $this->properties = $row; + $this->__info['sys.orig'] = $row; + $this->__info['changed'] = FALSE; + $this->__info['changelist'] = array(); + return $this; + } + + /** + * Imports data from another bean. Chainable. + * Copies the properties from the source bean to the internal + * property list. + * + * Usage: + * + * + * $copy->importFrom( $bean ); + * + * + * The example above demonstrates how to make a shallow copy + * of a bean using the importFrom() method. + * + * @param OODBBean $sourceBean the source bean to take properties from + * + * @return OODBBean + */ + public function importFrom( OODBBean $sourceBean ) + { + $this->__info['tainted'] = TRUE; + $this->__info['changed'] = TRUE; + $this->properties = $sourceBean->properties; + + return $this; + } + + /** + * Injects the properties of another bean but keeps the original ID. + * Just like import() but keeps the original ID. + * Chainable. + * + * @param OODBBean $otherBean the bean whose properties you would like to copy + * + * @return OODBBean + */ + public function inject( OODBBean $otherBean ) + { + $myID = $this->properties['id']; + $this->import( $otherBean->export( FALSE, FALSE, TRUE ) ); + $this->id = $myID; + + return $this; + } + + /** + * Exports the bean as an array. + * This function exports the contents of a bean to an array and returns + * the resulting array. Depending on the parameters you can also + * export an entire graph of beans, apply filters or exclude meta data. + * + * Usage: + * + * + * $bookData = $book->export( TRUE, TRUE, FALSE, [ 'author' ] ); + * + * + * The example above exports all bean properties to an array + * called $bookData including its meta data, parent objects but without + * any beans of type 'author'. + * + * @param boolean $meta set to TRUE if you want to export meta data as well + * @param boolean $parents set to TRUE if you want to export parents as well + * @param boolean $onlyMe set to TRUE if you want to export only this bean + * @param array $filters optional whitelist for export + * + * @return array + */ + public function export( $meta = FALSE, $parents = FALSE, $onlyMe = FALSE, $filters = array() ) + { + $arr = array(); + if ( $parents ) { + foreach ( $this as $key => $value ) { + if ( substr( $key, -3 ) != '_id' ) continue; + + $prop = substr( $key, 0, strlen( $key ) - 3 ); + $this->$prop; + } + } + $hasFilters = is_array( $filters ) && count( $filters ); + foreach ( $this as $key => $value ) { + if ( !$onlyMe && is_array( $value ) ) { + $vn = array(); + + foreach ( $value as $i => $b ) { + if ( !( $b instanceof OODBBean ) ) continue; + $vn[] = $b->export( $meta, FALSE, FALSE, $filters ); + $value = $vn; + } + } elseif ( $value instanceof OODBBean ) { if ( $hasFilters ) { //has to be on one line, otherwise code coverage miscounts as miss + if ( !in_array( strtolower( $value->getMeta( 'type' ) ), $filters ) ) continue; + } + $value = $value->export( $meta, $parents, FALSE, $filters ); + } + $arr[$key] = $value; + } + if ( $meta ) { + $arr['__info'] = $this->__info; + } + return $arr; + } + + /** + * Implements isset() function for use as an array. + * This allows you to use isset() on bean properties. + * + * Usage: + * + * + * $book->title = 'my book'; + * echo isset($book['title']); //TRUE + * + * + * The example illustrates how one can apply the + * isset() function to a bean. + * + * @param string $property name of the property you want to check + * + * @return boolean + */ + public function __isset( $property ) + { + $property = $this->beau( $property ); + if ( strpos( $property, 'xown' ) === 0 && ctype_upper( substr( $property, 4, 1 ) ) ) { + $property = substr($property, 1); + } + return isset( $this->properties[$property] ); + } + + /** + * Checks whether a related bean exists. + * For instance if a post bean has a related author, this method + * can be used to check if the author is set without loading the author. + * This method works by checking the related ID-field. + * + * @param string $property name of the property you wish to check + * + * @return boolean + */ + public function exists( $property ) + { + $property = $this->beau( $property ); + /* fixes issue #549, see Base/Bean test */ + $hiddenRelationField = "{$property}_id"; + if ( array_key_exists( $hiddenRelationField, $this->properties ) ) { + if ( !is_null( $this->properties[$hiddenRelationField] ) ) { + return TRUE; + } + } + return FALSE; + } + + /** + * Returns the ID of the bean. + * If for some reason the ID has not been set, this method will + * return NULL. This is actually the same as accessing the + * id property using $bean->id. The ID of a bean is it's primary + * key and should always correspond with a table column named + * 'id'. + * + * @return string|null + */ + public function getID() + { + return ( isset( $this->properties['id'] ) ) ? (string) $this->properties['id'] : NULL; + } + + /** + * Unsets a property of a bean. + * Magic method, gets called implicitly when + * performing the unset() operation + * on a bean property. + * + * @param string $property property to unset + * + * @return void + */ + public function __unset( $property ) + { + $property = $this->beau( $property ); + + if ( strpos( $property, 'xown' ) === 0 && ctype_upper( substr( $property, 4, 1 ) ) ) { + $property = substr($property, 1); + } + unset( $this->properties[$property] ); + $shadowKey = 'sys.shadow.'.$property; + if ( isset( $this->__info[ $shadowKey ] ) ) unset( $this->__info[$shadowKey] ); + //also clear modifiers + $this->clearModifiers(); + return; + } + + /** + * Adds WHERE clause conditions to ownList retrieval. + * For instance to get the pages that belong to a book you would + * issue the following command: $book->ownPage + * However, to order these pages by number use: + * + * + * $book->with(' ORDER BY `number` ASC ')->ownPage + * + * + * the additional SQL snippet will be merged into the final + * query. + * + * @param string $sql SQL to be added to retrieval query. + * @param array $bindings array with parameters to bind to SQL snippet + * + * @return OODBBean + */ + public function with( $sql, $bindings = array() ) + { + $this->withSql = $sql; + $this->withParams = $bindings; + return $this; + } + + /** + * Just like with(). Except that this method prepends the SQL query snippet + * with AND which makes it slightly more comfortable to use a conditional + * SQL snippet. For instance to filter an own-list with pages (belonging to + * a book) on specific chapters you can use: + * + * $book->withCondition(' chapter = 3 ')->ownPage + * + * This will return in the own list only the pages having 'chapter == 3'. + * + * @param string $sql SQL to be added to retrieval query (prefixed by AND) + * @param array $bindings array with parameters to bind to SQL snippet + * + * @return OODBBean + */ + public function withCondition( $sql, $bindings = array() ) + { + $this->withSql = ' AND ' . $sql; + $this->withParams = $bindings; + return $this; + } + + /** + * Tells the bean to (re)load the following list without any + * conditions. If you have an ownList or sharedList with a + * condition you can use this method to reload the entire list. + * + * Usage: + * + * + * $bean->with( ' LIMIT 3 ' )->ownPage; //Just 3 + * $bean->all()->ownPage; //Reload all pages + * + * + * @return self + */ + public function all() + { + $this->all = TRUE; + return $this; + } + + /** + * Tells the bean to only access the list but not load + * its contents. Use this if you only want to add something to a list + * and you have no interest in retrieving its contents from the database. + * + * Usage: + * + * + * $book->noLoad()->ownPage[] = $newPage; + * + * + * In the example above we add the $newPage bean to the + * page list of book without loading all the pages first. + * If you know in advance that you are not going to use + * the contents of the list, you may use the noLoad() modifier + * to make sure the queries required to load the list will not + * be executed. + * + * @return self + */ + public function noLoad() + { + $this->noLoad = TRUE; + return $this; + } + + /** + * Prepares an own-list to use an alias. This is best explained using + * an example. Imagine a project and a person. The project always involves + * two persons: a teacher and a student. The person beans have been aliased in this + * case, so to the project has a teacher_id pointing to a person, and a student_id + * also pointing to a person. Given a project, we obtain the teacher like this: + * + * + * $project->fetchAs('person')->teacher; + * + * + * Now, if we want all projects of a teacher we cant say: + * + * + * $teacher->ownProject + * + * + * because the $teacher is a bean of type 'person' and no project has been + * assigned to a person. Instead we use the alias() method like this: + * + * + * $teacher->alias('teacher')->ownProject + * + * + * now we get the projects associated with the person bean aliased as + * a teacher. + * + * @param string $aliasName the alias name to use + * + * @return OODBBean + */ + public function alias( $aliasName ) + { + $this->aliasName = $this->beau( $aliasName ); + return $this; + } + + /** + * Returns properties of bean as an array. + * This method returns the raw internal property list of the + * bean. Only use this method for optimization purposes. Otherwise + * use the export() method to export bean data to arrays. + * + * @return array + */ + public function getProperties() + { + return $this->properties; + } + + /** + * Returns properties of bean as an array. + * This method returns the raw internal property list of the + * bean. Only use this method for optimization purposes. Otherwise + * use the export() method to export bean data to arrays. + * This method returns an array with the properties array and + * the type (string). + * + * @return array + */ + public function getPropertiesAndType() + { + return array( $this->properties, $this->__info['type'] ); + } + + /** + * Turns a camelcase property name into an underscored property name. + * + * Examples: + * + * - oneACLRoute -> one_acl_route + * - camelCase -> camel_case + * + * Also caches the result to improve performance. + * + * @param string $property property to un-beautify + * + * @return string + */ + public function beau( $property ) + { + static $beautifulColumns = array(); + + if ( ctype_lower( $property ) ) return $property; + if ( + ( strpos( $property, 'own' ) === 0 && ctype_upper( substr( $property, 3, 1 ) ) ) + || ( strpos( $property, 'xown' ) === 0 && ctype_upper( substr( $property, 4, 1 ) ) ) + || ( strpos( $property, 'shared' ) === 0 && ctype_upper( substr( $property, 6, 1 ) ) ) + ) { + + $property = preg_replace( '/List$/', '', $property ); + return $property; + } + if ( !isset( $beautifulColumns[$property] ) ) { + $beautifulColumns[$property] = AQueryWriter::camelsSnake( $property ); + } + return $beautifulColumns[$property]; + } + + /** + * Modifiers are a powerful concept in RedBeanPHP, they make it possible + * to change the way a property has to be loaded. + * RedBeanPHP uses property modifiers using a prefix notation like this: + * + * + * $book->fetchAs('page')->cover; + * + * + * Here, we load a bean of type page, identified by the cover property + * (or cover_id in the database). Because the modifier is called before + * the property is accessed, the modifier must be remembered somehow, + * this changes the state of the bean. Accessing a property causes the + * bean to clear its modifiers. To clear the modifiers manually you can + * use this method. + * + * Usage: + * + * + * $book->with( 'LIMIT 1' ); + * $book->clearModifiers()->ownPageList; + * + * + * In the example above, the 'LIMIT 1' clause is + * cleared before accessing the pages of the book, causing all pages + * to be loaded in the list instead of just one. + * + * @return self + */ + public function clearModifiers() + { + $this->withSql = ''; + $this->withParams = array(); + $this->aliasName = NULL; + $this->fetchType = NULL; + $this->noLoad = FALSE; + $this->all = FALSE; + $this->via = NULL; + return $this; + } + + /** + * Determines whether a list is opened in exclusive mode or not. + * If a list has been opened in exclusive mode this method will return TRUE, + * othwerwise it will return FALSE. + * + * @param string $listName name of the list to check + * + * @return boolean + */ + public function isListInExclusiveMode( $listName ) + { + $listName = $this->beau( $listName ); + + if ( strpos( $listName, 'xown' ) === 0 && ctype_upper( substr( $listName, 4, 1 ) ) ) { + $listName = substr($listName, 1); + } + $listName = lcfirst( substr( $listName, 3 ) ); + return ( isset( $this->__info['sys.exclusive-'.$listName] ) && $this->__info['sys.exclusive-'.$listName] ); + } + + /** + * Magic Getter. Gets the value for a specific property in the bean. + * If the property does not exist this getter will make sure no error + * occurs. This is because RedBean allows you to query (probe) for + * properties. If the property can not be found this method will + * return NULL instead. + * + * Usage: + * + * + * $title = $book->title; + * $pages = $book->ownPageList; + * $tags = $book->sharedTagList; + * + * + * The example aboves lists several ways to invoke the magic getter. + * You can use the magic setter to access properties, own-lists, + * exclusive own-lists (xownLists) and shared-lists. + * + * @param string $property name of the property you wish to obtain the value of + * + * @return mixed + */ + public function &__get( $property ) + { + $isEx = FALSE; + $isOwn = FALSE; + $isShared = FALSE; + if ( !ctype_lower( $property ) ) { + $property = $this->beau( $property ); + if ( strpos( $property, 'xown' ) === 0 && ctype_upper( substr( $property, 4, 1 ) ) ) { + $property = substr($property, 1); + $listName = lcfirst( substr( $property, 3 ) ); + $isEx = TRUE; + $isOwn = TRUE; + $this->__info['sys.exclusive-'.$listName] = TRUE; + } elseif ( strpos( $property, 'own' ) === 0 && ctype_upper( substr( $property, 3, 1 ) ) ) { + $isOwn = TRUE; + $listName = lcfirst( substr( $property, 3 ) ); + } elseif ( strpos( $property, 'shared' ) === 0 && ctype_upper( substr( $property, 6, 1 ) ) ) { + $isShared = TRUE; + } + } + $fieldLink = $property . '_id'; + $exists = isset( $this->properties[$property] ); + + //If not exists and no field link and no list, bail out. + if ( !$exists && !isset($this->$fieldLink) && (!$isOwn && !$isShared )) { + $this->clearModifiers(); + /** + * Github issue: + * Remove $NULL to directly return NULL #625 + * @@ -1097,8 +1097,7 @@ public function &__get( $property ) + * $this->all = FALSE; + * $this->via = NULL; + * + * - $NULL = NULL; + * - return $NULL; + * + return NULL; + * + * leads to regression: + * PHP Stack trace: + * PHP 1. {main}() testje.php:0 + * PHP 2. RedBeanPHP\OODBBean->__get() testje.php:22 + * Notice: Only variable references should be returned by reference in rb.php on line 2529 + */ + $NULL = NULL; + return $NULL; + } + + $hasAlias = (!is_null($this->aliasName)); + $differentAlias = ($hasAlias && $isOwn && isset($this->__info['sys.alias.'.$listName])) ? + ($this->__info['sys.alias.'.$listName] !== $this->aliasName) : FALSE; + $hasSQL = ($this->withSql !== '' || $this->via !== NULL); + $hasAll = (boolean) ($this->all); + + //If exists and no list or exits and list not changed, bail out. + if ( $exists && ((!$isOwn && !$isShared ) || (!$hasSQL && !$differentAlias && !$hasAll)) ) { + $this->clearModifiers(); + return $this->properties[$property]; + } + + list( $redbean, , , $toolbox ) = $this->beanHelper->getExtractedToolbox(); + + //If it's another bean, then we load it and return + if ( isset( $this->$fieldLink ) ) { + $this->__info['tainted'] = TRUE; + if ( isset( $this->__info["sys.parentcache.$property"] ) ) { + $bean = $this->__info["sys.parentcache.$property"]; + } else { + if ( isset( self::$aliases[$property] ) ) { + $type = self::$aliases[$property]; + } elseif ( $this->fetchType ) { + $type = $this->fetchType; + $this->fetchType = NULL; + } else { + $type = $property; + } + $bean = NULL; + if ( !is_null( $this->properties[$fieldLink] ) ) { + $bean = $redbean->load( $type, $this->properties[$fieldLink] ); + } + } + $this->properties[$property] = $bean; + $this->clearModifiers(); + return $this->properties[$property]; + } + + /* Implicit: elseif ( $isOwn || $isShared ) */ + if ( $this->noLoad ) { + $beans = array(); + } elseif ( $isOwn ) { + $beans = $this->getOwnList( $listName, $redbean ); + } else { + $beans = $this->getSharedList( lcfirst( substr( $property, 6 ) ), $redbean, $toolbox ); + } + $this->properties[$property] = $beans; + $this->__info["sys.shadow.$property"] = $beans; + $this->__info['tainted'] = TRUE; + + $this->clearModifiers(); + return $this->properties[$property]; + + } + + /** + * Magic Setter. Sets the value for a specific property. + * This setter acts as a hook for OODB to mark beans as tainted. + * The tainted meta property can be retrieved using getMeta("tainted"). + * The tainted meta property indicates whether a bean has been modified and + * can be used in various caching mechanisms. + * + * @param string $property name of the property you wish to assign a value to + * @param mixed $value the value you want to assign + * + * @return void + */ + public function __set( $property, $value ) + { + $isEx = FALSE; + $isOwn = FALSE; + $isShared = FALSE; + + if ( !ctype_lower( $property ) ) { + $property = $this->beau( $property ); + if ( strpos( $property, 'xown' ) === 0 && ctype_upper( substr( $property, 4, 1 ) ) ) { + $property = substr($property, 1); + $listName = lcfirst( substr( $property, 3 ) ); + $isEx = TRUE; + $isOwn = TRUE; + $this->__info['sys.exclusive-'.$listName] = TRUE; + } elseif ( strpos( $property, 'own' ) === 0 && ctype_upper( substr( $property, 3, 1 ) ) ) { + $isOwn = TRUE; + $listName = lcfirst( substr( $property, 3 ) ); + } elseif ( strpos( $property, 'shared' ) === 0 && ctype_upper( substr( $property, 6, 1 ) ) ) { + $isShared = TRUE; + } + } elseif ( self::$convertArraysToJSON && is_array( $value ) ) { + $value = json_encode( $value ); + } + + $hasAlias = (!is_null($this->aliasName)); + $differentAlias = ($hasAlias && $isOwn && isset($this->__info['sys.alias.'.$listName])) ? + ($this->__info['sys.alias.'.$listName] !== $this->aliasName) : FALSE; + $hasSQL = ($this->withSql !== '' || $this->via !== NULL); + $exists = isset( $this->properties[$property] ); + $fieldLink = $property . '_id'; + $isFieldLink = (($pos = strrpos($property, '_id')) !== FALSE) && array_key_exists( ($fieldName = substr($property, 0, $pos)), $this->properties ); + + + if ( ($isOwn || $isShared) && (!$exists || $hasSQL || $differentAlias) ) { + + if ( !$this->noLoad ) { + list( $redbean, , , $toolbox ) = $this->beanHelper->getExtractedToolbox(); + if ( $isOwn ) { + $beans = $this->getOwnList( $listName, $redbean ); + } else { + $beans = $this->getSharedList( lcfirst( substr( $property, 6 ) ), $redbean, $toolbox ); + } + $this->__info["sys.shadow.$property"] = $beans; + } + } + + $this->clearModifiers(); + + $this->__info['tainted'] = TRUE; + $this->__info['changed'] = TRUE; + array_push( $this->__info['changelist'], $property ); + + if ( array_key_exists( $fieldLink, $this->properties ) && !( $value instanceof OODBBean ) ) { + if ( is_null( $value ) || $value === FALSE ) { + + unset( $this->properties[ $property ]); + $this->properties[ $fieldLink ] = NULL; + + return; + } else { + throw new RedException( 'Cannot cast to bean.' ); + } + } + + if ( $isFieldLink ){ + unset( $this->properties[ $fieldName ]); + $this->properties[ $property ] = NULL; + } + + + if ( $value === FALSE ) { + $value = '0'; + } elseif ( $value === TRUE ) { + $value = '1'; + /* for some reason there is some kind of bug in xdebug so that it doesnt count this line otherwise... */ + } elseif ( $value instanceof \DateTime ) { $value = $value->format( 'Y-m-d H:i:s' ); } + $this->properties[$property] = $value; + } + + /** + * @deprecated + * + * Sets a property of the bean allowing you to keep track of + * the state yourself. This method sets a property of the bean and + * allows you to control how the state of the bean will be affected. + * + * While there may be some circumstances where this method is needed, + * this method is considered to be extremely dangerous. + * This method is only for advanced users. + * + * @param string $property property + * @param mixed $value value + * @param boolean $updateShadow whether you want to update the shadow + * @param boolean $taint whether you want to mark the bean as tainted + * + * @return void + */ + public function setProperty( $property, $value, $updateShadow = FALSE, $taint = FALSE ) + { + $this->properties[$property] = $value; + + if ( $updateShadow ) { + $this->__info['sys.shadow.' . $property] = $value; + } + + if ( $taint ) { + $this->__info['tainted'] = TRUE; + $this->__info['changed'] = TRUE; + } + } + + /** + * Returns the value of a meta property. A meta property + * contains additional information about the bean object that will not + * be stored in the database. Meta information is used to instruct + * RedBeanPHP as well as other systems how to deal with the bean. + * If the property cannot be found this getter will return NULL instead. + * + * Example: + * + * + * $bean->setMeta( 'flush-cache', TRUE ); + * + * + * RedBeanPHP also stores meta data in beans, this meta data uses + * keys prefixed with 'sys.' (system). + * + * @param string $path path to property in meta data + * @param mixed $default default value + * + * @return mixed + */ + public function getMeta( $path, $default = NULL ) + { + return ( isset( $this->__info[$path] ) ) ? $this->__info[$path] : $default; + } + + /** + * Returns a value from the data bundle. + * The data bundle might contain additional data send from an SQL query, + * for instance, the total number of rows. If the property cannot be + * found, the default value will be returned. If no default has + * been specified, this method returns NULL. + * + * @param string $key key + * @param mixed $default default (defaults to NULL) + * + * @return mixed; + */ + public function info( $key, $default = NULL ) { + return ( isset( $this->__info['data.bundle'][$key] ) ) ? $this->__info['data.bundle'][$key] : $default; + } + + /** + * Gets and unsets a meta property. + * Moves a meta property out of the bean. + * This is a short-cut method that can be used instead + * of combining a get/unset. + * + * @param string $path path to property in meta data + * @param mixed $default default value + * + * @return mixed + */ + public function moveMeta( $path, $value = NULL ) + { + if ( isset( $this->__info[$path] ) ) { + $value = $this->__info[ $path ]; + unset( $this->__info[ $path ] ); + } + return $value; + } + + /** + * Stores a value in the specified Meta information property. + * The first argument should be the key to store the value under, + * the second argument should be the value. It is common to use + * a path-like notation for meta data in RedBeanPHP like: + * 'my.meta.data', however the dots are purely for readability, the + * meta data methods do not store nested structures or hierarchies. + * + * @param string $path path / key to store value under + * @param mixed $value value to store in bean (not in database) as meta data + * + * @return OODBBean + */ + public function setMeta( $path, $value ) + { + $this->__info[$path] = $value; + if ( $path == 'type' && !empty($this->beanHelper)) { + $this->__info['model'] = $this->beanHelper->getModelForBean( $this ); + } + + return $this; + } + + /** + * Copies the meta information of the specified bean + * This is a convenience method to enable you to + * exchange meta information easily. + * + * @param OODBBean $bean bean to copy meta data of + * + * @return OODBBean + */ + public function copyMetaFrom( OODBBean $bean ) + { + $this->__info = $bean->__info; + + return $this; + } + + /** + * Sends the call to the registered model. + * This method can also be used to override bean behaviour. + * In that case you don't want an error or exception to be triggered + * if the method does not exist in the model (because it's optional). + * Unfortunately we cannot add an extra argument to __call() for this + * because the signature is fixed. Another option would be to set + * a special flag ( i.e. $this->isOptionalCall ) but that would + * cause additional complexity because we have to deal with extra temporary state. + * So, instead I allowed the method name to be prefixed with '@', in practice + * nobody creates methods like that - however the '@' symbol in PHP is widely known + * to suppress error handling, so we can reuse the semantics of this symbol. + * If a method name gets passed starting with '@' the overrideDontFail variable + * will be set to TRUE and the '@' will be stripped from the function name before + * attempting to invoke the method on the model. This way, we have all the + * logic in one place. + * + * @param string $method name of the method + * @param array $args argument list + * + * @return mixed + */ + public function __call( $method, $args ) + { + if ( empty( $this->__info['model'] ) ) { + return NULL; + } + + $overrideDontFail = FALSE; + if ( strpos( $method, '@' ) === 0 ) { + $method = substr( $method, 1 ); + $overrideDontFail = TRUE; + } + + if ( !is_callable( array( $this->__info['model'], $method ) ) ) { + + if ( self::$errorHandlingFUSE === FALSE || $overrideDontFail ) { + return NULL; + } + + if ( in_array( $method, array( 'update', 'open', 'delete', 'after_delete', 'after_update', 'dispense' ), TRUE ) ) { + return NULL; + } + + $message = "FUSE: method does not exist in model: $method"; + if ( self::$errorHandlingFUSE === self::C_ERR_LOG ) { + error_log( $message ); + return NULL; + } elseif ( self::$errorHandlingFUSE === self::C_ERR_NOTICE ) { + trigger_error( $message, E_USER_NOTICE ); + return NULL; + } elseif ( self::$errorHandlingFUSE === self::C_ERR_WARN ) { + trigger_error( $message, E_USER_WARNING ); + return NULL; + } elseif ( self::$errorHandlingFUSE === self::C_ERR_EXCEPTION ) { + throw new \Exception( $message ); + } elseif ( self::$errorHandlingFUSE === self::C_ERR_FUNC ) { + $func = self::$errorHandler; + return $func(array( + 'message' => $message, + 'method' => $method, + 'args' => $args, + 'bean' => $this + )); + } + trigger_error( $message, E_USER_ERROR ); + return NULL; + } + + return call_user_func_array( array( $this->__info['model'], $method ), $args ); + } + + /** + * Implementation of __toString Method + * Routes call to Model. If the model implements a __toString() method this + * method will be called and the result will be returned. In case of an + * echo-statement this result will be printed. If the model does not + * implement a __toString method, this method will return a JSON + * representation of the current bean. + * + * @return string + */ + public function __toString() + { + $string = $this->__call( '@__toString', array() ); + + if ( $string === NULL ) { + $list = array(); + foreach($this->properties as $property => $value) { + if (is_scalar($value)) { + if ( self::$enforceUTF8encoding ) { + $list[$property] = mb_convert_encoding($value, 'UTF-8', 'UTF-8'); + } else { + $list[$property] = $value; + } + } + } + $data = json_encode( $list ); + return $data; + } else { + return $string; + } + } + + /** + * Implementation of Array Access Interface, you can access bean objects + * like an array. + * Call gets routed to __set. + * + * @param mixed $offset offset string + * @param mixed $value value + * + * @return void + */ + public function offsetSet( $offset, $value ) + { + $this->__set( $offset, $value ); + } + + /** + * Implementation of Array Access Interface, you can access bean objects + * like an array. + * + * Array functions do not reveal x-own-lists and list-alias because + * you dont want duplicate entries in foreach-loops. + * Also offers a slight performance improvement for array access. + * + * @param mixed $offset property + * + * @return boolean + */ + public function offsetExists( $offset ) + { + return $this->__isset( $offset ); + } + + /** + * Implementation of Array Access Interface, you can access bean objects + * like an array. + * Unsets a value from the array/bean. + * + * Array functions do not reveal x-own-lists and list-alias because + * you dont want duplicate entries in foreach-loops. + * Also offers a slight performance improvement for array access. + * + * @param mixed $offset property + * + * @return void + */ + public function offsetUnset( $offset ) + { + $this->__unset( $offset ); + } + + /** + * Implementation of Array Access Interface, you can access bean objects + * like an array. + * Returns value of a property. + * + * Array functions do not reveal x-own-lists and list-alias because + * you dont want duplicate entries in foreach-loops. + * Also offers a slight performance improvement for array access. + * + * @param mixed $offset property + * + * @return mixed + */ + public function &offsetGet( $offset ) + { + return $this->__get( $offset ); + } + + /** + * Chainable method to cast a certain ID to a bean; for instance: + * $person = $club->fetchAs('person')->member; + * This will load a bean of type person using member_id as ID. + * + * @param string $type preferred fetch type + * + * @return OODBBean + */ + public function fetchAs( $type ) + { + $this->fetchType = $type; + + return $this; + } + + /** + * Prepares to load a bean using the bean type specified by + * another property. + * Similar to fetchAs but uses a column instead of a direct value. + * + * Usage: + * + * + * $car = R::load( 'car', $id ); + * $engine = $car->poly('partType')->part; + * + * + * In the example above, we have a bean of type car that + * may consists of several parts (i.e. chassis, wheels). + * To obtain the 'engine' we access the property 'part' + * using the type (i.e. engine) specified by the property + * indicated by the argument of poly(). + * This essentially is a polymorph relation, hence the name. + * In database this relation might look like this: + * + * partType | part_id + * -------------------- + * engine | 1020300 + * wheel | 4820088 + * chassis | 7823122 + * + * @param string $field field name to use for mapping + * + * @return OODBBean + */ + public function poly( $field ) + { + return $this->fetchAs( $this->$field ); + } + + /** + * Traverses a bean property with the specified function. + * Recursively iterates through the property invoking the + * function for each bean along the way passing the bean to it. + * + * Can be used together with with, withCondition, alias and fetchAs. + * + * + * $task + * ->withCondition(' priority >= ? ', [ $priority ]) + * ->traverse('ownTaskList', function( $t ) use ( &$todo ) { + * $todo[] = $t->descr; + * } ); + * + * + * In the example, we create a to-do list by traversing a + * hierarchical list of tasks while filtering out all tasks + * having a low priority. + * + * @param string $property property + * @param callable $function function + * @param integer $maxDepth maximum depth for traversal + * + * @return OODBBean + * @throws RedException + */ + public function traverse( $property, $function, $maxDepth = NULL, $depth = 1 ) + { + $this->via = NULL; + if ( strpos( $property, 'shared' ) !== FALSE ) { + throw new RedException( 'Traverse only works with (x)own-lists.' ); + } + + if ( !is_null( $maxDepth ) ) { + if ( !$maxDepth-- ) return $this; + } + + $oldFetchType = $this->fetchType; + $oldAliasName = $this->aliasName; + $oldWith = $this->withSql; + $oldBindings = $this->withParams; + + $beans = $this->$property; + + if ( $beans === NULL ) return $this; + + if ( !is_array( $beans ) ) $beans = array( $beans ); + + foreach( $beans as $bean ) { + $function( $bean, $depth ); + $bean->fetchType = $oldFetchType; + $bean->aliasName = $oldAliasName; + $bean->withSql = $oldWith; + $bean->withParams = $oldBindings; + + $bean->traverse( $property, $function, $maxDepth, $depth + 1 ); + } + + return $this; + } + + /** + * Implementation of Countable interface. Makes it possible to use + * count() function on a bean. This method gets invoked if you use + * the count() function on a bean. The count() method will return + * the number of properties of the bean, this includes the id property. + * + * Usage: + * + * + * $bean = R::dispense('bean'); + * $bean->property1 = 1; + * $bean->property2 = 2; + * echo count($bean); //prints 3 (cause id is also a property) + * + * + * The example above will print the number 3 to stdout. + * Although we have assigned values to just two properties, the + * primary key id is also a property of the bean and together + * that makes 3. Besides using the count() function, you can also + * call this method using a method notation: $bean->count(). + * + * @return integer + */ + public function count() + { + return count( $this->properties ); + } + + /** + * Checks whether a bean is empty or not. + * A bean is empty if it has no other properties than the id field OR + * if all the other properties are 'empty()' (this might + * include NULL and FALSE values). + * + * Usage: + * + * + * $newBean = R::dispense( 'bean' ); + * $newBean->isEmpty(); // TRUE + * + * + * The example above demonstrates that newly dispensed beans are + * considered 'empty'. + * + * @return boolean + */ + public function isEmpty() + { + $empty = TRUE; + foreach ( $this->properties as $key => $value ) { + if ( $key == 'id' ) { + continue; + } + if ( !empty( $value ) ) { + $empty = FALSE; + } + } + + return $empty; + } + + /** + * Chainable setter. + * This method is actually the same as just setting a value + * using a magic setter (->property = ...). The difference + * is that you can chain these setters like this: + * + * Usage: + * + * + * $book->setAttr('title', 'mybook')->setAttr('author', 'me'); + * + * + * This is the same as setting both properties $book->title and + * $book->author. Sometimes a chained notation can improve the + * readability of the code. + * + * @param string $property the property of the bean + * @param mixed $value the value you want to set + * + * @return OODBBean + */ + public function setAttr( $property, $value ) + { + $this->$property = $value; + + return $this; + } + + /** + * Convience method. + * Unsets all properties in the internal properties array. + * + * Usage: + * + * + * $bean->property = 1; + * $bean->unsetAll( array( 'property' ) ); + * $bean->property; //NULL + * + * + * In the example above the 'property' of the bean will be + * unset, resulting in the getter returning NULL instead of 1. + * + * @param array $properties properties you want to unset. + * + * @return OODBBean + */ + public function unsetAll( $properties ) + { + foreach ( $properties as $prop ) { + if ( isset( $this->properties[$prop] ) ) { + unset( $this->properties[$prop] ); + } + } + return $this; + } + + /** + * Returns original (old) value of a property. + * You can use this method to see what has changed in a + * bean. The original value of a property is the value that + * this property has had since the bean has been retrieved + * from the databases. + * + * + * $book->title = 'new title'; + * $oldTitle = $book->old('title'); + * + * + * The example shows how to use the old() method. + * Here we set the title property of the bean to 'new title', then + * we obtain the original value using old('title') and store it in + * a variable $oldTitle. + * + * @param string $property name of the property you want the old value of + * + * @return mixed + */ + public function old( $property ) + { + $old = $this->getMeta( 'sys.orig', array() ); + + if ( array_key_exists( $property, $old ) ) { + return $old[$property]; + } + + return NULL; + } + + /** + * Convenience method. + * + * Returns TRUE if the bean has been changed, or FALSE otherwise. + * Same as $bean->getMeta('tainted'); + * Note that a bean becomes tainted as soon as you retrieve a list from + * the bean. This is because the bean lists are arrays and the bean cannot + * determine whether you have made modifications to a list so RedBeanPHP + * will mark the whole bean as tainted. + * + * @return boolean + */ + public function isTainted() + { + return $this->getMeta( 'tainted' ); + } + + /** + * Returns TRUE if the value of a certain property of the bean has been changed and + * FALSE otherwise. + * + * Note that this method will return TRUE if applied to a loaded list. + * Also note that this method keeps track of the bean's history regardless whether + * it has been stored or not. Storing a bean does not undo it's history, + * to clean the history of a bean use: clearHistory(). + * + * @param string $property name of the property you want the change-status of + * + * @return boolean + */ + public function hasChanged( $property ) + { + return ( array_key_exists( $property, $this->properties ) ) ? + $this->old( $property ) != $this->properties[$property] : FALSE; + } + + /** + * Returns TRUE if the specified list exists, has been loaded + * and has been changed: + * beans have been added or deleted. + * This method will not tell you anything about + * the state of the beans in the list. + * + * Usage: + * + * + * $book->hasListChanged( 'ownPage' ); // FALSE + * array_pop( $book->ownPageList ); + * $book->hasListChanged( 'ownPage' ); // TRUE + * + * + * In the example, the first time we ask whether the + * own-page list has been changed we get FALSE. Then we pop + * a page from the list and the hasListChanged() method returns TRUE. + * + * @param string $property name of the list to check + * + * @return boolean + */ + public function hasListChanged( $property ) + { + if ( !array_key_exists( $property, $this->properties ) ) return FALSE; + $diffAdded = array_diff_assoc( $this->properties[$property], $this->__info['sys.shadow.'.$property] ); + if ( count( $diffAdded ) ) return TRUE; + $diffMissing = array_diff_assoc( $this->__info['sys.shadow.'.$property], $this->properties[$property] ); + if ( count( $diffMissing ) ) return TRUE; + return FALSE; + } + + /** + * Clears (syncs) the history of the bean. + * Resets all shadow values of the bean to their current value. + * + * Usage: + * + * + * $book->title = 'book'; + * echo $book->hasChanged( 'title' ); //TRUE + * R::store( $book ); + * echo $book->hasChanged( 'title' ); //TRUE + * $book->clearHistory(); + * echo $book->hasChanged( 'title' ); //FALSE + * + * + * Note that even after store(), the history of the bean still + * contains the act of changing the title of the book. + * Only after invoking clearHistory() will the history of the bean + * be cleared and will hasChanged() return FALSE. + * + * @return self + */ + public function clearHistory() + { + $this->__info['sys.orig'] = array(); + foreach( $this->properties as $key => $value ) { + if ( is_scalar($value) ) { + $this->__info['sys.orig'][$key] = $value; + } else { + $this->__info['sys.shadow.'.$key] = $value; + } + } + $this->__info[ 'changelist' ] = array(); + return $this; + } + + /** + * Creates a N-M relation by linking an intermediate bean. + * This method can be used to quickly connect beans using indirect + * relations. For instance, given an album and a song you can connect the two + * using a track with a number like this: + * + * Usage: + * + * + * $album->link('track', array('number'=>1))->song = $song; + * + * + * or: + * + * + * $album->link($trackBean)->song = $song; + * + * + * What this method does is adding the link bean to the own-list, in this case + * ownTrack. If the first argument is a string and the second is an array or + * a JSON string then the linking bean gets dispensed on-the-fly as seen in + * example #1. After preparing the linking bean, the bean is returned thus + * allowing the chained setter: ->song = $song. + * + * @param string|OODBBean $typeOrBean type of bean to dispense or the full bean + * @param string|array $qualification JSON string or array (optional) + * + * @return OODBBean + */ + public function link( $typeOrBean, $qualification = array() ) + { + if ( is_string( $typeOrBean ) ) { + $typeOrBean = AQueryWriter::camelsSnake( $typeOrBean ); + $bean = $this->beanHelper->getToolBox()->getRedBean()->dispense( $typeOrBean ); + if ( is_string( $qualification ) ) { + $data = json_decode( $qualification, TRUE ); + } else { + $data = $qualification; + } + foreach ( $data as $key => $value ) { + $bean->$key = $value; + } + } else { + $bean = $typeOrBean; + } + $list = 'own' . ucfirst( $bean->getMeta( 'type' ) ); + array_push( $this->$list, $bean ); + return $bean; + } + + /** + * Returns a bean of the given type with the same ID of as + * the current one. This only happens in a one-to-one relation. + * This is as far as support for 1-1 goes in RedBeanPHP. This + * method will only return a reference to the bean, changing it + * and storing the bean will not update the related one-bean. + * + * Usage: + * + * + * $author = R::load( 'author', $id ); + * $biography = $author->one( 'bio' ); + * + * + * The example loads the biography associated with the author + * using a one-to-one relation. These relations are generally not + * created (nor supported) by RedBeanPHP. + * + * @param $type type of bean to load + * + * @return OODBBean + */ + public function one( $type ) { + return $this->beanHelper + ->getToolBox() + ->getRedBean() + ->load( $type, $this->id ); + } + + /** + * Reloads the bean. + * Returns the same bean freshly loaded from the database. + * This method is equal to the following code: + * + * + * $id = $bean->id; + * $type = $bean->getMeta( 'type' ); + * $bean = R::load( $type, $id ); + * + * + * This is just a convenience method to reload beans + * quickly. + * + * Usage: + * + * + * R::exec( ...update query... ); + * $book = $book->fresh(); + * + * + * The code snippet above illustrates how to obtain changes + * caused by an UPDATE query, simply by reloading the bean using + * the fresh() method. + * + * @return OODBBean + */ + public function fresh() + { + return $this->beanHelper + ->getToolbox() + ->getRedBean() + ->load( $this->getMeta( 'type' ), $this->properties['id'] ); + } + + /** + * Registers a association renaming globally. + * Use via() and link() to associate shared beans using a + * 3rd bean that will act as an intermediate type. For instance + * consider an employee and a project. We could associate employees + * with projects using a sharedEmployeeList. But, maybe there is more + * to the relationship than just the association. Maybe we want + * to qualify the relation between a project and an employee with + * a role: 'developer', 'designer', 'tester' and so on. In that case, + * it might be better to introduce a new concept to reflect this: + * the participant. However, we still want the flexibility to + * query our employees in one go. This is where link() and via() + * can help. You can still introduce the more applicable + * concept (participant) and have your easy access to the shared beans. + * + * + * $Anna = R::dispense( 'employee' ); + * $Anna->badge = 'Anna'; + * $project = R::dispense( 'project' ); + * $project->name = 'x'; + * $Anna->link( 'participant', array( + * 'arole' => 'developer' + * ) )->project = $project; + * R::storeAll( array( $project, $Anna ) ); + * $employees = $project + * ->with(' ORDER BY badge ASC ') + * ->via( 'participant' ) + * ->sharedEmployee; + * + * + * This piece of code creates a project and an employee. + * It then associates the two using a via-relation called + * 'participant' ( employee <-> participant <-> project ). + * So, there will be a table named 'participant' instead of + * a table named 'employee_project'. Using the via() method, the + * employees associated with the project are retrieved 'via' + * the participant table (and an SQL snippet to order them by badge). + * + * @param string $via type you wish to use for shared lists + * + * @return OODBBean + */ + public function via( $via ) + { + $this->via = AQueryWriter::camelsSnake( $via ); + + return $this; + } + + /** + * Counts all own beans of type $type. + * Also works with alias(), with() and withCondition(). + * Own-beans or xOwn-beans (exclusively owned beans) are beans + * that have been associated using a one-to-many relation. They can + * be accessed through the ownXList where X is the type of the + * associated beans. + * + * Usage: + * + * + * $Bill->alias( 'author' ) + * ->countOwn( 'book' ); + * + * + * The example above counts all the books associated with 'author' + * $Bill. + * + * @param string $type the type of bean you want to count + * + * @return integer + */ + public function countOwn( $type ) + { + $type = $this->beau( $type ); + if ( $this->aliasName ) { + $myFieldLink = $this->aliasName . '_id'; + $this->aliasName = NULL; + } else { + $myFieldLink = $this->__info['type'] . '_id'; + } + $count = 0; + if ( $this->getID() ) { + reset( $this->withParams ); + $firstKey = count( $this->withParams ) > 0 + ? key( $this->withParams ) + : 0; + if ( is_int( $firstKey ) ) { + $sql = "{$myFieldLink} = ? {$this->withSql}"; + $bindings = array_merge( array( $this->getID() ), $this->withParams ); + } else { + $sql = "{$myFieldLink} = :slot0 {$this->withSql}"; + $bindings = $this->withParams; + $bindings[':slot0'] = $this->getID(); + } + if ( !self::$useFluidCount ) { + $count = $this->beanHelper->getToolbox()->getWriter()->queryRecordCount( $type, array(), $sql, $bindings ); + } else { + $count = $this->beanHelper->getToolbox()->getRedBean()->count( $type, $sql, $bindings ); + } + } + $this->clearModifiers(); + return (int) $count; + } + + /** + * Counts all shared beans of type $type. + * Also works with via(), with() and withCondition(). + * Shared beans are beans that have an many-to-many relation. + * They can be accessed using the sharedXList, where X the + * type of the shared bean. + * + * Usage: + * + * + * $book = R::dispense( 'book' ); + * $book->sharedPageList = R::dispense( 'page', 5 ); + * R::store( $book ); + * echo $book->countShared( 'page' ); + * + * + * The code snippet above will output '5', because there + * are 5 beans of type 'page' in the shared list. + * + * @param string $type type of bean you wish to count + * + * @return integer + */ + public function countShared( $type ) + { + $toolbox = $this->beanHelper->getToolbox(); + $redbean = $toolbox->getRedBean(); + $writer = $toolbox->getWriter(); + if ( $this->via ) { + $oldName = $writer->getAssocTable( array( $this->__info['type'], $type ) ); + if ( $oldName !== $this->via ) { + //set the new renaming rule + $writer->renameAssocTable( $oldName, $this->via ); + $this->via = NULL; + } + } + $type = $this->beau( $type ); + $count = 0; + if ( $this->getID() ) { + $count = $redbean->getAssociationManager()->relatedCount( $this, $type, $this->withSql, $this->withParams ); + } + $this->clearModifiers(); + return (integer) $count; + } + + /** + * Iterates through the specified own-list and + * fetches all properties (with their type) and + * returns the references. + * Use this method to quickly load indirectly related + * beans in an own-list. Whenever you cannot use a + * shared-list this method offers the same convenience + * by aggregating the parent beans of all children in + * the specified own-list. + * + * Example: + * + * + * $quest->aggr( 'xownQuestTarget', 'target', 'quest' ); + * + * + * Loads (in batch) and returns references to all + * quest beans residing in the $questTarget->target properties + * of each element in the xownQuestTargetList. + * + * @param string $list the list you wish to process + * @param string $property the property to load + * @param string $type the type of bean residing in this property (optional) + * + * @return array + */ + public function &aggr( $list, $property, $type = NULL ) + { + $this->via = NULL; + $ids = $beanIndex = $references = array(); + + if ( strlen( $list ) < 4 ) throw new RedException('Invalid own-list.'); + if ( strpos( $list, 'own') !== 0 ) throw new RedException('Only own-lists can be aggregated.'); + if ( !ctype_upper( substr( $list, 3, 1 ) ) ) throw new RedException('Invalid own-list.'); + + if ( is_null( $type ) ) $type = $property; + + foreach( $this->$list as $bean ) { + $field = $property . '_id'; + if ( isset( $bean->$field) ) { + $ids[] = $bean->$field; + $beanIndex[$bean->$field] = $bean; + } + } + + $beans = $this->beanHelper->getToolBox()->getRedBean()->batch( $type, $ids ); + + //now preload the beans as well + foreach( $beans as $bean ) { + $beanIndex[$bean->id]->setProperty( $property, $bean ); + } + + foreach( $beanIndex as $indexedBean ) { + $references[] = $indexedBean->$property; + } + + return $references; + } + + /** + * Tests whether the database identities of two beans are equal. + * Two beans are considered 'equal' if: + * + * a. the types of the beans match + * b. the ids of the beans match + * + * Returns TRUE if the beans are considered equal according to this + * specification and FALSE otherwise. + * + * Usage: + * + * + * $coffee->fetchAs( 'flavour' )->taste->equals( + * R::enum('flavour:mocca') + * ); + * + * + * The example above compares the flavour label 'mocca' with + * the flavour label attachec to the $coffee bean. This illustrates + * how to use equals() with RedBeanPHP-style enums. + * + * @param OODBBean|null $bean other bean + * + * @return boolean + */ + public function equals(OODBBean $bean) + { + if ( is_null($bean) ) return false; + + return (bool) ( + ( (string) $this->properties['id'] === (string) $bean->properties['id'] ) + && ( (string) $this->__info['type'] === (string) $bean->__info['type'] ) + ); + } + + /** + * Magic method jsonSerialize, + * implementation for the \JsonSerializable interface, + * this method gets called by json_encode and + * facilitates a better JSON representation + * of the bean. Exports the bean on JSON serialization, + * for the JSON fans. + * + * Models can override jsonSerialize (issue #651) by + * implementing a __jsonSerialize method which should return + * an array. The __jsonSerialize override gets called with + * the @ modifier to prevent errors or warnings. + * + * @see http://php.net/manual/en/class.jsonserializable.php + * + * @return array + */ + public function jsonSerialize() + { + $json = $this->__call( '@__jsonSerialize', array( ) ); + + if ( $json !== NULL ) { + return $json; + } + + return $this->export(); + } +} +} + +namespace RedBeanPHP { + +use RedBeanPHP\Observer as Observer; + +/** + * Observable + * Base class for Observables + * + * @file RedBeanPHP/Observable.php + * @author Gabor de Mooij and the RedBeanPHP community + * @license BSD/GPLv2 + * + * @copyright + * copyright (c) G.J.G.T. (Gabor) de Mooij and the RedBeanPHP Community. + * This source file is subject to the BSD/GPLv2 License that is bundled + * with this source code in the file license.txt. + */ +abstract class Observable { //bracket must be here - otherwise coverage software does not understand. + + /** + * @var array + */ + private $observers = array(); + + /** + * Implementation of the Observer Pattern. + * Adds an event listener to the observable object. + * First argument should be the name of the event you wish to listen for. + * Second argument should be the object that wants to be notified in case + * the event occurs. + * + * @param string $eventname event identifier + * @param Observer $observer observer instance + * + * @return void + */ + public function addEventListener( $eventname, Observer $observer ) + { + if ( !isset( $this->observers[$eventname] ) ) { + $this->observers[$eventname] = array(); + } + + if ( in_array( $observer, $this->observers[$eventname] ) ) { + return; + } + + $this->observers[$eventname][] = $observer; + } + + /** + * Notifies listeners. + * Sends the signal $eventname, the event identifier and a message object + * to all observers that have been registered to receive notification for + * this event. Part of the observer pattern implementation in RedBeanPHP. + * + * @param string $eventname event you want signal + * @param mixed $info message object to send along + * + * @return void + */ + public function signal( $eventname, $info ) + { + if ( !isset( $this->observers[$eventname] ) ) { + $this->observers[$eventname] = array(); + } + + foreach ( $this->observers[$eventname] as $observer ) { + $observer->onEvent( $eventname, $info ); + } + } +} +} + +namespace RedBeanPHP { + +/** + * Observer. + * + * Interface for Observer object. Implementation of the + * observer pattern. + * + * @file RedBeanPHP/Observer.php + * @author Gabor de Mooij and the RedBeanPHP community + * @license BSD/GPLv2 + * @desc Part of the observer pattern in RedBean + * + * @copyright + * copyright (c) G.J.G.T. (Gabor) de Mooij and the RedBeanPHP Community. + * This source file is subject to the BSD/GPLv2 License that is bundled + * with this source code in the file license.txt. + */ +interface Observer +{ + /** + * An observer object needs to be capable of receiving + * notifications. Therefore the observer needs to implement the + * onEvent method with two parameters: the event identifier specifying the + * current event and a message object (in RedBeanPHP this can also be a bean). + * + * @param string $eventname event identifier + * @param mixed $bean a message sent along with the notification + * + * @return void + */ + public function onEvent( $eventname, $bean ); +} +} + +namespace RedBeanPHP { + +/** + * Adapter Interface. + * Describes the API for a RedBeanPHP Database Adapter. + * This interface defines the API contract for + * a RedBeanPHP Database Adapter. + * + * @file RedBeanPHP/Adapter.php + * @author Gabor de Mooij and the RedBeanPHP Community + * @license BSD/GPLv2 + * + * @copyright + * (c) G.J.G.T. (Gabor) de Mooij and the RedBeanPHP Community. + * This source file is subject to the BSD/GPLv2 License that is bundled + * with this source code in the file license.txt. + */ +interface Adapter +{ + /** + * Should returns a string containing the most recent SQL query + * that has been processed by the adapter. + * + * @return string + */ + public function getSQL(); + + /** + * Executes an SQL Statement using an array of values to bind + * If $noevent is TRUE then this function will not signal its + * observers to notify about the SQL execution; this to prevent + * infinite recursion when using observers. + * + * @param string $sql string containing SQL code for database + * @param array $bindings array of values to bind to parameters in query string + * @param boolean $noevent no event firing + * + * @return void + */ + public function exec( $sql, $bindings = array(), $noevent = FALSE ); + + /** + * Executes an SQL Query and returns a resultset. + * This method returns a multi dimensional resultset similar to getAll + * The values array can be used to bind values to the place holders in the + * SQL query. + * + * @param string $sql string containing SQL code for database + * @param array $bindings array of values to bind to parameters in query string + * + * @return array + */ + public function get( $sql, $bindings = array() ); + + /** + * Executes an SQL Query and returns a resultset. + * This method returns a single row (one array) resultset. + * The values array can be used to bind values to the place holders in the + * SQL query. + * + * @param string $sql string containing SQL code for database + * @param array $bindings array of values to bind to parameters in query string + * + * @return array + */ + public function getRow( $sql, $bindings = array() ); + + /** + * Executes an SQL Query and returns a resultset. + * This method returns a single column (one array) resultset. + * The values array can be used to bind values to the place holders in the + * SQL query. + * + * @param string $sql string containing SQL code for database + * @param array $bindings array of values to bind to parameters in query string + * + * @return array + */ + public function getCol( $sql, $bindings = array() ); + + /** + * Executes an SQL Query and returns a resultset. + * This method returns a single cell, a scalar value as the resultset. + * The values array can be used to bind values to the place holders in the + * SQL query. + * + * @param string $sql string containing SQL code for database + * @param array $bindings array of values to bind to parameters in query string + * + * @return string + */ + public function getCell( $sql, $bindings = array() ); + + /** + * Executes the SQL query specified in $sql and indexes + * the row by the first column. + * + * @param string $sql string containing SQL code for database + * @param array $bindings array of values to bind to parameters in query string + * + * @return array + */ + public function getAssoc( $sql, $bindings = array() ); + + /** + * Executes the SQL query specified in $sql and returns + * an associative array where the column names are the keys. + * + * @param string $sql Sstring containing SQL code for databaseQL + * @param array $bindings values to bind + * + * @return array + */ + public function getAssocRow( $sql, $bindings = array() ); + + /** + * Returns the latest insert ID. + * + * @return integer + */ + public function getInsertID(); + + /** + * Returns the number of rows that have been + * affected by the last update statement. + * + * @return integer + */ + public function getAffectedRows(); + + /** + * Returns a database agnostic Cursor object. + * + * @param string $sql string containing SQL code for database + * @param array $bindings array of values to bind to parameters in query string + * + * @return Cursor + */ + public function getCursor( $sql, $bindings = array() ); + + /** + * Returns the original database resource. This is useful if you want to + * perform operations on the driver directly instead of working with the + * adapter. RedBean will only access the adapter and never to talk + * directly to the driver though. + * + * @return mixed + */ + public function getDatabase(); + + /** + * This method is part of the RedBean Transaction Management + * mechanisms. + * Starts a transaction. + * + * @return void + */ + public function startTransaction(); + + /** + * This method is part of the RedBean Transaction Management + * mechanisms. + * Commits the transaction. + * + * @return void + */ + public function commit(); + + /** + * This method is part of the RedBean Transaction Management + * mechanisms. + * Rolls back the transaction. + * + * @return void + */ + public function rollback(); + + /** + * Closes database connection. + * + * @return void + */ + public function close(); + + /** + * Sets a driver specific option. + * Using this method you can access driver-specific functions. + * If the selected option exists the value will be passed and + * this method will return boolean TRUE, otherwise it will return + * boolean FALSE. + * + * @param string $optionKey option key + * @param string $optionValue option value + * + * @return boolean + */ + public function setOption( $optionKey, $optionValue ); + + /** + * Returns the version string from the database server. + * + * @return string + */ + public function getDatabaseServerVersion(); +} +} + +namespace RedBeanPHP\Adapter { + +use RedBeanPHP\Observable as Observable; +use RedBeanPHP\Adapter as Adapter; +use RedBeanPHP\Driver as Driver; + +/** + * DBAdapter (Database Adapter) + * + * An adapter class to connect various database systems to RedBean + * Database Adapter Class. The task of the database adapter class is to + * communicate with the database driver. You can use all sorts of database + * drivers with RedBeanPHP. The default database drivers that ships with + * the RedBeanPHP library is the RPDO driver ( which uses the PHP Data Objects + * Architecture aka PDO ). + * + * @file RedBeanPHP/Adapter/DBAdapter.php + * @author Gabor de Mooij and the RedBeanPHP Community. + * @license BSD/GPLv2 + * + * @copyright + * (c) copyright G.J.G.T. (Gabor) de Mooij and the RedBeanPHP community. + * This source file is subject to the BSD/GPLv2 License that is bundled + * with this source code in the file license.txt. + */ +class DBAdapter extends Observable implements Adapter +{ + /** + * @var Driver + */ + private $db = NULL; + + /** + * @var string + */ + private $sql = ''; + + /** + * Constructor. + * + * Creates an instance of the RedBean Adapter Class. + * This class provides an interface for RedBean to work + * with ADO compatible DB instances. + * + * Usage: + * + * + * $database = new RPDO( $dsn, $user, $pass ); + * $adapter = new DBAdapter( $database ); + * $writer = new PostgresWriter( $adapter ); + * $oodb = new OODB( $writer, FALSE ); + * $bean = $oodb->dispense( 'bean' ); + * $bean->name = 'coffeeBean'; + * $id = $oodb->store( $bean ); + * $bean = $oodb->load( 'bean', $id ); + * + * + * The example above creates the 3 RedBeanPHP core objects: + * the Adapter, the Query Writer and the OODB instance and + * wires them together. The example also demonstrates some of + * the methods that can be used with OODB, as you see, they + * closely resemble their facade counterparts. + * + * The wiring process: create an RPDO instance using your database + * connection parameters. Create a database adapter from the RPDO + * object and pass that to the constructor of the writer. Next, + * create an OODB instance from the writer. Now you have an OODB + * object. + * + * @param Driver $database ADO Compatible DB Instance + */ + public function __construct( $database ) + { + $this->db = $database; + } + + /** + * Returns a string containing the most recent SQL query + * processed by the database adapter, thus conforming to the + * interface: + * + * @see Adapter::getSQL + * + * Methods like get(), getRow() and exec() cause this SQL cache + * to get filled. If no SQL query has been processed yet this function + * will return an empty string. + * + * @return string + */ + public function getSQL() + { + return $this->sql; + } + + /** + * @see Adapter::exec + */ + public function exec( $sql, $bindings = array(), $noevent = FALSE ) + { + if ( !$noevent ) { + $this->sql = $sql; + $this->signal( 'sql_exec', $this ); + } + + return $this->db->Execute( $sql, $bindings ); + } + + /** + * @see Adapter::get + */ + public function get( $sql, $bindings = array() ) + { + $this->sql = $sql; + $this->signal( 'sql_exec', $this ); + + return $this->db->GetAll( $sql, $bindings ); + } + + /** + * @see Adapter::getRow + */ + public function getRow( $sql, $bindings = array() ) + { + $this->sql = $sql; + $this->signal( 'sql_exec', $this ); + + return $this->db->GetRow( $sql, $bindings ); + } + + /** + * @see Adapter::getCol + */ + public function getCol( $sql, $bindings = array() ) + { + $this->sql = $sql; + $this->signal( 'sql_exec', $this ); + + return $this->db->GetCol( $sql, $bindings ); + } + + /** + * @see Adapter::getAssoc + */ + public function getAssoc( $sql, $bindings = array() ) + { + $this->sql = $sql; + + $this->signal( 'sql_exec', $this ); + + $rows = $this->db->GetAll( $sql, $bindings ); + + if ( !$rows ) return array(); + + $assoc = array(); + + foreach ( $rows as $row ) { + if ( empty( $row ) ) continue; + + $key = array_shift( $row ); + switch ( count( $row ) ) { + case 0: + $value = $key; + break; + case 1: + $value = reset( $row ); + break; + default: + $value = $row; + } + + $assoc[$key] = $value; + } + + return $assoc; + } + + /** + * @see Adapter::getAssocRow + */ + public function getAssocRow($sql, $bindings = array()) + { + $this->sql = $sql; + $this->signal( 'sql_exec', $this ); + + return $this->db->GetAssocRow( $sql, $bindings ); + } + + /** + * @see Adapter::getCell + */ + public function getCell( $sql, $bindings = array(), $noSignal = NULL ) + { + $this->sql = $sql; + + if ( !$noSignal ) $this->signal( 'sql_exec', $this ); + + return $this->db->GetOne( $sql, $bindings ); + } + + /** + * @see Adapter::getCursor + */ + public function getCursor( $sql, $bindings = array() ) + { + return $this->db->GetCursor( $sql, $bindings ); + } + + /** + * @see Adapter::getInsertID + */ + public function getInsertID() + { + return $this->db->getInsertID(); + } + + /** + * @see Adapter::getAffectedRows + */ + public function getAffectedRows() + { + return $this->db->Affected_Rows(); + } + + /** + * @see Adapter::getDatabase + */ + public function getDatabase() + { + return $this->db; + } + + /** + * @see Adapter::startTransaction + */ + public function startTransaction() + { + $this->db->StartTrans(); + } + + /** + * @see Adapter::commit + */ + public function commit() + { + $this->db->CommitTrans(); + } + + /** + * @see Adapter::rollback + */ + public function rollback() + { + $this->db->FailTrans(); + } + + /** + * @see Adapter::close. + */ + public function close() + { + $this->db->close(); + } + + /** + * Sets initialization code for connection. + * + * @param callable $code + */ + public function setInitCode($code) { + $this->db->setInitCode($code); + } + + /** + * @see Adapter::setOption + */ + public function setOption( $optionKey, $optionValue ) { + if ( method_exists( $this->db, $optionKey ) ) { + call_user_func( array( $this->db, $optionKey ), $optionValue ); + return TRUE; + } + return FALSE; + } + + /** + * @see Adapter::getDatabaseServerVersion + */ + public function getDatabaseServerVersion() + { + return $this->db->DatabaseServerVersion(); + } +} +} + +namespace RedBeanPHP { + +/** + * Database Cursor Interface. + * A cursor is used by Query Writers to fetch Query Result rows + * one row at a time. This is useful if you expect the result set to + * be quite large. This interface dscribes the API of a database + * cursor. There can be multiple implementations of the Cursor, + * by default RedBeanPHP offers the PDOCursor for drivers shipping + * with RedBeanPHP and the NULLCursor. + * + * @file RedBeanPHP/Cursor.php + * @author Gabor de Mooij and the RedBeanPHP Community + * @license BSD/GPLv2 + * + * @copyright + * copyright (c) G.J.G.T. (Gabor) de Mooij and the RedBeanPHP Community + * This source file is subject to the BSD/GPLv2 License that is bundled + * with this source code in the file license.txt. + */ +interface Cursor +{ + /** + * Should retrieve the next row of the result set. + * This method is used to iterate over the result set. + * + * @return array + */ + public function getNextItem(); + + /** + * Resets the cursor by closing it and re-executing the statement. + * This reloads fresh data from the database for the whole collection. + * + * @return void + */ + public function reset(); + + /** + * Closes the database cursor. + * Some databases require a cursor to be closed before executing + * another statement/opening a new cursor. + * + * @return void + */ + public function close(); +} +} + +namespace RedBeanPHP\Cursor { + +use RedBeanPHP\Cursor as Cursor; + +/** + * PDO Database Cursor + * Implementation of PDO Database Cursor. + * Used by the BeanCollection to fetch one bean at a time. + * The PDO Cursor is used by Query Writers to support retrieval + * of large bean collections. For instance, this class is used to + * implement the findCollection()/BeanCollection functionality. + * + * @file RedBeanPHP/Cursor/PDOCursor.php + * @author Gabor de Mooij and the RedBeanPHP Community + * @license BSD/GPLv2 + * + * @copyright + * (c) G.J.G.T. (Gabor) de Mooij and the RedBeanPHP Community. + * This source file is subject to the BSD/GPLv2 License that is bundled + * with this source code in the file license.txt. + */ +class PDOCursor implements Cursor +{ + /** + * @var PDOStatement + */ + protected $res; + + /** + * @var string + */ + protected $fetchStyle; + + /** + * Constructor, creates a new instance of a PDO Database Cursor. + * + * @param PDOStatement $res the PDO statement + * @param string $fetchStyle fetch style constant to use + * + * @return void + */ + public function __construct( \PDOStatement $res, $fetchStyle ) + { + $this->res = $res; + $this->fetchStyle = $fetchStyle; + } + + /** + * @see Cursor::getNextItem + */ + public function getNextItem() + { + return $this->res->fetch(); + } + + /** + * @see Cursor::reset + */ + public function reset() + { + $this->close(); + $this->res->execute(); + } + + /** + * @see Cursor::close + */ + public function close() + { + $this->res->closeCursor(); + } +} +} + +namespace RedBeanPHP\Cursor { + +use RedBeanPHP\Cursor as Cursor; + +/** + * NULL Database Cursor + * Implementation of the NULL Cursor. + * Used for an empty BeanCollection. This Cursor + * can be used for instance if a query fails but the interface + * demands a cursor to be returned. + * + * @file RedBeanPHP/Cursor/NULLCursor.php + * @author Gabor de Mooij and the RedBeanPHP Community + * @license BSD/GPLv2 + * + * @copyright + * (c) G.J.G.T. (Gabor) de Mooij and the RedBeanPHP Community. + * This source file is subject to the BSD/GPLv2 License that is bundled + * with this source code in the file license.txt. + */ +class NullCursor implements Cursor +{ + /** + * @see Cursor::getNextItem + */ + public function getNextItem() + { + return NULL; + } + + /** + * @see Cursor::reset + */ + public function reset() + { + return NULL; + } + + /** + * @see Cursor::close + */ + public function close() + { + return NULL; + } +} +} + +namespace RedBeanPHP { + +use RedBeanPHP\Cursor as Cursor; +use RedBeanPHP\Repository as Repository; + +/** + * BeanCollection. + * + * The BeanCollection represents a collection of beans and + * makes it possible to use database cursors. The BeanCollection + * has a method next() to obtain the first, next and last bean + * in the collection. The BeanCollection does not implement the array + * interface nor does it try to act like an array because it cannot go + * backward or rewind itself. + * + * Use the BeanCollection for large datasets where skip/limit is not an + * option. Keep in mind that ID-marking (querying a start ID) is a decent + * alternative though. + * + * @file RedBeanPHP/BeanCollection.php + * @author Gabor de Mooij and the RedBeanPHP community + * @license BSD/GPLv2 + * + * @copyright + * copyright (c) G.J.G.T. (Gabor) de Mooij and the RedBeanPHP Community + * This source file is subject to the BSD/GPLv2 License that is bundled + * with this source code in the file license.txt. + */ +class BeanCollection +{ + /** + * @var Cursor + */ + protected $cursor = NULL; + + /** + * @var Repository + */ + protected $repository = NULL; + + /** + * @var string + */ + protected $type = NULL; + + /** + * Constructor, creates a new instance of the BeanCollection. + * + * @param string $type type of beans in this collection + * @param Repository $repository repository to use to generate bean objects + * @param Cursor $cursor cursor object to use + * + * @return void + */ + public function __construct( $type, Repository $repository, Cursor $cursor ) + { + $this->type = $type; + $this->cursor = $cursor; + $this->repository = $repository; + } + + /** + * Returns the next bean in the collection. + * If called the first time, this will return the first bean in the collection. + * If there are no more beans left in the collection, this method + * will return NULL. + * + * @return OODBBean|NULL + */ + public function next() + { + $row = $this->cursor->getNextItem(); + if ( $row ) { + $beans = $this->repository->convertToBeans( $this->type, array( $row ) ); + return reset( $beans ); + } + return NULL; + } + + /** + * Resets the collection from the start, like a fresh() on a bean. + * + * @return void + */ + public function reset() + { + $this->cursor->reset(); + } + + /** + * Closes the underlying cursor (needed for some databases). + * + * @return void + */ + public function close() + { + $this->cursor->close(); + } +} +} + +namespace RedBeanPHP { + +/** + * QueryWriter + * Interface for QueryWriters. + * Describes the API for a QueryWriter. + * + * Terminology: + * + * - beautified property (a camelCased property, has to be converted first) + * - beautified type (a camelCased type, has to be converted first) + * - type (a bean type, corresponds directly to a table) + * - property (a bean property, corresponds directly to a column) + * - table (a checked and quoted type, ready for use in a query) + * - column (a checked and quoted property, ready for use in query) + * - tableNoQ (same as type, but in context of a database operation) + * - columnNoQ (same as property, but in context of a database operation) + * + * @file RedBeanPHP/QueryWriter.php + * @author Gabor de Mooij and the RedBeanPHP community + * @license BSD/GPLv2 + * + * @copyright + * copyright (c) G.J.G.T. (Gabor) de Mooij and the RedBeanPHP Community. + * This source file is subject to the BSD/GPLv2 License that is bundled + * with this source code in the file license.txt. + */ +interface QueryWriter +{ + /** + * SQL filter constants + */ + const C_SQLFILTER_READ = 'r'; + const C_SQLFILTER_WRITE = 'w'; + + /** + * Query Writer constants. + */ + const C_SQLSTATE_NO_SUCH_TABLE = 1; + const C_SQLSTATE_NO_SUCH_COLUMN = 2; + const C_SQLSTATE_INTEGRITY_CONSTRAINT_VIOLATION = 3; + const C_SQLSTATE_LOCK_TIMEOUT = 4; + + /** + * Define data type regions + * + * 00 - 80: normal data types + * 80 - 99: special data types, only scan/code if requested + * 99 : specified by user, don't change + */ + const C_DATATYPE_RANGE_SPECIAL = 80; + const C_DATATYPE_RANGE_SPECIFIED = 99; + + /** + * Define GLUE types for use with glueSQLCondition methods. + * Determines how to prefix a snippet of SQL before appending it + * to other SQL (or integrating it, mixing it otherwise). + * + * WHERE - glue as WHERE condition + * AND - glue as AND condition + */ + const C_GLUE_WHERE = 1; + const C_GLUE_AND = 2; + + /** + * CTE Select Snippet + * Constants specifying select snippets for CTE queries + */ + const C_CTE_SELECT_NORMAL = FALSE; + const C_CTE_SELECT_COUNT = TRUE; + + /** + * Parses an sql string to create joins if needed. + * + * For instance with $type = 'book' and $sql = ' @joined.author.name LIKE ? OR @joined.detail.title LIKE ? ' + * parseJoin will return the following SQL: + * ' LEFT JOIN `author` ON `author`.id = `book`.author_id + * LEFT JOIN `detail` ON `detail`.id = `book`.detail_id + * WHERE author.name LIKE ? OR detail.title LIKE ? ' + * + * @note this feature requires Narrow Field Mode to be activated (default). + * + * @note A default implementation is available in AQueryWriter + * unless a database uses very different SQL this should suffice. + * + * @param string $type the source type for the join + * @param string $sql the sql string to be parsed + * + * @return string + */ + public function parseJoin( $type, $sql ); + + /** + * Writes an SQL Snippet for a JOIN, returns the + * SQL snippet string. + * + * @note A default implementation is available in AQueryWriter + * unless a database uses very different SQL this should suffice. + * + * @param string $type source type + * @param string $targetType target type (type to join) + * @param string $leftRight type of join (possible: 'LEFT', 'RIGHT' or 'INNER') + * @param string $joinType relation between joined tables (possible: 'parent', 'own', 'shared') + * @param boolean $firstOfChain is it the join of a chain (or the only join) + * @param string $suffix suffix to add for aliasing tables (for joining same table multiple times) + * + * @return string $joinSQLSnippet + */ + public function writeJoin( $type, $targetType, $leftRight, $joinType, $firstOfChain, $suffix ); + + /** + * Glues an SQL snippet to the beginning of a WHERE clause. + * This ensures users don't have to add WHERE to their query snippets. + * + * The snippet gets prefixed with WHERE or AND + * if it starts with a condition. + * + * If the snippet does NOT start with a condition (or this function thinks so) + * the snippet is returned as-is. + * + * The GLUE type determines the prefix: + * + * * NONE prefixes with WHERE + * * WHERE prefixes with WHERE and replaces AND if snippets starts with AND + * * AND prefixes with AND + * + * This method will never replace WHERE with AND since a snippet should never + * begin with WHERE in the first place. OR is not supported. + * + * Only a limited set of clauses will be recognized as non-conditions. + * For instance beginning a snippet with complex statements like JOIN or UNION + * will not work. This is too complex for use in a snippet. + * + * @note A default implementation is available in AQueryWriter + * unless a database uses very different SQL this should suffice. + * + * @param string $sql SQL Snippet + * @param integer $glue the GLUE type - how to glue (C_GLUE_WHERE or C_GLUE_AND) + * + * @return string + */ + public function glueSQLCondition( $sql, $glue = NULL ); + + /** + * Determines if there is a LIMIT 1 clause in the SQL. + * If not, it will add a LIMIT 1. (used for findOne). + * + * @note A default implementation is available in AQueryWriter + * unless a database uses very different SQL this should suffice. + * + * @param string $sql query to scan and adjust + * + * @return string + */ + public function glueLimitOne( $sql ); + + /** + * Returns the tables that are in the database. + * + * @return array + */ + public function getTables(); + + /** + * This method will create a table for the bean. + * This methods accepts a type and infers the corresponding table name. + * + * @param string $type type of bean you want to create a table for + * + * @return void + */ + public function createTable( $type ); + + /** + * Returns an array containing all the columns of the specified type. + * The format of the return array looks like this: + * $field => $type where $field is the name of the column and $type + * is a database specific description of the datatype. + * + * This methods accepts a type and infers the corresponding table name. + * + * @param string $type type of bean you want to obtain a column list of + * + * @return array + */ + public function getColumns( $type ); + + /** + * Returns the Column Type Code (integer) that corresponds + * to the given value type. This method is used to determine the minimum + * column type required to represent the given value. There are two modes of + * operation: with or without special types. Scanning without special types + * requires the second parameter to be set to FALSE. This is useful when the + * column has already been created and prevents it from being modified to + * an incompatible type leading to data loss. Special types will be taken + * into account when a column does not exist yet (parameter is then set to TRUE). + * + * Special column types are determines by the AQueryWriter constant + * C_DATA_TYPE_ONLY_IF_NOT_EXISTS (usually 80). Another 'very special' type is type + * C_DATA_TYPE_MANUAL (usually 99) which represents a user specified type. Although + * no special treatment has been associated with the latter for now. + * + * @param string $value value + * @param boolean $alsoScanSpecialForTypes take special types into account + * + * @return integer + */ + public function scanType( $value, $alsoScanSpecialForTypes = FALSE ); + + /** + * This method will add a column to a table. + * This methods accepts a type and infers the corresponding table name. + * + * @param string $type name of the table + * @param string $column name of the column + * @param integer $field data type for field + * + * @return void + */ + public function addColumn( $type, $column, $field ); + + /** + * Returns the Type Code for a Column Description. + * Given an SQL column description this method will return the corresponding + * code for the writer. If the include specials flag is set it will also + * return codes for special columns. Otherwise special columns will be identified + * as specified columns. + * + * @param string $typedescription description + * @param boolean $includeSpecials whether you want to get codes for special columns as well + * + * @return integer + */ + public function code( $typedescription, $includeSpecials = FALSE ); + + /** + * This method will widen the column to the specified data type. + * This methods accepts a type and infers the corresponding table name. + * + * @param string $type type / table that needs to be adjusted + * @param string $column column that needs to be altered + * @param integer $datatype target data type + * + * @return void + */ + public function widenColumn( $type, $column, $datatype ); + + /** + * Selects records from the database. + * This methods selects the records from the database that match the specified + * type, conditions (optional) and additional SQL snippet (optional). + * + * @param string $type name of the table you want to query + * @param array $conditions criteria ( $column => array( $values ) ) + * @param string $addSql additional SQL snippet + * @param array $bindings bindings for SQL snippet + * + * @return array + */ + public function queryRecord( $type, $conditions = array(), $addSql = NULL, $bindings = array() ); + + /** + * Selects records from the database and returns a cursor. + * This methods selects the records from the database that match the specified + * type, conditions (optional) and additional SQL snippet (optional). + * + * @param string $type name of the table you want to query + * @param array $conditions criteria ( $column => array( $values ) ) + * @param string $addSQL additional SQL snippet + * @param array $bindings bindings for SQL snippet + * + * @return Cursor + */ + public function queryRecordWithCursor( $type, $addSql = NULL, $bindings = array() ); + + /** + * Returns records through an intermediate type. This method is used to obtain records using a link table and + * allows the SQL snippets to reference columns in the link table for additional filtering or ordering. + * + * @param string $sourceType source type, the reference type you want to use to fetch related items on the other side + * @param string $destType destination type, the target type you want to get beans of + * @param mixed $linkID ID to use for the link table + * @param string $addSql Additional SQL snippet + * @param array $bindings Bindings for SQL snippet + * + * @return array + */ + public function queryRecordRelated( $sourceType, $destType, $linkID, $addSql = '', $bindings = array() ); + + /** + * Returns the row that links $sourceType $sourcID to $destType $destID in an N-M relation. + * + * @param string $sourceType source type, the first part of the link you're looking for + * @param string $destType destination type, the second part of the link you're looking for + * @param string $sourceID ID for the source + * @param string $destID ID for the destination + * + * @return array|null + */ + public function queryRecordLink( $sourceType, $destType, $sourceID, $destID ); + + /** + * Counts the number of records in the database that match the + * conditions and additional SQL. + * + * @param string $type name of the table you want to query + * @param array $conditions criteria ( $column => array( $values ) ) + * @param string $addSQL additional SQL snippet + * @param array $bindings bindings for SQL snippet + * + * @return integer + */ + public function queryRecordCount( $type, $conditions = array(), $addSql = NULL, $bindings = array() ); + + /** + * Returns the number of records linked through $linkType and satisfying the SQL in $addSQL/$bindings. + * + * @param string $sourceType source type + * @param string $targetType the thing you want to count + * @param mixed $linkID the of the source type + * @param string $addSQL additional SQL snippet + * @param array $bindings bindings for SQL snippet + * + * @return integer + */ + public function queryRecordCountRelated( $sourceType, $targetType, $linkID, $addSQL = '', $bindings = array() ); + + /** + * Returns all rows of specified type that have been tagged with one of the + * strings in the specified tag list array. + * + * Note that the additional SQL snippet can only be used for pagination, + * the SQL snippet will be appended to the end of the query. + * + * @param string $type the bean type you want to query + * @param array $tagList an array of strings, each string containing a tag title + * @param boolean $all if TRUE only return records that have been associated with ALL the tags in the list + * @param string $addSql addition SQL snippet, for pagination + * @param array $bindings parameter bindings for additional SQL snippet + * + * @return array + */ + public function queryTagged( $type, $tagList, $all = FALSE, $addSql = '', $bindings = array() ); + + /** + * Like queryTagged but only counts. + * + * @param string $type the bean type you want to query + * @param array $tagList an array of strings, each string containing a tag title + * @param boolean $all if TRUE only return records that have been associated with ALL the tags in the list + * @param string $addSql addition SQL snippet, for pagination + * @param array $bindings parameter bindings for additional SQL snippet + * + * @return integer + */ + public function queryCountTagged( $type, $tagList, $all = FALSE, $addSql = '', $bindings = array() ); + + /** + * Returns all parent rows or child rows of a specified row. + * Given a type specifier and a primary key id, this method returns either all child rows + * as defined by having _id = id or all parent rows as defined per id = _id + * taking into account an optional SQL snippet along with parameters. + * + * The $select parameter can be used to adjust the select snippet of the query. + * Possible values are: C_CTE_SELECT_NORMAL (just select all columns, default), C_CTE_SELECT_COUNT + * (count rows) used for countParents and countChildren functions - or you can specify a + * string yourself like 'count(distinct brand)'. + * + * @param string $type the bean type you want to query rows for + * @param integer $id id of the reference row + * @param boolean $up TRUE to query parent rows, FALSE to query child rows + * @param string $addSql optional SQL snippet to embed in the query + * @param array $bindings parameter bindings for additional SQL snippet + * @param mixed $select Select Snippet to use when querying (optional) + * + * @return array + */ + public function queryRecursiveCommonTableExpression( $type, $id, $up = TRUE, $addSql = NULL, $bindings = array(), $select = QueryWriter::C_CTE_SELECT_NORMAL ); + + /** + * This method should update (or insert a record), it takes + * a table name, a list of update values ( $field => $value ) and an + * primary key ID (optional). If no primary key ID is provided, an + * INSERT will take place. + * Returns the new ID. + * This methods accepts a type and infers the corresponding table name. + * + * @param string $type name of the table to update + * @param array $updatevalues list of update values + * @param integer $id optional primary key ID value + * + * @return integer + */ + public function updateRecord( $type, $updatevalues, $id = NULL ); + + /** + * Deletes records from the database. + * @note $addSql is always prefixed with ' WHERE ' or ' AND .' + * + * @param string $type name of the table you want to query + * @param array $conditions criteria ( $column => array( $values ) ) + * @param string $addSql additional SQL + * @param array $bindings bindings + * + * @return void + */ + public function deleteRecord( $type, $conditions = array(), $addSql = '', $bindings = array() ); + + /** + * Deletes all links between $sourceType and $destType in an N-M relation. + * + * @param string $sourceType source type + * @param string $destType destination type + * @param string $sourceID source ID + * + * @return void + */ + public function deleteRelations( $sourceType, $destType, $sourceID ); + + /** + * @see QueryWriter::addUniqueConstaint + */ + public function addUniqueIndex( $type, $columns ); + + /** + * This method will add a UNIQUE constraint index to a table on columns $columns. + * This methods accepts a type and infers the corresponding table name. + * + * @param string $type target bean type + * @param array $columnsPartOfIndex columns to include in index + * + * @return void + */ + public function addUniqueConstraint( $type, $columns ); + + /** + * This method will check whether the SQL state is in the list of specified states + * and returns TRUE if it does appear in this list or FALSE if it + * does not. The purpose of this method is to translate the database specific state to + * a one of the constants defined in this class and then check whether it is in the list + * of standard states provided. + * + * @param string $state SQL state to consider + * @param array $list list of standardized SQL state constants to check against + * @param array $extraDriverDetails Some databases communicate state information in a driver-specific format + * rather than through the main sqlState code. For those databases, this extra + * information can be used to determine the standardized state + * + * @return boolean + */ + public function sqlStateIn( $state, $list, $extraDriverDetails = array() ); + + /** + * This method will remove all beans of a certain type. + * This methods accepts a type and infers the corresponding table name. + * + * @param string $type bean type + * + * @return void + */ + public function wipe( $type ); + + /** + * This method will add a foreign key from type and field to + * target type and target field. + * The foreign key is created without an action. On delete/update + * no action will be triggered. The FK is only used to allow database + * tools to generate pretty diagrams and to make it easy to add actions + * later on. + * This methods accepts a type and infers the corresponding table name. + * + * + * @param string $type type that will have a foreign key field + * @param string $targetType points to this type + * @param string $property field that contains the foreign key value + * @param string $targetProperty field where the fk points to + * @param string $isDep whether target is dependent and should cascade on update/delete + * + * @return void + */ + public function addFK( $type, $targetType, $property, $targetProperty, $isDep = FALSE ); + + /** + * This method will add an index to a type and field with name + * $name. + * This methods accepts a type and infers the corresponding table name. + * + * @param string $type type to add index to + * @param string $name name of the new index + * @param string $property field to index + * + * @return void + */ + public function addIndex( $type, $name, $property ); + + /** + * Checks and filters a database structure element like a table of column + * for safe use in a query. A database structure has to conform to the + * RedBeanPHP DB security policy which basically means only alphanumeric + * symbols are allowed. This security policy is more strict than conventional + * SQL policies and does therefore not require database specific escaping rules. + * + * @param string $databaseStructure name of the column/table to check + * @param boolean $noQuotes TRUE to NOT put backticks or quotes around the string + * + * @return string + */ + public function esc( $databaseStructure, $dontQuote = FALSE ); + + /** + * Removes all tables and views from the database. + * + * @return void + */ + public function wipeAll(); + + /** + * Renames an association. For instance if you would like to refer to + * album_song as: track you can specify this by calling this method like: + * + * + * renameAssociation('album_song','track') + * + * + * This allows: + * + * + * $album->sharedSong + * + * + * to add/retrieve beans from track instead of album_song. + * Also works for exportAll(). + * + * This method also accepts a single associative array as + * its first argument. + * + * @param string|array $fromType original type name, or array + * @param string $toType new type name (only if 1st argument is string) + * + * @return void + */ + public function renameAssocTable( $fromType, $toType = NULL ); + + /** + * Returns the format for link tables. + * Given an array containing two type names this method returns the + * name of the link table to be used to store and retrieve + * association records. For instance, given two types: person and + * project, the corresponding link table might be: 'person_project'. + * + * @param array $types two types array($type1, $type2) + * + * @return string + */ + public function getAssocTable( $types ); + +} +} + +namespace RedBeanPHP\QueryWriter { + +use RedBeanPHP\Adapter\DBAdapter as DBAdapter; +use RedBeanPHP\RedException as RedException; +use RedBeanPHP\QueryWriter as QueryWriter; +use RedBeanPHP\OODBBean as OODBBean; +use RedBeanPHP\RedException\SQL as SQLException; + +/** + * RedBeanPHP Abstract Query Writer. + * Represents an abstract Database to RedBean + * To write a driver for a different database for RedBean + * Contains a number of functions all implementors can + * inherit or override. + * + * @file RedBeanPHP/QueryWriter/AQueryWriter.php + * @author Gabor de Mooij and the RedBeanPHP Community + * @license BSD/GPLv2 + * + * @copyright + * (c) copyright G.J.G.T. (Gabor) de Mooij and the RedBeanPHP Community. + * This source file is subject to the BSD/GPLv2 License that is bundled + * with this source code in the file license.txt. + */ +abstract class AQueryWriter +{ + /** + * Constant: Select Snippet 'FOR UPDATE' + */ + const C_SELECT_SNIPPET_FOR_UPDATE = 'FOR UPDATE'; + const C_DATA_TYPE_ONLY_IF_NOT_EXISTS = 80; + const C_DATA_TYPE_MANUAL = 99; + + /** + * @var array + */ + private static $sqlFilters = array(); + + /** + * @var boolean + */ + private static $flagSQLFilterSafeMode = FALSE; + + /** + * @var boolean + */ + private static $flagNarrowFieldMode = TRUE; + + /** + * @var boolean + */ + protected static $flagUseJSONColumns = FALSE; + + /** + * @var boolean + */ + protected static $enableISNULLConditions = FALSE; + + /** + * @var array + */ + public static $renames = array(); + + /** + * @var DBAdapter + */ + protected $adapter; + + /** + * @var string + */ + protected $defaultValue = 'NULL'; + + /** + * @var string + */ + protected $quoteCharacter = ''; + + /** + * @var boolean + */ + protected $flagUseCache = TRUE; + + /** + * @var array + */ + protected $cache = array(); + + /** + * @var integer + */ + protected $maxCacheSizePerType = 20; + + /** + * @var string + */ + protected $sqlSelectSnippet = ''; + + /** + * @var array + */ + public $typeno_sqltype = array(); + + /** + * @var bool + */ + protected static $noNuke = false; + + /** + * Sets a data definition template to change the data + * creation statements per type. + * + * For instance to add ROW_FORMAT=DYNAMIC to all MySQL tables + * upon creation: + * + * + * $sql = $writer->getDDLTemplate( 'createTable', '*' ); + * $writer->setDDLTemplate( 'createTable', '*', $sql . ' ROW_FORMAT=DYNAMIC ' ); + * + * + * For property-specific templates set $beanType to: + * account.username -- then the template will only be applied to SQL statements relating + * to that column/property. + * + * @param string $type ( 'createTable' | 'widenColumn' | 'addColumn' ) + * @param string $beanType ( type of bean or '*' to apply to all types ) + * @param string $template SQL template, contains %s for slots + * + * @return void + */ + public function setDDLTemplate( $type, $beanType, $template ) + { + $this->DDLTemplates[ $type ][ $beanType ] = $template; + } + + /** + * Returns the specified data definition template. + * If no template can be found for the specified type, the template for + * '*' will be returned instead. + * + * @param string $type ( 'createTable' | 'widenColumn' | 'addColumn' ) + * @param string $beanType ( type of bean or '*' to apply to all types ) + * @param string $property specify if you're looking for a property-specific template + * + * @return string + */ + public function getDDLTemplate( $type, $beanType = '*', $property = NULL ) + { + $key = ( $property ) ? "{$beanType}.{$property}" : $beanType; + if ( isset( $this->DDLTemplates[ $type ][ $key ] ) ) { + return $this->DDLTemplates[ $type ][ $key ]; + } + if ( isset( $this->DDLTemplates[ $type ][ $beanType ] ) ) { + return $this->DDLTemplates[ $type ][ $beanType ]; + } + return $this->DDLTemplates[ $type ][ '*' ]; + } + + /** + * Toggles support for IS-NULL-conditions. + * If IS-NULL-conditions are enabled condition arrays + * for functions including findLike() are treated so that + * 'field' => NULL will be interpreted as field IS NULL + * instead of being skipped. Returns the previous + * value of the flag. + * + * @param boolean $flag TRUE or FALSE + * + * @return boolean + */ + public static function useISNULLConditions( $flag ) + { + $old = self::$enableISNULLConditions; + self::$enableISNULLConditions = $flag; + return $old; + } + + /** + * Toggles support for automatic generation of JSON columns. + * Using JSON columns means that strings containing JSON will + * cause the column to be created (not modified) as a JSON column. + * However it might also trigger exceptions if this means the DB attempts to + * convert a non-json column to a JSON column. Returns the previous + * value of the flag. + * + * @param boolean $flag TRUE or FALSE + * + * @return boolean + */ + public static function useJSONColumns( $flag ) + { + $old = self::$flagUseJSONColumns; + self::$flagUseJSONColumns = $flag; + return $old; + } + + /** + * Toggles support for nuke(). + * Can be used to turn off the nuke() feature for security reasons. + * Returns the old flag value. + * + * @param boolean $flag TRUE or FALSE + * + * @return boolean + */ + public static function forbidNuke( $flag ) { + $old = self::$noNuke; + self::$noNuke = (bool) $flag; + return $old; + } + + /** + * Checks whether a number can be treated like an int. + * + * @param string $value string representation of a certain value + * + * @return boolean + */ + public static function canBeTreatedAsInt( $value ) + { + return (bool) ( strval( $value ) === strval( intval( $value ) ) ); + } + + /** + * @see QueryWriter::getAssocTableFormat + */ + public static function getAssocTableFormat( $types ) + { + sort( $types ); + + $assoc = implode( '_', $types ); + + return ( isset( self::$renames[$assoc] ) ) ? self::$renames[$assoc] : $assoc; + } + + /** + * @see QueryWriter::renameAssociation + */ + public static function renameAssociation( $from, $to = NULL ) + { + if ( is_array( $from ) ) { + foreach ( $from as $key => $value ) self::$renames[$key] = $value; + + return; + } + + self::$renames[$from] = $to; + } + + /** + * Globally available service method for RedBeanPHP. + * Converts a camel cased string to a snake cased string. + * + * @param string $camel camelCased string to convert to snake case + * + * @return string + */ + public static function camelsSnake( $camel ) + { + return strtolower( preg_replace( '/(?<=[a-z])([A-Z])|([A-Z])(?=[a-z])/', '_$1$2', $camel ) ); + } + + /** + * Globally available service method for RedBeanPHP. + * Converts a snake cased string to a camel cased string. + * + * @param string $snake snake_cased string to convert to camelCase + * @param boolean $dolphin exception for Ids - (bookId -> bookID) + * too complicated for the human mind, only dolphins can understand this + * + * @return string + */ + public static function snakeCamel( $snake, $dolphinMode = false ) + { + $camel = lcfirst( str_replace(' ', '', ucwords( str_replace('_', ' ', $snake ) ) ) ); + if ( $dolphinMode ) { + $camel = preg_replace( '/(\w)Id$/', '$1ID', $camel ); + } + return $camel; + } + + /** + * Clears renames. + * + * @return void + */ + public static function clearRenames() + { + self::$renames = array(); + } + + /** + * Toggles 'Narrow Field Mode'. + * In Narrow Field mode the queryRecord method will + * narrow its selection field to + * + * SELECT {table}.* + * + * instead of + * + * SELECT * + * + * This is a better way of querying because it allows + * more flexibility (for instance joins). However if you need + * the wide selector for backward compatibility; use this method + * to turn OFF Narrow Field Mode by passing FALSE. + * Default is TRUE. + * + * @param boolean $narrowField TRUE = Narrow Field FALSE = Wide Field + * + * @return void + */ + public static function setNarrowFieldMode( $narrowField ) + { + self::$flagNarrowFieldMode = (boolean) $narrowField; + } + + /** + * Sets SQL filters. + * This is a lowlevel method to set the SQL filter array. + * The format of this array is: + * + * + * array( + * '' => array( + * '' => array( + * '' => '' + * ) + * ) + * ) + * + * + * Example: + * + * + * array( + * QueryWriter::C_SQLFILTER_READ => array( + * 'book' => array( + * 'title' => ' LOWER(book.title) ' + * ) + * ) + * + * + * Note that you can use constants instead of magical chars + * as keys for the uppermost array. + * This is a lowlevel method. For a more friendly method + * please take a look at the facade: R::bindFunc(). + * + * @param array list of filters to set + * + * @return void + */ + public static function setSQLFilters( $sqlFilters, $safeMode = FALSE ) + { + self::$flagSQLFilterSafeMode = (boolean) $safeMode; + self::$sqlFilters = $sqlFilters; + } + + /** + * Returns current SQL Filters. + * This method returns the raw SQL filter array. + * This is a lowlevel method. For a more friendly method + * please take a look at the facade: R::bindFunc(). + * + * @return array + */ + public static function getSQLFilters() + { + return self::$sqlFilters; + } + + /** + * Returns a cache key for the cache values passed. + * This method returns a fingerprint string to be used as a key to store + * data in the writer cache. + * + * @param array $keyValues key-value to generate key for + * + * @return string + */ + private function getCacheKey( $keyValues ) + { + return json_encode( $keyValues ); + } + + /** + * Returns the values associated with the provided cache tag and key. + * + * @param string $cacheTag cache tag to use for lookup + * @param string $key key to use for lookup + * + * @return mixed + */ + private function getCached( $cacheTag, $key ) + { + $sql = $this->adapter->getSQL(); + if ($this->updateCache()) { + if ( isset( $this->cache[$cacheTag][$key] ) ) { + return $this->cache[$cacheTag][$key]; + } + } + + return NULL; + } + + /** + * Checks if the previous query had a keep-cache tag. + * If so, the cache will persist, otherwise the cache will be flushed. + * + * Returns TRUE if the cache will remain and FALSE if a flush has + * been performed. + * + * @return boolean + */ + private function updateCache() + { + $sql = $this->adapter->getSQL(); + if ( strpos( $sql, '-- keep-cache' ) !== strlen( $sql ) - 13 ) { + // If SQL has been taken place outside of this method then something else then + // a select query might have happened! (or instruct to keep cache) + $this->cache = array(); + return FALSE; + } + return TRUE; + } + + /** + * Stores data from the writer in the cache under a specific key and cache tag. + * A cache tag is used to make sure the cache remains consistent. In most cases the cache tag + * will be the bean type, this makes sure queries associated with a certain reference type will + * never contain conflicting data. + * Why not use the cache tag as a key? Well + * we need to make sure the cache contents fits the key (and key is based on the cache values). + * Otherwise it would be possible to store two different result sets under the same key (the cache tag). + * + * In previous versions you could only store one key-entry, I have changed this to + * improve caching efficiency (issue #400). + * + * @param string $cacheTag cache tag (secondary key) + * @param string $key key to store values under + * @param array $values content to be stored + * + * @return void + */ + private function putResultInCache( $cacheTag, $key, $values ) + { + if ( isset( $this->cache[$cacheTag] ) ) { + if ( count( $this->cache[$cacheTag] ) > $this->maxCacheSizePerType ) array_shift( $this->cache[$cacheTag] ); + } else { + $this->cache[$cacheTag] = array(); + } + $this->cache[$cacheTag][$key] = $values; + } + + /** + * Creates an SQL snippet from a list of conditions of format: + * + * + * array( + * key => array( + * value1, value2, value3 .... + * ) + * ) + * + * + * @param array $conditions list of conditions + * @param array $bindings parameter bindings for SQL snippet + * @param string $addSql additional SQL snippet to append to result + * + * @return string + */ + private function makeSQLFromConditions( $conditions, &$bindings, $addSql = '' ) + { + reset( $bindings ); + $firstKey = key( $bindings ); + $paramTypeIsNum = ( is_numeric( $firstKey ) ); + $counter = 0; + + $sqlConditions = array(); + foreach ( $conditions as $column => $values ) { + if ( $values === NULL ) { + if ( self::$enableISNULLConditions ) { + $sqlConditions[] = $this->esc( $column ) . ' IS NULL'; + } + continue; + } + + if ( is_array( $values ) ) { + if ( empty( $values ) ) continue; + } else { + $values = array( $values ); + } + + $checkOODB = reset( $values ); + if ( $checkOODB instanceof OODBBean && $checkOODB->getMeta( 'type' ) === $column && substr( $column, -3 ) != '_id' ) + $column = $column . '_id'; + + + $sql = $this->esc( $column ); + $sql .= ' IN ( '; + + if ( $paramTypeIsNum ) { + $sql .= implode( ',', array_fill( 0, count( $values ), '?' ) ) . ' ) '; + + array_unshift($sqlConditions, $sql); + + foreach ( $values as $k => $v ) { + if ( $v instanceof OODBBean ) { + $v = $v->id; + } + $values[$k] = strval( $v ); + + array_unshift( $bindings, $v ); + } + } else { + + $slots = array(); + + foreach( $values as $k => $v ) { + if ( $v instanceof OODBBean ) { + $v = $v->id; + } + $slot = ':slot'.$counter++; + $slots[] = $slot; + $bindings[$slot] = strval( $v ); + } + + $sql .= implode( ',', $slots ).' ) '; + $sqlConditions[] = $sql; + } + } + + $sql = ''; + if ( !empty( $sqlConditions ) ) { + $sql .= " WHERE ( " . implode( ' AND ', $sqlConditions ) . ") "; + } + + $addSql = $this->glueSQLCondition( $addSql, !empty( $sqlConditions ) ? QueryWriter::C_GLUE_AND : NULL ); + if ( $addSql ) $sql .= $addSql; + + return $sql; + } + + /** + * Returns the table names and column names for a relational query. + * + * @param string $sourceType type of the source bean + * @param string $destType type of the bean you want to obtain using the relation + * @param boolean $noQuote TRUE if you want to omit quotes + * + * @return array + */ + private function getRelationalTablesAndColumns( $sourceType, $destType, $noQuote = FALSE ) + { + $linkTable = $this->esc( $this->getAssocTable( array( $sourceType, $destType ) ), $noQuote ); + $sourceCol = $this->esc( $sourceType . '_id', $noQuote ); + + if ( $sourceType === $destType ) { + $destCol = $this->esc( $destType . '2_id', $noQuote ); + } else { + $destCol = $this->esc( $destType . '_id', $noQuote ); + } + + $sourceTable = $this->esc( $sourceType, $noQuote ); + $destTable = $this->esc( $destType, $noQuote ); + + return array( $sourceTable, $destTable, $linkTable, $sourceCol, $destCol ); + } + + /** + * Determines whether a string can be considered JSON or not. + * This is used by writers that support JSON columns. However + * we dont want that code duplicated over all JSON supporting + * Query Writers. + * + * @param string $value value to determine 'JSONness' of. + * + * @return boolean + */ + protected function isJSON( $value ) + { + return ( + is_string($value) && + is_array(json_decode($value, TRUE)) && + (json_last_error() == JSON_ERROR_NONE) + ); + } + + /** + * Given a type and a property name this method + * returns the foreign key map section associated with this pair. + * + * @param string $type name of the type + * @param string $property name of the property + * + * @return array|NULL + */ + protected function getForeignKeyForTypeProperty( $type, $property ) + { + $property = $this->esc( $property, TRUE ); + + try { + $map = $this->getKeyMapForType( $type ); + } catch ( SQLException $e ) { + return NULL; + } + + foreach( $map as $key ) { + if ( $key['from'] === $property ) return $key; + } + return NULL; + } + + /** + * Returns the foreign key map (FKM) for a type. + * A foreign key map describes the foreign keys in a table. + * A FKM always has the same structure: + * + * + * array( + * 'name' => + * 'from' => + * 'table' => + * 'to' => (most of the time 'id') + * 'on_update' => + * 'on_delete' => + * ) + * + * + * @note the keys in the result array are FKDLs, i.e. descriptive unique + * keys per source table. Also see: AQueryWriter::makeFKLabel for details. + * + * @param string $type the bean type you wish to obtain a key map of + * + * @return array + */ + protected function getKeyMapForType( $type ) + { + return array(); + } + + /** + * This method makes a key for a foreign key description array. + * This key is a readable string unique for every source table. + * This uniform key is called the FKDL Foreign Key Description Label. + * Note that the source table is not part of the FKDL because + * this key is supposed to be 'per source table'. If you wish to + * include a source table, prefix the key with 'on_table__'. + * + * @param string $from the column of the key in the source table + * @param string $type the type (table) where the key points to + * @param string $to the target column of the foreign key (mostly just 'id') + * + * @return string + */ + protected function makeFKLabel($from, $type, $to) + { + return "from_{$from}_to_table_{$type}_col_{$to}"; + } + + /** + * Returns an SQL Filter snippet for reading. + * + * @param string $type type of bean + * + * @return string + */ + protected function getSQLFilterSnippet( $type ) + { + $existingCols = array(); + if (self::$flagSQLFilterSafeMode) { + $existingCols = $this->getColumns( $type ); + } + + $sqlFilters = array(); + if ( isset( self::$sqlFilters[QueryWriter::C_SQLFILTER_READ][$type] ) ) { + foreach( self::$sqlFilters[QueryWriter::C_SQLFILTER_READ][$type] as $property => $sqlFilter ) { + if ( !self::$flagSQLFilterSafeMode || isset( $existingCols[$property] ) ) { + $sqlFilters[] = $sqlFilter.' AS '.$property.' '; + } + } + } + $sqlFilterStr = ( count($sqlFilters) ) ? ( ','.implode( ',', $sqlFilters ) ) : ''; + return $sqlFilterStr; + } + + /** + * Generates a list of parameters (slots) for an SQL snippet. + * This method calculates the correct number of slots to insert in the + * SQL snippet and determines the correct type of slot. If the bindings + * array contains named parameters this method will return named ones and + * update the keys in the value list accordingly (that's why we use the &). + * + * If you pass an offset the bindings will be re-added to the value list. + * Some databases cant handle duplicate parameter names in queries. + * + * @param array &$valueList list of values to generate slots for (gets modified if needed) + * @param array $otherBindings list of additional bindings + * @param integer $offset start counter at... + * + * @return string + */ + protected function getParametersForInClause( &$valueList, $otherBindings, $offset = 0 ) + { + if ( is_array( $otherBindings ) && count( $otherBindings ) > 0 ) { + reset( $otherBindings ); + + $key = key( $otherBindings ); + + if ( !is_numeric($key) ) { + $filler = array(); + $newList = (!$offset) ? array() : $valueList; + $counter = $offset; + + foreach( $valueList as $value ) { + $slot = ':slot' . ( $counter++ ); + $filler[] = $slot; + $newList[$slot] = $value; + } + + // Change the keys! + $valueList = $newList; + + return implode( ',', $filler ); + } + } + + return implode( ',', array_fill( 0, count( $valueList ), '?' ) ); + } + + /** + * Adds a data type to the list of data types. + * Use this method to add a new column type definition to the writer. + * Used for UUID support. + * + * @param integer $dataTypeID magic number constant assigned to this data type + * @param string $SQLDefinition SQL column definition (i.e. INT(11)) + * + * @return self + */ + protected function addDataType( $dataTypeID, $SQLDefinition ) + { + $this->typeno_sqltype[ $dataTypeID ] = $SQLDefinition; + $this->sqltype_typeno[ $SQLDefinition ] = $dataTypeID; + return $this; + } + + /** + * Returns the sql that should follow an insert statement. + * + * @param string $table name + * + * @return string + */ + protected function getInsertSuffix( $table ) + { + return ''; + } + + /** + * Checks whether a value starts with zeros. In this case + * the value should probably be stored using a text datatype instead of a + * numerical type in order to preserve the zeros. + * + * @param string $value value to be checked. + * + * @return boolean + */ + protected function startsWithZeros( $value ) + { + $value = strval( $value ); + + if ( strlen( $value ) > 1 && strpos( $value, '0' ) === 0 && strpos( $value, '0.' ) !== 0 ) { + return TRUE; + } else { + return FALSE; + } + } + + /** + * Inserts a record into the database using a series of insert columns + * and corresponding insertvalues. Returns the insert id. + * + * @param string $table table to perform query on + * @param array $insertcolumns columns to be inserted + * @param array $insertvalues values to be inserted + * + * @return integer + */ + protected function insertRecord( $type, $insertcolumns, $insertvalues ) + { + $default = $this->defaultValue; + $suffix = $this->getInsertSuffix( $type ); + $table = $this->esc( $type ); + + if ( count( $insertvalues ) > 0 && is_array( $insertvalues[0] ) && count( $insertvalues[0] ) > 0 ) { + + $insertSlots = array(); + foreach ( $insertcolumns as $k => $v ) { + $insertcolumns[$k] = $this->esc( $v ); + + if (isset(self::$sqlFilters['w'][$type][$v])) { + $insertSlots[] = self::$sqlFilters['w'][$type][$v]; + } else { + $insertSlots[] = '?'; + } + } + + $insertSQL = "INSERT INTO $table ( id, " . implode( ',', $insertcolumns ) . " ) VALUES + ( $default, " . implode( ',', $insertSlots ) . " ) $suffix"; + + $ids = array(); + foreach ( $insertvalues as $i => $insertvalue ) { + $ids[] = $this->adapter->getCell( $insertSQL, $insertvalue, $i ); + } + + $result = count( $ids ) === 1 ? array_pop( $ids ) : $ids; + } else { + $result = $this->adapter->getCell( "INSERT INTO $table (id) VALUES($default) $suffix" ); + } + + if ( $suffix ) return $result; + + $last_id = $this->adapter->getInsertID(); + + return $last_id; + } + + /** + * Checks table name or column name. + * + * @param string $table table string + * + * @return string + */ + protected function check( $struct ) + { + if ( !is_string( $struct ) || !preg_match( '/^[a-zA-Z0-9_]+$/', $struct ) ) { + throw new RedException( 'Identifier does not conform to RedBeanPHP security policies.' ); + } + + return $struct; + } + + /** + * Checks whether the specified type (i.e. table) already exists in the database. + * Not part of the Object Database interface! + * + * @param string $table table name + * + * @return boolean + */ + public function tableExists( $table ) + { + $tables = $this->getTables(); + + return in_array( $table, $tables ); + } + + /** + * @see QueryWriter::glueSQLCondition + */ + public function glueSQLCondition( $sql, $glue = NULL ) + { + static $snippetCache = array(); + + if ( trim( $sql ) === '' ) { + return $sql; + } + + $key = $glue . '|' . $sql; + + if ( isset( $snippetCache[$key] ) ) { + return $snippetCache[$key]; + } + + $lsql = ltrim( $sql ); + + if ( preg_match( '/^(INNER|LEFT|RIGHT|JOIN|AND|OR|WHERE|ORDER|GROUP|HAVING|LIMIT|OFFSET)\s+/i', $lsql ) ) { + if ( $glue === QueryWriter::C_GLUE_WHERE && stripos( $lsql, 'AND' ) === 0 ) { + $snippetCache[$key] = ' WHERE ' . substr( $lsql, 3 ); + } else { + $snippetCache[$key] = $sql; + } + } else { + $snippetCache[$key] = ( ( $glue === QueryWriter::C_GLUE_AND ) ? ' AND ' : ' WHERE ') . $sql; + } + + return $snippetCache[$key]; + } + + /** + * @see QueryWriter::glueLimitOne + */ + public function glueLimitOne( $sql = '') + { + return ( strpos( strtoupper( ' ' . $sql ), ' LIMIT ' ) === FALSE ) ? ( $sql . ' LIMIT 1 ' ) : $sql; + } + + /** + * @see QueryWriter::esc + */ + public function esc( $dbStructure, $dontQuote = FALSE ) + { + $this->check( $dbStructure ); + + return ( $dontQuote ) ? $dbStructure : $this->quoteCharacter . $dbStructure . $this->quoteCharacter; + } + + /** + * @see QueryWriter::addColumn + */ + public function addColumn( $beanType, $column, $field ) + { + $table = $beanType; + $type = $field; + $table = $this->esc( $table ); + $column = $this->esc( $column ); + + $type = ( isset( $this->typeno_sqltype[$type] ) ) ? $this->typeno_sqltype[$type] : ''; + + $this->adapter->exec( sprintf( $this->getDDLTemplate('addColumn', $beanType, $column ), $table, $column, $type ) ); + } + + /** + * @see QueryWriter::updateRecord + */ + public function updateRecord( $type, $updatevalues, $id = NULL ) + { + $table = $type; + + if ( !$id ) { + $insertcolumns = $insertvalues = array(); + + foreach ( $updatevalues as $pair ) { + $insertcolumns[] = $pair['property']; + $insertvalues[] = $pair['value']; + } + + //Otherwise psql returns string while MySQL/SQLite return numeric causing problems with additions (array_diff) + return (string) $this->insertRecord( $table, $insertcolumns, array( $insertvalues ) ); + } + + if ( $id && !count( $updatevalues ) ) { + return $id; + } + + $table = $this->esc( $table ); + $sql = "UPDATE $table SET "; + + $p = $v = array(); + + foreach ( $updatevalues as $uv ) { + + if ( isset( self::$sqlFilters['w'][$type][$uv['property']] ) ) { + $p[] = " {$this->esc( $uv["property"] )} = ". self::$sqlFilters['w'][$type][$uv['property']]; + } else { + $p[] = " {$this->esc( $uv["property"] )} = ? "; + } + + $v[] = $uv['value']; + } + + $sql .= implode( ',', $p ) . ' WHERE id = ? '; + + $v[] = $id; + + $this->adapter->exec( $sql, $v ); + + return $id; + } + + /** + * @see QueryWriter::parseJoin + */ + public function parseJoin( $type, $sql, $cteType = NULL ) + { + if ( strpos( $sql, '@' ) === FALSE ) { + return $sql; + } + + $sql = ' ' . $sql; + $joins = array(); + $joinSql = ''; + + if ( !preg_match_all( '#@((shared|own|joined)\.[^\s(,=!?]+)#', $sql, $matches ) ) + return $sql; + + $expressions = $matches[1]; + // Sort to make the joins from the longest to the shortest + uasort( $expressions, function($a, $b) { + return substr_count( $b, '.' ) - substr_count( $a, '.' ); + }); + + $nsuffix = 1; + foreach ( $expressions as $exp ) { + $explosion = explode( '.', $exp ); + $joinTable = $type; + $joinType = array_shift( $explosion ); + $lastPart = array_pop( $explosion ); + $lastJoin = end($explosion); + if ( ( $index = strpos( $lastJoin, '[' ) ) !== FALSE ) { + $lastJoin = substr( $lastJoin, 0, $index); + } + reset($explosion); + + // Let's check if we already joined that chain + // If that's the case we skip this + $joinKey = implode( '.', $explosion ); + foreach ( $joins as $chain => $suffix ) { + if ( strpos ( $chain, $joinKey ) === 0 ) { + $sql = str_replace( "@{$exp}", "{$lastJoin}__rb{$suffix}.{$lastPart}", $sql ); + continue 2; + } + } + $sql = str_replace( "@{$exp}", "{$lastJoin}__rb{$nsuffix}.{$lastPart}", $sql ); + $joins[$joinKey] = $nsuffix; + + // We loop on the elements of the join + $i = 0; + while ( TRUE ) { + $joinInfo = $explosion[$i]; + if ( $i ) { + $joinType = $explosion[$i-1]; + $joinTable = $explosion[$i-2]; + } + + $aliases = array(); + if ( ( $index = strpos( $joinInfo, '[' ) ) !== FALSE ) { + if ( preg_match_all( '#(([^\s:/\][]+)[/\]])#', $joinInfo, $matches ) ) { + $aliases = $matches[2]; + $joinInfo = substr( $joinInfo, 0, $index); + } + } + if ( ( $index = strpos( $joinTable, '[' ) ) !== FALSE ) { + $joinTable = substr( $joinTable, 0, $index); + } + + if ( $i ) { + $joinSql .= $this->writeJoin( $joinTable, $joinInfo, 'INNER', $joinType, FALSE, "__rb{$nsuffix}", $aliases, NULL ); + } else { + $joinSql .= $this->writeJoin( $joinTable, $joinInfo, 'LEFT', $joinType, TRUE, "__rb{$nsuffix}", $aliases, $cteType ); + } + + $i += 2; + if ( !isset( $explosion[$i] ) ) { + break; + } + } + $nsuffix++; + } + + $sql = str_ireplace( ' where ', ' WHERE ', $sql ); + if ( strpos( $sql, ' WHERE ') === FALSE ) { + if ( preg_match( '/^(ORDER|GROUP|HAVING|LIMIT|OFFSET)\s+/i', trim($sql) ) ) { + $sql = "{$joinSql} {$sql}"; + } else { + $sql = "{$joinSql} WHERE {$sql}"; + } + } else { + $sqlParts = explode( ' WHERE ', $sql, 2 ); + $sql = "{$sqlParts[0]} {$joinSql} WHERE {$sqlParts[1]}"; + } + + return $sql; + } + + /** + * @see QueryWriter::writeJoin + */ + public function writeJoin( $type, $targetType, $leftRight = 'LEFT', $joinType = 'parent', $firstOfChain = TRUE, $suffix = '', $aliases = array(), $cteType = NULL ) + { + if ( $leftRight !== 'LEFT' && $leftRight !== 'RIGHT' && $leftRight !== 'INNER' ) + throw new RedException( 'Invalid JOIN.' ); + + $globalAliases = OODBBean::getAliases(); + if ( isset( $globalAliases[$targetType] ) ) { + $destType = $globalAliases[$targetType]; + $asTargetTable = $this->esc( $targetType.$suffix ); + } else { + $destType = $targetType; + $asTargetTable = $this->esc( $destType.$suffix ); + } + + if ( $firstOfChain ) { + $table = $this->esc( $type ); + } else { + $table = $this->esc( $type.$suffix ); + } + $targetTable = $this->esc( $destType ); + + if ( $joinType == 'shared' ) { + + if ( isset( $globalAliases[$type] ) ) { + $field = $this->esc( $globalAliases[$type], TRUE ); + if ( $aliases && count( $aliases ) === 1 ) { + $assocTable = reset( $aliases ); + } else { + $assocTable = $this->getAssocTable( array( $cteType ? $cteType : $globalAliases[$type], $destType ) ); + } + } else { + $field = $this->esc( $type, TRUE ); + if ( $aliases && count( $aliases ) === 1 ) { + $assocTable = reset( $aliases ); + } else { + $assocTable = $this->getAssocTable( array( $cteType ? $cteType : $type, $destType ) ); + } + } + $linkTable = $this->esc( $assocTable ); + $asLinkTable = $this->esc( $assocTable.$suffix ); + $leftField = "id"; + $rightField = $cteType ? "{$cteType}_id" : "{$field}_id"; + $linkField = $this->esc( $destType, TRUE ); + $linkLeftField = "id"; + $linkRightField = "{$linkField}_id"; + + $joinSql = " {$leftRight} JOIN {$linkTable}"; + if ( isset( $globalAliases[$targetType] ) || $suffix ) { + $joinSql .= " AS {$asLinkTable}"; + } + $joinSql .= " ON {$table}.{$leftField} = {$asLinkTable}.{$rightField}"; + $joinSql .= " {$leftRight} JOIN {$targetTable}"; + if ( isset( $globalAliases[$targetType] ) || $suffix ) { + $joinSql .= " AS {$asTargetTable}"; + } + $joinSql .= " ON {$asTargetTable}.{$linkLeftField} = {$asLinkTable}.{$linkRightField}"; + + } elseif ( $joinType == 'own' ) { + + $field = $this->esc( $type, TRUE ); + $rightField = "id"; + + $joinSql = " {$leftRight} JOIN {$targetTable}"; + if ( isset( $globalAliases[$targetType] ) || $suffix ) { + $joinSql .= " AS {$asTargetTable}"; + } + + if ( $aliases ) { + $conditions = array(); + foreach ( $aliases as $alias ) { + $conditions[] = "{$asTargetTable}.{$alias}_id = {$table}.{$rightField}"; + } + $joinSql .= " ON ( " . implode( ' OR ', $conditions ) . " ) "; + } else { + $leftField = $cteType ? "{$cteType}_id" : "{$field}_id"; + $joinSql .= " ON {$asTargetTable}.{$leftField} = {$table}.{$rightField} "; + } + + } else { + + $field = $this->esc( $targetType, TRUE ); + $leftField = "id"; + + $joinSql = " {$leftRight} JOIN {$targetTable}"; + if ( isset( $globalAliases[$targetType] ) || $suffix ) { + $joinSql .= " AS {$asTargetTable}"; + } + + if ( $aliases ) { + $conditions = array(); + foreach ( $aliases as $alias ) { + $conditions[] = "{$asTargetTable}.{$leftField} = {$table}.{$alias}_id"; + } + $joinSql .= " ON ( " . implode( ' OR ', $conditions ) . " ) "; + } else { + $rightField = "{$field}_id"; + $joinSql .= " ON {$asTargetTable}.{$leftField} = {$table}.{$rightField} "; + } + + } + + return $joinSql; + } + + /** + * Sets an SQL snippet to be used for the next queryRecord() operation. + * A select snippet will be inserted at the end of the SQL select statement and + * can be used to modify SQL-select commands to enable locking, for instance + * using the 'FOR UPDATE' snippet (this will generate an SQL query like: + * 'SELECT * FROM ... FOR UPDATE'. After the query has been executed the + * SQL snippet will be erased. Note that only the first upcoming direct or + * indirect invocation of queryRecord() through batch(), find() or load() + * will be affected. The SQL snippet will be cached. + * + * @param string $sql SQL snippet to use in SELECT statement. + * + * return self + */ + public function setSQLSelectSnippet( $sqlSelectSnippet = '' ) { + $this->sqlSelectSnippet = $sqlSelectSnippet; + return $this; + } + + /** + * @see QueryWriter::queryRecord + */ + public function queryRecord( $type, $conditions = array(), $addSql = NULL, $bindings = array() ) + { + if ( $this->flagUseCache && $this->sqlSelectSnippet != self::C_SELECT_SNIPPET_FOR_UPDATE ) { + $key = $this->getCacheKey( array( $conditions, trim("$addSql {$this->sqlSelectSnippet}"), $bindings, 'select' ) ); + if ( $cached = $this->getCached( $type, $key ) ) { + return $cached; + } + } + + $table = $this->esc( $type ); + + $sqlFilterStr = ''; + if ( count( self::$sqlFilters ) ) { + $sqlFilterStr = $this->getSQLFilterSnippet( $type ); + } + + if ( is_array ( $conditions ) && !empty ( $conditions ) ) { + $sql = $this->makeSQLFromConditions( $conditions, $bindings, $addSql ); + } else { + $sql = $this->glueSQLCondition( $addSql ); + } + $sql = $this->parseJoin( $type, $sql ); + $fieldSelection = self::$flagNarrowFieldMode ? "{$table}.*" : '*'; + $sql = "SELECT {$fieldSelection} {$sqlFilterStr} FROM {$table} {$sql} {$this->sqlSelectSnippet} -- keep-cache"; + $this->sqlSelectSnippet = ''; + $rows = $this->adapter->get( $sql, $bindings ); + + if ( $this->flagUseCache && !empty( $key ) ) { + $this->putResultInCache( $type, $key, $rows ); + } + + return $rows; + } + + /** + * @see QueryWriter::queryRecordWithCursor + */ + public function queryRecordWithCursor( $type, $addSql = NULL, $bindings = array() ) + { + $table = $this->esc( $type ); + + $sqlFilterStr = ''; + if ( count( self::$sqlFilters ) ) { + $sqlFilterStr = $this->getSQLFilterSnippet( $type ); + } + + $sql = $this->glueSQLCondition( $addSql, NULL ); + + $sql = $this->parseJoin( $type, $sql ); + $fieldSelection = self::$flagNarrowFieldMode ? "{$table}.*" : '*'; + + $sql = "SELECT {$fieldSelection} {$sqlFilterStr} FROM {$table} {$sql} -- keep-cache"; + + return $this->adapter->getCursor( $sql, $bindings ); + } + + /** + * @see QueryWriter::queryRecordRelated + */ + public function queryRecordRelated( $sourceType, $destType, $linkIDs, $addSql = '', $bindings = array() ) + { + list( $sourceTable, $destTable, $linkTable, $sourceCol, $destCol ) = $this->getRelationalTablesAndColumns( $sourceType, $destType ); + + if ( $this->flagUseCache ) { + $key = $this->getCacheKey( array( $sourceType, implode( ',', $linkIDs ), trim($addSql), $bindings, 'selectrelated' ) ); + if ( $cached = $this->getCached( $destType, $key ) ) { + return $cached; + } + } + + $addSql = $this->glueSQLCondition( $addSql, QueryWriter::C_GLUE_WHERE ); + $inClause = $this->getParametersForInClause( $linkIDs, $bindings ); + + $sqlFilterStr = ''; + if ( count( self::$sqlFilters ) ) { + $sqlFilterStr = $this->getSQLFilterSnippet( $destType ); + } + + if ( $sourceType === $destType ) { + $inClause2 = $this->getParametersForInClause( $linkIDs, $bindings, count( $bindings ) ); //for some databases + $sql = " + SELECT + {$destTable}.* {$sqlFilterStr} , + COALESCE( + NULLIF({$linkTable}.{$sourceCol}, {$destTable}.id), + NULLIF({$linkTable}.{$destCol}, {$destTable}.id)) AS linked_by + FROM {$linkTable} + INNER JOIN {$destTable} ON + ( {$destTable}.id = {$linkTable}.{$destCol} AND {$linkTable}.{$sourceCol} IN ($inClause) ) OR + ( {$destTable}.id = {$linkTable}.{$sourceCol} AND {$linkTable}.{$destCol} IN ($inClause2) ) + {$addSql} + -- keep-cache"; + + $linkIDs = array_merge( $linkIDs, $linkIDs ); + } else { + $sql = " + SELECT + {$destTable}.* {$sqlFilterStr}, + {$linkTable}.{$sourceCol} AS linked_by + FROM {$linkTable} + INNER JOIN {$destTable} ON + ( {$destTable}.id = {$linkTable}.{$destCol} AND {$linkTable}.{$sourceCol} IN ($inClause) ) + {$addSql} + -- keep-cache"; + } + + $bindings = array_merge( $linkIDs, $bindings ); + + $rows = $this->adapter->get( $sql, $bindings ); + + if ( $this->flagUseCache ) { + $this->putResultInCache( $destType, $key, $rows ); + } + + return $rows; + } + + /** + * @see QueryWriter::queryRecordLink + */ + public function queryRecordLink( $sourceType, $destType, $sourceID, $destID ) + { + list( $sourceTable, $destTable, $linkTable, $sourceCol, $destCol ) = $this->getRelationalTablesAndColumns( $sourceType, $destType ); + + if ( $this->flagUseCache ) { + $key = $this->getCacheKey( array( $sourceType, $destType, $sourceID, $destID, 'selectlink' ) ); + if ( $cached = $this->getCached( $linkTable, $key ) ) { + return $cached; + } + } + + $sqlFilterStr = ''; + if ( count( self::$sqlFilters ) ) { + $linkType = $this->getAssocTable( array( $sourceType, $destType ) ); + $sqlFilterStr = $this->getSQLFilterSnippet( "{$linkType}" ); + } + + if ( $sourceTable === $destTable ) { + $sql = "SELECT {$linkTable}.* {$sqlFilterStr} FROM {$linkTable} + WHERE ( {$sourceCol} = ? AND {$destCol} = ? ) OR + ( {$destCol} = ? AND {$sourceCol} = ? ) -- keep-cache"; + $row = $this->adapter->getRow( $sql, array( $sourceID, $destID, $sourceID, $destID ) ); + } else { + $sql = "SELECT {$linkTable}.* {$sqlFilterStr} FROM {$linkTable} + WHERE {$sourceCol} = ? AND {$destCol} = ? -- keep-cache"; + $row = $this->adapter->getRow( $sql, array( $sourceID, $destID ) ); + } + + if ( $this->flagUseCache ) { + $this->putResultInCache( $linkTable, $key, $row ); + } + + return $row; + } + + /** + * Returns or counts all rows of specified type that have been tagged with one of the + * strings in the specified tag list array. + * + * Note that the additional SQL snippet can only be used for pagination, + * the SQL snippet will be appended to the end of the query. + * + * @param string $type the bean type you want to query + * @param array $tagList an array of strings, each string containing a tag title + * @param boolean $all if TRUE only return records that have been associated with ALL the tags in the list + * @param string $addSql addition SQL snippet, for pagination + * @param array $bindings parameter bindings for additional SQL snippet + * @param string $wrap SQL wrapper string (use %s for subquery) + * + * @return array + */ + private function queryTaggedGeneric( $type, $tagList, $all = FALSE, $addSql = '', $bindings = array(), $wrap = '%s' ) + { + if ( $this->flagUseCache ) { + $key = $this->getCacheKey( array( implode( ',', $tagList ), $all, trim($addSql), $bindings, 'selectTagged' ) ); + if ( $cached = $this->getCached( $type, $key ) ) { + return $cached; + } + } + + $assocType = $this->getAssocTable( array( $type, 'tag' ) ); + $assocTable = $this->esc( $assocType ); + $assocField = $type . '_id'; + $table = $this->esc( $type ); + $slots = implode( ',', array_fill( 0, count( $tagList ), '?' ) ); + $score = ( $all ) ? count( $tagList ) : 1; + + $sql = " + SELECT {$table}.* FROM {$table} + INNER JOIN {$assocTable} ON {$assocField} = {$table}.id + INNER JOIN tag ON {$assocTable}.tag_id = tag.id + WHERE tag.title IN ({$slots}) + GROUP BY {$table}.id + HAVING count({$table}.id) >= ? + {$addSql} + -- keep-cache + "; + $sql = sprintf($wrap,$sql); + + $bindings = array_merge( $tagList, array( $score ), $bindings ); + $rows = $this->adapter->get( $sql, $bindings ); + + if ( $this->flagUseCache ) { + $this->putResultInCache( $type, $key, $rows ); + } + + return $rows; + } + + /** + * @see QueryWriter::queryTagged + */ + public function queryTagged( $type, $tagList, $all = FALSE, $addSql = '', $bindings = array() ) + { + return $this->queryTaggedGeneric( $type, $tagList, $all, $addSql, $bindings ); + } + + /** + * @see QueryWriter::queryCountTagged + */ + public function queryCountTagged( $type, $tagList, $all = FALSE, $addSql = '', $bindings = array() ) + { + $rows = $this->queryTaggedGeneric( $type, $tagList, $all, $addSql, $bindings, 'SELECT COUNT(*) AS counted FROM (%s) AS counting' ); + return intval($rows[0]['counted']); + } + + /** + * @see QueryWriter::queryRecordCount + */ + public function queryRecordCount( $type, $conditions = array(), $addSql = NULL, $bindings = array() ) + { + if ( $this->flagUseCache ) { + $key = $this->getCacheKey( array( $conditions, trim($addSql), $bindings, 'count' ) ); + if ( $cached = $this->getCached( $type, $key ) ) { + return $cached; + } + } + + $table = $this->esc( $type ); + + if ( is_array ( $conditions ) && !empty ( $conditions ) ) { + $sql = $this->makeSQLFromConditions( $conditions, $bindings, $addSql ); + } else { + $sql = $this->glueSQLCondition( $addSql ); + } + + $sql = $this->parseJoin( $type, $sql ); + + $sql = "SELECT COUNT(*) FROM {$table} {$sql} -- keep-cache"; + $count = (int) $this->adapter->getCell( $sql, $bindings ); + + if ( $this->flagUseCache ) { + $this->putResultInCache( $type, $key, $count ); + } + + return $count; + } + + /** + * @see QueryWriter::queryRecordCountRelated + */ + public function queryRecordCountRelated( $sourceType, $destType, $linkID, $addSql = '', $bindings = array() ) + { + list( $sourceTable, $destTable, $linkTable, $sourceCol, $destCol ) = $this->getRelationalTablesAndColumns( $sourceType, $destType ); + + if ( $this->flagUseCache ) { + $cacheType = "#{$sourceType}/{$destType}"; + $key = $this->getCacheKey( array( $sourceType, $destType, $linkID, trim($addSql), $bindings, 'countrelated' ) ); + if ( $cached = $this->getCached( $cacheType, $key ) ) { + return $cached; + } + } + + if ( $sourceType === $destType ) { + $sql = " + SELECT COUNT(*) FROM {$linkTable} + INNER JOIN {$destTable} ON + ( {$destTable}.id = {$linkTable}.{$destCol} AND {$linkTable}.{$sourceCol} = ? ) OR + ( {$destTable}.id = {$linkTable}.{$sourceCol} AND {$linkTable}.{$destCol} = ? ) + {$addSql} + -- keep-cache"; + + $bindings = array_merge( array( $linkID, $linkID ), $bindings ); + } else { + $sql = " + SELECT COUNT(*) FROM {$linkTable} + INNER JOIN {$destTable} ON + ( {$destTable}.id = {$linkTable}.{$destCol} AND {$linkTable}.{$sourceCol} = ? ) + {$addSql} + -- keep-cache"; + + $bindings = array_merge( array( $linkID ), $bindings ); + } + + $count = (int) $this->adapter->getCell( $sql, $bindings ); + + if ( $this->flagUseCache ) { + $this->putResultInCache( $cacheType, $key, $count ); + } + + return $count; + } + + /** + * @see QueryWriter::queryRecursiveCommonTableExpression + */ + public function queryRecursiveCommonTableExpression( $type, $id, $up = TRUE, $addSql = NULL, $bindings = array(), $selectForm = FALSE ) + { + if ($selectForm === QueryWriter::C_CTE_SELECT_COUNT) { + $selectForm = "count(redbeantree.*)"; + } elseif ( $selectForm === QueryWriter::C_CTE_SELECT_NORMAL ) { + $selectForm = "redbeantree.*"; + } + $alias = $up ? 'parent' : 'child'; + $direction = $up ? " {$alias}.{$type}_id = {$type}.id " : " {$alias}.id = {$type}.{$type}_id "; + /* allow numeric and named param bindings, if '0' exists then numeric */ + if ( array_key_exists( 0,$bindings ) ) { + array_unshift( $bindings, $id ); + $idSlot = '?'; + } else { + $idSlot = ':slot0'; + $bindings[$idSlot] = $id; + } + $sql = $this->glueSQLCondition( $addSql, QueryWriter::C_GLUE_WHERE ); + $sql = $this->parseJoin( 'redbeantree', $sql, $type ); + $rows = $this->adapter->get(" + WITH RECURSIVE redbeantree AS + ( + SELECT * + FROM {$type} WHERE {$type}.id = {$idSlot} + UNION ALL + SELECT {$type}.* FROM {$type} + INNER JOIN redbeantree {$alias} ON {$direction} + ) + SELECT {$selectForm} FROM redbeantree {$sql};", + $bindings + ); + return $rows; + } + + /** + * @see QueryWriter::deleteRecord + */ + public function deleteRecord( $type, $conditions = array(), $addSql = NULL, $bindings = array() ) + { + $table = $this->esc( $type ); + + if ( is_array ( $conditions ) && !empty ( $conditions ) ) { + $sql = $this->makeSQLFromConditions( $conditions, $bindings, $addSql ); + } else { + $sql = $this->glueSQLCondition( $addSql ); + } + + $sql = "DELETE FROM {$table} {$sql}"; + + return $this->adapter->exec( $sql, $bindings ); + } + + /** + * @see QueryWriter::deleteRelations + */ + public function deleteRelations( $sourceType, $destType, $sourceID ) + { + list( $sourceTable, $destTable, $linkTable, $sourceCol, $destCol ) = $this->getRelationalTablesAndColumns( $sourceType, $destType ); + + if ( $sourceTable === $destTable ) { + $sql = "DELETE FROM {$linkTable} + WHERE ( {$sourceCol} = ? ) OR + ( {$destCol} = ? ) + "; + + $this->adapter->exec( $sql, array( $sourceID, $sourceID ) ); + } else { + $sql = "DELETE FROM {$linkTable} + WHERE {$sourceCol} = ? "; + + $this->adapter->exec( $sql, array( $sourceID ) ); + } + } + + /** + * @see QueryWriter::widenColumn + */ + public function widenColumn( $type, $property, $dataType ) + { + if ( !isset($this->typeno_sqltype[$dataType]) ) return FALSE; + + $table = $this->esc( $type ); + $column = $this->esc( $property ); + + $newType = $this->typeno_sqltype[$dataType]; + + $this->adapter->exec( sprintf( $this->getDDLTemplate( 'widenColumn', $type, $column ), $type, $column, $column, $newType ) ); + + return TRUE; + } + + /** + * @see QueryWriter::wipe + */ + public function wipe( $type ) + { + $table = $this->esc( $type ); + + $this->adapter->exec( "TRUNCATE $table " ); + } + + /** + * @see QueryWriter::renameAssocTable + */ + public function renameAssocTable( $from, $to = NULL ) + { + self::renameAssociation( $from, $to ); + } + + /** + * @see QueryWriter::getAssocTable + */ + public function getAssocTable( $types ) + { + return self::getAssocTableFormat( $types ); + } + + /** + * Turns caching on or off. Default: off. + * If caching is turned on retrieval queries fired after eachother will + * use a result row cache. + * + * @param boolean + * + * @return void + */ + public function setUseCache( $yesNo ) + { + $this->flushCache(); + + $this->flagUseCache = (bool) $yesNo; + } + + /** + * Flushes the Query Writer Cache. + * Clears the internal query cache array and returns its overall + * size. + * + * @return mixed + */ + public function flushCache( $newMaxCacheSizePerType = NULL, $countCache = TRUE ) + { + if ( !is_null( $newMaxCacheSizePerType ) && $newMaxCacheSizePerType > 0 ) { + $this->maxCacheSizePerType = $newMaxCacheSizePerType; + } + $count = $countCache ? count( $this->cache, COUNT_RECURSIVE ) : NULL; + $this->cache = array(); + return $count; + } + + /** + * @deprecated Use esc() instead. + * + * @param string $column column to be escaped + * @param boolean $noQuotes omit quotes + * + * @return string + */ + public function safeColumn( $column, $noQuotes = FALSE ) + { + return $this->esc( $column, $noQuotes ); + } + + /** + * @deprecated Use esc() instead. + * + * @param string $table table to be escaped + * @param boolean $noQuotes omit quotes + * + * @return string + */ + public function safeTable( $table, $noQuotes = FALSE ) + { + return $this->esc( $table, $noQuotes ); + } + + /** + * @see QueryWriter::addUniqueConstraint + */ + public function addUniqueIndex( $type, $properties ) + { + return $this->addUniqueConstraint( $type, $properties ); + } +} +} + +namespace RedBeanPHP\QueryWriter { + +use RedBeanPHP\QueryWriter\AQueryWriter as AQueryWriter; +use RedBeanPHP\QueryWriter as QueryWriter; +use RedBeanPHP\Adapter\DBAdapter as DBAdapter; +use RedBeanPHP\Adapter as Adapter; +use RedBeanPHP\RedException\SQL as SQLException; + +/** + * RedBeanPHP MySQLWriter. + * This is a QueryWriter class for RedBeanPHP. + * This QueryWriter provides support for the MySQL/MariaDB database platform. + * + * @file RedBeanPHP/QueryWriter/MySQL.php + * @author Gabor de Mooij and the RedBeanPHP Community + * @license BSD/GPLv2 + * + * @copyright + * (c) G.J.G.T. (Gabor) de Mooij and the RedBeanPHP Community. + * This source file is subject to the BSD/GPLv2 License that is bundled + * with this source code in the file license.txt. + */ +class MySQL extends AQueryWriter implements QueryWriter +{ + /** + * Data types + */ + const C_DATATYPE_BOOL = 0; + const C_DATATYPE_UINT32 = 2; + const C_DATATYPE_DOUBLE = 3; + const C_DATATYPE_TEXT7 = 4; //InnoDB cant index varchar(255) utf8mb4 - so keep 191 as long as possible + const C_DATATYPE_TEXT8 = 5; + const C_DATATYPE_TEXT16 = 6; + const C_DATATYPE_TEXT32 = 7; + const C_DATATYPE_SPECIAL_DATE = 80; + const C_DATATYPE_SPECIAL_DATETIME = 81; + const C_DATATYPE_SPECIAL_TIME = 83; //MySQL time column (only manual) + const C_DATATYPE_SPECIAL_POINT = 90; + const C_DATATYPE_SPECIAL_LINESTRING = 91; + const C_DATATYPE_SPECIAL_POLYGON = 92; + const C_DATATYPE_SPECIAL_MONEY = 93; + const C_DATATYPE_SPECIAL_JSON = 94; //JSON support (only manual) + + const C_DATATYPE_SPECIFIED = 99; + + /** + * @var DBAdapter + */ + protected $adapter; + + /** + * @var string + */ + protected $quoteCharacter = '`'; + + /** + * @var array + */ + protected $DDLTemplates = array( + 'addColumn' => array( + '*' => 'ALTER TABLE %s ADD %s %s ' + ), + 'createTable' => array( + '*' => 'CREATE TABLE %s (id INT( 11 ) UNSIGNED NOT NULL AUTO_INCREMENT, PRIMARY KEY ( id )) ENGINE = InnoDB DEFAULT CHARSET=%s COLLATE=%s ' + ), + 'widenColumn' => array( + '*' => 'ALTER TABLE `%s` CHANGE %s %s %s ' + ) + ); + + /** + * @see AQueryWriter::getKeyMapForType + */ + protected function getKeyMapForType( $type ) + { + $databaseName = $this->adapter->getCell('SELECT DATABASE()'); + $table = $this->esc( $type, TRUE ); + $keys = $this->adapter->get(' + SELECT + information_schema.key_column_usage.constraint_name AS `name`, + information_schema.key_column_usage.referenced_table_name AS `table`, + information_schema.key_column_usage.column_name AS `from`, + information_schema.key_column_usage.referenced_column_name AS `to`, + information_schema.referential_constraints.update_rule AS `on_update`, + information_schema.referential_constraints.delete_rule AS `on_delete` + FROM information_schema.key_column_usage + INNER JOIN information_schema.referential_constraints + ON information_schema.referential_constraints.constraint_name = information_schema.key_column_usage.constraint_name + WHERE + information_schema.key_column_usage.table_schema = :database + AND information_schema.referential_constraints.constraint_schema = :database + AND information_schema.key_column_usage.constraint_schema = :database + AND information_schema.key_column_usage.table_name = :table + AND information_schema.key_column_usage.constraint_name != \'PRIMARY\' + AND information_schema.key_column_usage.referenced_table_name IS NOT NULL + ', array( ':database' => $databaseName, ':table' => $table ) ); + $keyInfoList = array(); + foreach ( $keys as $k ) { + $label = $this->makeFKLabel( $k['from'], $k['table'], $k['to'] ); + $keyInfoList[$label] = array( + 'name' => $k['name'], + 'from' => $k['from'], + 'table' => $k['table'], + 'to' => $k['to'], + 'on_update' => $k['on_update'], + 'on_delete' => $k['on_delete'] + ); + } + return $keyInfoList; + } + + /** + * Constructor + * Most of the time, you do not need to use this constructor, + * since the facade takes care of constructing and wiring the + * RedBeanPHP core objects. However if you would like to + * assemble an OODB instance yourself, this is how it works: + * + * Usage: + * + * + * $database = new RPDO( $dsn, $user, $pass ); + * $adapter = new DBAdapter( $database ); + * $writer = new PostgresWriter( $adapter ); + * $oodb = new OODB( $writer, FALSE ); + * $bean = $oodb->dispense( 'bean' ); + * $bean->name = 'coffeeBean'; + * $id = $oodb->store( $bean ); + * $bean = $oodb->load( 'bean', $id ); + * + * + * The example above creates the 3 RedBeanPHP core objects: + * the Adapter, the Query Writer and the OODB instance and + * wires them together. The example also demonstrates some of + * the methods that can be used with OODB, as you see, they + * closely resemble their facade counterparts. + * + * The wiring process: create an RPDO instance using your database + * connection parameters. Create a database adapter from the RPDO + * object and pass that to the constructor of the writer. Next, + * create an OODB instance from the writer. Now you have an OODB + * object. + * + * @param Adapter $adapter Database Adapter + * @param array $options options array + */ + public function __construct( Adapter $adapter, $options = array() ) + { + $this->typeno_sqltype = array( + MySQL::C_DATATYPE_BOOL => ' TINYINT(1) UNSIGNED ', + MySQL::C_DATATYPE_UINT32 => ' INT(11) UNSIGNED ', + MySQL::C_DATATYPE_DOUBLE => ' DOUBLE ', + MySQL::C_DATATYPE_TEXT7 => ' VARCHAR(191) ', + MYSQL::C_DATATYPE_TEXT8 => ' VARCHAR(255) ', + MySQL::C_DATATYPE_TEXT16 => ' TEXT ', + MySQL::C_DATATYPE_TEXT32 => ' LONGTEXT ', + MySQL::C_DATATYPE_SPECIAL_DATE => ' DATE ', + MySQL::C_DATATYPE_SPECIAL_DATETIME => ' DATETIME ', + MySQL::C_DATATYPE_SPECIAL_TIME => ' TIME ', + MySQL::C_DATATYPE_SPECIAL_POINT => ' POINT ', + MySQL::C_DATATYPE_SPECIAL_LINESTRING => ' LINESTRING ', + MySQL::C_DATATYPE_SPECIAL_POLYGON => ' POLYGON ', + MySQL::C_DATATYPE_SPECIAL_MONEY => ' DECIMAL(10,2) ', + MYSQL::C_DATATYPE_SPECIAL_JSON => ' JSON ' + ); + + $this->sqltype_typeno = array(); + + foreach ( $this->typeno_sqltype as $k => $v ) { + $this->sqltype_typeno[trim( strtolower( $v ) )] = $k; + } + + $this->adapter = $adapter; + $this->encoding = $this->adapter->getDatabase()->getMysqlEncoding(); + $me = $this; + if (!isset($options['noInitcode'])) + $this->adapter->setInitCode(function($version) use(&$me) { + try { + if (strpos($version, 'maria')===FALSE && intval($version)>=8) { + $me->useFeature('ignoreDisplayWidth'); + } + } catch( \Exception $e ){} + }); + } + + /** + * Enables certain features/dialects. + * + * - ignoreDisplayWidth required for MySQL8+ + * (automatically set by setup() if you pass dsn instead of PDO object) + * + * @param string $name feature ID + * + * @return void + */ + public function useFeature($name) { + if ($name == 'ignoreDisplayWidth') { + $this->typeno_sqltype[MySQL::C_DATATYPE_BOOL] = ' TINYINT UNSIGNED '; + $this->typeno_sqltype[MySQL::C_DATATYPE_UINT32] = ' INT UNSIGNED '; + foreach ( $this->typeno_sqltype as $k => $v ) { + $this->sqltype_typeno[trim( strtolower( $v ) )] = $k; + } + } + } + + /** + * This method returns the datatype to be used for primary key IDS and + * foreign keys. Returns one if the data type constants. + * + * @return integer + */ + public function getTypeForID() + { + return self::C_DATATYPE_UINT32; + } + + /** + * @see QueryWriter::getTables + */ + public function getTables() + { + return $this->adapter->getCol( 'show tables' ); + } + + /** + * @see QueryWriter::createTable + */ + public function createTable( $type ) + { + $table = $this->esc( $type ); + + $charset_collate = $this->adapter->getDatabase()->getMysqlEncoding( TRUE ); + $charset = $charset_collate['charset']; + $collate = $charset_collate['collate']; + + $sql = sprintf( $this->getDDLTemplate( 'createTable', $type ), $table, $charset, $collate ); + + $this->adapter->exec( $sql ); + } + + /** + * @see QueryWriter::getColumns + */ + public function getColumns( $table ) + { + $columnsRaw = $this->adapter->get( "DESCRIBE " . $this->esc( $table ) ); + + $columns = array(); + foreach ( $columnsRaw as $r ) { + $columns[$r['Field']] = $r['Type']; + } + + return $columns; + } + + /** + * @see QueryWriter::scanType + */ + public function scanType( $value, $flagSpecial = FALSE ) + { + $this->svalue = $value; + + if ( is_null( $value ) ) return MySQL::C_DATATYPE_BOOL; + if ( $value === INF ) return MySQL::C_DATATYPE_TEXT7; + + if ( $flagSpecial ) { + if ( preg_match( '/^-?\d+\.\d{2}$/', $value ) ) { + return MySQL::C_DATATYPE_SPECIAL_MONEY; + } + if ( preg_match( '/^\d{4}\-\d\d-\d\d$/', $value ) ) { + return MySQL::C_DATATYPE_SPECIAL_DATE; + } + if ( preg_match( '/^\d{4}\-\d\d-\d\d\s\d\d:\d\d:\d\d$/', $value ) ) { + return MySQL::C_DATATYPE_SPECIAL_DATETIME; + } + if ( preg_match( '/^POINT\(/', $value ) ) { + return MySQL::C_DATATYPE_SPECIAL_POINT; + } + if ( preg_match( '/^LINESTRING\(/', $value ) ) { + return MySQL::C_DATATYPE_SPECIAL_LINESTRING; + } + if ( preg_match( '/^POLYGON\(/', $value ) ) { + return MySQL::C_DATATYPE_SPECIAL_POLYGON; + } + if ( self::$flagUseJSONColumns && $this->isJSON( $value ) ) { + return self::C_DATATYPE_SPECIAL_JSON; + } + } + + //setter turns TRUE FALSE into 0 and 1 because database has no real bools (TRUE and FALSE only for test?). + if ( $value === FALSE || $value === TRUE || $value === '0' || $value === '1' || $value === 0 || $value === 1 ) { + return MySQL::C_DATATYPE_BOOL; + } + + if ( is_float( $value ) ) return self::C_DATATYPE_DOUBLE; + + if ( !$this->startsWithZeros( $value ) ) { + + if ( is_numeric( $value ) && ( floor( $value ) == $value ) && $value >= 0 && $value <= 4294967295 ) { + return MySQL::C_DATATYPE_UINT32; + } + + if ( is_numeric( $value ) ) { + return MySQL::C_DATATYPE_DOUBLE; + } + } + + if ( mb_strlen( $value, 'UTF-8' ) <= 191 ) { + return MySQL::C_DATATYPE_TEXT7; + } + + if ( mb_strlen( $value, 'UTF-8' ) <= 255 ) { + return MySQL::C_DATATYPE_TEXT8; + } + + if ( mb_strlen( $value, 'UTF-8' ) <= 65535 ) { + return MySQL::C_DATATYPE_TEXT16; + } + + return MySQL::C_DATATYPE_TEXT32; + } + + /** + * @see QueryWriter::code + */ + public function code( $typedescription, $includeSpecials = FALSE ) + { + if ( isset( $this->sqltype_typeno[$typedescription] ) ) { + $r = $this->sqltype_typeno[$typedescription]; + } else { + $r = self::C_DATATYPE_SPECIFIED; + } + + if ( $includeSpecials ) { + return $r; + } + + if ( $r >= QueryWriter::C_DATATYPE_RANGE_SPECIAL ) { + return self::C_DATATYPE_SPECIFIED; + } + + return $r; + } + + /** + * @see QueryWriter::addUniqueIndex + */ + public function addUniqueConstraint( $type, $properties ) + { + $tableNoQ = $this->esc( $type, TRUE ); + $columns = array(); + foreach( $properties as $key => $column ) $columns[$key] = $this->esc( $column ); + $table = $this->esc( $type ); + sort( $columns ); // Else we get multiple indexes due to order-effects + $name = 'UQ_' . sha1( implode( ',', $columns ) ); + try { + $sql = "ALTER TABLE $table + ADD UNIQUE INDEX $name (" . implode( ',', $columns ) . ")"; + $this->adapter->exec( $sql ); + } catch ( SQLException $e ) { + //do nothing, dont use alter table ignore, this will delete duplicate records in 3-ways! + return FALSE; + } + return TRUE; + } + + /** + * @see QueryWriter::addIndex + */ + public function addIndex( $type, $name, $property ) + { + try { + $table = $this->esc( $type ); + $name = preg_replace( '/\W/', '', $name ); + $column = $this->esc( $property ); + $this->adapter->exec( "CREATE INDEX $name ON $table ($column) " ); + return TRUE; + } catch ( SQLException $e ) { + return FALSE; + } + } + + /** + * @see QueryWriter::addFK + * @return bool + */ + public function addFK( $type, $targetType, $property, $targetProperty, $isDependent = FALSE ) + { + $table = $this->esc( $type ); + $targetTable = $this->esc( $targetType ); + $targetTableNoQ = $this->esc( $targetType, TRUE ); + $field = $this->esc( $property ); + $fieldNoQ = $this->esc( $property, TRUE ); + $targetField = $this->esc( $targetProperty ); + $targetFieldNoQ = $this->esc( $targetProperty, TRUE ); + $tableNoQ = $this->esc( $type, TRUE ); + $fieldNoQ = $this->esc( $property, TRUE ); + if ( !is_null( $this->getForeignKeyForTypeProperty( $tableNoQ, $fieldNoQ ) ) ) return FALSE; + + //Widen the column if it's incapable of representing a foreign key (at least INT). + $columns = $this->getColumns( $tableNoQ ); + $idType = $this->getTypeForID(); + if ( $this->code( $columns[$fieldNoQ] ) !== $idType ) { + $this->widenColumn( $type, $property, $idType ); + } + + $fkName = 'fk_'.($tableNoQ.'_'.$fieldNoQ); + $cName = 'c_'.$fkName; + try { + $this->adapter->exec( " + ALTER TABLE {$table} + ADD CONSTRAINT $cName + FOREIGN KEY $fkName ( `{$fieldNoQ}` ) REFERENCES `{$targetTableNoQ}` + (`{$targetFieldNoQ}`) ON DELETE " . ( $isDependent ? 'CASCADE' : 'SET NULL' ) . ' ON UPDATE '.( $isDependent ? 'CASCADE' : 'SET NULL' ).';'); + } catch ( SQLException $e ) { + // Failure of fk-constraints is not a problem + } + return TRUE; + } + + /** + * @see QueryWriter::sqlStateIn + */ + public function sqlStateIn( $state, $list, $extraDriverDetails = array() ) + { + $stateMap = array( + '42S02' => QueryWriter::C_SQLSTATE_NO_SUCH_TABLE, + '42S22' => QueryWriter::C_SQLSTATE_NO_SUCH_COLUMN, + '23000' => QueryWriter::C_SQLSTATE_INTEGRITY_CONSTRAINT_VIOLATION, + ); + + if ( $state == 'HY000' && !empty( $extraDriverDetails[1] ) ) { + $driverCode = $extraDriverDetails[1]; + + if ( $driverCode == '1205' && in_array( QueryWriter::C_SQLSTATE_LOCK_TIMEOUT, $list ) ) { + return TRUE; + } + } + + return in_array( ( isset( $stateMap[$state] ) ? $stateMap[$state] : '0' ), $list ); + } + + /** + * @see QueryWriter::wipeAll + */ + public function wipeAll() + { + if (AQueryWriter::$noNuke) throw new \Exception('The nuke() command has been disabled using noNuke() or R::feature(novice/...).'); + $this->adapter->exec( 'SET FOREIGN_KEY_CHECKS = 0;' ); + + foreach ( $this->getTables() as $t ) { + try { $this->adapter->exec( "DROP TABLE IF EXISTS `$t`" ); } catch ( SQLException $e ) { ; } + try { $this->adapter->exec( "DROP VIEW IF EXISTS `$t`" ); } catch ( SQLException $e ) { ; } + } + + $this->adapter->exec( 'SET FOREIGN_KEY_CHECKS = 1;' ); + } +} +} + +namespace RedBeanPHP\QueryWriter { +use RedBeanPHP\QueryWriter\AQueryWriter as AQueryWriter; +use RedBeanPHP\QueryWriter as QueryWriter; +use RedBeanPHP\Adapter\DBAdapter as DBAdapter; +use RedBeanPHP\Adapter as Adapter; +use RedBeanPHP\RedException\SQL as SQLException; + +/** + * RedBeanPHP CUBRID Writer. + * This is a QueryWriter class for RedBeanPHP. + * This QueryWriter provides support for the CUBRID database platform. + * + * @file RedBeanPHP/QueryWriter/CUBRID.php + * @author Gabor de Mooij and the RedBeanPHP Community + * @license BSD/GPLv2 + * + * @copyright + * (c) copyright G.J.G.T. (Gabor) de Mooij and the RedBeanPHP Community. + * This source file is subject to the BSD/GPLv2 License that is bundled + * with this source code in the file license.txt. + */ +class CUBRID extends AQueryWriter implements QueryWriter +{ + /** + * Data types + */ + const C_DATATYPE_INTEGER = 0; + const C_DATATYPE_DOUBLE = 1; + const C_DATATYPE_STRING = 2; + const C_DATATYPE_SPECIAL_DATE = 80; + const C_DATATYPE_SPECIAL_DATETIME = 81; + const C_DATATYPE_SPECIFIED = 99; + + /** + * @var DBAdapter + */ + protected $adapter; + + /** + * @var string + */ + protected $quoteCharacter = '`'; + + /** + * This method adds a foreign key from type and field to + * target type and target field. + * The foreign key is created without an action. On delete/update + * no action will be triggered. The FK is only used to allow database + * tools to generate pretty diagrams and to make it easy to add actions + * later on. + * This methods accepts a type and infers the corresponding table name. + * + * @param string $type type that will have a foreign key field + * @param string $targetType points to this type + * @param string $property field that contains the foreign key value + * @param string $targetProperty field where the fk points to + * @param boolean $isDep is dependent + * + * @return bool + */ + protected function buildFK( $type, $targetType, $property, $targetProperty, $isDep = FALSE ) + { + $table = $this->esc( $type ); + $tableNoQ = $this->esc( $type, TRUE ); + $targetTable = $this->esc( $targetType ); + $targetTableNoQ = $this->esc( $targetType, TRUE ); + $column = $this->esc( $property ); + $columnNoQ = $this->esc( $property, TRUE ); + $targetColumn = $this->esc( $targetProperty ); + if ( !is_null( $this->getForeignKeyForTypeProperty( $tableNoQ, $columnNoQ ) ) ) return FALSE; + $needsToDropFK = FALSE; + $casc = ( $isDep ? 'CASCADE' : 'SET NULL' ); + $sql = "ALTER TABLE $table ADD CONSTRAINT FOREIGN KEY($column) REFERENCES $targetTable($targetColumn) ON DELETE $casc "; + try { + $this->adapter->exec( $sql ); + } catch( SQLException $e ) { + return FALSE; + } + return TRUE; + } + + /** + * @see AQueryWriter::getKeyMapForType + */ + protected function getKeyMapForType( $type ) + { + $sqlCode = $this->adapter->get("SHOW CREATE TABLE `{$type}`"); + if (!isset($sqlCode[0])) return array(); + $matches = array(); + preg_match_all( '/CONSTRAINT\s+\[([\w_]+)\]\s+FOREIGN\s+KEY\s+\(\[([\w_]+)\]\)\s+REFERENCES\s+\[([\w_]+)\](\s+ON\s+DELETE\s+(CASCADE|SET\sNULL|RESTRICT|NO\sACTION)\s+ON\s+UPDATE\s+(SET\sNULL|RESTRICT|NO\sACTION))?/', $sqlCode[0]['CREATE TABLE'], $matches ); + $list = array(); + if (!isset($matches[0])) return $list; + $max = count($matches[0]); + for($i = 0; $i < $max; $i++) { + $label = $this->makeFKLabel( $matches[2][$i], $matches[3][$i], 'id' ); + $list[ $label ] = array( + 'name' => $matches[1][$i], + 'from' => $matches[2][$i], + 'table' => $matches[3][$i], + 'to' => 'id', + 'on_update' => $matches[6][$i], + 'on_delete' => $matches[5][$i] + ); + } + return $list; + } + + /** + * Constructor + * Most of the time, you do not need to use this constructor, + * since the facade takes care of constructing and wiring the + * RedBeanPHP core objects. However if you would like to + * assemble an OODB instance yourself, this is how it works: + * + * Usage: + * + * + * $database = new RPDO( $dsn, $user, $pass ); + * $adapter = new DBAdapter( $database ); + * $writer = new PostgresWriter( $adapter ); + * $oodb = new OODB( $writer, FALSE ); + * $bean = $oodb->dispense( 'bean' ); + * $bean->name = 'coffeeBean'; + * $id = $oodb->store( $bean ); + * $bean = $oodb->load( 'bean', $id ); + * + * + * The example above creates the 3 RedBeanPHP core objects: + * the Adapter, the Query Writer and the OODB instance and + * wires them together. The example also demonstrates some of + * the methods that can be used with OODB, as you see, they + * closely resemble their facade counterparts. + * + * The wiring process: create an RPDO instance using your database + * connection parameters. Create a database adapter from the RPDO + * object and pass that to the constructor of the writer. Next, + * create an OODB instance from the writer. Now you have an OODB + * object. + * + * @param Adapter $adapter Database Adapter + */ + public function __construct( Adapter $adapter ) + { + $this->typeno_sqltype = array( + CUBRID::C_DATATYPE_INTEGER => ' INTEGER ', + CUBRID::C_DATATYPE_DOUBLE => ' DOUBLE ', + CUBRID::C_DATATYPE_STRING => ' STRING ', + CUBRID::C_DATATYPE_SPECIAL_DATE => ' DATE ', + CUBRID::C_DATATYPE_SPECIAL_DATETIME => ' DATETIME ', + ); + + $this->sqltype_typeno = array(); + + foreach ( $this->typeno_sqltype as $k => $v ) { + $this->sqltype_typeno[trim( ( $v ) )] = $k; + } + + $this->sqltype_typeno['STRING(1073741823)'] = self::C_DATATYPE_STRING; + + $this->adapter = $adapter; + } + + /** + * This method returns the datatype to be used for primary key IDS and + * foreign keys. Returns one if the data type constants. + * + * @return integer + */ + public function getTypeForID() + { + return self::C_DATATYPE_INTEGER; + } + + /** + * @see QueryWriter::getTables + */ + public function getTables() + { + $rows = $this->adapter->getCol( "SELECT class_name FROM db_class WHERE is_system_class = 'NO';" ); + + return $rows; + } + + /** + * @see QueryWriter::createTable + */ + public function createTable( $table ) + { + $sql = 'CREATE TABLE ' + . $this->esc( $table ) + . ' ("id" integer AUTO_INCREMENT, CONSTRAINT "pk_' + . $this->esc( $table, TRUE ) + . '_id" PRIMARY KEY("id"))'; + + $this->adapter->exec( $sql ); + } + + /** + * @see QueryWriter::getColumns + */ + public function getColumns( $table ) + { + $table = $this->esc( $table ); + + $columnsRaw = $this->adapter->get( "SHOW COLUMNS FROM $table" ); + + $columns = array(); + foreach ( $columnsRaw as $r ) { + $columns[$r['Field']] = $r['Type']; + } + + return $columns; + } + + /** + * @see QueryWriter::scanType + */ + public function scanType( $value, $flagSpecial = FALSE ) + { + $this->svalue = $value; + + if ( is_null( $value ) ) { + return self::C_DATATYPE_INTEGER; + } + + if ( $flagSpecial ) { + if ( preg_match( '/^\d{4}\-\d\d-\d\d$/', $value ) ) { + return self::C_DATATYPE_SPECIAL_DATE; + } + if ( preg_match( '/^\d{4}\-\d\d-\d\d\s\d\d:\d\d:\d\d$/', $value ) ) { + return self::C_DATATYPE_SPECIAL_DATETIME; + } + } + + $value = strval( $value ); + + if ( !$this->startsWithZeros( $value ) ) { + if ( is_numeric( $value ) && ( floor( $value ) == $value ) && $value >= -2147483647 && $value <= 2147483647 ) { + return self::C_DATATYPE_INTEGER; + } + if ( is_numeric( $value ) ) { + return self::C_DATATYPE_DOUBLE; + } + } + + return self::C_DATATYPE_STRING; + } + + /** + * @see QueryWriter::code + */ + public function code( $typedescription, $includeSpecials = FALSE ) + { + $r = ( ( isset( $this->sqltype_typeno[$typedescription] ) ) ? $this->sqltype_typeno[$typedescription] : self::C_DATATYPE_SPECIFIED ); + + if ( $includeSpecials ) { + return $r; + } + + if ( $r >= QueryWriter::C_DATATYPE_RANGE_SPECIAL ) { + return self::C_DATATYPE_SPECIFIED; + } + + return $r; + } + + /** + * @see QueryWriter::addColumn + */ + public function addColumn( $type, $column, $field ) + { + $table = $type; + $type = $field; + + $table = $this->esc( $table ); + $column = $this->esc( $column ); + + $type = array_key_exists( $type, $this->typeno_sqltype ) ? $this->typeno_sqltype[$type] : ''; + + $this->adapter->exec( "ALTER TABLE $table ADD COLUMN $column $type " ); + } + + /** + * @see QueryWriter::addUniqueIndex + */ + public function addUniqueConstraint( $type, $properties ) + { + $tableNoQ = $this->esc( $type, TRUE ); + $columns = array(); + foreach( $properties as $key => $column ) $columns[$key] = $this->esc( $column ); + $table = $this->esc( $type ); + sort( $columns ); // else we get multiple indexes due to order-effects + $name = 'UQ_' . sha1( implode( ',', $columns ) ); + $sql = "ALTER TABLE $table ADD CONSTRAINT UNIQUE $name (" . implode( ',', $columns ) . ")"; + try { + $this->adapter->exec( $sql ); + } catch( SQLException $e ) { + return FALSE; + } + return TRUE; + } + + /** + * @see QueryWriter::sqlStateIn + */ + public function sqlStateIn( $state, $list, $extraDriverDetails = array() ) + { + return ( $state == 'HY000' ) ? ( count( array_diff( array( + QueryWriter::C_SQLSTATE_INTEGRITY_CONSTRAINT_VIOLATION, + QueryWriter::C_SQLSTATE_NO_SUCH_COLUMN, + QueryWriter::C_SQLSTATE_NO_SUCH_TABLE + ), $list ) ) !== 3 ) : FALSE; + } + + /** + * @see QueryWriter::addIndex + */ + public function addIndex( $type, $name, $column ) + { + try { + $table = $this->esc( $type ); + $name = preg_replace( '/\W/', '', $name ); + $column = $this->esc( $column ); + $this->adapter->exec( "CREATE INDEX $name ON $table ($column) " ); + return TRUE; + } catch ( SQLException $e ) { + return FALSE; + } + } + + /** + * @see QueryWriter::addFK + */ + public function addFK( $type, $targetType, $property, $targetProperty, $isDependent = FALSE ) + { + return $this->buildFK( $type, $targetType, $property, $targetProperty, $isDependent ); + } + + /** + * @see QueryWriter::wipeAll + */ + public function wipeAll() + { + if (AQueryWriter::$noNuke) throw new \Exception('The nuke() command has been disabled using noNuke() or R::feature(novice/...).'); + foreach ( $this->getTables() as $t ) { + foreach ( $this->getKeyMapForType( $t ) as $k ) { + $this->adapter->exec( "ALTER TABLE \"$t\" DROP FOREIGN KEY \"{$k['name']}\"" ); + } + } + foreach ( $this->getTables() as $t ) { + $this->adapter->exec( "DROP TABLE \"$t\"" ); + } + } + + /** + * @see QueryWriter::esc + */ + public function esc( $dbStructure, $noQuotes = FALSE ) + { + return parent::esc( strtolower( $dbStructure ), $noQuotes ); + } +} +} + +namespace RedBeanPHP { + +/** + * RedBean\Exception Base. + * Represents the base class for RedBeanPHP\Exceptions. + * + * @file RedBeanPHP/Exception.php + * @author Gabor de Mooij and the RedBeanPHP Community + * @license BSD/GPLv2 + * + * @copyright + * copyright (c) G.J.G.T. (Gabor) de Mooij and the RedBeanPHP Community + * This source file is subject to the BSD/GPLv2 License that is bundled + * with this source code in the file license.txt. + */ +class RedException extends \Exception +{ +} +} + +namespace RedBeanPHP\RedException { + +use RedBeanPHP\RedException as RedException; + +/** + * SQL Exception. + * Represents a generic database exception independent of the underlying driver. + * + * @file RedBeanPHP/RedException/SQL.php + * @author Gabor de Mooij and the RedBeanPHP Community + * @license BSD/GPLv2 + * + * @copyright + * (c) copyright G.J.G.T. (Gabor) de Mooij and the RedBeanPHP Community. + * This source file is subject to the BSD/GPLv2 License that is bundled + * with this source code in the file license.txt. + */ +class SQL extends RedException +{ + /** + * @var string + */ + private $sqlState; + + /** + * @var array + */ + private $driverDetails = array(); + + /** + * @return array + */ + public function getDriverDetails() + { + return $this->driverDetails; + } + + /** + * @param array $driverDetails + */ + public function setDriverDetails($driverDetails) + { + $this->driverDetails = $driverDetails; + } + + /** + * Returns an ANSI-92 compliant SQL state. + * + * @return string + */ + public function getSQLState() + { + return $this->sqlState; + } + + /** + * Returns the raw SQL STATE, possibly compliant with + * ANSI SQL error codes - but this depends on database driver. + * + * @param string $sqlState SQL state error code + * + * @return void + */ + public function setSQLState( $sqlState ) + { + $this->sqlState = $sqlState; + } + + /** + * To String prints both code and SQL state. + * + * @return string + */ + public function __toString() + { + return '[' . $this->getSQLState() . '] - ' . $this->getMessage()."\n". + 'trace: ' . $this->getTraceAsString(); + } +} +} + +namespace RedBeanPHP { + +use RedBeanPHP\Adapter\DBAdapter as DBAdapter; +use RedBeanPHP\QueryWriter as QueryWriter; +use RedBeanPHP\BeanHelper as BeanHelper; +use RedBeanPHP\RedException\SQL as SQLException; +use RedBeanPHP\QueryWriter\AQueryWriter as AQueryWriter; +use RedBeanPHP\Cursor as Cursor; +use RedBeanPHP\Cursor\NullCursor as NullCursor; + +/** + * Abstract Repository. + * + * OODB manages two repositories, a fluid one that + * adjust the database schema on-the-fly to accomodate for + * new bean types (tables) and new properties (columns) and + * a frozen one for use in a production environment. OODB + * allows you to swap the repository instances using the freeze() + * method. + * + * @file RedBeanPHP/Repository.php + * @author Gabor de Mooij and the RedBeanPHP community + * @license BSD/GPLv2 + * + * @copyright + * copyright (c) G.J.G.T. (Gabor) de Mooij and the RedBeanPHP Community + * This source file is subject to the BSD/GPLv2 License that is bundled + * with this source code in the file license.txt. + */ +abstract class Repository +{ + /** + * @var array + */ + protected $stash = NULL; + + /* + * @var integer + */ + protected $nesting = 0; + + /** + * @var DBAdapter + */ + protected $writer; + + /** + * @var boolean + */ + protected $partialBeans = FALSE; + + /** + * Toggles 'partial bean mode'. If this mode has been + * selected the repository will only update the fields of a bean that + * have been changed rather than the entire bean. + * Pass the value TRUE to select 'partial mode' for all beans. + * Pass the value FALSE to disable 'partial mode'. + * Pass an array of bean types if you wish to use partial mode only + * for some types. + * This method will return the previous value. + * + * @param boolean|array $yesNoBeans List of type names or 'all' + * + * @return mixed + */ + public function usePartialBeans( $yesNoBeans ) + { + $oldValue = $this->partialBeans; + $this->partialBeans = $yesNoBeans; + return $oldValue; + } + + /** + * Fully processes a bean and updates the associated records in the database. + * First the bean properties will be grouped as 'embedded' bean, + * addition, deleted 'trash can' or residue. Next, the different groups + * of beans will be processed accordingly and the reference bean (i.e. + * the one that was passed to the method as an argument) will be stored. + * Each type of list (own/shared) has 3 bean processors: + * + * - trashCanProcessor : removes the bean or breaks its association with the current bean + * - additionProcessor : associates the bean with the current one + * - residueProcessor : manages beans in lists that 'remain' but may need to be updated + * + * This method first groups the beans and then calls the + * internal processing methods. + * + * @param OODBBean $bean bean to process + * + * @return void + */ + protected function storeBeanWithLists( OODBBean $bean ) + { + $sharedAdditions = $sharedTrashcan = $sharedresidue = $sharedItems = $ownAdditions = $ownTrashcan = $ownresidue = $embeddedBeans = array(); //Define groups + foreach ( $bean as $property => $value ) { + $value = ( $value instanceof SimpleModel ) ? $value->unbox() : $value; + if ( $value instanceof OODBBean ) { + $this->processEmbeddedBean( $embeddedBeans, $bean, $property, $value ); + $bean->setMeta("sys.typeof.{$property}", $value->getMeta('type')); + } elseif ( is_array( $value ) ) { + foreach($value as &$item) { + $item = ( $item instanceof SimpleModel ) ? $item->unbox() : $item; + } + $originals = $bean->moveMeta( 'sys.shadow.' . $property, array() ); + if ( strpos( $property, 'own' ) === 0 ) { + list( $ownAdditions, $ownTrashcan, $ownresidue ) = $this->processGroups( $originals, $value, $ownAdditions, $ownTrashcan, $ownresidue ); + $listName = lcfirst( substr( $property, 3 ) ); + if ($bean->moveMeta( 'sys.exclusive-'. $listName ) ) { + OODBBean::setMetaAll( $ownTrashcan, 'sys.garbage', TRUE ); + OODBBean::setMetaAll( $ownAdditions, 'sys.buildcommand.fkdependson', $bean->getMeta( 'type' ) ); + } + unset( $bean->$property ); + } elseif ( strpos( $property, 'shared' ) === 0 ) { + list( $sharedAdditions, $sharedTrashcan, $sharedresidue ) = $this->processGroups( $originals, $value, $sharedAdditions, $sharedTrashcan, $sharedresidue ); + unset( $bean->$property ); + } + } + } + $this->storeBean( $bean ); + $this->processTrashcan( $bean, $ownTrashcan ); + $this->processAdditions( $bean, $ownAdditions ); + $this->processResidue( $ownresidue ); + $this->processSharedTrashcan( $bean, $sharedTrashcan ); + $this->processSharedAdditions( $bean, $sharedAdditions ); + $this->processSharedResidue( $bean, $sharedresidue ); + } + + /** + * Process groups. Internal function. Processes different kind of groups for + * storage function. Given a list of original beans and a list of current beans, + * this function calculates which beans remain in the list (residue), which + * have been deleted (are in the trashcan) and which beans have been added + * (additions). + * + * @param array $originals originals + * @param array $current the current beans + * @param array $additions beans that have been added + * @param array $trashcan beans that have been deleted + * @param array $residue beans that have been left untouched + * + * @return array + */ + protected function processGroups( $originals, $current, $additions, $trashcan, $residue ) + { + return array( + array_merge( $additions, array_diff( $current, $originals ) ), + array_merge( $trashcan, array_diff( $originals, $current ) ), + array_merge( $residue, array_intersect( $current, $originals ) ) + ); + } + + /** + * Processes a list of beans from a bean. + * A bean may contain lists. This + * method handles shared addition lists; i.e. + * the $bean->sharedObject properties. + * Shared beans will be associated with eachother using the + * Association Manager. + * + * @param OODBBean $bean the bean + * @param array $sharedAdditions list with shared additions + * + * @return void + */ + protected function processSharedAdditions( $bean, $sharedAdditions ) + { + foreach ( $sharedAdditions as $addition ) { + if ( $addition instanceof OODBBean ) { + $this->oodb->getAssociationManager()->associate( $addition, $bean ); + } else { + throw new RedException( 'Array may only contain OODBBeans' ); + } + } + } + + /** + * Processes a list of beans from a bean. + * A bean may contain lists. This + * method handles own lists; i.e. + * the $bean->ownObject properties. + * A residue is a bean in an own-list that stays + * where it is. This method checks if there have been any + * modification to this bean, in that case + * the bean is stored once again, otherwise the bean will be left untouched. + * + * @param array $ownresidue list to process + * + * @return void + */ + protected function processResidue( $ownresidue ) + { + foreach ( $ownresidue as $residue ) { + if ( $residue->getMeta( 'tainted' ) ) { + $this->store( $residue ); + } + } + } + + /** + * Processes a list of beans from a bean. A bean may contain lists. This + * method handles own lists; i.e. the $bean->ownObject properties. + * A trash can bean is a bean in an own-list that has been removed + * (when checked with the shadow). This method + * checks if the bean is also in the dependency list. If it is the bean will be removed. + * If not, the connection between the bean and the owner bean will be broken by + * setting the ID to NULL. + * + * @param OODBBean $bean bean to process + * @param array $ownTrashcan list to process + * + * @return void + */ + protected function processTrashcan( $bean, $ownTrashcan ) + { + foreach ( $ownTrashcan as $trash ) { + + $myFieldLink = $bean->getMeta( 'type' ) . '_id'; + $alias = $bean->getMeta( 'sys.alias.' . $trash->getMeta( 'type' ) ); + if ( $alias ) $myFieldLink = $alias . '_id'; + + if ( $trash->getMeta( 'sys.garbage' ) === TRUE ) { + $this->trash( $trash ); + } else { + $trash->$myFieldLink = NULL; + $this->store( $trash ); + } + } + } + + /** + * Unassociates the list items in the trashcan. + * This bean processor processes the beans in the shared trash can. + * This group of beans has been deleted from a shared list. + * The affected beans will no longer be associated with the bean + * that contains the shared list. + * + * @param OODBBean $bean bean to process + * @param array $sharedTrashcan list to process + * + * @return void + */ + protected function processSharedTrashcan( $bean, $sharedTrashcan ) + { + foreach ( $sharedTrashcan as $trash ) { + $this->oodb->getAssociationManager()->unassociate( $trash, $bean ); + } + } + + /** + * Stores all the beans in the residue group. + * This bean processor processes the beans in the shared residue + * group. This group of beans 'remains' in the list but might need + * to be updated or synced. The affected beans will be stored + * to perform the required database queries. + * + * @param OODBBean $bean bean to process + * @param array $sharedresidue list to process + * + * @return void + */ + protected function processSharedResidue( $bean, $sharedresidue ) + { + foreach ( $sharedresidue as $residue ) { + $this->store( $residue ); + } + } + + /** + * Determines whether the bean has 'loaded lists' or + * 'loaded embedded beans' that need to be processed + * by the store() method. + * + * @param OODBBean $bean bean to be examined + * + * @return boolean + */ + protected function hasListsOrObjects( OODBBean $bean ) + { + $processLists = FALSE; + foreach ( $bean as $value ) { + if ( is_array( $value ) || is_object( $value ) ) { + $processLists = TRUE; + break; + } + } + + return $processLists; + } + + /** + * Converts an embedded bean to an ID, removes the bean property and + * stores the bean in the embedded beans array. The id will be + * assigned to the link field property, i.e. 'bean_id'. + * + * @param array $embeddedBeans destination array for embedded bean + * @param OODBBean $bean target bean to process + * @param string $property property that contains the embedded bean + * @param OODBBean $value embedded bean itself + * + * @return void + */ + protected function processEmbeddedBean( &$embeddedBeans, $bean, $property, OODBBean $value ) + { + $linkField = $property . '_id'; + if ( !$value->id || $value->getMeta( 'tainted' ) ) { + $this->store( $value ); + } + $id = $value->id; + if ($bean->$linkField != $id) $bean->$linkField = $id; + $bean->setMeta( 'cast.' . $linkField, 'id' ); + $embeddedBeans[$linkField] = $value; + unset( $bean->$property ); + } + + /** + * Constructor, requires a query writer and OODB. + * Creates a new instance of the bean respository class. + * + * @param OODB $oodb instance of object database + * @param QueryWriter $writer the Query Writer to use for this repository + * + * @return void + */ + public function __construct( OODB $oodb, QueryWriter $writer ) + { + $this->writer = $writer; + $this->oodb = $oodb; + } + + /** + * Checks whether a OODBBean bean is valid. + * If the type is not valid or the ID is not valid it will + * throw an exception: Security. To be valid a bean + * must abide to the following rules: + * + * - It must have an primary key id property named: id + * - It must have a type + * - The type must conform to the RedBeanPHP naming policy + * - All properties must be valid + * - All values must be valid + * + * @param OODBBean $bean the bean that needs to be checked + * + * @return void + */ + public function check( OODBBean $bean ) + { + //Is all meta information present? + if ( !isset( $bean->id ) ) { + throw new RedException( 'Bean has incomplete Meta Information id ' ); + } + if ( !( $bean->getMeta( 'type' ) ) ) { + throw new RedException( 'Bean has incomplete Meta Information II' ); + } + //Pattern of allowed characters + $pattern = '/[^a-z0-9_]/i'; + //Does the type contain invalid characters? + if ( preg_match( $pattern, $bean->getMeta( 'type' ) ) ) { + throw new RedException( 'Bean Type is invalid' ); + } + //Are the properties and values valid? + foreach ( $bean as $prop => $value ) { + if ( + is_array( $value ) + || ( is_object( $value ) ) + ) { + throw new RedException( "Invalid Bean value: property $prop" ); + } else if ( + strlen( $prop ) < 1 + || preg_match( $pattern, $prop ) + ) { + throw new RedException( "Invalid Bean property: property $prop" ); + } + } + } + + /** + * Dispenses a new bean (a OODBBean Bean Object) + * of the specified type. Always + * use this function to get an empty bean object. Never + * instantiate a OODBBean yourself because it needs + * to be configured before you can use it with RedBean. This + * function applies the appropriate initialization / + * configuration for you. + * + * To use a different class for beans (instead of OODBBean) set: + * REDBEAN_OODBBEAN_CLASS to the name of the class to be used. + * + * @param string $type type of bean you want to dispense + * @param int $number number of beans you would like to get + * @param boolean $alwaysReturnArray if TRUE always returns the result as an array + * + * @return OODBBean + */ + public function dispense( $type, $number = 1, $alwaysReturnArray = FALSE ) + { + $OODBBEAN = defined( 'REDBEAN_OODBBEAN_CLASS' ) ? REDBEAN_OODBBEAN_CLASS : '\RedBeanPHP\OODBBean'; + $beans = array(); + for ( $i = 0; $i < $number; $i++ ) { + $bean = new $OODBBEAN; + $bean->initializeForDispense( $type, $this->oodb->getBeanHelper() ); + $this->check( $bean ); + $this->oodb->signal( 'dispense', $bean ); + $beans[] = $bean; + } + + return ( count( $beans ) === 1 && !$alwaysReturnArray ) ? array_pop( $beans ) : $beans; + } + + /** + * Searches the database for a bean that matches conditions $conditions and sql $addSQL + * and returns an array containing all the beans that have been found. + * + * Conditions need to take form: + * + * + * array( + * 'PROPERTY' => array( POSSIBLE VALUES... 'John', 'Steve' ) + * 'PROPERTY' => array( POSSIBLE VALUES... ) + * ); + * + * + * All conditions are glued together using the AND-operator, while all value lists + * are glued using IN-operators thus acting as OR-conditions. + * + * Note that you can use property names; the columns will be extracted using the + * appropriate bean formatter. + * + * @param string $type type of beans you are looking for + * @param array $conditions list of conditions + * @param string $sql SQL to be used in query + * @param array $bindings whether you prefer to use a WHERE clause or not (TRUE = not) + * + * @return array + */ + public function find( $type, $conditions = array(), $sql = NULL, $bindings = array() ) + { + //for backward compatibility, allow mismatch arguments: + if ( is_array( $sql ) ) { + if ( isset( $sql[1] ) ) { + $bindings = $sql[1]; + } + $sql = $sql[0]; + } + try { + $beans = $this->convertToBeans( $type, $this->writer->queryRecord( $type, $conditions, $sql, $bindings ) ); + + return $beans; + } catch ( SQLException $exception ) { + $this->handleException( $exception ); + } + + return array(); + } + + /** + * Finds a BeanCollection. + * Given a type, an SQL snippet and optionally some parameter bindings + * this methods returns a BeanCollection for your query. + * + * The BeanCollection represents a collection of beans and + * makes it possible to use database cursors. The BeanCollection + * has a method next() to obtain the first, next and last bean + * in the collection. The BeanCollection does not implement the array + * interface nor does it try to act like an array because it cannot go + * backward or rewind itself. + * + * @param string $type type of beans you are looking for + * @param string $sql SQL to be used in query + * @param array $bindings whether you prefer to use a WHERE clause or not (TRUE = not) + * + * @return BeanCollection + */ + public function findCollection( $type, $sql, $bindings = array() ) + { + try { + $cursor = $this->writer->queryRecordWithCursor( $type, $sql, $bindings ); + return new BeanCollection( $type, $this, $cursor ); + } catch ( SQLException $exception ) { + $this->handleException( $exception ); + } + return new BeanCollection( $type, $this, new NullCursor ); + } + + /** + * Stores a bean in the database. This method takes a + * OODBBean Bean Object $bean and stores it + * in the database. If the database schema is not compatible + * with this bean and RedBean runs in fluid mode the schema + * will be altered to store the bean correctly. + * If the database schema is not compatible with this bean and + * RedBean runs in frozen mode it will throw an exception. + * This function returns the primary key ID of the inserted + * bean. + * + * The return value is an integer if possible. If it is not possible to + * represent the value as an integer a string will be returned. We use + * explicit casts instead of functions to preserve performance + * (0.13 vs 0.28 for 10000 iterations on Core i3). + * + * @param OODBBean|SimpleModel $bean bean to store + * + * @return integer|string + */ + public function store( $bean ) + { + $processLists = $this->hasListsOrObjects( $bean ); + if ( !$processLists && !$bean->getMeta( 'tainted' ) ) { + return $bean->getID(); //bail out! + } + $this->oodb->signal( 'update', $bean ); + $processLists = $this->hasListsOrObjects( $bean ); //check again, might have changed by model! + if ( $processLists ) { + $this->storeBeanWithLists( $bean ); + } else { + $this->storeBean( $bean ); + } + $this->oodb->signal( 'after_update', $bean ); + + return ( (string) $bean->id === (string) (int) $bean->id ) ? (int) $bean->id : (string) $bean->id; + } + + /** + * Returns an array of beans. Pass a type and a series of ids and + * this method will bring you the corresponding beans. + * + * important note: Because this method loads beans using the load() + * function (but faster) it will return empty beans with ID 0 for + * every bean that could not be located. The resulting beans will have the + * passed IDs as their keys. + * + * @param string $type type of beans + * @param array $ids ids to load + * + * @return array + */ + public function batch( $type, $ids ) + { + if ( !$ids ) { + return array(); + } + $collection = array(); + try { + $rows = $this->writer->queryRecord( $type, array( 'id' => $ids ) ); + } catch ( SQLException $e ) { + $this->handleException( $e ); + $rows = FALSE; + } + $this->stash[$this->nesting] = array(); + if ( !$rows ) { + return array(); + } + foreach ( $rows as $row ) { + $this->stash[$this->nesting][$row['id']] = $row; + } + foreach ( $ids as $id ) { + $collection[$id] = $this->load( $type, $id ); + } + $this->stash[$this->nesting] = NULL; + + return $collection; + } + + /** + * This is a convenience method; it converts database rows + * (arrays) into beans. Given a type and a set of rows this method + * will return an array of beans of the specified type loaded with + * the data fields provided by the result set from the database. + * + * New in 4.3.2: meta mask. The meta mask is a special mask to send + * data from raw result rows to the meta store of the bean. This is + * useful for bundling additional information with custom queries. + * Values of every column whos name starts with $mask will be + * transferred to the meta section of the bean under key 'data.bundle'. + * + * @param string $type type of beans you would like to have + * @param array $rows rows from the database result + * @param string $mask meta mask to apply (optional) + * + * @return array + */ + public function convertToBeans( $type, $rows, $mask = '__meta' ) + { + $masktype = gettype( $mask ); + switch ( $masktype ) { + case 'string': + break; + case 'array': + $maskflip = array(); + foreach ( $mask as $m ) { + if ( !is_string( $m ) ) { + $mask = NULL; + $masktype = 'NULL'; + break 2; + } + $maskflip[$m] = TRUE; + } + $mask = $maskflip; + break; + default: + $mask = NULL; + $masktype = 'NULL'; + } + + $collection = array(); + $this->stash[$this->nesting] = array(); + foreach ( $rows as $row ) { + if ( $mask !== NULL ) { + $meta = array(); + foreach( $row as $key => $value ) { + if ( $masktype === 'string' ) { + if ( strpos( $key, $mask ) === 0 ) { + unset( $row[$key] ); + $meta[$key] = $value; + } + } elseif ( $masktype === 'array' ) { + if ( isset( $mask[$key] ) ) { + unset( $row[$key] ); + $meta[$key] = $value; + } + } + } + } + + $id = $row['id']; + $this->stash[$this->nesting][$id] = $row; + $collection[$id] = $this->load( $type, $id ); + + if ( $mask !== NULL ) { + $collection[$id]->setMeta( 'data.bundle', $meta ); + } + } + $this->stash[$this->nesting] = NULL; + + return $collection; + } + + /** + * Counts the number of beans of type $type. + * This method accepts a second argument to modify the count-query. + * A third argument can be used to provide bindings for the SQL snippet. + * + * @param string $type type of bean we are looking for + * @param string $addSQL additional SQL snippet + * @param array $bindings parameters to bind to SQL + * + * @return integer + */ + public function count( $type, $addSQL = '', $bindings = array() ) + { + $type = AQueryWriter::camelsSnake( $type ); + if ( count( explode( '_', $type ) ) > 2 ) { + throw new RedException( 'Invalid type for count.' ); + } + + try { + $count = (int) $this->writer->queryRecordCount( $type, array(), $addSQL, $bindings ); + } catch ( SQLException $exception ) { + $this->handleException( $exception ); + $count = 0; + } + return $count; + } + + /** + * Removes a bean from the database. + * This function will remove the specified OODBBean + * Bean Object from the database. + * + * @param OODBBean|SimpleModel $bean bean you want to remove from database + * + * @return void + */ + public function trash( $bean ) + { + $this->oodb->signal( 'delete', $bean ); + foreach ( $bean as $property => $value ) { + if ( $value instanceof OODBBean ) { + unset( $bean->$property ); + } + if ( is_array( $value ) ) { + if ( strpos( $property, 'own' ) === 0 ) { + unset( $bean->$property ); + } elseif ( strpos( $property, 'shared' ) === 0 ) { + unset( $bean->$property ); + } + } + } + try { + $deleted = $this->writer->deleteRecord( $bean->getMeta( 'type' ), array( 'id' => array( $bean->id ) ), NULL ); + } catch ( SQLException $exception ) { + $this->handleException( $exception ); + } + $bean->id = 0; + $this->oodb->signal( 'after_delete', $bean ); + return isset($deleted) ? $deleted : 0; + } + + /** + * Checks whether the specified table already exists in the database. + * Not part of the Object Database interface! + * + * @deprecated Use AQueryWriter::typeExists() instead. + * + * @param string $table table name + * + * @return boolean + */ + public function tableExists( $table ) + { + return $this->writer->tableExists( $table ); + } + + /** + * Trash all beans of a given type. + * Wipes an entire type of bean. After this operation there + * will be no beans left of the specified type. + * This method will ignore exceptions caused by database + * tables that do not exist. + * + * @param string $type type of bean you wish to delete all instances of + * + * @return boolean + */ + public function wipe( $type ) + { + try { + $this->writer->wipe( $type ); + + return TRUE; + } catch ( SQLException $exception ) { + if ( !$this->writer->sqlStateIn( $exception->getSQLState(), array( QueryWriter::C_SQLSTATE_NO_SUCH_TABLE ), $exception->getDriverDetails() ) ) { + throw $exception; + } + + return FALSE; + } + } +} +} + +namespace RedBeanPHP\Repository { + +use RedBeanPHP\OODBBean as OODBBean; +use RedBeanPHP\QueryWriter as QueryWriter; +use RedBeanPHP\RedException as RedException; +use RedBeanPHP\BeanHelper as BeanHelper; +use RedBeanPHP\RedException\SQL as SQLException; +use RedBeanPHP\Repository as Repository; + +/** + * Fluid Repository. + * OODB manages two repositories, a fluid one that + * adjust the database schema on-the-fly to accomodate for + * new bean types (tables) and new properties (columns) and + * a frozen one for use in a production environment. OODB + * allows you to swap the repository instances using the freeze() + * method. + * + * @file RedBeanPHP/Repository/Fluid.php + * @author Gabor de Mooij and the RedBeanPHP community + * @license BSD/GPLv2 + * + * @copyright + * copyright (c) G.J.G.T. (Gabor) de Mooij and the RedBeanPHP Community + * This source file is subject to the BSD/GPLv2 License that is bundled + * with this source code in the file license.txt. + */ +class Fluid extends Repository +{ + /** + * Figures out the desired type given the cast string ID. + * Given a cast ID, this method will return the associated + * type (INT(10) or VARCHAR for instance). The returned type + * can be processed by the Query Writer to build the specified + * column for you in the database. The Cast ID is actually just + * a superset of the QueryWriter types. In addition to default + * Query Writer column types you can pass the following 'cast types': + * 'id' and 'string'. These will map to Query Writer specific + * column types (probably INT and VARCHAR). + * + * @param string $cast cast identifier + * + * @return integer + */ + private function getTypeFromCast( $cast ) + { + if ( $cast == 'string' ) { + $typeno = $this->writer->scanType( 'STRING' ); + } elseif ( $cast == 'id' ) { + $typeno = $this->writer->getTypeForID(); + } elseif ( isset( $this->writer->sqltype_typeno[$cast] ) ) { + $typeno = $this->writer->sqltype_typeno[$cast]; + } else { + throw new RedException( 'Invalid Cast' ); + } + + return $typeno; + } + + /** + * Orders the Query Writer to create a table if it does not exist already and + * adds a note in the build report about the creation. + * + * @param OODBBean $bean bean to update report of + * @param string $table table to check and create if not exists + * + * @return void + */ + private function createTableIfNotExists( OODBBean $bean, $table ) + { + //Does table exist? If not, create + if ( !$this->tableExists( $this->writer->esc( $table, TRUE ) ) ) { + $this->writer->createTable( $table ); + $bean->setMeta( 'buildreport.flags.created', TRUE ); + } + } + + /** + * Modifies the table to fit the bean data. + * Given a property and a value and the bean, this method will + * adjust the table structure to fit the requirements of the property and value. + * This may include adding a new column or widening an existing column to hold a larger + * or different kind of value. This method employs the writer to adjust the table + * structure in the database. Schema updates are recorded in meta properties of the bean. + * + * This method will also apply indexes, unique constraints and foreign keys. + * + * @param OODBBean $bean bean to get cast data from and store meta in + * @param string $property property to store + * @param mixed $value value to store + * + * @return void + */ + private function modifySchema( OODBBean $bean, $property, $value, &$columns = NULL ) + { + $doFKStuff = FALSE; + $table = $bean->getMeta( 'type' ); + if ($columns === NULL) { + $columns = $this->writer->getColumns( $table ); + } + $columnNoQ = $this->writer->esc( $property, TRUE ); + if ( !$this->oodb->isChilled( $bean->getMeta( 'type' ) ) ) { + if ( $bean->getMeta( "cast.$property", -1 ) !== -1 ) { //check for explicitly specified types + $cast = $bean->getMeta( "cast.$property" ); + $typeno = $this->getTypeFromCast( $cast ); + } else { + $cast = FALSE; + $typeno = $this->writer->scanType( $value, TRUE ); + } + if ( isset( $columns[$this->writer->esc( $property, TRUE )] ) ) { //Is this property represented in the table ? + if ( !$cast ) { //rescan without taking into account special types >80 + $typeno = $this->writer->scanType( $value, FALSE ); + } + $sqlt = $this->writer->code( $columns[$this->writer->esc( $property, TRUE )] ); + if ( $typeno > $sqlt ) { //no, we have to widen the database column type + $this->writer->widenColumn( $table, $property, $typeno ); + $bean->setMeta( 'buildreport.flags.widen', TRUE ); + $doFKStuff = TRUE; + } + } else { + $this->writer->addColumn( $table, $property, $typeno ); + $bean->setMeta( 'buildreport.flags.addcolumn', TRUE ); + $doFKStuff = TRUE; + } + if ($doFKStuff) { + if (strrpos($columnNoQ, '_id')===(strlen($columnNoQ)-3)) { + $destinationColumnNoQ = substr($columnNoQ, 0, strlen($columnNoQ)-3); + $indexName = "index_foreignkey_{$table}_{$destinationColumnNoQ}"; + $this->writer->addIndex($table, $indexName, $columnNoQ); + $typeof = $bean->getMeta("sys.typeof.{$destinationColumnNoQ}", $destinationColumnNoQ); + $isLink = $bean->getMeta( 'sys.buildcommand.unique', FALSE ); + //Make FK CASCADING if part of exclusive list (dependson=typeof) or if link bean + $isDep = ( $bean->moveMeta( 'sys.buildcommand.fkdependson' ) === $typeof || is_array( $isLink ) ); + $result = $this->writer->addFK( $table, $typeof, $columnNoQ, 'id', $isDep ); + //If this is a link bean and all unique columns have been added already, then apply unique constraint + if ( is_array( $isLink ) && !count( array_diff( $isLink, array_keys( $this->writer->getColumns( $table ) ) ) ) ) { + $this->writer->addUniqueConstraint( $table, $bean->moveMeta('sys.buildcommand.unique') ); + $bean->setMeta("sys.typeof.{$destinationColumnNoQ}", NULL); + } + } + } + } + } + + /** + * Part of the store() functionality. + * Handles all new additions after the bean has been saved. + * Stores addition bean in own-list, extracts the id and + * adds a foreign key. Also adds a constraint in case the type is + * in the dependent list. + * + * Note that this method raises a custom exception if the bean + * is not an instance of OODBBean. Therefore it does not use + * a type hint. This allows the user to take action in case + * invalid objects are passed in the list. + * + * @param OODBBean $bean bean to process + * @param array $ownAdditions list of addition beans in own-list + * + * @return void + */ + protected function processAdditions( $bean, $ownAdditions ) + { + $beanType = $bean->getMeta( 'type' ); + + foreach ( $ownAdditions as $addition ) { + if ( $addition instanceof OODBBean ) { + + $myFieldLink = $beanType . '_id'; + $alias = $bean->getMeta( 'sys.alias.' . $addition->getMeta( 'type' ) ); + if ( $alias ) $myFieldLink = $alias . '_id'; + + $addition->$myFieldLink = $bean->id; + $addition->setMeta( 'cast.' . $myFieldLink, 'id' ); + + if ($alias) { + $addition->setMeta( "sys.typeof.{$alias}", $beanType ); + } else { + $addition->setMeta( "sys.typeof.{$beanType}", $beanType ); + } + + $this->store( $addition ); + } else { + throw new RedException( 'Array may only contain OODBBeans' ); + } + } + } + + /** + * Stores a cleaned bean; i.e. only scalar values. This is the core of the store() + * method. When all lists and embedded beans (parent objects) have been processed and + * removed from the original bean the bean is passed to this method to be stored + * in the database. + * + * @param OODBBean $bean the clean bean + * + * @return void + */ + protected function storeBean( OODBBean $bean ) + { + if ( $bean->getMeta( 'changed' ) ) { + $this->check( $bean ); + $table = $bean->getMeta( 'type' ); + $this->createTableIfNotExists( $bean, $table ); + + $updateValues = array(); + + $partial = ( $this->partialBeans === TRUE || ( is_array( $this->partialBeans ) && in_array( $table, $this->partialBeans ) ) ); + if ( $partial ) { + $mask = $bean->getMeta( 'changelist' ); + $bean->setMeta( 'changelist', array() ); + } + + $columnCache = NULL; + foreach ( $bean as $property => $value ) { + if ( $partial && !in_array( $property, $mask ) ) continue; + if ( $property !== 'id' ) { + $this->modifySchema( $bean, $property, $value, $columnCache ); + } + if ( $property !== 'id' ) { + $updateValues[] = array( 'property' => $property, 'value' => $value ); + } + } + + $bean->id = $this->writer->updateRecord( $table, $updateValues, $bean->id ); + $bean->setMeta( 'changed', FALSE ); + } + $bean->setMeta( 'tainted', FALSE ); + } + + /** + * Exception handler. + * Fluid and Frozen mode have different ways of handling + * exceptions. Fluid mode (using the fluid repository) ignores + * exceptions caused by the following: + * + * - missing tables + * - missing column + * + * In these situations, the repository will behave as if + * no beans could be found. This is because in fluid mode + * it might happen to query a table or column that has not been + * created yet. In frozen mode, this is not supposed to happen + * and the corresponding exceptions will be thrown. + * + * @param \Exception $exception exception + * + * @return void + */ + protected function handleException( \Exception $exception ) + { + if ( !$this->writer->sqlStateIn( $exception->getSQLState(), + array( + QueryWriter::C_SQLSTATE_NO_SUCH_TABLE, + QueryWriter::C_SQLSTATE_NO_SUCH_COLUMN ), + $exception->getDriverDetails() ) + ) { + throw $exception; + } + } + + /** + * Loads a bean from the object database. + * It searches for a OODBBean Bean Object in the + * database. It does not matter how this bean has been stored. + * RedBean uses the primary key ID $id and the string $type + * to find the bean. The $type specifies what kind of bean you + * are looking for; this is the same type as used with the + * dispense() function. If RedBean finds the bean it will return + * the OODB Bean object; if it cannot find the bean + * RedBean will return a new bean of type $type and with + * primary key ID 0. In the latter case it acts basically the + * same as dispense(). + * + * Important note: + * If the bean cannot be found in the database a new bean of + * the specified type will be generated and returned. + * + * @param string $type type of bean you want to load + * @param integer $id ID of the bean you want to load + * + * @return OODBBean + */ + public function load( $type, $id ) + { + $rows = array(); + $bean = $this->dispense( $type ); + if ( isset( $this->stash[$this->nesting][$id] ) ) { + $row = $this->stash[$this->nesting][$id]; + } else { + try { + $rows = $this->writer->queryRecord( $type, array( 'id' => array( $id ) ) ); + } catch ( SQLException $exception ) { + if ( + $this->writer->sqlStateIn( + $exception->getSQLState(), + array( + QueryWriter::C_SQLSTATE_NO_SUCH_COLUMN, + QueryWriter::C_SQLSTATE_NO_SUCH_TABLE + ), + $exception->getDriverDetails() + ) + ) { + $rows = array(); + } else { + throw $exception; + } + } + if ( !count( $rows ) ) { + return $bean; + } + $row = array_pop( $rows ); + } + $bean->importRow( $row ); + $this->nesting++; + $this->oodb->signal( 'open', $bean ); + $this->nesting--; + + return $bean->setMeta( 'tainted', FALSE ); + } +} +} + +namespace RedBeanPHP\Repository { + +use RedBeanPHP\OODBBean as OODBBean; +use RedBeanPHP\QueryWriter as QueryWriter; +use RedBeanPHP\RedException as RedException; +use RedBeanPHP\BeanHelper as BeanHelper; +use RedBeanPHP\RedException\SQL as SQLException; +use RedBeanPHP\Repository as Repository; + +/** + * Frozen Repository. + * OODB manages two repositories, a fluid one that + * adjust the database schema on-the-fly to accomodate for + * new bean types (tables) and new properties (columns) and + * a frozen one for use in a production environment. OODB + * allows you to swap the repository instances using the freeze() + * method. + * + * @file RedBeanPHP/Repository/Frozen.php + * @author Gabor de Mooij and the RedBeanPHP community + * @license BSD/GPLv2 + * + * @copyright + * copyright (c) G.J.G.T. (Gabor) de Mooij and the RedBeanPHP Community + * This source file is subject to the BSD/GPLv2 License that is bundled + * with this source code in the file license.txt. + */ +class Frozen extends Repository +{ + /** + * Exception handler. + * Fluid and Frozen mode have different ways of handling + * exceptions. Fluid mode (using the fluid repository) ignores + * exceptions caused by the following: + * + * - missing tables + * - missing column + * + * In these situations, the repository will behave as if + * no beans could be found. This is because in fluid mode + * it might happen to query a table or column that has not been + * created yet. In frozen mode, this is not supposed to happen + * and the corresponding exceptions will be thrown. + * + * @param \Exception $exception exception + * + * @return void + */ + protected function handleException( \Exception $exception ) + { + throw $exception; + } + + /** + * Stores a cleaned bean; i.e. only scalar values. This is the core of the store() + * method. When all lists and embedded beans (parent objects) have been processed and + * removed from the original bean the bean is passed to this method to be stored + * in the database. + * + * @param OODBBean $bean the clean bean + * + * @return void + */ + protected function storeBean( OODBBean $bean ) + { + if ( $bean->getMeta( 'changed' ) ) { + + list( $properties, $table ) = $bean->getPropertiesAndType(); + $id = $properties['id']; + unset($properties['id']); + $updateValues = array(); + $k1 = 'property'; + $k2 = 'value'; + + $partial = ( $this->partialBeans === TRUE || ( is_array( $this->partialBeans ) && in_array( $table, $this->partialBeans ) ) ); + if ( $partial ) { + $mask = $bean->getMeta( 'changelist' ); + $bean->setMeta( 'changelist', array() ); + } + + foreach( $properties as $key => $value ) { + if ( $partial && !in_array( $key, $mask ) ) continue; + $updateValues[] = array( $k1 => $key, $k2 => $value ); + } + $bean->id = $this->writer->updateRecord( $table, $updateValues, $id ); + $bean->setMeta( 'changed', FALSE ); + } + $bean->setMeta( 'tainted', FALSE ); + } + + /** + * Part of the store() functionality. + * Handles all new additions after the bean has been saved. + * Stores addition bean in own-list, extracts the id and + * adds a foreign key. Also adds a constraint in case the type is + * in the dependent list. + * + * Note that this method raises a custom exception if the bean + * is not an instance of OODBBean. Therefore it does not use + * a type hint. This allows the user to take action in case + * invalid objects are passed in the list. + * + * @param OODBBean $bean bean to process + * @param array $ownAdditions list of addition beans in own-list + * + * @return void + * @throws RedException + */ + protected function processAdditions( $bean, $ownAdditions ) + { + $beanType = $bean->getMeta( 'type' ); + + $cachedIndex = array(); + foreach ( $ownAdditions as $addition ) { + if ( $addition instanceof OODBBean ) { + + $myFieldLink = $beanType . '_id'; + $alias = $bean->getMeta( 'sys.alias.' . $addition->getMeta( 'type' ) ); + if ( $alias ) $myFieldLink = $alias . '_id'; + + $addition->$myFieldLink = $bean->id; + $addition->setMeta( 'cast.' . $myFieldLink, 'id' ); + $this->store( $addition ); + + } else { + throw new RedException( 'Array may only contain OODBBeans' ); + } + } + } + + /** + * Loads a bean from the object database. + * It searches for a OODBBean Bean Object in the + * database. It does not matter how this bean has been stored. + * RedBean uses the primary key ID $id and the string $type + * to find the bean. The $type specifies what kind of bean you + * are looking for; this is the same type as used with the + * dispense() function. If RedBean finds the bean it will return + * the OODB Bean object; if it cannot find the bean + * RedBean will return a new bean of type $type and with + * primary key ID 0. In the latter case it acts basically the + * same as dispense(). + * + * Important note: + * If the bean cannot be found in the database a new bean of + * the specified type will be generated and returned. + * + * @param string $type type of bean you want to load + * @param integer $id ID of the bean you want to load + * + * @return OODBBean + * @throws SQLException + */ + public function load( $type, $id ) + { + $rows = array(); + $bean = $this->dispense( $type ); + if ( isset( $this->stash[$this->nesting][$id] ) ) { + $row = $this->stash[$this->nesting][$id]; + } else { + $rows = $this->writer->queryRecord( $type, array( 'id' => array( $id ) ) ); + if ( !count( $rows ) ) { + return $bean; + } + $row = array_pop( $rows ); + } + $bean->importRow( $row ); + $this->nesting++; + $this->oodb->signal( 'open', $bean ); + $this->nesting--; + + return $bean->setMeta( 'tainted', FALSE ); + } +} +} + +namespace RedBeanPHP { + +use RedBeanPHP\Adapter\DBAdapter as DBAdapter; +use RedBeanPHP\QueryWriter as QueryWriter; +use RedBeanPHP\BeanHelper as BeanHelper; +use RedBeanPHP\QueryWriter\AQueryWriter as AQueryWriter; +use RedBeanPHP\Repository as Repository; +use RedBeanPHP\Repository\Fluid as FluidRepo; +use RedBeanPHP\Repository\Frozen as FrozenRepo; + +/** + * RedBean Object Oriented DataBase. + * + * The RedBean OODB Class is the main class of RedBeanPHP. + * It takes OODBBean objects and stores them to and loads them from the + * database as well as providing other CRUD functions. This class acts as a + * object database. + * + * @file RedBeanPHP/OODB.php + * @author Gabor de Mooij and the RedBeanPHP community + * @license BSD/GPLv2 + * + * @copyright + * copyright (c) G.J.G.T. (Gabor) de Mooij and the RedBeanPHP Community + * This source file is subject to the BSD/GPLv2 License that is bundled + * with this source code in the file license.txt. + */ +class OODB extends Observable +{ + /** + * @var array + */ + private static $sqlFilters = array(); + + /** + * @var array + */ + protected $chillList = array(); + + /** + * @var array + */ + protected $stash = NULL; + + /* + * @var integer + */ + protected $nesting = 0; + + /** + * @var DBAdapter + */ + protected $writer; + + /** + * @var boolean + */ + protected $isFrozen = FALSE; + + /** + * @var FacadeBeanHelper + */ + protected $beanhelper = NULL; + + /** + * @var AssociationManager + */ + protected $assocManager = NULL; + + /** + * @var Repository + */ + protected $repository = NULL; + + /** + * @var FrozenRepo + */ + protected $frozenRepository = NULL; + + /** + * @var FluidRepo + */ + protected $fluidRepository = NULL; + + /** + * @var boolean + */ + protected static $autoClearHistoryAfterStore = FALSE; + + /** + * If set to TRUE, this method will call clearHistory every time + * the bean gets stored. + * + * @param boolean $autoClear auto clear option + * + * @return void + */ + public static function autoClearHistoryAfterStore( $autoClear = TRUE ) + { + self::$autoClearHistoryAfterStore = (boolean) $autoClear; + } + + /** + * Unboxes a bean from a FUSE model if needed and checks whether the bean is + * an instance of OODBBean. + * + * @param OODBBean $bean bean you wish to unbox + * + * @return OODBBean + */ + protected function unboxIfNeeded( $bean ) + { + if ( $bean instanceof SimpleModel ) { + $bean = $bean->unbox(); + } + if ( !( $bean instanceof OODBBean ) ) { + throw new RedException( 'OODB Store requires a bean, got: ' . gettype( $bean ) ); + } + + return $bean; + } + + /** + * Constructor, requires a query writer. + * Most of the time, you do not need to use this constructor, + * since the facade takes care of constructing and wiring the + * RedBeanPHP core objects. However if you would like to + * assemble an OODB instance yourself, this is how it works: + * + * Usage: + * + * + * $database = new RPDO( $dsn, $user, $pass ); + * $adapter = new DBAdapter( $database ); + * $writer = new PostgresWriter( $adapter ); + * $oodb = new OODB( $writer, FALSE ); + * $bean = $oodb->dispense( 'bean' ); + * $bean->name = 'coffeeBean'; + * $id = $oodb->store( $bean ); + * $bean = $oodb->load( 'bean', $id ); + * + * + * The example above creates the 3 RedBeanPHP core objects: + * the Adapter, the Query Writer and the OODB instance and + * wires them together. The example also demonstrates some of + * the methods that can be used with OODB, as you see, they + * closely resemble their facade counterparts. + * + * The wiring process: create an RPDO instance using your database + * connection parameters. Create a database adapter from the RPDO + * object and pass that to the constructor of the writer. Next, + * create an OODB instance from the writer. Now you have an OODB + * object. + * + * @param QueryWriter $writer writer + * @param array|boolean $frozen mode of operation: TRUE (frozen), FALSE (default, fluid) or ARRAY (chilled) + */ + public function __construct( QueryWriter $writer, $frozen = FALSE ) + { + if ( $writer instanceof QueryWriter ) { + $this->writer = $writer; + } + + $this->freeze( $frozen ); + } + + /** + * Toggles fluid or frozen mode. In fluid mode the database + * structure is adjusted to accomodate your objects. In frozen mode + * this is not the case. + * + * You can also pass an array containing a selection of frozen types. + * Let's call this chill mode, it's just like fluid mode except that + * certain types (i.e. tables) aren't touched. + * + * @param boolean|array $toggle TRUE if you want to use OODB instance in frozen mode + * + * @return void + */ + public function freeze( $toggle ) + { + if ( is_array( $toggle ) ) { + $this->chillList = $toggle; + $this->isFrozen = FALSE; + } else { + $this->isFrozen = (boolean) $toggle; + } + + if ( $this->isFrozen ) { + if ( !$this->frozenRepository ) { + $this->frozenRepository = new FrozenRepo( $this, $this->writer ); + } + + $this->repository = $this->frozenRepository; + + } else { + if ( !$this->fluidRepository ) { + $this->fluidRepository = new FluidRepo( $this, $this->writer ); + } + + $this->repository = $this->fluidRepository; + } + + if ( count( self::$sqlFilters ) ) { + AQueryWriter::setSQLFilters( self::$sqlFilters, ( !$this->isFrozen ) ); + } + + } + + /** + * Returns the current mode of operation of RedBean. + * In fluid mode the database + * structure is adjusted to accomodate your objects. + * In frozen mode + * this is not the case. + * + * @return boolean + */ + public function isFrozen() + { + return (bool) $this->isFrozen; + } + + /** + * Determines whether a type is in the chill list. + * If a type is 'chilled' it's frozen, so its schema cannot be + * changed anymore. However other bean types may still be modified. + * This method is a convenience method for other objects to check if + * the schema of a certain type is locked for modification. + * + * @param string $type the type you wish to check + * + * @return boolean + */ + public function isChilled( $type ) + { + return (boolean) ( in_array( $type, $this->chillList ) ); + } + + /** + * Dispenses a new bean (a OODBBean Bean Object) + * of the specified type. Always + * use this function to get an empty bean object. Never + * instantiate a OODBBean yourself because it needs + * to be configured before you can use it with RedBean. This + * function applies the appropriate initialization / + * configuration for you. + * + * @param string $type type of bean you want to dispense + * @param string $number number of beans you would like to get + * @param boolean $alwaysReturnArray if TRUE always returns the result as an array + * + * @return OODBBean + */ + public function dispense( $type, $number = 1, $alwaysReturnArray = FALSE ) + { + if ( $number < 1 ) { + if ( $alwaysReturnArray ) return array(); + return NULL; + } + + return $this->repository->dispense( $type, $number, $alwaysReturnArray ); + } + + /** + * Sets bean helper to be given to beans. + * Bean helpers assist beans in getting a reference to a toolbox. + * + * @param BeanHelper $beanhelper helper + * + * @return void + */ + public function setBeanHelper( BeanHelper $beanhelper ) + { + $this->beanhelper = $beanhelper; + } + + /** + * Returns the current bean helper. + * Bean helpers assist beans in getting a reference to a toolbox. + * + * @return BeanHelper + */ + public function getBeanHelper() + { + return $this->beanhelper; + } + + /** + * Checks whether a OODBBean bean is valid. + * If the type is not valid or the ID is not valid it will + * throw an exception: Security. + * + * @param OODBBean $bean the bean that needs to be checked + * + * @return void + */ + public function check( OODBBean $bean ) + { + $this->repository->check( $bean ); + } + + /** + * Searches the database for a bean that matches conditions $conditions and sql $addSQL + * and returns an array containing all the beans that have been found. + * + * Conditions need to take form: + * + * + * array( + * 'PROPERTY' => array( POSSIBLE VALUES... 'John', 'Steve' ) + * 'PROPERTY' => array( POSSIBLE VALUES... ) + * ); + * + * + * All conditions are glued together using the AND-operator, while all value lists + * are glued using IN-operators thus acting as OR-conditions. + * + * Note that you can use property names; the columns will be extracted using the + * appropriate bean formatter. + * + * @param string $type type of beans you are looking for + * @param array $conditions list of conditions + * @param string $sql SQL to be used in query + * @param array $bindings a list of values to bind to query parameters + * + * @return array + */ + public function find( $type, $conditions = array(), $sql = NULL, $bindings = array() ) + { + return $this->repository->find( $type, $conditions, $sql, $bindings ); + } + + /** + * Same as find() but returns a BeanCollection. + * + * @param string $type type of beans you are looking for + * @param string $sql SQL to be used in query + * @param array $bindings a list of values to bind to query parameters + * + * @return BeanCollection + */ + public function findCollection( $type, $sql = NULL, $bindings = array() ) + { + return $this->repository->findCollection( $type, $sql, $bindings ); + } + + /** + * Checks whether the specified table already exists in the database. + * Not part of the Object Database interface! + * + * @deprecated Use AQueryWriter::typeExists() instead. + * + * @param string $table table name + * + * @return boolean + */ + public function tableExists( $table ) + { + return $this->repository->tableExists( $table ); + } + + /** + * Stores a bean in the database. This method takes a + * OODBBean Bean Object $bean and stores it + * in the database. If the database schema is not compatible + * with this bean and RedBean runs in fluid mode the schema + * will be altered to store the bean correctly. + * If the database schema is not compatible with this bean and + * RedBean runs in frozen mode it will throw an exception. + * This function returns the primary key ID of the inserted + * bean. + * + * The return value is an integer if possible. If it is not possible to + * represent the value as an integer a string will be returned. We use + * explicit casts instead of functions to preserve performance + * (0.13 vs 0.28 for 10000 iterations on Core i3). + * + * @param OODBBean|SimpleModel $bean bean to store + * + * @return integer|string + */ + public function store( $bean ) + { + $bean = $this->unboxIfNeeded( $bean ); + $id = $this->repository->store( $bean ); + if ( self::$autoClearHistoryAfterStore ) { + $bean->clearHistory(); + } + return $id; + } + + /** + * Loads a bean from the object database. + * It searches for a OODBBean Bean Object in the + * database. It does not matter how this bean has been stored. + * RedBean uses the primary key ID $id and the string $type + * to find the bean. The $type specifies what kind of bean you + * are looking for; this is the same type as used with the + * dispense() function. If RedBean finds the bean it will return + * the OODB Bean object; if it cannot find the bean + * RedBean will return a new bean of type $type and with + * primary key ID 0. In the latter case it acts basically the + * same as dispense(). + * + * Important note: + * If the bean cannot be found in the database a new bean of + * the specified type will be generated and returned. + * + * @param string $type type of bean you want to load + * @param integer $id ID of the bean you want to load + * + * @return OODBBean + */ + public function load( $type, $id ) + { + return $this->repository->load( $type, $id ); + } + + /** + * Removes a bean from the database. + * This function will remove the specified OODBBean + * Bean Object from the database. + * + * @param OODBBean|SimpleModel $bean bean you want to remove from database + * + * @return void + */ + public function trash( $bean ) + { + $bean = $this->unboxIfNeeded( $bean ); + return $this->repository->trash( $bean ); + } + + /** + * Returns an array of beans. Pass a type and a series of ids and + * this method will bring you the corresponding beans. + * + * important note: Because this method loads beans using the load() + * function (but faster) it will return empty beans with ID 0 for + * every bean that could not be located. The resulting beans will have the + * passed IDs as their keys. + * + * @param string $type type of beans + * @param array $ids ids to load + * + * @return array + */ + public function batch( $type, $ids ) + { + return $this->repository->batch( $type, $ids ); + } + + /** + * This is a convenience method; it converts database rows + * (arrays) into beans. Given a type and a set of rows this method + * will return an array of beans of the specified type loaded with + * the data fields provided by the result set from the database. + * + * @param string $type type of beans you would like to have + * @param array $rows rows from the database result + * @param string $mask mask to apply for meta data + * + * @return array + */ + public function convertToBeans( $type, $rows, $mask = NULL ) + { + return $this->repository->convertToBeans( $type, $rows, $mask ); + } + + /** + * Counts the number of beans of type $type. + * This method accepts a second argument to modify the count-query. + * A third argument can be used to provide bindings for the SQL snippet. + * + * @param string $type type of bean we are looking for + * @param string $addSQL additional SQL snippet + * @param array $bindings parameters to bind to SQL + * + * @return integer + */ + public function count( $type, $addSQL = '', $bindings = array() ) + { + return $this->repository->count( $type, $addSQL, $bindings ); + } + + /** + * Trash all beans of a given type. Wipes an entire type of bean. + * + * @param string $type type of bean you wish to delete all instances of + * + * @return boolean + */ + public function wipe( $type ) + { + return $this->repository->wipe( $type ); + } + + /** + * Returns an Association Manager for use with OODB. + * A simple getter function to obtain a reference to the association manager used for + * storage and more. + * + * @return AssociationManager + */ + public function getAssociationManager() + { + if ( !isset( $this->assocManager ) ) { + throw new RedException( 'No association manager available.' ); + } + + return $this->assocManager; + } + + /** + * Sets the association manager instance to be used by this OODB. + * A simple setter function to set the association manager to be used for storage and + * more. + * + * @param AssociationManager $assocManager sets the association manager to be used + * + * @return void + */ + public function setAssociationManager( AssociationManager $assocManager ) + { + $this->assocManager = $assocManager; + } + + /** + * Returns the currently used repository instance. + * For testing purposes only. + * + * @return Repository + */ + public function getCurrentRepository() + { + return $this->repository; + } + + /** + * Clears all function bindings. + * + * @return void + */ + public function clearAllFuncBindings() + { + self::$sqlFilters = array(); + AQueryWriter::setSQLFilters( self::$sqlFilters, FALSE ); + } + + /** + * Binds an SQL function to a column. + * This method can be used to setup a decode/encode scheme or + * perform UUID insertion. This method is especially useful for handling + * MySQL spatial columns, because they need to be processed first using + * the asText/GeomFromText functions. + * + * @param string $mode mode to set function for, i.e. read or write + * @param string $field field (table.column) to bind SQL function to + * @param string $function SQL function to bind to field + * @param boolean $isTemplate TRUE if $function is an SQL string, FALSE for just a function name + * + * @return void + */ + public function bindFunc( $mode, $field, $function, $isTemplate = FALSE ) + { + list( $type, $property ) = explode( '.', $field ); + $mode = ($mode === 'write') ? QueryWriter::C_SQLFILTER_WRITE : QueryWriter::C_SQLFILTER_READ; + + if ( !isset( self::$sqlFilters[$mode] ) ) self::$sqlFilters[$mode] = array(); + if ( !isset( self::$sqlFilters[$mode][$type] ) ) self::$sqlFilters[$mode][$type] = array(); + + if ( is_null( $function ) ) { + unset( self::$sqlFilters[$mode][$type][$property] ); + } else { + if ($mode === QueryWriter::C_SQLFILTER_WRITE) { + if ($isTemplate) { + $code = sprintf( $function, '?' ); + } else { + $code = "{$function}(?)"; + } + self::$sqlFilters[$mode][$type][$property] = $code; + } else { + if ($isTemplate) { + $code = sprintf( $function, $field ); + } else { + $code = "{$function}({$field})"; + } + self::$sqlFilters[$mode][$type][$property] = $code; + } + } + AQueryWriter::setSQLFilters( self::$sqlFilters, ( !$this->isFrozen ) ); + } +} +} + +namespace RedBeanPHP { + +use RedBeanPHP\OODB as OODB; +use RedBeanPHP\QueryWriter as QueryWriter; +use RedBeanPHP\Adapter\DBAdapter as DBAdapter; +use RedBeanPHP\Adapter as Adapter; + +/** + * ToolBox. + * + * The toolbox is an integral part of RedBeanPHP providing the basic + * architectural building blocks to manager objects, helpers and additional tools + * like plugins. A toolbox contains the three core components of RedBeanPHP: + * the adapter, the query writer and the core functionality of RedBeanPHP in + * OODB. + * + * @file RedBeanPHP/ToolBox.php + * @author Gabor de Mooij and the RedBeanPHP community + * @license BSD/GPLv2 + * + * @copyright + * copyright (c) G.J.G.T. (Gabor) de Mooij and the RedBeanPHP Community. + * This source file is subject to the BSD/GPLv2 License that is bundled + * with this source code in the file license.txt. + */ +class ToolBox +{ + /** + * @var OODB + */ + protected $oodb; + + /** + * @var QueryWriter + */ + protected $writer; + + /** + * @var DBAdapter + */ + protected $adapter; + + /** + * Constructor. + * The toolbox is an integral part of RedBeanPHP providing the basic + * architectural building blocks to manager objects, helpers and additional tools + * like plugins. A toolbox contains the three core components of RedBeanPHP: + * the adapter, the query writer and the core functionality of RedBeanPHP in + * OODB. + * + * Usage: + * + * + * $toolbox = new ToolBox( $oodb, $adapter, $writer ); + * $plugin = new MyPlugin( $toolbox ); + * + * + * The example above illustrates how the toolbox is used. + * The core objects are passed to the ToolBox constructor to + * assemble a toolbox instance. The toolbox is then passed to + * the plugin, helper or manager object. Instances of + * TagManager, AssociationManager and so on are examples of + * this, they all require a toolbox. The toolbox can also + * be obtained from the facade using: R::getToolBox(); + * + * @param OODB $oodb Object Database, OODB + * @param DBAdapter $adapter Database Adapter + * @param QueryWriter $writer Query Writer + */ + public function __construct( OODB $oodb, Adapter $adapter, QueryWriter $writer ) + { + $this->oodb = $oodb; + $this->adapter = $adapter; + $this->writer = $writer; + return $this; + } + + /** + * Returns the query writer in this toolbox. + * The Query Writer is responsible for building the queries for a + * specific database and executing them through the adapter. + * + * Usage: + * + * + * $toolbox = R::getToolBox(); + * $redbean = $toolbox->getRedBean(); + * $adapter = $toolbox->getDatabaseAdapter(); + * $writer = $toolbox->getWriter(); + * + * + * The example above illustrates how to obtain the core objects + * from a toolbox instance. If you are working with the R-object + * only, the following shortcuts exist as well: + * + * - R::getRedBean() + * - R::getDatabaseAdapter() + * - R::getWriter() + * + * @return QueryWriter + */ + public function getWriter() + { + return $this->writer; + } + + /** + * Returns the OODB instance in this toolbox. + * OODB is responsible for creating, storing, retrieving and deleting + * single beans. Other components rely + * on OODB for their basic functionality. + * + * Usage: + * + * + * $toolbox = R::getToolBox(); + * $redbean = $toolbox->getRedBean(); + * $adapter = $toolbox->getDatabaseAdapter(); + * $writer = $toolbox->getWriter(); + * + * + * The example above illustrates how to obtain the core objects + * from a toolbox instance. If you are working with the R-object + * only, the following shortcuts exist as well: + * + * - R::getRedBean() + * - R::getDatabaseAdapter() + * - R::getWriter() + * + * @return OODB + */ + public function getRedBean() + { + return $this->oodb; + } + + /** + * Returns the database adapter in this toolbox. + * The adapter is responsible for executing the query and binding the values. + * The adapter also takes care of transaction handling. + * + * Usage: + * + * + * $toolbox = R::getToolBox(); + * $redbean = $toolbox->getRedBean(); + * $adapter = $toolbox->getDatabaseAdapter(); + * $writer = $toolbox->getWriter(); + * + * + * The example above illustrates how to obtain the core objects + * from a toolbox instance. If you are working with the R-object + * only, the following shortcuts exist as well: + * + * - R::getRedBean() + * - R::getDatabaseAdapter() + * - R::getWriter() + * + * @return DBAdapter + */ + public function getDatabaseAdapter() + { + return $this->adapter; + } +} +} + +namespace RedBeanPHP { + + +/** + * RedBeanPHP Finder. + * Service class to find beans. For the most part this class + * offers user friendly utility methods for interacting with the + * OODB::find() method, which is rather complex. This class can be + * used to find beans using plain old SQL queries. + * + * @file RedBeanPHP/Finder.php + * @author Gabor de Mooij and the RedBeanPHP Community + * @license BSD/GPLv2 + * + * @copyright + * copyright (c) G.J.G.T. (Gabor) de Mooij and the RedBeanPHP Community + * This source file is subject to the BSD/GPLv2 License that is bundled + * with this source code in the file license.txt. + */ +class Finder +{ + /** + * @var ToolBox + */ + protected $toolbox; + + /** + * @var OODB + */ + protected $redbean; + + /** + * Constructor. + * The Finder requires a toolbox. + * + * @param ToolBox $toolbox + */ + public function __construct( ToolBox $toolbox ) + { + $this->toolbox = $toolbox; + $this->redbean = $toolbox->getRedBean(); + } + + /** + * A custom record-to-bean mapping function for findMulti. + * + * Usage: + * + * + * $collection = R::findMulti( 'shop,product,price', + * 'SELECT shop.*, product.*, price.* FROM shop + * LEFT JOIN product ON product.shop_id = shop.id + * LEFT JOIN price ON price.product_id = product.id', [], [ + * Finder::map( 'shop', 'product' ), + * Finder::map( 'product', 'price' ), + * ]); + * + * + * @param string $parentName name of the parent bean + * @param string $childName name of the child bean + * + * @return array + */ + public static function map($parentName,$childName) { + return array( + 'a' => $parentName, + 'b' => $childName, + 'matcher' => function( $parent, $child ) use ( $parentName, $childName ) { + $propertyName = 'own' . ucfirst( $childName ); + if (!isset($parent[$propertyName])) { + $parent->noLoad()->{$propertyName} = array(); + } + $property = "{$parentName}ID"; + return ( $child->$property == $parent->id ); + }, + 'do' => function( $parent, $child ) use ( $childName ) { + $list = 'own'.ucfirst( $childName ).'List'; + $parent->noLoad()->{$list}[$child->id] = $child; + } + ); + } + + /** + * A custom record-to-bean mapping function for findMulti. + * + * Usage: + * + * + * $collection = R::findMulti( 'book,book_tag,tag', + * 'SELECT book.*, book_tag.*, tag.* FROM book + * LEFT JOIN book_tag ON book_tag.book_id = book.id + * LEFT JOIN tag ON book_tag.tag_id = tag.id', [], [ + * Finder::nmMap( 'book', 'tag' ), + * ]); + * + * + * @param string $parentName name of the parent bean + * @param string $childName name of the child bean + * + * @return array + */ + public static function nmMap( $parentName, $childName ) + { + $types = array($parentName, $childName); + sort( $types ); + $link = implode( '_', $types ); + return array( + 'a' => $parentName, + 'b' => $childName, + 'matcher' => function( $parent, $child, $beans ) use ( $parentName, $childName, $link ) { + $propertyName = 'shared' . ucfirst( $childName ); + if (!isset($parent[$propertyName])) { + $parent->noLoad()->{$propertyName} = array(); + } + foreach( $beans[$link] as $linkBean ) { + if ( $linkBean["{$parentName}ID"] == $parent->id && $linkBean["{$childName}ID"] == $child->id ) { + return true; + } + } + }, + 'do' => function( $parent, $child ) use ( $childName ) { + $list = 'shared'.ucfirst( $childName ).'List'; + $parent->noLoad()->{$list}[$child->id] = $child; + } + ); + } + + /** + * Finder::onMap() -> One-to-N mapping. + * A custom record-to-bean mapping function for findMulti. + * Opposite of Finder::map(). Maps child beans to parents. + * + * Usage: + * + * + * $collection = R::findMulti( 'shop,product', + * 'SELECT shop.*, product.* FROM shop + * LEFT JOIN product ON product.shop_id = shop.id', + * [], [ + * Finder::onmap( 'product', 'shop' ), + * ]); + * + * + * Can also be used for instance to attach related beans + * in one-go to save some queries: + * + * Given $users that have a country_id: + * + * + * $all = R::findMulti('country', + * R::genSlots( $users, + * 'SELECT country.* FROM country WHERE id IN ( %s )' ), + * array_column( $users, 'country_id' ), + * [Finder::onmap('country', $users)] + * ); + * + * + * For your convenience, an even shorter notation has been added: + * + * $countries = R::loadJoined( $users, 'country' ); + * + * @param string $parentName name of the parent bean + * @param string|array $childName name of the child bean + * + * @return array + */ + public static function onMap($parentName,$childNameOrBeans) { + return array( + 'a' => $parentName, + 'b' => $childNameOrBeans, + 'matcher' => array( $parentName, "{$parentName}_id" ), + 'do' => 'match' + ); + } + + /** + * Finds a bean using a type and a where clause (SQL). + * As with most Query tools in RedBean you can provide values to + * be inserted in the SQL statement by populating the value + * array parameter; you can either use the question mark notation + * or the slot-notation (:keyname). + * + * @param string $type type the type of bean you are looking for + * @param string $sql sql SQL query to find the desired bean, starting right after WHERE clause + * @param array $bindings values array of values to be bound to parameters in query + * + * @return array + */ + public function find( $type, $sql = NULL, $bindings = array() ) + { + if ( !is_array( $bindings ) ) { + throw new RedException( + 'Expected array, ' . gettype( $bindings ) . ' given.' + ); + } + + return $this->redbean->find( $type, array(), $sql, $bindings ); + } + + /** + * Like find() but also exports the beans as an array. + * This method will perform a find-operation. For every bean + * in the result collection this method will call the export() method. + * This method returns an array containing the array representations + * of every bean in the result set. + * + * @see Finder::find + * + * @param string $type type the type of bean you are looking for + * @param string $sql sql SQL query to find the desired bean, starting right after WHERE clause + * @param array $bindings values array of values to be bound to parameters in query + * + * @return array + */ + public function findAndExport( $type, $sql = NULL, $bindings = array() ) + { + $arr = array(); + foreach ( $this->find( $type, $sql, $bindings ) as $key => $item ) { + $arr[] = $item->export(); + } + + return $arr; + } + + /** + * Like find() but returns just one bean instead of an array of beans. + * This method will return only the first bean of the array. + * If no beans are found, this method will return NULL. + * + * @see Finder::find + * + * @param string $type type the type of bean you are looking for + * @param string $sql sql SQL query to find the desired bean, starting right after WHERE clause + * @param array $bindings values array of values to be bound to parameters in query + * + * @return OODBBean|NULL + */ + public function findOne( $type, $sql = NULL, $bindings = array() ) + { + $sql = $this->toolbox->getWriter()->glueLimitOne( $sql ); + + $items = $this->find( $type, $sql, $bindings ); + + if ( empty($items) ) { + return NULL; + } + + return reset( $items ); + } + + /** + * Like find() but returns the last bean of the result array. + * Opposite of Finder::findLast(). + * If no beans are found, this method will return NULL. + * + * @see Finder::find + * + * @param string $type the type of bean you are looking for + * @param string $sql SQL query to find the desired bean, starting right after WHERE clause + * @param array $bindings values array of values to be bound to parameters in query + * + * @return OODBBean|NULL + */ + public function findLast( $type, $sql = NULL, $bindings = array() ) + { + $items = $this->find( $type, $sql, $bindings ); + + if ( empty($items) ) { + return NULL; + } + + return end( $items ); + } + + /** + * Tries to find beans of a certain type, + * if no beans are found, it dispenses a bean of that type. + * Note that this function always returns an array. + * + * @see Finder::find + * + * @param string $type the type of bean you are looking for + * @param string $sql SQL query to find the desired bean, starting right after WHERE clause + * @param array $bindings values array of values to be bound to parameters in query + * + * @return array + */ + public function findOrDispense( $type, $sql = NULL, $bindings = array() ) + { + $foundBeans = $this->find( $type, $sql, $bindings ); + + if ( empty( $foundBeans ) ) { + return array( $this->redbean->dispense( $type ) ); + } else { + return $foundBeans; + } + } + + /** + * Finds a BeanCollection using the repository. + * A bean collection can be used to retrieve one bean at a time using + * cursors - this is useful for processing large datasets. A bean collection + * will not load all beans into memory all at once, just one at a time. + * + * @param string $type the type of bean you are looking for + * @param string $sql SQL query to find the desired bean, starting right after WHERE clause + * @param array $bindings values array of values to be bound to parameters in query + * + * @return BeanCollection + */ + public function findCollection( $type, $sql, $bindings = array() ) + { + return $this->redbean->findCollection( $type, $sql, $bindings ); + } + + /** + * Finds or creates a bean. + * Tries to find a bean with certain properties specified in the second + * parameter ($like). If the bean is found, it will be returned. + * If multiple beans are found, only the first will be returned. + * If no beans match the criteria, a new bean will be dispensed, + * the criteria will be imported as properties and this new bean + * will be stored and returned. + * + * Format of criteria set: property => value + * The criteria set also supports OR-conditions: property => array( value1, orValue2 ) + * + * @param string $type type of bean to search for + * @param array $like criteria set describing bean to search for + * @param boolean $hasBeenCreated set to TRUE if bean has been created + * + * @return OODBBean + */ + public function findOrCreate( $type, $like = array(), $sql = '', &$hasBeenCreated = false ) + { + $sql = $this->toolbox->getWriter()->glueLimitOne( $sql ); + $beans = $this->findLike( $type, $like, $sql ); + if ( count( $beans ) ) { + $bean = reset( $beans ); + $hasBeenCreated = false; + return $bean; + } + + $bean = $this->redbean->dispense( $type ); + $bean->import( $like ); + $this->redbean->store( $bean ); + $hasBeenCreated = true; + return $bean; + } + + /** + * Finds beans by its type and a certain criteria set. + * + * Format of criteria set: property => value + * The criteria set also supports OR-conditions: property => array( value1, orValue2 ) + * + * If the additional SQL is a condition, this condition will be glued to the rest + * of the query using an AND operator. Note that this is as far as this method + * can go, there is no way to glue additional SQL using an OR-condition. + * This method provides access to an underlying mechanism in the RedBeanPHP architecture + * to find beans using criteria sets. However, please do not use this method + * for complex queries, use plain SQL instead ( the regular find method ) as it is + * more suitable for the job. This method is + * meant for basic search-by-example operations. + * + * @param string $type type of bean to search for + * @param array $conditions criteria set describing the bean to search for + * @param string $sql additional SQL (for sorting) + * @param array $bindings bindings + * + * @return array + */ + public function findLike( $type, $conditions = array(), $sql = '', $bindings = array() ) + { + return $this->redbean->find( $type, $conditions, $sql, $bindings ); + } + + /** + * Returns a hashmap with bean arrays keyed by type using an SQL + * query as its resource. Given an SQL query like 'SELECT movie.*, review.* FROM movie... JOIN review' + * this method will return movie and review beans. + * + * Example: + * + * + * $stuff = $finder->findMulti('movie,review', ' + * SELECT movie.*, review.* FROM movie + * LEFT JOIN review ON review.movie_id = movie.id'); + * + * + * After this operation, $stuff will contain an entry 'movie' containing all + * movies and an entry named 'review' containing all reviews (all beans). + * You can also pass bindings. + * + * If you want to re-map your beans, so you can use $movie->ownReviewList without + * having RedBeanPHP executing an SQL query you can use the fourth parameter to + * define a selection of remapping closures. + * + * The remapping argument (optional) should contain an array of arrays. + * Each array in the remapping array should contain the following entries: + * + * + * array( + * 'a' => TYPE A + * 'b' => TYPE B OR BEANS + * 'matcher' => + * MATCHING FUNCTION ACCEPTING A, B and ALL BEANS + * OR ARRAY + * WITH FIELD on B that should match with FIELD on A + * AND FIELD on A that should match with FIELD on B + * OR TRUE + * TO JUST PERFORM THE DO-FUNCTION ON EVERY A-BEAN + * + * 'do' => OPERATION FUNCTION ACCEPTING A, B, ALL BEANS, ALL REMAPPINGS + * (ONLY IF MATCHER IS ALSO A FUNCTION) + * ) + * + * + * Using this mechanism you can build your own 'preloader' with tiny function + * snippets (and those can be re-used and shared online of course). + * + * Example: + * + * + * array( + * 'a' => 'movie' //define A as movie + * 'b' => 'review' //define B as review + * matcher' => function( $a, $b ) { + * return ( $b->movie_id == $a->id ); //Perform action if review.movie_id equals movie.id + * } + * 'do' => function( $a, $b ) { + * $a->noLoad()->ownReviewList[] = $b; //Add the review to the movie + * $a->clearHistory(); //optional, act 'as if these beans have been loaded through ownReviewList'. + * } + * ) + * + * + * The Query Template parameter is optional as well but can be used to + * set a different SQL template (sprintf-style) for processing the original query. + * + * @note the SQL query provided IS NOT THE ONE used internally by this function, + * this function will pre-process the query to get all the data required to find the beans. + * + * @note if you use the 'book.*' notation make SURE you're + * selector starts with a SPACE. ' book.*' NOT ',book.*'. This is because + * it's actually an SQL-like template SLOT, not real SQL. + * + * @note instead of an SQL query you can pass a result array as well. + * + * @note the performance of this function is poor, if you deal with large number of records + * please use plain SQL instead. This function has been added as a bridge between plain SQL + * and bean oriented approaches but it is really on the edge of both worlds. You can safely + * use this function to load additional records as beans in paginated context, let's say + * 50-250 records. Anything above that will gradually perform worse. RedBeanPHP was never + * intended to replace SQL but offer tooling to integrate SQL with object oriented + * designs. If you have come to this function, you have reached the final border between + * SQL-oriented design and OOP. Anything after this will be just as good as custom mapping + * or plain old database querying. I recommend the latter. + * + * @param string|array $types a list of types (either array or comma separated string) + * @param string|array $sql optional, an SQL query or an array of prefetched records + * @param array $bindings optional, bindings for SQL query + * @param array $remappings optional, an array of remapping arrays + * @param string $queryTemplate optional, query template + * + * @return array + */ + public function findMulti( $types, $sql = NULL, $bindings = array(), $remappings = array(), $queryTemplate = ' %s.%s AS %s__%s' ) + { + if ( !is_array( $types ) ) $types = array_map( 'trim', explode( ',', $types ) ); + if ( is_null( $sql ) ) { + $beans = array(); + foreach( $types as $type ) $beans[$type] = $this->redbean->find( $type ); + } else { + if ( !is_array( $sql ) ) { + $writer = $this->toolbox->getWriter(); + $adapter = $this->toolbox->getDatabaseAdapter(); + + //Repair the query, replace book.* with book.id AS book_id etc.. + foreach( $types as $type ) { + $regex = "#( (`?{$type}`?)\.\*)#"; + if ( preg_match( $regex, $sql, $matches ) ) { + $pattern = $matches[1]; + $table = $matches[2]; + $newSelectorArray = array(); + $columns = $writer->getColumns( $type ); + foreach( $columns as $column => $definition ) { + $newSelectorArray[] = sprintf( $queryTemplate, $table, $column, $type, $column ); + } + $newSelector = implode( ',', $newSelectorArray ); + $sql = str_replace( $pattern, $newSelector, $sql ); + } + } + + $rows = $adapter->get( $sql, $bindings ); + } else { + $rows = $sql; + } + + //Gather the bean data from the query results using the prefix + $wannaBeans = array(); + foreach( $types as $type ) { + $wannaBeans[$type] = array(); + $prefix = "{$type}__"; + foreach( $rows as $rowkey=>$row ) { + $wannaBean = array(); + foreach( $row as $cell => $value ) { + if ( strpos( $cell, $prefix ) === 0 ) { + $property = substr( $cell, strlen( $prefix ) ); + unset( $rows[$rowkey][$cell] ); + $wannaBean[$property] = $value; + } + } + if ( !isset( $wannaBean['id'] ) ) continue; + if ( is_null( $wannaBean['id'] ) ) continue; + $wannaBeans[$type][$wannaBean['id']] = $wannaBean; + } + } + + //Turn the rows into beans + $beans = array(); + foreach( $wannaBeans as $type => $wannabees ) { + $beans[$type] = $this->redbean->convertToBeans( $type, $wannabees ); + } + } + + //Apply additional re-mappings + foreach($remappings as $remapping) { + $a = $remapping['a']; + $b = $remapping['b']; + if (is_array($b)) { + $firstBean = reset($b); + $type = $firstBean->getMeta('type'); + $beans[$type] = $b; + $b = $type; + } + $matcher = $remapping['matcher']; + if (is_callable($matcher) || $matcher === TRUE) { + $do = $remapping['do']; + foreach( $beans[$a] as $bean ) { + if ( $matcher === TRUE ) { + $do( $bean, $beans[$b], $beans, $remapping ); + continue; + } + foreach( $beans[$b] as $putBean ) { + if ( $matcher( $bean, $putBean, $beans ) ) $do( $bean, $putBean, $beans, $remapping ); + } + } + } else { + list($field1, $field2) = $matcher; + foreach( $beans[$b] as $key => $bean ) { + $beans[$b][$key]->{$field1} = $beans[$a][$bean->{$field2}]; + } + } + } + return $beans; + } +} +} + +namespace RedBeanPHP { + +use RedBeanPHP\Adapter\DBAdapter as DBAdapter; +use RedBeanPHP\QueryWriter as QueryWriter; +use RedBeanPHP\RedException as RedException; +use RedBeanPHP\RedException\SQL as SQLException; + +/** + * Association Manager. + * The association manager can be used to create and manage + * many-to-many relations (for example sharedLists). In a many-to-many relation, + * one bean can be associated with many other beans, while each of those beans + * can also be related to multiple beans. + * + * @file RedBeanPHP/AssociationManager.php + * @author Gabor de Mooij and the RedBeanPHP Community + * @license BSD/GPLv2 + * + * @copyright + * (c) G.J.G.T. (Gabor) de Mooij and the RedBeanPHP Community. + * This source file is subject to the BSD/GPLv2 License that is bundled + * with this source code in the file license.txt. + */ +class AssociationManager extends Observable +{ + /** + * @var OODB + */ + protected $oodb; + + /** + * @var DBAdapter + */ + protected $adapter; + + /** + * @var QueryWriter + */ + protected $writer; + + /** + * Exception handler. + * Fluid and Frozen mode have different ways of handling + * exceptions. Fluid mode (using the fluid repository) ignores + * exceptions caused by the following: + * + * - missing tables + * - missing column + * + * In these situations, the repository will behave as if + * no beans could be found. This is because in fluid mode + * it might happen to query a table or column that has not been + * created yet. In frozen mode, this is not supposed to happen + * and the corresponding exceptions will be thrown. + * + * @param \Exception $exception exception + * + * @return void + */ + private function handleException( \Exception $exception ) + { + if ( $this->oodb->isFrozen() || !$this->writer->sqlStateIn( $exception->getSQLState(), + array( + QueryWriter::C_SQLSTATE_NO_SUCH_TABLE, + QueryWriter::C_SQLSTATE_NO_SUCH_COLUMN ), + $exception->getDriverDetails() + ) + ) { + throw $exception; + } + } + + /** + * Internal method. + * Returns the many-to-many related rows of table $type for bean $bean using additional SQL in $sql and + * $bindings bindings. If $getLinks is TRUE, link rows are returned instead. + * + * @param OODBBean $bean reference bean instance + * @param string $type target bean type + * @param string $sql additional SQL snippet + * @param array $bindings bindings for query + * + * @return array + */ + private function relatedRows( $bean, $type, $sql = '', $bindings = array() ) + { + $ids = array( $bean->id ); + $sourceType = $bean->getMeta( 'type' ); + try { + return $this->writer->queryRecordRelated( $sourceType, $type, $ids, $sql, $bindings ); + } catch ( SQLException $exception ) { + $this->handleException( $exception ); + return array(); + } + } + + /** + * Associates a pair of beans. This method associates two beans, no matter + * what types. Accepts a base bean that contains data for the linking record. + * This method is used by associate. This method also accepts a base bean to be used + * as the template for the link record in the database. + * + * @param OODBBean $bean1 first bean + * @param OODBBean $bean2 second bean + * @param OODBBean $bean base bean (association record) + * + * @return mixed + */ + protected function associateBeans( OODBBean $bean1, OODBBean $bean2, OODBBean $bean ) + { + $type = $bean->getMeta( 'type' ); + $property1 = $bean1->getMeta( 'type' ) . '_id'; + $property2 = $bean2->getMeta( 'type' ) . '_id'; + + if ( $property1 == $property2 ) { + $property2 = $bean2->getMeta( 'type' ) . '2_id'; + } + + $this->oodb->store( $bean1 ); + $this->oodb->store( $bean2 ); + + $bean->setMeta( "cast.$property1", "id" ); + $bean->setMeta( "cast.$property2", "id" ); + $bean->setMeta( 'sys.buildcommand.unique', array( $property1, $property2 ) ); + + $bean->$property1 = $bean1->id; + $bean->$property2 = $bean2->id; + + $results = array(); + + try { + $id = $this->oodb->store( $bean ); + $results[] = $id; + } catch ( SQLException $exception ) { + if ( !$this->writer->sqlStateIn( $exception->getSQLState(), + array( QueryWriter::C_SQLSTATE_INTEGRITY_CONSTRAINT_VIOLATION ), + $exception->getDriverDetails() ) + ) { + throw $exception; + } + } + + return $results; + } + + /** + * Constructor, creates a new instance of the Association Manager. + * The association manager can be used to create and manage + * many-to-many relations (for example sharedLists). In a many-to-many relation, + * one bean can be associated with many other beans, while each of those beans + * can also be related to multiple beans. To create an Association Manager + * instance you'll need to pass a ToolBox object. + * + * @param ToolBox $tools toolbox supplying core RedBeanPHP objects + */ + public function __construct( ToolBox $tools ) + { + $this->oodb = $tools->getRedBean(); + $this->adapter = $tools->getDatabaseAdapter(); + $this->writer = $tools->getWriter(); + $this->toolbox = $tools; + } + + /** + * Creates a table name based on a types array. + * Manages the get the correct name for the linking table for the + * types provided. + * + * @param array $types 2 types as strings + * + * @return string + */ + public function getTable( $types ) + { + return $this->writer->getAssocTable( $types ); + } + + /** + * Associates two beans in a many-to-many relation. + * This method will associate two beans and store the connection between the + * two in a link table. Instead of two single beans this method also accepts + * two sets of beans. Returns the ID or the IDs of the linking beans. + * + * @param OODBBean|array $beans1 one or more beans to form the association + * @param OODBBean|array $beans2 one or more beans to form the association + * + * @return array + */ + public function associate( $beans1, $beans2 ) + { + if ( !is_array( $beans1 ) ) { + $beans1 = array( $beans1 ); + } + + if ( !is_array( $beans2 ) ) { + $beans2 = array( $beans2 ); + } + + $results = array(); + foreach ( $beans1 as $bean1 ) { + foreach ( $beans2 as $bean2 ) { + $table = $this->getTable( array( $bean1->getMeta( 'type' ), $bean2->getMeta( 'type' ) ) ); + $bean = $this->oodb->dispense( $table ); + $results[] = $this->associateBeans( $bean1, $bean2, $bean ); + } + } + + return ( count( $results ) > 1 ) ? $results : reset( $results ); + } + + /** + * Counts the number of related beans in an N-M relation. + * This method returns the number of beans of type $type associated + * with reference bean(s) $bean. The query can be tuned using an + * SQL snippet for additional filtering. + * + * @param OODBBean|array $bean a bean object or an array of beans + * @param string $type type of bean you're interested in + * @param string $sql SQL snippet (optional) + * @param array $bindings bindings for your SQL string + * + * @return integer + */ + public function relatedCount( $bean, $type, $sql = NULL, $bindings = array() ) + { + if ( !( $bean instanceof OODBBean ) ) { + throw new RedException( + 'Expected array or OODBBean but got:' . gettype( $bean ) + ); + } + + if ( !$bean->id ) { + return 0; + } + + $beanType = $bean->getMeta( 'type' ); + + try { + return $this->writer->queryRecordCountRelated( $beanType, $type, $bean->id, $sql, $bindings ); + } catch ( SQLException $exception ) { + $this->handleException( $exception ); + + return 0; + } + } + + /** + * Breaks the association between two beans. This method unassociates two beans. If the + * method succeeds the beans will no longer form an association. In the database + * this means that the association record will be removed. This method uses the + * OODB trash() method to remove the association links, thus giving FUSE models the + * opportunity to hook-in additional business logic. If the $fast parameter is + * set to boolean TRUE this method will remove the beans without their consent, + * bypassing FUSE. This can be used to improve performance. + * + * @param OODBBean $beans1 first bean in target association + * @param OODBBean $beans2 second bean in target association + * @param boolean $fast if TRUE, removes the entries by query without FUSE + * + * @return void + */ + public function unassociate( $beans1, $beans2, $fast = NULL ) + { + $beans1 = ( !is_array( $beans1 ) ) ? array( $beans1 ) : $beans1; + $beans2 = ( !is_array( $beans2 ) ) ? array( $beans2 ) : $beans2; + + foreach ( $beans1 as $bean1 ) { + foreach ( $beans2 as $bean2 ) { + try { + $this->oodb->store( $bean1 ); + $this->oodb->store( $bean2 ); + + $type1 = $bean1->getMeta( 'type' ); + $type2 = $bean2->getMeta( 'type' ); + + $row = $this->writer->queryRecordLink( $type1, $type2, $bean1->id, $bean2->id ); + + if ( !$row ) return; + + $linkType = $this->getTable( array( $type1, $type2 ) ); + + if ( $fast ) { + $this->writer->deleteRecord( $linkType, array( 'id' => $row['id'] ) ); + + return; + } + + $beans = $this->oodb->convertToBeans( $linkType, array( $row ) ); + + if ( count( $beans ) > 0 ) { + $bean = reset( $beans ); + $this->oodb->trash( $bean ); + } + } catch ( SQLException $exception ) { + $this->handleException( $exception ); + } + } + } + } + + /** + * Removes all relations for a bean. This method breaks every connection between + * a certain bean $bean and every other bean of type $type. Warning: this method + * is really fast because it uses a direct SQL query however it does not inform the + * models about this. If you want to notify FUSE models about deletion use a foreach-loop + * with unassociate() instead. (that might be slower though) + * + * @param OODBBean $bean reference bean + * @param string $type type of beans that need to be unassociated + * + * @return void + */ + public function clearRelations( OODBBean $bean, $type ) + { + $this->oodb->store( $bean ); + try { + $this->writer->deleteRelations( $bean->getMeta( 'type' ), $type, $bean->id ); + } catch ( SQLException $exception ) { + $this->handleException( $exception ); + } + } + + /** + * Returns all the beans associated with $bean. + * This method will return an array containing all the beans that have + * been associated once with the associate() function and are still + * associated with the bean specified. The type parameter indicates the + * type of beans you are looking for. You can also pass some extra SQL and + * values for that SQL to filter your results after fetching the + * related beans. + * + * Don't try to make use of subqueries, a subquery using IN() seems to + * be slower than two queries! + * + * Since 3.2, you can now also pass an array of beans instead just one + * bean as the first parameter. + * + * @param OODBBean|array $bean the bean you have + * @param string $type the type of beans you want + * @param string $sql SQL snippet for extra filtering + * @param array $bindings values to be inserted in SQL slots + * + * @return array + */ + public function related( $bean, $type, $sql = '', $bindings = array() ) + { + $sql = $this->writer->glueSQLCondition( $sql ); + $rows = $this->relatedRows( $bean, $type, $sql, $bindings ); + $links = array(); + + foreach ( $rows as $key => $row ) { + if ( !isset( $links[$row['id']] ) ) $links[$row['id']] = array(); + $links[$row['id']][] = $row['linked_by']; + unset( $rows[$key]['linked_by'] ); + } + + $beans = $this->oodb->convertToBeans( $type, $rows ); + foreach ( $beans as $bean ) $bean->setMeta( 'sys.belongs-to', $links[$bean->id] ); + + return $beans; + } +} +} + +namespace RedBeanPHP { + +use RedBeanPHP\ToolBox as ToolBox; +use RedBeanPHP\OODBBean as OODBBean; + +/** + * Bean Helper Interface. + * + * Interface for Bean Helper. + * A little bolt that glues the whole machinery together. + * The Bean Helper is passed to the OODB RedBeanPHP Object to + * faciliatte the creation of beans and providing them with + * a toolbox. The Helper also facilitates the FUSE feature, + * determining how beans relate to their models. By overriding + * the getModelForBean method you can tune the FUSEing to + * fit your business application needs. + * + * @file RedBeanPHP/IBeanHelper.php + * @author Gabor de Mooij and the RedBeanPHP Community + * @license BSD/GPLv2 + * + * @copyright + * copyright (c) G.J.G.T. (Gabor) de Mooij and the RedBeanPHP Community + * This source file is subject to the BSD/GPLv2 License that is bundled + * with this source code in the file license.txt. + */ +interface BeanHelper +{ + /** + * Returns a toolbox to empower the bean. + * This allows beans to perform OODB operations by themselves, + * as such the bean is a proxy for OODB. This allows beans to implement + * their magic getters and setters and return lists. + * + * @return ToolBox + */ + public function getToolbox(); + + /** + * Does approximately the same as getToolbox but also extracts the + * toolbox for you. + * This method returns a list with all toolbox items in Toolbox Constructor order: + * OODB, adapter, writer and finally the toolbox itself!. + * + * @return array + */ + public function getExtractedToolbox(); + + /** + * Given a certain bean this method will + * return the corresponding model. + * + * @param OODBBean $bean bean to obtain the corresponding model of + * + * @return SimpleModel|CustomModel|NULL + */ + public function getModelForBean( OODBBean $bean ); +} +} + +namespace RedBeanPHP\BeanHelper { + +use RedBeanPHP\BeanHelper as BeanHelper; +use RedBeanPHP\Facade as Facade; +use RedBeanPHP\OODBBean as OODBBean; +use RedBeanPHP\SimpleModelHelper as SimpleModelHelper; + +/** + * Bean Helper. + * + * The Bean helper helps beans to access access the toolbox and + * FUSE models. This Bean Helper makes use of the facade to obtain a + * reference to the toolbox. + * + * @file RedBeanPHP/BeanHelperFacade.php + * @author Gabor de Mooij and the RedBeanPHP Community + * @license BSD/GPLv2 + * + * @copyright + * (c) copyright G.J.G.T. (Gabor) de Mooij and the RedBeanPHP Community + * This source file is subject to the BSD/GPLv2 License that is bundled + * with this source code in the file license.txt. + */ +class SimpleFacadeBeanHelper implements BeanHelper +{ + /** + * Factory function to create instance of Simple Model, if any. + * + * @var \Closure + */ + private static $factory = null; + + /** + * Factory method using a customizable factory function to create + * the instance of the Simple Model. + * + * @param string $modelClassName name of the class + * + * @return SimpleModel + */ + public static function factory( $modelClassName ) + { + $factory = self::$factory; + return ( $factory ) ? $factory( $modelClassName ) : new $modelClassName(); + } + + /** + * Sets the factory function to create the model when using FUSE + * to connect a bean to a model. + * + * @param \Closure $factory factory function + * + * @return void + */ + public static function setFactoryFunction( $factory ) + { + self::$factory = $factory; + } + + /** + * @see BeanHelper::getToolbox + */ + public function getToolbox() + { + return Facade::getToolBox(); + } + + /** + * @see BeanHelper::getModelForBean + */ + public function getModelForBean( OODBBean $bean ) + { + $model = $bean->getMeta( 'type' ); + $prefix = defined( 'REDBEAN_MODEL_PREFIX' ) ? REDBEAN_MODEL_PREFIX : '\\Model_'; + + if ( strpos( $model, '_' ) !== FALSE ) { + $modelParts = explode( '_', $model ); + $modelName = ''; + foreach( $modelParts as $part ) { + $modelName .= ucfirst( $part ); + } + $modelName = $prefix . $modelName; + if ( !class_exists( $modelName ) ) { + $modelName = $prefix . ucfirst( $model ); + if ( !class_exists( $modelName ) ) { + return NULL; + } + } + } else { + $modelName = $prefix . ucfirst( $model ); + if ( !class_exists( $modelName ) ) { + return NULL; + } + } + $obj = self::factory( $modelName ); + $obj->loadBean( $bean ); + return $obj; + } + + /** + * @see BeanHelper::getExtractedToolbox + */ + public function getExtractedToolbox() + { + return Facade::getExtractedToolbox(); + } +} +} + +namespace RedBeanPHP { + +use RedBeanPHP\OODBBean as OODBBean; + +/** + * SimpleModel + * Base Model For All RedBeanPHP Models using FUSE. + * + * RedBeanPHP FUSE is a mechanism to connect beans to posthoc + * models. Models are connected to beans by naming conventions. + * Actions on beans will result in actions on models. + * + * @file RedBeanPHP/SimpleModel.php + * @author Gabor de Mooij and the RedBeanPHP Team + * @license BSD/GPLv2 + * + * @copyright + * copyright (c) G.J.G.T. (Gabor) de Mooij and the RedBeanPHP Community + * This source file is subject to the BSD/GPLv2 License that is bundled + * with this source code in the file license.txt. + */ +class SimpleModel +{ + /** + * @var OODBBean + */ + protected $bean; + + /** + * Used by FUSE: the ModelHelper class to connect a bean to a model. + * This method loads a bean in the model. + * + * @param OODBBean $bean bean to load + * + * @return void + */ + public function loadBean( OODBBean $bean ) + { + $this->bean = $bean; + } + + /** + * Magic Getter to make the bean properties available from + * the $this-scope. + * + * @note this method returns a value, not a reference! + * To obtain a reference unbox the bean first! + * + * @param string $prop property to get + * + * @return mixed + */ + public function __get( $prop ) + { + return $this->bean->$prop; + } + + /** + * Magic Setter. + * Sets the value directly as a bean property. + * + * @param string $prop property to set value of + * @param mixed $value value to set + * + * @return void + */ + public function __set( $prop, $value ) + { + $this->bean->$prop = $value; + } + + /** + * Isset implementation. + * Implements the isset function for array-like access. + * + * @param string $key key to check + * + * @return boolean + */ + public function __isset( $key ) + { + return isset( $this->bean->$key ); + } + + /** + * Box the bean using the current model. + * This method wraps the current bean in this model. + * This method can be reached using FUSE through a simple + * OODBBean. The method returns a RedBeanPHP Simple Model. + * This is useful if you would like to rely on PHP type hinting. + * You can box your beans before passing them to functions or methods + * with typed parameters. + * + * Note about beans vs models: + * Use unbox to obtain the bean powering the model. If you want to use bean functionality, + * you should -always- unbox first. While some functionality (like magic get/set) is + * available in the model, this is just read-only. To use a model as a typical RedBean + * OODBBean you should always unbox the model to a bean. Models are meant to + * expose only domain logic added by the developer (business logic, no ORM logic). + * + * @return SimpleModel + */ + public function box() + { + return $this; + } + + /** + * Unbox the bean from the model. + * This method returns the bean inside the model. + * + * Note about beans vs models: + * Use unbox to obtain the bean powering the model. If you want to use bean functionality, + * you should -always- unbox first. While some functionality (like magic get/set) is + * available in the model, this is just read-only. To use a model as a typical RedBean + * OODBBean you should always unbox the model to a bean. Models are meant to + * expose only domain logic added by the developer (business logic, no ORM logic). + * + * @return OODBBean + */ + public function unbox() + { + return $this->bean; + } +} +} + +namespace RedBeanPHP { + +use RedBeanPHP\Observer as Observer; +use RedBeanPHP\OODBBean as OODBBean; +use RedBeanPHP\Observable as Observable; + +/** + * RedBean Model Helper. + * + * Connects beans to models. + * This is the core of so-called FUSE. + * + * @file RedBeanPHP/ModelHelper.php + * @author Gabor de Mooij and the RedBeanPHP Community + * @license BSD/GPLv2 + * + * @copyright + * copyright (c) G.J.G.T. (Gabor) de Mooij and the RedBeanPHP Community + * This source file is subject to the BSD/GPLv2 License that is bundled + * with this source code in the file license.txt. + */ +class SimpleModelHelper implements Observer +{ + /** + * Gets notified by an observable. + * This method decouples the FUSE system from the actual beans. + * If a FUSE event happens 'update', this method will attempt to + * invoke the corresponding method on the bean. + * + * @param string $eventName i.e. 'delete', 'after_delete' + * @param OODBean $bean affected bean + * + * @return void + */ + public function onEvent( $eventName, $bean ) + { + $bean->$eventName(); + } + + /** + * Attaches the FUSE event listeners. Now the Model Helper will listen for + * CRUD events. If a CRUD event occurs it will send a signal to the model + * that belongs to the CRUD bean and this model will take over control from + * there. This method will attach the following event listeners to the observable: + * + * - 'update' (gets called by R::store, before the records gets inserted / updated) + * - 'after_update' (gets called by R::store, after the records have been inserted / updated) + * - 'open' (gets called by R::load, after the record has been retrieved) + * - 'delete' (gets called by R::trash, before deletion of record) + * - 'after_delete' (gets called by R::trash, after deletion) + * - 'dispense' (gets called by R::dispense) + * + * For every event type, this method will register this helper as a listener. + * The observable will notify the listener (this object) with the event ID and the + * affected bean. This helper will then process the event (onEvent) by invoking + * the event on the bean. If a bean offers a method with the same name as the + * event ID, this method will be invoked. + * + * @param Observable $observable object to observe + * + * @return void + */ + public function attachEventListeners( Observable $observable ) + { + foreach ( array( 'update', 'open', 'delete', 'after_delete', 'after_update', 'dispense' ) as $eventID ) { + $observable->addEventListener( $eventID, $this ); + } + } +} +} + +namespace RedBeanPHP { + +use RedBeanPHP\ToolBox as ToolBox; +use RedBeanPHP\AssociationManager as AssociationManager; +use RedBeanPHP\OODBBean as OODBBean; + +/** + * RedBeanPHP Tag Manager. + * + * The tag manager offers an easy way to quickly implement basic tagging + * functionality. + * + * Provides methods to tag beans and perform tag-based searches in the + * bean database. + * + * @file RedBeanPHP/TagManager.php + * @author Gabor de Mooij and the RedBeanPHP community + * @license BSD/GPLv2 + * + * @copyright + * copyright (c) G.J.G.T. (Gabor) de Mooij and the RedBeanPHP Community + * This source file is subject to the BSD/GPLv2 License that is bundled + * with this source code in the file license.txt. + */ +class TagManager +{ + /** + * @var ToolBox + */ + protected $toolbox; + + /** + * @var AssociationManager + */ + protected $associationManager; + + /** + * @var OODBBean + */ + protected $redbean; + + /** + * Checks if the argument is a comma separated string, in this case + * it will split the string into words and return an array instead. + * In case of an array the argument will be returned 'as is'. + * + * @param array|string $tagList list of tags + * + * @return array + */ + private function extractTagsIfNeeded( $tagList ) + { + if ( $tagList !== FALSE && !is_array( $tagList ) ) { + $tags = explode( ',', (string) $tagList ); + } else { + $tags = $tagList; + } + + return $tags; + } + + /** + * Finds a tag bean by it's title. + * Internal method. + * + * @param string $title title to search for + * + * @return OODBBean + */ + protected function findTagByTitle( $title ) + { + $beans = $this->redbean->find( 'tag', array( 'title' => array( $title ) ) ); + + if ( $beans ) { + $bean = reset( $beans ); + + return $bean; + } + + return NULL; + } + + /** + * Constructor. + * The tag manager offers an easy way to quickly implement basic tagging + * functionality. + * + * @param ToolBox $toolbox toolbox object + */ + public function __construct( ToolBox $toolbox ) + { + $this->toolbox = $toolbox; + $this->redbean = $toolbox->getRedBean(); + + $this->associationManager = $this->redbean->getAssociationManager(); + } + + /** + * Tests whether a bean has been associated with one ore more + * of the listed tags. If the third parameter is TRUE this method + * will return TRUE only if all tags that have been specified are indeed + * associated with the given bean, otherwise FALSE. + * If the third parameter is FALSE this + * method will return TRUE if one of the tags matches, FALSE if none + * match. + * + * Tag list can be either an array with tag names or a comma separated list + * of tag names. + * + * Usage: + * + * + * R::hasTag( $blog, 'horror,movie', TRUE ); + * + * + * The example above returns TRUE if the $blog bean has been tagged + * as BOTH horror and movie. If the post has only been tagged as 'movie' + * or 'horror' this operation will return FALSE because the third parameter + * has been set to TRUE. + * + * @param OODBBean $bean bean to check for tags + * @param array|string $tags list of tags + * @param boolean $all whether they must all match or just some + * + * @return boolean + */ + public function hasTag( $bean, $tags, $all = FALSE ) + { + $foundtags = $this->tag( $bean ); + + $tags = $this->extractTagsIfNeeded( $tags ); + $same = array_intersect( $tags, $foundtags ); + + if ( $all ) { + return ( implode( ',', $same ) === implode( ',', $tags ) ); + } + + return (bool) ( count( $same ) > 0 ); + } + + /** + * Removes all specified tags from the bean. The tags specified in + * the second parameter will no longer be associated with the bean. + * + * Tag list can be either an array with tag names or a comma separated list + * of tag names. + * + * Usage: + * + * + * R::untag( $blog, 'smart,interesting' ); + * + * + * In the example above, the $blog bean will no longer + * be associated with the tags 'smart' and 'interesting'. + * + * @param OODBBean $bean tagged bean + * @param array $tagList list of tags (names) + * + * @return void + */ + public function untag( $bean, $tagList ) + { + $tags = $this->extractTagsIfNeeded( $tagList ); + + foreach ( $tags as $tag ) { + if ( $t = $this->findTagByTitle( $tag ) ) { + $this->associationManager->unassociate( $bean, $t ); + } + } + } + + /** + * Part of RedBeanPHP Tagging API. + * Tags a bean or returns tags associated with a bean. + * If $tagList is NULL or omitted this method will return a + * comma separated list of tags associated with the bean provided. + * If $tagList is a comma separated list (string) of tags all tags will + * be associated with the bean. + * You may also pass an array instead of a string. + * + * Usage: + * + * + * R::tag( $meal, "TexMex,Mexican" ); + * $tags = R::tag( $meal ); + * + * + * The first line in the example above will tag the $meal + * as 'TexMex' and 'Mexican Cuisine'. The second line will + * retrieve all tags attached to the meal object. + * + * @param OODBBean $bean bean to tag + * @param mixed $tagList tags to attach to the specified bean + * + * @return string + */ + public function tag( OODBBean $bean, $tagList = NULL ) + { + if ( is_null( $tagList ) ) { + + $tags = $bean->sharedTag; + $foundTags = array(); + + foreach ( $tags as $tag ) { + $foundTags[] = $tag->title; + } + + return $foundTags; + } + + $this->associationManager->clearRelations( $bean, 'tag' ); + $this->addTags( $bean, $tagList ); + + return $tagList; + } + + /** + * Part of RedBeanPHP Tagging API. + * Adds tags to a bean. + * If $tagList is a comma separated list of tags all tags will + * be associated with the bean. + * You may also pass an array instead of a string. + * + * Usage: + * + * + * R::addTags( $blog, ["halloween"] ); + * + * + * The example adds the tag 'halloween' to the $blog + * bean. + * + * @param OODBBean $bean bean to tag + * @param array $tagList list of tags to add to bean + * + * @return void + */ + public function addTags( OODBBean $bean, $tagList ) + { + $tags = $this->extractTagsIfNeeded( $tagList ); + + if ( $tagList === FALSE ) { + return; + } + + foreach ( $tags as $tag ) { + if ( !$t = $this->findTagByTitle( $tag ) ) { + $t = $this->redbean->dispense( 'tag' ); + $t->title = $tag; + + $this->redbean->store( $t ); + } + + $this->associationManager->associate( $bean, $t ); + } + } + + /** + * Returns all beans that have been tagged with one or more + * of the specified tags. + * + * Tag list can be either an array with tag names or a comma separated list + * of tag names. + * + * Usage: + * + * + * $watchList = R::tagged( + * 'movie', + * 'horror,gothic', + * ' ORDER BY movie.title DESC LIMIT ?', + * [ 10 ] + * ); + * + * + * The example uses R::tagged() to find all movies that have been + * tagged as 'horror' or 'gothic', order them by title and limit + * the number of movies to be returned to 10. + * + * @param string $beanType type of bean you are looking for + * @param array|string $tagList list of tags to match + * @param string $sql additional SQL (use only for pagination) + * @param array $bindings bindings + * + * @return array + */ + public function tagged( $beanType, $tagList, $sql = '', $bindings = array() ) + { + $tags = $this->extractTagsIfNeeded( $tagList ); + $records = $this->toolbox->getWriter()->queryTagged( $beanType, $tags, FALSE, $sql, $bindings ); + + return $this->redbean->convertToBeans( $beanType, $records ); + } + + /** + * Returns all beans that have been tagged with ALL of the tags given. + * This method works the same as R::tagged() except that this method only returns + * beans that have been tagged with all the specified labels. + * + * Tag list can be either an array with tag names or a comma separated list + * of tag names. + * + * Usage: + * + * + * $watchList = R::taggedAll( + * 'movie', + * [ 'gothic', 'short' ], + * ' ORDER BY movie.id DESC LIMIT ? ', + * [ 4 ] + * ); + * + * + * The example above returns at most 4 movies (due to the LIMIT clause in the SQL + * Query Snippet) that have been tagged as BOTH 'short' AND 'gothic'. + * + * @param string $beanType type of bean you are looking for + * @param array|string $tagList list of tags to match + * @param string $sql additional sql snippet + * @param array $bindings bindings + * + * @return array + */ + public function taggedAll( $beanType, $tagList, $sql = '', $bindings = array() ) + { + $tags = $this->extractTagsIfNeeded( $tagList ); + $records = $this->toolbox->getWriter()->queryTagged( $beanType, $tags, TRUE, $sql, $bindings ); + + return $this->redbean->convertToBeans( $beanType, $records ); + } + + /** + * Like taggedAll() but only counts. + * + * @see taggedAll + * + * @param string $beanType type of bean you are looking for + * @param array|string $tagList list of tags to match + * @param string $sql additional sql snippet + * @param array $bindings bindings + * + * @return integer + */ + public function countTaggedAll( $beanType, $tagList, $sql = '', $bindings = array() ) + { + $tags = $this->extractTagsIfNeeded( $tagList ); + return $this->toolbox->getWriter()->queryCountTagged( $beanType, $tags, TRUE, $sql, $bindings ); + } + + /** + * Like tagged() but only counts. + * + * @see tagged + * + * @param string $beanType type of bean you are looking for + * @param array|string $tagList list of tags to match + * @param string $sql additional sql snippet + * @param array $bindings bindings + * + * @return integer + */ + public function countTagged( $beanType, $tagList, $sql = '', $bindings = array() ) + { + $tags = $this->extractTagsIfNeeded( $tagList ); + return $this->toolbox->getWriter()->queryCountTagged( $beanType, $tags, FALSE, $sql, $bindings ); + } +} +} + +namespace RedBeanPHP { + +use RedBeanPHP\ToolBox as ToolBox; +use RedBeanPHP\OODBBean as OODBBean; + +/** + * Label Maker. + * Makes so-called label beans. + * A label is a bean with only an id, type and name property. + * Labels can be used to create simple entities like categories, tags or enums. + * This service class provides convenience methods to deal with this kind of + * beans. + * + * @file RedBeanPHP/LabelMaker.php + * @author Gabor de Mooij and the RedBeanPHP Community + * @license BSD/GPLv2 + * + * @copyright + * copyright (c) G.J.G.T. (Gabor) de Mooij and the RedBeanPHP Community + * This source file is subject to the BSD/GPLv2 License that is bundled + * with this source code in the file license.txt. + */ +class LabelMaker +{ + /** + * @var ToolBox + */ + protected $toolbox; + + /** + * Constructor. + * + * @param ToolBox $toolbox + */ + public function __construct( ToolBox $toolbox ) + { + $this->toolbox = $toolbox; + } + + /** + * A label is a bean with only an id, type and name property. + * This function will dispense beans for all entries in the array. The + * values of the array will be assigned to the name property of each + * individual bean. + * + * + * $people = R::dispenseLabels( 'person', [ 'Santa', 'Claus' ] ); + * + * + * @param string $type type of beans you would like to have + * @param array $labels list of labels, names for each bean + * + * @return array + */ + public function dispenseLabels( $type, $labels ) + { + $labelBeans = array(); + foreach ( $labels as $label ) { + $labelBean = $this->toolbox->getRedBean()->dispense( $type ); + $labelBean->name = $label; + $labelBeans[] = $labelBean; + } + + return $labelBeans; + } + + /** + * Gathers labels from beans. This function loops through the beans, + * collects the value of the name property for each individual bean + * and stores the names in a new array. The array then gets sorted using the + * default sort function of PHP (sort). + * + * Usage: + * + * + * $o1->name = 'hamburger'; + * $o2->name = 'pizza'; + * implode( ',', R::gatherLabels( [ $o1, $o2 ] ) ); //hamburger,pizza + * + * + * Note that the return value is an array of strings, not beans. + * + * @param array $beans list of beans to loop through + * + * @return array + */ + public function gatherLabels( $beans ) + { + $labels = array(); + + foreach ( $beans as $bean ) { + $labels[] = $bean->name; + } + + sort( $labels ); + + return $labels; + } + + /** + * Fetches an ENUM from the database and creates it if necessary. + * An ENUM has the following format: + * + * + * ENUM:VALUE + * + * + * If you pass 'ENUM' only, this method will return an array of its + * values: + * + * + * implode( ',', R::gatherLabels( R::enum( 'flavour' ) ) ) //'BANANA,MOCCA' + * + * + * If you pass 'ENUM:VALUE' this method will return the specified enum bean + * and create it in the database if it does not exist yet: + * + * + * $bananaFlavour = R::enum( 'flavour:banana' ); + * $bananaFlavour->name; + * + * + * So you can use this method to set an ENUM value in a bean: + * + * + * $shake->flavour = R::enum( 'flavour:banana' ); + * + * + * the property flavour now contains the enum bean, a parent bean. + * In the database, flavour_id will point to the flavour record with name 'banana'. + * + * @param string $enum ENUM specification for label + * + * @return array|OODBBean + */ + public function enum( $enum ) + { + $oodb = $this->toolbox->getRedBean(); + + if ( strpos( $enum, ':' ) === FALSE ) { + $type = $enum; + $value = FALSE; + } else { + list( $type, $value ) = explode( ':', $enum ); + $value = preg_replace( '/\W+/', '_', strtoupper( trim( $value ) ) ); + } + + /** + * We use simply find here, we could use inspect() in fluid mode etc, + * but this would be useless. At first sight it looks clean, you could even + * bake this into find(), however, find not only has to deal with the primary + * search type, people can also include references in the SQL part, so avoiding + * find failures does not matter, this is still the quickest way making use + * of existing functionality. + * + * @note There seems to be a bug in XDebug v2.3.2 causing suppressed + * exceptions like these to surface anyway, to prevent this use: + * + * "xdebug.default_enable = 0" + * + * Also see Github Issue #464 + */ + $values = $oodb->find( $type ); + + if ( $value === FALSE ) { + return $values; + } + + foreach( $values as $enumItem ) { + if ( $enumItem->name === $value ) return $enumItem; + } + + $newEnumItems = $this->dispenseLabels( $type, array( $value ) ); + $newEnumItem = reset( $newEnumItems ); + + $oodb->store( $newEnumItem ); + + return $newEnumItem; + } +} +} + +namespace RedBeanPHP { + +use RedBeanPHP\QueryWriter as QueryWriter; +use RedBeanPHP\Adapter\DBAdapter as DBAdapter; +use RedBeanPHP\RedException\SQL as SQLException; +use RedBeanPHP\Logger as Logger; +use RedBeanPHP\Logger\RDefault as RDefault; +use RedBeanPHP\Logger\RDefault\Debug as Debug; +use RedBeanPHP\Adapter as Adapter; +use RedBeanPHP\QueryWriter\AQueryWriter as AQueryWriter; +use RedBeanPHP\RedException as RedException; +use RedBeanPHP\BeanHelper\SimpleFacadeBeanHelper as SimpleFacadeBeanHelper; +use RedBeanPHP\Driver\RPDO as RPDO; +use RedBeanPHP\Util\MultiLoader as MultiLoader; +use RedBeanPHP\Util\Transaction as Transaction; +use RedBeanPHP\Util\Dump as Dump; +use RedBeanPHP\Util\DispenseHelper as DispenseHelper; +use RedBeanPHP\Util\ArrayTool as ArrayTool; +use RedBeanPHP\Util\QuickExport as QuickExport; +use RedBeanPHP\Util\MatchUp as MatchUp; +use RedBeanPHP\Util\Look as Look; +use RedBeanPHP\Util\Diff as Diff; +use RedBeanPHP\Util\Tree as Tree; +use RedBeanPHP\Util\Feature; + +/** + * RedBean Facade + * + * Version Information + * RedBean Version @version 5.7 + * + * This class hides the object landscape of + * RedBeanPHP behind a single letter class providing + * almost all functionality with simple static calls. + * + * @file RedBeanPHP/Facade.php + * @author Gabor de Mooij and the RedBeanPHP Community + * @license BSD/GPLv2 + * + * @copyright + * copyright (c) G.J.G.T. (Gabor) de Mooij and the RedBeanPHP Community + * This source file is subject to the BSD/GPLv2 License that is bundled + * with this source code in the file license.txt. + */ +class Facade +{ + /** + * RedBeanPHP version constant. + */ + const C_REDBEANPHP_VERSION = '5.7'; + + /** + * @var ToolBox + */ + public static $toolbox; + + /** + * @var OODB + */ + private static $redbean; + + /** + * @var QueryWriter + */ + private static $writer; + + /** + * @var DBAdapter + */ + private static $adapter; + + /** + * @var AssociationManager + */ + private static $associationManager; + + /** + * @var TagManager + */ + private static $tagManager; + + /** + * @var DuplicationManager + */ + private static $duplicationManager; + + /** + * @var LabelMaker + */ + private static $labelMaker; + + /** + * @var Finder + */ + private static $finder; + + /** + * @var Tree + */ + private static $tree; + + /** + * @var Logger + */ + private static $logger; + + /** + * @var array + */ + private static $plugins = array(); + + /** + * @var string + */ + private static $exportCaseStyle = 'default'; + + /** + * @var flag allows transactions through facade in fluid mode + */ + private static $allowFluidTransactions = FALSE; + + /** + * @var flag allows to unfreeze if needed with store(all) + */ + private static $allowHybridMode = FALSE; + + /** + * Not in use (backward compatibility SQLHelper) + */ + public static $f; + + /** + * @var string + */ + public static $currentDB = ''; + + /** + * @var array + */ + public static $toolboxes = array(); + + /** + * Internal Query function, executes the desired query. Used by + * all facade query functions. This keeps things DRY. + * + * @param string $method desired query method (i.e. 'cell', 'col', 'exec' etc..) + * @param string $sql the sql you want to execute + * @param array $bindings array of values to be bound to query statement + * + * @return array + */ + private static function query( $method, $sql, $bindings ) + { + if ( !self::$redbean->isFrozen() ) { + try { + $rs = Facade::$adapter->$method( $sql, $bindings ); + } catch ( SQLException $exception ) { + if ( self::$writer->sqlStateIn( $exception->getSQLState(), + array( + QueryWriter::C_SQLSTATE_NO_SUCH_COLUMN, + QueryWriter::C_SQLSTATE_NO_SUCH_TABLE ) + ,$exception->getDriverDetails() + ) + ) { + return ( $method === 'getCell' ) ? NULL : array(); + } else { + throw $exception; + } + } + + return $rs; + } else { + return Facade::$adapter->$method( $sql, $bindings ); + } + } + + /** + * Sets allow hybrid mode flag. In Hybrid mode (default off), + * store/storeAll take an extra argument to switch to fluid + * mode in case of an exception. You can use this to speed up + * fluid mode. This method returns the previous value of the + * flag. + * + * @param boolean $hybrid + */ + public static function setAllowHybridMode( $hybrid ) + { + $old = self::$allowHybridMode; + self::$allowHybridMode = $hybrid; + return $old; + } + + /** + * Returns the RedBeanPHP version string. + * The RedBeanPHP version string always has the same format "X.Y" + * where X is the major version number and Y is the minor version number. + * Point releases are not mentioned in the version string. + * + * @return string + */ + public static function getVersion() + { + return self::C_REDBEANPHP_VERSION; + } + + /** + * Returns the version string from the database server. + * + * @return string + */ + public static function getDatabaseServerVersion() + { + return self::$adapter->getDatabaseServerVersion(); + } + + /** + * Tests the database connection. + * Returns TRUE if connection has been established and + * FALSE otherwise. Suppresses any warnings that may + * occur during the testing process and catches all + * exceptions that might be thrown during the test. + * + * @return boolean + */ + public static function testConnection() + { + if ( !isset( self::$adapter ) ) return FALSE; + + $database = self::$adapter->getDatabase(); + try { + @$database->connect(); + } catch ( \Exception $e ) {} + return $database->isConnected(); + } + + /** + * Kickstarts redbean for you. This method should be called before you start using + * RedBeanPHP. The Setup() method can be called without any arguments, in this case it will + * try to create a SQLite database in /tmp called red.db (this only works on UNIX-like systems). + * + * Usage: + * + * + * R::setup( 'mysql:host=localhost;dbname=mydatabase', 'dba', 'dbapassword' ); + * + * + * You can replace 'mysql:' with the name of the database you want to use. + * Possible values are: + * + * - pgsql (PostgreSQL database) + * - sqlite (SQLite database) + * - mysql (MySQL database) + * - mysql (also for Maria database) + * - sqlsrv (MS SQL Server - community supported experimental driver) + * - CUBRID (CUBRID driver - basic support provided by Plugin) + * + * Note that setup() will not immediately establish a connection to the database. + * Instead, it will prepare the connection and connect 'lazily', i.e. the moment + * a connection is really required, for instance when attempting to load + * a bean. + * + * @param string $dsn Database connection string + * @param string $username Username for database + * @param string $password Password for database + * @param boolean $frozen TRUE if you want to setup in frozen mode + * @param boolean $partialBeans TRUE to enable partial bean updates + * @param array $options Additional (PDO) options to pass + * + * @return ToolBox + */ + public static function setup( $dsn = NULL, $username = NULL, $password = NULL, $frozen = FALSE, $partialBeans = FALSE, $options = array() ) + { + if ( is_null( $dsn ) ) { + $dsn = 'sqlite:' . DIRECTORY_SEPARATOR . sys_get_temp_dir() . DIRECTORY_SEPARATOR . 'red.db'; + } + + self::addDatabase( 'default', $dsn, $username, $password, $frozen, $partialBeans, $options ); + self::selectDatabase( 'default' ); + + return self::$toolbox; + } + + /** + * Toggles 'Narrow Field Mode'. + * In Narrow Field mode the queryRecord method will + * narrow its selection field to + * + * + * SELECT {table}.* + * + * + * instead of + * + * + * SELECT * + * + * + * This is a better way of querying because it allows + * more flexibility (for instance joins). However if you need + * the wide selector for backward compatibility; use this method + * to turn OFF Narrow Field Mode by passing FALSE. + * Default is TRUE. + * + * @param boolean $narrowField TRUE = Narrow Field FALSE = Wide Field + * + * @return void + */ + public static function setNarrowFieldMode( $mode ) + { + AQueryWriter::setNarrowFieldMode( $mode ); + } + + /** + * Toggles fluid transactions. By default fluid transactions + * are not active. Starting, committing or rolling back a transaction + * through the facade in fluid mode will have no effect. If you wish + * to replace this standard portable behavor with behavior depending + * on how the used database platform handles fluid (DDL) transactions + * set this flag to TRUE. + * + * @param boolean $mode allow fluid transaction mode + * + * @return void + */ + public static function setAllowFluidTransactions( $mode ) + { + self::$allowFluidTransactions = $mode; + } + + /** + * Toggles support for IS-NULL-conditions. + * If IS-NULL-conditions are enabled condition arrays + * for functions including findLike() are treated so that + * 'field' => NULL will be interpreted as field IS NULL + * instead of being skipped. Returns the previous + * value of the flag. + * + * @param boolean $flag TRUE or FALSE + * + * @return boolean + */ + public static function useISNULLConditions( $mode ) + { + self::getWriter()->flushCache(); /* otherwise same queries might fail (see Unit test XNull) */ + return AQueryWriter::useISNULLConditions( $mode ); + } + + /** + * Wraps a transaction around a closure or string callback. + * If an Exception is thrown inside, the operation is automatically rolled back. + * If no Exception happens, it commits automatically. + * It also supports (simulated) nested transactions (that is useful when + * you have many methods that needs transactions but are unaware of + * each other). + * + * Example: + * + * + * $from = 1; + * $to = 2; + * $amount = 300; + * + * R::transaction(function() use($from, $to, $amount) + * { + * $accountFrom = R::load('account', $from); + * $accountTo = R::load('account', $to); + * $accountFrom->money -= $amount; + * $accountTo->money += $amount; + * R::store($accountFrom); + * R::store($accountTo); + * }); + * + * + * @param callable $callback Closure (or other callable) with the transaction logic + * + * @return mixed + */ + public static function transaction( $callback ) + { + return Transaction::transaction( self::$adapter, $callback ); + } + + /** + * Adds a database to the facade, afterwards you can select the database using + * selectDatabase($key), where $key is the name you assigned to this database. + * + * Usage: + * + * + * R::addDatabase( 'database-1', 'sqlite:/tmp/db1.txt' ); + * R::selectDatabase( 'database-1' ); //to select database again + * + * + * This method allows you to dynamically add (and select) new databases + * to the facade. Adding a database with the same key will cause an exception. + * + * @param string $key ID for the database + * @param string $dsn DSN for the database + * @param string $user user for connection + * @param NULL|string $pass password for connection + * @param bool $frozen whether this database is frozen or not + * + * @return void + */ + public static function addDatabase( $key, $dsn, $user = NULL, $pass = NULL, $frozen = FALSE, $partialBeans = FALSE, $options = array() ) + { + if ( isset( self::$toolboxes[$key] ) ) { + throw new RedException( 'A database has already been specified for this key.' ); + } + + self::$toolboxes[$key] = self::createToolbox($dsn, $user, $pass, $frozen, $partialBeans, $options); + } + + /** + * Creates a toolbox. This method can be called if you want to use redbean non-static. + * It has the same interface as R::setup(). The createToolbx() method can be called + * without any arguments, in this case it will try to create a SQLite database in + * /tmp called red.db (this only works on UNIX-like systems). + * + * Usage: + * + * + * R::createToolbox( 'mysql:host=localhost;dbname=mydatabase', 'dba', 'dbapassword' ); + * + * + * You can replace 'mysql:' with the name of the database you want to use. + * Possible values are: + * + * - pgsql (PostgreSQL database) + * - sqlite (SQLite database) + * - mysql (MySQL database) + * - mysql (also for Maria database) + * - sqlsrv (MS SQL Server - community supported experimental driver) + * - CUBRID (CUBRID driver - basic support provided by Plugin) + * + * Note that createToolbox() will not immediately establish a connection to the database. + * Instead, it will prepare the connection and connect 'lazily', i.e. the moment + * a connection is really required, for instance when attempting to load a bean. + * + * @param string $dsn Database connection string + * @param string $username Username for database + * @param string $password Password for database + * @param boolean $frozen TRUE if you want to setup in frozen mode + * + * @return ToolBox + */ + public static function createToolbox( $dsn = NULL, $username = NULL, $password = NULL, $frozen = FALSE, $partialBeans = FALSE, $options = array() ) + { + if ( is_object($dsn) ) { + $db = new RPDO( $dsn ); + $dbType = $db->getDatabaseType(); + } else { + $db = new RPDO( $dsn, $username, $password, $options ); + $dbType = substr( $dsn, 0, strpos( $dsn, ':' ) ); + } + + $adapter = new DBAdapter( $db ); + + $writers = array( + 'pgsql' => 'PostgreSQL', + 'sqlite' => 'SQLiteT', + 'cubrid' => 'CUBRID', + 'mysql' => 'MySQL', + 'sqlsrv' => 'SQLServer', + ); + + $wkey = trim( strtolower( $dbType ) ); + if ( !isset( $writers[$wkey] ) ) { + $wkey = preg_replace( '/\W/', '' , $wkey ); + throw new RedException( 'Unsupported database ('.$wkey.').' ); + } + $writerClass = '\\RedBeanPHP\\QueryWriter\\'.$writers[$wkey]; + $writer = new $writerClass( $adapter ); + $redbean = new OODB( $writer, $frozen ); + + if ( $partialBeans ) { + $redbean->getCurrentRepository()->usePartialBeans( $partialBeans ); + } + + return new ToolBox( $redbean, $adapter, $writer ); + } + + /** + * Determines whether a database identified with the specified key has + * already been added to the facade. This function will return TRUE + * if the database indicated by the key is available and FALSE otherwise. + * + * @param string $key the key/name of the database to check for + * + * @return boolean + */ + public static function hasDatabase( $key ) + { + return ( isset( self::$toolboxes[$key] ) ); + } + + /** + * Selects a different database for the Facade to work with. + * If you use the R::setup() you don't need this method. This method is meant + * for multiple database setups. This method selects the database identified by the + * database ID ($key). Use addDatabase() to add a new database, which in turn + * can be selected using selectDatabase(). If you use R::setup(), the resulting + * database will be stored under key 'default', to switch (back) to this database + * use R::selectDatabase( 'default' ). This method returns TRUE if the database has been + * switched and FALSE otherwise (for instance if you already using the specified database). + * + * @param string $key Key of the database to select + * + * @return boolean + */ + public static function selectDatabase( $key, $force = FALSE ) + { + if ( self::$currentDB === $key && !$force ) { + return FALSE; + } + + if ( !isset( self::$toolboxes[$key] ) ) { + throw new RedException( 'Database not found in registry. Add database using R::addDatabase().' ); + } + + self::configureFacadeWithToolbox( self::$toolboxes[$key] ); + self::$currentDB = $key; + + return TRUE; + } + + /** + * Toggles DEBUG mode. + * In Debug mode all SQL that happens under the hood will + * be printed to the screen and/or logged. + * If no database connection has been configured using R::setup() or + * R::selectDatabase() this method will throw an exception. + * + * There are 2 debug styles: + * + * Classic: separate parameter bindings, explicit and complete but less readable + * Fancy: interpersed bindings, truncates large strings, highlighted schema changes + * + * Fancy style is more readable but sometimes incomplete. + * + * The first parameter turns debugging ON or OFF. + * The second parameter indicates the mode of operation: + * + * 0 Log and write to STDOUT classic style (default) + * 1 Log only, class style + * 2 Log and write to STDOUT fancy style + * 3 Log only, fancy style + * + * This function always returns the logger instance created to generate the + * debug messages. + * + * @param boolean $tf debug mode (TRUE or FALSE) + * @param integer $mode mode of operation + * + * @return RDefault + * @throws RedException + */ + public static function debug( $tf = TRUE, $mode = 0 ) + { + if ($mode > 1) { + $mode -= 2; + $logger = new Debug; + } else { + $logger = new RDefault; + } + + if ( !isset( self::$adapter ) ) { + throw new RedException( 'Use R::setup() first.' ); + } + $logger->setMode($mode); + self::$adapter->getDatabase()->setDebugMode( $tf, $logger ); + + return $logger; + } + + /** + * Turns on the fancy debugger. + * In 'fancy' mode the debugger will output queries with bound + * parameters inside the SQL itself. This method has been added to + * offer a convenient way to activate the fancy debugger system + * in one call. + * + * @param boolean $toggle TRUE to activate debugger and select 'fancy' mode + * + * @return void + */ + public static function fancyDebug( $toggle = TRUE ) + { + self::debug( $toggle, 2 ); + } + + /** + * Inspects the database schema. If you pass the type of a bean this + * method will return the fields of its table in the database. + * The keys of this array will be the field names and the values will be + * the column types used to store their values. + * If no type is passed, this method returns a list of all tables in the database. + * + * @param string $type Type of bean (i.e. table) you want to inspect + * + * @return array + */ + public static function inspect( $type = NULL ) + { + return ($type === NULL) ? self::$writer->getTables() : self::$writer->getColumns( $type ); + } + + /** + * Stores a bean in the database. This method takes a + * OODBBean Bean Object $bean and stores it + * in the database. If the database schema is not compatible + * with this bean and RedBean runs in fluid mode the schema + * will be altered to store the bean correctly. + * If the database schema is not compatible with this bean and + * RedBean runs in frozen mode it will throw an exception. + * This function returns the primary key ID of the inserted + * bean. + * + * The return value is an integer if possible. If it is not possible to + * represent the value as an integer a string will be returned. + * + * Usage: + * + * + * $post = R::dispense('post'); + * $post->title = 'my post'; + * $id = R::store( $post ); + * $post = R::load( 'post', $id ); + * R::trash( $post ); + * + * + * In the example above, we create a new bean of type 'post'. + * We then set the title of the bean to 'my post' and we + * store the bean. The store() method will return the primary + * key ID $id assigned by the database. We can now use this + * ID to load the bean from the database again and delete it. + * + * If the second parameter is set to TRUE and + * Hybrid mode is allowed (default OFF for novice), then RedBeanPHP + * will automatically temporarily switch to fluid mode to attempt to store the + * bean in case of an SQLException. + * + * @param OODBBean|SimpleModel $bean bean to store + * @param boolean $unfreezeIfNeeded retries in fluid mode in hybrid mode + * + * @return integer|string + */ + public static function store( $bean, $unfreezeIfNeeded = FALSE ) + { + $result = NULL; + try { + $result = self::$redbean->store( $bean ); + } catch (SQLException $exception) { + $wasFrozen = self::$redbean->isFrozen(); + if ( !self::$allowHybridMode || !$unfreezeIfNeeded ) throw $exception; + self::freeze( FALSE ); + $result = self::$redbean->store( $bean ); + self::freeze( $wasFrozen ); + } + return $result; + } + + /** + * Toggles fluid or frozen mode. In fluid mode the database + * structure is adjusted to accomodate your objects. In frozen mode + * this is not the case. + * + * You can also pass an array containing a selection of frozen types. + * Let's call this chilly mode, it's just like fluid mode except that + * certain types (i.e. tables) aren't touched. + * + * @param boolean|array $tf mode of operation (TRUE means frozen) + */ + public static function freeze( $tf = TRUE ) + { + self::$redbean->freeze( $tf ); + } + + /** + * Loads multiple types of beans with the same ID. + * This might look like a strange method, however it can be useful + * for loading a one-to-one relation. In a typical 1-1 relation, + * you have two records sharing the same primary key. + * RedBeanPHP has only limited support for 1-1 relations. + * In general it is recommended to use 1-N for this. + * + * Usage: + * + * + * list( $author, $bio ) = R::loadMulti( 'author, bio', $id ); + * + * + * @param string|array $types the set of types to load at once + * @param mixed $id the common ID + * + * @return OODBBean + */ + public static function loadMulti( $types, $id ) + { + return MultiLoader::load( self::$redbean, $types, $id ); + } + + /** + * Loads a bean from the object database. + * It searches for a OODBBean Bean Object in the + * database. It does not matter how this bean has been stored. + * RedBean uses the primary key ID $id and the string $type + * to find the bean. The $type specifies what kind of bean you + * are looking for; this is the same type as used with the + * dispense() function. If RedBean finds the bean it will return + * the OODB Bean object; if it cannot find the bean + * RedBean will return a new bean of type $type and with + * primary key ID 0. In the latter case it acts basically the + * same as dispense(). + * + * Important note: + * If the bean cannot be found in the database a new bean of + * the specified type will be generated and returned. + * + * Usage: + * + * + * $post = R::dispense('post'); + * $post->title = 'my post'; + * $id = R::store( $post ); + * $post = R::load( 'post', $id ); + * R::trash( $post ); + * + * + * In the example above, we create a new bean of type 'post'. + * We then set the title of the bean to 'my post' and we + * store the bean. The store() method will return the primary + * key ID $id assigned by the database. We can now use this + * ID to load the bean from the database again and delete it. + * + * @param string $type type of bean you want to load + * @param integer $id ID of the bean you want to load + * @param string $snippet string to use after select (optional) + * + * @return OODBBean + */ + public static function load( $type, $id, $snippet = NULL ) + { + if ( $snippet !== NULL ) self::$writer->setSQLSelectSnippet( $snippet ); + $bean = self::$redbean->load( $type, $id ); + return $bean; + } + + /** + * Same as load, but selects the bean for update, thus locking the bean. + * This equals an SQL query like 'SELECT ... FROM ... FOR UPDATE'. + * Use this method if you want to load a bean you intend to UPDATE. + * This method should be used to 'LOCK a bean'. + * + * Usage: + * + * + * $bean = R::loadForUpdate( 'bean', $id ); + * ...update... + * R::store( $bean ); + * + * + * @param string $type type of bean you want to load + * @param integer $id ID of the bean you want to load + * + * @return OODBBean + */ + public static function loadForUpdate( $type, $id ) + { + return self::load( $type, $id, AQueryWriter::C_SELECT_SNIPPET_FOR_UPDATE ); + } + + /** + * Same as find(), but selects the beans for update, thus locking the beans. + * This equals an SQL query like 'SELECT ... FROM ... FOR UPDATE'. + * Use this method if you want to load a bean you intend to UPDATE. + * This method should be used to 'LOCK a bean'. + * + * Usage: + * + * + * $bean = R::findForUpdate( + * 'bean', + * ' title LIKE ? ', + * array('title') + * ); + * ...update... + * R::store( $bean ); + * + * + * @param string $type the type of bean you are looking for + * @param string $sql SQL query to find the desired bean, starting right after WHERE clause + * @param array $bindings array of values to be bound to parameters in query + * + * @return array + */ + public static function findForUpdate( $type, $sql = NULL, $bindings = array() ) + { + return self::find( $type, $sql, $bindings, AQueryWriter::C_SELECT_SNIPPET_FOR_UPDATE ); + } + + /** + * Convenience method. + * Same as findForUpdate but returns just one bean and adds LIMIT-clause. + * + * @param string $type the type of bean you are looking for + * @param string $sql SQL query to find the desired bean, starting right after WHERE clause + * @param array $bindings array of values to be bound to parameters in query + * + * @return array + */ + public static function findOneForUpdate( $type, $sql = NULL, $bindings = array() ) + { + $sql = self::getWriter()->glueLimitOne( $sql ); + $beans = self::findForUpdate($type, $sql, $bindings); + return !empty($beans) ? reset($beans) : NULL; + } + + /** + * Removes a bean from the database. + * This function will remove the specified OODBBean + * Bean Object from the database. + * + * This facade method also accepts a type-id combination, + * in the latter case this method will attempt to load the specified bean + * and THEN trash it. + * + * Usage: + * + * + * $post = R::dispense('post'); + * $post->title = 'my post'; + * $id = R::store( $post ); + * $post = R::load( 'post', $id ); + * R::trash( $post ); + * + * + * In the example above, we create a new bean of type 'post'. + * We then set the title of the bean to 'my post' and we + * store the bean. The store() method will return the primary + * key ID $id assigned by the database. We can now use this + * ID to load the bean from the database again and delete it. + * + * @param string|OODBBean|SimpleModel $beanOrType bean you want to remove from database + * @param integer $id ID if the bean to trash (optional, type-id variant only) + * + * @return void + */ + public static function trash( $beanOrType, $id = NULL ) + { + if ( is_string( $beanOrType ) ) return self::trash( self::load( $beanOrType, $id ) ); + return self::$redbean->trash( $beanOrType ); + } + + /** + * Dispenses a new RedBean OODB Bean for use with + * the rest of the methods. RedBeanPHP thinks in beans, the bean is the + * primary way to interact with RedBeanPHP and the database managed by + * RedBeanPHP. To load, store and delete data from the database using RedBeanPHP + * you exchange these RedBeanPHP OODB Beans. The only exception to this rule + * are the raw query methods like R::getCell() or R::exec() and so on. + * The dispense method is the 'preferred way' to create a new bean. + * + * Usage: + * + * + * $book = R::dispense( 'book' ); + * $book->title = 'My Book'; + * R::store( $book ); + * + * + * This method can also be used to create an entire bean graph at once. + * Given an array with keys specifying the property names of the beans + * and a special _type key to indicate the type of bean, one can + * make the Dispense Helper generate an entire hierarchy of beans, including + * lists. To make dispense() generate a list, simply add a key like: + * ownXList or sharedXList where X is the type of beans it contains and + * a set its value to an array filled with arrays representing the beans. + * Note that, although the type may have been hinted at in the list name, + * you still have to specify a _type key for every bean array in the list. + * Note that, if you specify an array to generate a bean graph, the number + * parameter will be ignored. + * + * Usage: + * + * + * $book = R::dispense( [ + * '_type' => 'book', + * 'title' => 'Gifted Programmers', + * 'author' => [ '_type' => 'author', 'name' => 'Xavier' ], + * 'ownPageList' => [ ['_type'=>'page', 'text' => '...'] ] + * ] ); + * + * + * @param string|array $typeOrBeanArray type or bean array to import + * @param integer $num number of beans to dispense + * @param boolean $alwaysReturnArray if TRUE always returns the result as an array + * + * @return array|OODBBean + */ + public static function dispense( $typeOrBeanArray, $num = 1, $alwaysReturnArray = FALSE ) + { + return DispenseHelper::dispense( self::$redbean, $typeOrBeanArray, $num, $alwaysReturnArray ); + } + + /** + * Takes a comma separated list of bean types + * and dispenses these beans. For each type in the list + * you can specify the number of beans to be dispensed. + * + * Usage: + * + * + * list( $book, $page, $text ) = R::dispenseAll( 'book,page,text' ); + * + * + * This will dispense a book, a page and a text. This way you can + * quickly dispense beans of various types in just one line of code. + * + * Usage: + * + * + * list($book, $pages) = R::dispenseAll('book,page*100'); + * + * + * This returns an array with a book bean and then another array + * containing 100 page beans. + * + * @param string $order a description of the desired dispense order using the syntax above + * @param boolean $onlyArrays return only arrays even if amount < 2 + * + * @return array + */ + public static function dispenseAll( $order, $onlyArrays = FALSE ) + { + return DispenseHelper::dispenseAll( self::$redbean, $order, $onlyArrays ); + } + + /** + * Convience method. Tries to find beans of a certain type, + * if no beans are found, it dispenses a bean of that type. + * Note that this function always returns an array. + * + * @param string $type type of bean you are looking for + * @param string $sql SQL code for finding the bean + * @param array $bindings parameters to bind to SQL + * + * @return array + */ + public static function findOrDispense( $type, $sql = NULL, $bindings = array() ) + { + DispenseHelper::checkType( $type ); + return self::$finder->findOrDispense( $type, $sql, $bindings ); + } + + /** + * Same as findOrDispense but returns just one element. + * + * @param string $type type of bean you are looking for + * @param string $sql SQL code for finding the bean + * @param array $bindings parameters to bind to SQL + * + * @return OODBBean + */ + public static function findOneOrDispense( $type, $sql = NULL, $bindings = array() ) + { + DispenseHelper::checkType( $type ); + $arrayOfBeans = self::findOrDispense( $type, $sql, $bindings ); + return reset($arrayOfBeans); + } + + /** + * Finds beans using a type and optional SQL statement. + * As with most Query tools in RedBean you can provide values to + * be inserted in the SQL statement by populating the value + * array parameter; you can either use the question mark notation + * or the slot-notation (:keyname). + * + * Your SQL does not have to start with a WHERE-clause condition. + * + * @param string $type the type of bean you are looking for + * @param string $sql SQL query to find the desired bean, starting right after WHERE clause + * @param array $bindings array of values to be bound to parameters in query + * @param string $snippet SQL snippet to include in query (for example: FOR UPDATE) + * + * @return array + */ + public static function find( $type, $sql = NULL, $bindings = array(), $snippet = NULL ) + { + if ( $snippet !== NULL ) self::$writer->setSQLSelectSnippet( $snippet ); + return self::$finder->find( $type, $sql, $bindings ); + } + + /** + * Alias for find(). + * + * @param string $type the type of bean you are looking for + * @param string $sql SQL query to find the desired bean, starting right after WHERE clause + * @param array $bindings array of values to be bound to parameters in query + * + * @return array + */ + public static function findAll( $type, $sql = NULL, $bindings = array() ) + { + return self::$finder->find( $type, $sql, $bindings ); + } + + /** + * Like find() but also exports the beans as an array. + * This method will perform a find-operation. For every bean + * in the result collection this method will call the export() method. + * This method returns an array containing the array representations + * of every bean in the result set. + * + * @see Finder::find + * + * @param string $type type the type of bean you are looking for + * @param string $sql sql SQL query to find the desired bean, starting right after WHERE clause + * @param array $bindings values array of values to be bound to parameters in query + * + * @return array + */ + public static function findAndExport( $type, $sql = NULL, $bindings = array() ) + { + return self::$finder->findAndExport( $type, $sql, $bindings ); + } + + /** + * Like R::find() but returns the first bean only. + * + * @param string $type the type of bean you are looking for + * @param string $sql SQL query to find the desired bean, starting right after WHERE clause + * @param array $bindings array of values to be bound to parameters in query + * + * @return OODBBean|NULL + */ + public static function findOne( $type, $sql = NULL, $bindings = array() ) + { + return self::$finder->findOne( $type, $sql, $bindings ); + } + + /** + * @deprecated + * + * Like find() but returns the last bean of the result array. + * Opposite of Finder::findLast(). + * If no beans are found, this method will return NULL. + * + * Please do not use this function, it is horribly ineffective. + * Instead use a reversed ORDER BY clause and a LIMIT 1 with R::findOne(). + * This function should never be used and only remains for + * the sake of backward compatibility. + * + * @see Finder::find + * + * @param string $type the type of bean you are looking for + * @param string $sql SQL query to find the desired bean, starting right after WHERE clause + * @param array $bindings values array of values to be bound to parameters in query + * + * @return OODBBean|NULL + */ + public static function findLast( $type, $sql = NULL, $bindings = array() ) + { + return self::$finder->findLast( $type, $sql, $bindings ); + } + + /** + * Finds a BeanCollection using the repository. + * A bean collection can be used to retrieve one bean at a time using + * cursors - this is useful for processing large datasets. A bean collection + * will not load all beans into memory all at once, just one at a time. + * + * @param string $type the type of bean you are looking for + * @param string $sql SQL query to find the desired bean, starting right after WHERE clause + * @param array $bindings values array of values to be bound to parameters in query + * + * @return BeanCollection + */ + public static function findCollection( $type, $sql = NULL, $bindings = array() ) + { + return self::$finder->findCollection( $type, $sql, $bindings ); + } + + /** + * Returns a hashmap with bean arrays keyed by type using an SQL + * query as its resource. Given an SQL query like 'SELECT movie.*, review.* FROM movie... JOIN review' + * this method will return movie and review beans. + * + * Example: + * + * + * $stuff = $finder->findMulti('movie,review', ' + * SELECT movie.*, review.* FROM movie + * LEFT JOIN review ON review.movie_id = movie.id'); + * + * + * After this operation, $stuff will contain an entry 'movie' containing all + * movies and an entry named 'review' containing all reviews (all beans). + * You can also pass bindings. + * + * If you want to re-map your beans, so you can use $movie->ownReviewList without + * having RedBeanPHP executing an SQL query you can use the fourth parameter to + * define a selection of remapping closures. + * + * The remapping argument (optional) should contain an array of arrays. + * Each array in the remapping array should contain the following entries: + * + * + * array( + * 'a' => TYPE A + * 'b' => TYPE B + * 'matcher' => MATCHING FUNCTION ACCEPTING A, B and ALL BEANS + * 'do' => OPERATION FUNCTION ACCEPTING A, B, ALL BEANS, ALL REMAPPINGS + * ) + * + * + * Using this mechanism you can build your own 'preloader' with tiny function + * snippets (and those can be re-used and shared online of course). + * + * Example: + * + * + * array( + * 'a' => 'movie' //define A as movie + * 'b' => 'review' //define B as review + * 'matcher' => function( $a, $b ) { + * return ( $b->movie_id == $a->id ); //Perform action if review.movie_id equals movie.id + * } + * 'do' => function( $a, $b ) { + * $a->noLoad()->ownReviewList[] = $b; //Add the review to the movie + * $a->clearHistory(); //optional, act 'as if these beans have been loaded through ownReviewList'. + * } + * ) + * + * + * @note the SQL query provided IS NOT THE ONE used internally by this function, + * this function will pre-process the query to get all the data required to find the beans. + * + * @note if you use the 'book.*' notation make SURE you're + * selector starts with a SPACE. ' book.*' NOT ',book.*'. This is because + * it's actually an SQL-like template SLOT, not real SQL. + * + * @note instead of an SQL query you can pass a result array as well. + * + * @param string|array $types a list of types (either array or comma separated string) + * @param string|array $sql an SQL query or an array of prefetched records + * @param array $bindings optional, bindings for SQL query + * @param array $remappings optional, an array of remapping arrays + * + * @return array + */ + public static function findMulti( $types, $sql, $bindings = array(), $remappings = array() ) + { + return self::$finder->findMulti( $types, $sql, $bindings, $remappings ); + } + + /** + * Returns an array of beans. Pass a type and a series of ids and + * this method will bring you the corresponding beans. + * + * important note: Because this method loads beans using the load() + * function (but faster) it will return empty beans with ID 0 for + * every bean that could not be located. The resulting beans will have the + * passed IDs as their keys. + * + * @param string $type type of beans + * @param array $ids ids to load + * + * @return array + */ + public static function batch( $type, $ids ) + { + return self::$redbean->batch( $type, $ids ); + } + + /** + * Alias for batch(). Batch method is older but since we added so-called *All + * methods like storeAll, trashAll, dispenseAll and findAll it seemed logical to + * improve the consistency of the Facade API and also add an alias for batch() called + * loadAll. + * + * @param string $type type of beans + * @param array $ids ids to load + * + * @return array + */ + public static function loadAll( $type, $ids ) + { + return self::$redbean->batch( $type, $ids ); + } + + /** + * Convenience function to execute Queries directly. + * Executes SQL. + * + * @param string $sql SQL query to execute + * @param array $bindings a list of values to be bound to query parameters + * + * @return integer + */ + public static function exec( $sql, $bindings = array() ) + { + return self::query( 'exec', $sql, $bindings ); + } + + /** + * Convenience function to fire an SQL query using the RedBeanPHP + * database adapter. This method allows you to directly query the + * database without having to obtain an database adapter instance first. + * Executes the specified SQL query together with the specified + * parameter bindings and returns all rows + * and all columns. + * + * @param string $sql SQL query to execute + * @param array $bindings a list of values to be bound to query parameters + * + * @return array + */ + public static function getAll( $sql, $bindings = array() ) + { + return self::query( 'get', $sql, $bindings ); + } + + /** + * Convenience function to fire an SQL query using the RedBeanPHP + * database adapter. This method allows you to directly query the + * database without having to obtain an database adapter instance first. + * Executes the specified SQL query together with the specified + * parameter bindings and returns a single cell. + * + * @param string $sql SQL query to execute + * @param array $bindings a list of values to be bound to query parameters + * + * @return string + */ + public static function getCell( $sql, $bindings = array() ) + { + return self::query( 'getCell', $sql, $bindings ); + } + + /** + * Convenience function to fire an SQL query using the RedBeanPHP + * database adapter. This method allows you to directly query the + * database without having to obtain an database adapter instance first. + * Executes the specified SQL query together with the specified + * parameter bindings and returns a PDOCursor instance. + * + * @param string $sql SQL query to execute + * @param array $bindings a list of values to be bound to query parameters + * + * @return RedBeanPHP\Cursor\PDOCursor + */ + public static function getCursor( $sql, $bindings = array() ) + { + return self::query( 'getCursor', $sql, $bindings ); + } + + /** + * Convenience function to fire an SQL query using the RedBeanPHP + * database adapter. This method allows you to directly query the + * database without having to obtain an database adapter instance first. + * Executes the specified SQL query together with the specified + * parameter bindings and returns a single row. + * + * @param string $sql SQL query to execute + * @param array $bindings a list of values to be bound to query parameters + * + * @return array + */ + public static function getRow( $sql, $bindings = array() ) + { + return self::query( 'getRow', $sql, $bindings ); + } + + /** + * Convenience function to fire an SQL query using the RedBeanPHP + * database adapter. This method allows you to directly query the + * database without having to obtain an database adapter instance first. + * Executes the specified SQL query together with the specified + * parameter bindings and returns a single column. + * + * @param string $sql SQL query to execute + * @param array $bindings a list of values to be bound to query parameters + * + * @return array + */ + public static function getCol( $sql, $bindings = array() ) + { + return self::query( 'getCol', $sql, $bindings ); + } + + /** + * Convenience function to execute Queries directly. + * Executes SQL. + * Results will be returned as an associative array. The first + * column in the select clause will be used for the keys in this array and + * the second column will be used for the values. If only one column is + * selected in the query, both key and value of the array will have the + * value of this field for each row. + * + * @param string $sql SQL query to execute + * @param array $bindings a list of values to be bound to query parameters + * + * @return array + */ + public static function getAssoc( $sql, $bindings = array() ) + { + return self::query( 'getAssoc', $sql, $bindings ); + } + + /** + *Convenience function to fire an SQL query using the RedBeanPHP + * database adapter. This method allows you to directly query the + * database without having to obtain an database adapter instance first. + * Executes the specified SQL query together with the specified + * parameter bindings and returns an associative array. + * Results will be returned as an associative array indexed by the first + * column in the select. + * + * @param string $sql SQL query to execute + * @param array $bindings a list of values to be bound to query parameters + * + * @return array + */ + public static function getAssocRow( $sql, $bindings = array() ) + { + return self::query( 'getAssocRow', $sql, $bindings ); + } + + /** + * Returns the insert ID for databases that support/require this + * functionality. Alias for R::getAdapter()->getInsertID(). + * + * @return mixed + */ + public static function getInsertID() + { + return self::$adapter->getInsertID(); + } + + /** + * Makes a copy of a bean. This method makes a deep copy + * of the bean.The copy will have the following features. + * - All beans in own-lists will be duplicated as well + * - All references to shared beans will be copied but not the shared beans themselves + * - All references to parent objects (_id fields) will be copied but not the parents themselves + * In most cases this is the desired scenario for copying beans. + * This function uses a trail-array to prevent infinite recursion, if a recursive bean is found + * (i.e. one that already has been processed) the ID of the bean will be returned. + * This should not happen though. + * + * Note: + * This function does a reflectional database query so it may be slow. + * + * @deprecated + * This function is deprecated in favour of R::duplicate(). + * This function has a confusing method signature, the R::duplicate() function + * only accepts two arguments: bean and filters. + * + * @param OODBBean $bean bean to be copied + * @param array $trail for internal usage, pass array() + * @param boolean $pid for internal usage + * @param array $filters white list filter with bean types to duplicate + * + * @return array + */ + public static function dup( $bean, $trail = array(), $pid = FALSE, $filters = array() ) + { + self::$duplicationManager->setFilters( $filters ); + return self::$duplicationManager->dup( $bean, $trail, $pid ); + } + + /** + * Makes a deep copy of a bean. This method makes a deep copy + * of the bean.The copy will have the following: + * + * * All beans in own-lists will be duplicated as well + * * All references to shared beans will be copied but not the shared beans themselves + * * All references to parent objects (_id fields) will be copied but not the parents themselves + * + * In most cases this is the desired scenario for copying beans. + * This function uses a trail-array to prevent infinite recursion, if a recursive bean is found + * (i.e. one that already has been processed) the ID of the bean will be returned. + * This should not happen though. + * + * Note: + * This function does a reflectional database query so it may be slow. + * + * Note: + * This is a simplified version of the deprecated R::dup() function. + * + * @param OODBBean $bean bean to be copied + * @param array $white white list filter with bean types to duplicate + * + * @return array + */ + public static function duplicate( $bean, $filters = array() ) + { + return self::dup( $bean, array(), FALSE, $filters ); + } + + /** + * Exports a collection of beans. Handy for XML/JSON exports with a + * Javascript framework like Dojo or ExtJS. + * What will be exported: + * + * * contents of the bean + * * all own bean lists (recursively) + * * all shared beans (not THEIR own lists) + * + * @param array|OODBBean $beans beans to be exported + * @param boolean $parents whether you want parent beans to be exported + * @param array $filters whitelist of types + * @param boolean $meta export meta data as well + * + * @return array + */ + public static function exportAll( $beans, $parents = FALSE, $filters = array(), $meta = FALSE ) + { + return self::$duplicationManager->exportAll( $beans, $parents, $filters, self::$exportCaseStyle, $meta ); + } + + /** + * Selects case style for export. + * This will determine the case style for the keys of exported beans (see exportAll). + * The following options are accepted: + * + * * 'default' RedBeanPHP by default enforces Snake Case (i.e. book_id is_valid ) + * * 'camel' Camel Case (i.e. bookId isValid ) + * * 'dolphin' Dolphin Case (i.e. bookID isValid ) Like CamelCase but ID is written all uppercase + * + * @warning RedBeanPHP transforms camelCase to snake_case using a slightly different + * algorithm, it also converts isACL to is_acl (not is_a_c_l) and bookID to book_id. + * Due to information loss this cannot be corrected. However if you might try + * DolphinCase for IDs it takes into account the exception concerning IDs. + * + * @param string $caseStyle case style identifier + * + * @return void + */ + public static function useExportCase( $caseStyle = 'default' ) + { + if ( !in_array( $caseStyle, array( 'default', 'camel', 'dolphin' ) ) ) throw new RedException( 'Invalid case selected.' ); + self::$exportCaseStyle = $caseStyle; + } + + /** + * Converts a series of rows to beans. + * This method converts a series of rows to beans. + * The type of the desired output beans can be specified in the + * first parameter. The second parameter is meant for the database + * result rows. + * + * Usage: + * + * + * $rows = R::getAll( 'SELECT * FROM ...' ) + * $beans = R::convertToBeans( $rows ); + * + * + * As of version 4.3.2 you can specify a meta-mask. + * Data from columns with names starting with the value specified in the mask + * will be transferred to the meta section of a bean (under data.bundle). + * + * + * $rows = R::getAll( 'SELECT FROM... COUNT(*) AS extra_count ...' ); + * $beans = R::convertToBeans( $rows, 'extra_' ); + * $bean = reset( $beans ); + * $data = $bean->getMeta( 'data.bundle' ); + * $extra_count = $data['extra_count']; + * + * + * New in 4.3.2: meta mask. The meta mask is a special mask to send + * data from raw result rows to the meta store of the bean. This is + * useful for bundling additional information with custom queries. + * Values of every column whos name starts with $mask will be + * transferred to the meta section of the bean under key 'data.bundle'. + * + * @param string $type type of beans to produce + * @param array $rows must contain an array of array + * @param string $metamask meta mask to apply (optional) + * + * @return array + */ + public static function convertToBeans( $type, $rows, $metamask = NULL ) + { + return self::$redbean->convertToBeans( $type, $rows, $metamask ); + } + + /** + * Just like converToBeans, but for one bean. + * + * @param string $type type of bean to produce + * @param array $row one row from the database + * @param string $metamask metamask (see convertToBeans) + * + * @return OODBBean|NULL + */ + public static function convertToBean( $type, $row, $metamask = NULL ) + { + if ( !count( $row ) ) return NULL; + $beans = self::$redbean->convertToBeans( $type, array( $row ), $metamask ); + $bean = reset( $beans ); + return $bean; + } + + /** + * Convenience function to 'find' beans from an SQL query. + * Used mostly to obtain a series of beans as well as + * pagination data (to paginate results) and optionally + * other data as well (that should not be considered part of + * a bean). + * + * Example: + * + * $books = R::findFromSQL('book'," + * SELECT *, count(*) OVER() AS total + * FROM book + * WHERE {$filter} + * OFFSET {$from} LIMIT {$to} ", ['total']); + * + * This is the same as doing (example uses PostgreSQL dialect): + * + * $rows = R::getAll(" + * SELECT *, count(*) OVER() AS total + * FROM book + * WHERE {$filter} + * OFFSET {$from} LIMIT {$to} + * ", $params); + * $books = R::convertToBeans('book', $rows, ['total']); + * + * The additional data can be obtained using: + * + * $book->info('total'); + * + * For further details see R::convertToBeans(). + * If you set $autoExtract to TRUE and meta mask is an array, + * an array will be returned containing two nested arrays, the + * first of those nested arrays will contain the meta values + * you requested, the second array will contain the beans. + * + * @param string $type Type of bean to produce + * @param string $sql SQL query snippet to use + * @param array $bindings bindings for query (optional) + * @param mixed $metamask meta mask (optional, defaults to 'extra_') + * @param boolean $autoExtract TRUE to return meta mask values as first item of array + * + * @return array + */ + public static function findFromSQL( $type, $sql, $bindings = array(), $metamask = 'extra_', $autoExtract = false) { + $rows = self::query( 'get', $sql, $bindings ); + $beans = array(); + if (count($rows)) $beans = self::$redbean->convertToBeans( $type, $rows, $metamask ); + if ($autoExtract && is_array($metamask)) { + $values = array(); + $firstBean = NULL; + if (count($beans)) $firstBean = reset($beans); + foreach($metamask as $key) { + $values[$key] = ($firstBean) ? $firstBean->info($key) : NULL; + } + return array( $values, $beans ); + } + return $beans; + } + + /** + * Tests whether a bean has been associated with one ore more + * of the listed tags. If the third parameter is TRUE this method + * will return TRUE only if all tags that have been specified are indeed + * associated with the given bean, otherwise FALSE. + * If the third parameter is FALSE this + * method will return TRUE if one of the tags matches, FALSE if none + * match. + * + * Tag list can be either an array with tag names or a comma separated list + * of tag names. + * + * Usage: + * + * + * R::hasTag( $blog, 'horror,movie', TRUE ); + * + * + * The example above returns TRUE if the $blog bean has been tagged + * as BOTH horror and movie. If the post has only been tagged as 'movie' + * or 'horror' this operation will return FALSE because the third parameter + * has been set to TRUE. + * + * @param OODBBean $bean bean to check for tags + * @param array|string $tags list of tags + * @param boolean $all whether they must all match or just some + * + * @return boolean + */ + public static function hasTag( $bean, $tags, $all = FALSE ) + { + return self::$tagManager->hasTag( $bean, $tags, $all ); + } + + /** + * Removes all specified tags from the bean. The tags specified in + * the second parameter will no longer be associated with the bean. + * + * Tag list can be either an array with tag names or a comma separated list + * of tag names. + * + * Usage: + * + * + * R::untag( $blog, 'smart,interesting' ); + * + * + * In the example above, the $blog bean will no longer + * be associated with the tags 'smart' and 'interesting'. + * + * @param OODBBean $bean tagged bean + * @param array $tagList list of tags (names) + * + * @return void + */ + public static function untag( $bean, $tagList ) + { + self::$tagManager->untag( $bean, $tagList ); + } + + /** + * Tags a bean or returns tags associated with a bean. + * If $tagList is NULL or omitted this method will return a + * comma separated list of tags associated with the bean provided. + * If $tagList is a comma separated list (string) of tags all tags will + * be associated with the bean. + * You may also pass an array instead of a string. + * + * Usage: + * + * + * R::tag( $meal, "TexMex,Mexican" ); + * $tags = R::tag( $meal ); + * + * + * The first line in the example above will tag the $meal + * as 'TexMex' and 'Mexican Cuisine'. The second line will + * retrieve all tags attached to the meal object. + * + * @param OODBBean $bean bean to tag + * @param mixed $tagList tags to attach to the specified bean + * + * @return string + */ + public static function tag( OODBBean $bean, $tagList = NULL ) + { + return self::$tagManager->tag( $bean, $tagList ); + } + + /** + * Adds tags to a bean. + * If $tagList is a comma separated list of tags all tags will + * be associated with the bean. + * You may also pass an array instead of a string. + * + * Usage: + * + * + * R::addTags( $blog, ["halloween"] ); + * + * + * The example adds the tag 'halloween' to the $blog + * bean. + * + * @param OODBBean $bean bean to tag + * @param array $tagList list of tags to add to bean + * + * @return void + */ + public static function addTags( OODBBean $bean, $tagList ) + { + self::$tagManager->addTags( $bean, $tagList ); + } + + /** + * Returns all beans that have been tagged with one or more + * of the specified tags. + * + * Tag list can be either an array with tag names or a comma separated list + * of tag names. + * + * Usage: + * + * + * $watchList = R::tagged( + * 'movie', + * 'horror,gothic', + * ' ORDER BY movie.title DESC LIMIT ?', + * [ 10 ] + * ); + * + * + * The example uses R::tagged() to find all movies that have been + * tagged as 'horror' or 'gothic', order them by title and limit + * the number of movies to be returned to 10. + * + * @param string $beanType type of bean you are looking for + * @param array|string $tagList list of tags to match + * @param string $sql additional SQL (use only for pagination) + * @param array $bindings bindings + * + * @return array + */ + public static function tagged( $beanType, $tagList, $sql = '', $bindings = array() ) + { + return self::$tagManager->tagged( $beanType, $tagList, $sql, $bindings ); + } + + /** + * Returns all beans that have been tagged with ALL of the tags given. + * This method works the same as R::tagged() except that this method only returns + * beans that have been tagged with all the specified labels. + * + * Tag list can be either an array with tag names or a comma separated list + * of tag names. + * + * Usage: + * + * + * $watchList = R::taggedAll( + * 'movie', + * [ 'gothic', 'short' ], + * ' ORDER BY movie.id DESC LIMIT ? ', + * [ 4 ] + * ); + * + * + * The example above returns at most 4 movies (due to the LIMIT clause in the SQL + * Query Snippet) that have been tagged as BOTH 'short' AND 'gothic'. + * + * @param string $beanType type of bean you are looking for + * @param array|string $tagList list of tags to match + * @param string $sql additional sql snippet + * @param array $bindings bindings + * + * @return array + */ + public static function taggedAll( $beanType, $tagList, $sql = '', $bindings = array() ) + { + return self::$tagManager->taggedAll( $beanType, $tagList, $sql, $bindings ); + } + + /** + * Same as taggedAll() but counts beans only (does not return beans). + * + * @see R::taggedAll + * + * @param string $beanType type of bean you are looking for + * @param array|string $tagList list of tags to match + * @param string $sql additional sql snippet + * @param array $bindings bindings + * + * @return integer + */ + public static function countTaggedAll( $beanType, $tagList, $sql = '', $bindings = array() ) + { + return self::$tagManager->countTaggedAll( $beanType, $tagList, $sql, $bindings ); + } + + /** + * Same as tagged() but counts beans only (does not return beans). + * + * @see R::tagged + * + * @param string $beanType type of bean you are looking for + * @param array|string $tagList list of tags to match + * @param string $sql additional sql snippet + * @param array $bindings bindings + * + * @return integer + */ + public static function countTagged( $beanType, $tagList, $sql = '', $bindings = array() ) + { + return self::$tagManager->countTagged( $beanType, $tagList, $sql, $bindings ); + } + + /** + * Wipes all beans of type $beanType. + * + * @param string $beanType type of bean you want to destroy entirely + * + * @return boolean + */ + public static function wipe( $beanType ) + { + return Facade::$redbean->wipe( $beanType ); + } + + /** + * Counts the number of beans of type $type. + * This method accepts a second argument to modify the count-query. + * A third argument can be used to provide bindings for the SQL snippet. + * + * @param string $type type of bean we are looking for + * @param string $addSQL additional SQL snippet + * @param array $bindings parameters to bind to SQL + * + * @return integer + */ + public static function count( $type, $addSQL = '', $bindings = array() ) + { + return Facade::$redbean->count( $type, $addSQL, $bindings ); + } + + /** + * Configures the facade, want to have a new Writer? A new Object Database or a new + * Adapter and you want it on-the-fly? Use this method to hot-swap your facade with a new + * toolbox. + * + * @param ToolBox $tb toolbox to configure facade with + * + * @return ToolBox + */ + public static function configureFacadeWithToolbox( ToolBox $tb ) + { + $oldTools = self::$toolbox; + self::$toolbox = $tb; + self::$writer = self::$toolbox->getWriter(); + self::$adapter = self::$toolbox->getDatabaseAdapter(); + self::$redbean = self::$toolbox->getRedBean(); + self::$finder = new Finder( self::$toolbox ); + self::$associationManager = new AssociationManager( self::$toolbox ); + self::$tree = new Tree( self::$toolbox ); + self::$redbean->setAssociationManager( self::$associationManager ); + self::$labelMaker = new LabelMaker( self::$toolbox ); + $helper = new SimpleModelHelper(); + $helper->attachEventListeners( self::$redbean ); + if (self::$redbean->getBeanHelper() == NULL) { + self::$redbean->setBeanHelper( new SimpleFacadeBeanHelper ); + } + self::$duplicationManager = new DuplicationManager( self::$toolbox ); + self::$tagManager = new TagManager( self::$toolbox ); + return $oldTools; + } + + /** + * Facade Convience method for adapter transaction system. + * Begins a transaction. + * + * Usage: + * + * + * R::begin(); + * try { + * $bean1 = R::dispense( 'bean' ); + * R::store( $bean1 ); + * $bean2 = R::dispense( 'bean' ); + * R::store( $bean2 ); + * R::commit(); + * } catch( \Exception $e ) { + * R::rollback(); + * } + * + * + * The example above illustrates how transactions in RedBeanPHP are used. + * In this example 2 beans are stored or nothing is stored at all. + * It's not possible for this piece of code to store only half of the beans. + * If an exception occurs, the transaction gets rolled back and the database + * will be left 'untouched'. + * + * In fluid mode transactions will be ignored and all queries will + * be executed as-is because database schema changes will automatically + * trigger the transaction system to commit everything in some database + * systems. If you use a database that can handle DDL changes you might wish + * to use setAllowFluidTransactions(TRUE). If you do this, the behavior of + * this function in fluid mode will depend on the database platform used. + * + * @return bool + */ + public static function begin() + { + if ( !self::$allowFluidTransactions && !self::$redbean->isFrozen() ) return FALSE; + self::$adapter->startTransaction(); + return TRUE; + } + + /** + * Facade Convience method for adapter transaction system. + * Commits a transaction. + * + * Usage: + * + * + * R::begin(); + * try { + * $bean1 = R::dispense( 'bean' ); + * R::store( $bean1 ); + * $bean2 = R::dispense( 'bean' ); + * R::store( $bean2 ); + * R::commit(); + * } catch( \Exception $e ) { + * R::rollback(); + * } + * + * + * The example above illustrates how transactions in RedBeanPHP are used. + * In this example 2 beans are stored or nothing is stored at all. + * It's not possible for this piece of code to store only half of the beans. + * If an exception occurs, the transaction gets rolled back and the database + * will be left 'untouched'. + * + * In fluid mode transactions will be ignored and all queries will + * be executed as-is because database schema changes will automatically + * trigger the transaction system to commit everything in some database + * systems. If you use a database that can handle DDL changes you might wish + * to use setAllowFluidTransactions(TRUE). If you do this, the behavior of + * this function in fluid mode will depend on the database platform used. + * + * @return bool + */ + public static function commit() + { + if ( !self::$allowFluidTransactions && !self::$redbean->isFrozen() ) return FALSE; + self::$adapter->commit(); + return TRUE; + } + + /** + * Facade Convience method for adapter transaction system. + * Rolls back a transaction. + * + * Usage: + * + * + * R::begin(); + * try { + * $bean1 = R::dispense( 'bean' ); + * R::store( $bean1 ); + * $bean2 = R::dispense( 'bean' ); + * R::store( $bean2 ); + * R::commit(); + * } catch( \Exception $e ) { + * R::rollback(); + * } + * + * + * The example above illustrates how transactions in RedBeanPHP are used. + * In this example 2 beans are stored or nothing is stored at all. + * It's not possible for this piece of code to store only half of the beans. + * If an exception occurs, the transaction gets rolled back and the database + * will be left 'untouched'. + * + * In fluid mode transactions will be ignored and all queries will + * be executed as-is because database schema changes will automatically + * trigger the transaction system to commit everything in some database + * systems. If you use a database that can handle DDL changes you might wish + * to use setAllowFluidTransactions(TRUE). If you do this, the behavior of + * this function in fluid mode will depend on the database platform used. + * + * @return bool + */ + public static function rollback() + { + if ( !self::$allowFluidTransactions && !self::$redbean->isFrozen() ) return FALSE; + self::$adapter->rollback(); + return TRUE; + } + + /** + * Returns a list of columns. Format of this array: + * array( fieldname => type ) + * Note that this method only works in fluid mode because it might be + * quite heavy on production servers! + * + * @param string $table name of the table (not type) you want to get columns of + * + * @return array + */ + public static function getColumns( $table ) + { + return self::$writer->getColumns( $table ); + } + + /** + * Generates question mark slots for an array of values. + * Given an array and an optional template string this method + * will produce string containing parameter slots for use in + * an SQL query string. + * + * Usage: + * + * + * R::genSlots( array( 'a', 'b' ) ); + * + * + * The statement in the example will produce the string: + * '?,?'. + * + * Another example, using a template string: + * + * + * R::genSlots( array('a', 'b'), ' IN( %s ) ' ); + * + * + * The statement in the example will produce the string: + * ' IN( ?,? ) '. + * + * @param array $array array to generate question mark slots for + * @param string $template template to use + * + * @return string + */ + public static function genSlots( $array, $template = NULL ) + { + return ArrayTool::genSlots( $array, $template ); + } + + /** + * Convenience method to quickly attach parent beans. + * Although usually this can also be done with findMulti(), that + * approach can be a bit verbose sometimes. This convenience method + * uses a default yet overridable SQL snippet to perform the + * operation, leveraging the power of findMulti(). + * + * Usage: + * + * + * $users = R::find('user'); + * $users = R::loadJoined( $users, 'country' ); + * + * + * This is an alternative for: + * + * + * $all = R::findMulti('country', + * R::genSlots( $users, + * 'SELECT country.* FROM country WHERE id IN ( %s )' ), + * array_column( $users, 'country_id' ), + * [Finder::onmap('country', $gebruikers)] + * ); + * + * + * @param array $beans a list of OODBBeans + * @param string $type a type string + * @param string $sqlTemplate an SQL template string for the SELECT-query + * + * @return array + */ + public static function loadJoined( $beans, $type, $sqlTemplate = 'SELECT %s.* FROM %s WHERE id IN (%s)' ) + { + if (!count($beans)) return array(); + $ids = array(); + $key = "{$type}_id"; + foreach( $beans as $bean ) $ids[] = $bean->{$key}; + $result = self::findMulti($type, self::genSlots( $beans,sprintf($sqlTemplate, $type, $type, '%s')), $ids, array( Finder::onmap($type, $beans) ) ); + $bean = reset($beans); + return $result[ $bean->getMeta('type') ]; + } + + /** + * Flattens a multi dimensional bindings array for use with genSlots(). + * + * Usage: + * + * + * R::flat( array( 'a', array( 'b' ), 'c' ) ); + * + * + * produces an array like: [ 'a', 'b', 'c' ] + * + * @param array $array array to flatten + * @param array $result result array parameter (for recursion) + * + * @return array + */ + public static function flat( $array, $result = array() ) + { + return ArrayTool::flat( $array, $result ); + } + + /** + * Nukes the entire database. + * This will remove all schema structures from the database. + * Only works in fluid mode. Be careful with this method. + * + * @warning dangerous method, will remove all tables, columns etc. + * + * @return void + */ + public static function nuke() + { + return self::wipeAll( TRUE ); + } + + /** + * Truncates or drops all database tables/views. + * Empties the database. If the deleteTables flag is set to TRUE + * this function will also remove the database structures. + * The latter only works in fluid mode. + * + * @param boolean $alsoDeleteTables TRUE to clear entire database. + * + * @return void + */ + public static function wipeAll( $alsoDeleteTables = FALSE ) + { + if ( $alsoDeleteTables ) { + if ( !self::$redbean->isFrozen() ) { + self::$writer->wipeAll(); + } + } else { + foreach ( self::$writer->getTables() as $table ) { + self::wipe( $table ); + } + } + } + + /** + * Short hand function to store a set of beans at once, IDs will be + * returned as an array. For information please consult the R::store() + * function. + * A loop saver. + * + * If the second parameter is set to TRUE and + * Hybrid mode is allowed (default OFF for novice), then RedBeanPHP + * will automatically temporarily switch to fluid mode to attempt to store the + * bean in case of an SQLException. + * + * @param array $beans list of beans to be stored + * @param boolean $unfreezeIfNeeded retries in fluid mode in hybrid mode + * + * @return array + */ + public static function storeAll( $beans, $unfreezeIfNeeded = FALSE ) + { + $ids = array(); + foreach ( $beans as $bean ) { + $ids[] = self::store( $bean, $unfreezeIfNeeded ); + } + return $ids; + } + + /** + * Short hand function to trash a set of beans at once. + * For information please consult the R::trash() function. + * A loop saver. + * + * @param array $beans list of beans to be trashed + * + * @return void + */ + public static function trashAll( $beans ) + { + $numberOfDeletion = 0; + foreach ( $beans as $bean ) { + $numberOfDeletion += self::trash( $bean ); + } + return $numberOfDeletion; + } + + /** + * Short hand function to trash a series of beans using + * only IDs. This function combines trashAll and batch loading + * in one call. Note that while this function accepts just + * bean IDs, the beans will still be loaded first. This is because + * the function still respects all the FUSE hooks that may have beeb + * associated with the domain logic associated with these beans. + * If you really want to delete just records from the database use + * a simple DELETE-FROM SQL query instead. + * + * @param string type $type the bean type you wish to trash + * @param string array $ids list of bean IDs + * + * @return void + */ + public static function trashBatch( $type, $ids ) + { + self::trashAll( self::batch( $type, $ids ) ); + } + + /** + * Short hand function to find and trash beans. + * This function combines trashAll and find. + * Given a bean type, a query snippet and optionally some parameter + * bindings, this function will search for the beans described in the + * query and its parameters and then feed them to the trashAll function + * to be trashed. + * + * Note that while this function accepts just + * a bean type and query snippet, the beans will still be loaded first. This is because + * the function still respects all the FUSE hooks that may have been + * associated with the domain logic associated with these beans. + * If you really want to delete just records from the database use + * a simple DELETE-FROM SQL query instead. + * + * Returns the number of beans deleted. + * + * @param string $type bean type to look for in database + * @param string $sqlSnippet an SQL query snippet + * @param array $bindings SQL parameter bindings + * + * @return int + */ + public static function hunt( $type, $sqlSnippet = NULL, $bindings = array() ) + { + $numberOfTrashedBeans = 0; + $beans = self::findCollection( $type, $sqlSnippet, $bindings ); + while( $bean = $beans->next() ) { + self::trash( $bean ); + $numberOfTrashedBeans++; + } + return $numberOfTrashedBeans; + } + + /** + * Toggles Writer Cache. + * Turns the Writer Cache on or off. The Writer Cache is a simple + * query based caching system that may improve performance without the need + * for cache management. This caching system will cache non-modifying queries + * that are marked with special SQL comments. As soon as a non-marked query + * gets executed the cache will be flushed. Only non-modifying select queries + * have been marked therefore this mechanism is a rather safe way of caching, requiring + * no explicit flushes or reloads. Of course this does not apply if you intend to test + * or simulate concurrent querying. + * + * @param boolean $yesNo TRUE to enable cache, FALSE to disable cache + * + * @return void + */ + public static function useWriterCache( $yesNo ) + { + self::getWriter()->setUseCache( $yesNo ); + } + + /** + * A label is a bean with only an id, type and name property. + * This function will dispense beans for all entries in the array. The + * values of the array will be assigned to the name property of each + * individual bean. + * + * @param string $type type of beans you would like to have + * @param array $labels list of labels, names for each bean + * + * @return array + */ + public static function dispenseLabels( $type, $labels ) + { + return self::$labelMaker->dispenseLabels( $type, $labels ); + } + + /** + * Generates and returns an ENUM value. This is how RedBeanPHP handles ENUMs. + * Either returns a (newly created) bean respresenting the desired ENUM + * value or returns a list of all enums for the type. + * + * To obtain (and add if necessary) an ENUM value: + * + * + * $tea->flavour = R::enum( 'flavour:apple' ); + * + * + * Returns a bean of type 'flavour' with name = apple. + * This will add a bean with property name (set to APPLE) to the database + * if it does not exist yet. + * + * To obtain all flavours: + * + * + * R::enum('flavour'); + * + * + * To get a list of all flavour names: + * + * + * R::gatherLabels( R::enum( 'flavour' ) ); + * + * + * @param string $enum either type or type-value + * + * @return array|OODBBean + */ + public static function enum( $enum ) + { + return self::$labelMaker->enum( $enum ); + } + + /** + * Gathers labels from beans. This function loops through the beans, + * collects the values of the name properties of each individual bean + * and stores the names in a new array. The array then gets sorted using the + * default sort function of PHP (sort). + * + * @param array $beans list of beans to loop + * + * @return array + */ + public static function gatherLabels( $beans ) + { + return self::$labelMaker->gatherLabels( $beans ); + } + + /** + * Closes the database connection. + * While database connections are closed automatically at the end of the PHP script, + * closing database connections is generally recommended to improve performance. + * Closing a database connection will immediately return the resources to PHP. + * + * Usage: + * + * + * R::setup( ... ); + * ... do stuff ... + * R::close(); + * + * + * @return void + */ + public static function close() + { + if ( isset( self::$adapter ) ) { + self::$adapter->close(); + } + } + + /** + * Simple convenience function, returns ISO date formatted representation + * of $time. + * + * @param mixed $time UNIX timestamp + * + * @return string + */ + public static function isoDate( $time = NULL ) + { + if ( !$time ) { + $time = time(); + } + + return @date( 'Y-m-d', $time ); + } + + /** + * Simple convenience function, returns ISO date time + * formatted representation + * of $time. + * + * @param mixed $time UNIX timestamp + * + * @return string + */ + public static function isoDateTime( $time = NULL ) + { + if ( !$time ) $time = time(); + return @date( 'Y-m-d H:i:s', $time ); + } + + /** + * Sets the database adapter you want to use. + * The database adapter manages the connection to the database + * and abstracts away database driver specific interfaces. + * + * @param Adapter $adapter Database Adapter for facade to use + * + * @return void + */ + public static function setDatabaseAdapter( Adapter $adapter ) + { + self::$adapter = $adapter; + } + + /** + * Sets the Query Writer you want to use. + * The Query Writer writes and executes database queries using + * the database adapter. It turns RedBeanPHP 'commands' into + * database 'statements'. + * + * @param QueryWriter $writer Query Writer instance for facade to use + * + * @return void + */ + public static function setWriter( QueryWriter $writer ) + { + self::$writer = $writer; + } + + /** + * Sets the OODB you want to use. + * The RedBeanPHP Object oriented database is the main RedBeanPHP + * interface that allows you to store and retrieve RedBeanPHP + * objects (i.e. beans). + * + * @param OODB $redbean Object Database for facade to use + */ + public static function setRedBean( OODB $redbean ) + { + self::$redbean = $redbean; + } + + /** + * Optional accessor for neat code. + * Sets the database adapter you want to use. + * + * @return DBAdapter + */ + public static function getDatabaseAdapter() + { + return self::$adapter; + } + + /** + * In case you use PDO (which is recommended and the default but not mandatory, hence + * the database adapter), you can use this method to obtain the PDO object directly. + * This is a convenience method, it will do the same as: + * + * + * R::getDatabaseAdapter()->getDatabase()->getPDO(); + * + * + * If the PDO object could not be found, for whatever reason, this method + * will return NULL instead. + * + * @return NULL|PDO + */ + public static function getPDO() + { + $databaseAdapter = self::getDatabaseAdapter(); + if ( is_null( $databaseAdapter ) ) return NULL; + $database = $databaseAdapter->getDatabase(); + if ( is_null( $database ) ) return NULL; + if ( !method_exists( $database, 'getPDO' ) ) return NULL; + return $database->getPDO(); + } + + /** + * Returns the current duplication manager instance. + * + * @return DuplicationManager + */ + public static function getDuplicationManager() + { + return self::$duplicationManager; + } + + /** + * Optional accessor for neat code. + * Sets the database adapter you want to use. + * + * @return QueryWriter + */ + public static function getWriter() + { + return self::$writer; + } + + /** + * Optional accessor for neat code. + * Sets the database adapter you want to use. + * + * @return OODB + */ + public static function getRedBean() + { + return self::$redbean; + } + + /** + * Returns the toolbox currently used by the facade. + * To set the toolbox use R::setup() or R::configureFacadeWithToolbox(). + * To create a toolbox use Setup::kickstart(). Or create a manual + * toolbox using the ToolBox class. + * + * @return ToolBox + */ + public static function getToolBox() + { + return self::$toolbox; + } + + /** + * Mostly for internal use, but might be handy + * for some users. + * This returns all the components of the currently + * selected toolbox. + * + * Returns the components in the following order: + * + * # OODB instance (getRedBean()) + * # Database Adapter + * # Query Writer + * # Toolbox itself + * + * @return array + */ + public static function getExtractedToolbox() + { + return array( self::$redbean, self::$adapter, self::$writer, self::$toolbox ); + } + + /** + * Facade method for AQueryWriter::renameAssociation() + * + * @param string|array $from + * @param string $to + * + * @return void + */ + public static function renameAssociation( $from, $to = NULL ) + { + AQueryWriter::renameAssociation( $from, $to ); + } + + /** + * Little helper method for Resty Bean Can server and others. + * Takes an array of beans and exports each bean. + * Unlike exportAll this method does not recurse into own lists + * and shared lists, the beans are exported as-is, only loaded lists + * are exported. + * + * @param array $beans beans + * + * @return array + */ + public static function beansToArray( $beans ) + { + $list = array(); + foreach( $beans as $bean ) $list[] = $bean->export(); + return $list; + } + + /** + * Sets the error mode for FUSE. + * What to do if a FUSE model method does not exist? + * You can set the following options: + * + * * OODBBean::C_ERR_IGNORE (default), ignores the call, returns NULL + * * OODBBean::C_ERR_LOG, logs the incident using error_log + * * OODBBean::C_ERR_NOTICE, triggers a E_USER_NOTICE + * * OODBBean::C_ERR_WARN, triggers a E_USER_WARNING + * * OODBBean::C_ERR_EXCEPTION, throws an exception + * * OODBBean::C_ERR_FUNC, allows you to specify a custom handler (function) + * * OODBBean::C_ERR_FATAL, triggers a E_USER_ERROR + * + * + * Custom handler method signature: handler( array ( + * 'message' => string + * 'bean' => OODBBean + * 'method' => string + * ) ) + * + * + * This method returns the old mode and handler as an array. + * + * @param integer $mode mode, determines how to handle errors + * @param callable|NULL $func custom handler (if applicable) + * + * @return array + */ + public static function setErrorHandlingFUSE( $mode, $func = NULL ) + { + return OODBBean::setErrorHandlingFUSE( $mode, $func ); + } + + /** + * Dumps bean data to array. + * Given a one or more beans this method will + * return an array containing first part of the string + * representation of each item in the array. + * + * Usage: + * + * + * echo R::dump( $bean ); + * + * + * The example shows how to echo the result of a simple + * dump. This will print the string representation of the + * specified bean to the screen, limiting the output per bean + * to 35 characters to improve readability. Nested beans will + * also be dumped. + * + * @param OODBBean|array $data either a bean or an array of beans + * + * @return array + */ + public static function dump( $data ) + { + return Dump::dump( $data ); + } + + /** + * Binds an SQL function to a column. + * This method can be used to setup a decode/encode scheme or + * perform UUID insertion. This method is especially useful for handling + * MySQL spatial columns, because they need to be processed first using + * the asText/GeomFromText functions. + * + * Example: + * + * + * R::bindFunc( 'read', 'location.point', 'asText' ); + * R::bindFunc( 'write', 'location.point', 'GeomFromText' ); + * + * + * Passing NULL as the function will reset (clear) the function + * for this column/mode. + * + * @param string $mode mode for function: i.e. read or write + * @param string $field field (table.column) to bind function to + * @param string $function SQL function to bind to specified column + * @param boolean $isTemplate TRUE if $function is an SQL string, FALSE for just a function name + * + * @return void + */ + public static function bindFunc( $mode, $field, $function, $isTemplate = FALSE ) + { + self::$redbean->bindFunc( $mode, $field, $function, $isTemplate ); + } + + /** + * Sets global aliases. + * Registers a batch of aliases in one go. This works the same as + * fetchAs but explicitly. For instance if you register + * the alias 'cover' for 'page' a property containing a reference to a + * page bean called 'cover' will correctly return the page bean and not + * a (non-existant) cover bean. + * + * + * R::aliases( array( 'cover' => 'page' ) ); + * $book = R::dispense( 'book' ); + * $page = R::dispense( 'page' ); + * $book->cover = $page; + * R::store( $book ); + * $book = $book->fresh(); + * $cover = $book->cover; + * echo $cover->getMeta( 'type' ); //page + * + * + * The format of the aliases registration array is: + * + * {alias} => {actual type} + * + * In the example above we use: + * + * cover => page + * + * From that point on, every bean reference to a cover + * will return a 'page' bean. + * + * @param array $list list of global aliases to use + * + * @return void + */ + public static function aliases( $list ) + { + OODBBean::aliases( $list ); + } + + /** + * Tries to find a bean matching a certain type and + * criteria set. If no beans are found a new bean + * will be created, the criteria will be imported into this + * bean and the bean will be stored and returned. + * If multiple beans match the criteria only the first one + * will be returned. + * + * @param string $type type of bean to search for + * @param array $like criteria set describing the bean to search for + * @param boolean $hasBeenCreated set to TRUE if bean has been created + * + * @return OODBBean + */ + public static function findOrCreate( $type, $like = array(), $sql = '', &$hasBeenCreated = false ) + { + return self::$finder->findOrCreate( $type, $like, $sql = '', $hasBeenCreated ); + } + + /** + * Tries to find beans matching the specified type and + * criteria set. + * + * If the optional additional SQL snippet is a condition, it will + * be glued to the rest of the query using the AND operator. + * + * @param string $type type of bean to search for + * @param array $like optional criteria set describing the bean to search for + * @param string $sql optional additional SQL for sorting + * @param array $bindings bindings + * + * @return array + */ + public static function findLike( $type, $like = array(), $sql = '', $bindings = array() ) + { + return self::$finder->findLike( $type, $like, $sql, $bindings ); + } + + /** + * Starts logging queries. + * Use this method to start logging SQL queries being + * executed by the adapter. Logging queries will not + * print them on the screen. Use R::getLogs() to + * retrieve the logs. + * + * Usage: + * + * + * R::startLogging(); + * R::store( R::dispense( 'book' ) ); + * R::find('book', 'id > ?',[0]); + * $logs = R::getLogs(); + * $count = count( $logs ); + * print_r( $logs ); + * R::stopLogging(); + * + * + * In the example above we start a logging session during + * which we store an empty bean of type book. To inspect the + * logs we invoke R::getLogs() after stopping the logging. + * + * @note you cannot use R::debug and R::startLogging + * at the same time because R::debug is essentially a + * special kind of logging. + * + * @return void + */ + public static function startLogging() + { + self::debug( TRUE, RDefault::C_LOGGER_ARRAY ); + } + + /** + * Stops logging and flushes the logs, + * convient method to stop logging of queries. + * Use this method to stop logging SQL queries being + * executed by the adapter. Logging queries will not + * print them on the screen. Use R::getLogs() to + * retrieve the logs. + * + * + * R::startLogging(); + * R::store( R::dispense( 'book' ) ); + * R::find('book', 'id > ?',[0]); + * $logs = R::getLogs(); + * $count = count( $logs ); + * print_r( $logs ); + * R::stopLogging(); + * + * + * In the example above we start a logging session during + * which we store an empty bean of type book. To inspect the + * logs we invoke R::getLogs() after stopping the logging. + * + * @note you cannot use R::debug and R::startLogging + * at the same time because R::debug is essentially a + * special kind of logging. + * + * @note by stopping the logging you also flush the logs. + * Therefore, only stop logging AFTER you have obtained the + * query logs using R::getLogs() + * + * @return void + */ + public static function stopLogging() + { + self::debug( FALSE ); + } + + /** + * Returns the log entries written after the startLogging. + * + * Use this method to obtain the query logs gathered + * by the logging mechanisms. + * Logging queries will not + * print them on the screen. Use R::getLogs() to + * retrieve the logs. + * + * + * R::startLogging(); + * R::store( R::dispense( 'book' ) ); + * R::find('book', 'id > ?',[0]); + * $logs = R::getLogs(); + * $count = count( $logs ); + * print_r( $logs ); + * R::stopLogging(); + * + * + * In the example above we start a logging session during + * which we store an empty bean of type book. To inspect the + * logs we invoke R::getLogs() after stopping the logging. + * + * The logs may look like: + * + * [1] => SELECT `book`.* FROM `book` WHERE id > ? -- keep-cache + * [2] => array ( 0 => 0, ) + * [3] => resultset: 1 rows + * + * Basically, element in the array is a log entry. + * Parameter bindings are represented as nested arrays (see 2). + * + * @note you cannot use R::debug and R::startLogging + * at the same time because R::debug is essentially a + * special kind of logging. + * + * @note by stopping the logging you also flush the logs. + * Therefore, only stop logging AFTER you have obtained the + * query logs using R::getLogs() + * + * @return array + */ + public static function getLogs() + { + return self::getLogger()->getLogs(); + } + + /** + * Resets the query counter. + * The query counter can be used to monitor the number + * of database queries that have + * been processed according to the database driver. You can use this + * to monitor the number of queries required to render a page. + * + * Usage: + * + * + * R::resetQueryCount(); + * echo R::getQueryCount() . ' queries processed.'; + * + * + * @return void + */ + public static function resetQueryCount() + { + self::$adapter->getDatabase()->resetCounter(); + } + + /** + * Returns the number of SQL queries processed. + * This method returns the number of database queries that have + * been processed according to the database driver. You can use this + * to monitor the number of queries required to render a page. + * + * Usage: + * + * + * echo R::getQueryCount() . ' queries processed.'; + * + * + * @return integer + */ + public static function getQueryCount() + { + return self::$adapter->getDatabase()->getQueryCount(); + } + + /** + * Returns the current logger instance being used by the + * database object. + * + * @return Logger + */ + public static function getLogger() + { + return self::$adapter->getDatabase()->getLogger(); + } + + /** + * @deprecated + */ + public static function setAutoResolve( $automatic = TRUE ){} + + /** + * Toggles 'partial bean mode'. If this mode has been + * selected the repository will only update the fields of a bean that + * have been changed rather than the entire bean. + * Pass the value TRUE to select 'partial mode' for all beans. + * Pass the value FALSE to disable 'partial mode'. + * Pass an array of bean types if you wish to use partial mode only + * for some types. + * This method will return the previous value. + * + * @param boolean|array $yesNoBeans List of type names or 'all' + * + * @return mixed + */ + public static function usePartialBeans( $yesNoBeans ) + { + return self::$redbean->getCurrentRepository()->usePartialBeans( $yesNoBeans ); + } + + /** + * Exposes the result of the specified SQL query as a CSV file. + * + * Usage: + * + * + * R::csv( 'SELECT + * `name`, + * population + * FROM city + * WHERE region = :region ', + * array( ':region' => 'Denmark' ), + * array( 'city', 'population' ), + * '/tmp/cities.csv' + * ); + * + * + * The command above will select all cities in Denmark + * and create a CSV with columns 'city' and 'population' and + * populate the cells under these column headers with the + * names of the cities and the population numbers respectively. + * + * @param string $sql SQL query to expose result of + * @param array $bindings parameter bindings + * @param array $columns column headers for CSV file + * @param string $path path to save CSV file to + * @param boolean $output TRUE to output CSV directly using readfile + * @param array $options delimiter, quote and escape character respectively + * + * @return void + */ + public static function csv( $sql = '', $bindings = array(), $columns = NULL, $path = '/tmp/redexport_%s.csv', $output = TRUE ) + { + $quickExport = new QuickExport( self::$toolbox ); + $quickExport->csv( $sql, $bindings, $columns, $path, $output ); + } + + /** + * MatchUp is a powerful productivity boosting method that can replace simple control + * scripts with a single RedBeanPHP command. Typically, matchUp() is used to + * replace login scripts, token generation scripts and password reset scripts. + * The MatchUp method takes a bean type, an SQL query snippet (starting at the WHERE clause), + * SQL bindings, a pair of task arrays and a bean reference. + * + * If the first 3 parameters match a bean, the first task list will be considered, + * otherwise the second one will be considered. On consideration, each task list, + * an array of keys and values will be executed. Every key in the task list should + * correspond to a bean property while every value can either be an expression to + * be evaluated or a closure (PHP 5.3+). After applying the task list to the bean + * it will be stored. If no bean has been found, a new bean will be dispensed. + * + * This method will return TRUE if the bean was found and FALSE if not AND + * there was a NOT-FOUND task list. If no bean was found AND there was also + * no second task list, NULL will be returned. + * + * To obtain the bean, pass a variable as the sixth parameter. + * The function will put the matching bean in the specified variable. + * + * @param string $type type of bean you're looking for + * @param string $sql SQL snippet (starting at the WHERE clause, omit WHERE-keyword) + * @param array $bindings array of parameter bindings for SQL snippet + * @param array $onFoundDo task list to be considered on finding the bean + * @param array $onNotFoundDo task list to be considered on NOT finding the bean + * @param OODBBean &$bean reference to obtain the found bean + * + * @return mixed + */ + public static function matchUp( $type, $sql, $bindings = array(), $onFoundDo = NULL, $onNotFoundDo = NULL, &$bean = NULL ) { + $matchUp = new MatchUp( self::$toolbox ); + return $matchUp->matchUp( $type, $sql, $bindings, $onFoundDo, $onNotFoundDo, $bean ); + } + + /** + * @deprecated + * + * Returns an instance of the Look Helper class. + * The instance will be configured with the current toolbox. + * + * In previous versions of RedBeanPHP you had to use: + * R::getLook()->look() instead of R::look(). However to improve useability of the + * library the look() function can now directly be invoked from the facade. + * + * For more details regarding the Look functionality, please consult R::look(). + * @see Facade::look + * @see Look::look + * + * @return Look + */ + public static function getLook() + { + return new Look( self::$toolbox ); + } + + /** + * Takes an full SQL query with optional bindings, a series of keys, a template + * and optionally a filter function and glue and assembles a view from all this. + * This is the fastest way from SQL to view. Typically this function is used to + * generate pulldown (select tag) menus with options queried from the database. + * + * Usage: + * + * + * $htmlPulldown = R::look( + * 'SELECT * FROM color WHERE value != ? ORDER BY value ASC', + * [ 'g' ], + * [ 'value', 'name' ], + * '', + * 'strtoupper', + * "\n" + * ); + * + * + * The example above creates an HTML fragment like this: + * + * + * + * + * to pick a color from a palette. The HTML fragment gets constructed by + * an SQL query that selects all colors that do not have value 'g' - this + * excludes green. Next, the bean properties 'value' and 'name' are mapped to the + * HTML template string, note that the order here is important. The mapping and + * the HTML template string follow vsprintf-rules. All property values are then + * passed through the specified filter function 'strtoupper' which in this case + * is a native PHP function to convert strings to uppercase characters only. + * Finally the resulting HTML fragment strings are glued together using a + * newline character specified in the last parameter for readability. + * + * In previous versions of RedBeanPHP you had to use: + * R::getLook()->look() instead of R::look(). However to improve useability of the + * library the look() function can now directly be invoked from the facade. + * + * @param string $sql query to execute + * @param array $bindings parameters to bind to slots mentioned in query or an empty array + * @param array $keys names in result collection to map to template + * @param string $template HTML template to fill with values associated with keys, use printf notation (i.e. %s) + * @param callable $filter function to pass values through (for translation for instance) + * @param string $glue optional glue to use when joining resulting strings + * + * @return string + */ + public static function look( $sql, $bindings = array(), $keys = array( 'selected', 'id', 'name' ), $template = '', $filter = 'trim', $glue = '' ) + { + return self::getLook()->look( $sql, $bindings, $keys, $template, $filter, $glue ); + } + + /** + * Calculates a diff between two beans (or arrays of beans). + * The result of this method is an array describing the differences of the second bean compared to + * the first, where the first bean is taken as reference. The array is keyed by type/property, id and property name, where + * type/property is either the type (in case of the root bean) or the property of the parent bean where the type resides. + * The diffs are mainly intended for logging, you cannot apply these diffs as patches to other beans. + * However this functionality might be added in the future. + * + * The keys of the array can be formatted using the $format parameter. + * A key will be composed of a path (1st), id (2nd) and property (3rd). + * Using printf-style notation you can determine the exact format of the key. + * The default format will look like: + * + * 'book.1.title' => array( , ) + * + * If you only want a simple diff of one bean and you don't care about ids, + * you might pass a format like: '%1$s.%3$s' which gives: + * + * 'book.1.title' => array( , ) + * + * The filter parameter can be used to set filters, it should be an array + * of property names that have to be skipped. By default this array is filled with + * two strings: 'created' and 'modified'. + * + * @param OODBBean|array $bean reference beans + * @param OODBBean|array $other beans to compare + * @param array $filters names of properties of all beans to skip + * @param string $format the format of the key, defaults to '%s.%s.%s' + * @param string $type type/property of bean to use for key generation + * + * @return array + */ + public static function diff( $bean, $other, $filters = array( 'created', 'modified' ), $pattern = '%s.%s.%s' ) + { + $diff = new Diff( self::$toolbox ); + return $diff->diff( $bean, $other, $filters, $pattern ); + } + + /** + * The gentleman's way to register a RedBeanPHP ToolBox instance + * with the facade. Stores the toolbox in the static toolbox + * registry of the facade class. This allows for a neat and + * explicit way to register a toolbox. + * + * @param string $key key to store toolbox instance under + * @param ToolBox $toolbox toolbox to register + * + * @return void + */ + public static function addToolBoxWithKey( $key, ToolBox $toolbox ) + { + self::$toolboxes[$key] = $toolbox; + } + + /** + * The gentleman's way to remove a RedBeanPHP ToolBox instance + * from the facade. Removes the toolbox identified by + * the specified key in the static toolbox + * registry of the facade class. This allows for a neat and + * explicit way to remove a toolbox. + * Returns TRUE if the specified toolbox was found and removed. + * Returns FALSE otherwise. + * + * @param string $key identifier of the toolbox to remove + * + * @return boolean + */ + public static function removeToolBoxByKey( $key ) + { + if ( !array_key_exists( $key, self::$toolboxes ) ) { + return FALSE; + } + unset( self::$toolboxes[$key] ); + return TRUE; + } + + /** + * Returns the toolbox associated with the specified key. + * + * @param string $key key to store toolbox instance under + * @param ToolBox $toolbox toolbox to register + * + * @return ToolBox|NULL + */ + public static function getToolBoxByKey( $key ) + { + if ( !array_key_exists( $key, self::$toolboxes ) ) { + return NULL; + } + return self::$toolboxes[$key]; + } + + /** + * Toggles JSON column features. + * Invoking this method with boolean TRUE causes 2 JSON features to be enabled. + * Beans will automatically JSONify any array that's not in a list property and + * the Query Writer (if capable) will attempt to create a JSON column for strings that + * appear to contain JSON. + * + * Feature #1: + * AQueryWriter::useJSONColumns + * + * Toggles support for automatic generation of JSON columns. + * Using JSON columns means that strings containing JSON will + * cause the column to be created (not modified) as a JSON column. + * However it might also trigger exceptions if this means the DB attempts to + * convert a non-json column to a JSON column. + * + * Feature #2: + * OODBBean::convertArraysToJSON + * + * Toggles array to JSON conversion. If set to TRUE any array + * set to a bean property that's not a list will be turned into + * a JSON string. Used together with AQueryWriter::useJSONColumns this + * extends the data type support for JSON columns. + * + * So invoking this method is the same as: + * + * + * AQueryWriter::useJSONColumns( $flag ); + * OODBBean::convertArraysToJSON( $flag ); + * + * + * Unlike the methods above, that return the previous state, this + * method does not return anything (void). + * + * @param boolean $flag feature flag (either TRUE or FALSE) + * + * @return void + */ + public static function useJSONFeatures( $flag ) + { + AQueryWriter::useJSONColumns( $flag ); + OODBBean::convertArraysToJSON( $flag ); + } + + /** + * Given a bean and an optional SQL snippet, + * this method will return the bean together with all + * child beans in a hierarchically structured + * bean table. + * + * @note that not all database support this functionality. You'll need + * at least MariaDB 10.2.2 or Postgres. This method does not include + * a warning mechanism in case your database does not support this + * functionality. + * + * @param OODBBean $bean bean to find children of + * @param string $sql optional SQL snippet + * @param array $bindings SQL snippet parameter bindings + */ + public static function children( OODBBean $bean, $sql = NULL, $bindings = array() ) + { + return self::$tree->children( $bean, $sql, $bindings ); + } + + /** + * Given a bean and an optional SQL snippet, + * this method will count all child beans in a hierarchically structured + * bean table. + * + * @note that not all database support this functionality. You'll need + * at least MariaDB 10.2.2 or Postgres. This method does not include + * a warning mechanism in case your database does not support this + * functionality. + * + * @note: + * You are allowed to use named parameter bindings as well as + * numeric parameter bindings (using the question mark notation). + * However, you can not mix. Also, if using named parameter bindings, + * parameter binding key ':slot0' is reserved for the ID of the bean + * and used in the query. + * + * @note: + * By default, if no select is given or select=TRUE this method will subtract 1 of + * the total count to omit the starting bean. If you provide your own select, + * this method assumes you take control of the resulting total yourself since + * it cannot 'predict' what or how you are trying to 'count'. + * + * @param OODBBean $bean bean to find children of + * @param string $sql optional SQL snippet + * @param array $bindings SQL snippet parameter bindings + * @param string|boolean $select select snippet to use (advanced, optional, see QueryWriter::queryRecursiveCommonTableExpression) + */ + public static function countChildren( OODBBean $bean, $sql = NULL, $bindings = array(), $select = QueryWriter::C_CTE_SELECT_COUNT ) + { + return self::$tree->countChildren( $bean, $sql, $bindings, $select ); + } + + /** + * Given a bean and an optional SQL snippet, + * this method will count all parent beans in a hierarchically structured + * bean table. + * + * @note that not all database support this functionality. You'll need + * at least MariaDB 10.2.2 or Postgres. This method does not include + * a warning mechanism in case your database does not support this + * functionality. + * + * @note: + * You are allowed to use named parameter bindings as well as + * numeric parameter bindings (using the question mark notation). + * However, you can not mix. Also, if using named parameter bindings, + * parameter binding key ':slot0' is reserved for the ID of the bean + * and used in the query. + * + * @note: + * By default, if no select is given or select=TRUE this method will subtract 1 of + * the total count to omit the starting bean. If you provide your own select, + * this method assumes you take control of the resulting total yourself since + * it cannot 'predict' what or how you are trying to 'count'. + * + * @param OODBBean $bean bean to find children of + * @param string $sql optional SQL snippet + * @param array $bindings SQL snippet parameter bindings + * @param string|boolean $select select snippet to use (advanced, optional, see QueryWriter::queryRecursiveCommonTableExpression) + */ + public static function countParents( OODBBean $bean, $sql = NULL, $bindings = array(), $select = QueryWriter::C_CTE_SELECT_COUNT ) + { + return self::$tree->countParents( $bean, $sql, $bindings, $select ); + } + + /** + * Given a bean and an optional SQL snippet, + * this method will return the bean along with all parent beans + * in a hierarchically structured bean table. + * + * @note that not all database support this functionality. You'll need + * at least MariaDB 10.2.2 or Postgres. This method does not include + * a warning mechanism in case your database does not support this + * functionality. + * + * @param OODBBean $bean bean to find parents of + * @param string $sql optional SQL snippet + * @param array $bindings SQL snippet parameter bindings + */ + public static function parents( OODBBean $bean, $sql = NULL, $bindings = array() ) + { + return self::$tree->parents( $bean, $sql, $bindings ); + } + + /** + * Toggles support for nuke(). + * Can be used to turn off the nuke() feature for security reasons. + * Returns the old flag value. + * + * @param boolean $flag TRUE or FALSE + * + * @return boolean + */ + public static function noNuke( $yesNo ) { + return AQueryWriter::forbidNuke( $yesNo ); + } + + /** + * Globally available service method for RedBeanPHP. + * Converts a snake cased string to a camel cased string. + * If the parameter is an array, the keys will be converted. + * + * @param string|array $snake snake_cased string to convert to camelCase + * @param boolean $dolphin exception for Ids - (bookId -> bookID) + * too complicated for the human mind, only dolphins can understand this + * + * @return string|array + */ + public static function camelfy( $snake, $dolphin = false ) + { + if ( is_array( $snake ) ) { + $newArray = array(); + foreach( $snake as $key => $value ) { + $newKey = self::camelfy( $key, $dolphin ); + if ( is_array( $value ) ) { + $value = self::camelfy( $value, $dolphin ); + } + $newArray[ $newKey ] = $value; + } + return $newArray; + } + return AQueryWriter::snakeCamel( $snake, $dolphin ); + } + + /** + * Globally available service method for RedBeanPHP. + * Converts a camel cased string to a snake cased string. + * If the parameter is an array, the keys will be converted. + * + * @param string|array $camel camelCased string to convert to snake case + * + * @return string|array + */ + public static function uncamelfy( $camel ) + { + if ( is_array( $camel ) ) { + $newArray = array(); + foreach( $camel as $key => $value ) { + $newKey = self::uncamelfy( $key ); + if ( is_array( $value ) ) { + $value = self::uncamelfy( $value ); + } + $newArray[ $newKey ] = $value; + } + return $newArray; + } + return AQueryWriter::camelsSnake( $camel ); + } + + /** + * Selects the feature set you want as specified by + * the label. + * + * Usage: + * + * + * R::useFeatureSet( 'novice/latest' ); + * + * + * @param string $label label + * + * @return void + */ + public static function useFeatureSet( $label ) { + return Feature::feature($label); + } + + /** + * Dynamically extends the facade with a plugin. + * Using this method you can register your plugin with the facade and then + * use the plugin by invoking the name specified plugin name as a method on + * the facade. + * + * Usage: + * + * + * R::ext( 'makeTea', function() { ... } ); + * + * + * Now you can use your makeTea plugin like this: + * + * + * R::makeTea(); + * + * + * @param string $pluginName name of the method to call the plugin + * @param callable $callable a PHP callable + * + * @return void + */ + public static function ext( $pluginName, $callable ) + { + if ( !preg_match( '#^[a-zA-Z_][a-zA-Z0-9_]*$#', $pluginName ) ) { + throw new RedException( 'Plugin name may only contain alphanumeric characters and underscores and cannot start with a number.' ); + } + self::$plugins[$pluginName] = $callable; + } + + /** + * Call static for use with dynamic plugins. This magic method will + * intercept static calls and route them to the specified plugin. + * + * @param string $pluginName name of the plugin + * @param array $params list of arguments to pass to plugin method + * + * @return mixed + */ + public static function __callStatic( $pluginName, $params ) + { + if ( !isset( self::$plugins[$pluginName] ) ) { + if ( !preg_match( '#^[a-zA-Z_][a-zA-Z0-9_]*$#', $pluginName ) ) { + throw new RedException( 'Plugin name may only contain alphanumeric characters and underscores and cannot start with a number.' ); + } + throw new RedException( 'Plugin \''.$pluginName.'\' does not exist, add this plugin using: R::ext(\''.$pluginName.'\')' ); + } + return call_user_func_array( self::$plugins[$pluginName], $params ); + } +} + +} + +namespace RedBeanPHP { + +use RedBeanPHP\ToolBox as ToolBox; +use RedBeanPHP\AssociationManager as AssociationManager; +use RedBeanPHP\OODB as OODB; +use RedBeanPHP\OODBBean as OODBBean; +use RedBeanPHP\QueryWriter\AQueryWriter as AQueryWriter; + +/** + * Duplication Manager + * The Duplication Manager creates deep copies from beans, this means + * it can duplicate an entire bean hierarchy. You can use this feature to + * implement versioning for instance. Because duplication and exporting are + * closely related this class is also used to export beans recursively + * (i.e. we make a duplicate and then convert to array). This class allows + * you to tune the duplication process by specifying filters determining + * which relations to take into account and by specifying tables + * (in which case no reflective queries have to be issued thus improving + * performance). This class also hosts the Camelfy function used to + * reformat the keys of an array, this method is publicly available and + * used internally by exportAll(). + * + * @file RedBeanPHP/DuplicationManager.php + * @author Gabor de Mooij and the RedBeanPHP Community + * @license BSD/GPLv2 + * + * @copyright + * copyright (c) G.J.G.T. (Gabor) de Mooij and the RedBeanPHP Community + * This source file is subject to the BSD/GPLv2 License that is bundled + * with this source code in the file license.txt. + */ +class DuplicationManager +{ + /** + * @var ToolBox + */ + protected $toolbox; + + /** + * @var AssociationManager + */ + protected $associationManager; + + /** + * @var OODB + */ + protected $redbean; + + /** + * @var array + */ + protected $tables = array(); + + /** + * @var array + */ + protected $columns = array(); + + /** + * @var array + */ + protected $filters = array(); + + /** + * @var array + */ + protected $cacheTables = FALSE; + + /** + * @var boolean + */ + protected $copyMeta = FALSE; + + /** + * Copies the shared beans in a bean, i.e. all the sharedBean-lists. + * + * @param OODBBean $copy target bean to copy lists to + * @param string $shared name of the shared list + * @param array $beans array with shared beans to copy + * + * @return void + */ + private function copySharedBeans( OODBBean $copy, $shared, $beans ) + { + $copy->$shared = array(); + + foreach ( $beans as $subBean ) { + array_push( $copy->$shared, $subBean ); + } + } + + /** + * Copies the own beans in a bean, i.e. all the ownBean-lists. + * Each bean in the own-list belongs exclusively to its owner so + * we need to invoke the duplicate method again to duplicate each bean here. + * + * @param OODBBean $copy target bean to copy lists to + * @param string $owned name of the own list + * @param array $beans array with shared beans to copy + * @param array $trail array with former beans to detect recursion + * @param boolean $preserveIDs TRUE means preserve IDs, for export only + * + * @return void + */ + private function copyOwnBeans( OODBBean $copy, $owned, $beans, $trail, $preserveIDs ) + { + $copy->$owned = array(); + foreach ( $beans as $subBean ) { + array_push( $copy->$owned, $this->duplicate( $subBean, $trail, $preserveIDs ) ); + } + } + + /** + * Creates a copy of bean $bean and copies all primitive properties (not lists) + * and the parents beans to the newly created bean. Also sets the ID of the bean + * to 0. + * + * @param OODBBean $bean bean to copy + * + * @return OODBBean + */ + private function createCopy( OODBBean $bean ) + { + $type = $bean->getMeta( 'type' ); + + $copy = $this->redbean->dispense( $type ); + $copy->setMeta( 'sys.dup-from-id', $bean->id ); + $copy->setMeta( 'sys.old-id', $bean->id ); + $copy->importFrom( $bean ); + if ($this->copyMeta) $copy->copyMetaFrom($bean); + $copy->id = 0; + + return $copy; + } + + /** + * Generates a key from the bean type and its ID and determines if the bean + * occurs in the trail, if not the bean will be added to the trail. + * Returns TRUE if the bean occurs in the trail and FALSE otherwise. + * + * @param array $trail list of former beans + * @param OODBBean $bean currently selected bean + * + * @return boolean + */ + private function inTrailOrAdd( &$trail, OODBBean $bean ) + { + $type = $bean->getMeta( 'type' ); + $key = $type . $bean->getID(); + + if ( isset( $trail[$key] ) ) { + return TRUE; + } + + $trail[$key] = $bean; + + return FALSE; + } + + /** + * Given the type name of a bean this method returns the canonical names + * of the own-list and the shared-list properties respectively. + * Returns a list with two elements: name of the own-list, and name + * of the shared list. + * + * @param string $typeName bean type name + * + * @return array + */ + private function getListNames( $typeName ) + { + $owned = 'own' . ucfirst( $typeName ); + $shared = 'shared' . ucfirst( $typeName ); + + return array( $owned, $shared ); + } + + /** + * Determines whether the bean has an own list based on + * schema inspection from realtime schema or cache. + * + * @param string $type bean type to get list for + * @param string $target type of list you want to detect + * + * @return boolean + */ + protected function hasOwnList( $type, $target ) + { + return isset( $this->columns[$target][$type . '_id'] ); + } + + /** + * Determines whether the bea has a shared list based on + * schema inspection from realtime schema or cache. + * + * @param string $type bean type to get list for + * @param string $target type of list you are looking for + * + * @return boolean + */ + protected function hasSharedList( $type, $target ) + { + return in_array( AQueryWriter::getAssocTableFormat( array( $type, $target ) ), $this->tables ); + } + + /** + * @see DuplicationManager::dup + * + * @param OODBBean $bean bean to be copied + * @param array $trail trail to prevent infinite loops + * @param boolean $preserveIDs preserve IDs + * + * @return OODBBean + */ + protected function duplicate( OODBBean $bean, $trail = array(), $preserveIDs = FALSE ) + { + if ( $this->inTrailOrAdd( $trail, $bean ) ) return $bean; + + $type = $bean->getMeta( 'type' ); + + $copy = $this->createCopy( $bean ); + foreach ( $this->tables as $table ) { + + if ( !empty( $this->filters ) ) { + if ( !in_array( $table, $this->filters ) ) continue; + } + + list( $owned, $shared ) = $this->getListNames( $table ); + + if ( $this->hasSharedList( $type, $table ) ) { + if ( $beans = $bean->$shared ) { + $this->copySharedBeans( $copy, $shared, $beans ); + } + } elseif ( $this->hasOwnList( $type, $table ) ) { + if ( $beans = $bean->$owned ) { + $this->copyOwnBeans( $copy, $owned, $beans, $trail, $preserveIDs ); + } + + $copy->setMeta( 'sys.shadow.' . $owned, NULL ); + } + + $copy->setMeta( 'sys.shadow.' . $shared, NULL ); + } + + $copy->id = ( $preserveIDs ) ? $bean->id : $copy->id; + + return $copy; + } + + /** + * Constructor, + * creates a new instance of DupManager. + * + * @param ToolBox $toolbox + */ + public function __construct( ToolBox $toolbox ) + { + $this->toolbox = $toolbox; + $this->redbean = $toolbox->getRedBean(); + $this->associationManager = $this->redbean->getAssociationManager(); + } + + /** + * Recursively turns the keys of an array into + * camelCase. + * + * @param array $array array to camelize + * @param boolean $dolphinMode whether you want the exception for IDs. + * + * @return array + */ + public function camelfy( $array, $dolphinMode = FALSE ) { + $newArray = array(); + foreach( $array as $key => $element ) { + $newKey = preg_replace_callback( '/_(\w)/', function( $matches ){ + return strtoupper( $matches[1] ); + }, $key); + + if ( $dolphinMode ) { + $newKey = preg_replace( '/(\w)Id$/', '$1ID', $newKey ); + } + + $newArray[$newKey] = ( is_array($element) ) ? $this->camelfy( $element, $dolphinMode ) : $element; + } + return $newArray; + } + + /** + * For better performance you can pass the tables in an array to this method. + * If the tables are available the duplication manager will not query them so + * this might be beneficial for performance. + * + * This method allows two array formats: + * + * + * array( TABLE1, TABLE2 ... ) + * + * + * or + * + * + * array( TABLE1 => array( COLUMN1, COLUMN2 ... ) ... ) + * + * + * @param array $tables a table cache array + * + * @return void + */ + public function setTables( $tables ) + { + foreach ( $tables as $key => $value ) { + if ( is_numeric( $key ) ) { + $this->tables[] = $value; + } else { + $this->tables[] = $key; + $this->columns[$key] = $value; + } + } + + $this->cacheTables = TRUE; + } + + /** + * Returns a schema array for cache. + * You can use the return value of this method as a cache, + * store it in RAM or on disk and pass it to setTables later. + * + * @return array + */ + public function getSchema() + { + return $this->columns; + } + + /** + * Indicates whether you want the duplication manager to cache the database schema. + * If this flag is set to TRUE the duplication manager will query the database schema + * only once. Otherwise the duplicationmanager will, by default, query the schema + * every time a duplication action is performed (dup()). + * + * @param boolean $yesNo TRUE to use caching, FALSE otherwise + */ + public function setCacheTables( $yesNo ) + { + $this->cacheTables = $yesNo; + } + + /** + * A filter array is an array with table names. + * By setting a table filter you can make the duplication manager only take into account + * certain bean types. Other bean types will be ignored when exporting or making a + * deep copy. If no filters are set all types will be taking into account, this is + * the default behavior. + * + * @param array $filters list of tables to be filtered + * + * @return void + */ + public function setFilters( $filters ) + { + if ( !is_array( $filters ) ) { + $filters = array( $filters ); + } + + $this->filters = $filters; + } + + /** + * Makes a copy of a bean. This method makes a deep copy + * of the bean.The copy will have the following features. + * - All beans in own-lists will be duplicated as well + * - All references to shared beans will be copied but not the shared beans themselves + * - All references to parent objects (_id fields) will be copied but not the parents themselves + * In most cases this is the desired scenario for copying beans. + * This function uses a trail-array to prevent infinite recursion, if a recursive bean is found + * (i.e. one that already has been processed) the ID of the bean will be returned. + * This should not happen though. + * + * Note: + * This function does a reflectional database query so it may be slow. + * + * Note: + * this function actually passes the arguments to a protected function called + * duplicate() that does all the work. This method takes care of creating a clone + * of the bean to avoid the bean getting tainted (triggering saving when storing it). + * + * @param OODBBean $bean bean to be copied + * @param array $trail for internal usage, pass array() + * @param boolean $preserveIDs for internal usage + * + * @return OODBBean + */ + public function dup( OODBBean $bean, $trail = array(), $preserveIDs = FALSE ) + { + if ( !count( $this->tables ) ) { + $this->tables = $this->toolbox->getWriter()->getTables(); + } + + if ( !count( $this->columns ) ) { + foreach ( $this->tables as $table ) { + $this->columns[$table] = $this->toolbox->getWriter()->getColumns( $table ); + } + } + + $rs = $this->duplicate( ( clone $bean ), $trail, $preserveIDs ); + + if ( !$this->cacheTables ) { + $this->tables = array(); + $this->columns = array(); + } + + return $rs; + } + + /** + * Exports a collection of beans recursively. + * This method will export an array of beans in the first argument to a + * set of arrays. This can be used to send JSON or XML representations + * of bean hierarchies to the client. + * + * For every bean in the array this method will export: + * + * - contents of the bean + * - all own bean lists (recursively) + * - all shared beans (but not THEIR own lists) + * + * If the second parameter is set to TRUE the parents of the beans in the + * array will be exported as well (but not THEIR parents). + * + * The third parameter can be used to provide a white-list array + * for filtering. This is an array of strings representing type names, + * only the type names in the filter list will be exported. + * + * The fourth parameter can be used to change the keys of the resulting + * export arrays. The default mode is 'snake case' but this leaves the + * keys as-is, because 'snake' is the default case style used by + * RedBeanPHP in the database. You can set this to 'camel' for + * camel cased keys or 'dolphin' (same as camelcase but id will be + * converted to ID instead of Id). + * + * @param array|OODBBean $beans beans to be exported + * @param boolean $parents also export parents + * @param array $filters only these types (whitelist) + * @param string $caseStyle case style identifier + * @param boolean $meta export meta data as well + * + * @return array + */ + public function exportAll( $beans, $parents = FALSE, $filters = array(), $caseStyle = 'snake', $meta = FALSE) + { + $array = array(); + if ( !is_array( $beans ) ) { + $beans = array( $beans ); + } + $this->copyMeta = $meta; + foreach ( $beans as $bean ) { + $this->setFilters( $filters ); + $duplicate = $this->dup( $bean, array(), TRUE ); + $array[] = $duplicate->export( $meta, $parents, FALSE, $filters ); + } + if ( $caseStyle === 'camel' ) $array = $this->camelfy( $array ); + if ( $caseStyle === 'dolphin' ) $array = $this->camelfy( $array, TRUE ); + return $array; + } +} +} + +namespace RedBeanPHP\Util { + +use RedBeanPHP\OODB as OODB; +use RedBeanPHP\OODBBean as OODBBean; +use RedBeanPHP\RedException as RedException; + +/** + * Array Tool Helper + * + * This code was originally part of the facade, however it has + * been decided to remove unique features to service classes like + * this to make them available to developers not using the facade class. + * + * This is a helper or service class containing frequently used + * array functions for dealing with SQL queries. + * + * @file RedBeanPHP/Util/ArrayTool.php + * @author Gabor de Mooij and the RedBeanPHP Community + * @license BSD/GPLv2 + * + * @copyright + * copyright (c) G.J.G.T. (Gabor) de Mooij and the RedBeanPHP Community + * This source file is subject to the BSD/GPLv2 License that is bundled + * with this source code in the file license.txt. + */ +class ArrayTool +{ + /** + * Generates question mark slots for an array of values. + * Given an array and an optional template string this method + * will produce string containing parameter slots for use in + * an SQL query string. + * + * Usage: + * + * + * R::genSlots( array( 'a', 'b' ) ); + * + * + * The statement in the example will produce the string: + * '?,?'. + * + * Another example, using a template string: + * + * + * R::genSlots( array('a', 'b'), ' IN( %s ) ' ); + * + * + * The statement in the example will produce the string: + * ' IN( ?,? ) '. + * + * @param array $array array to generate question mark slots for + * @param string $template template to use + * + * @return string + */ + public static function genSlots( $array, $template = NULL ) + { + $str = count( $array ) ? implode( ',', array_fill( 0, count( $array ), '?' ) ) : ''; + return ( is_null( $template ) || $str === '' ) ? $str : sprintf( $template, $str ); + } + + /** + * Flattens a multi dimensional bindings array for use with genSlots(). + * + * Usage: + * + * + * R::flat( array( 'a', array( 'b' ), 'c' ) ); + * + * + * produces an array like: [ 'a', 'b', 'c' ] + * + * @param array $array array to flatten + * @param array $result result array parameter (for recursion) + * + * @return array + */ + public static function flat( $array, $result = array() ) + { + foreach( $array as $value ) { + if ( is_array( $value ) ) $result = self::flat( $value, $result ); + else $result[] = $value; + } + return $result; + } +} +} + +namespace RedBeanPHP\Util { + +use RedBeanPHP\OODB as OODB; +use RedBeanPHP\RedException as RedException; + +/** + * Dispense Helper + * + * A helper class containing a dispense utility. + * + * @file RedBeanPHP/Util/DispenseHelper.php + * @author Gabor de Mooij and the RedBeanPHP Community + * @license BSD/GPLv2 + * + * @copyright + * copyright (c) G.J.G.T. (Gabor) de Mooij and the RedBeanPHP Community + * This source file is subject to the BSD/GPLv2 License that is bundled + * with this source code in the file license.txt. + */ +class DispenseHelper +{ + /** + * @var boolean + */ + private static $enforceNamingPolicy = TRUE; + + /** + * Sets the enforce naming policy flag. If set to + * TRUE the RedBeanPHP naming policy will be enforced. + * Otherwise it will not. Use at your own risk. + * Setting this to FALSE is not recommended. + * + * @param boolean $yesNo whether to enforce RB name policy + * + * @return void + */ + public static function setEnforceNamingPolicy( $yesNo ) + { + self::$enforceNamingPolicy = (boolean) $yesNo; + } + + /** + * Checks whether the bean type conforms to the RedbeanPHP + * naming policy. This method will throw an exception if the + * type does not conform to the RedBeanPHP database column naming + * policy. + * + * The RedBeanPHP naming policy for beans states that valid + * bean type names contain only: + * + * - lowercase alphanumeric characters a-z + * - numbers 0-9 + * - at least one character + * + * Although there are no restrictions on length, database + * specific implementations may apply further restrictions + * regarding the length of a table which means these restrictions + * also apply to bean types. + * + * The RedBeanPHP naming policy ensures that, without any + * configuration, the core functionalities work across many + * databases and operating systems, including those that are + * case insensitive or restricted to the ASCII character set. + * + * Although these restrictions can be bypassed, this is not + * recommended. + * + * @param string $type type of bean + * + * @return void + */ + public static function checkType( $type ) + { + if ( !preg_match( '/^[a-z0-9]+$/', $type ) ) { + throw new RedException( 'Invalid type: ' . $type ); + } + } + + /** + * Dispenses a new RedBean OODB Bean for use with + * the rest of the methods. RedBeanPHP thinks in beans, the bean is the + * primary way to interact with RedBeanPHP and the database managed by + * RedBeanPHP. To load, store and delete data from the database using RedBeanPHP + * you exchange these RedBeanPHP OODB Beans. The only exception to this rule + * are the raw query methods like R::getCell() or R::exec() and so on. + * The dispense method is the 'preferred way' to create a new bean. + * + * Usage: + * + * + * $book = R::dispense( 'book' ); + * $book->title = 'My Book'; + * R::store( $book ); + * + * + * This method can also be used to create an entire bean graph at once. + * Given an array with keys specifying the property names of the beans + * and a special _type key to indicate the type of bean, one can + * make the Dispense Helper generate an entire hierarchy of beans, including + * lists. To make dispense() generate a list, simply add a key like: + * ownXList or sharedXList where X is the type of beans it contains and + * a set its value to an array filled with arrays representing the beans. + * Note that, although the type may have been hinted at in the list name, + * you still have to specify a _type key for every bean array in the list. + * Note that, if you specify an array to generate a bean graph, the number + * parameter will be ignored. + * + * Usage: + * + * + * $book = R::dispense( [ + * '_type' => 'book', + * 'title' => 'Gifted Programmers', + * 'author' => [ '_type' => 'author', 'name' => 'Xavier' ], + * 'ownPageList' => [ ['_type'=>'page', 'text' => '...'] ] + * ] ); + * + * + * @param string|array $typeOrBeanArray type or bean array to import + * @param integer $num number of beans to dispense + * @param boolean $alwaysReturnArray if TRUE always returns the result as an array + * + * @return array|OODBBean + */ + public static function dispense( OODB $oodb, $typeOrBeanArray, $num = 1, $alwaysReturnArray = FALSE ) { + + if ( is_array($typeOrBeanArray) ) { + + if ( !isset( $typeOrBeanArray['_type'] ) ) { + $list = array(); + foreach( $typeOrBeanArray as $beanArray ) { + if ( + !( is_array( $beanArray ) + && isset( $beanArray['_type'] ) ) ) { + throw new RedException( 'Invalid Array Bean' ); + } + } + foreach( $typeOrBeanArray as $beanArray ) $list[] = self::dispense( $oodb, $beanArray ); + return $list; + } + + $import = $typeOrBeanArray; + $type = $import['_type']; + unset( $import['_type'] ); + } else { + $type = $typeOrBeanArray; + } + + if (self::$enforceNamingPolicy) self::checkType( $type ); + + $beanOrBeans = $oodb->dispense( $type, $num, $alwaysReturnArray ); + + if ( isset( $import ) ) { + $beanOrBeans->import( $import ); + } + + return $beanOrBeans; + } + + + /** + * Takes a comma separated list of bean types + * and dispenses these beans. For each type in the list + * you can specify the number of beans to be dispensed. + * + * Usage: + * + * + * list( $book, $page, $text ) = R::dispenseAll( 'book,page,text' ); + * + * + * This will dispense a book, a page and a text. This way you can + * quickly dispense beans of various types in just one line of code. + * + * Usage: + * + * + * list($book, $pages) = R::dispenseAll('book,page*100'); + * + * + * This returns an array with a book bean and then another array + * containing 100 page beans. + * + * @param OODB $oodb OODB + * @param string $order a description of the desired dispense order using the syntax above + * @param boolean $onlyArrays return only arrays even if amount < 2 + * + * @return array + */ + public static function dispenseAll( OODB $oodb, $order, $onlyArrays = FALSE ) + { + $list = array(); + + foreach( explode( ',', $order ) as $order ) { + if ( strpos( $order, '*' ) !== FALSE ) { + list( $type, $amount ) = explode( '*', $order ); + } else { + $type = $order; + $amount = 1; + } + + $list[] = self::dispense( $oodb, $type, $amount, $onlyArrays ); + } + + return $list; + } +} +} + +namespace RedBeanPHP\Util { + +use RedBeanPHP\OODB as OODB; +use RedBeanPHP\OODBBean as OODBBean; + +/** + * Dump helper + * + * This code was originally part of the facade, however it has + * been decided to remove unique features to service classes like + * this to make them available to developers not using the facade class. + * + * Dumps the contents of a bean in an array for + * debugging purposes. + * + * @file RedBeanPHP/Util/Dump.php + * @author Gabor de Mooij and the RedBeanPHP Community + * @license BSD/GPLv2 + * + * @copyright + * copyright (c) G.J.G.T. (Gabor) de Mooij and the RedBeanPHP Community + * This source file is subject to the BSD/GPLv2 License that is bundled + * with this source code in the file license.txt. + */ +class Dump +{ + /** + * Dumps bean data to array. + * Given a one or more beans this method will + * return an array containing first part of the string + * representation of each item in the array. + * + * Usage: + * + * + * echo R::dump( $bean ); + * + * + * The example shows how to echo the result of a simple + * dump. This will print the string representation of the + * specified bean to the screen, limiting the output per bean + * to 35 characters to improve readability. Nested beans will + * also be dumped. + * + * @param OODBBean|array $data either a bean or an array of beans + * + * @return array + */ + public static function dump( $data ) + { + $array = array(); + if ( $data instanceof OODBBean ) { + $str = strval( $data ); + if (strlen($str) > 35) { + $beanStr = substr( $str, 0, 35 ).'... '; + } else { + $beanStr = $str; + } + return $beanStr; + } + if ( is_array( $data ) ) { + foreach( $data as $key => $item ) { + $array[$key] = self::dump( $item ); + } + } + return $array; + } +} +} + +namespace RedBeanPHP\Util { + +use RedBeanPHP\OODB as OODB; + +/** + * Multi Bean Loader Helper + * + * This code was originally part of the facade, however it has + * been decided to remove unique features to service classes like + * this to make them available to developers not using the facade class. + * + * This helper class offers limited support for one-to-one + * relations by providing a service to load a set of beans + * with differnt types and a common ID. + * + * @file RedBeanPHP/Util/MultiLoader.php + * @author Gabor de Mooij and the RedBeanPHP Community + * @license BSD/GPLv2 + * + * @copyright + * copyright (c) G.J.G.T. (Gabor) de Mooij and the RedBeanPHP Community + * This source file is subject to the BSD/GPLv2 License that is bundled + * with this source code in the file license.txt. + */ +class MultiLoader +{ + /** + * Loads multiple types of beans with the same ID. + * This might look like a strange method, however it can be useful + * for loading a one-to-one relation. In a typical 1-1 relation, + * you have two records sharing the same primary key. + * RedBeanPHP has only limited support for 1-1 relations. + * In general it is recommended to use 1-N for this. + * + * Usage: + * + * + * list( $author, $bio ) = R::loadMulti( 'author, bio', $id ); + * + * + * @param OODB $oodb OODB object + * @param string|array $types the set of types to load at once + * @param mixed $id the common ID + * + * @return OODBBean + */ + public static function load( OODB $oodb, $types, $id ) + { + if ( is_string( $types ) ) $types = explode( ',', $types ); + if ( !is_array( $types ) ) return array(); + foreach ( $types as $k => $typeItem ) { + $types[$k] = $oodb->load( $typeItem, $id ); + } + return $types; + } +} +} + +namespace RedBeanPHP\Util { + +use RedBeanPHP\OODB as OODB; +use RedBeanPHP\OODBBean as OODBBean; +use RedBeanPHP\RedException as RedException; +use RedBeanPHP\Adapter as Adapter; + +/** + * Transaction Helper + * + * This code was originally part of the facade, however it has + * been decided to remove unique features to service classes like + * this to make them available to developers not using the facade class. + * + * Database transaction helper. This is a convenience class + * to perform a callback in a database transaction. This class + * contains a method to wrap your callback in a transaction. + * + * @file RedBeanPHP/Util/Transaction.php + * @author Gabor de Mooij and the RedBeanPHP Community + * @license BSD/GPLv2 + * + * @copyright + * copyright (c) G.J.G.T. (Gabor) de Mooij and the RedBeanPHP Community + * This source file is subject to the BSD/GPLv2 License that is bundled + * with this source code in the file license.txt. + */ +class Transaction +{ + /** + * Wraps a transaction around a closure or string callback. + * If an Exception is thrown inside, the operation is automatically rolled back. + * If no Exception happens, it commits automatically. + * It also supports (simulated) nested transactions (that is useful when + * you have many methods that needs transactions but are unaware of + * each other). + * + * Example: + * + * + * $from = 1; + * $to = 2; + * $amount = 300; + * + * R::transaction(function() use($from, $to, $amount) + * { + * $accountFrom = R::load('account', $from); + * $accountTo = R::load('account', $to); + * $accountFrom->money -= $amount; + * $accountTo->money += $amount; + * R::store($accountFrom); + * R::store($accountTo); + * }); + * + * + * @param Adapter $adapter Database Adapter providing transaction mechanisms. + * @param callable $callback Closure (or other callable) with the transaction logic + * + * @return mixed + */ + public static function transaction( Adapter $adapter, $callback ) + { + if ( !is_callable( $callback ) ) { + throw new RedException( 'R::transaction needs a valid callback.' ); + } + + static $depth = 0; + $result = null; + try { + if ( $depth == 0 ) { + $adapter->startTransaction(); + } + $depth++; + $result = call_user_func( $callback ); //maintain 5.2 compatibility + $depth--; + if ( $depth == 0 ) { + $adapter->commit(); + } + } catch ( \Exception $exception ) { + $depth--; + if ( $depth == 0 ) { + $adapter->rollback(); + } + throw $exception; + } + return $result; + } +} +} + +namespace RedBeanPHP\Util { + +use RedBeanPHP\OODB as OODB; +use RedBeanPHP\OODBBean as OODBBean; +use RedBeanPHP\ToolBox as ToolBox; + +/** + * Quick Export Utility + * + * The Quick Export Utility Class provides functionality to easily + * expose the result of SQL queries as well-known formats like CSV. + * + * @file RedBeanPHP/Util/QuickExporft.php + * @author Gabor de Mooij and the RedBeanPHP Community + * @license BSD/GPLv2 + * + * @copyright + * copyright (c) G.J.G.T. (Gabor) de Mooij and the RedBeanPHP Community + * This source file is subject to the BSD/GPLv2 License that is bundled + * with this source code in the file license.txt. + */ +class QuickExport +{ + /** + * @var Finder + */ + protected $toolbox; + + /** + * @boolean + */ + private static $test = FALSE; + + /** + * Constructor. + * The Quick Export requires a toolbox. + * + * @param ToolBox $toolbox + */ + public function __construct( ToolBox $toolbox ) + { + $this->toolbox = $toolbox; + } + + /** + * Makes csv() testable. + */ + public static function operation( $name, $arg1, $arg2 = TRUE ) { + $out = ''; + switch( $name ) { + case 'test': + self::$test = (boolean) $arg1; + break; + case 'header': + $out = ( self::$test ) ? $arg1 : header( $arg1, $arg2 ); + break; + case 'readfile': + $out = ( self::$test ) ? file_get_contents( $arg1 ) : readfile( $arg1 ); + break; + case 'exit': + $out = ( self::$test ) ? 'exit' : exit(); + break; + } + return $out; + } + + /** + * Exposes the result of the specified SQL query as a CSV file. + * + * Usage: + * + * + * R::csv( 'SELECT + * `name`, + * population + * FROM city + * WHERE region = :region ', + * array( ':region' => 'Denmark' ), + * array( 'city', 'population' ), + * '/tmp/cities.csv' + * ); + * + * + * The command above will select all cities in Denmark + * and create a CSV with columns 'city' and 'population' and + * populate the cells under these column headers with the + * names of the cities and the population numbers respectively. + * + * @param string $sql SQL query to expose result of + * @param array $bindings parameter bindings + * @param array $columns column headers for CSV file + * @param string $path path to save CSV file to + * @param boolean $output TRUE to output CSV directly using readfile + * @param array $options delimiter, quote and escape character respectively + * + * @return void + */ + public function csv( $sql = '', $bindings = array(), $columns = NULL, $path = '/tmp/redexport_%s.csv', $output = TRUE, $options = array(',','"','\\') ) + { + list( $delimiter, $enclosure, $escapeChar ) = $options; + $path = sprintf( $path, date('Ymd_his') ); + $handle = fopen( $path, 'w' ); + if ($columns) if (PHP_VERSION_ID>=505040) fputcsv($handle, $columns, $delimiter, $enclosure, $escapeChar ); else fputcsv($handle, $columns, $delimiter, $enclosure ); + $cursor = $this->toolbox->getDatabaseAdapter()->getCursor( $sql, $bindings ); + while( $row = $cursor->getNextItem() ) { + if (PHP_VERSION_ID>=505040) fputcsv($handle, $row, $delimiter, $enclosure, $escapeChar ); else fputcsv($handle, $row, $delimiter, $enclosure ); + } + fclose($handle); + if ( $output ) { + $file = basename($path); + $out = self::operation('header',"Pragma: public"); + $out .= self::operation('header',"Expires: 0"); + $out .= self::operation('header',"Cache-Control: must-revalidate, post-check=0, pre-check=0"); + $out .= self::operation('header',"Cache-Control: private", FALSE ); + $out .= self::operation('header',"Content-Type: text/csv"); + $out .= self::operation('header',"Content-Disposition: attachment; filename={$file}" ); + $out .= self::operation('header',"Content-Transfer-Encoding: binary"); + $out .= self::operation('readfile',$path ); + @unlink( $path ); + self::operation('exit', FALSE); + return $out; + } + } +} +} + +namespace RedBeanPHP\Util { + +use RedBeanPHP\OODB as OODB; +use RedBeanPHP\OODBBean as OODBBean; +use RedBeanPHP\ToolBox as ToolBox; +use RedBeanPHP\Finder; + +/** + * MatchUp Utility + * + * Tired of creating login systems and password-forget systems? + * MatchUp is an ORM-translation of these kind of problems. + * A matchUp is a match-and-update combination in terms of beans. + * Typically login related problems are all about a match and + * a conditional update. + * + * @file RedBeanPHP/Util/MatchUp.php + * @author Gabor de Mooij and the RedBeanPHP Community + * @license BSD/GPLv2 + * + * @copyright + * copyright (c) G.J.G.T. (Gabor) de Mooij and the RedBeanPHP Community + * This source file is subject to the BSD/GPLv2 License that is bundled + * with this source code in the file license.txt. + */ +class MatchUp +{ + /** + * @var Toolbox + */ + protected $toolbox; + + /** + * Constructor. + * The MatchUp class requires a toolbox + * + * @param ToolBox $toolbox + */ + public function __construct( ToolBox $toolbox ) + { + $this->toolbox = $toolbox; + } + + /** + * MatchUp is a powerful productivity boosting method that can replace simple control + * scripts with a single RedBeanPHP command. Typically, matchUp() is used to + * replace login scripts, token generation scripts and password reset scripts. + * The MatchUp method takes a bean type, an SQL query snippet (starting at the WHERE clause), + * SQL bindings, a pair of task arrays and a bean reference. + * + * If the first 3 parameters match a bean, the first task list will be considered, + * otherwise the second one will be considered. On consideration, each task list, + * an array of keys and values will be executed. Every key in the task list should + * correspond to a bean property while every value can either be an expression to + * be evaluated or a closure (PHP 5.3+). After applying the task list to the bean + * it will be stored. If no bean has been found, a new bean will be dispensed. + * + * This method will return TRUE if the bean was found and FALSE if not AND + * there was a NOT-FOUND task list. If no bean was found AND there was also + * no second task list, NULL will be returned. + * + * To obtain the bean, pass a variable as the sixth parameter. + * The function will put the matching bean in the specified variable. + * + * Usage (this example resets a password in one go): + * + * + * $newpass = '1234'; + * $didResetPass = R::matchUp( + * 'account', ' token = ? AND tokentime > ? ', + * [ $token, time()-100 ], + * [ 'pass' => $newpass, 'token' => '' ], + * NULL, + * $account ); + * + * + * @param string $type type of bean you're looking for + * @param string $sql SQL snippet (starting at the WHERE clause, omit WHERE-keyword) + * @param array $bindings array of parameter bindings for SQL snippet + * @param array $onFoundDo task list to be considered on finding the bean + * @param array $onNotFoundDo task list to be considered on NOT finding the bean + * @param OODBBean &$bean reference to obtain the found bean + * + * @return mixed + */ + public function matchUp( $type, $sql, $bindings = array(), $onFoundDo = NULL, $onNotFoundDo = NULL, &$bean = NULL ) + { + $finder = new Finder( $this->toolbox ); + $oodb = $this->toolbox->getRedBean(); + $bean = $finder->findOne( $type, $sql, $bindings ); + if ( $bean && $onFoundDo ) { + foreach( $onFoundDo as $property => $value ) { + if ( function_exists('is_callable') && is_callable( $value ) ) { + $bean[$property] = call_user_func_array( $value, array( $bean ) ); + } else { + $bean[$property] = $value; + } + } + $oodb->store( $bean ); + return TRUE; + } + if ( $onNotFoundDo ) { + $bean = $oodb->dispense( $type ); + foreach( $onNotFoundDo as $property => $value ) { + if ( function_exists('is_callable') && is_callable( $value ) ) { + $bean[$property] = call_user_func_array( $value, array( $bean ) ); + } else { + $bean[$property] = $value; + } + } + $oodb->store( $bean ); + return FALSE; + } + return NULL; + } +} +} + +namespace RedBeanPHP\Util { + +use RedBeanPHP\OODB as OODB; +use RedBeanPHP\OODBBean as OODBBean; +use RedBeanPHP\ToolBox as ToolBox; +use RedBeanPHP\Finder; + +/** + * Look Utility + * + * The Look Utility class provides an easy way to generate + * tables and selects (pulldowns) from the database. + * + * @file RedBeanPHP/Util/Look.php + * @author Gabor de Mooij and the RedBeanPHP Community + * @license BSD/GPLv2 + * + * @copyright + * copyright (c) G.J.G.T. (Gabor) de Mooij and the RedBeanPHP Community + * This source file is subject to the BSD/GPLv2 License that is bundled + * with this source code in the file license.txt. + */ +class Look +{ + /** + * @var Toolbox + */ + protected $toolbox; + + /** + * Constructor. + * The MatchUp class requires a toolbox + * + * @param ToolBox $toolbox + */ + public function __construct( ToolBox $toolbox ) + { + $this->toolbox = $toolbox; + } + + /** + * Takes an full SQL query with optional bindings, a series of keys, a template + * and optionally a filter function and glue and assembles a view from all this. + * This is the fastest way from SQL to view. Typically this function is used to + * generate pulldown (select tag) menus with options queried from the database. + * + * Usage: + * + * + * $htmlPulldown = R::look( + * 'SELECT * FROM color WHERE value != ? ORDER BY value ASC', + * [ 'g' ], + * [ 'value', 'name' ], + * '', + * 'strtoupper', + * "\n" + * ); + * + * + * The example above creates an HTML fragment like this: + * + * + * + * + * to pick a color from a palette. The HTML fragment gets constructed by + * an SQL query that selects all colors that do not have value 'g' - this + * excludes green. Next, the bean properties 'value' and 'name' are mapped to the + * HTML template string, note that the order here is important. The mapping and + * the HTML template string follow vsprintf-rules. All property values are then + * passed through the specified filter function 'strtoupper' which in this case + * is a native PHP function to convert strings to uppercase characters only. + * Finally the resulting HTML fragment strings are glued together using a + * newline character specified in the last parameter for readability. + * + * In previous versions of RedBeanPHP you had to use: + * R::getLook()->look() instead of R::look(). However to improve useability of the + * library the look() function can now directly be invoked from the facade. + * + * @param string $sql query to execute + * @param array $bindings parameters to bind to slots mentioned in query or an empty array + * @param array $keys names in result collection to map to template + * @param string $template HTML template to fill with values associated with keys, use printf notation (i.e. %s) + * @param callable $filter function to pass values through (for translation for instance) + * @param string $glue optional glue to use when joining resulting strings + * + * @return string + */ + public function look( $sql, $bindings = array(), $keys = array( 'selected', 'id', 'name' ), $template = '', $filter = 'trim', $glue = '' ) + { + $adapter = $this->toolbox->getDatabaseAdapter(); + $lines = array(); + $rows = $adapter->get( $sql, $bindings ); + foreach( $rows as $row ) { + $values = array(); + foreach( $keys as $key ) { + if (!empty($filter)) { + $values[] = call_user_func_array( $filter, array( $row[$key] ) ); + } else { + $values[] = $row[$key]; + } + } + $lines[] = vsprintf( $template, $values ); + } + $string = implode( $glue, $lines ); + return $string; + } +} +} + +namespace RedBeanPHP\Util { + +use RedBeanPHP\OODB as OODB; +use RedBeanPHP\OODBBean as OODBBean; +use RedBeanPHP\ToolBox as ToolBox; +use RedBeanPHP\Finder; + +/** + * Diff Utility + * + * The Look Utility class provides an easy way to generate + * tables and selects (pulldowns) from the database. + * + * @file RedBeanPHP/Util/Diff.php + * @author Gabor de Mooij and the RedBeanPHP Community + * @license BSD/GPLv2 + * + * @copyright + * copyright (c) G.J.G.T. (Gabor) de Mooij and the RedBeanPHP Community + * This source file is subject to the BSD/GPLv2 License that is bundled + * with this source code in the file license.txt. + */ +class Diff +{ + /** + * @var Toolbox + */ + protected $toolbox; + + /** + * Constructor. + * The MatchUp class requires a toolbox + * + * @param ToolBox $toolbox + */ + public function __construct( ToolBox $toolbox ) + { + $this->toolbox = $toolbox; + } + + /** + * Calculates a diff between two beans (or arrays of beans). + * The result of this method is an array describing the differences of the second bean compared to + * the first, where the first bean is taken as reference. The array is keyed by type/property, id and property name, where + * type/property is either the type (in case of the root bean) or the property of the parent bean where the type resides. + * The diffs are mainly intended for logging, you cannot apply these diffs as patches to other beans. + * However this functionality might be added in the future. + * + * The keys of the array can be formatted using the $format parameter. + * A key will be composed of a path (1st), id (2nd) and property (3rd). + * Using printf-style notation you can determine the exact format of the key. + * The default format will look like: + * + * 'book.1.title' => array( , ) + * + * If you only want a simple diff of one bean and you don't care about ids, + * you might pass a format like: '%1$s.%3$s' which gives: + * + * 'book.1.title' => array( , ) + * + * The filter parameter can be used to set filters, it should be an array + * of property names that have to be skipped. By default this array is filled with + * two strings: 'created' and 'modified'. + * + * @param OODBBean|array $beans reference beans + * @param OODBBean|array $others beans to compare + * @param array $filters names of properties of all beans to skip + * @param string $format the format of the key, defaults to '%s.%s.%s' + * @param string $type type/property of bean to use for key generation + * + * @return array + */ + public function diff( $beans, $others, $filters = array( 'created', 'modified' ), $format = '%s.%s.%s', $type = NULL ) + { + $diff = array(); + + if ( !is_array( $beans ) ) $beans = array( $beans ); + $beansI = array(); + foreach ( $beans as $bean ) { + if ( !( $bean instanceof OODBBean ) ) continue; + $beansI[$bean->id] = $bean; + } + + if ( !is_array( $others ) ) $others = array( $others ); + $othersI = array(); + foreach ( $others as $other ) { + if ( !( $other instanceof OODBBean ) ) continue; + $othersI[$other->id] = $other; + } + + if ( count( $beansI ) == 0 || count( $othersI ) == 0 ) { + return array(); + } + + $type = $type != NULL ? $type : reset($beansI)->getMeta( 'type' ); + + foreach( $beansI as $id => $bean ) { + if ( !isset( $othersI[$id] ) ) continue; + $other = $othersI[$id]; + foreach( $bean as $property => $value ) { + if ( in_array( $property, $filters ) ) continue; + $key = vsprintf( $format, array( $type, $bean->id, $property ) ); + $compare = $other->{$property}; + if ( !is_object( $value ) && !is_array( $value ) && $value != $compare ) { + $diff[$key] = array( $value, $compare ); + } else { + $diff = array_merge( $diff, $this->diff( $value, $compare, $filters, $format, $key ) ); + } + } + } + + return $diff; + } +} +} + +namespace RedBeanPHP\Util { + +use RedBeanPHP\ToolBox; +use RedBeanPHP\OODBBean; + +/** + * Tree + * + * Given a bean, finds it children or parents + * in a hierchical structure. + * + * @experimental feature + * + * @file RedBeanPHP/Util/Tree.php + * @author Gabor de Mooij and the RedBeanPHP Community + * @license BSD/GPLv2 + * + * @copyright + * copyright (c) G.J.G.T. (Gabor) de Mooij and the RedBeanPHP Community + * This source file is subject to the BSD/GPLv2 License that is bundled + * with this source code in the file license.txt. + */ +class Tree { + + /** + * @var ToolBox + */ + protected $toolbox; + + /** + * @var QueryWriter + */ + protected $writer; + + /** + * @var OODB + */ + protected $oodb; + + /** + * Constructor, creates a new instance of + * the Tree. + * + * @param ToolBox $toolbox toolbox + */ + public function __construct( ToolBox $toolbox ) + { + $this->toolbox = $toolbox; + $this->writer = $toolbox->getWriter(); + $this->oodb = $toolbox->getRedBean(); + } + + /** + * Returns all child beans associates with the specified + * bean in a tree structure. + * + * @note this only works for databases that support + * recusrive common table expressions. + * + * Usage: + * + * + * $newsArticles = R::children( $newsPage, ' ORDER BY title ASC ' ) + * $newsArticles = R::children( $newsPage, ' WHERE title = ? ', [ $t ] ); + * $newsArticles = R::children( $newsPage, ' WHERE title = :t ', [ ':t' => $t ] ); + * + * + * Note: + * You are allowed to use named parameter bindings as well as + * numeric parameter bindings (using the question mark notation). + * However, you can not mix. Also, if using named parameter bindings, + * parameter binding key ':slot0' is reserved for the ID of the bean + * and used in the query. + * + * @param OODBBean $bean reference bean to find children of + * @param string $sql optional SQL snippet + * @param array $bindings optional parameter bindings for SQL snippet + * + * @return array + */ + public function children( OODBBean $bean, $sql = NULL, $bindings = array() ) + { + $type = $bean->getMeta('type'); + $id = $bean->id; + + $rows = $this->writer->queryRecursiveCommonTableExpression( $type, $id, FALSE, $sql, $bindings ); + + return $this->oodb->convertToBeans( $type, $rows ); + } + + /** + * Returns all parent beans associates with the specified + * bean in a tree structure. + * + * @note this only works for databases that support + * recusrive common table expressions. + * + * + * $newsPages = R::parents( $newsArticle, ' ORDER BY title ASC ' ); + * $newsPages = R::parents( $newsArticle, ' WHERE title = ? ', [ $t ] ); + * $newsPages = R::parents( $newsArticle, ' WHERE title = :t ', [ ':t' => $t ] ); + * + * + * Note: + * You are allowed to use named parameter bindings as well as + * numeric parameter bindings (using the question mark notation). + * However, you can not mix. Also, if using named parameter bindings, + * parameter binding key ':slot0' is reserved for the ID of the bean + * and used in the query. + * + * @param OODBBean $bean reference bean to find parents of + * @param string $sql optional SQL snippet + * @param array $bindings optional parameter bindings for SQL snippet + * + * @return array + */ + public function parents( OODBBean $bean, $sql = NULL, $bindings = array() ) + { + $type = $bean->getMeta('type'); + $id = $bean->id; + + $rows = $this->writer->queryRecursiveCommonTableExpression( $type, $id, TRUE, $sql, $bindings ); + + return $this->oodb->convertToBeans( $type, $rows ); + } + + /** + * Counts all children beans associates with the specified + * bean in a tree structure. + * + * @note this only works for databases that support + * recusrive common table expressions. + * + * + * $count = R::countChildren( $newsArticle ); + * $count = R::countChildren( $newsArticle, ' WHERE title = ? ', [ $t ] ); + * $count = R::countChildren( $newsArticle, ' WHERE title = :t ', [ ':t' => $t ] ); + * + * + * @note: + * You are allowed to use named parameter bindings as well as + * numeric parameter bindings (using the question mark notation). + * However, you can not mix. Also, if using named parameter bindings, + * parameter binding key ':slot0' is reserved for the ID of the bean + * and used in the query. + * + * @note: + * By default, if no SQL or select is given or select=TRUE this method will subtract 1 of + * the total count to omit the starting bean. If you provide your own select, + * this method assumes you take control of the resulting total yourself since + * it cannot 'predict' what or how you are trying to 'count'. + * + * @param OODBBean $bean reference bean to find children of + * @param string $sql optional SQL snippet + * @param array $bindings optional parameter bindings for SQL snippet + * @param string|boolean $select select snippet to use (advanced, optional, see QueryWriter::queryRecursiveCommonTableExpression) + * + * @return integer + */ + public function countChildren( OODBBean $bean, $sql = NULL, $bindings = array(), $select = TRUE ) { + $type = $bean->getMeta('type'); + $id = $bean->id; + $rows = $this->writer->queryRecursiveCommonTableExpression( $type, $id, FALSE, $sql, $bindings, $select ); + $first = reset($rows); + $cell = reset($first); + return (intval($cell) - (($select === TRUE && is_null($sql)) ? 1 : 0)); + } + + /** + * Counts all parent beans associates with the specified + * bean in a tree structure. + * + * @note this only works for databases that support + * recusrive common table expressions. + * + * + * $count = R::countParents( $newsArticle ); + * $count = R::countParents( $newsArticle, ' WHERE title = ? ', [ $t ] ); + * $count = R::countParents( $newsArticle, ' WHERE title = :t ', [ ':t' => $t ] ); + * + * + * Note: + * You are allowed to use named parameter bindings as well as + * numeric parameter bindings (using the question mark notation). + * However, you can not mix. Also, if using named parameter bindings, + * parameter binding key ':slot0' is reserved for the ID of the bean + * and used in the query. + * + * Note: + * By default, if no SQL or select is given or select=TRUE this method will subtract 1 of + * the total count to omit the starting bean. If you provide your own select, + * this method assumes you take control of the resulting total yourself since + * it cannot 'predict' what or how you are trying to 'count'. + * + * @param OODBBean $bean reference bean to find parents of + * @param string $sql optional SQL snippet + * @param array $bindings optional parameter bindings for SQL snippet + * @param string|boolean $select select snippet to use (advanced, optional, see QueryWriter::queryRecursiveCommonTableExpression) + * + * @return integer + */ + public function countParents( OODBBean $bean, $sql = NULL, $bindings = array(), $select = TRUE ) { + $type = $bean->getMeta('type'); + $id = $bean->id; + $rows = $this->writer->queryRecursiveCommonTableExpression( $type, $id, TRUE, $sql, $bindings, $select ); + $first = reset($rows); + $cell = reset($first); + return (intval($cell) - (($select === TRUE && is_null($sql)) ? 1 : 0)); + } +} +} + +namespace RedBeanPHP\Util { +use RedBeanPHP\Facade as R; +use RedBeanPHP\OODBBean; + +/** + * Feature Utility + * + * The Feature Utility class provides an easy way to turn + * on or off features. This allows us to introduce new features + * without accidentally breaking backward compatibility. + * + * @file RedBeanPHP/Util/Feature.php + * @author Gabor de Mooij and the RedBeanPHP Community + * @license BSD/GPLv2 + * + * @copyright + * copyright (c) G.J.G.T. (Gabor) de Mooij and the RedBeanPHP Community + * This source file is subject to the BSD/GPLv2 License that is bundled + * with this source code in the file license.txt. + */ +class Feature +{ + /* Feature set constants */ + const C_FEATURE_NOVICE_LATEST = 'novice/latest'; + const C_FEATURE_LATEST = 'latest'; + const C_FEATURE_NOVICE_5_5 = 'novice/5.5'; + const C_FEATURE_5_5 = '5.5'; + const C_FEATURE_NOVICE_5_4 = 'novice/5.4'; + const C_FEATURE_5_4 = '5.4'; + const C_FEATURE_NOVICE_5_3 = 'novice/5.3'; + const C_FEATURE_5_3 = '5.3'; + const C_FEATURE_ORIGINAL = 'original'; + + /** + * Selects the feature set you want as specified by + * the label. + * + * Available labels: + * + * novice/latest: + * - forbid R::nuke() + * - enable automatic relation resolver based on foreign keys + * - forbid R::store(All)( $bean, TRUE ) (Hybrid mode) + * - use IS-NULL conditions in findLike() etc + * + * latest: + * - allow R::nuke() + * - enable auto resolve + * - allow hybrid mode + * - use IS-NULL conditions in findLike() etc + * + * novice/X or X: + * - keep everything as it was in version X + * + * Usage: + * + * + * R::useFeatureSet( 'novice/latest' ); + * + * + * @param string $label label + * + * @return void + */ + public static function feature( $label ) { + switch( $label ) { + case self::C_FEATURE_NOVICE_LATEST: + case self::C_FEATURE_NOVICE_5_4: + case self::C_FEATURE_NOVICE_5_5: + OODBBean::useFluidCount( TRUE ); + R::noNuke( TRUE ); + R::setAllowHybridMode( FALSE ); + R::useISNULLConditions( TRUE ); + break; + case self::C_FEATURE_LATEST: + case self::C_FEATURE_5_4: + case self::C_FEATURE_5_5: + OODBBean::useFluidCount( TRUE ); + R::noNuke( FALSE ); + R::setAllowHybridMode( TRUE ); + R::useISNULLConditions( TRUE ); + break; + case self::C_FEATURE_NOVICE_5_3: + OODBBean::useFluidCount( TRUE ); + R::noNuke( TRUE ); + R::setAllowHybridMode( FALSE ); + R::useISNULLConditions( FALSE ); + break; + case self::C_FEATURE_5_3: + OODBBean::useFluidCount( TRUE ); + R::noNuke( FALSE ); + R::setAllowHybridMode( FALSE ); + R::useISNULLConditions( FALSE ); + break; + case self::C_FEATURE_ORIGINAL: + OODBBean::useFluidCount( TRUE ); + R::noNuke( FALSE ); + R::setAllowHybridMode( FALSE ); + R::useISNULLConditions( FALSE ); + break; + default: + throw new \Exception("Unknown feature set label."); + break; + } + } +} +} + +namespace RedBeanPHP { + +/** + * RedBean Plugin. + * Marker interface for plugins. + * Use this interface when defining new plugins, it's an + * easy way for the rest of the application to recognize your + * plugin. This plugin interface does not require you to + * implement a specific API. + * + * @file RedBean/Plugin.php + * @author Gabor de Mooij and the RedBeanPHP Community + * @license BSD/GPLv2 + * + * @copyright + * copyright (c) G.J.G.T. (Gabor) de Mooij and the RedBeanPHP Community + * This source file is subject to the BSD/GPLv2 License that is bundled + * with this source code in the file license.txt. + */ +interface Plugin +{ +} + +; +} +namespace { + +//make some classes available for backward compatibility +class RedBean_SimpleModel extends \RedBeanPHP\SimpleModel {}; + +if (!class_exists('R')) { + class R extends \RedBeanPHP\Facade{}; +} + + + +/** + * Support functions for RedBeanPHP. + * Additional convenience shortcut functions for RedBeanPHP. + * + * @file RedBeanPHP/Functions.php + * @author Gabor de Mooij and the RedBeanPHP community + * @license BSD/GPLv2 + * + * @copyright + * copyright (c) G.J.G.T. (Gabor) de Mooij and the RedBeanPHP Community. + * This source file is subject to the BSD/GPLv2 License that is bundled + * with this source code in the file license.txt. + */ + +/** + * Convenience function for ENUM short syntax in queries. + * + * Usage: + * + * + * R::find( 'paint', ' color_id = ? ', [ EID('color:yellow') ] ); + * + * + * If a function called EID() already exists you'll have to write this + * wrapper yourself ;) + * + * @param string $enumName enum code as you would pass to R::enum() + * + * @return mixed + */ +if (!function_exists('EID')) { + + function EID($enumName) + { + return \RedBeanPHP\Facade::enum( $enumName )->id; + } + +} + +/** + * Prints the result of R::dump() to the screen using + * print_r. + * + * @param mixed $data data to dump + * + * @return void + */ +if ( !function_exists( 'dmp' ) ) { + + function dmp( $list ) + { + print_r( \RedBeanPHP\Facade::dump( $list ) ); + } +} + +/** + * Function alias for R::genSlots(). + */ +if ( !function_exists( 'genslots' ) ) { + + function genslots( $slots, $tpl = NULL ) + { + return \RedBeanPHP\Facade::genSlots( $slots, $tpl ); + } +} + +/** + * Function alias for R::flat(). + */ +if ( !function_exists( 'array_flatten' ) ) { + + function array_flatten( $array ) + { + return \RedBeanPHP\Facade::flat( $array ); + } +} + +/** + * Function pstr() generates [ $value, \PDO::PARAM_STR ] + * Ensures that your parameter is being treated as a string. + * + * Usage: + * + * + * R::find('book', 'title = ?', [ pstr('1') ]); + * + */ +if ( !function_exists( 'pstr' ) ) { + + function pstr( $value ) + { + return array( strval( $value ) , \PDO::PARAM_STR ); + } +} + + +/** + * Function pint() generates [ $value, \PDO::PARAM_INT ] + * Ensures that your parameter is being treated as an integer. + * + * Usage: + * + * + * R::find('book', ' pages > ? ', [ pint(2) ] ); + * + */ +if ( !function_exists( 'pint' ) ) { + + function pint( $value ) + { + return array( intval( $value ) , \PDO::PARAM_INT ); + } +} + +} diff --git a/license-model.php b/license-model.php new file mode 100644 index 0000000..6f2b115 --- /dev/null +++ b/license-model.php @@ -0,0 +1,47 @@ + +

    Add License

    + + + +
    +
    + + +
    +
    + + +
    + + + \ No newline at end of file diff --git a/licenseEdit.php b/licenseEdit.php new file mode 100644 index 0000000..6f5c4e8 --- /dev/null +++ b/licenseEdit.php @@ -0,0 +1,34 @@ +
    +

    Edit License

    + + + +
    +
    + + +
    +
    + + +
    +
    + + +
    +
    + + +
    +
    + + +
    + + +
    \ No newline at end of file diff --git a/licenseListing.php b/licenseListing.php new file mode 100644 index 0000000..6361357 --- /dev/null +++ b/licenseListing.php @@ -0,0 +1,101 @@ + + +
    +

    Licenses   Add

    +
    +
    +
    +
    + + + + +
    + +
    +
    + +
    +
    + + + + + + + + + + + + + $value) { + echo ' '; + echo ' '; + echo ' '; + echo ' '; + echo ' '; + echo ' '; + echo ' '; + echo ' '; + echo ''; + } + ?> + +
    IDRelationship #EmailAPI KeyIPStatusDate
    ' . $value->id . '
    edit delete
    ' . $value->relationship_num . ' ' . $value->email . ' ' . $value->apikey . ' ' . $value->ip . ' ' . $value->status . ' ' . $value->created_at . '
    +
    + + 0 ? ($currentPage - $range) : 1; + $endPage = ($currentPage + $range) < $totalPages ? ($currentPage + $range) : $totalPages; + + ?> + + +
    \ No newline at end of file diff --git a/location-model.php b/location-model.php new file mode 100644 index 0000000..eeaf8fa --- /dev/null +++ b/location-model.php @@ -0,0 +1,49 @@ + +

    Add Location

    + + + +
    +
    + + +
    +
    + + +
    +
    + + +
    +
    + + +
    + +
    +
    \ No newline at end of file diff --git a/locationEdit.php b/locationEdit.php new file mode 100644 index 0000000..a518e5a --- /dev/null +++ b/locationEdit.php @@ -0,0 +1,29 @@ +
    +

    Edit Location

    + + + +
    + +
    + + +
    +
    + + +
    +
    + + +
    +
    + + +
    + + +
    +
    \ No newline at end of file diff --git a/locationListing.php b/locationListing.php new file mode 100644 index 0000000..7578c71 --- /dev/null +++ b/locationListing.php @@ -0,0 +1,103 @@ + + +
    +

    Locations   Add

    +
    +
    +
    +
    + + + + +
    +
    +
    +
    + +
    + + + + + + + + + + + + + $value) { + echo ' '; + echo ' '; + + echo ' '; + echo ' '; + echo ' '; + echo ' '; + + echo ''; + } + ?> + +
    IDNameLocation/Account IDWebhook URLAPI Key
    ' . $value->id . '
    edit delete
    ' . $value->name . ' ' . $value->location_id . ' ' . $value->webhook . ' ' . $value->apikey . '
    +
    + + 0 ? ($currentPage - $range) : 1; + $endPage = ($currentPage + $range) < $totalPages ? ($currentPage + $range) : $totalPages; + + ?> + + +
    \ No newline at end of file diff --git a/login.php b/login.php new file mode 100644 index 0000000..6eee0d7 --- /dev/null +++ b/login.php @@ -0,0 +1,50 @@ + + + + + + + Login Page + + + + + + +
    +
    +
    +
    +
    +
    Login
    + + + +
    +
    + + +
    +
    + + +
    +
    + +
    +
    +
    +
    +
    +
    +
    + + + + + + + + \ No newline at end of file diff --git a/migration.sql b/migration.sql new file mode 100644 index 0000000..e8aacd0 --- /dev/null +++ b/migration.sql @@ -0,0 +1,233 @@ +SET NAMES utf8mb4; + +INSERT INTO `calendar` (`id`, `slot`, `days`, `calendar`, `created_at`, `updated_at`) VALUES +(1, '{\"sunday\":[{\"from\":\"17:13\",\"to\":\"19:13\",\"point\":\"5\"},{\"from\":\"20:45\",\"to\":\"23:45\",\"point\":\"4\"}]}', 5, '50cd8253f9b01', '2023-12-13 15:13:23', '2023-12-13 20:47:46'), +(2, '{\"sunday\":[{\"from\":\"20:50\",\"to\":\"22:50\",\"point\":\"34\"}],\"monday\":[{\"from\":\"03:46\",\"to\":\"04:46\",\"point\":\"3\"}],\"tuesday\":[{\"from\":\"01:54\",\"to\":\"05:54\",\"point\":\"5\"}]}', 5, 'c8be33b88c7cb', '2023-12-13 20:50:42', '2023-12-14 00:59:12'), +(3, '{\"sunday\":[{\"from\":\"03:38\",\"to\":\"05:38\",\"point\":\"20\"}],\"thursday\":[{\"from\":\"04:38\",\"to\":\"06:38\",\"point\":\"34\"}]}', 1, '381593c1994c6', '2023-12-16 02:39:11', '2023-12-16 02:39:11'), +(4, '{\"sunday\":[{\"from\":\"20:40\",\"to\":\"21:40\",\"point\":\"4\"}],\"tuesday\":[{\"from\":\"19:40\",\"to\":\"22:40\",\"point\":\"4\"}]}', 5, '78b583337f5cf', '2023-12-18 19:40:21', '2023-12-18 19:59:25'), +(5, '{\"tuesday\":[{\"from\":\"20:11\",\"to\":\"23:11\",\"point\":\"30\"}]}', 3, 'da610b4f73fcd', '2023-12-18 20:11:27', '2023-12-18 20:11:27'), +(6, '{\"tuesday\":[{\"from\":\"21:10\",\"to\":\"23:10\",\"point\":\"30\"}]}', 3, '3b649bd662201', '2023-12-18 20:10:44', '2023-12-18 20:10:44'), +(7, '{\"tuesday\":[{\"from\":\"21:10\",\"to\":\"23:10\",\"point\":\"30\"}]}', 3, 'a63a01e97f9a6', '2023-12-18 20:12:05', '2023-12-18 20:12:05'), +(8, '{\"tuesday\":[{\"from\":\"21:13\",\"to\":\"12:13\",\"point\":\"23\"}]}', 3, '5d316e074adde', '2023-12-18 20:13:16', '2023-12-18 20:13:16'), +(9, '{\"thursday\":[{\"from\":\"13:54\",\"to\":\"16:54\",\"point\":\"20\"}]}', 3, '29a919949ceb7', '2023-12-20 13:01:20', '2023-12-20 13:01:20'), +(10, '{\"thursday\":[{\"from\":\"21:02\",\"to\":\"12:02\",\"point\":\"15\"}],\"friday\":[{\"from\":\"18:02\",\"to\":\"19:02\",\"point\":\"14\"}]}', 3, 'ed2ca9d8b720f', '2023-12-20 15:02:37', '2023-12-20 15:02:37'), +(11, '{\"thursday\":[{\"from\":\"21:02\",\"to\":\"12:02\",\"point\":\"15\"}],\"friday\":[{\"from\":\"18:02\",\"to\":\"19:02\",\"point\":\"14\"}]}', 3, '4f20856fb7568', '2023-12-20 15:03:22', '2023-12-20 15:03:22'), +(12, '{\"sunday\":[{\"from\":\"00:00\",\"to\":\"23:59\",\"point\":\"5\"}],\"saturday\":[{\"from\":\"00:00\",\"to\":\"23:59\",\"point\":\"10\"}]}', 3, 'cc97105cf395b', '2023-12-22 12:36:38', '2023-12-22 12:49:56'), +(13, '{\"monday\":[{\"from\":\"15:00\",\"to\":\"17:00\",\"point\":\"1\"}],\"tuesday\":[{\"from\":\"15:00\",\"to\":\"17:00\",\"point\":\"5\"}],\"wednesday\":[{\"from\":\"13:00\",\"to\":\"14:00\",\"point\":\"2\"}]}', 12, 'f1afb12fd65b1', '2023-12-22 13:46:32', '2023-12-22 13:50:31'), +(14, '{\"monday\":[{\"from\":\"18:00\",\"to\":\"20:00\",\"point\":\"3\"},{\"from\":\"10:00\",\"to\":\"12:00\",\"point\":\"1\"}]}', 5, 'b15dc3c0d6d55', '2023-12-22 14:05:34', '2023-12-22 14:05:34'); + + + + + +INSERT INTO `license` (`id`, `relationship_num`, `email`, `apikey`, `ip`, `status`, `created_at`, `updated_at`) VALUES +(3, '1001', 'arin@team-followup.com', '3df120851f52158682ee9de50e4261b3', '', 'active', '2023-09-07', '2023-09-07 11:10:24'), +(4, '1002', 'olu@locationflowsolutions.com', '44fdd885a36335af76c04b166e9a2b45', '', 'active', '2023-09-07', '2023-09-07 11:19:13'), +(5, '1003', 'brent.oliver@gymlaunch.com', 'a63ddca64598f4840c27e213891a5100', '', 'active', '2023-09-07', '2023-09-07 13:38:53'), +(6, '1004', 'bentleyvisualmedia@gmail.com', 'b067d63361f2116d545d3935bfb0fcc0', '', 'active', '2023-09-08', '2023-09-08 16:08:32'), +(7, '1007', 'joe@accelerra.co.uk', '36b3f289b0abe2875e488d5d14aafbf9', '', 'inactive', '2023-10-10', '2023-12-27 21:38:29'), +(8, '1008', 'ricardo@unknownemail.com', 'bbff746e31c314f27471b0c3693a90e5', '', 'inactive', '2023-11-28', '2024-02-02 19:20:04'), +(9, '1009', 'john@dokimarketing.com', '851b23d65b74f45b33c88d99dbf87aba', '', 'active', '2023-12-22', '2023-12-22 15:58:53'), +(10, '1010', 'logan@roofreach-ai.com', '9ec1130b249332d4ececd66886a34e6e', '', 'active', '2024-01-14', '2024-01-14 10:26:17'), +(11, '1011', 'bailey@roofingaisystems.com', 'ecb483afbf57d16a24fec51485651cd7', '', 'active', '2024-01-28', '2024-01-28 21:41:08'), +(12, '1012', 'cam@pinpointscaling.co.uk', 'ee9fbecd7d0aee6ceb928be32a28dd42', '', 'active', '2024-02-02', '2024-02-13 22:17:36'), +(14, '1013', 'gr@hoffmarketing.com', 'f13a2cc62a328023e901e32f6ee8a5e4', '', 'active', '2024-02-13', '2024-02-13 17:33:00'), +(15, '1014', 'Ahmed@denflow.ai', 'c51d7dd1a1bc12ae8149dd5b8134c6aa', '', 'active', '2024-02-13', '2024-02-13 22:21:37'), +(16, '1015', 'sebastian.maldonado.braven@gmail.com', '26a6a3be58c088fdcd2994f76df9a2b9', '', 'active', '2024-02-27', '2024-02-27 14:42:44'), +(17, '1016', 'dino@fullcontrolexpansion.com', '8eb65157239a27d7f53746fd26fe32ce', '', 'inactive', '2024-02-27', '2024-06-14 14:26:35'), +(18, '1017', 'travis@gocrowndigital.com', 'c01f9bcc9391e562a4b60057077ecdfb', '', 'inactive', '2024-03-06', '2024-06-12 15:00:23'), +(19, '1018', 'ej07kim@gmail.com', 'b6ec627194408a4782a797e359afa7e6', '', 'active', '2024-03-06', '2024-03-06 21:00:35'), +(20, '1019', 'diegoespi317@gmail.com', 'f2ad2df703612b0b93f7dfcf92b42270', '', 'inactive', '2024-03-12', '2024-04-02 10:44:03'), +(21, '1020', 'alex@tcmstrategy.com', '38625d777ae9ab4b61e520a1866e223d', '', 'active', '2024-03-18', '2024-03-18 12:56:00'), +(22, '1021', 'bobby@constantconnectionsgroup.com', '94fef52043e2f581d8cc505c32cc5e36', '', 'active', '2024-03-22', '2024-03-22 17:27:00'), +(23, '1022', 'justin@evermetric.com', '195acb3ce6b03058b608ea83e0f926aa', '', 'active', '2024-03-25', '2024-04-30 17:15:40'), +(24, '1023', 'luke@elitescaledigital.com', 'edd25eb25e9f32cbb9afb549101a3bad', '', 'inactive', '2024-03-26', '2024-06-18 21:24:15'), +(25, '1024', 'rotem.kraizberg@gmail.com', '19db4f255cbafe0e16f624e758101043', '', 'inactive', '2024-03-29', '2024-04-30 17:15:51'), +(26, '1025', 'gavin@roofreach-ai.com', '7ff75b1bc97bf532b725ac57484b4e4c', '', 'active', '2024-04-02', '2024-04-02 18:04:23'), +(27, '1026', 'edgarbuelna20@gmail.com', '3b56a954926b67831d2961b3e3524c85', '', 'inactive', '2024-04-11', '2024-08-09 14:40:52'), +(28, '1027', 'vishal@acepointmedia.com', '1bfa00df499460da603ebce35380ab60', '', 'active', '2024-04-11', '2024-04-11 15:29:15'), +(29, '1028', 'pasha@dentmarketing.ca', '748feee3de92902d6064783ffde851a5', '', 'active', '2024-04-12', '2024-04-12 13:31:34'), +(30, '1029', 'ian@ael-media.com', '0f4f568eaa02263e4a7da24651b98ad1', '', 'inactive', '2024-04-12', '2024-07-16 21:12:53'), +(31, '1030', 'solarmarketingpro@gmail.com', 'e0e078df81f92ff4f9ecd55105b4ae77', '', 'inactive', '2024-04-23', '2024-07-15 16:42:20'), +(32, '1031', 'raidenwburr@gmail.com', '0d3a7fbfbbf1a7a9ef954985715efdf6', '', 'active', '2024-04-25', '2024-04-25 20:46:03'), +(33, '1032', 'Evan@pingmarketingteam.com', '3c972e47103545176f0b46c1ecc7e541', '', 'active', '2024-04-26', '2024-04-26 18:35:36'), +(34, '1033', 'eli@fuzesystems.co', '0e10c71a42af8c218966018396e969c9', '', 'active', '2024-04-29', '2024-04-29 15:31:26'), +(35, '1034', 'austinjireh8@gmail.com', 'f3b93fe5ae5b15cd2890b16c0887cef0', '', 'active', '2024-04-30', '2024-04-30 13:10:23'), +(36, '1035', 'Warriorpipeline@gmail.com', '514fedc8d1dc327d75a537758cf13dfd', '', 'active', '2024-05-03', '2024-05-03 13:24:18'), +(37, '1036', 'damien@maestromedia.fr', 'c693f2788fedde782021e4bfa22e9d7f', '', 'active', '2024-05-03', '2024-05-03 14:26:16'), +(39, '1037', 'jonas@oakstellar.com', '2eb5f3fe065c0f95ef88c8d38aeb969a', '', 'inactive', '2024-05-10', '2024-09-04 17:26:13'), +(40, '1038', 'clutchdigitalio@gmail.com', '741cb0489adb1801e873d9d4a9ce6cac', '', 'active', '2024-05-13', '2024-05-13 13:12:26'), +(41, '1039', 'faisal@inflow-agency.com', '03dbb535f5410e368dd6272156963355', '', 'inactive', '2024-05-14', '2024-08-15 13:45:14'), +(42, '1040', 'arcasmail@gmail.com', '048ca52fa58b0616f9c388449c0b475c', '', 'inactive', '2024-05-20', '2024-09-18 13:53:13'), +(43, '1041', 'orien@daly-media.co.uk', 'cda0978fc74d531ccdef1710a9fb801d', '', 'active', '2024-05-22', '2024-05-22 13:37:28'), +(44, '1042', 'Luke@hw-media.co.uk', '34b7377406ddebf03f0ee6a4ab575a5d', '', 'active', '2024-05-23', '2024-05-23 13:31:20'), +(45, '1043', 'joe@casesondemand.net', '7a37dfcd31de894cbc7c63e39a1e917b', '', 'inactive', '2024-05-29', '2024-09-16 16:00:24'), +(46, '1044', 'andrey@ivsgroupuk.com', '24930fb7f7f1471316ffcc6868bca0f8', '', 'active', '2024-06-04', '2024-06-04 13:45:14'), +(47, '1045', 'laith@plaidmedia.org', 'ed2f0c063dbe9d7096dea2184b27fbce', '', 'active', '2024-06-12', '2024-06-12 19:44:52'), +(48, '1046', 'Caleb@rapidmediaco.com', '5e493cbc08cffd207baa426db25db65d', '', 'inactive', '2024-06-19', '2024-08-19 13:08:34'), +(49, '1047', 'devin@daylight-digital.com', 'eddd3a40749d64f55463892215996753', '', 'active', '2024-06-28', '2024-06-28 13:30:13'), +(50, '1048', 'oliver@mymedigrowth.com', '16732fdf5e0d540e9774af352ea7ee14', '', 'active', '2024-07-01', '2024-07-01 14:23:42'), +(51, '1049', 'Jacob@stackedstrategies.com', '430abf9083d104f6250020c203cea57a', '', 'active', '2024-07-08', '2024-07-08 16:22:25'), +(52, '1050', 'louis@theborregobrothers.com', 'd65d4fe08736d80ad561b8328e772271', '', 'inactive', '2024-07-08', '2024-10-28 14:24:04'), +(53, '1051', 'cam@pinpointscaling.co.uk', 'f7348039c4d9ee3da6a6b94de46eb2fc', '', 'active', '2024-07-15', '2024-07-15 13:10:42'), +(54, '1052', 'ivan@newlybooked.com', '91d3123c47b736ee4bd4a045681a3671', '', 'active', '2024-07-15', '2024-07-15 19:15:20'), +(55, '1053', 'robertprice2000@hotmail.co.uk', '9df794545846e2df71412347d7a702eb', '', 'active', '2024-07-19', '2024-07-19 13:06:29'), +(56, '1054', 'lucas@monadispatch.com', '5737f8403bb6555664bbb3337104ba42', '', 'active', '2024-07-22', '2024-07-22 18:42:56'), +(57, '1055', 'cate@sanesocialmedia.com', '7befec1cdbecc0dfa5392d20c969defd', '', 'active', '2024-07-26', '2024-07-26 13:59:49'), +(58, '1056', 'gabegoertzen@gmail.com', '4689420a66b7d22c945b01bee353b78e', '', 'active', '2024-07-29', '2024-07-29 13:46:33'), +(59, '1057', 'sohaib@spamgt.com', '6eb931d900001c12bd70a0d853a924f1', '', 'inactive', '2024-07-29', '2024-08-30 14:43:16'), +(61, '1058', 'todd@redlightsystems.com', 'd1256ec6baf78668a54b8e80afbff254', '', 'active', '2024-08-06', '2024-08-06 16:20:54'), +(62, '1059', 'cj@roistrategic.com', '2720b8bd88a027f4582de663adf401a9', '', 'active', '2024-08-07', '2024-08-07 13:57:15'), +(63, '1060', 'vincent@medspaadvertising.com', '586c2ffd19d4728c5cd0634fdc940369', '', 'active', '2024-08-07', '2024-08-07 20:56:49'), +(64, '1061', 'Stasiu@sandcleads.com', 'e3cf0557fb94a69c4669a3f07eb233f0', '', 'active', '2024-08-08', '2024-08-08 21:01:53'), +(65, '1062', 'sherman@digitaldoorknockers.com', 'cbb8f77941d15db44ece38cddd1854ef', '', 'active', '2024-08-13', '2024-08-13 15:37:53'), +(67, '1063', 'callum.mills@servedia.co', 'd0699797b55c85e4c55e9c15548d1491', '', 'active', '2024-08-16', '2024-08-16 15:23:11'), +(68, '1064', 'michael@llmedia.info', 'a3f4e47ae5411ca88dc648e756ac0193', '', 'active', '2024-08-19', '2024-08-19 13:35:21'), +(69, '1065', 'david@ll.media', '97fa155d1f5d1e92e867363572532c64', '', 'active', '2024-08-19', '2024-08-19 13:37:37'), +(70, '1066', 'millerepalmer@gmail.com', 'e10027ff27561f7b9d4929039eede97e', '', 'active', '2024-08-21', '2024-08-21 13:18:13'), +(71, '1067', 'sean@theperfectlook.co', '6ff86fc9de3a60e53757aeb39031965a', '', 'active', '2024-08-23', '2024-08-23 13:27:24'), +(72, '1068', 'mark@chirojump.com', 'cff66714d74dd6302bbf32c076804512', '', 'active', '2024-08-27', '2024-08-27 16:10:50'), +(73, '1069', 'bartvandaatselaar@gmail.com', '19f46574d8ca0ea61f94611d7fc2c912', '', 'active', '2024-09-02', '2024-09-02 14:00:52'), +(74, '1070', 'basnagel2000@gmail.com', 'f6326961ac7d010dcb13972feb5445dc', '', 'active', '2024-09-02', '2024-09-02 14:01:04'), +(75, '1071', 'support@jcatmediallc.com', 'd98d77bd15f2f6c33e85092ff9c838ee', '', 'active', '2024-09-04', '2024-09-04 16:39:09'), +(76, '1072', 'kontakt.ascendmarketing@gmail.com', '6a1f683233195879e70b59e250df2f67', '', 'active', '2024-09-12', '2024-09-12 13:14:49'), +(77, '1073', 'Asa@ablazeai.com', 'cb11fd7e3ff0d2f0f2ffaa5aca3a202f', '', 'active', '2024-09-12', '2024-09-12 13:15:50'), +(78, '1074', 'bart@fitnessproductions.nl', '2679df978497627563be18e6a057f0fb', '', 'active', '2024-09-12', '2024-09-12 13:49:26'), +(79, '1075', 'asa@ablazeai.com', 'dc2f862305a569e0333e2ef6485f780f', '', 'active', '2024-09-12', '2024-09-12 17:35:52'), +(80, '1076', 'info@azureonmarketing.com', 'b8684a0f4f7dfc85686a59043f9dbe8f', '', 'active', '2024-09-16', '2024-09-16 13:22:47'), +(81, '1077', 'gemma@elevatedental.agency', '3feecdbf743dd95ee344d4fb539516bd', '', 'active', '2024-09-23', '2024-09-23 14:41:58'), +(82, '1078', 'andyceo@neomarkmedia.com', '41a7f2bd3fbff7747f9842bc8b075355', '', 'active', '2024-09-26', '2024-10-31 15:12:28'), +(83, '1079', 'yuvraj@badhaodigital.com', 'fb301067b8076c590d78c9ed2a83c64e', '', 'active', '2024-09-26', '2024-09-26 21:01:08'), +(84, '1080', 'joe@socialforgesolutions.com', '315aeef54d1d1a186cc184646ce69a76', '', 'active', '2024-10-02', '2024-10-02 20:26:46'), +(85, '1081', 'gabriel@midtouchmedia.com', '3d0f591837f689f84ed2a75d6ee5d2e5', '', 'active', '2024-10-03', '2024-10-03 13:16:52'), +(86, '1082', 'Cristiano@ruchedigital.com', 'ae8d4b1b47f2c0ddfe5d2ecb7d1a6adf', '', 'active', '2024-10-04', '2024-10-04 14:10:21'), +(87, '1083', 'benmacmahon16@gmail.com', '88eebc2c9a945280b295553083cf085e', '', 'active', '2024-10-07', '2024-10-07 14:34:03'), +(88, '1084', 'certifiedappointments@gmail.com', '2442c537428edc1f733146a22dcfe11b', '', 'active', '2024-10-07', '2024-10-07 14:36:02'), +(89, '1085', 'michaelerichoward@googlemail.com', '87db6dc7c1096c0913af001aa2222fbb', '', 'active', '2024-10-08', '2024-10-08 14:00:59'), +(91, '1086', 'hardikdudeja7@gmail.com', 'ce3ad59a2ff80c17ff0329f499597a95', '', 'active', '2024-10-09', '2024-10-09 17:41:41'), +(92, '1087', 'lukeborges0@gmail.com', '0f62f93196b0dc1a4a395c759ccfb929', '', 'active', '2024-10-17', '2024-10-17 14:22:20'), +(93, '1088', 'Kristian@stargoop.com', '91465e7fbf8c92470ac726072e92a1d6', '', 'active', '2024-10-18', '2024-10-18 13:27:45'), +(94, '1089', 'joshua@redlinedigital.ca', '73252554730488a226cfc3114c3330ed', '', 'active', '2024-10-21', '2024-10-21 14:15:46'); + + + + + +INSERT INTO `project` (`id`, `project_name`, `user_id`, `slot`, `days`, `alert`, `score_threshold`, `actual_score`, `webhook`, `calendar`, `created_at`, `updated_at`, `payload`, `location`, `access_token`, `refresh_token`, `token_expiry`) VALUES +(78, 'No Bad Days Fitness', NULL, '{\"sunday\":[{\"from\":\"00:00\",\"to\":\"00:00\",\"point\":\"2\"}],\"monday\":[{\"from\":\"00:00\",\"to\":\"16:00\",\"point\":\"1\"},{\"from\":\"16:00\",\"to\":\"19:00\",\"point\":\"2\"},{\"from\":\"19:00\",\"to\":\"00:00\",\"point\":\"1\"}],\"tuesday\":[{\"from\":\"00:00\",\"to\":\"16:00\",\"point\":\"1\"},{\"from\":\"16:00\",\"to\":\"19:00\",\"point\":\"2\"},{\"from\":\"19:00\",\"to\":\"00:00\",\"point\":\"1\"}],\"wednesday\":[{\"from\":\"00:00\",\"to\":\"16:00\",\"point\":\"1\"},{\"from\":\"16:00\",\"to\":\"19:00\",\"point\":\"2\"},{\"from\":\"19:00\",\"to\":\"00:00\",\"point\":\"1\"}],\"thursday\":[{\"from\":\"00:00\",\"to\":\"16:00\",\"point\":\"1\"},{\"from\":\"16:00\",\"to\":\"19:00\",\"point\":\"2\"},{\"from\":\"19:00\",\"to\":\"00:00\",\"point\":\"1\"}],\"friday\":[{\"from\":\"00:00\",\"to\":\"16:00\",\"point\":\"1\"},{\"from\":\"16:00\",\"to\":\"19:00\",\"point\":\"2\"},{\"from\":\"19:00\",\"to\":\"00:00\",\"point\":\"1\"}],\"saturday\":[{\"from\":\"00:00\",\"to\":\"00:00\",\"point\":\"2\"}]}', 12, 'On', 15, 1, 'https://hook.eu1.make.com/g2cnu8lsuuqf72ufipq20lwa0abb8xkw', 'OTTXZjW9AHLmO9ttljjL', '2024-01-12 11:17:46', '2024-10-31 05:00:07', '{\"Project\":\"No Bad Days Fitness\", \"Calendar Link\":\"https://link.localbestgyms.com/widget/booking/OTTXZjW9AHLmO9ttljjL\"}', 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJsb2NhdGlvbl9pZCI6IktsUTRsZDVCM0RKbThZNlFvaHY4IiwiY29tcGFueV9pZCI6IkVFRHdsY2pnQTlYS29CbVQxNVQwIiwidmVyc2lvbiI6MSwiaWF0IjoxNjY4MTA4NTQxNjQxLCJzdWIiOiJ1c2VyX2lkIn0.Afu51evYMgav5Ma1niAw6nZ7VJd8R_413oEGhMeucOw', NULL, NULL, NULL), +(79, 'BFS Fitness Club', NULL, '{\"sunday\":[{\"from\":\"00:00\",\"to\":\"00:00\",\"point\":\"2\"}],\"monday\":[{\"from\":\"00:00\",\"to\":\"16:00\",\"point\":\"1\"},{\"from\":\"16:00\",\"to\":\"19:00\",\"point\":\"2\"},{\"from\":\"19:00\",\"to\":\"00:00\",\"point\":\"1\"}],\"tuesday\":[{\"from\":\"00:00\",\"to\":\"16:00\",\"point\":\"1\"},{\"from\":\"16:00\",\"to\":\"19:00\",\"point\":\"2\"},{\"from\":\"19:00\",\"to\":\"00:00\",\"point\":\"1\"}],\"wednesday\":[{\"from\":\"00:00\",\"to\":\"16:00\",\"point\":\"1\"},{\"from\":\"16:00\",\"to\":\"19:00\",\"point\":\"2\"},{\"from\":\"19:00\",\"to\":\"00:00\",\"point\":\"1\"}],\"thursday\":[{\"from\":\"00:00\",\"to\":\"16:00\",\"point\":\"1\"},{\"from\":\"16:00\",\"to\":\"19:00\",\"point\":\"2\"},{\"from\":\"19:00\",\"to\":\"00:00\",\"point\":\"1\"}],\"friday\":[{\"from\":\"00:00\",\"to\":\"16:00\",\"point\":\"1\"},{\"from\":\"16:00\",\"to\":\"19:00\",\"point\":\"2\"},{\"from\":\"19:00\",\"to\":\"00:00\",\"point\":\"1\"}],\"saturday\":[{\"from\":\"00:00\",\"to\":\"00:00\",\"point\":\"2\"}]}', 12, 'On', 15, 3, 'https://hook.eu1.make.com/g2cnu8lsuuqf72ufipq20lwa0abb8xkw', 'JL51L2IzywQYBYzeSnz9', '2024-01-12 11:18:41', '2024-10-31 05:00:16', '{\"Project\":\"BFS Fitness Club\", \"Calendar Link\":\"https://link.localbestgyms.com/widget/booking/JL51L2IzywQYBYzeSnz9\"}', 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJsb2NhdGlvbl9pZCI6IlVrYUdISjl1N1lWdGFLNG41MVo2IiwiY29tcGFueV9pZCI6IkVFRHdsY2pnQTlYS29CbVQxNVQwIiwidmVyc2lvbiI6MSwiaWF0IjoxNjc4NzQxNDE1OTQ4LCJzdWIiOiJ1c2VyX2lkIn0.Nq_03tly5m1vnkHbDnqblJzIwj0wvHJN39NVLgPazyA', NULL, NULL, NULL), +(80, 'DriveRx Fitness', NULL, '{\"sunday\":[{\"from\":\"00:00\",\"to\":\"00:00\",\"point\":\"2\"}],\"monday\":[{\"from\":\"00:00\",\"to\":\"16:00\",\"point\":\"1\"},{\"from\":\"16:00\",\"to\":\"19:00\",\"point\":\"2\"},{\"from\":\"19:00\",\"to\":\"00:00\",\"point\":\"1\"}],\"tuesday\":[{\"from\":\"00:00\",\"to\":\"16:00\",\"point\":\"1\"},{\"from\":\"16:00\",\"to\":\"19:00\",\"point\":\"2\"},{\"from\":\"19:00\",\"to\":\"00:00\",\"point\":\"1\"}],\"wednesday\":[{\"from\":\"00:00\",\"to\":\"16:00\",\"point\":\"1\"},{\"from\":\"16:00\",\"to\":\"19:00\",\"point\":\"2\"},{\"from\":\"19:00\",\"to\":\"00:00\",\"point\":\"1\"}],\"thursday\":[{\"from\":\"00:00\",\"to\":\"16:00\",\"point\":\"1\"},{\"from\":\"16:00\",\"to\":\"19:00\",\"point\":\"2\"},{\"from\":\"19:00\",\"to\":\"00:00\",\"point\":\"1\"}],\"friday\":[{\"from\":\"00:00\",\"to\":\"16:00\",\"point\":\"1\"},{\"from\":\"16:00\",\"to\":\"19:00\",\"point\":\"2\"},{\"from\":\"19:00\",\"to\":\"00:00\",\"point\":\"1\"}],\"saturday\":[{\"from\":\"00:00\",\"to\":\"00:00\",\"point\":\"2\"}]}', 12, 'On', 15, 37, 'https://hook.eu1.make.com/g2cnu8lsuuqf72ufipq20lwa0abb8xkw', '3MDc2tKsm5PW2zZtAT2n', '2024-01-12 14:25:44', '2024-10-31 05:00:45', '{\"Project\":\"DriveRx Fitness\", \"Calendar Link\":\"https://link.localbestgyms.com/widget/booking/3MDc2tKsm5PW2zZtAT2n\"}', 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJsb2NhdGlvbl9pZCI6InhRZlBOS3pFUGJ4SGhJNHFKanNZIiwiY29tcGFueV9pZCI6IkVFRHdsY2pnQTlYS29CbVQxNVQwIiwidmVyc2lvbiI6MSwiaWF0IjoxNjcwODc5Njc3ODUzLCJzdWIiOiJ1c2VyX2lkIn0.uJq7Dis9g9EbLLvRnUhtTAkKaInEJb0onWkqRJZ8jrc', NULL, NULL, NULL), +(81, 'Balance Sports Performance', NULL, '{\"sunday\":[{\"from\":\"00:00\",\"to\":\"00:00\",\"point\":\"2\"}],\"monday\":[{\"from\":\"00:00\",\"to\":\"16:00\",\"point\":\"1\"},{\"from\":\"16:00\",\"to\":\"19:00\",\"point\":\"2\"},{\"from\":\"19:00\",\"to\":\"00:00\",\"point\":\"1\"}],\"tuesday\":[{\"from\":\"00:00\",\"to\":\"16:00\",\"point\":\"1\"},{\"from\":\"16:00\",\"to\":\"19:00\",\"point\":\"2\"},{\"from\":\"19:00\",\"to\":\"00:00\",\"point\":\"1\"}],\"wednesday\":[{\"from\":\"00:00\",\"to\":\"16:00\",\"point\":\"1\"},{\"from\":\"16:00\",\"to\":\"19:00\",\"point\":\"2\"},{\"from\":\"19:00\",\"to\":\"00:00\",\"point\":\"1\"}],\"thursday\":[{\"from\":\"00:00\",\"to\":\"16:00\",\"point\":\"1\"},{\"from\":\"16:00\",\"to\":\"19:00\",\"point\":\"2\"},{\"from\":\"19:00\",\"to\":\"00:00\",\"point\":\"1\"}],\"friday\":[{\"from\":\"00:00\",\"to\":\"16:00\",\"point\":\"1\"},{\"from\":\"16:00\",\"to\":\"19:00\",\"point\":\"2\"},{\"from\":\"19:00\",\"to\":\"00:00\",\"point\":\"1\"}],\"saturday\":[{\"from\":\"00:00\",\"to\":\"00:00\",\"point\":\"2\"}]}', 12, 'On', 15, 42, 'https://hook.eu1.make.com/g2cnu8lsuuqf72ufipq20lwa0abb8xkw', 'toIpOJsRpI5VWKVw948B', '2024-01-12 14:26:46', '2024-04-10 05:00:37', '{\"Project\":\"Balance Sports Performance\", \"Calendar Link\":\"https://link.localbestgyms.com/widget/booking/toIpOJsRpI5VWKVw948B\"}', 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJsb2NhdGlvbl9pZCI6IkFRalpRbnpQSmU0RnpjczZQZ1BYIiwiY29tcGFueV9pZCI6IkVFRHdsY2pnQTlYS29CbVQxNVQwIiwidmVyc2lvbiI6MSwiaWF0IjoxNjcxMDQ5ODkzNzY4LCJzdWIiOiJ1c2VyX2lkIn0.Jb4m3TAVOXTHs4MIVCfiYyMUgQBxZ8qB9CJ8Jiv6vzQ', NULL, NULL, NULL), +(82, 'Transformation Fitness', NULL, '{\"sunday\":[{\"from\":\"00:00\",\"to\":\"00:00\",\"point\":\"2\"}],\"monday\":[{\"from\":\"00:00\",\"to\":\"16:00\",\"point\":\"1\"},{\"from\":\"16:00\",\"to\":\"19:00\",\"point\":\"2\"},{\"from\":\"19:00\",\"to\":\"00:00\",\"point\":\"1\"}],\"tuesday\":[{\"from\":\"00:00\",\"to\":\"16:00\",\"point\":\"1\"},{\"from\":\"16:00\",\"to\":\"19:00\",\"point\":\"2\"},{\"from\":\"19:00\",\"to\":\"00:00\",\"point\":\"1\"}],\"wednesday\":[{\"from\":\"00:00\",\"to\":\"16:00\",\"point\":\"1\"},{\"from\":\"16:00\",\"to\":\"19:00\",\"point\":\"2\"},{\"from\":\"19:00\",\"to\":\"00:00\",\"point\":\"1\"}],\"thursday\":[{\"from\":\"00:00\",\"to\":\"16:00\",\"point\":\"1\"},{\"from\":\"16:00\",\"to\":\"19:00\",\"point\":\"2\"},{\"from\":\"19:00\",\"to\":\"00:00\",\"point\":\"1\"}],\"friday\":[{\"from\":\"00:00\",\"to\":\"16:00\",\"point\":\"1\"},{\"from\":\"16:00\",\"to\":\"19:00\",\"point\":\"2\"},{\"from\":\"19:00\",\"to\":\"00:00\",\"point\":\"1\"}],\"saturday\":[{\"from\":\"00:00\",\"to\":\"00:00\",\"point\":\"2\"}]}', 12, 'On', 15, 7, 'https://hook.eu1.make.com/g2cnu8lsuuqf72ufipq20lwa0abb8xkw', 'zzY1nbwt91408shqZQkm', '2024-01-12 14:28:58', '2024-10-31 05:03:04', '{\"Project\":\"Transformation Fitness\", \"Calendar Link\":\"https://link.localbestgyms.com/widget/booking/zzY1nbwt91408shqZQkm\"}', 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJsb2NhdGlvbl9pZCI6IjhMaktUNU9taHBkTDg3azFpMkNqIiwiY29tcGFueV9pZCI6IkVFRHdsY2pnQTlYS29CbVQxNVQwIiwidmVyc2lvbiI6MSwiaWF0IjoxNjM5NTg2NTAzODcxLCJzdWIiOiJ1c2VyX2lkIn0.HTQB1atxyeeYvIQ6fRMFR3ff3OTeB_hjP-J0bLLd-M8', NULL, NULL, NULL), +(83, 'Egley Train Boise', NULL, '{\"sunday\":[{\"from\":\"00:00\",\"to\":\"00:00\",\"point\":\"2\"}],\"monday\":[{\"from\":\"00:00\",\"to\":\"16:00\",\"point\":\"1\"},{\"from\":\"16:00\",\"to\":\"19:00\",\"point\":\"2\"},{\"from\":\"19:00\",\"to\":\"00:00\",\"point\":\"1\"}],\"tuesday\":[{\"from\":\"00:00\",\"to\":\"16:00\",\"point\":\"1\"},{\"from\":\"16:00\",\"to\":\"19:00\",\"point\":\"2\"},{\"from\":\"19:00\",\"to\":\"00:00\",\"point\":\"1\"}],\"wednesday\":[{\"from\":\"00:00\",\"to\":\"16:00\",\"point\":\"1\"},{\"from\":\"16:00\",\"to\":\"19:00\",\"point\":\"2\"},{\"from\":\"19:00\",\"to\":\"00:00\",\"point\":\"1\"}],\"thursday\":[{\"from\":\"00:00\",\"to\":\"16:00\",\"point\":\"1\"},{\"from\":\"16:00\",\"to\":\"19:00\",\"point\":\"2\"},{\"from\":\"19:00\",\"to\":\"00:00\",\"point\":\"1\"}],\"friday\":[{\"from\":\"00:00\",\"to\":\"16:00\",\"point\":\"1\"},{\"from\":\"16:00\",\"to\":\"19:00\",\"point\":\"2\"},{\"from\":\"19:00\",\"to\":\"00:00\",\"point\":\"1\"}],\"saturday\":[{\"from\":\"00:00\",\"to\":\"00:00\",\"point\":\"2\"}]}', 12, 'On', 15, 4, 'https://hook.eu1.make.com/g2cnu8lsuuqf72ufipq20lwa0abb8xkw', 'gZJAggtDb0DDFDirLsax', '2024-01-12 14:30:04', '2024-10-31 05:03:13', '{\"Project\":\"Egley Train Boise\", \"Calendar Link\":\"https://link.localbestgyms.com/widget/booking/gZJAggtDb0DDFDirLsax\"}', 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJsb2NhdGlvbl9pZCI6InQwalRmR1dXQ1piTjh0ZE1zbGl1IiwiY29tcGFueV9pZCI6IkVFRHdsY2pnQTlYS29CbVQxNVQwIiwidmVyc2lvbiI6MSwiaWF0IjoxNjU1OTI5MzUzNzYyLCJzdWIiOiJ1c2VyX2lkIn0.HWsIwo9IF6TMmKeY1O6hz6sfyC8E6B4OECwyjs66Bcg', NULL, NULL, NULL), +(84, 'Bochner\'s Realistic Self-Defense Training and Fitness Center', NULL, '{\"sunday\":[{\"from\":\"00:00\",\"to\":\"00:00\",\"point\":\"2\"}],\"monday\":[{\"from\":\"00:00\",\"to\":\"16:00\",\"point\":\"1\"},{\"from\":\"16:00\",\"to\":\"19:00\",\"point\":\"2\"},{\"from\":\"19:00\",\"to\":\"00:00\",\"point\":\"1\"}],\"tuesday\":[{\"from\":\"00:00\",\"to\":\"16:00\",\"point\":\"1\"},{\"from\":\"16:00\",\"to\":\"19:00\",\"point\":\"2\"},{\"from\":\"19:00\",\"to\":\"00:00\",\"point\":\"1\"}],\"wednesday\":[{\"from\":\"00:00\",\"to\":\"16:00\",\"point\":\"1\"},{\"from\":\"16:00\",\"to\":\"19:00\",\"point\":\"2\"},{\"from\":\"19:00\",\"to\":\"00:00\",\"point\":\"1\"}],\"thursday\":[{\"from\":\"00:00\",\"to\":\"16:00\",\"point\":\"1\"},{\"from\":\"16:00\",\"to\":\"19:00\",\"point\":\"2\"},{\"from\":\"19:00\",\"to\":\"00:00\",\"point\":\"1\"}],\"friday\":[{\"from\":\"00:00\",\"to\":\"16:00\",\"point\":\"1\"},{\"from\":\"16:00\",\"to\":\"19:00\",\"point\":\"2\"},{\"from\":\"19:00\",\"to\":\"00:00\",\"point\":\"1\"}],\"saturday\":[{\"from\":\"00:00\",\"to\":\"00:00\",\"point\":\"2\"}]}', 12, 'On', 15, 7, 'https://hook.eu1.make.com/g2cnu8lsuuqf72ufipq20lwa0abb8xkw', 'eK5zoN7kJ8y1L8RGfJ54', '2024-01-12 14:32:26', '2024-10-31 05:03:19', '{\"Project\":\"Bochner\'s Realistic Self-Defense Training and Fitness Center\", \"Calendar Link\":\"https://link.localbestgyms.com/widget/booking/eK5zoN7kJ8y1L8RGfJ54\"}', 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJsb2NhdGlvbl9pZCI6ImxUYVBDekFjb0FkM25qZEVoTDFEIiwiY29tcGFueV9pZCI6IkVFRHdsY2pnQTlYS29CbVQxNVQwIiwidmVyc2lvbiI6MSwiaWF0IjoxNjYxODAyNzQ0NzA2LCJzdWIiOiJ1c2VyX2lkIn0.IQFSOextPRdBAo0O6MxcOyrxMZs4O0ZmyCko-MZ9PW0', NULL, NULL, NULL), +(85, 'The YourLife Gym', NULL, '{\"sunday\":[{\"from\":\"00:00\",\"to\":\"00:00\",\"point\":\"2\"}],\"monday\":[{\"from\":\"00:00\",\"to\":\"16:00\",\"point\":\"1\"},{\"from\":\"16:00\",\"to\":\"19:00\",\"point\":\"2\"},{\"from\":\"19:00\",\"to\":\"00:00\",\"point\":\"1\"}],\"tuesday\":[{\"from\":\"00:00\",\"to\":\"16:00\",\"point\":\"1\"},{\"from\":\"16:00\",\"to\":\"19:00\",\"point\":\"2\"},{\"from\":\"19:00\",\"to\":\"00:00\",\"point\":\"1\"}],\"wednesday\":[{\"from\":\"00:00\",\"to\":\"16:00\",\"point\":\"1\"},{\"from\":\"16:00\",\"to\":\"19:00\",\"point\":\"2\"},{\"from\":\"19:00\",\"to\":\"00:00\",\"point\":\"1\"}],\"thursday\":[{\"from\":\"00:00\",\"to\":\"16:00\",\"point\":\"1\"},{\"from\":\"16:00\",\"to\":\"19:00\",\"point\":\"2\"},{\"from\":\"19:00\",\"to\":\"00:00\",\"point\":\"1\"}],\"friday\":[{\"from\":\"00:00\",\"to\":\"16:00\",\"point\":\"1\"},{\"from\":\"16:00\",\"to\":\"19:00\",\"point\":\"2\"},{\"from\":\"19:00\",\"to\":\"00:00\",\"point\":\"1\"}],\"saturday\":[{\"from\":\"00:00\",\"to\":\"00:00\",\"point\":\"2\"}]}', 12, 'On', 15, 41, 'https://hook.eu1.make.com/g2cnu8lsuuqf72ufipq20lwa0abb8xkw', 'ly7WqjsKTuSqkTSo2MWi', '2024-01-12 14:33:11', '2024-03-03 05:01:43', '{\"Project\":\"The YourLife Gym\", \"Calendar Link\":\"https://link.localbestgyms.com/widget/booking/ly7WqjsKTuSqkTSo2MWi\"}', 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJsb2NhdGlvbl9pZCI6IkxjdFUxRzhWekNDakhmdmo5UkVwIiwiY29tcGFueV9pZCI6IkVFRHdsY2pnQTlYS29CbVQxNVQwIiwidmVyc2lvbiI6MSwiaWF0IjoxNjYxOTcxODcxNDYxLCJzdWIiOiJ1c2VyX2lkIn0.L_uxLg17Trppxi38CYGekAYjWt0UlrKmMR9xJsHYbWY', NULL, NULL, NULL), +(86, 'No Doubt Fitness', NULL, '{\"sunday\":[{\"from\":\"00:00\",\"to\":\"00:00\",\"point\":\"2\"}],\"monday\":[{\"from\":\"00:00\",\"to\":\"16:00\",\"point\":\"1\"},{\"from\":\"16:00\",\"to\":\"19:00\",\"point\":\"2\"},{\"from\":\"19:00\",\"to\":\"00:00\",\"point\":\"1\"}],\"tuesday\":[{\"from\":\"00:00\",\"to\":\"16:00\",\"point\":\"1\"},{\"from\":\"16:00\",\"to\":\"19:00\",\"point\":\"2\"},{\"from\":\"19:00\",\"to\":\"00:00\",\"point\":\"1\"}],\"wednesday\":[{\"from\":\"00:00\",\"to\":\"16:00\",\"point\":\"1\"},{\"from\":\"16:00\",\"to\":\"19:00\",\"point\":\"2\"},{\"from\":\"19:00\",\"to\":\"00:00\",\"point\":\"1\"}],\"thursday\":[{\"from\":\"00:00\",\"to\":\"16:00\",\"point\":\"1\"},{\"from\":\"16:00\",\"to\":\"19:00\",\"point\":\"2\"},{\"from\":\"19:00\",\"to\":\"00:00\",\"point\":\"1\"}],\"friday\":[{\"from\":\"00:00\",\"to\":\"16:00\",\"point\":\"1\"},{\"from\":\"16:00\",\"to\":\"19:00\",\"point\":\"2\"},{\"from\":\"19:00\",\"to\":\"00:00\",\"point\":\"1\"}],\"saturday\":[{\"from\":\"00:00\",\"to\":\"00:00\",\"point\":\"2\"}]}', 12, 'On', 15, 0, 'https://hook.eu1.make.com/g2cnu8lsuuqf72ufipq20lwa0abb8xkw', 'Gm9esEicp8r0OgHw3pEJ', '2024-01-12 14:34:48', '2024-03-25 05:02:15', '{\"Project\":\"No Doubt Fitness\", \"Calendar Link\":\"https://link.localbestgyms.com/widget/booking/Gm9esEicp8r0OgHw3pEJ\"}', 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJsb2NhdGlvbl9pZCI6IkJINzZxUVZ6WEJHNzlUcHdvQzN2IiwiY29tcGFueV9pZCI6IkVFRHdsY2pnQTlYS29CbVQxNVQwIiwidmVyc2lvbiI6MSwiaWF0IjoxNjYyNzM1MjE1MjI1LCJzdWIiOiJ1c2VyX2lkIn0.YHVghMo7zAYLbhmO_YRM_1b-32P7vhllXleOrnfdY1w', NULL, NULL, NULL), +(87, 'Recalibrated Performance', NULL, '{\"sunday\":[{\"from\":\"00:00\",\"to\":\"00:00\",\"point\":\"2\"}],\"monday\":[{\"from\":\"00:00\",\"to\":\"16:00\",\"point\":\"1\"},{\"from\":\"16:00\",\"to\":\"19:00\",\"point\":\"2\"},{\"from\":\"19:00\",\"to\":\"00:00\",\"point\":\"1\"}],\"tuesday\":[{\"from\":\"00:00\",\"to\":\"16:00\",\"point\":\"1\"},{\"from\":\"16:00\",\"to\":\"19:00\",\"point\":\"2\"},{\"from\":\"19:00\",\"to\":\"00:00\",\"point\":\"1\"}],\"wednesday\":[{\"from\":\"00:00\",\"to\":\"16:00\",\"point\":\"1\"},{\"from\":\"16:00\",\"to\":\"19:00\",\"point\":\"2\"},{\"from\":\"19:00\",\"to\":\"00:00\",\"point\":\"1\"}],\"thursday\":[{\"from\":\"00:00\",\"to\":\"16:00\",\"point\":\"1\"},{\"from\":\"16:00\",\"to\":\"19:00\",\"point\":\"2\"},{\"from\":\"19:00\",\"to\":\"00:00\",\"point\":\"1\"}],\"friday\":[{\"from\":\"00:00\",\"to\":\"16:00\",\"point\":\"1\"},{\"from\":\"16:00\",\"to\":\"19:00\",\"point\":\"2\"},{\"from\":\"19:00\",\"to\":\"00:00\",\"point\":\"1\"}],\"saturday\":[{\"from\":\"00:00\",\"to\":\"00:00\",\"point\":\"2\"}]}', 12, 'On', 15, 4, 'https://hook.eu1.make.com/g2cnu8lsuuqf72ufipq20lwa0abb8xkw', 'c5dM5TQiOGqCpl0k3u0S', '2024-01-12 14:34:49', '2024-10-25 05:01:57', '{\"Project\":\"Recalibrated Performance\", \"Calendar Link\":\"https://link.localbestgyms.com/widget/booking/c5dM5TQiOGqCpl0k3u0S\"}', 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJsb2NhdGlvbl9pZCI6ImNLcGdiNzliWklkWlhjUTFnMDBFIiwiY29tcGFueV9pZCI6IkVFRHdsY2pnQTlYS29CbVQxNVQwIiwidmVyc2lvbiI6MSwiaWF0IjoxNjYwNjgzMjYyNTA2LCJzdWIiOiJ1c2VyX2lkIn0.ltm8Bozm3jH29CdSu3jZArPydGiepF6vN8KUYulppCw', NULL, NULL, NULL), +(88, 'Atom Olson Fitness', NULL, '{\"sunday\":[{\"from\":\"00:00\",\"to\":\"00:00\",\"point\":\"2\"}],\"monday\":[{\"from\":\"00:00\",\"to\":\"16:00\",\"point\":\"1\"},{\"from\":\"16:00\",\"to\":\"19:00\",\"point\":\"2\"},{\"from\":\"19:00\",\"to\":\"00:00\",\"point\":\"1\"}],\"tuesday\":[{\"from\":\"00:00\",\"to\":\"16:00\",\"point\":\"1\"},{\"from\":\"16:00\",\"to\":\"19:00\",\"point\":\"2\"},{\"from\":\"19:00\",\"to\":\"00:00\",\"point\":\"1\"}],\"wednesday\":[{\"from\":\"00:00\",\"to\":\"16:00\",\"point\":\"1\"},{\"from\":\"16:00\",\"to\":\"19:00\",\"point\":\"2\"},{\"from\":\"19:00\",\"to\":\"00:00\",\"point\":\"1\"}],\"thursday\":[{\"from\":\"00:00\",\"to\":\"16:00\",\"point\":\"1\"},{\"from\":\"16:00\",\"to\":\"19:00\",\"point\":\"2\"},{\"from\":\"19:00\",\"to\":\"00:00\",\"point\":\"1\"}],\"friday\":[{\"from\":\"00:00\",\"to\":\"16:00\",\"point\":\"1\"},{\"from\":\"16:00\",\"to\":\"19:00\",\"point\":\"2\"},{\"from\":\"19:00\",\"to\":\"00:00\",\"point\":\"1\"}],\"saturday\":[{\"from\":\"00:00\",\"to\":\"00:00\",\"point\":\"2\"}]}', 12, 'On', 15, 13, 'https://hook.eu1.make.com/g2cnu8lsuuqf72ufipq20lwa0abb8xkw', 'uNHrsZxMcSU3AvXzBW1e', '2024-01-12 14:37:37', '2024-10-31 05:03:43', '{\"Project\":\"Atom Olson Fitness\", \"Calendar Link\":\"https://link.localbestgyms.com/widget/booking/uNHrsZxMcSU3AvXzBW1e\"}', 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJsb2NhdGlvbl9pZCI6ImNWZEk2WDdHTmN5dVdvQ3NZTXVUIiwiY29tcGFueV9pZCI6IkVFRHdsY2pnQTlYS29CbVQxNVQwIiwidmVyc2lvbiI6MSwiaWF0IjoxNjYzMTY4NzI4MzQxLCJzdWIiOiJ1c2VyX2lkIn0.kW37YG13lUdb4UM6MGIFdbK26Pm0GCUAb0HAX6W2kMc', NULL, NULL, NULL), +(89, 'Lazaro Health and Fitness', NULL, '{\"sunday\":[{\"from\":\"00:00\",\"to\":\"00:00\",\"point\":\"2\"}],\"monday\":[{\"from\":\"00:00\",\"to\":\"16:00\",\"point\":\"1\"},{\"from\":\"16:00\",\"to\":\"19:00\",\"point\":\"2\"},{\"from\":\"19:00\",\"to\":\"00:00\",\"point\":\"1\"}],\"tuesday\":[{\"from\":\"00:00\",\"to\":\"16:00\",\"point\":\"1\"},{\"from\":\"16:00\",\"to\":\"19:00\",\"point\":\"2\"},{\"from\":\"19:00\",\"to\":\"00:00\",\"point\":\"1\"}],\"wednesday\":[{\"from\":\"00:00\",\"to\":\"16:00\",\"point\":\"1\"},{\"from\":\"16:00\",\"to\":\"19:00\",\"point\":\"2\"},{\"from\":\"19:00\",\"to\":\"00:00\",\"point\":\"1\"}],\"thursday\":[{\"from\":\"00:00\",\"to\":\"16:00\",\"point\":\"1\"},{\"from\":\"16:00\",\"to\":\"19:00\",\"point\":\"2\"},{\"from\":\"19:00\",\"to\":\"00:00\",\"point\":\"1\"}],\"friday\":[{\"from\":\"00:00\",\"to\":\"16:00\",\"point\":\"1\"},{\"from\":\"16:00\",\"to\":\"19:00\",\"point\":\"2\"},{\"from\":\"19:00\",\"to\":\"00:00\",\"point\":\"1\"}],\"saturday\":[{\"from\":\"00:00\",\"to\":\"00:00\",\"point\":\"2\"}]}', 12, 'Off', 15, 42, 'https://hook.eu1.make.com/g2cnu8lsuuqf72ufipq20lwa0abb8xkw', 'COe25dXQbKCIHH5EcZiD', '2024-01-12 14:38:49', '2024-02-02 20:42:39', '{\"Project\":\"Lazaro Health and Fitness\", \"Calendar Link\":\"https://link.localbestgyms.com/widget/booking/COe25dXQbKCIHH5EcZiD\"}', 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJsb2NhdGlvbl9pZCI6IjN2TVVFcWNPR21Wa245bGVhdXFDIiwiY29tcGFueV9pZCI6IkVFRHdsY2pnQTlYS29CbVQxNVQwIiwidmVyc2lvbiI6MSwiaWF0IjoxNjYxMjczNTY4MDgxLCJzdWIiOiJ1c2VyX2lkIn0.v_SaiCCw-B83ZK85JfgG-Yg3SWldqVJ9YS0fjZDisCA', NULL, NULL, NULL), +(90, 'Fishers Fit Body Boot Camp', NULL, '{\"sunday\":[{\"from\":\"00:00\",\"to\":\"00:00\",\"point\":\"2\"}],\"monday\":[{\"from\":\"00:00\",\"to\":\"16:00\",\"point\":\"1\"},{\"from\":\"16:00\",\"to\":\"19:00\",\"point\":\"2\"},{\"from\":\"19:00\",\"to\":\"00:00\",\"point\":\"1\"}],\"tuesday\":[{\"from\":\"00:00\",\"to\":\"16:00\",\"point\":\"1\"},{\"from\":\"16:00\",\"to\":\"19:00\",\"point\":\"2\"},{\"from\":\"19:00\",\"to\":\"00:00\",\"point\":\"1\"}],\"wednesday\":[{\"from\":\"00:00\",\"to\":\"16:00\",\"point\":\"1\"},{\"from\":\"16:00\",\"to\":\"19:00\",\"point\":\"2\"},{\"from\":\"19:00\",\"to\":\"00:00\",\"point\":\"1\"}],\"thursday\":[{\"from\":\"00:00\",\"to\":\"16:00\",\"point\":\"1\"},{\"from\":\"16:00\",\"to\":\"19:00\",\"point\":\"2\"},{\"from\":\"19:00\",\"to\":\"00:00\",\"point\":\"1\"}],\"friday\":[{\"from\":\"00:00\",\"to\":\"16:00\",\"point\":\"1\"},{\"from\":\"16:00\",\"to\":\"19:00\",\"point\":\"2\"},{\"from\":\"19:00\",\"to\":\"00:00\",\"point\":\"1\"}],\"saturday\":[{\"from\":\"00:00\",\"to\":\"00:00\",\"point\":\"2\"}]}', 12, 'Off', 15, 42, 'https://hook.eu1.make.com/g2cnu8lsuuqf72ufipq20lwa0abb8xkw', 'Y4iKEoos56tOKjHjiHT0', '2024-01-12 14:40:17', '2024-02-02 20:42:31', '{\"Project\":\"Fishers Fit Body Boot Camp\", \"Calendar Link\":\"https://link.localbestgyms.com/widget/booking/Y4iKEoos56tOKjHjiHT0\"}', 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJsb2NhdGlvbl9pZCI6Im5FR2M4ZkI5VmE1b3dKNUJVMWhpIiwiY29tcGFueV9pZCI6IkVFRHdsY2pnQTlYS29CbVQxNVQwIiwidmVyc2lvbiI6MSwiaWF0IjoxNjY1NzY1NjIxNTA1LCJzdWIiOiJ1c2VyX2lkIn0.bJWnx58j2TKN96TrIHaK1zr1BFmxvuj3ElV_7kzJ300', NULL, NULL, NULL), +(91, 'B3 Fitness', NULL, '{\"sunday\":[{\"from\":\"00:00\",\"to\":\"00:00\",\"point\":\"2\"}],\"monday\":[{\"from\":\"00:00\",\"to\":\"16:00\",\"point\":\"1\"},{\"from\":\"16:00\",\"to\":\"19:00\",\"point\":\"2\"},{\"from\":\"19:00\",\"to\":\"00:00\",\"point\":\"1\"}],\"tuesday\":[{\"from\":\"00:00\",\"to\":\"16:00\",\"point\":\"1\"},{\"from\":\"16:00\",\"to\":\"19:00\",\"point\":\"2\"},{\"from\":\"19:00\",\"to\":\"00:00\",\"point\":\"1\"}],\"wednesday\":[{\"from\":\"00:00\",\"to\":\"16:00\",\"point\":\"1\"},{\"from\":\"16:00\",\"to\":\"19:00\",\"point\":\"2\"},{\"from\":\"19:00\",\"to\":\"00:00\",\"point\":\"1\"}],\"thursday\":[{\"from\":\"00:00\",\"to\":\"16:00\",\"point\":\"1\"},{\"from\":\"16:00\",\"to\":\"19:00\",\"point\":\"2\"},{\"from\":\"19:00\",\"to\":\"00:00\",\"point\":\"1\"}],\"friday\":[{\"from\":\"00:00\",\"to\":\"16:00\",\"point\":\"1\"},{\"from\":\"16:00\",\"to\":\"19:00\",\"point\":\"2\"},{\"from\":\"19:00\",\"to\":\"00:00\",\"point\":\"1\"}],\"saturday\":[{\"from\":\"00:00\",\"to\":\"00:00\",\"point\":\"2\"}]}', 12, 'Off', 15, 42, 'https://hook.eu1.make.com/g2cnu8lsuuqf72ufipq20lwa0abb8xkw', 's8BRasW1C7oOBJV7vHbQ', '2024-01-12 14:41:16', '2024-02-02 20:42:23', '{\"Project\":\"B3 Fitness\", \"Calendar Link\":\"https://link.localbestgyms.com/widget/booking/s8BRasW1C7oOBJV7vHbQ\"}', 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJsb2NhdGlvbl9pZCI6Ing1bnZYVTRFR0hMb3Q2bWw3ZTBSIiwiY29tcGFueV9pZCI6IkVFRHdsY2pnQTlYS29CbVQxNVQwIiwidmVyc2lvbiI6MSwiaWF0IjoxNjYwNjY3Mzk3NTQxLCJzdWIiOiJ1c2VyX2lkIn0.YW0cWlLYLbYVQoVXgce2Sc_LUpUFUxN7zEU8YeQscwM', NULL, NULL, NULL), +(92, 'EMF Fitness', NULL, '{\"sunday\":[{\"from\":\"00:00\",\"to\":\"00:00\",\"point\":\"2\"}],\"monday\":[{\"from\":\"00:00\",\"to\":\"16:00\",\"point\":\"1\"},{\"from\":\"16:00\",\"to\":\"19:00\",\"point\":\"2\"},{\"from\":\"19:00\",\"to\":\"00:00\",\"point\":\"1\"}],\"tuesday\":[{\"from\":\"00:00\",\"to\":\"16:00\",\"point\":\"1\"},{\"from\":\"16:00\",\"to\":\"19:00\",\"point\":\"2\"},{\"from\":\"19:00\",\"to\":\"00:00\",\"point\":\"1\"}],\"wednesday\":[{\"from\":\"00:00\",\"to\":\"16:00\",\"point\":\"1\"},{\"from\":\"16:00\",\"to\":\"19:00\",\"point\":\"2\"},{\"from\":\"19:00\",\"to\":\"00:00\",\"point\":\"1\"}],\"thursday\":[{\"from\":\"00:00\",\"to\":\"16:00\",\"point\":\"1\"},{\"from\":\"16:00\",\"to\":\"19:00\",\"point\":\"2\"},{\"from\":\"19:00\",\"to\":\"00:00\",\"point\":\"1\"}],\"friday\":[{\"from\":\"00:00\",\"to\":\"16:00\",\"point\":\"1\"},{\"from\":\"16:00\",\"to\":\"19:00\",\"point\":\"2\"},{\"from\":\"19:00\",\"to\":\"00:00\",\"point\":\"1\"}],\"saturday\":[{\"from\":\"00:00\",\"to\":\"00:00\",\"point\":\"2\"}]}', 12, 'Off', 15, 42, 'https://hook.eu1.make.com/g2cnu8lsuuqf72ufipq20lwa0abb8xkw', '45Y0BJJylwSqnjGQe4Au', '2024-01-12 14:41:59', '2024-02-02 20:41:17', '{\"Project\":\"EMF Fitness\", \"Calendar Link\":\"https://link.localbestgyms.com/widget/booking/45Y0BJJylwSqnjGQe4Au\"}', 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJsb2NhdGlvbl9pZCI6InNqZkNZV2FLSjljNFNTWXp3VUdNIiwiY29tcGFueV9pZCI6IkVFRHdsY2pnQTlYS29CbVQxNVQwIiwidmVyc2lvbiI6MSwiaWF0IjoxNjY1NTIxODMzNzU1LCJzdWIiOiJ1c2VyX2lkIn0.PavTQDPFh_tn89Es99mKnKMTOxF3Iw5e06GSMMdbTuU', NULL, NULL, NULL), +(93, 'AlphaGainz', NULL, '{\"sunday\":[{\"from\":\"00:00\",\"to\":\"00:00\",\"point\":\"2\"}],\"monday\":[{\"from\":\"00:00\",\"to\":\"16:00\",\"point\":\"1\"},{\"from\":\"16:00\",\"to\":\"19:00\",\"point\":\"2\"},{\"from\":\"19:00\",\"to\":\"00:00\",\"point\":\"1\"}],\"tuesday\":[{\"from\":\"00:00\",\"to\":\"16:00\",\"point\":\"1\"},{\"from\":\"16:00\",\"to\":\"19:00\",\"point\":\"2\"},{\"from\":\"19:00\",\"to\":\"00:00\",\"point\":\"1\"}],\"wednesday\":[{\"from\":\"00:00\",\"to\":\"16:00\",\"point\":\"1\"},{\"from\":\"16:00\",\"to\":\"19:00\",\"point\":\"2\"},{\"from\":\"19:00\",\"to\":\"00:00\",\"point\":\"1\"}],\"thursday\":[{\"from\":\"00:00\",\"to\":\"16:00\",\"point\":\"1\"},{\"from\":\"16:00\",\"to\":\"19:00\",\"point\":\"2\"},{\"from\":\"19:00\",\"to\":\"00:00\",\"point\":\"1\"}],\"friday\":[{\"from\":\"00:00\",\"to\":\"16:00\",\"point\":\"1\"},{\"from\":\"16:00\",\"to\":\"19:00\",\"point\":\"2\"},{\"from\":\"19:00\",\"to\":\"00:00\",\"point\":\"1\"}],\"saturday\":[{\"from\":\"00:00\",\"to\":\"00:00\",\"point\":\"2\"}]}', 12, 'Off', 15, 42, 'https://hook.eu1.make.com/g2cnu8lsuuqf72ufipq20lwa0abb8xkw', 'XybaVxFyWSh4y17huWwZ', '2024-01-12 14:42:49', '2024-02-02 20:41:11', '{\"Project\":\"AlphaGainz\", \"Calendar Link\":\"https://link.localbestgyms.com/widget/booking/XybaVxFyWSh4y17huWwZ\"}', 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJsb2NhdGlvbl9pZCI6Ild4VllLWGJXcGM4VXJlSWFIWFhKIiwiY29tcGFueV9pZCI6IkVFRHdsY2pnQTlYS29CbVQxNVQwIiwidmVyc2lvbiI6MSwiaWF0IjoxNjY0MzgzNjEyNjUyLCJzdWIiOiJ1c2VyX2lkIn0.J8Yqd2kbj167nrJEGbm9sd2RzV6ajUEguEnb6tm4MLI', NULL, NULL, NULL), +(94, 'Egans Fitness Aiea', NULL, '{\"sunday\":[{\"from\":\"00:00\",\"to\":\"00:00\",\"point\":\"2\"}],\"monday\":[{\"from\":\"00:00\",\"to\":\"16:00\",\"point\":\"1\"},{\"from\":\"16:00\",\"to\":\"19:00\",\"point\":\"2\"},{\"from\":\"19:00\",\"to\":\"00:00\",\"point\":\"1\"}],\"tuesday\":[{\"from\":\"00:00\",\"to\":\"16:00\",\"point\":\"1\"},{\"from\":\"16:00\",\"to\":\"19:00\",\"point\":\"2\"},{\"from\":\"19:00\",\"to\":\"00:00\",\"point\":\"1\"}],\"wednesday\":[{\"from\":\"00:00\",\"to\":\"16:00\",\"point\":\"1\"},{\"from\":\"16:00\",\"to\":\"19:00\",\"point\":\"2\"},{\"from\":\"19:00\",\"to\":\"00:00\",\"point\":\"1\"}],\"thursday\":[{\"from\":\"00:00\",\"to\":\"16:00\",\"point\":\"1\"},{\"from\":\"16:00\",\"to\":\"19:00\",\"point\":\"2\"},{\"from\":\"19:00\",\"to\":\"00:00\",\"point\":\"1\"}],\"friday\":[{\"from\":\"00:00\",\"to\":\"16:00\",\"point\":\"1\"},{\"from\":\"16:00\",\"to\":\"19:00\",\"point\":\"2\"},{\"from\":\"19:00\",\"to\":\"00:00\",\"point\":\"1\"}],\"saturday\":[{\"from\":\"00:00\",\"to\":\"00:00\",\"point\":\"2\"}]}', 12, 'Off', 15, 42, 'https://hook.eu1.make.com/g2cnu8lsuuqf72ufipq20lwa0abb8xkw', 'MSaDdycBZErghHdiRL2N', '2024-01-12 14:43:43', '2024-02-02 20:41:04', '{\"Project\":\"Elite Boxing Fitness (Stockton)\", \"Calendar Link\":\"https://link.localbestgyms.com/widget/booking/MSaDdycBZErghHdiRL2N\"}', 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJsb2NhdGlvbl9pZCI6Inp6UGlEaVBVNHVobHVqd1VsdWtmIiwiY29tcGFueV9pZCI6IkVFRHdsY2pnQTlYS29CbVQxNVQwIiwidmVyc2lvbiI6MSwiaWF0IjoxNjk3NTY2NjQ4NjA5LCJzdWIiOiJ1c2VyX2lkIn0.Hbc5HesljR1B7LEIUg_5pM3newdrYZeYMwo8i5mEk9g', NULL, NULL, NULL), +(95, 'Elite Boxing Fitness (Stockton)', NULL, '{\"sunday\":[{\"from\":\"00:00\",\"to\":\"00:00\",\"point\":\"2\"}],\"monday\":[{\"from\":\"00:00\",\"to\":\"16:00\",\"point\":\"1\"},{\"from\":\"16:00\",\"to\":\"19:00\",\"point\":\"2\"},{\"from\":\"19:00\",\"to\":\"00:00\",\"point\":\"1\"}],\"tuesday\":[{\"from\":\"00:00\",\"to\":\"16:00\",\"point\":\"1\"},{\"from\":\"16:00\",\"to\":\"19:00\",\"point\":\"2\"},{\"from\":\"19:00\",\"to\":\"00:00\",\"point\":\"1\"}],\"wednesday\":[{\"from\":\"00:00\",\"to\":\"16:00\",\"point\":\"1\"},{\"from\":\"16:00\",\"to\":\"19:00\",\"point\":\"2\"},{\"from\":\"19:00\",\"to\":\"00:00\",\"point\":\"1\"}],\"thursday\":[{\"from\":\"00:00\",\"to\":\"16:00\",\"point\":\"1\"},{\"from\":\"16:00\",\"to\":\"19:00\",\"point\":\"2\"},{\"from\":\"19:00\",\"to\":\"00:00\",\"point\":\"1\"}],\"friday\":[{\"from\":\"00:00\",\"to\":\"16:00\",\"point\":\"1\"},{\"from\":\"16:00\",\"to\":\"19:00\",\"point\":\"2\"},{\"from\":\"19:00\",\"to\":\"00:00\",\"point\":\"1\"}],\"saturday\":[{\"from\":\"00:00\",\"to\":\"00:00\",\"point\":\"2\"}]}', 12, 'Off', 15, 42, 'https://hook.eu1.make.com/g2cnu8lsuuqf72ufipq20lwa0abb8xkw', 'gKFSZNpiAY4F3DI2kMwj', '2024-01-12 14:46:07', '2024-02-02 20:40:59', '{\"Project\":\"Elite Boxing Fitness (Stockton)\", \"Calendar Link\":\"https://link.localbestgyms.com/widget/booking/gKFSZNpiAY4F3DI2kMwj\"}', 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJsb2NhdGlvbl9pZCI6InhjSEVyeDFxakdiVE1EZDFVYjhCIiwiY29tcGFueV9pZCI6IkVFRHdsY2pnQTlYS29CbVQxNVQwIiwidmVyc2lvbiI6MSwiaWF0IjoxNjU0NjE2Mjg3NDkyLCJzdWIiOiJ1c2VyX2lkIn0.nV_RFsJSvVhP6GRS3kANpscxy0JTiGYJON8Hj547l-Q', NULL, NULL, NULL), +(96, 'Fit Theorem - Novi ', NULL, '{\"sunday\":[{\"from\":\"00:00\",\"to\":\"00:00\",\"point\":\"2\"}],\"monday\":[{\"from\":\"00:00\",\"to\":\"16:00\",\"point\":\"1\"},{\"from\":\"16:00\",\"to\":\"19:00\",\"point\":\"2\"},{\"from\":\"19:00\",\"to\":\"00:00\",\"point\":\"1\"}],\"tuesday\":[{\"from\":\"00:00\",\"to\":\"16:00\",\"point\":\"1\"},{\"from\":\"16:00\",\"to\":\"19:00\",\"point\":\"2\"},{\"from\":\"19:00\",\"to\":\"00:00\",\"point\":\"1\"}],\"wednesday\":[{\"from\":\"00:00\",\"to\":\"16:00\",\"point\":\"1\"},{\"from\":\"16:00\",\"to\":\"19:00\",\"point\":\"2\"},{\"from\":\"19:00\",\"to\":\"00:00\",\"point\":\"1\"}],\"thursday\":[{\"from\":\"00:00\",\"to\":\"16:00\",\"point\":\"1\"},{\"from\":\"16:00\",\"to\":\"19:00\",\"point\":\"2\"},{\"from\":\"19:00\",\"to\":\"00:00\",\"point\":\"1\"}],\"friday\":[{\"from\":\"00:00\",\"to\":\"16:00\",\"point\":\"1\"},{\"from\":\"16:00\",\"to\":\"19:00\",\"point\":\"2\"},{\"from\":\"19:00\",\"to\":\"00:00\",\"point\":\"1\"}],\"saturday\":[{\"from\":\"00:00\",\"to\":\"00:00\",\"point\":\"2\"}]}', 12, 'Off', 15, 42, 'https://hook.eu1.make.com/g2cnu8lsuuqf72ufipq20lwa0abb8xkw', '5Kg1uoGHVrwFKDfsKQDK', '2024-01-12 14:48:56', '2024-02-02 20:40:48', '{\"Project\":\"Fit Theorem - Novi \", \"Calendar Link\":\"https://link.localbestgyms.com/widget/booking/5Kg1uoGHVrwFKDfsKQDK\"}', 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJsb2NhdGlvbl9pZCI6Iko2ZlJrVVdPRm50REFMVFgxOEg3IiwiY29tcGFueV9pZCI6IkVFRHdsY2pnQTlYS29CbVQxNVQwIiwidmVyc2lvbiI6MSwiaWF0IjoxNjYyMTU0NTI3MzExLCJzdWIiOiJ1c2VyX2lkIn0.mQtozMa-u1m0qAxNTII5YO2p54EoZk9e4htIdc01-pk', NULL, NULL, NULL), +(97, 'Gulf Coast Performance', NULL, '{\"sunday\":[{\"from\":\"00:00\",\"to\":\"00:00\",\"point\":\"2\"}],\"monday\":[{\"from\":\"00:00\",\"to\":\"16:00\",\"point\":\"1\"},{\"from\":\"16:00\",\"to\":\"19:00\",\"point\":\"2\"},{\"from\":\"19:00\",\"to\":\"00:00\",\"point\":\"1\"}],\"tuesday\":[{\"from\":\"00:00\",\"to\":\"16:00\",\"point\":\"1\"},{\"from\":\"16:00\",\"to\":\"19:00\",\"point\":\"2\"},{\"from\":\"19:00\",\"to\":\"00:00\",\"point\":\"1\"}],\"wednesday\":[{\"from\":\"00:00\",\"to\":\"16:00\",\"point\":\"1\"},{\"from\":\"16:00\",\"to\":\"19:00\",\"point\":\"2\"},{\"from\":\"19:00\",\"to\":\"00:00\",\"point\":\"1\"}],\"thursday\":[{\"from\":\"00:00\",\"to\":\"16:00\",\"point\":\"1\"},{\"from\":\"16:00\",\"to\":\"19:00\",\"point\":\"2\"},{\"from\":\"19:00\",\"to\":\"00:00\",\"point\":\"1\"}],\"friday\":[{\"from\":\"00:00\",\"to\":\"16:00\",\"point\":\"1\"},{\"from\":\"16:00\",\"to\":\"19:00\",\"point\":\"2\"},{\"from\":\"19:00\",\"to\":\"00:00\",\"point\":\"1\"}],\"saturday\":[{\"from\":\"00:00\",\"to\":\"00:00\",\"point\":\"2\"}]}', 12, 'Off', 15, 42, 'https://hook.eu1.make.com/g2cnu8lsuuqf72ufipq20lwa0abb8xkw', 'KsvRnGNXA4gLHuUmHXIz', '2024-01-12 14:50:29', '2024-02-02 20:40:42', '{\"Project\":\"Gulf Coast Performance\", \"Calendar Link\":\"https://link.localbestgyms.com/widget/booking/KsvRnGNXA4gLHuUmHXIz\"}', 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJsb2NhdGlvbl9pZCI6Imlhd3VqdzFtWGpxbzVDb0JCUHg4IiwiY29tcGFueV9pZCI6IkVFRHdsY2pnQTlYS29CbVQxNVQwIiwidmVyc2lvbiI6MSwiaWF0IjoxNjYzODY3MDEzMjM5LCJzdWIiOiJ1c2VyX2lkIn0.4ej8T6jTx1g0wXUoxUXVg7iDi6B94rXJEYWPc6NcLuQ', NULL, NULL, NULL), +(98, 'G-Five Training and Wellness', NULL, '{\"sunday\":[{\"from\":\"00:00\",\"to\":\"00:00\",\"point\":\"2\"}],\"monday\":[{\"from\":\"00:00\",\"to\":\"16:00\",\"point\":\"1\"},{\"from\":\"16:00\",\"to\":\"19:00\",\"point\":\"2\"},{\"from\":\"19:00\",\"to\":\"00:00\",\"point\":\"1\"}],\"tuesday\":[{\"from\":\"00:00\",\"to\":\"16:00\",\"point\":\"1\"},{\"from\":\"16:00\",\"to\":\"19:00\",\"point\":\"2\"},{\"from\":\"19:00\",\"to\":\"00:00\",\"point\":\"1\"}],\"wednesday\":[{\"from\":\"00:00\",\"to\":\"16:00\",\"point\":\"1\"},{\"from\":\"16:00\",\"to\":\"19:00\",\"point\":\"2\"},{\"from\":\"19:00\",\"to\":\"00:00\",\"point\":\"1\"}],\"thursday\":[{\"from\":\"00:00\",\"to\":\"16:00\",\"point\":\"1\"},{\"from\":\"16:00\",\"to\":\"19:00\",\"point\":\"2\"},{\"from\":\"19:00\",\"to\":\"00:00\",\"point\":\"1\"}],\"friday\":[{\"from\":\"00:00\",\"to\":\"16:00\",\"point\":\"1\"},{\"from\":\"16:00\",\"to\":\"19:00\",\"point\":\"2\"},{\"from\":\"19:00\",\"to\":\"00:00\",\"point\":\"1\"}],\"saturday\":[{\"from\":\"00:00\",\"to\":\"00:00\",\"point\":\"2\"}]}', 12, 'Off', 15, 42, 'https://hook.eu1.make.com/g2cnu8lsuuqf72ufipq20lwa0abb8xkw', 'JBtL6Aqa9QDYZGxPpGNb', '2024-01-12 14:56:16', '2024-02-02 20:40:35', '{\"Project\":\"G-Five Training and Wellness\", \"Calendar Link\":\"https://link.localbestgyms.com/widget/booking/JBtL6Aqa9QDYZGxPpGNb\"}', 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJsb2NhdGlvbl9pZCI6IkhoSUFkMTBkREdWYzVKZm5BWlIyIiwiY29tcGFueV9pZCI6IkVFRHdsY2pnQTlYS29CbVQxNVQwIiwidmVyc2lvbiI6MSwiaWF0IjoxNjg0NDIwMzM1MjQ5LCJzdWIiOiJ1c2VyX2lkIn0.QaBqZNYNeMODkTfaveTn6TwFsQvP0GvXlUFUViz2EeE', NULL, NULL, NULL), +(99, 'A1 Health & Fitness', NULL, '{\"sunday\":[{\"from\":\"00:00\",\"to\":\"00:00\",\"point\":\"2\"}],\"monday\":[{\"from\":\"00:00\",\"to\":\"16:00\",\"point\":\"1\"},{\"from\":\"16:00\",\"to\":\"19:00\",\"point\":\"2\"},{\"from\":\"19:00\",\"to\":\"00:00\",\"point\":\"1\"}],\"tuesday\":[{\"from\":\"00:00\",\"to\":\"16:00\",\"point\":\"1\"},{\"from\":\"16:00\",\"to\":\"19:00\",\"point\":\"2\"},{\"from\":\"19:00\",\"to\":\"00:00\",\"point\":\"1\"}],\"wednesday\":[{\"from\":\"00:00\",\"to\":\"16:00\",\"point\":\"1\"},{\"from\":\"16:00\",\"to\":\"19:00\",\"point\":\"2\"},{\"from\":\"19:00\",\"to\":\"00:00\",\"point\":\"1\"}],\"thursday\":[{\"from\":\"00:00\",\"to\":\"16:00\",\"point\":\"1\"},{\"from\":\"16:00\",\"to\":\"19:00\",\"point\":\"2\"},{\"from\":\"19:00\",\"to\":\"00:00\",\"point\":\"1\"}],\"friday\":[{\"from\":\"00:00\",\"to\":\"16:00\",\"point\":\"1\"},{\"from\":\"16:00\",\"to\":\"19:00\",\"point\":\"2\"},{\"from\":\"19:00\",\"to\":\"00:00\",\"point\":\"1\"}],\"saturday\":[{\"from\":\"00:00\",\"to\":\"00:00\",\"point\":\"2\"}]}', 12, 'On', 15, 20, 'https://hook.eu1.make.com/g2cnu8lsuuqf72ufipq20lwa0abb8xkw', 'Fx6FwRYgwQBxNRxuLN0Y', '2024-01-12 14:56:57', '2024-10-31 05:04:20', '{\"Project\":\"A1 Health & Fitness\", \"Calendar Link\":\"https://link.localbestgyms.com/widget/booking/Fx6FwRYgwQBxNRxuLN0Y\"}', 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJsb2NhdGlvbl9pZCI6IjBJUzNWVFZQUnFyUjRVMjZMSkIzIiwiY29tcGFueV9pZCI6IkVFRHdsY2pnQTlYS29CbVQxNVQwIiwidmVyc2lvbiI6MSwiaWF0IjoxNjU2NDQ1NjM5MDMxLCJzdWIiOiJ1c2VyX2lkIn0.Wuvc2UcrErNPXFLFKvshckT3TFl5wkjzyoi_DoxkV6c', NULL, NULL, NULL), +(100, 'Southside Knockout Orland Park', NULL, '{\"sunday\":[{\"from\":\"00:00\",\"to\":\"00:00\",\"point\":\"2\"}],\"monday\":[{\"from\":\"00:00\",\"to\":\"16:00\",\"point\":\"1\"},{\"from\":\"16:00\",\"to\":\"19:00\",\"point\":\"2\"},{\"from\":\"19:00\",\"to\":\"00:00\",\"point\":\"1\"}],\"tuesday\":[{\"from\":\"00:00\",\"to\":\"16:00\",\"point\":\"1\"},{\"from\":\"16:00\",\"to\":\"19:00\",\"point\":\"2\"},{\"from\":\"19:00\",\"to\":\"00:00\",\"point\":\"1\"}],\"wednesday\":[{\"from\":\"00:00\",\"to\":\"16:00\",\"point\":\"1\"},{\"from\":\"16:00\",\"to\":\"19:00\",\"point\":\"2\"},{\"from\":\"19:00\",\"to\":\"00:00\",\"point\":\"1\"}],\"thursday\":[{\"from\":\"00:00\",\"to\":\"16:00\",\"point\":\"1\"},{\"from\":\"16:00\",\"to\":\"19:00\",\"point\":\"2\"},{\"from\":\"19:00\",\"to\":\"00:00\",\"point\":\"1\"}],\"friday\":[{\"from\":\"00:00\",\"to\":\"16:00\",\"point\":\"1\"},{\"from\":\"16:00\",\"to\":\"19:00\",\"point\":\"2\"},{\"from\":\"19:00\",\"to\":\"00:00\",\"point\":\"1\"}],\"saturday\":[{\"from\":\"00:00\",\"to\":\"00:00\",\"point\":\"2\"}]}', 12, 'On', 15, 729, 'https://hook.eu1.make.com/g2cnu8lsuuqf72ufipq20lwa0abb8xkw', 'dAaKgPMYHSiM7iK5Sp2v', '2024-01-12 14:58:07', '2024-10-31 05:13:54', '{\"Project\":\"Southside Knockout Orland Park\", \"Calendar Link\":\"https://link.localbestgyms.com/widget/booking/dAaKgPMYHSiM7iK5Sp2v\"}', 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJsb2NhdGlvbl9pZCI6IlZkNmo4NVRpSjJZaEQzYVFWM2pSIiwiY29tcGFueV9pZCI6IkVFRHdsY2pnQTlYS29CbVQxNVQwIiwidmVyc2lvbiI6MSwiaWF0IjoxNjkyMzg1ODk5NjQ0LCJzdWIiOiJ1c2VyX2lkIn0.IrlkkZkT_AeILu7yn1n47gX1U--PXWWYRKLy3eRhi5g', NULL, NULL, NULL), +(101, 'CaliUnity', NULL, '{\"sunday\":[{\"from\":\"00:00\",\"to\":\"00:00\",\"point\":\"2\"}],\"monday\":[{\"from\":\"00:00\",\"to\":\"16:00\",\"point\":\"1\"},{\"from\":\"16:00\",\"to\":\"19:00\",\"point\":\"2\"},{\"from\":\"19:00\",\"to\":\"00:00\",\"point\":\"1\"}],\"tuesday\":[{\"from\":\"00:00\",\"to\":\"16:00\",\"point\":\"1\"},{\"from\":\"16:00\",\"to\":\"19:00\",\"point\":\"2\"},{\"from\":\"19:00\",\"to\":\"00:00\",\"point\":\"1\"}],\"wednesday\":[{\"from\":\"00:00\",\"to\":\"16:00\",\"point\":\"1\"},{\"from\":\"16:00\",\"to\":\"19:00\",\"point\":\"2\"},{\"from\":\"19:00\",\"to\":\"00:00\",\"point\":\"1\"}],\"thursday\":[{\"from\":\"00:00\",\"to\":\"16:00\",\"point\":\"1\"},{\"from\":\"16:00\",\"to\":\"19:00\",\"point\":\"2\"},{\"from\":\"19:00\",\"to\":\"00:00\",\"point\":\"1\"}],\"friday\":[{\"from\":\"00:00\",\"to\":\"16:00\",\"point\":\"1\"},{\"from\":\"16:00\",\"to\":\"19:00\",\"point\":\"2\"},{\"from\":\"19:00\",\"to\":\"00:00\",\"point\":\"1\"}],\"saturday\":[{\"from\":\"00:00\",\"to\":\"00:00\",\"point\":\"2\"}]}', 12, 'On', 15, 2, 'https://hook.eu1.make.com/g2cnu8lsuuqf72ufipq20lwa0abb8xkw', '4tNTFrjLrdpYt5GGmasE', '2024-01-12 14:59:04', '2024-10-01 05:12:18', '{\"Project\":\"CaliUnity\", \"Calendar Link\":\"https://link.localbestgyms.com/widget/booking/4tNTFrjLrdpYt5GGmasE\"}', 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJsb2NhdGlvbl9pZCI6IjBWMlByWXdsT1dqb1hrbkRDV3hWIiwiY29tcGFueV9pZCI6IkVFRHdsY2pnQTlYS29CbVQxNVQwIiwidmVyc2lvbiI6MSwiaWF0IjoxNjYyMTEwOTI1MTI3LCJzdWIiOiJ1c2VyX2lkIn0.HA3HfYqeuNtGWwjRWSvygiCCTXio2w9k_gHbp5Z7F08', NULL, NULL, NULL), +(102, 'Armada Cross Training', NULL, '{\"sunday\":[{\"from\":\"00:00\",\"to\":\"00:00\",\"point\":\"2\"}],\"monday\":[{\"from\":\"00:00\",\"to\":\"16:00\",\"point\":\"1\"},{\"from\":\"16:00\",\"to\":\"19:00\",\"point\":\"2\"},{\"from\":\"19:00\",\"to\":\"00:00\",\"point\":\"1\"}],\"tuesday\":[{\"from\":\"00:00\",\"to\":\"16:00\",\"point\":\"1\"},{\"from\":\"16:00\",\"to\":\"19:00\",\"point\":\"2\"},{\"from\":\"19:00\",\"to\":\"00:00\",\"point\":\"1\"}],\"wednesday\":[{\"from\":\"00:00\",\"to\":\"16:00\",\"point\":\"1\"},{\"from\":\"16:00\",\"to\":\"19:00\",\"point\":\"2\"},{\"from\":\"19:00\",\"to\":\"00:00\",\"point\":\"1\"}],\"thursday\":[{\"from\":\"00:00\",\"to\":\"16:00\",\"point\":\"1\"},{\"from\":\"16:00\",\"to\":\"19:00\",\"point\":\"2\"},{\"from\":\"19:00\",\"to\":\"00:00\",\"point\":\"1\"}],\"friday\":[{\"from\":\"00:00\",\"to\":\"16:00\",\"point\":\"1\"},{\"from\":\"16:00\",\"to\":\"19:00\",\"point\":\"2\"},{\"from\":\"19:00\",\"to\":\"00:00\",\"point\":\"1\"}],\"saturday\":[{\"from\":\"00:00\",\"to\":\"00:00\",\"point\":\"2\"}]}', 12, 'On', 15, 6, 'https://hook.eu1.make.com/g2cnu8lsuuqf72ufipq20lwa0abb8xkw', 'D7aBAiNVTpkEK3XiNdZF', '2024-01-12 15:00:05', '2024-10-31 05:14:00', '{\"Project\":\"Armada Cross Training\", \"Calendar Link\":\"https://link.localbestgyms.com/widget/booking/D7aBAiNVTpkEK3XiNdZF\"}', 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJsb2NhdGlvbl9pZCI6ImFjaGhxYTBjUmlpV1dTUmtIN3FZIiwiY29tcGFueV9pZCI6IkVFRHdsY2pnQTlYS29CbVQxNVQwIiwidmVyc2lvbiI6MSwiaWF0IjoxNjY3ODQ2ODEwNjMxLCJzdWIiOiJ1c2VyX2lkIn0.IbTGi5UrKNr9B5gI7mSScc-m60PVwwCgdEwxHPbxyOY', NULL, NULL, NULL), +(103, 'F45 Training Colorado Springs Central', NULL, '{\"sunday\":[{\"from\":\"00:00\",\"to\":\"00:00\",\"point\":\"2\"}],\"monday\":[{\"from\":\"00:00\",\"to\":\"16:00\",\"point\":\"1\"},{\"from\":\"16:00\",\"to\":\"19:00\",\"point\":\"2\"},{\"from\":\"19:00\",\"to\":\"00:00\",\"point\":\"1\"}],\"tuesday\":[{\"from\":\"00:00\",\"to\":\"16:00\",\"point\":\"1\"},{\"from\":\"16:00\",\"to\":\"19:00\",\"point\":\"2\"},{\"from\":\"19:00\",\"to\":\"00:00\",\"point\":\"1\"}],\"wednesday\":[{\"from\":\"00:00\",\"to\":\"16:00\",\"point\":\"1\"},{\"from\":\"16:00\",\"to\":\"19:00\",\"point\":\"2\"},{\"from\":\"19:00\",\"to\":\"00:00\",\"point\":\"1\"}],\"thursday\":[{\"from\":\"00:00\",\"to\":\"16:00\",\"point\":\"1\"},{\"from\":\"16:00\",\"to\":\"19:00\",\"point\":\"2\"},{\"from\":\"19:00\",\"to\":\"00:00\",\"point\":\"1\"}],\"friday\":[{\"from\":\"00:00\",\"to\":\"16:00\",\"point\":\"1\"},{\"from\":\"16:00\",\"to\":\"19:00\",\"point\":\"2\"},{\"from\":\"19:00\",\"to\":\"00:00\",\"point\":\"1\"}],\"saturday\":[{\"from\":\"00:00\",\"to\":\"00:00\",\"point\":\"2\"}]}', 12, 'On', 15, 0, 'https://hook.eu1.make.com/g2cnu8lsuuqf72ufipq20lwa0abb8xkw', 'tfKTnUJsMEU4Xb1zBkj6', '2024-01-12 15:07:43', '2024-10-10 05:11:20', '{\"Project\":\"F45 Training Colorado Springs Central\", \"Calendar Link\":\"https://link.localbestgyms.com/widget/booking/tfKTnUJsMEU4Xb1zBkj6\"}', 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJsb2NhdGlvbl9pZCI6Im83WjNYQlR3ZEpiREpiWlNoRTJBIiwiY29tcGFueV9pZCI6IkVFRHdsY2pnQTlYS29CbVQxNVQwIiwidmVyc2lvbiI6MSwiaWF0IjoxNjc4NDY5MDA0OTE5LCJzdWIiOiJ1c2VyX2lkIn0.J87rwQn_KOK7FFU5coSjOMhdMcJjPTFs4Sui6ybuXuk', NULL, NULL, NULL), +(104, 'Empower Fitness', NULL, '{\"sunday\":[{\"from\":\"00:00\",\"to\":\"00:00\",\"point\":\"2\"}],\"monday\":[{\"from\":\"00:00\",\"to\":\"16:00\",\"point\":\"1\"},{\"from\":\"16:00\",\"to\":\"19:00\",\"point\":\"2\"},{\"from\":\"19:00\",\"to\":\"00:00\",\"point\":\"1\"}],\"tuesday\":[{\"from\":\"00:00\",\"to\":\"16:00\",\"point\":\"1\"},{\"from\":\"16:00\",\"to\":\"19:00\",\"point\":\"2\"},{\"from\":\"19:00\",\"to\":\"00:00\",\"point\":\"1\"}],\"wednesday\":[{\"from\":\"00:00\",\"to\":\"16:00\",\"point\":\"1\"},{\"from\":\"16:00\",\"to\":\"19:00\",\"point\":\"2\"},{\"from\":\"19:00\",\"to\":\"00:00\",\"point\":\"1\"}],\"thursday\":[{\"from\":\"00:00\",\"to\":\"16:00\",\"point\":\"1\"},{\"from\":\"16:00\",\"to\":\"19:00\",\"point\":\"2\"},{\"from\":\"19:00\",\"to\":\"00:00\",\"point\":\"1\"}],\"friday\":[{\"from\":\"00:00\",\"to\":\"16:00\",\"point\":\"1\"},{\"from\":\"16:00\",\"to\":\"19:00\",\"point\":\"2\"},{\"from\":\"19:00\",\"to\":\"00:00\",\"point\":\"1\"}],\"saturday\":[{\"from\":\"00:00\",\"to\":\"00:00\",\"point\":\"2\"}]}', 12, 'On', 15, 20, 'https://hook.eu1.make.com/g2cnu8lsuuqf72ufipq20lwa0abb8xkw', '7DzccNWgbctsPoAKovys', '2024-01-12 15:13:10', '2024-04-12 05:02:20', '{\"Project\":\"Empower Fitness\", \"Calendar Link\":\"https://link.localbestgyms.com/widget/booking/7DzccNWgbctsPoAKovys\"}', 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJsb2NhdGlvbl9pZCI6Imhwdlk3Q2c0ZjZUQjhqQlhZdjJtIiwiY29tcGFueV9pZCI6IkVFRHdsY2pnQTlYS29CbVQxNVQwIiwidmVyc2lvbiI6MSwiaWF0IjoxNjgzODMzNTc5MTA3LCJzdWIiOiJ1c2VyX2lkIn0.mHc9xn2YYrpZ_pBwdPkSKzCyJfdPu8Z6WqGBl9usrQo', NULL, NULL, NULL), +(105, 'Newport LKD', NULL, '{\"sunday\":[{\"from\":\"00:00\",\"to\":\"00:00\",\"point\":\"2\"}],\"monday\":[{\"from\":\"00:00\",\"to\":\"16:00\",\"point\":\"1\"},{\"from\":\"16:00\",\"to\":\"19:00\",\"point\":\"2\"},{\"from\":\"19:00\",\"to\":\"00:00\",\"point\":\"1\"}],\"tuesday\":[{\"from\":\"00:00\",\"to\":\"16:00\",\"point\":\"1\"},{\"from\":\"16:00\",\"to\":\"19:00\",\"point\":\"2\"},{\"from\":\"19:00\",\"to\":\"00:00\",\"point\":\"1\"}],\"wednesday\":[{\"from\":\"00:00\",\"to\":\"16:00\",\"point\":\"1\"},{\"from\":\"16:00\",\"to\":\"19:00\",\"point\":\"2\"},{\"from\":\"19:00\",\"to\":\"00:00\",\"point\":\"1\"}],\"thursday\":[{\"from\":\"00:00\",\"to\":\"16:00\",\"point\":\"1\"},{\"from\":\"16:00\",\"to\":\"19:00\",\"point\":\"2\"},{\"from\":\"19:00\",\"to\":\"00:00\",\"point\":\"1\"}],\"friday\":[{\"from\":\"00:00\",\"to\":\"16:00\",\"point\":\"1\"},{\"from\":\"16:00\",\"to\":\"19:00\",\"point\":\"2\"},{\"from\":\"19:00\",\"to\":\"00:00\",\"point\":\"1\"}],\"saturday\":[{\"from\":\"00:00\",\"to\":\"00:00\",\"point\":\"2\"}]}', 12, 'On', 15, 18, 'https://hook.eu1.make.com/g2cnu8lsuuqf72ufipq20lwa0abb8xkw', 'Pe1uNtsPsornOSRLrLM2', '2024-01-12 15:13:56', '2024-10-31 05:14:05', '{\"Project\":\"Newport LKD\", \"Calendar Link\":\"https://link.localbestgyms.com/widget/booking/Pe1uNtsPsornOSRLrLM2\"}', 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJsb2NhdGlvbl9pZCI6IkFQVFRacFBCaVRqQ01PZGlzT2lLIiwiY29tcGFueV9pZCI6IkVFRHdsY2pnQTlYS29CbVQxNVQwIiwidmVyc2lvbiI6MSwiaWF0IjoxNjczMDI4ODI5MjE5LCJzdWIiOiJ1c2VyX2lkIn0.HAsSC5YKc3QZ1kFw4fjGsVlR7mv39C27PQ9aeXHY8RE', NULL, NULL, NULL), +(106, 'Peak 360', NULL, '{\"sunday\":[{\"from\":\"00:00\",\"to\":\"00:00\",\"point\":\"2\"}],\"monday\":[{\"from\":\"00:00\",\"to\":\"16:00\",\"point\":\"1\"},{\"from\":\"16:00\",\"to\":\"19:00\",\"point\":\"2\"},{\"from\":\"19:00\",\"to\":\"00:00\",\"point\":\"1\"}],\"tuesday\":[{\"from\":\"00:00\",\"to\":\"16:00\",\"point\":\"1\"},{\"from\":\"16:00\",\"to\":\"19:00\",\"point\":\"2\"},{\"from\":\"19:00\",\"to\":\"00:00\",\"point\":\"1\"}],\"wednesday\":[{\"from\":\"00:00\",\"to\":\"16:00\",\"point\":\"1\"},{\"from\":\"16:00\",\"to\":\"19:00\",\"point\":\"2\"},{\"from\":\"19:00\",\"to\":\"00:00\",\"point\":\"1\"}],\"thursday\":[{\"from\":\"00:00\",\"to\":\"16:00\",\"point\":\"1\"},{\"from\":\"16:00\",\"to\":\"19:00\",\"point\":\"2\"},{\"from\":\"19:00\",\"to\":\"00:00\",\"point\":\"1\"}],\"friday\":[{\"from\":\"00:00\",\"to\":\"16:00\",\"point\":\"1\"},{\"from\":\"16:00\",\"to\":\"19:00\",\"point\":\"2\"},{\"from\":\"19:00\",\"to\":\"00:00\",\"point\":\"1\"}],\"saturday\":[{\"from\":\"00:00\",\"to\":\"00:00\",\"point\":\"2\"}]}', 12, 'On', 15, 50, 'https://hook.eu1.make.com/g2cnu8lsuuqf72ufipq20lwa0abb8xkw', 'V7H3GharFwzTlTasxumj', '2024-01-12 15:15:04', '2024-05-11 05:02:49', '{\"Project\":\"Peak 360\", \"Calendar Link\":\"https://link.localbestgyms.com/widget/booking/V7H3GharFwzTlTasxumj\"}', 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJsb2NhdGlvbl9pZCI6Im1lanRUM3E1bUh2dER1NmhweVNjIiwiY29tcGFueV9pZCI6IkVFRHdsY2pnQTlYS29CbVQxNVQwIiwidmVyc2lvbiI6MSwiaWF0IjoxNjg0MzU3NzU4MTc3LCJzdWIiOiJ1c2VyX2lkIn0.1R0zZ-6S5m8cq-M4FT3MQGQI0rWC7bHN-wor7HSjPvw', NULL, NULL, NULL), +(107, 'Soar Over Obstacles', NULL, '{\"sunday\":[{\"from\":\"00:00\",\"to\":\"00:00\",\"point\":\"2\"}],\"monday\":[{\"from\":\"00:00\",\"to\":\"16:00\",\"point\":\"1\"},{\"from\":\"16:00\",\"to\":\"19:00\",\"point\":\"2\"},{\"from\":\"19:00\",\"to\":\"00:00\",\"point\":\"1\"}],\"tuesday\":[{\"from\":\"00:00\",\"to\":\"16:00\",\"point\":\"1\"},{\"from\":\"16:00\",\"to\":\"19:00\",\"point\":\"2\"},{\"from\":\"19:00\",\"to\":\"00:00\",\"point\":\"1\"}],\"wednesday\":[{\"from\":\"00:00\",\"to\":\"16:00\",\"point\":\"1\"},{\"from\":\"16:00\",\"to\":\"19:00\",\"point\":\"2\"},{\"from\":\"19:00\",\"to\":\"00:00\",\"point\":\"1\"}],\"thursday\":[{\"from\":\"00:00\",\"to\":\"16:00\",\"point\":\"1\"},{\"from\":\"16:00\",\"to\":\"19:00\",\"point\":\"2\"},{\"from\":\"19:00\",\"to\":\"00:00\",\"point\":\"1\"}],\"friday\":[{\"from\":\"00:00\",\"to\":\"16:00\",\"point\":\"1\"},{\"from\":\"16:00\",\"to\":\"19:00\",\"point\":\"2\"},{\"from\":\"19:00\",\"to\":\"00:00\",\"point\":\"1\"}],\"saturday\":[{\"from\":\"00:00\",\"to\":\"00:00\",\"point\":\"2\"}]}', 12, 'On', 15, 4, 'https://hook.eu1.make.com/g2cnu8lsuuqf72ufipq20lwa0abb8xkw', 'rrT2vsaMI4ILm1ZhHjym', '2024-01-12 15:16:00', '2024-05-14 05:03:25', '{\"Project\":\"Soar Over Obstacles\", \"Calendar Link\":\"https://link.localbestgyms.com/widget/booking/rrT2vsaMI4ILm1ZhHjym\"}', 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJsb2NhdGlvbl9pZCI6IkduWmwzZmdjSkQ5SVpDUkFCNmxwIiwiY29tcGFueV9pZCI6IkVFRHdsY2pnQTlYS29CbVQxNVQwIiwidmVyc2lvbiI6MSwiaWF0IjoxNjg0NDM3ODk3NjczLCJzdWIiOiJ1c2VyX2lkIn0.Kie23Tmg4qje7zZYozi8HGHcCFP7lJWkkI5mHUB6GKQ', NULL, NULL, NULL), +(108, 'RUF Fitness', NULL, '{\"sunday\":[{\"from\":\"00:00\",\"to\":\"00:00\",\"point\":\"2\"}],\"monday\":[{\"from\":\"00:00\",\"to\":\"16:00\",\"point\":\"1\"},{\"from\":\"16:00\",\"to\":\"19:00\",\"point\":\"2\"},{\"from\":\"19:00\",\"to\":\"00:00\",\"point\":\"1\"}],\"tuesday\":[{\"from\":\"00:00\",\"to\":\"16:00\",\"point\":\"1\"},{\"from\":\"16:00\",\"to\":\"19:00\",\"point\":\"2\"},{\"from\":\"19:00\",\"to\":\"00:00\",\"point\":\"1\"}],\"wednesday\":[{\"from\":\"00:00\",\"to\":\"16:00\",\"point\":\"1\"},{\"from\":\"16:00\",\"to\":\"19:00\",\"point\":\"2\"},{\"from\":\"19:00\",\"to\":\"00:00\",\"point\":\"1\"}],\"thursday\":[{\"from\":\"00:00\",\"to\":\"16:00\",\"point\":\"1\"},{\"from\":\"16:00\",\"to\":\"19:00\",\"point\":\"2\"},{\"from\":\"19:00\",\"to\":\"00:00\",\"point\":\"1\"}],\"friday\":[{\"from\":\"00:00\",\"to\":\"16:00\",\"point\":\"1\"},{\"from\":\"16:00\",\"to\":\"19:00\",\"point\":\"2\"},{\"from\":\"19:00\",\"to\":\"00:00\",\"point\":\"1\"}],\"saturday\":[{\"from\":\"00:00\",\"to\":\"00:00\",\"point\":\"2\"}]}', 12, 'On', 15, 0, 'https://hook.eu1.make.com/g2cnu8lsuuqf72ufipq20lwa0abb8xkw', '4KDMjSpHsgl6nqjqsYnK', '2024-01-12 15:16:37', '2024-06-19 05:03:12', '{\"Project\":\"RUF Fitness\", \"Calendar Link\":\"https://link.localbestgyms.com/widget/booking/4KDMjSpHsgl6nqjqsYnK\"}', 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJsb2NhdGlvbl9pZCI6IlE0czJuOG1xUGVuWTFWR1FyeTVhIiwiY29tcGFueV9pZCI6IkVFRHdsY2pnQTlYS29CbVQxNVQwIiwidmVyc2lvbiI6MSwiaWF0IjoxNjUzNjAwNTIwOTQ2LCJzdWIiOiJ1c2VyX2lkIn0.E9liDtPI8ZU6UtKI4dEeMJzAk4NuAXDDQMQFI-FOhHc', NULL, NULL, NULL), +(109, 'Wine Country Crossfit', NULL, '{\"sunday\":[{\"from\":\"00:00\",\"to\":\"00:00\",\"point\":\"2\"}],\"monday\":[{\"from\":\"00:00\",\"to\":\"16:00\",\"point\":\"1\"},{\"from\":\"16:00\",\"to\":\"19:00\",\"point\":\"2\"},{\"from\":\"19:00\",\"to\":\"00:00\",\"point\":\"1\"}],\"tuesday\":[{\"from\":\"00:00\",\"to\":\"16:00\",\"point\":\"1\"},{\"from\":\"16:00\",\"to\":\"19:00\",\"point\":\"2\"},{\"from\":\"19:00\",\"to\":\"00:00\",\"point\":\"1\"}],\"wednesday\":[{\"from\":\"00:00\",\"to\":\"16:00\",\"point\":\"1\"},{\"from\":\"16:00\",\"to\":\"19:00\",\"point\":\"2\"},{\"from\":\"19:00\",\"to\":\"00:00\",\"point\":\"1\"}],\"thursday\":[{\"from\":\"00:00\",\"to\":\"16:00\",\"point\":\"1\"},{\"from\":\"16:00\",\"to\":\"19:00\",\"point\":\"2\"},{\"from\":\"19:00\",\"to\":\"00:00\",\"point\":\"1\"}],\"friday\":[{\"from\":\"00:00\",\"to\":\"16:00\",\"point\":\"1\"},{\"from\":\"16:00\",\"to\":\"19:00\",\"point\":\"2\"},{\"from\":\"19:00\",\"to\":\"00:00\",\"point\":\"1\"}],\"saturday\":[{\"from\":\"00:00\",\"to\":\"00:00\",\"point\":\"2\"}]}', 12, 'On', 15, 19, 'https://hook.eu1.make.com/g2cnu8lsuuqf72ufipq20lwa0abb8xkw', 'jtIj87jTgIPx1Hk6LVM9', '2024-01-12 15:17:10', '2024-10-31 05:14:26', '{\"Project\":\"Wine Country Crossfit\", \"Calendar Link\":\"https://link.localbestgyms.com/widget/booking/jtIj87jTgIPx1Hk6LVM9\"}', 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJsb2NhdGlvbl9pZCI6IlRDcHlvT0lsdnZNdzE3NnBSWDBWIiwiY29tcGFueV9pZCI6IkVFRHdsY2pnQTlYS29CbVQxNVQwIiwidmVyc2lvbiI6MSwiaWF0IjoxNjU4MzM3ODgyMzc2LCJzdWIiOiJ1c2VyX2lkIn0.KY_NJuH4KDt9JEu6b9hdGMfOUE6GjLG_BxZi3Gw9WLQ', NULL, NULL, NULL), +(110, 'Steel Mill Fleming Island', NULL, '{\"sunday\":[{\"from\":\"00:00\",\"to\":\"00:00\",\"point\":\"2\"}],\"monday\":[{\"from\":\"00:00\",\"to\":\"16:00\",\"point\":\"1\"},{\"from\":\"16:00\",\"to\":\"19:00\",\"point\":\"2\"},{\"from\":\"19:00\",\"to\":\"00:00\",\"point\":\"1\"}],\"tuesday\":[{\"from\":\"00:00\",\"to\":\"16:00\",\"point\":\"1\"},{\"from\":\"16:00\",\"to\":\"19:00\",\"point\":\"2\"},{\"from\":\"19:00\",\"to\":\"00:00\",\"point\":\"1\"}],\"wednesday\":[{\"from\":\"00:00\",\"to\":\"16:00\",\"point\":\"1\"},{\"from\":\"16:00\",\"to\":\"19:00\",\"point\":\"2\"},{\"from\":\"19:00\",\"to\":\"00:00\",\"point\":\"1\"}],\"thursday\":[{\"from\":\"00:00\",\"to\":\"16:00\",\"point\":\"1\"},{\"from\":\"16:00\",\"to\":\"19:00\",\"point\":\"2\"},{\"from\":\"19:00\",\"to\":\"00:00\",\"point\":\"1\"}],\"friday\":[{\"from\":\"00:00\",\"to\":\"16:00\",\"point\":\"1\"},{\"from\":\"16:00\",\"to\":\"19:00\",\"point\":\"2\"},{\"from\":\"19:00\",\"to\":\"00:00\",\"point\":\"1\"}],\"saturday\":[{\"from\":\"00:00\",\"to\":\"00:00\",\"point\":\"2\"}]}', 12, 'On', 15, 14, 'https://hook.eu1.make.com/g2cnu8lsuuqf72ufipq20lwa0abb8xkw', 'WaOLlr3W3vOfVX9Ockj5', '2024-01-12 15:19:40', '2024-10-31 05:14:32', '{\"Project\":\"Steel Mill Fleming Island\", \"Calendar Link\":\"https://link.localbestgyms.com/widget/booking/WaOLlr3W3vOfVX9Ockj5\"}', 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJsb2NhdGlvbl9pZCI6IkFkVjBYdFNVNTZGbVMza09HUkI1IiwiY29tcGFueV9pZCI6IkVFRHdsY2pnQTlYS29CbVQxNVQwIiwidmVyc2lvbiI6MSwiaWF0IjoxNjYyNTY3MDc4Njk4LCJzdWIiOiJ1c2VyX2lkIn0.wGlYDRn67XEe-v8blBXCIQuPR_MSVh5sGe4BNaPXqbc', NULL, NULL, NULL), +(111, 'Empower Fit', NULL, '{\"sunday\":[{\"from\":\"00:00\",\"to\":\"00:00\",\"point\":\"2\"}],\"monday\":[{\"from\":\"00:00\",\"to\":\"16:00\",\"point\":\"1\"},{\"from\":\"16:00\",\"to\":\"19:00\",\"point\":\"2\"},{\"from\":\"19:00\",\"to\":\"00:00\",\"point\":\"1\"}],\"tuesday\":[{\"from\":\"00:00\",\"to\":\"16:00\",\"point\":\"1\"},{\"from\":\"16:00\",\"to\":\"19:00\",\"point\":\"2\"},{\"from\":\"19:00\",\"to\":\"00:00\",\"point\":\"1\"}],\"wednesday\":[{\"from\":\"00:00\",\"to\":\"16:00\",\"point\":\"1\"},{\"from\":\"16:00\",\"to\":\"19:00\",\"point\":\"2\"},{\"from\":\"19:00\",\"to\":\"00:00\",\"point\":\"1\"}],\"thursday\":[{\"from\":\"00:00\",\"to\":\"16:00\",\"point\":\"1\"},{\"from\":\"16:00\",\"to\":\"19:00\",\"point\":\"2\"},{\"from\":\"19:00\",\"to\":\"00:00\",\"point\":\"1\"}],\"friday\":[{\"from\":\"00:00\",\"to\":\"16:00\",\"point\":\"1\"},{\"from\":\"16:00\",\"to\":\"19:00\",\"point\":\"2\"},{\"from\":\"19:00\",\"to\":\"00:00\",\"point\":\"1\"}],\"saturday\":[{\"from\":\"00:00\",\"to\":\"00:00\",\"point\":\"2\"}]}', 12, 'On', 15, 27, 'https://hook.eu1.make.com/g2cnu8lsuuqf72ufipq20lwa0abb8xkw', 'xLHuykMQdr5MHKkBrs6j', '2024-01-12 15:20:34', '2024-10-31 05:14:54', '{\"Project\":\"Empower Fit\", \"Calendar Link\":\"https://link.localbestgyms.com/widget/booking/xLHuykMQdr5MHKkBrs6j\"}', 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJsb2NhdGlvbl9pZCI6IlNzNFEwN0tRQllCZFhYNmEzV3dBIiwiY29tcGFueV9pZCI6IkVFRHdsY2pnQTlYS29CbVQxNVQwIiwidmVyc2lvbiI6MSwiaWF0IjoxNjg0OTU5Mjk4MDY5LCJzdWIiOiJ1c2VyX2lkIn0.cDqYs5DRLydsORmrX1z4LzKB7HsLj5v_ZXa5ztnGb68', NULL, NULL, NULL), +(112, 'F45 Training Coeur D\'Alene', NULL, '{\"sunday\":[{\"from\":\"00:00\",\"to\":\"00:00\",\"point\":\"2\"}],\"monday\":[{\"from\":\"00:00\",\"to\":\"16:00\",\"point\":\"1\"},{\"from\":\"16:00\",\"to\":\"19:00\",\"point\":\"2\"},{\"from\":\"19:00\",\"to\":\"00:00\",\"point\":\"1\"}],\"tuesday\":[{\"from\":\"00:00\",\"to\":\"16:00\",\"point\":\"1\"},{\"from\":\"16:00\",\"to\":\"19:00\",\"point\":\"2\"},{\"from\":\"19:00\",\"to\":\"00:00\",\"point\":\"1\"}],\"wednesday\":[{\"from\":\"00:00\",\"to\":\"16:00\",\"point\":\"1\"},{\"from\":\"16:00\",\"to\":\"19:00\",\"point\":\"2\"},{\"from\":\"19:00\",\"to\":\"00:00\",\"point\":\"1\"}],\"thursday\":[{\"from\":\"00:00\",\"to\":\"16:00\",\"point\":\"1\"},{\"from\":\"16:00\",\"to\":\"19:00\",\"point\":\"2\"},{\"from\":\"19:00\",\"to\":\"00:00\",\"point\":\"1\"}],\"friday\":[{\"from\":\"00:00\",\"to\":\"16:00\",\"point\":\"1\"},{\"from\":\"16:00\",\"to\":\"19:00\",\"point\":\"2\"},{\"from\":\"19:00\",\"to\":\"00:00\",\"point\":\"1\"}],\"saturday\":[{\"from\":\"00:00\",\"to\":\"00:00\",\"point\":\"2\"}]}', 12, 'On', 15, 32, 'https://hook.eu1.make.com/g2cnu8lsuuqf72ufipq20lwa0abb8xkw', 'GFwKOQXHlB2uepE2pGbW', '2024-01-12 15:21:17', '2024-10-31 05:15:05', '{\"Project\":\"F45 Training Coeur D\'Alene\", \"Calendar Link\":\"https://link.localbestgyms.com/widget/booking/GFwKOQXHlB2uepE2pGbW\"}', 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJsb2NhdGlvbl9pZCI6IkU5YnN2ZmZSMFhZcGN2cVRSVmFlIiwiY29tcGFueV9pZCI6IkVFRHdsY2pnQTlYS29CbVQxNVQwIiwidmVyc2lvbiI6MSwiaWF0IjoxNjU1ODM5OTQxOTY1LCJzdWIiOiJ1c2VyX2lkIn0.1-suMs0NV7pbNr8ZPMp08kKpeGM3oodJkzXvLK7ysmQ', NULL, NULL, NULL), +(113, 'Train PTM', NULL, '{\"sunday\":[{\"from\":\"00:00\",\"to\":\"00:00\",\"point\":\"2\"}],\"monday\":[{\"from\":\"00:00\",\"to\":\"16:00\",\"point\":\"1\"},{\"from\":\"16:00\",\"to\":\"19:00\",\"point\":\"2\"},{\"from\":\"19:00\",\"to\":\"00:00\",\"point\":\"1\"}],\"tuesday\":[{\"from\":\"00:00\",\"to\":\"16:00\",\"point\":\"1\"},{\"from\":\"16:00\",\"to\":\"19:00\",\"point\":\"2\"},{\"from\":\"19:00\",\"to\":\"00:00\",\"point\":\"1\"}],\"wednesday\":[{\"from\":\"00:00\",\"to\":\"16:00\",\"point\":\"1\"},{\"from\":\"16:00\",\"to\":\"19:00\",\"point\":\"2\"},{\"from\":\"19:00\",\"to\":\"00:00\",\"point\":\"1\"}],\"thursday\":[{\"from\":\"00:00\",\"to\":\"16:00\",\"point\":\"1\"},{\"from\":\"16:00\",\"to\":\"19:00\",\"point\":\"2\"},{\"from\":\"19:00\",\"to\":\"00:00\",\"point\":\"1\"}],\"friday\":[{\"from\":\"00:00\",\"to\":\"16:00\",\"point\":\"1\"},{\"from\":\"16:00\",\"to\":\"19:00\",\"point\":\"2\"},{\"from\":\"19:00\",\"to\":\"00:00\",\"point\":\"1\"}],\"saturday\":[{\"from\":\"00:00\",\"to\":\"00:00\",\"point\":\"2\"}]}', 12, 'On', 15, 18, 'https://hook.eu1.make.com/g2cnu8lsuuqf72ufipq20lwa0abb8xkw', 'rSb1ljJSzwpsUFSFsjB3', '2024-01-12 15:21:46', '2024-10-29 05:12:25', '{\"Project\":\"Train PTM\", \"Calendar Link\":\"https://link.localbestgyms.com/widget/booking/rSb1ljJSzwpsUFSFsjB3\"}', 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJsb2NhdGlvbl9pZCI6Ijc4eHZoTno1U09oRU1GVW5hWXFIIiwiY29tcGFueV9pZCI6IkVFRHdsY2pnQTlYS29CbVQxNVQwIiwidmVyc2lvbiI6MSwiaWF0IjoxNjg0MTcxMTIwMjgyLCJzdWIiOiJ1c2VyX2lkIn0.4k3F32U6eaY5uXPeKFtnxL7vZ0g0IXLCVSwP826N9AM', NULL, NULL, NULL), +(114, 'Midwest Muscle', NULL, '{\"sunday\":[{\"from\":\"00:00\",\"to\":\"00:00\",\"point\":\"2\"}],\"monday\":[{\"from\":\"00:00\",\"to\":\"16:00\",\"point\":\"1\"},{\"from\":\"16:00\",\"to\":\"19:00\",\"point\":\"2\"},{\"from\":\"19:00\",\"to\":\"00:00\",\"point\":\"1\"}],\"tuesday\":[{\"from\":\"00:00\",\"to\":\"16:00\",\"point\":\"1\"},{\"from\":\"16:00\",\"to\":\"19:00\",\"point\":\"2\"},{\"from\":\"19:00\",\"to\":\"00:00\",\"point\":\"1\"}],\"wednesday\":[{\"from\":\"00:00\",\"to\":\"16:00\",\"point\":\"1\"},{\"from\":\"16:00\",\"to\":\"19:00\",\"point\":\"2\"},{\"from\":\"19:00\",\"to\":\"00:00\",\"point\":\"1\"}],\"thursday\":[{\"from\":\"00:00\",\"to\":\"16:00\",\"point\":\"1\"},{\"from\":\"16:00\",\"to\":\"19:00\",\"point\":\"2\"},{\"from\":\"19:00\",\"to\":\"00:00\",\"point\":\"1\"}],\"friday\":[{\"from\":\"00:00\",\"to\":\"16:00\",\"point\":\"1\"},{\"from\":\"16:00\",\"to\":\"19:00\",\"point\":\"2\"},{\"from\":\"19:00\",\"to\":\"00:00\",\"point\":\"1\"}],\"saturday\":[{\"from\":\"00:00\",\"to\":\"00:00\",\"point\":\"2\"}]}', 12, 'On', 15, 111, 'https://hook.eu1.make.com/g2cnu8lsuuqf72ufipq20lwa0abb8xkw', 'Wbk0GmUk81r3eClWCBhz', '2024-01-12 15:22:23', '2024-10-31 05:15:11', '{\"Project\":\"Midwest Muscle\", \"Calendar Link\":\"https://link.localbestgyms.com/widget/booking/Wbk0GmUk81r3eClWCBhz\"}', 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJsb2NhdGlvbl9pZCI6IkQzQUtleXpjQ1pLdFhScXFFSVAzIiwiY29tcGFueV9pZCI6IkVFRHdsY2pnQTlYS29CbVQxNVQwIiwidmVyc2lvbiI6MSwiaWF0IjoxNjg0ODczNTI4MTUyLCJzdWIiOiJ1c2VyX2lkIn0.6GJr0vmf6At4OCBnaF1NpP0zKU0a7GbQ08h3BIFHghY', NULL, NULL, NULL), +(115, 'Fit Results', NULL, '{\"sunday\":[{\"from\":\"00:00\",\"to\":\"00:00\",\"point\":\"2\"}],\"monday\":[{\"from\":\"00:00\",\"to\":\"16:00\",\"point\":\"1\"},{\"from\":\"16:00\",\"to\":\"19:00\",\"point\":\"2\"},{\"from\":\"19:00\",\"to\":\"00:00\",\"point\":\"1\"}],\"tuesday\":[{\"from\":\"00:00\",\"to\":\"16:00\",\"point\":\"1\"},{\"from\":\"16:00\",\"to\":\"19:00\",\"point\":\"2\"},{\"from\":\"19:00\",\"to\":\"00:00\",\"point\":\"1\"}],\"wednesday\":[{\"from\":\"00:00\",\"to\":\"16:00\",\"point\":\"1\"},{\"from\":\"16:00\",\"to\":\"19:00\",\"point\":\"2\"},{\"from\":\"19:00\",\"to\":\"00:00\",\"point\":\"1\"}],\"thursday\":[{\"from\":\"00:00\",\"to\":\"16:00\",\"point\":\"1\"},{\"from\":\"16:00\",\"to\":\"19:00\",\"point\":\"2\"},{\"from\":\"19:00\",\"to\":\"00:00\",\"point\":\"1\"}],\"friday\":[{\"from\":\"00:00\",\"to\":\"16:00\",\"point\":\"1\"},{\"from\":\"16:00\",\"to\":\"19:00\",\"point\":\"2\"},{\"from\":\"19:00\",\"to\":\"00:00\",\"point\":\"1\"}],\"saturday\":[{\"from\":\"00:00\",\"to\":\"00:00\",\"point\":\"2\"}]}', 12, 'On', 15, 0, 'https://hook.eu1.make.com/g2cnu8lsuuqf72ufipq20lwa0abb8xkw', '69qvBR6v28tCkj6TQcJP', '2024-01-12 15:23:23', '2024-02-15 05:04:05', '{\"Project\":\"Fit Results\", \"Calendar Link\":\"https://link.localbestgyms.com/widget/booking/69qvBR6v28tCkj6TQcJP\"}', 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJsb2NhdGlvbl9pZCI6ImtCM1ZKSElVUTBPYWc2aVlXVk5zIiwiY29tcGFueV9pZCI6IkVFRHdsY2pnQTlYS29CbVQxNVQwIiwidmVyc2lvbiI6MSwiaWF0IjoxNjgzNzUwNTE3NDIyLCJzdWIiOiJ1c2VyX2lkIn0.5WRSIeDryKn8v1yVOFUj4m1hVTSW-ngqIo6mDft39YA', NULL, NULL, NULL), +(116, 'Powerhouse Training Covina', NULL, '{\"sunday\":[{\"from\":\"00:00\",\"to\":\"00:00\",\"point\":\"2\"}],\"monday\":[{\"from\":\"00:00\",\"to\":\"16:00\",\"point\":\"1\"},{\"from\":\"16:00\",\"to\":\"19:00\",\"point\":\"2\"},{\"from\":\"19:00\",\"to\":\"00:00\",\"point\":\"1\"}],\"tuesday\":[{\"from\":\"00:00\",\"to\":\"16:00\",\"point\":\"1\"},{\"from\":\"16:00\",\"to\":\"19:00\",\"point\":\"2\"},{\"from\":\"19:00\",\"to\":\"00:00\",\"point\":\"1\"}],\"wednesday\":[{\"from\":\"00:00\",\"to\":\"16:00\",\"point\":\"1\"},{\"from\":\"16:00\",\"to\":\"19:00\",\"point\":\"2\"},{\"from\":\"19:00\",\"to\":\"00:00\",\"point\":\"1\"}],\"thursday\":[{\"from\":\"00:00\",\"to\":\"16:00\",\"point\":\"1\"},{\"from\":\"16:00\",\"to\":\"19:00\",\"point\":\"2\"},{\"from\":\"19:00\",\"to\":\"00:00\",\"point\":\"1\"}],\"friday\":[{\"from\":\"00:00\",\"to\":\"16:00\",\"point\":\"1\"},{\"from\":\"16:00\",\"to\":\"19:00\",\"point\":\"2\"},{\"from\":\"19:00\",\"to\":\"00:00\",\"point\":\"1\"}],\"saturday\":[{\"from\":\"00:00\",\"to\":\"00:00\",\"point\":\"2\"}]}', 12, 'On', 15, 60, 'https://hook.eu1.make.com/g2cnu8lsuuqf72ufipq20lwa0abb8xkw', 'lwWBMoeHARriTFBUOLvV', '2024-01-12 15:24:40', '2024-05-30 05:04:22', '{\"Project\":\"Powerhouse Training Covina\", \"Calendar Link\":\"https://link.localbestgyms.com/widget/booking/lwWBMoeHARriTFBUOLvV\"}', 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJsb2NhdGlvbl9pZCI6IjVyM3FLYVMyUlVBNjlwN3dyUk1iIiwiY29tcGFueV9pZCI6IkVFRHdsY2pnQTlYS29CbVQxNVQwIiwidmVyc2lvbiI6MSwiaWF0IjoxNjg0MjYxMjkyMzM1LCJzdWIiOiJ1c2VyX2lkIn0.QRhvzhFi_1rX0NCdJiSrQq1hywnKi4NKQTzwaBKr45M', NULL, NULL, NULL), +(117, 'CJJF Texas', NULL, '{\"sunday\":[{\"from\":\"00:00\",\"to\":\"00:00\",\"point\":\"2\"}],\"monday\":[{\"from\":\"00:00\",\"to\":\"16:00\",\"point\":\"1\"},{\"from\":\"16:00\",\"to\":\"19:00\",\"point\":\"2\"},{\"from\":\"19:00\",\"to\":\"00:00\",\"point\":\"1\"}],\"tuesday\":[{\"from\":\"00:00\",\"to\":\"16:00\",\"point\":\"1\"},{\"from\":\"16:00\",\"to\":\"19:00\",\"point\":\"2\"},{\"from\":\"19:00\",\"to\":\"00:00\",\"point\":\"1\"}],\"wednesday\":[{\"from\":\"00:00\",\"to\":\"16:00\",\"point\":\"1\"},{\"from\":\"16:00\",\"to\":\"19:00\",\"point\":\"2\"},{\"from\":\"19:00\",\"to\":\"00:00\",\"point\":\"1\"}],\"thursday\":[{\"from\":\"00:00\",\"to\":\"16:00\",\"point\":\"1\"},{\"from\":\"16:00\",\"to\":\"19:00\",\"point\":\"2\"},{\"from\":\"19:00\",\"to\":\"00:00\",\"point\":\"1\"}],\"friday\":[{\"from\":\"00:00\",\"to\":\"16:00\",\"point\":\"1\"},{\"from\":\"16:00\",\"to\":\"19:00\",\"point\":\"2\"},{\"from\":\"19:00\",\"to\":\"00:00\",\"point\":\"1\"}],\"saturday\":[{\"from\":\"00:00\",\"to\":\"00:00\",\"point\":\"2\"}]}', 12, 'On', 15, 26, 'https://hook.eu1.make.com/g2cnu8lsuuqf72ufipq20lwa0abb8xkw', 'ksrKihTv60Howjov9ew4', '2024-01-12 15:25:35', '2024-10-31 05:15:16', '{\"Project\":\"Lights Out Boxing\", \"Calendar Link\":\"https://link.localbestgyms.com/widget/booking/ksrKihTv60Howjov9ew4\"}', 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJsb2NhdGlvbl9pZCI6IjFvd1FvenU5Z1FrR0d4d1l4Z0k4IiwiY29tcGFueV9pZCI6IkVFRHdsY2pnQTlYS29CbVQxNVQwIiwidmVyc2lvbiI6MSwiaWF0IjoxNjg0MTY1MzY3MzgyLCJzdWIiOiJ1c2VyX2lkIn0.1lbrjX8HwdqgVPsgCDsVDzl9Z0gDeVNcaWtOQ2rBX8I', NULL, NULL, NULL), +(118, 'Lights Out Boxing', NULL, '{\"sunday\":[{\"from\":\"00:00\",\"to\":\"00:00\",\"point\":\"2\"}],\"monday\":[{\"from\":\"00:00\",\"to\":\"16:00\",\"point\":\"1\"},{\"from\":\"16:00\",\"to\":\"19:00\",\"point\":\"2\"},{\"from\":\"19:00\",\"to\":\"00:00\",\"point\":\"1\"}],\"tuesday\":[{\"from\":\"00:00\",\"to\":\"16:00\",\"point\":\"1\"},{\"from\":\"16:00\",\"to\":\"19:00\",\"point\":\"2\"},{\"from\":\"19:00\",\"to\":\"00:00\",\"point\":\"1\"}],\"wednesday\":[{\"from\":\"00:00\",\"to\":\"16:00\",\"point\":\"1\"},{\"from\":\"16:00\",\"to\":\"19:00\",\"point\":\"2\"},{\"from\":\"19:00\",\"to\":\"00:00\",\"point\":\"1\"}],\"thursday\":[{\"from\":\"00:00\",\"to\":\"16:00\",\"point\":\"1\"},{\"from\":\"16:00\",\"to\":\"19:00\",\"point\":\"2\"},{\"from\":\"19:00\",\"to\":\"00:00\",\"point\":\"1\"}],\"friday\":[{\"from\":\"00:00\",\"to\":\"16:00\",\"point\":\"1\"},{\"from\":\"16:00\",\"to\":\"19:00\",\"point\":\"2\"},{\"from\":\"19:00\",\"to\":\"00:00\",\"point\":\"1\"}],\"saturday\":[{\"from\":\"00:00\",\"to\":\"00:00\",\"point\":\"2\"}]}', 12, 'On', 15, 121, 'https://hook.eu1.make.com/g2cnu8lsuuqf72ufipq20lwa0abb8xkw', 'rEOz0hbfpdhO2zQFxHqd', '2024-01-12 15:26:24', '2024-10-10 05:12:46', '{\"Project\":\"Lights Out Boxing\", \"Calendar Link\":\"https://link.localbestgyms.com/widget/booking/rEOz0hbfpdhO2zQFxHqd\"}', 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJsb2NhdGlvbl9pZCI6Im4zTVdlOTliQk5pZDhPM1ljZ2NLIiwiY29tcGFueV9pZCI6IkVFRHdsY2pnQTlYS29CbVQxNVQwIiwidmVyc2lvbiI6MSwiaWF0IjoxNjYzMDg5NjgwNTI1LCJzdWIiOiJ1c2VyX2lkIn0.7BIpf_19_eEWpAsOi-xR--cYDxxT7oau8U9Wu2sNXyc', NULL, NULL, NULL), +(119, 'Booty Lab', NULL, '{\"sunday\":[{\"from\":\"00:00\",\"to\":\"00:00\",\"point\":\"2\"}],\"monday\":[{\"from\":\"00:00\",\"to\":\"16:00\",\"point\":\"1\"},{\"from\":\"16:00\",\"to\":\"19:00\",\"point\":\"2\"},{\"from\":\"19:00\",\"to\":\"00:00\",\"point\":\"1\"}],\"tuesday\":[{\"from\":\"00:00\",\"to\":\"16:00\",\"point\":\"1\"},{\"from\":\"16:00\",\"to\":\"19:00\",\"point\":\"2\"},{\"from\":\"19:00\",\"to\":\"00:00\",\"point\":\"1\"}],\"wednesday\":[{\"from\":\"00:00\",\"to\":\"16:00\",\"point\":\"1\"},{\"from\":\"16:00\",\"to\":\"19:00\",\"point\":\"2\"},{\"from\":\"19:00\",\"to\":\"00:00\",\"point\":\"1\"}],\"thursday\":[{\"from\":\"00:00\",\"to\":\"16:00\",\"point\":\"1\"},{\"from\":\"16:00\",\"to\":\"19:00\",\"point\":\"2\"},{\"from\":\"19:00\",\"to\":\"00:00\",\"point\":\"1\"}],\"friday\":[{\"from\":\"00:00\",\"to\":\"16:00\",\"point\":\"1\"},{\"from\":\"16:00\",\"to\":\"19:00\",\"point\":\"2\"},{\"from\":\"19:00\",\"to\":\"00:00\",\"point\":\"1\"}],\"saturday\":[{\"from\":\"00:00\",\"to\":\"00:00\",\"point\":\"2\"}]}', 12, 'On', 15, 0, 'https://hook.eu1.make.com/g2cnu8lsuuqf72ufipq20lwa0abb8xkw', 'C5tXFKwg8fCNiB9NgR8o', '2024-01-12 15:26:46', '2024-10-31 05:15:22', '{\"Project\":\"Booty Lab\", \"Calendar Link\":\"https://link.localbestgyms.com/widget/booking/C5tXFKwg8fCNiB9NgR8o\"}', 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJsb2NhdGlvbl9pZCI6IjduWHZJdExYcXdlTHNOb0pTU1E5IiwiY29tcGFueV9pZCI6IkVFRHdsY2pnQTlYS29CbVQxNVQwIiwidmVyc2lvbiI6MSwiaWF0IjoxNjYwMDY2NDQ1NzE2LCJzdWIiOiJ1c2VyX2lkIn0.f0fPlJc6Bdn9_7DvuGCp8DdluL1Z2HO9b2wOdOOTJ0A', NULL, NULL, NULL), +(120, 'Legacy Gym Columbus', NULL, '{\"sunday\":[{\"from\":\"00:00\",\"to\":\"00:00\",\"point\":\"2\"}],\"monday\":[{\"from\":\"00:00\",\"to\":\"16:00\",\"point\":\"1\"},{\"from\":\"16:00\",\"to\":\"19:00\",\"point\":\"2\"},{\"from\":\"19:00\",\"to\":\"00:00\",\"point\":\"1\"}],\"tuesday\":[{\"from\":\"00:00\",\"to\":\"16:00\",\"point\":\"1\"},{\"from\":\"16:00\",\"to\":\"19:00\",\"point\":\"2\"},{\"from\":\"19:00\",\"to\":\"00:00\",\"point\":\"1\"}],\"wednesday\":[{\"from\":\"00:00\",\"to\":\"16:00\",\"point\":\"1\"},{\"from\":\"16:00\",\"to\":\"19:00\",\"point\":\"2\"},{\"from\":\"19:00\",\"to\":\"00:00\",\"point\":\"1\"}],\"thursday\":[{\"from\":\"00:00\",\"to\":\"16:00\",\"point\":\"1\"},{\"from\":\"16:00\",\"to\":\"19:00\",\"point\":\"2\"},{\"from\":\"19:00\",\"to\":\"00:00\",\"point\":\"1\"}],\"friday\":[{\"from\":\"00:00\",\"to\":\"16:00\",\"point\":\"1\"},{\"from\":\"16:00\",\"to\":\"19:00\",\"point\":\"2\"},{\"from\":\"19:00\",\"to\":\"00:00\",\"point\":\"1\"}],\"saturday\":[{\"from\":\"00:00\",\"to\":\"00:00\",\"point\":\"2\"}]}', 12, 'On', 15, 78, 'https://hook.eu1.make.com/g2cnu8lsuuqf72ufipq20lwa0abb8xkw', 'FeDCSL9ZjKyNrHA4Z4RP', '2024-01-12 15:26:47', '2024-10-31 05:16:03', '{\"Project\":\"Legacy Gym Columbus\", \"Calendar Link\":\"https://link.localbestgyms.com/widget/booking/FeDCSL9ZjKyNrHA4Z4RP\"}', 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJsb2NhdGlvbl9pZCI6IlZMenQwYUxkVlNPVkpmTVFlQ09QIiwiY29tcGFueV9pZCI6IkVFRHdsY2pnQTlYS29CbVQxNVQwIiwidmVyc2lvbiI6MSwiaWF0IjoxNjgxNDg3OTQ1MjE3LCJzdWIiOiJ1c2VyX2lkIn0.o8w_heWPJfbvgx3IKylHDXrsbD02Dm6Hvbsb9t4lQOg', NULL, NULL, NULL), +(121, 'WEFiT', NULL, '{\"sunday\":[{\"from\":\"00:00\",\"to\":\"00:00\",\"point\":\"2\"}],\"monday\":[{\"from\":\"00:00\",\"to\":\"16:00\",\"point\":\"1\"},{\"from\":\"16:00\",\"to\":\"19:00\",\"point\":\"2\"},{\"from\":\"19:00\",\"to\":\"00:00\",\"point\":\"1\"}],\"tuesday\":[{\"from\":\"00:00\",\"to\":\"16:00\",\"point\":\"1\"},{\"from\":\"16:00\",\"to\":\"19:00\",\"point\":\"2\"},{\"from\":\"19:00\",\"to\":\"00:00\",\"point\":\"1\"}],\"wednesday\":[{\"from\":\"00:00\",\"to\":\"16:00\",\"point\":\"1\"},{\"from\":\"16:00\",\"to\":\"19:00\",\"point\":\"2\"},{\"from\":\"19:00\",\"to\":\"00:00\",\"point\":\"1\"}],\"thursday\":[{\"from\":\"00:00\",\"to\":\"16:00\",\"point\":\"1\"},{\"from\":\"16:00\",\"to\":\"19:00\",\"point\":\"2\"},{\"from\":\"19:00\",\"to\":\"00:00\",\"point\":\"1\"}],\"friday\":[{\"from\":\"00:00\",\"to\":\"16:00\",\"point\":\"1\"},{\"from\":\"16:00\",\"to\":\"19:00\",\"point\":\"2\"},{\"from\":\"19:00\",\"to\":\"00:00\",\"point\":\"1\"}],\"saturday\":[{\"from\":\"00:00\",\"to\":\"00:00\",\"point\":\"2\"}]}', 12, 'On', 15, 7, 'https://hook.eu1.make.com/g2cnu8lsuuqf72ufipq20lwa0abb8xkw', 'hOCcfOIxELgCOplzNQKK', '2024-01-12 15:26:48', '2024-02-13 05:05:59', '{\"Project\":\"WEFiT\", \"Calendar Link\":\"https://link.localbestgyms.com/widget/booking/hOCcfOIxELgCOplzNQKK\"}', 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJsb2NhdGlvbl9pZCI6ImVmYkFCN3dzWGFyNTVqZ2U5QTBPIiwiY29tcGFueV9pZCI6IkVFRHdsY2pnQTlYS29CbVQxNVQwIiwidmVyc2lvbiI6MSwiaWF0IjoxNjYzODc4NTUzMDYzLCJzdWIiOiJ1c2VyX2lkIn0.YqLAMFSCga9B-feY15X5JgKkeS9JTa9ht-F7P76jaFk', NULL, NULL, NULL), +(122, 'Clean Cut Elite', NULL, '{\"sunday\":[{\"from\":\"00:00\",\"to\":\"00:00\",\"point\":\"2\"}],\"monday\":[{\"from\":\"00:00\",\"to\":\"16:00\",\"point\":\"1\"},{\"from\":\"16:00\",\"to\":\"19:00\",\"point\":\"2\"},{\"from\":\"19:00\",\"to\":\"00:00\",\"point\":\"1\"}],\"tuesday\":[{\"from\":\"00:00\",\"to\":\"16:00\",\"point\":\"1\"},{\"from\":\"16:00\",\"to\":\"19:00\",\"point\":\"2\"},{\"from\":\"19:00\",\"to\":\"00:00\",\"point\":\"1\"}],\"wednesday\":[{\"from\":\"00:00\",\"to\":\"16:00\",\"point\":\"1\"},{\"from\":\"16:00\",\"to\":\"19:00\",\"point\":\"2\"},{\"from\":\"19:00\",\"to\":\"00:00\",\"point\":\"1\"}],\"thursday\":[{\"from\":\"00:00\",\"to\":\"16:00\",\"point\":\"1\"},{\"from\":\"16:00\",\"to\":\"19:00\",\"point\":\"2\"},{\"from\":\"19:00\",\"to\":\"00:00\",\"point\":\"1\"}],\"friday\":[{\"from\":\"00:00\",\"to\":\"16:00\",\"point\":\"1\"},{\"from\":\"16:00\",\"to\":\"19:00\",\"point\":\"2\"},{\"from\":\"19:00\",\"to\":\"00:00\",\"point\":\"1\"}],\"saturday\":[{\"from\":\"00:00\",\"to\":\"00:00\",\"point\":\"2\"}]}', 12, 'On', 15, 16, 'https://hook.eu1.make.com/g2cnu8lsuuqf72ufipq20lwa0abb8xkw', 'j8QMNffOtlrpEsi8iUSk', '2024-01-12 15:26:48', '2024-10-31 05:16:09', '{\"Project\":\"Clean Cut Elite\", \"Calendar Link\":\"https://link.localbestgyms.com/widget/booking/j8QMNffOtlrpEsi8iUSk\"}', 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJsb2NhdGlvbl9pZCI6IndsblFzRUFaY29UN1hLVTBsZkdVIiwiY29tcGFueV9pZCI6IkVFRHdsY2pnQTlYS29CbVQxNVQwIiwidmVyc2lvbiI6MSwiaWF0IjoxNjU5NzE2NDM2NDQxLCJzdWIiOiJ1c2VyX2lkIn0.x_y5xzi4HuUwV_s4eZzCAx2vLjaVYTbcF8YvsNN1NVM', NULL, NULL, NULL), +(123, 'Catalyst Fitness', NULL, '{\"sunday\":[{\"from\":\"00:00\",\"to\":\"00:00\",\"point\":\"2\"}],\"monday\":[{\"from\":\"00:00\",\"to\":\"16:00\",\"point\":\"1\"},{\"from\":\"16:00\",\"to\":\"19:00\",\"point\":\"2\"},{\"from\":\"19:00\",\"to\":\"00:00\",\"point\":\"1\"}],\"tuesday\":[{\"from\":\"00:00\",\"to\":\"16:00\",\"point\":\"1\"},{\"from\":\"16:00\",\"to\":\"19:00\",\"point\":\"2\"},{\"from\":\"19:00\",\"to\":\"00:00\",\"point\":\"1\"}],\"wednesday\":[{\"from\":\"00:00\",\"to\":\"16:00\",\"point\":\"1\"},{\"from\":\"16:00\",\"to\":\"19:00\",\"point\":\"2\"},{\"from\":\"19:00\",\"to\":\"00:00\",\"point\":\"1\"}],\"thursday\":[{\"from\":\"00:00\",\"to\":\"16:00\",\"point\":\"1\"},{\"from\":\"16:00\",\"to\":\"19:00\",\"point\":\"2\"},{\"from\":\"19:00\",\"to\":\"00:00\",\"point\":\"1\"}],\"friday\":[{\"from\":\"00:00\",\"to\":\"16:00\",\"point\":\"1\"},{\"from\":\"16:00\",\"to\":\"19:00\",\"point\":\"2\"},{\"from\":\"19:00\",\"to\":\"00:00\",\"point\":\"1\"}],\"saturday\":[{\"from\":\"00:00\",\"to\":\"00:00\",\"point\":\"2\"}]}', 12, 'On', 15, 43, 'https://hook.eu1.make.com/g2cnu8lsuuqf72ufipq20lwa0abb8xkw', 'QrP2X2pWwFGqSCyxHoqP', '2024-01-12 15:26:49', '2024-05-25 05:04:23', '{\"Project\":\"Catalyst Fitness\", \"Calendar Link\":\"https://link.localbestgyms.com/widget/booking/QrP2X2pWwFGqSCyxHoqP\"}', 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJsb2NhdGlvbl9pZCI6IlNxZmZzUUxGYjRNR3VqTjJodnQ1IiwiY29tcGFueV9pZCI6IkVFRHdsY2pnQTlYS29CbVQxNVQwIiwidmVyc2lvbiI6MSwiaWF0IjoxNjgzODM3OTU3NTM4LCJzdWIiOiJ1c2VyX2lkIn0.-UhhtuJwW3l-JsrgKULkzCepgBxuSAlPr0vY2Vcm0j0', NULL, NULL, NULL), +(124, 'LM Fitness Center', NULL, '{\"sunday\":[{\"from\":\"00:00\",\"to\":\"00:00\",\"point\":\"2\"}],\"monday\":[{\"from\":\"00:00\",\"to\":\"16:00\",\"point\":\"1\"},{\"from\":\"16:00\",\"to\":\"19:00\",\"point\":\"2\"},{\"from\":\"19:00\",\"to\":\"00:00\",\"point\":\"1\"}],\"tuesday\":[{\"from\":\"00:00\",\"to\":\"16:00\",\"point\":\"1\"},{\"from\":\"16:00\",\"to\":\"19:00\",\"point\":\"2\"},{\"from\":\"19:00\",\"to\":\"00:00\",\"point\":\"1\"}],\"wednesday\":[{\"from\":\"00:00\",\"to\":\"16:00\",\"point\":\"1\"},{\"from\":\"16:00\",\"to\":\"19:00\",\"point\":\"2\"},{\"from\":\"19:00\",\"to\":\"00:00\",\"point\":\"1\"}],\"thursday\":[{\"from\":\"00:00\",\"to\":\"16:00\",\"point\":\"1\"},{\"from\":\"16:00\",\"to\":\"19:00\",\"point\":\"2\"},{\"from\":\"19:00\",\"to\":\"00:00\",\"point\":\"1\"}],\"friday\":[{\"from\":\"00:00\",\"to\":\"16:00\",\"point\":\"1\"},{\"from\":\"16:00\",\"to\":\"19:00\",\"point\":\"2\"},{\"from\":\"19:00\",\"to\":\"00:00\",\"point\":\"1\"}],\"saturday\":[{\"from\":\"00:00\",\"to\":\"00:00\",\"point\":\"2\"}]}', 12, 'On', 15, 9, 'https://hook.eu1.make.com/g2cnu8lsuuqf72ufipq20lwa0abb8xkw', 'HYmJMZlbp380XtXjy5AC', '2024-01-12 15:26:50', '2024-10-31 05:16:14', '{\"Project\":\"LM Fitness Center\", \"Calendar Link\":\"https://link.localbestgyms.com/widget/booking/HYmJMZlbp380XtXjy5AC\"}', 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJsb2NhdGlvbl9pZCI6IlBVaVJwb2EzY1F0eTRsQ1hOM3NGIiwiY29tcGFueV9pZCI6IkVFRHdsY2pnQTlYS29CbVQxNVQwIiwidmVyc2lvbiI6MSwiaWF0IjoxNjUyNDY3Mjc5NzU4LCJzdWIiOiJ1c2VyX2lkIn0.cOU2jTZMOe_-sTtLcq4bQaLi73DBPjgKP8ENhNKgLDg', NULL, NULL, NULL), +(125, 'Buena Park 1 Fit', NULL, '{\"sunday\":[{\"from\":\"00:00\",\"to\":\"00:00\",\"point\":\"2\"}],\"monday\":[{\"from\":\"00:00\",\"to\":\"16:00\",\"point\":\"1\"},{\"from\":\"16:00\",\"to\":\"19:00\",\"point\":\"2\"},{\"from\":\"19:00\",\"to\":\"00:00\",\"point\":\"1\"}],\"tuesday\":[{\"from\":\"00:00\",\"to\":\"16:00\",\"point\":\"1\"},{\"from\":\"16:00\",\"to\":\"19:00\",\"point\":\"2\"},{\"from\":\"19:00\",\"to\":\"00:00\",\"point\":\"1\"}],\"wednesday\":[{\"from\":\"00:00\",\"to\":\"16:00\",\"point\":\"1\"},{\"from\":\"16:00\",\"to\":\"19:00\",\"point\":\"2\"},{\"from\":\"19:00\",\"to\":\"00:00\",\"point\":\"1\"}],\"thursday\":[{\"from\":\"00:00\",\"to\":\"16:00\",\"point\":\"1\"},{\"from\":\"16:00\",\"to\":\"19:00\",\"point\":\"2\"},{\"from\":\"19:00\",\"to\":\"00:00\",\"point\":\"1\"}],\"friday\":[{\"from\":\"00:00\",\"to\":\"16:00\",\"point\":\"1\"},{\"from\":\"16:00\",\"to\":\"19:00\",\"point\":\"2\"},{\"from\":\"19:00\",\"to\":\"00:00\",\"point\":\"1\"}],\"saturday\":[{\"from\":\"00:00\",\"to\":\"00:00\",\"point\":\"2\"}]}', 12, 'On', 15, 9, 'https://hook.eu1.make.com/g2cnu8lsuuqf72ufipq20lwa0abb8xkw', 'yCpyFRGKyp6GJCqvC7aG', '2024-01-12 15:26:51', '2024-10-31 05:16:33', '{\"Project\":\"Buena Park 1 Fit\", \"Calendar Link\":\"https://link.localbestgyms.com/widget/booking/yCpyFRGKyp6GJCqvC7aG\"}', 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJsb2NhdGlvbl9pZCI6IkpUWWhxYzI3TUZYdEtGUVduakwyIiwiY29tcGFueV9pZCI6IkVFRHdsY2pnQTlYS29CbVQxNVQwIiwidmVyc2lvbiI6MSwiaWF0IjoxNjQwMjE0ODU5NDYyLCJzdWIiOiJ1c2VyX2lkIn0.YEq-jJ5CSlxgiiVov60wIrUpXtcvSfROPEvlR4Wx2rs', NULL, NULL, NULL), +(131, 'Performance Arc', NULL, '{\"sunday\":[{\"from\":\"00:00\",\"to\":\"00:00\",\"point\":\"2\"}],\"monday\":[{\"from\":\"00:00\",\"to\":\"16:00\",\"point\":\"1\"},{\"from\":\"16:00\",\"to\":\"19:00\",\"point\":\"2\"},{\"from\":\"19:00\",\"to\":\"00:00\",\"point\":\"1\"}],\"tuesday\":[{\"from\":\"00:00\",\"to\":\"16:00\",\"point\":\"1\"},{\"from\":\"16:00\",\"to\":\"19:00\",\"point\":\"2\"},{\"from\":\"19:00\",\"to\":\"00:00\",\"point\":\"1\"}],\"wednesday\":[{\"from\":\"00:00\",\"to\":\"16:00\",\"point\":\"1\"},{\"from\":\"16:00\",\"to\":\"19:00\",\"point\":\"2\"},{\"from\":\"19:00\",\"to\":\"00:00\",\"point\":\"1\"}],\"thursday\":[{\"from\":\"00:00\",\"to\":\"16:00\",\"point\":\"1\"},{\"from\":\"16:00\",\"to\":\"19:00\",\"point\":\"2\"},{\"from\":\"19:00\",\"to\":\"00:00\",\"point\":\"1\"}],\"friday\":[{\"from\":\"00:00\",\"to\":\"16:00\",\"point\":\"1\"},{\"from\":\"16:00\",\"to\":\"19:00\",\"point\":\"2\"},{\"from\":\"19:00\",\"to\":\"00:00\",\"point\":\"1\"}],\"saturday\":[{\"from\":\"00:00\",\"to\":\"00:00\",\"point\":\"2\"}]}', 12, 'On', 15, 44, 'https://hook.eu1.make.com/g2cnu8lsuuqf72ufipq20lwa0abb8xkw', 'Bny3AIXD73nPLEQHyLl9', '2024-01-12 15:41:58', '2024-10-31 05:16:49', '{\"Project\":\"Performance Arc\", \"Calendar Link\":\"https://link.localbestgyms.com/widget/booking/Bny3AIXD73nPLEQHyLl9\"}', 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJsb2NhdGlvbl9pZCI6IjN0cjhaNXdSV3RIWlUySTZ4cDZPIiwiY29tcGFueV9pZCI6IkVFRHdsY2pnQTlYS29CbVQxNVQwIiwidmVyc2lvbiI6MSwiaWF0IjoxNjgzOTIzMDg1NDg0LCJzdWIiOiJ1c2VyX2lkIn0.iwBC7Lu13UQRQ1uaA2uiua0BWgzNqqD7dPsojdOClcQ', NULL, NULL, NULL), +(132, 'F45 Training Brookfield', NULL, '{\"sunday\":[{\"from\":\"00:00\",\"to\":\"00:00\",\"point\":\"2\"}],\"monday\":[{\"from\":\"00:00\",\"to\":\"16:00\",\"point\":\"1\"},{\"from\":\"16:00\",\"to\":\"19:00\",\"point\":\"2\"},{\"from\":\"19:00\",\"to\":\"00:00\",\"point\":\"1\"}],\"tuesday\":[{\"from\":\"00:00\",\"to\":\"16:00\",\"point\":\"1\"},{\"from\":\"16:00\",\"to\":\"19:00\",\"point\":\"2\"},{\"from\":\"19:00\",\"to\":\"00:00\",\"point\":\"1\"}],\"wednesday\":[{\"from\":\"00:00\",\"to\":\"16:00\",\"point\":\"1\"},{\"from\":\"16:00\",\"to\":\"19:00\",\"point\":\"2\"},{\"from\":\"19:00\",\"to\":\"00:00\",\"point\":\"1\"}],\"thursday\":[{\"from\":\"00:00\",\"to\":\"16:00\",\"point\":\"1\"},{\"from\":\"16:00\",\"to\":\"19:00\",\"point\":\"2\"},{\"from\":\"19:00\",\"to\":\"00:00\",\"point\":\"1\"}],\"friday\":[{\"from\":\"00:00\",\"to\":\"16:00\",\"point\":\"1\"},{\"from\":\"16:00\",\"to\":\"19:00\",\"point\":\"2\"},{\"from\":\"19:00\",\"to\":\"00:00\",\"point\":\"1\"}],\"saturday\":[{\"from\":\"00:00\",\"to\":\"00:00\",\"point\":\"2\"}]}', 12, 'On', 15, 40, 'https://hook.eu1.make.com/g2cnu8lsuuqf72ufipq20lwa0abb8xkw', 'fdH5SgeBkv9JYGEuksrD', '2024-01-12 15:43:09', '2024-03-06 05:05:45', '{\"Project\":\"F45 Training Brookfield\", \"Calendar Link\":\"https://link.localbestgyms.com/widget/booking/fdH5SgeBkv9JYGEuksrD\"}', 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJsb2NhdGlvbl9pZCI6IlJyWEtQVWJHZzFvNVA1dVRwNENjIiwiY29tcGFueV9pZCI6IkVFRHdsY2pnQTlYS29CbVQxNVQwIiwidmVyc2lvbiI6MSwiaWF0IjoxNjYzMDg1ODQyODAwLCJzdWIiOiJ1c2VyX2lkIn0.be_Qn0e9ovGfiXLmIIYquhqQFvqqtzwdpQ5jncUEe1w', NULL, NULL, NULL); + + + + +INSERT INTO `report` (`id`, `project`, `location_id`, `type`, `report`, `new_lead`, `outbound_dial`, `pickup`, `conversation`, `booked_appointment`, `callback_request`, `created_at`, `status`, `updated_at`, `date`, `webhook_sent`) VALUES +(1, 'CC - Noah Provencal', 607, 'call', 'Company,Agent,Total Calls,Average Duration,Unique Leads\n', NULL, NULL, NULL, NULL, NULL, NULL, NULL, 1, NULL, '2024-10-31', 0), +(2, 'CC - Tom Roseingrave', 520, 'call', 'Company,Agent,Total Calls,Average Duration,Unique Leads\n', NULL, NULL, NULL, NULL, NULL, NULL, NULL, 1, NULL, '2024-10-31', 0), +(3, 'Noah Provencal - Epique Realty', 515, 'call', 'Company,Agent,Total Calls,Average Duration,Unique Leads\n', NULL, NULL, NULL, NULL, NULL, NULL, NULL, 1, NULL, '2024-10-31', 0), +(4, 'CC - Chyann Wray', 915, 'call', 'Company,Agent,Total Calls,Average Duration,Unique Leads\n', NULL, NULL, NULL, NULL, NULL, NULL, NULL, 1, NULL, '2024-10-31', 0), +(5, 'Synergy Day Spa', 931, 'call', 'Company,Agent,Total Calls,Average Duration,Unique Leads\n', NULL, NULL, NULL, NULL, NULL, NULL, NULL, 1, NULL, '2024-10-31', 0), +(6, 'Sqin Toronto', 821, 'call', 'Company,Agent,Total Calls,Average Duration,Unique Leads\n[object Object],Fritz Neri,83,6s,17\n', NULL, NULL, NULL, NULL, NULL, NULL, NULL, 1, NULL, '2024-10-31', 0), +(7, 'Botanica Beauty Studio', 1102, 'call', 'Company,Agent,Total Calls,Average Duration,Unique Leads\n[object Object],Fritz Neri,128,45s,36\n', NULL, NULL, NULL, NULL, NULL, NULL, NULL, 1, NULL, '2024-10-31', 0), +(8, 'Reign Medspa', 819, 'call', 'Company,Agent,Total Calls,Average Duration,Unique Leads\n[object Object],Fritz Neri,87,18s,20\n', NULL, NULL, NULL, NULL, NULL, NULL, NULL, 1, NULL, '2024-10-31', 0), +(9, 'Collagen Bar', 822, 'call', 'Company,Agent,Total Calls,Average Duration,Unique Leads\n[object Object],Fritz Neri,77,26s,20\n', NULL, NULL, NULL, NULL, NULL, NULL, NULL, 1, NULL, '2024-10-31', 0), +(10, 'Active Sports Premium Club', 878, 'call', 'Company,Agent,Total Calls,Average Duration,Unique Leads\nActive Sports Premium Club,Bas Nagel,79,4m 7s,32\nActive Sports Premium Club,Bart van Daatselaar,12,6m 33s,10\n', NULL, NULL, NULL, NULL, NULL, NULL, NULL, 1, NULL, '2024-10-31', 0), +(11, 'Feel So Good', 881, 'call', 'Company,Agent,Total Calls,Average Duration,Unique Leads\nFeel So Good,Bas Nagel,29,20s,10\nFeel So Good,Bart van Daatselaar,2,24m 49s,1\n', NULL, NULL, NULL, NULL, NULL, NULL, NULL, 1, NULL, '2024-10-31', 0), +(12, 'Circles Waalre', 880, 'call', 'Company,Agent,Total Calls,Average Duration,Unique Leads\nCircles Waalre,Bas Nagel,12,2m 25s,5\nCircles Waalre,Bart van Daatselaar,5,18s,4\n', NULL, NULL, NULL, NULL, NULL, NULL, NULL, 1, NULL, '2024-10-31', 0), +(13, 'CCM Snapshot Dutch (MASTER)', 877, 'call', 'Company,Agent,Total Calls,Average Duration,Unique Leads\n', NULL, NULL, NULL, NULL, NULL, NULL, NULL, 1, NULL, '2024-10-31', 0), +(14, 'Bodywork Sportstudio', 879, 'call', 'Company,Agent,Total Calls,Average Duration,Unique Leads\nBodywork Sportstudio,Bas Nagel,40,6m 49s,14\nBodywork Sportstudio,Bart van Daatselaar,2,4s,2\n', NULL, NULL, NULL, NULL, NULL, NULL, NULL, 1, NULL, '2024-10-31', 0), +(15, 'Victory insulation', 307, 'call', 'Company,Agent,Total Calls,Average Duration,Unique Leads\n', NULL, NULL, NULL, NULL, NULL, NULL, NULL, 1, NULL, '2024-10-31', 0), +(16, 'Apex Commercial Roofing', 674, 'call', 'Company,Agent,Total Calls,Average Duration,Unique Leads\nApex Commercial Roofing,Jhet Baclaan,2,5s,2\n', NULL, NULL, NULL, NULL, NULL, NULL, NULL, 1, NULL, '2024-10-31', 0), +(17, 'Choice Roofing Solutions LLC', 379, 'call', 'Company,Agent,Total Calls,Average Duration,Unique Leads\n', NULL, NULL, NULL, NULL, NULL, NULL, NULL, 1, NULL, '2024-10-31', 0), +(18, 'High Efficiency Roofing Solutions LLC', 574, 'call', 'Company,Agent,Total Calls,Average Duration,Unique Leads\n', NULL, NULL, NULL, NULL, NULL, NULL, NULL, 1, NULL, '2024-10-31', 0), +(19, 'Apex Jiu-Jitsu', 1098, 'call', 'Company,Agent,Total Calls,Average Duration,Unique Leads\nApex Jiu-Jitsu,Sergius Padilla,13,37s,10\nApex Jiu-Jitsu,Mohsin Khan,13,37s,10\n', NULL, NULL, NULL, NULL, NULL, NULL, NULL, 1, NULL, '2024-11-01', 0), +(20, 'Loyalty Jiu Jitsu Cranbourne', 975, 'call', 'Company,Agent,Total Calls,Average Duration,Unique Leads\n', NULL, NULL, NULL, NULL, NULL, NULL, NULL, 1, NULL, '2024-11-01', 0), +(21, 'Life BJJ Gold Coast', 950, 'call', 'Company,Agent,Total Calls,Average Duration,Unique Leads\n', NULL, NULL, NULL, NULL, NULL, NULL, NULL, 1, NULL, '2024-11-01', 0), +(22, 'FITNAS Studio', 952, 'call', 'Company,Agent,Total Calls,Average Duration,Unique Leads\nFITNAS Studio,Mohsin Khan,15,13s,15\n', NULL, NULL, NULL, NULL, NULL, NULL, NULL, 1, NULL, '2024-11-01', 0), +(23, 'Krownd', 1047, 'call', 'Company,Agent,Total Calls,Average Duration,Unique Leads\nKrownd,Ben MacMahon,18,27s,15\n', NULL, NULL, NULL, NULL, NULL, NULL, NULL, 1, NULL, '2024-11-01', 0), +(24, 'Le Blanc Med Spa', 901, 'call', 'Company,Agent,Total Calls,Average Duration,Unique Leads\nLe Blanc Med Spa,Joyce Liscano,7,30s,4\n', NULL, NULL, NULL, NULL, NULL, NULL, NULL, 1, NULL, '2024-11-01', 0), +(25, 'Sadhna Art of Wellness', 1028, 'call', 'Company,Agent,Total Calls,Average Duration,Unique Leads\n', NULL, NULL, NULL, NULL, NULL, NULL, NULL, 1, NULL, '2024-11-01', 0), +(26, 'Quakertown Academy of MMA & Fitness', 597, 'call', 'Company,Agent,Total Calls,Average Duration,Unique Leads\nQuakertown Academy of MMA & Fitness,Ranelyn Maneja,14,8s,9\nQuakertown Academy of MMA & Fitness,Janine Dalere,12,10s,8\n', NULL, NULL, NULL, NULL, NULL, NULL, NULL, 1, NULL, '2024-11-01', 0), +(27, 'Strength Lab', 598, 'call', 'Company,Agent,Total Calls,Average Duration,Unique Leads\nStrength Lab,Ranelyn Maneja,27,6s,14\nStrength Lab,Ichiro Malibong,5,0s,4\n', NULL, NULL, NULL, NULL, NULL, NULL, NULL, 1, NULL, '2024-11-01', 0), +(28, 'Carpenter\'s Garcia LLC', 1070, 'call', 'Company,Agent,Total Calls,Average Duration,Unique Leads\n', NULL, NULL, NULL, NULL, NULL, NULL, NULL, 1, NULL, '2024-11-01', 0), +(29, 'Five Stars Contractors', 1081, 'call', 'Company,Agent,Total Calls,Average Duration,Unique Leads\nFive Stars Contractors,Ruche Digital,2,24s,1\n', NULL, NULL, NULL, NULL, NULL, NULL, NULL, 1, NULL, '2024-11-01', 0), +(30, 'Carlino Aesthetics', 997, 'call', 'Company,Agent,Total Calls,Average Duration,Unique Leads\nCarlino Aesthetics,David Stark,15,4m 11s,10\n', NULL, NULL, NULL, NULL, NULL, NULL, NULL, 1, NULL, '2024-11-01', 0), +(31, 'Tropicalaser', 994, 'call', 'Company,Agent,Total Calls,Average Duration,Unique Leads\n', NULL, NULL, NULL, NULL, NULL, NULL, NULL, 1, NULL, '2024-11-01', 0), +(32, 'Skin Deep Beauty', 993, 'call', 'Company,Agent,Total Calls,Average Duration,Unique Leads\n', NULL, NULL, NULL, NULL, NULL, NULL, NULL, 1, NULL, '2024-11-01', 0), +(33, 'CellTechMD', 956, 'call', 'Company,Agent,Total Calls,Average Duration,Unique Leads\nCellTechMD,David Stark,68,51s,26\n', NULL, NULL, NULL, NULL, NULL, NULL, NULL, 1, NULL, '2024-11-01', 0), +(34, 'Bask on Main (test)', 656, 'call', 'Company,Agent,Total Calls,Average Duration,Unique Leads\n', NULL, NULL, NULL, NULL, NULL, NULL, NULL, 1, NULL, '2024-11-01', 0), +(35, 'LifeGaines', 996, 'call', 'Company,Agent,Total Calls,Average Duration,Unique Leads\n', NULL, NULL, NULL, NULL, NULL, NULL, NULL, 1, NULL, '2024-11-01', 0), +(36, 'Anti Aging Med Spa', 1100, 'call', 'Company,Agent,Total Calls,Average Duration,Unique Leads\nAnti Aging Med Spa,David Stark,17,2m 28s,12\n', NULL, NULL, NULL, NULL, NULL, NULL, NULL, 1, NULL, '2024-11-01', 0), +(37, 'Hi Doc Medical & Wellness', 995, 'call', 'Company,Agent,Total Calls,Average Duration,Unique Leads\nHi Doc Medical & Wellness,David Stark,15,14s,9\n', NULL, NULL, NULL, NULL, NULL, NULL, NULL, 1, NULL, '2024-11-01', 0), +(38, 'NW Mortgage Hub, Inc.', 336, 'call', 'Company,Agent,Total Calls,Average Duration,Unique Leads\n', NULL, NULL, NULL, NULL, NULL, NULL, NULL, 1, NULL, '2024-11-01', 0), +(39, '🥖 MVMT CrossFit', 1068, 'call', 'Company,Agent,Total Calls,Average Duration,Unique Leads\n🥖 MVMT CrossFit,Nella Mezzasoma,11,3s,9\n', NULL, NULL, NULL, NULL, NULL, NULL, NULL, 1, NULL, '2024-11-01', 0), +(40, 'GGA Snapshot ENG', 1054, 'call', 'Company,Agent,Total Calls,Average Duration,Unique Leads\n', NULL, NULL, NULL, NULL, NULL, NULL, NULL, 1, NULL, '2024-11-01', 0), +(41, '🥖 CrossFit Hope', 1065, 'call', 'Company,Agent,Total Calls,Average Duration,Unique Leads\n', NULL, NULL, NULL, NULL, NULL, NULL, NULL, 1, NULL, '2024-11-01', 0), +(42, 'Iron Bird Fit', 1062, 'call', 'Company,Agent,Total Calls,Average Duration,Unique Leads\n', NULL, NULL, NULL, NULL, NULL, NULL, NULL, 1, NULL, '2024-11-12', 0); + + + +INSERT INTO `campaign` (`id`, `name`, `file_id`, `data`, `user_id`, `created_at`, `updated_at`) VALUES +(2, 'Sample sheet', '1bLkw9XNBx2XTymsGTZemqzl88KNKsKoooWGVBPtCAi0', NULL, 10, '2024-12-20', '2024-12-20 20:20:49'), +(3, 'Testting', '1bLkw9XNBx2XTymsGTZemqzl88KNKsKoooWGVBPtCAi0', NULL, 10, '2024-12-20', '2024-12-20 21:45:44'), +(4, 'New2', '1bLkw9XNBx2XTymsGTZemqzl88KNKsKoooWGVBPtCAi0', NULL, 10, '2024-12-20', '2024-12-20 22:04:57'), +(5, 'New Emmy', '1bLkw9XNBx2XTymsGTZemqzl88KNKsKoooWGVBPtCAi0', NULL, 2, '2024-12-20', '2024-12-20 22:05:59'), +(6, 'Newwww', '1bLkw9XNBx2XTymsGTZemqzl88KNKsKoooWGVBPtCAi0', NULL, 2, '2024-12-20', '2024-12-31 18:16:28'), +(7, 'Tst ag', '1bLkw9XNBx2XTymsGTZemqzl88KNKsKoooWGVBPtCAi0', NULL, 2, '2025-01-29', '2025-01-29 12:42:12'); + + +INSERT INTO `user` (`id`, `email`, `password`, `status`, `role`, `company`, `drive_access_token`, `drive_refresh_token`, `created_at`, `updated_at`) VALUES +(2, 'admin@manaknight.com', '$2a$10$LVzS6puBDllaSLt5eGxWCubf6qvLPKtk8taUwnU2tzYfQUKikFlwO', 'active', 'admin', 'Team Follow Up', 'ya29.a0AXeO80QLAusz5s5MA7VNCTvTbjCD4fvMUxbpYi3YZxdWkF_oGjaTzqABwRsR_ZgJwbwsDpKHiYZwWjGuZBQrcCF0MlCCdVLxcYHrcOej53a2u1jwLA4y6QUf6X1BaQIZEO-uVUZ0OD5JKrP098LrdrRlYGsiBid67rYXwKVoaCgYKAcMSARASFQHGX2MiZNcwyHXTbgvh6diBE3VT-w0175', '1//03noSXsyWetHtCgYIARAAGAMSNwF-L9IrbWjCPDsg5f1Xk7BroV5KMMaRR4q8bfE2eNdRwTBfXAOjZSN7r_5XCgWph-wGhZBx6xM', NULL, '2025-01-29 12:31:44'), +(3, 'adminteam@manaknight.com', '$2y$10$GpPSVZINjfrlMxm0.pVXhO6oCahQQTy32hWyBNg6gMELih5zLdr.W', 'active', 'admin', 'Team Follow Up', NULL, NULL, '2023-12-13', '2023-12-13 19:40:19'), +(5, 'admintest@manaknight.com', '$2y$10$tPLIU5zu0h48T/8i443.3epeaLXpbNMuULbQ7Ju9X7cGGrjqAugkK', 'active', 'admin', 'Team Follow Up', NULL, NULL, '2023-12-18', '2023-12-18 20:03:27'), +(9, 'gls@manaknight.com', '$2y$10$0o1TpLTQwVqdVcfXhlH0AO3xT79KK0mcN2bh0iSA5IzKNpE7gEKPa', 'active', 'client', 'GLS', NULL, NULL, NULL, '2024-03-04 20:50:40'), +(10, 'emmy@manaknight.com', '$2a$10$LVzS6puBDllaSLt5eGxWCubf6qvLPKtk8taUwnU2tzYfQUKikFlwO', 'active', 'client', 'Team Follow Up', 'ya29.a0ARW5m77GHU7yS5Cfk0zxjL-PcN1oQ4usNwkIfKCChflapA_991O5mVXdZFsHhuiAL8dnHs0CpAJguNrjGwnaMF4YFn7IxVC5Tlddk20kKX2z_L5TQWhykLVXbmCpamcbji6gnwPjaMAnt_S1_3OZp7NDd7zpfoz0uhZIJti5aCgYKAbESARASFQHGX2MibQDtWjAw9btwO5P7urVgEw0175', '1//03nzwtiJYvYkvCgYIARAAGAMSNwF-L9IrZLUjJLpTPSqABB-s_NVAS5stK_h9yZ0tUHED4yTpifxifaaxCOaAJqYnEdq8j_2Rnh8', '2024-02-16', '2025-01-09 18:58:57'), +(11, 'teamfollowup@manaknight.com', '$2y$10$e189rlZouySxKwsLLVtqxO2e8cbQzyh8b1b2f4eiKPfFaurUPbjQm', 'active', 'client', 'Team Follow Up', NULL, NULL, '2024-02-16', '2024-03-04 20:31:07'), +(14, 'testcomp@mkd.com', '$2y$10$BhVBeBoQobBtqxTydotETOmGHu31sqBxmh5lVeVLl8BBUPQv9yAB2', 'active', 'client', 'Comp', NULL, NULL, '2024-03-04', '2024-03-04 20:35:30'); \ No newline at end of file diff --git a/mysql-adapter.php b/mysql-adapter.php new file mode 100644 index 0000000..c2352f3 --- /dev/null +++ b/mysql-adapter.php @@ -0,0 +1,48 @@ +get_config(); + R::setup("mysql:host={$config['database-hostname']};dbname={$config['database-name']}", "{$config['database-u-ser']}", "{$config['database-password']}"); + // R::debug(TRUE, 2); + $this->_con = R::getToolBox(); + } + + /** + * Get Instance + * + * @return mixed + */ + public static function get_instance() + { + if (self::$instance == null) { + self::$instance = new MySqlAdapter(); + } + + return self::$instance; + } + + /** + * Get Connection + * + * @return mixed + */ + public function get_connection() + { + return $this->_con; + } +} diff --git a/mysql-database-service.php b/mysql-database-service.php new file mode 100644 index 0000000..8625002 --- /dev/null +++ b/mysql-database-service.php @@ -0,0 +1,658 @@ +_instance = MySqlAdapter::get_instance(); + $this->_db = $this->_instance->get_connection(); + } + + /** + * If you need to modify payload before create, overload this function + * + * @param mixed $data + * @return mixed + */ + protected function _pre_create_processing($data) + { + return $data; + } + + /** + * If you need to modify payload before edit, overload this function + * + * @param mixed $data + * @return mixed + */ + protected function _post_edit_processing($data) + { + return $data; + } + + /** + * Allow user to add extra counting condition so user don't have to change main function + * + * @param mixed $parameters + * @return $db + */ + protected function _custom_counting_conditions(&$db) + { + return $db; + } + + /** + * Raw Mysql query + * + * @param string $sql + * @return mixed + */ + public function raw_query($sql) + { + } + + /** + * Raw no error query for writes + * + * @param string $sql + * @return mixed + */ + public function raw_no_error_query($sql) + { + } + + + /** + * Raw Mysql query + * + * @param string $sql + * @return mixed + */ + public function raw_prepare_query($sql, $parameters) + { + } + + + /** + * Get Model + * + * @param integer $id + * @return mixed + */ + public function get($id) + { + return R::load($this->_table, $id); + } + public function get_like($field, $value) + { + // Match strings that start with $value + return R::find($this->_table, ' ' . $field . ' LIKE ? ORDER BY id DESC LIMIT 1', [$value . '%']); + } + // public function get_like($field, $value) + // { + // // Use a custom SQL query with REGEXP for more precise pattern matching + // $sql = 'SELECT * FROM ' . $this->_table . ' WHERE ' . $field . ' LIKE ? ORDER BY id DESC'; + // return R::getAll($sql, [$value . '%']); + // } + /** + * Get Model by field + * + * @param string $field + * @param mixed $value + * @return mixed + */ + public function get_by_field($field, $value) + { + return R::findOne($this->_table, " $field = ? ", ["$value"]); + } + + + /** + * Get One Model by fields + * + * @param string $field + * @param mixed $value + * @return mixed + */ + public function get_one_by_fields($where) + { + $sql = []; + foreach ($where as $key => $value) { + if (is_string($value) && strlen($value) > 0) { + $sql[] = " `$key` = '$value' "; + } else { + $sql[] = " `$key` = '$value' "; + } + } + + return R::findOne($this->_table, implode(' AND ', $sql)); + } + + /** + * Get Model by fields + * + * @param string $field + * @param mixed $value + * @return mixed + */ + public function get_by_fields($where) + { + $sql = []; + foreach ($where as $key => $value) { + if (is_string($value) && strlen($value) > 0) { + // $sql[] = "$key"; + // echo "1"; + $sql[] = " `$key` = '$value' "; + } else { + $sql[] = " `$key` = '$value' "; + } + } + + return R::find($this->_table, implode(' AND ', $sql)); + // R::fancyDebug( TRUE ); + + // $logs = R::getDatabaseAdapter() + // ->getDatabase() + // ->getLogger(); + // echo "
    ";
    +    // print_r( $where );
    +    // print_r( $sql );
    +    // print_r( $logs->grep( 'SELECT' ) );
    +    // die;
    +  }
    +
    +  /**
    +   * Get all Model
    +   * @param array $where
    +   * @return array
    +   */
    +  public function get_all($where = array())
    +  {
    +    $sql = [];
    +    foreach ($where as $key => $value) {
    +      if (is_string($value) && strlen($value) > 0) {
    +        $sql[] = "$key";
    +      } else {
    +        $sql[] = "$key = $value";
    +      }
    +    }
    +
    +    return R::findAll($this->_table, implode(' AND ', $sql));
    +  }
    +
    +  /**
    +   * Get all Model
    +   * @param string $status
    +   * @return array
    +   */
    +  public function get_all_by_status($status)
    +  {
    +    return R::findAll($this->_table, "status = ", [$status]);
    +  }
    +
    +  /**
    +   * Get all Model key value
    +   * @param string $field
    +   * @param string $status
    +   * @return array
    +   */
    +  public function get_all_by_key_value($field, $status)
    +  {
    +    $results = R::findAll($this->_table, "status = ", [$status]);
    +    $key_value = [];
    +
    +    foreach ($results as $key => $value) {
    +      $key_value[$value['id']] = $value[$field];
    +    }
    +
    +    return $key_value;
    +  }
    +
    +  /**
    +   * Create
    +   *
    +   * @param array $data
    +   * @return mixed
    +   */
    +  public function create($data)
    +  {
    +    try {
    +      if ($this->_use_timestamps) {
    +        if (!isset($data[$this->_created_field])) {
    +          $data[$this->_created_field] = date('Y-m-j');
    +        }
    +        if (!isset($data[$this->_updated_field])) {
    +          $data[$this->_updated_field] = date('Y-m-j H:i:s');
    +        }
    +      }
    +
    +      $data = $this->_pre_create_processing($data);
    +
    +      $row = R::dispense($this->_table);
    +
    +      foreach ($data as $key => $value) {
    +        $row[$key] = $value;
    +      }
    +
    +      $id = R::store($row);
    +
    +      if ($id) {
    +        return $id;
    +      }
    +
    +      return FALSE;
    +    } catch (Exception $e) {
    +      echo $e;
    +    }
    +  }
    +
    +  /**
    +   * Bulk Create
    +   *
    +   * @param [type] $params
    +   * @return void
    +   */
    +  public function batch_insert($params)
    +  {
    +    if ($this->_use_timestamps) {
    +      $rows = [];
    +      foreach ($params as $key => $value) {
    +        $params[$key][$this->_created_field] = date('Y-m-j');
    +        $params[$key][$this->_updated_field] = date('Y-m-j H:i:s');
    +        $row = R::dispense($this->_table);
    +
    +        foreach ($value as $field => $val) {
    +          $row[$field] = $val;
    +        }
    +
    +        $rows[] = $row;
    +      }
    +    }
    +
    +    return R::storeAll($rows);
    +  }
    +
    +  /**
    +   * Edit Model
    +   * @param array $data
    +   * @param integer $id
    +   * @return bool
    +   */
    +  public function edit($data, $id)
    +  {
    +    if ($this->_use_timestamps) {
    +      if (!isset($data[$this->_updated_field])) {
    +        $data[$this->_updated_field] = date('Y-m-j H:i:s');
    +      }
    +    }
    +
    +    $data = $this->_post_edit_processing($data);
    +
    +    $row = R::load($this->_table, $id);
    +
    +    foreach ($data as $key => $value) {
    +      $row[$key] = $value;
    +    }
    +
    +    return R::store($row);
    +  }
    +
    +  /**
    +   * Edit Model
    +   * @param array $data
    +   * @param integer $id
    +   * @return bool
    +   */
    +  public function edit_raw($data, $id)
    +  {
    +    if ($this->_use_timestamps) {
    +      $data[$this->_updated_field] = date('Y-m-j H:i:s');
    +    }
    +
    +    $row = R::load($this->_table, $id);
    +
    +    foreach ($data as $key => $value) {
    +      $row[$key] = $value;
    +    }
    +
    +    return R::store($row);
    +  }
    +
    +  /**
    +   * Soft Delete Model
    +   * @param array $data
    +   * @param integer $id
    +   * @return bool
    +   */
    +  public function delete($id)
    +  {
    +    $row = R::load($this->_table, $id);
    +    $row['status'] = 0;
    +    return R::store($row);
    +  }
    +
    +  /**
    +   * Real Delete Model
    +   * @param integer $id
    +   * @return bool
    +   */
    +  public function real_delete($id)
    +  {
    +    $row = R::load($this->_table, $id);
    +    R::trash($row);
    +  }
    +
    +  /**
    +   * Real Delete Model
    +   * @param Array
    +   * @return bool
    +   */
    +  public function real_delete_by_fields($where = [])
    +  {
    +    $sql = [];
    +    foreach ($where as $key => $value) {
    +      if (is_string($value) && strlen($value) > 0 && is_int($key)) {
    +        $sql[] = "$value";
    +      } else {
    +        $sql[] = "$key = $value";
    +      }
    +    }
    +
    +    $rows = R::find($this->_table, implode(' AND ', $sql));
    +    foreach ($rows as $row) {
    +      R::trash($row);
    +    }
    +  }
    +
    +  /**
    +   * Real Delete Model
    +   * @return bool
    +   */
    +  public function real_delete_all()
    +  {
    +    return R::wipe($this->_table);
    +  }
    +
    +  /**
    +   * Get All Validation Rules
    +   *
    +   * @param string $key
    +   * @return array
    +   */
    +  public function get_all_validation_rule()
    +  {
    +    return $this->_validation_rules;
    +  }
    +
    +  /**
    +   * Get All Allowed Rules
    +   *
    +   * @param string $key
    +   * @return array
    +   */
    +  public function get_all_allowed_fields()
    +  {
    +    return $this->_allowed_fields;
    +  }
    +
    +  /**
    +   * Get All Edit Validation Rules
    +   *
    +   * @param string $key
    +   * @return array
    +   */
    +  public function get_all_edit_validation_rule()
    +  {
    +    return $this->_validation_edit_rules;
    +  }
    +
    +  /**
    +   * Fill validation rules
    +   *
    +   * @param mixed $form_validation
    +   * @param mixed $validation_rules
    +   * @return void
    +   */
    +  public function set_form_validation($form_validation, $validation_rules)
    +  {
    +  }
    +
    +  /**
    +   * Count number of model
    +   *
    +   * @access public
    +   * @param mixed $parameters
    +   * @return integer $result
    +   */
    +  public function count($parameters)
    +  {
    +    $sql = [];
    +    foreach ($parameters as $key => $value) {
    +      if (is_string($value) && strlen($value) > 0) {
    +        $sql[] = "$key";
    +      } else {
    +        $sql[] = "$key = $value";
    +      }
    +    }
    +
    +    return R::count($this->_table, implode(' AND ', $sql));
    +  }
    +
    +  /**
    +   * Paginated
    +   *
    +   * @param integer $page
    +   * @param integer $limit
    +   * @param array $where
    +   * @param string $order_by
    +   * @param string $direction
    +   * @return mixed
    +   */
    +  public function get_paginated($page = 0, $limit = 25, $parameters = [], $order_by = '', $direction = 'ASC')
    +  {
    +    $sql = [];
    +    $last_id = 0;
    +    foreach ($parameters as $key => $value) {
    +      if (is_string($value) && strlen($value) > 0 && is_numeric($key)) {
    +        $sql[] = " $value ";
    +      } else {
    +        $sql[] = " `$key` = $value ";
    +      }
    +    }
    +
    +    // echo "
    ";
    +    // print_r( $sql);
    +    // print_r( $parameters);
    +    // die;
    +
    +    $total = R::count($this->_table, implode(' AND ', $sql));
    +    $offset = ($page - 1) * $limit;
    +    $limit_query = " ORDER BY $order_by $direction LIMIT $offset, $limit ";
    +    $final_list = R::find($this->_table, implode(' AND ', $sql) . $limit_query);
    +
    +    //select MODE 2 to see parameters filled in
    +    // R::fancyDebug();   //since 4.2
    +
    +    // $logs = R::getDatabaseAdapter()
    +    //         ->getDatabase()
    +    //         ->getLogger();
    +    // echo "
    ";
    +    // print_r( $logs);
    +    // die;
    +
    +    // echo "
    ";
    +    // print_r( $sql);
    +    // print_r( implode(' AND ', $sql));
    +    // $logs = R::getDatabaseAdapter()->getDatabase()->getLogger();
    +    // print_r( $final_list);
    +    // print_r( $logs);
    +    // die;
    +    if ($final_list) {
    +      $last_id = $final_list[array_key_last($final_list)]['id'];
    +    }
    +
    +    return [
    +      'total' => $total,
    +      'last_page' => ceil($total / $limit),
    +      'page' => $page,
    +      'id' => $last_id,
    +      'data' => $final_list
    +    ];
    +  }
    +
    +  /**
    +   * Cursor Pagination
    +   *
    +   * @param integer $page
    +   * @param integer $limit
    +   * @param array $where
    +   * @param string $order_by
    +   * @param string $direction
    +   * @return mixed
    +   */
    +  public function get_cursor_paginated($page = 1, $limit = 25, $parameters = [], $order_by = '', $direction = 'ASC', $id)
    +  {
    +    $sql = [];
    +    $last_id = 0;
    +
    +    foreach ($parameters as $key => $value) {
    +      if (is_string($value) && strlen($value) > 0) {
    +        $sql[] = "$key";
    +      } else {
    +        $sql[] = "$key = $value";
    +      }
    +    }
    +
    +    if (count($sql) < 1) {
    +      $total = R::count($this->_table);
    +    } else {
    +      $total = R::count($this->_table, implode(' AND ', $sql));
    +    }
    +
    +    $limit_query = " ORDER BY $order_by $direction LIMIT $limit";
    +    $sql[] = 'id > ' . $id;
    +    $final_list = R::find($this->_table, implode(' AND ', $sql) . $limit_query);
    +    if ($final_list) {
    +      $last_id = $final_list[array_key_last($final_list)]['id'];
    +    }
    +    $last_page = ceil($total / $limit);
    +    return [
    +      'total' => $total,
    +      'size' => $limit,
    +      'page' => $page,
    +      'last_page' => $last_page,
    +      'data' => $final_list,
    +      'id' => $last_id
    +    ];
    +  }
    +
    +  /**
    +   * Join All
    +   *
    +   * @param string $table
    +   * @param string $field
    +   * @param array $where
    +   * @param array $custom_duplicate_names
    +   * @return void
    +   */
    +  public function _join($table, $field, $where, $custom_duplicate_names = [])
    +  {
    +  }
    +
    +  /**
    +   * Join Paginate
    +   *
    +   * @param string $table
    +   * @param string $field
    +   * @param array $where
    +   * @param integer $page
    +   * @param integer $limit
    +   * @param string $order_by
    +   * @param string $direction
    +   * @param array $custom_duplicate_names
    +   * @return mixed
    +   */
    +  public function _join_paginate($table, $field, $where, $page = 0, $limit = 10, $order_by = '', $direction = 'ASC', $custom_duplicate_names = [])
    +  {
    +  }
    +
    +  /**
    +   * Filter all keys before inserting to make sure they are allowed
    +   *
    +   * @param mixed $data
    +   * @return mixed
    +   */
    +  protected function _filter_allow_keys($data)
    +  {
    +    $clean_data = [];
    +    $allowed_fields = $this->_allowed_fields;
    +    $allowed_fields[] = $this->_primary_key;
    +
    +    if ($this->_use_timestamps) {
    +      $allowed_fields[] = $this->_created_field;
    +      $allowed_fields[] = $this->_updated_field;
    +    }
    +
    +    foreach ($data as $key => $val) {
    +      if (!in_array($key, $allowed_fields)) {
    +        continue;
    +      }
    +      $clean_data[$key] = $val;
    +    }
    +    return $clean_data;
    +  }
    +
    +
    +  /**
    +   * escapeLikeString data
    +   *
    +   * @param mixed $data
    +   * @return mixed
    +   */
    +  public function escapeLikeString($data)
    +  {
    +  }
    +
    +  /**
    +   * Get Last ID in table
    +   *
    +   * @return integer
    +   */
    +  public function get_last_id()
    +  {
    +    return FALSE;
    +  }
    +
    +  /**
    +   * Get Database Table Schema
    +   *
    +   * @return mixed
    +   */
    +  public function get_schema()
    +  {
    +    return R::inspect($this->_table);
    +  }
    +}
    diff --git a/mysql.php b/mysql.php
    new file mode 100644
    index 0000000..212cbbd
    --- /dev/null
    +++ b/mysql.php
    @@ -0,0 +1,872 @@
    +$W){unset($hf[$z][$md]);if(is_array($W)){$hf[$z][stripslashes($md)]=$W;$hf[]=&$hf[$z][stripslashes($md)];}else$hf[$z][stripslashes($md)]=($wc?$W:stripslashes($W));}}}}function
    +bracket_escape($Wc,$_a=false){static$Pg=array(':'=>':1',']'=>':2','['=>':3','"'=>':4');return
    +strtr($Wc,($_a?array_flip($Pg):$Pg));}function
    +min_version($rh,$Fd="",$f=null){global$e;if(!$f)$f=$e;$Pf=$f->server_info;if($Fd&&preg_match('~([\d.]+)-MariaDB~',$Pf,$C)){$Pf=$C[1];$rh=$Fd;}return(version_compare($Pf,$rh)>=0);}function
    +charset($e){return(min_version("5.5.3",0,$e)?"utf8mb4":"utf8");}function
    +script($Yf,$Og="\n"){return"$Yf$Og";}function
    +script_src($hh){return"\n";}function
    +nonce(){return' nonce="'.get_nonce().'"';}function
    +target_blank(){return' target="_blank" rel="noreferrer noopener"';}function
    +h($ig){return
    +str_replace("\0","�",htmlspecialchars($ig,ENT_QUOTES,'utf-8'));}function
    +nl_br($ig){return
    +str_replace("\n","
    ",$ig);}function +checkbox($E,$Y,$Na,$qd="",$qe="",$Ra="",$rd=""){$K="".($qe?script("qsl('input').onclick = function () { $qe };",""):"");return($qd!=""||$Ra?"$K".h($qd)."":$K);}function +optionlist($ue,$Kf=null,$lh=false){$K="";foreach($ue +as$md=>$W){$ve=array($md=>$W);if(is_array($W)){$K.='';$ve=$W;}foreach($ve +as$z=>$X)$K.=''.h($X);if(is_array($W))$K.='';}return$K;}function +html_select($E,$ue,$Y="",$pe=true,$rd=""){if($pe)return"".(is_string($pe)?script("qsl('select').onchange = function () { $pe };",""):"");$K="";foreach($ue +as$z=>$X)$K.="";return$K;}function +select_input($wa,$ue,$Y="",$pe="",$Ue=""){$xg=($ue?"select":"input");return"<$xg$wa".($ue?">
    \n";}function +selectOrderPrint($we,$d,$w){print_fieldset("sort",'Sort',$we);$t=0;foreach((array)$_GET["order"]as$z=>$X){if($X!=""){echo"
    ".select_input(" name='order[$t]'",$d,$X,"selectFieldChange"),checkbox("desc[$t]",1,isset($_GET["desc"][$z]),'descending')."
    \n";$t++;}}echo"
    ".select_input(" name='order[$t]'",$d,"","selectAddRow"),checkbox("desc[$t]",1,false,'descending')."
    \n","\n";}function +selectLimitPrint($_){echo"
    ".'Limit'."
    ";echo"",script("qsl('input').oninput = selectFieldChange;",""),"
    \n";}function +selectLengthPrint($Cg){if($Cg!==null){echo"
    ".'Text length'."
    ","","
    \n";}}function +selectActionPrint($w){echo"
    ".'Action'."
    ",""," ","\n","var indexColumns = ";$d=array();foreach($w +as$v){$rb=reset($v["columns"]);if($v["type"]!="FULLTEXT"&&$rb)$d[$rb]=1;}$d[""]=1;foreach($d +as$z=>$X)json_row($z);echo";\n","selectFieldChange.call(qs('#form')['select']);\n","\n","
    \n";}function +selectCommandPrint(){return!information_schema(DB);}function +selectImportPrint(){return!information_schema(DB);}function +selectEmailPrint($Wb,$d){}function +selectColumnsProcess($d,$w){global$Fc,$Jc;$N=array();$s=array();foreach((array)$_GET["columns"]as$z=>$X){if($X["fun"]=="count"||($X["col"]!=""&&(!$X["fun"]||in_array($X["fun"],$Fc)||in_array($X["fun"],$Jc)))){$N[$z]=apply_sql_function($X["fun"],($X["col"]!=""?idf_escape($X["col"]):"*"));if(!in_array($X["fun"],$Jc))$s[]=$N[$z];}}return +array($N,$s);}function +selectSearchProcess($m,$w){global$e,$j;$K=array();foreach($w +as$t=>$v){if($v["type"]=="FULLTEXT"&&$_GET["fulltext"][$t]!="")$K[]="MATCH (".implode(", ",array_map('idf_escape',$v["columns"])).") AGAINST (".q($_GET["fulltext"][$t]).(isset($_GET["boolean"][$t])?" IN BOOLEAN MODE":"").")";}foreach((array)$_GET["where"]as$z=>$X){if("$X[col]$X[val]"!=""&&in_array($X["op"],$this->operators)){$af="";$db=" $X[op]";if(preg_match('~IN$~',$X["op"])){$Zc=process_length($X["val"]);$db.=" ".($Zc!=""?$Zc:"(NULL)");}elseif($X["op"]=="SQL")$db=" $X[val]";elseif($X["op"]=="LIKE %%")$db=" LIKE ".$this->processInput($m[$X["col"]],"%$X[val]%");elseif($X["op"]=="ILIKE %%")$db=" ILIKE ".$this->processInput($m[$X["col"]],"%$X[val]%");elseif($X["op"]=="FIND_IN_SET"){$af="$X[op](".q($X["val"]).", ";$db=")";}elseif(!preg_match('~NULL$~',$X["op"]))$db.=" ".$this->processInput($m[$X["col"]],$X["val"]);if($X["col"]!="")$K[]=$af.$j->convertSearch(idf_escape($X["col"]),$X,$m[$X["col"]]).$db;else{$Ya=array();foreach($m +as$E=>$l){if((preg_match('~^[-\d.'.(preg_match('~IN$~',$X["op"])?',':'').']+$~',$X["val"])||!preg_match('~'.number_type().'|bit~',$l["type"]))&&(!preg_match("~[\x80-\xFF]~",$X["val"])||preg_match('~char|text|enum|set~',$l["type"]))&&(!preg_match('~date|timestamp~',$l["type"])||preg_match('~^\d+-\d+-\d+~',$X["val"])))$Ya[]=$af.$j->convertSearch(idf_escape($E),$X,$l).$db;}$K[]=($Ya?"(".implode(" OR ",$Ya).")":"1 = 0");}}}return$K;}function +selectOrderProcess($m,$w){$K=array();foreach((array)$_GET["order"]as$z=>$X){if($X!="")$K[]=(preg_match('~^((COUNT\(DISTINCT |[A-Z0-9_]+\()(`(?:[^`]|``)+`|"(?:[^"]|"")+")\)|COUNT\(\*\))$~',$X)?$X:idf_escape($X)).(isset($_GET["desc"][$z])?" DESC":"");}return$K;}function +selectLimitProcess(){return(isset($_GET["limit"])?$_GET["limit"]:"50");}function +selectLengthProcess(){return(isset($_GET["text_length"])?$_GET["text_length"]:"100");}function +selectEmailProcess($Z,$Ac){return +false;}function +selectQueryBuild($N,$Z,$s,$we,$_,$F){return"";}function +messageQuery($I,$Dg,$qc=false){global$y,$j;restart_session();$Rc=&get_session("queries");if(!$Rc[$_GET["db"]])$Rc[$_GET["db"]]=array();if(strlen($I)>1e6)$I=preg_replace('~[\x80-\xFF]+$~','',substr($I,0,1e6))."\n…";$Rc[$_GET["db"]][]=array($I,time(),$Dg);$cg="sql-".count($Rc[$_GET["db"]]);$K="".'SQL command'."\n";if(!$qc&&($wh=$j->warnings())){$u="warnings-".count($Rc[$_GET["db"]]);$K="".'Warnings'.", $K\n";}return" ".@date("H:i:s").""." $K';}function +editRowPrint($Q,$m,$L,$fh){}function +editFunctions($l){global$Rb;$K=($l["null"]?"NULL/":"");$fh=isset($_GET["select"])||where($_GET);foreach($Rb +as$z=>$Fc){if(!$z||(!isset($_GET["call"])&&$fh)){foreach($Fc +as$Re=>$X){if(!$Re||preg_match("~$Re~",$l["type"]))$K.="/$X";}}if($z&&!preg_match('~set|blob|bytea|raw|file|bool~',$l["type"]))$K.="/SQL";}if($l["auto_increment"]&&!$fh)$K='Auto Increment';return +explode("/",$K);}function +editInput($Q,$l,$wa,$Y){if($l["type"]=="enum")return(isset($_GET["select"])?" ":"").($l["null"]?" ":"").enum_input("radio",$wa,$l,$Y,0);return"";}function +editHint($Q,$l,$Y){return"";}function +processInput($l,$Y,$q=""){if($q=="SQL")return$Y;$E=$l["field"];$K=q($Y);if(preg_match('~^(now|getdate|uuid)$~',$q))$K="$q()";elseif(preg_match('~^current_(date|timestamp)$~',$q))$K=$q;elseif(preg_match('~^([+-]|\|\|)$~',$q))$K=idf_escape($E)." $q $K";elseif(preg_match('~^[+-] interval$~',$q))$K=idf_escape($E)." $q ".(preg_match("~^(\\d+|'[0-9.: -]') [A-Z_]+\$~i",$Y)?$Y:$K);elseif(preg_match('~^(addtime|subtime|concat)$~',$q))$K="$q(".idf_escape($E).", $K)";elseif(preg_match('~^(md5|sha1|password|encrypt)$~',$q))$K="$q($K)";return +unconvert_field($l,$K);}function +dumpOutput(){$K=array('text'=>'open','file'=>'save');if(function_exists('gzencode'))$K['gz']='gzip';return$K;}function +dumpFormat(){return +array('sql'=>'SQL','csv'=>'CSV,','csv;'=>'CSV;','tsv'=>'TSV');}function +dumpDatabase($i){}function +dumpTable($Q,$kg,$ld=0){if($_POST["format"]!="sql"){echo"\xef\xbb\xbf";if($kg)dump_csv(array_keys(fields($Q)));}else{if($ld==2){$m=array();foreach(fields($Q)as$E=>$l)$m[]=idf_escape($E)." $l[full_type]";$g="CREATE TABLE ".table($Q)." (".implode(", ",$m).")";}else$g=create_sql($Q,$_POST["auto_increment"],$kg);set_utf8mb4($g);if($kg&&$g){if($kg=="DROP+CREATE"||$ld==1)echo"DROP ".($ld==2?"VIEW":"TABLE")." IF EXISTS ".table($Q).";\n";if($ld==1)$g=remove_definer($g);echo"$g;\n\n";}}}function +dumpData($Q,$kg,$I){global$e,$y;$Id=($y=="sqlite"?0:1048576);if($kg){if($_POST["format"]=="sql"){if($kg=="TRUNCATE+INSERT")echo +truncate_sql($Q).";\n";$m=fields($Q);}$J=$e->query($I,1);if($J){$ed="";$Ia="";$nd=array();$mg="";$tc=($Q!=''?'fetch_assoc':'fetch_row');while($L=$J->$tc()){if(!$nd){$oh=array();foreach($L +as$X){$l=$J->fetch_field();$nd[]=$l->name;$z=idf_escape($l->name);$oh[]="$z = VALUES($z)";}$mg=($kg=="INSERT+UPDATE"?"\nON DUPLICATE KEY UPDATE ".implode(", ",$oh):"").";\n";}if($_POST["format"]!="sql"){if($kg=="table"){dump_csv($nd);$kg="INSERT";}dump_csv($L);}else{if(!$ed)$ed="INSERT INTO ".table($Q)." (".implode(", ",array_map('idf_escape',$nd)).") VALUES";foreach($L +as$z=>$X){$l=$m[$z];$L[$z]=($X!==null?unconvert_field($l,preg_match(number_type(),$l["type"])&&!preg_match('~\[~',$l["full_type"])&&is_numeric($X)?$X:q(($X===false?0:$X))):"NULL");}$Ff=($Id?"\n":" ")."(".implode(",\t",$L).")";if(!$Ia)$Ia=$ed.$Ff;elseif(strlen($Ia)+4+strlen($Ff)+strlen($mg)<$Id)$Ia.=",$Ff";else{echo$Ia.$mg;$Ia=$ed.$Ff;}}}if($Ia)echo$Ia.$mg;}elseif($_POST["format"]=="sql")echo"-- ".str_replace("\n"," ",$e->error)."\n";}}function +dumpFilename($Vc){return +friendly_url($Vc!=""?$Vc:(SERVER!=""?SERVER:"localhost"));}function +dumpHeaders($Vc,$Ud=false){$Fe=$_POST["output"];$nc=(preg_match('~sql~',$_POST["format"])?"sql":($Ud?"tar":"csv"));header("Content-Type: ".($Fe=="gz"?"application/x-gzip":($nc=="tar"?"application/x-tar":($nc=="sql"||$Fe!="file"?"text/plain":"text/csv")."; charset=utf-8")));if($Fe=="gz")ob_start('ob_gzencode',1e6);return$nc;}function +importServerPath(){return"adminer.sql";}function +homepage(){echo'

    +',$this->name(),' ',$ga,' +',(version_compare($ga,$_COOKIE["adminer_version"])<0?h($_COOKIE["adminer_version"]):""),' +

    +';if($Td=="auth"){$Fe="";foreach((array)$_SESSION["pwds"]as$qh=>$Qf){foreach($Qf +as$O=>$mh){foreach($mh +as$V=>$G){if($G!==null){$xb=$_SESSION["db"][$qh][$O][$V];foreach(($xb?array_keys($xb):array(""))as$i)$Fe.="
  • ($Kb[$qh]) ".h($V.($O!=""?"@".$this->serverName($O):"").($i!=""?" - $i":""))."\n";}}}}if($Fe)echo"
      \n$Fe
    \n".script("mixin(qs('#logins'), {onmouseover: menuOver, onmouseout: menuOut});");}else{$S=array();if($_GET["ns"]!==""&&!$Td&&DB!=""){$e->select_db(DB);$S=table_status('',true);}echo +script_src(preg_replace("~\\?.*~","",ME)."?file=jush.js&version=4.8.1");if(support("sql")){echo' +';if($S){$Bd=array();foreach($S +as$Q=>$U)$Bd[]=preg_quote($Q,'/');echo"var jushLinks = { $y: [ '".js_escape(ME).(support("table")?"table=":"select=")."\$&', /\\b(".implode("|",$Bd).")\\b/g ] };\n";foreach(array("bac","bra","sqlite_quo","mssql_bra")as$X)echo"jushLinks.$X = jushLinks.$y;\n";}$Pf=$e->server_info;echo'bodyLoad(\'',(is_object($e)?preg_replace('~^(\d\.?\d).*~s','\1',$Pf):""),'\'',(preg_match('~MariaDB~',$Pf)?", true":""),'); + +';}$this->databasesPrint($Td);if(DB==""||!$Td){echo"

    ".'No tables.'."\n";else$this->tablesPrint($S);}}}function +databasesPrint($Td){global$b,$e;$h=$this->databases();if(DB&&$h&&!in_array(DB,$h))array_unshift($h,DB);echo'

    +

    +';hidden_fields_get();$vb=script("mixin(qsl('select'), {onmousedown: dbMouseDown, onchange: dbChange});");echo"".'DB'.": ".($h?"$vb":"\n"),"\n";foreach(array("import","sql","schema","dump","privileges")as$X){if(isset($_GET[$X])){echo"";break;}}echo"

    \n";}function +tablesPrint($S){echo"
      ".script("mixin(qs('#tables'), {onmouseover: menuOver, onmouseout: menuOut});");foreach($S +as$Q=>$fg){$E=$this->tableName($fg);if($E!=""){echo'
    • ".'select'." ",(support("table")||support("indexes")?'$E":"$E")."\n";}}echo"
    \n";}}$b=(function_exists('adminer_object')?adminer_object():new +Adminer);$Kb=array("server"=>"MySQL")+$Kb;if(!defined("DRIVER")){define("DRIVER","server");if(extension_loaded("mysqli")){class +Min_DB +extends +MySQLi{var$extension="MySQLi";function +__construct(){parent::init();}function +connect($O="",$V="",$G="",$ub=null,$Ve=null,$Xf=null){global$b;mysqli_report(MYSQLI_REPORT_OFF);list($Tc,$Ve)=explode(":",$O,2);$dg=$b->connectSsl();if($dg)$this->ssl_set($dg['key'],$dg['cert'],$dg['ca'],'','');$K=@$this->real_connect(($O!=""?$Tc:ini_get("mysqli.default_host")),($O.$V!=""?$V:ini_get("mysqli.default_user")),($O.$V.$G!=""?$G:ini_get("mysqli.default_pw")),$ub,(is_numeric($Ve)?$Ve:ini_get("mysqli.default_port")),(!is_numeric($Ve)?$Ve:$Xf),($dg?64:0));$this->options(MYSQLI_OPT_LOCAL_INFILE,false);return$K;}function +set_charset($La){if(parent::set_charset($La))return +true;parent::set_charset('utf8');return$this->query("SET NAMES $La");}function +result($I,$l=0){$J=$this->query($I);if(!$J)return +false;$L=$J->fetch_array();return$L[$l];}function +quote($ig){return"'".$this->escape_string($ig)."'";}}}elseif(extension_loaded("mysql")&&!((ini_bool("sql.safe_mode")||ini_bool("mysql.allow_local_infile"))&&extension_loaded("pdo_mysql"))){class +Min_DB{var$extension="MySQL",$server_info,$affected_rows,$errno,$error,$_link,$_result;function +connect($O,$V,$G){if(ini_bool("mysql.allow_local_infile")){$this->error=sprintf('Disable %s or enable %s or %s extensions.',"'mysql.allow_local_infile'","MySQLi","PDO_MySQL");return +false;}$this->_link=@mysql_connect(($O!=""?$O:ini_get("mysql.default_host")),("$O$V"!=""?$V:ini_get("mysql.default_user")),("$O$V$G"!=""?$G:ini_get("mysql.default_password")),true,131072);if($this->_link)$this->server_info=mysql_get_server_info($this->_link);else$this->error=mysql_error();return(bool)$this->_link;}function +set_charset($La){if(function_exists('mysql_set_charset')){if(mysql_set_charset($La,$this->_link))return +true;mysql_set_charset('utf8',$this->_link);}return$this->query("SET NAMES $La");}function +quote($ig){return"'".mysql_real_escape_string($ig,$this->_link)."'";}function +select_db($ub){return +mysql_select_db($ub,$this->_link);}function +query($I,$Yg=false){$J=@($Yg?mysql_unbuffered_query($I,$this->_link):mysql_query($I,$this->_link));$this->error="";if(!$J){$this->errno=mysql_errno($this->_link);$this->error=mysql_error($this->_link);return +false;}if($J===true){$this->affected_rows=mysql_affected_rows($this->_link);$this->info=mysql_info($this->_link);return +true;}return +new +Min_Result($J);}function +multi_query($I){return$this->_result=$this->query($I);}function +store_result(){return$this->_result;}function +next_result(){return +false;}function +result($I,$l=0){$J=$this->query($I);if(!$J||!$J->num_rows)return +false;return +mysql_result($J->_result,0,$l);}}class +Min_Result{var$num_rows,$_result,$_offset=0;function +__construct($J){$this->_result=$J;$this->num_rows=mysql_num_rows($J);}function +fetch_assoc(){return +mysql_fetch_assoc($this->_result);}function +fetch_row(){return +mysql_fetch_row($this->_result);}function +fetch_field(){$K=mysql_fetch_field($this->_result,$this->_offset++);$K->orgtable=$K->table;$K->orgname=$K->name;$K->charsetnr=($K->blob?63:0);return$K;}function +__destruct(){mysql_free_result($this->_result);}}}elseif(extension_loaded("pdo_mysql")){class +Min_DB +extends +Min_PDO{var$extension="PDO_MySQL";function +connect($O,$V,$G){global$b;$ue=array(PDO::MYSQL_ATTR_LOCAL_INFILE=>false);$dg=$b->connectSsl();if($dg){if(!empty($dg['key']))$ue[PDO::MYSQL_ATTR_SSL_KEY]=$dg['key'];if(!empty($dg['cert']))$ue[PDO::MYSQL_ATTR_SSL_CERT]=$dg['cert'];if(!empty($dg['ca']))$ue[PDO::MYSQL_ATTR_SSL_CA]=$dg['ca'];}$this->dsn("mysql:charset=utf8;host=".str_replace(":",";unix_socket=",preg_replace('~:(\d)~',';port=\1',$O)),$V,$G,$ue);return +true;}function +set_charset($La){$this->query("SET NAMES $La");}function +select_db($ub){return$this->query("USE ".idf_escape($ub));}function +query($I,$Yg=false){$this->pdo->setAttribute(PDO::MYSQL_ATTR_USE_BUFFERED_QUERY,!$Yg);return +parent::query($I,$Yg);}}}class +Min_Driver +extends +Min_SQL{function +insert($Q,$P){return($P?parent::insert($Q,$P):queries("INSERT INTO ".table($Q)." ()\nVALUES ()"));}function +insertUpdate($Q,$M,$cf){$d=array_keys(reset($M));$af="INSERT INTO ".table($Q)." (".implode(", ",$d).") VALUES\n";$oh=array();foreach($d +as$z)$oh[$z]="$z = VALUES($z)";$mg="\nON DUPLICATE KEY UPDATE ".implode(", ",$oh);$oh=array();$zd=0;foreach($M +as$P){$Y="(".implode(", ",$P).")";if($oh&&(strlen($af)+$zd+strlen($Y)+strlen($mg)>1e6)){if(!queries($af.implode(",\n",$oh).$mg))return +false;$oh=array();$zd=0;}$oh[]=$Y;$zd+=strlen($Y)+2;}return +queries($af.implode(",\n",$oh).$mg);}function +slowQuery($I,$Eg){if(min_version('5.7.8','10.1.2')){if(preg_match('~MariaDB~',$this->_conn->server_info))return"SET STATEMENT max_statement_time=$Eg FOR $I";elseif(preg_match('~^(SELECT\b)(.+)~is',$I,$C))return"$C[1] /*+ MAX_EXECUTION_TIME(".($Eg*1000).") */ $C[2]";}}function +convertSearch($Wc,$X,$l){return(preg_match('~char|text|enum|set~',$l["type"])&&!preg_match("~^utf8~",$l["collation"])&&preg_match('~[\x80-\xFF]~',$X['val'])?"CONVERT($Wc USING ".charset($this->_conn).")":$Wc);}function +warnings(){$J=$this->_conn->query("SHOW WARNINGS");if($J&&$J->num_rows){ob_start();select($J);return +ob_get_clean();}}function +tableHelp($E){$Ed=preg_match('~MariaDB~',$this->_conn->server_info);if(information_schema(DB))return +strtolower(($Ed?"information-schema-$E-table/":str_replace("_","-",$E)."-table.html"));if(DB=="mysql")return($Ed?"mysql$E-table/":"system-database.html");}}function +idf_escape($Wc){return"`".str_replace("`","``",$Wc)."`";}function +table($Wc){return +idf_escape($Wc);}function +connect(){global$b,$Xg,$jg;$e=new +Min_DB;$nb=$b->credentials();if($e->connect($nb[0],$nb[1],$nb[2])){$e->set_charset(charset($e));$e->query("SET sql_quote_show_create = 1, autocommit = 1");if(min_version('5.7.8',10.2,$e)){$jg['Strings'][]="json";$Xg["json"]=4294967295;}return$e;}$K=$e->error;if(function_exists('iconv')&&!is_utf8($K)&&strlen($Ff=iconv("windows-1250","utf-8",$K))>strlen($K))$K=$Ff;return$K;}function +get_databases($yc){$K=get_session("dbs");if($K===null){$I=(min_version(5)?"SELECT SCHEMA_NAME FROM information_schema.SCHEMATA ORDER BY SCHEMA_NAME":"SHOW DATABASES");$K=($yc?slow_query($I):get_vals($I));restart_session();set_session("dbs",$K);stop_session();}return$K;}function +limit($I,$Z,$_,$he=0,$Of=" "){return" $I$Z".($_!==null?$Of."LIMIT $_".($he?" OFFSET $he":""):"");}function +limit1($Q,$I,$Z,$Of="\n"){return +limit($I,$Z,1,0,$Of);}function +db_collation($i,$Xa){global$e;$K=null;$g=$e->result("SHOW CREATE DATABASE ".idf_escape($i),1);if(preg_match('~ COLLATE ([^ ]+)~',$g,$C))$K=$C[1];elseif(preg_match('~ CHARACTER SET ([^ ]+)~',$g,$C))$K=$Xa[$C[1]][-1];return$K;}function +engines(){$K=array();foreach(get_rows("SHOW ENGINES")as$L){if(preg_match("~YES|DEFAULT~",$L["Support"]))$K[]=$L["Engine"];}return$K;}function +logged_user(){global$e;return$e->result("SELECT USER()");}function +tables_list(){return +get_key_vals(min_version(5)?"SELECT TABLE_NAME, TABLE_TYPE FROM information_schema.TABLES WHERE TABLE_SCHEMA = DATABASE() ORDER BY TABLE_NAME":"SHOW TABLES");}function +count_tables($h){$K=array();foreach($h +as$i)$K[$i]=count(get_vals("SHOW TABLES IN ".idf_escape($i)));return$K;}function +table_status($E="",$rc=false){$K=array();foreach(get_rows($rc&&min_version(5)?"SELECT TABLE_NAME AS Name, ENGINE AS Engine, TABLE_COMMENT AS Comment FROM information_schema.TABLES WHERE TABLE_SCHEMA = DATABASE() ".($E!=""?"AND TABLE_NAME = ".q($E):"ORDER BY Name"):"SHOW TABLE STATUS".($E!=""?" LIKE ".q(addcslashes($E,"%_\\")):""))as$L){if($L["Engine"]=="InnoDB")$L["Comment"]=preg_replace('~(?:(.+); )?InnoDB free: .*~','\1',$L["Comment"]);if(!isset($L["Engine"]))$L["Comment"]="";if($E!="")return$L;$K[$L["Name"]]=$L;}return$K;}function +is_view($R){return$R["Engine"]===null;}function +fk_support($R){return +preg_match('~InnoDB|IBMDB2I~i',$R["Engine"])||(preg_match('~NDB~i',$R["Engine"])&&min_version(5.6));}function +fields($Q){$K=array();foreach(get_rows("SHOW FULL COLUMNS FROM ".table($Q))as$L){preg_match('~^([^( ]+)(?:\((.+)\))?( unsigned)?( zerofill)?$~',$L["Type"],$C);$K[$L["Field"]]=array("field"=>$L["Field"],"full_type"=>$L["Type"],"type"=>$C[1],"length"=>$C[2],"unsigned"=>ltrim($C[3].$C[4]),"default"=>($L["Default"]!=""||preg_match("~char|set~",$C[1])?(preg_match('~text~',$C[1])?stripslashes(preg_replace("~^'(.*)'\$~",'\1',$L["Default"])):$L["Default"]):null),"null"=>($L["Null"]=="YES"),"auto_increment"=>($L["Extra"]=="auto_increment"),"on_update"=>(preg_match('~^on update (.+)~i',$L["Extra"],$C)?$C[1]:""),"collation"=>$L["Collation"],"privileges"=>array_flip(preg_split('~, *~',$L["Privileges"])),"comment"=>$L["Comment"],"primary"=>($L["Key"]=="PRI"),"generated"=>preg_match('~^(VIRTUAL|PERSISTENT|STORED)~',$L["Extra"]),);}return$K;}function +indexes($Q,$f=null){$K=array();foreach(get_rows("SHOW INDEX FROM ".table($Q),$f)as$L){$E=$L["Key_name"];$K[$E]["type"]=($E=="PRIMARY"?"PRIMARY":($L["Index_type"]=="FULLTEXT"?"FULLTEXT":($L["Non_unique"]?($L["Index_type"]=="SPATIAL"?"SPATIAL":"INDEX"):"UNIQUE")));$K[$E]["columns"][]=$L["Column_name"];$K[$E]["lengths"][]=($L["Index_type"]=="SPATIAL"?null:$L["Sub_part"]);$K[$E]["descs"][]=null;}return$K;}function +foreign_keys($Q){global$e,$oe;static$Re='(?:`(?:[^`]|``)+`|"(?:[^"]|"")+")';$K=array();$lb=$e->result("SHOW CREATE TABLE ".table($Q),1);if($lb){preg_match_all("~CONSTRAINT ($Re) FOREIGN KEY ?\\(((?:$Re,? ?)+)\\) REFERENCES ($Re)(?:\\.($Re))? \\(((?:$Re,? ?)+)\\)(?: ON DELETE ($oe))?(?: ON UPDATE ($oe))?~",$lb,$Gd,PREG_SET_ORDER);foreach($Gd +as$C){preg_match_all("~$Re~",$C[2],$Yf);preg_match_all("~$Re~",$C[5],$yg);$K[idf_unescape($C[1])]=array("db"=>idf_unescape($C[4]!=""?$C[3]:$C[4]),"table"=>idf_unescape($C[4]!=""?$C[4]:$C[3]),"source"=>array_map('idf_unescape',$Yf[0]),"target"=>array_map('idf_unescape',$yg[0]),"on_delete"=>($C[6]?$C[6]:"RESTRICT"),"on_update"=>($C[7]?$C[7]:"RESTRICT"),);}}return$K;}function +view($E){global$e;return +array("select"=>preg_replace('~^(?:[^`]|`[^`]*`)*\s+AS\s+~isU','',$e->result("SHOW CREATE VIEW ".table($E),1)));}function +collations(){$K=array();foreach(get_rows("SHOW COLLATION")as$L){if($L["Default"])$K[$L["Charset"]][-1]=$L["Collation"];else$K[$L["Charset"]][]=$L["Collation"];}ksort($K);foreach($K +as$z=>$X)asort($K[$z]);return$K;}function +information_schema($i){return(min_version(5)&&$i=="information_schema")||(min_version(5.5)&&$i=="performance_schema");}function +error(){global$e;return +h(preg_replace('~^You have an error.*syntax to use~U',"Syntax error",$e->error));}function +create_database($i,$Wa){return +queries("CREATE DATABASE ".idf_escape($i).($Wa?" COLLATE ".q($Wa):""));}function +drop_databases($h){$K=apply_queries("DROP DATABASE",$h,'idf_escape');restart_session();set_session("dbs",null);return$K;}function +rename_database($E,$Wa){$K=false;if(create_database($E,$Wa)){$S=array();$th=array();foreach(tables_list()as$Q=>$U){if($U=='VIEW')$th[]=$Q;else$S[]=$Q;}$K=(!$S&&!$th)||move_tables($S,$th,$E);drop_databases($K?array(DB):array());}return$K;}function +auto_increment(){$za=" PRIMARY KEY";if($_GET["create"]!=""&&$_POST["auto_increment_col"]){foreach(indexes($_GET["create"])as$v){if(in_array($_POST["fields"][$_POST["auto_increment_col"]]["orig"],$v["columns"],true)){$za="";break;}if($v["type"]=="PRIMARY")$za=" UNIQUE";}}return" AUTO_INCREMENT$za";}function +alter_table($Q,$E,$m,$_c,$bb,$Zb,$Wa,$ya,$Ne){$sa=array();foreach($m +as$l)$sa[]=($l[1]?($Q!=""?($l[0]!=""?"CHANGE ".idf_escape($l[0]):"ADD"):" ")." ".implode($l[1]).($Q!=""?$l[2]:""):"DROP ".idf_escape($l[0]));$sa=array_merge($sa,$_c);$fg=($bb!==null?" COMMENT=".q($bb):"").($Zb?" ENGINE=".q($Zb):"").($Wa?" COLLATE ".q($Wa):"").($ya!=""?" AUTO_INCREMENT=$ya":"");if($Q=="")return +queries("CREATE TABLE ".table($E)." (\n".implode(",\n",$sa)."\n)$fg$Ne");if($Q!=$E)$sa[]="RENAME TO ".table($E);if($fg)$sa[]=ltrim($fg);return($sa||$Ne?queries("ALTER TABLE ".table($Q)."\n".implode(",\n",$sa).$Ne):true);}function +alter_indexes($Q,$sa){foreach($sa +as$z=>$X)$sa[$z]=($X[2]=="DROP"?"\nDROP INDEX ".idf_escape($X[1]):"\nADD $X[0] ".($X[0]=="PRIMARY"?"KEY ":"").($X[1]!=""?idf_escape($X[1])." ":"")."(".implode(", ",$X[2]).")");return +queries("ALTER TABLE ".table($Q).implode(",",$sa));}function +truncate_tables($S){return +apply_queries("TRUNCATE TABLE",$S);}function +drop_views($th){return +queries("DROP VIEW ".implode(", ",array_map('table',$th)));}function +drop_tables($S){return +queries("DROP TABLE ".implode(", ",array_map('table',$S)));}function +move_tables($S,$th,$yg){global$e;$wf=array();foreach($S +as$Q)$wf[]=table($Q)." TO ".idf_escape($yg).".".table($Q);if(!$wf||queries("RENAME TABLE ".implode(", ",$wf))){$Bb=array();foreach($th +as$Q)$Bb[table($Q)]=view($Q);$e->select_db($yg);$i=idf_escape(DB);foreach($Bb +as$E=>$sh){if(!queries("CREATE VIEW $E AS ".str_replace(" $i."," ",$sh["select"]))||!queries("DROP VIEW $i.$E"))return +false;}return +true;}return +false;}function +copy_tables($S,$th,$yg){queries("SET sql_mode = 'NO_AUTO_VALUE_ON_ZERO'");foreach($S +as$Q){$E=($yg==DB?table("copy_$Q"):idf_escape($yg).".".table($Q));if(($_POST["overwrite"]&&!queries("\nDROP TABLE IF EXISTS $E"))||!queries("CREATE TABLE $E LIKE ".table($Q))||!queries("INSERT INTO $E SELECT * FROM ".table($Q)))return +false;foreach(get_rows("SHOW TRIGGERS LIKE ".q(addcslashes($Q,"%_\\")))as$L){$Sg=$L["Trigger"];if(!queries("CREATE TRIGGER ".($yg==DB?idf_escape("copy_$Sg"):idf_escape($yg).".".idf_escape($Sg))." $L[Timing] $L[Event] ON $E FOR EACH ROW\n$L[Statement];"))return +false;}}foreach($th +as$Q){$E=($yg==DB?table("copy_$Q"):idf_escape($yg).".".table($Q));$sh=view($Q);if(($_POST["overwrite"]&&!queries("DROP VIEW IF EXISTS $E"))||!queries("CREATE VIEW $E AS $sh[select]"))return +false;}return +true;}function +trigger($E){if($E=="")return +array();$M=get_rows("SHOW TRIGGERS WHERE `Trigger` = ".q($E));return +reset($M);}function +triggers($Q){$K=array();foreach(get_rows("SHOW TRIGGERS LIKE ".q(addcslashes($Q,"%_\\")))as$L)$K[$L["Trigger"]]=array($L["Timing"],$L["Event"]);return$K;}function +trigger_options(){return +array("Timing"=>array("BEFORE","AFTER"),"Event"=>array("INSERT","UPDATE","DELETE"),"Type"=>array("FOR EACH ROW"),);}function +routine($E,$U){global$e,$bc,$cd,$Xg;$qa=array("bool","boolean","integer","double precision","real","dec","numeric","fixed","national char","national varchar");$Zf="(?:\\s|/\\*[\s\S]*?\\*/|(?:#|-- )[^\n]*\n?|--\r?\n)";$Wg="((".implode("|",array_merge(array_keys($Xg),$qa)).")\\b(?:\\s*\\(((?:[^'\")]|$bc)++)\\))?\\s*(zerofill\\s*)?(unsigned(?:\\s+zerofill)?)?)(?:\\s*(?:CHARSET|CHARACTER\\s+SET)\\s*['\"]?([^'\"\\s,]+)['\"]?)?";$Re="$Zf*(".($U=="FUNCTION"?"":$cd).")?\\s*(?:`((?:[^`]|``)*)`\\s*|\\b(\\S+)\\s+)$Wg";$g=$e->result("SHOW CREATE $U ".idf_escape($E),2);preg_match("~\\(((?:$Re\\s*,?)*)\\)\\s*".($U=="FUNCTION"?"RETURNS\\s+$Wg\\s+":"")."(.*)~is",$g,$C);$m=array();preg_match_all("~$Re\\s*,?~is",$C[1],$Gd,PREG_SET_ORDER);foreach($Gd +as$Ie)$m[]=array("field"=>str_replace("``","`",$Ie[2]).$Ie[3],"type"=>strtolower($Ie[5]),"length"=>preg_replace_callback("~$bc~s",'normalize_enum',$Ie[6]),"unsigned"=>strtolower(preg_replace('~\s+~',' ',trim("$Ie[8] $Ie[7]"))),"null"=>1,"full_type"=>$Ie[4],"inout"=>strtoupper($Ie[1]),"collation"=>strtolower($Ie[9]),);if($U!="FUNCTION")return +array("fields"=>$m,"definition"=>$C[11]);return +array("fields"=>$m,"returns"=>array("type"=>$C[12],"length"=>$C[13],"unsigned"=>$C[15],"collation"=>$C[16]),"definition"=>$C[17],"language"=>"SQL",);}function +routines(){return +get_rows("SELECT ROUTINE_NAME AS SPECIFIC_NAME, ROUTINE_NAME, ROUTINE_TYPE, DTD_IDENTIFIER FROM information_schema.ROUTINES WHERE ROUTINE_SCHEMA = ".q(DB));}function +routine_languages(){return +array();}function +routine_id($E,$L){return +idf_escape($E);}function +last_id(){global$e;return$e->result("SELECT LAST_INSERT_ID()");}function +explain($e,$I){return$e->query("EXPLAIN ".(min_version(5.1)&&!min_version(5.7)?"PARTITIONS ":"").$I);}function +found_rows($R,$Z){return($Z||$R["Engine"]!="InnoDB"?null:$R["Rows"]);}function +types(){return +array();}function +schemas(){return +array();}function +get_schema(){return"";}function +set_schema($Hf,$f=null){return +true;}function +create_sql($Q,$ya,$kg){global$e;$K=$e->result("SHOW CREATE TABLE ".table($Q),1);if(!$ya)$K=preg_replace('~ AUTO_INCREMENT=\d+~','',$K);return$K;}function +truncate_sql($Q){return"TRUNCATE ".table($Q);}function +use_sql($ub){return"USE ".idf_escape($ub);}function +trigger_sql($Q){$K="";foreach(get_rows("SHOW TRIGGERS LIKE ".q(addcslashes($Q,"%_\\")),null,"-- ")as$L)$K.="\nCREATE TRIGGER ".idf_escape($L["Trigger"])." $L[Timing] $L[Event] ON ".table($L["Table"])." FOR EACH ROW\n$L[Statement];;\n";return$K;}function +show_variables(){return +get_key_vals("SHOW VARIABLES");}function +process_list(){return +get_rows("SHOW FULL PROCESSLIST");}function +show_status(){return +get_key_vals("SHOW STATUS");}function +convert_field($l){if(preg_match("~binary~",$l["type"]))return"HEX(".idf_escape($l["field"]).")";if($l["type"]=="bit")return"BIN(".idf_escape($l["field"])." + 0)";if(preg_match("~geometry|point|linestring|polygon~",$l["type"]))return(min_version(8)?"ST_":"")."AsWKT(".idf_escape($l["field"]).")";}function +unconvert_field($l,$K){if(preg_match("~binary~",$l["type"]))$K="UNHEX($K)";if($l["type"]=="bit")$K="CONV($K, 2, 10) + 0";if(preg_match("~geometry|point|linestring|polygon~",$l["type"]))$K=(min_version(8)?"ST_":"")."GeomFromText($K, SRID($l[field]))";return$K;}function +support($sc){return!preg_match("~scheme|sequence|type|view_trigger|materializedview".(min_version(8)?"":"|descidx".(min_version(5.1)?"":"|event|partitioning".(min_version(5)?"":"|routine|trigger|view")))."~",$sc);}function +kill_process($X){return +queries("KILL ".number($X));}function +connection_id(){return"SELECT CONNECTION_ID()";}function +max_connections(){global$e;return$e->result("SELECT @@max_connections");}function +driver_config(){$Xg=array();$jg=array();foreach(array('Numbers'=>array("tinyint"=>3,"smallint"=>5,"mediumint"=>8,"int"=>10,"bigint"=>20,"decimal"=>66,"float"=>12,"double"=>21),'Date and time'=>array("date"=>10,"datetime"=>19,"timestamp"=>19,"time"=>10,"year"=>4),'Strings'=>array("char"=>255,"varchar"=>65535,"tinytext"=>255,"text"=>65535,"mediumtext"=>16777215,"longtext"=>4294967295),'Lists'=>array("enum"=>65535,"set"=>64),'Binary'=>array("bit"=>20,"binary"=>255,"varbinary"=>65535,"tinyblob"=>255,"blob"=>65535,"mediumblob"=>16777215,"longblob"=>4294967295),'Geometry'=>array("geometry"=>0,"point"=>0,"linestring"=>0,"polygon"=>0,"multipoint"=>0,"multilinestring"=>0,"multipolygon"=>0,"geometrycollection"=>0),)as$z=>$X){$Xg+=$X;$jg[$z]=array_keys($X);}return +array('possible_drivers'=>array("MySQLi","MySQL","PDO_MySQL"),'jush'=>"sql",'types'=>$Xg,'structured_types'=>$jg,'unsigned'=>array("unsigned","zerofill","unsigned zerofill"),'operators'=>array("=","<",">","<=",">=","!=","LIKE","LIKE %%","REGEXP","IN","FIND_IN_SET","IS NULL","NOT LIKE","NOT REGEXP","NOT IN","IS NOT NULL","SQL"),'functions'=>array("char_length","date","from_unixtime","lower","round","floor","ceil","sec_to_time","time_to_sec","upper"),'grouping'=>array("avg","count","count distinct","group_concat","max","min","sum"),'edit_functions'=>array(array("char"=>"md5/sha1/password/encrypt/uuid","binary"=>"md5/sha1","date|time"=>"now",),array(number_type()=>"+/-","date"=>"+ interval/- interval","time"=>"addtime/subtime","char|text"=>"concat",)),);}}$eb=driver_config();$Ze=$eb['possible_drivers'];$y=$eb['jush'];$Xg=$eb['types'];$jg=$eb['structured_types'];$eh=$eb['unsigned'];$se=$eb['operators'];$Fc=$eb['functions'];$Jc=$eb['grouping'];$Rb=$eb['edit_functions'];if($b->operators===null)$b->operators=$se;define("SERVER",$_GET[DRIVER]);define("DB",$_GET["db"]);define("ME",preg_replace('~\?.*~','',relative_uri()).'?'.(sid()?SID.'&':'').(SERVER!==null?DRIVER."=".urlencode(SERVER).'&':'').(isset($_GET["username"])?"username=".urlencode($_GET["username"]).'&':'').(DB!=""?'db='.urlencode(DB).'&'.(isset($_GET["ns"])?"ns=".urlencode($_GET["ns"])."&":""):''));$ga="4.8.1";function +page_header($Gg,$k="",$Ha=array(),$Hg=""){global$ca,$ga,$b,$Kb,$y;page_headers();if(is_ajax()&&$k){page_messages($k);exit;}$Ig=$Gg.($Hg!=""?": $Hg":"");$Jg=strip_tags($Ig.(SERVER!=""&&SERVER!="localhost"?h(" - ".SERVER):"")." - ".$b->name());echo' + + + +',$Jg,' + +',script_src(preg_replace("~\\?.*~","",ME)."?file=functions.js&version=4.8.1");if($b->head()){echo' + +';foreach($b->css()as$pb){echo' +';}}echo' + +';$vc=get_temp_dir()."/adminer.version";if(!$_COOKIE["adminer_version"]&&function_exists('openssl_verify')&&file_exists($vc)&&filemtime($vc)+86400>time()){$rh=unserialize(file_get_contents($vc));$jf="-----BEGIN PUBLIC KEY----- +MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAwqWOVuF5uw7/+Z70djoK +RlHIZFZPO0uYRezq90+7Amk+FDNd7KkL5eDve+vHRJBLAszF/7XKXe11xwliIsFs +DFWQlsABVZB3oisKCBEuI71J4kPH8dKGEWR9jDHFw3cWmoH3PmqImX6FISWbG3B8 +h7FIx3jEaw5ckVPVTeo5JRm/1DZzJxjyDenXvBQ/6o9DgZKeNDgxwKzH+sw9/YCO +jHnq1cFpOIISzARlrHMa/43YfeNRAm/tsBXjSxembBPo7aQZLAWHmaj5+K19H10B +nCpz9Y++cipkVEiKRGih4ZEvjoFysEOdRLj6WiD/uUNky4xGeA6LaJqh5XpkFkcQ +fQIDAQAB +-----END PUBLIC KEY----- +";if(openssl_verify($rh["version"],base64_decode($rh["signature"]),$jf)==1)$_COOKIE["adminer_version"]=$rh["version"];}echo' +mixin(document.body, {onkeydown: bodyKeydown, onclick: bodyClick',(isset($_COOKIE["adminer_version"])?"":", onload: partial(verifyVersion, '$ga', '".js_escape(ME)."', '".get_token()."')");?>}); +document.body.className = document.body.className.replace(/ nojs/, ' js'); +var offlineMessage = ' + + +',script("mixin(qs('#help'), {onmouseover: function () { helpOpen = 1; }, onmouseout: helpMouseout});"),' +
    +';if($Ha!==null){$A=substr(preg_replace('~\b(username|db|ns)=[^&]*&~','',ME),0,-1);echo'

    $Ig

    \n","\n";restart_session();page_messages($k);$h=&get_session("dbs");if(DB!=""&&$h&&!in_array(DB,$h,true))$h=null;stop_session();define("PAGE_HEADER",1);}function +page_headers(){global$b;header("Content-Type: text/html; charset=utf-8");header("Cache-Control: no-cache");header("X-Frame-Options: deny");header("X-XSS-Protection: 0");header("X-Content-Type-Options: nosniff");header("Referrer-Policy: origin-when-cross-origin");foreach($b->csp()as$ob){$Pc=array();foreach($ob +as$z=>$X)$Pc[]="$z $X";header("Content-Security-Policy: ".implode("; ",$Pc));}$b->headers();}function +csp(){return +array(array("script-src"=>"'self' 'unsafe-inline' 'nonce-".get_nonce()."' 'strict-dynamic'","connect-src"=>"'self'","frame-src"=>"https://www.adminer.org","object-src"=>"'none'","base-uri"=>"'none'","form-action"=>"'self'",),);}function +get_nonce(){static$ce;if(!$ce)$ce=base64_encode(rand_string());return$ce;}function +page_messages($k){$gh=preg_replace('~^[^?]*~','',$_SERVER["REQUEST_URI"]);$Rd=$_SESSION["messages"][$gh];if($Rd){echo"
    ".implode("
    \n
    ",$Rd)."
    ".script("messagesPrint();");unset($_SESSION["messages"][$gh]);}if($k)echo"
    $k
    \n";}function +page_footer($Td=""){global$b,$T;echo'
    + +';if($Td!="auth"){echo'
    +

    + + +

    +
    +';}echo' +',script("setupSubmitHighlight(document);");}function +int32($Wd){while($Wd>=2147483648)$Wd-=4294967296;while($Wd<=-2147483649)$Wd+=4294967296;return(int)$Wd;}function +long2str($W,$vh){$Ff='';foreach($W +as$X)$Ff.=pack('V',$X);if($vh)return +substr($Ff,0,end($W));return$Ff;}function +str2long($Ff,$vh){$W=array_values(unpack('V*',str_pad($Ff,4*ceil(strlen($Ff)/4),"\0")));if($vh)$W[]=strlen($Ff);return$W;}function +xxtea_mx($Bh,$Ah,$ng,$md){return +int32((($Bh>>5&0x7FFFFFF)^$Ah<<2)+(($Ah>>3&0x1FFFFFFF)^$Bh<<4))^int32(($ng^$Ah)+($md^$Bh));}function +encrypt_string($hg,$z){if($hg=="")return"";$z=array_values(unpack("V*",pack("H*",md5($z))));$W=str2long($hg,true);$Wd=count($W)-1;$Bh=$W[$Wd];$Ah=$W[0];$H=floor(6+52/($Wd+1));$ng=0;while($H-->0){$ng=int32($ng+0x9E3779B9);$Qb=$ng>>2&3;for($Ge=0;$Ge<$Wd;$Ge++){$Ah=$W[$Ge+1];$Vd=xxtea_mx($Bh,$Ah,$ng,$z[$Ge&3^$Qb]);$Bh=int32($W[$Ge]+$Vd);$W[$Ge]=$Bh;}$Ah=$W[0];$Vd=xxtea_mx($Bh,$Ah,$ng,$z[$Ge&3^$Qb]);$Bh=int32($W[$Wd]+$Vd);$W[$Wd]=$Bh;}return +long2str($W,false);}function +decrypt_string($hg,$z){if($hg=="")return"";if(!$z)return +false;$z=array_values(unpack("V*",pack("H*",md5($z))));$W=str2long($hg,false);$Wd=count($W)-1;$Bh=$W[$Wd];$Ah=$W[0];$H=floor(6+52/($Wd+1));$ng=int32($H*0x9E3779B9);while($ng){$Qb=$ng>>2&3;for($Ge=$Wd;$Ge>0;$Ge--){$Bh=$W[$Ge-1];$Vd=xxtea_mx($Bh,$Ah,$ng,$z[$Ge&3^$Qb]);$Ah=int32($W[$Ge]-$Vd);$W[$Ge]=$Ah;}$Bh=$W[$Wd];$Vd=xxtea_mx($Bh,$Ah,$ng,$z[$Ge&3^$Qb]);$Ah=int32($W[0]-$Vd);$W[0]=$Ah;$ng=int32($ng-0x9E3779B9);}return +long2str($W,true);}$e='';$Oc=$_SESSION["token"];if(!$Oc)$_SESSION["token"]=rand(1,1e6);$T=get_token();$Te=array();if($_COOKIE["adminer_permanent"]){foreach(explode(" ",$_COOKIE["adminer_permanent"])as$X){list($z)=explode(":",$X);$Te[$z]=$X;}}function +add_invalid_login(){global$b;$p=file_open_lock(get_temp_dir()."/adminer.invalid");if(!$p)return;$hd=unserialize(stream_get_contents($p));$Dg=time();if($hd){foreach($hd +as$id=>$X){if($X[0]<$Dg)unset($hd[$id]);}}$gd=&$hd[$b->bruteForceKey()];if(!$gd)$gd=array($Dg+30*60,0);$gd[1]++;file_write_unlock($p,serialize($hd));}function +check_invalid_login(){global$b;$hd=unserialize(@file_get_contents(get_temp_dir()."/adminer.invalid"));$gd=($hd?$hd[$b->bruteForceKey()]:array());$be=($gd[1]>29?$gd[0]-time():0);if($be>0)auth_error(lang(array('Too many unsuccessful logins, try again in %d minute.','Too many unsuccessful logins, try again in %d minutes.'),ceil($be/60)));}$xa=$_POST["auth"];if($xa){session_regenerate_id();$qh=$xa["driver"];$O=$xa["server"];$V=$xa["username"];$G=(string)$xa["password"];$i=$xa["db"];set_password($qh,$O,$V,$G);$_SESSION["db"][$qh][$O][$V][$i]=true;if($xa["permanent"]){$z=base64_encode($qh)."-".base64_encode($O)."-".base64_encode($V)."-".base64_encode($i);$ef=$b->permanentLogin(true);$Te[$z]="$z:".base64_encode($ef?encrypt_string($G,$ef):"");cookie("adminer_permanent",implode(" ",$Te));}if(count($_POST)==1||DRIVER!=$qh||SERVER!=$O||$_GET["username"]!==$V||DB!=$i)redirect(auth_url($qh,$O,$V,$i));}elseif($_POST["logout"]&&(!$Oc||verify_token())){foreach(array("pwds","db","dbs","queries")as$z)set_session($z,null);unset_permanent();redirect(substr(preg_replace('~\b(username|db|ns)=[^&]*&~','',ME),0,-1),'Logout successful.'.' '.'Thanks for using Adminer, consider donating.');}elseif($Te&&!$_SESSION["pwds"]){session_regenerate_id();$ef=$b->permanentLogin();foreach($Te +as$z=>$X){list(,$Qa)=explode(":",$X);list($qh,$O,$V,$i)=array_map('base64_decode',explode("-",$z));set_password($qh,$O,$V,decrypt_string(base64_decode($Qa),$ef));$_SESSION["db"][$qh][$O][$V][$i]=true;}}function +unset_permanent(){global$Te;foreach($Te +as$z=>$X){list($qh,$O,$V,$i)=array_map('base64_decode',explode("-",$z));if($qh==DRIVER&&$O==SERVER&&$V==$_GET["username"]&&$i==DB)unset($Te[$z]);}cookie("adminer_permanent",implode(" ",$Te));}function +auth_error($k){global$b,$Oc;$Rf=session_name();if(isset($_GET["username"])){header("HTTP/1.1 403 Forbidden");if(($_COOKIE[$Rf]||$_GET[$Rf])&&!$Oc)$k='Session expired, please login again.';else{restart_session();add_invalid_login();$G=get_password();if($G!==null){if($G===false)$k.=($k?'
    ':'').sprintf('Master password expired. Implement %s method to make it permanent.',target_blank(),'permanentLogin()');set_password(DRIVER,SERVER,$_GET["username"],null);}unset_permanent();}}if(!$_COOKIE[$Rf]&&$_GET[$Rf]&&ini_bool("session.use_only_cookies"))$k='Session support must be enabled.';$Je=session_get_cookie_params();cookie("adminer_key",($_COOKIE["adminer_key"]?$_COOKIE["adminer_key"]:rand_string()),$Je["lifetime"]);page_header('Login',$k,null);echo"
    \n","
    ";if(hidden_fields($_POST,array("auth")))echo"

    ".'The action will be performed after successful login with the same credentials.'."\n";echo"

    \n";$b->loginForm();echo"
    \n";page_footer("auth");exit;}if(isset($_GET["username"])&&!class_exists("Min_DB")){unset($_SESSION["pwds"][DRIVER]);unset_permanent();page_header('No extension',sprintf('None of the supported PHP extensions (%s) are available.',implode(", ",$Ze)),false);page_footer("auth");exit;}stop_session(true);if(isset($_GET["username"])&&is_string(get_password())){list($Tc,$Ve)=explode(":",SERVER,2);if(preg_match('~^\s*([-+]?\d+)~',$Ve,$C)&&($C[1]<1024||$C[1]>65535))auth_error('Connecting to privileged ports is not allowed.');check_invalid_login();$e=connect();$j=new +Min_Driver($e);}$Cd=null;if(!is_object($e)||($Cd=$b->login($_GET["username"],get_password()))!==true){$k=(is_string($e)?h($e):(is_string($Cd)?$Cd:'Invalid credentials.'));auth_error($k.(preg_match('~^ | $~',get_password())?'
    '.'There is a space in the input password which might be the cause.':''));}if($_POST["logout"]&&$Oc&&!verify_token()){page_header('Logout','Invalid CSRF token. Send the form again.');page_footer("db");exit;}if($xa&&$_POST["token"])$_POST["token"]=$T;$k='';if($_POST){if(!verify_token()){$bd="max_input_vars";$Md=ini_get($bd);if(extension_loaded("suhosin")){foreach(array("suhosin.request.max_vars","suhosin.post.max_vars")as$z){$X=ini_get($z);if($X&&(!$Md||$X<$Md)){$bd=$z;$Md=$X;}}}$k=(!$_POST["token"]&&$Md?sprintf('Maximum number of allowed fields exceeded. Please increase %s.',"'$bd'"):'Invalid CSRF token. Send the form again.'.' '.'If you did not send this request from Adminer then close this page.');}}elseif($_SERVER["REQUEST_METHOD"]=="POST"){$k=sprintf('Too big POST data. Reduce the data or increase the %s configuration directive.',"'post_max_size'");if(isset($_GET["sql"]))$k.=' '.'You can upload a big SQL file via FTP and import it from server.';}function +select($J,$f=null,$ze=array(),$_=0){global$y;$Bd=array();$w=array();$d=array();$Fa=array();$Xg=array();$K=array();odd('');for($t=0;(!$_||$t<$_)&&($L=$J->fetch_row());$t++){if(!$t){echo"
    \n","\n","";for($x=0;$xfetch_field();$E=$l->name;$ye=$l->orgtable;$xe=$l->orgname;$K[$l->table]=$ye;if($ze&&$y=="sql")$Bd[$x]=($E=="table"?"table=":($E=="possible_keys"?"indexes=":null));elseif($ye!=""){if(!isset($w[$ye])){$w[$ye]=array();foreach(indexes($ye,$f)as$v){if($v["type"]=="PRIMARY"){$w[$ye]=array_flip($v["columns"]);break;}}$d[$ye]=$w[$ye];}if(isset($d[$ye][$xe])){unset($d[$ye][$xe]);$w[$ye][$xe]=$x;$Bd[$x]=$ye;}}if($l->charsetnr==63)$Fa[$x]=true;$Xg[$x]=$l->type;echo"name!=$xe?" title='".h(($ye!=""?"$ye.":"").$xe)."'":"").">".h($E).($ze?doc_link(array('sql'=>"explain-output.html#explain_".strtolower($E),'mariadb'=>"explain/#the-columns-in-explain-select",)):"");}echo"\n";}echo"";foreach($L +as$z=>$X){$A="";if(isset($Bd[$z])&&!$d[$Bd[$z]]){if($ze&&$y=="sql"){$Q=$L[array_search("table=",$Bd)];$A=ME.$Bd[$z].urlencode($ze[$Q]!=""?$ze[$Q]:$Q);}else{$A=ME."edit=".urlencode($Bd[$z]);foreach($w[$Bd[$z]]as$Ua=>$x)$A.="&where".urlencode("[".bracket_escape($Ua)."]")."=".urlencode($L[$x]);}}elseif(is_url($X))$A=$X;if($X===null)$X="NULL";elseif($Fa[$z]&&!is_utf8($X))$X="".lang(array('%d byte','%d bytes'),strlen($X))."";else{$X=h($X);if($Xg[$z]==254)$X="$X";}if($A)$X="$X";echo"
    $X";}}echo($t?"
    \n
    ":"

    ".'No rows.')."\n";return$K;}function +referencable_primary($Mf){$K=array();foreach(table_status('',true)as$rg=>$Q){if($rg!=$Mf&&fk_support($Q)){foreach(fields($rg)as$l){if($l["primary"]){if($K[$rg]){unset($K[$rg]);break;}$K[$rg]=$l;}}}}return$K;}function +adminer_settings(){parse_str($_COOKIE["adminer_settings"],$Tf);return$Tf;}function +adminer_setting($z){$Tf=adminer_settings();return$Tf[$z];}function +set_adminer_settings($Tf){return +cookie("adminer_settings",http_build_query($Tf+adminer_settings()));}function +textarea($E,$Y,$M=10,$Ya=80){global$y;echo"";}function +edit_type($z,$l,$Xa,$o=array(),$pc=array()){global$jg,$Xg,$eh,$oe;$U=$l["type"];echo'',"',($eh?"':''),(isset($l['on_update'])?"':''),($o?" ":" ");}function +process_length($zd){global$bc;return(preg_match("~^\\s*\\(?\\s*$bc(?:\\s*,\\s*$bc)*+\\s*\\)?\\s*\$~",$zd)&&preg_match_all("~$bc~",$zd,$Gd)?"(".implode(",",$Gd[0]).")":preg_replace('~^[0-9].*~','(\0)',preg_replace('~[^-0-9,+()[\]]~','',$zd)));}function +process_type($l,$Va="COLLATE"){global$eh;return" $l[type]".process_length($l["length"]).(preg_match(number_type(),$l["type"])&&in_array($l["unsigned"],$eh)?" $l[unsigned]":"").(preg_match('~char|text|enum|set~',$l["type"])&&$l["collation"]?" $Va ".q($l["collation"]):"");}function +process_field($l,$Vg){return +array(idf_escape(trim($l["field"])),process_type($Vg),($l["null"]?" NULL":" NOT NULL"),default_value($l),(preg_match('~timestamp|datetime~',$l["type"])&&$l["on_update"]?" ON UPDATE $l[on_update]":""),(support("comment")&&$l["comment"]!=""?" COMMENT ".q($l["comment"]):""),($l["auto_increment"]?auto_increment():null),);}function +default_value($l){$zb=$l["default"];return($zb===null?"":" DEFAULT ".(preg_match('~char|binary|text|enum|set~',$l["type"])||preg_match('~^(?![a-z])~i',$zb)?q($zb):$zb));}function +type_class($U){foreach(array('char'=>'text','date'=>'time|year','binary'=>'blob','enum'=>'set',)as$z=>$X){if(preg_match("~$z|$X~",$U))return" class='$z'";}}function +edit_fields($m,$Xa,$U="TABLE",$o=array()){global$cd;$m=array_values($m);$_b=(($_POST?$_POST["defaults"]:adminer_setting("defaults"))?"":" class='hidden'");$cb=(($_POST?$_POST["comments"]:adminer_setting("comments"))?"":" class='hidden'");echo' +';if($U=="PROCEDURE"){echo'';}echo'',($U=="TABLE"?'Column name':'Parameter name'),'Type',script("qs('#enum-edit').onblur = editingLengthBlur;"),'Length +','Options';if($U=="TABLE"){echo'NULL +AI',doc_link(array('sql'=>"example-auto-increment.html",'mariadb'=>"auto_increment/",)),'Default value +',(support("comment")?"".'Comment':"");}echo'',"".script("row_count = ".count($m).";"),' + +',script("mixin(qsl('tbody'), {onclick: editingClick, onkeydown: editingKeydown, oninput: editingInput});");foreach($m +as$t=>$l){$t++;$_e=$l[($_POST?"orig":"field")];$Hb=(isset($_POST["add"][$t-1])||(isset($l["field"])&&!$_POST["drop_col"][$t]))&&(support("drop_col")||$_e=="");echo' +',($U=="PROCEDURE"?"".html_select("fields[$t][inout]",explode("|",$cd),$l["inout"]):""),'';if($Hb){echo'';}echo'';edit_type("fields[$t]",$l,$Xa,$o);if($U=="TABLE"){echo'',checkbox("fields[$t][null]",1,$l["null"],"","","block","label-null"),'',checkbox("fields[$t][has_default]",1,$l["has_default"],"","","","label-default"),'',(support("comment")?"":"");}echo"",(support("move_col")?" "." "." ":""),($_e==""||support("drop_col")?"":"");}}function +process_fields(&$m){$he=0;if($_POST["up"]){$td=0;foreach($m +as$z=>$l){if(key($_POST["up"])==$z){unset($m[$z]);array_splice($m,$td,0,array($l));break;}if(isset($l["field"]))$td=$he;$he++;}}elseif($_POST["down"]){$Cc=false;foreach($m +as$z=>$l){if(isset($l["field"])&&$Cc){unset($m[key($_POST["down"])]);array_splice($m,$he,0,array($Cc));break;}if(key($_POST["down"])==$z)$Cc=$l;$he++;}}elseif($_POST["add"]){$m=array_values($m);array_splice($m,key($_POST["add"]),0,array(array()));}elseif(!$_POST["drop_col"])return +false;return +true;}function +normalize_enum($C){return"'".str_replace("'","''",addcslashes(stripcslashes(str_replace($C[0][0].$C[0][0],$C[0][0],substr($C[0],1,-1))),'\\'))."'";}function +grant($r,$gf,$d,$ne){if(!$gf)return +true;if($gf==array("ALL PRIVILEGES","GRANT OPTION"))return($r=="GRANT"?queries("$r ALL PRIVILEGES$ne WITH GRANT OPTION"):queries("$r ALL PRIVILEGES$ne")&&queries("$r GRANT OPTION$ne"));return +queries("$r ".preg_replace('~(GRANT OPTION)\([^)]*\)~','\1',implode("$d, ",$gf).$d).$ne);}function +drop_create($Lb,$g,$Mb,$Ag,$Nb,$B,$Qd,$Od,$Pd,$ke,$Zd){if($_POST["drop"])query_redirect($Lb,$B,$Qd);elseif($ke=="")query_redirect($g,$B,$Pd);elseif($ke!=$Zd){$mb=queries($g);queries_redirect($B,$Od,$mb&&queries($Lb));if($mb)queries($Mb);}else +queries_redirect($B,$Od,queries($Ag)&&queries($Nb)&&queries($Lb)&&queries($g));}function +create_trigger($ne,$L){global$y;$Fg=" $L[Timing] $L[Event]".(preg_match('~ OF~',$L["Event"])?" $L[Of]":"");return"CREATE TRIGGER ".idf_escape($L["Trigger"]).($y=="mssql"?$ne.$Fg:$Fg.$ne).rtrim(" $L[Type]\n$L[Statement]",";").";";}function +create_routine($Cf,$L){global$cd,$y;$P=array();$m=(array)$L["fields"];ksort($m);foreach($m +as$l){if($l["field"]!="")$P[]=(preg_match("~^($cd)\$~",$l["inout"])?"$l[inout] ":"").idf_escape($l["field"]).process_type($l,"CHARACTER SET");}$Ab=rtrim("\n$L[definition]",";");return"CREATE $Cf ".idf_escape(trim($L["name"]))." (".implode(", ",$P).")".(isset($_GET["function"])?" RETURNS".process_type($L["returns"],"CHARACTER SET"):"").($L["language"]?" LANGUAGE $L[language]":"").($y=="pgsql"?" AS ".q($Ab):"$Ab;");}function +remove_definer($I){return +preg_replace('~^([A-Z =]+) DEFINER=`'.preg_replace('~@(.*)~','`@`(%|\1)',logged_user()).'`~','\1',$I);}function +format_foreign_key($n){global$oe;$i=$n["db"];$de=$n["ns"];return" FOREIGN KEY (".implode(", ",array_map('idf_escape',$n["source"])).") REFERENCES ".($i!=""&&$i!=$_GET["db"]?idf_escape($i).".":"").($de!=""&&$de!=$_GET["ns"]?idf_escape($de).".":"").table($n["table"])." (".implode(", ",array_map('idf_escape',$n["target"])).")".(preg_match("~^($oe)\$~",$n["on_delete"])?" ON DELETE $n[on_delete]":"").(preg_match("~^($oe)\$~",$n["on_update"])?" ON UPDATE $n[on_update]":"");}function +tar_file($vc,$Kg){$K=pack("a100a8a8a8a12a12",$vc,644,0,0,decoct($Kg->size),decoct(time()));$Pa=8*32;for($t=0;$tsend();echo +str_repeat("\0",511-($Kg->size+511)%512);}function +ini_bytes($bd){$X=ini_get($bd);switch(strtolower(substr($X,-1))){case'g':$X*=1024;case'm':$X*=1024;case'k':$X*=1024;}return$X;}function +doc_link($Qe,$Bg="?"){global$y,$e;$Pf=$e->server_info;$rh=preg_replace('~^(\d\.?\d).*~s','\1',$Pf);$ih=array('sql'=>"https://dev.mysql.com/doc/refman/$rh/en/",'sqlite'=>"https://www.sqlite.org/",'pgsql'=>"https://www.postgresql.org/docs/$rh/",'mssql'=>"https://msdn.microsoft.com/library/",'oracle'=>"https://www.oracle.com/pls/topic/lookup?ctx=db".preg_replace('~^.* (\d+)\.(\d+)\.\d+\.\d+\.\d+.*~s','\1\2',$Pf)."&id=",);if(preg_match('~MariaDB~',$Pf)){$ih['sql']="https://mariadb.com/kb/en/library/";$Qe['sql']=(isset($Qe['mariadb'])?$Qe['mariadb']:str_replace(".html","/",$Qe['sql']));}return($Qe[$y]?"$Bg":"");}function +ob_gzencode($ig){return +gzencode($ig);}function +db_size($i){global$e;if(!$e->select_db($i))return"?";$K=0;foreach(table_status()as$R)$K+=$R["Data_length"]+$R["Index_length"];return +format_number($K);}function +set_utf8mb4($g){global$e;static$P=false;if(!$P&&preg_match('~\butf8mb4~i',$g)){$P=true;echo"SET NAMES ".charset($e).";\n\n";}}function +connect_error(){global$b,$e,$T,$k,$Kb;if(DB!=""){header("HTTP/1.1 404 Not Found");page_header('Database'.": ".h(DB),'Invalid database.',true);}else{if($_POST["db"]&&!$k)queries_redirect(substr(ME,0,-1),'Databases have been dropped.',drop_databases($_POST["db"]));page_header('Select database',$k,false);echo"

    ".sprintf('%s version: %s through PHP extension %s',$Kb[DRIVER],"".h($e->server_info)."","$e->extension")."\n","

    ".sprintf('Logged as: %s',"".h(logged_user())."")."\n";$h=$b->databases();if($h){$If=support("scheme");$Xa=collations();echo"

    \n","\n",script("mixin(qsl('table'), {onclick: tableClick, ondblclick: partialArg(tableClick, true)});"),"".(support("database")?"\n";$h=($_GET["dbsize"]?count_tables($h):array_flip($h));foreach($h +as$i=>$S){$Bf=h(ME)."db=".urlencode($i);$u=h("Db-".$i);echo"".(support("database")?"
    ":"")."".'Database'." - ".'Refresh'.""."".'Collation'."".'Tables'."".'Size'." - ".'Compute'."".script("qsl('a').onclick = partial(ajaxSetHtml, '".js_escape(ME)."script=connect');","")."
    ".checkbox("db[]",$i,in_array($i,(array)$_POST["db"]),"","","",$u):""),"".h($i)."";$Wa=h(db_collation($i,$Xa));echo"".(support("database")?"$Wa":$Wa),"".($_GET["dbsize"]?$S:"?")."","".($_GET["dbsize"]?db_size($i):"?"),"\n";}echo"
    \n",(support("database")?"\n":""),"\n","
    \n",script("tableCheck();");}}page_footer("db");}if(isset($_GET["status"]))$_GET["variables"]=$_GET["status"];if(isset($_GET["import"]))$_GET["sql"]=$_GET["import"];if(!(DB!=""?$e->select_db(DB):isset($_GET["sql"])||isset($_GET["dump"])||isset($_GET["database"])||isset($_GET["processlist"])||isset($_GET["privileges"])||isset($_GET["user"])||isset($_GET["variables"])||$_GET["script"]=="connect"||$_GET["script"]=="kill")){if(DB!=""||$_GET["refresh"]){restart_session();set_session("dbs",null);}connect_error();exit;}$oe="RESTRICT|NO ACTION|CASCADE|SET NULL|SET DEFAULT";class +TmpFile{var$handler;var$size;function +__construct(){$this->handler=tmpfile();}function +write($hb){$this->size+=strlen($hb);fwrite($this->handler,$hb);}function +send(){fseek($this->handler,0);fpassthru($this->handler);fclose($this->handler);}}$bc="'(?:''|[^'\\\\]|\\\\.)*'";$cd="IN|OUT|INOUT";if(isset($_GET["select"])&&($_POST["edit"]||$_POST["clone"])&&!$_POST["save"])$_GET["edit"]=$_GET["select"];if(isset($_GET["callf"]))$_GET["call"]=$_GET["callf"];if(isset($_GET["function"]))$_GET["procedure"]=$_GET["function"];if(isset($_GET["download"])){$a=$_GET["download"];$m=fields($a);header("Content-Type: application/octet-stream");header("Content-Disposition: attachment; filename=".friendly_url("$a-".implode("_",$_GET["where"])).".".friendly_url($_GET["field"]));$N=array(idf_escape($_GET["field"]));$J=$j->select($a,$N,array(where($_GET,$m)),$N);$L=($J?$J->fetch_row():array());echo$j->value($L[0],$m[$_GET["field"]]);exit;}elseif(isset($_GET["table"])){$a=$_GET["table"];$m=fields($a);if(!$m)$k=error();$R=table_status1($a,true);$E=$b->tableName($R);page_header(($m&&is_view($R)?$R['Engine']=='materialized view'?'Materialized view':'View':'Table').": ".($E!=""?$E:h($a)),$k);$b->selectLinks($R);$bb=$R["Comment"];if($bb!="")echo"

    ".'Comment'.": ".h($bb)."\n";if($m)$b->tableStructurePrint($m);if(!is_view($R)){if(support("indexes")){echo"

    ".'Indexes'."

    \n";$w=indexes($a);if($w)$b->tableIndexesPrint($w);echo'

    ".'Foreign keys'."

    \n";$o=foreign_keys($a);if($o){echo"\n","\n";foreach($o +as$E=>$n){echo"","
    ".'Source'."".'Target'."".'ON DELETE'."".'ON UPDATE'."
    ".implode(", ",array_map('h',$n["source"]))."","".($n["db"]!=""?"".h($n["db"]).".":"").($n["ns"]!=""?"".h($n["ns"]).".":"").h($n["table"])."","(".implode(", ",array_map('h',$n["target"])).")","".h($n["on_delete"])."\n","".h($n["on_update"])."\n",''.'Alter'.'';}echo"
    \n";}echo'

    ".'Triggers'."

    \n";$Ug=triggers($a);if($Ug){echo"\n";foreach($Ug +as$z=>$X)echo"
    ".h($X[0])."".h($X[1])."".h($z)."".'Alter'."\n";echo"
    \n";}echo'
    + +qs(\'#schema\').onselectstart = function () { return false; }; +var tablePos = {',implode(",",$tg)."\n",'}; +var em = qs(\'#schema\').offsetHeight / ',$Mg,'; +document.onmousemove = schemaMousemove; +document.onmouseup = partialArg(schemaMouseup, \'',js_escape(DB),'\'); + +';foreach($Hf +as$E=>$Q){echo"
    ",''.h($E)."",script("qsl('div').onmousedown = schemaMousedown;");foreach($Q["fields"]as$l){$X=''.h($l["field"]).'';echo"
    ".($l["primary"]?"$X":$X);}foreach((array)$Q["references"]as$zg=>$uf){foreach($uf +as$vd=>$qf){$wd=$vd-$sg[$E][1];$t=0;foreach($qf[0]as$Yf)echo"\n
    ";}}foreach((array)$tf[$E]as$zg=>$uf){foreach($uf +as$vd=>$d){$wd=$vd-$sg[$E][1];$t=0;foreach($d +as$yg)echo"\n
    ";}}echo"\n
    \n";}foreach($Hf +as$E=>$Q){foreach((array)$Q["references"]as$zg=>$uf){foreach($uf +as$vd=>$qf){$Sd=$Mg;$Kd=-10;foreach($qf[0]as$z=>$Yf){$Xe=$Q["pos"][0]+$Q["fields"][$Yf]["pos"];$Ye=$Hf[$zg]["pos"][0]+$Hf[$zg]["fields"][$qf[1][$z]]["pos"];$Sd=min($Sd,$Xe,$Ye);$Kd=max($Kd,$Xe,$Ye);}echo"
    \n";}}}echo'
    +
    + +';$wb=array('','USE','DROP+CREATE','CREATE');$ug=array('','DROP+CREATE','CREATE');$tb=array('','TRUNCATE+INSERT','INSERT');if($y=="sql")$tb[]='INSERT+UPDATE';parse_str($_COOKIE["adminer_export"],$L);if(!$L)$L=array("output"=>"text","format"=>"sql","db_style"=>(DB!=""?"":"CREATE"),"table_style"=>"DROP+CREATE","data_style"=>"INSERT");if(!isset($L["events"])){$L["routines"]=$L["events"]=($_GET["dump"]=="");$L["triggers"]=$L["table_style"];}echo"
    ".'Output'."".html_select("output",$b->dumpOutput(),$L["output"],0)."\n";echo"
    ".'Format'."".html_select("format",$b->dumpFormat(),$L["format"],0)."\n";echo($y=="sqlite"?"":"
    ".'Database'."".html_select('db_style',$wb,$L["db_style"]).(support("routine")?checkbox("routines",1,$L["routines"],'Routines'):"").(support("event")?checkbox("events",1,$L["events"],'Events'):"")),"
    ".'Tables'."".html_select('table_style',$ug,$L["table_style"]).checkbox("auto_increment",1,$L["auto_increment"],'Auto Increment').(support("trigger")?checkbox("triggers",1,$L["triggers"],'Triggers'):""),"
    ".'Data'."".html_select('data_style',$tb,$L["data_style"]),'
    +

    + + + +',script("qsl('table').onclick = dumpClick;");$bf=array();if(DB!=""){$Na=($a!=""?"":" checked");echo"","\n";$th="";$vg=tables_list();foreach($vg +as$E=>$U){$af=preg_replace('~_.*~','',$E);$Na=($a==""||$a==(substr($a,-1)=="%"?"$af%":$E));$df="\n";$h=$b->databases();if($h){foreach($h +as$i){if(!information_schema($i)){$af=preg_replace('~_.*~','',$i);echo"
    ".script("qs('#check-tables').onclick = partial(formCheck, /^tables\\[/);",""),"".script("qs('#check-data').onclick = partial(formCheck, /^data\\[/);",""),"
    ".checkbox("tables[]",$E,$Na,$E,"","block");if($U!==null&&!preg_match('~table~i',$U))$th.="$df\n";else +echo"$df\n";$bf[$af]++;}echo$th;if($vg)echo +script("ajaxSetHtml('".js_escape(ME)."script=db');");}else{echo"
    ","",script("qs('#check-databases').onclick = partial(formCheck, /^databases\\[/);",""),"
    ".checkbox("databases[]",$i,$a==""||$a=="$af%",$i,"","block")."\n";$bf[$af]++;}}}else +echo"
    ";}echo'
    +

    +';$xc=true;foreach($bf +as$z=>$X){if($z!=""&&$X>1){echo($xc?"

    ":" ")."".h($z)."";$xc=false;}}}elseif(isset($_GET["privileges"])){page_header('Privileges');echo'

    \n";hidden_fields_get();echo"\n",($r?"":"\n"),"\n","\n";while($L=$J->fetch_assoc())echo'
    ".'Username'."".'Server'."
    '.h($L["User"])."".h($L["Host"]).''.'Edit'."\n";if(!$r||DB!="")echo"\n";echo"
    \n","

    \n";}elseif(isset($_GET["sql"])){if(!$k&&$_POST["export"]){dump_headers("sql");$b->dumpTable("","");$b->dumpData("","table",$_POST["query"]);exit;}restart_session();$Sc=&get_session("queries");$Rc=&$Sc[DB];if(!$k&&$_POST["clear"]){$Rc=array();redirect(remove_from_uri("history"));}page_header((isset($_GET["import"])?'Import':'SQL command'),$k);if(!$k&&$_POST){$p=false;if(!isset($_GET["import"]))$I=$_POST["query"];elseif($_POST["webfile"]){$bg=$b->importServerPath();$p=@fopen((file_exists($bg)?$bg:"compress.zlib://$bg.gz"),"rb");$I=($p?fread($p,1e6):false);}else$I=get_file("sql_file",true);if(is_string($I)){if(function_exists('memory_get_usage'))@ini_set("memory_limit",max(ini_bytes("memory_limit"),2*strlen($I)+memory_get_usage()+8e6));if($I!=""&&strlen($I)<1e6){$H=$I.(preg_match("~;[ \t\r\n]*\$~",$I)?"":";");if(!$Rc||reset(end($Rc))!=$H){restart_session();$Rc[]=array($H,time());set_session("queries",$Sc);stop_session();}}$Zf="(?:\\s|/\\*[\s\S]*?\\*/|(?:#|-- )[^\n]*\n?|--\r?\n)";$Cb=";";$he=0;$Yb=true;$f=connect();if(is_object($f)&&DB!=""){$f->select_db(DB);if($_GET["ns"]!="")set_schema($_GET["ns"],$f);}$ab=0;$dc=array();$Ke='[\'"'.($y=="sql"?'`#':($y=="sqlite"?'`[':($y=="mssql"?'[':''))).']|/\*|-- |$'.($y=="pgsql"?'|\$[^$]*\$':'');$Ng=microtime(true);parse_str($_COOKIE["adminer_export"],$la);$Pb=$b->dumpFormat();unset($Pb["sql"]);while($I!=""){if(!$he&&preg_match("~^$Zf*+DELIMITER\\s+(\\S+)~i",$I,$C)){$Cb=$C[1];$I=substr($I,strlen($C[0]));}else{preg_match('('.preg_quote($Cb)."\\s*|$Ke)",$I,$C,PREG_OFFSET_CAPTURE,$he);list($Cc,$We)=$C[0];if(!$Cc&&$p&&!feof($p))$I.=fread($p,1e5);else{if(!$Cc&&rtrim($I)=="")break;$he=$We+strlen($Cc);if($Cc&&rtrim($Cc)!=$Cb){while(preg_match('('.($Cc=='/*'?'\*/':($Cc=='['?']':(preg_match('~^-- |^#~',$Cc)?"\n":preg_quote($Cc)."|\\\\."))).'|$)s',$I,$C,PREG_OFFSET_CAPTURE,$he)){$Ff=$C[0][0];if(!$Ff&&$p&&!feof($p))$I.=fread($p,1e5);else{$he=$C[0][1]+strlen($Ff);if($Ff[0]!="\\")break;}}}else{$Yb=false;$H=substr($I,0,$We);$ab++;$df="
    ".$b->sqlCommandQuery($H)."
    \n";if($y=="sqlite"&&preg_match("~^$Zf*+ATTACH\\b~i",$H,$C)){echo$df,"

    ".'ATTACH queries are not supported.'."\n";$dc[]=" $ab";if($_POST["error_stops"])break;}else{if(!$_POST["only_errors"]){echo$df;ob_flush();flush();}$eg=microtime(true);if($e->multi_query($H)&&is_object($f)&&preg_match("~^$Zf*+USE\\b~i",$H))$f->query($H);do{$J=$e->store_result();if($e->error){echo($_POST["only_errors"]?$df:""),"

    ".'Error in query'.($e->errno?" ($e->errno)":"").": ".error()."\n";$dc[]=" $ab";if($_POST["error_stops"])break +2;}else{$Dg=" (".format_time($eg).")".(strlen($H)<1000?" ".'Edit'."":"");$na=$e->affected_rows;$wh=($_POST["only_errors"]?"":$j->warnings());$xh="warnings-$ab";if($wh)$Dg.=", ".'Warnings'."".script("qsl('a').onclick = partial(toggle, '$xh');","");$lc=null;$mc="explain-$ab";if(is_object($J)){$_=$_POST["limit"];$ze=select($J,$f,array(),$_);if(!$_POST["only_errors"]){echo"

    \n";$ee=$J->num_rows;echo"

    ".($ee?($_&&$ee>$_?sprintf('%d / ',$_):"").lang(array('%d row','%d rows'),$ee):""),$Dg;if($f&&preg_match("~^($Zf|\\()*+SELECT\\b~i",$H)&&($lc=explain($f,$H)))echo", Explain".script("qsl('a').onclick = partial(toggle, '$mc');","");$u="export-$ab";echo", ".'Export'."".script("qsl('a').onclick = partial(toggle, '$u');","")."\n"."

    \n";}}else{if(preg_match("~^$Zf*+(CREATE|DROP|ALTER)$Zf++(DATABASE|SCHEMA)\\b~i",$H)){restart_session();set_session("dbs",null);stop_session();}if(!$_POST["only_errors"])echo"

    ".lang(array('Query executed OK, %d row affected.','Query executed OK, %d rows affected.'),$na)."$Dg\n";}echo($wh?"

    \n":"");if($lc){echo"\n";}}$eg=microtime(true);}while($e->next_result());}$I=substr($I,$he);$he=0;}}}}if($Yb)echo"

    ".'No commands to execute.'."\n";elseif($_POST["only_errors"]){echo"

    ".lang(array('%d query executed OK.','%d queries executed OK.'),$ab-count($dc))," (".format_time($Ng).")\n";}elseif($dc&&$ab>1)echo"

    ".'Error in query'.": ".implode("",$dc)."\n";}else +echo"

    ".upload_error($I)."\n";}echo' +

    +';$jc="";if(!isset($_GET["import"])){$H=$_GET["sql"];if($_POST)$H=$_POST["query"];elseif($_GET["history"]=="all")$H=$Rc;elseif($_GET["history"]!="")$H=$Rc[$_GET["history"]][0];echo"

    ";textarea("query",$H,20);echo +script(($_POST?"":"qs('textarea').focus();\n")."qs('#form').onsubmit = partial(sqlSubmit, qs('#form'), '".js_escape(remove_from_uri("sql|limit|error_stops|only_errors|history"))."');"),"

    $jc\n",'Limit rows'.": \n";}else{echo"

    ".'File upload'."
    ";$Kc=(extension_loaded("zlib")?"[.gz]":"");echo(ini_bool("file_uploads")?"SQL$Kc (< ".ini_get("upload_max_filesize")."B): \n$jc":'File uploads are disabled.'),"
    \n";$Yc=$b->importServerPath();if($Yc){echo"
    ".'From server'."
    ",sprintf('Webserver file %s',"".h($Yc)."$Kc"),' ',"
    \n";}echo"

    ";}echo +checkbox("error_stops",1,($_POST?$_POST["error_stops"]:isset($_GET["import"])||$_GET["error_stops"]),'Stop on error')."\n",checkbox("only_errors",1,($_POST?$_POST["only_errors"]:isset($_GET["import"])||$_GET["only_errors"]),'Show only errors')."\n","\n";if(!isset($_GET["import"])&&$Rc){print_fieldset("history",'History',$_GET["history"]!="");for($X=end($Rc);$X;$X=prev($Rc)){$z=key($Rc);list($H,$Dg,$Tb)=$X;echo''.'Edit'.""." ".@date("H:i:s",$Dg).""." ".shorten_utf8(ltrim(str_replace("\n"," ",str_replace("\r","",preg_replace('~^(#|-- ).*~m','',$H)))),80,"").($Tb?" ($Tb)":"")."
    \n";}echo"\n","".'Edit all'."\n","\n";}echo'

    +';}elseif(isset($_GET["edit"])){$a=$_GET["edit"];$m=fields($a);$Z=(isset($_GET["select"])?($_POST["check"]&&count($_POST["check"])==1?where_check($_POST["check"][0],$m):""):where($_GET,$m));$fh=(isset($_GET["select"])?$_POST["edit"]:$Z);foreach($m +as$E=>$l){if(!isset($l["privileges"][$fh?"update":"insert"])||$b->fieldName($l)==""||$l["generated"])unset($m[$E]);}if($_POST&&!$k&&!isset($_GET["select"])){$B=$_POST["referer"];if($_POST["insert"])$B=($fh?null:$_SERVER["REQUEST_URI"]);elseif(!preg_match('~^.+&select=.+$~',$B))$B=ME."select=".urlencode($a);$w=indexes($a);$ah=unique_array($_GET["where"],$w);$mf="\nWHERE $Z";if(isset($_POST["delete"]))queries_redirect($B,'Item has been deleted.',$j->delete($a,$mf,!$ah));else{$P=array();foreach($m +as$E=>$l){$X=process_input($l);if($X!==false&&$X!==null)$P[idf_escape($E)]=$X;}if($fh){if(!$P)redirect($B);queries_redirect($B,'Item has been updated.',$j->update($a,$P,$mf,!$ah));if(is_ajax()){page_headers();page_messages($k);exit;}}else{$J=$j->insert($a,$P);$ud=($J?last_id():0);queries_redirect($B,sprintf('Item%s has been inserted.',($ud?" $ud":"")),$J);}}}$L=null;if($_POST["save"])$L=(array)$_POST["fields"];elseif($Z){$N=array();foreach($m +as$E=>$l){if(isset($l["privileges"]["select"])){$ua=convert_field($l);if($_POST["clone"]&&$l["auto_increment"])$ua="''";if($y=="sql"&&preg_match("~enum|set~",$l["type"]))$ua="1*".idf_escape($E);$N[]=($ua?"$ua AS ":"").idf_escape($E);}}$L=array();if(!support("table"))$N=array("*");if($N){$J=$j->select($a,$N,array($Z),$N,array(),(isset($_GET["select"])?2:1));if(!$J)$k=error();else{$L=$J->fetch_assoc();if(!$L)$L=false;}if(isset($_GET["select"])&&(!$L||$J->fetch_assoc()))$L=null;}}if(!support("table")&&!$m){if(!$Z){$J=$j->select($a,array("*"),$Z,array("*"));$L=($J?$J->fetch_assoc():false);if(!$L)$L=array($j->primary=>"");}if($L){foreach($L +as$z=>$X){if(!$Z)$L[$z]=null;$m[$z]=array("field"=>$z,"null"=>($z!=$j->primary),"auto_increment"=>($z==$j->primary));}}}edit_form($a,$m,$L,$fh);}elseif(isset($_GET["create"])){$a=$_GET["create"];$Le=array();foreach(array('HASH','LINEAR HASH','KEY','LINEAR KEY','RANGE','LIST')as$z)$Le[$z]=$z;$sf=referencable_primary($a);$o=array();foreach($sf +as$rg=>$l)$o[str_replace("`","``",$rg)."`".str_replace("`","``",$l["field"])]=$rg;$Be=array();$R=array();if($a!=""){$Be=fields($a);$R=table_status($a);if(!$R)$k='No tables.';}$L=$_POST;$L["fields"]=(array)$L["fields"];if($L["auto_increment_col"])$L["fields"][$L["auto_increment_col"]]["auto_increment"]=true;if($_POST)set_adminer_settings(array("comments"=>$_POST["comments"],"defaults"=>$_POST["defaults"]));if($_POST&&!process_fields($L["fields"])&&!$k){if($_POST["drop"])queries_redirect(substr(ME,0,-1),'Table has been dropped.',drop_tables(array($a)));else{$m=array();$ra=array();$jh=false;$_c=array();$Ae=reset($Be);$pa=" FIRST";foreach($L["fields"]as$z=>$l){$n=$o[$l["type"]];$Vg=($n!==null?$sf[$n]:$l);if($l["field"]!=""){if(!$l["has_default"])$l["default"]=null;if($z==$L["auto_increment_col"])$l["auto_increment"]=true;$if=process_field($l,$Vg);$ra[]=array($l["orig"],$if,$pa);if(!$Ae||$if!=process_field($Ae,$Ae)){$m[]=array($l["orig"],$if,$pa);if($l["orig"]!=""||$pa)$jh=true;}if($n!==null)$_c[idf_escape($l["field"])]=($a!=""&&$y!="sqlite"?"ADD":" ").format_foreign_key(array('table'=>$o[$l["type"]],'source'=>array($l["field"]),'target'=>array($Vg["field"]),'on_delete'=>$l["on_delete"],));$pa=" AFTER ".idf_escape($l["field"]);}elseif($l["orig"]!=""){$jh=true;$m[]=array($l["orig"]);}if($l["orig"]!=""){$Ae=next($Be);if(!$Ae)$pa="";}}$Ne="";if($Le[$L["partition_by"]]){$Oe=array();if($L["partition_by"]=='RANGE'||$L["partition_by"]=='LIST'){foreach(array_filter($L["partition_names"])as$z=>$X){$Y=$L["partition_values"][$z];$Oe[]="\n PARTITION ".idf_escape($X)." VALUES ".($L["partition_by"]=='RANGE'?"LESS THAN":"IN").($Y!=""?" ($Y)":" MAXVALUE");}}$Ne.="\nPARTITION BY $L[partition_by]($L[partition])".($Oe?" (".implode(",",$Oe)."\n)":($L["partitions"]?" PARTITIONS ".(+$L["partitions"]):""));}elseif(support("partitioning")&&preg_match("~partitioned~",$R["Create_options"]))$Ne.="\nREMOVE PARTITIONING";$D='Table has been altered.';if($a==""){cookie("adminer_engine",$L["Engine"]);$D='Table has been created.';}$E=trim($L["name"]);queries_redirect(ME.(support("table")?"table=":"select=").urlencode($E),$D,alter_table($a,$E,($y=="sqlite"&&($jh||$_c)?$ra:$m),$_c,($L["Comment"]!=$R["Comment"]?$L["Comment"]:null),($L["Engine"]&&$L["Engine"]!=$R["Engine"]?$L["Engine"]:""),($L["Collation"]&&$L["Collation"]!=$R["Collation"]?$L["Collation"]:""),($L["Auto_increment"]!=""?number($L["Auto_increment"]):""),$Ne));}}page_header(($a!=""?'Alter table':'Create table'),$k,array("table"=>$a),h($a));if(!$_POST){$L=array("Engine"=>$_COOKIE["adminer_engine"],"fields"=>array(array("field"=>"","type"=>(isset($Xg["int"])?"int":(isset($Xg["integer"])?"integer":"")),"on_update"=>"")),"partition_names"=>array(""),);if($a!=""){$L=$R;$L["name"]=$a;$L["fields"]=array();if(!$_GET["auto_increment"])$L["Auto_increment"]="";foreach($Be +as$l){$l["has_default"]=isset($l["default"]);$L["fields"][]=$l;}if(support("partitioning")){$Ec="FROM information_schema.PARTITIONS WHERE TABLE_SCHEMA = ".q(DB)." AND TABLE_NAME = ".q($a);$J=$e->query("SELECT PARTITION_METHOD, PARTITION_ORDINAL_POSITION, PARTITION_EXPRESSION $Ec ORDER BY PARTITION_ORDINAL_POSITION DESC LIMIT 1");list($L["partition_by"],$L["partitions"],$L["partition"])=$J->fetch_row();$Oe=get_key_vals("SELECT PARTITION_NAME, PARTITION_DESCRIPTION $Ec AND PARTITION_NAME != '' ORDER BY PARTITION_ORDINAL_POSITION");$Oe[""]="";$L["partition_names"]=array_keys($Oe);$L["partition_values"]=array_values($Oe);}}}$Xa=collations();$ac=engines();foreach($ac +as$Zb){if(!strcasecmp($Zb,$L["Engine"])){$L["Engine"]=$Zb;break;}}echo' +
    +

    +';if(support("columns")||$a==""){echo'Table name: +';if($a==""&&!$_POST)echo +script("focus(qs('#form')['name']);");echo($ac?"".on_help("getTarget(event).value",1).script("qsl('select').onchange = helpClose;"):""),' ',($Xa&&!preg_match("~sqlite|mssql~",$y)?html_select("Collation",array(""=>"(".'collation'.")")+$Xa,$L["Collation"]):""),' +';}echo' +';if(support("columns")){echo'

    + +';edit_fields($L["fields"],$Xa,"TABLE",$o);echo'
    +',script("editFields();"),'
    +

    +Auto Increment: +',checkbox("defaults",1,($_POST?$_POST["defaults"]:adminer_setting("defaults")),'Default values',"columnShow(this.checked, 5)","jsonly"),(support("comment")?checkbox("comments",1,($_POST?$_POST["comments"]:adminer_setting("comments")),'Comment',"editingCommentsClick(this, true);","jsonly").' ':''),'

    + +';}echo' +';if($a!=""){echo'',confirm(sprintf('Drop %s?',$a));}if(support("partitioning")){$Me=preg_match('~RANGE|LIST~',$L["partition_by"]);print_fieldset("partition",'Partition by',$L["partition_by"]);echo'

    +',"".on_help("getTarget(event).value.replace(/./, 'PARTITION BY \$&')",1).script("qsl('select').onchange = partitionByChange;"),'() +Partitions: + + +';foreach($L["partition_names"]as$z=>$X){echo'',' + +';}echo' +

    +';}elseif(isset($_GET["indexes"])){$a=$_GET["indexes"];$ad=array("PRIMARY","UNIQUE","INDEX");$R=table_status($a,true);if(preg_match('~MyISAM|M?aria'.(min_version(5.6,'10.0.5')?'|InnoDB':'').'~i',$R["Engine"]))$ad[]="FULLTEXT";if(preg_match('~MyISAM|M?aria'.(min_version(5.7,'10.2.2')?'|InnoDB':'').'~i',$R["Engine"]))$ad[]="SPATIAL";$w=indexes($a);$cf=array();if($y=="mongo"){$cf=$w["_id_"];unset($ad[0]);unset($w["_id_"]);}$L=$_POST;if($_POST&&!$k&&!$_POST["add"]&&!$_POST["drop_col"]){$sa=array();foreach($L["indexes"]as$v){$E=$v["name"];if(in_array($v["type"],$ad)){$d=array();$_d=array();$Eb=array();$P=array();ksort($v["columns"]);foreach($v["columns"]as$z=>$c){if($c!=""){$zd=$v["lengths"][$z];$Db=$v["descs"][$z];$P[]=idf_escape($c).($zd?"(".(+$zd).")":"").($Db?" DESC":"");$d[]=$c;$_d[]=($zd?$zd:null);$Eb[]=$Db;}}if($d){$kc=$w[$E];if($kc){ksort($kc["columns"]);ksort($kc["lengths"]);ksort($kc["descs"]);if($v["type"]==$kc["type"]&&array_values($kc["columns"])===$d&&(!$kc["lengths"]||array_values($kc["lengths"])===$_d)&&array_values($kc["descs"])===$Eb){unset($w[$E]);continue;}}$sa[]=array($v["type"],$E,$P);}}}foreach($w +as$E=>$kc)$sa[]=array($kc["type"],$E,"DROP");if(!$sa)redirect(ME."table=".urlencode($a));queries_redirect(ME."table=".urlencode($a),'Indexes have been altered.',alter_indexes($a,$sa));}page_header('Indexes',$k,array("table"=>$a),h($a));$m=array_keys(fields($a));if($_POST["add"]){foreach($L["indexes"]as$z=>$v){if($v["columns"][count($v["columns"])]!="")$L["indexes"][$z]["columns"][]="";}$v=end($L["indexes"]);if($v["type"]||array_filter($v["columns"],'strlen'))$L["indexes"][]=array("columns"=>array(1=>""));}if(!$L){foreach($w +as$z=>$v){$w[$z]["name"]=$z;$w[$z]["columns"][]="";}$w[]=array("columns"=>array(1=>""));$L["indexes"]=$w;}echo' +
    +
    + + + +';if($cf){echo"
    Index Type +Column (length) +Name + +
    PRIMARY";foreach($cf["columns"]as$z=>$c){echo +select_input(" disabled",$m,$c)," ";}echo"\n";}$x=1;foreach($L["indexes"]as$v){if(!$_POST["drop_col"]||$x!=key($_POST["drop_col"])){echo"
    ".html_select("indexes[$x][type]",array(-1=>"")+$ad,$v["type"],($x==count($L["indexes"])?"indexesAddRow.call(this);":1),"label-type"),"";ksort($v["columns"]);$t=1;foreach($v["columns"]as$z=>$c){echo"".select_input(" name='indexes[$x][columns][$t]' title='".'Column'."'",($m?array_combine($m,$m):$m),$c,"partial(".($t==count($v["columns"])?"indexesAddColumn":"indexesChangeColumn").", '".js_escape($y=="sql"?"":$_GET["indexes"]."_")."')"),($y=="sql"||$y=="mssql"?"":""),(support("descidx")?checkbox("indexes[$x][descs][$t]",1,$v["descs"][$z],'descending'):"")," ";$t++;}echo"\n","".script("qsl('input').onclick = partial(editingRemoveRow, 'indexes\$1[type]');");}$x++;}echo'
    +
    +

    + + +

    +';}elseif(isset($_GET["database"])){$L=$_POST;if($_POST&&!$k&&!isset($_POST["add_x"])){$E=trim($L["name"]);if($_POST["drop"]){$_GET["db"]="";queries_redirect(remove_from_uri("db|database"),'Database has been dropped.',drop_databases(array(DB)));}elseif(DB!==$E){if(DB!=""){$_GET["db"]=$E;queries_redirect(preg_replace('~\bdb=[^&]*&~','',ME)."db=".urlencode($E),'Database has been renamed.',rename_database($E,$L["collation"]));}else{$h=explode("\n",str_replace("\r","",$E));$lg=true;$td="";foreach($h +as$i){if(count($h)==1||$i!=""){if(!create_database($i,$L["collation"]))$lg=false;$td=$i;}}restart_session();set_session("dbs",null);queries_redirect(ME."db=".urlencode($td),'Database has been created.',$lg);}}else{if(!$L["collation"])redirect(substr(ME,0,-1));query_redirect("ALTER DATABASE ".idf_escape($E).(preg_match('~^[a-z0-9_]+$~i',$L["collation"])?" COLLATE $L[collation]":""),substr(ME,0,-1),'Database has been altered.');}}page_header(DB!=""?'Alter database':'Create database',$k,array(),h(DB));$Xa=collations();$E=DB;if($_POST)$E=$L["name"];elseif(DB!="")$L["collation"]=db_collation(DB,$Xa);elseif($y=="sql"){foreach(get_vals("SHOW GRANTS")as$r){if(preg_match('~ ON (`(([^\\\\`]|``|\\\\.)*)%`\.\*)?~',$r,$C)&&$C[1]){$E=stripcslashes(idf_unescape("`$C[2]`"));break;}}}echo' +
    +

    +',($_POST["add_x"]||strpos($E,"\n")?'
    ':'')."\n".($Xa?html_select("collation",array(""=>"(".'collation'.")")+$Xa,$L["collation"]).doc_link(array('sql'=>"charset-charsets.html",'mariadb'=>"supported-character-sets-and-collations/",)):""),script("focus(qs('#name'));"),' +';if(DB!="")echo"".confirm(sprintf('Drop %s?',DB))."\n";elseif(!$_POST["add_x"]&&$_GET["db"]=="")echo"\n";echo' +

    +';}elseif(isset($_GET["call"])){$da=($_GET["name"]?$_GET["name"]:$_GET["call"]);page_header('Call'.": ".h($da),$k);$Cf=routine($_GET["call"],(isset($_GET["callf"])?"FUNCTION":"PROCEDURE"));$Zc=array();$Ee=array();foreach($Cf["fields"]as$t=>$l){if(substr($l["inout"],-3)=="OUT")$Ee[$t]="@".idf_escape($l["field"])." AS ".idf_escape($l["field"]);if(!$l["inout"]||substr($l["inout"],0,2)=="IN")$Zc[]=$t;}if(!$k&&$_POST){$Ja=array();foreach($Cf["fields"]as$z=>$l){if(in_array($z,$Zc)){$X=process_input($l);if($X===false)$X="''";if(isset($Ee[$z]))$e->query("SET @".idf_escape($l["field"])." = $X");}$Ja[]=(isset($Ee[$z])?"@".idf_escape($l["field"]):$X);}$I=(isset($_GET["callf"])?"SELECT":"CALL")." ".table($da)."(".implode(", ",$Ja).")";$eg=microtime(true);$J=$e->multi_query($I);$na=$e->affected_rows;echo$b->selectQuery($I,$eg,!$J);if(!$J)echo"

    ".error()."\n";else{$f=connect();if(is_object($f))$f->select_db(DB);do{$J=$e->store_result();if(is_object($J))select($J,$f);else +echo"

    ".lang(array('Routine has been called, %d row affected.','Routine has been called, %d rows affected.'),$na)." ".@date("H:i:s")."\n";}while($e->next_result());if($Ee)select($e->query("SELECT ".implode(", ",$Ee)));}}echo' +

    +';if($Zc){echo"\n";foreach($Zc +as$z){$l=$Cf["fields"][$z];$E=$l["field"];echo"
    ".$b->fieldName($l);$Y=$_POST["fields"][$E];if($Y!=""){if($l["type"]=="enum")$Y=+$Y;if($l["type"]=="set")$Y=array_sum($Y);}input($l,$Y,(string)$_POST["function"][$E]);echo"\n";}echo"
    \n";}echo'

    + + +

    +';}elseif(isset($_GET["foreign"])){$a=$_GET["foreign"];$E=$_GET["name"];$L=$_POST;if($_POST&&!$k&&!$_POST["add"]&&!$_POST["change"]&&!$_POST["change-js"]){$D=($_POST["drop"]?'Foreign key has been dropped.':($E!=""?'Foreign key has been altered.':'Foreign key has been created.'));$B=ME."table=".urlencode($a);if(!$_POST["drop"]){$L["source"]=array_filter($L["source"],'strlen');ksort($L["source"]);$yg=array();foreach($L["source"]as$z=>$X)$yg[$z]=$L["target"][$z];$L["target"]=$yg;}if($y=="sqlite")queries_redirect($B,$D,recreate_table($a,$a,array(),array(),array(" $E"=>($_POST["drop"]?"":" ".format_foreign_key($L)))));else{$sa="ALTER TABLE ".table($a);$Lb="\nDROP ".($y=="sql"?"FOREIGN KEY ":"CONSTRAINT ").idf_escape($E);if($_POST["drop"])query_redirect($sa.$Lb,$B,$D);else{query_redirect($sa.($E!=""?"$Lb,":"")."\nADD".format_foreign_key($L),$B,$D);$k='Source and target columns must have the same data type, there must be an index on the target columns and referenced data must exist.'."
    $k";}}}page_header('Foreign key',$k,array("table"=>$a),h($a));if($_POST){ksort($L["source"]);if($_POST["add"])$L["source"][]="";elseif($_POST["change"]||$_POST["change-js"])$L["target"]=array();}elseif($E!=""){$o=foreign_keys($a);$L=$o[$E];$L["source"][]="";}else{$L["table"]=$a;$L["source"]=array("");}echo' +
    +';$Yf=array_keys(fields($a));if($L["db"]!="")$e->select_db($L["db"]);if($L["ns"]!="")set_schema($L["ns"]);$rf=array_keys(array_filter(table_status('',true),'fk_support'));$yg=array_keys(fields(in_array($L["table"],$rf)?$L["table"]:reset($rf)));$pe="this.form['change-js'].value = '1'; this.form.submit();";echo"

    ".'Target table'.": ".html_select("table",$rf,$L["table"],$pe)."\n";if($y=="pgsql")echo'Schema'.": ".html_select("ns",$b->schemas(),$L["ns"]!=""?$L["ns"]:$_GET["ns"],$pe);elseif($y!="sqlite"){$xb=array();foreach($b->databases()as$i){if(!information_schema($i))$xb[]=$i;}echo'DB'.": ".html_select("db",$xb,$L["db"]!=""?$L["db"]:$_GET["db"],$pe);}echo' +

    + + +';$x=0;foreach($L["source"]as$z=>$X){echo"","
    SourceTarget
    ".html_select("source[".(+$z)."]",array(-1=>"")+$Yf,$X,($x==count($L["source"])-1?"foreignAddRow.call(this);":1),"label-source"),"".html_select("target[".(+$z)."]",$yg,$L["target"][$z],1,"label-target");$x++;}echo'
    +

    +ON DELETE: ',html_select("on_delete",array(-1=>"")+explode("|",$oe),$L["on_delete"]),' ON UPDATE: ',html_select("on_update",array(-1=>"")+explode("|",$oe),$L["on_update"]),doc_link(array('sql'=>"innodb-foreign-key-constraints.html",'mariadb'=>"foreign-keys/",)),'

    + +

    +';if($E!=""){echo'',confirm(sprintf('Drop %s?',$E));}echo' +

    +';}elseif(isset($_GET["view"])){$a=$_GET["view"];$L=$_POST;$Ce="VIEW";if($y=="pgsql"&&$a!=""){$fg=table_status($a);$Ce=strtoupper($fg["Engine"]);}if($_POST&&!$k){$E=trim($L["name"]);$ua=" AS\n$L[select]";$B=ME."table=".urlencode($E);$D='View has been altered.';$U=($_POST["materialized"]?"MATERIALIZED VIEW":"VIEW");if(!$_POST["drop"]&&$a==$E&&$y!="sqlite"&&$U=="VIEW"&&$Ce=="VIEW")query_redirect(($y=="mssql"?"ALTER":"CREATE OR REPLACE")." VIEW ".table($E).$ua,$B,$D);else{$_g=$E."_adminer_".uniqid();drop_create("DROP $Ce ".table($a),"CREATE $U ".table($E).$ua,"DROP $U ".table($E),"CREATE $U ".table($_g).$ua,"DROP $U ".table($_g),($_POST["drop"]?substr(ME,0,-1):$B),'View has been dropped.',$D,'View has been created.',$a,$E);}}if(!$_POST&&$a!=""){$L=view($a);$L["name"]=$a;$L["materialized"]=($Ce!="VIEW");if(!$k)$k=error();}page_header(($a!=""?'Alter view':'Create view'),$k,array("table"=>$a),h($a));echo' +
    +

    Name: +',(support("materializedview")?" ".checkbox("materialized",1,$L["materialized"],'Materialized view'):""),'

    ';textarea("select",$L["select"]);echo'

    + +';if($a!=""){echo'',confirm(sprintf('Drop %s?',$a));}echo' +

    +';}elseif(isset($_GET["event"])){$aa=$_GET["event"];$fd=array("YEAR","QUARTER","MONTH","DAY","HOUR","MINUTE","WEEK","SECOND","YEAR_MONTH","DAY_HOUR","DAY_MINUTE","DAY_SECOND","HOUR_MINUTE","HOUR_SECOND","MINUTE_SECOND");$gg=array("ENABLED"=>"ENABLE","DISABLED"=>"DISABLE","SLAVESIDE_DISABLED"=>"DISABLE ON SLAVE");$L=$_POST;if($_POST&&!$k){if($_POST["drop"])query_redirect("DROP EVENT ".idf_escape($aa),substr(ME,0,-1),'Event has been dropped.');elseif(in_array($L["INTERVAL_FIELD"],$fd)&&isset($gg[$L["STATUS"]])){$Gf="\nON SCHEDULE ".($L["INTERVAL_VALUE"]?"EVERY ".q($L["INTERVAL_VALUE"])." $L[INTERVAL_FIELD]".($L["STARTS"]?" STARTS ".q($L["STARTS"]):"").($L["ENDS"]?" ENDS ".q($L["ENDS"]):""):"AT ".q($L["STARTS"]))." ON COMPLETION".($L["ON_COMPLETION"]?"":" NOT")." PRESERVE";queries_redirect(substr(ME,0,-1),($aa!=""?'Event has been altered.':'Event has been created.'),queries(($aa!=""?"ALTER EVENT ".idf_escape($aa).$Gf.($aa!=$L["EVENT_NAME"]?"\nRENAME TO ".idf_escape($L["EVENT_NAME"]):""):"CREATE EVENT ".idf_escape($L["EVENT_NAME"]).$Gf)."\n".$gg[$L["STATUS"]]." COMMENT ".q($L["EVENT_COMMENT"]).rtrim(" DO\n$L[EVENT_DEFINITION]",";").";"));}}page_header(($aa!=""?'Alter event'.": ".h($aa):'Create event'),$k);if(!$L&&$aa!=""){$M=get_rows("SELECT * FROM information_schema.EVENTS WHERE EVENT_SCHEMA = ".q(DB)." AND EVENT_NAME = ".q($aa));$L=reset($M);}echo' +
    + +
    Name +
    Start +
    End +
    Every ',html_select("INTERVAL_FIELD",$fd,$L["INTERVAL_FIELD"]),'
    Status',html_select("STATUS",$gg,$L["STATUS"]),'
    Comment +
    ',checkbox("ON_COMPLETION","PRESERVE",$L["ON_COMPLETION"]=="PRESERVE",'On completion preserve'),'
    +

    ';textarea("EVENT_DEFINITION",$L["EVENT_DEFINITION"]);echo'

    + +';if($aa!=""){echo'',confirm(sprintf('Drop %s?',$aa));}echo' +

    +';}elseif(isset($_GET["procedure"])){$da=($_GET["name"]?$_GET["name"]:$_GET["procedure"]);$Cf=(isset($_GET["function"])?"FUNCTION":"PROCEDURE");$L=$_POST;$L["fields"]=(array)$L["fields"];if($_POST&&!process_fields($L["fields"])&&!$k){$_e=routine($_GET["procedure"],$Cf);$_g="$L[name]_adminer_".uniqid();drop_create("DROP $Cf ".routine_id($da,$_e),create_routine($Cf,$L),"DROP $Cf ".routine_id($L["name"],$L),create_routine($Cf,array("name"=>$_g)+$L),"DROP $Cf ".routine_id($_g,$L),substr(ME,0,-1),'Routine has been dropped.','Routine has been altered.','Routine has been created.',$da,$L["name"]);}page_header(($da!=""?(isset($_GET["function"])?'Alter function':'Alter procedure').": ".h($da):(isset($_GET["function"])?'Create function':'Create procedure')),$k);if(!$_POST&&$da!=""){$L=routine($_GET["procedure"],$Cf);$L["name"]=$da;}$Xa=get_vals("SHOW CHARACTER SET");sort($Xa);$Df=routine_languages();echo' +
    +

    Name: +',($Df?'Language'.": ".html_select("language",$Df,$L["language"])."\n":""),' +

    + +';edit_fields($L["fields"],$Xa,$Cf);if(isset($_GET["function"])){echo"
    ".'Return type';edit_type("returns",$L["returns"],$Xa,array(),($y=="pgsql"?array("void","trigger"):array()));}echo'
    +',script("editFields();"),'
    +

    ';textarea("definition",$L["definition"]);echo'

    + +';if($da!=""){echo'',confirm(sprintf('Drop %s?',$da));}echo' +

    +';}elseif(isset($_GET["trigger"])){$a=$_GET["trigger"];$E=$_GET["name"];$Tg=trigger_options();$L=(array)trigger($E,$a)+array("Trigger"=>$a."_bi");if($_POST){if(!$k&&in_array($_POST["Timing"],$Tg["Timing"])&&in_array($_POST["Event"],$Tg["Event"])&&in_array($_POST["Type"],$Tg["Type"])){$ne=" ON ".table($a);$Lb="DROP TRIGGER ".idf_escape($E).($y=="pgsql"?$ne:"");$B=ME."table=".urlencode($a);if($_POST["drop"])query_redirect($Lb,$B,'Trigger has been dropped.');else{if($E!="")queries($Lb);queries_redirect($B,($E!=""?'Trigger has been altered.':'Trigger has been created.'),queries(create_trigger($ne,$_POST)));if($E!="")queries(create_trigger($ne,$L+array("Type"=>reset($Tg["Type"]))));}}$L=$_POST;}page_header(($E!=""?'Alter trigger'.": ".h($E):'Create trigger'),$k,array("table"=>$a));echo' +
    + +
    Time',html_select("Timing",$Tg["Timing"],$L["Timing"],"triggerChange(/^".preg_quote($a,"/")."_[ba][iud]$/, '".js_escape($a)."', this.form);"),'
    Event',html_select("Event",$Tg["Event"],$L["Event"],"this.form['Timing'].onchange();"),(in_array("UPDATE OF",$Tg["Event"])?" ":""),'
    Type',html_select("Type",$Tg["Type"],$L["Type"]),'
    +

    Name: +',script("qs('#form')['Timing'].onchange();"),'

    ';textarea("Statement",$L["Statement"]);echo'

    + +';if($E!=""){echo'',confirm(sprintf('Drop %s?',$E));}echo' +

    +';}elseif(isset($_GET["user"])){$fa=$_GET["user"];$gf=array(""=>array("All privileges"=>""));foreach(get_rows("SHOW PRIVILEGES")as$L){foreach(explode(",",($L["Privilege"]=="Grant option"?"":$L["Context"]))as$ib)$gf[$ib][$L["Privilege"]]=$L["Comment"];}$gf["Server Admin"]+=$gf["File access on server"];$gf["Databases"]["Create routine"]=$gf["Procedures"]["Create routine"];unset($gf["Procedures"]["Create routine"]);$gf["Columns"]=array();foreach(array("Select","Insert","Update","References")as$X)$gf["Columns"][$X]=$gf["Tables"][$X];unset($gf["Server Admin"]["Usage"]);foreach($gf["Tables"]as$z=>$X)unset($gf["Databases"][$z]);$Yd=array();if($_POST){foreach($_POST["objects"]as$z=>$X)$Yd[$X]=(array)$Yd[$X]+(array)$_POST["grants"][$z];}$Gc=array();$le="";if(isset($_GET["host"])&&($J=$e->query("SHOW GRANTS FOR ".q($fa)."@".q($_GET["host"])))){while($L=$J->fetch_row()){if(preg_match('~GRANT (.*) ON (.*) TO ~',$L[0],$C)&&preg_match_all('~ *([^(,]*[^ ,(])( *\([^)]+\))?~',$C[1],$Gd,PREG_SET_ORDER)){foreach($Gd +as$X){if($X[1]!="USAGE")$Gc["$C[2]$X[2]"][$X[1]]=true;if(preg_match('~ WITH GRANT OPTION~',$L[0]))$Gc["$C[2]$X[2]"]["GRANT OPTION"]=true;}}if(preg_match("~ IDENTIFIED BY PASSWORD '([^']+)~",$L[0],$C))$le=$C[1];}}if($_POST&&!$k){$me=(isset($_GET["host"])?q($fa)."@".q($_GET["host"]):"''");if($_POST["drop"])query_redirect("DROP USER $me",ME."privileges=",'User has been dropped.');else{$ae=q($_POST["user"])."@".q($_POST["host"]);$Pe=$_POST["pass"];if($Pe!=''&&!$_POST["hashed"]&&!min_version(8)){$Pe=$e->result("SELECT PASSWORD(".q($Pe).")");$k=!$Pe;}$mb=false;if(!$k){if($me!=$ae){$mb=queries((min_version(5)?"CREATE USER":"GRANT USAGE ON *.* TO")." $ae IDENTIFIED BY ".(min_version(8)?"":"PASSWORD ").q($Pe));$k=!$mb;}elseif($Pe!=$le)queries("SET PASSWORD FOR $ae = ".q($Pe));}if(!$k){$_f=array();foreach($Yd +as$ge=>$r){if(isset($_GET["grant"]))$r=array_filter($r);$r=array_keys($r);if(isset($_GET["grant"]))$_f=array_diff(array_keys(array_filter($Yd[$ge],'strlen')),$r);elseif($me==$ae){$je=array_keys((array)$Gc[$ge]);$_f=array_diff($je,$r);$r=array_diff($r,$je);unset($Gc[$ge]);}if(preg_match('~^(.+)\s*(\(.*\))?$~U',$ge,$C)&&(!grant("REVOKE",$_f,$C[2]," ON $C[1] FROM $ae")||!grant("GRANT",$r,$C[2]," ON $C[1] TO $ae"))){$k=true;break;}}}if(!$k&&isset($_GET["host"])){if($me!=$ae)queries("DROP USER $me");elseif(!isset($_GET["grant"])){foreach($Gc +as$ge=>$_f){if(preg_match('~^(.+)(\(.*\))?$~U',$ge,$C))grant("REVOKE",array_keys($_f),$C[2]," ON $C[1] FROM $ae");}}}queries_redirect(ME."privileges=",(isset($_GET["host"])?'User has been altered.':'User has been created.'),!$k);if($mb)$e->query("DROP USER $ae");}}page_header((isset($_GET["host"])?'Username'.": ".h("$fa@$_GET[host]"):'Create user'),$k,array("privileges"=>array('','Privileges')));if($_POST){$L=$_POST;$Gc=$Yd;}else{$L=$_GET+array("host"=>$e->result("SELECT SUBSTRING_INDEX(CURRENT_USER, '@', -1)"));$L["pass"]=$le;if($le!="")$L["hashed"]=true;$Gc[(DB==""||$Gc?"":idf_escape(addcslashes(DB,"%_\\"))).".*"]=array();}echo'
    + +
    Server +
    Username +
    Password +';if(!$L["hashed"])echo +script("typePassword(qs('#pass'));");echo(min_version(8)?"":checkbox("hashed",1,$L["hashed"],'Hashed',"typePassword(this.form['pass'], this.checked);")),'
    + +';echo"\n","\n";foreach(array(""=>"","Server Admin"=>'Server',"Databases"=>'Database',"Tables"=>'Table',"Columns"=>'Column',"Procedures"=>'Routine',)as$ib=>$Db){foreach((array)$gf[$ib]as$ff=>$bb){echo"$Db'.h($ff);$t=0;foreach($Gc +as$ge=>$r){$E="'grants[$t][".h(strtoupper($ff))."]'";$Y=$r[strtoupper($ff)];if($ib=="Server Admin"&&$ge!=(isset($Gc["*.*"])?"*.*":".*"))echo"
    ".'Privileges'.doc_link(array('sql'=>"grant.html#priv_level"));$t=0;foreach($Gc +as$ge=>$r){echo''.($ge!="*.*"?"":"*.*");$t++;}echo"
    ";elseif(isset($_GET["grant"]))echo"";else{echo"";}$t++;}}}echo"
    \n",'

    + +';if(isset($_GET["host"])){echo'',confirm(sprintf('Drop %s?',"$fa@$_GET[host]"));}echo' +

    +';}elseif(isset($_GET["processlist"])){if(support("kill")){if($_POST&&!$k){$pd=0;foreach((array)$_POST["kill"]as$X){if(kill_process($X))$pd++;}queries_redirect(ME."processlist=",lang(array('%d process has been killed.','%d processes have been killed.'),$pd),$pd||!$_POST["kill"]);}}page_header('Process list',$k);echo' +
    +
    + +',script("mixin(qsl('table'), {onclick: tableClick, ondblclick: partialArg(tableClick, true)});");$t=-1;foreach(process_list()as$t=>$L){if(!$t){echo"".(support("kill")?"\n";}echo"".(support("kill")?"
    ":"");foreach($L +as$z=>$X)echo"$z".doc_link(array('sql'=>"show-processlist.html#processlist_".strtolower($z),));echo"
    ".checkbox("kill[]",$L[$y=="sql"?"Id":"pid"],0):"");foreach($L +as$z=>$X)echo"".(($y=="sql"&&$z=="Info"&&preg_match("~Query|Killed~",$L["Command"])&&$X!="")||($y=="pgsql"&&$z=="current_query"&&$X!="")||($y=="oracle"&&$z=="sql_text"&&$X!="")?"".shorten_utf8($X,100,"").' '.'Clone'.'':h($X));echo"\n";}echo'
    +
    +

    +';if(support("kill")){echo($t+1)."/".sprintf('%d in total',max_connections()),"

    \n";}echo' +

    +',script("tableCheck();");}elseif(isset($_GET["select"])){$a=$_GET["select"];$R=table_status1($a);$w=indexes($a);$m=fields($a);$o=column_foreign_keys($a);$ie=$R["Oid"];parse_str($_COOKIE["adminer_import"],$ma);$Af=array();$d=array();$Cg=null;foreach($m +as$z=>$l){$E=$b->fieldName($l);if(isset($l["privileges"]["select"])&&$E!=""){$d[$z]=html_entity_decode(strip_tags($E),ENT_QUOTES);if(is_shortable($l))$Cg=$b->selectLengthProcess();}$Af+=$l["privileges"];}list($N,$s)=$b->selectColumnsProcess($d,$w);$jd=count($s)selectSearchProcess($m,$w);$we=$b->selectOrderProcess($m,$w);$_=$b->selectLimitProcess();if($_GET["val"]&&is_ajax()){header("Content-Type: text/plain; charset=utf-8");foreach($_GET["val"]as$bh=>$L){$ua=convert_field($m[key($L)]);$N=array($ua?$ua:idf_escape(key($L)));$Z[]=where_check($bh,$m);$K=$j->select($a,$N,$Z,$N);if($K)echo +reset($K->fetch_row());}exit;}$cf=$dh=null;foreach($w +as$v){if($v["type"]=="PRIMARY"){$cf=array_flip($v["columns"]);$dh=($N?$cf:array());foreach($dh +as$z=>$X){if(in_array(idf_escape($z),$N))unset($dh[$z]);}break;}}if($ie&&!$cf){$cf=$dh=array($ie=>0);$w[]=array("type"=>"PRIMARY","columns"=>array($ie));}if($_POST&&!$k){$zh=$Z;if(!$_POST["all"]&&is_array($_POST["check"])){$Oa=array();foreach($_POST["check"]as$Ma)$Oa[]=where_check($Ma,$m);$zh[]="((".implode(") OR (",$Oa)."))";}$zh=($zh?"\nWHERE ".implode(" AND ",$zh):"");if($_POST["export"]){cookie("adminer_import","output=".urlencode($_POST["output"])."&format=".urlencode($_POST["format"]));dump_headers($a);$b->dumpTable($a,"");$Ec=($N?implode(", ",$N):"*").convert_fields($d,$m,$N)."\nFROM ".table($a);$Ic=($s&&$jd?"\nGROUP BY ".implode(", ",$s):"").($we?"\nORDER BY ".implode(", ",$we):"");if(!is_array($_POST["check"])||$cf)$I="SELECT $Ec$zh$Ic";else{$Zg=array();foreach($_POST["check"]as$X)$Zg[]="(SELECT".limit($Ec,"\nWHERE ".($Z?implode(" AND ",$Z)." AND ":"").where_check($X,$m).$Ic,1).")";$I=implode(" UNION ALL ",$Zg);}$b->dumpData($a,"table",$I);exit;}if(!$b->selectEmailProcess($Z,$o)){if($_POST["save"]||$_POST["delete"]){$J=true;$na=0;$P=array();if(!$_POST["delete"]){foreach($d +as$E=>$X){$X=process_input($m[$E]);if($X!==null&&($_POST["clone"]||$X!==false))$P[idf_escape($E)]=($X!==false?$X:idf_escape($E));}}if($_POST["delete"]||$P){if($_POST["clone"])$I="INTO ".table($a)." (".implode(", ",array_keys($P)).")\nSELECT ".implode(", ",$P)."\nFROM ".table($a);if($_POST["all"]||($cf&&is_array($_POST["check"]))||$jd){$J=($_POST["delete"]?$j->delete($a,$zh):($_POST["clone"]?queries("INSERT $I$zh"):$j->update($a,$P,$zh)));$na=$e->affected_rows;}else{foreach((array)$_POST["check"]as$X){$yh="\nWHERE ".($Z?implode(" AND ",$Z)." AND ":"").where_check($X,$m);$J=($_POST["delete"]?$j->delete($a,$yh,1):($_POST["clone"]?queries("INSERT".limit1($a,$I,$yh)):$j->update($a,$P,$yh,1)));if(!$J)break;$na+=$e->affected_rows;}}}$D=lang(array('%d item has been affected.','%d items have been affected.'),$na);if($_POST["clone"]&&$J&&$na==1){$ud=last_id();if($ud)$D=sprintf('Item%s has been inserted.'," $ud");}queries_redirect(remove_from_uri($_POST["all"]&&$_POST["delete"]?"page":""),$D,$J);if(!$_POST["delete"]){edit_form($a,$m,(array)$_POST["fields"],!$_POST["clone"]);page_footer();exit;}}elseif(!$_POST["import"]){if(!$_POST["val"])$k='Ctrl+click on a value to modify it.';else{$J=true;$na=0;foreach($_POST["val"]as$bh=>$L){$P=array();foreach($L +as$z=>$X){$z=bracket_escape($z,1);$P[idf_escape($z)]=(preg_match('~char|text~',$m[$z]["type"])||$X!=""?$b->processInput($m[$z],$X):"NULL");}$J=$j->update($a,$P," WHERE ".($Z?implode(" AND ",$Z)." AND ":"").where_check($bh,$m),!$jd&&!$cf," ");if(!$J)break;$na+=$e->affected_rows;}queries_redirect(remove_from_uri(),lang(array('%d item has been affected.','%d items have been affected.'),$na),$J);}}elseif(!is_string($uc=get_file("csv_file",true)))$k=upload_error($uc);elseif(!preg_match('~~u',$uc))$k='File must be in UTF-8 encoding.';else{cookie("adminer_import","output=".urlencode($ma["output"])."&format=".urlencode($_POST["separator"]));$J=true;$Ya=array_keys($m);preg_match_all('~(?>"[^"]*"|[^"\r\n]+)+~',$uc,$Gd);$na=count($Gd[0]);$j->begin();$Of=($_POST["separator"]=="csv"?",":($_POST["separator"]=="tsv"?"\t":";"));$M=array();foreach($Gd[0]as$z=>$X){preg_match_all("~((?>\"[^\"]*\")+|[^$Of]*)$Of~",$X.$Of,$Hd);if(!$z&&!array_diff($Hd[1],$Ya)){$Ya=$Hd[1];$na--;}else{$P=array();foreach($Hd[1]as$t=>$Ua)$P[idf_escape($Ya[$t])]=($Ua==""&&$m[$Ya[$t]]["null"]?"NULL":q(str_replace('""','"',preg_replace('~^"|"$~','',$Ua))));$M[]=$P;}}$J=(!$M||$j->insertUpdate($a,$M,$cf));if($J)$J=$j->commit();queries_redirect(remove_from_uri("page"),lang(array('%d row has been imported.','%d rows have been imported.'),$na),$J);$j->rollback();}}}$rg=$b->tableName($R);if(is_ajax()){page_headers();ob_start();}else +page_header('Select'.": $rg",$k);$P=null;if(isset($Af["insert"])||!support("table")){$P="";foreach((array)$_GET["where"]as$X){if($o[$X["col"]]&&count($o[$X["col"]])==1&&($X["op"]=="="||(!$X["op"]&&!preg_match('~[_%]~',$X["val"]))))$P.="&set".urlencode("[".bracket_escape($X["col"])."]")."=".urlencode($X["val"]);}}$b->selectLinks($R,$P);if(!$d&&support("table"))echo"

    ".'Unable to select the table'.($m?".":": ".error())."\n";else{echo"

    \n","
    ";hidden_fields_get();echo(DB!=""?''.(isset($_GET["ns"])?'':""):"");echo'',"
    \n";$b->selectColumnsPrint($N,$d);$b->selectSearchPrint($Z,$d,$w);$b->selectOrderPrint($we,$d,$w);$b->selectLimitPrint($_);$b->selectLengthPrint($Cg);$b->selectActionPrint($w);echo"
    \n";$F=$_GET["page"];if($F=="last"){$Dc=$e->result(count_rows($a,$Z,$jd,$s));$F=floor(max(0,$Dc-1)/$_);}$Jf=$N;$Hc=$s;if(!$Jf){$Jf[]="*";$jb=convert_fields($d,$m,$N);if($jb)$Jf[]=substr($jb,2);}foreach($N +as$z=>$X){$l=$m[idf_unescape($X)];if($l&&($ua=convert_field($l)))$Jf[$z]="$ua AS $X";}if(!$jd&&$dh){foreach($dh +as$z=>$X){$Jf[]=idf_escape($z);if($Hc)$Hc[]=idf_escape($z);}}$J=$j->select($a,$Jf,$Z,$Hc,$we,$_,$F,true);if(!$J)echo"

    ".error()."\n";else{if($y=="mssql"&&$F)$J->seek($_*$F);$Xb=array();echo"

    \n";$M=array();while($L=$J->fetch_assoc()){if($F&&$y=="oracle")unset($L["RNUM"]);$M[]=$L;}if($_GET["page"]!="last"&&$_!=""&&$s&&$jd&&$y=="sql")$Dc=$e->result(" SELECT FOUND_ROWS()");if(!$M)echo"

    ".'No rows.'."\n";else{$Ba=$b->backwardKeys($a,$rg);echo"

    ","",script("mixin(qs('#table'), {onclick: tableClick, ondblclick: partialArg(tableClick, true), onkeydown: editingKeydown});"),"".(!$s&&$N?"":"\n";if(is_ajax()){if($_%2==1&&$F%2==1)odd();ob_end_clean();}foreach($b->rowDescriptions($M,$o)as$Wd=>$L){$ah=unique_array($M[$Wd],$w);if(!$ah){$ah=array();foreach($M[$Wd]as$z=>$X){if(!preg_match('~^(COUNT\((\*|(DISTINCT )?`(?:[^`]|``)+`)\)|(AVG|GROUP_CONCAT|MAX|MIN|SUM)\(`(?:[^`]|``)+`\))$~',$z))$ah[$z]=$X;}}$bh="";foreach($ah +as$z=>$X){if(($y=="sql"||$y=="pgsql")&&preg_match('~char|text|enum|set~',$m[$z]["type"])&&strlen($X)>64){$z=(strpos($z,'(')?$z:idf_escape($z));$z="MD5(".($y!='sql'||preg_match("~^utf8~",$m[$z]["collation"])?$z:"CONVERT($z USING ".charset($e).")").")";$X=md5($X);}$bh.="&".($X!==null?urlencode("where[".bracket_escape($z)."]")."=".urlencode($X):"null%5B%5D=".urlencode($z));}echo"".(!$s&&$N?"":"";}}}if($Ba)echo"\n";}if(is_ajax())exit;echo"
    ".script("qs('#all-page').onclick = partial(formCheck, /check/);","")." ".'Modify'."");$Xd=array();$Fc=array();reset($N);$of=1;foreach($M[0]as$z=>$X){if(!isset($dh[$z])){$X=$_GET["columns"][key($N)];$l=$m[$N?($X?$X["col"]:current($N)):$z];$E=($l?$b->fieldName($l,$of):($X["fun"]?"*":$z));if($E!=""){$of++;$Xd[$z]=$E;$c=idf_escape($z);$Uc=remove_from_uri('(order|desc)[^=]*|page').'&order%5B0%5D='.urlencode($z);$Db="&desc%5B0%5D=1";echo"".script("mixin(qsl('th'), {onmouseover: partial(columnMouse), onmouseout: partial(columnMouse, ' hidden')});",""),'';echo +apply_sql_function($X["fun"],$E)."";echo"";}$Fc[$z]=$X["fun"];next($N);}}$_d=array();if($_GET["modify"]){foreach($M +as$L){foreach($L +as$z=>$X)$_d[$z]=max($_d[$z],min(40,strlen(utf8_decode($X))));}}echo($Ba?"".'Relations':"")."
    ".checkbox("check[]",substr($bh,1),in_array(substr($bh,1),(array)$_POST["check"])).($jd||information_schema(DB)?"":" ".'edit'.""));foreach($L +as$z=>$X){if(isset($Xd[$z])){$l=$m[$z];$X=$j->value($X,$l);if($X!=""&&(!isset($Xb[$z])||$Xb[$z]!=""))$Xb[$z]=(is_mail($X)?$Xd[$z]:"");$A="";if(preg_match('~blob|bytea|raw|file~',$l["type"])&&$X!="")$A=ME.'download='.urlencode($a).'&field='.urlencode($z).$bh;if(!$A&&$X!==null){foreach((array)$o[$z]as$n){if(count($o[$z])==1||end($n["source"])==$z){$A="";foreach($n["source"]as$t=>$Yf)$A.=where_link($t,$n["target"][$t],$M[$Wd][$Yf]);$A=($n["db"]!=""?preg_replace('~([?&]db=)[^&]+~','\1'.urlencode($n["db"]),ME):ME).'select='.urlencode($n["table"]).$A;if($n["ns"])$A=preg_replace('~([?&]ns=)[^&]+~','\1'.urlencode($n["ns"]),$A);if(count($n["source"])==1)break;}}}if($z=="COUNT(*)"){$A=ME."select=".urlencode($a);$t=0;foreach((array)$_GET["where"]as$W){if(!array_key_exists($W["col"],$ah))$A.=where_link($t++,$W["col"],$W["val"],$W["op"]);}foreach($ah +as$md=>$W)$A.=where_link($t++,$md,$W);}$X=select_value($X,$A,$l,$Cg);$u=h("val[$bh][".bracket_escape($z)."]");$Y=$_POST["val"][$bh][bracket_escape($z)];$Sb=!is_array($L[$z])&&is_utf8($X)&&$M[$Wd][$z]==$L[$z]&&!$Fc[$z];$Bg=preg_match('~text|lob~',$l["type"]);echo"".($Bg?"":"");}else{$Dd=strpos($X,"");echo" data-text='".($Dd?2:($Bg?1:0))."'".($Sb?"":" data-warning='".h('Use edit link to modify this value.')."'").">$X";$b->backwardKeysPrint($Ba,$M[$Wd]);echo"
    \n","
    \n";}if(!is_ajax()){if($M||$F){$ic=true;if($_GET["page"]!="last"){if($_==""||(count($M)<$_&&($M||!$F)))$Dc=($F?$F*$_:0)+count($M);elseif($y!="sql"||!$jd){$Dc=($jd?false:found_rows($R,$Z));if($Dc$_||$F));if($He){echo(($Dc===false?count($M)+1:$Dc-$F*$_)>$_?'

    '.'Load more data'.''.script("qsl('a').onclick = partial(selectLoadMore, ".(+$_).", '".'Loading'."…');",""):''),"\n";}}echo"

    \n";if($b->selectImportPrint()){echo"
    ","".'Import'."",script("qsl('a').onclick = partial(toggle, 'import');",""),"","
    ";}echo"\n","\n",(!$s&&$N?"":script("tableCheck();"));}}}if(is_ajax()){ob_end_clean();exit;}}elseif(isset($_GET["variables"])){$fg=isset($_GET["status"]);page_header($fg?'Status':'Variables');$ph=($fg?show_status():show_variables());if(!$ph)echo"

    ".'No rows.'."\n";else{echo"\n";foreach($ph +as$z=>$X){echo"","
    ".h($z)."","".h($X);}echo"
    \n";}}elseif(isset($_GET["script"])){header("Content-Type: text/javascript; charset=utf-8");if($_GET["script"]=="db"){$og=array("Data_length"=>0,"Index_length"=>0,"Data_free"=>0);foreach(table_status()as$E=>$R){json_row("Comment-$E",h($R["Comment"]));if(!is_view($R)){foreach(array("Engine","Collation")as$z)json_row("$z-$E",h($R[$z]));foreach($og+array("Auto_increment"=>0,"Rows"=>0)as$z=>$X){if($R[$z]!=""){$X=format_number($R[$z]);json_row("$z-$E",($z=="Rows"&&$X&&$R["Engine"]==($ag=="pgsql"?"table":"InnoDB")?"~ $X":$X));if(isset($og[$z]))$og[$z]+=($R["Engine"]!="InnoDB"||$z!="Data_free"?$R[$z]:0);}elseif(array_key_exists($z,$R))json_row("$z-$E");}}}foreach($og +as$z=>$X)json_row("sum-$z",format_number($X));json_row("");}elseif($_GET["script"]=="kill")$e->query("KILL ".number($_POST["kill"]));else{foreach(count_tables($b->databases())as$i=>$X){json_row("tables-$i",$X);json_row("size-$i",db_size($i));}json_row("");}exit;}else{$wg=array_merge((array)$_POST["tables"],(array)$_POST["views"]);if($wg&&!$k&&!$_POST["search"]){$J=true;$D="";if($y=="sql"&&$_POST["tables"]&&count($_POST["tables"])>1&&($_POST["drop"]||$_POST["truncate"]||$_POST["copy"]))queries("SET foreign_key_checks = 0");if($_POST["truncate"]){if($_POST["tables"])$J=truncate_tables($_POST["tables"]);$D='Tables have been truncated.';}elseif($_POST["move"]){$J=move_tables((array)$_POST["tables"],(array)$_POST["views"],$_POST["target"]);$D='Tables have been moved.';}elseif($_POST["copy"]){$J=copy_tables((array)$_POST["tables"],(array)$_POST["views"],$_POST["target"]);$D='Tables have been copied.';}elseif($_POST["drop"]){if($_POST["views"])$J=drop_views($_POST["views"]);if($J&&$_POST["tables"])$J=drop_tables($_POST["tables"]);$D='Tables have been dropped.';}elseif($y!="sql"){$J=($y=="sqlite"?queries("VACUUM"):apply_queries("VACUUM".($_POST["optimize"]?"":" ANALYZE"),$_POST["tables"]));$D='Tables have been optimized.';}elseif(!$_POST["tables"])$D='No tables.';elseif($J=queries(($_POST["optimize"]?"OPTIMIZE":($_POST["check"]?"CHECK":($_POST["repair"]?"REPAIR":"ANALYZE")))." TABLE ".implode(", ",array_map('idf_escape',$_POST["tables"])))){while($L=$J->fetch_assoc())$D.="".h($L["Table"]).": ".h($L["Msg_text"])."
    ";}queries_redirect(substr(ME,0,-1),$D,$J);}page_header(($_GET["ns"]==""?'Database'.": ".h(DB):'Schema'.": ".h($_GET["ns"])),$k,true);if($b->homepage()){if($_GET["ns"]!==""){echo"

    ".'Tables and views'."

    \n";$vg=tables_list();if(!$vg)echo"

    ".'No tables.'."\n";else{echo"

    \n";if(support("table")){echo"
    ".'Search data in tables'."
    ","",script("qsl('input').onkeydown = partialArg(bodyKeydown, 'search');","")," \n","
    \n";if($_POST["search"]&&$_POST["query"]!=""){$_GET["where"][0]["op"]="LIKE %%";search_tables();}}echo"
    \n","\n",script("mixin(qsl('table'), {onclick: tableClick, ondblclick: partialArg(tableClick, true)});"),'','\n";$S=0;foreach($vg +as$E=>$U){$sh=($U!==null&&!preg_match('~table|sequence~i',$U));$u=h("Table-".$E);echo'
    '.script("qs('#check-all').onclick = partial(formCheck, /^(tables|views)\[/);",""),''.'Table',''.'Engine'.doc_link(array('sql'=>'storage-engines.html')),''.'Collation'.doc_link(array('sql'=>'charset-charsets.html','mariadb'=>'supported-character-sets-and-collations/')),''.'Data Length'.doc_link(array('sql'=>'show-table-status.html',)),''.'Index Length'.doc_link(array('sql'=>'show-table-status.html',)),''.'Data Free'.doc_link(array('sql'=>'show-table-status.html')),''.'Auto Increment'.doc_link(array('sql'=>'example-auto-increment.html','mariadb'=>'auto_increment/')),''.'Rows'.doc_link(array('sql'=>'show-table-status.html',)),(support("comment")?''.'Comment'.doc_link(array('sql'=>'show-table-status.html',)):''),"
    '.checkbox(($sh?"views[]":"tables[]"),$E,in_array($E,$wg,true),"","","",$u),''.(support("table")||support("indexes")?"".h($E).'':h($E));if($sh){echo''.(preg_match('~materialized~i',$U)?'Materialized view':'View').'','?';}else{foreach(array("Engine"=>array(),"Collation"=>array(),"Data_length"=>array("create",'Alter table'),"Index_length"=>array("indexes",'Alter indexes'),"Data_free"=>array("edit",'New item'),"Auto_increment"=>array("auto_increment=1&create",'Alter table'),"Rows"=>array("select",'Select data'),)as$z=>$A){$u=" id='$z-".h($E)."'";echo($A?"".(support("table")||$z=="Rows"||(support("indexes")&&$z!="Data_length")?"?":"?"):"");}$S++;}echo(support("comment")?"":"");}echo"
    ".sprintf('%d in total',count($vg)),"".h($y=="sql"?$e->result("SELECT @@default_storage_engine"):""),"".h(db_collation(DB,collations()));foreach(array("Data_length","Index_length","Data_free")as$z)echo"";echo"
    \n","
    \n";if(!information_schema(DB)){echo"\n";}echo"
    \n",script("tableCheck();");}echo'

    ".'Routines'."

    \n";$Ef=routines();if($Ef){echo"\n",'\n";odd('');foreach($Ef +as$L){$E=($L["SPECIFIC_NAME"]==$L["ROUTINE_NAME"]?"":"&name=".urlencode($L["ROUTINE_NAME"]));echo'','
    '.'Name'.''.'Type'.''.'Return type'."
    '.h($L["ROUTINE_NAME"]).'',''.h($L["ROUTINE_TYPE"]),''.h($L["DTD_IDENTIFIER"]),''.'Alter'."";}echo"
    \n";}echo'

    ".'Events'."

    \n";$M=get_rows("SHOW EVENTS");if($M){echo"\n","\n";foreach($M +as$L){echo"","
    ".'Name'."".'Schedule'."".'Start'."".'End'."
    ".h($L["Name"]),"".($L["Execute at"]?'At given time'."".$L["Execute at"]:'Every'." ".$L["Interval value"]." ".$L["Interval field"]."$L[Starts]"),"$L[Ends]",''.'Alter'.'';}echo"
    \n";$gc=$e->result("SELECT @@event_scheduler");if($gc&&$gc!="ON")echo"

    event_scheduler: ".h($gc)."\n";}echo'

    +
    +
    +

    Privacy Policy

    +
    +
    + + + + +

    Introduction

    + +

    Darcan LTD trading as Call Center Mastery (herein referred to as Call Center Mastery) understands that your privacy is important to you. We are committed to protecting the privacy of your personally-identifiable information as you use this website. This Privacy Policy tells you how we protect and use information that we gather from you. By using this website, you consent to the terms described in the most recent version of this Privacy Policy. You should also read our Terms of Use to understand the general rules about your use of this website, and any additional terms that may apply when you access particular services or materials on certain areas of this website. “We,” “our” means Call Center Mastery and its affiliates. “You,” “your,” visitor,” or “user” means the individual accessing this site.

    + +

    Personal and non-personal information

    + +

    Our Privacy Policy identifies how we treat your personal and non-personal information.

    + +

    What is non-personal information and how is it collected and used?

    + +

    Non personal information is information that cannot identify you. If you visit this web site to read information, such as information about one of our services, we may collect certain non-personal information about you from your computer’s web browser. Because non-personal information cannot identify you or be tied to you in any way, there are no restrictions on the ways that we can use or share non-personal information.

    + +

    What is personal information and how is it collected?

    + +

    Personal information is information that identifies you as an individual, such as your name, mailing address, e-mail address, telephone number, and fax number. We may collect personal information from you in a variety of ways:

    + +
      +
    • When you send us an application or other form
    • +
    • When you conduct a transaction with us, our affiliates, or others
    • +
    • When we collect information about in you in support of a transaction, such as credit card information
    • +
    + +

    In some places on this web site you have the opportunity to send us personal information about yourself, to elect to receive particular information, to purchase access to one of our products or services, or to participate in an activity.

    + +

    Are cookies or other technologies used to collect personal information?

    + +

    Yes, we may use cookies and related technologies, such as web beacons, to collect information on our web site. A cookie is a text file that is placed on your hard disk by a web page server. Cookies cannot be used to run programs or deliver viruses to your computer. Cookies are uniquely assigned to you, and can only be read by a web server in the domain that issued the cookie to you. One of the primary purposes of cookies is to provide a convenience feature to save you time. The purpose of a cookie is to tell the Web server that you have returned to a specific page. For example, if you register with us, a cookie helps Call Center Mastery to recall your specific information on subsequent visits. This simplifies the process of recording your personal information, such as billing addresses, shipping addresses, and so on. When you return to the same Call Center Mastery website, the information you previously provided can be retrieved, so you can easily use the features that you customized.

    + +

    A web beacon is a small graphic image that allows the party that set the web beacon to monitor and collect certain information about the viewer of the web page, web-based document or e-mail message, such as the type of browser requesting the web beacon, the IP address of the computer that the web beacon is sent to and the time the web beacon was viewed. Web beacons can be very small and invisible to the user, but, in general, any electronic image viewed as part of a web page or e-mail, including HTML based content, can act as a web beacon. We may use web beacons to count visitors to the web pages on the web site or to monitor how our users navigate the web site, and we may include web beacons in e-mail messages in order to count how many messages sent were actually opened, acted upon or forwarded.

    + +

    Third party vendors also may use cookies on our web site. For instance, we may contract with third parties who will use cookies on our web site to track and analyze anonymous usage and volume statistical information from our visitors and members. Such information is shared externally only on an anonymous, aggregated basis. These third parties use persistent cookies to help us to improve the visitor experience, to manage our site content, and to track visitor behaviour. We may also contract with a third party to send e-mail to our registered [users/members].

    + +

    To help measure and improve the effectiveness of our e-mail communications, the third party sets cookies. All data collected by this third party on behalf of Call Center Mastery is used solely by or on behalf of Call Center Mastery and is shared externally only on an anonymous, aggregated basis. From time to time we may allow third parties to post advertisements on our web site, and those third-party advertisements may include a cookie or web beacon served by the third party. This Privacy Policy does not cover the use of information collected from you by third party ad servers. We do not control cookies in such third party ads, and you should check the privacy policies of those advertisers and/or ad services to learn about their use of cookies and other technology before linking to an ad. We will not share your personal information with these companies, but these companies may use information about your visits to this and other web sites in order to provide advertisements on this site and other sites about goods and services that may be of interest to you, and they may share your personal information that you provide to them with others.

    + +

    You have the ability to accept or decline cookies. Most Web browsers automatically accept cookies, but you can usually modify your browser setting to decline cookies if you prefer. If you choose to decline cookies, you may not be able to fully experience the interactive features of the Call Center Mastery websites you visit.

    + +

    How does Call Center Mastery use personal information?

    + +

    Call Center Mastery may keep and use personal information we collect from or about you to provide you with access to this web site or other products or services, to respond to your requests, to bill you for products/services you purchased, and to provide ongoing service and support, to contact you with information that might be of interest to you, including information about products and services of ours and of others, or ask for your opinion about our products or the products of others, for record keeping and analytical purposes and to research, develop and improve programs, products, services and content.

    + +

    Personal information collected online may be combined with information you provide to us through other sources We may also remove your personal identifiers (your name, email address, social security number, etc). In this case, you would no longer be identified as a single unique individual. Once we have de-identified information, it is non-personal information and we may treat it like other non-personal information. Finally, we may use your personal information to protect our rights or property, or to protect someone’s health, safety or welfare, and to comply with a law or regulation, court order or other legal process.

    + +

    Does Call Center Mastery share personal information with others?

    + +

    We will not share your personal information collected from this web site with an unrelated third party without your permission, except as otherwise provided in this Privacy Policy. In the ordinary course of business, we may share some personal information with companies that we hire to perform services or functions on our behalf. In all cases in which we share your personal information with a third party for the purpose of providing a service to us, we will not authorize them to keep, disclose or use your information with others except for the purpose of providing the services we asked them to provide.

    + +

    We will not sell, exchange or publish your personal information, except in conjunction with a corporate sale, merger, dissolution, or acquisition. For some sorts of transactions, in addition to our direct collection of information, our third party service vendors (such as credit card companies, clearinghouses and banks) who may provide such services as credit, insurance, and escrow services may collect personal information directly from you to assist you with your transaction. We do not control how these third parties use such information, but we do ask them to disclose how they use your personal information before they collect it. If you submit a review for a third party (person or business) using our Facebook Fan Review Application, during the submission process we ask your permission to gather your basic information (such as name and email address) which we then share with the third party for whom you are submitting the review. We may be legally compelled to release your personal information in response to a court order, subpoena, search warrant, law or regulation.

    + +

    We may cooperate with law enforcement authorities in investigating and prosecuting web site visitors who violate our rules or engage in behavior, which is harmful to other visitors (or illegal). We may disclose your personal information to third parties if we feel that the disclosure is necessary to protect our rights or property, protect someone’s health, safety or welfare, or to comply with a law or regulation, court order or other legal process. As discussed in the section on cookies and other technologies, from time to time we may allow a third party to serve advertisements on this web site.

    + +

    If you share information with the advertiser, including by clicking on their ads, this Privacy Policy does not control the advertisers use of your personal information, and you should check the privacy policies of those advertisers and/or ad services to learn about their use of cookies and other technology before linking to an ad.

    + +

    How is personal information used for communications?

    + +

    We may contact you periodically by e-mail, mail or telephone to provide information regarding programs, products, services and content that may be of interest to you. In addition, some of the features on this web site allow you to communicate with us using an online form. If your communication requests a response from us, we may send you a response via e-mail. The e-mail response or confirmation may include your personal information. We cannot guarantee that our e-mails to you will be secure from unauthorized interception.

    + +

    How is personal information secured?

    + +

    We have implemented generally accepted standards of technology and operational security in order to protect personally-identifiable information from loss, misuse, alteration, or destruction. Only authorized personnel and third party vendors have access to your personal information, and these employees and vendors are required to treat this information as confidential. Despite these precautions, we cannot guarantee that unauthorized persons will not obtain access to your personal information.

    + +

    Links

    + +

    This site contains links to other sites that provide information that we consider to be interesting. Call Center Mastery is not responsible for the privacy practices or the content of such web sites.

    + +

    Public discussions

    + +

    This site may provide public discussions on various business valuation topics. Please note that any information you post in these discussions will become public, so please do not post sensitive information in the public discussions. Whenever you publicly disclose information online, that information could be collected and used by others. We are not responsible for any action or policies of any third parties who collect information that users disclose in any such forums on the web site. Call Center Mastery does not agree or disagree with anything posted on the discussion board. Also remember that you must comply with our other published policies regarding postings on our public forums.

    + +

    How can a user access, change, and/or delete personal information?

    + +

    You may access, correct, update, and/or delete any personally-identifiable information that you submit to the web site. You may also unsubscribe from mailing lists or any registrations on the web site. To do so, please either follow instructions on the page of the web site on which you have provided such information or subscribed or registered or contact us at [admin@cc-mastery.com]

    + +

    Children’s privacy

    + +

    Call Center Mastery will not intentionally collect any personal information (such as a child’s name or email address) from children under the age of 13. If you think that we have collected personal information from a child under the age of 13, please contact us.

    + +

    Changes

    + +

    Call Center Mastery reserves the right to modify this statement at any time. Any changes to this Privacy Policy will be listed in this section, and if such changes are material, a notice will be included on the homepage of the web site for a period of time. If you have any questions about privacy at any websites operated by Call Center Mastery or about our website practices, please contact us at: admin@cc-mastery.com

    +
    + + + \ No newline at end of file diff --git a/project-model.php b/project-model.php new file mode 100644 index 0000000..3ee8ae1 --- /dev/null +++ b/project-model.php @@ -0,0 +1,61 @@ + 0 + ) { + formData[day] = dayData; + } + }); + } + + function submitForm(event) { + event.preventDefault(); + + updateFormData(); // Ensure formData is updated before submission + + // Display the collected data (you can replace this with your own logic) + alert(JSON.stringify(formData)); + } +}); diff --git a/projectAdd.php b/projectAdd.php new file mode 100644 index 0000000..33e9f3f --- /dev/null +++ b/projectAdd.php @@ -0,0 +1,514 @@ +
    +

    Add Availability

    + + + +
    +
    + + +
    + +
    + + +
    +
    + + + +
    +
    + + + +
    +
    + + +
    + + +
    + + +
    + + + + + +
    +
    +
    + + +
    +
    + + + +
    +
    + + +
    + + +
    +
    + Add Time +
    +
    + +
    + +
    + + + +
    +
    +
    + + +
    +
    + + +
    +
    + + +
    +
    +
    + Add Time +
    +
    +
    + + +
    + + + +
    +
    +
    + + +
    +
    + + +
    +
    + + +
    +
    +
    + Add Time +
    +
    +
    + + +
    + + + +
    +
    +
    + + +
    +
    + + +
    +
    + + +
    +
    +
    + Add Time +
    +
    +
    + +
    + + + +
    +
    +
    + + +
    +
    + + +
    +
    + + +
    +
    +
    + Add Time +
    +
    +
    + + +
    + + + +
    +
    +
    + + +
    +
    + + +
    +
    + + +
    +
    +
    + Add Time +
    +
    +
    + + +
    + + + +
    +
    +
    + + +
    +
    + + +
    +
    + + +
    +
    +
    + Add Time +
    +
    +
    +
    +
    + + +
    + + +
    +
    + + \ No newline at end of file diff --git a/projectEdit.php b/projectEdit.php new file mode 100644 index 0000000..949d9d6 --- /dev/null +++ b/projectEdit.php @@ -0,0 +1,555 @@ +
    +

    Edit Availability

    + + + +
    +
    + + +
    + +
    + + +
    +
    + + +
    +
    + + +
    +
    + + +
    +
    + + +
    + + + + slot); + // echo json_encode($slots); + } ?> +
    + + +
    + sunday) ? 'checked' : '' ?>> + + + + +
    sunday) ? 'style="display:flex"' : '' ?>> + sunday)) { + foreach ($slots->sunday as $sun) { + ?> +
    +
    + + +
    +
    + + + +
    +
    + + +
    +
    + +
    + Add Time +
    +
    + +
    + + +
    + monday) ? 'checked' : '' ?>> + + +
    monday) ? 'style="display:flex"' : '' ?>> + monday)) { + foreach ($slots->monday as $mon) { + ?> +
    +
    + + +
    +
    + + +
    +
    + + +
    + +
    + +
    + Add Time +
    +
    +
    + + +
    + tuesday) ? 'checked' : '' ?>> + + +
    tuesday) ? 'style="display:flex"' : '' ?>> + tuesday)) { + foreach ($slots->tuesday as $tue) { + ?> +
    +
    + + +
    +
    + + +
    +
    + + +
    + +
    + +
    + Add Time +
    +
    +
    + + +
    + wednesday) ? 'checked' : '' ?>> + + +
    wednesday) ? 'style="display:flex"' : '' ?>> + wednesday)) { + foreach ($slots->wednesday as $wed) { + ?> +
    +
    + + +
    +
    + + +
    +
    + + +
    + +
    + +
    + Add Time +
    +
    +
    + +
    + thursday) ? 'checked' : '' ?>> + + +
    thursday) ? 'style="display:flex"' : '' ?>> + thursday)) { + foreach ($slots->thursday as $thurs) { + ?> +
    +
    + + +
    +
    + + +
    +
    + + +
    +
    + +
    + Add Time +
    +
    +
    + + +
    + friday) ? 'checked' : '' ?>> + + +
    friday) ? 'style="display:flex"' : '' ?>> + friday)) { + foreach ($slots->friday as $fri) { + ?> +
    +
    + + +
    +
    + + +
    +
    + + +
    +
    + +
    + Add Time +
    +
    +
    + + +
    + saturday) ? 'checked' : '' ?>> + + +
    saturday) ? 'style="display:flex"' : '' ?>> + saturday)) { + foreach ($slots->saturday as $sat) { + ?> +
    +
    + + +
    +
    + + +
    +
    + + +
    +
    + +
    + Add Time +
    +
    +
    +
    + +
    + + +
    + + +
    +
    + + \ No newline at end of file diff --git a/projectEditMulti.php b/projectEditMulti.php new file mode 100644 index 0000000..8a7744b --- /dev/null +++ b/projectEditMulti.php @@ -0,0 +1,558 @@ +
    +

    Edit Availability

    + + + +
    +
    + + +
    + +
    + + +
    +
    + + +
    +
    + + +
    +
    + + +
    +
    + + +
    + + + + slot); + // echo json_encode($slots); + } ?> +
    + + +
    + sunday) ? 'checked' : '' ?>> + + + + +
    sunday) ? 'style="display:flex"' : '' ?>> + sunday)) { + foreach ($slots->sunday as $sun) { + ?> +
    +
    + + +
    +
    + + + +
    +
    + + +
    +
    + +
    + Add Time +
    +
    + +
    + + +
    + monday) ? 'checked' : '' ?>> + + +
    monday) ? 'style="display:flex"' : '' ?>> + monday)) { + foreach ($slots->monday as $mon) { + ?> +
    +
    + + +
    +
    + + +
    +
    + + +
    + +
    + +
    + Add Time +
    +
    +
    + + +
    + tuesday) ? 'checked' : '' ?>> + + +
    tuesday) ? 'style="display:flex"' : '' ?>> + tuesday)) { + foreach ($slots->tuesday as $tue) { + ?> +
    +
    + + +
    +
    + + +
    +
    + + +
    + +
    + +
    + Add Time +
    +
    +
    + + +
    + wednesday) ? 'checked' : '' ?>> + + +
    wednesday) ? 'style="display:flex"' : '' ?>> + wednesday)) { + foreach ($slots->wednesday as $wed) { + ?> +
    +
    + + +
    +
    + + +
    +
    + + +
    + +
    + +
    + Add Time +
    +
    +
    + +
    + thursday) ? 'checked' : '' ?>> + + +
    thursday) ? 'style="display:flex"' : '' ?>> + thursday)) { + foreach ($slots->thursday as $thurs) { + ?> +
    +
    + + +
    +
    + + +
    +
    + + +
    +
    + +
    + Add Time +
    +
    +
    + + +
    + friday) ? 'checked' : '' ?>> + + +
    friday) ? 'style="display:flex"' : '' ?>> + friday)) { + foreach ($slots->friday as $fri) { + ?> +
    +
    + + +
    +
    + + +
    +
    + + +
    +
    + +
    + Add Time +
    +
    +
    + + +
    + saturday) ? 'checked' : '' ?>> + + +
    saturday) ? 'style="display:flex"' : '' ?>> + saturday)) { + foreach ($slots->saturday as $sat) { + ?> +
    +
    + + +
    +
    + + +
    +
    + + +
    +
    + +
    + Add Time +
    +
    +
    +
    + +
    + + +
    + + +
    +
    + + \ No newline at end of file diff --git a/projectListing.php b/projectListing.php new file mode 100644 index 0000000..ba1a20f --- /dev/null +++ b/projectListing.php @@ -0,0 +1,354 @@ + + +
    +

    Availability Checker   Add

    + + + +
    +
    + + + + + + + + + + + + + + + + + + + + $value) { + $role = $_SESSION['role'] ; + echo ' '; + echo ''; // Checkbox for multiselect + echo ' '; + // echo ' '; + echo ' '; + // echo ' '; + + echo ' '; + echo ' '; + echo ' '; + + echo ' '; + echo ' '; + echo ' '; + echo ''; + } + ?> + +
    IDProject NameDaysScore ThresholdActual ScoreAlert CheckWebhook
    ' . $value->id . '
    edit delete
    ' . $value->id . '
    delete
    ' . $value->project_name . ' ' . $value->slot . ' ' . $value->days . ' ' . $value->score_threshold . ' ' . $value->actual_score . '
    + + +
    ' . $value->webhook . ' Check + Duplicate + Authorize +
    + +
    +
    + + + 0 ? ($currentPage - $range) : 1; + $endPage = ($currentPage + $range) < $totalPages ? ($currentPage + $range) : $totalPages; + + ?> + + +
    + + + + \ No newline at end of file diff --git a/put_v1_api_image_edit.php b/put_v1_api_image_edit.php new file mode 100644 index 0000000..1b6cedf --- /dev/null +++ b/put_v1_api_image_edit.php @@ -0,0 +1,66 @@ +save_rules($model->get_all_edit_validation_rule()); + $_POST = Flight::request()->data->getData(); + + $model_row = $model->get($id); + + if (!$model_row) + { + echo json_encode([ + 'code' => 404, + 'error' => false + ]); + http_response_code(404); + exit; + } + + if ($validation->validate($_POST)) + { + $result = $model->edit([ + 'url' => $request->get_input_post('url'), + 'user_id' => $request->get_input_post('user_id'), + 'caption' => $request->get_input_post('caption'), + ], $id); + + if ($result) + { + echo json_encode([ + 'code' => 200, + 'error' => false, + 'id' => $id, + 'message' => "Data has been updated" + ]); + http_response_code(200); + exit; + } + else + { + echo json_encode([ + 'code' => 409, + 'error' => false, + 'id' => $id, + 'message' => "Error!" + ]); + http_response_code(409); + exit; + } + } + else + { + $message = $validation->get_errors(); + $result['error'] = true; + $result['http_code'] = 403; + $result['message'] = $message; + + echo json_encode($result); + http_response_code(403); + exit; + } +}); \ No newline at end of file diff --git a/query-service.php b/query-service.php new file mode 100644 index 0000000..0ca4126 --- /dev/null +++ b/query-service.php @@ -0,0 +1,154 @@ + + * + */ +class QueryService +{ + public function create_where($type_array, $fields=[], $operators=[], $field_values=[]) + { + $where = []; + $model_fields = array_keys($type_array); + + if (count($fields) != count($operators) || count($operators) != count($field_values)) + { + return $where; + } + + for ($i=0; $i < count($fields); $i++) + { + if (in_array($fields[$i], $model_fields)) + { + $type = $type_array[$fields[$i]]; + $single_field_values = $field_values[$i]; + + switch( $single_field_values ) + { + case 'integer': + $single_field_values = filter_var($single_field_values, FILTER_SANITIZE_NUMBER_INT ); + break; + case 'string': + case 'date': + case 'datetime': + if ($operators[$i] == 'LIKE') + { + $single_field_values = "'%" . filter_var( $single_field_values, FILTER_SANITIZE_STRING ) . "%'"; + } + else + { + $single_field_values = "'" . filter_var( $single_field_values, FILTER_SANITIZE_STRING ) . "'"; + } + break; + case 'float': + $single_field_values = filter_var( $single_field_values, FILTER_SANITIZE_NUMBER_FLOAT ); + break; + } + + switch ($operators[$i]) + { + case 'EQUAL': + $where[] = " `{$fields[$i]}` = '{$single_field_values}' "; + break; + case 'NOT_EQUAL': + $where[] = "{ `$fields[$i]}` != '{$single_field_values}' "; + break; + case 'GREATER_THAN': + if ($type == 'integer' || $type == 'float') + { + $where[] = " `{$fields[$i]}` > {$single_field_values}"; + } + break; + case 'GREATER_THAN_EQUAL': + if ($type == 'integer' || $type == 'float') + { + $where[] = " `{$fields[$i]}` >= {$single_field_values}"; + } + break; + case 'LESS_THAN': + if ($type == 'integer' || $type == 'float') + { + $where[] = " `{$fields[$i]}` < {$single_field_values}"; + } + break; + case 'LESS_THAN_EQUAL': + if ($type == 'integer' || $type == 'float') + { + $where[] = " `{$fields[$i]}` <= {$single_field_values}"; + } + break; + case 'LIKE': + if ($type != 'integer' && $type != 'float' && $type != 'date' && $type != 'datetime') + { + $where[] = " `{$fields[$i]}` LIKE {$single_field_values}"; + } + break; + //TODO: IN Operator + } + } + } + + return $where; + } + + public function create_join_query ($where_query, $column_fields, $join_tables, $join_field, $fields, $operators, $values, $page, $per_page, $sort, $direction) + { + $select = []; + + $from = "FROM {$join_tables['a']} a "; + $from_list = []; + $join = []; + + foreach ($join_tables as $key => $value) + { + $from_list[] = " $value $key"; + if ($key != 'a') + { + $join[] = " INNER JOIN $value $key ON a.id = {$key}.{$join_field} "; + } + } + + foreach ($column_fields as $key => $value) + { + $select[] = " $value "; + } + + $from = $from . implode (' ', $join); + $sql = 'SELECT ' . implode(',', $select) . $from; + $total_sql = 'SELECT count(*) as total ' . $from; + + if ($sort) + { + $sql .= " ORDER BY {$sort} {$direction}"; + } + + if ($per_page && $page) + { + $offset = ($page - 1) * $per_page; + $sql .= " LIMIT $per_page, $offset"; + } + + return [ + 'total' => $total_sql, + 'query' => $sql + ]; + } + + public function perform_join ($model, $query, $page, $per_page) + { + $total = $model->raw_query($query['total']); + $list = $model->raw_query($query['query']); + $last_id = $list[array_key_last($list)]['id']; + return [ + 'total' => $total->total, + 'num_page' => ceil($total->total / $per_page), + 'page' => $page, + 'list' => $list, + 'id' => $last_id + ]; + } +} \ No newline at end of file diff --git a/report-model.php b/report-model.php new file mode 100644 index 0000000..8cd1b64 --- /dev/null +++ b/report-model.php @@ -0,0 +1,152 @@ +_primary_key; + // // } + + // // $this->db->order_by($this->clean_alpha_num_field($order_by), $this->clean_alpha_field($direction)); + + // // if (!empty($where)) { + // // foreach ($where as $field => $value) { + // // if (is_numeric($field) && strlen($value) > 0) { + // // $this->db->where($value); + // // continue; + // // } + + // // if ($field === NULL && $value === NULL) { + // // continue; + // // } + + // // if ($value !== NULL) { + // // if (is_numeric($value)) { + // // $this->db->where($field, $value); + // // continue; + // // } + + // // if (is_string($value)) { + // // $this->db->like($field, $value); + // // continue; + // // } + + // // $this->db->where($field, $value); + // // } + // // } + // // } + + // // $query = $this->db->get($this->_table); + + // $sql = []; + // foreach ($where as $key => $value) { + // if (is_string($value) && strlen($value) > 0) { + // $sql[] = "$key"; + // } else { + // $sql[] = "$key = $value"; + // } + // } + + // return R::findAll($this->_table, implode(' AND ', $sql)); + // $result = []; + + // // if ($query->num_rows() > 0) { + // // foreach ($query->result() as $row) { + // // $result[] = $row; + // // } + // // } + + // return $result; + // } +} diff --git a/reportListing.php b/reportListing.php new file mode 100644 index 0000000..2a60578 --- /dev/null +++ b/reportListing.php @@ -0,0 +1,163 @@ + + +
    +

    Report   Export

    +
    +
    +
    +
    + + + + +
    +
    +
    +
    + +
    + + + + + + + + + + + + + + + + $value) { + echo ' '; + // echo ' '; + // echo ' '; + echo ' '; + echo ' '; + echo ' '; + echo ' '; + + + // echo ' '; + // echo ' '; + // echo ' '; + // echo ' '; + // echo ' '; + // echo ' '; + echo ' + '; + + echo ''; + } + ?> + +
    IDProjectDateType
    ' . $value->id . '
    edit delete
    ' . $value->id . '
    delete
    ' . $value->id . ' ' . $value->project . ' ' . $value->date . ' ' . $value->type . ' ' . $value->new_lead . ' ' . $value->outbound_dial . ' ' . $value->pickup . ' ' . $value->conversation . ' ' . $value->booked_appointment . ' ' . $value->callback_request . ' +
    + + +
    +
    +
    + + + 0 ? ($currentPage - $range) : 1; + $endPage = ($currentPage + $range) < $totalPages ? ($currentPage + $range) : $totalPages; + + ?> + + +
    + + + \ No newline at end of file diff --git a/report_cronjob.php b/report_cronjob.php new file mode 100644 index 0000000..190cb2e --- /dev/null +++ b/report_cronjob.php @@ -0,0 +1,436 @@ + +get_all(); +// Log the start of the script +error_log('Cron job started: ' . date('Y-m-d H:i:s')); + + + + + +// $config = MkdConfig::get_instance()->get_config(); +// $apikey = $config['gohighlevel_key']; +foreach ($loctions as $key => $location) { + # code... + $apikey = $location->apikey; + $curlOptions1 = [ + CURLOPT_URL => "https://rest.gohighlevel.com/v1/locations", + CURLOPT_RETURNTRANSFER => true, + CURLOPT_ENCODING => "", + CURLOPT_MAXREDIRS => 10, + CURLOPT_TIMEOUT => 30, + CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1, + CURLOPT_CUSTOMREQUEST => "GET", + CURLOPT_HTTPHEADER => [ + "Accept: application/json", + "Authorization: Bearer " . $apikey, + "Version: 2021-04-15" + ], + ]; + + // Initialize cURL session + $ch1 = curl_init(); + + // Set cURL options + curl_setopt_array($ch1, $curlOptions1); + + // Execute cURL session and get the result + $response1 = curl_exec($ch1); + + if (curl_errno($ch1)) { + error_log('Curl error ' . curl_error($ch1)); + } else { + // Log the webhook response + error_log(' response ' . $response1); + + $response1 = json_decode($response1); + + + $currentTimestamp = time(); + $previousDayStartTimestamp2 = strtotime('yesterday', strtotime(date('Y-m-d', $currentTimestamp))) * 1000; + // Set up cURL options + $curlOptions = [ + CURLOPT_URL => "https://rest.gohighlevel.com/v1/contacts?startAfter=" . $previousDayStartTimestamp2, + CURLOPT_RETURNTRANSFER => true, + CURLOPT_ENCODING => "", + CURLOPT_MAXREDIRS => 10, + CURLOPT_TIMEOUT => 30, + CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1, + CURLOPT_CUSTOMREQUEST => "GET", + CURLOPT_HTTPHEADER => [ + "Accept: application/json", + "Authorization: Bearer " . $apikey, + "Version: 2021-04-15" + ], + ]; + + // Initialize cURL session + $ch = curl_init(); + + // Set cURL options + curl_setopt_array($ch, $curlOptions); + + // Execute cURL session and get the result + $response = curl_exec($ch); + + // Check for cURL errors + if (curl_errno($ch)) { + error_log('Curl error ' . curl_error($ch)); + } else { + // Log the webhook response + // error_log(' response ' . $response); + + $response = json_decode($response); + $con_count = count($response->contacts); + // error_log(' con ' . $con_count); + } + + // Close cURL session + curl_close($ch); + // foreach ($response->contacts as $con) { + // error_log(' response ' . json_encode($res)); + // if ($app->status == "booked") { + // $con_count[] = $con; + // } + // Log the webhook response + // } + // Set up cURL options + + // foreach ($response1 as $res1) { + // error_log(' response ' . json_encode($res1)); + + $curlOptions2 = [ + CURLOPT_URL => "https://rest.gohighlevel.com/v1/users?locationId={$response1->locations[0]->id}", + CURLOPT_RETURNTRANSFER => true, + CURLOPT_ENCODING => "", + CURLOPT_MAXREDIRS => 10, + CURLOPT_TIMEOUT => 30, + CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1, + CURLOPT_CUSTOMREQUEST => "GET", + CURLOPT_HTTPHEADER => [ + "Accept: application/json", + "Authorization: Bearer " . $apikey, + "Version: 2021-04-15" + ], + ]; + + // Initialize cURL session + $ch2 = curl_init(); + + // Set cURL options + curl_setopt_array($ch2, $curlOptions2); + + // Execute cURL session and get the result + $response2 = curl_exec($ch2); + if (curl_errno($ch2)) { + error_log('Curl error ' . curl_error($ch2)); + } else { + + $currentTimestamp = time(); + + + + // Calculate start timestamp for the previous day + $previousDayStartTimestamp = strtotime('yesterday', strtotime(date('Y-m-d', $currentTimestamp))) * 1000; + + // Calculate end timestamp for the previous day + $previousDayEndTimestamp = $previousDayStartTimestamp + (24 * 60 * 60 * 1000) - 1; + + // error_log(' startOfYesterdayTimestamp ' . $previousDayStartTimestamp); + // error_log(' endOfYesterdayTimestamp ' . $previousDayEndTimestamp); + $response2 = json_decode($response2); + $app_count = []; + + foreach ($response2->users as $res2) { + + $curlOptions3 = [ + CURLOPT_URL => "https://rest.gohighlevel.com/v1/appointments?userId=" . $res2->id . "&startDate=" . $previousDayStartTimestamp . "&endDate=" . $previousDayEndTimestamp, + CURLOPT_RETURNTRANSFER => true, + CURLOPT_ENCODING => "", + CURLOPT_MAXREDIRS => 10, + CURLOPT_TIMEOUT => 30, + CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1, + CURLOPT_CUSTOMREQUEST => "GET", + CURLOPT_HTTPHEADER => [ + "Accept: application/json", + "Authorization: Bearer " . $apikey, + "Version: 2021-04-15" + ], + ]; + + // Initialize cURL session + $ch3 = curl_init(); + + // Set cURL options + curl_setopt_array($ch3, $curlOptions3); + + // Execute cURL session and get the result + $response3 = curl_exec($ch3); + $user_app_count = []; + + if (curl_errno($ch3)) { + error_log('Curl error ' . curl_error($ch3)); + } else { + $response3 = json_decode($response3); + foreach ($response3->appointments as $app) { + if ($app->status == "booked") { + $user_app_count[] = $app; + } + // Log the webhook response + error_log(' response ' . $response3); + } + $app_count[] = $user_app_count; + } + curl_close($ch3); + } + } + curl_close($ch2); + + // Convert to seconds + $previousDayStartTimestampInSeconds = $previousDayStartTimestamp / 1000; + + // Format the timestamp + $formattedDate = date('Y-m-d', $previousDayStartTimestampInSeconds); + $project = $response1->locations[0]->name; + $date = $formattedDate; + + $new_lead = $con_count; + $outbound_dial = 0; + $pickup = 0; + $conversation = 0; + $booked_appointment = count($app_count); + $callback_request = 0; + $current_date = date('Y-m-d H:i:s'); + + $data = [ + 'project' => $project, + 'date' => $date, + 'new_lead' => $new_lead, + 'outbound_dial' => $outbound_dial, + 'pickup' => $pickup, + 'conversation' => $conversation, + 'booked_appointment' => $booked_appointment, + 'callback_request' => $callback_request, + 'created_at' => $current_date + ]; + + error_log(' data ' . json_encode($data)); + + // Insert data into the database using LicenseModel + // $reportModel = new ReportModel(); + // $reportModel->create($data); + } + curl_close($ch1); + // error_log(' response ' . $response1); +} + + + + + +// // Check for cURL errors +// if (curl_errno($ch)) { +// error_log('Curl error ' . curl_error($ch)); +// } else { +// // Log the webhook response +// // error_log(' response ' . $response); + +// $response = json_decode($response); +// foreach ($response->users as $res) { +// error_log(' response ' . json_encode($res)); + +// // Set up cURL options +// $curlOptions2 = [ +// CURLOPT_URL => "https://rest.gohighlevel.com/v1/locations/" . $res->roles->locationIds[0], +// CURLOPT_RETURNTRANSFER => true, +// CURLOPT_ENCODING => "", +// CURLOPT_MAXREDIRS => 10, +// CURLOPT_TIMEOUT => 30, +// CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1, +// CURLOPT_CUSTOMREQUEST => "GET", +// CURLOPT_HTTPHEADER => [ +// "Accept: application/json", +// "Authorization: Bearer " . $apikey, +// "Version: 2021-04-15" +// ], +// ]; + +// // Initialize cURL session +// $ch2 = curl_init(); + +// // Set cURL options +// curl_setopt_array($ch2, $curlOptions2); + +// // Execute cURL session and get the result +// $response2 = curl_exec($ch2); + +// if (curl_errno($ch2)) { +// error_log('Curl error ' . curl_error($ch2)); +// } else { +// // Log the webhook response +// // error_log(' response ' . $response2); + +// // $response2 = json_decode($response2); +// // foreach ($response2 as $res2) { +// // error_log(' response ' . json_encode($res2)); +// // } +// } +// curl_close($ch2); + +// $currentTimestamp = time(); + + + +// // Calculate start timestamp for the previous day +// $previousDayStartTimestamp = strtotime('yesterday', strtotime(date('Y-m-d', $currentTimestamp))) * 1000; + +// // Calculate end timestamp for the previous day +// $previousDayEndTimestamp = $previousDayStartTimestamp + (24 * 60 * 60 * 1000) - 1; + +// // error_log(' startOfYesterdayTimestamp ' . $previousDayStartTimestamp); +// // error_log(' endOfYesterdayTimestamp ' . $previousDayEndTimestamp); + +// $curlOptions3 = [ +// CURLOPT_URL => "https://rest.gohighlevel.com/v1/appointments?userId=" . $res->id . "&startDate=" . $previousDayStartTimestamp . "&endDate=" . $previousDayEndTimestamp, +// CURLOPT_RETURNTRANSFER => true, +// CURLOPT_ENCODING => "", +// CURLOPT_MAXREDIRS => 10, +// CURLOPT_TIMEOUT => 30, +// CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1, +// CURLOPT_CUSTOMREQUEST => "GET", +// CURLOPT_HTTPHEADER => [ +// "Accept: application/json", +// "Authorization: Bearer " . $apikey, +// "Version: 2021-04-15" +// ], +// ]; + +// // Initialize cURL session +// $ch3 = curl_init(); + +// // Set cURL options +// curl_setopt_array($ch3, $curlOptions3); + +// // Execute cURL session and get the result +// $response3 = curl_exec($ch3); +// $app_count = []; + +// if (curl_errno($ch3)) { +// error_log('Curl error ' . curl_error($ch3)); +// } else { +// $response3 = json_decode($response3); +// foreach ($response3->appointments as $app) { +// if ($app->status == "booked") { +// $app_count[] = $app; +// } +// // Log the webhook response +// error_log(' response ' . $response3); +// } +// // $response2 = json_decode($response2); +// // foreach ($response2 as $res2) { +// // error_log(' response ' . json_encode($res2)); +// // } +// } +// curl_close($ch3); +// // $curlOptions4 = [ +// // CURLOPT_URL => "https://rest.gohighlevel.com/v1/contacts?startAfter=" . $previousDayStartTimestamp, +// // CURLOPT_RETURNTRANSFER => true, +// // CURLOPT_ENCODING => "", +// // CURLOPT_MAXREDIRS => 10, +// // CURLOPT_TIMEOUT => 30, +// // CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1, +// // CURLOPT_CUSTOMREQUEST => "GET", +// // CURLOPT_HTTPHEADER => [ +// // "Accept: application/json", +// // "Authorization: Bearer " . $apikey, +// // "Version: 2021-04-15" +// // ], +// // ]; + +// // // Initialize cURL session +// // $ch4 = curl_init(); + +// // // Set cURL options +// // curl_setopt_array($ch4, $curlOptions4); + +// // // Execute cURL session and get the result +// // $response4 = curl_exec($ch4); +// // $con_count = []; + +// // if (curl_errno($ch4)) { +// // error_log('Curl error ' . curl_error($ch4)); +// // } else { +// // $response4 = json_decode($response4); +// // foreach ($response4->contacts as $con) { +// // if ($con->status == "booked") { +// // $con_count[] = $app; +// // } +// // // Log the webhook response +// // error_log(' response ' . $response4); +// // } +// // // $response2 = json_decode($response2); +// // // foreach ($response2 as $res2) { +// // // error_log(' response ' . json_encode($res2)); +// // // } +// // } +// // curl_close($ch4); +// // Convert to seconds +// $previousDayStartTimestampInSeconds = $previousDayStartTimestamp / 1000; + +// // Format the timestamp +// $formattedDate = date('Y-m-d', $previousDayStartTimestampInSeconds); +// $project = ""; +// $date = $formattedDate; +// $ghl_user_id = $res->id; +// $username = $res->name; +// $new_lead = 0; +// $outbound_dial = 0; +// $pickup = 0; +// $conversation = 0; +// $booked_appointment = count($app_count); +// $callback_request = 0; +// $current_date = date('Y-m-d H:i:s'); + +// $data = [ +// 'project' => $project, +// 'date' => $date, +// 'ghl_user_id' => $ghl_user_id, +// 'username' => $username, +// 'new_lead' => $new_lead, +// 'outbound_dial' => $outbound_dial, +// 'pickup' => $pickup, +// 'conversation' => $conversation, +// 'booked_appointment' => $booked_appointment, +// 'callback_request' => $callback_request, +// 'created_at' => $current_date +// ]; + +// error_log(' data ' . json_encode($data)); + +// // Insert data into the database using LicenseModel +// $reportModel = new ReportModel(); +// $reportModel->create($data); +// } +// } + +// // Close cURL session +// curl_close($ch); + + + +// Log the end of the script +error_log('Cron job completed: ' . date('Y-m-d H:i:s')); diff --git a/sample.tsv b/sample.tsv new file mode 100644 index 0000000..6bb3341 --- /dev/null +++ b/sample.tsv @@ -0,0 +1,118 @@ +Date Campaign Name Ad Set Name Ad Name Amount spent Reach Impressions CPM Unique Outbound Clicks Unique Outbound Click CTR Cost Per Unique Outbound Click New Leads CPL Optin % Appointments Booked CPA Appointments Booked % Showed Appointments CPS Qualified Appointments CPQ Sales CAC Cash Collected CC ROI Contract Value CV ROI +2024-11-19 Agencies W/ Call Centers | ABO | 5 Videos | USA, CA, AUS | Conversions [Schedules] [2024-11-18] Broad | Video Only | USA, CA, AUS | 18-45 | MF [2024-11-18] Mixed Ad Copy | Video | Hook 1 + Body 1 $124.37 293 318 $124.37 4 1.37% $9.89 0 $0.00 0.00% 0 $0.00 0.00% 0 $0.00 0 $0.00 0 $0.00 $0.00 0.00 $0.00 0.00 +2024-11-19 Agencies W/ Call Centers | ABO | 5 Videos | USA, CA, AUS | Conversions [Schedules] [2024-11-18] Broad | Video Only | USA, CA, AUS | 18-45 | MF [2024-11-18] Mixed Ad Copy | Video | Hook 2 + Body 1 $51.18 553 652 $78.50 6 1.08% $8.53 0 $0.00 0.00% 0 $0.00 0.00% 0 $0.00 0 $0.00 0 $0.00 $0.00 0.00 $0.00 0.00 +2024-11-19 Agencies W/ Call Centers | ABO | 5 Videos | USA, CA, AUS | Conversions [Schedules] [2024-11-18] Broad | Video Only | USA, CA, AUS | 18-45 | MF [2024-11-18] Mixed Ad Copy | Video | Hook 3 + Body 1 $42.20 648 715 $59.02 1 0.15% $42.20 1 $42.20 100.00% 1 $42.20 100.00% 1 $42.20 1 $42.20 1 $42.20 $2,000.00 47.39 $8,000.00 189.57 +2024-11-19 Agencies W/ Call Centers | ABO | 5 Videos | USA, CA, AUS | Conversions [Schedules] [2024-11-18] Broad | Video Only | USA, CA, AUS | 18-45 | MF [2024-11-18] Mixed Ad Copy | Video | Hook 4 + Body 1 $103.86 973 1064 $97.61 12 1.23% $8.65 7 $14.84 58.33% 1 $103.86 14.29% 1 $103.86 1 $103.86 0 $0.00 $0.00 0.00 $0.00 0.00 +2024-11-19 Agencies W/ Call Centers | ABO | 5 Videos | USA, CA, AUS | Conversions [Schedules] [2024-11-18] Broad | Video Only | USA, CA, AUS | 18-45 | MF [2024-11-18] Mixed Ad Copy | Video | Hook 5 + Body 1 $9.29 157 160 $58.06 0 0.00% $0.00 0 $0.00 0.00% 0 $0.00 0.00% 0 $0.00 0 $0.00 0 $0.00 $0.00 0.00 $0.00 0.00 +2024-11-20 Agencies W/ Call Centers | ABO | 5 Videos | USA, CA, AUS | Conversions [Schedules] [2024-11-18] Broad | Video Only | USA, CA, AUS | 18-45 | MF [2024-11-18] Mixed Ad Copy | Video | Hook 1 + Body 1 $11.40 364 397 $28.72 0 0.00% $0.00 0 $0.00 0.00% 0 $0.00 0.00% 0 $0.00 0 $0.00 0 $0.00 $0.00 0.00 $0.00 0.00 +2024-11-20 Agencies W/ Call Centers | ABO | 5 Videos | USA, CA, AUS | Conversions [Schedules] [2024-11-18] Broad | Video Only | USA, CA, AUS | 18-45 | MF [2024-11-18] Mixed Ad Copy | Video | Hook 2 + Body 1 $10.96 274 326 $33.62 2 0.73% $5.48 0 $0.00 0.00% 0 $0.00 0.00% 0 $0.00 0 $0.00 0 $0.00 $0.00 0.00 $0.00 0.00 +2024-11-20 Agencies W/ Call Centers | ABO | 5 Videos | USA, CA, AUS | Conversions [Schedules] [2024-11-18] Broad | Video Only | USA, CA, AUS | 18-45 | MF [2024-11-18] Mixed Ad Copy | Video | Hook 3 + Body 1 $35.29 1038 1149 $28.36 5 0.48% $6.52 0 $0.00 0.00% 0 $0.00 0.00% 0 $0.00 0 $0.00 0 $0.00 $0.00 0.00 $0.00 0.00 +2024-11-20 Agencies W/ Call Centers | ABO | 5 Videos | USA, CA, AUS | Conversions [Schedules] [2024-11-18] Broad | Video Only | USA, CA, AUS | 18-45 | MF [2024-11-18] Mixed Ad Copy | Video | Hook 4 + Body 1 $182.91 1943 2219 $82.43 18 0.93% $10.16 4 $45.73 22.22% 2 $91.46 50.00% 1 $182.91 1 $182.91 0 $0.00 $0.00 0.00 $0.00 0.00 +2024-11-20 Agencies W/ Call Centers | ABO | 5 Videos | USA, CA, AUS | Conversions [Schedules] [2024-11-18] Broad | Video Only | USA, CA, AUS | 18-45 | MF [2024-11-18] Mixed Ad Copy | Video | Hook 5 + Body 1 $6.65 351 395 $16.84 1 0.28% $6.65 0 $0.00 0.00% 0 $0.00 0.00% 0 $0.00 0 $0.00 0 $0.00 $0.00 0.00 $0.00 0.00 +2024-11-21 Agencies W/ Call Centers | ABO | 5 Videos | USA, CA, AUS | Conversions [Schedules] [2024-11-18] Broad | Video Only | USA, CA, AUS | 18-45 | MF [2024-11-18] Mixed Ad Copy | Video | Hook 1 + Body 1 $7.62 332 359 $21.23 3 0.90% $2.54 0 $0.00 0.00% 0 $0.00 0.00% 0 $0.00 0 $0.00 0 $0.00 $0.00 0.00 $0.00 0.00 +2024-11-21 Agencies W/ Call Centers | ABO | 5 Videos | USA, CA, AUS | Conversions [Schedules] [2024-11-18] Broad | Video Only | USA, CA, AUS | 18-45 | MF [2024-11-18] Mixed Ad Copy | Video | Hook 2 + Body 1 $1.03 27 31 $33.23 0 0.00% $0.00 0 $0.00 0.00% 0 $0.00 0.00% 0 $0.00 0 $0.00 0 $0.00 $0.00 0.00 $0.00 0.00 +2024-11-21 Agencies W/ Call Centers | ABO | 5 Videos | USA, CA, AUS | Conversions [Schedules] [2024-11-18] Broad | Video Only | USA, CA, AUS | 18-45 | MF [2024-11-18] Mixed Ad Copy | Video | Hook 3 + Body 1 $28.29 517 572 $49.46 6 1.16% $4.72 2 $14.15 33.33% 0 $0.00 0.00% 0 $0.00 0 $0.00 0 $0.00 $0.00 0.00 $0.00 0.00 +2024-11-21 Agencies W/ Call Centers | ABO | 5 Videos | USA, CA, AUS | Conversions [Schedules] [2024-11-18] Broad | Video Only | USA, CA, AUS | 18-45 | MF [2024-11-18] Mixed Ad Copy | Video | Hook 4 + Body 1 $208.82 2303 2581 $80.91 19 0.83% $10.99 4 $52.21 21.05% 3 $69.61 75.00% 1 $208.82 0 $0.00 0 $0.00 $0.00 0.00 $0.00 0.00 +2024-11-21 Agencies W/ Call Centers | ABO | 5 Videos | USA, CA, AUS | Conversions [Schedules] [2024-11-18] Broad | Video Only | USA, CA, AUS | 18-45 | MF [2024-11-18] Mixed Ad Copy | Video | Hook 5 + Body 1 $9.94 154 172 $57.79 0 0.00% $0.00 0 $0.00 0.00% 0 $0.00 0.00% 0 $0.00 0 $0.00 0 $0.00 $0.00 0.00 $0.00 0.00 +2024-11-22 Agencies W/ Call Centers | ABO | 5 Videos | USA, CA, AUS | Conversions [Schedules] [2024-11-18] Broad | Video Only | USA, CA, AUS | 18-45 | MF [2024-11-18] Mixed Ad Copy | Video | Hook 1 + Body 1 $5.77 151 170 $33.94 2 1.32% $2.89 0 $0.00 0.00% 0 $0.00 0.00% 0 $0.00 0 $0.00 0 $0.00 $0.00 0.00 $0.00 0.00 +2024-11-22 Agencies W/ Call Centers | ABO | 5 Videos | USA, CA, AUS | Conversions [Schedules] [2024-11-18] Broad | Video Only | USA, CA, AUS | 18-45 | MF [2024-11-18] Mixed Ad Copy | Video | Hook 3 + Body 1 $29.85 352 452 $66.04 4 1.14% $7.46 0 $0.00 0.00% 0 $0.00 0.00% 0 $0.00 0 $0.00 0 $0.00 $0.00 0.00 $0.00 0.00 +2024-11-22 Agencies W/ Call Centers | ABO | 5 Videos | USA, CA, AUS | Conversions [Schedules] [2024-11-18] Broad | Video Only | USA, CA, AUS | 18-45 | MF [2024-11-18] Mixed Ad Copy | Video | Hook 4 + Body 1 $204.92 1998 2289 $89.52 15 0.75% $13.66 3 $68.31 20.00% 1 $204.92 33.33% 1 $204.92 1 $204.92 0 $0.00 $0.00 0.00 $0.00 0.00 +2024-11-22 Agencies W/ Call Centers | ABO | 5 Videos | USA, CA, AUS | Conversions [Schedules] [2024-11-18] Broad | Video Only | USA, CA, AUS | 18-45 | MF [2024-11-18] Mixed Ad Copy | Video | Hook 5 + Body 1 $7.26 111 120 $60.50 2 1.80% $3.63 0 $0.00 0.00% 0 $0.00 0.00% 0 $0.00 0 $0.00 0 $0.00 $0.00 0.00 $0.00 0.00 +2024-11-23 Agencies W/ Call Centers | ABO | 5 Videos | USA, CA, AUS | Conversions [Schedules] [2024-11-18] Broad | Video Only | USA, CA, AUS | 18-45 | MF [2024-11-18] Mixed Ad Copy | Video | Hook 1 + Body 1 $0.35 22 23 $15.22 0 0.00% $0.00 0 $0.00 0.00% 0 $0.00 0.00% 0 $0.00 0 $0.00 0 $0.00 $0.00 0.00 $0.00 0.00 +2024-11-23 Agencies W/ Call Centers | ABO | 5 Videos | USA, CA, AUS | Conversions [Schedules] [2024-11-18] Broad | Video Only | USA, CA, AUS | 18-45 | MF [2024-11-18] Mixed Ad Copy | Video | Hook 3 + Body 1 $9.46 168 210 $45.05 2 1.19% $4.73 0 $0.00 0.00% 0 $0.00 0.00% 0 $0.00 0 $0.00 0 $0.00 $0.00 0.00 $0.00 0.00 +2024-11-23 Agencies W/ Call Centers | ABO | 5 Videos | USA, CA, AUS | Conversions [Schedules] [2024-11-18] Broad | Video Only | USA, CA, AUS | 18-45 | MF [2024-11-18] Mixed Ad Copy | Video | Hook 4 + Body 1 $192.97 2312 2599 $74.25 21 0.91% $9.19 7 $27.57 33.33% 2 $96.49 28.57% 1 $192.97 0 $0.00 0 $0.00 $0.00 0.00 $0.00 0.00 +2024-11-23 Agencies W/ Call Centers | ABO | 5 Videos | USA, CA, AUS | Conversions [Schedules] [2024-11-18] Broad | Video Only | USA, CA, AUS | 18-45 | MF [2024-11-18] Mixed Ad Copy | Video | Hook 5 + Body 1 $2.28 38 42 $54.29 0 0.00% $0.00 0 $0.00 0.00% 0 $0.00 0.00% 0 $0.00 0 $0.00 0 $0.00 $0.00 0.00 $0.00 0.00 +2024-11-24 Agencies W/ Call Centers | ABO | 5 Videos | USA, CA, AUS | Conversions [Schedules] [2024-11-18] Broad | Video Only | USA, CA, AUS | 18-45 | MF [2024-11-18] Mixed Ad Copy | Video | Hook 3 + Body 1 $9.72 313 348 $27.93 0 0.00% $0.00 0 $0.00 0.00% 1 $9.72 0.00% 1 $9.72 1 $9.72 1 $9.72 $2,000.00 205.76 $8,000.00 823.05 +2024-11-24 Agencies W/ Call Centers | ABO | 5 Videos | USA, CA, AUS | Conversions [Schedules] [2024-11-18] Broad | Video Only | USA, CA, AUS | 18-45 | MF [2024-11-18] Mixed Ad Copy | Video | Hook 4 + Body 1 $226.34 3092 3522 $64.26 35 1.13% $6.47 3 $75.45 8.57% 1 $226.34 33.33% 1 $226.34 1 $226.34 0 $0.00 $0.00 0.00 $0.00 0.00 +2024-11-24 Agencies W/ Call Centers | ABO | 5 Videos | USA, CA, AUS | Conversions [Schedules] [2024-11-18] Broad | Video Only | USA, CA, AUS | 18-45 | MF [2024-11-18] Mixed Ad Copy | Video | Hook 5 + Body 1 $2.90 52 55 $52.73 0 0.00% $0.00 0 $0.00 0.00% 0 $0.00 0.00% 0 $0.00 0 $0.00 0 $0.00 $0.00 0.00 $0.00 0.00 +2024-11-25 Agencies W/ Call Centers | ABO | 5 Videos | USA, CA, AUS | Conversions [Schedules] [2024-11-18] Broad | Video Only | USA, CA, AUS | 18-45 | MF [2024-11-18] Mixed Ad Copy | Video | Hook 3 + Body 1 $19.81 596 675 $29.35 4 0.67% $4.95 1 $19.81 25.00% 2 $9.91 200.00% 2 $9.91 2 $9.91 1 $19.81 $2,000.00 100.96 $8,000.00 403.84 +2024-11-25 Agencies W/ Call Centers | ABO | 5 Videos | USA, CA, AUS | Conversions [Schedules] [2024-11-18] Broad | Video Only | USA, CA, AUS | 18-45 | MF [2024-11-18] Mixed Ad Copy | Video | Hook 4 + Body 1 $235.19 3081 3504 $67.12 31 1.01% $7.59 11 $21.38 35.48% 3 $78.40 27.27% 1 $235.19 1 $235.19 1 $235.19 $500.00 2.13 $8,000.00 34.02 +2024-11-25 Agencies W/ Call Centers | ABO | 5 Videos | USA, CA, AUS | Conversions [Schedules] [2024-11-18] Broad | Video Only | USA, CA, AUS | 18-45 | MF [2024-11-18] Mixed Ad Copy | Video | Hook 5 + Body 1 $2.35 53 61 $38.52 1 1.89% $2.35 0 $0.00 0.00% 0 $0.00 0.00% 0 $0.00 0 $0.00 0 $0.00 $0.00 0.00 $0.00 0.00 +2024-11-26 Agencies W/ Call Centers | ABO | 5 Videos | USA, CA, AUS | Conversions [Schedules] [2024-11-18] Broad | Video Only | USA, CA, AUS | 18-45 | MF [2024-11-18] Mixed Ad Copy | Video | Hook 3 + Body 1 $19.89 318 385 $51.66 2 0.63% $9.95 0 $0.00 0.00% 0 $0.00 0.00% 0 $0.00 0 $0.00 0 $0.00 $0.00 0.00 $0.00 0.00 +2024-11-26 Agencies W/ Call Centers | ABO | 5 Videos | USA, CA, AUS | Conversions [Schedules] [2024-11-18] Broad | Video Only | USA, CA, AUS | 18-45 | MF [2024-11-18] Mixed Ad Copy | Video | Hook 4 + Body 1 $206.76 2932 3322 $62.24 24 0.82% $8.62 5 $41.35 20.83% 2 $103.38 40.00% 1 $206.76 1 $206.76 0 $0.00 $0.00 0.00 $0.00 0.00 +2024-11-26 Agencies W/ Call Centers | ABO | 5 Videos | USA, CA, AUS | Conversions [Schedules] [2024-11-18] Broad | Video Only | USA, CA, AUS | 18-45 | MF [2024-11-18] Mixed Ad Copy | Video | Hook 5 + Body 1 $5.17 104 114 $45.35 0 0.00% $0.00 0 $0.00 0.00% 0 $0.00 0.00% 0 $0.00 0 $0.00 0 $0.00 $0.00 0.00 $0.00 0.00 +2024-11-27 Agencies W/ Call Centers | ABO | 5 Videos | USA, CA, AUS | Conversions [Schedules] [2024-11-18] Broad | Video Only | USA, CA, AUS | 18-45 | MF [2024-11-18] Mixed Ad Copy | Video | Hook 3 + Body 1 $34.21 347 424 $80.68 3 0.86% $11.40 1 $34.21 33.33% 0 $0.00 0.00% 0 $0.00 0 $0.00 0 $0.00 $0.00 0.00 $0.00 0.00 +2024-11-27 Agencies W/ Call Centers | ABO | 5 Videos | USA, CA, AUS | Conversions [Schedules] [2024-11-18] Broad | Video Only | USA, CA, AUS | 18-45 | MF [2024-11-18] Mixed Ad Copy | Video | Hook 4 + Body 1 $212.45 2130 2415 $87.97 20 0.94% $10.62 3 $70.82 15.00% 0 $0.00 0.00% 0 $0.00 0 $0.00 0 $0.00 $0.00 0.00 $0.00 0.00 +2024-11-27 Agencies W/ Call Centers | ABO | 5 Videos | USA, CA, AUS | Conversions [Schedules] [2024-11-18] Broad | Video Only | USA, CA, AUS | 18-45 | MF [2024-11-18] Mixed Ad Copy | Video | Hook 5 + Body 1 $6.62 187 210 $31.52 1 0.53% $6.62 0 $0.00 0.00% 0 $0.00 0.00% 0 $0.00 0 $0.00 0 $0.00 $0.00 0.00 $0.00 0.00 +2024-11-28 Agencies W/ Call Centers | ABO | 5 Videos | USA, CA, AUS | Conversions [Schedules] [2024-11-18] Broad | Video Only | USA, CA, AUS | 18-45 | MF [2024-11-18] Mixed Ad Copy | Video | Hook 3 + Body 1 $13.13 105 124 $105.89 0 0.00% $0.00 0 $0.00 0.00% 0 $0.00 0.00% 0 $0.00 0 $0.00 0 $0.00 $0.00 0.00 $0.00 0.00 +2024-11-28 Agencies W/ Call Centers | ABO | 5 Videos | USA, CA, AUS | Conversions [Schedules] [2024-11-18] Broad | Video Only | USA, CA, AUS | 18-45 | MF [2024-11-18] Mixed Ad Copy | Video | Hook 4 + Body 1 $224.50 2431 2824 $79.50 21 0.86% $10.69 3 $74.83 14.29% 0 $0.00 0.00% 0 $0.00 0 $0.00 0 $0.00 $0.00 0.00 $0.00 0.00 +2024-11-28 Agencies W/ Call Centers | ABO | 5 Videos | USA, CA, AUS | Conversions [Schedules] [2024-11-18] Broad | Video Only | USA, CA, AUS | 18-45 | MF [2024-11-18] Mixed Ad Copy | Video | Hook 5 + Body 1 $5.93 64 77 $77.01 0 0.00% $0.00 0 $0.00 0.00% 0 $0.00 0.00% 0 $0.00 0 $0.00 0 $0.00 $0.00 0.00 $0.00 0.00 +2024-11-29 Agencies W/ Call Centers | ABO | 5 Videos | USA, CA, AUS | Conversions [Schedules] [2024-11-18] Broad | Video Only | USA, CA, AUS | 18-45 | MF [2024-11-18] Mixed Ad Copy | Video | Hook 3 + Body 1 $3.77 48 56 $67.32 0 0.00% $0.00 0 $0.00 0.00% 0 $0.00 0.00% 0 $0.00 0 $0.00 0 $0.00 $0.00 0.00 $0.00 0.00 +2024-11-29 Agencies W/ Call Centers | ABO | 5 Videos | USA, CA, AUS | Conversions [Schedules] [2024-11-18] Broad | Video Only | USA, CA, AUS | 18-45 | MF [2024-11-18] Mixed Ad Copy | Video | Hook 4 + Body 1 $221.51 2070 2438 $90.86 16 0.77% $13.84 2 $110.76 12.50% 0 $0.00 0.00% 0 $0.00 0 $0.00 0 $0.00 $0.00 0.00 $0.00 0.00 +2024-11-29 Agencies W/ Call Centers | ABO | 5 Videos | USA, CA, AUS | Conversions [Schedules] [2024-11-18] Broad | Video Only | USA, CA, AUS | 18-45 | MF [2024-11-18] Mixed Ad Copy | Video | Hook 5 + Body 1 $10.21 61 74 $137.97 2 3.28% $5.11 0 $0.00 0.00% 0 $0.00 0.00% 0 $0.00 0 $0.00 0 $0.00 $0.00 0.00 $0.00 0.00 +2024-11-30 Agencies W/ Call Centers | ABO | 5 Videos | USA, CA, AUS | Conversions [Schedules] [2024-11-18] Broad | Video Only | USA, CA, AUS | 18-45 | MF [2024-11-18] Mixed Ad Copy | Video | Hook 3 + Body 1 $12.08 54 66 $183.03 1 1.85% $12.08 0 $0.00 0.00% 0 $0.00 0.00% 0 $0.00 0 $0.00 0 $0.00 $0.00 0.00 $0.00 0.00 +2024-11-30 Agencies W/ Call Centers | ABO | 5 Videos | USA, CA, AUS | Conversions [Schedules] [2024-11-18] Broad | Video Only | USA, CA, AUS | 18-45 | MF [2024-11-18] Mixed Ad Copy | Video | Hook 4 + Body 1 $206.63 1776 2101 $98.35 19 1.07% $10.88 1 $206.63 5.26% 0 $0.00 0.00% 0 $0.00 0 $0.00 0 $0.00 $0.00 0.00 $0.00 0.00 +2024-11-30 Agencies W/ Call Centers | ABO | 5 Videos | USA, CA, AUS | Conversions [Schedules] [2024-11-18] Broad | Video Only | USA, CA, AUS | 18-45 | MF [2024-11-18] Mixed Ad Copy | Video | Hook 5 + Body 1 $6.16 58 69 $89.28 1 1.72% $6.16 0 $0.00 0.00% 0 $0.00 0.00% 0 $0.00 0 $0.00 0 $0.00 $0.00 0.00 $0.00 0.00 +2024-12-01 Agencies W/ Call Centers | ABO | 5 Videos | USA, CA, AUS | Conversions [Schedules] [2024-11-18] Broad | Video Only | USA, CA, AUS | 18-45 | MF [2024-11-18] Mixed Ad Copy | Video | Hook 3 + Body 1 $8.38 80 85 $98.59 1 1.25% $8.38 0 $0.00 0.00% 0 $0.00 0.00% 0 $0.00 0 $0.00 0 $0.00 $0.00 0.00 $0.00 0.00 +2024-12-01 Agencies W/ Call Centers | ABO | 5 Videos | USA, CA, AUS | Conversions [Schedules] [2024-11-18] Broad | Video Only | USA, CA, AUS | 18-45 | MF [2024-11-18] Mixed Ad Copy | Video | Hook 4 + Body 1 $271.01 2241 2659 $101.92 15 0.67% $18.07 3 $90.34 20.00% 1 $271.01 33.33% 0 $0.00 0 $0.00 0 $0.00 $0.00 0.00 $0.00 0.00 +2024-12-01 [UNKNOWN] [UNKNOWN] [UNKNOWN] 1 1 1 $0.00 1 $0.00 1 $0.00 $2,000.00 #DIV/0! $8,000.00 #DIV/0! +2024-12-02 Agencies W/ Call Centers | ABO | 5 Videos | USA, CA, AUS | Conversions [Schedules] [2024-11-18] Broad | Video Only | USA, CA, AUS | 18-45 | MF [2024-11-18] Mixed Ad Copy | Video | Hook 3 + Body 1 $156.42 1032 1235 $126.66 9 0.87% $17.38 1 $156.42 11.11% 0 $0.00 0.00% 0 $0.00 0 $0.00 0 $0.00 $0.00 0.00 $0.00 0.00 +2024-12-02 Agencies W/ Call Centers | ABO | 5 Videos | USA, CA, AUS | Conversions [Schedules] [2024-11-18] Broad | Video Only | USA, CA, AUS | 18-45 | MF [2024-11-18] Mixed Ad Copy | Video | Hook 4 + Body 1 $92.71 949 1025 $90.45 4 0.42% $23.18 0 $0.00 0.00% 0 $0.00 0.00% 0 $0.00 0 $0.00 0 $0.00 $0.00 0.00 $0.00 0.00 +2024-12-02 Agencies W/ Call Centers | ABO | 1 Video | USA, CA, AUS | Conversions [Schedules] [2024-12-02] Broad | Video Only | USA, CA, AUS | 18-45 | MF [2024-12-02] Mixed Ad Copy | Video | Hook 3 + Body 1 $34.08 226 238 $143.19 1 0.44% $34.08 1 $34.08 100.00% 0 $0.00 0.00% 0 $0.00 0 $0.00 0 $0.00 $0.00 0.00 $0.00 0.00 +2024-12-02 Agencies W/ Call Centers | ABO | 5 Insta Images | USA, CA, AUS | Conversions [Schedules] [2024-12-02] Broad | Insta Image Only | USA, CA, AUS | 18-45 | MF [2024-12-02] Mixed Ad Copy | Image | Image 5 $16.33 147 224 $72.90 2 1.36% $8.16 0 $0.00 0.00% 0 $0.00 0.00% 0 $0.00 0 $0.00 0 $0.00 $0.00 0.00 $0.00 0.00 +2024-12-02 Agencies W/ Call Centers | ABO | 5 Insta Images | USA, CA, AUS | Conversions [Schedules] [2024-12-02] Broad | Insta Image Only | USA, CA, AUS | 18-45 | MF [2024-12-02] Mixed Ad Copy | Image | Image 3 $40.62 468 565 $71.89 8 1.71% $5.08 0 $0.00 0.00% 0 $0.00 0.00% 0 $0.00 0 $0.00 0 $0.00 $0.00 0.00 $0.00 0.00 +2024-12-02 Agencies W/ Call Centers | ABO | 5 Insta Images | USA, CA, AUS | Conversions [Schedules] [2024-12-02] Broad | Insta Image Only | USA, CA, AUS | 18-45 | MF [2024-12-02] Mixed Ad Copy | Image | Image 1 $1.82 11 12 $151.67 0 0.00% $0.00 0 $0.00 0.00% 0 $0.00 0.00% 0 $0.00 0 $0.00 0 $0.00 $0.00 0.00 $0.00 0.00 +2024-12-02 Agencies W/ Call Centers | ABO | 5 Insta Images | USA, CA, AUS | Conversions [Schedules] [2024-12-02] Broad | Insta Image Only | USA, CA, AUS | 18-45 | MF [2024-12-02] Mixed Ad Copy | Image | Image 4 $23.13 262 297 $77.88 4 1.53% $5.78 1 $23.13 25.00% 0 $0.00 0.00% 0 $0.00 0 $0.00 0 $0.00 $0.00 0.00 $0.00 0.00 +2024-12-02 Agencies W/ Call Centers | ABO | 5 Insta Images | USA, CA, AUS | Conversions [Schedules] [2024-12-02] Broad | Insta Image Only | USA, CA, AUS | 18-45 | MF [2024-12-02] Mixed Ad Copy | Image | Image 2 $31.41 406 504 $62.32 5 1.23% $6.28 0 $0.00 0.00% 0 $0.00 0.00% 0 $0.00 0 $0.00 0 $0.00 $0.00 0.00 $0.00 0.00 +2024-12-03 Agencies W/ Call Centers | ABO | 2 Videos | USA, CA, AUS | Conversions [Schedules] [2024-12-03] Broad | Video Only | USA, CA, AUS | 18-45 | MF [2024-12-03] Mixed Ad Copy | Video | Hook 4b + Body 1 $3.22 54 60 $53.67 1 1.85% $3.22 0 $0.00 0.00% 0 $0.00 0.00% 0 $0.00 0 $0.00 0 $0.00 $0.00 0.00 $0.00 0.00 +2024-12-03 Agencies W/ Call Centers | ABO | 2 Videos | USA, CA, AUS | Conversions [Schedules] [2024-12-03] Broad | Video Only | USA, CA, AUS | 18-45 | MF [2024-12-03] Mixed Ad Copy | Video | Hook 4a + Body 1 $118.94 1186 1293 $91.99 14 1.18% $8.50 3 $39.65 21.43% 0 $0.00 0.00% 0 $0.00 0 $0.00 0 $0.00 $0.00 0.00 $0.00 0.00 +2024-12-03 Agencies W/ Call Centers | ABO | 1 Video | USA, CA, AUS | Conversions [Schedules] [2024-12-02] Broad | Video Only | USA, CA, AUS | 18-45 | MF [2024-12-02] Mixed Ad Copy | Video | Hook 3 + Body 1 $51.38 585 653 $78.68 2 0.34% $25.69 0 $0.00 0.00% 0 $0.00 0.00% 0 $0.00 0 $0.00 0 $0.00 $0.00 0.00 $0.00 0.00 +2024-12-03 Agencies W/ Call Centers | ABO | 5 Insta Images | USA, CA, AUS | Conversions [Schedules] [2024-12-02] Broad | Insta Image Only | USA, CA, AUS | 18-45 | MF [2024-12-02] Mixed Ad Copy | Image | Image 5 $48.65 452 741 $65.65 7 1.55% $6.95 1 $48.65 14.29% 1 $48.65 100.00% 1 $48.65 1 $48.65 0 $0.00 $0.00 0.00 $0.00 0.00 +2024-12-03 Agencies W/ Call Centers | ABO | 5 Insta Images | USA, CA, AUS | Conversions [Schedules] [2024-12-02] Broad | Insta Image Only | USA, CA, AUS | 18-45 | MF [2024-12-02] Mixed Ad Copy | Image | Image 3 $23.18 337 382 $60.68 5 1.48% $4.64 0 $0.00 0.00% 0 $0.00 0.00% 0 $0.00 0 $0.00 0 $0.00 $0.00 0.00 $0.00 0.00 +2024-12-03 Agencies W/ Call Centers | ABO | 5 Insta Images | USA, CA, AUS | Conversions [Schedules] [2024-12-02] Broad | Insta Image Only | USA, CA, AUS | 18-45 | MF [2024-12-02] Mixed Ad Copy | Image | Image 1 $3.36 51 52 $64.62 0 0.00% $0.00 0 $0.00 0.00% 0 $0.00 0.00% 0 $0.00 0 $0.00 0 $0.00 $0.00 0.00 $0.00 0.00 +2024-12-03 Agencies W/ Call Centers | ABO | 5 Insta Images | USA, CA, AUS | Conversions [Schedules] [2024-12-02] Broad | Insta Image Only | USA, CA, AUS | 18-45 | MF [2024-12-02] Mixed Ad Copy | Image | Image 4 $15.01 145 160 $93.81 3 2.07% $5.00 0 $0.00 0.00% 0 $0.00 0.00% 0 $0.00 0 $0.00 0 $0.00 $0.00 0.00 $0.00 0.00 +2024-12-03 Agencies W/ Call Centers | ABO | 5 Insta Images | USA, CA, AUS | Conversions [Schedules] [2024-12-02] Broad | Insta Image Only | USA, CA, AUS | 18-45 | MF [2024-12-02] Mixed Ad Copy | Image | Image 2 $6.13 88 98 $62.55 0 0.00% $0.00 0 $0.00 0.00% 0 $0.00 0.00% 0 $0.00 0 $0.00 0 $0.00 $0.00 0.00 $0.00 0.00 +2024-12-03 Agencies W/ Call Centers | ABO | 5 Videos | USA, CA, AUS | Conversions [Schedules] [2024-11-18] Broad | Video Only | USA, CA, AUS | 18-45 | MF [2024-11-18] Mixed Ad Copy | Video | Hook 3 + Body 1 $95.98 752 845 $113.59 5 0.66% $19.20 0 $0.00 0.00% 0 $0.00 0.00% 0 $0.00 0 $0.00 0 $0.00 $0.00 0.00 $0.00 0.00 +2024-12-04 Agencies W/ Call Centers | ABO | 2 Videos | USA, CA, AUS | Conversions [Schedules] [2024-12-03] Broad | Video Only | USA, CA, AUS | 18-45 | MF [2024-12-03] Mixed Ad Copy | Video | Hook 4b + Body 1 $7.75 55 68 $113.97 0 0.00% $0.00 0 $0.00 0.00% 0 $0.00 0.00% 0 $0.00 0 $0.00 0 $0.00 $0.00 0.00 $0.00 0.00 +2024-12-04 Agencies W/ Call Centers | ABO | 2 Videos | USA, CA, AUS | Conversions [Schedules] [2024-12-03] Broad | Video Only | USA, CA, AUS | 18-45 | MF [2024-12-03] Mixed Ad Copy | Video | Hook 4a + Body 1 $201.15 1871 2211 $90.98 21 1.12% $9.58 5 $40.23 23.81% 1 $201.15 20.00% 0 $0.00 0 $0.00 0 $0.00 $0.00 0.00 $0.00 0.00 +2024-12-04 Agencies W/ Call Centers | ABO | 1 Video | USA, CA, AUS | Conversions [Schedules] [2024-12-02] Broad | Video Only | USA, CA, AUS | 18-45 | MF [2024-12-02] Mixed Ad Copy | Video | Hook 3 + Body 1 $59.56 632 740 $80.49 2 0.32% $29.78 0 $0.00 0.00% 0 $0.00 0.00% 0 $0.00 0 $0.00 0 $0.00 $0.00 0.00 $0.00 0.00 +2024-12-04 Agencies W/ Call Centers | ABO | 5 Insta Images | USA, CA, AUS | Conversions [Schedules] [2024-12-02] Broad | Insta Image Only | USA, CA, AUS | 18-45 | MF [2024-12-02] Mixed Ad Copy | Image | Image 5 $54.63 518 754 $72.45 7 1.35% $7.80 0 $0.00 0.00% 0 $0.00 0.00% 0 $0.00 0 $0.00 0 $0.00 $0.00 0.00 $0.00 0.00 +2024-12-04 Agencies W/ Call Centers | ABO | 5 Insta Images | USA, CA, AUS | Conversions [Schedules] [2024-12-02] Broad | Insta Image Only | USA, CA, AUS | 18-45 | MF [2024-12-02] Mixed Ad Copy | Image | Image 3 $19.26 194 211 $91.28 2 1.03% $9.63 1 $19.26 50.00% 0 $0.00 0.00% 0 $0.00 0 $0.00 0 $0.00 $0.00 0.00 $0.00 0.00 +2024-12-04 Agencies W/ Call Centers | ABO | 5 Insta Images | USA, CA, AUS | Conversions [Schedules] [2024-12-02] Broad | Insta Image Only | USA, CA, AUS | 18-45 | MF [2024-12-02] Mixed Ad Copy | Image | Image 1 $2.01 17 17 $118.24 0 0.00% $0.00 0 $0.00 0.00% 0 $0.00 0.00% 0 $0.00 0 $0.00 0 $0.00 $0.00 0.00 $0.00 0.00 +2024-12-04 Agencies W/ Call Centers | ABO | 5 Insta Images | USA, CA, AUS | Conversions [Schedules] [2024-12-02] Broad | Insta Image Only | USA, CA, AUS | 18-45 | MF [2024-12-02] Mixed Ad Copy | Image | Image 4 $12.08 115 150 $80.53 0 0.00% $0.00 0 $0.00 0.00% 0 $0.00 0.00% 0 $0.00 0 $0.00 0 $0.00 $0.00 0.00 $0.00 0.00 +2024-12-04 Agencies W/ Call Centers | ABO | 5 Insta Images | USA, CA, AUS | Conversions [Schedules] [2024-12-02] Broad | Insta Image Only | USA, CA, AUS | 18-45 | MF [2024-12-02] Mixed Ad Copy | Image | Image 2 $4.96 48 55 $90.18 0 0.00% $0.00 0 $0.00 0.00% 0 $0.00 0.00% 0 $0.00 0 $0.00 0 $0.00 $0.00 0.00 $0.00 0.00 +2024-12-05 Agencies W/ Call Centers | ABO | 2 Videos | USA, CA, AUS | Conversions [Schedules] [2024-12-03] Broad | Video Only | USA, CA, AUS | 18-45 | MF [2024-12-03] Mixed Ad Copy | Video | Hook 4b + Body 1 $7.54 68 81 $93.09 1 1.47% $7.54 0 $0.00 0.00% 0 $0.00 0.00% 0 $0.00 0 $0.00 0 $0.00 $0.00 0.00 $0.00 0.00 +2024-12-05 Agencies W/ Call Centers | ABO | 2 Videos | USA, CA, AUS | Conversions [Schedules] [2024-12-03] Broad | Video Only | USA, CA, AUS | 18-45 | MF [2024-12-03] Mixed Ad Copy | Video | Hook 4a + Body 1 $187.92 1818 2056 $91.40 15 0.83% $12.53 2 $93.96 13.33% 2 $93.96 100.00% 2 $93.96 2 $93.96 1 $187.92 $500.00 2.66 $8,000.00 42.57 +2024-12-05 Agencies W/ Call Centers | ABO | 5 Insta Images | USA, CA, AUS | Conversions [Schedules] [2024-12-02] Broad | Insta Image Only | USA, CA, AUS | 18-45 | MF [2024-12-02] Mixed Ad Copy | Image | Image 5 $39.97 491 618 $64.68 7 1.43% $5.71 1 $39.97 14.29% 0 $0.00 0.00% 0 $0.00 0 $0.00 0 $0.00 $0.00 0.00 $0.00 0.00 +2024-12-05 Agencies W/ Call Centers | ABO | 5 Insta Images | USA, CA, AUS | Conversions [Schedules] [2024-12-02] Broad | Insta Image Only | USA, CA, AUS | 18-45 | MF [2024-12-02] Mixed Ad Copy | Image | Image 3 $30.22 322 384 $78.70 7 2.17% $4.32 0 $0.00 0.00% 0 $0.00 0.00% 0 $0.00 0 $0.00 0 $0.00 $0.00 0.00 $0.00 0.00 +2024-12-05 Agencies W/ Call Centers | ABO | 5 Insta Images | USA, CA, AUS | Conversions [Schedules] [2024-12-02] Broad | Insta Image Only | USA, CA, AUS | 18-45 | MF [2024-12-02] Mixed Ad Copy | Image | Image 1 $0.13 1 1 $130.00 0 0.00% $0.00 0 $0.00 0.00% 0 $0.00 0.00% 0 $0.00 0 $0.00 0 $0.00 $0.00 0.00 $0.00 0.00 +2024-12-05 Agencies W/ Call Centers | ABO | 5 Insta Images | USA, CA, AUS | Conversions [Schedules] [2024-12-02] Broad | Insta Image Only | USA, CA, AUS | 18-45 | MF [2024-12-02] Mixed Ad Copy | Image | Image 4 $24.55 252 304 $80.76 3 1.19% $8.18 0 $0.00 0.00% 0 $0.00 0.00% 0 $0.00 0 $0.00 0 $0.00 $0.00 0.00 $0.00 0.00 +2024-12-05 Agencies W/ Call Centers | ABO | 5 Insta Images | USA, CA, AUS | Conversions [Schedules] [2024-12-02] Broad | Insta Image Only | USA, CA, AUS | 18-45 | MF [2024-12-02] Mixed Ad Copy | Image | Image 2 $6.78 95 102 $66.47 0 0.00% $0.00 0 $0.00 0.00% 0 $0.00 0.00% 0 $0.00 0 $0.00 0 $0.00 $0.00 0.00 $0.00 0.00 +2024-12-06 Agencies W/ Call Centers | ABO | 2 Videos | USA, CA, AUS | Conversions [Schedules] [2024-12-03] Broad | Video Only | USA, CA, AUS | 18-45 | MF [2024-12-03] Mixed Ad Copy | Video | Hook 4b + Body 1 $8.51 75 91 $93.52 0 0.00% $0.00 0 $0.00 0.00% 0 $0.00 0.00% 0 $0.00 0 $0.00 0 $0.00 $0.00 0.00 $0.00 0.00 +2024-12-06 Agencies W/ Call Centers | ABO | 2 Videos | USA, CA, AUS | Conversions [Schedules] [2024-12-03] Broad | Video Only | USA, CA, AUS | 18-45 | MF [2024-12-03] Mixed Ad Copy | Video | Hook 4a + Body 1 $181.20 1642 1869 $96.95 20 1.22% $9.06 8 $22.65 40.00% 1 $181.20 12.50% 1 $181.20 1 $181.20 1 $181.20 $2,000.00 11.04 $8,000.00 44.15 +2024-12-06 Agencies W/ Call Centers | ABO | 5 Insta Images | USA, CA, AUS | Conversions [Schedules] [2024-12-02] Broad | Insta Image Only | USA, CA, AUS | 18-45 | MF [2024-12-02] Mixed Ad Copy | Image | Image 5 $54.81 581 807 $67.92 7 1.20% $7.83 0 $0.00 0.00% 0 $0.00 0.00% 0 $0.00 0 $0.00 0 $0.00 $0.00 0.00 $0.00 0.00 +2024-12-06 Agencies W/ Call Centers | ABO | 5 Insta Images | USA, CA, AUS | Conversions [Schedules] [2024-12-02] Broad | Insta Image Only | USA, CA, AUS | 18-45 | MF [2024-12-02] Mixed Ad Copy | Image | Image 3 $27.29 331 372 $73.36 7 2.11% $3.90 0 $0.00 0.00% 0 $0.00 0.00% 0 $0.00 0 $0.00 0 $0.00 $0.00 0.00 $0.00 0.00 +2024-12-06 Agencies W/ Call Centers | ABO | 5 Insta Images | USA, CA, AUS | Conversions [Schedules] [2024-12-02] Broad | Insta Image Only | USA, CA, AUS | 18-45 | MF [2024-12-02] Mixed Ad Copy | Image | Image 1 $0.10 2 2 $50.00 0 0.00% $0.00 0 $0.00 0.00% 0 $0.00 0.00% 0 $0.00 0 $0.00 0 $0.00 $0.00 0.00 $0.00 0.00 +2024-12-06 Agencies W/ Call Centers | ABO | 5 Insta Images | USA, CA, AUS | Conversions [Schedules] [2024-12-02] Broad | Insta Image Only | USA, CA, AUS | 18-45 | MF [2024-12-02] Mixed Ad Copy | Image | Image 4 $7.38 86 94 $78.51 0 0.00% $0.00 0 $0.00 0.00% 0 $0.00 0.00% 0 $0.00 0 $0.00 0 $0.00 $0.00 0.00 $0.00 0.00 +2024-12-06 Agencies W/ Call Centers | ABO | 5 Insta Images | USA, CA, AUS | Conversions [Schedules] [2024-12-02] Broad | Insta Image Only | USA, CA, AUS | 18-45 | MF [2024-12-02] Mixed Ad Copy | Image | Image 2 $3.89 51 54 $72.04 0 0.00% $0.00 0 $0.00 0.00% 0 $0.00 0.00% 0 $0.00 0 $0.00 0 $0.00 $0.00 0.00 $0.00 0.00 +2024-12-07 Agencies W/ Call Centers | ABO | 2 Videos | USA, CA, AUS | Conversions [Schedules] [2024-12-03] Broad | Video Only | USA, CA, AUS | 18-45 | MF [2024-12-03] Mixed Ad Copy | Video | Hook 4b + Body 1 $10.21 111 134 $76.19 0 0.00% $0.00 0 $0.00 0.00% 0 $0.00 0.00% 0 $0.00 0 $0.00 0 $0.00 $0.00 0.00 $0.00 0.00 +2024-12-07 Agencies W/ Call Centers | ABO | 2 Videos | USA, CA, AUS | Conversions [Schedules] [2024-12-03] Broad | Video Only | USA, CA, AUS | 18-45 | MF [2024-12-03] Mixed Ad Copy | Video | Hook 4a + Body 1 $183.68 1975 2247 $81.74 14 0.71% $13.12 4 $45.92 28.57% 1 $183.68 25.00% 1 $183.68 0 $0.00 0 $0.00 $0.00 0.00 $0.00 0.00 +2024-12-07 Agencies W/ Call Centers | ABO | 5 Insta Images | USA, CA, AUS | Conversions [Schedules] [2024-12-02] Broad | Insta Image Only | USA, CA, AUS | 18-45 | MF [2024-12-02] Mixed Ad Copy | Image | Image 5 $66.26 794 907 $73.05 17 2.14% $3.90 1 $66.26 5.88% 0 $0.00 0.00% 0 $0.00 0 $0.00 0 $0.00 $0.00 0.00 $0.00 0.00 +2024-12-07 Agencies W/ Call Centers | ABO | 5 Insta Images | USA, CA, AUS | Conversions [Schedules] [2024-12-02] Broad | Insta Image Only | USA, CA, AUS | 18-45 | MF [2024-12-02] Mixed Ad Copy | Image | Image 3 $20.50 292 323 $63.47 2 0.68% $10.25 1 $20.50 50.00% 0 $0.00 0.00% 0 $0.00 0 $0.00 0 $0.00 $0.00 0.00 $0.00 0.00 +2024-12-07 Agencies W/ Call Centers | ABO | 5 Insta Images | USA, CA, AUS | Conversions [Schedules] [2024-12-02] Broad | Insta Image Only | USA, CA, AUS | 18-45 | MF [2024-12-02] Mixed Ad Copy | Image | Image 1 $0.31 5 5 $62.00 0 0.00% $0.00 0 $0.00 0.00% 0 $0.00 0.00% 0 $0.00 0 $0.00 0 $0.00 $0.00 0.00 $0.00 0.00 +2024-12-07 Agencies W/ Call Centers | ABO | 5 Insta Images | USA, CA, AUS | Conversions [Schedules] [2024-12-02] Broad | Insta Image Only | USA, CA, AUS | 18-45 | MF [2024-12-02] Mixed Ad Copy | Image | Image 4 $10.63 130 155 $68.58 1 0.77% $10.63 0 $0.00 0.00% 0 $0.00 0.00% 0 $0.00 0 $0.00 0 $0.00 $0.00 0.00 $0.00 0.00 +2024-12-07 Agencies W/ Call Centers | ABO | 5 Insta Images | USA, CA, AUS | Conversions [Schedules] [2024-12-02] Broad | Insta Image Only | USA, CA, AUS | 18-45 | MF [2024-12-02] Mixed Ad Copy | Image | Image 2 $2.92 50 62 $47.10 0 0.00% $0.00 0 $0.00 0.00% 0 $0.00 0.00% 0 $0.00 0 $0.00 0 $0.00 $0.00 0.00 $0.00 0.00 +2024-12-08 Agencies W/ Call Centers | ABO | 2 Videos | USA, CA, AUS | Conversions [Schedules] [2024-12-03] Broad | Video Only | USA, CA, AUS | 18-45 | MF [2024-12-03] Mixed Ad Copy | Video | Hook 4b + Body 1 $5.16 91 104 $49.62 0 0.00% $0.00 0 $0.00 0.00% 0 $0.00 0.00% 0 $0.00 0 $0.00 0 $0.00 $0.00 0.00 $0.00 0.00 +2024-12-08 Agencies W/ Call Centers | ABO | 2 Videos | USA, CA, AUS | Conversions [Schedules] [2024-12-03] Broad | Video Only | USA, CA, AUS | 18-45 | MF [2024-12-03] Mixed Ad Copy | Video | Hook 4a + Body 1 $206.91 2188 2439 $84.83 15 0.69% $13.79 1 $206.91 6.67% 0 $0.00 0.00% 0 $0.00 0 $0.00 0 $0.00 $0.00 0.00 $0.00 0.00 +2024-12-08 Agencies W/ Call Centers | ABO | 5 Insta Images | USA, CA, AUS | Conversions [Schedules] [2024-12-02] Broad | Insta Image Only | USA, CA, AUS | 18-45 | MF [2024-12-02] Mixed Ad Copy | Image | Image 5 $60.83 573 658 $92.45 8 1.40% $7.60 0 $0.00 0.00% 0 $0.00 0.00% 0 $0.00 0 $0.00 0 $0.00 $0.00 0.00 $0.00 0.00 +2024-12-08 Agencies W/ Call Centers | ABO | 5 Insta Images | USA, CA, AUS | Conversions [Schedules] [2024-12-02] Broad | Insta Image Only | USA, CA, AUS | 18-45 | MF [2024-12-02] Mixed Ad Copy | Image | Image 3 $20.98 140 150 $139.87 0 0.00% $0.00 0 $0.00 0.00% 0 $0.00 0.00% 0 $0.00 0 $0.00 0 $0.00 $0.00 0.00 $0.00 0.00 +2024-12-08 Agencies W/ Call Centers | ABO | 5 Insta Images | USA, CA, AUS | Conversions [Schedules] [2024-12-02] Broad | Insta Image Only | USA, CA, AUS | 18-45 | MF [2024-12-02] Mixed Ad Copy | Image | Image 1 $0.67 7 9 $74.44 0 0.00% $0.00 0 $0.00 0.00% 0 $0.00 0.00% 0 $0.00 0 $0.00 0 $0.00 $0.00 0.00 $0.00 0.00 +2024-12-08 Agencies W/ Call Centers | ABO | 5 Insta Images | USA, CA, AUS | Conversions [Schedules] [2024-12-02] Broad | Insta Image Only | USA, CA, AUS | 18-45 | MF [2024-12-02] Mixed Ad Copy | Image | Image 2 $10.93 87 102 $107.16 3 3.45% $3.64 0 $0.00 $0.00 0.00% 0 $0.00 0.00% 0 0 $0.00 0 $0.00 $0.00 0.00 $0.00 0.00 +2024-12-09 Agencies W/ Call Centers | ABO | 2 Videos | USA, CA, AUS | Conversions [Schedules] [2024-12-03] Broad | Video Only | USA, CA, AUS | 18-45 | MF [2024-12-03] Mixed Ad Copy | Video | Hook 4b + Body 1 $9.83 110 132 $74.47 1 0.91% $9.83 0 $0.00 $0.00 0.00% 0 $0.00 0.00% 0 0 $0.00 0 $0.00 $0.00 0.00 $0.00 0.00 +2024-12-09 Agencies W/ Call Centers | ABO | 2 Videos | USA, CA, AUS | Conversions [Schedules] [2024-12-03] Broad | Video Only | USA, CA, AUS | 18-45 | MF [2024-12-03] Mixed Ad Copy | Video | Hook 4a + Body 1 $229.92 2340 2669 $86.14 21 0.90% $10.95 4 $57.48 $0.19 100.00% 230 $0.25 100.00% $229.92 1 $229.92 0 $0.00 $0.00 0.00 $0.00 0.00 +2024-12-09 Agencies W/ Call Centers | ABO | 5 Insta Images | USA, CA, AUS | Conversions [Schedules] [2024-12-02] Broad | Insta Image Only | USA, CA, AUS | 18-45 | MF [2024-12-02] Mixed Ad Copy | Image | Image 5 $71.02 928 1131 $62.79 8 0.86% $8.88 0 $0.00 $0.00 0.00% 0 $0.00 0.00% 0 0 $0.00 0 $0.00 $0.00 0.00 $0.00 0.00 +2024-12-09 Agencies W/ Call Centers | ABO | 5 Insta Images | USA, CA, AUS | Conversions [Schedules] [2024-12-02] Broad | Insta Image Only | USA, CA, AUS | 18-45 | MF [2024-12-02] Mixed Ad Copy | Image | Image 3 $17.06 283 303 $56.30 6 2.12% $2.84 1 $17.06 $0.17 0.00% 0 $0.00 0.00% 0 0 $0.00 0 $0.00 $0.00 0.00 $0.00 0.00 +2024-12-09 Agencies W/ Call Centers | ABO | 5 Insta Images | USA, CA, AUS | Conversions [Schedules] [2024-12-02] Broad | Insta Image Only | USA, CA, AUS | 18-45 | MF [2024-12-02] Mixed Ad Copy | Image | Image 1 $1.14 14 14 $81.43 0 0.00% $0.00 0 $0.00 $0.00 0.00% 0 $0.00 0.00% 0 0 $0.00 0 $0.00 $0.00 0.00 $0.00 0.00 +2024-12-09 Agencies W/ Call Centers | ABO | 5 Insta Images | USA, CA, AUS | Conversions [Schedules] [2024-12-02] Broad | Insta Image Only | USA, CA, AUS | 18-45 | MF [2024-12-02] Mixed Ad Copy | Image | Image 2 $10.96 133 165 $66.42 0 0.00% $0.00 0 $0.00 $0.00 0.00% 0 $0.00 0.00% 0 0 $0.00 0 $0.00 $0.00 0.00 $0.00 0.00 +2024-12-10 Agencies W/ Call Centers | ABO | 2 Videos | USA, CA, AUS | Conversions [Schedules] [2024-12-03] Broad | Video Only | USA, CA, AUS | 18-45 | MF [2024-12-03] Mixed Ad Copy | Video | Hook 4b + Body 1 $11.74 134 171 $68.65 1 0.75% $11.74 0 $0.00 $0.00 0.00% 0 $0.00 0.00% 0 0 $0.00 0 $0.00 $0.00 0.00 $0.00 0.00 +2024-12-10 Agencies W/ Call Centers | ABO | 2 Videos | USA, CA, AUS | Conversions [Schedules] [2024-12-03] Broad | Video Only | USA, CA, AUS | 18-45 | MF [2024-12-03] Mixed Ad Copy | Video | Hook 4a + Body 1 $252.25 2746 3271 $77.12 17 0.62% $14.84 2 $126.13 $0.12 0.00% 0 $0.00 0.00% 0 0 $0.00 0 $0.00 $0.00 0.00 $0.00 0.00 +2024-12-10 Agencies W/ Call Centers | ABO | 5 Insta Images | USA, CA, AUS | Conversions [Schedules] [2024-12-02] Broad | Insta Image Only | USA, CA, AUS | 18-45 | MF [2024-12-02] Mixed Ad Copy | Image | Image 5 $76.54 921 1227 $62.38 7 0.76% $10.93 0 $0.00 $0.00 0.00% 0 $0.00 0.00% 0 0 $0.00 0 $0.00 $0.00 0.00 $0.00 0.00 +2024-12-10 Agencies W/ Call Centers | ABO | 5 Insta Images | USA, CA, AUS | Conversions [Schedules] [2024-12-02] Broad | Insta Image Only | USA, CA, AUS | 18-45 | MF [2024-12-02] Mixed Ad Copy | Image | Image 3 $25.28 179 205 $123.32 1 0.56% $25.28 0 $0.00 $0.00 0.00% 0 $0.00 0.00% 0 0 $0.00 0 $0.00 $0.00 0.00 $0.00 0.00 +2024-12-10 Agencies W/ Call Centers | ABO | 5 Insta Images | USA, CA, AUS | Conversions [Schedules] [2024-12-02] Broad | Insta Image Only | USA, CA, AUS | 18-45 | MF [2024-12-02] Mixed Ad Copy | Image | Image 1 $0.96 10 13 $73.85 0 0.00% $0.00 0 $0.00 $0.00 0.00% 0 $0.00 0.00% 0 0 $0.00 0 $0.00 $0.00 0.00 $0.00 0.00 +2024-12-10 Agencies W/ Call Centers | ABO | 5 Insta Images | USA, CA, AUS | Conversions [Schedules] [2024-12-02] Broad | Insta Image Only | USA, CA, AUS | 18-45 | MF [2024-12-02] Mixed Ad Copy | Image | Image 2 $5.99 47 59 $101.53 0 0.00% $0.00 0 $0.00 $0.00 0.00% 0 $0.00 0.00% 0 0 $0.00 0 $0.00 $0.00 0.00 $0.00 0.00 +2024-12-11 Agencies W/ Call Centers | ABO | 5 Insta Images | USA, CA, AUS | Conversions [Schedules] [2024-12-02] Broad | Insta Image Only | USA, CA, AUS | 18-45 | MF [2024-12-02] Mixed Ad Copy | Image | Image 2 $1.48 26 29 $51.03 1 0.03846154 $1.48 0 $0.00 $0.00 0.00% 0 $0.00 0.00% 0 0 $0.00 0 $0.00 $0.00 0.00 $0.00 0.00 +2024-12-11 Agencies W/ Call Centers | ABO | 5 Insta Images | USA, CA, AUS | Conversions [Schedules] [2024-12-02] Broad | Insta Image Only | USA, CA, AUS | 18-45 | MF [2024-12-02] Mixed Ad Copy | Image | Image 1 $0.34 4 4 $85.00 0 0 $0.00 0 $0.00 $0.00 0.00% 0 $0.00 0.00% 0 0 $0.00 0 $0.00 $0.00 0.00 $0.00 0.00 +2024-12-11 Agencies W/ Call Centers | ABO | 5 Insta Images | USA, CA, AUS | Conversions [Schedules] [2024-12-02] Broad | Insta Image Only | USA, CA, AUS | 18-45 | MF [2024-12-02] Mixed Ad Copy | Image | Image 3 $10.02 111 122 $82.13 1 0.00900901 $10.02 0 $0.00 $0.00 0.00% 0 $0.00 0.00% 0 0 $0.00 0 $0.00 $0.00 0.00 $0.00 0.00 +2024-12-11 Agencies W/ Call Centers | ABO | 5 Insta Images | USA, CA, AUS | Conversions [Schedules] [2024-12-02] Broad | Insta Image Only | USA, CA, AUS | 18-45 | MF [2024-12-02] Mixed Ad Copy | Image | Image 5 $42.23 574 689 $61.29 10 0.0174216 $4.22 0 $0.00 $0.00 0.00% 0 $0.00 0.00% 0 0 $0.00 0 $0.00 $0.00 0.00 $0.00 0.00 +2024-12-11 Agencies W/ Call Centers | ABO | 2 Videos | USA, CA, AUS | Conversions [Schedules] [2024-12-03] Broad | Video Only | USA, CA, AUS | 18-45 | MF [2024-12-03] Mixed Ad Copy | Video | Hook 4b + Body 1 $14.29 156 192 $74.43 1 0.00641026 $14.29 0 $0.00 $0.00 0.00% 0 $0.00 0.00% 0 0 $0.00 0 $0.00 $0.00 0.00 $0.00 0.00 +2024-12-11 Agencies W/ Call Centers | ABO | 2 Videos | USA, CA, AUS | Conversions [Schedules] [2024-12-03] Broad | Video Only | USA, CA, AUS | 18-45 | MF [2024-12-03] Mixed Ad Copy | Video | Hook 4a + Body 1 $186.79 2149 2504 $74.60 17 0.00791066 $10.99 0 $0.00 $0.00 0.00% 0 $0.00 0.00% 0 0 $0.00 0 $0.00 $0.00 0.00 $0.00 0.00 \ No newline at end of file diff --git a/schema.sql b/schema.sql new file mode 100644 index 0000000..e0fa496 --- /dev/null +++ b/schema.sql @@ -0,0 +1,170 @@ +CREATE TABLE images( + id INT UNSIGNED NOT NULL AUTO_INCREMENT, +url TEXT, +user_id INT, +caption TEXT, +created_at DATE, +updated_at DATETIME, +PRIMARY KEY ( id ) +); + +CREATE TABLE user( + id INT UNSIGNED NOT NULL AUTO_INCREMENT, +username varchar(255) DEFAULT '', +role varchar(255) DEFAULT '', +email varchar(255) DEFAULT '', +password varchar(255) DEFAULT '', +profile_id INT, +reset_token INT, +reset_token_expire INT, +gender INT, +status INT, +created_at DATE, +updated_at DATETIME, +PRIMARY KEY ( id ) +); + +CREATE TABLE profile( + id INT UNSIGNED NOT NULL AUTO_INCREMENT, +user_id INT, +is_google INT, +is_facebook INT, +first_name varchar(100) DEFAULT '', +last_name varchar(100) DEFAULT '', +stripe_id varchar(255) DEFAULT '', +phone varchar(15) DEFAULT '', +street varchar(255) DEFAULT '', +city varchar(255) DEFAULT '', +state varchar(255) DEFAULT '', +country varchar(255) DEFAULT '', +zip varchar(10) DEFAULT '', +created_at DATE, +updated_at DATETIME, +PRIMARY KEY ( id ) +); + +CREATE TABLE role( + id INT UNSIGNED NOT NULL AUTO_INCREMENT, +name varchar(255) DEFAULT '', +created_at DATE, +updated_at DATETIME, +PRIMARY KEY ( id ) +); + +CREATE TABLE permission( + id INT UNSIGNED NOT NULL AUTO_INCREMENT, +name varchar(255) DEFAULT '', +created_at DATE, +updated_at DATETIME, +PRIMARY KEY ( id ) +); + +CREATE TABLE signup( + id INT UNSIGNED NOT NULL AUTO_INCREMENT, +first_name varchar(255) DEFAULT '', +last_name varchar(255) DEFAULT '', +email varchar(255) DEFAULT '', +phone varchar(255) DEFAULT '', +postal_code varchar(255) DEFAULT '', +created_at DATE, +updated_at DATETIME, +PRIMARY KEY ( id ) +); + +CREATE TABLE volunteer( + id INT UNSIGNED NOT NULL AUTO_INCREMENT, +first_name varchar(255) DEFAULT '', +last_name varchar(255) DEFAULT '', +email varchar(255) DEFAULT '', +phone varchar(255) DEFAULT '', +postal_code varchar(255) DEFAULT '', +role varchar(255) DEFAULT '', +created_at DATE, +updated_at DATETIME, +PRIMARY KEY ( id ) +); + +CREATE TABLE lawnsign( + id INT UNSIGNED NOT NULL AUTO_INCREMENT, +first_name varchar(255) DEFAULT '', +last_name varchar(255) DEFAULT '', +address varchar(255) DEFAULT '', +email varchar(255) DEFAULT '', +phone varchar(255) DEFAULT '', +postal_code varchar(255) DEFAULT '', +created_at DATE, +updated_at DATETIME, +PRIMARY KEY ( id ) +); + +CREATE TABLE contact( + id INT UNSIGNED NOT NULL AUTO_INCREMENT, +first_name varchar(255) DEFAULT '', +last_name varchar(255) DEFAULT '', +email varchar(255) DEFAULT '', +phone varchar(255) DEFAULT '', +postal_code varchar(255) DEFAULT '', +contact_list varchar(255) DEFAULT '', +comment TEXT, +created_at DATE, +updated_at DATETIME, +PRIMARY KEY ( id ) +); + +CREATE TABLE donation( + id INT UNSIGNED NOT NULL AUTO_INCREMENT, +first_name varchar(255) DEFAULT '', +last_name varchar(255) DEFAULT '', +email varchar(255) DEFAULT '', +phone varchar(255) DEFAULT '', +postal_code varchar(255) DEFAULT '', +amount varchar(255) DEFAULT '', +created_at DATE, +updated_at DATETIME, +PRIMARY KEY ( id ) +); + +CREATE TABLE permission_role_user( + id INT UNSIGNED NOT NULL AUTO_INCREMENT, +role_id INT, +user_id INT, +permission_id INT, +created_at DATE, +updated_at DATETIME, +PRIMARY KEY ( id ) +); + +CREATE TABLE email( + id INT UNSIGNED NOT NULL AUTO_INCREMENT, +slug varchar(255) DEFAULT '', +subject TEXT, +body TEXT, +tags TEXT, +created_at DATE, +updated_at DATETIME, +PRIMARY KEY ( id ) +); + +CREATE TABLE sms( + id INT UNSIGNED NOT NULL AUTO_INCREMENT, +slug varchar(255) DEFAULT '', +body TEXT, +tags TEXT, +created_at DATE, +updated_at DATETIME, +PRIMARY KEY ( id ) +); + +CREATE TABLE token( + id INT UNSIGNED NOT NULL AUTO_INCREMENT, +token TEXT, +data TEXT, +type INT, +user_id INT, +ttl INT, +issue_at DATETIME, +expire_at DATETIME, +status INT, +PRIMARY KEY ( id ) +); + diff --git a/style.css b/style.css new file mode 100644 index 0000000..19b2c02 --- /dev/null +++ b/style.css @@ -0,0 +1,81 @@ +body.auth-pages { + background-color: #060632; +} + +.login-widget { + width: 30%; + margin-top: 30%; + background-color: white; + padding: 40px 20px; +} + +.remove-validation-custom { + color: red; + margin-bottom: 0px; +} + +.wrapper { + display: flex; + width: 100%; + align-items: stretch; +} + +#sidebar { + min-width: 240px; + max-width: 240px; + background: #151515; + color: #fff; + z-index: 2; + transition: all 0.3s; + -webkit-box-shadow: 2px 0px 4px 0px rgb(0 0 0 / 75%); + -moz-box-shadow: 2px 0px 4px 0px rgba(0, 0, 0, 0.75); + box-shadow: 2px 0px 4px 0px rgb(0 0 0 / 75%); +} + +#sidebar ul li a { + padding: 10px; + font-size: 1.1em; + display: block; + color: white; +} + +#sidebar a.active { + color: black; + background-color: #fff; +} + +#sidebar .sidebar-header { + padding: 15px; + background: #2c5ed6; + height: 50px; +} + +#sidebar ul.components { + padding: 0px 0px 20px 0px; +} + +#sidebar a.active { + color: black; + background-color: #fff; +} + +#content { + width: 100%; + padding: 50px 0px 0px 0px; + min-height: 100vh; + transition: all 0.3s; +} + +@media only screen and (max-width: 767px) { + .login-widget { + width: 50%; + margin-top: 50%; + } +} + +@media only screen and (max-width: 400px) { + .login-widget { + width: 90%; + margin-top: 10%; + } +} diff --git a/team_followup_2023-10-16.sql b/team_followup_2023-10-16.sql new file mode 100644 index 0000000..05d8fe8 --- /dev/null +++ b/team_followup_2023-10-16.sql @@ -0,0 +1,87 @@ +# ************************************************************ +# Sequel Pro SQL dump +# Version 4541 +# +# http://www.sequelpro.com/ +# https://github.com/sequelpro/sequelpro +# +# Host: 127.0.0.1 (MySQL 5.5.5-10.11.2-MariaDB) +# Database: team_followup +# Generation Time: 2023-10-16 23:15:41 +0000 +# ************************************************************ + + +/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */; +/*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */; +/*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */; +/*!40101 SET NAMES utf8 */; +/*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */; +/*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */; +/*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */; + + +# Dump of table accesslog +# ------------------------------------------------------------ + +DROP TABLE IF EXISTS `accesslog`; + +CREATE TABLE `accesslog` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `relationship_num` varchar(255) NOT NULL, + `ip` varchar(255) NOT NULL, + `created_at` datetime NOT NULL, + `updated_at` varchar(191) DEFAULT NULL, + PRIMARY KEY (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_unicode_ci; + +LOCK TABLES `accesslog` WRITE; +/*!40000 ALTER TABLE `accesslog` DISABLE KEYS */; + +INSERT INTO `accesslog` (`id`, `relationship_num`, `ip`, `created_at`, `updated_at`) +VALUES + (3,'1000','127.0.0.1','2023-09-06 21:56:17','2023-09-6 21:56:17'); + +/*!40000 ALTER TABLE `accesslog` ENABLE KEYS */; +UNLOCK TABLES; + + +# Dump of table license +# ------------------------------------------------------------ + +DROP TABLE IF EXISTS `license`; + +CREATE TABLE `license` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `relationship_num` varchar(255) NOT NULL, + `email` varchar(255) NOT NULL, + `apikey` varchar(255) NOT NULL, + `ip` varchar(255) NOT NULL, + `status` varchar(255) NOT NULL, + `created_at` date NOT NULL, + `updated_at` datetime NOT NULL, + PRIMARY KEY (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_unicode_ci; + +LOCK TABLES `license` WRITE; +/*!40000 ALTER TABLE `license` DISABLE KEYS */; + +INSERT INTO `license` (`id`, `relationship_num`, `email`, `apikey`, `ip`, `status`, `created_at`, `updated_at`) +VALUES + (4,'4000','ryan@manaknight.com','abc','','active','2023-09-06','2023-09-06 13:18:30'), + (5,'5000','ryan@manaknight.com','abc','','active','2023-09-06','2023-09-06 13:18:30'), + (6,'2345','wongryan2001@gmail.com','e750b10010d5447b51ec35bd194b19e4','','active','2023-09-07','2023-09-07 09:28:15'), + (7,'2345','wongryan2001@gmail.com','98f2acce631c7a146e01c4d69a8d6cd8','','active','2023-09-07','2023-09-07 09:28:46'), + (8,'10012','wongryan2001@gmail.com','70e52251408159907c7fb5abfa32d5f2','','active','2023-09-07','2023-09-07 09:29:02'), + (9,'2011','ryan11@manaknight.com','b9246f6914aa5f1e367871dfad3252a1f','','inactive','2023-09-07','2023-09-07 09:42:01'); + +/*!40000 ALTER TABLE `license` ENABLE KEYS */; +UNLOCK TABLES; + + + +/*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */; +/*!40101 SET SQL_MODE=@OLD_SQL_MODE */; +/*!40014 SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS */; +/*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */; +/*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */; +/*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */; diff --git a/terms.php b/terms.php new file mode 100644 index 0000000..327f9f5 --- /dev/null +++ b/terms.php @@ -0,0 +1,273 @@ + + + + + Terms & Conditions | Team Follow-up + + +
    +
    +
    +

    TERMS AND CONDITIONS

    +
    +
    + + +

    General

    +

    This website (the “Site”) is owned and operated by Darcan LTD trading as Call Center Mastery (herein referred to as Call Center Mastery) (“Call Center Mastery,” “we” or “us”). By using the Site, you agree to be bound by these Terms of Service and to use the Site in accordance with these Terms of Service, our Privacy Policy and any additional terms and conditions that may apply to specific sections of the Site or to products and services available through the Site or from Call Center Mastery. Accessing the Site, in any manner, whether automated or otherwise, constitutes use of the Site and your agreement to be bound by these Terms of Service.

    +

    We reserve the right to change these Terms of Service or to impose new conditions on use of the Site, from time to time, in which case we will post the revised Terms of Service on this website. By continuing to use the Site after we post any such changes, you accept the Terms of Service, as modified.

    +

    Intellectual Property Rights

    +

    Our limited license to you

    +

    This Site and all the materials available on the Site are the property of us and/or our affiliates or licensors, and are protected by copyright, trademark, and other intellectual property laws. The Site is provided solely for your personal noncommercial use. You may not use the Site or the materials available on the Site in a manner that constitutes an infringement of our rights or that has not been authorized by us. More specifically, unless explicitly authorized in these Terms of Service or by the owner of the materials, you may not modify, copy, reproduce, republish, upload, post, transmit, translate, sell, create derivative works, exploit, or distribute in any manner or medium (including by email or other electronic means) any material from the Site. You may, however, from time to time, download and/or print one copy of individual pages of the Site for your personal, non-commercial use, provided that you keep intact all copyright and other proprietary notices.

    +

    Your license to us

    +

    By posting or submitting any material (including, without limitation, comments, blog entries, Facebook postings, photos and videos) to us via the Site, internet groups, social media venues, or to any of our staff via email, text or otherwise, you are representing: (i) that you are the owner of the material, or are making your posting or submission with the express consent of the owner of the material; and (ii) that you are thirteen years of age or older. In addition, when you submit, email, text or deliver or post any material, you are granting us, and anyone authorized by us, a royalty-free, perpetual, irrevocable, non-exclusive, unrestricted, worldwide license to use, copy, modify, transmit, sell, exploit, create derivative works from, distribute, and/or publicly perform or display such material, in whole or in part, in any manner or medium, now known or hereafter developed, for any purpose. The foregoing grant shall include the right to exploit any proprietary rights in such posting or submission, including, but not limited to, rights under copyright, trademark, service mark or patent laws under any relevant jurisdiction. Also, in connection with the exercise of such rights, you grant us, and anyone authorized by us, the right to identify you as the author of any of your postings or submissions by name, email address or screen name, as we deem appropriate.

    +

    You acknowledge and agree that any contributions originally created by you for us shall be deemed a “work made for hire” when the work performed is within the scope of the definition of a work made for hire in Section 101 of the United States Copyright Law, as amended. As such, the copyrights in those works shall belong to Call Center Mastery from their creation. Thus, Call Center Mastery shall be deemed the author and exclusive owner thereof and shall have the right to exploit any or all of the results and proceeds in any and all media, now known or hereafter devised, throughout the universe, in perpetuity, in all languages, as Call Center Mastery determines. In the event that any of the results and proceeds of your submissions hereunder are not deemed a “work made for hire” under Section 101 of the Copyright Act, as amended, you hereby, without additional compensation, irrevocably assign, convey and transfer to Call Center Mastery all proprietary rights, including without limitation, all copyrights and trademarks throughout the universe, in perpetuity in every medium, whether now known or hereafter devised, to such material and any and all right, title and interest in and to all such proprietary rights in every medium, whether now known or hereafter devised, throughout the universe, in perpetuity. Any posted material which are reproductions of prior works by you shall be co-owned by us.

    +

    You acknowledge that Call Center Mastery has the right but not the obligation to use and display any postings or contributions of any kind and that Call Center Mastery may elect to cease the use and display of any such materials (or any portion thereof), at any time for any reason whatsoever.

    +

    Limitations on Linking and Framing

    +

    You may establish a hypertext link to the Site so long as the link does not state or imply any sponsorship of your site by us or by the Site. However, you may not, without our prior written permission, frame or inline link any of the content of the Site, or incorporate into another website or other service any of our material, content or intellectual property.

    +

    Disclaimers

    +

    Throughout the Site, we may provide links and pointers to Internet sites maintained by third parties. Our linking to such third-party sites does not imply an endorsement or sponsorship of such sites, or the information, products or services offered on or through the sites. In addition, neither we nor affiliates operate or control in any respect any information, products or services that third parties may provide on or through the Site or on websites linked to by us on the Site. If applicable, any opinions, advice, statements, services, offers, or other information or content expressed or made available by third parties, including information providers, are those of the respective authors or distributors, and not Call Center Mastery. Neither Call Center Mastery nor any third-party provider of information guarantees the accuracy, completeness, or usefulness of any content. Furthermore, Call Center Mastery neither endorses nor is responsible for the accuracy and reliability of any opinion, advice, or statement made on any of the Sites by anyone other than an authorized Call Center Mastery representative while acting in his/her official capacity.

    +

    THE INFORMATION, PRODUCTS AND SERVICES OFFERED ON OR THROUGH THE SITE AND BY Call Center Mastery AND ANY THIRD-PARTY SITES ARE PROVIDED “AS IS” AND WITHOUT WARRANTIES OF ANY KIND EITHER EXPRESS OR IMPLIED. TO THE FULLEST EXTENT PERMISSIBLE PURSUANT TO APPLICABLE LAW, WE DISCLAIM ALL WARRANTIES, EXPRESS OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. WE DO NOT WARRANT THAT THE SITE OR ANY OF ITS FUNCTIONS WILL BE UNINTERRUPTED OR ERROR-FREE, THAT DEFECTS WILL BE CORRECTED, OR THAT ANY PART OF THIS SITE, INCLUDING BULLETIN BOARDS, OR THE SERVERS THAT MAKE IT AVAILABLE, ARE FREE OF VIRUSES OR OTHER HARMFUL COMPONENTS.

    +

    WE DO NOT WARRANT OR MAKE ANY REPRESENTATIONS REGARDING THE USE OR THE RESULTS OF THE USE OF THE SITE OR MATERIALS ON THIS SITE OR ON THIRD-PARTY SITES IN TERMS OF THEIR CORRECTNESS, ACCURACY, TIMELINESS, RELIABILITY OR OTHERWISE.

    +

    You agree at all times to defend, indemnify and hold harmless Call Center Mastery its affiliates, their successors, transferees, assignees and licensees and their respective parent and subsidiary companies, agents, associates, officers, directors, shareholders and employees of each from and against any and all claims, causes of action, damages, liabilities, costs and expenses, including legal fees and expenses, arising out of or related to your breach of any obligation, warranty, representation or covenant set forth herein.

    +

    Online commerce

    +

    Certain sections of the Site may allow you to purchase many different types of products and services online that are provided by third parties. We are not responsible for the quality, accuracy, timeliness, reliability or any other aspect of these products and services. If you make a purchase from a merchant on the Site or on a site linked to by the Site, the information obtained during your visit to that merchant’s online store or site, and the information that you give as part of the transaction, such as your credit card number and contact information, may be collected by both the merchant and us. A merchant may have privacy and data collection practices that are different from ours. We have no responsibility or liability for these independent policies. In addition, when you purchase products or services on or through the Site, you may be subject to additional terms and conditions that specifically apply to your purchase or use of such products or services. For more information regarding a merchant, its online store, its privacy policies, and/or any additional terms and conditions that may apply, visit that merchant’s website and click on its information links or contact the merchant directly. You release us and our affiliates from any damages that you incur, and agree not to assert any claims against us or them, arising from your purchase or use of any products or services made available by third parties through the Site.

    +

    Your participation, correspondence or business dealings with any third party found on or through our Site, regarding payment and delivery of specific goods and services, and any other terms, conditions, representations or warranties associated with such dealings, are solely between you and such third party. You agree that Call Center Mastery shall not be responsible or liable for any loss, damage, or other matters of any sort incurred as the result of such dealings.

    +

    You agree to be financially responsible for all purchases made by you or someone acting on your behalf through the Site. You agree to use the Site and to purchase services or products through the Site for legitimate, non-commercial purposes only. You also agree not to make any purchases for speculative, false or fraudulent purposes or for the purpose of anticipating demand for a particular product or service. You agree to only purchase goods or services for yourself or for another person for whom you are legally permitted to do so. When making a purchase for a third party that requires you to submit the third party’s personal information to us or a merchant, you represent that you have obtained the express consent of such third party to provide such third party’s personal information.

    +

    Your purchase is for personal use only. Sharing of purchases is not permitted and will be considered unauthorized, an infringing use of our copyrighted material, and may subject violators to liability. If payment for a course is declined, our system will automatically disable access to our premium materials. (We understand. This usually happens because a credit card expires.) We want to help restore your access, so we’ll make every attempt to contact you to help resolve this issue. Once the billing issue is resolved, we’ll restore access.

    +

    Interactive features

    +

    This Site may include a variety of features, such as bulletin boards, web logs, chat rooms, and email services, which allow feedback to us and real-time interaction between users, and other features which allow users to communicate with others. Responsibility for what is posted on bulletin boards, web logs, chat rooms, and other public posting areas on the Site, or sent via any email services on the Site, lies with each user – you alone are responsible for the material you post or send. We do not control the messages, information or files that you or others may provide through the Site. It is a condition of your use of the Site that you do not:

    +
      +
    • Restrict or inhibit any other user from using and enjoying the Site.
    • +
    • Use the Site to impersonate any person or entity, or falsely state or otherwise misrepresent your affiliation with a person or entity.
    • +
    • Interfere with or disrupt any servers or networks used to provide the Site or its features, or disobey any requirements, procedures, policies or regulations of the networks we use to provide the Site.
    • +
    • Use the Site to instigate or encourage others to commit illegal activities or cause injury or property damage to any person.
    • +
    • Gain unauthorized access to the Site, or any account, computer system, or network connected to this Site, by means such as hacking, password mining or other illicit means.
    • +
    • Obtain or attempt to obtain any materials or information through any means not intentionally made available through this Site.
    • +
    • Use the Site to post or transmit any unlawful, threatening, abusive, libelous, defamatory, obscene, vulgar, pornographic, profane or indecent information of any kind, including without limitation any transmissions constituting or encouraging conduct that would constitute a criminal offense, give rise to civil liability or otherwise violate any local, state, national or international law.
    • +
    • Use the Site to post or transmit any information, software or other material that violates or infringes upon the rights of others, including material that is an invasion of privacy or publicity rights or that is protected by copyright, trademark or other proprietary right, or derivative works with respect thereto, without first obtaining permission from the owner or rights holder.
    • +
    • Use the Site to post or transmit any information, software or other material that contains a virus or other harmful component.
    • +
    • Use the Site to post, transmit or in any way exploit any information, software or other material for commercial purposes, or that contains advertising.
    • +
    • Use the Site to advertise or solicit to anyone to buy or sell products or services, or to make donations of any kind, without our express written approval.
    • +
    • Gather for marketing purposes any email addresses or other personal information that has been posted by other users of the Site.
    • +
    +

    Call Center Mastery may host message boards, chats and other public forums on its Sites. Any user failing to comply with the terms and conditions of this Agreement may be expelled from and refused continued access to, the message boards, chats or other public forums in the future. Call Center Mastery or its designated agents may remove or alter any user-created content at any time for any reason. Message boards, chats and other public forums are intended to serve as discussion centers for users and subscribers. Information and content posted within these public forums may be provided by Call Center Mastery staff, Call Center Mastery’s outside contributors, or by users not connected with Call Center Mastery, some of whom may employ anonymous user names. Call Center Mastery expressly disclaims all responsibility and endorsement and makes no representation as to the validity of any opinion, advice, information or statement made or displayed in these forums by third parties, nor are we responsible for any errors or omissions in such postings, or for hyperlinks embedded in any messages. Under no circumstances will we, our affiliates, suppliers or agents be liable for any loss or damage caused by your reliance on information obtained through these forums. The opinions expressed in these forums are solely the opinions of the participants, and do not reflect the opinions of Call Center Mastery or any of its subsidiaries or affiliates.

    +

    Call Center Mastery has no obligation whatsoever to monitor any of the content or postings on the message boards, chat rooms or other public forums on the Sites. However, you acknowledge and agree that we have the absolute right to monitor the same at our sole discretion. In addition, we reserve the right to alter, edit, refuse to post or remove any postings or content, in whole or in part, for any reason and to disclose such materials and the circumstances surrounding their transmission to any third party in order to satisfy any applicable law, regulation, legal process or governmental request and to protect ourselves, our clients, sponsors, users and visitors.

    +

    We occasionally include access to an online community as part of our programs. We want every single member to add value to the group. Our goal is to make your community the most valuable community you’re a member of. Therefore, we reserve the right to remove anyone at any time. We rarely do this, but we want to let you know how seriously we take our communities.

    +

    Registration

    +

    To access certain features of the Site, we may ask you to provide certain demographic information including your gender, year of birth, zip code and country. In addition, if you elect to sign-up for a particular feature of the Site, such as chat rooms, web logs, or bulletin boards, you may also be asked to register with us on the form provided and such registration may require you to provide personally identifiable information such as your name and email address. You agree to provide true, accurate, current and complete information about yourself as prompted by the Site’s registration form. If we have reasonable grounds to suspect that such information is untrue, inaccurate, or incomplete, we have the right to suspend or terminate your account and refuse any and all current or future use of the Site (or any portion thereof). Our use of any personally identifiable information you provide to us as part of the registration process is governed by the terms of our Privacy Policy.

    +

    Passwords

    +

    To use certain features of the Site, you will need a username and password, which you will receive through the Site’s registration process. You are responsible for maintaining the confidentiality of the password and account, and are responsible for all activities (whether by you or by others) that occur under your password or account. You agree to notify us immediately of any unauthorized use of your password or account or any other breach of security, and to ensure that you exit from your account at the end of each session. We cannot and will not be liable for any loss or damage arising from your failure to protect your password or account information.

    +

    Limitation of liability

    +General + +‍ + +This website (the “Site”) is owned and operated by Darcan LTD trading as Call Center Mastery (herein referred to as Call Center Mastery) (“Call Center Mastery,” “we” or “us”). By using the Site, you agree to be bound by these Terms of Service and to use the Site in accordance with these Terms of Service, our Privacy Policy and any additional terms and conditions that may apply to specific sections of the Site or to products and services available through the Site or from Call Center Mastery. Accessing the Site, in any manner, whether automated or otherwise, constitutes use of the Site and your agreement to be bound by these Terms of Service. + +‍ + +We reserve the right to change these Terms of Service or to impose new conditions on use of the Site, from time to time, in which case we will post the revised Terms of Service on this website. By continuing to use the Site after we post any such changes, you accept the Terms of Service, as modified. Intellectual Property Rights + +‍ + +Our limited license to you + +‍ + +This Site and all the materials available on the Site are the property of us and/or our affiliates or licensors, and are protected by copyright, trademark, and other intellectual property laws. The Site is provided solely for your personal noncommercial use. You may not use the Site or the materials available on the Site in a manner that constitutes an infringement of our rights or that has not been authorized by us. More specifically, unless explicitly authorized in these Terms of Service or by the owner of the materials, you may not modify, copy, reproduce, republish, upload, post, transmit, translate, sell, create derivative works, exploit, or distribute in any manner or medium (including by email or other electronic means) any material from the Site. You may, however, from time to time, download and/or print one copy of individual pages of the Site for your personal, non-commercial use, provided that you keep intact all copyright and other proprietary notices. + +‍ + +Your license to us + +‍ + +By posting or submitting any material (including, without limitation, comments, blog entries, Facebook postings, photos and videos) to us via the Site, internet groups, social media venues, or to any of our staff via email, text or otherwise, you are representing: (i) that you are the owner of the material, or are making your posting or submission with the express consent of the owner of the material; and (ii) that you are thirteen years of age or older. In addition, when you submit, email, text or deliver or post any material, you are granting us, and anyone authorized by us, a royalty-free, perpetual, irrevocable, non-exclusive, unrestricted, worldwide license to use, copy, modify, transmit, sell, exploit, create derivative works from, distribute, and/or publicly perform or display such material, in whole or in part, in any manner or medium, now known or hereafter developed, for any purpose. The foregoing grant shall include the right to exploit any proprietary rights in such posting or submission, including, but not limited to, rights under copyright, trademark, service mark or patent laws under any relevant jurisdiction. Also, in connection with the exercise of such rights, you grant us, and anyone authorized by us, the right to identify you as the author of any of your postings or submissions by name, email address or screen name, as we deem appropriate. + +‍ + +You acknowledge and agree that any contributions originally created by you for us shall be deemed a “work made for hire” when the work performed is within the scope of the definition of a work made for hire in Section 101 of the United States Copyright Law, as amended. As such, the copyrights in those works shall belong to Call Center Mastery from their creation. Thus, Call Center Mastery shall be deemed the author and exclusive owner thereof and shall have the right to exploit any or all of the results and proceeds in any and all media, now known or hereafter devised, throughout the universe, in perpetuity, in all languages, as Call Center Mastery determines. In the event that any of the results and proceeds of your submissions hereunder are not deemed a “work made for hire” under Section 101 of the Copyright Act, as amended, you hereby, without additional compensation, irrevocably assign, convey and transfer to Call Center Mastery all proprietary rights, including without limitation, all copyrights and trademarks throughout the universe, in perpetuity in every medium, whether now known or hereafter devised, to such material and any and all right, title and interest in and to all such proprietary rights in every medium, whether now known or hereafter devised, throughout the universe, in perpetuity. Any posted material which are reproductions of prior works by you shall be co-owned by us. + +‍ + +You acknowledge that Call Center Mastery has the right but not the obligation to use and display any postings or contributions of any kind and that Call Center Mastery may elect to cease the use and display of any such materials (or any portion thereof), at any time for any reason whatsoever. + +‍ + +Limitations on Linking and Framing. You may establish a hypertext link to the Site so long as the link does not state or imply any sponsorship of your site by us or by the Site. However, you may not, without our prior written permission, frame or inline link any of the content of the Site, or incorporate into another website or other service any of our material, content or intellectual property. + +‍ + +Disclaimers + +‍ + +Throughout the Site, we may provide links and pointers to Internet sites maintained by third parties. Our linking to such third-party sites does not imply an endorsement or sponsorship of such sites, or the information, products or services offered on or through the sites. In addition, neither we nor affiliates operate or control in any respect any information, products or services that third parties may provide on or through the Site or on websites linked to by us on the Site. If applicable, any opinions, advice, statements, services, offers, or other information or content expressed or made available by third parties, including information providers, are those of the respective authors or distributors, and not Call Center Mastery. Neither Call Center Mastery nor any third-party provider of information guarantees the accuracy, completeness, or usefulness of any content. Furthermore, Call Center Mastery neither endorses nor is responsible for the accuracy and reliability of any opinion, advice, or statement made on any of the Sites by anyone other than an authorized Call Center Mastery representative while acting in his/her official capacity. + +‍ + +THE INFORMATION, PRODUCTS AND SERVICES OFFERED ON OR THROUGH THE SITE AND BY Call Center Mastery AND ANY THIRD-PARTY SITES ARE PROVIDED “AS IS” AND WITHOUT WARRANTIES OF ANY KIND EITHER EXPRESS OR IMPLIED. TO THE FULLEST EXTENT PERMISSIBLE PURSUANT TO APPLICABLE LAW, WE DISCLAIM ALL WARRANTIES, EXPRESS OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. WE DO NOT WARRANT THAT THE SITE OR ANY OF ITS FUNCTIONS WILL BE UNINTERRUPTED OR ERROR-FREE, THAT DEFECTS WILL BE CORRECTED, OR THAT ANY PART OF THIS SITE, INCLUDING BULLETIN BOARDS, OR THE SERVERS THAT MAKE IT AVAILABLE, ARE FREE OF VIRUSES OR OTHER HARMFUL COMPONENTS. + +‍ + +WE DO NOT WARRANT OR MAKE ANY REPRESENTATIONS REGARDING THE USE OR THE RESULTS OF THE USE OF THE SITE OR MATERIALS ON THIS SITE OR ON THIRD-PARTY SITES IN TERMS OF THEIR CORRECTNESS, ACCURACY, TIMELINESS, RELIABILITY OR OTHERWISE. + +‍ + +You agree at all times to defend, indemnify and hold harmless Call Center Mastery its affiliates, their successors, transferees, assignees and licensees and their respective parent and subsidiary companies, agents, associates, officers, directors, shareholders and employees of each from and against any and all claims, causes of action, damages, liabilities, costs and expenses, including legal fees and expenses, arising out of or related to your breach of any obligation, warranty, representation or covenant set forth herein. + +‍ + +Online commerce + +‍ + +Certain sections of the Site may allow you to purchase many different types of products and services online that are provided by third parties. We are not responsible for the quality, accuracy, timeliness, reliability or any other aspect of these products and services. If you make a purchase from a merchant on the Site or on a site linked to by the Site, the information obtained during your visit to that merchant’s online store or site, and the information that you give as part of the transaction, such as your credit card number and contact information, may be collected by both the merchant and us. A merchant may have privacy and data collection practices that are different from ours. We have no responsibility or liability for these independent policies. In addition, when you purchase products or services on or through the Site, you may be subject to additional terms and conditions that specifically apply to your purchase or use of such products or services. For more information regarding a merchant, its online store, its privacy policies, and/or any additional terms and conditions that may apply, visit that merchant’s website and click on its information links or contact the merchant directly. You release us and our affiliates from any damages that you incur, and agree not to assert any claims against us or them, arising from your purchase or use of any products or services made available by third parties through the Site. + +Your participation, correspondence or business dealings with any third party found on or through our Site, regarding payment and delivery of specific goods and services, and any other terms, conditions, representations or warranties associated with such dealings, are solely between you and such third party. You agree that Call Center Mastery shall not be responsible or liable for any loss, damage, or other matters of any sort incurred as the result of such dealings. + +‍ + +You agree to be financially responsible for all purchases made by you or someone acting on your behalf through the Site. You agree to use the Site and to purchase services or products through the Site for legitimate, non-commercial purposes only. You also agree not to make any purchases for speculative, false or fraudulent purposes or for the purpose of anticipating demand for a particular product or service. You agree to only purchase goods or services for yourself or for another person for whom you are legally permitted to do so. When making a purchase for a third party that requires you to submit the third party’s personal information to us or a merchant, you represent that you have obtained the express consent of such third party to provide such third party’s personal information. + +‍ + +Your purchase is for personal use only. Sharing of purchases is not permitted and will be considered unauthorized, an infringing use of our copyrighted material, and may subject violators to liability. If payment for a course is declined, our system will automatically disable access to our premium materials. (We understand. This usually happens because a credit card expires.) We want to help restore your access, so we’ll make every attempt to contact you to help resolve this issue. Once the billing issue is resolved, we’ll restore access. + +‍ + +Interactive features + +‍ + +This Site may include a variety of features, such as bulletin boards, web logs, chat rooms, and email services, which allow feedback to us and real-time interaction between users, and other features which allow users to communicate with others. Responsibility for what is posted on bulletin boards, web logs, chat rooms, and other public posting areas on the Site, or sent via any email services on the Site, lies with each user – you alone are responsible for the material you post or send. We do not control the messages, information or files that you or others may provide through the Site. It is a condition of your use of the Site that you do not: + +Restrict or inhibit any other user from using and enjoying the Site. + +‍ + +Use the Site to impersonate any person or entity, or falsely state or otherwise misrepresent your affiliation with a person or entity. + +‍ + +Interfere with or disrupt any servers or networks used to provide the Site or its features, or disobey any requirements, procedures, policies or regulations of the networks we use to provide the Site. + +‍ + +Use the Site to instigate or encourage others to commit illegal activities or cause injury or property damage to any person. + +‍ + +Gain unauthorized access to the Site, or any account, computer system, or network connected to this Site, by means such as hacking, password mining or other illicit means. + +‍ + +Obtain or attempt to obtain any materials or information through any means not intentionally made available through this Site. + +‍ + +Use the Site to post or transmit any unlawful, threatening, abusive, libelous, defamatory, obscene, vulgar, pornographic, profane or indecent information of any kind, including without limitation any transmissions constituting or encouraging conduct that would constitute a criminal offense, give rise to civil liability or otherwise violate any local, state, national or international law. + +‍ + +Use the Site to post or transmit any information, software or other material that violates or infringes upon the rights of others, including material that is an invasion of privacy or publicity rights or that is protected by copyright, trademark or other proprietary right, or derivative works with respect thereto, without first obtaining permission from the owner or rights holder. + +Use the Site to post or transmit any information, software or other material that contains a virus or other harmful component. + +‍ + +Use the Site to post, transmit or in any way exploit any information, software or other material for commercial purposes, or that contains advertising. + +‍ + +Use the Site to advertise or solicit to anyone to buy or sell products or services, or to make donations of any kind, without our express written approval. + +‍ + +Gather for marketing purposes any email addresses or other personal information that has been posted by other users of the Site. + +‍ + +Call Center Mastery may host message boards, chats and other public forums on its Sites. Any user failing to comply with the terms and conditions of this Agreement may be expelled from and refused continued access to, the message boards, chats or other public forums in the future. Call Center Mastery or its designated agents may remove or alter any user-created content at any time for any reason. Message boards, chats and other public forums are intended to serve as discussion centers for users and subscribers. Information and content posted within these public forums may be provided by Call Center Mastery staff, Call Center Mastery’s outside contributors, or by users not connected with Call Center Mastery, some of whom may employ anonymous user names. Call Center Mastery expressly disclaims all responsibility and endorsement and makes no representation as to the validity of any opinion, advice, information or statement made or displayed in these forums by third parties, nor are we responsible for any errors or omissions in such postings, or for hyperlinks embedded in any messages. Under no circumstances will we, our affiliates, suppliers or agents be liable for any loss or damage caused by your reliance on information obtained through these forums. The opinions expressed in these forums are solely the opinions of the participants, and do not reflect the opinions of Call Center Mastery or any of its subsidiaries or affiliates. + +‍ + +Call Center Mastery has no obligation whatsoever to monitor any of the content or postings on the message boards, chat rooms or other public forums on the Sites. However, you acknowledge and agree that we have the absolute right to monitor the same at our sole discretion. In addition, we reserve the right to alter, edit, refuse to post or remove any postings or content, in whole or in part, for any reason and to disclose such materials and the circumstances surrounding their transmission to any third party in order to satisfy any applicable law, regulation, legal process or governmental request and to protect ourselves, our clients, sponsors, users and visitors. + +‍ + +We occasionally include access to an online community as part of our programs. We want every single member to add value to the group. Our goal is to make your community the most valuable community you’re a member of. Therefore, we reserve the right to remove anyone at any time. We rarely do this, but we want to let you know how seriously we take our communities. + +‍ + +Registration + +‍ + +To access certain features of the Site, we may ask you to provide certain demographic information including your gender, year of birth, zip code and country. In addition, if you elect to sign-up for a particular feature of the Site, such as chat rooms, web logs, or bulletin boards, you may also be asked to register with us on the form provided and such registration may require you to provide personally identifiable information such as your name and email address. You agree to provide true, accurate, current and complete information about yourself as prompted by the Site’s registration form. If we have reasonable grounds to suspect that such information is untrue, inaccurate, or incomplete, we have the right to suspend or terminate your account and refuse any and all current or future use of the Site (or any portion thereof). Our use of any personally identifiable information you provide to us as part of the registration process is governed by the terms of our Privacy Policy. + +‍ + +Passwords + +‍ + +To use certain features of the Site, you will need a username and password, which you will receive through the Site’s registration process. You are responsible for maintaining the confidentiality of the password and account, and are responsible for all activities (whether by you or by others) that occur under your password or account. You agree to notify us immediately of any unauthorized use of your password or account or any other breach of security, and to ensure that you exit from your account at the end of each session. We cannot and will not be liable for any loss or damage arising from your failure to protect your password or account information. + +‍ + +Limitation of liability + +‍ + +UNDER NO CIRCUMSTANCES, INCLUDING, BUT NOT LIMITED TO, NEGLIGENCE, SHALL WE, OUR SUBSIDIARY AND PARENT COMPANIES OR AFFILIATES BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL OR CONSEQUENTIAL DAMAGES THAT RESULT FROM THE USE OF, OR THE INABILITY TO USE, THE SITE, INCLUDING OUR MESSAGING, BLOGS, COMMENTS OF OTHERS, BOOKS, EMAILS, PRODUCTS, OR SERVICES, OR THIRD-PARTY MATERIALS, PRODUCTS, OR SERVICES MADE AVAILABLE THROUGH THE SITE OR BY US IN ANY WAY, EVEN IF WE ARE ADVISED BEFOREHAND OF THE POSSIBILITY OF SUCH DAMAGES. (BECAUSE SOME STATES DO NOT ALLOW THE EXCLUSION OR LIMITATION OF CERTAIN CATEGORIES OF DAMAGES, THE ABOVE LIMITATION MAY NOT APPLY TO YOU. IN SUCH STATES, OUR LIABILITY AND THE LIABILITY OF OUR SUBSIDIARY AND PARENT COMPANIES OR AFFILIATES IS LIMITED TO THE FULLEST EXTENT PERMITTED BY SUCH STATE LAW.) YOU SPECIFICALLY ACKNOWLEDGE AND AGREE THAT WE ARE NOT LIABLE FOR ANY DEFAMATORY, OFFENSIVE OR ILLEGAL CONDUCT OF ANY USER. IF YOU ARE DISSATISFIED WITH THE SITE, ANY MATERIALS, PRODUCTS, OR SERVICES ON THE SITE, OR WITH ANY OF THE SITE’S TERMS AND CONDITIONS, YOUR SOLE AND EXCLUSIVE REMEDY IS TO DISCONTINUE USING THE SITE AND THE PRODUCTS, SERVICES AND/OR MATERIALS Call Center Mastery IS NOT AN INVESTMENT ADVISORY SERVICE, IS NOT AN INVESTMENT ADVISER, AND DOES NOT PROVIDE PERSONALIZED FINANCIAL ADVICE OR ACT AS A FINANCIAL ADVISOR. + +‍ + +WE EXIST FOR EDUCATIONAL PURPOSES ONLY, AND THE MATERIALS AND INFORMATION CONTAINED HEREIN AND IN OUR PRODUCTS AND SERVICES ARE FOR GENERAL INFORMATIONAL PURPOSES ONLY. NONE OF THE INFORMATION PROVIDED BY US IS INTENDED AS INVESTMENT, TAX, ACCOUNTING OR LEGAL ADVICE, AS AN OFFER OR SOLICITATION OF AN OFFER TO BUY OR SELL, OR AS AN ENDORSEMENT, RECOMMENDATION OR SPONSORSHIP OF ANY Call Center Mastery, SECURITY, OR FUND. OUR INFORMATION SHOULD NOT BE RELIED UPON FOR PURPOSES OF TRANSACTING IN SECURITIES OR OTHER INVESTMENTS. + +‍ + +WE DO NOT OFFER OR PROVIDE TAX, LEGAL OR INVESTMENT ADVICE AND YOU ARE RESPONSIBLE FOR CONSULTING TAX, LEGAL, OR FINANCIAL PROFESSIONALS BEFORE ACTING ON ANY INFORMATION PROVIDED BY US. THIS SITE IS CONTINUALLY UNDER DEVELOPMENT AND Call Center Mastery MAKES NO WARRANTY OF ANY KIND, IMPLIED OR EXPRESS, AS TO ITS ACCURACY, COMPLETENESS OR APPROPRIATENESS FOR ANY PURPOSE. YOU acknowledge and agrees that no representation has been made by Call Center Mastery OR ITS AFFILIATES and relied upon as to the future income, expenses, sales volume or potential profitability that may be derived from the participation in THIS PROGRAM. + +‍ + +Termination + +‍ + +We may cancel or terminate your right to use the Site or any part of the Site at any time without notice. In the event of cancellation or termination, you are no longer authorized to access the part of the Site affected by such cancellation or termination. The restrictions imposed on you with respect to material downloaded from the Site, and the disclaimers and limitations of liabilities set forth in these Terms of Service, shall survive. + +‍ + +Refund policy + +‍ + +Your purchase of a product or service or ticket to an event may or may not provide for any refund. Each specific product, service, event or course will specify its own refund policy. + +‍ + +Other + +‍ + +The Digital Millennium Copyright Act of 1998 (the “DMCA”) provides recourse for copyright owners who believe that material appearing on the Internet infringes their rights under the U.S. copyright law. If you believe in good faith that materials hosted by Call Center Mastery infringe your copyright, you, or your agent may send to Call Center Mastery a notice requesting that the material be removed or access to it be blocked. Any notification by a copyright owner or a person authorized to act on its behalf that fails to comply with requirements of the DMCA shall not be considered sufficient notice and shall not be deemed to confer upon Call Center Mastery actual knowledge of facts or circumstances from which infringing material or acts are evident. If you believe in good faith that a notice of copyright infringement has been wrongly filed against you, the DMCA permits you to send to Call Center Mastery a counter-notice. All notices and counter notices must meet the then current statutory requirements imposed by the DMCA; see http://www.loc.gov/copyright for details. Call Center Mastery’s Copyright Agent for notice of claims of copyright infringement or counter notices can be reached as follows: admin@cc-mastery.com + +‍ + +This Agreement shall be binding upon and inure to the benefit of Call Center Mastery and our respective assigns, successors, heirs, and legal representatives. Neither this Agreement nor any rights hereunder may be assigned without the prior written consent of Call Center Mastery. Notwithstanding the foregoing, all rights and obligations under this Agreement may be freely assigned by Call Center Mastery to any affiliated entity or any of its wholly owned subsidiaries These Terms of Use shall be governed by and construed in accordance with the laws of The United Kingdom and any dispute shall be subject to binding arbitration in ­­­The United Kingdom. If any provision of this agreement shall be unlawful, void or for any reason unenforceable, then that provision shall be deemed severable from this agreement and shall not affect the validity and enforceability of any remaining provisions. + +‍ + +Disclaimer + +‍ + +Although it is highly unlikely, This policy may be changed at any time at our discretion. If we should update this policy, we will post the updates to this page on our Website. If you have any questions or concerns regarding our privacy policy please direct them to: admin@cc-mastery.com + +
    + + + \ No newline at end of file diff --git a/test.html b/test.html new file mode 100644 index 0000000..e030cc0 --- /dev/null +++ b/test.html @@ -0,0 +1,409 @@ + + + + + + + + + + + + + + + + + + + + + + +
    + +
    +
    +

    Add Project

    +
    + +
    +
    + + + + + +
    +
    +
    + + +
    +
    + + +
    +
    +
    + Add Time +
    +
    +
    +
    + + +
    +
    + + +
    +
    + + +
    +
    + + +
    +
    + + +
    +
    + + +
    +
    + + +
    +
    + + +
    +
    +
    + +
    + + diff --git a/tt.php b/tt.php new file mode 100644 index 0000000..891062c --- /dev/null +++ b/tt.php @@ -0,0 +1,86 @@ + 'Project' + ]; + // $config = MkdConfig::get_instance()->get_config(); + // $apikey = $config['gohighlevel_key']; + // $cid = $_POST['calendar_id']; + $pid = 412; + // $pid = 169; + $projectModel = new ProjectModel(); + $model = $projectModel->get((int)$pid); + $apikey = $model->location; + $curl = curl_init(); + + curl_setopt_array($curl, [ + CURLOPT_URL => "https://rest.gohighlevel.com/v1/calendars/services", + CURLOPT_RETURNTRANSFER => true, + CURLOPT_ENCODING => "", + CURLOPT_MAXREDIRS => 10, + CURLOPT_TIMEOUT => 30, + CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1, + CURLOPT_CUSTOMREQUEST => "GET", + CURLOPT_HTTPHEADER => [ + "Accept: application/json", + "Authorization: Bearer " . $apikey, + "Version: 2021-04-15" + ], + ]); + + $response = curl_exec($curl); + $err = curl_error($curl); + $status_code = curl_getinfo($curl, CURLINFO_HTTP_CODE); + + curl_close($curl); + + // print_r($response); + + if ($status_code != 200) { + echo "Something went wrong."; + exit; + } + + $cid = $model->calendar; + $data = json_decode($response, true); + + // Search for the service with the specified ID + $searchedService = null; + if (!empty($data['services']) && count($data['services'] ) > 0) { + foreach ($data['services'] as $service) { + if ($service['id'] === $cid) { + $searchedService = $service; + break; + } + } + } + + if ($searchedService == null) { + echo "Calendar Service with ID '" . $cid . "' not found."; + } + + $slots = json_decode($model->slot); + try { + echo checkServiceInSlot($searchedService["availability"]["officeHours"], + $slots, + $model, + $searchedService["availability"]["eventTiming"], + $searchedService["availability"]["schedule"], + $searchedService["id"]); + } catch (\Throwable $th) { + echo "Something went wrong."; + print_r($th); + exit; + } + + + exit; + + \ No newline at end of file diff --git a/user-model.php b/user-model.php new file mode 100644 index 0000000..87018d5 --- /dev/null +++ b/user-model.php @@ -0,0 +1,43 @@ + +

    Add Users

    + + + +
    +
    + + +
    +
    + + +
    +
    + + +
    + +
    + + +
    + +
    + \ No newline at end of file diff --git a/userEdit.php b/userEdit.php new file mode 100644 index 0000000..66eca90 --- /dev/null +++ b/userEdit.php @@ -0,0 +1,39 @@ +
    +

    Edit User

    + + + +
    +
    + + +
    +
    + + +
    +
    + + +
    +
    + + +
    +
    + + +
    + +
    +
    \ No newline at end of file diff --git a/userListing.php b/userListing.php new file mode 100644 index 0000000..347d154 --- /dev/null +++ b/userListing.php @@ -0,0 +1,83 @@ + + +
    +

    Users   Add

    + +
    + + + + + + + + + + + + $value) { + echo ' '; + echo ' '; + echo ' '; + echo ' '; + echo ' '; + echo ' '; + echo ''; + } + ?> + +
    IDEmailCompanyRoleStatus
    ' . $value->id . '
    edit delete
    ' . $value->email . ' ' . $value->company . ' ' . $value->role . ' ' . $value->status . '
    +
    + + 0 ? ($currentPage - $range) : 1; +$endPage = ($currentPage + $range) < $totalPages ? ($currentPage + $range) : $totalPages; + +?> + + +
    diff --git a/validation-service.php b/validation-service.php new file mode 100644 index 0000000..ac2e5f8 --- /dev/null +++ b/validation-service.php @@ -0,0 +1,90 @@ +_validator = new Validator(); + } + + public function save_rules($rules) + { + $this->_rules = $rules; + } + + public function get_rules() + { + return $this->_rules; + } + + public function validate ($data) + { + $rules = $this->make_rules($this->_rules); + $validation = $this->_validator->make($data, $rules['array_rules']); + $validation->setAliases($rules['array_alias']); + + // $validation->setMessages([ + // 'required' => ':attribute harus diisi', + // 'email' => ':email tidak valid', + // ]); + $validation->validate(); + + if ($validation->fails()) + { + // handling errors + $this->_errors = $validation->errors(); + + return false; + } + else + { + return true; + } + } + + public function get_errors () + { + return $this->_errors->toArray(); + } + + + + protected function make_rules() + { + $array_rules = []; + $array_alias = []; + foreach($this->_rules as $role_key => $role_value) + { + $array_rules[$role_value[0]] = $role_value[2]; + $array_alias[$role_value[0]] = $role_value[1]; + } + + $response['array_alias'] = $array_alias; + $response['array_rules'] = $array_rules; + return $response; + } + + // 'name' => 'required', + // 'email' => 'required|email', + // 'password' => 'required|min:6', + // 'confirm_password' => 'required|same:password', + // 'avatar' => 'required|uploaded_file:0,500K,png,jpeg', + // 'skills' => 'array', + // 'skills.*.id' => 'required|numeric', + // 'skills.*.percentage' => 'required|numeric' +} \ No newline at end of file