initial commit

This commit is contained in:
2026-06-25 13:03:45 +06:00
commit 4589f4a8d0
3229 changed files with 941958 additions and 0 deletions

View File

@@ -0,0 +1,170 @@
<?php
/*
CaptchaComponent
*/
App::uses('Component', 'Controller');
class CaptchaComponent extends Component{
public $settings = array(
'characters' => null,
'winHeight' => 50,
'winWidth' => 320,
'fontSize' => 25,
'fontPath' => 'tahomabd.ttf',
'bgNoise' => false,
'lineNoise' => false,
'bgColor' => '#F58220',
'noiseColor' => '#000',
'textColor' => '#fff',
'noiseLevel' => '45'
);
////////////////////////////////////////////////////////////////////////////////
function __construct($collection, $settings){
$this->Controller = $collection->getController();
}
////////////////////////////////////////////////////////////////////////////////
public function ShowImage($custom = array())
{
$new_settings = array_merge($this->settings, $custom);
$this->settings = $new_settings;
$this -> win();
}
////////////////////////////////////////////////////////////////////////////////
private function win()
{
//background image
$image = imagecreatetruecolor($this->settings['winWidth'], $this->settings['winHeight'])
or die("<b>" . __FILE__ . "</b><br />" . __LINE__ . " :
" ."Cannot Initialize new GD image stream");
$bgColor = $this->hex2rgb($this->settings['bgColor']);
$noiseColor = $this->hex2rgb($this->settings['noiseColor']);
$textColor = $this->hex2rgb($this->settings['textColor']);
$bg = imagecolorallocate($image, $bgColor[0], $bgColor[1], $bgColor[2]);
imagefill($image, 10, 10, $bg);
for ($x=0; $x < $this->settings['noiseLevel']; $x++)
{
for ($y=0; $y < $this->settings['noiseLevel']; $y++)
{
$temp_color = imagecolorallocate($image,$noiseColor[0],$noiseColor[1],$noiseColor[2]);
imagesetpixel( $image, rand(0,$this->settings['winWidth']), rand(0,$this->settings['winHeight']) , $temp_color );
}
}
$char_color = imagecolorallocatealpha($image, $textColor[0], $textColor[1], $textColor[2], 0);
//Font
$font = $this->settings['fontPath'];
$font_size = $this->settings['fontSize'];
////////////////////////////////////
//Image characters
$char = "";
if ( empty($this->settings['characters']) )
{
$this->settings['characters'] = mt_rand(100,10000);
}
$r_x1 = 10; $r_x2 = 20;
$r_y1 = $this->settings['winHeight']/1.8; $r_y2 = $r_y1+10;
$this->settings['characters'] = (string)$this->settings['characters'];
for($i = 0; $i < strlen($this->settings['characters']); $i++ )
{
$char = $this->settings['characters'][$i];
$random_x = mt_rand($r_x1 , $r_x2);
$random_y = mt_rand($r_y1 , $r_y2);
$random_angle = mt_rand(-20 , 20);
imagettftext($image, $font_size, $random_angle,
$random_x, $random_y, $char_color, $font, $char);
$r_x1+=40; $r_x2+=40;
}
////////////////////////////////////
if ($this -> settings['bgNoise'])
$image = $this -> apply_wave($image, $this->settings['winWidth'],
$this->settings['winHeight']);
////////////////////////////////////
//lines
if ($this -> settings['lineNoise'])
{
for ($i=0; $i<$this->settings['winWidth']; $i++ )
{
if ($i%10 == 0)
{
imageline ( $image, $i, 0,
$i+10, 50, $char_color );
imageline ( $image, $i, 0,
$i-10, 50, $char_color );
}
}
}
////////////////////////////////////
return imagepng($image);
imagedestroy($image);
}
///////////////////////////////////////////////////////////
private function apply_wave($image, $width, $height)
{
$x_period = 10;
$y_period = 10;
$y_amplitude = 5;
$x_amplitude = 5;
$xp = $x_period*rand(1,3);
$k = rand(0,100);
for ($a = 0; $a<$width; $a++)
imagecopy($image, $image, $a-1, sin($k+$a/$xp)*$x_amplitude,
$a, 0, 1, $height);
$yp = $y_period*rand(1,2);
$k = rand(0,100);
for ($a = 0; $a<$height; $a++)
imagecopy($image, $image, sin($k+$a/$yp)*$y_amplitude,
$a-1, 0, $a, $width, 1);
return $image;
}
private function hex2rgb($hex) {
$hex = str_replace("#", "", $hex);
if(strlen($hex) == 3) {
$r = hexdec(substr($hex,0,1).substr($hex,0,1));
$g = hexdec(substr($hex,1,1).substr($hex,1,1));
$b = hexdec(substr($hex,2,1).substr($hex,2,1));
} else {
$r = hexdec(substr($hex,0,2));
$g = hexdec(substr($hex,2,2));
$b = hexdec(substr($hex,4,2));
}
$rgb = array($r, $g, $b);
//return implode(",", $rgb); // returns the rgb values separated by commas
return $rgb; // returns an array with the rgb values
}
}

View File

@@ -0,0 +1,262 @@
<?php
App::uses('Component', 'Controller');
use GuzzleHttp\Client;
require_once(APP . 'Controller' . DS . 'Traits' . DS . 'ImageUploadTrait.php');
class ImageUploadComponent extends Component
{
use ImageUploadTrait;
private $total_image = NUMBER_OF_EXPENSE_IMAGES ?? 3;
public function AddImageBucket($imagesData, $id, $unique_url, $app_name)
{
$images = null;
$imageCount = count($imagesData);
$imageResponses = [];
if ($imageCount >= 1) {
$imageResponse = null;
$message = "It is not valid format.";
$firebasePath = $this->getRootFolder() . '/' . $app_name . '/' . $unique_url . '/' . $id . '/';
foreach ($imagesData as $key => $image) {
if ($key > ($this->total_image - 1)) {
break;
}
$message = "Image has not been provided.";
if (!empty($image['data'])) {
$base64Image = $image['data'];
$decodedImage = base64_decode($base64Image);
$isImage = $this->checkImageFromBase64EncodedString($base64Image) || $this->checkImageFromBase64DecodedString($decodedImage);
$message = "Image is not valid format.";
if ($isImage) {
$no = $key + 1;
$filename = 'img' . $no . '.jpg';
$result = $this->uploadBase64Image($base64Image, $firebasePath . $filename);
if ($result['status']) {
if ($no == 1) {
$images = $filename;
} else {
$images = $images . ',' . $filename;
}
}
$message = $result['message'];
}
}
$imageResponse["img" . $key] = $message;
}
$imageResponses[$unique_url] = $imageResponse;
}
return ["images" => $images, "response" => $imageResponses];
}
public function DeleteImageBucket($Expanses, $unique_url, $app_name)
{
$deleteResponses = [];
foreach ($Expanses as $item => $Expanse) {
$firebasePath = $this->getRootFolder() . '/' . $app_name . '/' . $unique_url . '/' . $Expanse['id'] . '/';
$response = $this->deleteFolder($firebasePath);
$deleteResponses[$item] = $response['message'];
}
return $deleteResponses;
}
public function DeleteSingleImageBucket($dbimages, $id, $unique_url, $app_name, $deletedimages)
{
$images = explode(',', $dbimages);
$firebasePath = $this->getRootFolder() . '/' . $app_name . '/' . $unique_url . '/' . $id . '/';
$delete_image_responses = null;
$message = "Image not found.";
foreach ($deletedimages as $key => $deletedimage) {
$image_name = $deletedimage['name'];
if (in_array($image_name, $images)) {
$result = $this->singleImageDelete($firebasePath . $image_name);
$images = array_diff($images, array($image_name));
// if image exist in db but not in firebase
if (!$result['status']) {
$images = array_filter($images, function ($item) use ($image_name) {
return $item !== $image_name;
});
}
$message = $result['message'];
}
$delete_image_responses[$image_name] = $message;
}
return ['images' => implode(',', $images), 'response' => $delete_image_responses];
}
public function ModifyImageBucket($dbimages, $id, $unique_url, $app_name, $expanse_images)
{
$images = [];
$imageResponse = null;
if (!empty($dbimages)) {
$images = explode(',', $dbimages);
}
if (!empty($expanse_images)) {
$firebasePath = $this->getRootFolder() . '/' . $app_name . '/' . $unique_url . '/' . $id . '/';
$image_count = count($expanse_images) + count($images);
if ($image_count <= $this->total_image) {
foreach ($expanse_images as $key => $image) {
$message = "Image has not been provided.";
$filename = $this->imageUniqueName($images);
if (!empty($image['data']) && !empty($filename)) {
$base64Image = $image['data'];
$result = $this->uploadBase64Image($base64Image, $firebasePath . $filename);
if ($result['status']) {
array_push($images, $filename);
}
$message = $result['message'];
}
$imageResponse["img" . $key] = $message;
}
}
if ($image_count > $this->total_image) {
$imageResponse['status'] = false;
$imageResponse['message'] = "Should be maximum 3 images. Given {$image_count}";
}
}
return ["images" => implode(',', $images), "response" => $imageResponse];
}
private function checkImageFromBase64EncodedString($base64String)
{
$result = false;
if (preg_match('/^data:(.*?);base64,/', $base64String, $match)) {
$mimeType = $match[1];
if (strpos($mimeType, 'image') !== false) {
$result = true;
}
}
return $result;
}
private function checkImageFromBase64DecodedString($decodedData)
{
$result = false;
$finfo = new finfo(FILEINFO_MIME_TYPE);
$mimeType = $finfo->buffer($decodedData);
if (strpos($mimeType, 'image') !== false) {
$result = true;
}
return $result;
}
private function imageUniqueName($dbImages)
{
$image_name = null;
for ($i = 1; $i <= $this->total_image; $i++) {
$filename = 'img' . $i . '.jpg';
// check image exist or not
if (!in_array($filename, $dbImages)) {
$image_name = $filename;
break;
}
}
return $image_name;
}
public function getRootFolder()
{
if (strpos(HTTP_SITE_URL, "expensecount.com") !== false) {
return "prod";
} elseif (strpos(HTTP_SITE_URL, "localhost") !== false) {
return "local";
}
return "dev";
}
public function ExportImageBucket($unique_url, $app_name)
{
$firebasePath = $this->getRootFolder() . '/' . $app_name . '/' . $unique_url . '/' ;
$expire_time='+2 days';
$expiresAt = new DateTime($expire_time);
$savePath = $this->getRootFolder() . '/' . $app_name . '/zip/'.$expiresAt->format('Y-m-d H:i:s').'/' . $unique_url . '/report.zip' ;
return $this->downloadImage($firebasePath, $expire_time,$savePath);
}
public function ReceiptExportInsideFirebase($unique_url, $app_name) {
$url = 'https://us-central1-expensecount-312c7.cloudfunctions.net/generateGroupZip';
$data = [
"prefix"=> $this->getRootFolder() . '/' . $app_name . '/' ,
"unique_url"=> $unique_url,
"zip_file_prefix" => $this->getRootFolder() . '/' . $app_name . '/zip/'
];
$client = new Client();
try {
$response = $client->post($url, [
'headers' => [
'Content-Type' => 'application/json'
],
'body' => json_encode($data)
]);
$result = json_decode($response->getBody()->getContents(), true);
if(!empty($result['zip_path'])) {
$result['link'] = $this->downloadLink($result['zip_path']);
$result['expire_time'] = new DateTime('+2 days');
}
return $result;
} catch (\Exception $e) {
return ["message"=> "Error: " . $e->getMessage(),"success"=>false];
}
}
}

View File

@@ -0,0 +1,87 @@
<?php
/*
CaptchaComponent
*/
App::uses('Component', 'Controller');
class MobileDetectComponent extends Component{
protected $accept;
protected $userAgent;
protected $isMobile = false;
protected $isAndroid = null;
protected $isBlackberry = null;
protected $isIphone = null;
protected $isIpad = null;
protected $isOpera = null;
protected $isPalm = null;
protected $isWindows = null;
protected $isGeneric = null;
protected $devices = array(
"android" => "android",
"blackberry" => "blackberry",
"iphone" => "(iphone|ipod)",
"ipad" => "ipad",
"opera" => "opera mini",
"palm" => "(avantgo|blazer|elaine|hiptop|palm|plucker|xiino)",
"windows" => "windows ce; (iemobile|ppc|smartphone)",
"generic" => "(kindle|mobile|mmp|midp|o2|pda|pocket|psp|symbian|smartphone|treo|up.browser|up.link|vodafone|wap)"
);
public function __construct() {
$this->userAgent = isset( $_SERVER['HTTP_USER_AGENT'] ) ? $_SERVER['HTTP_USER_AGENT'] : '';
$this->accept = isset( $_SERVER['HTTP_ACCEPT'] ) ? $_SERVER['HTTP_ACCEPT'] : '';
if (isset($_SERVER['HTTP_X_WAP_PROFILE'])|| isset($_SERVER['HTTP_PROFILE'])) {
$this->isMobile = true;
} elseif (strpos($this->accept,'text/vnd.wap.wml') > 0 || strpos($this->accept,'application/vnd.wap.xhtml+xml') > 0) {
$this->isMobile = true;
} else {
foreach ($this->devices as $device => $regexp) {
if ($this->isDevice($device)) {
$this->isMobile = true;
}
}
}
}
/**
* Overloads isAndroid() | isBlackberry() | isOpera() | isPalm() | isWindows() | isGeneric() through isDevice()
*
* @param string $name
* @param array $arguments
* @return bool
*/
public function __call($name, $arguments) {
$device = strtolower(substr($name, 2));
if ($name == "is" . ucfirst($device)) {
return $this->isDevice($device);
} else {
trigger_error("Method $name not defined", E_USER_ERROR);
}
}
/**
* Returns true if any type of mobile device detected, including special ones
* @return bool
*/
public function isMobile() {
return $this->isMobile;
}
protected function isDevice($device) {
$var = "is" . ucfirst($device);
$return = $this->$var === null ? (bool) preg_match("/" . $this->devices[$device] . "/i", $this->userAgent) : $this->$var;
if ($device != 'generic' && $return == true) {
$this->isGeneric = false;
}
return $return;
}
}

View File

@@ -0,0 +1,162 @@
<?php
App::uses('Component', 'Controller');
App::uses('CakeEmail', 'Network/Email');
require_once(APP . 'Controller' . DS . 'Traits' . DS . 'ApiCallTrait.php');
use Google\Auth\Credentials\ServiceAccountCredentials;
class NotificationComponent extends Component
{
use ApiCallTrait;
/**
* @var AppModel|bool|mixed|Model|object|string|null
*/
private $PushToken;
public function initialize(Controller $controller)
{
$this->PushToken = ClassRegistry::init('PushToken');
}
public function sendEmail($data) {
$default = array(
'host' => Configure::read('expensecount.email.smtp.host'),
'port' => 587,
'auth' => 'plain',
'username' => Configure::read('expensecount.email.smtp.username'),
'password' => Configure::read('expensecount.email.smtp.password'),
'tsl' => false,
'transport' => 'Smtp',
'from' => array(Configure::read('expensecount.email.smtp.from') => 'ExpenseCount'),
'returnPath' => Configure::read('expensecount.email.smtp.from'),
'layout' => false,
'emailFormat' => 'html',
'charset' => 'utf-8',
'headerCharset' => 'utf-8',
);
try {
$Email = new CakeEmail($default);
$Email->to($data["email_to"]);
$Email->emailFormat('html');
$Email->template($data["template"], null)->viewVars(array('data' => $data));
$Email->subject($data["subject"]);
$Email->replyTo($data["email_from"]);
$Email->from(array(Configure::read('expensecount.email.smtp.from') => "ExpenseCount.com"));
$Email->delivery = 'Smtp';
return $Email->send();
}catch (Exception $e) {
CakeLog::write(LOG_ERR, 'Email sending failed: ' . $e->getMessage());
return $e->getMessage();
}
}
private function getAccessToken()
{
$cacheFile = TMP . 'fcm_token.cache';
if (file_exists($cacheFile)) {
$cached = json_decode(file_get_contents($cacheFile), true);
if (!empty($cached['token']) && $cached['expires_at'] > time()) {
return $cached['token'];
}
}
$scopes = ['https://www.googleapis.com/auth/firebase.messaging'];
$credentials = new ServiceAccountCredentials(
$scopes,
json_decode(file_get_contents(FIREBASE_SECRET), true)
);
$token = $credentials->fetchAuthToken();
if (!isset($token['access_token'])) {
throw new \Exception('Unable to get access token');
}
// Google tokens expire in 3600s; cache for 55 minutes to stay safely within that.
file_put_contents($cacheFile, json_encode([
'token' => $token['access_token'],
'expires_at' => time() + 3300,
]));
return $token['access_token'];
}
public function sendPushNotification($tokens, $message, $async = false)
{
$projectId = FIREBASE_PROJECT_ID;
if (empty($projectId)) {
CakeLog::write(LOG_ERR, 'Push notification: Firebase project_id not configured');
return ['error' => 'Firebase project_id not configured'];
}
$url = "https://fcm.googleapis.com/v1/projects/{$projectId}/messages:send";
try {
$accessToken = $this->getAccessToken();
} catch (\Exception $e) {
CakeLog::write(LOG_ERR, 'Push notification: Failed to get access token: ' . $e->getMessage());
return ['error' => 'Failed to get access token: ' . $e->getMessage()];
}
$responses = [];
foreach ($tokens as $key => $token) {
$payload = [
"message" => [
"token" => $token['token'],
"data" => $message,
"apns" => [
"payload" => [
"aps" => [
"alert" => [
"title" => $message['title'],
"body" => $message['body']
],
"sound" => "default",
"badge" => 1,
"mutable-content" => 1,
"content-available" => 1
]
]
]
]
];
$apiResponse = $this->fireAndForget($url, $payload, $accessToken);
$responses[] = ['request' => [$token['user_id'], $token['device_id']], 'response' => $apiResponse];
}
return $responses;
}
public function send($users,$message){
$tokens = array();
if(!empty($users)){
$filter = array('status' => 1,'user_id' => $users);
$db_token = $this->PushToken->getToken($filter);
if(!empty($db_token)) {
foreach ($db_token as $token) {
$tokens[] = ['user_id'=>$token['PushToken']['user_id'],
'device_id'=>$token['PushToken']['device_id'],
'token'=> $token['PushToken']['push_token']];
}
}
if(!empty($tokens)) {
$response = $this->sendPushNotification($tokens, $message, true);
}else{
$response["status"] = "-99";
$response["status_desc"] = "Push Token Not Found!";
$response["input"] = $users;
}
}else{
$response["status"] = "-99";
$response["status_desc"] = "User Not Found!";
$response["input"] = $users;
}
return $response;
}
}

View File

@@ -0,0 +1,44 @@
<?php
App::uses('Component', 'Controller');
class PermissionComponent extends Component
{
/**
* @var AppModel|bool|mixed|Model|object|string|null
*/
private $GroupSetting;
private $PermissionTable;
private $permission = NO_PERMIT;
private $restricted_modes = ['ON' => 1, 'OFF' => 0];
public function initialize(Controller $controller)
{
$this->GroupSetting = ClassRegistry::init('GroupSetting');
$this->PermissionTable = ClassRegistry::init('PermissionTable');
}
public function PermissionByUserId($unique_url, $user_id)
{
$group_setting = $this->GroupSetting->getGroupSettingByURL($unique_url);
if (empty($group_setting)) {
return PUBLIC_PERMIT;
}
$restricted_mode = $group_setting['is_restricted_mode'] ;
if($restricted_mode == $this->restricted_modes['OFF']){
return PUBLIC_PERMIT;
}
if ($restricted_mode == $this->restricted_modes['ON']) {
$user_permission = $this->PermissionTable->getPermissionByUrlAndUserId($unique_url, $user_id);
if (!empty($user_permission['PermissionTable'])) {
$this->permission = $user_permission['PermissionTable']['role'];
}
}
return $this->permission;
}
}

View File