Merge branch 'master' into develop

# Conflicts:
#	application/config/autoload.php
#	application/config/config.php
#	application/controllers/Appointments.php
#	application/controllers/Backend.php
#	application/controllers/Backend_api.php
#	application/controllers/Captcha.php
#	application/controllers/Console.php
#	application/controllers/Google.php
#	application/controllers/Installation.php
#	application/core/EA_Controller.php
#	application/core/EA_Model.php
#	application/libraries/Google_sync.php
#	application/libraries/Ics_file.php
#	application/models/Settings_model.php
#	application/views/appointments/book_success.php
#	assets/css/layouts/backend_layout.scss
#	assets/css/no_privileges.css
#	assets/js/backend_calendar_table_view.js
#	assets/js/backend_calendar_unavailability_events_modal.js
#	assets/js/frontend_book.js
#	assets/js/general_functions.js
#	assets/js/pages/booking_confirmation.js
#	assets/js/working_plan_exceptions_modal.js
#	composer.json
#	composer.lock
#	docs/console.md
#	docs/docker.md
#	docs/faq.md
#	docs/get-involved.md
#	docs/google-calendar-sync.md
#	docs/installation-guide.md
#	docs/manage-translations.md
#	docs/readme.md
#	docs/rest-api.md
#	docs/update-guide.md
#	gulpfile.js
#	system/core/Input.php
#	system/core/Output.php
#	system/core/Security.php
#	system/libraries/Session/drivers/Session_files_driver.php
This commit is contained in:
Alex Tselegidis 2022-03-25 10:53:46 +01:00
commit a3b8228122
35 changed files with 2277 additions and 652 deletions

View file

@ -3,6 +3,26 @@
This file contains the code changes that were introduced into each release (starting from v1.1.0) so that is easy for This file contains the code changes that were introduced into each release (starting from v1.1.0) so that is easy for
developers to maintain and readjust their custom modifications on the main project codebase. developers to maintain and readjust their custom modifications on the main project codebase.
## [1.4.3] - 2022-03-08
### Added
- #1208: Security configuration enhancements in the application.
- #1209: Add support for PHP 8.1.
### Changed
- #1207: Replace CodeIgniter with the system directory for smaller package size and more control over the framework.
- #1210: Move the change language operation into a new public controller.
- #1212: Remove the Google Calendar read-only scope use as it is not needed.
- #1213: Switch to go-to-latest database migration configuration for simplicity.
- #1216 Replace Google Client JS with the Google Calendar Template link in the book success page enhancement.
### Fixed
- #1211: The table calendar view breaks for secretaries and providers due to appointment and unavailability removals bug.
- #1214: Provider and secretary users can only add unavailabilities for their authorized users bug.
## [1.4.2] - 2021-07-27 ## [1.4.2] - 2021-07-27
### Added ### Added

View file

@ -18,6 +18,9 @@
<img alt="GitHub" src="https://img.shields.io/github/license/alextselegidis/easyappointments?style=for-the-badge"> <img alt="GitHub" src="https://img.shields.io/github/license/alextselegidis/easyappointments?style=for-the-badge">
<img alt="GitHub release (latest by date)" src="https://img.shields.io/github/v/release/alextselegidis/easyappointments?style=for-the-badge"> <img alt="GitHub release (latest by date)" src="https://img.shields.io/github/v/release/alextselegidis/easyappointments?style=for-the-badge">
<img alt="GitHub All Releases" src="https://img.shields.io/github/downloads/alextselegidis/easyappointments/total?style=for-the-badge"> <img alt="GitHub All Releases" src="https://img.shields.io/github/downloads/alextselegidis/easyappointments/total?style=for-the-badge">
<a href="https://discord.com/invite/UeeSkaw">
<img alt="Chat On Discord" src="https://img.shields.io/badge/chat-on%20discord-7289da?style=for-the-badge&logo=discord&logoColor=white">
</a>
</p> </p>
<p align="center"> <p align="center">

View file

@ -354,7 +354,7 @@ $config['sess_regenerate_destroy'] = FALSE;
$config['cookie_prefix'] = ''; $config['cookie_prefix'] = '';
$config['cookie_domain'] = ''; $config['cookie_domain'] = '';
$config['cookie_path'] = '/'; $config['cookie_path'] = '/';
$config['cookie_secure'] = FALSE; $config['cookie_secure'] = strpos($config['base_url'], 'https') !== FALSE;
/* /*
|-------------------------------------------------------------------------- |--------------------------------------------------------------------------
@ -444,6 +444,17 @@ $config['rewrite_short_tags'] = FALSE;
*/ */
$config['proxy_ips'] = ''; $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 */ /* End of file config.php */
/* Location: ./application/config/config.php */ /* Location: ./application/config/config.php */

View file

@ -21,6 +21,16 @@ use Gregwar\Captcha\CaptchaBuilder;
* @package Controllers * @package Controllers
*/ */
class Captcha extends EA_Controller { class Captcha extends EA_Controller {
/**
* Class Constructor
*/
public function __construct()
{
parent::__construct();
$this->load->library('captcha_builder');
}
/** /**
* Make a request to this method to get a captcha image. * Make a request to this method to get a captcha image.
*/ */

View file

@ -252,6 +252,11 @@ class Google extends EA_Controller {
*/ */
public function oauth(int $provider_id) public function oauth(int $provider_id)
{ {
if ( ! $this->session->userdata('user_id'))
{
show_error('Forbidden', 403);
}
// Store the provider id for use on the callback function. // Store the provider id for use on the callback function.
session(['oauth_provider_id' => $provider_id]); session(['oauth_provider_id' => $provider_id]);
@ -272,6 +277,11 @@ class Google extends EA_Controller {
*/ */
public function oauth_callback() public function oauth_callback()
{ {
if ( ! session('user_id'))
{
abort(403, 'Forbidden');
}
$code = request('code'); $code = request('code');
if (empty($code)) if (empty($code))

View file

@ -0,0 +1,77 @@
<?php defined('BASEPATH') or exit('No direct script access allowed');
/* ----------------------------------------------------------------------------
* Easy!Appointments - Open Source Web Scheduler
*
* @package EasyAppointments
* @author A.Tselegidis <alextselegidis@gmail.com>
* @copyright Copyright (c) 2013 - 2020, Alex Tselegidis
* @license https://opensource.org/licenses/GPL-3.0 - GPLv3
* @link https://easyappointments.org
* @since v1.4.3
* ---------------------------------------------------------------------------- */
/**
* Localization Controller
*
* Contains all the location related methods.
*
* @package Controllers
*/
class Localization extends EA_Controller {
/**
* Change system language for current user.
*
* The language setting is stored in session data and retrieved every time the user visits any of the system pages.
*
* Notice: This method used to be in the Backend_api.php.
*/
public function ajax_change_language()
{
try
{
// Check if language exists in the available languages.
$found = FALSE;
$language = $this->input->post('language');
if (empty($language))
{
throw new Exception('No language provided.');
}
foreach (config('available_languages') as $available_language)
{
if ($available_language === $language)
{
$found = TRUE;
break;
}
}
if ( ! $found)
{
throw new Exception('The translations for the provided language do not exist: ' . $language);
}
$this->session->set_userdata('language', $language);
$this->config->set_item('language', $language);
$response = AJAX_SUCCESS;
}
catch (Exception $exception)
{
$this->output->set_status_header(500);
$response = [
'message' => $exception->getMessage(),
'trace' => config('debug') ? $exception->getTrace() : []
];
}
$this->output
->set_content_type('application/json')
->set_output(json_encode($response));
}
}

View file

@ -56,6 +56,7 @@
* @property Api $api * @property Api $api
* @property Availability $availability * @property Availability $availability
* @property Email_messages $email_messages * @property Email_messages $email_messages
* @property Captcha_builder $captcha_builder
* @property Google_Sync $google_sync * @property Google_Sync $google_sync
* @property Ics_file $ics_file * @property Ics_file $ics_file
* @property Instance $instance * @property Instance $instance
@ -74,6 +75,8 @@ class EA_Controller extends CI_Controller {
$this->configure_language(); $this->configure_language();
$this->load_common_script_vars(); $this->load_common_script_vars();
rate_limit($this->input->ip_address());
} }
/** /**

View file

@ -0,0 +1,84 @@
<?php defined('BASEPATH') or exit('No direct script access allowed');
/* ----------------------------------------------------------------------------
* Easy!Appointments - Open Source Web Scheduler
*
* @package EasyAppointments
* @author A.Tselegidis <alextselegidis@gmail.com>
* @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 200.
* @param int $duration In seconds, defaults to 2 minutes.
*/
function rate_limit($ip, $max_requests = 100, $duration = 120)
{
$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;
}
}
}
}

View file

@ -89,7 +89,9 @@ class Availability {
$working_plan = json_decode($provider['settings']['working_plan'], TRUE); $working_plan = json_decode($provider['settings']['working_plan'], TRUE);
// Get the provider's working plan exceptions. // Get the provider's working plan exceptions.
$working_plan_exceptions = json_decode($provider['settings']['working_plan_exceptions'], TRUE); $working_plan_exceptions_json = $provider['settings']['working_plan_exceptions'];
$working_plan_exceptions = $working_plan_exceptions_json ? json_decode($provider['settings']['working_plan_exceptions'], TRUE) : NULL;
$escaped_provider_id = $this->CI->db->escape($provider['id']); $escaped_provider_id = $this->CI->db->escape($provider['id']);

View file

@ -0,0 +1,806 @@
<?php defined('BASEPATH') or exit('No direct script access allowed');
/* ----------------------------------------------------------------------------
* Easy!Appointments - Open Source Web Scheduler
*
* @package EasyAppointments
* @author A.Tselegidis <alextselegidis@gmail.com>
* @copyright Copyright (c) 2013 - 2020, Alex Tselegidis
* @license http://opensource.org/licenses/GPL-3.0 - GPLv3
* @link http://easyappointments.org
* @since v1.4.3
* ---------------------------------------------------------------------------- */
use Gregwar\Captcha\PhraseBuilder;
use Gregwar\Captcha\PhraseBuilderInterface;
/**
* Class Captcha_builder
*
* This class replaces the Gregwar\Captcha\CaptchaBuilder so that it becomes PHP 8.1 compatible.
*/
class Captcha_builder {
/**
* @var array
*/
protected $fingerprint = [];
/**
* @var bool
*/
protected $useFingerprint = FALSE;
/**
* @var array
*/
protected $textColor = [];
/**
* @var array
*/
protected $lineColor = NULL;
/**
* @var array
*/
protected $backgroundColor = NULL;
/**
* @var array
*/
protected $backgroundImages = [];
/**
* @var resource
*/
protected $contents = NULL;
/**
* @var string
*/
protected $phrase = NULL;
/**
* @var PhraseBuilderInterface
*/
protected $builder;
/**
* @var bool
*/
protected $distortion = TRUE;
/**
* The maximum number of lines to draw in front of
* the image. null - use default algorithm
*/
protected $maxFrontLines = NULL;
/**
* The maximum number of lines to draw behind
* the image. null - use default algorithm
*/
protected $maxBehindLines = NULL;
/**
* The maximum angle of char
*/
protected $maxAngle = 8;
/**
* The maximum offset of char
*/
protected $maxOffset = 5;
/**
* Is the interpolation enabled ?
*
* @var bool
*/
protected $interpolation = TRUE;
/**
* Ignore all effects
*
* @var bool
*/
protected $ignoreAllEffects = FALSE;
/**
* Allowed image types for the background images
*
* @var array
*/
protected $allowedBackgroundImageTypes = ['image/png', 'image/jpeg', 'image/gif'];
/**
* The image contents
*/
public function getContents()
{
return $this->contents;
}
/**
* Enable/Disables the interpolation
*
* @param $interpolate bool True to enable, false to disable
*
* @return Captcha_builder
*/
public function setInterpolation($interpolate = TRUE)
{
$this->interpolation = $interpolate;
return $this;
}
/**
* Temporary dir, for OCR check
*/
public $tempDir = 'temp/';
public function __construct($phrase = NULL, PhraseBuilderInterface $builder = NULL)
{
if ($builder === NULL)
{
$this->builder = new PhraseBuilder;
}
else
{
$this->builder = $builder;
}
$this->phrase = is_string($phrase) ? $phrase : $this->builder->build($phrase);
}
/**
* Setting the phrase
*/
public function setPhrase($phrase)
{
$this->phrase = (string)$phrase;
}
/**
* Enables/disable distortion
*/
public function setDistortion($distortion)
{
$this->distortion = (bool)$distortion;
return $this;
}
public function setMaxBehindLines($maxBehindLines)
{
$this->maxBehindLines = $maxBehindLines;
return $this;
}
public function setMaxFrontLines($maxFrontLines)
{
$this->maxFrontLines = $maxFrontLines;
return $this;
}
public function setMaxAngle($maxAngle)
{
$this->maxAngle = $maxAngle;
return $this;
}
public function setMaxOffset($maxOffset)
{
$this->maxOffset = $maxOffset;
return $this;
}
/**
* Gets the captcha phrase
*/
public function getPhrase()
{
return $this->phrase;
}
/**
* Returns true if the given phrase is good
*/
public function testPhrase($phrase)
{
return ($this->builder->niceize($phrase) == $this->builder->niceize($this->getPhrase()));
}
/**
* Instantiation
*/
public static function create($phrase = NULL)
{
return new self($phrase);
}
/**
* Sets the text color to use
*/
public function setTextColor($r, $g, $b)
{
$this->textColor = [$r, $g, $b];
return $this;
}
/**
* Sets the background color to use
*/
public function setBackgroundColor($r, $g, $b)
{
$this->backgroundColor = [$r, $g, $b];
return $this;
}
public function setLineColor($r, $g, $b)
{
$this->lineColor = [$r, $g, $b];
return $this;
}
/**
* Sets the ignoreAllEffects value
*
* @param bool $ignoreAllEffects
* @return Captcha_builder
*/
public function setIgnoreAllEffects($ignoreAllEffects)
{
$this->ignoreAllEffects = $ignoreAllEffects;
return $this;
}
/**
* Sets the list of background images to use (one image is randomly selected)
*/
public function setBackgroundImages(array $backgroundImages)
{
$this->backgroundImages = $backgroundImages;
return $this;
}
/**
* Draw lines over the image
*/
protected function drawLine($image, $width, $height, $tcol = NULL)
{
if ($this->lineColor === NULL)
{
$red = $this->rand(100, 255);
$green = $this->rand(100, 255);
$blue = $this->rand(100, 255);
}
else
{
$red = $this->lineColor[0];
$green = $this->lineColor[1];
$blue = $this->lineColor[2];
}
if ($tcol === NULL)
{
$tcol = imagecolorallocate($image, $red, $green, $blue);
}
if ($this->rand(0, 1))
{ // Horizontal
$Xa = $this->rand(0, $width / 2);
$Ya = $this->rand(0, $height);
$Xb = $this->rand($width / 2, $width);
$Yb = $this->rand(0, $height);
}
else
{ // Vertical
$Xa = $this->rand(0, $width);
$Ya = $this->rand(0, $height / 2);
$Xb = $this->rand(0, $width);
$Yb = $this->rand($height / 2, $height);
}
imagesetthickness($image, $this->rand(1, 3));
imageline($image, $Xa, $Ya, $Xb, $Yb, $tcol);
}
/**
* Apply some post effects
*/
protected function postEffect($image)
{
if ( ! function_exists('imagefilter'))
{
return;
}
if ($this->backgroundColor != NULL || $this->textColor != NULL)
{
return;
}
// Negate ?
if ($this->rand(0, 1) == 0)
{
imagefilter($image, IMG_FILTER_NEGATE);
}
// Edge ?
if ($this->rand(0, 10) == 0)
{
imagefilter($image, IMG_FILTER_EDGEDETECT);
}
// Contrast
imagefilter($image, IMG_FILTER_CONTRAST, $this->rand(-50, 10));
// Colorize
if ($this->rand(0, 5) == 0)
{
imagefilter($image, IMG_FILTER_COLORIZE, $this->rand(-80, 50), $this->rand(-80, 50), $this->rand(-80, 50));
}
}
/**
* Writes the phrase on the image
*/
protected function writePhrase($image, $phrase, $font, $width, $height)
{
$length = mb_strlen($phrase);
if ($length === 0)
{
return \imagecolorallocate($image, 0, 0, 0);
}
// Gets the text size and start position
$size = (int)round($width / $length) - $this->rand(0, 3) - 1;
$box = \imagettfbbox($size, 0, $font, $phrase);
$textWidth = $box[2] - $box[0];
$textHeight = $box[1] - $box[7];
$x = (int)round(($width - $textWidth) / 2);
$y = (int)round(($height - $textHeight) / 2) + $size;
if ( ! $this->textColor)
{
$textColor = [$this->rand(0, 150), $this->rand(0, 150), $this->rand(0, 150)];
}
else
{
$textColor = $this->textColor;
}
$col = \imagecolorallocate($image, $textColor[0], $textColor[1], $textColor[2]);
// Write the letters one by one, with random angle
for ($i = 0; $i < $length; $i++)
{
$symbol = mb_substr($phrase, $i, 1);
$box = \imagettfbbox($size, 0, $font, $symbol);
$w = $box[2] - $box[0];
$angle = $this->rand(-$this->maxAngle, $this->maxAngle);
$offset = $this->rand(-$this->maxOffset, $this->maxOffset);
\imagettftext($image, $size, $angle, $x, $y + $offset, $col, $font, $symbol);
$x += $w;
}
return $col;
}
/**
* Try to read the code against an OCR
*/
public function isOCRReadable()
{
if ( ! is_dir($this->tempDir))
{
@mkdir($this->tempDir, 0755, TRUE);
}
$tempj = $this->tempDir . uniqid('captcha', TRUE) . '.jpg';
$tempp = $this->tempDir . uniqid('captcha', TRUE) . '.pgm';
$this->save($tempj);
shell_exec("convert $tempj $tempp");
$value = trim(strtolower(shell_exec("ocrad $tempp")));
@unlink($tempj);
@unlink($tempp);
return $this->testPhrase($value);
}
/**
* Builds while the code is readable against an OCR
*/
public function buildAgainstOCR($width = 150, $height = 40, $font = NULL, $fingerprint = NULL)
{
do
{
$this->build($width, $height, $font, $fingerprint);
}
while ($this->isOCRReadable());
}
/**
* Generate the image
*/
public function build($width = 150, $height = 40, $font = NULL, $fingerprint = NULL)
{
if (NULL !== $fingerprint)
{
$this->fingerprint = $fingerprint;
$this->useFingerprint = TRUE;
}
else
{
$this->fingerprint = [];
$this->useFingerprint = FALSE;
}
if ($font === NULL)
{
$font = __DIR__ . '/../../vendor/gregwar/captcha/src/Gregwar/Captcha/Font/captcha' . $this->rand(0, 5) . '.ttf';
}
if (empty($this->backgroundImages))
{
// if background images list is not set, use a color fill as a background
$image = imagecreatetruecolor($width, $height);
if ($this->backgroundColor == NULL)
{
$bg = imagecolorallocate($image, $this->rand(200, 255), $this->rand(200, 255), $this->rand(200, 255));
}
else
{
$color = $this->backgroundColor;
$bg = imagecolorallocate($image, $color[0], $color[1], $color[2]);
}
$this->background = $bg;
imagefill($image, 0, 0, $bg);
}
else
{
// use a random background image
$randomBackgroundImage = $this->backgroundImages[rand(0, count($this->backgroundImages) - 1)];
$imageType = $this->validateBackgroundImage($randomBackgroundImage);
$image = $this->createBackgroundImageFromType($randomBackgroundImage, $imageType);
}
// Apply effects
if ( ! $this->ignoreAllEffects)
{
$square = $width * $height;
$effects = $this->rand($square / 3000, $square / 2000);
// set the maximum number of lines to draw in front of the text
if ($this->maxBehindLines != NULL && $this->maxBehindLines > 0)
{
$effects = min($this->maxBehindLines, $effects);
}
if ($this->maxBehindLines !== 0)
{
for ($e = 0; $e < $effects; $e++)
{
$this->drawLine($image, $width, $height);
}
}
}
// Write CAPTCHA text
$color = $this->writePhrase($image, $this->phrase, $font, $width, $height);
// Apply effects
if ( ! $this->ignoreAllEffects)
{
$square = $width * $height;
$effects = $this->rand($square / 3000, $square / 2000);
// set the maximum number of lines to draw in front of the text
if ($this->maxFrontLines != NULL && $this->maxFrontLines > 0)
{
$effects = min($this->maxFrontLines, $effects);
}
if ($this->maxFrontLines !== 0)
{
for ($e = 0; $e < $effects; $e++)
{
$this->drawLine($image, $width, $height, $color);
}
}
}
// Distort the image
if ($this->distortion && ! $this->ignoreAllEffects)
{
$image = $this->distort($image, $width, $height, $bg);
}
// Post effects
if ( ! $this->ignoreAllEffects)
{
$this->postEffect($image);
}
$this->contents = $image;
return $this;
}
/**
* Distorts the image
*/
public function distort($image, $width, $height, $bg)
{
$contents = imagecreatetruecolor($width, $height);
$X = $this->rand(0, $width);
$Y = $this->rand(0, $height);
$phase = $this->rand(0, 10);
$scale = 1.1 + $this->rand(0, 10000) / 30000;
for ($x = 0; $x < $width; $x++)
{
for ($y = 0; $y < $height; $y++)
{
$Vx = $x - $X;
$Vy = $y - $Y;
$Vn = sqrt($Vx * $Vx + $Vy * $Vy);
if ($Vn != 0)
{
$Vn2 = $Vn + 4 * sin($Vn / 30);
$nX = $X + ($Vx * $Vn2 / $Vn);
$nY = $Y + ($Vy * $Vn2 / $Vn);
}
else
{
$nX = $X;
$nY = $Y;
}
$nY = $nY + $scale * sin($phase + $nX * 0.2);
if ($this->interpolation)
{
$p = $this->interpolate(
$nX - floor($nX),
$nY - floor($nY),
$this->getCol($image, floor($nX), floor($nY), $bg),
$this->getCol($image, ceil($nX), floor($nY), $bg),
$this->getCol($image, floor($nX), ceil($nY), $bg),
$this->getCol($image, ceil($nX), ceil($nY), $bg)
);
}
else
{
$p = $this->getCol($image, round($nX), round($nY), $bg);
}
if ($p == 0)
{
$p = $bg;
}
imagesetpixel($contents, $x, $y, $p);
}
}
return $contents;
}
/**
* Saves the Captcha to a jpeg file
*/
public function save($filename, $quality = 90)
{
imagejpeg($this->contents, $filename, $quality);
}
/**
* Gets the image GD
*/
public function getGd()
{
return $this->contents;
}
/**
* Gets the image contents
*/
public function get($quality = 90)
{
ob_start();
$this->output($quality);
return ob_get_clean();
}
/**
* Gets the HTML inline base64
*/
public function inline($quality = 90)
{
return 'data:image/jpeg;base64,' . base64_encode($this->get($quality));
}
/**
* Outputs the image
*/
public function output($quality = 90)
{
imagejpeg($this->contents, NULL, $quality);
}
/**
* @return array
*/
public function getFingerprint()
{
return $this->fingerprint;
}
/**
* Returns a random number or the next number in the
* fingerprint
*/
protected function rand($min, $max)
{
if ( ! is_array($this->fingerprint))
{
$this->fingerprint = [];
}
if ($this->useFingerprint)
{
$value = current($this->fingerprint);
next($this->fingerprint);
}
else
{
$value = mt_rand((int)$min, (int)$max);
$this->fingerprint[] = $value;
}
return $value;
}
/**
* @param $x
* @param $y
* @param $nw
* @param $ne
* @param $sw
* @param $se
*
* @return int
*/
protected function interpolate($x, $y, $nw, $ne, $sw, $se)
{
[$r0, $g0, $b0] = $this->getRGB($nw);
[$r1, $g1, $b1] = $this->getRGB($ne);
[$r2, $g2, $b2] = $this->getRGB($sw);
[$r3, $g3, $b3] = $this->getRGB($se);
$cx = 1.0 - $x;
$cy = 1.0 - $y;
$m0 = $cx * $r0 + $x * $r1;
$m1 = $cx * $r2 + $x * $r3;
$r = (int)($cy * $m0 + $y * $m1);
$m0 = $cx * $g0 + $x * $g1;
$m1 = $cx * $g2 + $x * $g3;
$g = (int)($cy * $m0 + $y * $m1);
$m0 = $cx * $b0 + $x * $b1;
$m1 = $cx * $b2 + $x * $b3;
$b = (int)($cy * $m0 + $y * $m1);
return ($r << 16) | ($g << 8) | $b;
}
/**
* @param $image
* @param $x
* @param $y
*
* @return int
*/
protected function getCol($image, $x, $y, $background)
{
$L = imagesx($image);
$H = imagesy($image);
if ($x < 0 || $x >= $L || $y < 0 || $y >= $H)
{
return $background;
}
return imagecolorat($image, $x, $y);
}
/**
* @param $col
*
* @return array
*/
protected function getRGB($col)
{
return [
(int)($col >> 16) & 0xff,
(int)($col >> 8) & 0xff,
(int)($col) & 0xff,
];
}
/**
* Validate the background image path. Return the image type if valid
*
* @param string $backgroundImage
* @return string
* @throws Exception
*/
protected function validateBackgroundImage($backgroundImage)
{
// check if file exists
if ( ! file_exists($backgroundImage))
{
$backgroundImageExploded = explode('/', $backgroundImage);
$imageFileName = count($backgroundImageExploded) > 1 ? $backgroundImageExploded[count($backgroundImageExploded) - 1] : $backgroundImage;
throw new Exception('Invalid background image: ' . $imageFileName);
}
// check image type
$finfo = finfo_open(FILEINFO_MIME_TYPE); // return mime type ala mimetype extension
$imageType = finfo_file($finfo, $backgroundImage);
finfo_close($finfo);
if ( ! in_array($imageType, $this->allowedBackgroundImageTypes))
{
throw new Exception('Invalid background image type! Allowed types are: ' . join(', ', $this->allowedBackgroundImageTypes));
}
return $imageType;
}
/**
* Create background image from type
*
* @param string $backgroundImage
* @param string $imageType
* @return resource
* @throws Exception
*/
protected function createBackgroundImageFromType($backgroundImage, $imageType)
{
switch ($imageType)
{
case 'image/jpeg':
$image = imagecreatefromjpeg($backgroundImage);
break;
case 'image/png':
$image = imagecreatefrompng($backgroundImage);
break;
case 'image/gif':
$image = imagecreatefromgif($backgroundImage);
break;
default:
throw new Exception('Not supported file type for background image!');
break;
}
return $image;
}
}

View file

@ -46,7 +46,12 @@ class Google_sync {
{ {
$this->CI =& get_instance(); $this->CI =& get_instance();
$http = new GuzzleHttp\Client([
'verify' => false
]);
$this->client = new Google_Client(); $this->client = new Google_Client();
$this->client->setHttpClient($http);
$this->client->setApplicationName(config('google_application_name')); $this->client->setApplicationName(config('google_application_name'));
$this->client->setClientId(config('google_client_id')); $this->client->setClientId(config('google_client_id'));
$this->client->setClientSecret(config('google_client_secret')); $this->client->setClientSecret(config('google_client_secret'));
@ -56,7 +61,6 @@ class Google_sync {
$this->client->setAccessType('offline'); $this->client->setAccessType('offline');
$this->client->addScope([ $this->client->addScope([
Google_Service_Calendar::CALENDAR, Google_Service_Calendar::CALENDAR,
Google_Service_Calendar::CALENDAR_READONLY
]); ]);
$this->service = new Google_Service_Calendar($this->client); $this->service = new Google_Service_Calendar($this->client);
} }

View file

@ -0,0 +1,422 @@
<?php defined('BASEPATH') or exit('No direct script access allowed');
/* ----------------------------------------------------------------------------
* Easy!Appointments - Open Source Web Scheduler
*
* @package EasyAppointments
* @author A.Tselegidis <alextselegidis@gmail.com>
* @copyright Copyright (c) 2013 - 2020, Alex Tselegidis
* @license http://opensource.org/licenses/GPL-3.0 - GPLv3
* @link http://easyappointments.org
* @since v1.4.3
* ---------------------------------------------------------------------------- */
use Jsvrcek\ICS\Model\Calendar;
use Jsvrcek\ICS\Model\CalendarEvent;
use Jsvrcek\ICS\Model\CalendarFreeBusy;
use Jsvrcek\ICS\Model\CalendarTodo;
use Jsvrcek\ICS\Utility\Provider;
/**
* Class Ics_calendar
*
* This class replaces the Jsvrcek\ICS\Model\Calendar so that it uses the new Ics_provider instances, which is
* compatible to PHP 8.1.
*
* There is no other change to the original file.
*/
class Ics_calendar extends Calendar {
/**
* @var string
*/
private $version = '2.0';
/**
* @var string
*/
private $prodId = '';
/**
* @var string
*/
private $name = '';
/**
* @var string
*/
private $calendarScale = 'GREGORIAN';
/**
* @var string
*/
private $method = 'PUBLISH';
/**
* @var array
*/
private $image = [];
/**
* @var array
*/
private $customHeaders = [];
/**
* @var \DateTimeZone
*/
private $timezone;
/**
* @var Provider
*/
private $events;
/**
* @var Provider
*/
private $todos;
/**
* @var Provider
*/
private $freeBusy;
/**
* @var string
*/
private $color;
/**
* Calendar constructor.
*/
public function __construct()
{
$this->timezone = new \DateTimeZone('America/New_York');
$this->events = new Ics_provider();
$this->todos = new Ics_provider();
$this->freeBusy = new Ics_provider();
}
/**
* For use if you want CalendarExport::getStream to get events in batches from a database during
* the output of the ics feed, instead of adding all events to the Calendar object before outputting
* the ics feed.
* - CalendarExport::getStream iterates through the Calendar::$events internal data array. The $eventsProvider
* closure will be called every time this data array reaches its end during iteration, and the closure should
* return the next batch of events
* - A $startKey argument with the current key of the data array will be passed to the $eventsProvider closure
* - The $eventsProvider must return an array of CalendarEvent objects
*
* Example: Calendar::setEventsProvider(function($startKey){
* //get database rows starting with $startKey
* //return an array of CalendarEvent objects
* })
*
* @param \Closure $eventsProvider
* @return \Jsvrcek\ICS\Model\Calendar
*/
public function setEventsProvider(\Closure $eventsProvider)
{
$this->events = new Ics_provider($eventsProvider);
return $this;
}
/**
* @return string
*/
public function getVersion()
{
return $this->version;
}
/**
* @param string $version
* @return \Jsvrcek\ICS\Model\Calendar
*/
public function setVersion($version)
{
$this->version = $version;
return $this;
}
/**
* @return string
*/
public function getProdId()
{
return $this->prodId;
}
/**
* @param string $prodId
* @return \Jsvrcek\ICS\Model\Calendar
*/
public function setProdId($prodId)
{
$this->prodId = $prodId;
return $this;
}
/**
* @return string
*/
public function getName()
{
return $this->name;
}
/**
* Sets the RFC-7986 "Name" field for the calendar
*
* @param string $name
*/
public function setName($name)
{
$this->name = $name;
}
/**
* @return string
*/
public function getCalendarScale()
{
return $this->calendarScale;
}
/**
* @param string $calendarScale
* @return \Jsvrcek\ICS\Model\Calendar
*/
public function setCalendarScale($calendarScale)
{
$this->calendarScale = $calendarScale;
return $this;
}
/**
* @return string
*/
public function getMethod()
{
return $this->method;
}
/**
* @param string $method
* @return \Jsvrcek\ICS\Model\Calendar
*/
public function setMethod($method)
{
$this->method = $method;
return $this;
}
/**
* @return array
*/
public function getImage()
{
return $this->image;
}
/**
* Images can come in one of two formats:
* 1: URI - where a URI to the relevant image is provided
* 2: BINARY - Where a Binary representation of the image is provided, normally Base64 Encoded.
*
* If sending a URI for the image, set the "VALUE" key to be "URI" and provide a URI key with the relevant URI.
* IE:
* $calendar->setImage(
* 'VALUE' => 'URL',
* 'URI' => 'https://some.domain.com/path/to/image.jpg'
* );
* It is optional to add a FMTTYPE key as well in the array, to indicate relevant mime type.
* IE: 'FMTTYPE' => 'image/jpg'
*
* When sending Binary version, you must provide the encoding type of the image, as well as the encoded string.
* IE:
* $calendar->setImage(
* 'VALUE' => 'BINARY',
* 'ENCODING' => 'BASE64',
* 'BINARY' => $base64_encoded_string
* );
* For Binary, it is RECOMMENDED to add the FMTTYPE as well, but still not REQUIRED
*
* @param array $image
*/
public function setImage($image)
{
// Do some validation on provided data.
if (array_key_exists('VALUE', $image) && in_array($image['VALUE'], ['URI', 'BINARY']))
{
if ($image['VALUE'] == 'URI' && $image['URI'])
{
$new_image = [
'VALUE' => 'URI',
'URI' => $image['URI']
];
}
elseif ($image['VALUE'] == 'BINARY' && $image['ENCODING'] && $image['BINARY'])
{
$new_image = [
'VALUE' => 'BINARY',
'ENCODING' => $image['ENCODING'],
'BINARY' => $image['BINARY']
];
}
else
{
return;
}
$new_image['DISPLAY'] = isset($image['DISPLAY']) ? $image['DISPLAY'] : '';
$new_image['FMTTYPE'] = isset($image['FMTTYPE']) ? $image['FMTTYPE'] : '';
$this->image = $new_image;
}
}
/**
* @return array
*/
public function getCustomHeaders()
{
return $this->customHeaders;
}
/**
* use to add custom headers as array key-value pairs<br>
* <strong>Example:</strong> $customHeaders = array('X-WR-TIMEZONE' => 'America/New_York')
*
* @param array $customHeaders
* @return \Jsvrcek\ICS\Model\Calendar
*/
public function setCustomHeaders(array $customHeaders)
{
$this->customHeaders = $customHeaders;
return $this;
}
/**
* @param string $key
* @param string $value
* @return \Jsvrcek\ICS\Model\Calendar
*/
public function addCustomHeader($key, $value)
{
$this->customHeaders[$key] = $value;
return $this;
}
/**
* @return \DateTimeZone
*/
public function getTimezone()
{
return $this->timezone;
}
/**
* @param \DateTimeZone $timezone
* @return \Jsvrcek\ICS\Model\Calendar
*/
public function setTimezone(\DateTimeZone $timezone)
{
$this->timezone = $timezone;
return $this;
}
/**
* @return Provider
*/
public function getEvents()
{
return $this->events;
}
/**
* @param CalendarEvent $event
* @return \Jsvrcek\ICS\Model\Calendar
*/
public function addEvent(CalendarEvent $event)
{
$this->events->add($event);
return $this;
}
/**
* @return Provider returs array of CalendarTodo objects
*/
public function getTodos()
{
return $this->todos;
}
/**
* @param CalendarTodo $todo
* @return \Jsvrcek\ICS\Model\Calendar
*/
public function addTodo(CalendarTodo $todo)
{
$this->todos[] = $todo;
return $this;
}
/**
* @param array $todos
* @return \Jsvrcek\ICS\Model\Calendar
*/
public function setTodos(array $todos)
{
$this->todos = $todos;
return $this;
}
/**
* @return Provider returs array of CalendarFreeBusy objects
*/
public function getFreeBusy()
{
return $this->freeBusy;
}
/**
* @param CalendarFreeBusy $todo
* @return \Jsvrcek\ICS\Model\Calendar
*/
public function addFreeBusy(CalendarFreeBusy $todo)
{
$this->freeBusy[] = $todo;
return $this;
}
/**
* @param array $freeBusy
* @return \Jsvrcek\ICS\Model\Calendar
*/
public function setFreeBusy(array $freeBusy)
{
$this->freeBusy = $freeBusy;
return $this;
}
/**
* @return string
*/
public function getColor()
{
return $this->color;
}
/**
* Set color as CSS3 string
*
* @param string $color
* @return \Jsvrcek\ICS\Model\Calendar
*/
public function setColor($color)
{
$this->color = $color;
return $this;
}
}

View file

@ -14,7 +14,6 @@
use Jsvrcek\ICS\CalendarExport; use Jsvrcek\ICS\CalendarExport;
use Jsvrcek\ICS\CalendarStream; use Jsvrcek\ICS\CalendarStream;
use Jsvrcek\ICS\Exception\CalendarEventException; use Jsvrcek\ICS\Exception\CalendarEventException;
use Jsvrcek\ICS\Model\Calendar;
use Jsvrcek\ICS\Model\CalendarAlarm; use Jsvrcek\ICS\Model\CalendarAlarm;
use Jsvrcek\ICS\Model\CalendarEvent; use Jsvrcek\ICS\Model\CalendarEvent;
use Jsvrcek\ICS\Model\Description\Location; use Jsvrcek\ICS\Model\Description\Location;
@ -33,6 +32,22 @@ use Jsvrcek\ICS\Utility\Formatter;
* @package Libraries * @package Libraries
*/ */
class Ics_file { class Ics_file {
/**
* @var EA_Controller
*/
protected $CI;
/**
* Availability constructor.
*/
public function __construct()
{
$this->CI =& get_instance();
$this->CI->load->library('ics_provider');
$this->CI->load->library('ics_calendar');
}
/** /**
* Get the ICS file contents for the provided arguments. * Get the ICS file contents for the provided arguments.
* *
@ -155,7 +170,7 @@ class Ics_file {
$event->setOrganizer($organizer); $event->setOrganizer($organizer);
// Setup calendar. // Setup calendar.
$calendar = new Calendar(); $calendar = new Ics_calendar();
$calendar $calendar
->setProdId('-//EasyAppointments//Open Source Web Scheduler//EN') ->setProdId('-//EasyAppointments//Open Source Web Scheduler//EN')

View file

@ -0,0 +1,175 @@
<?php defined('BASEPATH') or exit('No direct script access allowed');
/* ----------------------------------------------------------------------------
* Easy!Appointments - Open Source Web Scheduler
*
* @package EasyAppointments
* @author A.Tselegidis <alextselegidis@gmail.com>
* @copyright Copyright (c) 2013 - 2020, Alex Tselegidis
* @license http://opensource.org/licenses/GPL-3.0 - GPLv3
* @link http://easyappointments.org
* @since v1.4.3
* ---------------------------------------------------------------------------- */
/**
* Class Ics_calendar
*
* This class replaces the Jsvrcek\ICS\Utility\Provider so that it becomes a PHP 8.1 compatible Iterator class.
*
* Since the method signatures changed in PHP 8.1, the ReturnTypeWillChange attribute allows us to keep compatibility
* between different PHP versions.
*/
class Ics_provider implements \Iterator {
/**
* @var \Closure
*/
private $provider;
/**
* @var array
*/
public $data = [];
/**
* @var array
*/
public $manuallyAddedData = [];
/**
* @var integer
*/
private $key;
/**
* @var mixed
*/
private $first;
/**
* @param \Closure $provider An optional closure for adding items in batches during iteration. The closure will be
* called each time the end of the internal data array is reached during iteration, and the current data
* array key value will be passed as an argument. The closure should return an array containing the next
* batch of items.
*/
public function __construct(\Closure $provider = NULL)
{
$this->provider = $provider;
}
/**
* for manually adding items, rather than using a provider closure to add items in batches during iteration
* Cannot be used in conjunction with a provider closure!
*
* @param mixed $item
* @return void
*/
#[ReturnTypeWillChange]
public function add($item)
{
$this->manuallyAddedData[] = $item;
}
/**
* @return false|mixed
* @see Iterator::current()
*/
#[ReturnTypeWillChange]
public function current()
{
return current($this->data);
}
/**
* @return float|int|null
* @see Iterator::key()
*/
#[ReturnTypeWillChange]
public function key()
{
return $this->key;
}
/**
* @return void
* @see Iterator::next()
*/
#[ReturnTypeWillChange]
public function next()
{
array_shift($this->data);
$this->key++;
}
/**
* @return void
* @see Iterator::rewind()
*/
#[ReturnTypeWillChange]
public function rewind()
{
$this->data = [];
$this->key = 0;
}
/**
* get next batch from provider if data array is at the end
*
* @return bool
* @see Iterator::valid()
*/
#[ReturnTypeWillChange]
public function valid()
{
if (count($this->data) < 1)
{
if ($this->provider instanceof \Closure)
{
$this->data = $this->provider->__invoke($this->key);
if (isset($this->data[0]))
{
$this->first = $this->data[0];
}
}
else
{
$this->data = $this->manuallyAddedData;
$this->manuallyAddedData = [];
}
}
return count($this->data) > 0;
}
/**
* Returns first event
*
* @return false|mixed
*/
#[ReturnTypeWillChange]
public function first()
{
if (isset($this->first))
{
return $this->first;
}
if ($this->provider instanceof \Closure)
{
if ($this->valid())
{
return $this->first;
}
else
{
return FALSE;
}
}
if ( ! isset($this->manuallyAddedData[0]))
{
return FALSE;
}
return $this->manuallyAddedData[0];
}
}

View file

View file

View file

@ -26,19 +26,26 @@
"services" "services"
], ],
"minimum-stability": "stable", "minimum-stability": "stable",
"autoload": {
"psr-4": {
"EA\\Engine\\": "engine/"
}
},
"require": { "require": {
"php": ">=7.4", "php": ">=7.3",
"ext-curl": "*", "ext-curl": "*",
"ext-json": "*", "ext-json": "*",
"ext-mbstring": "*", "ext-mbstring": "*",
"ext-gd": "*", "ext-gd": "*",
"gregwar/captcha": "^1.1", "gregwar/captcha": "^1.1",
"phpmailer/phpmailer": "^6.1",
"jsvrcek/ics": "^0.8", "jsvrcek/ics": "^0.8",
"google/apiclient": "^2.12" "monolog/monolog": "^1",
"google/apiclient": "^2.0"
}, },
"require-dev": { "require-dev": {
"roave/security-advisories": "dev-master", "roave/security-advisories": "dev-master",
"phpunit/phpunit": "^9.5" "phpunit/phpunit": "^9"
}, },
"scripts": { "scripts": {
"test": "php vendor/bin/phpunit tests" "test": "php vendor/bin/phpunit tests"

949
composer.lock generated

File diff suppressed because it is too large Load diff

View file

@ -89,6 +89,6 @@ php index.php console help
This command will give more information about the console capabilities. This command will give more information about the console capabilities.
*This document applies to Easy!Appointments v1.4.2.* *This document applies to Easy!Appointments v1.4.3.*
[Back](readme.md) [Back](readme.md)

View file

@ -45,6 +45,6 @@ You can remove the docker containers with `docker rm easyappointments-server eas
You can remove the server image with `docker rmi easyappointments-server:v1`. You can remove the server image with `docker rmi easyappointments-server:v1`.
*This document applies to Easy!Appointments v1.4.2.* *This document applies to Easy!Appointments v1.4.3.*
[Back](readme.md) [Back](readme.md)

View file

@ -54,6 +54,6 @@ You get this warning because PHP is not configured with a timezone setting. This
`date_default_timezone_set('America/Los_Angeles'); // Use your own timezone string.` `date_default_timezone_set('America/Los_Angeles'); // Use your own timezone string.`
*This document applies to Easy!Appointments v1.4.2.* *This document applies to Easy!Appointments v1.4.3.*
[Back](readme.md) [Back](readme.md)

View file

@ -20,6 +20,6 @@ You are more than welcome to help with the translation progress of the user inte
It would be much appreciated if you would take 5 minutes of your time to fill this small form on your experience with Easy!Appointments. User feedback is very important and will help with the future planning of the project. Fill the [E!A Feedback Form](https://docs.google.com/forms/d/15dw1jl7lUgw4q-XXMn13Gx_e8zJxAiyWYMOdqtZqIHU/viewform). It would be much appreciated if you would take 5 minutes of your time to fill this small form on your experience with Easy!Appointments. User feedback is very important and will help with the future planning of the project. Fill the [E!A Feedback Form](https://docs.google.com/forms/d/15dw1jl7lUgw4q-XXMn13Gx_e8zJxAiyWYMOdqtZqIHU/viewform).
*This document applies to Easy!Appointments v1.4.2.* *This document applies to Easy!Appointments v1.4.3.*
[Back](readme.md) [Back](readme.md)

View file

@ -49,6 +49,6 @@ Google Developers https://developers.google.com/google-apps/calendar
E!A Support Group https://groups.google.com/forum/#!forum/easy-appointments E!A Support Group https://groups.google.com/forum/#!forum/easy-appointments
*This document applies to Easy!Appointments v1.4.2.* *This document applies to Easy!Appointments v1.4.3.*
[Back](readme.md) [Back](readme.md)

View file

@ -29,6 +29,6 @@ Finally just add a link in your website that points to your Easy!Appointments in
Happy Bookin'! Happy Bookin'!
*This document applies to Easy!Appointments v1.4.2.* *This document applies to Easy!Appointments v1.4.3.*
[Back](readme.md) [Back](readme.md)

View file

@ -14,6 +14,6 @@ Easy!Appointments is based upon CodeIgniter (PHP Framework) and it uses its buil
Follow these steps in order to add or adjust your translations and modify the message of the user interface of Easy!Appointments. If you want contribute to the translation process of Easy!Appointments please read the [Get Involved](https://github.com/alextselegidis/easyappointments/wiki/Get-Involved!) wiki page for more information. Please share your translations with the user community. Follow these steps in order to add or adjust your translations and modify the message of the user interface of Easy!Appointments. If you want contribute to the translation process of Easy!Appointments please read the [Get Involved](https://github.com/alextselegidis/easyappointments/wiki/Get-Involved!) wiki page for more information. Please share your translations with the user community.
*This document applies to Easy!Appointments v1.4.2.* *This document applies to Easy!Appointments v1.4.3.*
[Back](readme.md) [Back](readme.md)

View file

@ -11,4 +11,4 @@ Welcome to the documentation pages of Easy!Appointments. Navigate through the av
- [Google Calendar Sync](google-calendar-sync.md) - [Google Calendar Sync](google-calendar-sync.md)
- [FAQ](faq.md) - [FAQ](faq.md)
*This document applies to Easy!Appointments v1.4.2.* *This document applies to Easy!Appointments v1.4.3.*

View file

@ -498,6 +498,6 @@ fastcgi_param PHP_AUTH_PW $http_authorization;
[[Source]](http://serverfault.com/a/520943) [[Source]](http://serverfault.com/a/520943)
*This document applies to Easy!Appointments v1.4.2.* *This document applies to Easy!Appointments v1.4.3.*
[Back](readme.md) [Back](readme.md)

View file

@ -110,6 +110,6 @@ Open your browser to the Easy!Appointments installation URL, login to the backen
Use the data of the old `configuration.php` file in the new `config.php`. Use the data of the old `configuration.php` file in the new `config.php`.
*This document applies to Easy!Appointments v1.4.2.* *This document applies to Easy!Appointments v1.4.3.*
[Back](readme.md) [Back](readme.md)

View file

@ -52,6 +52,7 @@ function archive(done) {
fs.copySync('storage/uploads/index.html', 'build/storage/uploads/index.html'); fs.copySync('storage/uploads/index.html', 'build/storage/uploads/index.html');
fs.copySync('index.php', 'build/index.php'); fs.copySync('index.php', 'build/index.php');
fs.copySync('patch.php', 'build/patch.php');
fs.copySync('composer.json', 'build/composer.json'); fs.copySync('composer.json', 'build/composer.json');
fs.copySync('composer.lock', 'build/composer.lock'); fs.copySync('composer.lock', 'build/composer.lock');
fs.copySync('config-sample.php', 'build/config-sample.php'); fs.copySync('config-sample.php', 'build/config-sample.php');

272
patch.php Normal file
View file

@ -0,0 +1,272 @@
<?php // Place this file in the root E!A directory and open it with the browser or execute in terminal.
/* ----------------------------------------------------------------------------
* Easy!Appointments - Patch Utility Script
*
* @package EasyAppointmentsPatch
* @version 1.0.0
* @author A.Tselegidis <info@alextselegidis.com>
* @copyright Copyright (c) 2013 - 2022, Alex Tselegidis
* @license https://opensource.org/licenses/GPL-3.0 - GPLv3
* @link https://easyappointments.org
* @support Easy!Appointments v1.x.x
* ---------------------------------------------------------------------------- */
// Config
define('FILES_JSON_URL', 'https://cdn.easyappointments.org/patch/files.json');
// Setup
error_reporting(E_ALL);
ini_set('display_errors', TRUE);
define('LINE_BREAK', php_sapi_name() === 'cli' ? "\n" : '<br>');
// Functions
function detect_local_version()
{
$config_file_path = __DIR__ . '/application/config/config.php';
if ( ! file_exists($config_file_path))
{
die('Failed to detect the local Easy!Appointments version, please move the patch.php script in the root directory of your Easy!Appointments installation.');
}
$contents = file_get_contents($config_file_path);
if ($contents === FALSE)
{
die('Could not read the local configuration file, please check the file permissions make sure it is readable: ' . $config_file_path);
}
preg_match("/config\['version'].*=.*'(.*)';/", $contents, $matches);
if (empty($matches) || empty($matches[1]))
{
die('Could not parse the version of your installation from "' . $config_file_path . '". Please make sure this file is in its original form.');
}
return $matches[1];
}
function get_applied_patches()
{
$patch_log_file_path = __DIR__ . '/patch-log.php';
if ( ! file_exists($patch_log_file_path))
{
return [];
}
return require $patch_log_file_path;
}
function get_pending_patches($local_version, $applied_patches)
{
$files_json_contents = file_get_contents(FILES_JSON_URL);
if ($files_json_contents === FALSE)
{
die('Could not read the remote "files.json", make sure the "allow_url_fopen" configuration is "On" inside your "php.ini" file:' . php_ini_loaded_file());
}
$all_patches = json_decode($files_json_contents, TRUE);
if (empty($all_patches))
{
die('Could not fetch remote patch information, please try again later.');
}
$version_patches = array_filter($all_patches, function ($single_patch_file) use ($local_version) {
return in_array($local_version, $single_patch_file['versions'], FALSE);
});
$pending_patches = array_filter($version_patches, function ($single_patch_file) use ($applied_patches) {
$version_patch_filename = basename($single_patch_file['url']);
foreach ($applied_patches as $applied_patch)
{
if (basename($applied_patch['url']) === $version_patch_filename)
{
return FALSE;
}
}
return TRUE;
});
if (empty($pending_patches))
{
die('There are no new patches to apply, you may check again later.');
}
return $pending_patches;
}
function apply_pending_patches($local_version, $pending_patches)
{
$new_patches = [];
foreach ($pending_patches as $pending_patch)
{
$patch_contents = file_get_contents($pending_patch['url']);
if ($patch_contents === FALSE)
{
die('Could not read the remote "' . basename($pending_patch['url']) . '", make sure the "allow_url_fopen" configuration is "On" inside your "php.ini" file.');
}
if (empty($patch_contents))
{
die('No contents received while fetching: ' . $pending_patch['url']);
}
preg_match('/Index: (.*)/', $patch_contents, $file_path_match);
preg_match('/@@ (.*) (.*) @@/', $patch_contents, $position_match);
$patch_body = substr($patch_contents, strpos($patch_contents, '@@'));
$patch_body_lines = explode("\n", $patch_body);
array_shift($patch_body_lines); // Remove the first @@ line of the patch body.
$original_code_lines = [];
foreach ($patch_body_lines as $patch_line)
{
if ( ! empty($patch_line[0]) && $patch_line[0] !== '+')
{
$original_code_lines[] = substr($patch_line, 1);
}
}
$trimmed_original_code_lines = array_map('trim', $original_code_lines);
$modified_code_lines = [];
foreach ($patch_body_lines as $patch_line)
{
if ( ! empty($patch_line[0]) && $patch_line[0] !== '-')
{
$modified_code_lines[] = substr($patch_line, 1);
}
}
$trimmed_modified_code_lines = array_map('trim', $modified_code_lines);
$file_code_contents = file_get_contents($file_path_match[1]);
if ($file_code_contents === FALSE)
{
die('Could not read the local source code file, please check the file permissions make sure it is readable: ' . $file_path_match[1]);
}
$file_code_lines = explode("\n", $file_code_contents);
$affected_position = explode(',', $position_match[1]);
$affected_code_lines = array_slice($file_code_lines, abs($affected_position[0]) - 1, $affected_position[1]);
$trimmed_affected_code_lines = array_map('trim', $affected_code_lines);
if ($trimmed_affected_code_lines === $trimmed_original_code_lines)
{
$pre_change_code_lines = array_slice($file_code_lines, 0, abs($affected_position[0]) - 1);
$post_change_code_lines = array_slice($file_code_lines, abs($affected_position[0]) + $affected_position[1] - 1);
$replaced_file_code_lines = array_merge($pre_change_code_lines, $modified_code_lines, $post_change_code_lines);
$patched_file_contents = implode("\n", $replaced_file_code_lines);
$result = file_put_contents($file_path_match[1], $patched_file_contents);
if ($result === FALSE)
{
die('Could not write the local source code file, please check the file permissions make sure it is writable: ' . $file_path_match[1]);
}
}
$success = TRUE;
$message = '';
if ($trimmed_affected_code_lines !== $trimmed_original_code_lines && array_intersect($trimmed_affected_code_lines, $trimmed_modified_code_lines) !== $trimmed_affected_code_lines)
{
$success = FALSE;
$message = 'IMPORTANT: The patch "' . basename($pending_patch['url']) . '" cannot be applied, because your local codebase is customized. Download and apply it manually: ' . $pending_patch['url'];
echo LINE_BREAK . LINE_BREAK . $message . LINE_BREAK;
}
$new_patches[] = [
'applied_at' => date('Y-m-d H:i:s'),
'local_version' => $local_version,
'url' => $pending_patch['url'],
'success' => $success,
'message' => $message
];
}
return $new_patches;
}
function update_patch_log($applied_patches, $new_patches)
{
$persisted_patches = array_merge($applied_patches, $new_patches);
$patch_log_file_path = __DIR__ . '/patch-log.php';
$contents = '
<?php
return ' . preg_replace("/[0-9]+ \=\>/i", '', var_export($persisted_patches, TRUE)) . ';';
$result = file_put_contents($patch_log_file_path, $contents);
if ($result === FALSE)
{
die('Could not write the local "patch-log.php" file, please check the file permissions make sure it is writable: ' . $patch_log_file_path);
}
}
function get_new_patch_filenames($new_patches)
{
$successful_new_patches = array_filter($new_patches, function ($new_patch) {
return $new_patch['success'];
});
$patch_filenames = array_map(function ($successful_new_patch) {
return basename($successful_new_patch['url']);
}, $successful_new_patches);
return implode(LINE_BREAK . LINE_BREAK . '○ ', $patch_filenames);
}
// Run
echo LINE_BREAK . '➜ Easy!Appointments - Patch Utility Script' . LINE_BREAK . LINE_BREAK;
$local_version = detect_local_version();
$applied_patches = get_applied_patches();
$pending_patches = get_pending_patches($local_version, $applied_patches);
$new_patches = apply_pending_patches($local_version, $pending_patches);
if (empty($new_patches))
{
echo LINE_BREAK . '➜ No patches were applied, please check the PHP error logs for more information at: ' . ini_get('error_log') . LINE_BREAK;
}
else
{
echo LINE_BREAK . 'The following patches were successfully applied: ' . LINE_BREAK . LINE_BREAK . '○ ' . get_new_patch_filenames($new_patches) . LINE_BREAK;
update_patch_log($applied_patches, $new_patches);
}

View file

@ -508,7 +508,7 @@ class CI_Input {
$which = FILTER_FLAG_IPV6; $which = FILTER_FLAG_IPV6;
break; break;
default: default:
$which = NULL; $which = [];
break; break;
} }

View file

@ -454,7 +454,7 @@ class CI_Output {
if ($this->parse_exec_vars === TRUE) if ($this->parse_exec_vars === TRUE)
{ {
$memory = round(memory_get_usage() / 1024 / 1024, 2).'MB'; $memory = round(memory_get_usage() / 1024 / 1024, 2).'MB';
$output = str_replace(array('{elapsed_time}', '{memory_usage}'), array($elapsed, $memory), $output); $output = str_replace(array('{elapsed_time}', '{memory_usage}'), array($elapsed, $memory), (string)$output);
} }
// -------------------------------------------------------------------- // --------------------------------------------------------------------

View file

@ -280,7 +280,7 @@ class CI_Security {
config_item('cookie_path'), config_item('cookie_path'),
config_item('cookie_domain'), config_item('cookie_domain'),
$secure_cookie, $secure_cookie,
config_item('cookie_httponly') (bool)config_item('cookie_httponly')
); );
log_message('info', 'CSRF cookie sent'); log_message('info', 'CSRF cookie sent');

View file

@ -129,6 +129,7 @@ class CI_Session_files_driver extends CI_Session_driver implements SessionHandle
* @param string $name Session cookie name * @param string $name Session cookie name
* @return bool * @return bool
*/ */
#[ReturnTypeWillChange]
public function open($save_path, $name) public function open($save_path, $name)
{ {
if ( ! is_dir($save_path)) if ( ! is_dir($save_path))
@ -165,7 +166,8 @@ class CI_Session_files_driver extends CI_Session_driver implements SessionHandle
* @param string $session_id Session ID * @param string $session_id Session ID
* @return string Serialized session data * @return string Serialized session data
*/ */
public function read($session_id) #[ReturnTypeWillChange]
public function read($session_id)
{ {
// This might seem weird, but PHP 5.6 introduces session_reset(), // This might seem weird, but PHP 5.6 introduces session_reset(),
// which re-reads session data // which re-reads session data
@ -238,7 +240,8 @@ class CI_Session_files_driver extends CI_Session_driver implements SessionHandle
* @param string $session_data Serialized session data * @param string $session_data Serialized session data
* @return bool * @return bool
*/ */
public function write($session_id, $session_data) #[ReturnTypeWillChange]
public function write($session_id, $session_data)
{ {
// If the two IDs don't match, we have a session_regenerate_id() call // If the two IDs don't match, we have a session_regenerate_id() call
// and we need to close the old handle and open a new one // and we need to close the old handle and open a new one
@ -295,7 +298,8 @@ class CI_Session_files_driver extends CI_Session_driver implements SessionHandle
* *
* @return bool * @return bool
*/ */
public function close() #[ReturnTypeWillChange]
public function close()
{ {
if (is_resource($this->_file_handle)) if (is_resource($this->_file_handle))
{ {
@ -318,7 +322,8 @@ class CI_Session_files_driver extends CI_Session_driver implements SessionHandle
* @param string $session_id Session ID * @param string $session_id Session ID
* @return bool * @return bool
*/ */
public function destroy($session_id) #[ReturnTypeWillChange]
public function destroy($session_id)
{ {
if ($this->close() === $this->_success) if ($this->close() === $this->_success)
{ {
@ -359,7 +364,8 @@ class CI_Session_files_driver extends CI_Session_driver implements SessionHandle
* @param int $maxlifetime Maximum lifetime of sessions * @param int $maxlifetime Maximum lifetime of sessions
* @return bool * @return bool
*/ */
public function gc($maxlifetime) #[ReturnTypeWillChange]
public function gc($maxlifetime)
{ {
if ( ! is_dir($this->_config['save_path']) OR ($directory = opendir($this->_config['save_path'])) === FALSE) if ( ! is_dir($this->_config['save_path']) OR ($directory = opendir($this->_config['save_path'])) === FALSE)
{ {