shell bypass 403

UnknownSec Shell

: /home/bouloter/sms/sauvegarde/app/Models/ [ drwxr-xr-x ]

name : Campaigns.php
<?php

namespace App\Models;

use App\Library\RouletteWheel;
use App\Library\SMSCounter;
use App\Library\Tool;
use Carbon\Carbon;
use Exception;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Support\Facades\DB;
use libphonenumber\NumberParseException;
use libphonenumber\PhoneNumberUtil;
use Throwable;
use Illuminate\Support\Facades\Log as MailLog;

/**
 * @method static where(string $string, string $uid)
 * @method static create(array $array)
 * @method static find($campaign_id)
 * @method static cursor()
 * @method static whereIn(string $string, mixed $ids)
 * @method static count()
 */
class Campaigns extends SendCampaignSMS
{

    /**
     * Campaign status
     */
    const STATUS_NEW = 'new';
    const STATUS_QUEUED = 'queued';
    const STATUS_SENDING = 'sending';
    const STATUS_FAILED = 'failed';
    const STATUS_DELIVERED = 'delivered';
    const STATUS_CANCELLED = 'cancelled';
    const STATUS_SCHEDULED = 'scheduled';
    const STATUS_PROCESSING = 'processing';


    /*
     * Campaign type
     */
    const TYPE_ONETIME = 'onetime';
    const TYPE_RECURRING = 'recurring';


    // Campaign settings
    const WORKER_DELAY = 1;

    public static $serverPools = [];
    public static $senderIdPools = [];
    protected $sendingSevers = null;
    protected $senderIds = null;
    protected $currentSubscription;

    protected $fillable = [
            'user_id',
            'campaign_name',
            'message',
            'media_url',
            'language',
            'gender',
            'sms_type',
            'upload_type',
            'status',
            'reason',
            'api_key',
            'cache',
            'timezone',
            'schedule_time',
            'schedule_type',
            'frequency_cycle',
            'frequency_amount',
            'frequency_unit',
            'recurring_end',
            'run_at',
            'delivery_at',
            'batch_id',
            'running_pid',
    ];

    protected $dates = ['created_at', 'updated_at', 'run_at', 'delivery_at', 'schedule_time', 'recurring_end'];

    /**
     * Bootstrap any application services.
     */
    public static function boot()
    {
        parent::boot();

        // Create uid when creating list.
        static::creating(function ($item) {
            // Create new uid
            $uid = uniqid();
            while (self::where('uid', $uid)->count() > 0) {
                $uid = uniqid();
            }
            $item->uid = $uid;
        });
    }


    /**
     * get user
     *
     * @return BelongsTo
     *
     */
    public function user(): BelongsTo
    {
        return $this->belongsTo(User::class);
    }

    /**
     * get customer
     *
     * @return BelongsTo
     *
     */
    public function customer(): BelongsTo
    {
        return $this->belongsTo(Customer::class, 'user_id');
    }

    /**
     * get sending server
     *
     * @return BelongsTo
     *
     */
    public function sendingServer(): BelongsTo
    {
        return $this->belongsTo(SendingServer::class);
    }

    /**
     * get reports
     *
     * @return HasMany
     */
    public function reports(): HasMany
    {
        return $this->hasMany(Reports::class, 'campaign_id', 'id');
    }

    /**
     * associate with contact groups
     *
     * @return HasMany
     */
    public function contactList(): HasMany
    {
        return $this->hasMany(CampaignsList::class, 'campaign_id');
    }

    /**
     * associate with recipients
     *
     * @return HasMany
     */
    public function recipients(): HasMany
    {
        return $this->hasMany(CampaignsRecipients::class, 'campaign_id');
    }


    /**
     * Get schedule recurs available values.
     *
     * @return array
     */
    public static function scheduleCycleValues(): array
    {
        return [
                'daily'   => [
                        'frequency_amount' => 1,
                        'frequency_unit'   => 'day',
                ],
                'monthly' => [
                        'frequency_amount' => 1,
                        'frequency_unit'   => 'month',
                ],
                'yearly'  => [
                        'frequency_amount' => 1,
                        'frequency_unit'   => 'year',
                ],
        ];
    }

    /**
     * Frequency time unit options.
     *
     * @return array
     */
    public static function timeUnitOptions(): array
    {
        return [
                ['value' => 'day', 'text' => 'day'],
                ['value' => 'week', 'text' => 'week'],
                ['value' => 'month', 'text' => 'month'],
                ['value' => 'year', 'text' => 'year'],
        ];
    }


    /**
     * contact count
     *
     * @param  false  $cache
     *
     * @return mixed|null
     */
    public function contactCount(bool $cache = false)
    {
        if ($cache) {
            return $this->readCache('ContactCount', 0);
        }
        $list_ids   = $this->contactList()->select('contact_list_id')->cursor()->pluck('contact_list_id')->all();
        $list_count = Contacts::whereIn('group_id', $list_ids)->where('status', 'subscribe')->count();

        $recipients_count = $this->recipients()->count();

        return $list_count + $recipients_count;

    }

    /**
     * show delivered count
     *
     * @param  false  $cache
     *
     * @return int
     */
    public function deliveredCount(bool $cache = false): int
    {
        if ($cache) {
            return $this->readCache('DeliveredCount', 0);
        }

        return $this->reports()->where('campaign_id', $this->id)->where('status', 'like', '%Delivered%')->count();
    }

    /**
     * show failed count
     *
     * @param  false  $cache
     *
     * @return int
     */
    public function failedCount(bool $cache = false): int
    {
        if ($cache) {
            return $this->readCache('FailedDeliveredCount', 0);
        }

        return $this->reports()->where('campaign_id', $this->id)->where('status', 'not like', '%Delivered%')->count();
    }

    /**
     * show not delivered count
     *
     * @param  false  $cache
     *
     * @return int
     */
    public function notDeliveredCount(bool $cache = false): int
    {
        if ($cache) {
            return $this->readCache('NotDeliveredCount', 0);
        }

        return $this->reports()->where('campaign_id', $this->id)->where('status', 'like', '%Sent%')->count();
    }

    public function nextScheduleDate($startDate, $interval, $intervalCount)
    {

        return match ($interval) {
            'month' => $startDate->addMonthsNoOverflow($intervalCount),
            'day' => $startDate->addDay($intervalCount),
            'week' => $startDate->addWeek($intervalCount),
            'year' => $startDate->addYearsNoOverflow($intervalCount),
            default => null,
        };
    }

    /**
     * Update Campaign cached data.
     *
     * @param  null  $key
     */
    public function updateCache($key = null)
    {
        // cache indexes
        $index = [
                'DeliveredCount'       => function ($campaign) {
                    return $campaign->deliveredCount();
                },
                'FailedDeliveredCount' => function ($campaign) {
                    return $campaign->failedCount();
                },
                'NotDeliveredCount'    => function ($campaign) {
                    return $campaign->notDeliveredCount();
                },
                'ContactCount'         => function ($campaign) {
                    return $campaign->contactCount(true);
                },
        ];

        // retrieve cached data
        $cache = json_decode($this->cache, true);
        if (is_null($cache)) {
            $cache = [];
        }

        if (is_null($key)) {
            foreach ($index as $key => $callback) {
                $cache[$key] = $callback($this);
            }
        } else {
            $callback    = $index[$key];
            $cache[$key] = $callback($this);
        }

        // write back to the DB
        $this->cache = json_encode($cache);
        $this->save();
    }

    /**
     * Retrieve Campaign cached data.
     *
     * @param $key
     * @param  null  $default
     *
     * @return mixed
     */
    public function readCache($key, $default = null)
    {
        $cache = json_decode($this->cache, true);
        if (is_null($cache)) {
            return $default;
        }
        if (array_key_exists($key, $cache)) {
            if (is_null($cache[$key])) {
                return $default;
            } else {
                return $cache[$key];
            }
        } else {
            return $default;
        }
    }

    /**
     * get active plan sending servers
     *
     * @return mixed
     */
    public function activePlanSendingServers()
    {
        return PlansSendingServer::where('plan_id', $this->user->customer->activeSubscription()->plan_id);
    }

    /**
     * get active customer sending servers
     *
     * @return mixed
     */
    public function activeCustomerSendingServers()
    {
        return SendingServer::where('user_id', $this->user->id)->where('status', true);
    }

    public function getCurrentSubscription()
    {
        if (empty($this->currentSubscription)) {
            $this->currentSubscription = $this->user->customer->activeSubscription();
        }

        return $this->currentSubscription;
    }

    public function getSendingServers()
    {
        if ( ! is_null($this->sendingSevers)) {
            return $this->sendingSevers;
        }

        $sending_server_id   = CampaignsSendingServer::where('campaign_id', $this->id)->first()->sending_server_id;
        $this->sendingSevers = SendingServer::find($sending_server_id);

        return $this->sendingSevers;
    }

    /**
     * get sender ids
     *
     * @return array
     */
    public function getSenderIds(): array
    {

        if ( ! is_null($this->senderIds)) {
            return $this->senderIds;
        }

        $result = CampaignsSenderid::where('campaign_id', $this->id)->cursor()->map(function ($sender_id) {
            return [$sender_id->sender_id, $sender_id->id];
        })->all();

        $assoc = [];
        foreach ($result as $server) {
            [$key, $fitness] = $server;
            $assoc[$key] = $fitness;
        }

        $this->senderIds = $assoc;

        return $this->senderIds;
    }

    /**
     * mark campaign as queued to processing
     */
    public function running()
    {
        $this->status = self::STATUS_PROCESSING;
        $this->run_at = Carbon::now();
        $this->save();
    }

    /**
     * mark campaign as failed
     *
     * @param  null  $reason
     */
    public function failed($reason = null)
    {
        $this->status = self::STATUS_FAILED;
        $this->reason = $reason;
        $this->save();
    }

    /**
     * set campaign warning
     *
     * @param  null  $reason
     */
    public function warning($reason = null)
    {
        $this->reason = $reason;
        $this->save();
    }

    public function preparedDataToSend()
    {

        try {
            // clean up the tracker to prevent the log file from growing very big
            $this->user->customer->cleanupQuotaTracker();

            // set campaign queued to processing
            $this->running();

            // Reset max_execution_time so that command can run for a long time without being terminated
            Tool::resetMaxExecutionTime();

            $this->singleProcess();

        } catch (Exception $exception) {
            $this->failed($exception->getMessage());
        } catch (Throwable $e) {
            $this->failed($e->getMessage());
        }

    }

    /**
     * @return $this
     */
    public function refreshStatus(): Campaigns
    {
        $campaign     = self::find($this->id);
        $this->status = $campaign->status;
        $this->save();

        return $this;
    }


    /**
     * Mark the campaign as delivered.
     */
    public function delivered()
    {
        $this->status      = self::STATUS_DELIVERED;
        $this->delivery_at = Carbon::now();
        $this->save();
    }

    /**
     * Mark the campaign as delivered.
     */
    public function cancelled()
    {
        $this->status = self::STATUS_CANCELLED;
        $this->save();
    }

    /**
     * Mark the campaign as processing.
     */
    public function processing()
    {
        $this->status      = self::STATUS_PROCESSING;
        $this->running_pid = getmypid();
        $this->run_at      = Carbon::now();
        $this->save();
    }

    /**
     * render sms with tag
     *
     * @param $msg
     * @param $data
     *
     * @return string|string[]
     */
    public function renderSMS($msg, $data)
    {
        preg_match_all('~{(.*?)}~s', $msg, $datas);

        foreach ($datas[1] as $value) {
            if (array_key_exists($value, $data)) {
                $msg = preg_replace("/\b$value\b/u", $data[$value], $msg);
            } else {
                $msg = str_ireplace($value, '', $msg);
            }
        }

        return str_ireplace(["{", "}"], '', $msg);
    }


    /**
     * get coverage
     *
     * @return array
     */
    public function getCoverage(): array
    {
        $data          = [];
        $plan_coverage = PlansCoverageCountries::where('plan_id', $this->user->customer->activeSubscription()->plan->id)->cursor();
        foreach ($plan_coverage as $coverage) {
            $data[$coverage->country->country_code] = json_decode($coverage->options, true);
        }

        return $data;

    }

    /**
     * send campaign
     *
     * @throws NumberParseException
     * @throws Throwable
     */
    public function singleProcess($partition = null)
    {

        $prepareForTemplateTag = [];
        $contactsData          = [];
        $cutting_array         = [];
        $total_list_contacts   = 0;

        $check_list_count = CampaignsList::where('campaign_id', $this->id)->count();
        if ($check_list_count > 0) {

            $list    = CampaignsList::where('campaign_id', $this->id)->select('contact_list_id')->cursor();
            $list_id = $list->pluck('contact_list_id')->all();

            Contacts::whereIn('group_id', $list_id)->where('status', 'subscribe')->chunk(5000, function ($lines) use (&$contactsData) {
                foreach ($lines as $line) {
                    $data = $line->toArray();
                    foreach ($line->custom_fields as $field) {
                        $data[$field->tag] = $field->value;
                    }
                    $contactsData[] = $data;
                }
            });

            $total_list_contacts = count($contactsData);
        }

        if (CampaignsRecipients::where('campaign_id', $this->id)->count() > 0) {
            CampaignsRecipients::where('campaign_id', $this->id)->select('recipient as phone')->chunk(500, function ($lines) use (&$contactsData, &$total_list_contacts) {
                foreach ($lines as $line) {
                    $data           = $line->toArray();
                    $data['id']     = $total_list_contacts++;
                    $contactsData[] = $data;
                }
            });
        }


        $collection = collect($contactsData);

        $contact_count  = $this->contactCount($this->cache);
        $cutting_system = $this->user->customer->getOption('cutting_system');

        $cost       = 0;
        $total_unit = 0;

        if ($cutting_system == 'yes' && $this->user->customer->getOption('cutting_value') != 0) {
            $cutting_value = $this->user->customer->getOption('cutting_value');
            $cutting_unit  = $this->user->customer->getOption('cutting_unit');
            $cutting_logic = $this->user->customer->getOption('cutting_logic');

            if ($cutting_unit == 'percentage') {
                $cutting_value = ($cutting_value / 100) * $contact_count;
            }

            $cutting_amount = (int) round($cutting_value);

            if ($cutting_logic == 'random') {
                $cutting_array = $collection->random($cutting_amount)->all();
            }

            if ($cutting_logic == 'start') {
                $cutting_array = $collection->slice(0, $cutting_amount)->all();
            }

            if ($cutting_logic == 'end') {
                $cutting_array = $collection->slice(-$cutting_amount)->all();
            }
        }

        $insertData = Tool::check_diff_multi($collection->all(), $cutting_array);


        $sending_server = $this->getSendingServers();
        $coverage       = $this->getCoverage();

        collect($cutting_array)->chunk(1000)->each(function ($lines) use (&$prepareForTemplateTag, $cost, &$total_unit, $sending_server, $coverage) {

            $check_sender_id = $this->getSenderIds();

            foreach ($lines as $line) {
                $message  = $this->renderSMS($this->message, $line);
                $sms_type = $this->sms_type;

                $phone = str_replace(['+', '(', ')', '-', ' '], '', $line['phone']);
                if (count($check_sender_id) > 0) {
                    $sender_id = $this->pickSenderIds();
                } else {
                    $sender_id = null;
                }

                if (Tool::validatePhone($phone)) {

                    try {
                        $phoneUtil         = PhoneNumberUtil::getInstance();
                        $phoneNumberObject = $phoneUtil->parse('+'.$phone);
                        $country_code      = $phoneNumberObject->getCountryCode();
                        if (is_array($coverage) && array_key_exists($country_code, $coverage)) {

                            if ($this->sms_type == 'plain' || $this->sms_type == 'unicode') {
                                $cost = $coverage[$country_code]['plain_sms'];
                            }

                            if ($this->sms_type == 'voice') {
                                $cost = $coverage[$country_code]['voice_sms'];
                            }

                            if ($this->sms_type == 'mms') {
                                $cost = $coverage[$country_code]['mms_sms'];
                            }

                            if ($this->sms_type == 'whatsapp') {
                                $cost = $coverage[$country_code]['whatsapp_sms'];
                            }

                            $sms_counter  = new SMSCounter();
                            $message_data = $sms_counter->count($message);
                            $sms_count    = $message_data->messages;

                            $price      = $cost * $sms_count;
                            $total_unit += (int) $price;

                            $preparedData['id']        = $line['id'];
                            $preparedData['user_id']   = $this->user_id;
                            $preparedData['phone']     = $line['phone'];
                            $preparedData['sender_id'] = $sender_id;
                            $preparedData['message']   = $message;
                            $preparedData['sms_type']  = $sms_type;
                            $preparedData['cost']      = (int) $price;
                            $preparedData['status']    = 'Delivered';

                        } else {

                            $sms_counter  = new SMSCounter();
                            $message_data = $sms_counter->count($message);
                            $sms_count    = $message_data->messages;

                            $price      = 1 * $sms_count;
                            $total_unit += (int) $price;

                            $preparedData['id']        = $line['id'];
                            $preparedData['user_id']   = $this->user_id;
                            $preparedData['phone']     = $line['phone'];
                            $preparedData['sender_id'] = $sender_id;
                            $preparedData['message']   = $message;
                            $preparedData['sms_type']  = $sms_type;
                            $preparedData['cost']      = (int) $price;
                            $preparedData['status']    = "Permission to send an SMS has not been enabled for the region indicated by the 'To' number: ".$line['phone'];

                        }
                    } catch (NumberParseException $exception) {
                        $sms_counter  = new SMSCounter();
                        $message_data = $sms_counter->count($message);
                        $sms_count    = $message_data->messages;

                        $price      = 1 * $sms_count;
                        $total_unit += (int) $price;

                        $preparedData['id']        = $line['id'];
                        $preparedData['user_id']   = $this->user_id;
                        $preparedData['phone']     = $line['phone'];
                        $preparedData['sender_id'] = $sender_id;
                        $preparedData['message']   = $message;
                        $preparedData['sms_type']  = $sms_type;
                        $preparedData['cost']      = (int) $price;
                        $preparedData['status']    = $exception->getMessage();

                    }
                } else {
                    $sms_counter  = new SMSCounter();
                    $message_data = $sms_counter->count($message);
                    $sms_count    = $message_data->messages;

                    $price      = 1 * $sms_count;
                    $total_unit += (int) $price;

                    $preparedData['id']        = $line['id'];
                    $preparedData['user_id']   = $this->user_id;
                    $preparedData['phone']     = $line['phone'];
                    $preparedData['sender_id'] = $sender_id;
                    $preparedData['message']   = $message;
                    $preparedData['sms_type']  = $sms_type;
                    $preparedData['cost']      = (int) $price;
                    $preparedData['status']    = __('locale.customer.invalid_phone_number', ['phone' => $phone]);

                }

                if ($this->user->customer->getOption('send_spam_message') == 'no') {
                    $spamWords = SpamWord::all()->filter(function ($spamWord) use ($message) {
                        if (true === str_contains(strtolower($message), strtolower($spamWord->word))) {
                            return true;
                        }

                        return false;
                    });

                    if ($spamWords->count()) {
                        $preparedData['status'] = 'Your message contains spam words.';
                    }
                }


                $preparedData['campaign_id']    = $this->id;
                $preparedData['sending_server'] = $sending_server;
                $prepareForTemplateTag[]        = $preparedData;
            }
        });


        collect($insertData)->chunk(1000)->each(function ($lines) use (&$prepareForTemplateTag, $cost, &$total_unit, $sending_server, $coverage) {

            $check_sender_id = $this->getSenderIds();

            foreach ($lines as $line) {

                $message  = $this->renderSMS($this->message, $line);
                $sms_type = $this->sms_type;

                if (count($check_sender_id) > 0) {
                    $sender_id = $this->pickSenderIds();
                } else {
                    $sender_id = null;
                }

                $phone = str_replace(['+', '(', ')', '-', ' '], '', $line['phone']);

                if (Tool::validatePhone($phone)) {

                    try {
                        $phoneUtil         = PhoneNumberUtil::getInstance();
                        $phoneNumberObject = $phoneUtil->parse('+'.$phone);
                        $country_code      = $phoneNumberObject->getCountryCode();
                        if (is_array($coverage) && array_key_exists($country_code, $coverage)) {

                            if ($this->sms_type == 'plain' || $this->sms_type == 'unicode') {
                                $cost = $coverage[$country_code]['plain_sms'];
                            }

                            if ($this->sms_type == 'voice') {
                                $cost = $coverage[$country_code]['voice_sms'];
                            }

                            if ($this->sms_type == 'mms') {
                                $cost = $coverage[$country_code]['mms_sms'];
                            }

                            if ($this->sms_type == 'whatsapp') {
                                $cost = $coverage[$country_code]['whatsapp_sms'];
                            }

                            $sms_counter  = new SMSCounter();
                            $message_data = $sms_counter->count($message);
                            $sms_count    = $message_data->messages;

                            $price      = $cost * $sms_count;
                            $total_unit += (int) $price;

                            $preparedData['id']        = $line['id'];
                            $preparedData['user_id']   = $this->user_id;
                            $preparedData['phone']     = $line['phone'];
                            $preparedData['sender_id'] = $sender_id;
                            $preparedData['message']   = $message;
                            $preparedData['sms_type']  = $sms_type;
                            $preparedData['cost']      = (int) $price;
                            $preparedData['status']    = null;

                        } else {

                            $sms_counter  = new SMSCounter();
                            $message_data = $sms_counter->count($message);
                            $sms_count    = $message_data->messages;

                            $price      = 1 * $sms_count;
                            $total_unit += (int) $price;

                            $preparedData['id']        = $line['id'];
                            $preparedData['user_id']   = $this->user_id;
                            $preparedData['phone']     = $line['phone'];
                            $preparedData['sender_id'] = $sender_id;
                            $preparedData['message']   = $message;
                            $preparedData['sms_type']  = $sms_type;
                            $preparedData['cost']      = (int) $price;
                            $preparedData['status']    = "Permission to send an SMS has not been enabled for the region indicated by the 'To' number: ".$line['phone'];

                        }
                    } catch (NumberParseException $exception) {
                        $sms_counter  = new SMSCounter();
                        $message_data = $sms_counter->count($message);
                        $sms_count    = $message_data->messages;

                        $price      = 1 * $sms_count;
                        $total_unit += (int) $price;

                        $preparedData['id']        = $line['id'];
                        $preparedData['user_id']   = $this->user_id;
                        $preparedData['phone']     = $line['phone'];
                        $preparedData['sender_id'] = $sender_id;
                        $preparedData['message']   = $message;
                        $preparedData['sms_type']  = $sms_type;
                        $preparedData['cost']      = (int) $price;
                        $preparedData['status']    = $exception->getMessage();

                    }
                } else {
                    $sms_counter  = new SMSCounter();
                    $message_data = $sms_counter->count($message);
                    $sms_count    = $message_data->messages;

                    $price      = 1 * $sms_count;
                    $total_unit += (int) $price;

                    $preparedData['id']        = $line['id'];
                    $preparedData['user_id']   = $this->user_id;
                    $preparedData['phone']     = $line['phone'];
                    $preparedData['sender_id'] = $sender_id;
                    $preparedData['message']   = $message;
                    $preparedData['sms_type']  = $sms_type;
                    $preparedData['cost']      = (int) $price;
                    $preparedData['status']    = __('locale.customer.invalid_phone_number', ['phone' => $phone]);

                }

                if ($this->user->customer->getOption('send_spam_message') == 'no') {
                    $spamWords = SpamWord::all()->filter(function ($spamWord) use ($message) {
                        if (true === str_contains(strtolower($message), strtolower($spamWord->word))) {
                            return true;
                        }

                        return false;
                    });

                    if ($spamWords->count()) {
                        $preparedData['status'] = 'Your message contains spam words.';
                    }
                }

                $preparedData['campaign_id']    = $this->id;
                $preparedData['sending_server'] = $sending_server;
                $prepareForTemplateTag[]        = $preparedData;
            }
        });


        if ($this->user->sms_unit != '-1' && $total_unit > $this->user->sms_unit) {
            $this->failed(sprintf("Campaign `%s` (%s) halted, customer exceeds sms credit", $this->campaign_name, $this->uid));
            sleep(60);
        } else {

            $user = User::find($this->user->id);
            if ($user->sms_unit != '-1') {

                DB::transaction(function () use ($user, $total_unit) {
                    $remaining_balance = $user->sms_unit - $total_unit;
                    $user->lockForUpdate();
                    $user->update(['sms_unit' => $remaining_balance]);
                });
            }

            try {
                $failed_cost = 0;

                $this->processing();

                collect($prepareForTemplateTag)->sortBy('id')->values()->chunk(3000)->each(function ($sendData) use (&$failed_cost) {
                    foreach ($sendData as $data) {
                        $status = null;
                        if ($this->sms_type == 'plain' || $this->sms_type == 'unicode') {
                            $status = $this->sendPlainSMS($data);
                        }

                        if ($this->sms_type == 'voice') {

                            $data['language'] = $this->language;
                            $data['gender']   = $this->gender;

                            $status = $this->sendVoiceSMS($data);
                        }

                        if ($this->sms_type == 'mms') {

                            $data['media_url'] = $this->media_url;
                            $status            = $this->sendMMS($data);
                        }

                        if ($this->sms_type == 'whatsapp') {
                            if (isset($this->media_url)) {
                                $data['media_url'] = $this->media_url;
                            }
                            $status = $this->sendWhatsApp($data);
                        }

                        if (substr_count($status, 'Delivered') == 1) {
                            $this->updateCache('DeliveredCount');
                        } else {
                            $failed_cost += $data['cost'];
                            $this->updateCache('FailedDeliveredCount');
                        }
                    }
                });

                unset($user);
                $user = User::find($this->user->id);

                if ($user->sms_unit != '-1') {

                    DB::transaction(function () use ($user, $failed_cost) {
                        $remaining_balance = $user->sms_unit + $failed_cost;
                        $user->lockForUpdate();
                        $user->update(['sms_unit' => $remaining_balance]);
                    });
                }

                $this->delivered();

            } catch (Exception $exception) {
                $this->failed($exception->getMessage());
            } finally {
                self::resetServerPools();
                $this->updateCache();
                $this->delivered();
            }
        }

    }

    /**
     * reset server pools
     */
    public static function resetServerPools()
    {
        self::$serverPools = [];
    }

    public function pickSendingServer()
    {
        $selection = $this->getSendingServers();

        // do not raise an exception, just wait if sending servers are available but exceeding sending limit
        $blacklisted = [];

        while (true) {
            $id = RouletteWheel::generate($selection);
            if (empty(self::$serverPools[$id])) {
                $server = SendingServer::find($id);
                if ($server->custom) {
                    $server['custom_info'] = $server->customSendingServer;
                }
                $server->cleanupQuotaTracker();
                self::$serverPools[$id] = $server;
            }

            if (self::$serverPools[$id]->overQuota()) {
                // log every 60 seconds
                if ( ! array_key_exists($id, $blacklisted) || time() - $blacklisted[$id] >= 60) {
                    $blacklisted[$id] = time();
                    $this->warning(sprintf('Sending server `%s` exceeds sending limit, skipped', self::$serverPools[$id]->name));
                }

                // if all sending servers are blacklisted
                if (sizeof($blacklisted) == sizeof($selection)) {
                    $this->warning(__('locale.campaigns.sending_server_exceed_sending_limit'));
                    sleep(30);
                }

                continue;
            }

            return self::$serverPools[$id];
        }
    }

    /**
     * pick sender id
     *
     *
     */
    public function pickSenderIds(): int|string
    {
        $selection = array_values(array_flip($this->getSenderIds()));
        shuffle($selection);
        while (true) {
            $element = array_pop($selection);
            if ($element) {
                return (string) $element;
            }
        }
    }

    /**
     * get sms type
     *
     * @return string
     */
    public function getSMSType(): string
    {
        $sms_type = $this->sms_type;

        if ($sms_type == 'plain') {
            return '<span class="badge bg-primary text-uppercase me-1 mb-1">'.__('locale.labels.plain').'</span>';
        }
        if ($sms_type == 'unicode') {
            return '<span class="badge bg-primary text-uppercase me-1 mb-1">'.__('locale.labels.unicode').'</span>';
        }

        if ($sms_type == 'voice') {
            return '<span class="badge bg-success text-uppercase me-1 mb-1">'.__('locale.labels.voice').'</span>';
        }

        if ($sms_type == 'mms') {
            return '<span class="badge bg-info text-uppercase me-1 mb-1">'.__('locale.labels.mms').'</span>';
        }

        if ($sms_type == 'whatsapp') {
            return '<span class="badge bg-warning text-uppercase mb-1">'.__('locale.labels.whatsapp').'</span>';
        }

        return '<span class="badge bg-danger text-uppercase mb-1">'.__('locale.labels.invalid').'</span>';
    }

    /**
     * get sms type
     *
     * @return string
     */
    public function getCampaignType(): string
    {
        $sms_type = $this->schedule_type;

        if ($sms_type == 'onetime') {
            return '<div>
                        <span class="badge badge-light-info text-uppercase me-1 mb-1">'.__('locale.labels.scheduled').'</span>
                        <p class="text-muted">'.Tool::customerDateTime($this->schedule_time).'</p>
                    </div>';
        }
        if ($sms_type == 'recurring') {
            return '<div>
                        <span class="badge badge-light-success text-uppercase me-1 mb-1">'.__('locale.labels.recurring').'</span>
                        <p class="text-muted">'.__('locale.labels.every').' '.$this->displayFrequencyTime().'</p>
                        <p class="text-muted">'.__('locale.labels.next_schedule_time').': '.Tool::formatDateTime($this->schedule_time).'</p>
                        <p class="text-muted">'.__('locale.labels.end_time').': '.Tool::formatDateTime($this->recurring_end).'</p>
                    </div>';
        }

        return '<span class="badge badge-light-primary text-uppercase me-1 mb-1">'.__('locale.labels.normal').'</span>';
    }

    /**
     * Display frequency time
     *
     * @return string
     */
    public function displayFrequencyTime(): string
    {
        return $this->frequency_amount.' '.Tool::getPluralParse($this->frequency_unit, $this->frequency_amount);
    }


    /**
     * get campaign status
     *
     * @return string
     */
    public function getStatus(): string
    {
        $status = $this->status;

        if ($status == self::STATUS_FAILED || $status == self::STATUS_CANCELLED) {
            return '<div>
                        <span class="badge bg-danger text-uppercase me-1 mb-1">'.__('locale.labels.'.$status).'</span>
                        <p class="text-muted" data-toggle="tooltip" data-placement="top" title="'.$this->reason.'">'.str_limit($this->reason, 40).'</p>
                    </div>';
        }
        if ($status == self::STATUS_SENDING || $status == self::STATUS_PROCESSING) {
            return '<div>
                        <span class="badge bg-primary text-uppercase mr-1 mb-1">'.__('locale.labels.'.$status).'</span>
                        <p class="text-muted">'.__('locale.labels.run_at').': '.Tool::customerDateTime($this->run_at).'</p>
                    </div>';
        }

        if ($status == self::STATUS_SCHEDULED) {
            return '<span class="badge bg-info text-uppercase mr-1 mb-1">'.__('locale.labels.scheduled').'</span>';
        }
        if ($status == self::STATUS_NEW || $status == self::STATUS_QUEUED) {
            return '<span class="badge bg-primary text-uppercase mr-1 mb-1">'.__('locale.labels.'.$status).'</span>';
        }


        return '<div>
                        <span class="badge bg-success text-uppercase mr-1 mb-1">'.__('locale.labels.delivered').'</span>
                        <p class="text-muted">'.__('locale.labels.delivered_at').': '.Tool::customerDateTime($this->delivery_at).'</p>
                    </div>';
    }


    /**
     * get route key by uid
     *
     * @return string
     */
    public function getRouteKeyName(): string
    {
        return 'uid';
    }


    /**
     * make ready to send
     *
     * @return void
     */
    public function queued()
    {
        $this->status = self::STATUS_QUEUED;
        $this->save();
    }


    /**
     * Check if the campaign is ready to start.
     */
    public function isQueued()
    {
        return $this->status == self::STATUS_QUEUED;
    }

    /**
     * get another running process
     *
     * @return bool
     */
    public function occupiedByOtherAnotherProcess()
    {
        if ( ! function_exists('posix_getpid')) {
            return false;
        }

        return ( ! is_null($this->running_pid) && posix_getpgid($this->running_pid));
    }


    /**
     * start campaign
     *
     * @return void
     */
    public function run()
    {

        try {
            if ( ! $this->refreshStatus()->isQueued()) {
                MailLog::info("Campaign ID: {$this->id} is not ready or already started");

                return;
            }

            if ($this->refreshStatus()->occupiedByOtherAnotherProcess()) {
                MailLog::info("Campaign ID: {$this->id} is occupied by another process PID: {$this->running_pid}");

                return;
            }

            $this->processing();
            MailLog::info('Starting campaign `'.$this->name.'`');

            Tool::resetMaxExecutionTime();

            // Only run multi-process if pcntl is enabled
            $processes = (int) $this->user->customer->getOption('max_process');
            if (extension_loaded('pcntl') && function_exists('pcntl_fork') && $processes > 1) {
                MailLog::info('Run in multi-process mode');
                $this->runMultiProcesses();
            } else {
                MailLog::info('Run in single-process mode');
                $this->singleProcess();
            }
            MailLog::info('Finish campaign `'.$this->name.'`');

        } catch (Exception $ex) {
            $this->failed($ex->getMessage());
            MailLog::error('Starting campaign failed. '.$ex->getMessage());
        } catch (Throwable $e) {
            $this->failed($e->getMessage());
            MailLog::error('Starting campaign failed. '.$e->getMessage());
        }
    }


    /**
     * Start the campaign, using PHP fork() to launch multiple processes.
     *
     * @return void
     */
    public function runMultiProcesses()
    {
        // processes to fork
        $count = (int) $this->user->customer->getOption('max_process');
        $count = ($count > 2) ? 2 : $count;

        MailLog::info("Forking {$count} process(es)");
        $parentPid = $this->running_pid;
        $children  = [];
        for ($i = 0; $i < $count; $i += 1) {
            $pid = pcntl_fork();

            if ( ! $pid) {

                DB::reconnect('mysql');

                MailLog::info(sprintf('Start child process %s of %s (forked from %s)', $i + 1, $count, $parentPid));
                sleep(self::WORKER_DELAY);
                $partition = [$i, $count];
                try {
                    $this->singleProcess($partition);
                } catch (NumberParseException|Throwable $e) {
                    $this->failed($e->getMessage());
                }

                exit($i + 1);
                // end child process
            } else {
                $children[] = $pid;
            }
        }

        // wait for child processes to finish
        foreach ($children as $ignored) {
            $pid = pcntl_wait($status);
            if (pcntl_wifexited($status)) {
                $code = pcntl_wexitstatus($status);
                MailLog::info("Child process $pid finished, status code: $code");
            } else {
                MailLog::warning("Child process $pid did not normally exit");
                $this->failed("Child process $pid did not normally exit");
            }
        }

        // after all child processes are done
        $this->refreshStatus();

        if ($this->status == self::STATUS_DELIVERED) {
            $this->delivered();
        }
    }


}

© 2025 UnknownSec
afwwrfwafr45458465
Password