diff --git a/application/config/autoload.php b/application/config/autoload.php index 30fcc15d..20beb636 100644 --- a/application/config/autoload.php +++ b/application/config/autoload.php @@ -65,7 +65,7 @@ $autoload['libraries'] = ['database', 'session']; | $autoload['helper'] = array('url', 'file'); */ -$autoload['helper'] = ['custom_exceptions', 'url', 'file', 'language', 'asset', 'config', 'render']; +$autoload['helper'] = ['custom_exceptions', 'url', 'file', 'language', 'asset', 'config', 'render', 'rate_limit', 'security']; /* diff --git a/application/config/config.php b/application/config/config.php index c217dcfd..f3a12058 100644 --- a/application/config/config.php +++ b/application/config/config.php @@ -367,7 +367,7 @@ $config['sess_regenerate_destroy'] = FALSE; $config['cookie_prefix'] = ""; $config['cookie_domain'] = ""; $config['cookie_path'] = "/"; -$config['cookie_secure'] = FALSE; +$config['cookie_secure'] = strpos($config['base_url'], 'https') !== FALSE; /* |-------------------------------------------------------------------------- @@ -457,6 +457,17 @@ $config['rewrite_short_tags'] = FALSE; */ $config['proxy_ips'] = ''; +/* +|-------------------------------------------------------------------------- +| Rate Limiting +|-------------------------------------------------------------------------- +| +| Toggle the rate limiting feature in your application. Using rate limiting +| will control the number of requests a client can sent to the app. +| +*/ +$config['rate_limiting'] = TRUE; + /* End of file config.php */ /* Location: ./application/config/config.php */ diff --git a/application/controllers/Appointments.php b/application/controllers/Appointments.php index 8406fea7..3ce6675b 100755 --- a/application/controllers/Appointments.php +++ b/application/controllers/Appointments.php @@ -88,6 +88,23 @@ class Appointments extends EA_Controller { $available_providers[$index] = $stripped_data; } + // Remove the data that are not needed inside the $available_services array. + foreach ($available_services as $index => $service) + { + $stripped_data = [ + 'id' => $service['id'], + 'name' => $service['name'], + 'duration' => $service['duration'], + 'location' => $service['location'], + 'price' => $service['price'], + 'currency' => $service['currency'], + 'description' => $service['description'], + 'category_id' => $service['category_id'], + 'category_name' => $service['category_name'] + ]; + $available_services[$index] = $stripped_data; + } + // If an appointment hash is provided then it means that the customer is trying to edit a registered // appointment record. if ($appointment_hash !== '') @@ -133,9 +150,40 @@ class Appointments extends EA_Controller { } $appointment = $results[0]; + + $appointment = [ + 'id' => $appointment['id'], + 'hash' => $appointment['hash'], + 'start_datetime' => $appointment['start_datetime'], + 'end_datetime' => $appointment['end_datetime'], + 'id_services' => $appointment['id_services'], + 'id_users_customer' => $appointment['id_users_customer'], + 'id_users_provider' => $appointment['id_users_provider'], + 'notes' => $appointment['notes'] + ]; + $provider = $this->providers_model->get_row($appointment['id_users_provider']); + + $provider = [ + 'id' => $provider['id'], + 'first_name' => $provider['first_name'], + 'last_name' => $provider['last_name'], + 'services' => $provider['services'], + 'timezone' => $provider['timezone'] + ]; + $customer = $this->customers_model->get_row($appointment['id_users_customer']); + $customer = [ + 'id' => $customer['id'], + 'first_name' => $customer['first_name'], + 'last_name' => $customer['last_name'], + 'timezone' => $customer['timezone'], + 'address' => $customer['address'], + 'city' => $customer['city'], + 'zip_code' => $customer['zip_code'] + ]; + $customer_token = md5(uniqid(mt_rand(), TRUE)); // Save the token for 10 minutes. @@ -197,6 +245,13 @@ class Appointments extends EA_Controller { { try { + $cancel_reason = $this->input->post('cancel_reason'); + + if ($this->input->method() !== 'post' || empty($cancel_reason)) + { + show_error('Bad Request', 400); + } + // Check whether the appointment hash exists in the database. $appointments = $this->appointments_model->get_batch(['hash' => $appointment_hash]); @@ -279,22 +334,25 @@ class Appointments extends EA_Controller { $exceptions = $this->session->flashdata('book_success'); $view = [ - 'appointment_data' => $appointment, + 'appointment_data' => [ + 'start_datetime' => $appointment['start_datetime'], + 'end_datetime' => $appointment['end_datetime'], + ], 'provider_data' => [ - 'id' => $provider['id'], 'first_name' => $provider['first_name'], 'last_name' => $provider['last_name'], 'email' => $provider['email'], 'timezone' => $provider['timezone'], ], 'customer_data' => [ - 'id' => $customer['id'], 'first_name' => $customer['first_name'], 'last_name' => $customer['last_name'], 'email' => $customer['email'], 'timezone' => $customer['timezone'], ], - 'service_data' => $service, + 'service_data' => [ + 'name' => $service['name'], + ], 'company_name' => $company_name, ]; @@ -386,7 +444,7 @@ class Appointments extends EA_Controller { * * @throws Exception */ - protected function search_any_provider($service_id, $date, $hour = null) + protected function search_any_provider($service_id, $date, $hour = NULL) { $available_providers = $this->providers_model->get_available_providers(); @@ -405,7 +463,7 @@ class Appointments extends EA_Controller { // Check if the provider is available for the requested date. $available_hours = $this->availability->get_available_hours($date, $service, $provider); - if (count($available_hours) > $max_hours_count && (empty($hour) || in_array($hour, $available_hours, false))) + if (count($available_hours) > $max_hours_count && (empty($hour) || in_array($hour, $available_hours, FALSE))) { $provider_id = $provider['id']; $max_hours_count = count($available_hours); @@ -448,7 +506,7 @@ class Appointments extends EA_Controller { $captcha_phrase = $this->session->userdata('captcha_phrase'); // Validate the CAPTCHA string. - if ($require_captcha === '1' && $captcha_phrase !== $captcha) + if ($require_captcha === '1' && strtolower($captcha_phrase) !== strtolower($captcha)) { $this->output ->set_content_type('application/json') @@ -473,6 +531,16 @@ class Appointments extends EA_Controller { $customer['language'] = config('language'); $customer_id = $this->customers_model->add($customer); + $appointment_start_instance = new DateTime($appointment['start_datetime']); + $appointment['end_datetime'] = $appointment_start_instance + ->add(new DateInterval('PT' . $service['duration'] . 'M')) + ->format('Y-m-d H:i:s'); + + if ( ! in_array($service['id'], $provider['services'])) + { + throw new Exception('Invalid provider record selected for appointment.'); + } + $appointment['id_users_customer'] = $customer_id; $appointment['is_unavailable'] = (int)$appointment['is_unavailable']; // needs to be type casted $appointment['id'] = $this->appointments_model->add($appointment); diff --git a/application/controllers/Backend_api.php b/application/controllers/Backend_api.php index c10f4aab..0dfe463a 100755 --- a/application/controllers/Backend_api.php +++ b/application/controllers/Backend_api.php @@ -55,6 +55,10 @@ class Backend_api extends EA_Controller { { $this->privileges = $this->roles_model->get_privileges($this->session->userdata('role_slug')); } + else + { + show_error('Forbidden', 403); + } } /** @@ -66,6 +70,11 @@ class Backend_api extends EA_Controller { { try { + if ($this->privileges[PRIV_APPOINTMENTS]['view'] == FALSE) + { + throw new Exception('You do not have the required privileges for this task.'); + } + $start_date = $this->input->post('startDate') . ' 00:00:00'; $end_date = $this->input->post('endDate') . ' 23:59:59'; @@ -1527,18 +1536,25 @@ class Backend_api extends EA_Controller { { try { - if ( ! $this->input->post('provider_id')) + $provider_id = $this->input->post('provider_id'); + + if ( ! $provider_id) { throw new Exception('Provider id is required in order to fetch the google calendars.'); } + if ($this->privileges[PRIV_USERS]['view'] == FALSE && $provider_id !== $this->session->userdata('user_id')) + { + throw new Exception('You do not have the required privileges for this task.'); + } + // Check if selected provider has sync enabled. - $google_sync = $this->providers_model->get_setting('google_sync', $this->input->post('provider_id')); + $google_sync = $this->providers_model->get_setting('google_sync', $provider_id); if ($google_sync) { $google_token = json_decode($this->providers_model->get_setting('google_token', - $this->input->post('provider_id'))); + $provider_id)); $this->google_sync->refresh_token($google_token->refresh_token); $calendars = $this->google_sync->get_google_calendars(); diff --git a/application/controllers/Google.php b/application/controllers/Google.php index 5cfc5b68..b7ded2f5 100644 --- a/application/controllers/Google.php +++ b/application/controllers/Google.php @@ -45,9 +45,9 @@ class Google extends EA_Controller { $CI = get_instance(); // The user must be logged in. - if ($CI->session->userdata('user_id') == FALSE && is_cli() === FALSE) + if ( ! $CI->session->userdata('user_id') && ! is_cli()) { - return; + show_error('Forbidden', 403); } if ($provider_id === NULL) @@ -204,7 +204,6 @@ class Google extends EA_Controller { continue; } - // Record doesn't exist in the Easy!Appointments, so add the event now. $appointment = [ 'start_datetime' => $event_start->format('Y-m-d H:i:s'), @@ -248,6 +247,11 @@ class Google extends EA_Controller { */ public function oauth($provider_id) { + if ( ! $this->session->userdata('user_id')) + { + show_error('Forbidden', 403); + } + // Store the provider id for use on the callback function. $this->session->set_userdata('oauth_provider_id', $provider_id); @@ -268,6 +272,11 @@ class Google extends EA_Controller { */ public function oauth_callback() { + if ( ! $this->session->userdata('user_id')) + { + show_error('Forbidden', 403); + } + $code = $this->input->get('code'); if (empty($code)) @@ -298,6 +307,4 @@ class Google extends EA_Controller { $this->output->set_output('Sync provider id not specified.'); } } - - } diff --git a/application/core/EA_Controller.php b/application/core/EA_Controller.php index 7c8c1921..bb71d9f9 100644 --- a/application/core/EA_Controller.php +++ b/application/core/EA_Controller.php @@ -51,6 +51,7 @@ * @property User_model $user_model * * @property Availability $availability + * @property Captcha_builder $captcha_builder * @property Google_Sync $google_sync * @property Ics_file $ics_file * @property Notifications $notifications @@ -66,6 +67,8 @@ class EA_Controller extends CI_Controller { parent::__construct(); $this->configure_language(); + + rate_limit($this->input->ip_address()); } /** diff --git a/application/core/EA_Model.php b/application/core/EA_Model.php index 58c1ffc8..964bd39d 100644 --- a/application/core/EA_Model.php +++ b/application/core/EA_Model.php @@ -51,6 +51,7 @@ * @property User_model $user_model * * @property Availability $availability + * @property Captcha_builder $captcha_builder * @property Google_Sync $google_sync * @property Ics_file $ics_file * @property Notifications $notifications diff --git a/application/helpers/rate_limit_helper.php b/application/helpers/rate_limit_helper.php new file mode 100644 index 00000000..bd741952 --- /dev/null +++ b/application/helpers/rate_limit_helper.php @@ -0,0 +1,84 @@ + + * @copyright Copyright (c) 2013 - 2020, Alex Tselegidis + * @license http://opensource.org/licenses/GPL-3.0 - GPLv3 + * @link http://easyappointments.org + * @since v1.1.0 + * ---------------------------------------------------------------------------- */ + +if ( ! function_exists('rate_limit')) +{ + /** + * Rate-limit the application requests. + * + * Example: + * + * rate_limit($CI->input->ip_address(), 100, 300); + * + * @link https://github.com/alexandrugaidei-atomate/ratelimit-codeigniter-filebased + * + * @param string $ip Client IP address. + * @param int $max_requests Number of allowed requests, defaults to 100. + * @param int $duration In seconds, defaults to 5 minutes. + */ + function rate_limit($ip, $max_requests = 100, $duration = 300) + { + $CI =& get_instance(); + + $rate_limiting = $CI->config->item('rate_limiting'); + + if ( ! $rate_limiting) + { + return; + } + + $CI->load->driver('cache', ['adapter' => 'file']); + + $cache_key = str_replace(':', '', 'rate_limit_key_' . $ip); + + $cache_remain_time_key = str_replace(':', '', 'rate_limit_tmp_' . $ip); + + $current_time = date('Y-m-d H:i:s'); + + if ($CI->cache->get($cache_key) === FALSE) // First request + { + $current_time_plus = date('Y-m-d H:i:s', strtotime('+' . $duration . ' seconds')); + + $CI->cache->save($cache_key, 1, $duration); + + $CI->cache->save($cache_remain_time_key, $current_time_plus, $duration * 2); + } + else // Consequent request + { + $requests = $CI->cache->get($cache_key); + + $time_lost = $CI->cache->get($cache_remain_time_key); + + if ($current_time > $time_lost) + { + $current_time_plus = date('Y-m-d H:i:s', strtotime('+' . $duration . ' seconds')); + + $CI->cache->save($cache_key, 1, $duration); + + $CI->cache->save($cache_remain_time_key, $current_time_plus, $duration * 2); + } + else + { + $CI->cache->save($cache_key, $requests + 1, $duration); + } + + $requests = $CI->cache->get($cache_key); + + if ($requests > $max_requests) + { + header('HTTP/1.0 429 Too Many Requests'); + exit; + } + } + } +} diff --git a/application/models/Settings_model.php b/application/models/Settings_model.php index db648f64..7ac70a87 100644 --- a/application/models/Settings_model.php +++ b/application/models/Settings_model.php @@ -73,7 +73,7 @@ class Settings_model extends EA_Model { if ($query->num_rows() > 0) { // Update setting - if ( ! $this->db->update('settings', ['value' => $value], ['name' => $name])) + if ( ! $this->db->update('settings', ['value' => xss_clean($value)], ['name' => $name])) { throw new Exception('Could not update database setting.'); } @@ -144,7 +144,7 @@ class Settings_model extends EA_Model { foreach ($settings as $setting) { $this->db->where('name', $setting['name']); - if ( ! $this->db->update('settings', ['value' => $setting['value']])) + if ( ! $this->db->update('settings', ['value' => xss_clean($setting['value'])])) { throw new Exception('Could not save setting (' . $setting['name'] . ' - ' . $setting['value'] . ')');