diff --git a/.github/workflows/phpstan.yml b/.github/workflows/phpstan.yml
new file mode 100644
index 0000000..e69de29
diff --git a/phpstan.neon b/phpstan.neon
new file mode 100644
index 0000000..4b39e40
--- /dev/null
+++ b/phpstan.neon
@@ -0,0 +1,7 @@
+parameters:
+ level: 0
+ paths:
+ - src
+ treatPhpDocTypesAsCertain: false
+ scanDirectories:
+ - ../../
\ No newline at end of file
diff --git a/src/Api/Admin.php b/src/Api/Admin.php
new file mode 100644
index 0000000..27ef88f
--- /dev/null
+++ b/src/Api/Admin.php
@@ -0,0 +1,1622 @@
+di['db']->find('service_proxmox_server');
+ $servers_grouped = array();
+
+ // Iterate through each server
+ foreach ($servers as $server) {
+ // Find all virtual machines (VMs) on this server and calculate CPU cores and RAM usage
+ $vms = $this->di['db']->find('service_proxmox', 'server_id=:id', array(':id' => $server->id));
+ $server_cpu_cores = 0;
+ $server_ram = 0;
+
+ // Count the number of VMs
+ $vm_count = 0;
+ foreach ($vms as $vm) {
+ // Sum up the CPU cores and RAM of each VM
+ $server_cpu_cores += $vm->cpu_cores;
+ $server_ram += $vm->ram;
+ $vm_count++;
+ }
+
+ // Calculate the percentage of RAM usage if the server's RAM is not zero
+ if ($server->ram != 0) {
+ $server_ram_percent = round($server_ram / $server->ram * 100, 0, PHP_ROUND_HALF_DOWN);
+ } else {
+ $server_ram_percent = 0;
+ }
+
+ // Retrieve the overprovisioning factor from the extension's configuration and calculate overprovisioned CPU cores
+ $config = $this->di['mod_config']('Serviceproxmox');
+ $overprovion_percent = $config['cpu_overprovisioning'];
+ $cpu_cores_overprovision = $server->cpu_cores + round($server->cpu_cores * $overprovion_percent / 100, 0, PHP_ROUND_HALF_DOWN);
+
+ // Retrieve the RAM overprovisioning factor from the extension's configuration and calculate overprovisioned RAM
+ $ram_overprovion_percent = $config['ram_overprovisioning'];
+ $ram_overprovision = round($server->ram / 1024 / 1024 / 1024, 0, PHP_ROUND_HALF_DOWN) + round(round($server->ram / 1024 / 1024 / 1024, 0, PHP_ROUND_HALF_DOWN) * $ram_overprovion_percent / 100, 0, PHP_ROUND_HALF_DOWN);
+
+ // Store the server's group information in the grouped servers array
+ $servers_grouped[$server['group']]['group'] = $server->group;
+
+ // Prepare the grouped Server's array for the Template to render.
+ $servers_grouped[$server['group']]['servers'][$server['id']] = array(
+ 'id' => $server->id,
+ 'name' => $server->name,
+ 'group' => $server->group,
+ 'ipv4' => $server->ipv4,
+ 'hostname' => $server->hostname,
+ 'port' => $server->port,
+ 'vm_count' => $vm_count,
+ 'access' => $this->getService()->find_access($server),
+ 'cpu_cores' => $server->cpu_cores,
+ 'cpu_cores_allocated' => $server->cpu_cores_allocated,
+ 'cpu_cores_overprovision' => $cpu_cores_overprovision,
+ 'cpu_cores_provisioned' => $server_cpu_cores,
+ 'ram_provisioned' => $server_ram,
+ 'ram_overprovision' => $ram_overprovision,
+ 'ram_used' => round($server->ram_allocated / 1024 / 1024 / 1024, 0, PHP_ROUND_HALF_DOWN), // TODO: Make nicer?
+ 'ram' => round($server->ram / 1024 / 1024 / 1024, 0, PHP_ROUND_HALF_DOWN), // TODO: Make nicer?
+ 'ram_percent' => $server_ram_percent,
+ 'active' => $server->active,
+ );
+ }
+ return $servers_grouped;
+ }
+
+ /**
+ * Get list of storage
+ *
+ * @return array
+ */
+ public function storage_get_list($data)
+ {
+ $storages = $this->di['db']->find('service_proxmox_storage');
+ $storages_grouped = array();
+ foreach ($storages as $storage) {
+ $server = $this->di['db']->getExistingModelById('service_proxmox_server', $storage->server_id, 'Server not found');
+ switch ($storage->type) {
+ case 'local':
+ $storage->type = 'Local';
+ break;
+ case 'nfs':
+ $storage->type = 'NFS';
+ break;
+ case 'dir':
+ $storage->type = 'Directory';
+ break;
+ case 'iscsi':
+ $storage->type = 'iSCSI';
+ break;
+ case 'lvm':
+ $storage->type = 'LVM';
+ break;
+ case 'lvmthin':
+ $storage->type = 'LVM thinpool';
+ break;
+ case 'rbd':
+ $storage->type = 'Ceph';
+ break;
+ case 'sheepdog':
+ $storage->type = 'Sheepdog';
+ break;
+ case 'glusterfs':
+ $storage->type = 'GlusterFS';
+ break;
+ case 'cephfs':
+ $storage->type = 'CephFS';
+ break;
+ case 'zfs':
+ $storage->type = 'ZFS';
+ break;
+ case 'zfspool':
+ $storage->type = 'ZFS Pool';
+ break;
+ case 'iscsidirect':
+ $storage->type = 'iSCSI Direct';
+ break;
+ case 'drbd':
+ $storage->type = 'DRBD';
+ break;
+ case 'dev':
+ $storage->type = 'Device';
+ break;
+ case 'pbs':
+ $storage->type = 'Proxmox Backup Server';
+ break;
+ }
+ $storages_grouped[$storage['type']]['group'] = $storage->type;
+ // Map storage group types to better descriptions for display
+ // TODO: Add translations
+
+
+ $storages_grouped[$storage['type']]['storages'][$storage['id']] = array(
+ 'id' => $storage->id,
+ 'servername' => $server->name,
+ 'storageclass' => $storage->storageclass,
+ 'name' => $storage->storage,
+ 'content' => $storage->content,
+ 'type' => $storage->type,
+ 'active' => $storage->active,
+ 'size' => $storage->size,
+ 'used' => $storage->used,
+ 'free' => $storage->free,
+ 'percent_used' => ($storage->size == 0 || $storage->used == 0 || $storage->free == 0) ? 0 : round($storage->used / $storage->size * 100, 2),
+ );
+
+ }
+ return $storages_grouped;
+ }
+
+ /**
+ * Get list of storageclasses
+ *
+ * @return array
+ */
+
+ public function storageclass_get_list($data)
+ {
+ $storageclasses = $this->di['db']->find('service_proxmox_storageclass');
+ return $storageclasses;
+ }
+
+ /**
+ * Get list of storage controllers
+ *
+ * @return array
+ */
+ public function storage_controller_get_list($data)
+ {
+ // Return Array of storage controllers:
+ // lsi | lsi53c810 | virtio-scsi-pci | virtio-scsi-single | megasas | pvscsi
+ $storage_controllers = array(
+ 'lsi' => 'LSI',
+ 'lsi53c810' => 'LSI 53C810',
+ 'virtio-scsi-pci' => 'VirtIO SCSI PCI',
+ 'virtio-scsi-single' => 'VirtIO SCSI Single',
+ 'megasas' => 'MegaSAS',
+ 'pvscsi' => 'PVSCSI',
+ 'sata' => 'SATA',
+ 'ide' => 'IDE',
+ );
+ return $storage_controllers;
+ }
+
+ /** *
+ * Get list of Active Services
+ *
+ * @return array
+ */
+ public function service_proxmox_get_list($data)
+ {
+ $services = $this->di['db']->find('service_proxmox');
+ return $services;
+ }
+
+ /**
+ * Create a new storageclass
+ *
+ * @return array
+ */
+ public function storageclass_create($data)
+ {
+ $storageclass = $this->di['db']->dispense('service_proxmox_storageclass');
+ $storageclass->storageclass = $data['storageClassName'];
+ $this->di['db']->store($storageclass);
+ return $storageclass;
+ }
+
+ /**
+ * Retrieve a single storageclass
+ *
+ * @return array
+ */
+ public function storageclass_get($data)
+ {
+ $storageclass = $this->di['db']->getExistingModelById('service_proxmox_storageclass', $data['id'], 'Storageclass not found');
+ return $storageclass;
+ }
+
+ /**
+ * Get list of server groups
+ *
+ * @return array
+ */
+ public function server_groups()
+ {
+ $sql = "SELECT DISTINCT `group` FROM `service_proxmox_server` WHERE `active` = 1";
+ $groups = $this->di['db']->getAll($sql);
+ return $groups;
+ }
+
+ /**
+ * get list of servers in a group
+ *
+ */
+ public function servers_in_group($data)
+ {
+ $sql = "SELECT * FROM `service_proxmox_server` WHERE `group` = '" . $data['group'] . "' AND `active` = 1";
+
+ $servers = $this->di['db']->getAll($sql);
+
+ // remove password & api keys from results
+ foreach ($servers as $key => $server) {
+ $servers[$key]['root_password'] = '';
+ $servers[$key]['tokenvalue'] = '';
+ }
+
+ return $servers;
+ }
+
+ /**
+ * Get list of qemu templates for a server
+ *
+ */
+ public function qemu_templates_on_server($data)
+ {
+ $sql = "SELECT * FROM `service_proxmox_qemu_template` WHERE `server_id` = '" . $data['server_id'] . "'";
+ $templates = $this->di['db']->getAll($sql);
+ return $templates;
+ }
+
+
+ /**
+ * Get list of OS types
+ *
+ * @return array
+ */
+
+ public function os_get_list()
+ {
+ $os_types = array(
+ 'other' => 'Other',
+ 'wxp' => 'Windows XP',
+ 'w2k' => 'Windows 2000',
+ 'w2k3' => 'Windows 2003',
+ 'w2k8' => 'Windows 2008',
+ 'wvista' => 'Windows Vista',
+ 'win7' => 'Windows 7',
+ 'win8' => 'Windows 8',
+ 'win10' => 'Windows 10',
+ 'win11' => 'Windows 11',
+ 'l24' => 'Linux 2.4 Kernel',
+ 'l26' => 'Linux 2.6 Kernel',
+ 'solaris' => 'Solaris',
+ );
+ return $os_types;
+ }
+
+ /**
+ * Get list of BIOS types
+ *
+ * @return array
+ */
+ public function bios_get_list()
+ {
+ $bios_types = array(
+ 'seabios' => 'SeaBIOS',
+ 'ovmf' => 'OVMF (UEFI)',
+ );
+ return $bios_types;
+ }
+
+ /**
+ * Get list of VNC types
+ *
+ * @return array
+ */
+ public function lxc_appliance_get_list()
+ {
+ // get all service_proxmox_lxc_appliances
+ $lxc_appliance = $this->di['db']->find('service_proxmox_lxc_appliance');
+ // sort alphabetically by headline
+ usort($lxc_appliance, function ($a, $b) {
+ return strcmp($a->headline, $b->headline);
+ });
+ return $lxc_appliance;
+ }
+
+ // Function to get list of lxc config templates
+ public function get_lxc_config_template()
+ {
+ $lxc_tmpl = $this->di['db']->find('service_proxmox_lxc_config_template');
+ return $lxc_tmpl;
+ }
+
+ // Function to enable qemu template
+ public function vm_config_template_enable($data)
+ {
+ error_log("vm_config_template_enable: " . $data['id']);
+ $vm_config_template = $this->di['db']->getExistingModelById('service_proxmox_vm_config_template', $data['id'], 'VM Config Template not found');
+ $vm_config_template->state = 'active';
+ $this->di['db']->store($vm_config_template);
+ return $vm_config_template;
+ }
+
+ // Function to disable qemu template
+ public function vm_config_template_disable($data)
+ {
+ error_log("vm_config_template_disable: " . $data['id']);
+ $vm_config_template = $this->di['db']->getExistingModelById('service_proxmox_vm_config_template', $data['id'], 'VM Config Template not found');
+ $vm_config_template->state = 'inactive';
+ $this->di['db']->store($vm_config_template);
+ return $vm_config_template;
+ }
+ /* ################################################################################################### */
+ /* ########################################## Servers ############################################## */
+ /* ################################################################################################### */
+
+ /**
+ * Create new hosting server
+ *
+ * @param string $name - server name
+ * @param string $ipv4 - server ipv4
+ * @param string $hostname - server hostname
+ * @param string $port - server port
+ * @param string $root_user - server root user
+ * @param string $root_password - server root password
+ * @param string $realm - server realm
+ *
+ * @return bool - server id
+ *
+ * @throws \Box_Exception
+ */
+ public function server_create($data)
+ {
+ // enable api token & secret
+ $required = array(
+ 'name' => 'Server name is missing',
+ 'ipv4' => 'Server ipv4 is missing',
+ 'hostname' => 'Server hostname is missing',
+ 'port' => 'Server port is missing',
+ 'auth_type' => 'Authentication type is missing',
+ 'realm' => 'Proxmox user realm is missing',
+ );
+ $this->di['validator']->checkRequiredParamsForArray($required, $data);
+
+ // check if server already exists based on name, ipv4 or hostname
+ $server = $this->di['db']->findOne('service_proxmox_server', 'name=:name OR ipv4=:ipv4 OR hostname=:hostname', array(':name' => $data['name'], ':ipv4' => $data['ipv4'], ':hostname' => $data['hostname']));
+ if ($server) {
+ throw new \Box_Exception('Server already exists');
+ }
+
+ $server = $this->di['db']->dispense('service_proxmox_server');
+ $server->name = $data['name'];
+ $server->group = $data['group'];
+ $server->ipv4 = $data['ipv4'];
+ $server->ipv6 = $data['ipv6'];
+ $server->hostname = $data['hostname'];
+ $server->port = $data['port'];
+ $server->realm = $data['realm'];
+ $server->active = $data['active'];
+ $server->created_at = date('Y-m-d H:i:s');
+ $server->updated_at = date('Y-m-d H:i:s');
+
+ $this->di['db']->store($server);
+ $this->di['logger']->info('Created Proxmox server %s', $server->id);
+
+ // check if auth_type is username or token
+ if ($data['auth_type'] == 'username') {
+ $server->root_user = $data['root_user'];
+ $server->root_password = $data['root_password'];
+ $server->tokenname = '';
+ $server->tokenvalue = '';
+ $this->di['db']->store($server);
+ $this->getService()->test_connection($server);
+ } else {
+ $server->root_user = '';
+ $server->root_password = '';
+ $server->tokenname = $data['tokenname'];
+ $server->tokenvalue = $data['tokenvalue'];
+ $this->di['db']->store($server);
+
+ }
+
+
+ // Validate server by testing connection
+
+
+
+ return true;
+ }
+
+ /**
+ * Get server details
+ *
+ * @param int $id - server id
+ * @return array
+ *
+ * @throws \Box_Exception
+ */
+ public function server_get($data)
+ {
+ // Retrieve associated server
+ $server = $this->di['db']->findOne('service_proxmox_server', 'id=:id', array(':id' => $data['server_id']));
+ if (!$server) {
+ throw new \Box_Exception('Server not found');
+ }
+
+ $output = array(
+ 'id' => $server->id,
+ 'name' => $server->name,
+ 'group' => $server->group,
+ 'ipv4' => $server->ipv4,
+ 'ipv6' => $server->ipv6,
+ 'hostname' => $server->hostname,
+ 'port' => $server->port,
+ 'realm' => $server->realm,
+ 'tokenname' => $server->tokenname,
+ 'tokenvalue' => str_repeat("*", 26),
+ 'root_user' => $server->root_user,
+ 'root_password' => $server->root_password,
+ 'admin_password' => $server->admin_password,
+ 'active' => $server->active,
+ );
+ return $output;
+ }
+
+ /**
+ * Update server configuration
+ *
+ * @param int $id - server id
+ * @param string $name - server name
+ * @param string $ipv4 - server ipv4
+ * @param string $hostname - server hostname
+ * @param string $port - server port
+ * @param string $root_user - server root user
+ * @param string $realm - server realm
+ *
+ * @return bool
+ * @throws \Box_Exception
+ */
+ public function server_update($data)
+ {
+ $required = array(
+ 'name' => 'Server name is missing',
+ 'root_user' => 'Root user is missing',
+ 'ipv4' => 'Server ipv4 is missing',
+ 'hostname' => 'Server hostname is missing',
+ 'port' => 'Server port is missing',
+ 'realm' => 'Proxmox user realm is missing',
+ );
+ $this->di['validator']->checkRequiredParamsForArray($required, $data);
+
+ $server = $this->di['db']->findOne('service_proxmox_server', 'id=:id', array(':id' => $data['server_id']));
+
+ $server->name = $data['name'];
+ $server->group = $data['group'];
+ $server->ipv4 = $data['ipv4'];
+ $server->ipv6 = $data['ipv6'];
+ $server->hostname = $data['hostname'];
+ $server->port = $data['port'];
+ $server->realm = $data['realm'];
+ $server->cpu_cores = $data['cpu_cores'];
+ $server->ram = $data['ram'];
+ $server->root_user = $data['root_user'];
+ $server->root_password = $data['root_password'];
+ $server->tokenname = $data['tokenname'];
+ $server->config = $data['config'];
+ $server->active = $data['active'];
+ $server->created_at = date('Y-m-d H:i:s');
+ $server->updated_at = date('Y-m-d H:i:s');
+
+ $this->di['db']->store($server);
+
+ $this->di['logger']->info('Update Proxmox server %s', $server->id);
+
+ return true;
+ }
+
+ /**
+ * Delete server
+ *
+ * @param int $id - server id
+ * @return bool
+ * @throws \Box_Exception
+ */
+ public function server_delete($data)
+ {
+ $required = array(
+ 'id' => 'Server id is missing',
+ );
+ $this->di['validator']->checkRequiredParamsForArray($required, $data);
+
+ // check if there are services provisioned on this server
+ $vms = $this->di['db']->find('service_proxmox', 'server_id=:server_id', array(':server_id' => $data['id']));
+
+ // if there are vms provisioned on this server, throw an exception
+ if (!empty($vms)) {
+ throw new \Box_Exception('VMs are still provisioned on this server');
+ } else {
+ // delete storages
+ $storages = $this->di['db']->find('service_proxmox_storage', 'server_id=:server_id', array(':server_id' => $data['id']));
+ foreach ($storages as $storage) {
+ $this->di['db']->trash($storage);
+ }
+
+ // delete server
+ $server = $this->di['db']->getExistingModelById('service_proxmox_server', $data['id'], 'Server not found');
+ $this->di['db']->trash($server);
+ }
+ }
+
+ /**
+ * Get server details from order id
+ * This is used to manage the service from the order.
+ * TODO: Remove this function and use server_get instead
+ * @param int $order_id
+ * @return array
+ */
+
+ public function server_get_from_order($data)
+ {
+ $required = array(
+ 'order_id' => 'Order id is missing',
+ );
+
+ $this->di['validator']->checkRequiredParamsForArray($required, $data);
+
+ $service = $this->di['db']->findOne(
+ 'service_proxmox',
+ "order_id=:id",
+ array(':id' => $data['order_id'])
+ );
+
+ if (!$service) {
+ return null;
+ }
+
+ $data = array('server_id' => $service['server_id']);
+ $output = $this->server_get($data);
+ return $output;
+ }
+
+
+ /**
+ * Receive Hardware Data from proxmox server
+ *
+ * @param int $server_id
+ * @return array
+ */
+ public function get_hardware_data($server_id)
+ {
+ $server = $this->di['db']->getExistingModelById('service_proxmox_server', $server_id, 'Server not found');
+ $service = $this->getService();
+ $hardware_data = $service->getHardwareData($server);
+ $server->cpu_cores = $hardware_data['cpuinfo']['cores'];
+ $server->ram = $hardware_data['memory']['total'];
+
+ $serverstorage = $service->getStorageData($server);
+
+ foreach ($serverstorage as $key => $value) {
+ $sql = "SELECT * FROM `service_proxmox_storage` WHERE server_id = " . $server_id . " AND storage = '" . $value['storage'] . "'";
+ $storage = $this->di['db']->getAll($sql);
+
+ // if the storage exists, update it, otherwise create it
+ if (!empty($storage)) {
+ $storage = $this->di['db']->findOne('service_proxmox_storage', 'server_id=:server_id AND storage=:storage', array(':server_id' => $server_id, ':storage' => $value['storage']));
+ } else {
+ $storage = $this->di['db']->dispense('service_proxmox_storage');
+ }
+
+ $storage->server_id = $server_id;
+ $storage->storage = $value['storage'];
+ $storage->type = $value['type'];
+ $storage->content = $value['content'];
+
+ $storage->used = $value['used'] / 1000 / 1000 / 1000;
+ $storage->size = $value['total'] / 1000 / 1000 / 1000;
+ $storage->free = $value['avail'] / 1000 / 1000 / 1000;
+
+ $storage->active = $value['active'];
+ $this->di['db']->store($storage);
+ }
+ $this->di['db']->store($server);
+ $allresources = $service->getAssignedResources($server);
+ // summarzie the fields cpus and maxmem for each vm and store it in the server table
+ $server->cpu_cores_allocated = 0;
+ $server->ram_allocated = 0;
+ foreach ($allresources as $key => $value) {
+ $server->cpu_cores_allocated += $value['cpus'];
+ $server->ram_allocated += $value['maxmem'];
+ }
+ $this->di['db']->store($server);
+ $qemu_templates = $service->getQemuTemplates($server);
+ error_log('qemu_templates: ' . print_r($qemu_templates, true));
+ foreach ($qemu_templates as $key => $value) {
+ // check if $value['template'] exists, and if it's content is 1
+ if (!empty($value['template'])) {
+ if ($value['template'] == 1) {
+ $sql = "SELECT * FROM `service_proxmox_qemu_template` WHERE server_id = " . $server_id . " AND vmid = " . $value['vmid'];
+ $template = $this->di['db']->getAll($sql);
+
+ // if the template exists, update it, otherwise create it
+ if (!empty($template)) {
+ $template = $this->di['db']->findOne('service_proxmox_qemu_template', 'server_id=:server_id AND vmid=:vmid', array(':server_id' => $server_id, ':vmid' => $value['vmid']));
+ } else {
+ $template = $this->di['db']->dispense('service_proxmox_qemu_template');
+ }
+ $template->vmid = $value['vmid'];
+ $template->server_id = $server_id;
+ $template->name = $value['name'];
+ $template->created_at = date('Y-m-d H:i:s');
+ $template->updated_at = date('Y-m-d H:i:s');
+
+ $stored = $this->di['db']->store($template);
+ error_log('template saved: ' . print_r($stored, true));
+ }
+ }
+ }
+
+ return $hardware_data;
+ }
+
+
+ /**
+ * Test connection to server
+ *
+ * @param int $id - server id
+ *
+ * @return bool
+ * @throws \Box_Exception
+ */
+ public function server_test_connection($data)
+ {
+
+ $required = array(
+ 'id' => 'Server id is missing',
+ );
+ $this->di['validator']->checkRequiredParamsForArray($required, $data);
+
+ $server = $this->di['db']->getExistingModelById('service_proxmox_server', $data['id'], 'Server not found');
+
+ if ($this->getService()->test_connection($server)) {
+ $this->get_hardware_data($data['id']);
+ return true;
+ } else {
+ return false;
+ }
+ }
+
+ /**
+ * Prepare Server for PVE
+ *
+ * @param int $id - server id
+ *
+ * @return bool
+ * @throws \Box_Exception
+ */
+ public function server_prepare_pve_setup($data)
+ {
+ $required = array(
+ 'id' => 'Server id is missing',
+ );
+ $this->di['validator']->checkRequiredParamsForArray($required, $data);
+
+ $server = $this->di['db']->getExistingModelById('service_proxmox_server', $data['id'], 'Server not found');
+ $updatedserver = $this->getService()->prepare_pve_setup($server);
+ if ($updatedserver) {
+ $this->di['db']->store($updatedserver);
+ return true;
+ } else {
+ return false;
+ }
+ }
+
+ /**
+ * Test access to server
+ * This method can be removed at a later date as it is primarily a debugging tool.
+ * @param int $id - server id
+ *
+ * @return bool
+ * @throws \Box_Exception
+ */
+ public function test_access($data)
+ {
+ $required = array(
+ 'id' => 'Server id is missing',
+ );
+ $this->di['validator']->checkRequiredParamsForArray($required, $data);
+
+ $server = $this->di['db']->getExistingModelById('service_proxmox_server', $data['id'], 'Server not found');
+
+ if ($this->getService()->test_access($server)) {
+ return true;
+ } else {
+ return false;
+ }
+ }
+
+
+ /**
+ * Get all available templates from any proxmox server
+ *
+ * @return array
+ * @throws \Box_Exception
+ */
+ public function pull_lxc_appliances()
+ {
+ $appliances = $this->getService()->getAvailableAppliances();
+
+
+ foreach ($appliances as $appliance) {
+ // check if the appliance already exists
+ //$appliance = $this->di['db']->findOne('service_proxmox_lxc_appliance', 'sha512sum=:sha512sum', array(':sha512sum' => $appliance['sha512sum']));
+ // if the appliance exists, update it, otherwise create it
+
+ $template = $this->di['db']->dispense('service_proxmox_lxc_appliance');
+ $template->headline = $appliance['headline'];
+ $template->package = $appliance['package'];
+ $template->section = $appliance['section'];
+ $template->type = $appliance['type'];
+ $template->source = $appliance['source'];
+ $template->headline = $appliance['headline'];
+ $template->location = $appliance['location'];
+ // if description is empty, use the headline
+ if (empty($appliance['description'])) {
+ $appliance['description'] = $appliance['headline'];
+ }
+ $template->description = $appliance['description'];
+ $template->template = $appliance['template'];
+ $template->os = $appliance['os'];
+ $template->infopage = $appliance['infopage'];
+ $template->version = $appliance['version'];
+ $template->sha512sum = $appliance['sha512sum'];
+ $template->architecture = $appliance['architecture'];
+ $this->di['db']->store($template);
+ }
+ return true;
+ }
+
+ /* ################################################################################################### */
+ /* ########################################## Storage ############################################## */
+ /* ################################################################################################### */
+
+
+ /**
+ * Get a storage
+ *
+ * @param int $id - storage id
+ *
+ * @return array
+ * @throws \Box_Exception
+ */
+ public function storage_get($data)
+ {
+ // Retrieve associated storage
+ $storage = $this->di['db']->findOne('service_proxmox_storage', 'id=:id', array(':id' => $data['storage_id']));
+ $server = $this->di['db']->getExistingModelById('service_proxmox_server', $storage->server_id, 'Server not found');
+ $output = array(
+ 'id' => $storage->id,
+ 'name' => $storage->name,
+ 'server_id' => $storage->server_id,
+ 'storageclass' => $storage->storageclass,
+ 'storage' => $storage->storage,
+ 'content' => $storage->content,
+ 'type' => $storage->type,
+ 'active' => $storage->active,
+ 'size' => $storage->size,
+ 'used' => $storage->used,
+ 'free' => $storage->free,
+ 'percent_used' => ($storage->size == 0 || $storage->used == 0 || $storage->free == 0) ? 0 : round($storage->used / $storage->size * 100, 2),
+ // add list of storage classes
+ 'storageclasses' => $this->storageclass_get_list($data),
+ 'server_name' => $server->name,
+ );
+
+ return $output;
+ }
+
+
+ /**
+ * Update a storage with storageclasses
+ * TODO: Implement & Fix functionality
+ * @param int $id - server id
+ *
+ * @return array
+ * @throws \Box_Exception
+ */
+ public function storage_update($data)
+ {
+ $required = array(
+ 'storageid' => 'Storage id is missing',
+ 'storageTypeTags' => 'Storage tags are missing',
+ );
+ $this->di['validator']->checkRequiredParamsForArray($required, $data);
+
+ // Retrieve associated storage
+ $storage = $this->di['db']->findOne('service_proxmox_storage', 'id=:id', array(':id' => $data['storageid']));
+
+ // $data['storageTypeTags']; contains an array of tags like this: Array([0] => ssd,hdd,sdf)
+ // This needs to be split up and stored as valid json in the storage table
+ error_log('storageTypeTags: ' . print_r($data['storageTypeTags'], true));
+ // Assuming you have $data['storagetype'] populated
+ $storageType = $data['storagetype'];
+ $tagArray = [];
+ foreach ($data['storageTypeTags'] as $tag) {
+ $splitTags = explode(',', $tag);
+ $tagArray = array_merge($tagArray, $splitTags); // Flat array of tags
+ }
+
+ // for every tag in the tagArray, save the id to the error_log
+ $jsonArray = [];
+ foreach ($tagArray as $tagId) {
+ // Fetch the tag details from DB
+ $tag_from_db = $this->di['db']->findOne('service_proxmox_tag', 'id=:id AND type=:type', [
+ ':id' => $tagId,
+ ':type' => $storageType
+ ]);
+
+ if ($tag_from_db) {
+ $jsonArray[] = [
+ 'id' => $tag_from_db->id,
+ 'name' => $tag_from_db->name
+ ];
+ } else {
+ error_log("No DB entry found for tagId: $tagId and type: $storageType");
+ }
+ }
+
+ $jsonString = json_encode($jsonArray);
+ $storage->storageclass = $jsonString;
+ $this->di['db']->store($storage);
+ return true;
+ }
+
+
+ /**
+ * Update Product
+ *
+ * @param array $data
+ *
+ * @return bool
+ * @throws \Box_Exception
+ */
+ public function product_update($data)
+ {
+ $required = array(
+ 'id' => 'Product id is missing',
+ 'group' => 'Server group is missing',
+ 'filling' => 'Filling method is missing',
+ 'show_stock' => 'Stock display (Show Stock) is missing',
+ 'server' => 'Server is missing',
+ 'virt' => 'Virtualization type is missing',
+
+ );
+
+ $this->di['validator']->checkRequiredParamsForArray($required, $data);
+
+ // Check if virt is lxc or qemu, and if lxc, check if lxc-templ is set. if qemu, check if vm-templ is set.
+ if ($data['virt'] == 'lxc' && empty($data['lxc-templ'])) {
+ throw new \Box_Exception('LXC Template is missing');
+ } elseif ($data['virt'] == 'qemu' && empty($data['vm-templ'])) {
+ throw new \Box_Exception('VM Template is missing');
+ }
+
+ // Retrieve associated product
+ $product = $this->di['db']->findOne('product', 'id=:id', array(':id' => $data['id']));
+
+ $config = array(
+ 'group' => $data['group'],
+ 'filling' => $data['filling'],
+ 'show_stock' => $data['show_stock'],
+ 'virt' => $data['virt'],
+ 'server' => $data['server'],
+ 'lxc-templ' => $data['lxc-templ'],
+ 'vm-templ' => $data['vm-templ'],
+ 'vmconftempl' => $data['vmconftempl'],
+ );
+
+ $product->config = json_encode($config);
+ $product->updated_at = date('Y-m-d H:i:s');
+ $this->di['db']->store($product);
+
+ $this->di['logger']->info('Update Proxmox product %s', $product->id);
+ return true;
+ }
+
+ /* ################################################################################################### */
+ /* #################################### Resource Management ######################################## */
+ /* ################################################################################################### */
+
+ /**
+ * Get list of vm templates
+ *
+ * @return array
+ */
+ public function service_get_vmtemplates()
+ {
+ $output = $this->getService()->get_vmtemplates();
+ return $output;
+ }
+
+ /**
+ * Get list of qemu templates
+ *
+ * @return array
+ */
+ public function service_get_qemutemplates()
+ {
+ $output = $this->getService()->get_qemutemplates();
+ return $output;
+ }
+
+ /**
+ * Get list of lxc templates
+ *
+ * @return array
+ */
+ public function service_get_lxctemplates()
+ {
+ $output = $this->getService()->get_lxctemplates();
+ return $output;
+ }
+
+ /**
+ * Get list of ip ranges
+ *
+ * @return array
+ */
+ public function service_get_ip_ranges()
+ {
+ $output = $this->getService()->get_ip_ranges();
+ return $output;
+ }
+
+ /**
+ * Get list of ip adresses
+ *
+ */
+ public function service_get_ip_adresses()
+ {
+ $output = $this->getService()->get_ip_adresses();
+ return $output;
+ }
+
+ /**
+ * Get list of vlans
+ *
+ * @return array
+ */
+ public function service_get_vlans()
+ {
+ $output = $this->getService()->get_vlans();
+ return $output;
+ }
+
+ /**
+ * Get list of tags by type
+ *
+ * @return array
+ */
+ public function service_get_tags($data)
+ {
+ $output = $this->getService()->get_tags($data);
+ return $output;
+ }
+
+ public function service_add_tag($data)
+ {
+ $output = $this->getService()->save_tag($data);
+ return $output;
+ }
+
+ /**
+ * Get list of tags by storage id
+ *
+ * @return array
+ */
+ public function service_get_tags_by_storage($data)
+ {
+ $output = $this->getService()->get_tags_by_storage($data);
+ error_log("service_get_tags_by_storage: " . print_r($output, true));
+ return $output;
+ }
+
+ /**
+ * Get a vm configuration templates
+ *
+ * @return array
+ */
+ public function vm_config_template_get($data)
+ {
+ error_log("vm_config_template_get");
+ $vm_config_template = $this->di['db']->findOne('service_proxmox_vm_config_template', 'id=:id', array(':id' => $data['id']));
+ if (!$vm_config_template) {
+ throw new \Box_Exception('VM template not found');
+ }
+ $output = array(
+ 'id' => $vm_config_template->id,
+ 'name' => $vm_config_template->name,
+ 'cores' => $vm_config_template->cores,
+ 'description' => $vm_config_template->description,
+ 'memory' => $vm_config_template->memory,
+ 'balloon' => $vm_config_template->balloon,
+ 'balloon_size' => $vm_config_template->balloon_size,
+ 'os' => $vm_config_template->os,
+ 'bios' => $vm_config_template->bios,
+ 'onboot' => $vm_config_template->onboot,
+ 'agent' => $vm_config_template->agent,
+ 'created_at' => $vm_config_template->created_at,
+ 'updated_at' => $vm_config_template->updated_at,
+ );
+
+ return $output;
+ }
+
+ /**
+ * Function to get storages for vm config template
+ *
+ * @return array
+ */
+
+ public function vm_config_template_get_storages($data)
+ {
+ error_log("Get list of storages for VM Template: " . print_r($data, true));
+ $vm_config_template = $this->di['db']->find('service_proxmox_vm_storage_template', 'template_id=:id', array(':id' => $data['id']));
+ //replace the storage_type with the name of the tag
+ foreach ($vm_config_template as $key => $value) {
+ $storage_tag_id = json_decode($value->storage_type);
+ error_log("storage_tag_id: " . print_r($storage_tag_id, true));
+ $tag = $this->di['db']->findOne('service_proxmox_tag', 'id=:id', array(':id' => $storage_tag_id));
+ $vm_config_template[$key]->storage_type = $tag->name;
+ }
+ return $vm_config_template;
+ }
+
+ /**
+ * Get list of lxc configuration templates
+ *
+ * @return array
+ */
+ public function lxc_config_template_get($data)
+ {
+ $lxc_config_template = $this->di['db']->findOne('service_proxmox_lxc_config_template', 'id=:id', array(':id' => $data['id']));
+ if (!$lxc_config_template) {
+ throw new \Box_Exception('LXC template not found');
+ }
+
+ $output = array(
+ 'id' => $lxc_config_template->id,
+ 'name' => $lxc_config_template->name,
+ 'template_id' => $lxc_config_template->template_id,
+ 'cores' => $lxc_config_template->cores,
+ 'description' => $lxc_config_template->description,
+ 'memory' => $lxc_config_template->memory,
+ 'swap' => $lxc_config_template->swap,
+ 'ostemplate' => $lxc_config_template->ostemplate,
+ 'onboot' => $lxc_config_template->onboot,
+ 'created_at' => $lxc_config_template->created_at,
+ 'updated_at' => $lxc_config_template->updated_at,
+ );
+
+ return $output;
+ }
+
+
+ /**
+ * Get ip range
+ *
+ * @return array
+ */
+ public function ip_range_get($data)
+ {
+ $ip_range = $this->di['db']->findOne('service_proxmox_ip_range', 'id=:id', array(':id' => $data['id']));
+ if (!$ip_range) {
+ throw new \Box_Exception('IP range not found');
+ }
+ $output = array(
+ 'id' => $ip_range->id,
+ 'cidr' => $ip_range->cidr,
+ 'gateway' => $ip_range->gateway,
+ 'broadcast' => $ip_range->broadcast,
+ 'type' => $ip_range->type,
+ 'created_at' => $ip_range->created_at,
+ 'updated_at' => $ip_range->updated_at,
+ );
+
+ return $output;
+ }
+
+ /**
+ * Get vlan
+ */
+ public function vlan_get($data)
+ {
+ $vlan = $this->di['db']->findOne('service_proxmox_client_vlan', 'id=:id', array(':id' => $data['id']));
+ if (!$vlan) {
+ throw new \Box_Exception('VLAN not found');
+ }
+
+ // fill client_name field
+ $client = $this->di['db']->findOne('client', 'id=:id', array(':id' => $vlan->client_id));
+ if (!$client) {
+ throw new \Box_Exception('Client not found');
+ }
+
+ // get IP Range cidr
+ $iprange = $this->di['db']->findOne('service_proxmox_ip_range', 'id=:id', array(':id' => $vlan->ip_range));
+ $output = array(
+ 'id' => $vlan->id,
+ 'client_id' => $vlan->client_id,
+ 'client_name' => $client->first_name . " " . $client->last_name,
+ 'vlan' => $vlan->vlan,
+ 'ip_range' => $vlan->ip_range,
+ 'cidr' => $iprange->cidr,
+ 'created_at' => $vlan->created_at,
+ 'updated_at' => $vlan->updated_at,
+ );
+
+ return $output;
+ }
+
+
+ /**
+ * Create vm configuration template
+ *
+ * @return bool
+ */
+ public function vm_config_template_create($data)
+ {
+ $required = array(
+ 'name' => 'Server name is missing',
+ );
+
+ $this->di['validator']->checkRequiredParamsForArray($required, $data);
+
+ // dispense new vm_config_template
+ $vm_config_template = $this->di['db']->dispense('service_proxmox_vm_config_template');
+ // Fill vm_config_template
+ $vm_config_template->name = $data['name'];
+ $vm_config_template->state = "draft";
+
+ $this->di['db']->store($vm_config_template);
+
+
+ $this->di['logger']->info('Create VM config Template %s', $vm_config_template->id);
+ return $vm_config_template;
+ }
+
+ /**
+ * Update vm configuration template
+ *
+ * @return bool
+ */
+ public function vm_template_update($data)
+ {
+ $required = array(
+ 'name' => 'Server name is missing',
+ 'description' => 'Server description is missing',
+ 'cpu_cores' => 'CPU cores are missing',
+ 'vmmemory' => 'memory is missing',
+ 'balloon' => 'Balloon is missing',
+ 'os' => 'OS is missing',
+ 'bios' => 'Bios Type is missing',
+ 'onboot' => 'Start on Boot is missing',
+ 'agent' => 'Run Agent is missing'
+ );
+
+ $this->di['validator']->checkRequiredParamsForArray($required, $data);
+ error_log("vm_template_update: " . print_r($data, true));
+ // Retrieve associated vm_config_template
+ $vm_config_template = $this->di['db']->findOne('service_proxmox_vm_config_template', 'id=:id', array(':id' => $data['id']));
+
+ // Fill vm_config_template
+ $vm_config_template->name = $data['name'];
+ $vm_config_template->description = $data['description'];
+ $vm_config_template->cores = $data['cpu_cores'];
+ $vm_config_template->memory = $data['vmmemory'];
+ $vm_config_template->balloon = $data['balloon'];
+ // if $data['ballon'] is true, check if $data['ballon_size'] is set, otherwise use $data['memory']
+ if ($data['balloon'] == true) {
+ if (isset($data['balloon_size'])) {
+ $vm_config_template->balloon_size = $data['balloon_size'];
+ } else {
+ $vm_config_template->balloon_size = $data['memory'];
+ }
+ }
+ $vm_config_template->os = $data['os'];
+ $vm_config_template->bios = $data['bios'];
+ $vm_config_template->onboot = $data['onboot'];
+ $vm_config_template->agent = $data['agent'];
+ $vm_config_template->updated_at = date('Y-m-d H:i:s');
+ $this->di['db']->store($vm_config_template);
+
+ $this->di['logger']->info('Update VM config Template %s', $vm_config_template->id);
+ return true;
+ }
+
+ /**
+ * Delete vm configuration template
+ *
+ * @return bool
+ */
+ public function vm_template_delete($id)
+ {
+ $vm_config_template = $this->di['db']->findOne('service_proxmox_vm_config_template', 'id = ?', [$id]);
+
+ // TODO: Check if vm_config_template is used by any product
+
+ $this->di['db']->trash($vm_config_template);
+ $this->di['logger']->info('Delete VM config Template %s', $id);
+ return true;
+ }
+
+ /**
+ * Create lxc configuration template
+ *
+ * @return bool
+ */
+ public function lxc_template_create($data)
+ {
+ $required = array(
+ 'description' => 'Template description is missing',
+ 'cpu_cores' => 'CPU cores are missing',
+ 'memory' => 'memory is missing',
+ 'swap' => 'Swap is missing',
+ 'ostemplate' => 'OS template is missing',
+ 'onboot' => 'Start on Boot is missing',
+ );
+
+ $this->di['validator']->checkRequiredParamsForArray($required, $data);
+
+ // dispense new vm_config_template
+ $lxc_config_template = $this->di['db']->dispense('service_proxmox_lxc_config_template');
+ // Fill vm_config_template
+ $lxc_config_template->description = $data['description'];
+ $lxc_config_template->cores = $data['cpu_cores'];
+ $lxc_config_template->memory = $data['memory'];
+ $lxc_config_template->swap = $data['swap'];
+ $lxc_config_template->template_id = $data['ostemplate'];
+ // get os template headline from $di['db'] and set it to $lxc_config_template->ostemplate
+ $ostemplate = $this->di['db']->findOne('service_proxmox_lxc_appliance', 'id = ?', [$data['ostemplate']]);
+ $lxc_config_template->ostemplate = $ostemplate->headline;
+ $lxc_config_template->onboot = $data['onboot'];
+ $lxc_config_template->created_at = date('Y-m-d H:i:s');
+ $lxc_config_template->updated_at = date('Y-m-d H:i:s');
+ $this->di['db']->store($lxc_config_template);
+
+ $this->di['db']->store($lxc_config_template);
+
+
+ $this->di['logger']->info('Create LXC config Template %s', $lxc_config_template->id);
+ return true;
+ }
+
+
+ /**
+ * Update lxc configuration template
+ *
+ * @return bool
+ */
+ public function lxc_template_update($data)
+ {
+ $required = array(
+ 'id' => 'Template ID is missing',
+ 'description' => 'Template description is missing',
+ 'name' => 'Server name is missing',
+ 'cpu_cores' => 'CPU cores are missing',
+ 'memory' => 'memory is missing',
+ 'swap' => 'Swap is missing',
+ 'ostemplate' => 'OS template is missing',
+ 'onboot' => 'Start on Boot is missing',
+ );
+
+ $this->di['validator']->checkRequiredParamsForArray($required, $data);
+
+ // Retrieve associated lxc_config_template
+ $lxc_config_template = $this->di['db']->findOne('service_proxmox_lxc_config_template', 'id=:id', array(':id' => $data['id']));
+
+ // Fill lxc_config_template
+ $lxc_config_template->description = $data['description'];
+ $lxc_config_template->name = $data['name'];
+ $lxc_config_template->cores = $data['cpu_cores'];
+ $lxc_config_template->memory = $data['memory'];
+ $lxc_config_template->swap = $data['swap'];
+ $lxc_config_template->template_id = $data['ostemplate'];
+ // get os template headline from $di['db'] and set it to $lxc_config_template->ostemplate
+ $ostemplate = $this->di['db']->findOne('service_proxmox_lxc_appliance', 'id = ?', [$data['ostemplate']]);
+ $lxc_config_template->ostemplate = $ostemplate->headline;
+ $lxc_config_template->onboot = $data['onboot'];
+ $lxc_config_template->updated_at = date('Y-m-d H:i:s');
+ $this->di['db']->store($lxc_config_template);
+
+ $this->di['logger']->info('Update LXC config Template %s', $lxc_config_template->id);
+ return true;
+ }
+
+ /**
+ * Delete lxc configuration template
+ *
+ * @return bool
+ */
+ public function lxc_template_delete($id)
+ {
+ $lxc_config_template = $this->di['db']->findOne('service_proxmox_lxc_config_template', 'id = ?', [$id]);
+ $this->di['db']->trash($lxc_config_template);
+ $this->di['logger']->info('Delete LXC config Template %s', $id);
+ return true;
+ }
+
+ /**
+ * Create VM Config Template Storage
+ *
+ * @return bool
+ */
+ public function vm_config_template_storage_create($data)
+ {
+ $required = array(
+ 'template_id' => 'Template ID is missing',
+ 'storage_size' => 'Storage size is missing',
+ 'storage_type_tags' => 'Storage type is missing',
+ 'storage_controller' => 'Storage controller is missing',
+ );
+
+ $this->di['validator']->checkRequiredParamsForArray($required, $data);
+
+
+ // dispense new vm_config_template_storage
+ $vm_config_template_storage = $this->di['db']->dispense('service_proxmox_vm_storage_template');
+ // Fill vm_config_template_storage
+ $vm_config_template_storage->template_id = $data['template_id'];
+ $vm_config_template_storage->size = $data['storage_size'];
+ $vm_config_template_storage->storage_type = json_encode($data['storage_type_tags']);
+ $vm_config_template_storage->controller = $data['storage_controller'];
+ $vm_config_template_storage->created_at = date('Y-m-d H:i:s');
+ $vm_config_template_storage->updated_at = date('Y-m-d H:i:s');
+ $this->di['db']->store($vm_config_template_storage);
+
+ }
+
+
+
+ /**
+ * Create ip range
+ *
+ * @return bool
+ */
+ public function ip_range_create($data)
+ {
+ $required = array(
+ 'cidr' => 'CIDR is missing',
+ 'gateway' => 'Gateway is missing',
+ 'broadcast' => 'Broadcast is missing',
+ 'type' => 'Type is missing',
+ );
+
+ $this->di['validator']->checkRequiredParamsForArray($required, $data);
+
+ // dispense new ip_network
+ $ip_range = $this->di['db']->dispense('service_proxmox_ip_range');
+ // Fill ip_network
+ $ip_range->cidr = $data['cidr'];
+ $ip_range->gateway = $data['gateway'];
+ $ip_range->broadcast = $data['broadcast'];
+ $ip_range->type = $data['type'];
+ $ip_range->created_at = date('Y-m-d H:i:s');
+ $ip_range->updated_at = date('Y-m-d H:i:s');
+ $this->di['db']->store($ip_range);
+
+
+ $this->di['logger']->info('Create IP Network %s', $ip_range->id);
+ return true;
+ }
+
+ /**
+ * Update ip range
+ *
+ * @return bool
+ */
+ public function ip_range_update($data)
+ {
+ $required = array(
+ 'id' => 'ID is missing',
+ 'cidr' => 'CIDR is missing',
+ 'gateway' => 'Gateway is missing',
+ 'broadcast' => 'Broadcast is missing',
+ 'type' => 'Type is missing',
+ );
+
+ $this->di['validator']->checkRequiredParamsForArray($required, $data);
+
+ // Retrieve associated ip_network
+ $ip_range = $this->di['db']->findOne('service_proxmox_ip_range', 'id=:id', array(':id' => $id));
+
+ // Fill ip_network
+ $ip_range->cidr = $data['cidr'];
+ $ip_range->gateway = $data['gateway'];
+ $ip_range->broadcast = $data['broadcast'];
+ $ip_range->type = $data['type'];
+ $ip_range->updated_at = date('Y-m-d H:i:s');
+ $this->di['db']->store($ip_range);
+
+ $this->di['logger']->info('Update IP Network %s', $ip_range->id);
+ return true;
+ }
+
+ /**
+ * Delete ip range
+ *
+ * @param int $id
+ * @return array
+ */
+ public function ip_range_delete($id)
+ {
+ $ip_network = $this->di['db']->findOne('service_proxmox_ip_range', 'id = ?', [$id]);
+ $this->di['db']->trash($ip_network);
+ $this->di['logger']->info('Delete IP Network %s', $id);
+ return true;
+ }
+
+
+ /**
+ * Create client vlan
+ *
+ * @return bool
+ */
+ public function client_vlan_create($data)
+ {
+ $required = array(
+ 'client_id' => 'Client ID is missing',
+ 'vlan' => 'VLAN ID is missing',
+ 'ip_range' => 'IP_range is missing',
+ );
+
+ $this->di['validator']->checkRequiredParamsForArray($required, $data);
+
+ // dispense new client_network
+ $client_network = $this->di['db']->dispense('service_proxmox_client_vlan');
+ // Fill client_network
+ $client_network->client_id = $data['client_id'];
+ $client_network->vlan = $data['vlan'];
+ $client_network->ip_range = $data['ip_range'];
+ $client_network->created_at = date('Y-m-d H:i:s');
+ $client_network->updated_at = date('Y-m-d H:i:s');
+ $this->di['db']->store($client_network);
+
+
+ $this->di['logger']->info('Create Client Network %s', $client_network->id);
+ return true;
+ }
+
+ /**
+ * Update client vlan
+ *
+ * @return bool
+ */
+ public function client_vlan_update($data)
+ {
+ $required = array(
+ 'id' => 'ID is missing',
+ 'client_id' => 'Client ID is missing',
+ 'vlan' => 'VLAN ID is missing',
+ 'ip_range' => 'ip_range is missing',
+ );
+
+ $this->di['validator']->checkRequiredParamsForArray($required, $data);
+
+ // Retrieve associated client_network
+ $client_network = $this->di['db']->findOne('service_proxmox_client_vlan', 'id=:id', array(':id' => $data['id']));
+
+ // Fill client_network
+ $client_network->client_id = $data['client_id'];
+ $client_network->vlan = $data['vlan'];
+ $client_network->ip_range = $data['ip_range'];
+ $client_network->updated_at = date('Y-m-d H:i:s');
+ $this->di['db']->store($client_network);
+
+ $this->di['logger']->info('Update Client Network %s', $client_network->id);
+ return true;
+ }
+
+
+ /**
+ * Delete client vlanx
+ *
+ * @param int $id
+ * @return bool
+ */
+ public function client_vlan_delete($data)
+ {
+ $client_network = $this->di['db']->findOne('service_proxmox_client_vlan', 'id = ?', [$data['id']]);
+ $this->di['db']->trash($client_network);
+ $this->di['logger']->info('Delete Client Network %s', $id);
+ return true;
+ }
+
+ /* ################################################################################################### */
+ /* ######################################## Permissions ############################################ */
+ /* ################################################################################################### */
+
+
+ /* ################################################################################################### */
+ /* ######################################## Maintenance ############################################ */
+ /* ################################################################################################### */
+
+ /**
+ * Backup Module Configuration Tables
+ *
+ * @return bool
+ */
+ public function proxmox_backup_config($data)
+ {
+ if ($this->getService()->pmxdbbackup($data)) {
+ return true;
+ } else {
+ return false;
+ }
+ }
+
+ // Function to list backups
+ /**
+ * List existing Backups
+ *
+ * @return array
+ */
+ public function proxmox_list_backups()
+ {
+ $output = $this->getService()->pmxbackuplist();
+ return $output;
+ }
+
+ /**
+ * Restore Backup
+ *
+ * @return bool
+ */
+ public function proxmox_restore_backup($data)
+ {
+ $this->proxmox_backup_config('backup');
+ $this->di['logger']->info('Restoring Proxmox server Backup: %s', $data['backup']);
+ if ($this->getService()->pmxbackuprestore($data)) {
+ return true;
+ } else {
+ return false;
+ }
+ }
+
+ /**
+ * Get module version
+ *
+ * @return string
+ */
+ public function get_module_version()
+ {
+ $config = $this->di['mod_config']('Serviceproxmox');
+ return $config['version'];
+ }
+
+} // EOF
\ No newline at end of file
diff --git a/src/Api/Client.php b/src/Api/Client.php
new file mode 100644
index 0000000..069fa8a
--- /dev/null
+++ b/src/Api/Client.php
@@ -0,0 +1,220 @@
+getService()->getServiceproxmoxByOrderId($data['order_id']);
+
+ return $this->getService()->customCall($model, $name, $data);
+ }
+
+ /**
+ * Get server details
+ *
+ * @param int $id - server id
+ * @return array
+ *
+ * @throws \Box_Exception
+ */
+ public function vm_get($data)
+ {
+ $required = array(
+ 'order_id' => 'Order ID is missing',
+ );
+ $this->di['validator']->checkRequiredParamsForArray($required, $data);
+
+ $order = $this->di['db']->findOne(
+ 'client_order',
+ "id=:id",
+ array(':id' => $data['order_id'])
+ );
+ if (!$order) {
+ throw new \Box_Exception('Order not found');
+ }
+
+ $service = $this->di['db']->findOne(
+ 'service_proxmox',
+ "order_id=:id",
+ array(':id' => $data['order_id'])
+ );
+ if (!$service) {
+ throw new \Box_Exception('Proxmox service not found');
+ }
+
+ // Retrieve associated
+ $server = $this->di['db']->findOne('service_proxmox_server', 'id=:id', array(':id' => $service['server_id']));
+
+ // if a server has been found, output its details, otherwise return an empty array
+ if (!$server) {
+ return array();
+ }
+ $vm_info = $this->getService()->vm_info($order, $service);
+ $output = array(
+ 'server' => $server->hostname,
+ 'username' => 'root',
+ 'cli' => $this->getService()->vm_cli($order, $service),
+ 'status' => $vm_info['status'],
+ );
+ return $output;
+ }
+
+ public function server_get($data)
+ {
+ $required = array(
+ 'order_id' => 'Order ID is missing',
+ );
+ $this->di['validator']->checkRequiredParamsForArray($required, $data);
+
+ $order = $this->di['db']->findOne(
+ 'client_order',
+ "id=:id",
+ array(':id' => $data['order_id'])
+ );
+ if (!$order) {
+ throw new \Box_Exception('Order not found');
+ }
+
+ $service = $this->di['db']->findOne(
+ 'service_proxmox',
+ "order_id=:id",
+ array(':id' => $data['order_id'])
+ );
+ if (!$service) {
+ throw new \Box_Exception('Proxmox service not found');
+ }
+
+ // Retrieve associated
+ $server = $this->di['db']->findOne('service_proxmox_server', 'id=:id', array(':id' => $service['server_id']));
+
+ // if a server has been found, output its details, otherwise return an empty array
+ if (!$server) {
+ return array();
+ }
+ return $server;
+ }
+
+ // function to return proxmox_service information from order
+ public function get_proxmox_service($data)
+ {
+ $required = array(
+ 'order_id' => 'Order ID is missing',
+ );
+ $this->di['validator']->checkRequiredParamsForArray($required, $data);
+
+ $order = $this->di['db']->findOne(
+ 'client_order',
+ "id=:id",
+ array(':id' => $data['order_id'])
+ );
+ if (!$order) {
+ throw new \Box_Exception('Order not found');
+ }
+
+ $service = $this->di['db']->findOne(
+ 'service_proxmox',
+ "order_id=:id",
+ array(':id' => $data['order_id'])
+ );
+ if (!$service) {
+ throw new \Box_Exception('Proxmox service not found');
+ }
+ return $service;
+ }
+
+
+ /**
+ * Reboot vm
+ */
+ public function vm_manage($data)
+ {
+ $required = array(
+ 'order_id' => 'Order ID is missing',
+ 'method' => 'Method is missing',
+ );
+ $this->di['validator']->checkRequiredParamsForArray($required, $data);
+
+ $order = $this->di['db']->findOne(
+ 'client_order',
+ "id=:id",
+ array(':id' => $data['order_id'])
+ );
+ if (!$order) {
+ throw new \Box_Exception('Order not found');
+ }
+
+ $service = $this->di['db']->findOne(
+ 'service_proxmox',
+ "order_id=:id",
+ array(':id' => $data['order_id'])
+ );
+ if (!$service) {
+ throw new \Box_Exception('Proxmox service not found');
+ }
+
+ // Retrieve associated server
+ $server = $this->di['db']->findOne('service_proxmox_server', 'id=:id', array(':id' => $service['server_id']));
+
+ switch ($data['method']) {
+ case 'reboot':
+ $this->getService()->vm_reboot($order, $service);
+ break;
+ case 'start':
+ $this->getService()->vm_start($order, $service);
+ break;
+ case 'shutdown':
+ $this->getService()->vm_shutdown($order, $service);
+ default:
+ return false;
+ }
+
+ return true;
+ }
+
+ /**
+ * Get VNC console from PVE Host
+ */
+ public function novnc_appjs_get($data)
+ {
+ $appjs = $this->getService()->get_novnc_appjs($data);
+ return $appjs;
+ }
+}
diff --git a/src/Controller/Admin.php b/src/Controller/Admin.php
new file mode 100644
index 0000000..24752a7
--- /dev/null
+++ b/src/Controller/Admin.php
@@ -0,0 +1,228 @@
+di = $di;
+ }
+
+ public function getDi(): ?\Pimple\Container
+ {
+ return $this->di;
+ }
+
+ /**
+ * Fetches the navigation array for the admin area
+ *
+ * @return array
+ */
+ public function fetchNavigation()
+ {
+ return array(
+ 'group' => array(
+ 'index' => 550,
+ 'location' => 'proxmox',
+ 'label' => __trans('Proxmox'),
+ 'class' => 'server',
+ 'sprite_class' => 'dark-sprite-icon sprite-graph',
+ ),
+ 'subpages' => array(
+ [
+ 'location' => 'proxmox',
+ 'label' => __trans('Proxmox Servers'),
+ 'uri' => $this->di['url']->adminLink('serviceproxmox'),
+ 'index' => 100,
+ 'class' => '',
+ ],
+ [
+ 'location' => 'proxmox',
+ 'label' => __trans('Proxmox Templates'),
+ 'uri' => $this->di['url']->adminLink('serviceproxmox/templates'),
+ 'index' => 200,
+ 'class' => '',
+ ],
+ [
+ 'location' => 'proxmox',
+ 'label' => __trans('IP Address Management'),
+ 'uri' => $this->di['url']->adminLink('serviceproxmox/ipam'),
+ 'index' => 300,
+ 'class' => '',
+ ],
+ ),
+ );
+ }
+
+ /**
+ * Registers the admin area routes
+ *
+ */
+ public function register(\Box_App &$app)
+ {
+ $app->get('/serviceproxmox', 'get_index', null, get_class($this));
+ $app->get('/serviceproxmox/templates', 'get_templates', null, get_class($this));
+ $app->get('/serviceproxmox/ipam', 'get_ipam', null, get_class($this));
+ $app->get('/serviceproxmox/maintenance/backup', 'start_backup', null, get_class($this));
+ $app->get('/serviceproxmox/server/:id', 'get_server', array('id' => '[0-9]+'), get_class($this));
+ $app->get('/serviceproxmox/storage', 'get_storage', null, get_class($this));
+ $app->get('/serviceproxmox/server/by_group/:id', 'get_server_by_group', array('id' => '[0-9]+'), get_class($this));
+ $app->get('/serviceproxmox/storage/:id', 'get_storage', array('id' => '[0-9]+'), get_class($this));
+ $app->get('/serviceproxmox/storageclass', 'get_storage', null, get_class($this));
+ $app->get('/serviceproxmox/storageclass/:id', 'get_storageclass ', array('id' => '[0-9]+'), get_class($this));
+ $app->get('/serviceproxmox/ipam/iprange', 'get_ip_range', null, get_class($this));
+ $app->get('/serviceproxmox/ipam/iprange/:id', 'get_ip_range', array('id' => '[0-9]+'), get_class($this));
+ $app->get('/serviceproxmox/ipam/client_vlan', 'get_client_vlan', null, get_class($this));
+ $app->get('/serviceproxmox/ipam/client_vlan/:id', 'get_client_vlan', array('id' => '[0-9]+'), get_class($this));
+ $app->get('/serviceproxmox/templates/lxc_config', 'get_lxc_config_template', null, get_class($this));
+ $app->get('/serviceproxmox/templates/lxc_config/:id', 'get_lxc_config_template', array('id' => '[0-9]+'), get_class($this));
+ $app->get('/serviceproxmox/templates/vm_config', 'get_vm_config_template', null, get_class($this));
+ $app->get('/serviceproxmox/templates/vm_config/:id', 'get_vm_config_template', array('id' => '[0-9]+'), get_class($this));
+ $app->get('/serviceproxmox/templates/enable/:id', 'enable_template', array('id' => '[0-9]+'), get_class($this));
+ $app->get('/serviceproxmox/templates/disable/:id', 'disable_template', array('id' => '[0-9]+'), get_class($this));
+ }
+
+ /**
+ * Renders the admin area index page
+ */
+ public function get_index(\Box_App $app)
+ {
+ $this->di['is_admin_logged'];
+ return $app->render('mod_serviceproxmox_index');
+ }
+
+ /**
+ * Renders the admin area templates page
+ */
+ public function get_templates(\Box_App $app)
+ {
+ return $app->render('mod_serviceproxmox_templates');
+ }
+
+ /**
+ * Enables the QEMU Template
+ *
+ */
+ public function enable_template(\Box_App $app, $id)
+ {
+ $api = $this->di['api_admin'];
+ $api->Serviceproxmox_vm_config_template_enable(array('id' => $id));
+ return $app->redirect('serviceproxmox/templates');
+ }
+
+ /**
+ * Disables the QEMU Template
+ *
+ */
+ public function disable_template(\Box_App $app, $id)
+ {
+ $api = $this->di['api_admin'];
+ $api->Serviceproxmox_vm_config_template_disable(array('id' => $id));
+ return $app->redirect('serviceproxmox/templates');
+ }
+ /**
+ * Renders the admin area ipam page
+ */
+ public function get_ipam(\Box_App $app)
+ {
+ return $app->render('mod_serviceproxmox_ipam');
+ }
+
+ /**
+ * Handles CRUD for Proxmox Servers
+ */
+ public function get_server(\Box_App $app, $id)
+ {
+ $api = $this->di['api_admin'];
+ $server = $api->Serviceproxmox_server_get(array('server_id' => $id));
+ return $app->render('mod_serviceproxmox_server', array('server' => $server));
+ }
+
+ public function get_server_by_group(\Box_App $app, $id)
+ {
+ $api = $this->di['api_admin'];
+ $server = $api->Serviceproxmox_servers_in_group(array('group' => $id));
+ return $app->render('mod_serviceproxmox_server', array('server' => $server));
+ }
+
+ /**
+ * Handles CRUD for Proxmox Storage
+ */
+ public function get_storage(\Box_App $app, $id)
+ {
+ $api = $this->di['api_admin'];
+ $storage = $api->Serviceproxmox_storage_get(array('storage_id' => $id));
+ return $app->render('mod_serviceproxmox_storage', array('storage' => $storage));
+ }
+
+ /**
+ * Handles CRUD for IP Range
+ */
+ public function get_ip_range(\Box_App $app, $id)
+ {
+ $api = $this->di['api_admin'];
+ $ip_range = $api->Serviceproxmox_ip_range_get(array('id' => $id));
+ return $app->render('mod_serviceproxmox_ipam_iprange', array('ip_range' => $ip_range));
+ }
+
+ /**
+ * Handles CRUD for Client VLAN
+ */
+ public function client_vlan(\Box_App $app, $id)
+ {
+ $api = $this->di['api_admin'];
+ $client_vlan = $api->Serviceproxmox_vlan_get(array('id' => $id));
+ return $app->render('mod_serviceproxmox_ipam_client_vlan', ['client_vlan' => $client_vlan]);
+ }
+
+ /**
+ * Handles CRUD for LXC Config Templates
+ */
+ public function get_lxc_config_template(\Box_App $app, $id)
+ {
+ $api = $this->di['api_admin'];
+ $lxc_config_template = $api->Serviceproxmox_lxc_config_template_get(array('id' => $id));
+ return $app->render('mod_serviceproxmox_templates_lxc', array('lxc_config_template' => $lxc_config_template));
+ }
+
+ /**
+ * Handles CRUD for VM Config Templates
+ */
+ public function get_vm_config_template(\Box_App $app, $id)
+ {
+ error_log("Controller get_vm_config_template");
+ $api = $this->di['api_admin'];
+ $vm_config_template = $api->Serviceproxmox_vm_config_template_get(array('id' => $id));
+ return $app->render('mod_serviceproxmox_templates_qemu', array('vm_config_template' => $vm_config_template));
+ }
+
+ /**
+ * Renders the admin area settings page
+ */
+ public function start_backup(\Box_App $app)
+ {
+
+ $api = $this->di['api_admin'];
+ $backup = $api->Serviceproxmox_proxmox_backup_config('backup');
+ return $app->redirect('extension/settings/serviceproxmox');
+ }
+}
diff --git a/src/Controller/Client.php b/src/Controller/Client.php
new file mode 100644
index 0000000..b7fcdf0
--- /dev/null
+++ b/src/Controller/Client.php
@@ -0,0 +1,80 @@
+di = $di;
+ }
+
+ public function getDi(): ?\Pimple\Container
+ {
+ return $this->di;
+ }
+
+ public function register(\Box_App &$app)
+ {
+ // register all routers to load novnc app.js & dependencies from proxmox
+ $app->get('/serviceproxmox/novnc/:filename.:fileending', 'get_novnc_appjs_filename', [], static::class);
+ $app->get('/serviceproxmox/novnc/:folder/:filename.:fileending', 'get_novnc_appjs_folder_filename', [], static::class);
+ $app->get('/serviceproxmox/novnc/:folder/:subfolder/:filename.:fileending', 'get_novnc_appjs_folder_subfolder_filename', [], static::class);
+ }
+
+ // create functions to call get_novnc_appjs with paths
+ // create function for only filename
+ public function get_novnc_appjs_filename(\Box_App $app, $filename, $fileending)
+ {
+ $file_path = $filename . '.' . $fileending;
+ return $this->get_novnc_appjs($app, $file_path);
+ }
+ // create function for filename and folder
+ public function get_novnc_appjs_folder_filename(\Box_App $app, $folder, $filename, $fileending)
+ {
+ $file_path = $folder . '/' . $filename . '.' . $fileending;
+ return $this->get_novnc_appjs($app, $file_path);
+ }
+ // create function for filename, folder and subfolder
+ public function get_novnc_appjs_folder_subfolder_filename(\Box_App $app, $folder, $subfolder, $filename, $fileending)
+ {
+ $file_path = $folder . '/' . $subfolder . '/' . $filename . '.' . $fileending;
+ return $this->get_novnc_appjs($app, $file_path);
+ }
+
+
+ // create get_novnc_appjs function
+ public function get_novnc_appjs(\Box_App $app, $file_path)
+ {
+ $api = $this->di['api_client'];
+ // print out $file;
+ // build path
+ $request_response = $api->Serviceproxmox_novnc_appjs_get($file_path);
+ // get content and content type from response
+ $content = $request_response->getContent();
+ $content_headers = $request_response->getHeaders();
+
+ header("Content-type: " . $content_headers['Content-Type']);
+ // replace every occurence of /novnc/ with /serviceproxmox/novnc/
+ $content = str_replace('/novnc/', '/serviceproxmox/novnc/', $content);
+ return $content;
+ }
+}
diff --git a/src/ProxmoxAuthentication.php b/src/ProxmoxAuthentication.php
new file mode 100644
index 0000000..44bae86
--- /dev/null
+++ b/src/ProxmoxAuthentication.php
@@ -0,0 +1,283 @@
+di['mod_config']('Serviceproxmox');
+ // Retrieve the server access information
+ $serveraccess = $this->find_access($server);
+
+ // Create a new instance of the PVE2_API class with the server access details
+ $proxmox = new PVE2_API($serveraccess, $server->root_user, $server->realm, $server->root_password, port: $server->port, tokenid: $server->tokenname, tokensecret: $server->tokenvalue, debug: $config['pmx_debug_logging']);
+
+ // Attempt to log in to the server using the API
+ if (!$proxmox->login()) {
+ throw new \Box_Exception("Failed to connect to the server.");
+ }
+
+ // Create an API token for the Admin user if not logged in via API token
+ if (empty($server->tokenname) || empty($server->tokenvalue)) {
+ // Check if the connecting user has the 'Realm.AllocateUser' permission
+ $permissions = $proxmox->get("/access/permissions");
+ $found_permission = 0;
+ // Iterate through the permissions and check for 'Realm.AllocateUser' permission
+ foreach ($permissions as $permission) {
+ if ($permission['Realm.AllocateUser'] == 1) {
+ $found_permission += 1;
+ }
+ }
+ // Throw an exception if the 'Realm.AllocateUser' permission is not found
+ if (!$found_permission) {
+ throw new \Box_Exception("User does not have 'Realm.AllocateUser' permission");
+ }
+
+ // Validate if there already is a group for fossbilling
+ $groups = $proxmox->get("/access/groups");
+ $foundgroups = 0;
+ // Iterate through the groups and check for a group beginning with 'fossbilling'
+ foreach ($groups as $group) {
+ if (strpos($group['groupid'], 'fossbilling') === 0) {
+ $foundgroups += 1;
+ $groupid = $group['groupid'];
+ }
+ }
+ // Handle the cases where there are no groups, one group, or multiple groups
+ switch ($foundgroups) {
+ case 0:
+ // Create a new group
+ $groupid = 'fossbilling_' . rand(1000, 9999);
+ $newgroup = $proxmox->post("/access/groups", array('groupid' => $groupid, 'comment' => 'fossbilling group'));
+ break;
+ case 1:
+ // Use the existing group
+ break;
+ default:
+ throw new \Box_Exception("More than one group found");
+ break;
+ }
+
+
+ // Validate if there already is a user and token for fossbilling
+ $users = $proxmox->get("/access/users");
+ $found = 0;
+ // Iterate through the users and check for a user beginning with 'fb'
+ foreach ($users as $user) {
+ if (strpos($user['userid'], 'fb') === 0) {
+ $found += 1;
+ $userid = $user['userid'];
+ }
+ // Handle the cases where there are no users, one user, or multiple users
+ switch ($found) {
+ case 0:
+ // Create a new user
+ $userid = 'fb_' . rand(1000, 9999) . '@pve'; // TODO: Make realm configurable in the module settings
+ $newuser = $proxmox->post("/access/users", array('userid' => $userid, 'password' => $this->di['tools'], 'enable' => 1, 'comment' => 'fossbilling user', 'groups' => $groupid));
+
+ // Create a token for the new user
+ $token = $proxmox->post("/access/users/" . $userid . "/token/fb_access", array());
+
+ // Check if the token was created successfully
+ if ($token) {
+ $server->tokenname = $token['full-tokenid'];
+ $server->tokenvalue = $token['value'];
+ } else {
+ throw new \Box_Exception("Failed to create token for fossbilling user");
+ break;
+ }
+ break;
+ case 1:
+ // Create a token for the existing user
+ $token = $proxmox->post("/access/users/" . $userid . "/token/fb_access", array());
+ if ($token) {
+ $server->tokenname = $token['full-tokenid'];
+ $server->tokenvalue = $token['value'];
+ } else {
+ throw new \Box_Exception("Failed to create token for fossbilling user");
+ break;
+ }
+ break;
+ default:
+ throw new \Box_Exception("There are more than one fossbilling users on the server. Please delete all but one.");
+ break;
+ }
+ // Create permissions for the newly created token
+ // Set up permissions for the token (Admin user) to manage users, groups, and other administrative tasks
+ $permissions = $proxmox->put("/access/acl/", array('path' => '/', 'roles' => 'PVEUserAdmin', 'propagate' => 1, 'users' => $userid));
+ $permissions = $proxmox->put("/access/acl/", array('path' => '/', 'roles' => 'PVEAuditor', 'propagate' => 1, 'users' => $userid));
+ $permissions = $proxmox->put("/access/acl/", array('path' => '/', 'roles' => 'PVESysAdmin', 'propagate' => 1, 'users' => $userid));
+ $permissions = $proxmox->put("/access/acl/", array('path' => '/', 'roles' => 'PVEPoolAdmin', 'propagate' => 1, 'users' => $userid));
+ $permissions = $proxmox->put("/access/acl/", array('path' => '/', 'roles' => 'PVEDatastoreAdmin', 'propagate' => 1, 'users' => $userid));
+ $permissions = $proxmox->put("/access/acl/", array('path' => '/', 'roles' => 'PVEUserAdmin', 'propagate' => 1, 'tokens' => $server->tokenname));
+ $permissions = $proxmox->put("/access/acl/", array('path' => '/', 'roles' => 'PVEAuditor', 'propagate' => 1, 'tokens' => $server->tokenname));
+ $permissions = $proxmox->put("/access/acl/", array('path' => '/', 'roles' => 'PVESysAdmin', 'propagate' => 1, 'tokens' => $server->tokenname));
+ $permissions = $proxmox->put("/access/acl/", array('path' => '/', 'roles' => 'PVEPoolAdmin', 'propagate' => 1, 'tokens' => $server->tokenname));
+ $permissions = $proxmox->put("/access/acl/", array('path' => '/', 'roles' => 'PVEDatastoreAdmin', 'propagate' => 1, 'tokens' => $server->tokenname));
+
+ // Sleep for 5 seconds
+ sleep(5);
+
+ // Check if the permissions were created correctly by logging in and creating another user
+ /*
+ echo "";
+ echo "";
+ echo "";
+ echo "";
+ echo "
";
+ */
+
+ // Delete the root password and unset the PVE2_API instance
+ $server->root_password = null;
+ unset($proxmox);
+
+ // Return the test_access result for the server
+ return $this->test_access($server);
+ }
+ } else {
+ // Validate Permissions for the token
+ $permissions = $proxmox->get("/access/acl/");
+ // Check for 'PVEUserAdmin', 'PVEAuditor', 'PVESysAdmin', 'PVEPoolAdmin', and 'PVEDatastoreAdmin' permissions, and if they don't exist, try to create them.
+ $required_permissions = array('PVEUserAdmin', 'PVEAuditor', 'PVESysAdmin', 'PVEPoolAdmin', 'PVEDatastoreAdmin');
+ foreach ($required_permissions as $permission) {
+ $found_permission = 0;
+ foreach ($permissions as $acl) {
+ if ($acl['roleid'] == $permission) {
+ $found_permission += 1;
+ }
+ }
+ if (!$found_permission) {
+ $permissions = $proxmox->put("/access/acl/", array('path' => '/', 'roles' => $permission, 'propagate' => 1, 'tokens' => $server->tokenname));
+ }
+ }
+
+ }
+ }
+
+
+ /**
+ * Tests the access to the server
+ *
+ * @param Server $server The server to test
+ * @return bool True if the test was successful, false otherwise
+ * @throws Box_Exception
+ */
+ public function test_access($server)
+ {
+ // Retrieve the server access information
+ $serveraccess = $this->find_access($server);
+ $config = $this->di['mod_config']('Serviceproxmox');
+ // Create a new instance of the PVE2_API class with the server access details
+ $proxmox = new PVE2_API($serveraccess, $server->root_user, $server->realm, $server->root_password, port: $server->port, tokenid: $server->tokenname, tokensecret: $server->tokenvalue, debug: $config['pmx_debug_logging']);
+
+ // Attempt to log in to the server using the API
+ if (!$proxmox->login()) {
+ throw new \Box_Exception("Failed to connect to the server. testpmx");
+ }
+
+ // Generate a random test user ID
+ $userid = 'tfb_' . rand(1000, 9999) . '@pve'; // TODO: Make realm configurable in the module settings
+
+ // Create a new user for testing purposes
+ $proxmox->post("/access/users", array('userid' => $userid, 'password' => $this->di['tools']->generatePassword(16, 4), 'enable' => '1', 'comment' => 'FOSSBilling test user ' . $userid));
+
+ // Retrieve the newly created user
+ $newuser = $proxmox->get("/access/users/" . $userid);
+
+ // Check if the new user was successfully created
+ if (!$newuser) {
+ throw new \Box_Exception("Failed to create test user for fossbilling");
+ } else {
+ // Delete the test user
+ $deleteuser = $proxmox->delete("/access/users/" . $userid);
+
+ // Check if the test user was successfully deleted
+ $deleteuser = $proxmox->get("/access/users/" . $userid);
+ if ($deleteuser) {
+ throw new \Box_Exception("Failed to delete test user for fossbilling. Check permissions.");
+ } else {
+ // Remove the root password from the server object
+ $server->root_password = null;
+ return $server;
+ }
+ }
+ }
+
+
+ /**
+ * Creates a new client user on the server
+ *
+ * @param Server $server The server to create the user on
+ * @param Client $client The client to create the user for
+ * @return void
+ */
+ public function create_client_user($server, $client)
+ {
+ $clientuser = $this->di['db']->dispense('service_proxmox_users');
+ $clientuser->client_id = $client->id;
+ $this->di['db']->store($clientuser);
+ $serveraccess = $this->find_access($server);
+ $config = $this->di['mod_config']('Serviceproxmox');
+ $proxmox = new PVE2_API($serveraccess, $server->root_user, $server->realm, $server->root_password, port: $server->port, tokenid: $server->tokenname, tokensecret: $server->tokenvalue, debug: $config['pmx_debug_logging']);
+ if (!$proxmox->login()) {
+ throw new \Box_Exception("Failed to connect to the server. create_client_user");
+ }
+ $userid = 'fb_customer_' . $client->id . '@pve'; // TODO: Make realm configurable in the module settings
+ $newuser = $proxmox->post("/access/users", array('userid' => $userid, 'password' => $this->di['tools']->generatePassword(16, 4), 'enable' => '1', 'comment' => 'fossbilling user ' . $client->id));
+ $newuser = $proxmox->get("/access/users/" . $userid);
+
+ // Create Token for Client
+ $clientuser->admin_tokenname = 'fb_admin_' . $client->id;
+ $clientuser->server_id = $server->id;
+ $admintoken_response = $proxmox->post("/access/users/" . $userid . "/token/" . $clientuser->admin_tokenname, array('comment' => 'fossbilling admin token for client id: ' . $client->id));
+ $clientuser->admin_tokenname = $admintoken_response['full-tokenid'];
+ $clientuser->admin_tokenvalue = $admintoken_response['value'];
+ $clientuser->view_tokenname = 'fb_view_' . $client->id;
+ $viewtoken_response = $proxmox->post("/access/users/" . $userid . "/token/" . $clientuser->view_tokenname, array('comment' => 'fossbilling view token for client id: ' . $client->id));
+ $clientuser->view_tokenname = $viewtoken_response['full-tokenid'];
+ $clientuser->view_tokenvalue = $viewtoken_response['value'];
+
+
+ $this->di['db']->store($clientuser);
+
+ // Check if the client already has a pool and if not create it.
+ $pool = $proxmox->get("/pools/" . $client->id);
+ if (!$pool) {
+ $pool = $proxmox->post("/pools", array('poolid' => 'fb_client_' . $client->id, 'comment' => 'fossbilling pool for client id: ' . $client->id));
+ }
+ // Add permissions for client
+
+ $permissions = $proxmox->put("/access/acl/", array('path' => '/pool/' . 'fb_client_' . $client->id, 'roles' => 'PVEVMUser,PVEVMAdmin,PVEDatastoreAdmin,PVEDatastoreUser', 'propagate' => 1, 'users' => $userid));
+
+ $permissions = $proxmox->put("/access/acl/", array('path' => '/pool/' . 'fb_client_' . $client->id, 'roles' => 'PVEVMUser,PVEDatastoreUser', 'propagate' => 1, 'tokens' => $clientuser->view_tokenname));
+
+ $permissions = $proxmox->put("/access/acl/", array('path' => '/pool/' . 'fb_client_' . $client->id, 'roles' => 'PVEVMAdmin,PVEDatastoreAdmin', 'propagate' => 1, 'tokens' => $clientuser->admin_tokenname));
+ }
+}
diff --git a/src/ProxmoxIPAM.php b/src/ProxmoxIPAM.php
new file mode 100644
index 0000000..5c1aa6a
--- /dev/null
+++ b/src/ProxmoxIPAM.php
@@ -0,0 +1,63 @@
+di['db']->find('service_proxmox_ip_range');
+ return $ip_ranges;
+ }
+
+ // Function that gets all IP Adresses
+ public function get_ip_adresses()
+ {
+ // get all the VM templates from the service_proxmox_vm_config_template table
+ $ip_addresses = $this->di['db']->find('service_proxmox_ipadress');
+ return $ip_addresses;
+ }
+
+ // Function that gets all the LXC templates and returns them as an array
+ public function get_vlans()
+ {
+ // get all the LXC templates from the service_proxmox_lxc_config_template table
+ $vlans = $this->di['db']->find('service_proxmox_client_vlan');
+ // Fill in the client name
+ foreach ($vlans as $vlan) {
+ $client = $this->di['db']->getExistingModelById('client', $vlan->client_id);
+ $vlan->client_name = $client->first_name . " " . $client->last_name;
+ }
+
+ return $vlans;
+ }
+
+ /* ################################################################################################### */
+ /* ################################### Manage PVE Network ######################################### */
+ /* ################################################################################################### */
+}
diff --git a/src/ProxmoxServer.php b/src/ProxmoxServer.php
new file mode 100644
index 0000000..840fb72
--- /dev/null
+++ b/src/ProxmoxServer.php
@@ -0,0 +1,260 @@
+find_access($server);
+ $config = $this->di['mod_config']('Serviceproxmox');
+ $proxmox = new PVE2_API($serveraccess, $server->root_user, $server->realm, $server->root_password, port: $server->port, tokenid: $server->tokenname, tokensecret: $server->tokenvalue,debug: $config['pmx_debug_logging']);
+ // check if tokenname and tokenvalue contain values by checking their content
+ if (empty($server->tokenname) || empty($server->tokenvalue)) {
+ if (!empty($server->root_user) && !empty($server->root_password)) {
+
+ if ($proxmox->login()) {
+ error_log("Serviceproxmox: Login with username and password successful");
+ return true;
+ } else {
+ throw new \Box_Exception("Login to Proxmox Host failed");
+ }
+ } else {
+ throw new \Box_Exception("No login information provided");
+ }
+ } else if ($proxmox->getVersion()) {
+ error_log("Serviceproxmox: Login with token successful!");
+ return true;
+ } else {
+ throw new \Box_Exception("Failed to connect to the server.");
+ }
+ }
+
+ /*
+ Validate token access and setup
+ */
+ public function test_token_connection($server)
+ {
+ // Test if login
+ $serveraccess = $this->find_access($server);
+ $config = $this->di['mod_config']('Serviceproxmox');
+ $proxmox = new PVE2_API($serveraccess, $server->root_user, $server->realm, $server->root_password, port: $server->port, tokenid: $server->tokenname, tokensecret: $server->tokenvalue,debug: $config['pmx_debug_logging']);
+ // check if tokenname and tokenvalue contain values by checking their content
+ if (empty($server->tokenname) || empty($server->tokenvalue)) {
+ throw new \Box_Exception("Token Access Failed: No tokenname or tokenvalue provided");
+ } else if ($proxmox->get_version()) {
+ error_log("Serviceproxmox: Login with token successful!");
+ $permissions = $proxmox->get("/access/permissions");
+ $found_permission = 0;
+ // Iterate through the permissions and check for 'Realm.AllocateUser' permission
+ foreach ($permissions as $permission) {
+ if ($permission['Realm.AllocateUser'] == 1) {
+ $found_permission += 1;
+ }
+ }
+ // Throw an exception if the 'Realm.AllocateUser' permission is not found
+ if (!$found_permission) {
+ throw new \Box_Exception("Token does not have 'Realm.AllocateUser' permission");
+ }
+
+ // Validate if there already is a group for fossbilling
+ $groups = $proxmox->get("/access/groups");
+ $foundgroups = 0;
+ // Iterate through the groups and check for a group beginning with 'fossbilling'
+ foreach ($groups as $group) {
+ if (strpos($group['groupid'], 'fossbilling') === 0) {
+ $foundgroups += 1;
+ $groupid = $group['groupid'];
+ }
+ // check if groupid is the same as the id of the token (fb_1234@pve!fb_access)
+ $fb_token_instanceid = explode('@', $server->tokenname)[1];
+
+
+ if ($group['groupid'] == $server->tokenname) {
+ $foundgroups += 1;
+ $groupid = $group['groupid'];
+ }
+
+ }
+ return true;
+ } else {
+ throw new \Box_Exception("Failed to connect to the server.");
+ }
+ }
+
+
+
+ /* Find best Server
+ */
+ public function find_empty($product)
+ {
+ $productconfig = json_decode($product->config, 1);
+ $group = $productconfig['group'];
+ $filling = $productconfig['filling'];
+
+ // retrieve overprovisioning information from extension settings
+ $config = $this->di['mod_config']('Serviceproxmox');
+ $cpu_overprovion_percent = $config['cpu_overprovisioning'];
+ $ram_overprovion_percent = $config['ram_overprovisioning'];
+ $avoid_overprovision = $config['avoid_overprovision'];
+
+ // Retrieve only non-full active servers sorted by ratio.
+ // priority is given to servers with the largest difference between ram and used ram
+ // if avoid_overprovision is set to true, servers with a ratio of >1 are ignored
+ $servers = $this->di['db']->find('service_proxmox_server', ['status' => 'active']);
+ //echo "";
+ $server = null;
+ $server_ratio = 0;
+ // use values from database to calculate ratio and store the server id, cpu and ram usage ratio if it's better than the previous one
+ foreach ($servers as $s) {
+ $cpu_usage = $s['cpu_cores_allocated'];
+ $ram_usage = $s['ram_allocated'];
+ $cpu_cores = $s['cpu_cores'];
+ $ram = $s['ram'];
+
+ $cpu_ratio = $cpu_usage / $cpu_cores;
+ $ram_ratio = $ram_usage / $ram;
+
+ // if avoid_overprovision is set to true, servers with a ratio of >1 are ignored
+ if ($avoid_overprovision && ($cpu_ratio > 1 || $ram_ratio > 1)) {
+ continue;
+ }
+ // calculate ratio with overprovisioning
+ $cpu_ratio = $cpu_ratio * (1 + $cpu_overprovion_percent / 100);
+ $ram_ratio = $ram_ratio * (1 + $ram_overprovion_percent / 100);
+ // check current best ratio
+ if ($cpu_ratio + $ram_ratio > $server_ratio) {
+ $server_ratio = $cpu_ratio + $ram_ratio;
+ $server = $s['id'];
+ }
+ }
+ // if no server is found, return null
+ if ($server == null) {
+ return null;
+ }
+ // if a server is found, return the id
+ return $server;
+ }
+
+ /*
+ Find access to server (hostname, ipv4, ipv6)
+ */
+ public function find_access($server)
+ {
+ if (!empty($server->hostname)) {
+ return $server->hostname;
+ } else if (!empty($server->ipv4)) {
+ return $server->ipv4;
+ } else if (!empty($server->ipv6)) {
+ return $server->ipv6;
+ } else {
+ throw new \Box_Exception('No IPv6, IPv4 or Hostname found for server ' . $server->id);
+ }
+ }
+
+ /*
+ Find server hardware usage information "getHardwareData"
+ */
+ public function getHardwareData($server)
+ {
+ // Retrieve associated server
+ $serveraccess = $this->find_access($server);
+ $config = $this->di['mod_config']('Serviceproxmox');
+ $proxmox = new PVE2_API($serveraccess, $server->root_user, $server->realm, $server->root_password, port: $server->port, tokenid: $server->tokenname, tokensecret: $server->tokenvalue,debug: $config['pmx_debug_logging']);
+
+ if ($proxmox->login()) {
+ error_log("ProxmoxServer.php: getHardwareData: Login successful");
+ $hardware = $proxmox->get("/nodes/" . $server->name . "/status");
+ return $hardware;
+ } else {
+ throw new \Box_Exception("Failed to connect to the server. hw Token Access Failed");
+ }
+ }
+
+ public function getStorageData($server)
+ {
+ // Retrieve associated server
+ $serveraccess = $this->find_access($server);
+ $config = $this->di['mod_config']('Serviceproxmox');
+ $proxmox = new PVE2_API($serveraccess, $server->root_user, $server->realm, $server->root_password, port: $server->port, tokenid: $server->tokenname, tokensecret: $server->tokenvalue,debug: $config['pmx_debug_logging']);
+ if ($proxmox->login()) {
+ $storage = $proxmox->get("/nodes/" . $server->name . "/storage");
+ return $storage;
+ } else {
+ throw new \Box_Exception("Failed to connect to the server. st");
+ }
+ }
+
+ // function to get all assigned cpu_cores and ram on a server (used to find free resources)
+ public function getAssignedResources($server)
+ {
+ // Retrieve associated server
+ $serveraccess = $this->find_access($server);
+ $config = $this->di['mod_config']('Serviceproxmox');
+ $proxmox = new PVE2_API($serveraccess, $server->root_user, $server->realm, $server->root_password, port: $server->port, tokenid: $server->tokenname, tokensecret: $server->tokenvalue,debug: $config['pmx_debug_logging']);
+ if ($proxmox->login()) {
+ $assigned_resources = $proxmox->get("/nodes/" . $server->name . "/qemu");
+ return $assigned_resources;
+ } else {
+ throw new \Box_Exception("Failed to connect to the server. st");
+ }
+ }
+
+ // function to get available appliances from a server
+ public function getAvailableAppliances()
+ {
+ $server = $this->di['db']->getExistingModelById('service_proxmox_server', 1, 'Server not found');
+
+ $serveraccess = $this->find_access($server);
+ $config = $this->di['mod_config']('Serviceproxmox');
+ $proxmox = new PVE2_API($serveraccess, $server->root_user, $server->realm, $server->root_password, port: $server->port, tokenid: $server->tokenname, tokensecret: $server->tokenvalue,debug: $config['pmx_debug_logging']);
+ if ($proxmox->login()) {
+ $appliances = $proxmox->get("/nodes/" . $server->name . "/aplinfo");
+ return $appliances;
+ } else {
+ throw new \Box_Exception("Failed to connect to the server. st");
+ }
+ }
+
+ // function to get available template vms from a server
+ public function getQemuTemplates($server)
+ {
+ $serveraccess = $this->find_access($server);
+ $config = $this->di['mod_config']('Serviceproxmox');
+ $proxmox = new PVE2_API($serveraccess, $server->root_user, $server->realm, $server->root_password, port: $server->port, tokenid: $server->tokenname, tokensecret: $server->tokenvalue,debug: $config['pmx_debug_logging']);
+ if ($proxmox->login()) {
+ $templates = $proxmox->get("/nodes/" . $server->name . "/qemu");
+ return $templates;
+ } else {
+ throw new \Box_Exception("Failed to connect to the server. st");
+ }
+ }
+}
diff --git a/src/ProxmoxTemplates.php b/src/ProxmoxTemplates.php
new file mode 100644
index 0000000..23a8e26
--- /dev/null
+++ b/src/ProxmoxTemplates.php
@@ -0,0 +1,75 @@
+di['db']->findAll('service_proxmox_vm_config_template');
+ return $templates;
+ }
+
+
+ // Function that gets all the LXC templates and returns them as an array
+ public function get_lxctemplates()
+ {
+ // get all the LXC templates from the service_proxmox_lxc_config_template table
+ $templates = $this->di['db']->findAll('service_proxmox_lxc_config_template');
+ return $templates;
+ }
+
+ // Function that gets all qemu templates and returns them as an array
+ public function get_qemutemplates()
+ {
+ // get all the qemu templates from the service_proxmox_qemu_template table
+ $qemu_templates = $this->di['db']->findAll('service_proxmox_qemu_template');
+ // Get server name for each template
+ foreach ($qemu_templates as $qemu_template) {
+ $server = $this->di['db']->getExistingModelById('service_proxmox_server', $qemu_template->server_id);
+ $qemu_template->server_name = $server->name;
+ }
+ return $qemu_templates;
+ }
+
+ // Function that gets a vm config template by id
+ public function get_vmconfig($id)
+ {
+ // get the vm config template from the service_proxmox_vm_config_template table
+ $template = $this->di['db']->getExistingModelById('service_proxmox_vm_config_template', $id);
+ return $template;
+ }
+
+ // Function that gets a lxc config template by id
+ public function get_lxc_conftempl($id)
+ {
+ // get the lxc config template from the service_proxmox_lxc_config_template table
+ $template = $this->di['db']->getExistingModelById('service_proxmox_lxc_config_template', $id);
+ return $template;
+ }
+}
diff --git a/src/ProxmoxVM.php b/src/ProxmoxVM.php
new file mode 100644
index 0000000..8b416e8
--- /dev/null
+++ b/src/ProxmoxVM.php
@@ -0,0 +1,314 @@
+vm_shutdown($order, $model);
+ // TODO: Check that the VM was shutdown, otherwise send an email to the admin
+
+ $model->updated_at = date('Y-m-d H:i:s');
+ $this->di['db']->store($model);
+
+ return true;
+ }
+
+ /**
+ * Unsuspend Proxmox VM
+ * @param $order
+ * @return boolean
+ */
+ public function unsuspend($order, $model)
+ {
+ // Power on VM?
+ $this->vm_start($order, $model);
+ $model->updated_at = date('Y-m-d H:i:s');
+ $this->di['db']->store($model);
+
+ return true;
+ }
+
+ /**
+ * Cancel Proxmox VM
+ * @param $order
+ * @return boolean
+ */
+ public function cancel($order, $model)
+ {
+ return $this->suspend($order, $model);
+ }
+
+ /**
+ * Uncancel Proxmox VM
+ * @param $order
+ * @return boolean
+ */
+ public function uncancel($order, $model)
+ {
+ return $this->unsuspend($order, $model);
+ }
+
+ /**
+ * Delete Proxmox VM
+ * @param $order
+ * @return boolean
+ */
+ public function delete($order, $model)
+ {
+ if (is_object($model)) {
+
+ $product = $this->di['db']->load('product', $order->product_id);
+ $product_config = json_decode($product->config, 1);
+
+ // Retrieve associated server
+ $server = $this->di['db']->findOne('service_proxmox_server', 'id=:id', array(':id' => $model->server_id));
+
+ // Connect to YNH API
+ $serveraccess = $this->find_access($server);
+ $config = $this->di['mod_config']('Serviceproxmox');
+ $proxmox = new PVE2_API($serveraccess, $server->root_user, $server->realm, $server->root_password, port: $server->port, tokenid: $server->tokenname, tokensecret: $server->tokenvalue,debug: $config['pmx_debug_logging']);
+
+ if ($proxmox->login()) {
+ $proxmox->post("/nodes/" . $model->node . "/" . $product_config['virt'] . "/" . $model->vmid . "/status/shutdown", array());
+ $status = $proxmox->get("/nodes/" . $model->node . "/" . $product_config['virt'] . "/" . $model->vmid . "/status/current");
+
+ // Wait until the server has been shut down if the server exists
+ if (!empty($status)) {
+ while ($status['status'] != 'stopped') {
+ sleep(10);
+ $proxmox->post("/nodes/" . $model->node . "/" . $product_config['virt'] . "/" . $model->vmid . "/status/shutdown", array());
+ $status = $proxmox->get("/nodes/" . $model->node . "/" . $product_config['virt'] . "/" . $model->vmid . "/status/current");
+ }
+ } else {
+ throw new \Box_Exception("VMID cannot be found");
+ }
+ if ($proxmox->delete("/nodes/" . $model->node . "/" . $product_config['virt'] . "/" . $model->vmid)) {
+ // TODO: Check if that was the last VM for the client and delete the client user on Proxmox
+ return true;
+ } else {
+ throw new \Box_Exception("VM not deleted");
+ }
+ } else {
+ throw new \Box_Exception("Login to Proxmox Host failed");
+ }
+ }
+ }
+
+ /*
+ * VM status
+ *
+ * TODO: Add more Information
+ */
+ public function vm_info($order, $service)
+ {
+ $product = $this->di['db']->load('product', $order->product_id);
+ $product_config = json_decode($product->config, 1);
+ $client = $this->di['db']->load('client', $order->client_id);
+
+ // Retrieve associated server
+ $server = $this->di['db']->findOne('service_proxmox_server', 'id=:id', array(':id' => $service->server_id));
+ $clientuser = $this->di['db']->findOne('service_proxmox_users', 'server_id = ? and client_id = ?', array($server->id, $client->id));
+
+ // Test if login
+ $serveraccess = $this->find_access($server);
+ $proxmox = new PVE2_API($serveraccess, $server->root_user, $server->realm, $server->root_password, port: $server->port, tokenid: $clientuser->admin_tokenname, tokensecret: $clientuser->admin_tokenvalue,debug: $config['pmx_debug_logging']);
+ if ($proxmox->get_version()) {
+ $status = $proxmox->get("/nodes/" . $server->name . "/" . $product_config['virt'] . "/" . $service->vmid . "/status/current");
+ // VM monitoring?
+
+ $output = array(
+ 'status' => $status['status']
+ );
+ return $output;
+ } else {
+ throw new \Box_Exception("Login to Proxmox Host failed.");
+ }
+ }
+
+ /*
+ Cold Reboot VM
+ */
+ public function vm_reboot($order, $service)
+ {
+ $product = $this->di['db']->load('product', $order->product_id);
+ $product_config = json_decode($product->config, 1);
+ $client = $this->di['db']->load('client', $order->client_id);
+
+ // Retrieve associated server
+ $server = $this->di['db']->findOne('service_proxmox_server', 'id=:id', array(':id' => $service->server_id));
+ $clientuser = $this->di['db']->findOne('service_proxmox_users', 'server_id = ? and client_id = ?', array($server->id, $client->id));
+
+ // Test if login
+ $serveraccess = $this->find_access($server);
+ $proxmox = new PVE2_API($serveraccess, $server->root_user, $server->realm, $server->root_password, port: $server->port, tokenid: $clientuser->admin_tokenname, tokensecret: $clientuser->admin_tokenvalue,debug: $config['pmx_debug_logging']);
+ if ($proxmox->login()) {
+ $proxmox->post("/nodes/" . $server->name . "/" . $product_config['virt'] . "/" . $service->vmid . "/status/shutdown", array());
+ $status = $proxmox->get("/nodes/" . $server->name . "/" . $product_config['virt'] . "/" . $service->vmid . "/status/current");
+
+ // Wait until the VM has been shut down if the VM exists
+ if (!empty($status)) {
+ while ($status['status'] != 'stopped') {
+ sleep(10);
+ $proxmox->post("/nodes/" . $server->name . "/" . $product_config['virt'] . "/" . $service->vmid . "/status/shutdown", array());
+ $status = $proxmox->get("/nodes/" . $server->name . "/" . $product_config['virt'] . "/" . $service->vmid . "/status/current");
+ }
+ }
+ // Restart
+ if ($proxmox->post("/nodes/" . $server->name . "/" . $product_config['virt'] . "/" . $service->vmid . "/status/start", array())) {
+ sleep(10);
+ $status = $proxmox->get("/nodes/" . $server->name . "/" . $product_config['virt'] . "/" . $service->vmid . "/status/current");
+ while ($status['status'] != 'running') {
+ $proxmox->post("/nodes/" . $server->name . "/" . $product_config['virt'] . "/" . $service->vmid . "/status/start", array());
+ sleep(10);
+ $status = $proxmox->get("/nodes/" . $server->name . "/" . $product_config['virt'] . "/" . $service->vmid . "/status/current");
+ // Starting twice => error...
+ }
+ return true;
+ } else {
+ throw new \Box_Exception("Reboot failed");
+ }
+ } else {
+ throw new \Box_Exception("Login to Proxmox Host failed.");
+ }
+ }
+
+ /*
+ Start VM
+ */
+ public function vm_start($order, $service)
+ {
+ $product = $this->di['db']->load('product', $order->product_id);
+ $product_config = json_decode($product->config, 1);
+ $client = $this->di['db']->load('client', $order->client_id);
+ // Retrieve associated server
+ // Retrieve associated server
+ $server = $this->di['db']->findOne('service_proxmox_server', 'id=:id', array(':id' => $service->server_id));
+ $clientuser = $this->di['db']->findOne('service_proxmox_users', 'server_id = ? and client_id = ?', array($server->id, $client->id));
+
+ // Test if login
+ $serveraccess = $this->find_access($server);
+ $proxmox = new PVE2_API($serveraccess, $server->root_user, $server->realm, $server->root_password, port: $server->port, tokenid: $clientuser->admin_tokenname, tokensecret: $clientuser->admin_tokenvalue,debug: $config['pmx_debug_logging']);
+ if ($proxmox->login()) {
+ $proxmox->post("/nodes/" . $server->name . "/" . $product_config['virt'] . "/" . $service->vmid . "/status/start", array());
+ return true;
+ } else {
+ throw new \Box_Exception("Login to Proxmox Host failed.");
+ }
+ }
+
+ /*
+ Shutdown VM
+ */
+ public function vm_shutdown($order, $service)
+ {
+ $product = $this->di['db']->load('product', $order->product_id);
+ $product_config = json_decode($product->config, 1);
+ $client = $this->di['db']->load('client', $order->client_id);
+ // Retrieve associated server
+ $server = $this->di['db']->findOne('service_proxmox_server', 'id=:id', array(':id' => $service->server_id));
+
+ // Test if login
+ // find service access for server
+ $clientuser = $this->di['db']->findOne('service_proxmox_users', 'server_id = ? and client_id = ?', array($server->id, $client->id));
+ //echo "D: ".var_dump($order);
+ $serveraccess = $this->find_access($server);
+ $proxmox = new PVE2_API($serveraccess, $server->root_user, $server->realm, $server->root_password, port: $server->port, tokenid: $clientuser->admin_tokenname, tokensecret: $clientuser->admin_tokenvalue,debug: $config['pmx_debug_logging']);
+ if ($proxmox->login()) {
+ $settings = array(
+ 'forceStop' => true
+ );
+
+ $proxmox->post("/nodes/" . $server->name . "/" . $product_config['virt'] . "/" . $service->vmid . "/status/shutdown", $settings);
+ return true;
+ } else {
+ throw new \Box_Exception("Login to Proxmox Host failed.");
+ }
+ }
+
+
+ /*
+ VM iframe for Web CLI
+ */
+ public function vm_cli($order, $service)
+ {
+
+ $product = $this->di['db']->load('product', $order->product_id);
+ $product_config = json_decode($product->config, 1);
+ $client = $this->di['db']->load('client', $order->client_id);
+
+ // Retrieve associated server
+ $server = $this->di['db']->findOne('service_proxmox_server', 'id=:id', array(':id' => $service->server_id));
+ $clientuser = $this->di['db']->findOne('service_proxmox_users', 'server_id = ? and client_id = ?', array($server->id, $client->id));
+
+ // Find access route
+ $serveraccess = $this->find_access($server);
+
+ // Setup console type
+ $console = ($product_config['virt'] == 'qemu') ? 'kvm' : 'shell';
+
+ // The user enters the password to see the iframe: TBD
+ //$password = 'test';
+ //$proxmox = new PVE2_API($serveraccess, $client->id, $server->name, $password);
+
+ $proxmox = new PVE2_API($serveraccess, $server->root_user, $server->realm, $server->root_password, port: $server->port, tokenid: $clientuser->view_tokenname, tokensecret: $clientuser->view_tokenvalue,debug: $config['pmx_debug_logging']);
+
+ if ($proxmox->login()) {
+ // Get VNC Web proxy ticket by calling /nodes/{node}/{type}/{vmid}/vncproxy
+ $vncproxy_response = $proxmox->post("/nodes/" . $server->name . "/" . $product_config['virt'] . "/" . $service->vmid . "/vncproxy", array('node' => $server->name, 'vmid' => $service->vmid));
+ $vncproxy_port = $vncproxy_response['data']['port'];
+ $vncproxy_ticket = $vncproxy_response['data']['ticket'];
+ // open a vnc web socket
+ // we'll do it ourselves until the upstream API supports it
+ $put_post_http_headers[] = "Authorization: PVEAPIToken={$clientuser->view_tokenname}={$clientuser->view_tokenvalue}";
+ // add the vncticket to the post_http_headers
+ $put_post_http_headers[] = "vncticket: {$vncproxy_ticket}";
+ // add the port to the post_http_headers
+ $put_post_http_headers[] = "port: {$vncproxy_port}";
+ // add the host to the post_http_headers
+ $put_post_http_headers[] = "host: {$serveraccess}";
+ // add the vmid to the post_http_headers
+ $put_post_http_headers[] = "vmid: {$service->vmid}";
+ // open websocket connection and display the console
+ /* curl_setopt($prox_ch, CURLOPT_URL, "https://{$serveraccess}:8006/api2/json/nodes/{$server->name}/{$product_config['virt']}/{$service->vmid}/vncwebsocket");
+ curl_setopt($prox_ch, CURLOPT_RETURNTRANSFER, true);
+ curl_setopt($prox_ch, CURLOPT_POSTFIELDS, $login_postfields_string);
+ curl_setopt($prox_ch, CURLOPT_SSL_VERIFYPEER, $this->verify_ssl);
+ curl_setopt($prox_ch, CURLOPT_SSL_VERIFYHOST, $this->verify_ssl);
+ */
+ // return array of vncport & ticket
+ return array('port' => $vncproxy_port, 'ticket' => $vncproxy_ticket);
+ } else {
+ throw new \Box_Exception("Login to Proxmox VM failed.");
+ }
+ }
+}
diff --git a/src/Service.php b/src/Service.php
new file mode 100644
index 0000000..2133495
--- /dev/null
+++ b/src/Service.php
@@ -0,0 +1,902 @@
+di = $di;
+ }
+
+ public function getDi(): ?\Pimple\Container
+ {
+ return $this->di;
+ }
+ use ProxmoxAuthentication;
+ use ProxmoxServer;
+ use ProxmoxVM;
+ use ProxmoxTemplates;
+ use ProxmoxIPAM;
+
+
+ public function validateCustomForm(array &$data, array $product)
+ {
+ if ($product['form_id']) {
+ $formbuilderService = $this->di['mod_service']('formbuilder');
+ $form = $formbuilderService->getForm($product['form_id']);
+
+ foreach ($form['fields'] as $field) {
+ if ($field['required'] == 1) {
+ $field_name = $field['name'];
+ if ((!isset($data[$field_name]) || empty($data[$field_name]))) {
+ throw new \Box_Exception("You must fill in all required fields. " . $field['label'] . " is missing", null, 9684);
+ }
+ }
+
+ if ($field['readonly'] == 1) {
+ $field_name = $field['name'];
+ if ($data[$field_name] != $field['default_value']) {
+ throw new \Box_Exception("Field " . $field['label'] . " is read only. You can not change its value", null, 5468);
+ }
+ }
+ }
+ }
+ }
+
+ /**
+ * Method to install module. In most cases you will provide your own
+ * database table or tables to store extension related data.
+ *
+ * If your extension is not very complicated then extension_meta
+ * database table might be enough.
+ *
+ * @return bool
+ */
+ public function install()
+ {
+ // read manifest.json to get current version number
+ $manifest = json_decode(file_get_contents(__DIR__ . '/manifest.json'), true);
+ $version = $manifest['version'];
+
+ // check if there is a sqldump backup with "uninstall" in it's name in the pmxconfig folder, if so, restore it
+ $filesystem = new Filesystem();
+ // list content of pmxconfig folder using symfony finder
+ $finder = new Finder();
+
+ // check if pmxconfig folder exists
+ if (!$filesystem->exists(PATH_ROOT . '/pmxconfig')) {
+ $filesystem->mkdir(PATH_ROOT . '/pmxconfig');
+ }
+
+ $pmxbackup_dir = $finder->in(PATH_ROOT . '/pmxconfig')->files()->name('proxmox_uninstall_*.sql');
+
+ // find newest file in pmxbackup_dir according to timestamp
+ $pmxbackup_file = array();
+ foreach ($pmxbackup_dir as $file) {
+ $pmxbackup_file[$file->getMTime()] = $file->getFilename();
+ }
+ ksort($pmxbackup_file);
+ $pmxbackup_file = array_reverse($pmxbackup_file);
+ $pmxbackup_file = reset($pmxbackup_file);
+
+ // if pmxbackup_file is not empty, restore the sql dump to database
+ if (!empty($pmxbackup_file)) {
+ // Load the backup
+ $dump = file_get_contents(PATH_ROOT . '/pmxconfig/' . $pmxbackup_file);
+
+ // Check if dump is not empty
+ if (!empty($dump)) {
+ // Check version number in first line of dump
+ $original_dump = $dump;
+ $version_line = strtok($dump, "\n");
+
+ // Get version number from line
+ $dump_version = str_replace('-- Proxmox module version: ', '', $version_line);
+ $dump = str_replace($version_line . "\n", '', $dump);
+
+ // Get db config
+ $db_user = $this->di['config']['db']['user'];
+ $db_password = $this->di['config']['db']['password'];
+ $db_name = $this->di['config']['db']['name'];
+
+ try {
+ // Create PDO instance
+ $pdo = new PDO('mysql:host=localhost;dbname=' . $db_name, $db_user, $db_password);
+ $pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
+
+ // If version number in dump is smaller than current version number, restore dump and run upgrade function
+ if ($dump_version < $version) {
+ // Split the dump into an array by each sql command
+ $query_array = explode(";", $dump);
+
+ // Execute each sql command
+ foreach ($query_array as $query) {
+ if (!empty(trim($query))) {
+ $pdo->exec($query);
+ }
+ }
+
+ $this->upgrade($dump_version); // Runs all migrations between current and next version
+ } elseif ($dump_version == $version) {
+ // Split the dump into an array by each sql command
+ $query_array = explode(";", $dump);
+
+ // Execute each sql command
+ foreach ($query_array as $query) {
+ if (!empty(trim($query))) {
+ $pdo->exec($query);
+ }
+ }
+ } else {
+ throw new \Box_Exception("The version number of the sql dump is bigger than the current version number of the module. Please check the installed Module version.", null, 9684);
+ }
+ } catch (Exception $e) {
+ throw new \Box_Exception('Error during restoration process: ' . $e->getMessage());
+ }
+ }
+ } else {
+ // Get a list of all SQL migration files
+ $migrations = glob(__DIR__ . '/migrations/*.sql');
+
+ // Sort the array of migration files by their version numbers (which are in their file names)
+ usort($migrations, function ($a, $b) {
+ return version_compare(basename($a, '.sql'), basename($b, '.sql'));
+ });
+
+ try {
+ // Create a new PDO instance, connecting to your MySQL database
+ $pdo = new PDO(
+ 'mysql:host=' . $this->di['config']['db']['host'] . ';dbname=' . $this->di['config']['db']['name'],
+ $this->di['config']['db']['user'],
+ $this->di['config']['db']['password']
+ );
+
+ // Loop through each migration file
+ foreach ($migrations as $migration) {
+ // Extract the version number from the file name
+ $filename = basename($migration, '.sql');
+ $version = str_replace('_', '.', $filename);
+
+ // Log the execution of the current migration
+ error_log('Running migration ' . $version . ' from ' . $migration);
+
+ // Read the SQL statements from the file into a string
+ $sql = file_get_contents($migration);
+
+ // Split the string of SQL statements into an array
+ // This uses the ';' character to identify the end of each SQL statement
+ $statements = explode(';', $sql);
+
+ // Loop through each SQL statement
+ foreach ($statements as $statement) {
+ // If the statement is not empty or just whitespace
+ if (trim($statement)) {
+ // Execute the SQL statement
+ $pdo->exec($statement);
+ }
+ }
+ }
+ } catch (PDOException $e) {
+ // If any errors occur while connecting to the database or executing SQL, log the error message and terminate the script
+ error_log('PDO Exception: ' . $e->getMessage());
+ exit(1);
+ }
+
+
+ // Create table for vm
+ }
+ // add default values to module config table:
+ // cpu_overprovisioning, ram_overprovisioning, storage_overprovisioning, avoid_overprovision, no_overprovision, use_auth_tokens
+ // example: $extensionService->setConfig(['ext' => 'mod_massmailer', 'limit' => '2', 'interval' => '10', 'test_client_id' => 1]);
+ $extensionService = $this->di['mod_service']('extension');
+ $extensionService->setConfig(['ext' => 'mod_serviceproxmox', 'cpu_overprovisioning' => '1', 'ram_overprovisioning' => '1', 'storage_overprovisioning' => '1', 'avoid_overprovision' => '0', 'no_overprovision' => '1', 'use_auth_tokens' => '1']);
+
+
+ return true;
+ }
+
+
+ /**
+ * Method to uninstall module.
+ * Now creates a sql dump of the database tables and stores it in the pmxconfig folder
+ *
+ * @return bool
+ */
+ public function uninstall()
+ {
+ $this->pmxdbbackup('uninstall');
+ $this->di['db']->exec("DROP TABLE IF EXISTS `service_proxmox`");
+ $this->di['db']->exec("DROP TABLE IF EXISTS `service_proxmox_server`");
+ $this->di['db']->exec("DROP TABLE IF EXISTS `service_proxmox_users`");
+ $this->di['db']->exec("DROP TABLE IF EXISTS `service_proxmox_storage`");
+ $this->di['db']->exec("DROP TABLE IF EXISTS `service_proxmox_templates`");
+ $this->di['db']->exec("DROP TABLE IF EXISTS `service_proxmox_vms`");
+ $this->di['db']->exec("DROP TABLE IF EXISTS `service_proxmox_vm_config_template`");
+ $this->di['db']->exec("DROP TABLE IF EXISTS `service_proxmox_vm_storage_template`");
+ $this->di['db']->exec("DROP TABLE IF EXISTS `service_proxmox_vm_network_template`");
+ $this->di['db']->exec("DROP TABLE IF EXISTS `service_proxmox_lxc_appliance`");
+ $this->di['db']->exec("DROP TABLE IF EXISTS `service_proxmox_storageclass`");
+ $this->di['db']->exec("DROP TABLE IF EXISTS `service_proxmox_client_network`");
+ $this->di['db']->exec("DROP TABLE IF EXISTS `service_proxmox_ip_networks`");
+ $this->di['db']->exec("DROP TABLE IF EXISTS `service_proxmox_ip_range`");
+ $this->di['db']->exec("DROP TABLE IF EXISTS `service_proxmox_client_vlan`");
+ $this->di['db']->exec("DROP TABLE IF EXISTS `service_proxmox_lxc_config_template`");
+ $this->di['db']->exec("DROP TABLE IF EXISTS `service_proxmox_lxc_network_template`");
+ $this->di['db']->exec("DROP TABLE IF EXISTS `service_proxmox_lxc_storage_template`");
+ $this->di['db']->exec("DROP TABLE IF EXISTS `service_proxmox_qemu_template`");
+ $this->di['db']->exec("DROP TABLE IF EXISTS `service_proxmox_ipam_settings`");
+ $this->di['db']->exec("DROP TABLE IF EXISTS `service_proxmox_ipadress`");
+
+ return true;
+ }
+
+ /**
+ * Method to upgrade module.
+ *
+ * @param string $previous_version
+ * @return bool
+ */
+ public function upgrade($previous_version)
+ {
+ // read current module version from manifest.json
+ $manifest = json_decode(file_get_contents(__DIR__ . '/manifest.json'), true);
+ $current_version = $manifest['version'];
+
+ // read migrations directory and find all files between current version and previous version
+ $migrations = glob(__DIR__ . '/migrations/*.sql');
+
+ // sort migrations by version number (Filenames: 0.0.1.sql, 0.0.2.sql etc.)
+ usort($migrations, function ($a, $b) {
+ return version_compare(basename($a, '.sql'), basename($b, '.sql'));
+ });
+
+ // Get db config
+ $db_user = $this->di['config']['db']['user'];
+ $db_password = $this->di['config']['db']['password'];
+ $db_name = $this->di['config']['db']['name'];
+
+ // Create PDO instance
+ $pdo = new PDO('mysql:host=localhost;dbname=' . $db_name, $db_user, $db_password);
+ $pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
+
+ foreach ($migrations as $migration) {
+ // get version from filename
+ // log to debug.log
+ error_log('found migration: ' . $migration);
+ $filename = basename($migration, '.sql');
+ $version = str_replace('_', '.', $filename);
+
+ // check if version is between previous and current version
+ error_log('version: ' . $version . ' previous_version: ' . $previous_version . ' current_version: ' . $current_version);
+
+ // Apply migration if version is larger than previous version and smaller or equal to current version
+ error_log('version_compare: ' . version_compare($version, $previous_version, '>') . ' version_compare2: ' . version_compare($version, $current_version, '<='));
+
+ if (version_compare($version, $previous_version, '>') && version_compare($version, $current_version, '<=')) {
+ error_log('applying migration: ' . $migration);
+
+ // run migration
+ $migration_sql = file_get_contents($migration);
+ $pdo->exec($migration_sql);
+ }
+ }
+
+ return true;
+ }
+
+ /**
+ * Method to check if all tables have been migrated to current Module Version.
+ * Not yet used, but will be in the admin settings page for the module
+ *
+ * @param string $action
+ * @return bool
+ */
+ public function check_db_migration()
+ {
+ // read current module version from manifest.json
+ $manifest = json_decode(file_get_contents(__DIR__ . '/manifest.json'), true);
+ $current_version = $manifest['version'];
+ $tables = array(
+ 'service_proxmox_server',
+ 'service_proxmox',
+ 'service_proxmox_users',
+ 'service_proxmox_storageclass',
+ 'service_proxmox_storage',
+ 'service_proxmox_lxc_appliance',
+ 'service_proxmox_vm_config_template',
+ 'service_proxmox_vm_storage_template',
+ 'service_proxmox_vm_network_template',
+ 'service_proxmox_lxc_config_template',
+ 'service_proxmox_lxc_storage_template',
+ 'service_proxmox_lxc_network_template',
+ 'service_proxmox_qemu_template',
+ 'service_proxmox_client_vlan',
+ 'service_proxmox_ip_range'
+
+ );
+
+ foreach ($tables as $table) {
+ $sql = "SELECT table_comment FROM INFORMATION_SCHEMA.TABLES WHERE table_schema='" . DB_NAME . "' AND table_name='" . $table . "'";
+ $result = $this->di['db']->query($sql);
+ $row = $result->fetch();
+ // check if version is the same as current version
+ if ($row['table_comment'] != $current_version) {
+ // if not, throw error to inform user about inconsistent database status
+ throw new \Box_Exception('Database migration is not up to date. Please run the database migration script.');
+ }
+ }
+ return true;
+ }
+
+ /**
+ * Method to create configuration Backups of Proxmox tables
+ *
+ * @param string $data - 'uninstall' or 'backup'
+ * @return bool
+ */
+ public function pmxdbbackup($data)
+ {
+ // create backup of all Proxmox tables
+ try {
+ $filesystem = new Filesystem();
+ $filesystem->mkdir([PATH_ROOT . '/pmxconfig'], 0750);
+ } catch (IOException $e) {
+ error_log($e->getMessage());
+ throw new \Box_Exception('Unable to create directory pmxconfig');
+ }
+ // create filename with timestamp
+ // check if $data is 'uninstall' or 'backup'
+ if ($data == 'uninstall') {
+ $filename = '/pmxconfig/proxmox_uninstall_' . date('Y-m-d_H-i-s') . '.sql';
+ } else {
+ $filename = '/pmxconfig/proxmox_backup_' . date('Y-m-d_H-i-s') . '.sql';
+ }
+
+ // get db config
+ $db_user = $this->di['config']['db']['user'];
+ $db_password = $this->di['config']['db']['password'];
+ $db_name = $this->di['config']['db']['name'];
+
+ try {
+ // create PDO instance
+ $pdo = new PDO('mysql:host=localhost;dbname=' . $db_name, $db_user, $db_password);
+
+ // List of tables to backup
+ $tables = array(
+ 'service_proxmox_server',
+ 'service_proxmox',
+ 'service_proxmox_users',
+ 'service_proxmox_storageclass',
+ 'service_proxmox_storage',
+ 'service_proxmox_lxc_appliance',
+ 'service_proxmox_vm_config_template',
+ 'service_proxmox_vm_storage_template',
+ 'service_proxmox_vm_network_template',
+ 'service_proxmox_lxc_config_template',
+ 'service_proxmox_lxc_storage_template',
+ 'service_proxmox_lxc_network_template',
+ 'service_proxmox_qemu_template',
+ 'service_proxmox_client_vlan',
+ 'service_proxmox_ip_range',
+ 'service_proxmox_ipadress',/*
+ 'service_proxmox_ipam_settings' */
+ );
+
+ // Initialize backup variable
+ $backup = '';
+
+ // Loop through tables and create SQL statement
+ foreach ($tables as $table) {
+ $result = $pdo->query('SELECT * FROM ' . $table);
+ $num_fields = $result->columnCount();
+
+ $backup .= 'DROP TABLE IF EXISTS ' . $table . ';';
+ $row2 = $pdo->query('SHOW CREATE TABLE ' . $table)->fetch(PDO::FETCH_NUM);
+ $backup .= "\n\n" . $row2[1] . ";\n\n";
+
+ while ($row = $result->fetch(PDO::FETCH_NUM)) {
+ $backup .= 'INSERT INTO ' . $table . ' VALUES(';
+ for ($j = 0; $j < $num_fields; $j++) {
+ $row[$j] = addslashes($row[$j]);
+ $row[$j] = preg_replace("/\n/", "\\n", $row[$j]);
+ if (isset($row[$j])) {
+ $backup .= '"' . $row[$j] . '"';
+ } else {
+ $backup .= '""';
+ }
+ if ($j < ($num_fields - 1)) {
+ $backup .= ',';
+ }
+ }
+ $backup .= ");\n";
+ }
+ $backup .= "\n\n\n";
+ }
+
+ // Save to file
+ $handle = fopen(PATH_ROOT . $filename, 'w+');
+ fwrite($handle, $backup);
+ fclose($handle);
+ // create PDO instance
+ $pdo = new PDO('mysql:host=localhost;dbname=' . $db_name, $db_user, $db_password);
+
+ // List of tables to backup
+ $tables = array(
+ 'service_proxmox_server',
+ 'service_proxmox',
+ 'service_proxmox_users',
+ 'service_proxmox_storageclass',
+ 'service_proxmox_storage',
+ 'service_proxmox_lxc_appliance',
+ 'service_proxmox_vm_config_template',
+ 'service_proxmox_vm_storage_template',
+ 'service_proxmox_vm_network_template',
+ 'service_proxmox_lxc_config_template',
+ 'service_proxmox_lxc_storage_template',
+ 'service_proxmox_lxc_network_template',
+ 'service_proxmox_qemu_template',
+ 'service_proxmox_client_vlan',
+ 'service_proxmox_ip_range'
+ );
+
+ // Initialize backup variable
+ $backup = '';
+
+ // Loop through tables and create SQL statement
+ foreach ($tables as $table) {
+ $result = $pdo->query('SELECT * FROM ' . $table);
+ $num_fields = $result->columnCount();
+
+ $backup .= 'DROP TABLE IF EXISTS ' . $table . ';';
+ $row2 = $pdo->query('SHOW CREATE TABLE ' . $table)->fetch(PDO::FETCH_NUM);
+ $backup .= "\n\n" . $row2[1] . ";\n\n";
+
+ while ($row = $result->fetch(PDO::FETCH_NUM)) {
+ $backup .= 'INSERT INTO ' . $table . ' VALUES(';
+ for ($j = 0; $j < $num_fields; $j++) {
+ $row[$j] = addslashes($row[$j]);
+ $row[$j] = preg_replace("/\n/", "\\n", $row[$j]);
+ if (isset($row[$j])) {
+ $backup .= '"' . $row[$j] . '"';
+ } else {
+ $backup .= '""';
+ }
+ if ($j < ($num_fields - 1)) {
+ $backup .= ',';
+ }
+ }
+ $backup .= ");\n";
+ }
+ $backup .= "\n\n\n";
+ }
+
+ // Save to file
+ $handle = fopen(PATH_ROOT . $filename, 'w+');
+ fwrite($handle, $backup);
+ fclose($handle);
+ } catch (Exception $e) {
+ throw new \Box_Exception('Error during backup process: ' . $e->getMessage());
+ }
+
+ // read current module version from manifest.json
+ $manifest = json_decode(file_get_contents(__DIR__ . '/manifest.json'), true);
+ $current_version = $manifest['version'];
+ // add version comment to backup file
+ $version_comment = '-- Proxmox module version: ' . $current_version . "\n";
+ $filename = PATH_ROOT . $filename;
+ $handle = fopen($filename, 'r+');
+ $len = strlen($version_comment);
+ $final_len = filesize($filename) + $len;
+ $cache_old = fread($handle, $len);
+ rewind($handle);
+ $i = 1;
+ while (ftell($handle) < $final_len) {
+ fwrite($handle, $version_comment);
+ $version_comment = $cache_old;
+ $cache_old = fread($handle, $len);
+ fseek($handle, $i * $len);
+ $i++;
+ }
+ fclose($handle);
+
+ return true;
+ }
+
+
+ /**
+ * Method to list all Proxmox backups
+ *
+ * @return array
+ */
+ public function pmxbackuplist()
+ {
+ $files = glob(PATH_ROOT . '/pmxconfig/*.sql');
+ $backups = array();
+ foreach ($files as $file) {
+ $backups[] = basename($file);
+ }
+ return $backups;
+ }
+
+ /**
+ * Method to restore Proxmox tables from backup
+ * It's a bit destructive, as it will drop & overwrite all existing tables
+ *
+ * @param string $data - filename of backup
+ * @return bool
+ */
+ public function pmxbackuprestore($data)
+ {
+ // get filename from $data and see if it exists using finder
+ $manifest = json_decode(file_get_contents(__DIR__ . '/manifest.json'), true);
+ $version = $manifest['version'];
+ //if the file exists, restore it
+ $dump = file_get_contents(PATH_ROOT . '/pmxconfig/' . $data['backup']);
+ // check if dump is not empty
+ if (!empty($dump)) {
+ // check version number in first line of dump format:
+ // -- Proxmox module version: 0.0.5
+ // get first line of dump
+ $version_line = strtok($dump, "\n");
+ // get version number from line
+ $dump_version = str_replace('-- Proxmox module version: ', '', $version_line);
+ $dump = str_replace($version_line . "\n", '', $dump);
+
+ // if version number in dump is smaller than current version number, restore dump and run upgrade function
+ if ($dump_version == $version) {
+ // get db config
+ $db_user = $this->di['config']['db']['user'];
+ $db_password = $this->di['config']['db']['password'];
+ $db_name = $this->di['config']['db']['name'];
+
+ try {
+ // create PDO instance
+ $pdo = new PDO('mysql:host=localhost;dbname=' . $db_name, $db_user, $db_password);
+ $pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
+
+ // split the dump into an array by each sql command
+ $query_array = explode(";", $dump);
+
+ // execute each sql command
+ foreach ($query_array as $query) {
+ if (!empty(trim($query))) {
+ $pdo->exec($query);
+ }
+ }
+
+ return true;
+ } catch (Exception $e) {
+ throw new \Box_Exception('Error during restoration process: ' . $e->getMessage());
+ }
+ } else {
+ throw new \Box_Exception("The sql dump file (V: $dump_version) is not compatible with the current module version (V: $version). Please check the file.", null);
+ }
+ } else {
+ throw new \Box_Exception("The sql dump file is empty. Please check the file.", null);
+ }
+ }
+
+ // Create function that runs with cron job hook
+ // This function will run every 5 minutes and update all servers
+ // Disabled for now
+ /*
+ public static function onBeforeAdminCronRun(\Box_Event $event)
+ {
+ // getting Dependency Injector
+ $di = $event->getDi();
+
+ // @note almost in all cases you will need Admin API
+ $api = $di['api_admin'];
+ // get all servers from database
+ // like this $vms = $this->di['db']->findAll('service_proxmox', 'server_id = :server_id', array(':server_id' => $data['id']));
+ $servers = $di['db']->findAll('service_proxmox_server');
+ // rum getHardwareData, getStorageData and getAssignedResources for each server
+ foreach ($servers as $server) {
+ $hardwareData = $api->getHardwareData($server['id']);
+ $storageData = $api->getStorageData($server['id']);
+ $assignedResources = $api->getAssignedResources($server['id']);
+ }
+ } */
+
+
+ /* ################################################################################################### */
+ /* ########################################### Orders ############################################## */
+ /* ################################################################################################### */
+
+ /**
+ * @param \Model_ClientOrder $order
+ * @return void
+ */
+ public function create($order)
+ {
+ $config = json_decode($order->config, 1);
+
+ $product = $this->di['db']->getExistingModelById('Product', $order->product_id, 'Product not found');
+
+ $model = $this->di['db']->dispense('service_proxmox');
+ $model->client_id = $order->client_id;
+ $model->order_id = $order->id;
+ $model->created_at = date('Y-m-d H:i:s');
+ $model->updated_at = date('Y-m-d H:i:s');
+
+ // Find suitable server and save it to service_proxmox
+ $model->server_id = $this->find_empty($product);
+ // Retrieve server info
+
+ $this->di['db']->store($model);
+
+ return $model;
+ }
+
+ /**
+ * @param \Model_ClientOrder $order
+ * @return boolean
+ */
+ public function activate($order, $model)
+ {
+ if (!is_object($model)) {
+ throw new \Box_Exception('Could not activate order. Service was not created');
+ }
+ $config = json_decode($order->config, 1);
+
+ $client = $this->di['db']->load('client', $order->client_id);
+ $product = $this->di['db']->load('product', $order->product_id);
+ if (!$product) {
+ throw new \Box_Exception('Could not activate order because ordered product does not exists');
+ }
+ $product_config = json_decode($product->config, 1);
+
+ // Allocate to an appropriate server id
+ $server = $this->di['db']->load('service_proxmox_server', $model->server_id);
+
+ // Retrieve or create client unser account in service_proxmox_users
+ $clientuser = $this->di['db']->findOne('service_proxmox_users', 'server_id = ? and client_id = ?', array($server->id, $client->id));
+ if (!$clientuser) {
+ $this->create_client_user($server, $client);
+ }
+
+ // Connect to Proxmox API
+ $serveraccess = $this->find_access($server);
+ // find client permissions for server
+ $clientuser = $this->di['db']->findOne('service_proxmox_users', 'server_id = ? and client_id = ?', array($server->id, $client->id));
+ $config = $this->di['mod_config']('Serviceproxmox');
+ $proxmox = new PVE2_API($serveraccess, $server->root_user, $server->realm, $server->root_password, port: $server->port, tokenid: $clientuser->admin_tokenname, tokensecret: $clientuser->admin_tokenvalue,debug: $config['pmx_debug_logging']);
+
+ // Create Proxmox VM
+ if ($proxmox->login()) {
+ // compile VMID by combining the server id, the client id, and the order id separated by padded zeroes for thee numbers per variable
+ $vmid = $server->id . str_pad($client->id, 3, '0', STR_PAD_LEFT) . str_pad($order->id, 3, '0', STR_PAD_LEFT);
+
+ // check if vmid is already in use
+ $vmid_in_use = $proxmox->get("/nodes/" . $server->node . "/qemu/" . $vmid);
+ if ($vmid_in_use) {
+ $vmid = $vmid . '1';
+ }
+
+ $proxmoxuser_password = $this->di['tools']->generatePassword(16, 4); // Generate password
+
+ // Create VM
+ $clone = '';
+ $container_settings = array();
+ $description = 'Service package ' . $model->id . ' belonging to client id: ' . $client->id;
+
+ if ($product_config['clone'] == true) {
+ $clone = '/' . $product_config['cloneid'] . '/clone'; // Define the route for cloning
+ $container_settings = array(
+ 'newid' => $vmid,
+ 'name' => $model->username,
+ 'description' => $description,
+ 'full' => true
+ );
+ } else { // TODO: Implement Container templates
+ if ($product_config['virt'] == 'qemu') {
+ $container_settings = array(
+ 'vmid' => $vmid,
+ 'name' => 'vm' . $vmid, // Hostname to define
+ 'node' => $server->name, // Node to create the VM on
+ 'description' => $description,
+ 'storage' => $product_config['storage'],
+ 'memory' => $product_config['memory'],
+ 'scsihw' => 'virtio-scsi-single',
+ 'scsi0' => "Storage01:10",
+ 'ostype' => "other",
+ 'kvm' => "0",
+ 'ide2' => $product_config['cdrom'] . ',media=cdrom',
+ 'sockets' => $product_config['cpu'],
+ 'cores' => $product_config['cpu'],
+ 'numa' => "0",
+ 'pool' => 'fb_client_' . $client->id,
+ );
+ } else {
+ $container_settings = array(
+ 'vmid' => $vmid,
+ 'hostname' => 'vm' . $vmid, // Hostname to define
+ 'description' => $description,
+ 'storage' => $product_config['storage'],
+ 'memory' => $product_config['memory'],
+ 'ostemplate' => $product_config['ostemplate'],
+ 'password' => $proxmoxuser_password,
+ 'net0' => $product_config['network']
+ );
+ // Storage to do for LXC
+ }
+ }
+
+ // If the VM is properly created
+ $vmurl = "/nodes/" . $server->name . "/" . $product_config['virt'] . $clone;
+
+ $vmcreate = $proxmox->post($vmurl, $container_settings);
+ //echo "Debug:\n " . var_dump($vmcreate) . "\n \n";
+ if ($vmcreate) {
+
+ // Start the vm
+ sleep(20);
+ $proxmox->post("/nodes/" . $server->name . "/" . $product_config['virt'] . "/" . $vmid . "/status/start", array());
+ $status = $proxmox->get("/nodes/" . $server->name . "/" . $product_config['virt'] . "/" . $vmid . "/status/current");
+
+ if (!empty($status)) {
+ sleep(10);
+ // Wait until it has been started
+ while ($status['status'] != 'running') {
+ $proxmox->post("/nodes/" . $server->name . "/" . $product_config['virt'] . "/" . $vmid . "/status/start", array());
+ sleep(10);
+ $status = $proxmox->get("/nodes/" . $server->name . "/" . $product_config['virt'] . "/" . $vmid . "/status/current");
+ // TODO: Check Startup
+ }
+ } else {
+ throw new \Box_Exception("VMID cannot be found");
+ }
+ } else {
+ throw new \Box_Exception("VPS has not been created");
+ }
+ } else {
+ throw new \Box_Exception('Login to Proxmox Host failed with client credentials', null, 7457);
+ }
+
+ // Retrieve VM IP
+
+ $model->server_id = $model->server_id;
+ $model->updated_at = date('Y-m-d H:i:s');
+ $model->vmid = $vmid;
+ $model->password = $proxmoxuser_password;
+ //$model->ipv4 = $ipv4; // TODO: Retrieve IP address of the VM from the PMX IPAM module
+ //$model->ipv6 = $ipv6; // TODO: Retrieve IP address of the VM from the PMX IPAM module
+ //$model->hostname = $hostname; // TODO: Retrieve hostname from the Order form
+ $this->di['db']->store($model);
+
+ return array(
+ 'ip' => 'to be sent by us shortly', // $model->ipv4 - Return IP address of the VM
+ 'username' => 'root',
+ 'password' => 'See Admin Area', // Password won't be sen't by E-Mail. It will be stored in the database and can be retrieved from the client area
+ );
+ }
+
+ /**
+ * Get the API array representation of a model
+ * Important to interact with the Order
+ * @param object $model
+ * @return array
+ */
+ public function toApiArray($model)
+ {
+ // Retrieve associated server
+ $server = $this->di['db']->findOne('service_proxmox_server', 'id=:id', array(':id' => $model->server_id));
+
+ return array(
+ 'id' => $model->id,
+ 'client_id' => $model->client_id,
+ 'server_id' => $model->server_id,
+ 'username' => $model->username,
+ 'mailbox_quota' => $model->mailbox_quota,
+ 'server' => $server,
+ );
+ }
+
+ // function to get novnc_appjs file
+ public function get_novnc_appjs($data)
+ {
+ // get list of servers
+
+ $servers = $this->di['db']->find('service_proxmox_server');
+ // select first server
+ $server = $servers['2'];
+
+ $hostname = $server->hostname;
+ // build url
+
+ $url = "https://$hostname:8006/novnc/" . $data; //$data['ver'];
+ // get file using symphony http client
+ // set options
+ $client = $this->getHttpClient()->withOptions([
+ 'verify_peer' => false,
+ 'verify_host' => false,
+ 'timeout' => 60,
+ ]);
+ $result = $client->request('GET', $url);
+ //echo "";
+ // return file
+ return $result;
+ }
+
+ public function getHttpClient()
+ {
+ return \Symfony\Component\HttpClient\HttpClient::create();
+ }
+
+ // function to get tags for type
+ public function get_tags($data)
+ {
+ // get list of tags for input type
+ $tags = $this->di['db']->find('service_proxmox_tag', 'type=:type', array(':type' => $data['type']));
+ // return tags
+ return $tags;
+ }
+
+ // function to save tags for type
+ public function save_tag($data)
+ {
+ // $data contains 'type' and 'tag'
+
+ // search if the tag already exists
+ error_log('saving tag: ' . print_r($data));
+ $tag_exists = $this->di['db']->findOne('service_proxmox_tag', 'type=:type AND name=:name', array(':type' => $data['type'], ':name' => $data['tag']));
+ // and if not create it
+ if (!$tag_exists) {
+ $model = $this->di['db']->dispense('service_proxmox_tag');
+ $model->type = $data['type'];
+ $model->name = $data['tag'];
+ $this->di['db']->store($model);
+ return $model;
+ }
+ // return the tag that was just created
+ return $tag_exists;
+ }
+
+
+ // Function to return tags for storage (stored in service_proxmox_storage->storageclass) ($data contains storageid)
+ public function get_tags_by_storage($data)
+ {
+ // get storageclass for storage
+ // log to debug.log
+ error_log('get_tags_by_storage: ' . $data['storageid']);
+ $storage = $this->di['db']->findOne('service_proxmox_storage', 'id=:id', array(':id' => $data['storageid']));
+ // return tags (saved in json format in $storage->storageclass) (F.ex ["ssd","hdd"])
+ // as well as the service_proxmox_tag id for each tag so there is a key value pair with id and name.
+
+ $tags = json_decode($storage->storageclass, true);
+ return $tags;
+
+
+ }
+
+
+
+}
diff --git a/src/assets/css/proxmox.css b/src/assets/css/proxmox.css
new file mode 100644
index 0000000..5345e4c
--- /dev/null
+++ b/src/assets/css/proxmox.css
@@ -0,0 +1,63 @@
+/* The switch - the box around the slider */
+.switch {
+ position: relative;
+ display: inline-block;
+ width: 60px;
+ height: 34px;
+ }
+
+ /* Hide default HTML checkbox */
+ .switch input {
+ opacity: 0;
+ width: 0;
+ height: 0;
+ }
+
+ /* The slider */
+ .slider {
+ position: absolute;
+ cursor: pointer;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ background-color: #ccc;
+ -webkit-transition: .4s;
+ transition: .4s;
+ }
+
+ .slider:before {
+ position: absolute;
+ content: "";
+ height: 26px;
+ width: 26px;
+ left: 4px;
+ bottom: 4px;
+ background-color: white;
+ -webkit-transition: .4s;
+ transition: .4s;
+ }
+
+ input:checked + .slider {
+ background-color: #2196F3;
+ }
+
+ input:focus + .slider {
+ box-shadow: 0 0 1px #2196F3;
+ }
+
+ input:checked + .slider:before {
+ -webkit-transform: translateX(26px);
+ -ms-transform: translateX(26px);
+ transform: translateX(26px);
+ }
+
+ /* Rounded sliders */
+ .slider.round {
+ border-radius: 34px;
+ }
+
+ .slider.round:before {
+ border-radius: 50%;
+ }
+
\ No newline at end of file
diff --git a/src/assets/vnc.html b/src/assets/vnc.html
new file mode 100644
index 0000000..a8cb768
--- /dev/null
+++ b/src/assets/vnc.html
@@ -0,0 +1,364 @@
+
+
+
+ Edit clipboard content in the textarea below. +
+ +noVNC
++ {{ 'ID' | trans }} + | ++ {{ 'Name' | trans }} + | ++ {{ 'Hostname' | trans }} + | ++ {{ 'IPv4' | trans }} + | ++ {{ 'VMs' | trans }} + | ++ {{ 'CPU' | trans }} + | ++ {{ 'RAM' | trans }} + | ++ {{ 'Active' | trans }} + | ++ | |
---|---|---|---|---|---|---|---|---|---|
+ + {{ server.id }} + + | ++ {{ server.name }} + | ++ + {{ server.hostname }}:{{server.port}} + + | ++ {{ server.ipv4 }} + | ++ {{ server.vm_count }} + | +
+ {{ server.cpu_cores_allocated }} / {{ server.cpu_cores_overprovision }} + (Phy: {{ server.cpu_cores }}) + |
+
+ {{ server.ram_used }} / {{ server.ram_overprovision }} GB + (Phy: {{ server.ram }} GB) + +
+
+
+
+ |
+ {% if server.active == 1 %}
+ + + | + {% else %} ++ + | + {% endif %} ++ + + + + + + + + + + + + | +
+ {{ 'The list is empty' | trans }} + | +
+ {{ 'Storage' | trans }} + | ++ {{ 'Servername' | trans }} + | ++ {{ 'Content' | trans }} + | ++ {{ 'Storage Space' | trans }} + | ++ {{ 'Active' | trans }} + | ++ | +
---|---|---|---|---|---|
+ {{ storage.name }} + | ++ {{ storage.servername }} + | +
+ {% set content = storage.content|split(',') %}
+ {% for line in content %}
+ {{ line }} + {% endfor %} + |
+
+ {{ storage.used }} / {{ storage.size }} GB
+
+
+
+ |
+ + {% if storage.active == 1 %} + + {% else %} + + {% endif %} + | ++ + + + | +
+ {{ 'Server' | trans }} + | ++ {{ 'VM ID' | trans }} + | ++ {{ 'Host Name' | trans }} + | ++ {{ 'CPU Cores' | trans }} + | ++ {{ 'RAM' | trans }} + | ++ {{ 'Status' | trans }} + | ++ {{ 'Created At' | trans }} + | ++ |
---|---|---|---|---|---|---|---|
+ {{ service.server_id }} + | ++ {{ service.id}} + | ++ {{ service.hostname }} + | ++ {{ service.cores }} + | ++ {{ service.ram }} MB + | ++ {% if service.status == 'running' %} + + {% else %} + + {% endif %} + | ++ {{ service.created_at }} + | ++ + + + | +
+ {{ 'CIDR' | trans }} + | ++ {{ 'Gateway' | trans }} + | ++ {{ 'Broadcast' | trans }} + | ++ {{ 'Type' | trans }} + | ++ |
---|---|---|---|---|
+ {{ iprange.cidr }} + | ++ {{ iprange.gateway }} + | ++ {{ iprange.broadcast }} + | ++ {{ iprange.type }} + | ++ + + + | +
+ {{ 'IP' | trans }} + | ++ {{ 'IP Range' | trans }} + | ++ {{ 'Dedicated' | trans }} + | ++ {{ 'Gateway' | trans }} + | ++ {{ 'VLAN' | trans }} + | ++ |
---|---|---|---|---|---|
+ {{ ipadress.ip }} + | ++ // display the ip_range cidr for ipaddress.ip_range_id found in the ip_ranges variable + {% for ip_range in ip_ranges %} + {% if ip_range.id == ipadress.ip_range_id %} + {{ ip_range.cidr }} + {% endif %} + {% endfor %} + | ++ {{ ipadress.dedicated }} + | ++ {{ ipadress.gateway }} + | ++ {{ ipadress.vlan }} + | ++ + + + | +
+ {{ 'Client' | trans }} + | ++ {{ 'VLAN ID' | trans }} + | ++ {{ 'IP Range' | trans }} + | ++ |
---|---|---|---|
+ {{ client_vlan.client_name }} + | ++ {{ client_vlan.vlan }} + | ++ {{ client_vlan.ip_range }} + | ++ + + + + + + | +
+ {{ 'Status' | trans }}: + | ++ {{ mf.status_name(order.status) }} + | +
+ {{ 'Server Name' | trans }}: + | ++ + {{ server.name }} + + | +
+ {{ 'Server Group' | trans }}: + | ++ + {{ server.group }} + + | +
+ {{ 'Server IPv4' | trans }}: + | ++ {{ server.ipv4 }} + | +
+ {{ 'Server IPv6' | trans }}: + | ++ {{ server.ipv6 }} + | +
+ {{ 'Server Hostname' | trans }}: + | ++ {{ server.hostname }} + | +
+ {{ 'Server root user' | trans }}: + | ++ {{ server.root_user }} + | +
+ {{ 'Server root password' | trans }}: + | ++ {{ server.root_password }} + | +
+
+ {{ order_actions }}
+
+
+
+ {{ 'Jump to control panel'|trans }}
+
+
+
+
+
+
+ {{ 'Sync with server'|trans }}
+
+
+
+ |
+
+ {{ 'Storage' | trans }} + | ++ {{ 'Servername' | trans }} + | ++ {{ 'Content' | trans }} + | ++ {{ 'Storage Space' | trans }} + | ++ {{ 'Active' | trans }} + | ++ | +
---|---|---|---|---|---|
+ {{ storage.name }} + | ++ {{ storage.servername }} + | +
+ {% set content = storage.content|split(',') %}
+ {% for line in content %}
+ {{ line }} + {% endfor %} + |
+
+ {{ storage.used }} / {{ storage.size }} GB
+
+
+
+ |
+ + {% if storage.active == 1 %} + + {% else %} + + {% endif %} + | ++ + + + | +
{{"Storage Name" | trans}}: | +{{ storage.storage }} | +
---|---|
{{"Server" | trans}}: | +{{ storage.server_name }} | +
{{"Storage Type" | trans}}: | +{{ storage.type }} | +
{{"Content" | trans}}: | +{{ storage.content }} | +
{{"Usage" | trans}}: | +{{ storage.used }} / {{ storage.size }} GB | +
+ {{ 'Name' | trans }} + | ++ {{ 'Description' | trans }} + | ++ {{ 'CPU Cores' | trans }} + | ++ {{ 'Memory' | trans }} + | ++ {{ 'Storage' | trans }} + | ++ {{ 'Created at' | trans }} + | ++ |
---|---|---|---|---|---|---|
+ {{ template.name }} - + + {% if template.state == 'draft' %} + {% set statusClass = 'badge bg-secondary' %} + {% elseif template.state == 'active' %} + {% set statusClass = 'badge bg-success' %} + {% elseif template.state == 'inactive' %} + {% set statusClass = 'badge bg-danger' %} + {% endif %} + + + {{ template.state|capitalize }} + + | ++ {{ template.description }} + | ++ {{ template.cores }} + | ++ {{ template.memory }} GB + | +
+ {# {% set content = template.storage|split('\n') %}
+ {% for line in content %}
+ {{ line }}
+ + {% endfor %} #} + |
+ + {{ template.created_at }} + | + ++ + + + | +
+ {{ 'id' | trans }} + | ++ {{ 'Template' | trans }} + | ++ {{ 'CPU Cores' | trans }} + | ++ {{ 'Memory' | trans }} + | ++ {{ 'Storage' | trans }} + | ++ {{ 'Created at' | trans }} + | ++ |
---|---|---|---|---|---|---|
+ {{ template.id }} + | ++ {{ template.ostemplate }} + | ++ {{ template.cores }} + | ++ {{ template.memory }} + | ++ {{ template.storage }} + | ++ {{ template.created_at }} + | ++ + + + | +
+ {{ 'VM ID' | trans }} + | ++ {{ 'Server' | trans }} + | ++ {{ 'VM Name' | trans }} + | ++ |
---|---|---|---|
+ {{ qemu_template.vmid }} + | ++ {{ qemu_template.server_name }} + | ++ {{ qemu_template.name }} + | ++ + + + + | +
+ {{ 'Type' | trans }} + | ++ {{ 'Description' | trans }} + | ++ {{ 'Architecture' | trans }} + | ++ {{ 'Version' | trans }} + | ++ |
---|---|---|---|---|
+ {{ lxc_appliance.type }} + | ++ {{ lxc_appliance.description }} + | ++ {{ lxc_appliance.architecture }} + | ++ {{ lxc_appliance.version }} + | ++ + + + + | +
+ | Storage Type | +Size (GB) | +Controller | ++ + + + + |
---|
+ Status: + + {{ server.status }} + +
+ ++ */ + function trigger_deprecation(string $package, string $version, string $message, mixed ...$args): void + { + @trigger_error(($package || $version ? "Since $package $version: " : '').($args ? vsprintf($message, $args) : $message), \E_USER_DEPRECATED); + } +} diff --git a/src/vendor/symfony/http-client-contracts/.gitignore b/src/vendor/symfony/http-client-contracts/.gitignore new file mode 100644 index 0000000..c49a5d8 --- /dev/null +++ b/src/vendor/symfony/http-client-contracts/.gitignore @@ -0,0 +1,3 @@ +vendor/ +composer.lock +phpunit.xml diff --git a/src/vendor/symfony/http-client-contracts/CHANGELOG.md b/src/vendor/symfony/http-client-contracts/CHANGELOG.md new file mode 100644 index 0000000..7932e26 --- /dev/null +++ b/src/vendor/symfony/http-client-contracts/CHANGELOG.md @@ -0,0 +1,5 @@ +CHANGELOG +========= + +The changelog is maintained for all Symfony contracts at the following URL: +https://github.com/symfony/contracts/blob/main/CHANGELOG.md diff --git a/src/vendor/symfony/http-client-contracts/ChunkInterface.php b/src/vendor/symfony/http-client-contracts/ChunkInterface.php new file mode 100644 index 0000000..0800cb3 --- /dev/null +++ b/src/vendor/symfony/http-client-contracts/ChunkInterface.php @@ -0,0 +1,71 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Contracts\HttpClient; + +use Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface; + +/** + * The interface of chunks returned by ResponseStreamInterface::current(). + * + * When the chunk is first, last or timeout, the content MUST be empty. + * When an unchecked timeout or a network error occurs, a TransportExceptionInterface + * MUST be thrown by the destructor unless one was already thrown by another method. + * + * @author Nicolas Grekas
+ */ +interface ChunkInterface +{ + /** + * Tells when the idle timeout has been reached. + * + * @throws TransportExceptionInterface on a network error + */ + public function isTimeout(): bool; + + /** + * Tells when headers just arrived. + * + * @throws TransportExceptionInterface on a network error or when the idle timeout is reached + */ + public function isFirst(): bool; + + /** + * Tells when the body just completed. + * + * @throws TransportExceptionInterface on a network error or when the idle timeout is reached + */ + public function isLast(): bool; + + /** + * Returns a [status code, headers] tuple when a 1xx status code was just received. + * + * @throws TransportExceptionInterface on a network error or when the idle timeout is reached + */ + public function getInformationalStatus(): ?array; + + /** + * Returns the content of the response chunk. + * + * @throws TransportExceptionInterface on a network error or when the idle timeout is reached + */ + public function getContent(): string; + + /** + * Returns the offset of the chunk in the response body. + */ + public function getOffset(): int; + + /** + * In case of error, returns the message that describes it. + */ + public function getError(): ?string; +} diff --git a/src/vendor/symfony/http-client-contracts/Exception/ClientExceptionInterface.php b/src/vendor/symfony/http-client-contracts/Exception/ClientExceptionInterface.php new file mode 100644 index 0000000..22d2b45 --- /dev/null +++ b/src/vendor/symfony/http-client-contracts/Exception/ClientExceptionInterface.php @@ -0,0 +1,21 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Contracts\HttpClient\Exception; + +/** + * When a 4xx response is returned. + * + * @author Nicolas Grekas
+ */ +interface ClientExceptionInterface extends HttpExceptionInterface +{ +} diff --git a/src/vendor/symfony/http-client-contracts/Exception/DecodingExceptionInterface.php b/src/vendor/symfony/http-client-contracts/Exception/DecodingExceptionInterface.php new file mode 100644 index 0000000..971a7a2 --- /dev/null +++ b/src/vendor/symfony/http-client-contracts/Exception/DecodingExceptionInterface.php @@ -0,0 +1,21 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Contracts\HttpClient\Exception; + +/** + * When a content-type cannot be decoded to the expected representation. + * + * @author Nicolas Grekas
+ */ +interface DecodingExceptionInterface extends ExceptionInterface +{ +} diff --git a/src/vendor/symfony/http-client-contracts/Exception/ExceptionInterface.php b/src/vendor/symfony/http-client-contracts/Exception/ExceptionInterface.php new file mode 100644 index 0000000..e553b47 --- /dev/null +++ b/src/vendor/symfony/http-client-contracts/Exception/ExceptionInterface.php @@ -0,0 +1,21 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Contracts\HttpClient\Exception; + +/** + * The base interface for all exceptions in the contract. + * + * @author Nicolas Grekas
+ */
+interface ExceptionInterface extends \Throwable
+{
+}
diff --git a/src/vendor/symfony/http-client-contracts/Exception/HttpExceptionInterface.php b/src/vendor/symfony/http-client-contracts/Exception/HttpExceptionInterface.php
new file mode 100644
index 0000000..17865ed
--- /dev/null
+++ b/src/vendor/symfony/http-client-contracts/Exception/HttpExceptionInterface.php
@@ -0,0 +1,24 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Contracts\HttpClient\Exception;
+
+use Symfony\Contracts\HttpClient\ResponseInterface;
+
+/**
+ * Base interface for HTTP-related exceptions.
+ *
+ * @author Anton Chernikov
+ */
+interface RedirectionExceptionInterface extends HttpExceptionInterface
+{
+}
diff --git a/src/vendor/symfony/http-client-contracts/Exception/ServerExceptionInterface.php b/src/vendor/symfony/http-client-contracts/Exception/ServerExceptionInterface.php
new file mode 100644
index 0000000..9bfe135
--- /dev/null
+++ b/src/vendor/symfony/http-client-contracts/Exception/ServerExceptionInterface.php
@@ -0,0 +1,21 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Contracts\HttpClient\Exception;
+
+/**
+ * When a 5xx response is returned.
+ *
+ * @author Nicolas Grekas
+ */
+interface ServerExceptionInterface extends HttpExceptionInterface
+{
+}
diff --git a/src/vendor/symfony/http-client-contracts/Exception/TimeoutExceptionInterface.php b/src/vendor/symfony/http-client-contracts/Exception/TimeoutExceptionInterface.php
new file mode 100644
index 0000000..08acf9f
--- /dev/null
+++ b/src/vendor/symfony/http-client-contracts/Exception/TimeoutExceptionInterface.php
@@ -0,0 +1,21 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Contracts\HttpClient\Exception;
+
+/**
+ * When an idle timeout occurs.
+ *
+ * @author Nicolas Grekas
+ */
+interface TimeoutExceptionInterface extends TransportExceptionInterface
+{
+}
diff --git a/src/vendor/symfony/http-client-contracts/Exception/TransportExceptionInterface.php b/src/vendor/symfony/http-client-contracts/Exception/TransportExceptionInterface.php
new file mode 100644
index 0000000..0c8d131
--- /dev/null
+++ b/src/vendor/symfony/http-client-contracts/Exception/TransportExceptionInterface.php
@@ -0,0 +1,21 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Contracts\HttpClient\Exception;
+
+/**
+ * When any error happens at the transport level.
+ *
+ * @author Nicolas Grekas
+ */
+interface TransportExceptionInterface extends ExceptionInterface
+{
+}
diff --git a/src/vendor/symfony/http-client-contracts/HttpClientInterface.php b/src/vendor/symfony/http-client-contracts/HttpClientInterface.php
new file mode 100644
index 0000000..158c1a7
--- /dev/null
+++ b/src/vendor/symfony/http-client-contracts/HttpClientInterface.php
@@ -0,0 +1,95 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Contracts\HttpClient;
+
+use Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface;
+use Symfony\Contracts\HttpClient\Test\HttpClientTestCase;
+
+/**
+ * Provides flexible methods for requesting HTTP resources synchronously or asynchronously.
+ *
+ * @see HttpClientTestCase for a reference test suite
+ *
+ * @method static withOptions(array $options) Returns a new instance of the client with new default options
+ *
+ * @author Nicolas Grekas
+ */
+interface HttpClientInterface
+{
+ public const OPTIONS_DEFAULTS = [
+ 'auth_basic' => null, // array|string - an array containing the username as first value, and optionally the
+ // password as the second one; or string like username:password - enabling HTTP Basic
+ // authentication (RFC 7617)
+ 'auth_bearer' => null, // string - a token enabling HTTP Bearer authorization (RFC 6750)
+ 'query' => [], // string[] - associative array of query string values to merge with the request's URL
+ 'headers' => [], // iterable|string[]|string[][] - headers names provided as keys or as part of values
+ 'body' => '', // array|string|resource|\Traversable|\Closure - the callback SHOULD yield a string
+ // smaller than the amount requested as argument; the empty string signals EOF; if
+ // an array is passed, it is meant as a form payload of field names and values
+ 'json' => null, // mixed - if set, implementations MUST set the "body" option to the JSON-encoded
+ // value and set the "content-type" header to a JSON-compatible value if it is not
+ // explicitly defined in the headers option - typically "application/json"
+ 'user_data' => null, // mixed - any extra data to attach to the request (scalar, callable, object...) that
+ // MUST be available via $response->getInfo('user_data') - not used internally
+ 'max_redirects' => 20, // int - the maximum number of redirects to follow; a value lower than or equal to 0
+ // means redirects should not be followed; "Authorization" and "Cookie" headers MUST
+ // NOT follow except for the initial host name
+ 'http_version' => null, // string - defaults to the best supported version, typically 1.1 or 2.0
+ 'base_uri' => null, // string - the URI to resolve relative URLs, following rules in RFC 3986, section 2
+ 'buffer' => true, // bool|resource|\Closure - whether the content of the response should be buffered or not,
+ // or a stream resource where the response body should be written,
+ // or a closure telling if/where the response should be buffered based on its headers
+ 'on_progress' => null, // callable(int $dlNow, int $dlSize, array $info) - throwing any exceptions MUST abort
+ // the request; it MUST be called on DNS resolution, on arrival of headers and on
+ // completion; it SHOULD be called on upload/download of data and at least 1/s
+ 'resolve' => [], // string[] - a map of host to IP address that SHOULD replace DNS resolution
+ 'proxy' => null, // string - by default, the proxy-related env vars handled by curl SHOULD be honored
+ 'no_proxy' => null, // string - a comma separated list of hosts that do not require a proxy to be reached
+ 'timeout' => null, // float - the idle timeout - defaults to ini_get('default_socket_timeout')
+ 'max_duration' => 0, // float - the maximum execution time for the request+response as a whole;
+ // a value lower than or equal to 0 means it is unlimited
+ 'bindto' => '0', // string - the interface or the local socket to bind to
+ 'verify_peer' => true, // see https://php.net/context.ssl for the following options
+ 'verify_host' => true,
+ 'cafile' => null,
+ 'capath' => null,
+ 'local_cert' => null,
+ 'local_pk' => null,
+ 'passphrase' => null,
+ 'ciphers' => null,
+ 'peer_fingerprint' => null,
+ 'capture_peer_cert_chain' => false,
+ 'extra' => [], // array - additional options that can be ignored if unsupported, unlike regular options
+ ];
+
+ /**
+ * Requests an HTTP resource.
+ *
+ * Responses MUST be lazy, but their status code MUST be
+ * checked even if none of their public methods are called.
+ *
+ * Implementations are not required to support all options described above; they can also
+ * support more custom options; but in any case, they MUST throw a TransportExceptionInterface
+ * when an unsupported option is passed.
+ *
+ * @throws TransportExceptionInterface When an unsupported option is passed
+ */
+ public function request(string $method, string $url, array $options = []): ResponseInterface;
+
+ /**
+ * Yields responses chunk by chunk as they complete.
+ *
+ * @param ResponseInterface|iterable
+ */
+interface ResponseInterface
+{
+ /**
+ * Gets the HTTP status code of the response.
+ *
+ * @throws TransportExceptionInterface when a network error occurs
+ */
+ public function getStatusCode(): int;
+
+ /**
+ * Gets the HTTP headers of the response.
+ *
+ * @param bool $throw Whether an exception should be thrown on 3/4/5xx status codes
+ *
+ * @return string[][] The headers of the response keyed by header names in lowercase
+ *
+ * @throws TransportExceptionInterface When a network error occurs
+ * @throws RedirectionExceptionInterface On a 3xx when $throw is true and the "max_redirects" option has been reached
+ * @throws ClientExceptionInterface On a 4xx when $throw is true
+ * @throws ServerExceptionInterface On a 5xx when $throw is true
+ */
+ public function getHeaders(bool $throw = true): array;
+
+ /**
+ * Gets the response body as a string.
+ *
+ * @param bool $throw Whether an exception should be thrown on 3/4/5xx status codes
+ *
+ * @throws TransportExceptionInterface When a network error occurs
+ * @throws RedirectionExceptionInterface On a 3xx when $throw is true and the "max_redirects" option has been reached
+ * @throws ClientExceptionInterface On a 4xx when $throw is true
+ * @throws ServerExceptionInterface On a 5xx when $throw is true
+ */
+ public function getContent(bool $throw = true): string;
+
+ /**
+ * Gets the response body decoded as array, typically from a JSON payload.
+ *
+ * @param bool $throw Whether an exception should be thrown on 3/4/5xx status codes
+ *
+ * @throws DecodingExceptionInterface When the body cannot be decoded to an array
+ * @throws TransportExceptionInterface When a network error occurs
+ * @throws RedirectionExceptionInterface On a 3xx when $throw is true and the "max_redirects" option has been reached
+ * @throws ClientExceptionInterface On a 4xx when $throw is true
+ * @throws ServerExceptionInterface On a 5xx when $throw is true
+ */
+ public function toArray(bool $throw = true): array;
+
+ /**
+ * Closes the response stream and all related buffers.
+ *
+ * No further chunk will be yielded after this method has been called.
+ */
+ public function cancel(): void;
+
+ /**
+ * Returns info coming from the transport layer.
+ *
+ * This method SHOULD NOT throw any ExceptionInterface and SHOULD be non-blocking.
+ * The returned info is "live": it can be empty and can change from one call to
+ * another, as the request/response progresses.
+ *
+ * The following info MUST be returned:
+ * - canceled (bool) - true if the response was canceled using ResponseInterface::cancel(), false otherwise
+ * - error (string|null) - the error message when the transfer was aborted, null otherwise
+ * - http_code (int) - the last response code or 0 when it is not known yet
+ * - http_method (string) - the HTTP verb of the last request
+ * - redirect_count (int) - the number of redirects followed while executing the request
+ * - redirect_url (string|null) - the resolved location of redirect responses, null otherwise
+ * - response_headers (array) - an array modelled after the special $http_response_header variable
+ * - start_time (float) - the time when the request was sent or 0.0 when it's pending
+ * - url (string) - the last effective URL of the request
+ * - user_data (mixed) - the value of the "user_data" request option, null if not set
+ *
+ * When the "capture_peer_cert_chain" option is true, the "peer_certificate_chain"
+ * attribute SHOULD list the peer certificates as an array of OpenSSL X.509 resources.
+ *
+ * Other info SHOULD be named after curl_getinfo()'s associative return value.
+ *
+ * @return mixed An array of all available info, or one of them when $type is
+ * provided, or null when an unsupported type is requested
+ */
+ public function getInfo(string $type = null);
+}
diff --git a/src/vendor/symfony/http-client-contracts/ResponseStreamInterface.php b/src/vendor/symfony/http-client-contracts/ResponseStreamInterface.php
new file mode 100644
index 0000000..fa3e5db
--- /dev/null
+++ b/src/vendor/symfony/http-client-contracts/ResponseStreamInterface.php
@@ -0,0 +1,26 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Contracts\HttpClient;
+
+/**
+ * Yields response chunks, returned by HttpClientInterface::stream().
+ *
+ * @author Nicolas Grekas
+ *
+ * @extends \Iterator
+ */
+final class AmpHttpClient implements HttpClientInterface, LoggerAwareInterface, ResetInterface
+{
+ use HttpClientTrait;
+ use LoggerAwareTrait;
+
+ private $defaultOptions = self::OPTIONS_DEFAULTS;
+ private static $emptyDefaults = self::OPTIONS_DEFAULTS;
+
+ /** @var AmpClientState */
+ private $multi;
+
+ /**
+ * @param array $defaultOptions Default requests' options
+ * @param callable|null $clientConfigurator A callable that builds a {@see DelegateHttpClient} from a {@see PooledHttpClient};
+ * passing null builds an {@see InterceptedHttpClient} with 2 retries on failures
+ * @param int $maxHostConnections The maximum number of connections to a single host
+ * @param int $maxPendingPushes The maximum number of pushed responses to accept in the queue
+ *
+ * @see HttpClientInterface::OPTIONS_DEFAULTS for available options
+ */
+ public function __construct(array $defaultOptions = [], callable $clientConfigurator = null, int $maxHostConnections = 6, int $maxPendingPushes = 50)
+ {
+ $this->defaultOptions['buffer'] = $this->defaultOptions['buffer'] ?? \Closure::fromCallable([__CLASS__, 'shouldBuffer']);
+
+ if ($defaultOptions) {
+ [, $this->defaultOptions] = self::prepareRequest(null, null, $defaultOptions, $this->defaultOptions);
+ }
+
+ $this->multi = new AmpClientState($clientConfigurator, $maxHostConnections, $maxPendingPushes, $this->logger);
+ }
+
+ /**
+ * @see HttpClientInterface::OPTIONS_DEFAULTS for available options
+ *
+ * {@inheritdoc}
+ */
+ public function request(string $method, string $url, array $options = []): ResponseInterface
+ {
+ [$url, $options] = self::prepareRequest($method, $url, $options, $this->defaultOptions);
+
+ $options['proxy'] = self::getProxy($options['proxy'], $url, $options['no_proxy']);
+
+ if (null !== $options['proxy'] && !class_exists(Http1TunnelConnector::class)) {
+ throw new \LogicException('You cannot use the "proxy" option as the "amphp/http-tunnel" package is not installed. Try running "composer require amphp/http-tunnel".');
+ }
+
+ if ($options['bindto']) {
+ if (0 === strpos($options['bindto'], 'if!')) {
+ throw new TransportException(__CLASS__.' cannot bind to network interfaces, use e.g. CurlHttpClient instead.');
+ }
+ if (0 === strpos($options['bindto'], 'host!')) {
+ $options['bindto'] = substr($options['bindto'], 5);
+ }
+ }
+
+ if (('' !== $options['body'] || 'POST' === $method || isset($options['normalized_headers']['content-length'])) && !isset($options['normalized_headers']['content-type'])) {
+ $options['headers'][] = 'Content-Type: application/x-www-form-urlencoded';
+ }
+
+ if (!isset($options['normalized_headers']['user-agent'])) {
+ $options['headers'][] = 'User-Agent: Symfony HttpClient/Amp';
+ }
+
+ if (0 < $options['max_duration']) {
+ $options['timeout'] = min($options['max_duration'], $options['timeout']);
+ }
+
+ if ($options['resolve']) {
+ $this->multi->dnsCache = $options['resolve'] + $this->multi->dnsCache;
+ }
+
+ if ($options['peer_fingerprint'] && !isset($options['peer_fingerprint']['pin-sha256'])) {
+ throw new TransportException(__CLASS__.' supports only "pin-sha256" fingerprints.');
+ }
+
+ $request = new Request(implode('', $url), $method);
+
+ if ($options['http_version']) {
+ switch ((float) $options['http_version']) {
+ case 1.0: $request->setProtocolVersions(['1.0']); break;
+ case 1.1: $request->setProtocolVersions(['1.1', '1.0']); break;
+ default: $request->setProtocolVersions(['2', '1.1', '1.0']); break;
+ }
+ }
+
+ foreach ($options['headers'] as $v) {
+ $h = explode(': ', $v, 2);
+ $request->addHeader($h[0], $h[1]);
+ }
+
+ $request->setTcpConnectTimeout(1000 * $options['timeout']);
+ $request->setTlsHandshakeTimeout(1000 * $options['timeout']);
+ $request->setTransferTimeout(1000 * $options['max_duration']);
+ if (method_exists($request, 'setInactivityTimeout')) {
+ $request->setInactivityTimeout(0);
+ }
+
+ if ('' !== $request->getUri()->getUserInfo() && !$request->hasHeader('authorization')) {
+ $auth = explode(':', $request->getUri()->getUserInfo(), 2);
+ $auth = array_map('rawurldecode', $auth) + [1 => ''];
+ $request->setHeader('Authorization', 'Basic '.base64_encode(implode(':', $auth)));
+ }
+
+ return new AmpResponse($this->multi, $request, $options, $this->logger);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function stream($responses, float $timeout = null): ResponseStreamInterface
+ {
+ if ($responses instanceof AmpResponse) {
+ $responses = [$responses];
+ } elseif (!is_iterable($responses)) {
+ throw new \TypeError(sprintf('"%s()" expects parameter 1 to be an iterable of AmpResponse objects, "%s" given.', __METHOD__, get_debug_type($responses)));
+ }
+
+ return new ResponseStream(AmpResponse::stream($responses, $timeout));
+ }
+
+ public function reset()
+ {
+ $this->multi->dnsCache = [];
+
+ foreach ($this->multi->pushedResponses as $authority => $pushedResponses) {
+ foreach ($pushedResponses as [$pushedUrl, $pushDeferred]) {
+ $pushDeferred->fail(new CancelledException());
+
+ if ($this->logger) {
+ $this->logger->debug(sprintf('Unused pushed response: "%s"', $pushedUrl));
+ }
+ }
+ }
+
+ $this->multi->pushedResponses = [];
+ }
+}
diff --git a/src/vendor/symfony/http-client/AsyncDecoratorTrait.php b/src/vendor/symfony/http-client/AsyncDecoratorTrait.php
new file mode 100644
index 0000000..aff402d
--- /dev/null
+++ b/src/vendor/symfony/http-client/AsyncDecoratorTrait.php
@@ -0,0 +1,48 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\HttpClient;
+
+use Symfony\Component\HttpClient\Response\AsyncResponse;
+use Symfony\Component\HttpClient\Response\ResponseStream;
+use Symfony\Contracts\HttpClient\ResponseInterface;
+use Symfony\Contracts\HttpClient\ResponseStreamInterface;
+
+/**
+ * Eases with processing responses while streaming them.
+ *
+ * @author Nicolas Grekas
+ */
+trait AsyncDecoratorTrait
+{
+ use DecoratorTrait;
+
+ /**
+ * {@inheritdoc}
+ *
+ * @return AsyncResponse
+ */
+ abstract public function request(string $method, string $url, array $options = []): ResponseInterface;
+
+ /**
+ * {@inheritdoc}
+ */
+ public function stream($responses, float $timeout = null): ResponseStreamInterface
+ {
+ if ($responses instanceof AsyncResponse) {
+ $responses = [$responses];
+ } elseif (!is_iterable($responses)) {
+ throw new \TypeError(sprintf('"%s()" expects parameter 1 to be an iterable of AsyncResponse objects, "%s" given.', __METHOD__, get_debug_type($responses)));
+ }
+
+ return new ResponseStream(AsyncResponse::stream($responses, $timeout, static::class));
+ }
+}
diff --git a/src/vendor/symfony/http-client/CHANGELOG.md b/src/vendor/symfony/http-client/CHANGELOG.md
new file mode 100644
index 0000000..7c2fc22
--- /dev/null
+++ b/src/vendor/symfony/http-client/CHANGELOG.md
@@ -0,0 +1,54 @@
+CHANGELOG
+=========
+
+5.4
+---
+
+ * Add `MockHttpClient::setResponseFactory()` method to be able to set response factory after client creating
+
+5.3
+---
+
+ * Implement `HttpClientInterface::withOptions()` from `symfony/contracts` v2.4
+ * Add `DecoratorTrait` to ease writing simple decorators
+
+5.2.0
+-----
+
+ * added `AsyncDecoratorTrait` to ease processing responses without breaking async
+ * added support for pausing responses with a new `pause_handler` callable exposed as an info item
+ * added `StreamableInterface` to ease turning responses into PHP streams
+ * added `MockResponse::getRequestMethod()` and `getRequestUrl()` to allow inspecting which request has been sent
+ * added `EventSourceHttpClient` a Server-Sent events stream implementing the [EventSource specification](https://www.w3.org/TR/eventsource/#eventsource)
+ * added option "extra.curl" to allow setting additional curl options in `CurlHttpClient`
+ * added `RetryableHttpClient` to automatically retry failed HTTP requests.
+ * added `extra.trace_content` option to `TraceableHttpClient` to prevent it from keeping the content in memory
+
+5.1.0
+-----
+
+ * added `NoPrivateNetworkHttpClient` decorator
+ * added `AmpHttpClient`, a portable HTTP/2 implementation based on Amp
+ * added `LoggerAwareInterface` to `ScopingHttpClient` and `TraceableHttpClient`
+ * made `HttpClient::create()` return an `AmpHttpClient` when `amphp/http-client` is found but curl is not or too old
+
+4.4.0
+-----
+
+ * added `canceled` to `ResponseInterface::getInfo()`
+ * added `HttpClient::createForBaseUri()`
+ * added `HttplugClient` with support for sync and async requests
+ * added `max_duration` option
+ * added support for NTLM authentication
+ * added `StreamWrapper` to cast any `ResponseInterface` instances to PHP streams.
+ * added `$response->toStream()` to cast responses to regular PHP streams
+ * made `Psr18Client` implement relevant PSR-17 factories and have streaming responses
+ * added `TraceableHttpClient`, `HttpClientDataCollector` and `HttpClientPass` to integrate with the web profiler
+ * allow enabling buffering conditionally with a Closure
+ * allow option "buffer" to be a stream resource
+ * allow arbitrary values for the "json" option
+
+4.3.0
+-----
+
+ * added the component
diff --git a/src/vendor/symfony/http-client/CachingHttpClient.php b/src/vendor/symfony/http-client/CachingHttpClient.php
new file mode 100644
index 0000000..e1d7023
--- /dev/null
+++ b/src/vendor/symfony/http-client/CachingHttpClient.php
@@ -0,0 +1,152 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\HttpClient;
+
+use Symfony\Component\HttpClient\Response\MockResponse;
+use Symfony\Component\HttpClient\Response\ResponseStream;
+use Symfony\Component\HttpFoundation\Request;
+use Symfony\Component\HttpKernel\HttpCache\HttpCache;
+use Symfony\Component\HttpKernel\HttpCache\StoreInterface;
+use Symfony\Component\HttpKernel\HttpClientKernel;
+use Symfony\Contracts\HttpClient\HttpClientInterface;
+use Symfony\Contracts\HttpClient\ResponseInterface;
+use Symfony\Contracts\HttpClient\ResponseStreamInterface;
+use Symfony\Contracts\Service\ResetInterface;
+
+/**
+ * Adds caching on top of an HTTP client.
+ *
+ * The implementation buffers responses in memory and doesn't stream directly from the network.
+ * You can disable/enable this layer by setting option "no_cache" under "extra" to true/false.
+ * By default, caching is enabled unless the "buffer" option is set to false.
+ *
+ * @author Nicolas Grekas
+ */
+class CachingHttpClient implements HttpClientInterface, ResetInterface
+{
+ use HttpClientTrait;
+
+ private $client;
+ private $cache;
+ private $defaultOptions = self::OPTIONS_DEFAULTS;
+
+ public function __construct(HttpClientInterface $client, StoreInterface $store, array $defaultOptions = [])
+ {
+ if (!class_exists(HttpClientKernel::class)) {
+ throw new \LogicException(sprintf('Using "%s" requires that the HttpKernel component version 4.3 or higher is installed, try running "composer require symfony/http-kernel:^5.4".', __CLASS__));
+ }
+
+ $this->client = $client;
+ $kernel = new HttpClientKernel($client);
+ $this->cache = new HttpCache($kernel, $store, null, $defaultOptions);
+
+ unset($defaultOptions['debug']);
+ unset($defaultOptions['default_ttl']);
+ unset($defaultOptions['private_headers']);
+ unset($defaultOptions['allow_reload']);
+ unset($defaultOptions['allow_revalidate']);
+ unset($defaultOptions['stale_while_revalidate']);
+ unset($defaultOptions['stale_if_error']);
+ unset($defaultOptions['trace_level']);
+ unset($defaultOptions['trace_header']);
+
+ if ($defaultOptions) {
+ [, $this->defaultOptions] = self::prepareRequest(null, null, $defaultOptions, $this->defaultOptions);
+ }
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function request(string $method, string $url, array $options = []): ResponseInterface
+ {
+ [$url, $options] = $this->prepareRequest($method, $url, $options, $this->defaultOptions, true);
+ $url = implode('', $url);
+
+ if (!empty($options['body']) || !empty($options['extra']['no_cache']) || !\in_array($method, ['GET', 'HEAD', 'OPTIONS'])) {
+ return $this->client->request($method, $url, $options);
+ }
+
+ $request = Request::create($url, $method);
+ $request->attributes->set('http_client_options', $options);
+
+ foreach ($options['normalized_headers'] as $name => $values) {
+ if ('cookie' !== $name) {
+ foreach ($values as $value) {
+ $request->headers->set($name, substr($value, 2 + \strlen($name)), false);
+ }
+
+ continue;
+ }
+
+ foreach ($values as $cookies) {
+ foreach (explode('; ', substr($cookies, \strlen('Cookie: '))) as $cookie) {
+ if ('' !== $cookie) {
+ $cookie = explode('=', $cookie, 2);
+ $request->cookies->set($cookie[0], $cookie[1] ?? '');
+ }
+ }
+ }
+ }
+
+ $response = $this->cache->handle($request);
+ $response = new MockResponse($response->getContent(), [
+ 'http_code' => $response->getStatusCode(),
+ 'response_headers' => $response->headers->allPreserveCase(),
+ ]);
+
+ return MockResponse::fromRequest($method, $url, $options, $response);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function stream($responses, float $timeout = null): ResponseStreamInterface
+ {
+ if ($responses instanceof ResponseInterface) {
+ $responses = [$responses];
+ } elseif (!is_iterable($responses)) {
+ throw new \TypeError(sprintf('"%s()" expects parameter 1 to be an iterable of ResponseInterface objects, "%s" given.', __METHOD__, get_debug_type($responses)));
+ }
+
+ $mockResponses = [];
+ $clientResponses = [];
+
+ foreach ($responses as $response) {
+ if ($response instanceof MockResponse) {
+ $mockResponses[] = $response;
+ } else {
+ $clientResponses[] = $response;
+ }
+ }
+
+ if (!$mockResponses) {
+ return $this->client->stream($clientResponses, $timeout);
+ }
+
+ if (!$clientResponses) {
+ return new ResponseStream(MockResponse::stream($mockResponses, $timeout));
+ }
+
+ return new ResponseStream((function () use ($mockResponses, $clientResponses, $timeout) {
+ yield from MockResponse::stream($mockResponses, $timeout);
+ yield $this->client->stream($clientResponses, $timeout);
+ })());
+ }
+
+ public function reset()
+ {
+ if ($this->client instanceof ResetInterface) {
+ $this->client->reset();
+ }
+ }
+}
diff --git a/src/vendor/symfony/http-client/Chunk/DataChunk.php b/src/vendor/symfony/http-client/Chunk/DataChunk.php
new file mode 100644
index 0000000..37ca848
--- /dev/null
+++ b/src/vendor/symfony/http-client/Chunk/DataChunk.php
@@ -0,0 +1,87 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\HttpClient\Chunk;
+
+use Symfony\Contracts\HttpClient\ChunkInterface;
+
+/**
+ * @author Nicolas Grekas
+ *
+ * @internal
+ */
+class DataChunk implements ChunkInterface
+{
+ private $offset = 0;
+ private $content = '';
+
+ public function __construct(int $offset = 0, string $content = '')
+ {
+ $this->offset = $offset;
+ $this->content = $content;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function isTimeout(): bool
+ {
+ return false;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function isFirst(): bool
+ {
+ return false;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function isLast(): bool
+ {
+ return false;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getInformationalStatus(): ?array
+ {
+ return null;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getContent(): string
+ {
+ return $this->content;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getOffset(): int
+ {
+ return $this->offset;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getError(): ?string
+ {
+ return null;
+ }
+}
diff --git a/src/vendor/symfony/http-client/Chunk/ErrorChunk.php b/src/vendor/symfony/http-client/Chunk/ErrorChunk.php
new file mode 100644
index 0000000..a19f433
--- /dev/null
+++ b/src/vendor/symfony/http-client/Chunk/ErrorChunk.php
@@ -0,0 +1,140 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\HttpClient\Chunk;
+
+use Symfony\Component\HttpClient\Exception\TimeoutException;
+use Symfony\Component\HttpClient\Exception\TransportException;
+use Symfony\Contracts\HttpClient\ChunkInterface;
+
+/**
+ * @author Nicolas Grekas
+ *
+ * @internal
+ */
+class ErrorChunk implements ChunkInterface
+{
+ private $didThrow = false;
+ private $offset;
+ private $errorMessage;
+ private $error;
+
+ /**
+ * @param \Throwable|string $error
+ */
+ public function __construct(int $offset, $error)
+ {
+ $this->offset = $offset;
+
+ if (\is_string($error)) {
+ $this->errorMessage = $error;
+ } else {
+ $this->error = $error;
+ $this->errorMessage = $error->getMessage();
+ }
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function isTimeout(): bool
+ {
+ $this->didThrow = true;
+
+ if (null !== $this->error) {
+ throw new TransportException($this->errorMessage, 0, $this->error);
+ }
+
+ return true;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function isFirst(): bool
+ {
+ $this->didThrow = true;
+ throw null !== $this->error ? new TransportException($this->errorMessage, 0, $this->error) : new TimeoutException($this->errorMessage);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function isLast(): bool
+ {
+ $this->didThrow = true;
+ throw null !== $this->error ? new TransportException($this->errorMessage, 0, $this->error) : new TimeoutException($this->errorMessage);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getInformationalStatus(): ?array
+ {
+ $this->didThrow = true;
+ throw null !== $this->error ? new TransportException($this->errorMessage, 0, $this->error) : new TimeoutException($this->errorMessage);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getContent(): string
+ {
+ $this->didThrow = true;
+ throw null !== $this->error ? new TransportException($this->errorMessage, 0, $this->error) : new TimeoutException($this->errorMessage);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getOffset(): int
+ {
+ return $this->offset;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getError(): ?string
+ {
+ return $this->errorMessage;
+ }
+
+ /**
+ * @return bool Whether the wrapped error has been thrown or not
+ */
+ public function didThrow(bool $didThrow = null): bool
+ {
+ if (null !== $didThrow && $this->didThrow !== $didThrow) {
+ return !$this->didThrow = $didThrow;
+ }
+
+ return $this->didThrow;
+ }
+
+ public function __sleep(): array
+ {
+ throw new \BadMethodCallException('Cannot serialize '.__CLASS__);
+ }
+
+ public function __wakeup()
+ {
+ throw new \BadMethodCallException('Cannot unserialize '.__CLASS__);
+ }
+
+ public function __destruct()
+ {
+ if (!$this->didThrow) {
+ $this->didThrow = true;
+ throw null !== $this->error ? new TransportException($this->errorMessage, 0, $this->error) : new TimeoutException($this->errorMessage);
+ }
+ }
+}
diff --git a/src/vendor/symfony/http-client/Chunk/FirstChunk.php b/src/vendor/symfony/http-client/Chunk/FirstChunk.php
new file mode 100644
index 0000000..d891ca8
--- /dev/null
+++ b/src/vendor/symfony/http-client/Chunk/FirstChunk.php
@@ -0,0 +1,28 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\HttpClient\Chunk;
+
+/**
+ * @author Nicolas Grekas
+ *
+ * @internal
+ */
+class FirstChunk extends DataChunk
+{
+ /**
+ * {@inheritdoc}
+ */
+ public function isFirst(): bool
+ {
+ return true;
+ }
+}
diff --git a/src/vendor/symfony/http-client/Chunk/InformationalChunk.php b/src/vendor/symfony/http-client/Chunk/InformationalChunk.php
new file mode 100644
index 0000000..c4452f1
--- /dev/null
+++ b/src/vendor/symfony/http-client/Chunk/InformationalChunk.php
@@ -0,0 +1,35 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\HttpClient\Chunk;
+
+/**
+ * @author Nicolas Grekas
+ *
+ * @internal
+ */
+class InformationalChunk extends DataChunk
+{
+ private $status;
+
+ public function __construct(int $statusCode, array $headers)
+ {
+ $this->status = [$statusCode, $headers];
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getInformationalStatus(): ?array
+ {
+ return $this->status;
+ }
+}
diff --git a/src/vendor/symfony/http-client/Chunk/LastChunk.php b/src/vendor/symfony/http-client/Chunk/LastChunk.php
new file mode 100644
index 0000000..84095d3
--- /dev/null
+++ b/src/vendor/symfony/http-client/Chunk/LastChunk.php
@@ -0,0 +1,28 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\HttpClient\Chunk;
+
+/**
+ * @author Nicolas Grekas
+ *
+ * @internal
+ */
+class LastChunk extends DataChunk
+{
+ /**
+ * {@inheritdoc}
+ */
+ public function isLast(): bool
+ {
+ return true;
+ }
+}
diff --git a/src/vendor/symfony/http-client/Chunk/ServerSentEvent.php b/src/vendor/symfony/http-client/Chunk/ServerSentEvent.php
new file mode 100644
index 0000000..f7ff4b9
--- /dev/null
+++ b/src/vendor/symfony/http-client/Chunk/ServerSentEvent.php
@@ -0,0 +1,79 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\HttpClient\Chunk;
+
+use Symfony\Contracts\HttpClient\ChunkInterface;
+
+/**
+ * @author Antoine Bluchet
+ */
+final class ServerSentEvent extends DataChunk implements ChunkInterface
+{
+ private $data = '';
+ private $id = '';
+ private $type = 'message';
+ private $retry = 0;
+
+ public function __construct(string $content)
+ {
+ parent::__construct(-1, $content);
+
+ // remove BOM
+ if (0 === strpos($content, "\xEF\xBB\xBF")) {
+ $content = substr($content, 3);
+ }
+
+ foreach (preg_split("/(?:\r\n|[\r\n])/", $content) as $line) {
+ if (0 === $i = strpos($line, ':')) {
+ continue;
+ }
+
+ $i = false === $i ? \strlen($line) : $i;
+ $field = substr($line, 0, $i);
+ $i += 1 + (' ' === ($line[1 + $i] ?? ''));
+
+ switch ($field) {
+ case 'id': $this->id = substr($line, $i); break;
+ case 'event': $this->type = substr($line, $i); break;
+ case 'data': $this->data .= ('' === $this->data ? '' : "\n").substr($line, $i); break;
+ case 'retry':
+ $retry = substr($line, $i);
+
+ if ('' !== $retry && \strlen($retry) === strspn($retry, '0123456789')) {
+ $this->retry = $retry / 1000.0;
+ }
+ break;
+ }
+ }
+ }
+
+ public function getId(): string
+ {
+ return $this->id;
+ }
+
+ public function getType(): string
+ {
+ return $this->type;
+ }
+
+ public function getData(): string
+ {
+ return $this->data;
+ }
+
+ public function getRetry(): float
+ {
+ return $this->retry;
+ }
+}
diff --git a/src/vendor/symfony/http-client/CurlHttpClient.php b/src/vendor/symfony/http-client/CurlHttpClient.php
new file mode 100644
index 0000000..ef6d700
--- /dev/null
+++ b/src/vendor/symfony/http-client/CurlHttpClient.php
@@ -0,0 +1,552 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\HttpClient;
+
+use Psr\Log\LoggerAwareInterface;
+use Psr\Log\LoggerInterface;
+use Symfony\Component\HttpClient\Exception\InvalidArgumentException;
+use Symfony\Component\HttpClient\Exception\TransportException;
+use Symfony\Component\HttpClient\Internal\CurlClientState;
+use Symfony\Component\HttpClient\Internal\PushedResponse;
+use Symfony\Component\HttpClient\Response\CurlResponse;
+use Symfony\Component\HttpClient\Response\ResponseStream;
+use Symfony\Contracts\HttpClient\HttpClientInterface;
+use Symfony\Contracts\HttpClient\ResponseInterface;
+use Symfony\Contracts\HttpClient\ResponseStreamInterface;
+use Symfony\Contracts\Service\ResetInterface;
+
+/**
+ * A performant implementation of the HttpClientInterface contracts based on the curl extension.
+ *
+ * This provides fully concurrent HTTP requests, with transparent
+ * HTTP/2 push when a curl version that supports it is installed.
+ *
+ * @author Nicolas Grekas
+ */
+final class CurlHttpClient implements HttpClientInterface, LoggerAwareInterface, ResetInterface
+{
+ use HttpClientTrait;
+
+ private $defaultOptions = self::OPTIONS_DEFAULTS + [
+ 'auth_ntlm' => null, // array|string - an array containing the username as first value, and optionally the
+ // password as the second one; or string like username:password - enabling NTLM auth
+ 'extra' => [
+ 'curl' => [], // A list of extra curl options indexed by their corresponding CURLOPT_*
+ ],
+ ];
+ private static $emptyDefaults = self::OPTIONS_DEFAULTS + ['auth_ntlm' => null];
+
+ /**
+ * @var LoggerInterface|null
+ */
+ private $logger;
+
+ /**
+ * An internal object to share state between the client and its responses.
+ *
+ * @var CurlClientState
+ */
+ private $multi;
+
+ /**
+ * @param array $defaultOptions Default request's options
+ * @param int $maxHostConnections The maximum number of connections to a single host
+ * @param int $maxPendingPushes The maximum number of pushed responses to accept in the queue
+ *
+ * @see HttpClientInterface::OPTIONS_DEFAULTS for available options
+ */
+ public function __construct(array $defaultOptions = [], int $maxHostConnections = 6, int $maxPendingPushes = 50)
+ {
+ if (!\extension_loaded('curl')) {
+ throw new \LogicException('You cannot use the "Symfony\Component\HttpClient\CurlHttpClient" as the "curl" extension is not installed.');
+ }
+
+ $this->defaultOptions['buffer'] = $this->defaultOptions['buffer'] ?? \Closure::fromCallable([__CLASS__, 'shouldBuffer']);
+
+ if ($defaultOptions) {
+ [, $this->defaultOptions] = self::prepareRequest(null, null, $defaultOptions, $this->defaultOptions);
+ }
+
+ $this->multi = new CurlClientState($maxHostConnections, $maxPendingPushes);
+ }
+
+ public function setLogger(LoggerInterface $logger): void
+ {
+ $this->logger = $this->multi->logger = $logger;
+ }
+
+ /**
+ * @see HttpClientInterface::OPTIONS_DEFAULTS for available options
+ *
+ * {@inheritdoc}
+ */
+ public function request(string $method, string $url, array $options = []): ResponseInterface
+ {
+ [$url, $options] = self::prepareRequest($method, $url, $options, $this->defaultOptions);
+ $scheme = $url['scheme'];
+ $authority = $url['authority'];
+ $host = parse_url($authority, \PHP_URL_HOST);
+ $proxy = self::getProxyUrl($options['proxy'], $url);
+ $url = implode('', $url);
+
+ if (!isset($options['normalized_headers']['user-agent'])) {
+ $options['headers'][] = 'User-Agent: Symfony HttpClient/Curl';
+ }
+
+ $curlopts = [
+ \CURLOPT_URL => $url,
+ \CURLOPT_TCP_NODELAY => true,
+ \CURLOPT_PROTOCOLS => \CURLPROTO_HTTP | \CURLPROTO_HTTPS,
+ \CURLOPT_REDIR_PROTOCOLS => \CURLPROTO_HTTP | \CURLPROTO_HTTPS,
+ \CURLOPT_FOLLOWLOCATION => true,
+ \CURLOPT_MAXREDIRS => 0 < $options['max_redirects'] ? $options['max_redirects'] : 0,
+ \CURLOPT_COOKIEFILE => '', // Keep track of cookies during redirects
+ \CURLOPT_TIMEOUT => 0,
+ \CURLOPT_PROXY => $proxy,
+ \CURLOPT_NOPROXY => $options['no_proxy'] ?? $_SERVER['no_proxy'] ?? $_SERVER['NO_PROXY'] ?? '',
+ \CURLOPT_SSL_VERIFYPEER => $options['verify_peer'],
+ \CURLOPT_SSL_VERIFYHOST => $options['verify_host'] ? 2 : 0,
+ \CURLOPT_CAINFO => $options['cafile'],
+ \CURLOPT_CAPATH => $options['capath'],
+ \CURLOPT_SSL_CIPHER_LIST => $options['ciphers'],
+ \CURLOPT_SSLCERT => $options['local_cert'],
+ \CURLOPT_SSLKEY => $options['local_pk'],
+ \CURLOPT_KEYPASSWD => $options['passphrase'],
+ \CURLOPT_CERTINFO => $options['capture_peer_cert_chain'],
+ ];
+
+ if (1.0 === (float) $options['http_version']) {
+ $curlopts[\CURLOPT_HTTP_VERSION] = \CURL_HTTP_VERSION_1_0;
+ } elseif (1.1 === (float) $options['http_version']) {
+ $curlopts[\CURLOPT_HTTP_VERSION] = \CURL_HTTP_VERSION_1_1;
+ } elseif (\defined('CURL_VERSION_HTTP2') && (\CURL_VERSION_HTTP2 & CurlClientState::$curlVersion['features']) && ('https:' === $scheme || 2.0 === (float) $options['http_version'])) {
+ $curlopts[\CURLOPT_HTTP_VERSION] = \CURL_HTTP_VERSION_2_0;
+ }
+
+ if (isset($options['auth_ntlm'])) {
+ $curlopts[\CURLOPT_HTTPAUTH] = \CURLAUTH_NTLM;
+ $curlopts[\CURLOPT_HTTP_VERSION] = \CURL_HTTP_VERSION_1_1;
+
+ if (\is_array($options['auth_ntlm'])) {
+ $count = \count($options['auth_ntlm']);
+ if ($count <= 0 || $count > 2) {
+ throw new InvalidArgumentException(sprintf('Option "auth_ntlm" must contain 1 or 2 elements, %d given.', $count));
+ }
+
+ $options['auth_ntlm'] = implode(':', $options['auth_ntlm']);
+ }
+
+ if (!\is_string($options['auth_ntlm'])) {
+ throw new InvalidArgumentException(sprintf('Option "auth_ntlm" must be a string or an array, "%s" given.', get_debug_type($options['auth_ntlm'])));
+ }
+
+ $curlopts[\CURLOPT_USERPWD] = $options['auth_ntlm'];
+ }
+
+ if (!\ZEND_THREAD_SAFE) {
+ $curlopts[\CURLOPT_DNS_USE_GLOBAL_CACHE] = false;
+ }
+
+ if (\defined('CURLOPT_HEADEROPT') && \defined('CURLHEADER_SEPARATE')) {
+ $curlopts[\CURLOPT_HEADEROPT] = \CURLHEADER_SEPARATE;
+ }
+
+ // curl's resolve feature varies by host:port but ours varies by host only, let's handle this with our own DNS map
+ if (isset($this->multi->dnsCache->hostnames[$host])) {
+ $options['resolve'] += [$host => $this->multi->dnsCache->hostnames[$host]];
+ }
+
+ if ($options['resolve'] || $this->multi->dnsCache->evictions) {
+ // First reset any old DNS cache entries then add the new ones
+ $resolve = $this->multi->dnsCache->evictions;
+ $this->multi->dnsCache->evictions = [];
+ $port = parse_url($authority, \PHP_URL_PORT) ?: ('http:' === $scheme ? 80 : 443);
+
+ if ($resolve && 0x072A00 > CurlClientState::$curlVersion['version_number']) {
+ // DNS cache removals require curl 7.42 or higher
+ $this->multi->reset();
+ }
+
+ foreach ($options['resolve'] as $host => $ip) {
+ $resolve[] = null === $ip ? "-$host:$port" : "$host:$port:$ip";
+ $this->multi->dnsCache->hostnames[$host] = $ip;
+ $this->multi->dnsCache->removals["-$host:$port"] = "-$host:$port";
+ }
+
+ $curlopts[\CURLOPT_RESOLVE] = $resolve;
+ }
+
+ if ('POST' === $method) {
+ // Use CURLOPT_POST to have browser-like POST-to-GET redirects for 301, 302 and 303
+ $curlopts[\CURLOPT_POST] = true;
+ } elseif ('HEAD' === $method) {
+ $curlopts[\CURLOPT_NOBODY] = true;
+ } else {
+ $curlopts[\CURLOPT_CUSTOMREQUEST] = $method;
+ }
+
+ if ('\\' !== \DIRECTORY_SEPARATOR && $options['timeout'] < 1) {
+ $curlopts[\CURLOPT_NOSIGNAL] = true;
+ }
+
+ if (\extension_loaded('zlib') && !isset($options['normalized_headers']['accept-encoding'])) {
+ $options['headers'][] = 'Accept-Encoding: gzip'; // Expose only one encoding, some servers mess up when more are provided
+ }
+ $body = $options['body'];
+
+ foreach ($options['headers'] as $i => $header) {
+ if (\is_string($body) && '' !== $body && 0 === stripos($header, 'Content-Length: ')) {
+ // Let curl handle Content-Length headers
+ unset($options['headers'][$i]);
+ continue;
+ }
+ if (':' === $header[-2] && \strlen($header) - 2 === strpos($header, ': ')) {
+ // curl requires a special syntax to send empty headers
+ $curlopts[\CURLOPT_HTTPHEADER][] = substr_replace($header, ';', -2);
+ } else {
+ $curlopts[\CURLOPT_HTTPHEADER][] = $header;
+ }
+ }
+
+ // Prevent curl from sending its default Accept and Expect headers
+ foreach (['accept', 'expect'] as $header) {
+ if (!isset($options['normalized_headers'][$header][0])) {
+ $curlopts[\CURLOPT_HTTPHEADER][] = $header.':';
+ }
+ }
+
+ if (!\is_string($body)) {
+ if (\is_resource($body)) {
+ $curlopts[\CURLOPT_INFILE] = $body;
+ } else {
+ $eof = false;
+ $buffer = '';
+ $curlopts[\CURLOPT_READFUNCTION] = static function ($ch, $fd, $length) use ($body, &$buffer, &$eof) {
+ return self::readRequestBody($length, $body, $buffer, $eof);
+ };
+ }
+
+ if (isset($options['normalized_headers']['content-length'][0])) {
+ $curlopts[\CURLOPT_INFILESIZE] = (int) substr($options['normalized_headers']['content-length'][0], \strlen('Content-Length: '));
+ }
+ if (!isset($options['normalized_headers']['transfer-encoding'])) {
+ $curlopts[\CURLOPT_HTTPHEADER][] = 'Transfer-Encoding:'.(isset($curlopts[\CURLOPT_INFILESIZE]) ? '' : ' chunked');
+ }
+
+ if ('POST' !== $method) {
+ $curlopts[\CURLOPT_UPLOAD] = true;
+
+ if (!isset($options['normalized_headers']['content-type']) && 0 !== ($curlopts[\CURLOPT_INFILESIZE] ?? null)) {
+ $curlopts[\CURLOPT_HTTPHEADER][] = 'Content-Type: application/x-www-form-urlencoded';
+ }
+ }
+ } elseif ('' !== $body || 'POST' === $method) {
+ $curlopts[\CURLOPT_POSTFIELDS] = $body;
+ }
+
+ if ($options['peer_fingerprint']) {
+ if (!isset($options['peer_fingerprint']['pin-sha256'])) {
+ throw new TransportException(__CLASS__.' supports only "pin-sha256" fingerprints.');
+ }
+
+ $curlopts[\CURLOPT_PINNEDPUBLICKEY] = 'sha256//'.implode(';sha256//', $options['peer_fingerprint']['pin-sha256']);
+ }
+
+ if ($options['bindto']) {
+ if (file_exists($options['bindto'])) {
+ $curlopts[\CURLOPT_UNIX_SOCKET_PATH] = $options['bindto'];
+ } elseif (!str_starts_with($options['bindto'], 'if!') && preg_match('/^(.*):(\d+)$/', $options['bindto'], $matches)) {
+ $curlopts[\CURLOPT_INTERFACE] = $matches[1];
+ $curlopts[\CURLOPT_LOCALPORT] = $matches[2];
+ } else {
+ $curlopts[\CURLOPT_INTERFACE] = $options['bindto'];
+ }
+ }
+
+ if (0 < $options['max_duration']) {
+ $curlopts[\CURLOPT_TIMEOUT_MS] = 1000 * $options['max_duration'];
+ }
+
+ if (!empty($options['extra']['curl']) && \is_array($options['extra']['curl'])) {
+ $this->validateExtraCurlOptions($options['extra']['curl']);
+ $curlopts += $options['extra']['curl'];
+ }
+
+ if ($pushedResponse = $this->multi->pushedResponses[$url] ?? null) {
+ unset($this->multi->pushedResponses[$url]);
+
+ if (self::acceptPushForRequest($method, $options, $pushedResponse)) {
+ $this->logger && $this->logger->debug(sprintf('Accepting pushed response: "%s %s"', $method, $url));
+
+ // Reinitialize the pushed response with request's options
+ $ch = $pushedResponse->handle;
+ $pushedResponse = $pushedResponse->response;
+ $pushedResponse->__construct($this->multi, $url, $options, $this->logger);
+ } else {
+ $this->logger && $this->logger->debug(sprintf('Rejecting pushed response: "%s"', $url));
+ $pushedResponse = null;
+ }
+ }
+
+ if (!$pushedResponse) {
+ $ch = curl_init();
+ $this->logger && $this->logger->info(sprintf('Request: "%s %s"', $method, $url));
+ $curlopts += [\CURLOPT_SHARE => $this->multi->share];
+ }
+
+ foreach ($curlopts as $opt => $value) {
+ if (null !== $value && !curl_setopt($ch, $opt, $value) && \CURLOPT_CERTINFO !== $opt && (!\defined('CURLOPT_HEADEROPT') || \CURLOPT_HEADEROPT !== $opt)) {
+ $constantName = $this->findConstantName($opt);
+ throw new TransportException(sprintf('Curl option "%s" is not supported.', $constantName ?? $opt));
+ }
+ }
+
+ return $pushedResponse ?? new CurlResponse($this->multi, $ch, $options, $this->logger, $method, self::createRedirectResolver($options, $host), CurlClientState::$curlVersion['version_number']);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function stream($responses, float $timeout = null): ResponseStreamInterface
+ {
+ if ($responses instanceof CurlResponse) {
+ $responses = [$responses];
+ } elseif (!is_iterable($responses)) {
+ throw new \TypeError(sprintf('"%s()" expects parameter 1 to be an iterable of CurlResponse objects, "%s" given.', __METHOD__, get_debug_type($responses)));
+ }
+
+ if (\is_resource($this->multi->handle) || $this->multi->handle instanceof \CurlMultiHandle) {
+ $active = 0;
+ while (\CURLM_CALL_MULTI_PERFORM === curl_multi_exec($this->multi->handle, $active)) {
+ }
+ }
+
+ return new ResponseStream(CurlResponse::stream($responses, $timeout));
+ }
+
+ public function reset()
+ {
+ $this->multi->reset();
+ }
+
+ /**
+ * Accepts pushed responses only if their headers related to authentication match the request.
+ */
+ private static function acceptPushForRequest(string $method, array $options, PushedResponse $pushedResponse): bool
+ {
+ if ('' !== $options['body'] || $method !== $pushedResponse->requestHeaders[':method'][0]) {
+ return false;
+ }
+
+ foreach (['proxy', 'no_proxy', 'bindto', 'local_cert', 'local_pk'] as $k) {
+ if ($options[$k] !== $pushedResponse->parentOptions[$k]) {
+ return false;
+ }
+ }
+
+ foreach (['authorization', 'cookie', 'range', 'proxy-authorization'] as $k) {
+ $normalizedHeaders = $options['normalized_headers'][$k] ?? [];
+ foreach ($normalizedHeaders as $i => $v) {
+ $normalizedHeaders[$i] = substr($v, \strlen($k) + 2);
+ }
+
+ if (($pushedResponse->requestHeaders[$k] ?? []) !== $normalizedHeaders) {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ /**
+ * Wraps the request's body callback to allow it to return strings longer than curl requested.
+ */
+ private static function readRequestBody(int $length, \Closure $body, string &$buffer, bool &$eof): string
+ {
+ if (!$eof && \strlen($buffer) < $length) {
+ if (!\is_string($data = $body($length))) {
+ throw new TransportException(sprintf('The return value of the "body" option callback must be a string, "%s" returned.', get_debug_type($data)));
+ }
+
+ $buffer .= $data;
+ $eof = '' === $data;
+ }
+
+ $data = substr($buffer, 0, $length);
+ $buffer = substr($buffer, $length);
+
+ return $data;
+ }
+
+ /**
+ * Resolves relative URLs on redirects and deals with authentication headers.
+ *
+ * Work around CVE-2018-1000007: Authorization and Cookie headers should not follow redirects - fixed in Curl 7.64
+ */
+ private static function createRedirectResolver(array $options, string $host): \Closure
+ {
+ $redirectHeaders = [];
+ if (0 < $options['max_redirects']) {
+ $redirectHeaders['host'] = $host;
+ $redirectHeaders['with_auth'] = $redirectHeaders['no_auth'] = array_filter($options['headers'], static function ($h) {
+ return 0 !== stripos($h, 'Host:');
+ });
+
+ if (isset($options['normalized_headers']['authorization'][0]) || isset($options['normalized_headers']['cookie'][0])) {
+ $redirectHeaders['no_auth'] = array_filter($options['headers'], static function ($h) {
+ return 0 !== stripos($h, 'Authorization:') && 0 !== stripos($h, 'Cookie:');
+ });
+ }
+ }
+
+ return static function ($ch, string $location, bool $noContent) use (&$redirectHeaders, $options) {
+ try {
+ $location = self::parseUrl($location);
+ } catch (InvalidArgumentException $e) {
+ return null;
+ }
+
+ if ($noContent && $redirectHeaders) {
+ $filterContentHeaders = static function ($h) {
+ return 0 !== stripos($h, 'Content-Length:') && 0 !== stripos($h, 'Content-Type:') && 0 !== stripos($h, 'Transfer-Encoding:');
+ };
+ $redirectHeaders['no_auth'] = array_filter($redirectHeaders['no_auth'], $filterContentHeaders);
+ $redirectHeaders['with_auth'] = array_filter($redirectHeaders['with_auth'], $filterContentHeaders);
+ }
+
+ if ($redirectHeaders && $host = parse_url('http:'.$location['authority'], \PHP_URL_HOST)) {
+ $requestHeaders = $redirectHeaders['host'] === $host ? $redirectHeaders['with_auth'] : $redirectHeaders['no_auth'];
+ curl_setopt($ch, \CURLOPT_HTTPHEADER, $requestHeaders);
+ } elseif ($noContent && $redirectHeaders) {
+ curl_setopt($ch, \CURLOPT_HTTPHEADER, $redirectHeaders['with_auth']);
+ }
+
+ $url = self::parseUrl(curl_getinfo($ch, \CURLINFO_EFFECTIVE_URL));
+ $url = self::resolveUrl($location, $url);
+
+ curl_setopt($ch, \CURLOPT_PROXY, self::getProxyUrl($options['proxy'], $url));
+
+ return implode('', $url);
+ };
+ }
+
+ private function findConstantName(int $opt): ?string
+ {
+ $constants = array_filter(get_defined_constants(), static function ($v, $k) use ($opt) {
+ return $v === $opt && 'C' === $k[0] && (str_starts_with($k, 'CURLOPT_') || str_starts_with($k, 'CURLINFO_'));
+ }, \ARRAY_FILTER_USE_BOTH);
+
+ return key($constants);
+ }
+
+ /**
+ * Prevents overriding options that are set internally throughout the request.
+ */
+ private function validateExtraCurlOptions(array $options): void
+ {
+ $curloptsToConfig = [
+ // options used in CurlHttpClient
+ \CURLOPT_HTTPAUTH => 'auth_ntlm',
+ \CURLOPT_USERPWD => 'auth_ntlm',
+ \CURLOPT_RESOLVE => 'resolve',
+ \CURLOPT_NOSIGNAL => 'timeout',
+ \CURLOPT_HTTPHEADER => 'headers',
+ \CURLOPT_INFILE => 'body',
+ \CURLOPT_READFUNCTION => 'body',
+ \CURLOPT_INFILESIZE => 'body',
+ \CURLOPT_POSTFIELDS => 'body',
+ \CURLOPT_UPLOAD => 'body',
+ \CURLOPT_INTERFACE => 'bindto',
+ \CURLOPT_TIMEOUT_MS => 'max_duration',
+ \CURLOPT_TIMEOUT => 'max_duration',
+ \CURLOPT_MAXREDIRS => 'max_redirects',
+ \CURLOPT_POSTREDIR => 'max_redirects',
+ \CURLOPT_PROXY => 'proxy',
+ \CURLOPT_NOPROXY => 'no_proxy',
+ \CURLOPT_SSL_VERIFYPEER => 'verify_peer',
+ \CURLOPT_SSL_VERIFYHOST => 'verify_host',
+ \CURLOPT_CAINFO => 'cafile',
+ \CURLOPT_CAPATH => 'capath',
+ \CURLOPT_SSL_CIPHER_LIST => 'ciphers',
+ \CURLOPT_SSLCERT => 'local_cert',
+ \CURLOPT_SSLKEY => 'local_pk',
+ \CURLOPT_KEYPASSWD => 'passphrase',
+ \CURLOPT_CERTINFO => 'capture_peer_cert_chain',
+ \CURLOPT_USERAGENT => 'normalized_headers',
+ \CURLOPT_REFERER => 'headers',
+ // options used in CurlResponse
+ \CURLOPT_NOPROGRESS => 'on_progress',
+ \CURLOPT_PROGRESSFUNCTION => 'on_progress',
+ ];
+
+ if (\defined('CURLOPT_UNIX_SOCKET_PATH')) {
+ $curloptsToConfig[\CURLOPT_UNIX_SOCKET_PATH] = 'bindto';
+ }
+
+ if (\defined('CURLOPT_PINNEDPUBLICKEY')) {
+ $curloptsToConfig[\CURLOPT_PINNEDPUBLICKEY] = 'peer_fingerprint';
+ }
+
+ $curloptsToCheck = [
+ \CURLOPT_PRIVATE,
+ \CURLOPT_HEADERFUNCTION,
+ \CURLOPT_WRITEFUNCTION,
+ \CURLOPT_VERBOSE,
+ \CURLOPT_STDERR,
+ \CURLOPT_RETURNTRANSFER,
+ \CURLOPT_URL,
+ \CURLOPT_FOLLOWLOCATION,
+ \CURLOPT_HEADER,
+ \CURLOPT_CONNECTTIMEOUT,
+ \CURLOPT_CONNECTTIMEOUT_MS,
+ \CURLOPT_HTTP_VERSION,
+ \CURLOPT_PORT,
+ \CURLOPT_DNS_USE_GLOBAL_CACHE,
+ \CURLOPT_PROTOCOLS,
+ \CURLOPT_REDIR_PROTOCOLS,
+ \CURLOPT_COOKIEFILE,
+ \CURLINFO_REDIRECT_COUNT,
+ ];
+
+ if (\defined('CURLOPT_HTTP09_ALLOWED')) {
+ $curloptsToCheck[] = \CURLOPT_HTTP09_ALLOWED;
+ }
+
+ if (\defined('CURLOPT_HEADEROPT')) {
+ $curloptsToCheck[] = \CURLOPT_HEADEROPT;
+ }
+
+ $methodOpts = [
+ \CURLOPT_POST,
+ \CURLOPT_PUT,
+ \CURLOPT_CUSTOMREQUEST,
+ \CURLOPT_HTTPGET,
+ \CURLOPT_NOBODY,
+ ];
+
+ foreach ($options as $opt => $optValue) {
+ if (isset($curloptsToConfig[$opt])) {
+ $constName = $this->findConstantName($opt) ?? $opt;
+ throw new InvalidArgumentException(sprintf('Cannot set "%s" with "extra.curl", use option "%s" instead.', $constName, $curloptsToConfig[$opt]));
+ }
+
+ if (\in_array($opt, $methodOpts)) {
+ throw new InvalidArgumentException('The HTTP method cannot be overridden using "extra.curl".');
+ }
+
+ if (\in_array($opt, $curloptsToCheck)) {
+ $constName = $this->findConstantName($opt) ?? $opt;
+ throw new InvalidArgumentException(sprintf('Cannot set "%s" with "extra.curl".', $constName));
+ }
+ }
+ }
+}
diff --git a/src/vendor/symfony/http-client/DataCollector/HttpClientDataCollector.php b/src/vendor/symfony/http-client/DataCollector/HttpClientDataCollector.php
new file mode 100644
index 0000000..1925786
--- /dev/null
+++ b/src/vendor/symfony/http-client/DataCollector/HttpClientDataCollector.php
@@ -0,0 +1,176 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\HttpClient\DataCollector;
+
+use Symfony\Component\HttpClient\TraceableHttpClient;
+use Symfony\Component\HttpFoundation\Request;
+use Symfony\Component\HttpFoundation\Response;
+use Symfony\Component\HttpKernel\DataCollector\DataCollector;
+use Symfony\Component\HttpKernel\DataCollector\LateDataCollectorInterface;
+use Symfony\Component\VarDumper\Caster\ImgStub;
+
+/**
+ * @author Jérémy Romey
+ */
+trait DecoratorTrait
+{
+ private $client;
+
+ public function __construct(HttpClientInterface $client = null)
+ {
+ $this->client = $client ?? HttpClient::create();
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function request(string $method, string $url, array $options = []): ResponseInterface
+ {
+ return $this->client->request($method, $url, $options);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function stream($responses, float $timeout = null): ResponseStreamInterface
+ {
+ return $this->client->stream($responses, $timeout);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function withOptions(array $options): self
+ {
+ $clone = clone $this;
+ $clone->client = $this->client->withOptions($options);
+
+ return $clone;
+ }
+
+ public function reset()
+ {
+ if ($this->client instanceof ResetInterface) {
+ $this->client->reset();
+ }
+ }
+}
diff --git a/src/vendor/symfony/http-client/DependencyInjection/HttpClientPass.php b/src/vendor/symfony/http-client/DependencyInjection/HttpClientPass.php
new file mode 100644
index 0000000..8f3c3c5
--- /dev/null
+++ b/src/vendor/symfony/http-client/DependencyInjection/HttpClientPass.php
@@ -0,0 +1,51 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\HttpClient\DependencyInjection;
+
+use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
+use Symfony\Component\DependencyInjection\ContainerBuilder;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+use Symfony\Component\DependencyInjection\Reference;
+use Symfony\Component\HttpClient\TraceableHttpClient;
+
+final class HttpClientPass implements CompilerPassInterface
+{
+ private $clientTag;
+
+ public function __construct(string $clientTag = 'http_client.client')
+ {
+ if (0 < \func_num_args()) {
+ trigger_deprecation('symfony/http-client', '5.3', 'Configuring "%s" is deprecated.', __CLASS__);
+ }
+
+ $this->clientTag = $clientTag;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function process(ContainerBuilder $container)
+ {
+ if (!$container->hasDefinition('data_collector.http_client')) {
+ return;
+ }
+
+ foreach ($container->findTaggedServiceIds($this->clientTag) as $id => $tags) {
+ $container->register('.debug.'.$id, TraceableHttpClient::class)
+ ->setArguments([new Reference('.debug.'.$id.'.inner'), new Reference('debug.stopwatch', ContainerInterface::IGNORE_ON_INVALID_REFERENCE)])
+ ->addTag('kernel.reset', ['method' => 'reset'])
+ ->setDecoratedService($id, null, 5);
+ $container->getDefinition('data_collector.http_client')
+ ->addMethodCall('registerClient', [$id, new Reference('.debug.'.$id)]);
+ }
+ }
+}
diff --git a/src/vendor/symfony/http-client/EventSourceHttpClient.php b/src/vendor/symfony/http-client/EventSourceHttpClient.php
new file mode 100644
index 0000000..60e4e82
--- /dev/null
+++ b/src/vendor/symfony/http-client/EventSourceHttpClient.php
@@ -0,0 +1,159 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\HttpClient;
+
+use Symfony\Component\HttpClient\Chunk\ServerSentEvent;
+use Symfony\Component\HttpClient\Exception\EventSourceException;
+use Symfony\Component\HttpClient\Response\AsyncContext;
+use Symfony\Component\HttpClient\Response\AsyncResponse;
+use Symfony\Contracts\HttpClient\ChunkInterface;
+use Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface;
+use Symfony\Contracts\HttpClient\HttpClientInterface;
+use Symfony\Contracts\HttpClient\ResponseInterface;
+use Symfony\Contracts\Service\ResetInterface;
+
+/**
+ * @author Antoine Bluchet
+ */
+final class EventSourceHttpClient implements HttpClientInterface, ResetInterface
+{
+ use AsyncDecoratorTrait, HttpClientTrait {
+ AsyncDecoratorTrait::withOptions insteadof HttpClientTrait;
+ }
+
+ private $reconnectionTime;
+
+ public function __construct(HttpClientInterface $client = null, float $reconnectionTime = 10.0)
+ {
+ $this->client = $client ?? HttpClient::create();
+ $this->reconnectionTime = $reconnectionTime;
+ }
+
+ public function connect(string $url, array $options = []): ResponseInterface
+ {
+ return $this->request('GET', $url, self::mergeDefaultOptions($options, [
+ 'buffer' => false,
+ 'headers' => [
+ 'Accept' => 'text/event-stream',
+ 'Cache-Control' => 'no-cache',
+ ],
+ ], true));
+ }
+
+ public function request(string $method, string $url, array $options = []): ResponseInterface
+ {
+ $state = new class() {
+ public $buffer = null;
+ public $lastEventId = null;
+ public $reconnectionTime;
+ public $lastError = null;
+ };
+ $state->reconnectionTime = $this->reconnectionTime;
+
+ if ($accept = self::normalizeHeaders($options['headers'] ?? [])['accept'] ?? []) {
+ $state->buffer = \in_array($accept, [['Accept: text/event-stream'], ['accept: text/event-stream']], true) ? '' : null;
+
+ if (null !== $state->buffer) {
+ $options['extra']['trace_content'] = false;
+ }
+ }
+
+ return new AsyncResponse($this->client, $method, $url, $options, static function (ChunkInterface $chunk, AsyncContext $context) use ($state, $method, $url, $options) {
+ if (null !== $state->buffer) {
+ $context->setInfo('reconnection_time', $state->reconnectionTime);
+ $isTimeout = false;
+ }
+ $lastError = $state->lastError;
+ $state->lastError = null;
+
+ try {
+ $isTimeout = $chunk->isTimeout();
+
+ if (null !== $chunk->getInformationalStatus() || $context->getInfo('canceled')) {
+ yield $chunk;
+
+ return;
+ }
+ } catch (TransportExceptionInterface $e) {
+ $state->lastError = $lastError ?? microtime(true);
+
+ if (null === $state->buffer || ($isTimeout && microtime(true) - $state->lastError < $state->reconnectionTime)) {
+ yield $chunk;
+ } else {
+ $options['headers']['Last-Event-ID'] = $state->lastEventId;
+ $state->buffer = '';
+ $state->lastError = microtime(true);
+ $context->getResponse()->cancel();
+ $context->replaceRequest($method, $url, $options);
+ if ($isTimeout) {
+ yield $chunk;
+ } else {
+ $context->pause($state->reconnectionTime);
+ }
+ }
+
+ return;
+ }
+
+ if ($chunk->isFirst()) {
+ if (preg_match('/^text\/event-stream(;|$)/i', $context->getHeaders()['content-type'][0] ?? '')) {
+ $state->buffer = '';
+ } elseif (null !== $lastError || (null !== $state->buffer && 200 === $context->getStatusCode())) {
+ throw new EventSourceException(sprintf('Response content-type is "%s" while "text/event-stream" was expected for "%s".', $context->getHeaders()['content-type'][0] ?? '', $context->getInfo('url')));
+ } else {
+ $context->passthru();
+ }
+
+ if (null === $lastError) {
+ yield $chunk;
+ }
+
+ return;
+ }
+
+ $rx = '/((?:\r\n|[\r\n]){2,})/';
+ $content = $state->buffer.$chunk->getContent();
+
+ if ($chunk->isLast()) {
+ $rx = substr_replace($rx, '|$', -2, 0);
+ }
+ $events = preg_split($rx, $content, -1, \PREG_SPLIT_DELIM_CAPTURE);
+ $state->buffer = array_pop($events);
+
+ for ($i = 0; isset($events[$i]); $i += 2) {
+ $event = new ServerSentEvent($events[$i].$events[1 + $i]);
+
+ if ('' !== $event->getId()) {
+ $context->setInfo('last_event_id', $state->lastEventId = $event->getId());
+ }
+
+ if ($event->getRetry()) {
+ $context->setInfo('reconnection_time', $state->reconnectionTime = $event->getRetry());
+ }
+
+ yield $event;
+ }
+
+ if (preg_match('/^(?::[^\r\n]*+(?:\r\n|[\r\n]))+$/m', $state->buffer)) {
+ $content = $state->buffer;
+ $state->buffer = '';
+
+ yield $context->createChunk($content);
+ }
+
+ if ($chunk->isLast()) {
+ yield $chunk;
+ }
+ });
+ }
+}
diff --git a/src/vendor/symfony/http-client/Exception/ClientException.php b/src/vendor/symfony/http-client/Exception/ClientException.php
new file mode 100644
index 0000000..4264534
--- /dev/null
+++ b/src/vendor/symfony/http-client/Exception/ClientException.php
@@ -0,0 +1,24 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\HttpClient\Exception;
+
+use Symfony\Contracts\HttpClient\Exception\ClientExceptionInterface;
+
+/**
+ * Represents a 4xx response.
+ *
+ * @author Nicolas Grekas
+ */
+final class ClientException extends \RuntimeException implements ClientExceptionInterface
+{
+ use HttpExceptionTrait;
+}
diff --git a/src/vendor/symfony/http-client/Exception/EventSourceException.php b/src/vendor/symfony/http-client/Exception/EventSourceException.php
new file mode 100644
index 0000000..30ab795
--- /dev/null
+++ b/src/vendor/symfony/http-client/Exception/EventSourceException.php
@@ -0,0 +1,21 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\HttpClient\Exception;
+
+use Symfony\Contracts\HttpClient\Exception\DecodingExceptionInterface;
+
+/**
+ * @author Nicolas Grekas
+ */
+final class EventSourceException extends \RuntimeException implements DecodingExceptionInterface
+{
+}
diff --git a/src/vendor/symfony/http-client/Exception/HttpExceptionTrait.php b/src/vendor/symfony/http-client/Exception/HttpExceptionTrait.php
new file mode 100644
index 0000000..8cbaa1c
--- /dev/null
+++ b/src/vendor/symfony/http-client/Exception/HttpExceptionTrait.php
@@ -0,0 +1,78 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\HttpClient\Exception;
+
+use Symfony\Contracts\HttpClient\ResponseInterface;
+
+/**
+ * @author Nicolas Grekas
+ *
+ * @internal
+ */
+trait HttpExceptionTrait
+{
+ private $response;
+
+ public function __construct(ResponseInterface $response)
+ {
+ $this->response = $response;
+ $code = $response->getInfo('http_code');
+ $url = $response->getInfo('url');
+ $message = sprintf('HTTP %d returned for "%s".', $code, $url);
+
+ $httpCodeFound = false;
+ $isJson = false;
+ foreach (array_reverse($response->getInfo('response_headers')) as $h) {
+ if (str_starts_with($h, 'HTTP/')) {
+ if ($httpCodeFound) {
+ break;
+ }
+
+ $message = sprintf('%s returned for "%s".', $h, $url);
+ $httpCodeFound = true;
+ }
+
+ if (0 === stripos($h, 'content-type:')) {
+ if (preg_match('/\bjson\b/i', $h)) {
+ $isJson = true;
+ }
+
+ if ($httpCodeFound) {
+ break;
+ }
+ }
+ }
+
+ // Try to guess a better error message using common API error formats
+ // The MIME type isn't explicitly checked because some formats inherit from others
+ // Ex: JSON:API follows RFC 7807 semantics, Hydra can be used in any JSON-LD-compatible format
+ if ($isJson && $body = json_decode($response->getContent(false), true)) {
+ if (isset($body['hydra:title']) || isset($body['hydra:description'])) {
+ // see http://www.hydra-cg.com/spec/latest/core/#description-of-http-status-codes-and-errors
+ $separator = isset($body['hydra:title'], $body['hydra:description']) ? "\n\n" : '';
+ $message = ($body['hydra:title'] ?? '').$separator.($body['hydra:description'] ?? '');
+ } elseif ((isset($body['title']) || isset($body['detail']))
+ && (\is_scalar($body['title'] ?? '') && \is_scalar($body['detail'] ?? ''))) {
+ // see RFC 7807 and https://jsonapi.org/format/#error-objects
+ $separator = isset($body['title'], $body['detail']) ? "\n\n" : '';
+ $message = ($body['title'] ?? '').$separator.($body['detail'] ?? '');
+ }
+ }
+
+ parent::__construct($message, $code);
+ }
+
+ public function getResponse(): ResponseInterface
+ {
+ return $this->response;
+ }
+}
diff --git a/src/vendor/symfony/http-client/Exception/InvalidArgumentException.php b/src/vendor/symfony/http-client/Exception/InvalidArgumentException.php
new file mode 100644
index 0000000..6c2fae7
--- /dev/null
+++ b/src/vendor/symfony/http-client/Exception/InvalidArgumentException.php
@@ -0,0 +1,21 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\HttpClient\Exception;
+
+use Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface;
+
+/**
+ * @author Nicolas Grekas
+ */
+final class InvalidArgumentException extends \InvalidArgumentException implements TransportExceptionInterface
+{
+}
diff --git a/src/vendor/symfony/http-client/Exception/JsonException.php b/src/vendor/symfony/http-client/Exception/JsonException.php
new file mode 100644
index 0000000..54502e6
--- /dev/null
+++ b/src/vendor/symfony/http-client/Exception/JsonException.php
@@ -0,0 +1,23 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\HttpClient\Exception;
+
+use Symfony\Contracts\HttpClient\Exception\DecodingExceptionInterface;
+
+/**
+ * Thrown by responses' toArray() method when their content cannot be JSON-decoded.
+ *
+ * @author Nicolas Grekas
+ */
+final class JsonException extends \JsonException implements DecodingExceptionInterface
+{
+}
diff --git a/src/vendor/symfony/http-client/Exception/RedirectionException.php b/src/vendor/symfony/http-client/Exception/RedirectionException.php
new file mode 100644
index 0000000..5b93670
--- /dev/null
+++ b/src/vendor/symfony/http-client/Exception/RedirectionException.php
@@ -0,0 +1,24 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\HttpClient\Exception;
+
+use Symfony\Contracts\HttpClient\Exception\RedirectionExceptionInterface;
+
+/**
+ * Represents a 3xx response.
+ *
+ * @author Nicolas Grekas
+ */
+final class RedirectionException extends \RuntimeException implements RedirectionExceptionInterface
+{
+ use HttpExceptionTrait;
+}
diff --git a/src/vendor/symfony/http-client/Exception/ServerException.php b/src/vendor/symfony/http-client/Exception/ServerException.php
new file mode 100644
index 0000000..c6f8273
--- /dev/null
+++ b/src/vendor/symfony/http-client/Exception/ServerException.php
@@ -0,0 +1,24 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\HttpClient\Exception;
+
+use Symfony\Contracts\HttpClient\Exception\ServerExceptionInterface;
+
+/**
+ * Represents a 5xx response.
+ *
+ * @author Nicolas Grekas
+ */
+final class ServerException extends \RuntimeException implements ServerExceptionInterface
+{
+ use HttpExceptionTrait;
+}
diff --git a/src/vendor/symfony/http-client/Exception/TimeoutException.php b/src/vendor/symfony/http-client/Exception/TimeoutException.php
new file mode 100644
index 0000000..a9155cc
--- /dev/null
+++ b/src/vendor/symfony/http-client/Exception/TimeoutException.php
@@ -0,0 +1,21 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\HttpClient\Exception;
+
+use Symfony\Contracts\HttpClient\Exception\TimeoutExceptionInterface;
+
+/**
+ * @author Nicolas Grekas
+ */
+final class TimeoutException extends TransportException implements TimeoutExceptionInterface
+{
+}
diff --git a/src/vendor/symfony/http-client/Exception/TransportException.php b/src/vendor/symfony/http-client/Exception/TransportException.php
new file mode 100644
index 0000000..a3a80c6
--- /dev/null
+++ b/src/vendor/symfony/http-client/Exception/TransportException.php
@@ -0,0 +1,21 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\HttpClient\Exception;
+
+use Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface;
+
+/**
+ * @author Nicolas Grekas
+ */
+class TransportException extends \RuntimeException implements TransportExceptionInterface
+{
+}
diff --git a/src/vendor/symfony/http-client/HttpClient.php b/src/vendor/symfony/http-client/HttpClient.php
new file mode 100644
index 0000000..8de6f9f
--- /dev/null
+++ b/src/vendor/symfony/http-client/HttpClient.php
@@ -0,0 +1,79 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\HttpClient;
+
+use Amp\Http\Client\Connection\ConnectionLimitingPool;
+use Amp\Promise;
+use Symfony\Contracts\HttpClient\HttpClientInterface;
+
+/**
+ * A factory to instantiate the best possible HTTP client for the runtime.
+ *
+ * @author Nicolas Grekas
+ */
+final class HttpClient
+{
+ /**
+ * @param array $defaultOptions Default request's options
+ * @param int $maxHostConnections The maximum number of connections to a single host
+ * @param int $maxPendingPushes The maximum number of pushed responses to accept in the queue
+ *
+ * @see HttpClientInterface::OPTIONS_DEFAULTS for available options
+ */
+ public static function create(array $defaultOptions = [], int $maxHostConnections = 6, int $maxPendingPushes = 50): HttpClientInterface
+ {
+ if ($amp = class_exists(ConnectionLimitingPool::class) && interface_exists(Promise::class)) {
+ if (!\extension_loaded('curl')) {
+ return new AmpHttpClient($defaultOptions, null, $maxHostConnections, $maxPendingPushes);
+ }
+
+ // Skip curl when HTTP/2 push is unsupported or buggy, see https://bugs.php.net/77535
+ if (\PHP_VERSION_ID < 70217 || (\PHP_VERSION_ID >= 70300 && \PHP_VERSION_ID < 70304) || !\defined('CURLMOPT_PUSHFUNCTION')) {
+ return new AmpHttpClient($defaultOptions, null, $maxHostConnections, $maxPendingPushes);
+ }
+
+ static $curlVersion = null;
+ $curlVersion = $curlVersion ?? curl_version();
+
+ // HTTP/2 push crashes before curl 7.61
+ if (0x073D00 > $curlVersion['version_number'] || !(\CURL_VERSION_HTTP2 & $curlVersion['features'])) {
+ return new AmpHttpClient($defaultOptions, null, $maxHostConnections, $maxPendingPushes);
+ }
+ }
+
+ if (\extension_loaded('curl')) {
+ if ('\\' !== \DIRECTORY_SEPARATOR || isset($defaultOptions['cafile']) || isset($defaultOptions['capath']) || \ini_get('curl.cainfo') || \ini_get('openssl.cafile') || \ini_get('openssl.capath')) {
+ return new CurlHttpClient($defaultOptions, $maxHostConnections, $maxPendingPushes);
+ }
+
+ @trigger_error('Configure the "curl.cainfo", "openssl.cafile" or "openssl.capath" php.ini setting to enable the CurlHttpClient', \E_USER_WARNING);
+ }
+
+ if ($amp) {
+ return new AmpHttpClient($defaultOptions, null, $maxHostConnections, $maxPendingPushes);
+ }
+
+ @trigger_error((\extension_loaded('curl') ? 'Upgrade' : 'Install').' the curl extension or run "composer require amphp/http-client:^4.2.1" to perform async HTTP operations, including full HTTP/2 support', \E_USER_NOTICE);
+
+ return new NativeHttpClient($defaultOptions, $maxHostConnections);
+ }
+
+ /**
+ * Creates a client that adds options (e.g. authentication headers) only when the request URL matches the provided base URI.
+ */
+ public static function createForBaseUri(string $baseUri, array $defaultOptions = [], int $maxHostConnections = 6, int $maxPendingPushes = 50): HttpClientInterface
+ {
+ $client = self::create([], $maxHostConnections, $maxPendingPushes);
+
+ return ScopingHttpClient::forBaseUri($client, $baseUri, $defaultOptions);
+ }
+}
diff --git a/src/vendor/symfony/http-client/HttpClientTrait.php b/src/vendor/symfony/http-client/HttpClientTrait.php
new file mode 100644
index 0000000..3d60443
--- /dev/null
+++ b/src/vendor/symfony/http-client/HttpClientTrait.php
@@ -0,0 +1,710 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\HttpClient;
+
+use Symfony\Component\HttpClient\Exception\InvalidArgumentException;
+use Symfony\Component\HttpClient\Exception\TransportException;
+
+/**
+ * Provides the common logic from writing HttpClientInterface implementations.
+ *
+ * All private methods are static to prevent implementers from creating memory leaks via circular references.
+ *
+ * @author Nicolas Grekas
+ */
+trait HttpClientTrait
+{
+ private static $CHUNK_SIZE = 16372;
+
+ /**
+ * {@inheritdoc}
+ */
+ public function withOptions(array $options): self
+ {
+ $clone = clone $this;
+ $clone->defaultOptions = self::mergeDefaultOptions($options, $this->defaultOptions);
+
+ return $clone;
+ }
+
+ /**
+ * Validates and normalizes method, URL and options, and merges them with defaults.
+ *
+ * @throws InvalidArgumentException When a not-supported option is found
+ */
+ private static function prepareRequest(?string $method, ?string $url, array $options, array $defaultOptions = [], bool $allowExtraOptions = false): array
+ {
+ if (null !== $method) {
+ if (\strlen($method) !== strspn($method, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ')) {
+ throw new InvalidArgumentException(sprintf('Invalid HTTP method "%s", only uppercase letters are accepted.', $method));
+ }
+ if (!$method) {
+ throw new InvalidArgumentException('The HTTP method cannot be empty.');
+ }
+ }
+
+ $options = self::mergeDefaultOptions($options, $defaultOptions, $allowExtraOptions);
+
+ $buffer = $options['buffer'] ?? true;
+
+ if ($buffer instanceof \Closure) {
+ $options['buffer'] = static function (array $headers) use ($buffer) {
+ if (!\is_bool($buffer = $buffer($headers))) {
+ if (!\is_array($bufferInfo = @stream_get_meta_data($buffer))) {
+ throw new \LogicException(sprintf('The closure passed as option "buffer" must return bool or stream resource, got "%s".', get_debug_type($buffer)));
+ }
+
+ if (false === strpbrk($bufferInfo['mode'], 'acew+')) {
+ throw new \LogicException(sprintf('The stream returned by the closure passed as option "buffer" must be writeable, got mode "%s".', $bufferInfo['mode']));
+ }
+ }
+
+ return $buffer;
+ };
+ } elseif (!\is_bool($buffer)) {
+ if (!\is_array($bufferInfo = @stream_get_meta_data($buffer))) {
+ throw new InvalidArgumentException(sprintf('Option "buffer" must be bool, stream resource or Closure, "%s" given.', get_debug_type($buffer)));
+ }
+
+ if (false === strpbrk($bufferInfo['mode'], 'acew+')) {
+ throw new InvalidArgumentException(sprintf('The stream in option "buffer" must be writeable, mode "%s" given.', $bufferInfo['mode']));
+ }
+ }
+
+ if (isset($options['json'])) {
+ if (isset($options['body']) && '' !== $options['body']) {
+ throw new InvalidArgumentException('Define either the "json" or the "body" option, setting both is not supported.');
+ }
+ $options['body'] = self::jsonEncode($options['json']);
+ unset($options['json']);
+
+ if (!isset($options['normalized_headers']['content-type'])) {
+ $options['normalized_headers']['content-type'] = ['Content-Type: application/json'];
+ }
+ }
+
+ if (!isset($options['normalized_headers']['accept'])) {
+ $options['normalized_headers']['accept'] = ['Accept: */*'];
+ }
+
+ if (isset($options['body'])) {
+ $options['body'] = self::normalizeBody($options['body']);
+
+ if (\is_string($options['body'])
+ && (string) \strlen($options['body']) !== substr($h = $options['normalized_headers']['content-length'][0] ?? '', 16)
+ && ('' !== $h || '' !== $options['body'])
+ ) {
+ if ('chunked' === substr($options['normalized_headers']['transfer-encoding'][0] ?? '', \strlen('Transfer-Encoding: '))) {
+ unset($options['normalized_headers']['transfer-encoding']);
+ $options['body'] = self::dechunk($options['body']);
+ }
+
+ $options['normalized_headers']['content-length'] = [substr_replace($h ?: 'Content-Length: ', \strlen($options['body']), 16)];
+ }
+ }
+
+ if (isset($options['peer_fingerprint'])) {
+ $options['peer_fingerprint'] = self::normalizePeerFingerprint($options['peer_fingerprint']);
+ }
+
+ // Validate on_progress
+ if (isset($options['on_progress']) && !\is_callable($onProgress = $options['on_progress'])) {
+ throw new InvalidArgumentException(sprintf('Option "on_progress" must be callable, "%s" given.', get_debug_type($onProgress)));
+ }
+
+ if (\is_array($options['auth_basic'] ?? null)) {
+ $count = \count($options['auth_basic']);
+ if ($count <= 0 || $count > 2) {
+ throw new InvalidArgumentException(sprintf('Option "auth_basic" must contain 1 or 2 elements, "%s" given.', $count));
+ }
+
+ $options['auth_basic'] = implode(':', $options['auth_basic']);
+ }
+
+ if (!\is_string($options['auth_basic'] ?? '')) {
+ throw new InvalidArgumentException(sprintf('Option "auth_basic" must be string or an array, "%s" given.', get_debug_type($options['auth_basic'])));
+ }
+
+ if (isset($options['auth_bearer'])) {
+ if (!\is_string($options['auth_bearer'])) {
+ throw new InvalidArgumentException(sprintf('Option "auth_bearer" must be a string, "%s" given.', get_debug_type($options['auth_bearer'])));
+ }
+ if (preg_match('{[^\x21-\x7E]}', $options['auth_bearer'])) {
+ throw new InvalidArgumentException('Invalid character found in option "auth_bearer": '.json_encode($options['auth_bearer']).'.');
+ }
+ }
+
+ if (isset($options['auth_basic'], $options['auth_bearer'])) {
+ throw new InvalidArgumentException('Define either the "auth_basic" or the "auth_bearer" option, setting both is not supported.');
+ }
+
+ if (null !== $url) {
+ // Merge auth with headers
+ if (($options['auth_basic'] ?? false) && !($options['normalized_headers']['authorization'] ?? false)) {
+ $options['normalized_headers']['authorization'] = ['Authorization: Basic '.base64_encode($options['auth_basic'])];
+ }
+ // Merge bearer with headers
+ if (($options['auth_bearer'] ?? false) && !($options['normalized_headers']['authorization'] ?? false)) {
+ $options['normalized_headers']['authorization'] = ['Authorization: Bearer '.$options['auth_bearer']];
+ }
+
+ unset($options['auth_basic'], $options['auth_bearer']);
+
+ // Parse base URI
+ if (\is_string($options['base_uri'])) {
+ $options['base_uri'] = self::parseUrl($options['base_uri']);
+ }
+
+ // Validate and resolve URL
+ $url = self::parseUrl($url, $options['query']);
+ $url = self::resolveUrl($url, $options['base_uri'], $defaultOptions['query'] ?? []);
+ }
+
+ // Finalize normalization of options
+ $options['http_version'] = (string) ($options['http_version'] ?? '') ?: null;
+ if (0 > $options['timeout'] = (float) ($options['timeout'] ?? \ini_get('default_socket_timeout'))) {
+ $options['timeout'] = 172800.0; // 2 days
+ }
+
+ $options['max_duration'] = isset($options['max_duration']) ? (float) $options['max_duration'] : 0;
+ $options['headers'] = array_merge(...array_values($options['normalized_headers']));
+
+ return [$url, $options];
+ }
+
+ /**
+ * @throws InvalidArgumentException When an invalid option is found
+ */
+ private static function mergeDefaultOptions(array $options, array $defaultOptions, bool $allowExtraOptions = false): array
+ {
+ $options['normalized_headers'] = self::normalizeHeaders($options['headers'] ?? []);
+
+ if ($defaultOptions['headers'] ?? false) {
+ $options['normalized_headers'] += self::normalizeHeaders($defaultOptions['headers']);
+ }
+
+ $options['headers'] = array_merge(...array_values($options['normalized_headers']) ?: [[]]);
+
+ if ($resolve = $options['resolve'] ?? false) {
+ $options['resolve'] = [];
+ foreach ($resolve as $k => $v) {
+ $options['resolve'][substr(self::parseUrl('http://'.$k)['authority'], 2)] = (string) $v;
+ }
+ }
+
+ // Option "query" is never inherited from defaults
+ $options['query'] = $options['query'] ?? [];
+
+ $options += $defaultOptions;
+
+ if (isset(self::$emptyDefaults)) {
+ foreach (self::$emptyDefaults as $k => $v) {
+ if (!isset($options[$k])) {
+ $options[$k] = $v;
+ }
+ }
+ }
+
+ if (isset($defaultOptions['extra'])) {
+ $options['extra'] += $defaultOptions['extra'];
+ }
+
+ if ($resolve = $defaultOptions['resolve'] ?? false) {
+ foreach ($resolve as $k => $v) {
+ $options['resolve'] += [substr(self::parseUrl('http://'.$k)['authority'], 2) => (string) $v];
+ }
+ }
+
+ if ($allowExtraOptions || !$defaultOptions) {
+ return $options;
+ }
+
+ // Look for unsupported options
+ foreach ($options as $name => $v) {
+ if (\array_key_exists($name, $defaultOptions) || 'normalized_headers' === $name) {
+ continue;
+ }
+
+ if ('auth_ntlm' === $name) {
+ if (!\extension_loaded('curl')) {
+ $msg = 'try installing the "curl" extension to use "%s" instead.';
+ } else {
+ $msg = 'try using "%s" instead.';
+ }
+
+ throw new InvalidArgumentException(sprintf('Option "auth_ntlm" is not supported by "%s", '.$msg, __CLASS__, CurlHttpClient::class));
+ }
+
+ $alternatives = [];
+
+ foreach ($defaultOptions as $k => $v) {
+ if (levenshtein($name, $k) <= \strlen($name) / 3 || str_contains($k, $name)) {
+ $alternatives[] = $k;
+ }
+ }
+
+ throw new InvalidArgumentException(sprintf('Unsupported option "%s" passed to "%s", did you mean "%s"?', $name, __CLASS__, implode('", "', $alternatives ?: array_keys($defaultOptions))));
+ }
+
+ return $options;
+ }
+
+ /**
+ * @return string[][]
+ *
+ * @throws InvalidArgumentException When an invalid header is found
+ */
+ private static function normalizeHeaders(array $headers): array
+ {
+ $normalizedHeaders = [];
+
+ foreach ($headers as $name => $values) {
+ if (\is_object($values) && method_exists($values, '__toString')) {
+ $values = (string) $values;
+ }
+
+ if (\is_int($name)) {
+ if (!\is_string($values)) {
+ throw new InvalidArgumentException(sprintf('Invalid value for header "%s": expected string, "%s" given.', $name, get_debug_type($values)));
+ }
+ [$name, $values] = explode(':', $values, 2);
+ $values = [ltrim($values)];
+ } elseif (!is_iterable($values)) {
+ if (\is_object($values)) {
+ throw new InvalidArgumentException(sprintf('Invalid value for header "%s": expected string, "%s" given.', $name, get_debug_type($values)));
+ }
+
+ $values = (array) $values;
+ }
+
+ $lcName = strtolower($name);
+ $normalizedHeaders[$lcName] = [];
+
+ foreach ($values as $value) {
+ $normalizedHeaders[$lcName][] = $value = $name.': '.$value;
+
+ if (\strlen($value) !== strcspn($value, "\r\n\0")) {
+ throw new InvalidArgumentException(sprintf('Invalid header: CR/LF/NUL found in "%s".', $value));
+ }
+ }
+ }
+
+ return $normalizedHeaders;
+ }
+
+ /**
+ * @param array|string|resource|\Traversable|\Closure $body
+ *
+ * @return string|resource|\Closure
+ *
+ * @throws InvalidArgumentException When an invalid body is passed
+ */
+ private static function normalizeBody($body)
+ {
+ if (\is_array($body)) {
+ array_walk_recursive($body, $caster = static function (&$v) use (&$caster) {
+ if (\is_object($v)) {
+ if ($vars = get_object_vars($v)) {
+ array_walk_recursive($vars, $caster);
+ $v = $vars;
+ } elseif (method_exists($v, '__toString')) {
+ $v = (string) $v;
+ }
+ }
+ });
+
+ return http_build_query($body, '', '&');
+ }
+
+ if (\is_string($body)) {
+ return $body;
+ }
+
+ $generatorToCallable = static function (\Generator $body): \Closure {
+ return static function () use ($body) {
+ while ($body->valid()) {
+ $chunk = $body->current();
+ $body->next();
+
+ if ('' !== $chunk) {
+ return $chunk;
+ }
+ }
+
+ return '';
+ };
+ };
+
+ if ($body instanceof \Generator) {
+ return $generatorToCallable($body);
+ }
+
+ if ($body instanceof \Traversable) {
+ return $generatorToCallable((static function ($body) { yield from $body; })($body));
+ }
+
+ if ($body instanceof \Closure) {
+ $r = new \ReflectionFunction($body);
+ $body = $r->getClosure();
+
+ if ($r->isGenerator()) {
+ $body = $body(self::$CHUNK_SIZE);
+
+ return $generatorToCallable($body);
+ }
+
+ return $body;
+ }
+
+ if (!\is_array(@stream_get_meta_data($body))) {
+ throw new InvalidArgumentException(sprintf('Option "body" must be string, stream resource, iterable or callable, "%s" given.', get_debug_type($body)));
+ }
+
+ return $body;
+ }
+
+ private static function dechunk(string $body): string
+ {
+ $h = fopen('php://temp', 'w+');
+ stream_filter_append($h, 'dechunk', \STREAM_FILTER_WRITE);
+ fwrite($h, $body);
+ $body = stream_get_contents($h, -1, 0);
+ rewind($h);
+ ftruncate($h, 0);
+
+ if (fwrite($h, '-') && '' !== stream_get_contents($h, -1, 0)) {
+ throw new TransportException('Request body has broken chunked encoding.');
+ }
+
+ return $body;
+ }
+
+ /**
+ * @param string|string[] $fingerprint
+ *
+ * @throws InvalidArgumentException When an invalid fingerprint is passed
+ */
+ private static function normalizePeerFingerprint($fingerprint): array
+ {
+ if (\is_string($fingerprint)) {
+ switch (\strlen($fingerprint = str_replace(':', '', $fingerprint))) {
+ case 32: $fingerprint = ['md5' => $fingerprint]; break;
+ case 40: $fingerprint = ['sha1' => $fingerprint]; break;
+ case 44: $fingerprint = ['pin-sha256' => [$fingerprint]]; break;
+ case 64: $fingerprint = ['sha256' => $fingerprint]; break;
+ default: throw new InvalidArgumentException(sprintf('Cannot auto-detect fingerprint algorithm for "%s".', $fingerprint));
+ }
+ } elseif (\is_array($fingerprint)) {
+ foreach ($fingerprint as $algo => $hash) {
+ $fingerprint[$algo] = 'pin-sha256' === $algo ? (array) $hash : str_replace(':', '', $hash);
+ }
+ } else {
+ throw new InvalidArgumentException(sprintf('Option "peer_fingerprint" must be string or array, "%s" given.', get_debug_type($fingerprint)));
+ }
+
+ return $fingerprint;
+ }
+
+ /**
+ * @param mixed $value
+ *
+ * @throws InvalidArgumentException When the value cannot be json-encoded
+ */
+ private static function jsonEncode($value, int $flags = null, int $maxDepth = 512): string
+ {
+ $flags = $flags ?? (\JSON_HEX_TAG | \JSON_HEX_APOS | \JSON_HEX_AMP | \JSON_HEX_QUOT | \JSON_PRESERVE_ZERO_FRACTION);
+
+ try {
+ $value = json_encode($value, $flags | (\PHP_VERSION_ID >= 70300 ? \JSON_THROW_ON_ERROR : 0), $maxDepth);
+ } catch (\JsonException $e) {
+ throw new InvalidArgumentException('Invalid value for "json" option: '.$e->getMessage());
+ }
+
+ if (\PHP_VERSION_ID < 70300 && \JSON_ERROR_NONE !== json_last_error() && (false === $value || !($flags & \JSON_PARTIAL_OUTPUT_ON_ERROR))) {
+ throw new InvalidArgumentException('Invalid value for "json" option: '.json_last_error_msg());
+ }
+
+ return $value;
+ }
+
+ /**
+ * Resolves a URL against a base URI.
+ *
+ * @see https://tools.ietf.org/html/rfc3986#section-5.2.2
+ *
+ * @throws InvalidArgumentException When an invalid URL is passed
+ */
+ private static function resolveUrl(array $url, ?array $base, array $queryDefaults = []): array
+ {
+ if (null !== $base && '' === ($base['scheme'] ?? '').($base['authority'] ?? '')) {
+ throw new InvalidArgumentException(sprintf('Invalid "base_uri" option: host or scheme is missing in "%s".', implode('', $base)));
+ }
+
+ if (null === $url['scheme'] && (null === $base || null === $base['scheme'])) {
+ throw new InvalidArgumentException(sprintf('Invalid URL: scheme is missing in "%s". Did you forget to add "http(s)://"?', implode('', $base ?? $url)));
+ }
+
+ if (null === $base && '' === $url['scheme'].$url['authority']) {
+ throw new InvalidArgumentException(sprintf('Invalid URL: no "base_uri" option was provided and host or scheme is missing in "%s".', implode('', $url)));
+ }
+
+ if (null !== $url['scheme']) {
+ $url['path'] = self::removeDotSegments($url['path'] ?? '');
+ } else {
+ if (null !== $url['authority']) {
+ $url['path'] = self::removeDotSegments($url['path'] ?? '');
+ } else {
+ if (null === $url['path']) {
+ $url['path'] = $base['path'];
+ $url['query'] = $url['query'] ?? $base['query'];
+ } else {
+ if ('/' !== $url['path'][0]) {
+ if (null === $base['path']) {
+ $url['path'] = '/'.$url['path'];
+ } else {
+ $segments = explode('/', $base['path']);
+ array_splice($segments, -1, 1, [$url['path']]);
+ $url['path'] = implode('/', $segments);
+ }
+ }
+
+ $url['path'] = self::removeDotSegments($url['path']);
+ }
+
+ $url['authority'] = $base['authority'];
+
+ if ($queryDefaults) {
+ $url['query'] = '?'.self::mergeQueryString(substr($url['query'] ?? '', 1), $queryDefaults, false);
+ }
+ }
+
+ $url['scheme'] = $base['scheme'];
+ }
+
+ if ('' === ($url['path'] ?? '')) {
+ $url['path'] = '/';
+ }
+
+ if ('?' === ($url['query'] ?? '')) {
+ $url['query'] = null;
+ }
+
+ return $url;
+ }
+
+ /**
+ * Parses a URL and fixes its encoding if needed.
+ *
+ * @throws InvalidArgumentException When an invalid URL is passed
+ */
+ private static function parseUrl(string $url, array $query = [], array $allowedSchemes = ['http' => 80, 'https' => 443]): array
+ {
+ if (false === $parts = parse_url($url)) {
+ throw new InvalidArgumentException(sprintf('Malformed URL "%s".', $url));
+ }
+
+ if ($query) {
+ $parts['query'] = self::mergeQueryString($parts['query'] ?? null, $query, true);
+ }
+
+ $port = $parts['port'] ?? 0;
+
+ if (null !== $scheme = $parts['scheme'] ?? null) {
+ if (!isset($allowedSchemes[$scheme = strtolower($scheme)])) {
+ throw new InvalidArgumentException(sprintf('Unsupported scheme in "%s".', $url));
+ }
+
+ $port = $allowedSchemes[$scheme] === $port ? 0 : $port;
+ $scheme .= ':';
+ }
+
+ if (null !== $host = $parts['host'] ?? null) {
+ if (!\defined('INTL_IDNA_VARIANT_UTS46') && preg_match('/[\x80-\xFF]/', $host)) {
+ throw new InvalidArgumentException(sprintf('Unsupported IDN "%s", try enabling the "intl" PHP extension or running "composer require symfony/polyfill-intl-idn".', $host));
+ }
+
+ $host = \defined('INTL_IDNA_VARIANT_UTS46') ? idn_to_ascii($host, \IDNA_DEFAULT | \IDNA_USE_STD3_RULES | \IDNA_CHECK_BIDI | \IDNA_CHECK_CONTEXTJ | \IDNA_NONTRANSITIONAL_TO_ASCII, \INTL_IDNA_VARIANT_UTS46) ?: strtolower($host) : strtolower($host);
+ $host .= $port ? ':'.$port : '';
+ }
+
+ foreach (['user', 'pass', 'path', 'query', 'fragment'] as $part) {
+ if (!isset($parts[$part])) {
+ continue;
+ }
+
+ if (str_contains($parts[$part], '%')) {
+ // https://tools.ietf.org/html/rfc3986#section-2.3
+ $parts[$part] = preg_replace_callback('/%(?:2[DE]|3[0-9]|[46][1-9A-F]|5F|[57][0-9A]|7E)++/i', function ($m) { return rawurldecode($m[0]); }, $parts[$part]);
+ }
+
+ // https://tools.ietf.org/html/rfc3986#section-3.3
+ $parts[$part] = preg_replace_callback("#[^-A-Za-z0-9._~!$&/'()[\]*+,;=:@{}%]++#", function ($m) { return rawurlencode($m[0]); }, $parts[$part]);
+ }
+
+ return [
+ 'scheme' => $scheme,
+ 'authority' => null !== $host ? '//'.(isset($parts['user']) ? $parts['user'].(isset($parts['pass']) ? ':'.$parts['pass'] : '').'@' : '').$host : null,
+ 'path' => isset($parts['path'][0]) ? $parts['path'] : null,
+ 'query' => isset($parts['query']) ? '?'.$parts['query'] : null,
+ 'fragment' => isset($parts['fragment']) ? '#'.$parts['fragment'] : null,
+ ];
+ }
+
+ /**
+ * Removes dot-segments from a path.
+ *
+ * @see https://tools.ietf.org/html/rfc3986#section-5.2.4
+ */
+ private static function removeDotSegments(string $path)
+ {
+ $result = '';
+
+ while (!\in_array($path, ['', '.', '..'], true)) {
+ if ('.' === $path[0] && (str_starts_with($path, $p = '../') || str_starts_with($path, $p = './'))) {
+ $path = substr($path, \strlen($p));
+ } elseif ('/.' === $path || str_starts_with($path, '/./')) {
+ $path = substr_replace($path, '/', 0, 3);
+ } elseif ('/..' === $path || str_starts_with($path, '/../')) {
+ $i = strrpos($result, '/');
+ $result = $i ? substr($result, 0, $i) : '';
+ $path = substr_replace($path, '/', 0, 4);
+ } else {
+ $i = strpos($path, '/', 1) ?: \strlen($path);
+ $result .= substr($path, 0, $i);
+ $path = substr($path, $i);
+ }
+ }
+
+ return $result;
+ }
+
+ /**
+ * Merges and encodes a query array with a query string.
+ *
+ * @throws InvalidArgumentException When an invalid query-string value is passed
+ */
+ private static function mergeQueryString(?string $queryString, array $queryArray, bool $replace): ?string
+ {
+ if (!$queryArray) {
+ return $queryString;
+ }
+
+ $query = [];
+
+ if (null !== $queryString) {
+ foreach (explode('&', $queryString) as $v) {
+ if ('' !== $v) {
+ $k = urldecode(explode('=', $v, 2)[0]);
+ $query[$k] = (isset($query[$k]) ? $query[$k].'&' : '').$v;
+ }
+ }
+ }
+
+ if ($replace) {
+ foreach ($queryArray as $k => $v) {
+ if (null === $v) {
+ unset($query[$k]);
+ }
+ }
+ }
+
+ $queryString = http_build_query($queryArray, '', '&', \PHP_QUERY_RFC3986);
+ $queryArray = [];
+
+ if ($queryString) {
+ if (str_contains($queryString, '%')) {
+ // https://tools.ietf.org/html/rfc3986#section-2.3 + some chars not encoded by browsers
+ $queryString = strtr($queryString, [
+ '%21' => '!',
+ '%24' => '$',
+ '%28' => '(',
+ '%29' => ')',
+ '%2A' => '*',
+ '%2F' => '/',
+ '%3A' => ':',
+ '%3B' => ';',
+ '%40' => '@',
+ '%5B' => '[',
+ '%5D' => ']',
+ ]);
+ }
+
+ foreach (explode('&', $queryString) as $v) {
+ $queryArray[rawurldecode(explode('=', $v, 2)[0])] = $v;
+ }
+ }
+
+ return implode('&', $replace ? array_replace($query, $queryArray) : ($query + $queryArray));
+ }
+
+ /**
+ * Loads proxy configuration from the same environment variables as curl when no proxy is explicitly set.
+ */
+ private static function getProxy(?string $proxy, array $url, ?string $noProxy): ?array
+ {
+ if (null === $proxy = self::getProxyUrl($proxy, $url)) {
+ return null;
+ }
+
+ $proxy = (parse_url($proxy) ?: []) + ['scheme' => 'http'];
+
+ if (!isset($proxy['host'])) {
+ throw new TransportException('Invalid HTTP proxy: host is missing.');
+ }
+
+ if ('http' === $proxy['scheme']) {
+ $proxyUrl = 'tcp://'.$proxy['host'].':'.($proxy['port'] ?? '80');
+ } elseif ('https' === $proxy['scheme']) {
+ $proxyUrl = 'ssl://'.$proxy['host'].':'.($proxy['port'] ?? '443');
+ } else {
+ throw new TransportException(sprintf('Unsupported proxy scheme "%s": "http" or "https" expected.', $proxy['scheme']));
+ }
+
+ $noProxy = $noProxy ?? $_SERVER['no_proxy'] ?? $_SERVER['NO_PROXY'] ?? '';
+ $noProxy = $noProxy ? preg_split('/[\s,]+/', $noProxy) : [];
+
+ return [
+ 'url' => $proxyUrl,
+ 'auth' => isset($proxy['user']) ? 'Basic '.base64_encode(rawurldecode($proxy['user']).':'.rawurldecode($proxy['pass'] ?? '')) : null,
+ 'no_proxy' => $noProxy,
+ ];
+ }
+
+ private static function getProxyUrl(?string $proxy, array $url): ?string
+ {
+ if (null !== $proxy) {
+ return $proxy;
+ }
+
+ // Ignore HTTP_PROXY except on the CLI to work around httpoxy set of vulnerabilities
+ $proxy = $_SERVER['http_proxy'] ?? (\in_array(\PHP_SAPI, ['cli', 'phpdbg'], true) ? $_SERVER['HTTP_PROXY'] ?? null : null) ?? $_SERVER['all_proxy'] ?? $_SERVER['ALL_PROXY'] ?? null;
+
+ if ('https:' === $url['scheme']) {
+ $proxy = $_SERVER['https_proxy'] ?? $_SERVER['HTTPS_PROXY'] ?? $proxy;
+ }
+
+ return $proxy;
+ }
+
+ private static function shouldBuffer(array $headers): bool
+ {
+ if (null === $contentType = $headers['content-type'][0] ?? null) {
+ return false;
+ }
+
+ if (false !== $i = strpos($contentType, ';')) {
+ $contentType = substr($contentType, 0, $i);
+ }
+
+ return $contentType && preg_match('#^(?:text/|application/(?:.+\+)?(?:json|xml)$)#i', $contentType);
+ }
+}
diff --git a/src/vendor/symfony/http-client/HttpOptions.php b/src/vendor/symfony/http-client/HttpOptions.php
new file mode 100644
index 0000000..da55f99
--- /dev/null
+++ b/src/vendor/symfony/http-client/HttpOptions.php
@@ -0,0 +1,331 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\HttpClient;
+
+use Symfony\Contracts\HttpClient\HttpClientInterface;
+
+/**
+ * A helper providing autocompletion for available options.
+ *
+ * @see HttpClientInterface for a description of each options.
+ *
+ * @author Nicolas Grekas
+ */
+class HttpOptions
+{
+ private $options = [];
+
+ public function toArray(): array
+ {
+ return $this->options;
+ }
+
+ /**
+ * @return $this
+ */
+ public function setAuthBasic(string $user, string $password = '')
+ {
+ $this->options['auth_basic'] = $user;
+
+ if ('' !== $password) {
+ $this->options['auth_basic'] .= ':'.$password;
+ }
+
+ return $this;
+ }
+
+ /**
+ * @return $this
+ */
+ public function setAuthBearer(string $token)
+ {
+ $this->options['auth_bearer'] = $token;
+
+ return $this;
+ }
+
+ /**
+ * @return $this
+ */
+ public function setQuery(array $query)
+ {
+ $this->options['query'] = $query;
+
+ return $this;
+ }
+
+ /**
+ * @return $this
+ */
+ public function setHeaders(iterable $headers)
+ {
+ $this->options['headers'] = $headers;
+
+ return $this;
+ }
+
+ /**
+ * @param array|string|resource|\Traversable|\Closure $body
+ *
+ * @return $this
+ */
+ public function setBody($body)
+ {
+ $this->options['body'] = $body;
+
+ return $this;
+ }
+
+ /**
+ * @param mixed $json
+ *
+ * @return $this
+ */
+ public function setJson($json)
+ {
+ $this->options['json'] = $json;
+
+ return $this;
+ }
+
+ /**
+ * @return $this
+ */
+ public function setUserData($data)
+ {
+ $this->options['user_data'] = $data;
+
+ return $this;
+ }
+
+ /**
+ * @return $this
+ */
+ public function setMaxRedirects(int $max)
+ {
+ $this->options['max_redirects'] = $max;
+
+ return $this;
+ }
+
+ /**
+ * @return $this
+ */
+ public function setHttpVersion(string $version)
+ {
+ $this->options['http_version'] = $version;
+
+ return $this;
+ }
+
+ /**
+ * @return $this
+ */
+ public function setBaseUri(string $uri)
+ {
+ $this->options['base_uri'] = $uri;
+
+ return $this;
+ }
+
+ /**
+ * @return $this
+ */
+ public function buffer(bool $buffer)
+ {
+ $this->options['buffer'] = $buffer;
+
+ return $this;
+ }
+
+ /**
+ * @return $this
+ */
+ public function setOnProgress(callable $callback)
+ {
+ $this->options['on_progress'] = $callback;
+
+ return $this;
+ }
+
+ /**
+ * @return $this
+ */
+ public function resolve(array $hostIps)
+ {
+ $this->options['resolve'] = $hostIps;
+
+ return $this;
+ }
+
+ /**
+ * @return $this
+ */
+ public function setProxy(string $proxy)
+ {
+ $this->options['proxy'] = $proxy;
+
+ return $this;
+ }
+
+ /**
+ * @return $this
+ */
+ public function setNoProxy(string $noProxy)
+ {
+ $this->options['no_proxy'] = $noProxy;
+
+ return $this;
+ }
+
+ /**
+ * @return $this
+ */
+ public function setTimeout(float $timeout)
+ {
+ $this->options['timeout'] = $timeout;
+
+ return $this;
+ }
+
+ /**
+ * @return $this
+ */
+ public function setMaxDuration(float $maxDuration)
+ {
+ $this->options['max_duration'] = $maxDuration;
+
+ return $this;
+ }
+
+ /**
+ * @return $this
+ */
+ public function bindTo(string $bindto)
+ {
+ $this->options['bindto'] = $bindto;
+
+ return $this;
+ }
+
+ /**
+ * @return $this
+ */
+ public function verifyPeer(bool $verify)
+ {
+ $this->options['verify_peer'] = $verify;
+
+ return $this;
+ }
+
+ /**
+ * @return $this
+ */
+ public function verifyHost(bool $verify)
+ {
+ $this->options['verify_host'] = $verify;
+
+ return $this;
+ }
+
+ /**
+ * @return $this
+ */
+ public function setCaFile(string $cafile)
+ {
+ $this->options['cafile'] = $cafile;
+
+ return $this;
+ }
+
+ /**
+ * @return $this
+ */
+ public function setCaPath(string $capath)
+ {
+ $this->options['capath'] = $capath;
+
+ return $this;
+ }
+
+ /**
+ * @return $this
+ */
+ public function setLocalCert(string $cert)
+ {
+ $this->options['local_cert'] = $cert;
+
+ return $this;
+ }
+
+ /**
+ * @return $this
+ */
+ public function setLocalPk(string $pk)
+ {
+ $this->options['local_pk'] = $pk;
+
+ return $this;
+ }
+
+ /**
+ * @return $this
+ */
+ public function setPassphrase(string $passphrase)
+ {
+ $this->options['passphrase'] = $passphrase;
+
+ return $this;
+ }
+
+ /**
+ * @return $this
+ */
+ public function setCiphers(string $ciphers)
+ {
+ $this->options['ciphers'] = $ciphers;
+
+ return $this;
+ }
+
+ /**
+ * @param string|array $fingerprint
+ *
+ * @return $this
+ */
+ public function setPeerFingerprint($fingerprint)
+ {
+ $this->options['peer_fingerprint'] = $fingerprint;
+
+ return $this;
+ }
+
+ /**
+ * @return $this
+ */
+ public function capturePeerCertChain(bool $capture)
+ {
+ $this->options['capture_peer_cert_chain'] = $capture;
+
+ return $this;
+ }
+
+ /**
+ * @return $this
+ */
+ public function setExtra(string $name, $value)
+ {
+ $this->options['extra'][$name] = $value;
+
+ return $this;
+ }
+}
diff --git a/src/vendor/symfony/http-client/HttplugClient.php b/src/vendor/symfony/http-client/HttplugClient.php
new file mode 100644
index 0000000..2d9eec3
--- /dev/null
+++ b/src/vendor/symfony/http-client/HttplugClient.php
@@ -0,0 +1,276 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\HttpClient;
+
+use GuzzleHttp\Promise\Promise as GuzzlePromise;
+use GuzzleHttp\Promise\RejectedPromise;
+use GuzzleHttp\Promise\Utils;
+use Http\Client\Exception\NetworkException;
+use Http\Client\Exception\RequestException;
+use Http\Client\HttpAsyncClient;
+use Http\Client\HttpClient as HttplugInterface;
+use Http\Discovery\Exception\NotFoundException;
+use Http\Discovery\Psr17FactoryDiscovery;
+use Http\Message\RequestFactory;
+use Http\Message\StreamFactory;
+use Http\Message\UriFactory;
+use Http\Promise\Promise;
+use Nyholm\Psr7\Factory\Psr17Factory;
+use Nyholm\Psr7\Request;
+use Nyholm\Psr7\Uri;
+use Psr\Http\Message\RequestFactoryInterface;
+use Psr\Http\Message\RequestInterface;
+use Psr\Http\Message\ResponseFactoryInterface;
+use Psr\Http\Message\ResponseInterface as Psr7ResponseInterface;
+use Psr\Http\Message\StreamFactoryInterface;
+use Psr\Http\Message\StreamInterface;
+use Psr\Http\Message\UriFactoryInterface;
+use Psr\Http\Message\UriInterface;
+use Symfony\Component\HttpClient\Internal\HttplugWaitLoop;
+use Symfony\Component\HttpClient\Response\HttplugPromise;
+use Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface;
+use Symfony\Contracts\HttpClient\HttpClientInterface;
+use Symfony\Contracts\HttpClient\ResponseInterface;
+use Symfony\Contracts\Service\ResetInterface;
+
+if (!interface_exists(HttplugInterface::class)) {
+ throw new \LogicException('You cannot use "Symfony\Component\HttpClient\HttplugClient" as the "php-http/httplug" package is not installed. Try running "composer require php-http/httplug".');
+}
+
+if (!interface_exists(RequestFactory::class)) {
+ throw new \LogicException('You cannot use "Symfony\Component\HttpClient\HttplugClient" as the "php-http/message-factory" package is not installed. Try running "composer require php-http/message-factory".');
+}
+
+/**
+ * An adapter to turn a Symfony HttpClientInterface into an Httplug client.
+ *
+ * Run "composer require nyholm/psr7" to install an efficient implementation of response
+ * and stream factories with flex-provided autowiring aliases.
+ *
+ * @author Nicolas Grekas
+ */
+final class HttplugClient implements HttplugInterface, HttpAsyncClient, RequestFactory, StreamFactory, UriFactory, ResetInterface
+{
+ private $client;
+ private $responseFactory;
+ private $streamFactory;
+
+ /**
+ * @var \SplObjectStorage
+ *
+ * @internal
+ */
+class AmpBody implements RequestBody, InputStream
+{
+ private $body;
+ private $info;
+ private $onProgress;
+ private $offset = 0;
+ private $length = -1;
+ private $uploaded;
+
+ public function __construct($body, &$info, \Closure $onProgress)
+ {
+ $this->body = $body;
+ $this->info = &$info;
+ $this->onProgress = $onProgress;
+
+ if (\is_resource($body)) {
+ $this->offset = ftell($body);
+ $this->length = fstat($body)['size'];
+ $this->body = new ResourceInputStream($body);
+ } elseif (\is_string($body)) {
+ $this->length = \strlen($body);
+ }
+ }
+
+ public function createBodyStream(): InputStream
+ {
+ if (null !== $this->uploaded) {
+ $this->uploaded = null;
+
+ if (\is_string($this->body)) {
+ $this->offset = 0;
+ } elseif ($this->body instanceof ResourceInputStream) {
+ fseek($this->body->getResource(), $this->offset);
+ }
+ }
+
+ return $this;
+ }
+
+ public function getHeaders(): Promise
+ {
+ return new Success([]);
+ }
+
+ public function getBodyLength(): Promise
+ {
+ return new Success($this->length - $this->offset);
+ }
+
+ public function read(): Promise
+ {
+ $this->info['size_upload'] += $this->uploaded;
+ $this->uploaded = 0;
+ ($this->onProgress)();
+
+ $chunk = $this->doRead();
+ $chunk->onResolve(function ($e, $data) {
+ if (null !== $data) {
+ $this->uploaded = \strlen($data);
+ } else {
+ $this->info['upload_content_length'] = $this->info['size_upload'];
+ }
+ });
+
+ return $chunk;
+ }
+
+ public static function rewind(RequestBody $body): RequestBody
+ {
+ if (!$body instanceof self) {
+ return $body;
+ }
+
+ $body->uploaded = null;
+
+ if ($body->body instanceof ResourceInputStream) {
+ fseek($body->body->getResource(), $body->offset);
+
+ return new $body($body->body, $body->info, $body->onProgress);
+ }
+
+ if (\is_string($body->body)) {
+ $body->offset = 0;
+ }
+
+ return $body;
+ }
+
+ private function doRead(): Promise
+ {
+ if ($this->body instanceof ResourceInputStream) {
+ return $this->body->read();
+ }
+
+ if (null === $this->offset || !$this->length) {
+ return new Success();
+ }
+
+ if (\is_string($this->body)) {
+ $this->offset = null;
+
+ return new Success($this->body);
+ }
+
+ if ('' === $data = ($this->body)(16372)) {
+ $this->offset = null;
+
+ return new Success();
+ }
+
+ if (!\is_string($data)) {
+ throw new TransportException(sprintf('Return value of the "body" option callback must be string, "%s" returned.', get_debug_type($data)));
+ }
+
+ return new Success($data);
+ }
+}
diff --git a/src/vendor/symfony/http-client/Internal/AmpClientState.php b/src/vendor/symfony/http-client/Internal/AmpClientState.php
new file mode 100644
index 0000000..3061f08
--- /dev/null
+++ b/src/vendor/symfony/http-client/Internal/AmpClientState.php
@@ -0,0 +1,217 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\HttpClient\Internal;
+
+use Amp\CancellationToken;
+use Amp\Deferred;
+use Amp\Http\Client\Connection\ConnectionLimitingPool;
+use Amp\Http\Client\Connection\DefaultConnectionFactory;
+use Amp\Http\Client\InterceptedHttpClient;
+use Amp\Http\Client\Interceptor\RetryRequests;
+use Amp\Http\Client\PooledHttpClient;
+use Amp\Http\Client\Request;
+use Amp\Http\Client\Response;
+use Amp\Http\Tunnel\Http1TunnelConnector;
+use Amp\Http\Tunnel\Https1TunnelConnector;
+use Amp\Promise;
+use Amp\Socket\Certificate;
+use Amp\Socket\ClientTlsContext;
+use Amp\Socket\ConnectContext;
+use Amp\Socket\Connector;
+use Amp\Socket\DnsConnector;
+use Amp\Socket\SocketAddress;
+use Amp\Success;
+use Psr\Log\LoggerInterface;
+
+/**
+ * Internal representation of the Amp client's state.
+ *
+ * @author Nicolas Grekas
+ *
+ * @internal
+ */
+final class AmpClientState extends ClientState
+{
+ public $dnsCache = [];
+ public $responseCount = 0;
+ public $pushedResponses = [];
+
+ private $clients = [];
+ private $clientConfigurator;
+ private $maxHostConnections;
+ private $maxPendingPushes;
+ private $logger;
+
+ public function __construct(?callable $clientConfigurator, int $maxHostConnections, int $maxPendingPushes, ?LoggerInterface &$logger)
+ {
+ $this->clientConfigurator = $clientConfigurator ?? static function (PooledHttpClient $client) {
+ return new InterceptedHttpClient($client, new RetryRequests(2));
+ };
+ $this->maxHostConnections = $maxHostConnections;
+ $this->maxPendingPushes = $maxPendingPushes;
+ $this->logger = &$logger;
+ }
+
+ /**
+ * @return Promise
+ *
+ * @internal
+ */
+class AmpListener implements EventListener
+{
+ private $info;
+ private $pinSha256;
+ private $onProgress;
+ private $handle;
+
+ public function __construct(array &$info, array $pinSha256, \Closure $onProgress, &$handle)
+ {
+ $info += [
+ 'connect_time' => 0.0,
+ 'pretransfer_time' => 0.0,
+ 'starttransfer_time' => 0.0,
+ 'total_time' => 0.0,
+ 'namelookup_time' => 0.0,
+ 'primary_ip' => '',
+ 'primary_port' => 0,
+ ];
+
+ $this->info = &$info;
+ $this->pinSha256 = $pinSha256;
+ $this->onProgress = $onProgress;
+ $this->handle = &$handle;
+ }
+
+ public function startRequest(Request $request): Promise
+ {
+ $this->info['start_time'] = $this->info['start_time'] ?? microtime(true);
+ ($this->onProgress)();
+
+ return new Success();
+ }
+
+ public function startDnsResolution(Request $request): Promise
+ {
+ ($this->onProgress)();
+
+ return new Success();
+ }
+
+ public function startConnectionCreation(Request $request): Promise
+ {
+ ($this->onProgress)();
+
+ return new Success();
+ }
+
+ public function startTlsNegotiation(Request $request): Promise
+ {
+ ($this->onProgress)();
+
+ return new Success();
+ }
+
+ public function startSendingRequest(Request $request, Stream $stream): Promise
+ {
+ $host = $stream->getRemoteAddress()->getHost();
+
+ if (false !== strpos($host, ':')) {
+ $host = '['.$host.']';
+ }
+
+ $this->info['primary_ip'] = $host;
+ $this->info['primary_port'] = $stream->getRemoteAddress()->getPort();
+ $this->info['pretransfer_time'] = microtime(true) - $this->info['start_time'];
+ $this->info['debug'] .= sprintf("* Connected to %s (%s) port %d\n", $request->getUri()->getHost(), $host, $this->info['primary_port']);
+
+ if ((isset($this->info['peer_certificate_chain']) || $this->pinSha256) && null !== $tlsInfo = $stream->getTlsInfo()) {
+ foreach ($tlsInfo->getPeerCertificates() as $cert) {
+ $this->info['peer_certificate_chain'][] = openssl_x509_read($cert->toPem());
+ }
+
+ if ($this->pinSha256) {
+ $pin = openssl_pkey_get_public($this->info['peer_certificate_chain'][0]);
+ $pin = openssl_pkey_get_details($pin)['key'];
+ $pin = \array_slice(explode("\n", $pin), 1, -2);
+ $pin = base64_decode(implode('', $pin));
+ $pin = base64_encode(hash('sha256', $pin, true));
+
+ if (!\in_array($pin, $this->pinSha256, true)) {
+ throw new TransportException(sprintf('SSL public key does not match pinned public key for "%s".', $this->info['url']));
+ }
+ }
+ }
+ ($this->onProgress)();
+
+ $uri = $request->getUri();
+ $requestUri = $uri->getPath() ?: '/';
+
+ if ('' !== $query = $uri->getQuery()) {
+ $requestUri .= '?'.$query;
+ }
+
+ if ('CONNECT' === $method = $request->getMethod()) {
+ $requestUri = $uri->getHost().': '.($uri->getPort() ?? ('https' === $uri->getScheme() ? 443 : 80));
+ }
+
+ $this->info['debug'] .= sprintf("> %s %s HTTP/%s \r\n", $method, $requestUri, $request->getProtocolVersions()[0]);
+
+ foreach ($request->getRawHeaders() as [$name, $value]) {
+ $this->info['debug'] .= $name.': '.$value."\r\n";
+ }
+ $this->info['debug'] .= "\r\n";
+
+ return new Success();
+ }
+
+ public function completeSendingRequest(Request $request, Stream $stream): Promise
+ {
+ ($this->onProgress)();
+
+ return new Success();
+ }
+
+ public function startReceivingResponse(Request $request, Stream $stream): Promise
+ {
+ $this->info['starttransfer_time'] = microtime(true) - $this->info['start_time'];
+ ($this->onProgress)();
+
+ return new Success();
+ }
+
+ public function completeReceivingResponse(Request $request, Stream $stream): Promise
+ {
+ $this->handle = null;
+ ($this->onProgress)();
+
+ return new Success();
+ }
+
+ public function completeDnsResolution(Request $request): Promise
+ {
+ $this->info['namelookup_time'] = microtime(true) - $this->info['start_time'];
+ ($this->onProgress)();
+
+ return new Success();
+ }
+
+ public function completeConnectionCreation(Request $request): Promise
+ {
+ $this->info['connect_time'] = microtime(true) - $this->info['start_time'];
+ ($this->onProgress)();
+
+ return new Success();
+ }
+
+ public function completeTlsNegotiation(Request $request): Promise
+ {
+ ($this->onProgress)();
+
+ return new Success();
+ }
+
+ public function abort(Request $request, \Throwable $cause): Promise
+ {
+ return new Success();
+ }
+}
diff --git a/src/vendor/symfony/http-client/Internal/AmpResolver.php b/src/vendor/symfony/http-client/Internal/AmpResolver.php
new file mode 100644
index 0000000..d31476a
--- /dev/null
+++ b/src/vendor/symfony/http-client/Internal/AmpResolver.php
@@ -0,0 +1,52 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\HttpClient\Internal;
+
+use Amp\Dns;
+use Amp\Dns\Record;
+use Amp\Promise;
+use Amp\Success;
+
+/**
+ * Handles local overrides for the DNS resolver.
+ *
+ * @author Nicolas Grekas
+ *
+ * @internal
+ */
+class AmpResolver implements Dns\Resolver
+{
+ private $dnsMap;
+
+ public function __construct(array &$dnsMap)
+ {
+ $this->dnsMap = &$dnsMap;
+ }
+
+ public function resolve(string $name, int $typeRestriction = null): Promise
+ {
+ if (!isset($this->dnsMap[$name]) || !\in_array($typeRestriction, [Record::A, null], true)) {
+ return Dns\resolver()->resolve($name, $typeRestriction);
+ }
+
+ return new Success([new Record($this->dnsMap[$name], Record::A, null)]);
+ }
+
+ public function query(string $name, int $type): Promise
+ {
+ if (!isset($this->dnsMap[$name]) || Record::A !== $type) {
+ return Dns\resolver()->query($name, $type);
+ }
+
+ return new Success([new Record($this->dnsMap[$name], Record::A, null)]);
+ }
+}
diff --git a/src/vendor/symfony/http-client/Internal/Canary.php b/src/vendor/symfony/http-client/Internal/Canary.php
new file mode 100644
index 0000000..3d14b5f
--- /dev/null
+++ b/src/vendor/symfony/http-client/Internal/Canary.php
@@ -0,0 +1,40 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\HttpClient\Internal;
+
+/**
+ * @author Nicolas Grekas
+ *
+ * @internal
+ */
+final class Canary
+{
+ private $canceller;
+
+ public function __construct(\Closure $canceller)
+ {
+ $this->canceller = $canceller;
+ }
+
+ public function cancel()
+ {
+ if (($canceller = $this->canceller) instanceof \Closure) {
+ $this->canceller = null;
+ $canceller();
+ }
+ }
+
+ public function __destruct()
+ {
+ $this->cancel();
+ }
+}
diff --git a/src/vendor/symfony/http-client/Internal/ClientState.php b/src/vendor/symfony/http-client/Internal/ClientState.php
new file mode 100644
index 0000000..52fe3c8
--- /dev/null
+++ b/src/vendor/symfony/http-client/Internal/ClientState.php
@@ -0,0 +1,26 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\HttpClient\Internal;
+
+/**
+ * Internal representation of the client state.
+ *
+ * @author Alexander M. Turek
+ *
+ * @internal
+ */
+final class HttplugWaitLoop
+{
+ private $client;
+ private $promisePool;
+ private $responseFactory;
+ private $streamFactory;
+
+ /**
+ * @param \SplObjectStorage
+ */
+class MockHttpClient implements HttpClientInterface, ResetInterface
+{
+ use HttpClientTrait;
+
+ private $responseFactory;
+ private $requestsCount = 0;
+ private $defaultOptions = [];
+
+ /**
+ * @param callable|callable[]|ResponseInterface|ResponseInterface[]|iterable|null $responseFactory
+ */
+ public function __construct($responseFactory = null, ?string $baseUri = 'https://example.com')
+ {
+ $this->setResponseFactory($responseFactory);
+ $this->defaultOptions['base_uri'] = $baseUri;
+ }
+
+ /**
+ * @param callable|callable[]|ResponseInterface|ResponseInterface[]|iterable|null $responseFactory
+ */
+ public function setResponseFactory($responseFactory): void
+ {
+ if ($responseFactory instanceof ResponseInterface) {
+ $responseFactory = [$responseFactory];
+ }
+
+ if (!$responseFactory instanceof \Iterator && null !== $responseFactory && !\is_callable($responseFactory)) {
+ $responseFactory = (static function () use ($responseFactory) {
+ yield from $responseFactory;
+ })();
+ }
+
+ $this->responseFactory = $responseFactory;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function request(string $method, string $url, array $options = []): ResponseInterface
+ {
+ [$url, $options] = $this->prepareRequest($method, $url, $options, $this->defaultOptions, true);
+ $url = implode('', $url);
+
+ if (null === $this->responseFactory) {
+ $response = new MockResponse();
+ } elseif (\is_callable($this->responseFactory)) {
+ $response = ($this->responseFactory)($method, $url, $options);
+ } elseif (!$this->responseFactory->valid()) {
+ throw new TransportException('The response factory iterator passed to MockHttpClient is empty.');
+ } else {
+ $responseFactory = $this->responseFactory->current();
+ $response = \is_callable($responseFactory) ? $responseFactory($method, $url, $options) : $responseFactory;
+ $this->responseFactory->next();
+ }
+ ++$this->requestsCount;
+
+ if (!$response instanceof ResponseInterface) {
+ throw new TransportException(sprintf('The response factory passed to MockHttpClient must return/yield an instance of ResponseInterface, "%s" given.', \is_object($response) ? \get_class($response) : \gettype($response)));
+ }
+
+ return MockResponse::fromRequest($method, $url, $options, $response);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function stream($responses, float $timeout = null): ResponseStreamInterface
+ {
+ if ($responses instanceof ResponseInterface) {
+ $responses = [$responses];
+ } elseif (!is_iterable($responses)) {
+ throw new \TypeError(sprintf('"%s()" expects parameter 1 to be an iterable of MockResponse objects, "%s" given.', __METHOD__, get_debug_type($responses)));
+ }
+
+ return new ResponseStream(MockResponse::stream($responses, $timeout));
+ }
+
+ public function getRequestsCount(): int
+ {
+ return $this->requestsCount;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function withOptions(array $options): self
+ {
+ $clone = clone $this;
+ $clone->defaultOptions = self::mergeDefaultOptions($options, $this->defaultOptions, true);
+
+ return $clone;
+ }
+
+ public function reset()
+ {
+ $this->requestsCount = 0;
+ }
+}
diff --git a/src/vendor/symfony/http-client/NativeHttpClient.php b/src/vendor/symfony/http-client/NativeHttpClient.php
new file mode 100644
index 0000000..63fcc1c
--- /dev/null
+++ b/src/vendor/symfony/http-client/NativeHttpClient.php
@@ -0,0 +1,468 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\HttpClient;
+
+use Psr\Log\LoggerAwareInterface;
+use Psr\Log\LoggerAwareTrait;
+use Symfony\Component\HttpClient\Exception\InvalidArgumentException;
+use Symfony\Component\HttpClient\Exception\TransportException;
+use Symfony\Component\HttpClient\Internal\NativeClientState;
+use Symfony\Component\HttpClient\Response\NativeResponse;
+use Symfony\Component\HttpClient\Response\ResponseStream;
+use Symfony\Contracts\HttpClient\HttpClientInterface;
+use Symfony\Contracts\HttpClient\ResponseInterface;
+use Symfony\Contracts\HttpClient\ResponseStreamInterface;
+use Symfony\Contracts\Service\ResetInterface;
+
+/**
+ * A portable implementation of the HttpClientInterface contracts based on PHP stream wrappers.
+ *
+ * PHP stream wrappers are able to fetch response bodies concurrently,
+ * but each request is opened synchronously.
+ *
+ * @author Nicolas Grekas
+ */
+final class NativeHttpClient implements HttpClientInterface, LoggerAwareInterface, ResetInterface
+{
+ use HttpClientTrait;
+ use LoggerAwareTrait;
+
+ private $defaultOptions = self::OPTIONS_DEFAULTS;
+ private static $emptyDefaults = self::OPTIONS_DEFAULTS;
+
+ /** @var NativeClientState */
+ private $multi;
+
+ /**
+ * @param array $defaultOptions Default request's options
+ * @param int $maxHostConnections The maximum number of connections to open
+ *
+ * @see HttpClientInterface::OPTIONS_DEFAULTS for available options
+ */
+ public function __construct(array $defaultOptions = [], int $maxHostConnections = 6)
+ {
+ $this->defaultOptions['buffer'] = $this->defaultOptions['buffer'] ?? \Closure::fromCallable([__CLASS__, 'shouldBuffer']);
+
+ if ($defaultOptions) {
+ [, $this->defaultOptions] = self::prepareRequest(null, null, $defaultOptions, $this->defaultOptions);
+ }
+
+ $this->multi = new NativeClientState();
+ $this->multi->maxHostConnections = 0 < $maxHostConnections ? $maxHostConnections : \PHP_INT_MAX;
+ }
+
+ /**
+ * @see HttpClientInterface::OPTIONS_DEFAULTS for available options
+ *
+ * {@inheritdoc}
+ */
+ public function request(string $method, string $url, array $options = []): ResponseInterface
+ {
+ [$url, $options] = self::prepareRequest($method, $url, $options, $this->defaultOptions);
+
+ if ($options['bindto']) {
+ if (file_exists($options['bindto'])) {
+ throw new TransportException(__CLASS__.' cannot bind to local Unix sockets, use e.g. CurlHttpClient instead.');
+ }
+ if (str_starts_with($options['bindto'], 'if!')) {
+ throw new TransportException(__CLASS__.' cannot bind to network interfaces, use e.g. CurlHttpClient instead.');
+ }
+ if (str_starts_with($options['bindto'], 'host!')) {
+ $options['bindto'] = substr($options['bindto'], 5);
+ }
+ }
+
+ $hasContentLength = isset($options['normalized_headers']['content-length']);
+ $hasBody = '' !== $options['body'] || 'POST' === $method || $hasContentLength;
+
+ $options['body'] = self::getBodyAsString($options['body']);
+
+ if ('chunked' === substr($options['normalized_headers']['transfer-encoding'][0] ?? '', \strlen('Transfer-Encoding: '))) {
+ unset($options['normalized_headers']['transfer-encoding']);
+ $options['headers'] = array_merge(...array_values($options['normalized_headers']));
+ $options['body'] = self::dechunk($options['body']);
+ }
+ if ('' === $options['body'] && $hasBody && !$hasContentLength) {
+ $options['headers'][] = 'Content-Length: 0';
+ }
+ if ($hasBody && !isset($options['normalized_headers']['content-type'])) {
+ $options['headers'][] = 'Content-Type: application/x-www-form-urlencoded';
+ }
+
+ if (\extension_loaded('zlib') && !isset($options['normalized_headers']['accept-encoding'])) {
+ // gzip is the most widely available algo, no need to deal with deflate
+ $options['headers'][] = 'Accept-Encoding: gzip';
+ }
+
+ if ($options['peer_fingerprint']) {
+ if (isset($options['peer_fingerprint']['pin-sha256']) && 1 === \count($options['peer_fingerprint'])) {
+ throw new TransportException(__CLASS__.' cannot verify "pin-sha256" fingerprints, please provide a "sha256" one.');
+ }
+
+ unset($options['peer_fingerprint']['pin-sha256']);
+ }
+
+ $info = [
+ 'response_headers' => [],
+ 'url' => $url,
+ 'error' => null,
+ 'canceled' => false,
+ 'http_method' => $method,
+ 'http_code' => 0,
+ 'redirect_count' => 0,
+ 'start_time' => 0.0,
+ 'connect_time' => 0.0,
+ 'redirect_time' => 0.0,
+ 'pretransfer_time' => 0.0,
+ 'starttransfer_time' => 0.0,
+ 'total_time' => 0.0,
+ 'namelookup_time' => 0.0,
+ 'size_upload' => 0,
+ 'size_download' => 0,
+ 'size_body' => \strlen($options['body']),
+ 'primary_ip' => '',
+ 'primary_port' => 'http:' === $url['scheme'] ? 80 : 443,
+ 'debug' => \extension_loaded('curl') ? '' : "* Enable the curl extension for better performance\n",
+ ];
+
+ if ($onProgress = $options['on_progress']) {
+ // Memoize the last progress to ease calling the callback periodically when no network transfer happens
+ $lastProgress = [0, 0];
+ $maxDuration = 0 < $options['max_duration'] ? $options['max_duration'] : \INF;
+ $onProgress = static function (...$progress) use ($onProgress, &$lastProgress, &$info, $maxDuration) {
+ if ($info['total_time'] >= $maxDuration) {
+ throw new TransportException(sprintf('Max duration was reached for "%s".', implode('', $info['url'])));
+ }
+
+ $progressInfo = $info;
+ $progressInfo['url'] = implode('', $info['url']);
+ unset($progressInfo['size_body']);
+
+ if ($progress && -1 === $progress[0]) {
+ // Response completed
+ $lastProgress[0] = max($lastProgress);
+ } else {
+ $lastProgress = $progress ?: $lastProgress;
+ }
+
+ $onProgress($lastProgress[0], $lastProgress[1], $progressInfo);
+ };
+ } elseif (0 < $options['max_duration']) {
+ $maxDuration = $options['max_duration'];
+ $onProgress = static function () use (&$info, $maxDuration): void {
+ if ($info['total_time'] >= $maxDuration) {
+ throw new TransportException(sprintf('Max duration was reached for "%s".', implode('', $info['url'])));
+ }
+ };
+ }
+
+ // Always register a notification callback to compute live stats about the response
+ $notification = static function (int $code, int $severity, ?string $msg, int $msgCode, int $dlNow, int $dlSize) use ($onProgress, &$info) {
+ $info['total_time'] = microtime(true) - $info['start_time'];
+
+ if (\STREAM_NOTIFY_PROGRESS === $code) {
+ $info['starttransfer_time'] = $info['starttransfer_time'] ?: $info['total_time'];
+ $info['size_upload'] += $dlNow ? 0 : $info['size_body'];
+ $info['size_download'] = $dlNow;
+ } elseif (\STREAM_NOTIFY_CONNECT === $code) {
+ $info['connect_time'] = $info['total_time'];
+ $info['debug'] .= $info['request_header'];
+ unset($info['request_header']);
+ } else {
+ return;
+ }
+
+ if ($onProgress) {
+ $onProgress($dlNow, $dlSize);
+ }
+ };
+
+ if ($options['resolve']) {
+ $this->multi->dnsCache = $options['resolve'] + $this->multi->dnsCache;
+ }
+
+ $this->logger && $this->logger->info(sprintf('Request: "%s %s"', $method, implode('', $url)));
+
+ if (!isset($options['normalized_headers']['user-agent'])) {
+ $options['headers'][] = 'User-Agent: Symfony HttpClient/Native';
+ }
+
+ if (0 < $options['max_duration']) {
+ $options['timeout'] = min($options['max_duration'], $options['timeout']);
+ }
+
+ $bindto = $options['bindto'];
+ if (!$bindto && (70322 === \PHP_VERSION_ID || 70410 === \PHP_VERSION_ID)) {
+ $bindto = '0:0';
+ }
+
+ $context = [
+ 'http' => [
+ 'protocol_version' => min($options['http_version'] ?: '1.1', '1.1'),
+ 'method' => $method,
+ 'content' => $options['body'],
+ 'ignore_errors' => true,
+ 'curl_verify_ssl_peer' => $options['verify_peer'],
+ 'curl_verify_ssl_host' => $options['verify_host'],
+ 'auto_decode' => false, // Disable dechunk filter, it's incompatible with stream_select()
+ 'timeout' => $options['timeout'],
+ 'follow_location' => false, // We follow redirects ourselves - the native logic is too limited
+ ],
+ 'ssl' => array_filter([
+ 'verify_peer' => $options['verify_peer'],
+ 'verify_peer_name' => $options['verify_host'],
+ 'cafile' => $options['cafile'],
+ 'capath' => $options['capath'],
+ 'local_cert' => $options['local_cert'],
+ 'local_pk' => $options['local_pk'],
+ 'passphrase' => $options['passphrase'],
+ 'ciphers' => $options['ciphers'],
+ 'peer_fingerprint' => $options['peer_fingerprint'],
+ 'capture_peer_cert_chain' => $options['capture_peer_cert_chain'],
+ 'allow_self_signed' => (bool) $options['peer_fingerprint'],
+ 'SNI_enabled' => true,
+ 'disable_compression' => true,
+ ], static function ($v) { return null !== $v; }),
+ 'socket' => [
+ 'bindto' => $bindto,
+ 'tcp_nodelay' => true,
+ ],
+ ];
+
+ $context = stream_context_create($context, ['notification' => $notification]);
+
+ $resolver = static function ($multi) use ($context, $options, $url, &$info, $onProgress) {
+ [$host, $port] = self::parseHostPort($url, $info);
+
+ if (!isset($options['normalized_headers']['host'])) {
+ $options['headers'][] = 'Host: '.$host.$port;
+ }
+
+ $proxy = self::getProxy($options['proxy'], $url, $options['no_proxy']);
+
+ if (!self::configureHeadersAndProxy($context, $host, $options['headers'], $proxy, 'https:' === $url['scheme'])) {
+ $ip = self::dnsResolve($host, $multi, $info, $onProgress);
+ $url['authority'] = substr_replace($url['authority'], $ip, -\strlen($host) - \strlen($port), \strlen($host));
+ }
+
+ return [self::createRedirectResolver($options, $host, $proxy, $info, $onProgress), implode('', $url)];
+ };
+
+ return new NativeResponse($this->multi, $context, implode('', $url), $options, $info, $resolver, $onProgress, $this->logger);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function stream($responses, float $timeout = null): ResponseStreamInterface
+ {
+ if ($responses instanceof NativeResponse) {
+ $responses = [$responses];
+ } elseif (!is_iterable($responses)) {
+ throw new \TypeError(sprintf('"%s()" expects parameter 1 to be an iterable of NativeResponse objects, "%s" given.', __METHOD__, get_debug_type($responses)));
+ }
+
+ return new ResponseStream(NativeResponse::stream($responses, $timeout));
+ }
+
+ public function reset()
+ {
+ $this->multi->reset();
+ }
+
+ private static function getBodyAsString($body): string
+ {
+ if (\is_resource($body)) {
+ return stream_get_contents($body);
+ }
+
+ if (!$body instanceof \Closure) {
+ return $body;
+ }
+
+ $result = '';
+
+ while ('' !== $data = $body(self::$CHUNK_SIZE)) {
+ if (!\is_string($data)) {
+ throw new TransportException(sprintf('Return value of the "body" option callback must be string, "%s" returned.', get_debug_type($data)));
+ }
+
+ $result .= $data;
+ }
+
+ return $result;
+ }
+
+ /**
+ * Extracts the host and the port from the URL.
+ */
+ private static function parseHostPort(array $url, array &$info): array
+ {
+ if ($port = parse_url($url['authority'], \PHP_URL_PORT) ?: '') {
+ $info['primary_port'] = $port;
+ $port = ':'.$port;
+ } else {
+ $info['primary_port'] = 'http:' === $url['scheme'] ? 80 : 443;
+ }
+
+ return [parse_url($url['authority'], \PHP_URL_HOST), $port];
+ }
+
+ /**
+ * Resolves the IP of the host using the local DNS cache if possible.
+ */
+ private static function dnsResolve($host, NativeClientState $multi, array &$info, ?\Closure $onProgress): string
+ {
+ if (null === $ip = $multi->dnsCache[$host] ?? null) {
+ $info['debug'] .= "* Hostname was NOT found in DNS cache\n";
+ $now = microtime(true);
+
+ if (!$ip = gethostbynamel($host)) {
+ throw new TransportException(sprintf('Could not resolve host "%s".', $host));
+ }
+
+ $info['namelookup_time'] = microtime(true) - ($info['start_time'] ?: $now);
+ $multi->dnsCache[$host] = $ip = $ip[0];
+ $info['debug'] .= "* Added {$host}:0:{$ip} to DNS cache\n";
+ } else {
+ $info['debug'] .= "* Hostname was found in DNS cache\n";
+ }
+
+ $info['primary_ip'] = $ip;
+
+ if ($onProgress) {
+ // Notify DNS resolution
+ $onProgress();
+ }
+
+ return $ip;
+ }
+
+ /**
+ * Handles redirects - the native logic is too buggy to be used.
+ */
+ private static function createRedirectResolver(array $options, string $host, ?array $proxy, array &$info, ?\Closure $onProgress): \Closure
+ {
+ $redirectHeaders = [];
+ if (0 < $maxRedirects = $options['max_redirects']) {
+ $redirectHeaders = ['host' => $host];
+ $redirectHeaders['with_auth'] = $redirectHeaders['no_auth'] = array_filter($options['headers'], static function ($h) {
+ return 0 !== stripos($h, 'Host:');
+ });
+
+ if (isset($options['normalized_headers']['authorization']) || isset($options['normalized_headers']['cookie'])) {
+ $redirectHeaders['no_auth'] = array_filter($redirectHeaders['no_auth'], static function ($h) {
+ return 0 !== stripos($h, 'Authorization:') && 0 !== stripos($h, 'Cookie:');
+ });
+ }
+ }
+
+ return static function (NativeClientState $multi, ?string $location, $context) use (&$redirectHeaders, $proxy, &$info, $maxRedirects, $onProgress): ?string {
+ if (null === $location || $info['http_code'] < 300 || 400 <= $info['http_code']) {
+ $info['redirect_url'] = null;
+
+ return null;
+ }
+
+ try {
+ $url = self::parseUrl($location);
+ } catch (InvalidArgumentException $e) {
+ $info['redirect_url'] = null;
+
+ return null;
+ }
+
+ $url = self::resolveUrl($url, $info['url']);
+ $info['redirect_url'] = implode('', $url);
+
+ if ($info['redirect_count'] >= $maxRedirects) {
+ return null;
+ }
+
+ $info['url'] = $url;
+ ++$info['redirect_count'];
+ $info['redirect_time'] = microtime(true) - $info['start_time'];
+
+ // Do like curl and browsers: turn POST to GET on 301, 302 and 303
+ if (\in_array($info['http_code'], [301, 302, 303], true)) {
+ $options = stream_context_get_options($context)['http'];
+
+ if ('POST' === $options['method'] || 303 === $info['http_code']) {
+ $info['http_method'] = $options['method'] = 'HEAD' === $options['method'] ? 'HEAD' : 'GET';
+ $options['content'] = '';
+ $filterContentHeaders = static function ($h) {
+ return 0 !== stripos($h, 'Content-Length:') && 0 !== stripos($h, 'Content-Type:') && 0 !== stripos($h, 'Transfer-Encoding:');
+ };
+ $options['header'] = array_filter($options['header'], $filterContentHeaders);
+ $redirectHeaders['no_auth'] = array_filter($redirectHeaders['no_auth'], $filterContentHeaders);
+ $redirectHeaders['with_auth'] = array_filter($redirectHeaders['with_auth'], $filterContentHeaders);
+
+ stream_context_set_option($context, ['http' => $options]);
+ }
+ }
+
+ [$host, $port] = self::parseHostPort($url, $info);
+
+ if (false !== (parse_url($location, \PHP_URL_HOST) ?? false)) {
+ // Authorization and Cookie headers MUST NOT follow except for the initial host name
+ $requestHeaders = $redirectHeaders['host'] === $host ? $redirectHeaders['with_auth'] : $redirectHeaders['no_auth'];
+ $requestHeaders[] = 'Host: '.$host.$port;
+ $dnsResolve = !self::configureHeadersAndProxy($context, $host, $requestHeaders, $proxy, 'https:' === $url['scheme']);
+ } else {
+ $dnsResolve = isset(stream_context_get_options($context)['ssl']['peer_name']);
+ }
+
+ if ($dnsResolve) {
+ $ip = self::dnsResolve($host, $multi, $info, $onProgress);
+ $url['authority'] = substr_replace($url['authority'], $ip, -\strlen($host) - \strlen($port), \strlen($host));
+ }
+
+ return implode('', $url);
+ };
+ }
+
+ private static function configureHeadersAndProxy($context, string $host, array $requestHeaders, ?array $proxy, bool $isSsl): bool
+ {
+ if (null === $proxy) {
+ stream_context_set_option($context, 'http', 'header', $requestHeaders);
+ stream_context_set_option($context, 'ssl', 'peer_name', $host);
+
+ return false;
+ }
+
+ // Matching "no_proxy" should follow the behavior of curl
+
+ foreach ($proxy['no_proxy'] as $rule) {
+ $dotRule = '.'.ltrim($rule, '.');
+
+ if ('*' === $rule || $host === $rule || str_ends_with($host, $dotRule)) {
+ stream_context_set_option($context, 'http', 'proxy', null);
+ stream_context_set_option($context, 'http', 'request_fulluri', false);
+ stream_context_set_option($context, 'http', 'header', $requestHeaders);
+ stream_context_set_option($context, 'ssl', 'peer_name', $host);
+
+ return false;
+ }
+ }
+
+ if (null !== $proxy['auth']) {
+ $requestHeaders[] = 'Proxy-Authorization: '.$proxy['auth'];
+ }
+
+ stream_context_set_option($context, 'http', 'proxy', $proxy['url']);
+ stream_context_set_option($context, 'http', 'request_fulluri', !$isSsl);
+ stream_context_set_option($context, 'http', 'header', $requestHeaders);
+ stream_context_set_option($context, 'ssl', 'peer_name', null);
+
+ return true;
+ }
+}
diff --git a/src/vendor/symfony/http-client/NoPrivateNetworkHttpClient.php b/src/vendor/symfony/http-client/NoPrivateNetworkHttpClient.php
new file mode 100644
index 0000000..911cce9
--- /dev/null
+++ b/src/vendor/symfony/http-client/NoPrivateNetworkHttpClient.php
@@ -0,0 +1,132 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\HttpClient;
+
+use Psr\Log\LoggerAwareInterface;
+use Psr\Log\LoggerInterface;
+use Symfony\Component\HttpClient\Exception\InvalidArgumentException;
+use Symfony\Component\HttpClient\Exception\TransportException;
+use Symfony\Component\HttpFoundation\IpUtils;
+use Symfony\Contracts\HttpClient\HttpClientInterface;
+use Symfony\Contracts\HttpClient\ResponseInterface;
+use Symfony\Contracts\HttpClient\ResponseStreamInterface;
+use Symfony\Contracts\Service\ResetInterface;
+
+/**
+ * Decorator that blocks requests to private networks by default.
+ *
+ * @author Hallison Boaventura
+ */
+final class Psr18Client implements ClientInterface, RequestFactoryInterface, StreamFactoryInterface, UriFactoryInterface, ResetInterface
+{
+ private $client;
+ private $responseFactory;
+ private $streamFactory;
+
+ public function __construct(HttpClientInterface $client = null, ResponseFactoryInterface $responseFactory = null, StreamFactoryInterface $streamFactory = null)
+ {
+ $this->client = $client ?? HttpClient::create();
+ $this->responseFactory = $responseFactory;
+ $this->streamFactory = $streamFactory ?? ($responseFactory instanceof StreamFactoryInterface ? $responseFactory : null);
+
+ if (null !== $this->responseFactory && null !== $this->streamFactory) {
+ return;
+ }
+
+ if (!class_exists(Psr17Factory::class) && !class_exists(Psr17FactoryDiscovery::class)) {
+ throw new \LogicException('You cannot use the "Symfony\Component\HttpClient\Psr18Client" as no PSR-17 factories have been provided. Try running "composer require nyholm/psr7".');
+ }
+
+ try {
+ $psr17Factory = class_exists(Psr17Factory::class, false) ? new Psr17Factory() : null;
+ $this->responseFactory = $this->responseFactory ?? $psr17Factory ?? Psr17FactoryDiscovery::findResponseFactory();
+ $this->streamFactory = $this->streamFactory ?? $psr17Factory ?? Psr17FactoryDiscovery::findStreamFactory();
+ } catch (NotFoundException $e) {
+ throw new \LogicException('You cannot use the "Symfony\Component\HttpClient\HttplugClient" as no PSR-17 factories have been found. Try running "composer require nyholm/psr7".', 0, $e);
+ }
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function sendRequest(RequestInterface $request): ResponseInterface
+ {
+ try {
+ $body = $request->getBody();
+
+ if ($body->isSeekable()) {
+ $body->seek(0);
+ }
+
+ $options = [
+ 'headers' => $request->getHeaders(),
+ 'body' => $body->getContents(),
+ ];
+
+ if ('1.0' === $request->getProtocolVersion()) {
+ $options['http_version'] = '1.0';
+ }
+
+ $response = $this->client->request($request->getMethod(), (string) $request->getUri(), $options);
+
+ $psrResponse = $this->responseFactory->createResponse($response->getStatusCode());
+
+ foreach ($response->getHeaders(false) as $name => $values) {
+ foreach ($values as $value) {
+ try {
+ $psrResponse = $psrResponse->withAddedHeader($name, $value);
+ } catch (\InvalidArgumentException $e) {
+ // ignore invalid header
+ }
+ }
+ }
+
+ $body = $response instanceof StreamableInterface ? $response->toStream(false) : StreamWrapper::createResource($response, $this->client);
+ $body = $this->streamFactory->createStreamFromResource($body);
+
+ if ($body->isSeekable()) {
+ $body->seek(0);
+ }
+
+ return $psrResponse->withBody($body);
+ } catch (TransportExceptionInterface $e) {
+ if ($e instanceof \InvalidArgumentException) {
+ throw new Psr18RequestException($e, $request);
+ }
+
+ throw new Psr18NetworkException($e, $request);
+ }
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function createRequest(string $method, $uri): RequestInterface
+ {
+ if ($this->responseFactory instanceof RequestFactoryInterface) {
+ return $this->responseFactory->createRequest($method, $uri);
+ }
+
+ if (class_exists(Request::class)) {
+ return new Request($method, $uri);
+ }
+
+ if (class_exists(Psr17FactoryDiscovery::class)) {
+ return Psr17FactoryDiscovery::findRequestFactory()->createRequest($method, $uri);
+ }
+
+ throw new \LogicException(sprintf('You cannot use "%s()" as the "nyholm/psr7" package is not installed. Try running "composer require nyholm/psr7".', __METHOD__));
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function createStream(string $content = ''): StreamInterface
+ {
+ $stream = $this->streamFactory->createStream($content);
+
+ if ($stream->isSeekable()) {
+ $stream->seek(0);
+ }
+
+ return $stream;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function createStreamFromFile(string $filename, string $mode = 'r'): StreamInterface
+ {
+ return $this->streamFactory->createStreamFromFile($filename, $mode);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function createStreamFromResource($resource): StreamInterface
+ {
+ return $this->streamFactory->createStreamFromResource($resource);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function createUri(string $uri = ''): UriInterface
+ {
+ if ($this->responseFactory instanceof UriFactoryInterface) {
+ return $this->responseFactory->createUri($uri);
+ }
+
+ if (class_exists(Uri::class)) {
+ return new Uri($uri);
+ }
+
+ if (class_exists(Psr17FactoryDiscovery::class)) {
+ return Psr17FactoryDiscovery::findUrlFactory()->createUri($uri);
+ }
+
+ throw new \LogicException(sprintf('You cannot use "%s()" as the "nyholm/psr7" package is not installed. Try running "composer require nyholm/psr7".', __METHOD__));
+ }
+
+ public function reset()
+ {
+ if ($this->client instanceof ResetInterface) {
+ $this->client->reset();
+ }
+ }
+}
+
+/**
+ * @internal
+ */
+class Psr18NetworkException extends \RuntimeException implements NetworkExceptionInterface
+{
+ private $request;
+
+ public function __construct(TransportExceptionInterface $e, RequestInterface $request)
+ {
+ parent::__construct($e->getMessage(), 0, $e);
+ $this->request = $request;
+ }
+
+ public function getRequest(): RequestInterface
+ {
+ return $this->request;
+ }
+}
+
+/**
+ * @internal
+ */
+class Psr18RequestException extends \InvalidArgumentException implements RequestExceptionInterface
+{
+ private $request;
+
+ public function __construct(TransportExceptionInterface $e, RequestInterface $request)
+ {
+ parent::__construct($e->getMessage(), 0, $e);
+ $this->request = $request;
+ }
+
+ public function getRequest(): RequestInterface
+ {
+ return $this->request;
+ }
+}
diff --git a/src/vendor/symfony/http-client/README.md b/src/vendor/symfony/http-client/README.md
new file mode 100644
index 0000000..0c55ccc
--- /dev/null
+++ b/src/vendor/symfony/http-client/README.md
@@ -0,0 +1,27 @@
+HttpClient component
+====================
+
+The HttpClient component provides powerful methods to fetch HTTP resources synchronously or asynchronously.
+
+Sponsor
+-------
+
+The Httpclient component for Symfony 5.4/6.0 is [backed][1] by [Klaxoon][2].
+
+Klaxoon is a platform that empowers organizations to run effective and
+productive workshops easily in a hybrid environment. Anytime, Anywhere.
+
+Help Symfony by [sponsoring][3] its development!
+
+Resources
+---------
+
+ * [Documentation](https://symfony.com/doc/current/components/http_client.html)
+ * [Contributing](https://symfony.com/doc/current/contributing/index.html)
+ * [Report issues](https://github.com/symfony/symfony/issues) and
+ [send Pull Requests](https://github.com/symfony/symfony/pulls)
+ in the [main Symfony repository](https://github.com/symfony/symfony)
+
+[1]: https://symfony.com/backers
+[2]: https://klaxoon.com
+[3]: https://symfony.com/sponsor
diff --git a/src/vendor/symfony/http-client/Response/AmpResponse.php b/src/vendor/symfony/http-client/Response/AmpResponse.php
new file mode 100644
index 0000000..900c70d
--- /dev/null
+++ b/src/vendor/symfony/http-client/Response/AmpResponse.php
@@ -0,0 +1,460 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\HttpClient\Response;
+
+use Amp\ByteStream\StreamException;
+use Amp\CancellationTokenSource;
+use Amp\Coroutine;
+use Amp\Deferred;
+use Amp\Http\Client\HttpException;
+use Amp\Http\Client\Request;
+use Amp\Http\Client\Response;
+use Amp\Loop;
+use Amp\Promise;
+use Amp\Success;
+use Psr\Log\LoggerInterface;
+use Symfony\Component\HttpClient\Chunk\FirstChunk;
+use Symfony\Component\HttpClient\Chunk\InformationalChunk;
+use Symfony\Component\HttpClient\Exception\InvalidArgumentException;
+use Symfony\Component\HttpClient\Exception\TransportException;
+use Symfony\Component\HttpClient\HttpClientTrait;
+use Symfony\Component\HttpClient\Internal\AmpBody;
+use Symfony\Component\HttpClient\Internal\AmpClientState;
+use Symfony\Component\HttpClient\Internal\Canary;
+use Symfony\Component\HttpClient\Internal\ClientState;
+use Symfony\Contracts\HttpClient\ResponseInterface;
+
+/**
+ * @author Nicolas Grekas
+ *
+ * @internal
+ */
+final class AmpResponse implements ResponseInterface, StreamableInterface
+{
+ use CommonResponseTrait;
+ use TransportResponseTrait;
+
+ private static $nextId = 'a';
+
+ private $multi;
+ private $options;
+ private $onProgress;
+
+ private static $delay;
+
+ /**
+ * @internal
+ */
+ public function __construct(AmpClientState $multi, Request $request, array $options, ?LoggerInterface $logger)
+ {
+ $this->multi = $multi;
+ $this->options = &$options;
+ $this->logger = $logger;
+ $this->timeout = $options['timeout'];
+ $this->shouldBuffer = $options['buffer'];
+
+ if ($this->inflate = \extension_loaded('zlib') && !$request->hasHeader('accept-encoding')) {
+ $request->setHeader('Accept-Encoding', 'gzip');
+ }
+
+ $this->initializer = static function (self $response) {
+ return null !== $response->options;
+ };
+
+ $info = &$this->info;
+ $headers = &$this->headers;
+ $canceller = new CancellationTokenSource();
+ $handle = &$this->handle;
+
+ $info['url'] = (string) $request->getUri();
+ $info['http_method'] = $request->getMethod();
+ $info['start_time'] = null;
+ $info['redirect_url'] = null;
+ $info['redirect_time'] = 0.0;
+ $info['redirect_count'] = 0;
+ $info['size_upload'] = 0.0;
+ $info['size_download'] = 0.0;
+ $info['upload_content_length'] = -1.0;
+ $info['download_content_length'] = -1.0;
+ $info['user_data'] = $options['user_data'];
+ $info['max_duration'] = $options['max_duration'];
+ $info['debug'] = '';
+
+ $onProgress = $options['on_progress'] ?? static function () {};
+ $onProgress = $this->onProgress = static function () use (&$info, $onProgress) {
+ $info['total_time'] = microtime(true) - $info['start_time'];
+ $onProgress((int) $info['size_download'], ((int) (1 + $info['download_content_length']) ?: 1) - 1, (array) $info);
+ };
+
+ $pauseDeferred = new Deferred();
+ $pause = new Success();
+
+ $throttleWatcher = null;
+
+ $this->id = $id = self::$nextId++;
+ Loop::defer(static function () use ($request, $multi, &$id, &$info, &$headers, $canceller, &$options, $onProgress, &$handle, $logger, &$pause) {
+ return new Coroutine(self::generateResponse($request, $multi, $id, $info, $headers, $canceller, $options, $onProgress, $handle, $logger, $pause));
+ });
+
+ $info['pause_handler'] = static function (float $duration) use (&$throttleWatcher, &$pauseDeferred, &$pause) {
+ if (null !== $throttleWatcher) {
+ Loop::cancel($throttleWatcher);
+ }
+
+ $pause = $pauseDeferred->promise();
+
+ if ($duration <= 0) {
+ $deferred = $pauseDeferred;
+ $pauseDeferred = new Deferred();
+ $deferred->resolve();
+ } else {
+ $throttleWatcher = Loop::delay(ceil(1000 * $duration), static function () use (&$pauseDeferred) {
+ $deferred = $pauseDeferred;
+ $pauseDeferred = new Deferred();
+ $deferred->resolve();
+ });
+ }
+ };
+
+ $multi->lastTimeout = null;
+ $multi->openHandles[$id] = $id;
+ ++$multi->responseCount;
+
+ $this->canary = new Canary(static function () use ($canceller, $multi, $id) {
+ $canceller->cancel();
+ unset($multi->openHandles[$id], $multi->handlesActivity[$id]);
+ });
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getInfo(string $type = null)
+ {
+ return null !== $type ? $this->info[$type] ?? null : $this->info;
+ }
+
+ public function __sleep(): array
+ {
+ throw new \BadMethodCallException('Cannot serialize '.__CLASS__);
+ }
+
+ public function __wakeup()
+ {
+ throw new \BadMethodCallException('Cannot unserialize '.__CLASS__);
+ }
+
+ public function __destruct()
+ {
+ try {
+ $this->doDestruct();
+ } finally {
+ // Clear the DNS cache when all requests completed
+ if (0 >= --$this->multi->responseCount) {
+ $this->multi->responseCount = 0;
+ $this->multi->dnsCache = [];
+ }
+ }
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ private static function schedule(self $response, array &$runningResponses): void
+ {
+ if (isset($runningResponses[0])) {
+ $runningResponses[0][1][$response->id] = $response;
+ } else {
+ $runningResponses[0] = [$response->multi, [$response->id => $response]];
+ }
+
+ if (!isset($response->multi->openHandles[$response->id])) {
+ $response->multi->handlesActivity[$response->id][] = null;
+ $response->multi->handlesActivity[$response->id][] = null !== $response->info['error'] ? new TransportException($response->info['error']) : null;
+ }
+ }
+
+ /**
+ * {@inheritdoc}
+ *
+ * @param AmpClientState $multi
+ */
+ private static function perform(ClientState $multi, array &$responses = null): void
+ {
+ if ($responses) {
+ foreach ($responses as $response) {
+ try {
+ if ($response->info['start_time']) {
+ $response->info['total_time'] = microtime(true) - $response->info['start_time'];
+ ($response->onProgress)();
+ }
+ } catch (\Throwable $e) {
+ $multi->handlesActivity[$response->id][] = null;
+ $multi->handlesActivity[$response->id][] = $e;
+ }
+ }
+ }
+ }
+
+ /**
+ * {@inheritdoc}
+ *
+ * @param AmpClientState $multi
+ */
+ private static function select(ClientState $multi, float $timeout): int
+ {
+ $timeout += microtime(true);
+ self::$delay = Loop::defer(static function () use ($timeout) {
+ if (0 < $timeout -= microtime(true)) {
+ self::$delay = Loop::delay(ceil(1000 * $timeout), [Loop::class, 'stop']);
+ } else {
+ Loop::stop();
+ }
+ });
+
+ Loop::run();
+
+ return null === self::$delay ? 1 : 0;
+ }
+
+ private static function generateResponse(Request $request, AmpClientState $multi, string $id, array &$info, array &$headers, CancellationTokenSource $canceller, array &$options, \Closure $onProgress, &$handle, ?LoggerInterface $logger, Promise &$pause)
+ {
+ $request->setInformationalResponseHandler(static function (Response $response) use ($multi, $id, &$info, &$headers) {
+ self::addResponseHeaders($response, $info, $headers);
+ $multi->handlesActivity[$id][] = new InformationalChunk($response->getStatus(), $response->getHeaders());
+ self::stopLoop();
+ });
+
+ try {
+ /* @var Response $response */
+ if (null === $response = yield from self::getPushedResponse($request, $multi, $info, $headers, $options, $logger)) {
+ $logger && $logger->info(sprintf('Request: "%s %s"', $info['http_method'], $info['url']));
+
+ $response = yield from self::followRedirects($request, $multi, $info, $headers, $canceller, $options, $onProgress, $handle, $logger, $pause);
+ }
+
+ $options = null;
+
+ $multi->handlesActivity[$id][] = new FirstChunk();
+
+ if ('HEAD' === $response->getRequest()->getMethod() || \in_array($info['http_code'], [204, 304], true)) {
+ $multi->handlesActivity[$id][] = null;
+ $multi->handlesActivity[$id][] = null;
+ self::stopLoop();
+
+ return;
+ }
+
+ if ($response->hasHeader('content-length')) {
+ $info['download_content_length'] = (float) $response->getHeader('content-length');
+ }
+
+ $body = $response->getBody();
+
+ while (true) {
+ self::stopLoop();
+
+ yield $pause;
+
+ if (null === $data = yield $body->read()) {
+ break;
+ }
+
+ $info['size_download'] += \strlen($data);
+ $multi->handlesActivity[$id][] = $data;
+ }
+
+ $multi->handlesActivity[$id][] = null;
+ $multi->handlesActivity[$id][] = null;
+ } catch (\Throwable $e) {
+ $multi->handlesActivity[$id][] = null;
+ $multi->handlesActivity[$id][] = $e;
+ } finally {
+ $info['download_content_length'] = $info['size_download'];
+ }
+
+ self::stopLoop();
+ }
+
+ private static function followRedirects(Request $originRequest, AmpClientState $multi, array &$info, array &$headers, CancellationTokenSource $canceller, array $options, \Closure $onProgress, &$handle, ?LoggerInterface $logger, Promise &$pause)
+ {
+ yield $pause;
+
+ $originRequest->setBody(new AmpBody($options['body'], $info, $onProgress));
+ $response = yield $multi->request($options, $originRequest, $canceller->getToken(), $info, $onProgress, $handle);
+ $previousUrl = null;
+
+ while (true) {
+ self::addResponseHeaders($response, $info, $headers);
+ $status = $response->getStatus();
+
+ if (!\in_array($status, [301, 302, 303, 307, 308], true) || null === $location = $response->getHeader('location')) {
+ return $response;
+ }
+
+ $urlResolver = new class() {
+ use HttpClientTrait {
+ parseUrl as public;
+ resolveUrl as public;
+ }
+ };
+
+ try {
+ $previousUrl = $previousUrl ?? $urlResolver::parseUrl($info['url']);
+ $location = $urlResolver::parseUrl($location);
+ $location = $urlResolver::resolveUrl($location, $previousUrl);
+ $info['redirect_url'] = implode('', $location);
+ } catch (InvalidArgumentException $e) {
+ return $response;
+ }
+
+ if (0 >= $options['max_redirects'] || $info['redirect_count'] >= $options['max_redirects']) {
+ return $response;
+ }
+
+ $logger && $logger->info(sprintf('Redirecting: "%s %s"', $status, $info['url']));
+
+ try {
+ // Discard body of redirects
+ while (null !== yield $response->getBody()->read()) {
+ }
+ } catch (HttpException|StreamException $e) {
+ // Ignore streaming errors on previous responses
+ }
+
+ ++$info['redirect_count'];
+ $info['url'] = $info['redirect_url'];
+ $info['redirect_url'] = null;
+ $previousUrl = $location;
+
+ $request = new Request($info['url'], $info['http_method']);
+ $request->setProtocolVersions($originRequest->getProtocolVersions());
+ $request->setTcpConnectTimeout($originRequest->getTcpConnectTimeout());
+ $request->setTlsHandshakeTimeout($originRequest->getTlsHandshakeTimeout());
+ $request->setTransferTimeout($originRequest->getTransferTimeout());
+
+ if (\in_array($status, [301, 302, 303], true)) {
+ $originRequest->removeHeader('transfer-encoding');
+ $originRequest->removeHeader('content-length');
+ $originRequest->removeHeader('content-type');
+
+ // Do like curl and browsers: turn POST to GET on 301, 302 and 303
+ if ('POST' === $response->getRequest()->getMethod() || 303 === $status) {
+ $info['http_method'] = 'HEAD' === $response->getRequest()->getMethod() ? 'HEAD' : 'GET';
+ $request->setMethod($info['http_method']);
+ }
+ } else {
+ $request->setBody(AmpBody::rewind($response->getRequest()->getBody()));
+ }
+
+ foreach ($originRequest->getRawHeaders() as [$name, $value]) {
+ $request->addHeader($name, $value);
+ }
+
+ if ($request->getUri()->getAuthority() !== $originRequest->getUri()->getAuthority()) {
+ $request->removeHeader('authorization');
+ $request->removeHeader('cookie');
+ $request->removeHeader('host');
+ }
+
+ yield $pause;
+
+ $response = yield $multi->request($options, $request, $canceller->getToken(), $info, $onProgress, $handle);
+ $info['redirect_time'] = microtime(true) - $info['start_time'];
+ }
+ }
+
+ private static function addResponseHeaders(Response $response, array &$info, array &$headers): void
+ {
+ $info['http_code'] = $response->getStatus();
+
+ if ($headers) {
+ $info['debug'] .= "< \r\n";
+ $headers = [];
+ }
+
+ $h = sprintf('HTTP/%s %s %s', $response->getProtocolVersion(), $response->getStatus(), $response->getReason());
+ $info['debug'] .= "< {$h}\r\n";
+ $info['response_headers'][] = $h;
+
+ foreach ($response->getRawHeaders() as [$name, $value]) {
+ $headers[strtolower($name)][] = $value;
+ $h = $name.': '.$value;
+ $info['debug'] .= "< {$h}\r\n";
+ $info['response_headers'][] = $h;
+ }
+
+ $info['debug'] .= "< \r\n";
+ }
+
+ /**
+ * Accepts pushed responses only if their headers related to authentication match the request.
+ */
+ private static function getPushedResponse(Request $request, AmpClientState $multi, array &$info, array &$headers, array $options, ?LoggerInterface $logger)
+ {
+ if ('' !== $options['body']) {
+ return null;
+ }
+
+ $authority = $request->getUri()->getAuthority();
+
+ foreach ($multi->pushedResponses[$authority] ?? [] as $i => [$pushedUrl, $pushDeferred, $pushedRequest, $pushedResponse, $parentOptions]) {
+ if ($info['url'] !== $pushedUrl || $info['http_method'] !== $pushedRequest->getMethod()) {
+ continue;
+ }
+
+ foreach ($parentOptions as $k => $v) {
+ if ($options[$k] !== $v) {
+ continue 2;
+ }
+ }
+
+ foreach (['authorization', 'cookie', 'range', 'proxy-authorization'] as $k) {
+ if ($pushedRequest->getHeaderArray($k) !== $request->getHeaderArray($k)) {
+ continue 2;
+ }
+ }
+
+ $response = yield $pushedResponse;
+
+ foreach ($response->getHeaderArray('vary') as $vary) {
+ foreach (preg_split('/\s*+,\s*+/', $vary) as $v) {
+ if ('*' === $v || ($pushedRequest->getHeaderArray($v) !== $request->getHeaderArray($v) && 'accept-encoding' !== strtolower($v))) {
+ $logger && $logger->debug(sprintf('Skipping pushed response: "%s"', $info['url']));
+ continue 3;
+ }
+ }
+ }
+
+ $pushDeferred->resolve();
+ $logger && $logger->debug(sprintf('Accepting pushed response: "%s %s"', $info['http_method'], $info['url']));
+ self::addResponseHeaders($response, $info, $headers);
+ unset($multi->pushedResponses[$authority][$i]);
+
+ if (!$multi->pushedResponses[$authority]) {
+ unset($multi->pushedResponses[$authority]);
+ }
+
+ return $response;
+ }
+ }
+
+ private static function stopLoop(): void
+ {
+ if (null !== self::$delay) {
+ Loop::cancel(self::$delay);
+ self::$delay = null;
+ }
+
+ Loop::defer([Loop::class, 'stop']);
+ }
+}
diff --git a/src/vendor/symfony/http-client/Response/AsyncContext.php b/src/vendor/symfony/http-client/Response/AsyncContext.php
new file mode 100644
index 0000000..646458e
--- /dev/null
+++ b/src/vendor/symfony/http-client/Response/AsyncContext.php
@@ -0,0 +1,195 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\HttpClient\Response;
+
+use Symfony\Component\HttpClient\Chunk\DataChunk;
+use Symfony\Component\HttpClient\Chunk\LastChunk;
+use Symfony\Component\HttpClient\Exception\TransportException;
+use Symfony\Contracts\HttpClient\ChunkInterface;
+use Symfony\Contracts\HttpClient\HttpClientInterface;
+use Symfony\Contracts\HttpClient\ResponseInterface;
+
+/**
+ * A DTO to work with AsyncResponse.
+ *
+ * @author Nicolas Grekas
+ */
+final class AsyncContext
+{
+ private $passthru;
+ private $client;
+ private $response;
+ private $info = [];
+ private $content;
+ private $offset;
+
+ public function __construct(&$passthru, HttpClientInterface $client, ResponseInterface &$response, array &$info, $content, int $offset)
+ {
+ $this->passthru = &$passthru;
+ $this->client = $client;
+ $this->response = &$response;
+ $this->info = &$info;
+ $this->content = $content;
+ $this->offset = $offset;
+ }
+
+ /**
+ * Returns the HTTP status without consuming the response.
+ */
+ public function getStatusCode(): int
+ {
+ return $this->response->getInfo('http_code');
+ }
+
+ /**
+ * Returns the headers without consuming the response.
+ */
+ public function getHeaders(): array
+ {
+ $headers = [];
+
+ foreach ($this->response->getInfo('response_headers') as $h) {
+ if (11 <= \strlen($h) && '/' === $h[4] && preg_match('#^HTTP/\d+(?:\.\d+)? ([123456789]\d\d)(?: |$)#', $h, $m)) {
+ $headers = [];
+ } elseif (2 === \count($m = explode(':', $h, 2))) {
+ $headers[strtolower($m[0])][] = ltrim($m[1]);
+ }
+ }
+
+ return $headers;
+ }
+
+ /**
+ * @return resource|null The PHP stream resource where the content is buffered, if it is
+ */
+ public function getContent()
+ {
+ return $this->content;
+ }
+
+ /**
+ * Creates a new chunk of content.
+ */
+ public function createChunk(string $data): ChunkInterface
+ {
+ return new DataChunk($this->offset, $data);
+ }
+
+ /**
+ * Pauses the request for the given number of seconds.
+ */
+ public function pause(float $duration): void
+ {
+ if (\is_callable($pause = $this->response->getInfo('pause_handler'))) {
+ $pause($duration);
+ } elseif (0 < $duration) {
+ usleep(1E6 * $duration);
+ }
+ }
+
+ /**
+ * Cancels the request and returns the last chunk to yield.
+ */
+ public function cancel(): ChunkInterface
+ {
+ $this->info['canceled'] = true;
+ $this->info['error'] = 'Response has been canceled.';
+ $this->response->cancel();
+
+ return new LastChunk();
+ }
+
+ /**
+ * Returns the current info of the response.
+ */
+ public function getInfo(string $type = null)
+ {
+ if (null !== $type) {
+ return $this->info[$type] ?? $this->response->getInfo($type);
+ }
+
+ return $this->info + $this->response->getInfo();
+ }
+
+ /**
+ * Attaches an info to the response.
+ *
+ * @return $this
+ */
+ public function setInfo(string $type, $value): self
+ {
+ if ('canceled' === $type && $value !== $this->info['canceled']) {
+ throw new \LogicException('You cannot set the "canceled" info directly.');
+ }
+
+ if (null === $value) {
+ unset($this->info[$type]);
+ } else {
+ $this->info[$type] = $value;
+ }
+
+ return $this;
+ }
+
+ /**
+ * Returns the currently processed response.
+ */
+ public function getResponse(): ResponseInterface
+ {
+ return $this->response;
+ }
+
+ /**
+ * Replaces the currently processed response by doing a new request.
+ */
+ public function replaceRequest(string $method, string $url, array $options = []): ResponseInterface
+ {
+ $this->info['previous_info'][] = $info = $this->response->getInfo();
+ if (null !== $onProgress = $options['on_progress'] ?? null) {
+ $thisInfo = &$this->info;
+ $options['on_progress'] = static function (int $dlNow, int $dlSize, array $info) use (&$thisInfo, $onProgress) {
+ $onProgress($dlNow, $dlSize, $thisInfo + $info);
+ };
+ }
+ if (0 < ($info['max_duration'] ?? 0) && 0 < ($info['total_time'] ?? 0)) {
+ if (0 >= $options['max_duration'] = $info['max_duration'] - $info['total_time']) {
+ throw new TransportException(sprintf('Max duration was reached for "%s".', $info['url']));
+ }
+ }
+
+ return $this->response = $this->client->request($method, $url, ['buffer' => false] + $options);
+ }
+
+ /**
+ * Replaces the currently processed response by another one.
+ */
+ public function replaceResponse(ResponseInterface $response): ResponseInterface
+ {
+ $this->info['previous_info'][] = $this->response->getInfo();
+
+ return $this->response = $response;
+ }
+
+ /**
+ * Replaces or removes the chunk filter iterator.
+ *
+ * @param ?callable(ChunkInterface, self): ?\Iterator $passthru
+ */
+ public function passthru(callable $passthru = null): void
+ {
+ $this->passthru = $passthru ?? static function ($chunk, $context) {
+ $context->passthru = null;
+
+ yield $chunk;
+ };
+ }
+}
diff --git a/src/vendor/symfony/http-client/Response/AsyncResponse.php b/src/vendor/symfony/http-client/Response/AsyncResponse.php
new file mode 100644
index 0000000..80c9f7d
--- /dev/null
+++ b/src/vendor/symfony/http-client/Response/AsyncResponse.php
@@ -0,0 +1,478 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\HttpClient\Response;
+
+use Symfony\Component\HttpClient\Chunk\ErrorChunk;
+use Symfony\Component\HttpClient\Chunk\FirstChunk;
+use Symfony\Component\HttpClient\Chunk\LastChunk;
+use Symfony\Component\HttpClient\Exception\TransportException;
+use Symfony\Contracts\HttpClient\ChunkInterface;
+use Symfony\Contracts\HttpClient\Exception\ExceptionInterface;
+use Symfony\Contracts\HttpClient\Exception\HttpExceptionInterface;
+use Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface;
+use Symfony\Contracts\HttpClient\HttpClientInterface;
+use Symfony\Contracts\HttpClient\ResponseInterface;
+
+/**
+ * Provides a single extension point to process a response's content stream.
+ *
+ * @author Nicolas Grekas
+ */
+final class AsyncResponse implements ResponseInterface, StreamableInterface
+{
+ use CommonResponseTrait;
+
+ private const FIRST_CHUNK_YIELDED = 1;
+ private const LAST_CHUNK_YIELDED = 2;
+
+ private $client;
+ private $response;
+ private $info = ['canceled' => false];
+ private $passthru;
+ private $stream;
+ private $yieldedState;
+
+ /**
+ * @param ?callable(ChunkInterface, AsyncContext): ?\Iterator $passthru
+ */
+ public function __construct(HttpClientInterface $client, string $method, string $url, array $options, callable $passthru = null)
+ {
+ $this->client = $client;
+ $this->shouldBuffer = $options['buffer'] ?? true;
+
+ if (null !== $onProgress = $options['on_progress'] ?? null) {
+ $thisInfo = &$this->info;
+ $options['on_progress'] = static function (int $dlNow, int $dlSize, array $info) use (&$thisInfo, $onProgress) {
+ $onProgress($dlNow, $dlSize, $thisInfo + $info);
+ };
+ }
+ $this->response = $client->request($method, $url, ['buffer' => false] + $options);
+ $this->passthru = $passthru;
+ $this->initializer = static function (self $response, float $timeout = null) {
+ if (null === $response->shouldBuffer) {
+ return false;
+ }
+
+ while (true) {
+ foreach (self::stream([$response], $timeout) as $chunk) {
+ if ($chunk->isTimeout() && $response->passthru) {
+ foreach (self::passthru($response->client, $response, new ErrorChunk($response->offset, new TransportException($chunk->getError()))) as $chunk) {
+ if ($chunk->isFirst()) {
+ return false;
+ }
+ }
+
+ continue 2;
+ }
+
+ if ($chunk->isFirst()) {
+ return false;
+ }
+ }
+
+ return false;
+ }
+ };
+ if (\array_key_exists('user_data', $options)) {
+ $this->info['user_data'] = $options['user_data'];
+ }
+ if (\array_key_exists('max_duration', $options)) {
+ $this->info['max_duration'] = $options['max_duration'];
+ }
+ }
+
+ public function getStatusCode(): int
+ {
+ if ($this->initializer) {
+ self::initialize($this);
+ }
+
+ return $this->response->getStatusCode();
+ }
+
+ public function getHeaders(bool $throw = true): array
+ {
+ if ($this->initializer) {
+ self::initialize($this);
+ }
+
+ $headers = $this->response->getHeaders(false);
+
+ if ($throw) {
+ $this->checkStatusCode();
+ }
+
+ return $headers;
+ }
+
+ public function getInfo(string $type = null)
+ {
+ if (null !== $type) {
+ return $this->info[$type] ?? $this->response->getInfo($type);
+ }
+
+ return $this->info + $this->response->getInfo();
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function toStream(bool $throw = true)
+ {
+ if ($throw) {
+ // Ensure headers arrived
+ $this->getHeaders(true);
+ }
+
+ $handle = function () {
+ $stream = $this->response instanceof StreamableInterface ? $this->response->toStream(false) : StreamWrapper::createResource($this->response);
+
+ return stream_get_meta_data($stream)['wrapper_data']->stream_cast(\STREAM_CAST_FOR_SELECT);
+ };
+
+ $stream = StreamWrapper::createResource($this);
+ stream_get_meta_data($stream)['wrapper_data']
+ ->bindHandles($handle, $this->content);
+
+ return $stream;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function cancel(): void
+ {
+ if ($this->info['canceled']) {
+ return;
+ }
+
+ $this->info['canceled'] = true;
+ $this->info['error'] = 'Response has been canceled.';
+ $this->close();
+ $client = $this->client;
+ $this->client = null;
+
+ if (!$this->passthru) {
+ return;
+ }
+
+ try {
+ foreach (self::passthru($client, $this, new LastChunk()) as $chunk) {
+ // no-op
+ }
+
+ $this->passthru = null;
+ } catch (ExceptionInterface $e) {
+ // ignore any errors when canceling
+ }
+ }
+
+ public function __destruct()
+ {
+ $httpException = null;
+
+ if ($this->initializer && null === $this->getInfo('error')) {
+ try {
+ self::initialize($this, -0.0);
+ $this->getHeaders(true);
+ } catch (HttpExceptionInterface $httpException) {
+ // no-op
+ }
+ }
+
+ if ($this->passthru && null === $this->getInfo('error')) {
+ $this->info['canceled'] = true;
+
+ try {
+ foreach (self::passthru($this->client, $this, new LastChunk()) as $chunk) {
+ // no-op
+ }
+ } catch (ExceptionInterface $e) {
+ // ignore any errors when destructing
+ }
+ }
+
+ if (null !== $httpException) {
+ throw $httpException;
+ }
+ }
+
+ /**
+ * @internal
+ */
+ public static function stream(iterable $responses, float $timeout = null, string $class = null): \Generator
+ {
+ while ($responses) {
+ $wrappedResponses = [];
+ $asyncMap = new \SplObjectStorage();
+ $client = null;
+
+ foreach ($responses as $r) {
+ if (!$r instanceof self) {
+ throw new \TypeError(sprintf('"%s::stream()" expects parameter 1 to be an iterable of AsyncResponse objects, "%s" given.', $class ?? static::class, get_debug_type($r)));
+ }
+
+ if (null !== $e = $r->info['error'] ?? null) {
+ yield $r => $chunk = new ErrorChunk($r->offset, new TransportException($e));
+ $chunk->didThrow() ?: $chunk->getContent();
+ continue;
+ }
+
+ if (null === $client) {
+ $client = $r->client;
+ } elseif ($r->client !== $client) {
+ throw new TransportException('Cannot stream AsyncResponse objects with many clients.');
+ }
+
+ $asyncMap[$r->response] = $r;
+ $wrappedResponses[] = $r->response;
+
+ if ($r->stream) {
+ yield from self::passthruStream($response = $r->response, $r, new FirstChunk(), $asyncMap);
+
+ if (!isset($asyncMap[$response])) {
+ array_pop($wrappedResponses);
+ }
+
+ if ($r->response !== $response && !isset($asyncMap[$r->response])) {
+ $asyncMap[$r->response] = $r;
+ $wrappedResponses[] = $r->response;
+ }
+ }
+ }
+
+ if (!$client || !$wrappedResponses) {
+ return;
+ }
+
+ foreach ($client->stream($wrappedResponses, $timeout) as $response => $chunk) {
+ $r = $asyncMap[$response];
+
+ if (null === $chunk->getError()) {
+ if ($chunk->isFirst()) {
+ // Ensure no exception is thrown on destruct for the wrapped response
+ $r->response->getStatusCode();
+ } elseif (0 === $r->offset && null === $r->content && $chunk->isLast()) {
+ $r->content = fopen('php://memory', 'w+');
+ }
+ }
+
+ if (!$r->passthru) {
+ if (null !== $chunk->getError() || $chunk->isLast()) {
+ unset($asyncMap[$response]);
+ } elseif (null !== $r->content && '' !== ($content = $chunk->getContent()) && \strlen($content) !== fwrite($r->content, $content)) {
+ $chunk = new ErrorChunk($r->offset, new TransportException(sprintf('Failed writing %d bytes to the response buffer.', \strlen($content))));
+ $r->info['error'] = $chunk->getError();
+ $r->response->cancel();
+ }
+
+ yield $r => $chunk;
+ continue;
+ }
+
+ if (null !== $chunk->getError()) {
+ // no-op
+ } elseif ($chunk->isFirst()) {
+ $r->yieldedState = self::FIRST_CHUNK_YIELDED;
+ } elseif (self::FIRST_CHUNK_YIELDED !== $r->yieldedState && null === $chunk->getInformationalStatus()) {
+ throw new \LogicException(sprintf('Instance of "%s" is already consumed and cannot be managed by "%s". A decorated client should not call any of the response\'s methods in its "request()" method.', get_debug_type($response), $class ?? static::class));
+ }
+
+ foreach (self::passthru($r->client, $r, $chunk, $asyncMap) as $chunk) {
+ yield $r => $chunk;
+ }
+
+ if ($r->response !== $response && isset($asyncMap[$response])) {
+ break;
+ }
+ }
+
+ if (null === $chunk->getError() && $chunk->isLast()) {
+ $r->yieldedState = self::LAST_CHUNK_YIELDED;
+ }
+ if (null === $chunk->getError() && self::LAST_CHUNK_YIELDED !== $r->yieldedState && $r->response === $response && null !== $r->client) {
+ throw new \LogicException('A chunk passthru must yield an "isLast()" chunk before ending a stream.');
+ }
+
+ $responses = [];
+ foreach ($asyncMap as $response) {
+ $r = $asyncMap[$response];
+
+ if (null !== $r->client) {
+ $responses[] = $asyncMap[$response];
+ }
+ }
+ }
+ }
+
+ /**
+ * @param \SplObjectStorage
+ *
+ * @internal
+ */
+trait CommonResponseTrait
+{
+ /**
+ * @var callable|null A callback that tells whether we're waiting for response headers
+ */
+ private $initializer;
+ private $shouldBuffer;
+ private $content;
+ private $offset = 0;
+ private $jsonData;
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getContent(bool $throw = true): string
+ {
+ if ($this->initializer) {
+ self::initialize($this);
+ }
+
+ if ($throw) {
+ $this->checkStatusCode();
+ }
+
+ if (null === $this->content) {
+ $content = null;
+
+ foreach (self::stream([$this]) as $chunk) {
+ if (!$chunk->isLast()) {
+ $content .= $chunk->getContent();
+ }
+ }
+
+ if (null !== $content) {
+ return $content;
+ }
+
+ if (null === $this->content) {
+ throw new TransportException('Cannot get the content of the response twice: buffering is disabled.');
+ }
+ } else {
+ foreach (self::stream([$this]) as $chunk) {
+ // Chunks are buffered in $this->content already
+ }
+ }
+
+ rewind($this->content);
+
+ return stream_get_contents($this->content);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function toArray(bool $throw = true): array
+ {
+ if ('' === $content = $this->getContent($throw)) {
+ throw new JsonException('Response body is empty.');
+ }
+
+ if (null !== $this->jsonData) {
+ return $this->jsonData;
+ }
+
+ try {
+ $content = json_decode($content, true, 512, \JSON_BIGINT_AS_STRING | (\PHP_VERSION_ID >= 70300 ? \JSON_THROW_ON_ERROR : 0));
+ } catch (\JsonException $e) {
+ throw new JsonException($e->getMessage().sprintf(' for "%s".', $this->getInfo('url')), $e->getCode());
+ }
+
+ if (\PHP_VERSION_ID < 70300 && \JSON_ERROR_NONE !== json_last_error()) {
+ throw new JsonException(json_last_error_msg().sprintf(' for "%s".', $this->getInfo('url')), json_last_error());
+ }
+
+ if (!\is_array($content)) {
+ throw new JsonException(sprintf('JSON content was expected to decode to an array, "%s" returned for "%s".', get_debug_type($content), $this->getInfo('url')));
+ }
+
+ if (null !== $this->content) {
+ // Option "buffer" is true
+ return $this->jsonData = $content;
+ }
+
+ return $content;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function toStream(bool $throw = true)
+ {
+ if ($throw) {
+ // Ensure headers arrived
+ $this->getHeaders($throw);
+ }
+
+ $stream = StreamWrapper::createResource($this);
+ stream_get_meta_data($stream)['wrapper_data']
+ ->bindHandles($this->handle, $this->content);
+
+ return $stream;
+ }
+
+ public function __sleep(): array
+ {
+ throw new \BadMethodCallException('Cannot serialize '.__CLASS__);
+ }
+
+ public function __wakeup()
+ {
+ throw new \BadMethodCallException('Cannot unserialize '.__CLASS__);
+ }
+
+ /**
+ * Closes the response and all its network handles.
+ */
+ abstract protected function close(): void;
+
+ private static function initialize(self $response): void
+ {
+ if (null !== $response->getInfo('error')) {
+ throw new TransportException($response->getInfo('error'));
+ }
+
+ try {
+ if (($response->initializer)($response, -0.0)) {
+ foreach (self::stream([$response], -0.0) as $chunk) {
+ if ($chunk->isFirst()) {
+ break;
+ }
+ }
+ }
+ } catch (\Throwable $e) {
+ // Persist timeouts thrown during initialization
+ $response->info['error'] = $e->getMessage();
+ $response->close();
+ throw $e;
+ }
+
+ $response->initializer = null;
+ }
+
+ private function checkStatusCode()
+ {
+ $code = $this->getInfo('http_code');
+
+ if (500 <= $code) {
+ throw new ServerException($this);
+ }
+
+ if (400 <= $code) {
+ throw new ClientException($this);
+ }
+
+ if (300 <= $code) {
+ throw new RedirectionException($this);
+ }
+ }
+}
diff --git a/src/vendor/symfony/http-client/Response/CurlResponse.php b/src/vendor/symfony/http-client/Response/CurlResponse.php
new file mode 100644
index 0000000..2418203
--- /dev/null
+++ b/src/vendor/symfony/http-client/Response/CurlResponse.php
@@ -0,0 +1,473 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\HttpClient\Response;
+
+use Psr\Log\LoggerInterface;
+use Symfony\Component\HttpClient\Chunk\FirstChunk;
+use Symfony\Component\HttpClient\Chunk\InformationalChunk;
+use Symfony\Component\HttpClient\Exception\TransportException;
+use Symfony\Component\HttpClient\Internal\Canary;
+use Symfony\Component\HttpClient\Internal\ClientState;
+use Symfony\Component\HttpClient\Internal\CurlClientState;
+use Symfony\Contracts\HttpClient\ResponseInterface;
+
+/**
+ * @author Nicolas Grekas
+ *
+ * @internal
+ */
+final class CurlResponse implements ResponseInterface, StreamableInterface
+{
+ use CommonResponseTrait {
+ getContent as private doGetContent;
+ }
+ use TransportResponseTrait;
+
+ private $multi;
+ private $debugBuffer;
+
+ /**
+ * @param \CurlHandle|resource|string $ch
+ *
+ * @internal
+ */
+ public function __construct(CurlClientState $multi, $ch, array $options = null, LoggerInterface $logger = null, string $method = 'GET', callable $resolveRedirect = null, int $curlVersion = null)
+ {
+ $this->multi = $multi;
+
+ if (\is_resource($ch) || $ch instanceof \CurlHandle) {
+ $this->handle = $ch;
+ $this->debugBuffer = fopen('php://temp', 'w+');
+ if (0x074000 === $curlVersion) {
+ fwrite($this->debugBuffer, 'Due to a bug in curl 7.64.0, the debug log is disabled; use another version to work around the issue.');
+ } else {
+ curl_setopt($ch, \CURLOPT_VERBOSE, true);
+ curl_setopt($ch, \CURLOPT_STDERR, $this->debugBuffer);
+ }
+ } else {
+ $this->info['url'] = $ch;
+ $ch = $this->handle;
+ }
+
+ $this->id = $id = (int) $ch;
+ $this->logger = $logger;
+ $this->shouldBuffer = $options['buffer'] ?? true;
+ $this->timeout = $options['timeout'] ?? null;
+ $this->info['http_method'] = $method;
+ $this->info['user_data'] = $options['user_data'] ?? null;
+ $this->info['max_duration'] = $options['max_duration'] ?? null;
+ $this->info['start_time'] = $this->info['start_time'] ?? microtime(true);
+ $info = &$this->info;
+ $headers = &$this->headers;
+ $debugBuffer = $this->debugBuffer;
+
+ if (!$info['response_headers']) {
+ // Used to keep track of what we're waiting for
+ curl_setopt($ch, \CURLOPT_PRIVATE, \in_array($method, ['GET', 'HEAD', 'OPTIONS', 'TRACE'], true) && 1.0 < (float) ($options['http_version'] ?? 1.1) ? 'H2' : 'H0'); // H = headers + retry counter
+ }
+
+ curl_setopt($ch, \CURLOPT_HEADERFUNCTION, static function ($ch, string $data) use (&$info, &$headers, $options, $multi, $id, &$location, $resolveRedirect, $logger): int {
+ return self::parseHeaderLine($ch, $data, $info, $headers, $options, $multi, $id, $location, $resolveRedirect, $logger);
+ });
+
+ if (null === $options) {
+ // Pushed response: buffer until requested
+ curl_setopt($ch, \CURLOPT_WRITEFUNCTION, static function ($ch, string $data) use ($multi, $id): int {
+ $multi->handlesActivity[$id][] = $data;
+ curl_pause($ch, \CURLPAUSE_RECV);
+
+ return \strlen($data);
+ });
+
+ return;
+ }
+
+ $execCounter = $multi->execCounter;
+ $this->info['pause_handler'] = static function (float $duration) use ($ch, $multi, $execCounter) {
+ if (0 < $duration) {
+ if ($execCounter === $multi->execCounter) {
+ $multi->execCounter = !\is_float($execCounter) ? 1 + $execCounter : \PHP_INT_MIN;
+ curl_multi_remove_handle($multi->handle, $ch);
+ }
+
+ $lastExpiry = end($multi->pauseExpiries);
+ $multi->pauseExpiries[(int) $ch] = $duration += microtime(true);
+ if (false !== $lastExpiry && $lastExpiry > $duration) {
+ asort($multi->pauseExpiries);
+ }
+ curl_pause($ch, \CURLPAUSE_ALL);
+ } else {
+ unset($multi->pauseExpiries[(int) $ch]);
+ curl_pause($ch, \CURLPAUSE_CONT);
+ curl_multi_add_handle($multi->handle, $ch);
+ }
+ };
+
+ $this->inflate = !isset($options['normalized_headers']['accept-encoding']);
+ curl_pause($ch, \CURLPAUSE_CONT);
+
+ if ($onProgress = $options['on_progress']) {
+ $url = isset($info['url']) ? ['url' => $info['url']] : [];
+ curl_setopt($ch, \CURLOPT_NOPROGRESS, false);
+ curl_setopt($ch, \CURLOPT_PROGRESSFUNCTION, static function ($ch, $dlSize, $dlNow) use ($onProgress, &$info, $url, $multi, $debugBuffer) {
+ try {
+ rewind($debugBuffer);
+ $debug = ['debug' => stream_get_contents($debugBuffer)];
+ $onProgress($dlNow, $dlSize, $url + curl_getinfo($ch) + $info + $debug);
+ } catch (\Throwable $e) {
+ $multi->handlesActivity[(int) $ch][] = null;
+ $multi->handlesActivity[(int) $ch][] = $e;
+
+ return 1; // Abort the request
+ }
+
+ return null;
+ });
+ }
+
+ curl_setopt($ch, \CURLOPT_WRITEFUNCTION, static function ($ch, string $data) use ($multi, $id): int {
+ if ('H' === (curl_getinfo($ch, \CURLINFO_PRIVATE)[0] ?? null)) {
+ $multi->handlesActivity[$id][] = null;
+ $multi->handlesActivity[$id][] = new TransportException(sprintf('Unsupported protocol for "%s"', curl_getinfo($ch, \CURLINFO_EFFECTIVE_URL)));
+
+ return 0;
+ }
+
+ curl_setopt($ch, \CURLOPT_WRITEFUNCTION, static function ($ch, string $data) use ($multi, $id): int {
+ $multi->handlesActivity[$id][] = $data;
+
+ return \strlen($data);
+ });
+
+ $multi->handlesActivity[$id][] = $data;
+
+ return \strlen($data);
+ });
+
+ $this->initializer = static function (self $response) {
+ $waitFor = curl_getinfo($ch = $response->handle, \CURLINFO_PRIVATE);
+
+ return 'H' === $waitFor[0];
+ };
+
+ // Schedule the request in a non-blocking way
+ $multi->lastTimeout = null;
+ $multi->openHandles[$id] = [$ch, $options];
+ curl_multi_add_handle($multi->handle, $ch);
+
+ $this->canary = new Canary(static function () use ($ch, $multi, $id) {
+ unset($multi->pauseExpiries[$id], $multi->openHandles[$id], $multi->handlesActivity[$id]);
+ curl_setopt($ch, \CURLOPT_PRIVATE, '_0');
+
+ if ($multi->performing) {
+ return;
+ }
+
+ curl_multi_remove_handle($multi->handle, $ch);
+ curl_setopt_array($ch, [
+ \CURLOPT_NOPROGRESS => true,
+ \CURLOPT_PROGRESSFUNCTION => null,
+ \CURLOPT_HEADERFUNCTION => null,
+ \CURLOPT_WRITEFUNCTION => null,
+ \CURLOPT_READFUNCTION => null,
+ \CURLOPT_INFILE => null,
+ ]);
+
+ if (!$multi->openHandles) {
+ // Schedule DNS cache eviction for the next request
+ $multi->dnsCache->evictions = $multi->dnsCache->evictions ?: $multi->dnsCache->removals;
+ $multi->dnsCache->removals = $multi->dnsCache->hostnames = [];
+ }
+ });
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getInfo(string $type = null)
+ {
+ if (!$info = $this->finalInfo) {
+ $info = array_merge($this->info, curl_getinfo($this->handle));
+ $info['url'] = $this->info['url'] ?? $info['url'];
+ $info['redirect_url'] = $this->info['redirect_url'] ?? null;
+
+ // workaround curl not subtracting the time offset for pushed responses
+ if (isset($this->info['url']) && $info['start_time'] / 1000 < $info['total_time']) {
+ $info['total_time'] -= $info['starttransfer_time'] ?: $info['total_time'];
+ $info['starttransfer_time'] = 0.0;
+ }
+
+ rewind($this->debugBuffer);
+ $info['debug'] = stream_get_contents($this->debugBuffer);
+ $waitFor = curl_getinfo($this->handle, \CURLINFO_PRIVATE);
+
+ if ('H' !== $waitFor[0] && 'C' !== $waitFor[0]) {
+ curl_setopt($this->handle, \CURLOPT_VERBOSE, false);
+ rewind($this->debugBuffer);
+ ftruncate($this->debugBuffer, 0);
+ $this->finalInfo = $info;
+ }
+ }
+
+ return null !== $type ? $info[$type] ?? null : $info;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getContent(bool $throw = true): string
+ {
+ $performing = $this->multi->performing;
+ $this->multi->performing = $performing || '_0' === curl_getinfo($this->handle, \CURLINFO_PRIVATE);
+
+ try {
+ return $this->doGetContent($throw);
+ } finally {
+ $this->multi->performing = $performing;
+ }
+ }
+
+ public function __destruct()
+ {
+ try {
+ if (null === $this->timeout) {
+ return; // Unused pushed response
+ }
+
+ $this->doDestruct();
+ } finally {
+ if (\is_resource($this->handle) || $this->handle instanceof \CurlHandle) {
+ curl_setopt($this->handle, \CURLOPT_VERBOSE, false);
+ }
+ }
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ private static function schedule(self $response, array &$runningResponses): void
+ {
+ if (isset($runningResponses[$i = (int) $response->multi->handle])) {
+ $runningResponses[$i][1][$response->id] = $response;
+ } else {
+ $runningResponses[$i] = [$response->multi, [$response->id => $response]];
+ }
+
+ if ('_0' === curl_getinfo($ch = $response->handle, \CURLINFO_PRIVATE)) {
+ // Response already completed
+ $response->multi->handlesActivity[$response->id][] = null;
+ $response->multi->handlesActivity[$response->id][] = null !== $response->info['error'] ? new TransportException($response->info['error']) : null;
+ }
+ }
+
+ /**
+ * {@inheritdoc}
+ *
+ * @param CurlClientState $multi
+ */
+ private static function perform(ClientState $multi, array &$responses = null): void
+ {
+ if ($multi->performing) {
+ if ($responses) {
+ $response = current($responses);
+ $multi->handlesActivity[(int) $response->handle][] = null;
+ $multi->handlesActivity[(int) $response->handle][] = new TransportException(sprintf('Userland callback cannot use the client nor the response while processing "%s".', curl_getinfo($response->handle, \CURLINFO_EFFECTIVE_URL)));
+ }
+
+ return;
+ }
+
+ try {
+ $multi->performing = true;
+ ++$multi->execCounter;
+ $active = 0;
+ while (\CURLM_CALL_MULTI_PERFORM === ($err = curl_multi_exec($multi->handle, $active))) {
+ }
+
+ if (\CURLM_OK !== $err) {
+ throw new TransportException(curl_multi_strerror($err));
+ }
+
+ while ($info = curl_multi_info_read($multi->handle)) {
+ if (\CURLMSG_DONE !== $info['msg']) {
+ continue;
+ }
+ $result = $info['result'];
+ $id = (int) $ch = $info['handle'];
+ $waitFor = @curl_getinfo($ch, \CURLINFO_PRIVATE) ?: '_0';
+
+ if (\in_array($result, [\CURLE_SEND_ERROR, \CURLE_RECV_ERROR, /* CURLE_HTTP2 */ 16, /* CURLE_HTTP2_STREAM */ 92], true) && $waitFor[1] && 'C' !== $waitFor[0]) {
+ curl_multi_remove_handle($multi->handle, $ch);
+ $waitFor[1] = (string) ((int) $waitFor[1] - 1); // decrement the retry counter
+ curl_setopt($ch, \CURLOPT_PRIVATE, $waitFor);
+ curl_setopt($ch, \CURLOPT_FORBID_REUSE, true);
+
+ if (0 === curl_multi_add_handle($multi->handle, $ch)) {
+ continue;
+ }
+ }
+
+ if (\CURLE_RECV_ERROR === $result && 'H' === $waitFor[0] && 400 <= ($responses[(int) $ch]->info['http_code'] ?? 0)) {
+ $multi->handlesActivity[$id][] = new FirstChunk();
+ }
+
+ $multi->handlesActivity[$id][] = null;
+ $multi->handlesActivity[$id][] = \in_array($result, [\CURLE_OK, \CURLE_TOO_MANY_REDIRECTS], true) || '_0' === $waitFor || curl_getinfo($ch, \CURLINFO_SIZE_DOWNLOAD) === curl_getinfo($ch, \CURLINFO_CONTENT_LENGTH_DOWNLOAD) ? null : new TransportException(ucfirst(curl_error($ch) ?: curl_strerror($result)).sprintf(' for "%s".', curl_getinfo($ch, \CURLINFO_EFFECTIVE_URL)));
+ }
+ } finally {
+ $multi->performing = false;
+ }
+ }
+
+ /**
+ * {@inheritdoc}
+ *
+ * @param CurlClientState $multi
+ */
+ private static function select(ClientState $multi, float $timeout): int
+ {
+ if (\PHP_VERSION_ID < 70211) {
+ // workaround https://bugs.php.net/76480
+ $timeout = min($timeout, 0.01);
+ }
+
+ if ($multi->pauseExpiries) {
+ $now = microtime(true);
+
+ foreach ($multi->pauseExpiries as $id => $pauseExpiry) {
+ if ($now < $pauseExpiry) {
+ $timeout = min($timeout, $pauseExpiry - $now);
+ break;
+ }
+
+ unset($multi->pauseExpiries[$id]);
+ curl_pause($multi->openHandles[$id][0], \CURLPAUSE_CONT);
+ curl_multi_add_handle($multi->handle, $multi->openHandles[$id][0]);
+ }
+ }
+
+ if (0 !== $selected = curl_multi_select($multi->handle, $timeout)) {
+ return $selected;
+ }
+
+ if ($multi->pauseExpiries && 0 < $timeout -= microtime(true) - $now) {
+ usleep((int) (1E6 * $timeout));
+ }
+
+ return 0;
+ }
+
+ /**
+ * Parses header lines as curl yields them to us.
+ */
+ private static function parseHeaderLine($ch, string $data, array &$info, array &$headers, ?array $options, CurlClientState $multi, int $id, ?string &$location, ?callable $resolveRedirect, ?LoggerInterface $logger): int
+ {
+ if (!str_ends_with($data, "\r\n")) {
+ return 0;
+ }
+
+ $waitFor = @curl_getinfo($ch, \CURLINFO_PRIVATE) ?: '_0';
+
+ if ('H' !== $waitFor[0]) {
+ return \strlen($data); // Ignore HTTP trailers
+ }
+
+ $statusCode = curl_getinfo($ch, \CURLINFO_RESPONSE_CODE);
+
+ if ($statusCode !== $info['http_code'] && !preg_match("#^HTTP/\d+(?:\.\d+)? {$statusCode}(?: |\r\n$)#", $data)) {
+ return \strlen($data); // Ignore headers from responses to CONNECT requests
+ }
+
+ if ("\r\n" !== $data) {
+ // Regular header line: add it to the list
+ self::addResponseHeaders([substr($data, 0, -2)], $info, $headers);
+
+ if (!str_starts_with($data, 'HTTP/')) {
+ if (0 === stripos($data, 'Location:')) {
+ $location = trim(substr($data, 9, -2));
+ }
+
+ return \strlen($data);
+ }
+
+ if (\function_exists('openssl_x509_read') && $certinfo = curl_getinfo($ch, \CURLINFO_CERTINFO)) {
+ $info['peer_certificate_chain'] = array_map('openssl_x509_read', array_column($certinfo, 'Cert'));
+ }
+
+ if (300 <= $info['http_code'] && $info['http_code'] < 400) {
+ if (curl_getinfo($ch, \CURLINFO_REDIRECT_COUNT) === $options['max_redirects']) {
+ curl_setopt($ch, \CURLOPT_FOLLOWLOCATION, false);
+ } elseif (303 === $info['http_code'] || ('POST' === $info['http_method'] && \in_array($info['http_code'], [301, 302], true))) {
+ curl_setopt($ch, \CURLOPT_POSTFIELDS, '');
+ }
+ }
+
+ return \strlen($data);
+ }
+
+ // End of headers: handle informational responses, redirects, etc.
+
+ if (200 > $statusCode) {
+ $multi->handlesActivity[$id][] = new InformationalChunk($statusCode, $headers);
+ $location = null;
+
+ return \strlen($data);
+ }
+
+ $info['redirect_url'] = null;
+
+ if (300 <= $statusCode && $statusCode < 400 && null !== $location) {
+ if ($noContent = 303 === $statusCode || ('POST' === $info['http_method'] && \in_array($statusCode, [301, 302], true))) {
+ $info['http_method'] = 'HEAD' === $info['http_method'] ? 'HEAD' : 'GET';
+ curl_setopt($ch, \CURLOPT_CUSTOMREQUEST, $info['http_method']);
+ }
+
+ if (null === $info['redirect_url'] = $resolveRedirect($ch, $location, $noContent)) {
+ $options['max_redirects'] = curl_getinfo($ch, \CURLINFO_REDIRECT_COUNT);
+ curl_setopt($ch, \CURLOPT_FOLLOWLOCATION, false);
+ curl_setopt($ch, \CURLOPT_MAXREDIRS, $options['max_redirects']);
+ } else {
+ $url = parse_url($location ?? ':');
+
+ if (isset($url['host']) && null !== $ip = $multi->dnsCache->hostnames[$url['host'] = strtolower($url['host'])] ?? null) {
+ // Populate DNS cache for redirects if needed
+ $port = $url['port'] ?? ('http' === ($url['scheme'] ?? parse_url(curl_getinfo($ch, \CURLINFO_EFFECTIVE_URL), \PHP_URL_SCHEME)) ? 80 : 443);
+ curl_setopt($ch, \CURLOPT_RESOLVE, ["{$url['host']}:$port:$ip"]);
+ $multi->dnsCache->removals["-{$url['host']}:$port"] = "-{$url['host']}:$port";
+ }
+ }
+ }
+
+ if (401 === $statusCode && isset($options['auth_ntlm']) && 0 === strncasecmp($headers['www-authenticate'][0] ?? '', 'NTLM ', 5)) {
+ // Continue with NTLM auth
+ } elseif ($statusCode < 300 || 400 <= $statusCode || null === $location || curl_getinfo($ch, \CURLINFO_REDIRECT_COUNT) === $options['max_redirects']) {
+ // Headers and redirects completed, time to get the response's content
+ $multi->handlesActivity[$id][] = new FirstChunk();
+
+ if ('HEAD' === $info['http_method'] || \in_array($statusCode, [204, 304], true)) {
+ $waitFor = '_0'; // no content expected
+ $multi->handlesActivity[$id][] = null;
+ $multi->handlesActivity[$id][] = null;
+ } else {
+ $waitFor[0] = 'C'; // C = content
+ }
+
+ curl_setopt($ch, \CURLOPT_PRIVATE, $waitFor);
+ } elseif (null !== $info['redirect_url'] && $logger) {
+ $logger->info(sprintf('Redirecting: "%s %s"', $info['http_code'], $info['redirect_url']));
+ }
+
+ $location = null;
+
+ return \strlen($data);
+ }
+}
diff --git a/src/vendor/symfony/http-client/Response/HttplugPromise.php b/src/vendor/symfony/http-client/Response/HttplugPromise.php
new file mode 100644
index 0000000..2efacca
--- /dev/null
+++ b/src/vendor/symfony/http-client/Response/HttplugPromise.php
@@ -0,0 +1,80 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\HttpClient\Response;
+
+use GuzzleHttp\Promise\Create;
+use GuzzleHttp\Promise\PromiseInterface as GuzzlePromiseInterface;
+use Http\Promise\Promise as HttplugPromiseInterface;
+use Psr\Http\Message\ResponseInterface as Psr7ResponseInterface;
+
+/**
+ * @author Tobias Nyholm
+ */
+class MockResponse implements ResponseInterface, StreamableInterface
+{
+ use CommonResponseTrait;
+ use TransportResponseTrait {
+ doDestruct as public __destruct;
+ }
+
+ private $body;
+ private $requestOptions = [];
+ private $requestUrl;
+ private $requestMethod;
+
+ private static $mainMulti;
+ private static $idSequence = 0;
+
+ /**
+ * @param string|string[]|iterable $body The response body as a string or an iterable of strings,
+ * yielding an empty string simulates an idle timeout,
+ * throwing an exception yields an ErrorChunk
+ *
+ * @see ResponseInterface::getInfo() for possible info, e.g. "response_headers"
+ */
+ public function __construct($body = '', array $info = [])
+ {
+ $this->body = is_iterable($body) ? $body : (string) $body;
+ $this->info = $info + ['http_code' => 200] + $this->info;
+
+ if (!isset($info['response_headers'])) {
+ return;
+ }
+
+ $responseHeaders = [];
+
+ foreach ($info['response_headers'] as $k => $v) {
+ foreach ((array) $v as $v) {
+ $responseHeaders[] = (\is_string($k) ? $k.': ' : '').$v;
+ }
+ }
+
+ $this->info['response_headers'] = [];
+ self::addResponseHeaders($responseHeaders, $this->info, $this->headers);
+ }
+
+ /**
+ * Returns the options used when doing the request.
+ */
+ public function getRequestOptions(): array
+ {
+ return $this->requestOptions;
+ }
+
+ /**
+ * Returns the URL used when doing the request.
+ */
+ public function getRequestUrl(): string
+ {
+ return $this->requestUrl;
+ }
+
+ /**
+ * Returns the method used when doing the request.
+ */
+ public function getRequestMethod(): string
+ {
+ return $this->requestMethod;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getInfo(string $type = null)
+ {
+ return null !== $type ? $this->info[$type] ?? null : $this->info;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function cancel(): void
+ {
+ $this->info['canceled'] = true;
+ $this->info['error'] = 'Response has been canceled.';
+ try {
+ $this->body = null;
+ } catch (TransportException $e) {
+ // ignore errors when canceling
+ }
+
+ $onProgress = $this->requestOptions['on_progress'] ?? static function () {};
+ $dlSize = isset($this->headers['content-encoding']) || 'HEAD' === ($this->info['http_method'] ?? null) || \in_array($this->info['http_code'], [204, 304], true) ? 0 : (int) ($this->headers['content-length'][0] ?? 0);
+ $onProgress($this->offset, $dlSize, $this->info);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function close(): void
+ {
+ $this->inflate = null;
+ $this->body = [];
+ }
+
+ /**
+ * @internal
+ */
+ public static function fromRequest(string $method, string $url, array $options, ResponseInterface $mock): self
+ {
+ $response = new self([]);
+ $response->requestOptions = $options;
+ $response->id = ++self::$idSequence;
+ $response->shouldBuffer = $options['buffer'] ?? true;
+ $response->initializer = static function (self $response) {
+ return \is_array($response->body[0] ?? null);
+ };
+
+ $response->info['redirect_count'] = 0;
+ $response->info['redirect_url'] = null;
+ $response->info['start_time'] = microtime(true);
+ $response->info['http_method'] = $method;
+ $response->info['http_code'] = 0;
+ $response->info['user_data'] = $options['user_data'] ?? null;
+ $response->info['max_duration'] = $options['max_duration'] ?? null;
+ $response->info['url'] = $url;
+
+ if ($mock instanceof self) {
+ $mock->requestOptions = $response->requestOptions;
+ $mock->requestMethod = $method;
+ $mock->requestUrl = $url;
+ }
+
+ self::writeRequest($response, $options, $mock);
+ $response->body[] = [$options, $mock];
+
+ return $response;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ protected static function schedule(self $response, array &$runningResponses): void
+ {
+ if (!$response->id) {
+ throw new InvalidArgumentException('MockResponse instances must be issued by MockHttpClient before processing.');
+ }
+
+ $multi = self::$mainMulti ?? self::$mainMulti = new ClientState();
+
+ if (!isset($runningResponses[0])) {
+ $runningResponses[0] = [$multi, []];
+ }
+
+ $runningResponses[0][1][$response->id] = $response;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ protected static function perform(ClientState $multi, array &$responses): void
+ {
+ foreach ($responses as $response) {
+ $id = $response->id;
+
+ if (null === $response->body) {
+ // Canceled response
+ $response->body = [];
+ } elseif ([] === $response->body) {
+ // Error chunk
+ $multi->handlesActivity[$id][] = null;
+ $multi->handlesActivity[$id][] = null !== $response->info['error'] ? new TransportException($response->info['error']) : null;
+ } elseif (null === $chunk = array_shift($response->body)) {
+ // Last chunk
+ $multi->handlesActivity[$id][] = null;
+ $multi->handlesActivity[$id][] = array_shift($response->body);
+ } elseif (\is_array($chunk)) {
+ // First chunk
+ try {
+ $offset = 0;
+ $chunk[1]->getStatusCode();
+ $chunk[1]->getHeaders(false);
+ self::readResponse($response, $chunk[0], $chunk[1], $offset);
+ $multi->handlesActivity[$id][] = new FirstChunk();
+ $buffer = $response->requestOptions['buffer'] ?? null;
+
+ if ($buffer instanceof \Closure && $response->content = $buffer($response->headers) ?: null) {
+ $response->content = \is_resource($response->content) ? $response->content : fopen('php://temp', 'w+');
+ }
+ } catch (\Throwable $e) {
+ $multi->handlesActivity[$id][] = null;
+ $multi->handlesActivity[$id][] = $e;
+ }
+ } elseif ($chunk instanceof \Throwable) {
+ $multi->handlesActivity[$id][] = null;
+ $multi->handlesActivity[$id][] = $chunk;
+ } else {
+ // Data or timeout chunk
+ $multi->handlesActivity[$id][] = $chunk;
+ }
+ }
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ protected static function select(ClientState $multi, float $timeout): int
+ {
+ return 42;
+ }
+
+ /**
+ * Simulates sending the request.
+ */
+ private static function writeRequest(self $response, array $options, ResponseInterface $mock)
+ {
+ $onProgress = $options['on_progress'] ?? static function () {};
+ $response->info += $mock->getInfo() ?: [];
+
+ // simulate "size_upload" if it is set
+ if (isset($response->info['size_upload'])) {
+ $response->info['size_upload'] = 0.0;
+ }
+
+ // simulate "total_time" if it is not set
+ if (!isset($response->info['total_time'])) {
+ $response->info['total_time'] = microtime(true) - $response->info['start_time'];
+ }
+
+ // "notify" DNS resolution
+ $onProgress(0, 0, $response->info);
+
+ // consume the request body
+ if (\is_resource($body = $options['body'] ?? '')) {
+ $data = stream_get_contents($body);
+ if (isset($response->info['size_upload'])) {
+ $response->info['size_upload'] += \strlen($data);
+ }
+ } elseif ($body instanceof \Closure) {
+ while ('' !== $data = $body(16372)) {
+ if (!\is_string($data)) {
+ throw new TransportException(sprintf('Return value of the "body" option callback must be string, "%s" returned.', get_debug_type($data)));
+ }
+
+ // "notify" upload progress
+ if (isset($response->info['size_upload'])) {
+ $response->info['size_upload'] += \strlen($data);
+ }
+
+ $onProgress(0, 0, $response->info);
+ }
+ }
+ }
+
+ /**
+ * Simulates reading the response.
+ */
+ private static function readResponse(self $response, array $options, ResponseInterface $mock, int &$offset)
+ {
+ $onProgress = $options['on_progress'] ?? static function () {};
+
+ // populate info related to headers
+ $info = $mock->getInfo() ?: [];
+ $response->info['http_code'] = ($info['http_code'] ?? 0) ?: $mock->getStatusCode() ?: 200;
+ $response->addResponseHeaders($info['response_headers'] ?? [], $response->info, $response->headers);
+ $dlSize = isset($response->headers['content-encoding']) || 'HEAD' === $response->info['http_method'] || \in_array($response->info['http_code'], [204, 304], true) ? 0 : (int) ($response->headers['content-length'][0] ?? 0);
+
+ $response->info = [
+ 'start_time' => $response->info['start_time'],
+ 'user_data' => $response->info['user_data'],
+ 'max_duration' => $response->info['max_duration'],
+ 'http_code' => $response->info['http_code'],
+ ] + $info + $response->info;
+
+ if (null !== $response->info['error']) {
+ throw new TransportException($response->info['error']);
+ }
+
+ if (!isset($response->info['total_time'])) {
+ $response->info['total_time'] = microtime(true) - $response->info['start_time'];
+ }
+
+ // "notify" headers arrival
+ $onProgress(0, $dlSize, $response->info);
+
+ // cast response body to activity list
+ $body = $mock instanceof self ? $mock->body : $mock->getContent(false);
+
+ if (!\is_string($body)) {
+ try {
+ foreach ($body as $chunk) {
+ if ('' === $chunk = (string) $chunk) {
+ // simulate an idle timeout
+ $response->body[] = new ErrorChunk($offset, sprintf('Idle timeout reached for "%s".', $response->info['url']));
+ } else {
+ $response->body[] = $chunk;
+ $offset += \strlen($chunk);
+ // "notify" download progress
+ $onProgress($offset, $dlSize, $response->info);
+ }
+ }
+ } catch (\Throwable $e) {
+ $response->body[] = $e;
+ }
+ } elseif ('' !== $body) {
+ $response->body[] = $body;
+ $offset = \strlen($body);
+ }
+
+ if (!isset($response->info['total_time'])) {
+ $response->info['total_time'] = microtime(true) - $response->info['start_time'];
+ }
+
+ // "notify" completion
+ $onProgress($offset, $dlSize, $response->info);
+
+ if ($dlSize && $offset !== $dlSize) {
+ throw new TransportException(sprintf('Transfer closed with %d bytes remaining to read.', $dlSize - $offset));
+ }
+ }
+}
diff --git a/src/vendor/symfony/http-client/Response/NativeResponse.php b/src/vendor/symfony/http-client/Response/NativeResponse.php
new file mode 100644
index 0000000..c00e946
--- /dev/null
+++ b/src/vendor/symfony/http-client/Response/NativeResponse.php
@@ -0,0 +1,376 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\HttpClient\Response;
+
+use Psr\Log\LoggerInterface;
+use Symfony\Component\HttpClient\Chunk\FirstChunk;
+use Symfony\Component\HttpClient\Exception\TransportException;
+use Symfony\Component\HttpClient\Internal\Canary;
+use Symfony\Component\HttpClient\Internal\ClientState;
+use Symfony\Component\HttpClient\Internal\NativeClientState;
+use Symfony\Contracts\HttpClient\ResponseInterface;
+
+/**
+ * @author Nicolas Grekas
+ *
+ * @internal
+ */
+final class NativeResponse implements ResponseInterface, StreamableInterface
+{
+ use CommonResponseTrait;
+ use TransportResponseTrait;
+
+ private $context;
+ private $url;
+ private $resolver;
+ private $onProgress;
+ private $remaining;
+ private $buffer;
+ private $multi;
+ private $pauseExpiry = 0;
+
+ /**
+ * @internal
+ */
+ public function __construct(NativeClientState $multi, $context, string $url, array $options, array &$info, callable $resolver, ?callable $onProgress, ?LoggerInterface $logger)
+ {
+ $this->multi = $multi;
+ $this->id = $id = (int) $context;
+ $this->context = $context;
+ $this->url = $url;
+ $this->logger = $logger;
+ $this->timeout = $options['timeout'];
+ $this->info = &$info;
+ $this->resolver = $resolver;
+ $this->onProgress = $onProgress;
+ $this->inflate = !isset($options['normalized_headers']['accept-encoding']);
+ $this->shouldBuffer = $options['buffer'] ?? true;
+
+ // Temporary resource to dechunk the response stream
+ $this->buffer = fopen('php://temp', 'w+');
+
+ $info['user_data'] = $options['user_data'];
+ $info['max_duration'] = $options['max_duration'];
+ ++$multi->responseCount;
+
+ $this->initializer = static function (self $response) {
+ return null === $response->remaining;
+ };
+
+ $pauseExpiry = &$this->pauseExpiry;
+ $info['pause_handler'] = static function (float $duration) use (&$pauseExpiry) {
+ $pauseExpiry = 0 < $duration ? microtime(true) + $duration : 0;
+ };
+
+ $this->canary = new Canary(static function () use ($multi, $id) {
+ if (null !== ($host = $multi->openHandles[$id][6] ?? null) && 0 >= --$multi->hosts[$host]) {
+ unset($multi->hosts[$host]);
+ }
+ unset($multi->openHandles[$id], $multi->handlesActivity[$id]);
+ });
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getInfo(string $type = null)
+ {
+ if (!$info = $this->finalInfo) {
+ $info = $this->info;
+ $info['url'] = implode('', $info['url']);
+ unset($info['size_body'], $info['request_header']);
+
+ if (null === $this->buffer) {
+ $this->finalInfo = $info;
+ }
+ }
+
+ return null !== $type ? $info[$type] ?? null : $info;
+ }
+
+ public function __destruct()
+ {
+ try {
+ $this->doDestruct();
+ } finally {
+ // Clear the DNS cache when all requests completed
+ if (0 >= --$this->multi->responseCount) {
+ $this->multi->responseCount = 0;
+ $this->multi->dnsCache = [];
+ }
+ }
+ }
+
+ private function open(): void
+ {
+ $url = $this->url;
+
+ set_error_handler(function ($type, $msg) use (&$url) {
+ if (\E_NOTICE !== $type || 'fopen(): Content-type not specified assuming application/x-www-form-urlencoded' !== $msg) {
+ throw new TransportException($msg);
+ }
+
+ $this->logger && $this->logger->info(sprintf('%s for "%s".', $msg, $url ?? $this->url));
+ });
+
+ try {
+ $this->info['start_time'] = microtime(true);
+
+ [$resolver, $url] = ($this->resolver)($this->multi);
+
+ while (true) {
+ $context = stream_context_get_options($this->context);
+
+ if ($proxy = $context['http']['proxy'] ?? null) {
+ $this->info['debug'] .= "* Establish HTTP proxy tunnel to {$proxy}\n";
+ $this->info['request_header'] = $url;
+ } else {
+ $this->info['debug'] .= "* Trying {$this->info['primary_ip']}...\n";
+ $this->info['request_header'] = $this->info['url']['path'].$this->info['url']['query'];
+ }
+
+ $this->info['request_header'] = sprintf("> %s %s HTTP/%s \r\n", $context['http']['method'], $this->info['request_header'], $context['http']['protocol_version']);
+ $this->info['request_header'] .= implode("\r\n", $context['http']['header'])."\r\n\r\n";
+
+ if (\array_key_exists('peer_name', $context['ssl']) && null === $context['ssl']['peer_name']) {
+ unset($context['ssl']['peer_name']);
+ $this->context = stream_context_create([], ['options' => $context] + stream_context_get_params($this->context));
+ }
+
+ // Send request and follow redirects when needed
+ $this->handle = $h = fopen($url, 'r', false, $this->context);
+ self::addResponseHeaders(stream_get_meta_data($h)['wrapper_data'], $this->info, $this->headers, $this->info['debug']);
+ $url = $resolver($this->multi, $this->headers['location'][0] ?? null, $this->context);
+
+ if (null === $url) {
+ break;
+ }
+
+ $this->logger && $this->logger->info(sprintf('Redirecting: "%s %s"', $this->info['http_code'], $url ?? $this->url));
+ }
+ } catch (\Throwable $e) {
+ $this->close();
+ $this->multi->handlesActivity[$this->id][] = null;
+ $this->multi->handlesActivity[$this->id][] = $e;
+
+ return;
+ } finally {
+ $this->info['pretransfer_time'] = $this->info['total_time'] = microtime(true) - $this->info['start_time'];
+ restore_error_handler();
+ }
+
+ if (isset($context['ssl']['capture_peer_cert_chain']) && isset(($context = stream_context_get_options($this->context))['ssl']['peer_certificate_chain'])) {
+ $this->info['peer_certificate_chain'] = $context['ssl']['peer_certificate_chain'];
+ }
+
+ stream_set_blocking($h, false);
+ $this->context = $this->resolver = null;
+
+ // Create dechunk buffers
+ if (isset($this->headers['content-length'])) {
+ $this->remaining = (int) $this->headers['content-length'][0];
+ } elseif ('chunked' === ($this->headers['transfer-encoding'][0] ?? null)) {
+ stream_filter_append($this->buffer, 'dechunk', \STREAM_FILTER_WRITE);
+ $this->remaining = -1;
+ } else {
+ $this->remaining = -2;
+ }
+
+ $this->multi->handlesActivity[$this->id] = [new FirstChunk()];
+
+ if ('HEAD' === $context['http']['method'] || \in_array($this->info['http_code'], [204, 304], true)) {
+ $this->multi->handlesActivity[$this->id][] = null;
+ $this->multi->handlesActivity[$this->id][] = null;
+
+ return;
+ }
+
+ $host = parse_url($this->info['redirect_url'] ?? $this->url, \PHP_URL_HOST);
+ $this->multi->lastTimeout = null;
+ $this->multi->openHandles[$this->id] = [&$this->pauseExpiry, $h, $this->buffer, $this->onProgress, &$this->remaining, &$this->info, $host];
+ $this->multi->hosts[$host] = 1 + ($this->multi->hosts[$host] ?? 0);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ private function close(): void
+ {
+ $this->canary->cancel();
+ $this->handle = $this->buffer = $this->inflate = $this->onProgress = null;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ private static function schedule(self $response, array &$runningResponses): void
+ {
+ if (!isset($runningResponses[$i = $response->multi->id])) {
+ $runningResponses[$i] = [$response->multi, []];
+ }
+
+ $runningResponses[$i][1][$response->id] = $response;
+
+ if (null === $response->buffer) {
+ // Response already completed
+ $response->multi->handlesActivity[$response->id][] = null;
+ $response->multi->handlesActivity[$response->id][] = null !== $response->info['error'] ? new TransportException($response->info['error']) : null;
+ }
+ }
+
+ /**
+ * {@inheritdoc}
+ *
+ * @param NativeClientState $multi
+ */
+ private static function perform(ClientState $multi, array &$responses = null): void
+ {
+ foreach ($multi->openHandles as $i => [$pauseExpiry, $h, $buffer, $onProgress]) {
+ if ($pauseExpiry) {
+ if (microtime(true) < $pauseExpiry) {
+ continue;
+ }
+
+ $multi->openHandles[$i][0] = 0;
+ }
+
+ $hasActivity = false;
+ $remaining = &$multi->openHandles[$i][4];
+ $info = &$multi->openHandles[$i][5];
+ $e = null;
+
+ // Read incoming buffer and write it to the dechunk one
+ try {
+ if ($remaining && '' !== $data = (string) fread($h, 0 > $remaining ? 16372 : $remaining)) {
+ fwrite($buffer, $data);
+ $hasActivity = true;
+ $multi->sleep = false;
+
+ if (-1 !== $remaining) {
+ $remaining -= \strlen($data);
+ }
+ }
+ } catch (\Throwable $e) {
+ $hasActivity = $onProgress = false;
+ }
+
+ if (!$hasActivity) {
+ if ($onProgress) {
+ try {
+ // Notify the progress callback so that it can e.g. cancel
+ // the request if the stream is inactive for too long
+ $info['total_time'] = microtime(true) - $info['start_time'];
+ $onProgress();
+ } catch (\Throwable $e) {
+ // no-op
+ }
+ }
+ } elseif ('' !== $data = stream_get_contents($buffer, -1, 0)) {
+ rewind($buffer);
+ ftruncate($buffer, 0);
+
+ if (null === $e) {
+ $multi->handlesActivity[$i][] = $data;
+ }
+ }
+
+ if (null !== $e || !$remaining || feof($h)) {
+ // Stream completed
+ $info['total_time'] = microtime(true) - $info['start_time'];
+ $info['starttransfer_time'] = $info['starttransfer_time'] ?: $info['total_time'];
+
+ if ($onProgress) {
+ try {
+ $onProgress(-1);
+ } catch (\Throwable $e) {
+ // no-op
+ }
+ }
+
+ if (null === $e) {
+ if (0 < $remaining) {
+ $e = new TransportException(sprintf('Transfer closed with %s bytes remaining to read.', $remaining));
+ } elseif (-1 === $remaining && fwrite($buffer, '-') && '' !== stream_get_contents($buffer, -1, 0)) {
+ $e = new TransportException('Transfer closed with outstanding data remaining from chunked response.');
+ }
+ }
+
+ $multi->handlesActivity[$i][] = null;
+ $multi->handlesActivity[$i][] = $e;
+ if (null !== ($host = $multi->openHandles[$i][6] ?? null) && 0 >= --$multi->hosts[$host]) {
+ unset($multi->hosts[$host]);
+ }
+ unset($multi->openHandles[$i]);
+ $multi->sleep = false;
+ }
+ }
+
+ if (null === $responses) {
+ return;
+ }
+
+ $maxHosts = $multi->maxHostConnections;
+
+ foreach ($responses as $i => $response) {
+ if (null !== $response->remaining || null === $response->buffer) {
+ continue;
+ }
+
+ if ($response->pauseExpiry && microtime(true) < $response->pauseExpiry) {
+ // Create empty open handles to tell we still have pending requests
+ $multi->openHandles[$i] = [\INF, null, null, null];
+ } elseif ($maxHosts && $maxHosts > ($multi->hosts[parse_url($response->url, \PHP_URL_HOST)] ?? 0)) {
+ // Open the next pending request - this is a blocking operation so we do only one of them
+ $response->open();
+ $multi->sleep = false;
+ self::perform($multi);
+ $maxHosts = 0;
+ }
+ }
+ }
+
+ /**
+ * {@inheritdoc}
+ *
+ * @param NativeClientState $multi
+ */
+ private static function select(ClientState $multi, float $timeout): int
+ {
+ if (!$multi->sleep = !$multi->sleep) {
+ return -1;
+ }
+
+ $_ = $handles = [];
+ $now = null;
+
+ foreach ($multi->openHandles as [$pauseExpiry, $h]) {
+ if (null === $h) {
+ continue;
+ }
+
+ if ($pauseExpiry && ($now ?? $now = microtime(true)) < $pauseExpiry) {
+ $timeout = min($timeout, $pauseExpiry - $now);
+ continue;
+ }
+
+ $handles[] = $h;
+ }
+
+ if (!$handles) {
+ usleep((int) (1E6 * $timeout));
+
+ return 0;
+ }
+
+ return stream_select($handles, $_, $_, (int) $timeout, (int) (1E6 * ($timeout - (int) $timeout)));
+ }
+}
diff --git a/src/vendor/symfony/http-client/Response/ResponseStream.php b/src/vendor/symfony/http-client/Response/ResponseStream.php
new file mode 100644
index 0000000..f86d2d4
--- /dev/null
+++ b/src/vendor/symfony/http-client/Response/ResponseStream.php
@@ -0,0 +1,54 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\HttpClient\Response;
+
+use Symfony\Contracts\HttpClient\ChunkInterface;
+use Symfony\Contracts\HttpClient\ResponseInterface;
+use Symfony\Contracts\HttpClient\ResponseStreamInterface;
+
+/**
+ * @author Nicolas Grekas
+ */
+final class ResponseStream implements ResponseStreamInterface
+{
+ private $generator;
+
+ public function __construct(\Generator $generator)
+ {
+ $this->generator = $generator;
+ }
+
+ public function key(): ResponseInterface
+ {
+ return $this->generator->key();
+ }
+
+ public function current(): ChunkInterface
+ {
+ return $this->generator->current();
+ }
+
+ public function next(): void
+ {
+ $this->generator->next();
+ }
+
+ public function rewind(): void
+ {
+ $this->generator->rewind();
+ }
+
+ public function valid(): bool
+ {
+ return $this->generator->valid();
+ }
+}
diff --git a/src/vendor/symfony/http-client/Response/StreamWrapper.php b/src/vendor/symfony/http-client/Response/StreamWrapper.php
new file mode 100644
index 0000000..50a7c36
--- /dev/null
+++ b/src/vendor/symfony/http-client/Response/StreamWrapper.php
@@ -0,0 +1,313 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\HttpClient\Response;
+
+use Symfony\Contracts\HttpClient\Exception\ExceptionInterface;
+use Symfony\Contracts\HttpClient\HttpClientInterface;
+use Symfony\Contracts\HttpClient\ResponseInterface;
+
+/**
+ * Allows turning ResponseInterface instances to PHP streams.
+ *
+ * @author Nicolas Grekas
+ */
+class StreamWrapper
+{
+ /** @var resource|null */
+ public $context;
+
+ /** @var HttpClientInterface */
+ private $client;
+
+ /** @var ResponseInterface */
+ private $response;
+
+ /** @var resource|string|null */
+ private $content;
+
+ /** @var resource|null */
+ private $handle;
+
+ private $blocking = true;
+ private $timeout;
+ private $eof = false;
+ private $offset = 0;
+
+ /**
+ * Creates a PHP stream resource from a ResponseInterface.
+ *
+ * @return resource
+ */
+ public static function createResource(ResponseInterface $response, HttpClientInterface $client = null)
+ {
+ if ($response instanceof StreamableInterface) {
+ $stack = debug_backtrace(\DEBUG_BACKTRACE_PROVIDE_OBJECT | \DEBUG_BACKTRACE_IGNORE_ARGS, 2);
+
+ if ($response !== ($stack[1]['object'] ?? null)) {
+ return $response->toStream(false);
+ }
+ }
+
+ if (null === $client && !method_exists($response, 'stream')) {
+ throw new \InvalidArgumentException(sprintf('Providing a client to "%s()" is required when the response doesn\'t have any "stream()" method.', __CLASS__));
+ }
+
+ static $registered = false;
+
+ if (!$registered = $registered || stream_wrapper_register(strtr(__CLASS__, '\\', '-'), __CLASS__)) {
+ throw new \RuntimeException(error_get_last()['message'] ?? 'Registering the "symfony" stream wrapper failed.');
+ }
+
+ $context = [
+ 'client' => $client ?? $response,
+ 'response' => $response,
+ ];
+
+ return fopen(strtr(__CLASS__, '\\', '-').'://'.$response->getInfo('url'), 'r', false, stream_context_create(['symfony' => $context]));
+ }
+
+ public function getResponse(): ResponseInterface
+ {
+ return $this->response;
+ }
+
+ /**
+ * @param resource|callable|null $handle The resource handle that should be monitored when
+ * stream_select() is used on the created stream
+ * @param resource|null $content The seekable resource where the response body is buffered
+ */
+ public function bindHandles(&$handle, &$content): void
+ {
+ $this->handle = &$handle;
+ $this->content = &$content;
+ $this->offset = null;
+ }
+
+ public function stream_open(string $path, string $mode, int $options): bool
+ {
+ if ('r' !== $mode) {
+ if ($options & \STREAM_REPORT_ERRORS) {
+ trigger_error(sprintf('Invalid mode "%s": only "r" is supported.', $mode), \E_USER_WARNING);
+ }
+
+ return false;
+ }
+
+ $context = stream_context_get_options($this->context)['symfony'] ?? null;
+ $this->client = $context['client'] ?? null;
+ $this->response = $context['response'] ?? null;
+ $this->context = null;
+
+ if (null !== $this->client && null !== $this->response) {
+ return true;
+ }
+
+ if ($options & \STREAM_REPORT_ERRORS) {
+ trigger_error('Missing options "client" or "response" in "symfony" stream context.', \E_USER_WARNING);
+ }
+
+ return false;
+ }
+
+ public function stream_read(int $count)
+ {
+ if (\is_resource($this->content)) {
+ // Empty the internal activity list
+ foreach ($this->client->stream([$this->response], 0) as $chunk) {
+ try {
+ if (!$chunk->isTimeout() && $chunk->isFirst()) {
+ $this->response->getStatusCode(); // ignore 3/4/5xx
+ }
+ } catch (ExceptionInterface $e) {
+ trigger_error($e->getMessage(), \E_USER_WARNING);
+
+ return false;
+ }
+ }
+
+ if (0 !== fseek($this->content, $this->offset ?? 0)) {
+ return false;
+ }
+
+ if ('' !== $data = fread($this->content, $count)) {
+ fseek($this->content, 0, \SEEK_END);
+ $this->offset += \strlen($data);
+
+ return $data;
+ }
+ }
+
+ if (\is_string($this->content)) {
+ if (\strlen($this->content) <= $count) {
+ $data = $this->content;
+ $this->content = null;
+ } else {
+ $data = substr($this->content, 0, $count);
+ $this->content = substr($this->content, $count);
+ }
+ $this->offset += \strlen($data);
+
+ return $data;
+ }
+
+ foreach ($this->client->stream([$this->response], $this->blocking ? $this->timeout : 0) as $chunk) {
+ try {
+ $this->eof = true;
+ $this->eof = !$chunk->isTimeout();
+
+ if (!$this->eof && !$this->blocking) {
+ return '';
+ }
+
+ $this->eof = $chunk->isLast();
+
+ if ($chunk->isFirst()) {
+ $this->response->getStatusCode(); // ignore 3/4/5xx
+ }
+
+ if ('' !== $data = $chunk->getContent()) {
+ if (\strlen($data) > $count) {
+ if (null === $this->content) {
+ $this->content = substr($data, $count);
+ }
+ $data = substr($data, 0, $count);
+ }
+ $this->offset += \strlen($data);
+
+ return $data;
+ }
+ } catch (ExceptionInterface $e) {
+ trigger_error($e->getMessage(), \E_USER_WARNING);
+
+ return false;
+ }
+ }
+
+ return '';
+ }
+
+ public function stream_set_option(int $option, int $arg1, ?int $arg2): bool
+ {
+ if (\STREAM_OPTION_BLOCKING === $option) {
+ $this->blocking = (bool) $arg1;
+ } elseif (\STREAM_OPTION_READ_TIMEOUT === $option) {
+ $this->timeout = $arg1 + $arg2 / 1e6;
+ } else {
+ return false;
+ }
+
+ return true;
+ }
+
+ public function stream_tell(): int
+ {
+ return $this->offset ?? 0;
+ }
+
+ public function stream_eof(): bool
+ {
+ return $this->eof && !\is_string($this->content);
+ }
+
+ public function stream_seek(int $offset, int $whence = \SEEK_SET): bool
+ {
+ if (null === $this->content && null === $this->offset) {
+ $this->response->getStatusCode();
+ $this->offset = 0;
+ }
+
+ if (!\is_resource($this->content) || 0 !== fseek($this->content, 0, \SEEK_END)) {
+ return false;
+ }
+
+ $size = ftell($this->content);
+
+ if (\SEEK_CUR === $whence) {
+ $offset += $this->offset ?? 0;
+ }
+
+ if (\SEEK_END === $whence || $size < $offset) {
+ foreach ($this->client->stream([$this->response]) as $chunk) {
+ try {
+ if ($chunk->isFirst()) {
+ $this->response->getStatusCode(); // ignore 3/4/5xx
+ }
+
+ // Chunks are buffered in $this->content already
+ $size += \strlen($chunk->getContent());
+
+ if (\SEEK_END !== $whence && $offset <= $size) {
+ break;
+ }
+ } catch (ExceptionInterface $e) {
+ trigger_error($e->getMessage(), \E_USER_WARNING);
+
+ return false;
+ }
+ }
+
+ if (\SEEK_END === $whence) {
+ $offset += $size;
+ }
+ }
+
+ if (0 <= $offset && $offset <= $size) {
+ $this->eof = false;
+ $this->offset = $offset;
+
+ return true;
+ }
+
+ return false;
+ }
+
+ public function stream_cast(int $castAs)
+ {
+ if (\STREAM_CAST_FOR_SELECT === $castAs) {
+ $this->response->getHeaders(false);
+
+ return (\is_callable($this->handle) ? ($this->handle)() : $this->handle) ?? false;
+ }
+
+ return false;
+ }
+
+ public function stream_stat(): array
+ {
+ try {
+ $headers = $this->response->getHeaders(false);
+ } catch (ExceptionInterface $e) {
+ trigger_error($e->getMessage(), \E_USER_WARNING);
+ $headers = [];
+ }
+
+ return [
+ 'dev' => 0,
+ 'ino' => 0,
+ 'mode' => 33060,
+ 'nlink' => 0,
+ 'uid' => 0,
+ 'gid' => 0,
+ 'rdev' => 0,
+ 'size' => (int) ($headers['content-length'][0] ?? -1),
+ 'atime' => 0,
+ 'mtime' => strtotime($headers['last-modified'][0] ?? '') ?: 0,
+ 'ctime' => 0,
+ 'blksize' => 0,
+ 'blocks' => 0,
+ ];
+ }
+
+ private function __construct()
+ {
+ }
+}
diff --git a/src/vendor/symfony/http-client/Response/StreamableInterface.php b/src/vendor/symfony/http-client/Response/StreamableInterface.php
new file mode 100644
index 0000000..eb1f933
--- /dev/null
+++ b/src/vendor/symfony/http-client/Response/StreamableInterface.php
@@ -0,0 +1,35 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\HttpClient\Response;
+
+use Symfony\Contracts\HttpClient\Exception\ClientExceptionInterface;
+use Symfony\Contracts\HttpClient\Exception\RedirectionExceptionInterface;
+use Symfony\Contracts\HttpClient\Exception\ServerExceptionInterface;
+use Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface;
+
+/**
+ * @author Nicolas Grekas
+ */
+interface StreamableInterface
+{
+ /**
+ * Casts the response to a PHP stream resource.
+ *
+ * @return resource
+ *
+ * @throws TransportExceptionInterface When a network error occurs
+ * @throws RedirectionExceptionInterface On a 3xx when $throw is true and the "max_redirects" option has been reached
+ * @throws ClientExceptionInterface On a 4xx when $throw is true
+ * @throws ServerExceptionInterface On a 5xx when $throw is true
+ */
+ public function toStream(bool $throw = true);
+}
diff --git a/src/vendor/symfony/http-client/Response/TraceableResponse.php b/src/vendor/symfony/http-client/Response/TraceableResponse.php
new file mode 100644
index 0000000..d656c0a
--- /dev/null
+++ b/src/vendor/symfony/http-client/Response/TraceableResponse.php
@@ -0,0 +1,219 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\HttpClient\Response;
+
+use Symfony\Component\HttpClient\Chunk\ErrorChunk;
+use Symfony\Component\HttpClient\Exception\ClientException;
+use Symfony\Component\HttpClient\Exception\RedirectionException;
+use Symfony\Component\HttpClient\Exception\ServerException;
+use Symfony\Component\HttpClient\TraceableHttpClient;
+use Symfony\Component\Stopwatch\StopwatchEvent;
+use Symfony\Contracts\HttpClient\Exception\ClientExceptionInterface;
+use Symfony\Contracts\HttpClient\Exception\RedirectionExceptionInterface;
+use Symfony\Contracts\HttpClient\Exception\ServerExceptionInterface;
+use Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface;
+use Symfony\Contracts\HttpClient\HttpClientInterface;
+use Symfony\Contracts\HttpClient\ResponseInterface;
+
+/**
+ * @author Nicolas Grekas
+ *
+ * @internal
+ */
+class TraceableResponse implements ResponseInterface, StreamableInterface
+{
+ private $client;
+ private $response;
+ private $content;
+ private $event;
+
+ public function __construct(HttpClientInterface $client, ResponseInterface $response, &$content, StopwatchEvent $event = null)
+ {
+ $this->client = $client;
+ $this->response = $response;
+ $this->content = &$content;
+ $this->event = $event;
+ }
+
+ public function __sleep(): array
+ {
+ throw new \BadMethodCallException('Cannot serialize '.__CLASS__);
+ }
+
+ public function __wakeup()
+ {
+ throw new \BadMethodCallException('Cannot unserialize '.__CLASS__);
+ }
+
+ public function __destruct()
+ {
+ try {
+ $this->response->__destruct();
+ } finally {
+ if ($this->event && $this->event->isStarted()) {
+ $this->event->stop();
+ }
+ }
+ }
+
+ public function getStatusCode(): int
+ {
+ try {
+ return $this->response->getStatusCode();
+ } finally {
+ if ($this->event && $this->event->isStarted()) {
+ $this->event->lap();
+ }
+ }
+ }
+
+ public function getHeaders(bool $throw = true): array
+ {
+ try {
+ return $this->response->getHeaders($throw);
+ } finally {
+ if ($this->event && $this->event->isStarted()) {
+ $this->event->lap();
+ }
+ }
+ }
+
+ public function getContent(bool $throw = true): string
+ {
+ try {
+ if (false === $this->content) {
+ return $this->response->getContent($throw);
+ }
+
+ return $this->content = $this->response->getContent(false);
+ } finally {
+ if ($this->event && $this->event->isStarted()) {
+ $this->event->stop();
+ }
+ if ($throw) {
+ $this->checkStatusCode($this->response->getStatusCode());
+ }
+ }
+ }
+
+ public function toArray(bool $throw = true): array
+ {
+ try {
+ if (false === $this->content) {
+ return $this->response->toArray($throw);
+ }
+
+ return $this->content = $this->response->toArray(false);
+ } finally {
+ if ($this->event && $this->event->isStarted()) {
+ $this->event->stop();
+ }
+ if ($throw) {
+ $this->checkStatusCode($this->response->getStatusCode());
+ }
+ }
+ }
+
+ public function cancel(): void
+ {
+ $this->response->cancel();
+
+ if ($this->event && $this->event->isStarted()) {
+ $this->event->stop();
+ }
+ }
+
+ public function getInfo(string $type = null)
+ {
+ return $this->response->getInfo($type);
+ }
+
+ /**
+ * Casts the response to a PHP stream resource.
+ *
+ * @return resource
+ *
+ * @throws TransportExceptionInterface When a network error occurs
+ * @throws RedirectionExceptionInterface On a 3xx when $throw is true and the "max_redirects" option has been reached
+ * @throws ClientExceptionInterface On a 4xx when $throw is true
+ * @throws ServerExceptionInterface On a 5xx when $throw is true
+ */
+ public function toStream(bool $throw = true)
+ {
+ if ($throw) {
+ // Ensure headers arrived
+ $this->response->getHeaders(true);
+ }
+
+ if ($this->response instanceof StreamableInterface) {
+ return $this->response->toStream(false);
+ }
+
+ return StreamWrapper::createResource($this->response, $this->client);
+ }
+
+ /**
+ * @internal
+ */
+ public static function stream(HttpClientInterface $client, iterable $responses, ?float $timeout): \Generator
+ {
+ $wrappedResponses = [];
+ $traceableMap = new \SplObjectStorage();
+
+ foreach ($responses as $r) {
+ if (!$r instanceof self) {
+ throw new \TypeError(sprintf('"%s::stream()" expects parameter 1 to be an iterable of TraceableResponse objects, "%s" given.', TraceableHttpClient::class, get_debug_type($r)));
+ }
+
+ $traceableMap[$r->response] = $r;
+ $wrappedResponses[] = $r->response;
+ if ($r->event && !$r->event->isStarted()) {
+ $r->event->start();
+ }
+ }
+
+ foreach ($client->stream($wrappedResponses, $timeout) as $r => $chunk) {
+ if ($traceableMap[$r]->event && $traceableMap[$r]->event->isStarted()) {
+ try {
+ if ($chunk->isTimeout() || !$chunk->isLast()) {
+ $traceableMap[$r]->event->lap();
+ } else {
+ $traceableMap[$r]->event->stop();
+ }
+ } catch (TransportExceptionInterface $e) {
+ $traceableMap[$r]->event->stop();
+ if ($chunk instanceof ErrorChunk) {
+ $chunk->didThrow(false);
+ } else {
+ $chunk = new ErrorChunk($chunk->getOffset(), $e);
+ }
+ }
+ }
+ yield $traceableMap[$r] => $chunk;
+ }
+ }
+
+ private function checkStatusCode(int $code)
+ {
+ if (500 <= $code) {
+ throw new ServerException($this);
+ }
+
+ if (400 <= $code) {
+ throw new ClientException($this);
+ }
+
+ if (300 <= $code) {
+ throw new RedirectionException($this);
+ }
+ }
+}
diff --git a/src/vendor/symfony/http-client/Response/TransportResponseTrait.php b/src/vendor/symfony/http-client/Response/TransportResponseTrait.php
new file mode 100644
index 0000000..566d61e
--- /dev/null
+++ b/src/vendor/symfony/http-client/Response/TransportResponseTrait.php
@@ -0,0 +1,312 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\HttpClient\Response;
+
+use Symfony\Component\HttpClient\Chunk\DataChunk;
+use Symfony\Component\HttpClient\Chunk\ErrorChunk;
+use Symfony\Component\HttpClient\Chunk\FirstChunk;
+use Symfony\Component\HttpClient\Chunk\LastChunk;
+use Symfony\Component\HttpClient\Exception\TransportException;
+use Symfony\Component\HttpClient\Internal\ClientState;
+
+/**
+ * Implements common logic for transport-level response classes.
+ *
+ * @author Nicolas Grekas
+ *
+ * @internal
+ */
+trait TransportResponseTrait
+{
+ private $canary;
+ private $headers = [];
+ private $info = [
+ 'response_headers' => [],
+ 'http_code' => 0,
+ 'error' => null,
+ 'canceled' => false,
+ ];
+
+ /** @var object|resource */
+ private $handle;
+ private $id;
+ private $timeout = 0;
+ private $inflate;
+ private $finalInfo;
+ private $logger;
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getStatusCode(): int
+ {
+ if ($this->initializer) {
+ self::initialize($this);
+ }
+
+ return $this->info['http_code'];
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getHeaders(bool $throw = true): array
+ {
+ if ($this->initializer) {
+ self::initialize($this);
+ }
+
+ if ($throw) {
+ $this->checkStatusCode();
+ }
+
+ return $this->headers;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function cancel(): void
+ {
+ $this->info['canceled'] = true;
+ $this->info['error'] = 'Response has been canceled.';
+ $this->close();
+ }
+
+ /**
+ * Closes the response and all its network handles.
+ */
+ protected function close(): void
+ {
+ $this->canary->cancel();
+ $this->inflate = null;
+ }
+
+ /**
+ * Adds pending responses to the activity list.
+ */
+ abstract protected static function schedule(self $response, array &$runningResponses): void;
+
+ /**
+ * Performs all pending non-blocking operations.
+ */
+ abstract protected static function perform(ClientState $multi, array &$responses): void;
+
+ /**
+ * Waits for network activity.
+ */
+ abstract protected static function select(ClientState $multi, float $timeout): int;
+
+ private static function addResponseHeaders(array $responseHeaders, array &$info, array &$headers, string &$debug = ''): void
+ {
+ foreach ($responseHeaders as $h) {
+ if (11 <= \strlen($h) && '/' === $h[4] && preg_match('#^HTTP/\d+(?:\.\d+)? (\d\d\d)(?: |$)#', $h, $m)) {
+ if ($headers) {
+ $debug .= "< \r\n";
+ $headers = [];
+ }
+ $info['http_code'] = (int) $m[1];
+ } elseif (2 === \count($m = explode(':', $h, 2))) {
+ $headers[strtolower($m[0])][] = ltrim($m[1]);
+ }
+
+ $debug .= "< {$h}\r\n";
+ $info['response_headers'][] = $h;
+ }
+
+ $debug .= "< \r\n";
+ }
+
+ /**
+ * Ensures the request is always sent and that the response code was checked.
+ */
+ private function doDestruct()
+ {
+ $this->shouldBuffer = true;
+
+ if ($this->initializer && null === $this->info['error']) {
+ self::initialize($this);
+ $this->checkStatusCode();
+ }
+ }
+
+ /**
+ * Implements an event loop based on a buffer activity queue.
+ *
+ * @param iterable
+ */
+interface RetryStrategyInterface
+{
+ /**
+ * Returns whether the request should be retried.
+ *
+ * @param ?string $responseContent Null is passed when the body did not arrive yet
+ *
+ * @return bool|null Returns null to signal that the body is required to take a decision
+ */
+ public function shouldRetry(AsyncContext $context, ?string $responseContent, ?TransportExceptionInterface $exception): ?bool;
+
+ /**
+ * Returns the time to wait in milliseconds.
+ */
+ public function getDelay(AsyncContext $context, ?string $responseContent, ?TransportExceptionInterface $exception): int;
+}
diff --git a/src/vendor/symfony/http-client/RetryableHttpClient.php b/src/vendor/symfony/http-client/RetryableHttpClient.php
new file mode 100644
index 0000000..bec1378
--- /dev/null
+++ b/src/vendor/symfony/http-client/RetryableHttpClient.php
@@ -0,0 +1,171 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\HttpClient;
+
+use Psr\Log\LoggerInterface;
+use Psr\Log\NullLogger;
+use Symfony\Component\HttpClient\Response\AsyncContext;
+use Symfony\Component\HttpClient\Response\AsyncResponse;
+use Symfony\Component\HttpClient\Retry\GenericRetryStrategy;
+use Symfony\Component\HttpClient\Retry\RetryStrategyInterface;
+use Symfony\Contracts\HttpClient\ChunkInterface;
+use Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface;
+use Symfony\Contracts\HttpClient\HttpClientInterface;
+use Symfony\Contracts\HttpClient\ResponseInterface;
+use Symfony\Contracts\Service\ResetInterface;
+
+/**
+ * Automatically retries failing HTTP requests.
+ *
+ * @author Jérémy Derussé
+ */
+class Stream extends File
+{
+ /**
+ * {@inheritdoc}
+ *
+ * @return int|false
+ */
+ #[\ReturnTypeWillChange]
+ public function getSize()
+ {
+ return false;
+ }
+}
diff --git a/src/vendor/symfony/http-foundation/File/UploadedFile.php b/src/vendor/symfony/http-foundation/File/UploadedFile.php
new file mode 100644
index 0000000..1161556
--- /dev/null
+++ b/src/vendor/symfony/http-foundation/File/UploadedFile.php
@@ -0,0 +1,290 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\HttpFoundation\File;
+
+use Symfony\Component\HttpFoundation\File\Exception\CannotWriteFileException;
+use Symfony\Component\HttpFoundation\File\Exception\ExtensionFileException;
+use Symfony\Component\HttpFoundation\File\Exception\FileException;
+use Symfony\Component\HttpFoundation\File\Exception\FileNotFoundException;
+use Symfony\Component\HttpFoundation\File\Exception\FormSizeFileException;
+use Symfony\Component\HttpFoundation\File\Exception\IniSizeFileException;
+use Symfony\Component\HttpFoundation\File\Exception\NoFileException;
+use Symfony\Component\HttpFoundation\File\Exception\NoTmpDirFileException;
+use Symfony\Component\HttpFoundation\File\Exception\PartialFileException;
+use Symfony\Component\Mime\MimeTypes;
+
+/**
+ * A file uploaded through a form.
+ *
+ * @author Bernhard Schussek
+ *
+ * @internal
+ */
+final class SessionBagProxy implements SessionBagInterface
+{
+ private $bag;
+ private $data;
+ private $usageIndex;
+ private $usageReporter;
+
+ public function __construct(SessionBagInterface $bag, array &$data, ?int &$usageIndex, ?callable $usageReporter)
+ {
+ $this->bag = $bag;
+ $this->data = &$data;
+ $this->usageIndex = &$usageIndex;
+ $this->usageReporter = $usageReporter;
+ }
+
+ public function getBag(): SessionBagInterface
+ {
+ ++$this->usageIndex;
+ if ($this->usageReporter && 0 <= $this->usageIndex) {
+ ($this->usageReporter)();
+ }
+
+ return $this->bag;
+ }
+
+ public function isEmpty(): bool
+ {
+ if (!isset($this->data[$this->bag->getStorageKey()])) {
+ return true;
+ }
+ ++$this->usageIndex;
+ if ($this->usageReporter && 0 <= $this->usageIndex) {
+ ($this->usageReporter)();
+ }
+
+ return empty($this->data[$this->bag->getStorageKey()]);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getName(): string
+ {
+ return $this->bag->getName();
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function initialize(array &$array): void
+ {
+ ++$this->usageIndex;
+ if ($this->usageReporter && 0 <= $this->usageIndex) {
+ ($this->usageReporter)();
+ }
+
+ $this->data[$this->bag->getStorageKey()] = &$array;
+
+ $this->bag->initialize($array);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getStorageKey(): string
+ {
+ return $this->bag->getStorageKey();
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function clear()
+ {
+ return $this->bag->clear();
+ }
+}
diff --git a/src/vendor/symfony/http-foundation/Session/SessionFactory.php b/src/vendor/symfony/http-foundation/Session/SessionFactory.php
new file mode 100644
index 0000000..04c4b06
--- /dev/null
+++ b/src/vendor/symfony/http-foundation/Session/SessionFactory.php
@@ -0,0 +1,40 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\HttpFoundation\Session;
+
+use Symfony\Component\HttpFoundation\RequestStack;
+use Symfony\Component\HttpFoundation\Session\Storage\SessionStorageFactoryInterface;
+
+// Help opcache.preload discover always-needed symbols
+class_exists(Session::class);
+
+/**
+ * @author Jérémy Derussé
+ * @author Rémon van de Kamp
+ */
+abstract class AbstractSessionHandler implements \SessionHandlerInterface, \SessionUpdateTimestampHandlerInterface
+{
+ private $sessionName;
+ private $prefetchId;
+ private $prefetchData;
+ private $newSessionId;
+ private $igbinaryEmptyData;
+
+ /**
+ * @return bool
+ */
+ #[\ReturnTypeWillChange]
+ public function open($savePath, $sessionName)
+ {
+ $this->sessionName = $sessionName;
+ if (!headers_sent() && !\ini_get('session.cache_limiter') && '0' !== \ini_get('session.cache_limiter')) {
+ header(sprintf('Cache-Control: max-age=%d, private, must-revalidate', 60 * (int) \ini_get('session.cache_expire')));
+ }
+
+ return true;
+ }
+
+ /**
+ * @return string
+ */
+ abstract protected function doRead(string $sessionId);
+
+ /**
+ * @return bool
+ */
+ abstract protected function doWrite(string $sessionId, string $data);
+
+ /**
+ * @return bool
+ */
+ abstract protected function doDestroy(string $sessionId);
+
+ /**
+ * @return bool
+ */
+ #[\ReturnTypeWillChange]
+ public function validateId($sessionId)
+ {
+ $this->prefetchData = $this->read($sessionId);
+ $this->prefetchId = $sessionId;
+
+ if (\PHP_VERSION_ID < 70317 || (70400 <= \PHP_VERSION_ID && \PHP_VERSION_ID < 70405)) {
+ // work around https://bugs.php.net/79413
+ foreach (debug_backtrace(\DEBUG_BACKTRACE_IGNORE_ARGS) as $frame) {
+ if (!isset($frame['class']) && isset($frame['function']) && \in_array($frame['function'], ['session_regenerate_id', 'session_create_id'], true)) {
+ return '' === $this->prefetchData;
+ }
+ }
+ }
+
+ return '' !== $this->prefetchData;
+ }
+
+ /**
+ * @return string
+ */
+ #[\ReturnTypeWillChange]
+ public function read($sessionId)
+ {
+ if (null !== $this->prefetchId) {
+ $prefetchId = $this->prefetchId;
+ $prefetchData = $this->prefetchData;
+ $this->prefetchId = $this->prefetchData = null;
+
+ if ($prefetchId === $sessionId || '' === $prefetchData) {
+ $this->newSessionId = '' === $prefetchData ? $sessionId : null;
+
+ return $prefetchData;
+ }
+ }
+
+ $data = $this->doRead($sessionId);
+ $this->newSessionId = '' === $data ? $sessionId : null;
+
+ return $data;
+ }
+
+ /**
+ * @return bool
+ */
+ #[\ReturnTypeWillChange]
+ public function write($sessionId, $data)
+ {
+ if (null === $this->igbinaryEmptyData) {
+ // see https://github.com/igbinary/igbinary/issues/146
+ $this->igbinaryEmptyData = \function_exists('igbinary_serialize') ? igbinary_serialize([]) : '';
+ }
+ if ('' === $data || $this->igbinaryEmptyData === $data) {
+ return $this->destroy($sessionId);
+ }
+ $this->newSessionId = null;
+
+ return $this->doWrite($sessionId, $data);
+ }
+
+ /**
+ * @return bool
+ */
+ #[\ReturnTypeWillChange]
+ public function destroy($sessionId)
+ {
+ if (!headers_sent() && filter_var(\ini_get('session.use_cookies'), \FILTER_VALIDATE_BOOLEAN)) {
+ if (!$this->sessionName) {
+ throw new \LogicException(sprintf('Session name cannot be empty, did you forget to call "parent::open()" in "%s"?.', static::class));
+ }
+ $cookie = SessionUtils::popSessionCookie($this->sessionName, $sessionId);
+
+ /*
+ * We send an invalidation Set-Cookie header (zero lifetime)
+ * when either the session was started or a cookie with
+ * the session name was sent by the client (in which case
+ * we know it's invalid as a valid session cookie would've
+ * started the session).
+ */
+ if (null === $cookie || isset($_COOKIE[$this->sessionName])) {
+ if (\PHP_VERSION_ID < 70300) {
+ setcookie($this->sessionName, '', 0, \ini_get('session.cookie_path'), \ini_get('session.cookie_domain'), filter_var(\ini_get('session.cookie_secure'), \FILTER_VALIDATE_BOOLEAN), filter_var(\ini_get('session.cookie_httponly'), \FILTER_VALIDATE_BOOLEAN));
+ } else {
+ $params = session_get_cookie_params();
+ unset($params['lifetime']);
+ setcookie($this->sessionName, '', $params);
+ }
+ }
+ }
+
+ return $this->newSessionId === $sessionId || $this->doDestroy($sessionId);
+ }
+}
diff --git a/src/vendor/symfony/http-foundation/Session/Storage/Handler/IdentityMarshaller.php b/src/vendor/symfony/http-foundation/Session/Storage/Handler/IdentityMarshaller.php
new file mode 100644
index 0000000..bea3a32
--- /dev/null
+++ b/src/vendor/symfony/http-foundation/Session/Storage/Handler/IdentityMarshaller.php
@@ -0,0 +1,42 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\HttpFoundation\Session\Storage\Handler;
+
+use Symfony\Component\Cache\Marshaller\MarshallerInterface;
+
+/**
+ * @author Ahmed TAILOULOUTE
+ */
+class SessionHandlerFactory
+{
+ /**
+ * @param \Redis|\RedisArray|\RedisCluster|\Predis\ClientInterface|RedisProxy|RedisClusterProxy|\Memcached|\PDO|string $connection Connection or DSN
+ */
+ public static function createHandler($connection): AbstractSessionHandler
+ {
+ if (!\is_string($connection) && !\is_object($connection)) {
+ throw new \TypeError(sprintf('Argument 1 passed to "%s()" must be a string or a connection object, "%s" given.', __METHOD__, get_debug_type($connection)));
+ }
+
+ if ($options = \is_string($connection) ? parse_url($connection) : false) {
+ parse_str($options['query'] ?? '', $options);
+ }
+
+ switch (true) {
+ case $connection instanceof \Redis:
+ case $connection instanceof \RedisArray:
+ case $connection instanceof \RedisCluster:
+ case $connection instanceof \Predis\ClientInterface:
+ case $connection instanceof RedisProxy:
+ case $connection instanceof RedisClusterProxy:
+ return new RedisSessionHandler($connection);
+
+ case $connection instanceof \Memcached:
+ return new MemcachedSessionHandler($connection);
+
+ case $connection instanceof \PDO:
+ return new PdoSessionHandler($connection);
+
+ case !\is_string($connection):
+ throw new \InvalidArgumentException(sprintf('Unsupported Connection: "%s".', get_debug_type($connection)));
+ case str_starts_with($connection, 'file://'):
+ $savePath = substr($connection, 7);
+
+ return new StrictSessionHandler(new NativeFileSessionHandler('' === $savePath ? null : $savePath));
+
+ case str_starts_with($connection, 'redis:'):
+ case str_starts_with($connection, 'rediss:'):
+ case str_starts_with($connection, 'memcached:'):
+ if (!class_exists(AbstractAdapter::class)) {
+ throw new \InvalidArgumentException(sprintf('Unsupported DSN "%s". Try running "composer require symfony/cache".', $connection));
+ }
+ $handlerClass = str_starts_with($connection, 'memcached:') ? MemcachedSessionHandler::class : RedisSessionHandler::class;
+ $connection = AbstractAdapter::createConnection($connection, ['lazy' => true]);
+
+ return new $handlerClass($connection, array_intersect_key($options ?: [], ['prefix' => 1, 'ttl' => 1]));
+
+ case str_starts_with($connection, 'pdo_oci://'):
+ if (!class_exists(DriverManager::class)) {
+ throw new \InvalidArgumentException(sprintf('Unsupported DSN "%s". Try running "composer require doctrine/dbal".', $connection));
+ }
+ $connection[3] = '-';
+ $params = class_exists(DsnParser::class) ? (new DsnParser())->parse($connection) : ['url' => $connection];
+ $config = new Configuration();
+ if (class_exists(DefaultSchemaManagerFactory::class)) {
+ $config->setSchemaManagerFactory(new DefaultSchemaManagerFactory());
+ }
+
+ $connection = DriverManager::getConnection($params, $config);
+ $connection = method_exists($connection, 'getNativeConnection') ? $connection->getNativeConnection() : $connection->getWrappedConnection();
+ // no break;
+
+ case str_starts_with($connection, 'mssql://'):
+ case str_starts_with($connection, 'mysql://'):
+ case str_starts_with($connection, 'mysql2://'):
+ case str_starts_with($connection, 'pgsql://'):
+ case str_starts_with($connection, 'postgres://'):
+ case str_starts_with($connection, 'postgresql://'):
+ case str_starts_with($connection, 'sqlsrv://'):
+ case str_starts_with($connection, 'sqlite://'):
+ case str_starts_with($connection, 'sqlite3://'):
+ return new PdoSessionHandler($connection, $options ?: []);
+ }
+
+ throw new \InvalidArgumentException(sprintf('Unsupported Connection: "%s".', $connection));
+ }
+}
diff --git a/src/vendor/symfony/http-foundation/Session/Storage/Handler/StrictSessionHandler.php b/src/vendor/symfony/http-foundation/Session/Storage/Handler/StrictSessionHandler.php
new file mode 100644
index 0000000..f7c385f
--- /dev/null
+++ b/src/vendor/symfony/http-foundation/Session/Storage/Handler/StrictSessionHandler.php
@@ -0,0 +1,118 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\HttpFoundation\Session\Storage\Handler;
+
+/**
+ * Adds basic `SessionUpdateTimestampHandlerInterface` behaviors to another `SessionHandlerInterface`.
+ *
+ * @author Nicolas Grekas
+ */
+class StrictSessionHandler extends AbstractSessionHandler
+{
+ private $handler;
+ private $doDestroy;
+
+ public function __construct(\SessionHandlerInterface $handler)
+ {
+ if ($handler instanceof \SessionUpdateTimestampHandlerInterface) {
+ throw new \LogicException(sprintf('"%s" is already an instance of "SessionUpdateTimestampHandlerInterface", you cannot wrap it with "%s".', get_debug_type($handler), self::class));
+ }
+
+ $this->handler = $handler;
+ }
+
+ /**
+ * Returns true if this handler wraps an internal PHP session save handler using \SessionHandler.
+ *
+ * @internal
+ */
+ public function isWrapper(): bool
+ {
+ return $this->handler instanceof \SessionHandler;
+ }
+
+ /**
+ * @return bool
+ */
+ #[\ReturnTypeWillChange]
+ public function open($savePath, $sessionName)
+ {
+ parent::open($savePath, $sessionName);
+
+ return $this->handler->open($savePath, $sessionName);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function doRead(string $sessionId)
+ {
+ return $this->handler->read($sessionId);
+ }
+
+ /**
+ * @return bool
+ */
+ #[\ReturnTypeWillChange]
+ public function updateTimestamp($sessionId, $data)
+ {
+ return $this->write($sessionId, $data);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function doWrite(string $sessionId, string $data)
+ {
+ return $this->handler->write($sessionId, $data);
+ }
+
+ /**
+ * @return bool
+ */
+ #[\ReturnTypeWillChange]
+ public function destroy($sessionId)
+ {
+ $this->doDestroy = true;
+ $destroyed = parent::destroy($sessionId);
+
+ return $this->doDestroy ? $this->doDestroy($sessionId) : $destroyed;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function doDestroy(string $sessionId)
+ {
+ $this->doDestroy = false;
+
+ return $this->handler->destroy($sessionId);
+ }
+
+ /**
+ * @return bool
+ */
+ #[\ReturnTypeWillChange]
+ public function close()
+ {
+ return $this->handler->close();
+ }
+
+ /**
+ * @return int|false
+ */
+ #[\ReturnTypeWillChange]
+ public function gc($maxlifetime)
+ {
+ return $this->handler->gc($maxlifetime);
+ }
+}
diff --git a/src/vendor/symfony/http-foundation/Session/Storage/MetadataBag.php b/src/vendor/symfony/http-foundation/Session/Storage/MetadataBag.php
new file mode 100644
index 0000000..52d3320
--- /dev/null
+++ b/src/vendor/symfony/http-foundation/Session/Storage/MetadataBag.php
@@ -0,0 +1,167 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\HttpFoundation\Session\Storage;
+
+use Symfony\Component\HttpFoundation\Session\SessionBagInterface;
+
+/**
+ * Metadata container.
+ *
+ * Adds metadata to the session.
+ *
+ * @author Drak
+ *
+ * @internal
+ */
+final class Mbstring
+{
+ public const MB_CASE_FOLD = \PHP_INT_MAX;
+
+ private const SIMPLE_CASE_FOLD = [
+ ['µ', 'ſ', "\xCD\x85", 'ς', "\xCF\x90", "\xCF\x91", "\xCF\x95", "\xCF\x96", "\xCF\xB0", "\xCF\xB1", "\xCF\xB5", "\xE1\xBA\x9B", "\xE1\xBE\xBE"],
+ ['μ', 's', 'ι', 'σ', 'β', 'θ', 'φ', 'π', 'κ', 'ρ', 'ε', "\xE1\xB9\xA1", 'ι'],
+ ];
+
+ private static $encodingList = ['ASCII', 'UTF-8'];
+ private static $language = 'neutral';
+ private static $internalEncoding = 'UTF-8';
+
+ public static function mb_convert_encoding($s, $toEncoding, $fromEncoding = null)
+ {
+ if (\is_array($fromEncoding) || (null !== $fromEncoding && false !== strpos($fromEncoding, ','))) {
+ $fromEncoding = self::mb_detect_encoding($s, $fromEncoding);
+ } else {
+ $fromEncoding = self::getEncoding($fromEncoding);
+ }
+
+ $toEncoding = self::getEncoding($toEncoding);
+
+ if ('BASE64' === $fromEncoding) {
+ $s = base64_decode($s);
+ $fromEncoding = $toEncoding;
+ }
+
+ if ('BASE64' === $toEncoding) {
+ return base64_encode($s);
+ }
+
+ if ('HTML-ENTITIES' === $toEncoding || 'HTML' === $toEncoding) {
+ if ('HTML-ENTITIES' === $fromEncoding || 'HTML' === $fromEncoding) {
+ $fromEncoding = 'Windows-1252';
+ }
+ if ('UTF-8' !== $fromEncoding) {
+ $s = iconv($fromEncoding, 'UTF-8//IGNORE', $s);
+ }
+
+ return preg_replace_callback('/[\x80-\xFF]+/', [__CLASS__, 'html_encoding_callback'], $s);
+ }
+
+ if ('HTML-ENTITIES' === $fromEncoding) {
+ $s = html_entity_decode($s, \ENT_COMPAT, 'UTF-8');
+ $fromEncoding = 'UTF-8';
+ }
+
+ return iconv($fromEncoding, $toEncoding.'//IGNORE', $s);
+ }
+
+ public static function mb_convert_variables($toEncoding, $fromEncoding, &...$vars)
+ {
+ $ok = true;
+ array_walk_recursive($vars, function (&$v) use (&$ok, $toEncoding, $fromEncoding) {
+ if (false === $v = self::mb_convert_encoding($v, $toEncoding, $fromEncoding)) {
+ $ok = false;
+ }
+ });
+
+ return $ok ? $fromEncoding : false;
+ }
+
+ public static function mb_decode_mimeheader($s)
+ {
+ return iconv_mime_decode($s, 2, self::$internalEncoding);
+ }
+
+ public static function mb_encode_mimeheader($s, $charset = null, $transferEncoding = null, $linefeed = null, $indent = null)
+ {
+ trigger_error('mb_encode_mimeheader() is bugged. Please use iconv_mime_encode() instead', \E_USER_WARNING);
+ }
+
+ public static function mb_decode_numericentity($s, $convmap, $encoding = null)
+ {
+ if (null !== $s && !\is_scalar($s) && !(\is_object($s) && method_exists($s, '__toString'))) {
+ trigger_error('mb_decode_numericentity() expects parameter 1 to be string, '.\gettype($s).' given', \E_USER_WARNING);
+
+ return null;
+ }
+
+ if (!\is_array($convmap) || (80000 > \PHP_VERSION_ID && !$convmap)) {
+ return false;
+ }
+
+ if (null !== $encoding && !\is_scalar($encoding)) {
+ trigger_error('mb_decode_numericentity() expects parameter 3 to be string, '.\gettype($s).' given', \E_USER_WARNING);
+
+ return ''; // Instead of null (cf. mb_encode_numericentity).
+ }
+
+ $s = (string) $s;
+ if ('' === $s) {
+ return '';
+ }
+
+ $encoding = self::getEncoding($encoding);
+
+ if ('UTF-8' === $encoding) {
+ $encoding = null;
+ if (!preg_match('//u', $s)) {
+ $s = @iconv('UTF-8', 'UTF-8//IGNORE', $s);
+ }
+ } else {
+ $s = iconv($encoding, 'UTF-8//IGNORE', $s);
+ }
+
+ $cnt = floor(\count($convmap) / 4) * 4;
+
+ for ($i = 0; $i < $cnt; $i += 4) {
+ // collector_decode_htmlnumericentity ignores $convmap[$i + 3]
+ $convmap[$i] += $convmap[$i + 2];
+ $convmap[$i + 1] += $convmap[$i + 2];
+ }
+
+ $s = preg_replace_callback('/(?:0*([0-9]+)|x0*([0-9a-fA-F]+))(?!&);?/', function (array $m) use ($cnt, $convmap) {
+ $c = isset($m[2]) ? (int) hexdec($m[2]) : $m[1];
+ for ($i = 0; $i < $cnt; $i += 4) {
+ if ($c >= $convmap[$i] && $c <= $convmap[$i + 1]) {
+ return self::mb_chr($c - $convmap[$i + 2]);
+ }
+ }
+
+ return $m[0];
+ }, $s);
+
+ if (null === $encoding) {
+ return $s;
+ }
+
+ return iconv('UTF-8', $encoding.'//IGNORE', $s);
+ }
+
+ public static function mb_encode_numericentity($s, $convmap, $encoding = null, $is_hex = false)
+ {
+ if (null !== $s && !\is_scalar($s) && !(\is_object($s) && method_exists($s, '__toString'))) {
+ trigger_error('mb_encode_numericentity() expects parameter 1 to be string, '.\gettype($s).' given', \E_USER_WARNING);
+
+ return null;
+ }
+
+ if (!\is_array($convmap) || (80000 > \PHP_VERSION_ID && !$convmap)) {
+ return false;
+ }
+
+ if (null !== $encoding && !\is_scalar($encoding)) {
+ trigger_error('mb_encode_numericentity() expects parameter 3 to be string, '.\gettype($s).' given', \E_USER_WARNING);
+
+ return null; // Instead of '' (cf. mb_decode_numericentity).
+ }
+
+ if (null !== $is_hex && !\is_scalar($is_hex)) {
+ trigger_error('mb_encode_numericentity() expects parameter 4 to be boolean, '.\gettype($s).' given', \E_USER_WARNING);
+
+ return null;
+ }
+
+ $s = (string) $s;
+ if ('' === $s) {
+ return '';
+ }
+
+ $encoding = self::getEncoding($encoding);
+
+ if ('UTF-8' === $encoding) {
+ $encoding = null;
+ if (!preg_match('//u', $s)) {
+ $s = @iconv('UTF-8', 'UTF-8//IGNORE', $s);
+ }
+ } else {
+ $s = iconv($encoding, 'UTF-8//IGNORE', $s);
+ }
+
+ static $ulenMask = ["\xC0" => 2, "\xD0" => 2, "\xE0" => 3, "\xF0" => 4];
+
+ $cnt = floor(\count($convmap) / 4) * 4;
+ $i = 0;
+ $len = \strlen($s);
+ $result = '';
+
+ while ($i < $len) {
+ $ulen = $s[$i] < "\x80" ? 1 : $ulenMask[$s[$i] & "\xF0"];
+ $uchr = substr($s, $i, $ulen);
+ $i += $ulen;
+ $c = self::mb_ord($uchr);
+
+ for ($j = 0; $j < $cnt; $j += 4) {
+ if ($c >= $convmap[$j] && $c <= $convmap[$j + 1]) {
+ $cOffset = ($c + $convmap[$j + 2]) & $convmap[$j + 3];
+ $result .= $is_hex ? sprintf('%X;', $cOffset) : ''.$cOffset.';';
+ continue 2;
+ }
+ }
+ $result .= $uchr;
+ }
+
+ if (null === $encoding) {
+ return $result;
+ }
+
+ return iconv('UTF-8', $encoding.'//IGNORE', $result);
+ }
+
+ public static function mb_convert_case($s, $mode, $encoding = null)
+ {
+ $s = (string) $s;
+ if ('' === $s) {
+ return '';
+ }
+
+ $encoding = self::getEncoding($encoding);
+
+ if ('UTF-8' === $encoding) {
+ $encoding = null;
+ if (!preg_match('//u', $s)) {
+ $s = @iconv('UTF-8', 'UTF-8//IGNORE', $s);
+ }
+ } else {
+ $s = iconv($encoding, 'UTF-8//IGNORE', $s);
+ }
+
+ if (\MB_CASE_TITLE == $mode) {
+ static $titleRegexp = null;
+ if (null === $titleRegexp) {
+ $titleRegexp = self::getData('titleCaseRegexp');
+ }
+ $s = preg_replace_callback($titleRegexp, [__CLASS__, 'title_case'], $s);
+ } else {
+ if (\MB_CASE_UPPER == $mode) {
+ static $upper = null;
+ if (null === $upper) {
+ $upper = self::getData('upperCase');
+ }
+ $map = $upper;
+ } else {
+ if (self::MB_CASE_FOLD === $mode) {
+ static $caseFolding = null;
+ if (null === $caseFolding) {
+ $caseFolding = self::getData('caseFolding');
+ }
+ $s = strtr($s, $caseFolding);
+ }
+
+ static $lower = null;
+ if (null === $lower) {
+ $lower = self::getData('lowerCase');
+ }
+ $map = $lower;
+ }
+
+ static $ulenMask = ["\xC0" => 2, "\xD0" => 2, "\xE0" => 3, "\xF0" => 4];
+
+ $i = 0;
+ $len = \strlen($s);
+
+ while ($i < $len) {
+ $ulen = $s[$i] < "\x80" ? 1 : $ulenMask[$s[$i] & "\xF0"];
+ $uchr = substr($s, $i, $ulen);
+ $i += $ulen;
+
+ if (isset($map[$uchr])) {
+ $uchr = $map[$uchr];
+ $nlen = \strlen($uchr);
+
+ if ($nlen == $ulen) {
+ $nlen = $i;
+ do {
+ $s[--$nlen] = $uchr[--$ulen];
+ } while ($ulen);
+ } else {
+ $s = substr_replace($s, $uchr, $i - $ulen, $ulen);
+ $len += $nlen - $ulen;
+ $i += $nlen - $ulen;
+ }
+ }
+ }
+ }
+
+ if (null === $encoding) {
+ return $s;
+ }
+
+ return iconv('UTF-8', $encoding.'//IGNORE', $s);
+ }
+
+ public static function mb_internal_encoding($encoding = null)
+ {
+ if (null === $encoding) {
+ return self::$internalEncoding;
+ }
+
+ $normalizedEncoding = self::getEncoding($encoding);
+
+ if ('UTF-8' === $normalizedEncoding || false !== @iconv($normalizedEncoding, $normalizedEncoding, ' ')) {
+ self::$internalEncoding = $normalizedEncoding;
+
+ return true;
+ }
+
+ if (80000 > \PHP_VERSION_ID) {
+ return false;
+ }
+
+ throw new \ValueError(sprintf('Argument #1 ($encoding) must be a valid encoding, "%s" given', $encoding));
+ }
+
+ public static function mb_language($lang = null)
+ {
+ if (null === $lang) {
+ return self::$language;
+ }
+
+ switch ($normalizedLang = strtolower($lang)) {
+ case 'uni':
+ case 'neutral':
+ self::$language = $normalizedLang;
+
+ return true;
+ }
+
+ if (80000 > \PHP_VERSION_ID) {
+ return false;
+ }
+
+ throw new \ValueError(sprintf('Argument #1 ($language) must be a valid language, "%s" given', $lang));
+ }
+
+ public static function mb_list_encodings()
+ {
+ return ['UTF-8'];
+ }
+
+ public static function mb_encoding_aliases($encoding)
+ {
+ switch (strtoupper($encoding)) {
+ case 'UTF8':
+ case 'UTF-8':
+ return ['utf8'];
+ }
+
+ return false;
+ }
+
+ public static function mb_check_encoding($var = null, $encoding = null)
+ {
+ if (PHP_VERSION_ID < 70200 && \is_array($var)) {
+ trigger_error('mb_check_encoding() expects parameter 1 to be string, array given', \E_USER_WARNING);
+
+ return null;
+ }
+
+ if (null === $encoding) {
+ if (null === $var) {
+ return false;
+ }
+ $encoding = self::$internalEncoding;
+ }
+
+ if (!\is_array($var)) {
+ return self::mb_detect_encoding($var, [$encoding]) || false !== @iconv($encoding, $encoding, $var);
+ }
+
+ foreach ($var as $key => $value) {
+ if (!self::mb_check_encoding($key, $encoding)) {
+ return false;
+ }
+ if (!self::mb_check_encoding($value, $encoding)) {
+ return false;
+ }
+ }
+
+ return true;
+
+ }
+
+ public static function mb_detect_encoding($str, $encodingList = null, $strict = false)
+ {
+ if (null === $encodingList) {
+ $encodingList = self::$encodingList;
+ } else {
+ if (!\is_array($encodingList)) {
+ $encodingList = array_map('trim', explode(',', $encodingList));
+ }
+ $encodingList = array_map('strtoupper', $encodingList);
+ }
+
+ foreach ($encodingList as $enc) {
+ switch ($enc) {
+ case 'ASCII':
+ if (!preg_match('/[\x80-\xFF]/', $str)) {
+ return $enc;
+ }
+ break;
+
+ case 'UTF8':
+ case 'UTF-8':
+ if (preg_match('//u', $str)) {
+ return 'UTF-8';
+ }
+ break;
+
+ default:
+ if (0 === strncmp($enc, 'ISO-8859-', 9)) {
+ return $enc;
+ }
+ }
+ }
+
+ return false;
+ }
+
+ public static function mb_detect_order($encodingList = null)
+ {
+ if (null === $encodingList) {
+ return self::$encodingList;
+ }
+
+ if (!\is_array($encodingList)) {
+ $encodingList = array_map('trim', explode(',', $encodingList));
+ }
+ $encodingList = array_map('strtoupper', $encodingList);
+
+ foreach ($encodingList as $enc) {
+ switch ($enc) {
+ default:
+ if (strncmp($enc, 'ISO-8859-', 9)) {
+ return false;
+ }
+ // no break
+ case 'ASCII':
+ case 'UTF8':
+ case 'UTF-8':
+ }
+ }
+
+ self::$encodingList = $encodingList;
+
+ return true;
+ }
+
+ public static function mb_strlen($s, $encoding = null)
+ {
+ $encoding = self::getEncoding($encoding);
+ if ('CP850' === $encoding || 'ASCII' === $encoding) {
+ return \strlen($s);
+ }
+
+ return @iconv_strlen($s, $encoding);
+ }
+
+ public static function mb_strpos($haystack, $needle, $offset = 0, $encoding = null)
+ {
+ $encoding = self::getEncoding($encoding);
+ if ('CP850' === $encoding || 'ASCII' === $encoding) {
+ return strpos($haystack, $needle, $offset);
+ }
+
+ $needle = (string) $needle;
+ if ('' === $needle) {
+ if (80000 > \PHP_VERSION_ID) {
+ trigger_error(__METHOD__.': Empty delimiter', \E_USER_WARNING);
+
+ return false;
+ }
+
+ return 0;
+ }
+
+ return iconv_strpos($haystack, $needle, $offset, $encoding);
+ }
+
+ public static function mb_strrpos($haystack, $needle, $offset = 0, $encoding = null)
+ {
+ $encoding = self::getEncoding($encoding);
+ if ('CP850' === $encoding || 'ASCII' === $encoding) {
+ return strrpos($haystack, $needle, $offset);
+ }
+
+ if ($offset != (int) $offset) {
+ $offset = 0;
+ } elseif ($offset = (int) $offset) {
+ if ($offset < 0) {
+ if (0 > $offset += self::mb_strlen($needle)) {
+ $haystack = self::mb_substr($haystack, 0, $offset, $encoding);
+ }
+ $offset = 0;
+ } else {
+ $haystack = self::mb_substr($haystack, $offset, 2147483647, $encoding);
+ }
+ }
+
+ $pos = '' !== $needle || 80000 > \PHP_VERSION_ID
+ ? iconv_strrpos($haystack, $needle, $encoding)
+ : self::mb_strlen($haystack, $encoding);
+
+ return false !== $pos ? $offset + $pos : false;
+ }
+
+ public static function mb_str_split($string, $split_length = 1, $encoding = null)
+ {
+ if (null !== $string && !\is_scalar($string) && !(\is_object($string) && method_exists($string, '__toString'))) {
+ trigger_error('mb_str_split() expects parameter 1 to be string, '.\gettype($string).' given', \E_USER_WARNING);
+
+ return null;
+ }
+
+ if (1 > $split_length = (int) $split_length) {
+ if (80000 > \PHP_VERSION_ID) {
+ trigger_error('The length of each segment must be greater than zero', \E_USER_WARNING);
+
+ return false;
+ }
+
+ throw new \ValueError('Argument #2 ($length) must be greater than 0');
+ }
+
+ if (null === $encoding) {
+ $encoding = mb_internal_encoding();
+ }
+
+ if ('UTF-8' === $encoding = self::getEncoding($encoding)) {
+ $rx = '/(';
+ while (65535 < $split_length) {
+ $rx .= '.{65535}';
+ $split_length -= 65535;
+ }
+ $rx .= '.{'.$split_length.'})/us';
+
+ return preg_split($rx, $string, -1, \PREG_SPLIT_DELIM_CAPTURE | \PREG_SPLIT_NO_EMPTY);
+ }
+
+ $result = [];
+ $length = mb_strlen($string, $encoding);
+
+ for ($i = 0; $i < $length; $i += $split_length) {
+ $result[] = mb_substr($string, $i, $split_length, $encoding);
+ }
+
+ return $result;
+ }
+
+ public static function mb_strtolower($s, $encoding = null)
+ {
+ return self::mb_convert_case($s, \MB_CASE_LOWER, $encoding);
+ }
+
+ public static function mb_strtoupper($s, $encoding = null)
+ {
+ return self::mb_convert_case($s, \MB_CASE_UPPER, $encoding);
+ }
+
+ public static function mb_substitute_character($c = null)
+ {
+ if (null === $c) {
+ return 'none';
+ }
+ if (0 === strcasecmp($c, 'none')) {
+ return true;
+ }
+ if (80000 > \PHP_VERSION_ID) {
+ return false;
+ }
+ if (\is_int($c) || 'long' === $c || 'entity' === $c) {
+ return false;
+ }
+
+ throw new \ValueError('Argument #1 ($substitute_character) must be "none", "long", "entity" or a valid codepoint');
+ }
+
+ public static function mb_substr($s, $start, $length = null, $encoding = null)
+ {
+ $encoding = self::getEncoding($encoding);
+ if ('CP850' === $encoding || 'ASCII' === $encoding) {
+ return (string) substr($s, $start, null === $length ? 2147483647 : $length);
+ }
+
+ if ($start < 0) {
+ $start = iconv_strlen($s, $encoding) + $start;
+ if ($start < 0) {
+ $start = 0;
+ }
+ }
+
+ if (null === $length) {
+ $length = 2147483647;
+ } elseif ($length < 0) {
+ $length = iconv_strlen($s, $encoding) + $length - $start;
+ if ($length < 0) {
+ return '';
+ }
+ }
+
+ return (string) iconv_substr($s, $start, $length, $encoding);
+ }
+
+ public static function mb_stripos($haystack, $needle, $offset = 0, $encoding = null)
+ {
+ [$haystack, $needle] = str_replace(self::SIMPLE_CASE_FOLD[0], self::SIMPLE_CASE_FOLD[1], [
+ self::mb_convert_case($haystack, \MB_CASE_LOWER, $encoding),
+ self::mb_convert_case($needle, \MB_CASE_LOWER, $encoding),
+ ]);
+
+ return self::mb_strpos($haystack, $needle, $offset, $encoding);
+ }
+
+ public static function mb_stristr($haystack, $needle, $part = false, $encoding = null)
+ {
+ $pos = self::mb_stripos($haystack, $needle, 0, $encoding);
+
+ return self::getSubpart($pos, $part, $haystack, $encoding);
+ }
+
+ public static function mb_strrchr($haystack, $needle, $part = false, $encoding = null)
+ {
+ $encoding = self::getEncoding($encoding);
+ if ('CP850' === $encoding || 'ASCII' === $encoding) {
+ $pos = strrpos($haystack, $needle);
+ } else {
+ $needle = self::mb_substr($needle, 0, 1, $encoding);
+ $pos = iconv_strrpos($haystack, $needle, $encoding);
+ }
+
+ return self::getSubpart($pos, $part, $haystack, $encoding);
+ }
+
+ public static function mb_strrichr($haystack, $needle, $part = false, $encoding = null)
+ {
+ $needle = self::mb_substr($needle, 0, 1, $encoding);
+ $pos = self::mb_strripos($haystack, $needle, $encoding);
+
+ return self::getSubpart($pos, $part, $haystack, $encoding);
+ }
+
+ public static function mb_strripos($haystack, $needle, $offset = 0, $encoding = null)
+ {
+ $haystack = self::mb_convert_case($haystack, \MB_CASE_LOWER, $encoding);
+ $needle = self::mb_convert_case($needle, \MB_CASE_LOWER, $encoding);
+
+ $haystack = str_replace(self::SIMPLE_CASE_FOLD[0], self::SIMPLE_CASE_FOLD[1], $haystack);
+ $needle = str_replace(self::SIMPLE_CASE_FOLD[0], self::SIMPLE_CASE_FOLD[1], $needle);
+
+ return self::mb_strrpos($haystack, $needle, $offset, $encoding);
+ }
+
+ public static function mb_strstr($haystack, $needle, $part = false, $encoding = null)
+ {
+ $pos = strpos($haystack, $needle);
+ if (false === $pos) {
+ return false;
+ }
+ if ($part) {
+ return substr($haystack, 0, $pos);
+ }
+
+ return substr($haystack, $pos);
+ }
+
+ public static function mb_get_info($type = 'all')
+ {
+ $info = [
+ 'internal_encoding' => self::$internalEncoding,
+ 'http_output' => 'pass',
+ 'http_output_conv_mimetypes' => '^(text/|application/xhtml\+xml)',
+ 'func_overload' => 0,
+ 'func_overload_list' => 'no overload',
+ 'mail_charset' => 'UTF-8',
+ 'mail_header_encoding' => 'BASE64',
+ 'mail_body_encoding' => 'BASE64',
+ 'illegal_chars' => 0,
+ 'encoding_translation' => 'Off',
+ 'language' => self::$language,
+ 'detect_order' => self::$encodingList,
+ 'substitute_character' => 'none',
+ 'strict_detection' => 'Off',
+ ];
+
+ if ('all' === $type) {
+ return $info;
+ }
+ if (isset($info[$type])) {
+ return $info[$type];
+ }
+
+ return false;
+ }
+
+ public static function mb_http_input($type = '')
+ {
+ return false;
+ }
+
+ public static function mb_http_output($encoding = null)
+ {
+ return null !== $encoding ? 'pass' === $encoding : 'pass';
+ }
+
+ public static function mb_strwidth($s, $encoding = null)
+ {
+ $encoding = self::getEncoding($encoding);
+
+ if ('UTF-8' !== $encoding) {
+ $s = iconv($encoding, 'UTF-8//IGNORE', $s);
+ }
+
+ $s = preg_replace('/[\x{1100}-\x{115F}\x{2329}\x{232A}\x{2E80}-\x{303E}\x{3040}-\x{A4CF}\x{AC00}-\x{D7A3}\x{F900}-\x{FAFF}\x{FE10}-\x{FE19}\x{FE30}-\x{FE6F}\x{FF00}-\x{FF60}\x{FFE0}-\x{FFE6}\x{20000}-\x{2FFFD}\x{30000}-\x{3FFFD}]/u', '', $s, -1, $wide);
+
+ return ($wide << 1) + iconv_strlen($s, 'UTF-8');
+ }
+
+ public static function mb_substr_count($haystack, $needle, $encoding = null)
+ {
+ return substr_count($haystack, $needle);
+ }
+
+ public static function mb_output_handler($contents, $status)
+ {
+ return $contents;
+ }
+
+ public static function mb_chr($code, $encoding = null)
+ {
+ if (0x80 > $code %= 0x200000) {
+ $s = \chr($code);
+ } elseif (0x800 > $code) {
+ $s = \chr(0xC0 | $code >> 6).\chr(0x80 | $code & 0x3F);
+ } elseif (0x10000 > $code) {
+ $s = \chr(0xE0 | $code >> 12).\chr(0x80 | $code >> 6 & 0x3F).\chr(0x80 | $code & 0x3F);
+ } else {
+ $s = \chr(0xF0 | $code >> 18).\chr(0x80 | $code >> 12 & 0x3F).\chr(0x80 | $code >> 6 & 0x3F).\chr(0x80 | $code & 0x3F);
+ }
+
+ if ('UTF-8' !== $encoding = self::getEncoding($encoding)) {
+ $s = mb_convert_encoding($s, $encoding, 'UTF-8');
+ }
+
+ return $s;
+ }
+
+ public static function mb_ord($s, $encoding = null)
+ {
+ if ('UTF-8' !== $encoding = self::getEncoding($encoding)) {
+ $s = mb_convert_encoding($s, 'UTF-8', $encoding);
+ }
+
+ if (1 === \strlen($s)) {
+ return \ord($s);
+ }
+
+ $code = ($s = unpack('C*', substr($s, 0, 4))) ? $s[1] : 0;
+ if (0xF0 <= $code) {
+ return (($code - 0xF0) << 18) + (($s[2] - 0x80) << 12) + (($s[3] - 0x80) << 6) + $s[4] - 0x80;
+ }
+ if (0xE0 <= $code) {
+ return (($code - 0xE0) << 12) + (($s[2] - 0x80) << 6) + $s[3] - 0x80;
+ }
+ if (0xC0 <= $code) {
+ return (($code - 0xC0) << 6) + $s[2] - 0x80;
+ }
+
+ return $code;
+ }
+
+ public static function mb_str_pad(string $string, int $length, string $pad_string = ' ', int $pad_type = \STR_PAD_RIGHT, string $encoding = null): string
+ {
+ if (!\in_array($pad_type, [\STR_PAD_RIGHT, \STR_PAD_LEFT, \STR_PAD_BOTH], true)) {
+ throw new \ValueError('mb_str_pad(): Argument #4 ($pad_type) must be STR_PAD_LEFT, STR_PAD_RIGHT, or STR_PAD_BOTH');
+ }
+
+ if (null === $encoding) {
+ $encoding = self::mb_internal_encoding();
+ }
+
+ try {
+ $validEncoding = @self::mb_check_encoding('', $encoding);
+ } catch (\ValueError $e) {
+ throw new \ValueError(sprintf('mb_str_pad(): Argument #5 ($encoding) must be a valid encoding, "%s" given', $encoding));
+ }
+
+ // BC for PHP 7.3 and lower
+ if (!$validEncoding) {
+ throw new \ValueError(sprintf('mb_str_pad(): Argument #5 ($encoding) must be a valid encoding, "%s" given', $encoding));
+ }
+
+ if (self::mb_strlen($pad_string, $encoding) <= 0) {
+ throw new \ValueError('mb_str_pad(): Argument #3 ($pad_string) must be a non-empty string');
+ }
+
+ $paddingRequired = $length - self::mb_strlen($string, $encoding);
+
+ if ($paddingRequired < 1) {
+ return $string;
+ }
+
+ switch ($pad_type) {
+ case \STR_PAD_LEFT:
+ return self::mb_substr(str_repeat($pad_string, $paddingRequired), 0, $paddingRequired, $encoding).$string;
+ case \STR_PAD_RIGHT:
+ return $string.self::mb_substr(str_repeat($pad_string, $paddingRequired), 0, $paddingRequired, $encoding);
+ default:
+ $leftPaddingLength = floor($paddingRequired / 2);
+ $rightPaddingLength = $paddingRequired - $leftPaddingLength;
+
+ return self::mb_substr(str_repeat($pad_string, $leftPaddingLength), 0, $leftPaddingLength, $encoding).$string.self::mb_substr(str_repeat($pad_string, $rightPaddingLength), 0, $rightPaddingLength, $encoding);
+ }
+ }
+
+ private static function getSubpart($pos, $part, $haystack, $encoding)
+ {
+ if (false === $pos) {
+ return false;
+ }
+ if ($part) {
+ return self::mb_substr($haystack, 0, $pos, $encoding);
+ }
+
+ return self::mb_substr($haystack, $pos, null, $encoding);
+ }
+
+ private static function html_encoding_callback(array $m)
+ {
+ $i = 1;
+ $entities = '';
+ $m = unpack('C*', htmlentities($m[0], \ENT_COMPAT, 'UTF-8'));
+
+ while (isset($m[$i])) {
+ if (0x80 > $m[$i]) {
+ $entities .= \chr($m[$i++]);
+ continue;
+ }
+ if (0xF0 <= $m[$i]) {
+ $c = (($m[$i++] - 0xF0) << 18) + (($m[$i++] - 0x80) << 12) + (($m[$i++] - 0x80) << 6) + $m[$i++] - 0x80;
+ } elseif (0xE0 <= $m[$i]) {
+ $c = (($m[$i++] - 0xE0) << 12) + (($m[$i++] - 0x80) << 6) + $m[$i++] - 0x80;
+ } else {
+ $c = (($m[$i++] - 0xC0) << 6) + $m[$i++] - 0x80;
+ }
+
+ $entities .= ''.$c.';';
+ }
+
+ return $entities;
+ }
+
+ private static function title_case(array $s)
+ {
+ return self::mb_convert_case($s[1], \MB_CASE_UPPER, 'UTF-8').self::mb_convert_case($s[2], \MB_CASE_LOWER, 'UTF-8');
+ }
+
+ private static function getData($file)
+ {
+ if (file_exists($file = __DIR__.'/Resources/unidata/'.$file.'.php')) {
+ return require $file;
+ }
+
+ return false;
+ }
+
+ private static function getEncoding($encoding)
+ {
+ if (null === $encoding) {
+ return self::$internalEncoding;
+ }
+
+ if ('UTF-8' === $encoding) {
+ return 'UTF-8';
+ }
+
+ $encoding = strtoupper($encoding);
+
+ if ('8BIT' === $encoding || 'BINARY' === $encoding) {
+ return 'CP850';
+ }
+
+ if ('UTF8' === $encoding) {
+ return 'UTF-8';
+ }
+
+ return $encoding;
+ }
+}
diff --git a/src/vendor/symfony/polyfill-mbstring/README.md b/src/vendor/symfony/polyfill-mbstring/README.md
new file mode 100644
index 0000000..478b40d
--- /dev/null
+++ b/src/vendor/symfony/polyfill-mbstring/README.md
@@ -0,0 +1,13 @@
+Symfony Polyfill / Mbstring
+===========================
+
+This component provides a partial, native PHP implementation for the
+[Mbstring](https://php.net/mbstring) extension.
+
+More information can be found in the
+[main Polyfill README](https://github.com/symfony/polyfill/blob/main/README.md).
+
+License
+=======
+
+This library is released under the [MIT license](LICENSE).
diff --git a/src/vendor/symfony/polyfill-mbstring/Resources/unidata/caseFolding.php b/src/vendor/symfony/polyfill-mbstring/Resources/unidata/caseFolding.php
new file mode 100644
index 0000000..512bba0
--- /dev/null
+++ b/src/vendor/symfony/polyfill-mbstring/Resources/unidata/caseFolding.php
@@ -0,0 +1,119 @@
+ 'i̇',
+ 'µ' => 'μ',
+ 'ſ' => 's',
+ 'ͅ' => 'ι',
+ 'ς' => 'σ',
+ 'ϐ' => 'β',
+ 'ϑ' => 'θ',
+ 'ϕ' => 'φ',
+ 'ϖ' => 'π',
+ 'ϰ' => 'κ',
+ 'ϱ' => 'ρ',
+ 'ϵ' => 'ε',
+ 'ẛ' => 'ṡ',
+ 'ι' => 'ι',
+ 'ß' => 'ss',
+ 'ʼn' => 'ʼn',
+ 'ǰ' => 'ǰ',
+ 'ΐ' => 'ΐ',
+ 'ΰ' => 'ΰ',
+ 'և' => 'եւ',
+ 'ẖ' => 'ẖ',
+ 'ẗ' => 'ẗ',
+ 'ẘ' => 'ẘ',
+ 'ẙ' => 'ẙ',
+ 'ẚ' => 'aʾ',
+ 'ẞ' => 'ss',
+ 'ὐ' => 'ὐ',
+ 'ὒ' => 'ὒ',
+ 'ὔ' => 'ὔ',
+ 'ὖ' => 'ὖ',
+ 'ᾀ' => 'ἀι',
+ 'ᾁ' => 'ἁι',
+ 'ᾂ' => 'ἂι',
+ 'ᾃ' => 'ἃι',
+ 'ᾄ' => 'ἄι',
+ 'ᾅ' => 'ἅι',
+ 'ᾆ' => 'ἆι',
+ 'ᾇ' => 'ἇι',
+ 'ᾈ' => 'ἀι',
+ 'ᾉ' => 'ἁι',
+ 'ᾊ' => 'ἂι',
+ 'ᾋ' => 'ἃι',
+ 'ᾌ' => 'ἄι',
+ 'ᾍ' => 'ἅι',
+ 'ᾎ' => 'ἆι',
+ 'ᾏ' => 'ἇι',
+ 'ᾐ' => 'ἠι',
+ 'ᾑ' => 'ἡι',
+ 'ᾒ' => 'ἢι',
+ 'ᾓ' => 'ἣι',
+ 'ᾔ' => 'ἤι',
+ 'ᾕ' => 'ἥι',
+ 'ᾖ' => 'ἦι',
+ 'ᾗ' => 'ἧι',
+ 'ᾘ' => 'ἠι',
+ 'ᾙ' => 'ἡι',
+ 'ᾚ' => 'ἢι',
+ 'ᾛ' => 'ἣι',
+ 'ᾜ' => 'ἤι',
+ 'ᾝ' => 'ἥι',
+ 'ᾞ' => 'ἦι',
+ 'ᾟ' => 'ἧι',
+ 'ᾠ' => 'ὠι',
+ 'ᾡ' => 'ὡι',
+ 'ᾢ' => 'ὢι',
+ 'ᾣ' => 'ὣι',
+ 'ᾤ' => 'ὤι',
+ 'ᾥ' => 'ὥι',
+ 'ᾦ' => 'ὦι',
+ 'ᾧ' => 'ὧι',
+ 'ᾨ' => 'ὠι',
+ 'ᾩ' => 'ὡι',
+ 'ᾪ' => 'ὢι',
+ 'ᾫ' => 'ὣι',
+ 'ᾬ' => 'ὤι',
+ 'ᾭ' => 'ὥι',
+ 'ᾮ' => 'ὦι',
+ 'ᾯ' => 'ὧι',
+ 'ᾲ' => 'ὰι',
+ 'ᾳ' => 'αι',
+ 'ᾴ' => 'άι',
+ 'ᾶ' => 'ᾶ',
+ 'ᾷ' => 'ᾶι',
+ 'ᾼ' => 'αι',
+ 'ῂ' => 'ὴι',
+ 'ῃ' => 'ηι',
+ 'ῄ' => 'ήι',
+ 'ῆ' => 'ῆ',
+ 'ῇ' => 'ῆι',
+ 'ῌ' => 'ηι',
+ 'ῒ' => 'ῒ',
+ 'ῖ' => 'ῖ',
+ 'ῗ' => 'ῗ',
+ 'ῢ' => 'ῢ',
+ 'ῤ' => 'ῤ',
+ 'ῦ' => 'ῦ',
+ 'ῧ' => 'ῧ',
+ 'ῲ' => 'ὼι',
+ 'ῳ' => 'ωι',
+ 'ῴ' => 'ώι',
+ 'ῶ' => 'ῶ',
+ 'ῷ' => 'ῶι',
+ 'ῼ' => 'ωι',
+ 'ff' => 'ff',
+ 'fi' => 'fi',
+ 'fl' => 'fl',
+ 'ffi' => 'ffi',
+ 'ffl' => 'ffl',
+ 'ſt' => 'st',
+ 'st' => 'st',
+ 'ﬓ' => 'մն',
+ 'ﬔ' => 'մե',
+ 'ﬕ' => 'մի',
+ 'ﬖ' => 'վն',
+ 'ﬗ' => 'մխ',
+];
diff --git a/src/vendor/symfony/polyfill-mbstring/Resources/unidata/lowerCase.php b/src/vendor/symfony/polyfill-mbstring/Resources/unidata/lowerCase.php
new file mode 100644
index 0000000..fac60b0
--- /dev/null
+++ b/src/vendor/symfony/polyfill-mbstring/Resources/unidata/lowerCase.php
@@ -0,0 +1,1397 @@
+ 'a',
+ 'B' => 'b',
+ 'C' => 'c',
+ 'D' => 'd',
+ 'E' => 'e',
+ 'F' => 'f',
+ 'G' => 'g',
+ 'H' => 'h',
+ 'I' => 'i',
+ 'J' => 'j',
+ 'K' => 'k',
+ 'L' => 'l',
+ 'M' => 'm',
+ 'N' => 'n',
+ 'O' => 'o',
+ 'P' => 'p',
+ 'Q' => 'q',
+ 'R' => 'r',
+ 'S' => 's',
+ 'T' => 't',
+ 'U' => 'u',
+ 'V' => 'v',
+ 'W' => 'w',
+ 'X' => 'x',
+ 'Y' => 'y',
+ 'Z' => 'z',
+ 'À' => 'à',
+ 'Á' => 'á',
+ 'Â' => 'â',
+ 'Ã' => 'ã',
+ 'Ä' => 'ä',
+ 'Å' => 'å',
+ 'Æ' => 'æ',
+ 'Ç' => 'ç',
+ 'È' => 'è',
+ 'É' => 'é',
+ 'Ê' => 'ê',
+ 'Ë' => 'ë',
+ 'Ì' => 'ì',
+ 'Í' => 'í',
+ 'Î' => 'î',
+ 'Ï' => 'ï',
+ 'Ð' => 'ð',
+ 'Ñ' => 'ñ',
+ 'Ò' => 'ò',
+ 'Ó' => 'ó',
+ 'Ô' => 'ô',
+ 'Õ' => 'õ',
+ 'Ö' => 'ö',
+ 'Ø' => 'ø',
+ 'Ù' => 'ù',
+ 'Ú' => 'ú',
+ 'Û' => 'û',
+ 'Ü' => 'ü',
+ 'Ý' => 'ý',
+ 'Þ' => 'þ',
+ 'Ā' => 'ā',
+ 'Ă' => 'ă',
+ 'Ą' => 'ą',
+ 'Ć' => 'ć',
+ 'Ĉ' => 'ĉ',
+ 'Ċ' => 'ċ',
+ 'Č' => 'č',
+ 'Ď' => 'ď',
+ 'Đ' => 'đ',
+ 'Ē' => 'ē',
+ 'Ĕ' => 'ĕ',
+ 'Ė' => 'ė',
+ 'Ę' => 'ę',
+ 'Ě' => 'ě',
+ 'Ĝ' => 'ĝ',
+ 'Ğ' => 'ğ',
+ 'Ġ' => 'ġ',
+ 'Ģ' => 'ģ',
+ 'Ĥ' => 'ĥ',
+ 'Ħ' => 'ħ',
+ 'Ĩ' => 'ĩ',
+ 'Ī' => 'ī',
+ 'Ĭ' => 'ĭ',
+ 'Į' => 'į',
+ 'İ' => 'i̇',
+ 'IJ' => 'ij',
+ 'Ĵ' => 'ĵ',
+ 'Ķ' => 'ķ',
+ 'Ĺ' => 'ĺ',
+ 'Ļ' => 'ļ',
+ 'Ľ' => 'ľ',
+ 'Ŀ' => 'ŀ',
+ 'Ł' => 'ł',
+ 'Ń' => 'ń',
+ 'Ņ' => 'ņ',
+ 'Ň' => 'ň',
+ 'Ŋ' => 'ŋ',
+ 'Ō' => 'ō',
+ 'Ŏ' => 'ŏ',
+ 'Ő' => 'ő',
+ 'Œ' => 'œ',
+ 'Ŕ' => 'ŕ',
+ 'Ŗ' => 'ŗ',
+ 'Ř' => 'ř',
+ 'Ś' => 'ś',
+ 'Ŝ' => 'ŝ',
+ 'Ş' => 'ş',
+ 'Š' => 'š',
+ 'Ţ' => 'ţ',
+ 'Ť' => 'ť',
+ 'Ŧ' => 'ŧ',
+ 'Ũ' => 'ũ',
+ 'Ū' => 'ū',
+ 'Ŭ' => 'ŭ',
+ 'Ů' => 'ů',
+ 'Ű' => 'ű',
+ 'Ų' => 'ų',
+ 'Ŵ' => 'ŵ',
+ 'Ŷ' => 'ŷ',
+ 'Ÿ' => 'ÿ',
+ 'Ź' => 'ź',
+ 'Ż' => 'ż',
+ 'Ž' => 'ž',
+ 'Ɓ' => 'ɓ',
+ 'Ƃ' => 'ƃ',
+ 'Ƅ' => 'ƅ',
+ 'Ɔ' => 'ɔ',
+ 'Ƈ' => 'ƈ',
+ 'Ɖ' => 'ɖ',
+ 'Ɗ' => 'ɗ',
+ 'Ƌ' => 'ƌ',
+ 'Ǝ' => 'ǝ',
+ 'Ə' => 'ə',
+ 'Ɛ' => 'ɛ',
+ 'Ƒ' => 'ƒ',
+ 'Ɠ' => 'ɠ',
+ 'Ɣ' => 'ɣ',
+ 'Ɩ' => 'ɩ',
+ 'Ɨ' => 'ɨ',
+ 'Ƙ' => 'ƙ',
+ 'Ɯ' => 'ɯ',
+ 'Ɲ' => 'ɲ',
+ 'Ɵ' => 'ɵ',
+ 'Ơ' => 'ơ',
+ 'Ƣ' => 'ƣ',
+ 'Ƥ' => 'ƥ',
+ 'Ʀ' => 'ʀ',
+ 'Ƨ' => 'ƨ',
+ 'Ʃ' => 'ʃ',
+ 'Ƭ' => 'ƭ',
+ 'Ʈ' => 'ʈ',
+ 'Ư' => 'ư',
+ 'Ʊ' => 'ʊ',
+ 'Ʋ' => 'ʋ',
+ 'Ƴ' => 'ƴ',
+ 'Ƶ' => 'ƶ',
+ 'Ʒ' => 'ʒ',
+ 'Ƹ' => 'ƹ',
+ 'Ƽ' => 'ƽ',
+ 'DŽ' => 'dž',
+ 'Dž' => 'dž',
+ 'LJ' => 'lj',
+ 'Lj' => 'lj',
+ 'NJ' => 'nj',
+ 'Nj' => 'nj',
+ 'Ǎ' => 'ǎ',
+ 'Ǐ' => 'ǐ',
+ 'Ǒ' => 'ǒ',
+ 'Ǔ' => 'ǔ',
+ 'Ǖ' => 'ǖ',
+ 'Ǘ' => 'ǘ',
+ 'Ǚ' => 'ǚ',
+ 'Ǜ' => 'ǜ',
+ 'Ǟ' => 'ǟ',
+ 'Ǡ' => 'ǡ',
+ 'Ǣ' => 'ǣ',
+ 'Ǥ' => 'ǥ',
+ 'Ǧ' => 'ǧ',
+ 'Ǩ' => 'ǩ',
+ 'Ǫ' => 'ǫ',
+ 'Ǭ' => 'ǭ',
+ 'Ǯ' => 'ǯ',
+ 'DZ' => 'dz',
+ 'Dz' => 'dz',
+ 'Ǵ' => 'ǵ',
+ 'Ƕ' => 'ƕ',
+ 'Ƿ' => 'ƿ',
+ 'Ǹ' => 'ǹ',
+ 'Ǻ' => 'ǻ',
+ 'Ǽ' => 'ǽ',
+ 'Ǿ' => 'ǿ',
+ 'Ȁ' => 'ȁ',
+ 'Ȃ' => 'ȃ',
+ 'Ȅ' => 'ȅ',
+ 'Ȇ' => 'ȇ',
+ 'Ȉ' => 'ȉ',
+ 'Ȋ' => 'ȋ',
+ 'Ȍ' => 'ȍ',
+ 'Ȏ' => 'ȏ',
+ 'Ȑ' => 'ȑ',
+ 'Ȓ' => 'ȓ',
+ 'Ȕ' => 'ȕ',
+ 'Ȗ' => 'ȗ',
+ 'Ș' => 'ș',
+ 'Ț' => 'ț',
+ 'Ȝ' => 'ȝ',
+ 'Ȟ' => 'ȟ',
+ 'Ƞ' => 'ƞ',
+ 'Ȣ' => 'ȣ',
+ 'Ȥ' => 'ȥ',
+ 'Ȧ' => 'ȧ',
+ 'Ȩ' => 'ȩ',
+ 'Ȫ' => 'ȫ',
+ 'Ȭ' => 'ȭ',
+ 'Ȯ' => 'ȯ',
+ 'Ȱ' => 'ȱ',
+ 'Ȳ' => 'ȳ',
+ 'Ⱥ' => 'ⱥ',
+ 'Ȼ' => 'ȼ',
+ 'Ƚ' => 'ƚ',
+ 'Ⱦ' => 'ⱦ',
+ 'Ɂ' => 'ɂ',
+ 'Ƀ' => 'ƀ',
+ 'Ʉ' => 'ʉ',
+ 'Ʌ' => 'ʌ',
+ 'Ɇ' => 'ɇ',
+ 'Ɉ' => 'ɉ',
+ 'Ɋ' => 'ɋ',
+ 'Ɍ' => 'ɍ',
+ 'Ɏ' => 'ɏ',
+ 'Ͱ' => 'ͱ',
+ 'Ͳ' => 'ͳ',
+ 'Ͷ' => 'ͷ',
+ 'Ϳ' => 'ϳ',
+ 'Ά' => 'ά',
+ 'Έ' => 'έ',
+ 'Ή' => 'ή',
+ 'Ί' => 'ί',
+ 'Ό' => 'ό',
+ 'Ύ' => 'ύ',
+ 'Ώ' => 'ώ',
+ 'Α' => 'α',
+ 'Β' => 'β',
+ 'Γ' => 'γ',
+ 'Δ' => 'δ',
+ 'Ε' => 'ε',
+ 'Ζ' => 'ζ',
+ 'Η' => 'η',
+ 'Θ' => 'θ',
+ 'Ι' => 'ι',
+ 'Κ' => 'κ',
+ 'Λ' => 'λ',
+ 'Μ' => 'μ',
+ 'Ν' => 'ν',
+ 'Ξ' => 'ξ',
+ 'Ο' => 'ο',
+ 'Π' => 'π',
+ 'Ρ' => 'ρ',
+ 'Σ' => 'σ',
+ 'Τ' => 'τ',
+ 'Υ' => 'υ',
+ 'Φ' => 'φ',
+ 'Χ' => 'χ',
+ 'Ψ' => 'ψ',
+ 'Ω' => 'ω',
+ 'Ϊ' => 'ϊ',
+ 'Ϋ' => 'ϋ',
+ 'Ϗ' => 'ϗ',
+ 'Ϙ' => 'ϙ',
+ 'Ϛ' => 'ϛ',
+ 'Ϝ' => 'ϝ',
+ 'Ϟ' => 'ϟ',
+ 'Ϡ' => 'ϡ',
+ 'Ϣ' => 'ϣ',
+ 'Ϥ' => 'ϥ',
+ 'Ϧ' => 'ϧ',
+ 'Ϩ' => 'ϩ',
+ 'Ϫ' => 'ϫ',
+ 'Ϭ' => 'ϭ',
+ 'Ϯ' => 'ϯ',
+ 'ϴ' => 'θ',
+ 'Ϸ' => 'ϸ',
+ 'Ϲ' => 'ϲ',
+ 'Ϻ' => 'ϻ',
+ 'Ͻ' => 'ͻ',
+ 'Ͼ' => 'ͼ',
+ 'Ͽ' => 'ͽ',
+ 'Ѐ' => 'ѐ',
+ 'Ё' => 'ё',
+ 'Ђ' => 'ђ',
+ 'Ѓ' => 'ѓ',
+ 'Є' => 'є',
+ 'Ѕ' => 'ѕ',
+ 'І' => 'і',
+ 'Ї' => 'ї',
+ 'Ј' => 'ј',
+ 'Љ' => 'љ',
+ 'Њ' => 'њ',
+ 'Ћ' => 'ћ',
+ 'Ќ' => 'ќ',
+ 'Ѝ' => 'ѝ',
+ 'Ў' => 'ў',
+ 'Џ' => 'џ',
+ 'А' => 'а',
+ 'Б' => 'б',
+ 'В' => 'в',
+ 'Г' => 'г',
+ 'Д' => 'д',
+ 'Е' => 'е',
+ 'Ж' => 'ж',
+ 'З' => 'з',
+ 'И' => 'и',
+ 'Й' => 'й',
+ 'К' => 'к',
+ 'Л' => 'л',
+ 'М' => 'м',
+ 'Н' => 'н',
+ 'О' => 'о',
+ 'П' => 'п',
+ 'Р' => 'р',
+ 'С' => 'с',
+ 'Т' => 'т',
+ 'У' => 'у',
+ 'Ф' => 'ф',
+ 'Х' => 'х',
+ 'Ц' => 'ц',
+ 'Ч' => 'ч',
+ 'Ш' => 'ш',
+ 'Щ' => 'щ',
+ 'Ъ' => 'ъ',
+ 'Ы' => 'ы',
+ 'Ь' => 'ь',
+ 'Э' => 'э',
+ 'Ю' => 'ю',
+ 'Я' => 'я',
+ 'Ѡ' => 'ѡ',
+ 'Ѣ' => 'ѣ',
+ 'Ѥ' => 'ѥ',
+ 'Ѧ' => 'ѧ',
+ 'Ѩ' => 'ѩ',
+ 'Ѫ' => 'ѫ',
+ 'Ѭ' => 'ѭ',
+ 'Ѯ' => 'ѯ',
+ 'Ѱ' => 'ѱ',
+ 'Ѳ' => 'ѳ',
+ 'Ѵ' => 'ѵ',
+ 'Ѷ' => 'ѷ',
+ 'Ѹ' => 'ѹ',
+ 'Ѻ' => 'ѻ',
+ 'Ѽ' => 'ѽ',
+ 'Ѿ' => 'ѿ',
+ 'Ҁ' => 'ҁ',
+ 'Ҋ' => 'ҋ',
+ 'Ҍ' => 'ҍ',
+ 'Ҏ' => 'ҏ',
+ 'Ґ' => 'ґ',
+ 'Ғ' => 'ғ',
+ 'Ҕ' => 'ҕ',
+ 'Җ' => 'җ',
+ 'Ҙ' => 'ҙ',
+ 'Қ' => 'қ',
+ 'Ҝ' => 'ҝ',
+ 'Ҟ' => 'ҟ',
+ 'Ҡ' => 'ҡ',
+ 'Ң' => 'ң',
+ 'Ҥ' => 'ҥ',
+ 'Ҧ' => 'ҧ',
+ 'Ҩ' => 'ҩ',
+ 'Ҫ' => 'ҫ',
+ 'Ҭ' => 'ҭ',
+ 'Ү' => 'ү',
+ 'Ұ' => 'ұ',
+ 'Ҳ' => 'ҳ',
+ 'Ҵ' => 'ҵ',
+ 'Ҷ' => 'ҷ',
+ 'Ҹ' => 'ҹ',
+ 'Һ' => 'һ',
+ 'Ҽ' => 'ҽ',
+ 'Ҿ' => 'ҿ',
+ 'Ӏ' => 'ӏ',
+ 'Ӂ' => 'ӂ',
+ 'Ӄ' => 'ӄ',
+ 'Ӆ' => 'ӆ',
+ 'Ӈ' => 'ӈ',
+ 'Ӊ' => 'ӊ',
+ 'Ӌ' => 'ӌ',
+ 'Ӎ' => 'ӎ',
+ 'Ӑ' => 'ӑ',
+ 'Ӓ' => 'ӓ',
+ 'Ӕ' => 'ӕ',
+ 'Ӗ' => 'ӗ',
+ 'Ә' => 'ә',
+ 'Ӛ' => 'ӛ',
+ 'Ӝ' => 'ӝ',
+ 'Ӟ' => 'ӟ',
+ 'Ӡ' => 'ӡ',
+ 'Ӣ' => 'ӣ',
+ 'Ӥ' => 'ӥ',
+ 'Ӧ' => 'ӧ',
+ 'Ө' => 'ө',
+ 'Ӫ' => 'ӫ',
+ 'Ӭ' => 'ӭ',
+ 'Ӯ' => 'ӯ',
+ 'Ӱ' => 'ӱ',
+ 'Ӳ' => 'ӳ',
+ 'Ӵ' => 'ӵ',
+ 'Ӷ' => 'ӷ',
+ 'Ӹ' => 'ӹ',
+ 'Ӻ' => 'ӻ',
+ 'Ӽ' => 'ӽ',
+ 'Ӿ' => 'ӿ',
+ 'Ԁ' => 'ԁ',
+ 'Ԃ' => 'ԃ',
+ 'Ԅ' => 'ԅ',
+ 'Ԇ' => 'ԇ',
+ 'Ԉ' => 'ԉ',
+ 'Ԋ' => 'ԋ',
+ 'Ԍ' => 'ԍ',
+ 'Ԏ' => 'ԏ',
+ 'Ԑ' => 'ԑ',
+ 'Ԓ' => 'ԓ',
+ 'Ԕ' => 'ԕ',
+ 'Ԗ' => 'ԗ',
+ 'Ԙ' => 'ԙ',
+ 'Ԛ' => 'ԛ',
+ 'Ԝ' => 'ԝ',
+ 'Ԟ' => 'ԟ',
+ 'Ԡ' => 'ԡ',
+ 'Ԣ' => 'ԣ',
+ 'Ԥ' => 'ԥ',
+ 'Ԧ' => 'ԧ',
+ 'Ԩ' => 'ԩ',
+ 'Ԫ' => 'ԫ',
+ 'Ԭ' => 'ԭ',
+ 'Ԯ' => 'ԯ',
+ 'Ա' => 'ա',
+ 'Բ' => 'բ',
+ 'Գ' => 'գ',
+ 'Դ' => 'դ',
+ 'Ե' => 'ե',
+ 'Զ' => 'զ',
+ 'Է' => 'է',
+ 'Ը' => 'ը',
+ 'Թ' => 'թ',
+ 'Ժ' => 'ժ',
+ 'Ի' => 'ի',
+ 'Լ' => 'լ',
+ 'Խ' => 'խ',
+ 'Ծ' => 'ծ',
+ 'Կ' => 'կ',
+ 'Հ' => 'հ',
+ 'Ձ' => 'ձ',
+ 'Ղ' => 'ղ',
+ 'Ճ' => 'ճ',
+ 'Մ' => 'մ',
+ 'Յ' => 'յ',
+ 'Ն' => 'ն',
+ 'Շ' => 'շ',
+ 'Ո' => 'ո',
+ 'Չ' => 'չ',
+ 'Պ' => 'պ',
+ 'Ջ' => 'ջ',
+ 'Ռ' => 'ռ',
+ 'Ս' => 'ս',
+ 'Վ' => 'վ',
+ 'Տ' => 'տ',
+ 'Ր' => 'ր',
+ 'Ց' => 'ց',
+ 'Ւ' => 'ւ',
+ 'Փ' => 'փ',
+ 'Ք' => 'ք',
+ 'Օ' => 'օ',
+ 'Ֆ' => 'ֆ',
+ 'Ⴀ' => 'ⴀ',
+ 'Ⴁ' => 'ⴁ',
+ 'Ⴂ' => 'ⴂ',
+ 'Ⴃ' => 'ⴃ',
+ 'Ⴄ' => 'ⴄ',
+ 'Ⴅ' => 'ⴅ',
+ 'Ⴆ' => 'ⴆ',
+ 'Ⴇ' => 'ⴇ',
+ 'Ⴈ' => 'ⴈ',
+ 'Ⴉ' => 'ⴉ',
+ 'Ⴊ' => 'ⴊ',
+ 'Ⴋ' => 'ⴋ',
+ 'Ⴌ' => 'ⴌ',
+ 'Ⴍ' => 'ⴍ',
+ 'Ⴎ' => 'ⴎ',
+ 'Ⴏ' => 'ⴏ',
+ 'Ⴐ' => 'ⴐ',
+ 'Ⴑ' => 'ⴑ',
+ 'Ⴒ' => 'ⴒ',
+ 'Ⴓ' => 'ⴓ',
+ 'Ⴔ' => 'ⴔ',
+ 'Ⴕ' => 'ⴕ',
+ 'Ⴖ' => 'ⴖ',
+ 'Ⴗ' => 'ⴗ',
+ 'Ⴘ' => 'ⴘ',
+ 'Ⴙ' => 'ⴙ',
+ 'Ⴚ' => 'ⴚ',
+ 'Ⴛ' => 'ⴛ',
+ 'Ⴜ' => 'ⴜ',
+ 'Ⴝ' => 'ⴝ',
+ 'Ⴞ' => 'ⴞ',
+ 'Ⴟ' => 'ⴟ',
+ 'Ⴠ' => 'ⴠ',
+ 'Ⴡ' => 'ⴡ',
+ 'Ⴢ' => 'ⴢ',
+ 'Ⴣ' => 'ⴣ',
+ 'Ⴤ' => 'ⴤ',
+ 'Ⴥ' => 'ⴥ',
+ 'Ⴧ' => 'ⴧ',
+ 'Ⴭ' => 'ⴭ',
+ 'Ꭰ' => 'ꭰ',
+ 'Ꭱ' => 'ꭱ',
+ 'Ꭲ' => 'ꭲ',
+ 'Ꭳ' => 'ꭳ',
+ 'Ꭴ' => 'ꭴ',
+ 'Ꭵ' => 'ꭵ',
+ 'Ꭶ' => 'ꭶ',
+ 'Ꭷ' => 'ꭷ',
+ 'Ꭸ' => 'ꭸ',
+ 'Ꭹ' => 'ꭹ',
+ 'Ꭺ' => 'ꭺ',
+ 'Ꭻ' => 'ꭻ',
+ 'Ꭼ' => 'ꭼ',
+ 'Ꭽ' => 'ꭽ',
+ 'Ꭾ' => 'ꭾ',
+ 'Ꭿ' => 'ꭿ',
+ 'Ꮀ' => 'ꮀ',
+ 'Ꮁ' => 'ꮁ',
+ 'Ꮂ' => 'ꮂ',
+ 'Ꮃ' => 'ꮃ',
+ 'Ꮄ' => 'ꮄ',
+ 'Ꮅ' => 'ꮅ',
+ 'Ꮆ' => 'ꮆ',
+ 'Ꮇ' => 'ꮇ',
+ 'Ꮈ' => 'ꮈ',
+ 'Ꮉ' => 'ꮉ',
+ 'Ꮊ' => 'ꮊ',
+ 'Ꮋ' => 'ꮋ',
+ 'Ꮌ' => 'ꮌ',
+ 'Ꮍ' => 'ꮍ',
+ 'Ꮎ' => 'ꮎ',
+ 'Ꮏ' => 'ꮏ',
+ 'Ꮐ' => 'ꮐ',
+ 'Ꮑ' => 'ꮑ',
+ 'Ꮒ' => 'ꮒ',
+ 'Ꮓ' => 'ꮓ',
+ 'Ꮔ' => 'ꮔ',
+ 'Ꮕ' => 'ꮕ',
+ 'Ꮖ' => 'ꮖ',
+ 'Ꮗ' => 'ꮗ',
+ 'Ꮘ' => 'ꮘ',
+ 'Ꮙ' => 'ꮙ',
+ 'Ꮚ' => 'ꮚ',
+ 'Ꮛ' => 'ꮛ',
+ 'Ꮜ' => 'ꮜ',
+ 'Ꮝ' => 'ꮝ',
+ 'Ꮞ' => 'ꮞ',
+ 'Ꮟ' => 'ꮟ',
+ 'Ꮠ' => 'ꮠ',
+ 'Ꮡ' => 'ꮡ',
+ 'Ꮢ' => 'ꮢ',
+ 'Ꮣ' => 'ꮣ',
+ 'Ꮤ' => 'ꮤ',
+ 'Ꮥ' => 'ꮥ',
+ 'Ꮦ' => 'ꮦ',
+ 'Ꮧ' => 'ꮧ',
+ 'Ꮨ' => 'ꮨ',
+ 'Ꮩ' => 'ꮩ',
+ 'Ꮪ' => 'ꮪ',
+ 'Ꮫ' => 'ꮫ',
+ 'Ꮬ' => 'ꮬ',
+ 'Ꮭ' => 'ꮭ',
+ 'Ꮮ' => 'ꮮ',
+ 'Ꮯ' => 'ꮯ',
+ 'Ꮰ' => 'ꮰ',
+ 'Ꮱ' => 'ꮱ',
+ 'Ꮲ' => 'ꮲ',
+ 'Ꮳ' => 'ꮳ',
+ 'Ꮴ' => 'ꮴ',
+ 'Ꮵ' => 'ꮵ',
+ 'Ꮶ' => 'ꮶ',
+ 'Ꮷ' => 'ꮷ',
+ 'Ꮸ' => 'ꮸ',
+ 'Ꮹ' => 'ꮹ',
+ 'Ꮺ' => 'ꮺ',
+ 'Ꮻ' => 'ꮻ',
+ 'Ꮼ' => 'ꮼ',
+ 'Ꮽ' => 'ꮽ',
+ 'Ꮾ' => 'ꮾ',
+ 'Ꮿ' => 'ꮿ',
+ 'Ᏸ' => 'ᏸ',
+ 'Ᏹ' => 'ᏹ',
+ 'Ᏺ' => 'ᏺ',
+ 'Ᏻ' => 'ᏻ',
+ 'Ᏼ' => 'ᏼ',
+ 'Ᏽ' => 'ᏽ',
+ 'Ა' => 'ა',
+ 'Ბ' => 'ბ',
+ 'Გ' => 'გ',
+ 'Დ' => 'დ',
+ 'Ე' => 'ე',
+ 'Ვ' => 'ვ',
+ 'Ზ' => 'ზ',
+ 'Თ' => 'თ',
+ 'Ი' => 'ი',
+ 'Კ' => 'კ',
+ 'Ლ' => 'ლ',
+ 'Მ' => 'მ',
+ 'Ნ' => 'ნ',
+ 'Ო' => 'ო',
+ 'Პ' => 'პ',
+ 'Ჟ' => 'ჟ',
+ 'Რ' => 'რ',
+ 'Ს' => 'ს',
+ 'Ტ' => 'ტ',
+ 'Უ' => 'უ',
+ 'Ფ' => 'ფ',
+ 'Ქ' => 'ქ',
+ 'Ღ' => 'ღ',
+ 'Ყ' => 'ყ',
+ 'Შ' => 'შ',
+ 'Ჩ' => 'ჩ',
+ 'Ც' => 'ც',
+ 'Ძ' => 'ძ',
+ 'Წ' => 'წ',
+ 'Ჭ' => 'ჭ',
+ 'Ხ' => 'ხ',
+ 'Ჯ' => 'ჯ',
+ 'Ჰ' => 'ჰ',
+ 'Ჱ' => 'ჱ',
+ 'Ჲ' => 'ჲ',
+ 'Ჳ' => 'ჳ',
+ 'Ჴ' => 'ჴ',
+ 'Ჵ' => 'ჵ',
+ 'Ჶ' => 'ჶ',
+ 'Ჷ' => 'ჷ',
+ 'Ჸ' => 'ჸ',
+ 'Ჹ' => 'ჹ',
+ 'Ჺ' => 'ჺ',
+ 'Ჽ' => 'ჽ',
+ 'Ჾ' => 'ჾ',
+ 'Ჿ' => 'ჿ',
+ 'Ḁ' => 'ḁ',
+ 'Ḃ' => 'ḃ',
+ 'Ḅ' => 'ḅ',
+ 'Ḇ' => 'ḇ',
+ 'Ḉ' => 'ḉ',
+ 'Ḋ' => 'ḋ',
+ 'Ḍ' => 'ḍ',
+ 'Ḏ' => 'ḏ',
+ 'Ḑ' => 'ḑ',
+ 'Ḓ' => 'ḓ',
+ 'Ḕ' => 'ḕ',
+ 'Ḗ' => 'ḗ',
+ 'Ḙ' => 'ḙ',
+ 'Ḛ' => 'ḛ',
+ 'Ḝ' => 'ḝ',
+ 'Ḟ' => 'ḟ',
+ 'Ḡ' => 'ḡ',
+ 'Ḣ' => 'ḣ',
+ 'Ḥ' => 'ḥ',
+ 'Ḧ' => 'ḧ',
+ 'Ḩ' => 'ḩ',
+ 'Ḫ' => 'ḫ',
+ 'Ḭ' => 'ḭ',
+ 'Ḯ' => 'ḯ',
+ 'Ḱ' => 'ḱ',
+ 'Ḳ' => 'ḳ',
+ 'Ḵ' => 'ḵ',
+ 'Ḷ' => 'ḷ',
+ 'Ḹ' => 'ḹ',
+ 'Ḻ' => 'ḻ',
+ 'Ḽ' => 'ḽ',
+ 'Ḿ' => 'ḿ',
+ 'Ṁ' => 'ṁ',
+ 'Ṃ' => 'ṃ',
+ 'Ṅ' => 'ṅ',
+ 'Ṇ' => 'ṇ',
+ 'Ṉ' => 'ṉ',
+ 'Ṋ' => 'ṋ',
+ 'Ṍ' => 'ṍ',
+ 'Ṏ' => 'ṏ',
+ 'Ṑ' => 'ṑ',
+ 'Ṓ' => 'ṓ',
+ 'Ṕ' => 'ṕ',
+ 'Ṗ' => 'ṗ',
+ 'Ṙ' => 'ṙ',
+ 'Ṛ' => 'ṛ',
+ 'Ṝ' => 'ṝ',
+ 'Ṟ' => 'ṟ',
+ 'Ṡ' => 'ṡ',
+ 'Ṣ' => 'ṣ',
+ 'Ṥ' => 'ṥ',
+ 'Ṧ' => 'ṧ',
+ 'Ṩ' => 'ṩ',
+ 'Ṫ' => 'ṫ',
+ 'Ṭ' => 'ṭ',
+ 'Ṯ' => 'ṯ',
+ 'Ṱ' => 'ṱ',
+ 'Ṳ' => 'ṳ',
+ 'Ṵ' => 'ṵ',
+ 'Ṷ' => 'ṷ',
+ 'Ṹ' => 'ṹ',
+ 'Ṻ' => 'ṻ',
+ 'Ṽ' => 'ṽ',
+ 'Ṿ' => 'ṿ',
+ 'Ẁ' => 'ẁ',
+ 'Ẃ' => 'ẃ',
+ 'Ẅ' => 'ẅ',
+ 'Ẇ' => 'ẇ',
+ 'Ẉ' => 'ẉ',
+ 'Ẋ' => 'ẋ',
+ 'Ẍ' => 'ẍ',
+ 'Ẏ' => 'ẏ',
+ 'Ẑ' => 'ẑ',
+ 'Ẓ' => 'ẓ',
+ 'Ẕ' => 'ẕ',
+ 'ẞ' => 'ß',
+ 'Ạ' => 'ạ',
+ 'Ả' => 'ả',
+ 'Ấ' => 'ấ',
+ 'Ầ' => 'ầ',
+ 'Ẩ' => 'ẩ',
+ 'Ẫ' => 'ẫ',
+ 'Ậ' => 'ậ',
+ 'Ắ' => 'ắ',
+ 'Ằ' => 'ằ',
+ 'Ẳ' => 'ẳ',
+ 'Ẵ' => 'ẵ',
+ 'Ặ' => 'ặ',
+ 'Ẹ' => 'ẹ',
+ 'Ẻ' => 'ẻ',
+ 'Ẽ' => 'ẽ',
+ 'Ế' => 'ế',
+ 'Ề' => 'ề',
+ 'Ể' => 'ể',
+ 'Ễ' => 'ễ',
+ 'Ệ' => 'ệ',
+ 'Ỉ' => 'ỉ',
+ 'Ị' => 'ị',
+ 'Ọ' => 'ọ',
+ 'Ỏ' => 'ỏ',
+ 'Ố' => 'ố',
+ 'Ồ' => 'ồ',
+ 'Ổ' => 'ổ',
+ 'Ỗ' => 'ỗ',
+ 'Ộ' => 'ộ',
+ 'Ớ' => 'ớ',
+ 'Ờ' => 'ờ',
+ 'Ở' => 'ở',
+ 'Ỡ' => 'ỡ',
+ 'Ợ' => 'ợ',
+ 'Ụ' => 'ụ',
+ 'Ủ' => 'ủ',
+ 'Ứ' => 'ứ',
+ 'Ừ' => 'ừ',
+ 'Ử' => 'ử',
+ 'Ữ' => 'ữ',
+ 'Ự' => 'ự',
+ 'Ỳ' => 'ỳ',
+ 'Ỵ' => 'ỵ',
+ 'Ỷ' => 'ỷ',
+ 'Ỹ' => 'ỹ',
+ 'Ỻ' => 'ỻ',
+ 'Ỽ' => 'ỽ',
+ 'Ỿ' => 'ỿ',
+ 'Ἀ' => 'ἀ',
+ 'Ἁ' => 'ἁ',
+ 'Ἂ' => 'ἂ',
+ 'Ἃ' => 'ἃ',
+ 'Ἄ' => 'ἄ',
+ 'Ἅ' => 'ἅ',
+ 'Ἆ' => 'ἆ',
+ 'Ἇ' => 'ἇ',
+ 'Ἐ' => 'ἐ',
+ 'Ἑ' => 'ἑ',
+ 'Ἒ' => 'ἒ',
+ 'Ἓ' => 'ἓ',
+ 'Ἔ' => 'ἔ',
+ 'Ἕ' => 'ἕ',
+ 'Ἠ' => 'ἠ',
+ 'Ἡ' => 'ἡ',
+ 'Ἢ' => 'ἢ',
+ 'Ἣ' => 'ἣ',
+ 'Ἤ' => 'ἤ',
+ 'Ἥ' => 'ἥ',
+ 'Ἦ' => 'ἦ',
+ 'Ἧ' => 'ἧ',
+ 'Ἰ' => 'ἰ',
+ 'Ἱ' => 'ἱ',
+ 'Ἲ' => 'ἲ',
+ 'Ἳ' => 'ἳ',
+ 'Ἴ' => 'ἴ',
+ 'Ἵ' => 'ἵ',
+ 'Ἶ' => 'ἶ',
+ 'Ἷ' => 'ἷ',
+ 'Ὀ' => 'ὀ',
+ 'Ὁ' => 'ὁ',
+ 'Ὂ' => 'ὂ',
+ 'Ὃ' => 'ὃ',
+ 'Ὄ' => 'ὄ',
+ 'Ὅ' => 'ὅ',
+ 'Ὑ' => 'ὑ',
+ 'Ὓ' => 'ὓ',
+ 'Ὕ' => 'ὕ',
+ 'Ὗ' => 'ὗ',
+ 'Ὠ' => 'ὠ',
+ 'Ὡ' => 'ὡ',
+ 'Ὢ' => 'ὢ',
+ 'Ὣ' => 'ὣ',
+ 'Ὤ' => 'ὤ',
+ 'Ὥ' => 'ὥ',
+ 'Ὦ' => 'ὦ',
+ 'Ὧ' => 'ὧ',
+ 'ᾈ' => 'ᾀ',
+ 'ᾉ' => 'ᾁ',
+ 'ᾊ' => 'ᾂ',
+ 'ᾋ' => 'ᾃ',
+ 'ᾌ' => 'ᾄ',
+ 'ᾍ' => 'ᾅ',
+ 'ᾎ' => 'ᾆ',
+ 'ᾏ' => 'ᾇ',
+ 'ᾘ' => 'ᾐ',
+ 'ᾙ' => 'ᾑ',
+ 'ᾚ' => 'ᾒ',
+ 'ᾛ' => 'ᾓ',
+ 'ᾜ' => 'ᾔ',
+ 'ᾝ' => 'ᾕ',
+ 'ᾞ' => 'ᾖ',
+ 'ᾟ' => 'ᾗ',
+ 'ᾨ' => 'ᾠ',
+ 'ᾩ' => 'ᾡ',
+ 'ᾪ' => 'ᾢ',
+ 'ᾫ' => 'ᾣ',
+ 'ᾬ' => 'ᾤ',
+ 'ᾭ' => 'ᾥ',
+ 'ᾮ' => 'ᾦ',
+ 'ᾯ' => 'ᾧ',
+ 'Ᾰ' => 'ᾰ',
+ 'Ᾱ' => 'ᾱ',
+ 'Ὰ' => 'ὰ',
+ 'Ά' => 'ά',
+ 'ᾼ' => 'ᾳ',
+ 'Ὲ' => 'ὲ',
+ 'Έ' => 'έ',
+ 'Ὴ' => 'ὴ',
+ 'Ή' => 'ή',
+ 'ῌ' => 'ῃ',
+ 'Ῐ' => 'ῐ',
+ 'Ῑ' => 'ῑ',
+ 'Ὶ' => 'ὶ',
+ 'Ί' => 'ί',
+ 'Ῠ' => 'ῠ',
+ 'Ῡ' => 'ῡ',
+ 'Ὺ' => 'ὺ',
+ 'Ύ' => 'ύ',
+ 'Ῥ' => 'ῥ',
+ 'Ὸ' => 'ὸ',
+ 'Ό' => 'ό',
+ 'Ὼ' => 'ὼ',
+ 'Ώ' => 'ώ',
+ 'ῼ' => 'ῳ',
+ 'Ω' => 'ω',
+ 'K' => 'k',
+ 'Å' => 'å',
+ 'Ⅎ' => 'ⅎ',
+ 'Ⅰ' => 'ⅰ',
+ 'Ⅱ' => 'ⅱ',
+ 'Ⅲ' => 'ⅲ',
+ 'Ⅳ' => 'ⅳ',
+ 'Ⅴ' => 'ⅴ',
+ 'Ⅵ' => 'ⅵ',
+ 'Ⅶ' => 'ⅶ',
+ 'Ⅷ' => 'ⅷ',
+ 'Ⅸ' => 'ⅸ',
+ 'Ⅹ' => 'ⅹ',
+ 'Ⅺ' => 'ⅺ',
+ 'Ⅻ' => 'ⅻ',
+ 'Ⅼ' => 'ⅼ',
+ 'Ⅽ' => 'ⅽ',
+ 'Ⅾ' => 'ⅾ',
+ 'Ⅿ' => 'ⅿ',
+ 'Ↄ' => 'ↄ',
+ 'Ⓐ' => 'ⓐ',
+ 'Ⓑ' => 'ⓑ',
+ 'Ⓒ' => 'ⓒ',
+ 'Ⓓ' => 'ⓓ',
+ 'Ⓔ' => 'ⓔ',
+ 'Ⓕ' => 'ⓕ',
+ 'Ⓖ' => 'ⓖ',
+ 'Ⓗ' => 'ⓗ',
+ 'Ⓘ' => 'ⓘ',
+ 'Ⓙ' => 'ⓙ',
+ 'Ⓚ' => 'ⓚ',
+ 'Ⓛ' => 'ⓛ',
+ 'Ⓜ' => 'ⓜ',
+ 'Ⓝ' => 'ⓝ',
+ 'Ⓞ' => 'ⓞ',
+ 'Ⓟ' => 'ⓟ',
+ 'Ⓠ' => 'ⓠ',
+ 'Ⓡ' => 'ⓡ',
+ 'Ⓢ' => 'ⓢ',
+ 'Ⓣ' => 'ⓣ',
+ 'Ⓤ' => 'ⓤ',
+ 'Ⓥ' => 'ⓥ',
+ 'Ⓦ' => 'ⓦ',
+ 'Ⓧ' => 'ⓧ',
+ 'Ⓨ' => 'ⓨ',
+ 'Ⓩ' => 'ⓩ',
+ 'Ⰰ' => 'ⰰ',
+ 'Ⰱ' => 'ⰱ',
+ 'Ⰲ' => 'ⰲ',
+ 'Ⰳ' => 'ⰳ',
+ 'Ⰴ' => 'ⰴ',
+ 'Ⰵ' => 'ⰵ',
+ 'Ⰶ' => 'ⰶ',
+ 'Ⰷ' => 'ⰷ',
+ 'Ⰸ' => 'ⰸ',
+ 'Ⰹ' => 'ⰹ',
+ 'Ⰺ' => 'ⰺ',
+ 'Ⰻ' => 'ⰻ',
+ 'Ⰼ' => 'ⰼ',
+ 'Ⰽ' => 'ⰽ',
+ 'Ⰾ' => 'ⰾ',
+ 'Ⰿ' => 'ⰿ',
+ 'Ⱀ' => 'ⱀ',
+ 'Ⱁ' => 'ⱁ',
+ 'Ⱂ' => 'ⱂ',
+ 'Ⱃ' => 'ⱃ',
+ 'Ⱄ' => 'ⱄ',
+ 'Ⱅ' => 'ⱅ',
+ 'Ⱆ' => 'ⱆ',
+ 'Ⱇ' => 'ⱇ',
+ 'Ⱈ' => 'ⱈ',
+ 'Ⱉ' => 'ⱉ',
+ 'Ⱊ' => 'ⱊ',
+ 'Ⱋ' => 'ⱋ',
+ 'Ⱌ' => 'ⱌ',
+ 'Ⱍ' => 'ⱍ',
+ 'Ⱎ' => 'ⱎ',
+ 'Ⱏ' => 'ⱏ',
+ 'Ⱐ' => 'ⱐ',
+ 'Ⱑ' => 'ⱑ',
+ 'Ⱒ' => 'ⱒ',
+ 'Ⱓ' => 'ⱓ',
+ 'Ⱔ' => 'ⱔ',
+ 'Ⱕ' => 'ⱕ',
+ 'Ⱖ' => 'ⱖ',
+ 'Ⱗ' => 'ⱗ',
+ 'Ⱘ' => 'ⱘ',
+ 'Ⱙ' => 'ⱙ',
+ 'Ⱚ' => 'ⱚ',
+ 'Ⱛ' => 'ⱛ',
+ 'Ⱜ' => 'ⱜ',
+ 'Ⱝ' => 'ⱝ',
+ 'Ⱞ' => 'ⱞ',
+ 'Ⱡ' => 'ⱡ',
+ 'Ɫ' => 'ɫ',
+ 'Ᵽ' => 'ᵽ',
+ 'Ɽ' => 'ɽ',
+ 'Ⱨ' => 'ⱨ',
+ 'Ⱪ' => 'ⱪ',
+ 'Ⱬ' => 'ⱬ',
+ 'Ɑ' => 'ɑ',
+ 'Ɱ' => 'ɱ',
+ 'Ɐ' => 'ɐ',
+ 'Ɒ' => 'ɒ',
+ 'Ⱳ' => 'ⱳ',
+ 'Ⱶ' => 'ⱶ',
+ 'Ȿ' => 'ȿ',
+ 'Ɀ' => 'ɀ',
+ 'Ⲁ' => 'ⲁ',
+ 'Ⲃ' => 'ⲃ',
+ 'Ⲅ' => 'ⲅ',
+ 'Ⲇ' => 'ⲇ',
+ 'Ⲉ' => 'ⲉ',
+ 'Ⲋ' => 'ⲋ',
+ 'Ⲍ' => 'ⲍ',
+ 'Ⲏ' => 'ⲏ',
+ 'Ⲑ' => 'ⲑ',
+ 'Ⲓ' => 'ⲓ',
+ 'Ⲕ' => 'ⲕ',
+ 'Ⲗ' => 'ⲗ',
+ 'Ⲙ' => 'ⲙ',
+ 'Ⲛ' => 'ⲛ',
+ 'Ⲝ' => 'ⲝ',
+ 'Ⲟ' => 'ⲟ',
+ 'Ⲡ' => 'ⲡ',
+ 'Ⲣ' => 'ⲣ',
+ 'Ⲥ' => 'ⲥ',
+ 'Ⲧ' => 'ⲧ',
+ 'Ⲩ' => 'ⲩ',
+ 'Ⲫ' => 'ⲫ',
+ 'Ⲭ' => 'ⲭ',
+ 'Ⲯ' => 'ⲯ',
+ 'Ⲱ' => 'ⲱ',
+ 'Ⲳ' => 'ⲳ',
+ 'Ⲵ' => 'ⲵ',
+ 'Ⲷ' => 'ⲷ',
+ 'Ⲹ' => 'ⲹ',
+ 'Ⲻ' => 'ⲻ',
+ 'Ⲽ' => 'ⲽ',
+ 'Ⲿ' => 'ⲿ',
+ 'Ⳁ' => 'ⳁ',
+ 'Ⳃ' => 'ⳃ',
+ 'Ⳅ' => 'ⳅ',
+ 'Ⳇ' => 'ⳇ',
+ 'Ⳉ' => 'ⳉ',
+ 'Ⳋ' => 'ⳋ',
+ 'Ⳍ' => 'ⳍ',
+ 'Ⳏ' => 'ⳏ',
+ 'Ⳑ' => 'ⳑ',
+ 'Ⳓ' => 'ⳓ',
+ 'Ⳕ' => 'ⳕ',
+ 'Ⳗ' => 'ⳗ',
+ 'Ⳙ' => 'ⳙ',
+ 'Ⳛ' => 'ⳛ',
+ 'Ⳝ' => 'ⳝ',
+ 'Ⳟ' => 'ⳟ',
+ 'Ⳡ' => 'ⳡ',
+ 'Ⳣ' => 'ⳣ',
+ 'Ⳬ' => 'ⳬ',
+ 'Ⳮ' => 'ⳮ',
+ 'Ⳳ' => 'ⳳ',
+ 'Ꙁ' => 'ꙁ',
+ 'Ꙃ' => 'ꙃ',
+ 'Ꙅ' => 'ꙅ',
+ 'Ꙇ' => 'ꙇ',
+ 'Ꙉ' => 'ꙉ',
+ 'Ꙋ' => 'ꙋ',
+ 'Ꙍ' => 'ꙍ',
+ 'Ꙏ' => 'ꙏ',
+ 'Ꙑ' => 'ꙑ',
+ 'Ꙓ' => 'ꙓ',
+ 'Ꙕ' => 'ꙕ',
+ 'Ꙗ' => 'ꙗ',
+ 'Ꙙ' => 'ꙙ',
+ 'Ꙛ' => 'ꙛ',
+ 'Ꙝ' => 'ꙝ',
+ 'Ꙟ' => 'ꙟ',
+ 'Ꙡ' => 'ꙡ',
+ 'Ꙣ' => 'ꙣ',
+ 'Ꙥ' => 'ꙥ',
+ 'Ꙧ' => 'ꙧ',
+ 'Ꙩ' => 'ꙩ',
+ 'Ꙫ' => 'ꙫ',
+ 'Ꙭ' => 'ꙭ',
+ 'Ꚁ' => 'ꚁ',
+ 'Ꚃ' => 'ꚃ',
+ 'Ꚅ' => 'ꚅ',
+ 'Ꚇ' => 'ꚇ',
+ 'Ꚉ' => 'ꚉ',
+ 'Ꚋ' => 'ꚋ',
+ 'Ꚍ' => 'ꚍ',
+ 'Ꚏ' => 'ꚏ',
+ 'Ꚑ' => 'ꚑ',
+ 'Ꚓ' => 'ꚓ',
+ 'Ꚕ' => 'ꚕ',
+ 'Ꚗ' => 'ꚗ',
+ 'Ꚙ' => 'ꚙ',
+ 'Ꚛ' => 'ꚛ',
+ 'Ꜣ' => 'ꜣ',
+ 'Ꜥ' => 'ꜥ',
+ 'Ꜧ' => 'ꜧ',
+ 'Ꜩ' => 'ꜩ',
+ 'Ꜫ' => 'ꜫ',
+ 'Ꜭ' => 'ꜭ',
+ 'Ꜯ' => 'ꜯ',
+ 'Ꜳ' => 'ꜳ',
+ 'Ꜵ' => 'ꜵ',
+ 'Ꜷ' => 'ꜷ',
+ 'Ꜹ' => 'ꜹ',
+ 'Ꜻ' => 'ꜻ',
+ 'Ꜽ' => 'ꜽ',
+ 'Ꜿ' => 'ꜿ',
+ 'Ꝁ' => 'ꝁ',
+ 'Ꝃ' => 'ꝃ',
+ 'Ꝅ' => 'ꝅ',
+ 'Ꝇ' => 'ꝇ',
+ 'Ꝉ' => 'ꝉ',
+ 'Ꝋ' => 'ꝋ',
+ 'Ꝍ' => 'ꝍ',
+ 'Ꝏ' => 'ꝏ',
+ 'Ꝑ' => 'ꝑ',
+ 'Ꝓ' => 'ꝓ',
+ 'Ꝕ' => 'ꝕ',
+ 'Ꝗ' => 'ꝗ',
+ 'Ꝙ' => 'ꝙ',
+ 'Ꝛ' => 'ꝛ',
+ 'Ꝝ' => 'ꝝ',
+ 'Ꝟ' => 'ꝟ',
+ 'Ꝡ' => 'ꝡ',
+ 'Ꝣ' => 'ꝣ',
+ 'Ꝥ' => 'ꝥ',
+ 'Ꝧ' => 'ꝧ',
+ 'Ꝩ' => 'ꝩ',
+ 'Ꝫ' => 'ꝫ',
+ 'Ꝭ' => 'ꝭ',
+ 'Ꝯ' => 'ꝯ',
+ 'Ꝺ' => 'ꝺ',
+ 'Ꝼ' => 'ꝼ',
+ 'Ᵹ' => 'ᵹ',
+ 'Ꝿ' => 'ꝿ',
+ 'Ꞁ' => 'ꞁ',
+ 'Ꞃ' => 'ꞃ',
+ 'Ꞅ' => 'ꞅ',
+ 'Ꞇ' => 'ꞇ',
+ 'Ꞌ' => 'ꞌ',
+ 'Ɥ' => 'ɥ',
+ 'Ꞑ' => 'ꞑ',
+ 'Ꞓ' => 'ꞓ',
+ 'Ꞗ' => 'ꞗ',
+ 'Ꞙ' => 'ꞙ',
+ 'Ꞛ' => 'ꞛ',
+ 'Ꞝ' => 'ꞝ',
+ 'Ꞟ' => 'ꞟ',
+ 'Ꞡ' => 'ꞡ',
+ 'Ꞣ' => 'ꞣ',
+ 'Ꞥ' => 'ꞥ',
+ 'Ꞧ' => 'ꞧ',
+ 'Ꞩ' => 'ꞩ',
+ 'Ɦ' => 'ɦ',
+ 'Ɜ' => 'ɜ',
+ 'Ɡ' => 'ɡ',
+ 'Ɬ' => 'ɬ',
+ 'Ɪ' => 'ɪ',
+ 'Ʞ' => 'ʞ',
+ 'Ʇ' => 'ʇ',
+ 'Ʝ' => 'ʝ',
+ 'Ꭓ' => 'ꭓ',
+ 'Ꞵ' => 'ꞵ',
+ 'Ꞷ' => 'ꞷ',
+ 'Ꞹ' => 'ꞹ',
+ 'Ꞻ' => 'ꞻ',
+ 'Ꞽ' => 'ꞽ',
+ 'Ꞿ' => 'ꞿ',
+ 'Ꟃ' => 'ꟃ',
+ 'Ꞔ' => 'ꞔ',
+ 'Ʂ' => 'ʂ',
+ 'Ᶎ' => 'ᶎ',
+ 'Ꟈ' => 'ꟈ',
+ 'Ꟊ' => 'ꟊ',
+ 'Ꟶ' => 'ꟶ',
+ 'A' => 'a',
+ 'B' => 'b',
+ 'C' => 'c',
+ 'D' => 'd',
+ 'E' => 'e',
+ 'F' => 'f',
+ 'G' => 'g',
+ 'H' => 'h',
+ 'I' => 'i',
+ 'J' => 'j',
+ 'K' => 'k',
+ 'L' => 'l',
+ 'M' => 'm',
+ 'N' => 'n',
+ 'O' => 'o',
+ 'P' => 'p',
+ 'Q' => 'q',
+ 'R' => 'r',
+ 'S' => 's',
+ 'T' => 't',
+ 'U' => 'u',
+ 'V' => 'v',
+ 'W' => 'w',
+ 'X' => 'x',
+ 'Y' => 'y',
+ 'Z' => 'z',
+ '𐐀' => '𐐨',
+ '𐐁' => '𐐩',
+ '𐐂' => '𐐪',
+ '𐐃' => '𐐫',
+ '𐐄' => '𐐬',
+ '𐐅' => '𐐭',
+ '𐐆' => '𐐮',
+ '𐐇' => '𐐯',
+ '𐐈' => '𐐰',
+ '𐐉' => '𐐱',
+ '𐐊' => '𐐲',
+ '𐐋' => '𐐳',
+ '𐐌' => '𐐴',
+ '𐐍' => '𐐵',
+ '𐐎' => '𐐶',
+ '𐐏' => '𐐷',
+ '𐐐' => '𐐸',
+ '𐐑' => '𐐹',
+ '𐐒' => '𐐺',
+ '𐐓' => '𐐻',
+ '𐐔' => '𐐼',
+ '𐐕' => '𐐽',
+ '𐐖' => '𐐾',
+ '𐐗' => '𐐿',
+ '𐐘' => '𐑀',
+ '𐐙' => '𐑁',
+ '𐐚' => '𐑂',
+ '𐐛' => '𐑃',
+ '𐐜' => '𐑄',
+ '𐐝' => '𐑅',
+ '𐐞' => '𐑆',
+ '𐐟' => '𐑇',
+ '𐐠' => '𐑈',
+ '𐐡' => '𐑉',
+ '𐐢' => '𐑊',
+ '𐐣' => '𐑋',
+ '𐐤' => '𐑌',
+ '𐐥' => '𐑍',
+ '𐐦' => '𐑎',
+ '𐐧' => '𐑏',
+ '𐒰' => '𐓘',
+ '𐒱' => '𐓙',
+ '𐒲' => '𐓚',
+ '𐒳' => '𐓛',
+ '𐒴' => '𐓜',
+ '𐒵' => '𐓝',
+ '𐒶' => '𐓞',
+ '𐒷' => '𐓟',
+ '𐒸' => '𐓠',
+ '𐒹' => '𐓡',
+ '𐒺' => '𐓢',
+ '𐒻' => '𐓣',
+ '𐒼' => '𐓤',
+ '𐒽' => '𐓥',
+ '𐒾' => '𐓦',
+ '𐒿' => '𐓧',
+ '𐓀' => '𐓨',
+ '𐓁' => '𐓩',
+ '𐓂' => '𐓪',
+ '𐓃' => '𐓫',
+ '𐓄' => '𐓬',
+ '𐓅' => '𐓭',
+ '𐓆' => '𐓮',
+ '𐓇' => '𐓯',
+ '𐓈' => '𐓰',
+ '𐓉' => '𐓱',
+ '𐓊' => '𐓲',
+ '𐓋' => '𐓳',
+ '𐓌' => '𐓴',
+ '𐓍' => '𐓵',
+ '𐓎' => '𐓶',
+ '𐓏' => '𐓷',
+ '𐓐' => '𐓸',
+ '𐓑' => '𐓹',
+ '𐓒' => '𐓺',
+ '𐓓' => '𐓻',
+ '𐲀' => '𐳀',
+ '𐲁' => '𐳁',
+ '𐲂' => '𐳂',
+ '𐲃' => '𐳃',
+ '𐲄' => '𐳄',
+ '𐲅' => '𐳅',
+ '𐲆' => '𐳆',
+ '𐲇' => '𐳇',
+ '𐲈' => '𐳈',
+ '𐲉' => '𐳉',
+ '𐲊' => '𐳊',
+ '𐲋' => '𐳋',
+ '𐲌' => '𐳌',
+ '𐲍' => '𐳍',
+ '𐲎' => '𐳎',
+ '𐲏' => '𐳏',
+ '𐲐' => '𐳐',
+ '𐲑' => '𐳑',
+ '𐲒' => '𐳒',
+ '𐲓' => '𐳓',
+ '𐲔' => '𐳔',
+ '𐲕' => '𐳕',
+ '𐲖' => '𐳖',
+ '𐲗' => '𐳗',
+ '𐲘' => '𐳘',
+ '𐲙' => '𐳙',
+ '𐲚' => '𐳚',
+ '𐲛' => '𐳛',
+ '𐲜' => '𐳜',
+ '𐲝' => '𐳝',
+ '𐲞' => '𐳞',
+ '𐲟' => '𐳟',
+ '𐲠' => '𐳠',
+ '𐲡' => '𐳡',
+ '𐲢' => '𐳢',
+ '𐲣' => '𐳣',
+ '𐲤' => '𐳤',
+ '𐲥' => '𐳥',
+ '𐲦' => '𐳦',
+ '𐲧' => '𐳧',
+ '𐲨' => '𐳨',
+ '𐲩' => '𐳩',
+ '𐲪' => '𐳪',
+ '𐲫' => '𐳫',
+ '𐲬' => '𐳬',
+ '𐲭' => '𐳭',
+ '𐲮' => '𐳮',
+ '𐲯' => '𐳯',
+ '𐲰' => '𐳰',
+ '𐲱' => '𐳱',
+ '𐲲' => '𐳲',
+ '𑢠' => '𑣀',
+ '𑢡' => '𑣁',
+ '𑢢' => '𑣂',
+ '𑢣' => '𑣃',
+ '𑢤' => '𑣄',
+ '𑢥' => '𑣅',
+ '𑢦' => '𑣆',
+ '𑢧' => '𑣇',
+ '𑢨' => '𑣈',
+ '𑢩' => '𑣉',
+ '𑢪' => '𑣊',
+ '𑢫' => '𑣋',
+ '𑢬' => '𑣌',
+ '𑢭' => '𑣍',
+ '𑢮' => '𑣎',
+ '𑢯' => '𑣏',
+ '𑢰' => '𑣐',
+ '𑢱' => '𑣑',
+ '𑢲' => '𑣒',
+ '𑢳' => '𑣓',
+ '𑢴' => '𑣔',
+ '𑢵' => '𑣕',
+ '𑢶' => '𑣖',
+ '𑢷' => '𑣗',
+ '𑢸' => '𑣘',
+ '𑢹' => '𑣙',
+ '𑢺' => '𑣚',
+ '𑢻' => '𑣛',
+ '𑢼' => '𑣜',
+ '𑢽' => '𑣝',
+ '𑢾' => '𑣞',
+ '𑢿' => '𑣟',
+ '𖹀' => '𖹠',
+ '𖹁' => '𖹡',
+ '𖹂' => '𖹢',
+ '𖹃' => '𖹣',
+ '𖹄' => '𖹤',
+ '𖹅' => '𖹥',
+ '𖹆' => '𖹦',
+ '𖹇' => '𖹧',
+ '𖹈' => '𖹨',
+ '𖹉' => '𖹩',
+ '𖹊' => '𖹪',
+ '𖹋' => '𖹫',
+ '𖹌' => '𖹬',
+ '𖹍' => '𖹭',
+ '𖹎' => '𖹮',
+ '𖹏' => '𖹯',
+ '𖹐' => '𖹰',
+ '𖹑' => '𖹱',
+ '𖹒' => '𖹲',
+ '𖹓' => '𖹳',
+ '𖹔' => '𖹴',
+ '𖹕' => '𖹵',
+ '𖹖' => '𖹶',
+ '𖹗' => '𖹷',
+ '𖹘' => '𖹸',
+ '𖹙' => '𖹹',
+ '𖹚' => '𖹺',
+ '𖹛' => '𖹻',
+ '𖹜' => '𖹼',
+ '𖹝' => '𖹽',
+ '𖹞' => '𖹾',
+ '𖹟' => '𖹿',
+ '𞤀' => '𞤢',
+ '𞤁' => '𞤣',
+ '𞤂' => '𞤤',
+ '𞤃' => '𞤥',
+ '𞤄' => '𞤦',
+ '𞤅' => '𞤧',
+ '𞤆' => '𞤨',
+ '𞤇' => '𞤩',
+ '𞤈' => '𞤪',
+ '𞤉' => '𞤫',
+ '𞤊' => '𞤬',
+ '𞤋' => '𞤭',
+ '𞤌' => '𞤮',
+ '𞤍' => '𞤯',
+ '𞤎' => '𞤰',
+ '𞤏' => '𞤱',
+ '𞤐' => '𞤲',
+ '𞤑' => '𞤳',
+ '𞤒' => '𞤴',
+ '𞤓' => '𞤵',
+ '𞤔' => '𞤶',
+ '𞤕' => '𞤷',
+ '𞤖' => '𞤸',
+ '𞤗' => '𞤹',
+ '𞤘' => '𞤺',
+ '𞤙' => '𞤻',
+ '𞤚' => '𞤼',
+ '𞤛' => '𞤽',
+ '𞤜' => '𞤾',
+ '𞤝' => '𞤿',
+ '𞤞' => '𞥀',
+ '𞤟' => '𞥁',
+ '𞤠' => '𞥂',
+ '𞤡' => '𞥃',
+);
diff --git a/src/vendor/symfony/polyfill-mbstring/Resources/unidata/titleCaseRegexp.php b/src/vendor/symfony/polyfill-mbstring/Resources/unidata/titleCaseRegexp.php
new file mode 100644
index 0000000..2a8f6e7
--- /dev/null
+++ b/src/vendor/symfony/polyfill-mbstring/Resources/unidata/titleCaseRegexp.php
@@ -0,0 +1,5 @@
+ 'A',
+ 'b' => 'B',
+ 'c' => 'C',
+ 'd' => 'D',
+ 'e' => 'E',
+ 'f' => 'F',
+ 'g' => 'G',
+ 'h' => 'H',
+ 'i' => 'I',
+ 'j' => 'J',
+ 'k' => 'K',
+ 'l' => 'L',
+ 'm' => 'M',
+ 'n' => 'N',
+ 'o' => 'O',
+ 'p' => 'P',
+ 'q' => 'Q',
+ 'r' => 'R',
+ 's' => 'S',
+ 't' => 'T',
+ 'u' => 'U',
+ 'v' => 'V',
+ 'w' => 'W',
+ 'x' => 'X',
+ 'y' => 'Y',
+ 'z' => 'Z',
+ 'µ' => 'Μ',
+ 'à' => 'À',
+ 'á' => 'Á',
+ 'â' => 'Â',
+ 'ã' => 'Ã',
+ 'ä' => 'Ä',
+ 'å' => 'Å',
+ 'æ' => 'Æ',
+ 'ç' => 'Ç',
+ 'è' => 'È',
+ 'é' => 'É',
+ 'ê' => 'Ê',
+ 'ë' => 'Ë',
+ 'ì' => 'Ì',
+ 'í' => 'Í',
+ 'î' => 'Î',
+ 'ï' => 'Ï',
+ 'ð' => 'Ð',
+ 'ñ' => 'Ñ',
+ 'ò' => 'Ò',
+ 'ó' => 'Ó',
+ 'ô' => 'Ô',
+ 'õ' => 'Õ',
+ 'ö' => 'Ö',
+ 'ø' => 'Ø',
+ 'ù' => 'Ù',
+ 'ú' => 'Ú',
+ 'û' => 'Û',
+ 'ü' => 'Ü',
+ 'ý' => 'Ý',
+ 'þ' => 'Þ',
+ 'ÿ' => 'Ÿ',
+ 'ā' => 'Ā',
+ 'ă' => 'Ă',
+ 'ą' => 'Ą',
+ 'ć' => 'Ć',
+ 'ĉ' => 'Ĉ',
+ 'ċ' => 'Ċ',
+ 'č' => 'Č',
+ 'ď' => 'Ď',
+ 'đ' => 'Đ',
+ 'ē' => 'Ē',
+ 'ĕ' => 'Ĕ',
+ 'ė' => 'Ė',
+ 'ę' => 'Ę',
+ 'ě' => 'Ě',
+ 'ĝ' => 'Ĝ',
+ 'ğ' => 'Ğ',
+ 'ġ' => 'Ġ',
+ 'ģ' => 'Ģ',
+ 'ĥ' => 'Ĥ',
+ 'ħ' => 'Ħ',
+ 'ĩ' => 'Ĩ',
+ 'ī' => 'Ī',
+ 'ĭ' => 'Ĭ',
+ 'į' => 'Į',
+ 'ı' => 'I',
+ 'ij' => 'IJ',
+ 'ĵ' => 'Ĵ',
+ 'ķ' => 'Ķ',
+ 'ĺ' => 'Ĺ',
+ 'ļ' => 'Ļ',
+ 'ľ' => 'Ľ',
+ 'ŀ' => 'Ŀ',
+ 'ł' => 'Ł',
+ 'ń' => 'Ń',
+ 'ņ' => 'Ņ',
+ 'ň' => 'Ň',
+ 'ŋ' => 'Ŋ',
+ 'ō' => 'Ō',
+ 'ŏ' => 'Ŏ',
+ 'ő' => 'Ő',
+ 'œ' => 'Œ',
+ 'ŕ' => 'Ŕ',
+ 'ŗ' => 'Ŗ',
+ 'ř' => 'Ř',
+ 'ś' => 'Ś',
+ 'ŝ' => 'Ŝ',
+ 'ş' => 'Ş',
+ 'š' => 'Š',
+ 'ţ' => 'Ţ',
+ 'ť' => 'Ť',
+ 'ŧ' => 'Ŧ',
+ 'ũ' => 'Ũ',
+ 'ū' => 'Ū',
+ 'ŭ' => 'Ŭ',
+ 'ů' => 'Ů',
+ 'ű' => 'Ű',
+ 'ų' => 'Ų',
+ 'ŵ' => 'Ŵ',
+ 'ŷ' => 'Ŷ',
+ 'ź' => 'Ź',
+ 'ż' => 'Ż',
+ 'ž' => 'Ž',
+ 'ſ' => 'S',
+ 'ƀ' => 'Ƀ',
+ 'ƃ' => 'Ƃ',
+ 'ƅ' => 'Ƅ',
+ 'ƈ' => 'Ƈ',
+ 'ƌ' => 'Ƌ',
+ 'ƒ' => 'Ƒ',
+ 'ƕ' => 'Ƕ',
+ 'ƙ' => 'Ƙ',
+ 'ƚ' => 'Ƚ',
+ 'ƞ' => 'Ƞ',
+ 'ơ' => 'Ơ',
+ 'ƣ' => 'Ƣ',
+ 'ƥ' => 'Ƥ',
+ 'ƨ' => 'Ƨ',
+ 'ƭ' => 'Ƭ',
+ 'ư' => 'Ư',
+ 'ƴ' => 'Ƴ',
+ 'ƶ' => 'Ƶ',
+ 'ƹ' => 'Ƹ',
+ 'ƽ' => 'Ƽ',
+ 'ƿ' => 'Ƿ',
+ 'Dž' => 'DŽ',
+ 'dž' => 'DŽ',
+ 'Lj' => 'LJ',
+ 'lj' => 'LJ',
+ 'Nj' => 'NJ',
+ 'nj' => 'NJ',
+ 'ǎ' => 'Ǎ',
+ 'ǐ' => 'Ǐ',
+ 'ǒ' => 'Ǒ',
+ 'ǔ' => 'Ǔ',
+ 'ǖ' => 'Ǖ',
+ 'ǘ' => 'Ǘ',
+ 'ǚ' => 'Ǚ',
+ 'ǜ' => 'Ǜ',
+ 'ǝ' => 'Ǝ',
+ 'ǟ' => 'Ǟ',
+ 'ǡ' => 'Ǡ',
+ 'ǣ' => 'Ǣ',
+ 'ǥ' => 'Ǥ',
+ 'ǧ' => 'Ǧ',
+ 'ǩ' => 'Ǩ',
+ 'ǫ' => 'Ǫ',
+ 'ǭ' => 'Ǭ',
+ 'ǯ' => 'Ǯ',
+ 'Dz' => 'DZ',
+ 'dz' => 'DZ',
+ 'ǵ' => 'Ǵ',
+ 'ǹ' => 'Ǹ',
+ 'ǻ' => 'Ǻ',
+ 'ǽ' => 'Ǽ',
+ 'ǿ' => 'Ǿ',
+ 'ȁ' => 'Ȁ',
+ 'ȃ' => 'Ȃ',
+ 'ȅ' => 'Ȅ',
+ 'ȇ' => 'Ȇ',
+ 'ȉ' => 'Ȉ',
+ 'ȋ' => 'Ȋ',
+ 'ȍ' => 'Ȍ',
+ 'ȏ' => 'Ȏ',
+ 'ȑ' => 'Ȑ',
+ 'ȓ' => 'Ȓ',
+ 'ȕ' => 'Ȕ',
+ 'ȗ' => 'Ȗ',
+ 'ș' => 'Ș',
+ 'ț' => 'Ț',
+ 'ȝ' => 'Ȝ',
+ 'ȟ' => 'Ȟ',
+ 'ȣ' => 'Ȣ',
+ 'ȥ' => 'Ȥ',
+ 'ȧ' => 'Ȧ',
+ 'ȩ' => 'Ȩ',
+ 'ȫ' => 'Ȫ',
+ 'ȭ' => 'Ȭ',
+ 'ȯ' => 'Ȯ',
+ 'ȱ' => 'Ȱ',
+ 'ȳ' => 'Ȳ',
+ 'ȼ' => 'Ȼ',
+ 'ȿ' => 'Ȿ',
+ 'ɀ' => 'Ɀ',
+ 'ɂ' => 'Ɂ',
+ 'ɇ' => 'Ɇ',
+ 'ɉ' => 'Ɉ',
+ 'ɋ' => 'Ɋ',
+ 'ɍ' => 'Ɍ',
+ 'ɏ' => 'Ɏ',
+ 'ɐ' => 'Ɐ',
+ 'ɑ' => 'Ɑ',
+ 'ɒ' => 'Ɒ',
+ 'ɓ' => 'Ɓ',
+ 'ɔ' => 'Ɔ',
+ 'ɖ' => 'Ɖ',
+ 'ɗ' => 'Ɗ',
+ 'ə' => 'Ə',
+ 'ɛ' => 'Ɛ',
+ 'ɜ' => 'Ɜ',
+ 'ɠ' => 'Ɠ',
+ 'ɡ' => 'Ɡ',
+ 'ɣ' => 'Ɣ',
+ 'ɥ' => 'Ɥ',
+ 'ɦ' => 'Ɦ',
+ 'ɨ' => 'Ɨ',
+ 'ɩ' => 'Ɩ',
+ 'ɪ' => 'Ɪ',
+ 'ɫ' => 'Ɫ',
+ 'ɬ' => 'Ɬ',
+ 'ɯ' => 'Ɯ',
+ 'ɱ' => 'Ɱ',
+ 'ɲ' => 'Ɲ',
+ 'ɵ' => 'Ɵ',
+ 'ɽ' => 'Ɽ',
+ 'ʀ' => 'Ʀ',
+ 'ʂ' => 'Ʂ',
+ 'ʃ' => 'Ʃ',
+ 'ʇ' => 'Ʇ',
+ 'ʈ' => 'Ʈ',
+ 'ʉ' => 'Ʉ',
+ 'ʊ' => 'Ʊ',
+ 'ʋ' => 'Ʋ',
+ 'ʌ' => 'Ʌ',
+ 'ʒ' => 'Ʒ',
+ 'ʝ' => 'Ʝ',
+ 'ʞ' => 'Ʞ',
+ 'ͅ' => 'Ι',
+ 'ͱ' => 'Ͱ',
+ 'ͳ' => 'Ͳ',
+ 'ͷ' => 'Ͷ',
+ 'ͻ' => 'Ͻ',
+ 'ͼ' => 'Ͼ',
+ 'ͽ' => 'Ͽ',
+ 'ά' => 'Ά',
+ 'έ' => 'Έ',
+ 'ή' => 'Ή',
+ 'ί' => 'Ί',
+ 'α' => 'Α',
+ 'β' => 'Β',
+ 'γ' => 'Γ',
+ 'δ' => 'Δ',
+ 'ε' => 'Ε',
+ 'ζ' => 'Ζ',
+ 'η' => 'Η',
+ 'θ' => 'Θ',
+ 'ι' => 'Ι',
+ 'κ' => 'Κ',
+ 'λ' => 'Λ',
+ 'μ' => 'Μ',
+ 'ν' => 'Ν',
+ 'ξ' => 'Ξ',
+ 'ο' => 'Ο',
+ 'π' => 'Π',
+ 'ρ' => 'Ρ',
+ 'ς' => 'Σ',
+ 'σ' => 'Σ',
+ 'τ' => 'Τ',
+ 'υ' => 'Υ',
+ 'φ' => 'Φ',
+ 'χ' => 'Χ',
+ 'ψ' => 'Ψ',
+ 'ω' => 'Ω',
+ 'ϊ' => 'Ϊ',
+ 'ϋ' => 'Ϋ',
+ 'ό' => 'Ό',
+ 'ύ' => 'Ύ',
+ 'ώ' => 'Ώ',
+ 'ϐ' => 'Β',
+ 'ϑ' => 'Θ',
+ 'ϕ' => 'Φ',
+ 'ϖ' => 'Π',
+ 'ϗ' => 'Ϗ',
+ 'ϙ' => 'Ϙ',
+ 'ϛ' => 'Ϛ',
+ 'ϝ' => 'Ϝ',
+ 'ϟ' => 'Ϟ',
+ 'ϡ' => 'Ϡ',
+ 'ϣ' => 'Ϣ',
+ 'ϥ' => 'Ϥ',
+ 'ϧ' => 'Ϧ',
+ 'ϩ' => 'Ϩ',
+ 'ϫ' => 'Ϫ',
+ 'ϭ' => 'Ϭ',
+ 'ϯ' => 'Ϯ',
+ 'ϰ' => 'Κ',
+ 'ϱ' => 'Ρ',
+ 'ϲ' => 'Ϲ',
+ 'ϳ' => 'Ϳ',
+ 'ϵ' => 'Ε',
+ 'ϸ' => 'Ϸ',
+ 'ϻ' => 'Ϻ',
+ 'а' => 'А',
+ 'б' => 'Б',
+ 'в' => 'В',
+ 'г' => 'Г',
+ 'д' => 'Д',
+ 'е' => 'Е',
+ 'ж' => 'Ж',
+ 'з' => 'З',
+ 'и' => 'И',
+ 'й' => 'Й',
+ 'к' => 'К',
+ 'л' => 'Л',
+ 'м' => 'М',
+ 'н' => 'Н',
+ 'о' => 'О',
+ 'п' => 'П',
+ 'р' => 'Р',
+ 'с' => 'С',
+ 'т' => 'Т',
+ 'у' => 'У',
+ 'ф' => 'Ф',
+ 'х' => 'Х',
+ 'ц' => 'Ц',
+ 'ч' => 'Ч',
+ 'ш' => 'Ш',
+ 'щ' => 'Щ',
+ 'ъ' => 'Ъ',
+ 'ы' => 'Ы',
+ 'ь' => 'Ь',
+ 'э' => 'Э',
+ 'ю' => 'Ю',
+ 'я' => 'Я',
+ 'ѐ' => 'Ѐ',
+ 'ё' => 'Ё',
+ 'ђ' => 'Ђ',
+ 'ѓ' => 'Ѓ',
+ 'є' => 'Є',
+ 'ѕ' => 'Ѕ',
+ 'і' => 'І',
+ 'ї' => 'Ї',
+ 'ј' => 'Ј',
+ 'љ' => 'Љ',
+ 'њ' => 'Њ',
+ 'ћ' => 'Ћ',
+ 'ќ' => 'Ќ',
+ 'ѝ' => 'Ѝ',
+ 'ў' => 'Ў',
+ 'џ' => 'Џ',
+ 'ѡ' => 'Ѡ',
+ 'ѣ' => 'Ѣ',
+ 'ѥ' => 'Ѥ',
+ 'ѧ' => 'Ѧ',
+ 'ѩ' => 'Ѩ',
+ 'ѫ' => 'Ѫ',
+ 'ѭ' => 'Ѭ',
+ 'ѯ' => 'Ѯ',
+ 'ѱ' => 'Ѱ',
+ 'ѳ' => 'Ѳ',
+ 'ѵ' => 'Ѵ',
+ 'ѷ' => 'Ѷ',
+ 'ѹ' => 'Ѹ',
+ 'ѻ' => 'Ѻ',
+ 'ѽ' => 'Ѽ',
+ 'ѿ' => 'Ѿ',
+ 'ҁ' => 'Ҁ',
+ 'ҋ' => 'Ҋ',
+ 'ҍ' => 'Ҍ',
+ 'ҏ' => 'Ҏ',
+ 'ґ' => 'Ґ',
+ 'ғ' => 'Ғ',
+ 'ҕ' => 'Ҕ',
+ 'җ' => 'Җ',
+ 'ҙ' => 'Ҙ',
+ 'қ' => 'Қ',
+ 'ҝ' => 'Ҝ',
+ 'ҟ' => 'Ҟ',
+ 'ҡ' => 'Ҡ',
+ 'ң' => 'Ң',
+ 'ҥ' => 'Ҥ',
+ 'ҧ' => 'Ҧ',
+ 'ҩ' => 'Ҩ',
+ 'ҫ' => 'Ҫ',
+ 'ҭ' => 'Ҭ',
+ 'ү' => 'Ү',
+ 'ұ' => 'Ұ',
+ 'ҳ' => 'Ҳ',
+ 'ҵ' => 'Ҵ',
+ 'ҷ' => 'Ҷ',
+ 'ҹ' => 'Ҹ',
+ 'һ' => 'Һ',
+ 'ҽ' => 'Ҽ',
+ 'ҿ' => 'Ҿ',
+ 'ӂ' => 'Ӂ',
+ 'ӄ' => 'Ӄ',
+ 'ӆ' => 'Ӆ',
+ 'ӈ' => 'Ӈ',
+ 'ӊ' => 'Ӊ',
+ 'ӌ' => 'Ӌ',
+ 'ӎ' => 'Ӎ',
+ 'ӏ' => 'Ӏ',
+ 'ӑ' => 'Ӑ',
+ 'ӓ' => 'Ӓ',
+ 'ӕ' => 'Ӕ',
+ 'ӗ' => 'Ӗ',
+ 'ә' => 'Ә',
+ 'ӛ' => 'Ӛ',
+ 'ӝ' => 'Ӝ',
+ 'ӟ' => 'Ӟ',
+ 'ӡ' => 'Ӡ',
+ 'ӣ' => 'Ӣ',
+ 'ӥ' => 'Ӥ',
+ 'ӧ' => 'Ӧ',
+ 'ө' => 'Ө',
+ 'ӫ' => 'Ӫ',
+ 'ӭ' => 'Ӭ',
+ 'ӯ' => 'Ӯ',
+ 'ӱ' => 'Ӱ',
+ 'ӳ' => 'Ӳ',
+ 'ӵ' => 'Ӵ',
+ 'ӷ' => 'Ӷ',
+ 'ӹ' => 'Ӹ',
+ 'ӻ' => 'Ӻ',
+ 'ӽ' => 'Ӽ',
+ 'ӿ' => 'Ӿ',
+ 'ԁ' => 'Ԁ',
+ 'ԃ' => 'Ԃ',
+ 'ԅ' => 'Ԅ',
+ 'ԇ' => 'Ԇ',
+ 'ԉ' => 'Ԉ',
+ 'ԋ' => 'Ԋ',
+ 'ԍ' => 'Ԍ',
+ 'ԏ' => 'Ԏ',
+ 'ԑ' => 'Ԑ',
+ 'ԓ' => 'Ԓ',
+ 'ԕ' => 'Ԕ',
+ 'ԗ' => 'Ԗ',
+ 'ԙ' => 'Ԙ',
+ 'ԛ' => 'Ԛ',
+ 'ԝ' => 'Ԝ',
+ 'ԟ' => 'Ԟ',
+ 'ԡ' => 'Ԡ',
+ 'ԣ' => 'Ԣ',
+ 'ԥ' => 'Ԥ',
+ 'ԧ' => 'Ԧ',
+ 'ԩ' => 'Ԩ',
+ 'ԫ' => 'Ԫ',
+ 'ԭ' => 'Ԭ',
+ 'ԯ' => 'Ԯ',
+ 'ա' => 'Ա',
+ 'բ' => 'Բ',
+ 'գ' => 'Գ',
+ 'դ' => 'Դ',
+ 'ե' => 'Ե',
+ 'զ' => 'Զ',
+ 'է' => 'Է',
+ 'ը' => 'Ը',
+ 'թ' => 'Թ',
+ 'ժ' => 'Ժ',
+ 'ի' => 'Ի',
+ 'լ' => 'Լ',
+ 'խ' => 'Խ',
+ 'ծ' => 'Ծ',
+ 'կ' => 'Կ',
+ 'հ' => 'Հ',
+ 'ձ' => 'Ձ',
+ 'ղ' => 'Ղ',
+ 'ճ' => 'Ճ',
+ 'մ' => 'Մ',
+ 'յ' => 'Յ',
+ 'ն' => 'Ն',
+ 'շ' => 'Շ',
+ 'ո' => 'Ո',
+ 'չ' => 'Չ',
+ 'պ' => 'Պ',
+ 'ջ' => 'Ջ',
+ 'ռ' => 'Ռ',
+ 'ս' => 'Ս',
+ 'վ' => 'Վ',
+ 'տ' => 'Տ',
+ 'ր' => 'Ր',
+ 'ց' => 'Ց',
+ 'ւ' => 'Ւ',
+ 'փ' => 'Փ',
+ 'ք' => 'Ք',
+ 'օ' => 'Օ',
+ 'ֆ' => 'Ֆ',
+ 'ა' => 'Ა',
+ 'ბ' => 'Ბ',
+ 'გ' => 'Გ',
+ 'დ' => 'Დ',
+ 'ე' => 'Ე',
+ 'ვ' => 'Ვ',
+ 'ზ' => 'Ზ',
+ 'თ' => 'Თ',
+ 'ი' => 'Ი',
+ 'კ' => 'Კ',
+ 'ლ' => 'Ლ',
+ 'მ' => 'Მ',
+ 'ნ' => 'Ნ',
+ 'ო' => 'Ო',
+ 'პ' => 'Პ',
+ 'ჟ' => 'Ჟ',
+ 'რ' => 'Რ',
+ 'ს' => 'Ს',
+ 'ტ' => 'Ტ',
+ 'უ' => 'Უ',
+ 'ფ' => 'Ფ',
+ 'ქ' => 'Ქ',
+ 'ღ' => 'Ღ',
+ 'ყ' => 'Ყ',
+ 'შ' => 'Შ',
+ 'ჩ' => 'Ჩ',
+ 'ც' => 'Ც',
+ 'ძ' => 'Ძ',
+ 'წ' => 'Წ',
+ 'ჭ' => 'Ჭ',
+ 'ხ' => 'Ხ',
+ 'ჯ' => 'Ჯ',
+ 'ჰ' => 'Ჰ',
+ 'ჱ' => 'Ჱ',
+ 'ჲ' => 'Ჲ',
+ 'ჳ' => 'Ჳ',
+ 'ჴ' => 'Ჴ',
+ 'ჵ' => 'Ჵ',
+ 'ჶ' => 'Ჶ',
+ 'ჷ' => 'Ჷ',
+ 'ჸ' => 'Ჸ',
+ 'ჹ' => 'Ჹ',
+ 'ჺ' => 'Ჺ',
+ 'ჽ' => 'Ჽ',
+ 'ჾ' => 'Ჾ',
+ 'ჿ' => 'Ჿ',
+ 'ᏸ' => 'Ᏸ',
+ 'ᏹ' => 'Ᏹ',
+ 'ᏺ' => 'Ᏺ',
+ 'ᏻ' => 'Ᏻ',
+ 'ᏼ' => 'Ᏼ',
+ 'ᏽ' => 'Ᏽ',
+ 'ᲀ' => 'В',
+ 'ᲁ' => 'Д',
+ 'ᲂ' => 'О',
+ 'ᲃ' => 'С',
+ 'ᲄ' => 'Т',
+ 'ᲅ' => 'Т',
+ 'ᲆ' => 'Ъ',
+ 'ᲇ' => 'Ѣ',
+ 'ᲈ' => 'Ꙋ',
+ 'ᵹ' => 'Ᵹ',
+ 'ᵽ' => 'Ᵽ',
+ 'ᶎ' => 'Ᶎ',
+ 'ḁ' => 'Ḁ',
+ 'ḃ' => 'Ḃ',
+ 'ḅ' => 'Ḅ',
+ 'ḇ' => 'Ḇ',
+ 'ḉ' => 'Ḉ',
+ 'ḋ' => 'Ḋ',
+ 'ḍ' => 'Ḍ',
+ 'ḏ' => 'Ḏ',
+ 'ḑ' => 'Ḑ',
+ 'ḓ' => 'Ḓ',
+ 'ḕ' => 'Ḕ',
+ 'ḗ' => 'Ḗ',
+ 'ḙ' => 'Ḙ',
+ 'ḛ' => 'Ḛ',
+ 'ḝ' => 'Ḝ',
+ 'ḟ' => 'Ḟ',
+ 'ḡ' => 'Ḡ',
+ 'ḣ' => 'Ḣ',
+ 'ḥ' => 'Ḥ',
+ 'ḧ' => 'Ḧ',
+ 'ḩ' => 'Ḩ',
+ 'ḫ' => 'Ḫ',
+ 'ḭ' => 'Ḭ',
+ 'ḯ' => 'Ḯ',
+ 'ḱ' => 'Ḱ',
+ 'ḳ' => 'Ḳ',
+ 'ḵ' => 'Ḵ',
+ 'ḷ' => 'Ḷ',
+ 'ḹ' => 'Ḹ',
+ 'ḻ' => 'Ḻ',
+ 'ḽ' => 'Ḽ',
+ 'ḿ' => 'Ḿ',
+ 'ṁ' => 'Ṁ',
+ 'ṃ' => 'Ṃ',
+ 'ṅ' => 'Ṅ',
+ 'ṇ' => 'Ṇ',
+ 'ṉ' => 'Ṉ',
+ 'ṋ' => 'Ṋ',
+ 'ṍ' => 'Ṍ',
+ 'ṏ' => 'Ṏ',
+ 'ṑ' => 'Ṑ',
+ 'ṓ' => 'Ṓ',
+ 'ṕ' => 'Ṕ',
+ 'ṗ' => 'Ṗ',
+ 'ṙ' => 'Ṙ',
+ 'ṛ' => 'Ṛ',
+ 'ṝ' => 'Ṝ',
+ 'ṟ' => 'Ṟ',
+ 'ṡ' => 'Ṡ',
+ 'ṣ' => 'Ṣ',
+ 'ṥ' => 'Ṥ',
+ 'ṧ' => 'Ṧ',
+ 'ṩ' => 'Ṩ',
+ 'ṫ' => 'Ṫ',
+ 'ṭ' => 'Ṭ',
+ 'ṯ' => 'Ṯ',
+ 'ṱ' => 'Ṱ',
+ 'ṳ' => 'Ṳ',
+ 'ṵ' => 'Ṵ',
+ 'ṷ' => 'Ṷ',
+ 'ṹ' => 'Ṹ',
+ 'ṻ' => 'Ṻ',
+ 'ṽ' => 'Ṽ',
+ 'ṿ' => 'Ṿ',
+ 'ẁ' => 'Ẁ',
+ 'ẃ' => 'Ẃ',
+ 'ẅ' => 'Ẅ',
+ 'ẇ' => 'Ẇ',
+ 'ẉ' => 'Ẉ',
+ 'ẋ' => 'Ẋ',
+ 'ẍ' => 'Ẍ',
+ 'ẏ' => 'Ẏ',
+ 'ẑ' => 'Ẑ',
+ 'ẓ' => 'Ẓ',
+ 'ẕ' => 'Ẕ',
+ 'ẛ' => 'Ṡ',
+ 'ạ' => 'Ạ',
+ 'ả' => 'Ả',
+ 'ấ' => 'Ấ',
+ 'ầ' => 'Ầ',
+ 'ẩ' => 'Ẩ',
+ 'ẫ' => 'Ẫ',
+ 'ậ' => 'Ậ',
+ 'ắ' => 'Ắ',
+ 'ằ' => 'Ằ',
+ 'ẳ' => 'Ẳ',
+ 'ẵ' => 'Ẵ',
+ 'ặ' => 'Ặ',
+ 'ẹ' => 'Ẹ',
+ 'ẻ' => 'Ẻ',
+ 'ẽ' => 'Ẽ',
+ 'ế' => 'Ế',
+ 'ề' => 'Ề',
+ 'ể' => 'Ể',
+ 'ễ' => 'Ễ',
+ 'ệ' => 'Ệ',
+ 'ỉ' => 'Ỉ',
+ 'ị' => 'Ị',
+ 'ọ' => 'Ọ',
+ 'ỏ' => 'Ỏ',
+ 'ố' => 'Ố',
+ 'ồ' => 'Ồ',
+ 'ổ' => 'Ổ',
+ 'ỗ' => 'Ỗ',
+ 'ộ' => 'Ộ',
+ 'ớ' => 'Ớ',
+ 'ờ' => 'Ờ',
+ 'ở' => 'Ở',
+ 'ỡ' => 'Ỡ',
+ 'ợ' => 'Ợ',
+ 'ụ' => 'Ụ',
+ 'ủ' => 'Ủ',
+ 'ứ' => 'Ứ',
+ 'ừ' => 'Ừ',
+ 'ử' => 'Ử',
+ 'ữ' => 'Ữ',
+ 'ự' => 'Ự',
+ 'ỳ' => 'Ỳ',
+ 'ỵ' => 'Ỵ',
+ 'ỷ' => 'Ỷ',
+ 'ỹ' => 'Ỹ',
+ 'ỻ' => 'Ỻ',
+ 'ỽ' => 'Ỽ',
+ 'ỿ' => 'Ỿ',
+ 'ἀ' => 'Ἀ',
+ 'ἁ' => 'Ἁ',
+ 'ἂ' => 'Ἂ',
+ 'ἃ' => 'Ἃ',
+ 'ἄ' => 'Ἄ',
+ 'ἅ' => 'Ἅ',
+ 'ἆ' => 'Ἆ',
+ 'ἇ' => 'Ἇ',
+ 'ἐ' => 'Ἐ',
+ 'ἑ' => 'Ἑ',
+ 'ἒ' => 'Ἒ',
+ 'ἓ' => 'Ἓ',
+ 'ἔ' => 'Ἔ',
+ 'ἕ' => 'Ἕ',
+ 'ἠ' => 'Ἠ',
+ 'ἡ' => 'Ἡ',
+ 'ἢ' => 'Ἢ',
+ 'ἣ' => 'Ἣ',
+ 'ἤ' => 'Ἤ',
+ 'ἥ' => 'Ἥ',
+ 'ἦ' => 'Ἦ',
+ 'ἧ' => 'Ἧ',
+ 'ἰ' => 'Ἰ',
+ 'ἱ' => 'Ἱ',
+ 'ἲ' => 'Ἲ',
+ 'ἳ' => 'Ἳ',
+ 'ἴ' => 'Ἴ',
+ 'ἵ' => 'Ἵ',
+ 'ἶ' => 'Ἶ',
+ 'ἷ' => 'Ἷ',
+ 'ὀ' => 'Ὀ',
+ 'ὁ' => 'Ὁ',
+ 'ὂ' => 'Ὂ',
+ 'ὃ' => 'Ὃ',
+ 'ὄ' => 'Ὄ',
+ 'ὅ' => 'Ὅ',
+ 'ὑ' => 'Ὑ',
+ 'ὓ' => 'Ὓ',
+ 'ὕ' => 'Ὕ',
+ 'ὗ' => 'Ὗ',
+ 'ὠ' => 'Ὠ',
+ 'ὡ' => 'Ὡ',
+ 'ὢ' => 'Ὢ',
+ 'ὣ' => 'Ὣ',
+ 'ὤ' => 'Ὤ',
+ 'ὥ' => 'Ὥ',
+ 'ὦ' => 'Ὦ',
+ 'ὧ' => 'Ὧ',
+ 'ὰ' => 'Ὰ',
+ 'ά' => 'Ά',
+ 'ὲ' => 'Ὲ',
+ 'έ' => 'Έ',
+ 'ὴ' => 'Ὴ',
+ 'ή' => 'Ή',
+ 'ὶ' => 'Ὶ',
+ 'ί' => 'Ί',
+ 'ὸ' => 'Ὸ',
+ 'ό' => 'Ό',
+ 'ὺ' => 'Ὺ',
+ 'ύ' => 'Ύ',
+ 'ὼ' => 'Ὼ',
+ 'ώ' => 'Ώ',
+ 'ᾀ' => 'ἈΙ',
+ 'ᾁ' => 'ἉΙ',
+ 'ᾂ' => 'ἊΙ',
+ 'ᾃ' => 'ἋΙ',
+ 'ᾄ' => 'ἌΙ',
+ 'ᾅ' => 'ἍΙ',
+ 'ᾆ' => 'ἎΙ',
+ 'ᾇ' => 'ἏΙ',
+ 'ᾐ' => 'ἨΙ',
+ 'ᾑ' => 'ἩΙ',
+ 'ᾒ' => 'ἪΙ',
+ 'ᾓ' => 'ἫΙ',
+ 'ᾔ' => 'ἬΙ',
+ 'ᾕ' => 'ἭΙ',
+ 'ᾖ' => 'ἮΙ',
+ 'ᾗ' => 'ἯΙ',
+ 'ᾠ' => 'ὨΙ',
+ 'ᾡ' => 'ὩΙ',
+ 'ᾢ' => 'ὪΙ',
+ 'ᾣ' => 'ὫΙ',
+ 'ᾤ' => 'ὬΙ',
+ 'ᾥ' => 'ὭΙ',
+ 'ᾦ' => 'ὮΙ',
+ 'ᾧ' => 'ὯΙ',
+ 'ᾰ' => 'Ᾰ',
+ 'ᾱ' => 'Ᾱ',
+ 'ᾳ' => 'ΑΙ',
+ 'ι' => 'Ι',
+ 'ῃ' => 'ΗΙ',
+ 'ῐ' => 'Ῐ',
+ 'ῑ' => 'Ῑ',
+ 'ῠ' => 'Ῠ',
+ 'ῡ' => 'Ῡ',
+ 'ῥ' => 'Ῥ',
+ 'ῳ' => 'ΩΙ',
+ 'ⅎ' => 'Ⅎ',
+ 'ⅰ' => 'Ⅰ',
+ 'ⅱ' => 'Ⅱ',
+ 'ⅲ' => 'Ⅲ',
+ 'ⅳ' => 'Ⅳ',
+ 'ⅴ' => 'Ⅴ',
+ 'ⅵ' => 'Ⅵ',
+ 'ⅶ' => 'Ⅶ',
+ 'ⅷ' => 'Ⅷ',
+ 'ⅸ' => 'Ⅸ',
+ 'ⅹ' => 'Ⅹ',
+ 'ⅺ' => 'Ⅺ',
+ 'ⅻ' => 'Ⅻ',
+ 'ⅼ' => 'Ⅼ',
+ 'ⅽ' => 'Ⅽ',
+ 'ⅾ' => 'Ⅾ',
+ 'ⅿ' => 'Ⅿ',
+ 'ↄ' => 'Ↄ',
+ 'ⓐ' => 'Ⓐ',
+ 'ⓑ' => 'Ⓑ',
+ 'ⓒ' => 'Ⓒ',
+ 'ⓓ' => 'Ⓓ',
+ 'ⓔ' => 'Ⓔ',
+ 'ⓕ' => 'Ⓕ',
+ 'ⓖ' => 'Ⓖ',
+ 'ⓗ' => 'Ⓗ',
+ 'ⓘ' => 'Ⓘ',
+ 'ⓙ' => 'Ⓙ',
+ 'ⓚ' => 'Ⓚ',
+ 'ⓛ' => 'Ⓛ',
+ 'ⓜ' => 'Ⓜ',
+ 'ⓝ' => 'Ⓝ',
+ 'ⓞ' => 'Ⓞ',
+ 'ⓟ' => 'Ⓟ',
+ 'ⓠ' => 'Ⓠ',
+ 'ⓡ' => 'Ⓡ',
+ 'ⓢ' => 'Ⓢ',
+ 'ⓣ' => 'Ⓣ',
+ 'ⓤ' => 'Ⓤ',
+ 'ⓥ' => 'Ⓥ',
+ 'ⓦ' => 'Ⓦ',
+ 'ⓧ' => 'Ⓧ',
+ 'ⓨ' => 'Ⓨ',
+ 'ⓩ' => 'Ⓩ',
+ 'ⰰ' => 'Ⰰ',
+ 'ⰱ' => 'Ⰱ',
+ 'ⰲ' => 'Ⰲ',
+ 'ⰳ' => 'Ⰳ',
+ 'ⰴ' => 'Ⰴ',
+ 'ⰵ' => 'Ⰵ',
+ 'ⰶ' => 'Ⰶ',
+ 'ⰷ' => 'Ⰷ',
+ 'ⰸ' => 'Ⰸ',
+ 'ⰹ' => 'Ⰹ',
+ 'ⰺ' => 'Ⰺ',
+ 'ⰻ' => 'Ⰻ',
+ 'ⰼ' => 'Ⰼ',
+ 'ⰽ' => 'Ⰽ',
+ 'ⰾ' => 'Ⰾ',
+ 'ⰿ' => 'Ⰿ',
+ 'ⱀ' => 'Ⱀ',
+ 'ⱁ' => 'Ⱁ',
+ 'ⱂ' => 'Ⱂ',
+ 'ⱃ' => 'Ⱃ',
+ 'ⱄ' => 'Ⱄ',
+ 'ⱅ' => 'Ⱅ',
+ 'ⱆ' => 'Ⱆ',
+ 'ⱇ' => 'Ⱇ',
+ 'ⱈ' => 'Ⱈ',
+ 'ⱉ' => 'Ⱉ',
+ 'ⱊ' => 'Ⱊ',
+ 'ⱋ' => 'Ⱋ',
+ 'ⱌ' => 'Ⱌ',
+ 'ⱍ' => 'Ⱍ',
+ 'ⱎ' => 'Ⱎ',
+ 'ⱏ' => 'Ⱏ',
+ 'ⱐ' => 'Ⱐ',
+ 'ⱑ' => 'Ⱑ',
+ 'ⱒ' => 'Ⱒ',
+ 'ⱓ' => 'Ⱓ',
+ 'ⱔ' => 'Ⱔ',
+ 'ⱕ' => 'Ⱕ',
+ 'ⱖ' => 'Ⱖ',
+ 'ⱗ' => 'Ⱗ',
+ 'ⱘ' => 'Ⱘ',
+ 'ⱙ' => 'Ⱙ',
+ 'ⱚ' => 'Ⱚ',
+ 'ⱛ' => 'Ⱛ',
+ 'ⱜ' => 'Ⱜ',
+ 'ⱝ' => 'Ⱝ',
+ 'ⱞ' => 'Ⱞ',
+ 'ⱡ' => 'Ⱡ',
+ 'ⱥ' => 'Ⱥ',
+ 'ⱦ' => 'Ⱦ',
+ 'ⱨ' => 'Ⱨ',
+ 'ⱪ' => 'Ⱪ',
+ 'ⱬ' => 'Ⱬ',
+ 'ⱳ' => 'Ⱳ',
+ 'ⱶ' => 'Ⱶ',
+ 'ⲁ' => 'Ⲁ',
+ 'ⲃ' => 'Ⲃ',
+ 'ⲅ' => 'Ⲅ',
+ 'ⲇ' => 'Ⲇ',
+ 'ⲉ' => 'Ⲉ',
+ 'ⲋ' => 'Ⲋ',
+ 'ⲍ' => 'Ⲍ',
+ 'ⲏ' => 'Ⲏ',
+ 'ⲑ' => 'Ⲑ',
+ 'ⲓ' => 'Ⲓ',
+ 'ⲕ' => 'Ⲕ',
+ 'ⲗ' => 'Ⲗ',
+ 'ⲙ' => 'Ⲙ',
+ 'ⲛ' => 'Ⲛ',
+ 'ⲝ' => 'Ⲝ',
+ 'ⲟ' => 'Ⲟ',
+ 'ⲡ' => 'Ⲡ',
+ 'ⲣ' => 'Ⲣ',
+ 'ⲥ' => 'Ⲥ',
+ 'ⲧ' => 'Ⲧ',
+ 'ⲩ' => 'Ⲩ',
+ 'ⲫ' => 'Ⲫ',
+ 'ⲭ' => 'Ⲭ',
+ 'ⲯ' => 'Ⲯ',
+ 'ⲱ' => 'Ⲱ',
+ 'ⲳ' => 'Ⲳ',
+ 'ⲵ' => 'Ⲵ',
+ 'ⲷ' => 'Ⲷ',
+ 'ⲹ' => 'Ⲹ',
+ 'ⲻ' => 'Ⲻ',
+ 'ⲽ' => 'Ⲽ',
+ 'ⲿ' => 'Ⲿ',
+ 'ⳁ' => 'Ⳁ',
+ 'ⳃ' => 'Ⳃ',
+ 'ⳅ' => 'Ⳅ',
+ 'ⳇ' => 'Ⳇ',
+ 'ⳉ' => 'Ⳉ',
+ 'ⳋ' => 'Ⳋ',
+ 'ⳍ' => 'Ⳍ',
+ 'ⳏ' => 'Ⳏ',
+ 'ⳑ' => 'Ⳑ',
+ 'ⳓ' => 'Ⳓ',
+ 'ⳕ' => 'Ⳕ',
+ 'ⳗ' => 'Ⳗ',
+ 'ⳙ' => 'Ⳙ',
+ 'ⳛ' => 'Ⳛ',
+ 'ⳝ' => 'Ⳝ',
+ 'ⳟ' => 'Ⳟ',
+ 'ⳡ' => 'Ⳡ',
+ 'ⳣ' => 'Ⳣ',
+ 'ⳬ' => 'Ⳬ',
+ 'ⳮ' => 'Ⳮ',
+ 'ⳳ' => 'Ⳳ',
+ 'ⴀ' => 'Ⴀ',
+ 'ⴁ' => 'Ⴁ',
+ 'ⴂ' => 'Ⴂ',
+ 'ⴃ' => 'Ⴃ',
+ 'ⴄ' => 'Ⴄ',
+ 'ⴅ' => 'Ⴅ',
+ 'ⴆ' => 'Ⴆ',
+ 'ⴇ' => 'Ⴇ',
+ 'ⴈ' => 'Ⴈ',
+ 'ⴉ' => 'Ⴉ',
+ 'ⴊ' => 'Ⴊ',
+ 'ⴋ' => 'Ⴋ',
+ 'ⴌ' => 'Ⴌ',
+ 'ⴍ' => 'Ⴍ',
+ 'ⴎ' => 'Ⴎ',
+ 'ⴏ' => 'Ⴏ',
+ 'ⴐ' => 'Ⴐ',
+ 'ⴑ' => 'Ⴑ',
+ 'ⴒ' => 'Ⴒ',
+ 'ⴓ' => 'Ⴓ',
+ 'ⴔ' => 'Ⴔ',
+ 'ⴕ' => 'Ⴕ',
+ 'ⴖ' => 'Ⴖ',
+ 'ⴗ' => 'Ⴗ',
+ 'ⴘ' => 'Ⴘ',
+ 'ⴙ' => 'Ⴙ',
+ 'ⴚ' => 'Ⴚ',
+ 'ⴛ' => 'Ⴛ',
+ 'ⴜ' => 'Ⴜ',
+ 'ⴝ' => 'Ⴝ',
+ 'ⴞ' => 'Ⴞ',
+ 'ⴟ' => 'Ⴟ',
+ 'ⴠ' => 'Ⴠ',
+ 'ⴡ' => 'Ⴡ',
+ 'ⴢ' => 'Ⴢ',
+ 'ⴣ' => 'Ⴣ',
+ 'ⴤ' => 'Ⴤ',
+ 'ⴥ' => 'Ⴥ',
+ 'ⴧ' => 'Ⴧ',
+ 'ⴭ' => 'Ⴭ',
+ 'ꙁ' => 'Ꙁ',
+ 'ꙃ' => 'Ꙃ',
+ 'ꙅ' => 'Ꙅ',
+ 'ꙇ' => 'Ꙇ',
+ 'ꙉ' => 'Ꙉ',
+ 'ꙋ' => 'Ꙋ',
+ 'ꙍ' => 'Ꙍ',
+ 'ꙏ' => 'Ꙏ',
+ 'ꙑ' => 'Ꙑ',
+ 'ꙓ' => 'Ꙓ',
+ 'ꙕ' => 'Ꙕ',
+ 'ꙗ' => 'Ꙗ',
+ 'ꙙ' => 'Ꙙ',
+ 'ꙛ' => 'Ꙛ',
+ 'ꙝ' => 'Ꙝ',
+ 'ꙟ' => 'Ꙟ',
+ 'ꙡ' => 'Ꙡ',
+ 'ꙣ' => 'Ꙣ',
+ 'ꙥ' => 'Ꙥ',
+ 'ꙧ' => 'Ꙧ',
+ 'ꙩ' => 'Ꙩ',
+ 'ꙫ' => 'Ꙫ',
+ 'ꙭ' => 'Ꙭ',
+ 'ꚁ' => 'Ꚁ',
+ 'ꚃ' => 'Ꚃ',
+ 'ꚅ' => 'Ꚅ',
+ 'ꚇ' => 'Ꚇ',
+ 'ꚉ' => 'Ꚉ',
+ 'ꚋ' => 'Ꚋ',
+ 'ꚍ' => 'Ꚍ',
+ 'ꚏ' => 'Ꚏ',
+ 'ꚑ' => 'Ꚑ',
+ 'ꚓ' => 'Ꚓ',
+ 'ꚕ' => 'Ꚕ',
+ 'ꚗ' => 'Ꚗ',
+ 'ꚙ' => 'Ꚙ',
+ 'ꚛ' => 'Ꚛ',
+ 'ꜣ' => 'Ꜣ',
+ 'ꜥ' => 'Ꜥ',
+ 'ꜧ' => 'Ꜧ',
+ 'ꜩ' => 'Ꜩ',
+ 'ꜫ' => 'Ꜫ',
+ 'ꜭ' => 'Ꜭ',
+ 'ꜯ' => 'Ꜯ',
+ 'ꜳ' => 'Ꜳ',
+ 'ꜵ' => 'Ꜵ',
+ 'ꜷ' => 'Ꜷ',
+ 'ꜹ' => 'Ꜹ',
+ 'ꜻ' => 'Ꜻ',
+ 'ꜽ' => 'Ꜽ',
+ 'ꜿ' => 'Ꜿ',
+ 'ꝁ' => 'Ꝁ',
+ 'ꝃ' => 'Ꝃ',
+ 'ꝅ' => 'Ꝅ',
+ 'ꝇ' => 'Ꝇ',
+ 'ꝉ' => 'Ꝉ',
+ 'ꝋ' => 'Ꝋ',
+ 'ꝍ' => 'Ꝍ',
+ 'ꝏ' => 'Ꝏ',
+ 'ꝑ' => 'Ꝑ',
+ 'ꝓ' => 'Ꝓ',
+ 'ꝕ' => 'Ꝕ',
+ 'ꝗ' => 'Ꝗ',
+ 'ꝙ' => 'Ꝙ',
+ 'ꝛ' => 'Ꝛ',
+ 'ꝝ' => 'Ꝝ',
+ 'ꝟ' => 'Ꝟ',
+ 'ꝡ' => 'Ꝡ',
+ 'ꝣ' => 'Ꝣ',
+ 'ꝥ' => 'Ꝥ',
+ 'ꝧ' => 'Ꝧ',
+ 'ꝩ' => 'Ꝩ',
+ 'ꝫ' => 'Ꝫ',
+ 'ꝭ' => 'Ꝭ',
+ 'ꝯ' => 'Ꝯ',
+ 'ꝺ' => 'Ꝺ',
+ 'ꝼ' => 'Ꝼ',
+ 'ꝿ' => 'Ꝿ',
+ 'ꞁ' => 'Ꞁ',
+ 'ꞃ' => 'Ꞃ',
+ 'ꞅ' => 'Ꞅ',
+ 'ꞇ' => 'Ꞇ',
+ 'ꞌ' => 'Ꞌ',
+ 'ꞑ' => 'Ꞑ',
+ 'ꞓ' => 'Ꞓ',
+ 'ꞔ' => 'Ꞔ',
+ 'ꞗ' => 'Ꞗ',
+ 'ꞙ' => 'Ꞙ',
+ 'ꞛ' => 'Ꞛ',
+ 'ꞝ' => 'Ꞝ',
+ 'ꞟ' => 'Ꞟ',
+ 'ꞡ' => 'Ꞡ',
+ 'ꞣ' => 'Ꞣ',
+ 'ꞥ' => 'Ꞥ',
+ 'ꞧ' => 'Ꞧ',
+ 'ꞩ' => 'Ꞩ',
+ 'ꞵ' => 'Ꞵ',
+ 'ꞷ' => 'Ꞷ',
+ 'ꞹ' => 'Ꞹ',
+ 'ꞻ' => 'Ꞻ',
+ 'ꞽ' => 'Ꞽ',
+ 'ꞿ' => 'Ꞿ',
+ 'ꟃ' => 'Ꟃ',
+ 'ꟈ' => 'Ꟈ',
+ 'ꟊ' => 'Ꟊ',
+ 'ꟶ' => 'Ꟶ',
+ 'ꭓ' => 'Ꭓ',
+ 'ꭰ' => 'Ꭰ',
+ 'ꭱ' => 'Ꭱ',
+ 'ꭲ' => 'Ꭲ',
+ 'ꭳ' => 'Ꭳ',
+ 'ꭴ' => 'Ꭴ',
+ 'ꭵ' => 'Ꭵ',
+ 'ꭶ' => 'Ꭶ',
+ 'ꭷ' => 'Ꭷ',
+ 'ꭸ' => 'Ꭸ',
+ 'ꭹ' => 'Ꭹ',
+ 'ꭺ' => 'Ꭺ',
+ 'ꭻ' => 'Ꭻ',
+ 'ꭼ' => 'Ꭼ',
+ 'ꭽ' => 'Ꭽ',
+ 'ꭾ' => 'Ꭾ',
+ 'ꭿ' => 'Ꭿ',
+ 'ꮀ' => 'Ꮀ',
+ 'ꮁ' => 'Ꮁ',
+ 'ꮂ' => 'Ꮂ',
+ 'ꮃ' => 'Ꮃ',
+ 'ꮄ' => 'Ꮄ',
+ 'ꮅ' => 'Ꮅ',
+ 'ꮆ' => 'Ꮆ',
+ 'ꮇ' => 'Ꮇ',
+ 'ꮈ' => 'Ꮈ',
+ 'ꮉ' => 'Ꮉ',
+ 'ꮊ' => 'Ꮊ',
+ 'ꮋ' => 'Ꮋ',
+ 'ꮌ' => 'Ꮌ',
+ 'ꮍ' => 'Ꮍ',
+ 'ꮎ' => 'Ꮎ',
+ 'ꮏ' => 'Ꮏ',
+ 'ꮐ' => 'Ꮐ',
+ 'ꮑ' => 'Ꮑ',
+ 'ꮒ' => 'Ꮒ',
+ 'ꮓ' => 'Ꮓ',
+ 'ꮔ' => 'Ꮔ',
+ 'ꮕ' => 'Ꮕ',
+ 'ꮖ' => 'Ꮖ',
+ 'ꮗ' => 'Ꮗ',
+ 'ꮘ' => 'Ꮘ',
+ 'ꮙ' => 'Ꮙ',
+ 'ꮚ' => 'Ꮚ',
+ 'ꮛ' => 'Ꮛ',
+ 'ꮜ' => 'Ꮜ',
+ 'ꮝ' => 'Ꮝ',
+ 'ꮞ' => 'Ꮞ',
+ 'ꮟ' => 'Ꮟ',
+ 'ꮠ' => 'Ꮠ',
+ 'ꮡ' => 'Ꮡ',
+ 'ꮢ' => 'Ꮢ',
+ 'ꮣ' => 'Ꮣ',
+ 'ꮤ' => 'Ꮤ',
+ 'ꮥ' => 'Ꮥ',
+ 'ꮦ' => 'Ꮦ',
+ 'ꮧ' => 'Ꮧ',
+ 'ꮨ' => 'Ꮨ',
+ 'ꮩ' => 'Ꮩ',
+ 'ꮪ' => 'Ꮪ',
+ 'ꮫ' => 'Ꮫ',
+ 'ꮬ' => 'Ꮬ',
+ 'ꮭ' => 'Ꮭ',
+ 'ꮮ' => 'Ꮮ',
+ 'ꮯ' => 'Ꮯ',
+ 'ꮰ' => 'Ꮰ',
+ 'ꮱ' => 'Ꮱ',
+ 'ꮲ' => 'Ꮲ',
+ 'ꮳ' => 'Ꮳ',
+ 'ꮴ' => 'Ꮴ',
+ 'ꮵ' => 'Ꮵ',
+ 'ꮶ' => 'Ꮶ',
+ 'ꮷ' => 'Ꮷ',
+ 'ꮸ' => 'Ꮸ',
+ 'ꮹ' => 'Ꮹ',
+ 'ꮺ' => 'Ꮺ',
+ 'ꮻ' => 'Ꮻ',
+ 'ꮼ' => 'Ꮼ',
+ 'ꮽ' => 'Ꮽ',
+ 'ꮾ' => 'Ꮾ',
+ 'ꮿ' => 'Ꮿ',
+ 'a' => 'A',
+ 'b' => 'B',
+ 'c' => 'C',
+ 'd' => 'D',
+ 'e' => 'E',
+ 'f' => 'F',
+ 'g' => 'G',
+ 'h' => 'H',
+ 'i' => 'I',
+ 'j' => 'J',
+ 'k' => 'K',
+ 'l' => 'L',
+ 'm' => 'M',
+ 'n' => 'N',
+ 'o' => 'O',
+ 'p' => 'P',
+ 'q' => 'Q',
+ 'r' => 'R',
+ 's' => 'S',
+ 't' => 'T',
+ 'u' => 'U',
+ 'v' => 'V',
+ 'w' => 'W',
+ 'x' => 'X',
+ 'y' => 'Y',
+ 'z' => 'Z',
+ '𐐨' => '𐐀',
+ '𐐩' => '𐐁',
+ '𐐪' => '𐐂',
+ '𐐫' => '𐐃',
+ '𐐬' => '𐐄',
+ '𐐭' => '𐐅',
+ '𐐮' => '𐐆',
+ '𐐯' => '𐐇',
+ '𐐰' => '𐐈',
+ '𐐱' => '𐐉',
+ '𐐲' => '𐐊',
+ '𐐳' => '𐐋',
+ '𐐴' => '𐐌',
+ '𐐵' => '𐐍',
+ '𐐶' => '𐐎',
+ '𐐷' => '𐐏',
+ '𐐸' => '𐐐',
+ '𐐹' => '𐐑',
+ '𐐺' => '𐐒',
+ '𐐻' => '𐐓',
+ '𐐼' => '𐐔',
+ '𐐽' => '𐐕',
+ '𐐾' => '𐐖',
+ '𐐿' => '𐐗',
+ '𐑀' => '𐐘',
+ '𐑁' => '𐐙',
+ '𐑂' => '𐐚',
+ '𐑃' => '𐐛',
+ '𐑄' => '𐐜',
+ '𐑅' => '𐐝',
+ '𐑆' => '𐐞',
+ '𐑇' => '𐐟',
+ '𐑈' => '𐐠',
+ '𐑉' => '𐐡',
+ '𐑊' => '𐐢',
+ '𐑋' => '𐐣',
+ '𐑌' => '𐐤',
+ '𐑍' => '𐐥',
+ '𐑎' => '𐐦',
+ '𐑏' => '𐐧',
+ '𐓘' => '𐒰',
+ '𐓙' => '𐒱',
+ '𐓚' => '𐒲',
+ '𐓛' => '𐒳',
+ '𐓜' => '𐒴',
+ '𐓝' => '𐒵',
+ '𐓞' => '𐒶',
+ '𐓟' => '𐒷',
+ '𐓠' => '𐒸',
+ '𐓡' => '𐒹',
+ '𐓢' => '𐒺',
+ '𐓣' => '𐒻',
+ '𐓤' => '𐒼',
+ '𐓥' => '𐒽',
+ '𐓦' => '𐒾',
+ '𐓧' => '𐒿',
+ '𐓨' => '𐓀',
+ '𐓩' => '𐓁',
+ '𐓪' => '𐓂',
+ '𐓫' => '𐓃',
+ '𐓬' => '𐓄',
+ '𐓭' => '𐓅',
+ '𐓮' => '𐓆',
+ '𐓯' => '𐓇',
+ '𐓰' => '𐓈',
+ '𐓱' => '𐓉',
+ '𐓲' => '𐓊',
+ '𐓳' => '𐓋',
+ '𐓴' => '𐓌',
+ '𐓵' => '𐓍',
+ '𐓶' => '𐓎',
+ '𐓷' => '𐓏',
+ '𐓸' => '𐓐',
+ '𐓹' => '𐓑',
+ '𐓺' => '𐓒',
+ '𐓻' => '𐓓',
+ '𐳀' => '𐲀',
+ '𐳁' => '𐲁',
+ '𐳂' => '𐲂',
+ '𐳃' => '𐲃',
+ '𐳄' => '𐲄',
+ '𐳅' => '𐲅',
+ '𐳆' => '𐲆',
+ '𐳇' => '𐲇',
+ '𐳈' => '𐲈',
+ '𐳉' => '𐲉',
+ '𐳊' => '𐲊',
+ '𐳋' => '𐲋',
+ '𐳌' => '𐲌',
+ '𐳍' => '𐲍',
+ '𐳎' => '𐲎',
+ '𐳏' => '𐲏',
+ '𐳐' => '𐲐',
+ '𐳑' => '𐲑',
+ '𐳒' => '𐲒',
+ '𐳓' => '𐲓',
+ '𐳔' => '𐲔',
+ '𐳕' => '𐲕',
+ '𐳖' => '𐲖',
+ '𐳗' => '𐲗',
+ '𐳘' => '𐲘',
+ '𐳙' => '𐲙',
+ '𐳚' => '𐲚',
+ '𐳛' => '𐲛',
+ '𐳜' => '𐲜',
+ '𐳝' => '𐲝',
+ '𐳞' => '𐲞',
+ '𐳟' => '𐲟',
+ '𐳠' => '𐲠',
+ '𐳡' => '𐲡',
+ '𐳢' => '𐲢',
+ '𐳣' => '𐲣',
+ '𐳤' => '𐲤',
+ '𐳥' => '𐲥',
+ '𐳦' => '𐲦',
+ '𐳧' => '𐲧',
+ '𐳨' => '𐲨',
+ '𐳩' => '𐲩',
+ '𐳪' => '𐲪',
+ '𐳫' => '𐲫',
+ '𐳬' => '𐲬',
+ '𐳭' => '𐲭',
+ '𐳮' => '𐲮',
+ '𐳯' => '𐲯',
+ '𐳰' => '𐲰',
+ '𐳱' => '𐲱',
+ '𐳲' => '𐲲',
+ '𑣀' => '𑢠',
+ '𑣁' => '𑢡',
+ '𑣂' => '𑢢',
+ '𑣃' => '𑢣',
+ '𑣄' => '𑢤',
+ '𑣅' => '𑢥',
+ '𑣆' => '𑢦',
+ '𑣇' => '𑢧',
+ '𑣈' => '𑢨',
+ '𑣉' => '𑢩',
+ '𑣊' => '𑢪',
+ '𑣋' => '𑢫',
+ '𑣌' => '𑢬',
+ '𑣍' => '𑢭',
+ '𑣎' => '𑢮',
+ '𑣏' => '𑢯',
+ '𑣐' => '𑢰',
+ '𑣑' => '𑢱',
+ '𑣒' => '𑢲',
+ '𑣓' => '𑢳',
+ '𑣔' => '𑢴',
+ '𑣕' => '𑢵',
+ '𑣖' => '𑢶',
+ '𑣗' => '𑢷',
+ '𑣘' => '𑢸',
+ '𑣙' => '𑢹',
+ '𑣚' => '𑢺',
+ '𑣛' => '𑢻',
+ '𑣜' => '𑢼',
+ '𑣝' => '𑢽',
+ '𑣞' => '𑢾',
+ '𑣟' => '𑢿',
+ '𖹠' => '𖹀',
+ '𖹡' => '𖹁',
+ '𖹢' => '𖹂',
+ '𖹣' => '𖹃',
+ '𖹤' => '𖹄',
+ '𖹥' => '𖹅',
+ '𖹦' => '𖹆',
+ '𖹧' => '𖹇',
+ '𖹨' => '𖹈',
+ '𖹩' => '𖹉',
+ '𖹪' => '𖹊',
+ '𖹫' => '𖹋',
+ '𖹬' => '𖹌',
+ '𖹭' => '𖹍',
+ '𖹮' => '𖹎',
+ '𖹯' => '𖹏',
+ '𖹰' => '𖹐',
+ '𖹱' => '𖹑',
+ '𖹲' => '𖹒',
+ '𖹳' => '𖹓',
+ '𖹴' => '𖹔',
+ '𖹵' => '𖹕',
+ '𖹶' => '𖹖',
+ '𖹷' => '𖹗',
+ '𖹸' => '𖹘',
+ '𖹹' => '𖹙',
+ '𖹺' => '𖹚',
+ '𖹻' => '𖹛',
+ '𖹼' => '𖹜',
+ '𖹽' => '𖹝',
+ '𖹾' => '𖹞',
+ '𖹿' => '𖹟',
+ '𞤢' => '𞤀',
+ '𞤣' => '𞤁',
+ '𞤤' => '𞤂',
+ '𞤥' => '𞤃',
+ '𞤦' => '𞤄',
+ '𞤧' => '𞤅',
+ '𞤨' => '𞤆',
+ '𞤩' => '𞤇',
+ '𞤪' => '𞤈',
+ '𞤫' => '𞤉',
+ '𞤬' => '𞤊',
+ '𞤭' => '𞤋',
+ '𞤮' => '𞤌',
+ '𞤯' => '𞤍',
+ '𞤰' => '𞤎',
+ '𞤱' => '𞤏',
+ '𞤲' => '𞤐',
+ '𞤳' => '𞤑',
+ '𞤴' => '𞤒',
+ '𞤵' => '𞤓',
+ '𞤶' => '𞤔',
+ '𞤷' => '𞤕',
+ '𞤸' => '𞤖',
+ '𞤹' => '𞤗',
+ '𞤺' => '𞤘',
+ '𞤻' => '𞤙',
+ '𞤼' => '𞤚',
+ '𞤽' => '𞤛',
+ '𞤾' => '𞤜',
+ '𞤿' => '𞤝',
+ '𞥀' => '𞤞',
+ '𞥁' => '𞤟',
+ '𞥂' => '𞤠',
+ '𞥃' => '𞤡',
+ 'ß' => 'SS',
+ 'ff' => 'FF',
+ 'fi' => 'FI',
+ 'fl' => 'FL',
+ 'ffi' => 'FFI',
+ 'ffl' => 'FFL',
+ 'ſt' => 'ST',
+ 'st' => 'ST',
+ 'և' => 'ԵՒ',
+ 'ﬓ' => 'ՄՆ',
+ 'ﬔ' => 'ՄԵ',
+ 'ﬕ' => 'ՄԻ',
+ 'ﬖ' => 'ՎՆ',
+ 'ﬗ' => 'ՄԽ',
+ 'ʼn' => 'ʼN',
+ 'ΐ' => 'Ϊ́',
+ 'ΰ' => 'Ϋ́',
+ 'ǰ' => 'J̌',
+ 'ẖ' => 'H̱',
+ 'ẗ' => 'T̈',
+ 'ẘ' => 'W̊',
+ 'ẙ' => 'Y̊',
+ 'ẚ' => 'Aʾ',
+ 'ὐ' => 'Υ̓',
+ 'ὒ' => 'Υ̓̀',
+ 'ὔ' => 'Υ̓́',
+ 'ὖ' => 'Υ̓͂',
+ 'ᾶ' => 'Α͂',
+ 'ῆ' => 'Η͂',
+ 'ῒ' => 'Ϊ̀',
+ 'ΐ' => 'Ϊ́',
+ 'ῖ' => 'Ι͂',
+ 'ῗ' => 'Ϊ͂',
+ 'ῢ' => 'Ϋ̀',
+ 'ΰ' => 'Ϋ́',
+ 'ῤ' => 'Ρ̓',
+ 'ῦ' => 'Υ͂',
+ 'ῧ' => 'Ϋ͂',
+ 'ῶ' => 'Ω͂',
+ 'ᾈ' => 'ἈΙ',
+ 'ᾉ' => 'ἉΙ',
+ 'ᾊ' => 'ἊΙ',
+ 'ᾋ' => 'ἋΙ',
+ 'ᾌ' => 'ἌΙ',
+ 'ᾍ' => 'ἍΙ',
+ 'ᾎ' => 'ἎΙ',
+ 'ᾏ' => 'ἏΙ',
+ 'ᾘ' => 'ἨΙ',
+ 'ᾙ' => 'ἩΙ',
+ 'ᾚ' => 'ἪΙ',
+ 'ᾛ' => 'ἫΙ',
+ 'ᾜ' => 'ἬΙ',
+ 'ᾝ' => 'ἭΙ',
+ 'ᾞ' => 'ἮΙ',
+ 'ᾟ' => 'ἯΙ',
+ 'ᾨ' => 'ὨΙ',
+ 'ᾩ' => 'ὩΙ',
+ 'ᾪ' => 'ὪΙ',
+ 'ᾫ' => 'ὫΙ',
+ 'ᾬ' => 'ὬΙ',
+ 'ᾭ' => 'ὭΙ',
+ 'ᾮ' => 'ὮΙ',
+ 'ᾯ' => 'ὯΙ',
+ 'ᾼ' => 'ΑΙ',
+ 'ῌ' => 'ΗΙ',
+ 'ῼ' => 'ΩΙ',
+ 'ᾲ' => 'ᾺΙ',
+ 'ᾴ' => 'ΆΙ',
+ 'ῂ' => 'ῊΙ',
+ 'ῄ' => 'ΉΙ',
+ 'ῲ' => 'ῺΙ',
+ 'ῴ' => 'ΏΙ',
+ 'ᾷ' => 'Α͂Ι',
+ 'ῇ' => 'Η͂Ι',
+ 'ῷ' => 'Ω͂Ι',
+);
diff --git a/src/vendor/symfony/polyfill-mbstring/bootstrap.php b/src/vendor/symfony/polyfill-mbstring/bootstrap.php
new file mode 100644
index 0000000..ecf1a03
--- /dev/null
+++ b/src/vendor/symfony/polyfill-mbstring/bootstrap.php
@@ -0,0 +1,151 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+use Symfony\Polyfill\Mbstring as p;
+
+if (\PHP_VERSION_ID >= 80000) {
+ return require __DIR__.'/bootstrap80.php';
+}
+
+if (!function_exists('mb_convert_encoding')) {
+ function mb_convert_encoding($string, $to_encoding, $from_encoding = null) { return p\Mbstring::mb_convert_encoding($string, $to_encoding, $from_encoding); }
+}
+if (!function_exists('mb_decode_mimeheader')) {
+ function mb_decode_mimeheader($string) { return p\Mbstring::mb_decode_mimeheader($string); }
+}
+if (!function_exists('mb_encode_mimeheader')) {
+ function mb_encode_mimeheader($string, $charset = null, $transfer_encoding = null, $newline = "\r\n", $indent = 0) { return p\Mbstring::mb_encode_mimeheader($string, $charset, $transfer_encoding, $newline, $indent); }
+}
+if (!function_exists('mb_decode_numericentity')) {
+ function mb_decode_numericentity($string, $map, $encoding = null) { return p\Mbstring::mb_decode_numericentity($string, $map, $encoding); }
+}
+if (!function_exists('mb_encode_numericentity')) {
+ function mb_encode_numericentity($string, $map, $encoding = null, $hex = false) { return p\Mbstring::mb_encode_numericentity($string, $map, $encoding, $hex); }
+}
+if (!function_exists('mb_convert_case')) {
+ function mb_convert_case($string, $mode, $encoding = null) { return p\Mbstring::mb_convert_case($string, $mode, $encoding); }
+}
+if (!function_exists('mb_internal_encoding')) {
+ function mb_internal_encoding($encoding = null) { return p\Mbstring::mb_internal_encoding($encoding); }
+}
+if (!function_exists('mb_language')) {
+ function mb_language($language = null) { return p\Mbstring::mb_language($language); }
+}
+if (!function_exists('mb_list_encodings')) {
+ function mb_list_encodings() { return p\Mbstring::mb_list_encodings(); }
+}
+if (!function_exists('mb_encoding_aliases')) {
+ function mb_encoding_aliases($encoding) { return p\Mbstring::mb_encoding_aliases($encoding); }
+}
+if (!function_exists('mb_check_encoding')) {
+ function mb_check_encoding($value = null, $encoding = null) { return p\Mbstring::mb_check_encoding($value, $encoding); }
+}
+if (!function_exists('mb_detect_encoding')) {
+ function mb_detect_encoding($string, $encodings = null, $strict = false) { return p\Mbstring::mb_detect_encoding($string, $encodings, $strict); }
+}
+if (!function_exists('mb_detect_order')) {
+ function mb_detect_order($encoding = null) { return p\Mbstring::mb_detect_order($encoding); }
+}
+if (!function_exists('mb_parse_str')) {
+ function mb_parse_str($string, &$result = []) { parse_str($string, $result); return (bool) $result; }
+}
+if (!function_exists('mb_strlen')) {
+ function mb_strlen($string, $encoding = null) { return p\Mbstring::mb_strlen($string, $encoding); }
+}
+if (!function_exists('mb_strpos')) {
+ function mb_strpos($haystack, $needle, $offset = 0, $encoding = null) { return p\Mbstring::mb_strpos($haystack, $needle, $offset, $encoding); }
+}
+if (!function_exists('mb_strtolower')) {
+ function mb_strtolower($string, $encoding = null) { return p\Mbstring::mb_strtolower($string, $encoding); }
+}
+if (!function_exists('mb_strtoupper')) {
+ function mb_strtoupper($string, $encoding = null) { return p\Mbstring::mb_strtoupper($string, $encoding); }
+}
+if (!function_exists('mb_substitute_character')) {
+ function mb_substitute_character($substitute_character = null) { return p\Mbstring::mb_substitute_character($substitute_character); }
+}
+if (!function_exists('mb_substr')) {
+ function mb_substr($string, $start, $length = 2147483647, $encoding = null) { return p\Mbstring::mb_substr($string, $start, $length, $encoding); }
+}
+if (!function_exists('mb_stripos')) {
+ function mb_stripos($haystack, $needle, $offset = 0, $encoding = null) { return p\Mbstring::mb_stripos($haystack, $needle, $offset, $encoding); }
+}
+if (!function_exists('mb_stristr')) {
+ function mb_stristr($haystack, $needle, $before_needle = false, $encoding = null) { return p\Mbstring::mb_stristr($haystack, $needle, $before_needle, $encoding); }
+}
+if (!function_exists('mb_strrchr')) {
+ function mb_strrchr($haystack, $needle, $before_needle = false, $encoding = null) { return p\Mbstring::mb_strrchr($haystack, $needle, $before_needle, $encoding); }
+}
+if (!function_exists('mb_strrichr')) {
+ function mb_strrichr($haystack, $needle, $before_needle = false, $encoding = null) { return p\Mbstring::mb_strrichr($haystack, $needle, $before_needle, $encoding); }
+}
+if (!function_exists('mb_strripos')) {
+ function mb_strripos($haystack, $needle, $offset = 0, $encoding = null) { return p\Mbstring::mb_strripos($haystack, $needle, $offset, $encoding); }
+}
+if (!function_exists('mb_strrpos')) {
+ function mb_strrpos($haystack, $needle, $offset = 0, $encoding = null) { return p\Mbstring::mb_strrpos($haystack, $needle, $offset, $encoding); }
+}
+if (!function_exists('mb_strstr')) {
+ function mb_strstr($haystack, $needle, $before_needle = false, $encoding = null) { return p\Mbstring::mb_strstr($haystack, $needle, $before_needle, $encoding); }
+}
+if (!function_exists('mb_get_info')) {
+ function mb_get_info($type = 'all') { return p\Mbstring::mb_get_info($type); }
+}
+if (!function_exists('mb_http_output')) {
+ function mb_http_output($encoding = null) { return p\Mbstring::mb_http_output($encoding); }
+}
+if (!function_exists('mb_strwidth')) {
+ function mb_strwidth($string, $encoding = null) { return p\Mbstring::mb_strwidth($string, $encoding); }
+}
+if (!function_exists('mb_substr_count')) {
+ function mb_substr_count($haystack, $needle, $encoding = null) { return p\Mbstring::mb_substr_count($haystack, $needle, $encoding); }
+}
+if (!function_exists('mb_output_handler')) {
+ function mb_output_handler($string, $status) { return p\Mbstring::mb_output_handler($string, $status); }
+}
+if (!function_exists('mb_http_input')) {
+ function mb_http_input($type = null) { return p\Mbstring::mb_http_input($type); }
+}
+
+if (!function_exists('mb_convert_variables')) {
+ function mb_convert_variables($to_encoding, $from_encoding, &...$vars) { return p\Mbstring::mb_convert_variables($to_encoding, $from_encoding, ...$vars); }
+}
+
+if (!function_exists('mb_ord')) {
+ function mb_ord($string, $encoding = null) { return p\Mbstring::mb_ord($string, $encoding); }
+}
+if (!function_exists('mb_chr')) {
+ function mb_chr($codepoint, $encoding = null) { return p\Mbstring::mb_chr($codepoint, $encoding); }
+}
+if (!function_exists('mb_scrub')) {
+ function mb_scrub($string, $encoding = null) { $encoding = null === $encoding ? mb_internal_encoding() : $encoding; return mb_convert_encoding($string, $encoding, $encoding); }
+}
+if (!function_exists('mb_str_split')) {
+ function mb_str_split($string, $length = 1, $encoding = null) { return p\Mbstring::mb_str_split($string, $length, $encoding); }
+}
+
+if (!function_exists('mb_str_pad')) {
+ function mb_str_pad(string $string, int $length, string $pad_string = ' ', int $pad_type = STR_PAD_RIGHT, ?string $encoding = null): string { return p\Mbstring::mb_str_pad($string, $length, $pad_string, $pad_type, $encoding); }
+}
+
+if (extension_loaded('mbstring')) {
+ return;
+}
+
+if (!defined('MB_CASE_UPPER')) {
+ define('MB_CASE_UPPER', 0);
+}
+if (!defined('MB_CASE_LOWER')) {
+ define('MB_CASE_LOWER', 1);
+}
+if (!defined('MB_CASE_TITLE')) {
+ define('MB_CASE_TITLE', 2);
+}
diff --git a/src/vendor/symfony/polyfill-mbstring/bootstrap80.php b/src/vendor/symfony/polyfill-mbstring/bootstrap80.php
new file mode 100644
index 0000000..2f9fb5b
--- /dev/null
+++ b/src/vendor/symfony/polyfill-mbstring/bootstrap80.php
@@ -0,0 +1,147 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+use Symfony\Polyfill\Mbstring as p;
+
+if (!function_exists('mb_convert_encoding')) {
+ function mb_convert_encoding(array|string|null $string, ?string $to_encoding, array|string|null $from_encoding = null): array|string|false { return p\Mbstring::mb_convert_encoding($string ?? '', (string) $to_encoding, $from_encoding); }
+}
+if (!function_exists('mb_decode_mimeheader')) {
+ function mb_decode_mimeheader(?string $string): string { return p\Mbstring::mb_decode_mimeheader((string) $string); }
+}
+if (!function_exists('mb_encode_mimeheader')) {
+ function mb_encode_mimeheader(?string $string, ?string $charset = null, ?string $transfer_encoding = null, ?string $newline = "\r\n", ?int $indent = 0): string { return p\Mbstring::mb_encode_mimeheader((string) $string, $charset, $transfer_encoding, (string) $newline, (int) $indent); }
+}
+if (!function_exists('mb_decode_numericentity')) {
+ function mb_decode_numericentity(?string $string, array $map, ?string $encoding = null): string { return p\Mbstring::mb_decode_numericentity((string) $string, $map, $encoding); }
+}
+if (!function_exists('mb_encode_numericentity')) {
+ function mb_encode_numericentity(?string $string, array $map, ?string $encoding = null, ?bool $hex = false): string { return p\Mbstring::mb_encode_numericentity((string) $string, $map, $encoding, (bool) $hex); }
+}
+if (!function_exists('mb_convert_case')) {
+ function mb_convert_case(?string $string, ?int $mode, ?string $encoding = null): string { return p\Mbstring::mb_convert_case((string) $string, (int) $mode, $encoding); }
+}
+if (!function_exists('mb_internal_encoding')) {
+ function mb_internal_encoding(?string $encoding = null): string|bool { return p\Mbstring::mb_internal_encoding($encoding); }
+}
+if (!function_exists('mb_language')) {
+ function mb_language(?string $language = null): string|bool { return p\Mbstring::mb_language($language); }
+}
+if (!function_exists('mb_list_encodings')) {
+ function mb_list_encodings(): array { return p\Mbstring::mb_list_encodings(); }
+}
+if (!function_exists('mb_encoding_aliases')) {
+ function mb_encoding_aliases(?string $encoding): array { return p\Mbstring::mb_encoding_aliases((string) $encoding); }
+}
+if (!function_exists('mb_check_encoding')) {
+ function mb_check_encoding(array|string|null $value = null, ?string $encoding = null): bool { return p\Mbstring::mb_check_encoding($value, $encoding); }
+}
+if (!function_exists('mb_detect_encoding')) {
+ function mb_detect_encoding(?string $string, array|string|null $encodings = null, ?bool $strict = false): string|false { return p\Mbstring::mb_detect_encoding((string) $string, $encodings, (bool) $strict); }
+}
+if (!function_exists('mb_detect_order')) {
+ function mb_detect_order(array|string|null $encoding = null): array|bool { return p\Mbstring::mb_detect_order($encoding); }
+}
+if (!function_exists('mb_parse_str')) {
+ function mb_parse_str(?string $string, &$result = []): bool { parse_str((string) $string, $result); return (bool) $result; }
+}
+if (!function_exists('mb_strlen')) {
+ function mb_strlen(?string $string, ?string $encoding = null): int { return p\Mbstring::mb_strlen((string) $string, $encoding); }
+}
+if (!function_exists('mb_strpos')) {
+ function mb_strpos(?string $haystack, ?string $needle, ?int $offset = 0, ?string $encoding = null): int|false { return p\Mbstring::mb_strpos((string) $haystack, (string) $needle, (int) $offset, $encoding); }
+}
+if (!function_exists('mb_strtolower')) {
+ function mb_strtolower(?string $string, ?string $encoding = null): string { return p\Mbstring::mb_strtolower((string) $string, $encoding); }
+}
+if (!function_exists('mb_strtoupper')) {
+ function mb_strtoupper(?string $string, ?string $encoding = null): string { return p\Mbstring::mb_strtoupper((string) $string, $encoding); }
+}
+if (!function_exists('mb_substitute_character')) {
+ function mb_substitute_character(string|int|null $substitute_character = null): string|int|bool { return p\Mbstring::mb_substitute_character($substitute_character); }
+}
+if (!function_exists('mb_substr')) {
+ function mb_substr(?string $string, ?int $start, ?int $length = null, ?string $encoding = null): string { return p\Mbstring::mb_substr((string) $string, (int) $start, $length, $encoding); }
+}
+if (!function_exists('mb_stripos')) {
+ function mb_stripos(?string $haystack, ?string $needle, ?int $offset = 0, ?string $encoding = null): int|false { return p\Mbstring::mb_stripos((string) $haystack, (string) $needle, (int) $offset, $encoding); }
+}
+if (!function_exists('mb_stristr')) {
+ function mb_stristr(?string $haystack, ?string $needle, ?bool $before_needle = false, ?string $encoding = null): string|false { return p\Mbstring::mb_stristr((string) $haystack, (string) $needle, (bool) $before_needle, $encoding); }
+}
+if (!function_exists('mb_strrchr')) {
+ function mb_strrchr(?string $haystack, ?string $needle, ?bool $before_needle = false, ?string $encoding = null): string|false { return p\Mbstring::mb_strrchr((string) $haystack, (string) $needle, (bool) $before_needle, $encoding); }
+}
+if (!function_exists('mb_strrichr')) {
+ function mb_strrichr(?string $haystack, ?string $needle, ?bool $before_needle = false, ?string $encoding = null): string|false { return p\Mbstring::mb_strrichr((string) $haystack, (string) $needle, (bool) $before_needle, $encoding); }
+}
+if (!function_exists('mb_strripos')) {
+ function mb_strripos(?string $haystack, ?string $needle, ?int $offset = 0, ?string $encoding = null): int|false { return p\Mbstring::mb_strripos((string) $haystack, (string) $needle, (int) $offset, $encoding); }
+}
+if (!function_exists('mb_strrpos')) {
+ function mb_strrpos(?string $haystack, ?string $needle, ?int $offset = 0, ?string $encoding = null): int|false { return p\Mbstring::mb_strrpos((string) $haystack, (string) $needle, (int) $offset, $encoding); }
+}
+if (!function_exists('mb_strstr')) {
+ function mb_strstr(?string $haystack, ?string $needle, ?bool $before_needle = false, ?string $encoding = null): string|false { return p\Mbstring::mb_strstr((string) $haystack, (string) $needle, (bool) $before_needle, $encoding); }
+}
+if (!function_exists('mb_get_info')) {
+ function mb_get_info(?string $type = 'all'): array|string|int|false { return p\Mbstring::mb_get_info((string) $type); }
+}
+if (!function_exists('mb_http_output')) {
+ function mb_http_output(?string $encoding = null): string|bool { return p\Mbstring::mb_http_output($encoding); }
+}
+if (!function_exists('mb_strwidth')) {
+ function mb_strwidth(?string $string, ?string $encoding = null): int { return p\Mbstring::mb_strwidth((string) $string, $encoding); }
+}
+if (!function_exists('mb_substr_count')) {
+ function mb_substr_count(?string $haystack, ?string $needle, ?string $encoding = null): int { return p\Mbstring::mb_substr_count((string) $haystack, (string) $needle, $encoding); }
+}
+if (!function_exists('mb_output_handler')) {
+ function mb_output_handler(?string $string, ?int $status): string { return p\Mbstring::mb_output_handler((string) $string, (int) $status); }
+}
+if (!function_exists('mb_http_input')) {
+ function mb_http_input(?string $type = null): array|string|false { return p\Mbstring::mb_http_input($type); }
+}
+
+if (!function_exists('mb_convert_variables')) {
+ function mb_convert_variables(?string $to_encoding, array|string|null $from_encoding, mixed &$var, mixed &...$vars): string|false { return p\Mbstring::mb_convert_variables((string) $to_encoding, $from_encoding ?? '', $var, ...$vars); }
+}
+
+if (!function_exists('mb_ord')) {
+ function mb_ord(?string $string, ?string $encoding = null): int|false { return p\Mbstring::mb_ord((string) $string, $encoding); }
+}
+if (!function_exists('mb_chr')) {
+ function mb_chr(?int $codepoint, ?string $encoding = null): string|false { return p\Mbstring::mb_chr((int) $codepoint, $encoding); }
+}
+if (!function_exists('mb_scrub')) {
+ function mb_scrub(?string $string, ?string $encoding = null): string { $encoding ??= mb_internal_encoding(); return mb_convert_encoding((string) $string, $encoding, $encoding); }
+}
+if (!function_exists('mb_str_split')) {
+ function mb_str_split(?string $string, ?int $length = 1, ?string $encoding = null): array { return p\Mbstring::mb_str_split((string) $string, (int) $length, $encoding); }
+}
+
+if (!function_exists('mb_str_pad')) {
+ function mb_str_pad(string $string, int $length, string $pad_string = ' ', int $pad_type = STR_PAD_RIGHT, ?string $encoding = null): string { return p\Mbstring::mb_str_pad($string, $length, $pad_string, $pad_type, $encoding); }
+}
+
+if (extension_loaded('mbstring')) {
+ return;
+}
+
+if (!defined('MB_CASE_UPPER')) {
+ define('MB_CASE_UPPER', 0);
+}
+if (!defined('MB_CASE_LOWER')) {
+ define('MB_CASE_LOWER', 1);
+}
+if (!defined('MB_CASE_TITLE')) {
+ define('MB_CASE_TITLE', 2);
+}
diff --git a/src/vendor/symfony/polyfill-mbstring/composer.json b/src/vendor/symfony/polyfill-mbstring/composer.json
new file mode 100644
index 0000000..943e502
--- /dev/null
+++ b/src/vendor/symfony/polyfill-mbstring/composer.json
@@ -0,0 +1,41 @@
+{
+ "name": "symfony/polyfill-mbstring",
+ "type": "library",
+ "description": "Symfony polyfill for the Mbstring extension",
+ "keywords": ["polyfill", "shim", "compatibility", "portable", "mbstring"],
+ "homepage": "https://symfony.com",
+ "license": "MIT",
+ "authors": [
+ {
+ "name": "Nicolas Grekas",
+ "email": "p@tchwork.com"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "require": {
+ "php": ">=7.1"
+ },
+ "provide": {
+ "ext-mbstring": "*"
+ },
+ "autoload": {
+ "psr-4": { "Symfony\\Polyfill\\Mbstring\\": "" },
+ "files": [ "bootstrap.php" ]
+ },
+ "suggest": {
+ "ext-mbstring": "For best performance"
+ },
+ "minimum-stability": "dev",
+ "extra": {
+ "branch-alias": {
+ "dev-main": "1.28-dev"
+ },
+ "thanks": {
+ "name": "symfony/polyfill",
+ "url": "https://github.com/symfony/polyfill"
+ }
+ }
+}
diff --git a/src/vendor/symfony/polyfill-php73/LICENSE b/src/vendor/symfony/polyfill-php73/LICENSE
new file mode 100644
index 0000000..7536cae
--- /dev/null
+++ b/src/vendor/symfony/polyfill-php73/LICENSE
@@ -0,0 +1,19 @@
+Copyright (c) 2018-present Fabien Potencier
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is furnished
+to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
diff --git a/src/vendor/symfony/polyfill-php73/Php73.php b/src/vendor/symfony/polyfill-php73/Php73.php
new file mode 100644
index 0000000..65c35a6
--- /dev/null
+++ b/src/vendor/symfony/polyfill-php73/Php73.php
@@ -0,0 +1,43 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Polyfill\Php73;
+
+/**
+ * @author Gabriel Caruso
+ *
+ * @internal
+ */
+final class Php80
+{
+ public static function fdiv(float $dividend, float $divisor): float
+ {
+ return @($dividend / $divisor);
+ }
+
+ public static function get_debug_type($value): string
+ {
+ switch (true) {
+ case null === $value: return 'null';
+ case \is_bool($value): return 'bool';
+ case \is_string($value): return 'string';
+ case \is_array($value): return 'array';
+ case \is_int($value): return 'int';
+ case \is_float($value): return 'float';
+ case \is_object($value): break;
+ case $value instanceof \__PHP_Incomplete_Class: return '__PHP_Incomplete_Class';
+ default:
+ if (null === $type = @get_resource_type($value)) {
+ return 'unknown';
+ }
+
+ if ('Unknown' === $type) {
+ $type = 'closed';
+ }
+
+ return "resource ($type)";
+ }
+
+ $class = \get_class($value);
+
+ if (false === strpos($class, '@')) {
+ return $class;
+ }
+
+ return (get_parent_class($class) ?: key(class_implements($class)) ?: 'class').'@anonymous';
+ }
+
+ public static function get_resource_id($res): int
+ {
+ if (!\is_resource($res) && null === @get_resource_type($res)) {
+ throw new \TypeError(sprintf('Argument 1 passed to get_resource_id() must be of the type resource, %s given', get_debug_type($res)));
+ }
+
+ return (int) $res;
+ }
+
+ public static function preg_last_error_msg(): string
+ {
+ switch (preg_last_error()) {
+ case \PREG_INTERNAL_ERROR:
+ return 'Internal error';
+ case \PREG_BAD_UTF8_ERROR:
+ return 'Malformed UTF-8 characters, possibly incorrectly encoded';
+ case \PREG_BAD_UTF8_OFFSET_ERROR:
+ return 'The offset did not correspond to the beginning of a valid UTF-8 code point';
+ case \PREG_BACKTRACK_LIMIT_ERROR:
+ return 'Backtrack limit exhausted';
+ case \PREG_RECURSION_LIMIT_ERROR:
+ return 'Recursion limit exhausted';
+ case \PREG_JIT_STACKLIMIT_ERROR:
+ return 'JIT stack limit exhausted';
+ case \PREG_NO_ERROR:
+ return 'No error';
+ default:
+ return 'Unknown error';
+ }
+ }
+
+ public static function str_contains(string $haystack, string $needle): bool
+ {
+ return '' === $needle || false !== strpos($haystack, $needle);
+ }
+
+ public static function str_starts_with(string $haystack, string $needle): bool
+ {
+ return 0 === strncmp($haystack, $needle, \strlen($needle));
+ }
+
+ public static function str_ends_with(string $haystack, string $needle): bool
+ {
+ if ('' === $needle || $needle === $haystack) {
+ return true;
+ }
+
+ if ('' === $haystack) {
+ return false;
+ }
+
+ $needleLength = \strlen($needle);
+
+ return $needleLength <= \strlen($haystack) && 0 === substr_compare($haystack, $needle, -$needleLength);
+ }
+}
diff --git a/src/vendor/symfony/polyfill-php80/PhpToken.php b/src/vendor/symfony/polyfill-php80/PhpToken.php
new file mode 100644
index 0000000..fe6e691
--- /dev/null
+++ b/src/vendor/symfony/polyfill-php80/PhpToken.php
@@ -0,0 +1,103 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Polyfill\Php80;
+
+/**
+ * @author Fedonyuk Anton
+ *
+ * @internal
+ */
+final class Php81
+{
+ public static function array_is_list(array $array): bool
+ {
+ if ([] === $array || $array === array_values($array)) {
+ return true;
+ }
+
+ $nextKey = -1;
+
+ foreach ($array as $k => $v) {
+ if ($k !== ++$nextKey) {
+ return false;
+ }
+ }
+
+ return true;
+ }
+}
diff --git a/src/vendor/symfony/polyfill-php81/README.md b/src/vendor/symfony/polyfill-php81/README.md
new file mode 100644
index 0000000..c07ef78
--- /dev/null
+++ b/src/vendor/symfony/polyfill-php81/README.md
@@ -0,0 +1,18 @@
+Symfony Polyfill / Php81
+========================
+
+This component provides features added to PHP 8.1 core:
+
+- [`array_is_list`](https://php.net/array_is_list)
+- [`enum_exists`](https://php.net/enum-exists)
+- [`MYSQLI_REFRESH_REPLICA`](https://php.net/mysqli.constants#constantmysqli-refresh-replica) constant
+- [`ReturnTypeWillChange`](https://wiki.php.net/rfc/internal_method_return_types)
+- [`CURLStringFile`](https://php.net/CURLStringFile) (but only if PHP >= 7.4 is used)
+
+More information can be found in the
+[main Polyfill README](https://github.com/symfony/polyfill/blob/main/README.md).
+
+License
+=======
+
+This library is released under the [MIT license](LICENSE).
diff --git a/src/vendor/symfony/polyfill-php81/Resources/stubs/CURLStringFile.php b/src/vendor/symfony/polyfill-php81/Resources/stubs/CURLStringFile.php
new file mode 100644
index 0000000..eb5952e
--- /dev/null
+++ b/src/vendor/symfony/polyfill-php81/Resources/stubs/CURLStringFile.php
@@ -0,0 +1,51 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+if (\PHP_VERSION_ID >= 70400 && extension_loaded('curl')) {
+ /**
+ * @property string $data
+ */
+ class CURLStringFile extends CURLFile
+ {
+ private $data;
+
+ public function __construct(string $data, string $postname, string $mime = 'application/octet-stream')
+ {
+ $this->data = $data;
+ parent::__construct('data://application/octet-stream;base64,'.base64_encode($data), $mime, $postname);
+ }
+
+ public function __set(string $name, $value): void
+ {
+ if ('data' !== $name) {
+ $this->$name = $value;
+
+ return;
+ }
+
+ if (is_object($value) ? !method_exists($value, '__toString') : !is_scalar($value)) {
+ throw new \TypeError('Cannot assign '.gettype($value).' to property CURLStringFile::$data of type string');
+ }
+
+ $this->name = 'data://application/octet-stream;base64,'.base64_encode($value);
+ }
+
+ public function __isset(string $name): bool
+ {
+ return isset($this->$name);
+ }
+
+ public function &__get(string $name)
+ {
+ return $this->$name;
+ }
+ }
+}
diff --git a/src/vendor/symfony/polyfill-php81/Resources/stubs/ReturnTypeWillChange.php b/src/vendor/symfony/polyfill-php81/Resources/stubs/ReturnTypeWillChange.php
new file mode 100644
index 0000000..cb7720a
--- /dev/null
+++ b/src/vendor/symfony/polyfill-php81/Resources/stubs/ReturnTypeWillChange.php
@@ -0,0 +1,20 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+if (\PHP_VERSION_ID < 80100) {
+ #[Attribute(Attribute::TARGET_METHOD)]
+ final class ReturnTypeWillChange
+ {
+ public function __construct()
+ {
+ }
+ }
+}
diff --git a/src/vendor/symfony/polyfill-php81/bootstrap.php b/src/vendor/symfony/polyfill-php81/bootstrap.php
new file mode 100644
index 0000000..9f872e0
--- /dev/null
+++ b/src/vendor/symfony/polyfill-php81/bootstrap.php
@@ -0,0 +1,28 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+use Symfony\Polyfill\Php81 as p;
+
+if (\PHP_VERSION_ID >= 80100) {
+ return;
+}
+
+if (defined('MYSQLI_REFRESH_SLAVE') && !defined('MYSQLI_REFRESH_REPLICA')) {
+ define('MYSQLI_REFRESH_REPLICA', 64);
+}
+
+if (!function_exists('array_is_list')) {
+ function array_is_list(array $array): bool { return p\Php81::array_is_list($array); }
+}
+
+if (!function_exists('enum_exists')) {
+ function enum_exists(string $enum, bool $autoload = true): bool { return $autoload && class_exists($enum) && false; }
+}
diff --git a/src/vendor/symfony/polyfill-php81/composer.json b/src/vendor/symfony/polyfill-php81/composer.json
new file mode 100644
index 0000000..e02d673
--- /dev/null
+++ b/src/vendor/symfony/polyfill-php81/composer.json
@@ -0,0 +1,36 @@
+{
+ "name": "symfony/polyfill-php81",
+ "type": "library",
+ "description": "Symfony polyfill backporting some PHP 8.1+ features to lower PHP versions",
+ "keywords": ["polyfill", "shim", "compatibility", "portable"],
+ "homepage": "https://symfony.com",
+ "license": "MIT",
+ "authors": [
+ {
+ "name": "Nicolas Grekas",
+ "email": "p@tchwork.com"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "require": {
+ "php": ">=7.1"
+ },
+ "autoload": {
+ "psr-4": { "Symfony\\Polyfill\\Php81\\": "" },
+ "files": [ "bootstrap.php" ],
+ "classmap": [ "Resources/stubs" ]
+ },
+ "minimum-stability": "dev",
+ "extra": {
+ "branch-alias": {
+ "dev-main": "1.28-dev"
+ },
+ "thanks": {
+ "name": "symfony/polyfill",
+ "url": "https://github.com/symfony/polyfill"
+ }
+ }
+}
diff --git a/src/vendor/symfony/service-contracts/Attribute/Required.php b/src/vendor/symfony/service-contracts/Attribute/Required.php
new file mode 100644
index 0000000..9df8511
--- /dev/null
+++ b/src/vendor/symfony/service-contracts/Attribute/Required.php
@@ -0,0 +1,25 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Contracts\Service\Attribute;
+
+/**
+ * A required dependency.
+ *
+ * This attribute indicates that a property holds a required dependency. The annotated property or method should be
+ * considered during the instantiation process of the containing class.
+ *
+ * @author Alexander M. Turek
+ */
+trait ServiceLocatorTrait
+{
+ private array $factories;
+ private array $loading = [];
+ private array $providedTypes;
+
+ /**
+ * @param callable[] $factories
+ */
+ public function __construct(array $factories)
+ {
+ $this->factories = $factories;
+ }
+
+ public function has(string $id): bool
+ {
+ return isset($this->factories[$id]);
+ }
+
+ public function get(string $id): mixed
+ {
+ if (!isset($this->factories[$id])) {
+ throw $this->createNotFoundException($id);
+ }
+
+ if (isset($this->loading[$id])) {
+ $ids = array_values($this->loading);
+ $ids = \array_slice($this->loading, array_search($id, $ids));
+ $ids[] = $id;
+
+ throw $this->createCircularReferenceException($id, $ids);
+ }
+
+ $this->loading[$id] = $id;
+ try {
+ return $this->factories[$id]($this);
+ } finally {
+ unset($this->loading[$id]);
+ }
+ }
+
+ public function getProvidedServices(): array
+ {
+ if (!isset($this->providedTypes)) {
+ $this->providedTypes = [];
+
+ foreach ($this->factories as $name => $factory) {
+ if (!\is_callable($factory)) {
+ $this->providedTypes[$name] = '?';
+ } else {
+ $type = (new \ReflectionFunction($factory))->getReturnType();
+
+ $this->providedTypes[$name] = $type ? ($type->allowsNull() ? '?' : '').($type instanceof \ReflectionNamedType ? $type->getName() : $type) : '?';
+ }
+ }
+ }
+
+ return $this->providedTypes;
+ }
+
+ private function createNotFoundException(string $id): NotFoundExceptionInterface
+ {
+ if (!$alternatives = array_keys($this->factories)) {
+ $message = 'is empty...';
+ } else {
+ $last = array_pop($alternatives);
+ if ($alternatives) {
+ $message = sprintf('only knows about the "%s" and "%s" services.', implode('", "', $alternatives), $last);
+ } else {
+ $message = sprintf('only knows about the "%s" service.', $last);
+ }
+ }
+
+ if ($this->loading) {
+ $message = sprintf('The service "%s" has a dependency on a non-existent service "%s". This locator %s', end($this->loading), $id, $message);
+ } else {
+ $message = sprintf('Service "%s" not found: the current service locator %s', $id, $message);
+ }
+
+ return new class($message) extends \InvalidArgumentException implements NotFoundExceptionInterface {
+ };
+ }
+
+ private function createCircularReferenceException(string $id, array $path): ContainerExceptionInterface
+ {
+ return new class(sprintf('Circular reference detected for service "%s", path: "%s".', $id, implode(' -> ', $path))) extends \RuntimeException implements ContainerExceptionInterface {
+ };
+ }
+}
diff --git a/src/vendor/symfony/service-contracts/ServiceProviderInterface.php b/src/vendor/symfony/service-contracts/ServiceProviderInterface.php
new file mode 100644
index 0000000..c05e4bf
--- /dev/null
+++ b/src/vendor/symfony/service-contracts/ServiceProviderInterface.php
@@ -0,0 +1,45 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Contracts\Service;
+
+use Psr\Container\ContainerInterface;
+
+/**
+ * A ServiceProviderInterface exposes the identifiers and the types of services provided by a container.
+ *
+ * @author Nicolas Grekas
+ * @author Mateusz Sip
+ */
+interface ServiceSubscriberInterface
+{
+ /**
+ * Returns an array of service types (or {@see SubscribedService} objects) required
+ * by such instances, optionally keyed by the service names used internally.
+ *
+ * For mandatory dependencies:
+ *
+ * * ['logger' => 'Psr\Log\LoggerInterface'] means the objects use the "logger" name
+ * internally to fetch a service which must implement Psr\Log\LoggerInterface.
+ * * ['loggers' => 'Psr\Log\LoggerInterface[]'] means the objects use the "loggers" name
+ * internally to fetch an iterable of Psr\Log\LoggerInterface instances.
+ * * ['Psr\Log\LoggerInterface'] is a shortcut for
+ * * ['Psr\Log\LoggerInterface' => 'Psr\Log\LoggerInterface']
+ *
+ * otherwise:
+ *
+ * * ['logger' => '?Psr\Log\LoggerInterface'] denotes an optional dependency
+ * * ['loggers' => '?Psr\Log\LoggerInterface[]'] denotes an optional iterable dependency
+ * * ['?Psr\Log\LoggerInterface'] is a shortcut for
+ * * ['Psr\Log\LoggerInterface' => '?Psr\Log\LoggerInterface']
+ *
+ * additionally, an array of {@see SubscribedService}'s can be returned:
+ *
+ * * [new SubscribedService('logger', Psr\Log\LoggerInterface::class)]
+ * * [new SubscribedService(type: Psr\Log\LoggerInterface::class, nullable: true)]
+ * * [new SubscribedService('http_client', HttpClientInterface::class, attributes: new Target('githubApi'))]
+ *
+ * @return string[]|SubscribedService[] The required service types, optionally keyed by service names
+ */
+ public static function getSubscribedServices(): array;
+}
diff --git a/src/vendor/symfony/service-contracts/ServiceSubscriberTrait.php b/src/vendor/symfony/service-contracts/ServiceSubscriberTrait.php
new file mode 100644
index 0000000..f3b450c
--- /dev/null
+++ b/src/vendor/symfony/service-contracts/ServiceSubscriberTrait.php
@@ -0,0 +1,78 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Contracts\Service;
+
+use Psr\Container\ContainerInterface;
+use Symfony\Contracts\Service\Attribute\Required;
+use Symfony\Contracts\Service\Attribute\SubscribedService;
+
+/**
+ * Implementation of ServiceSubscriberInterface that determines subscribed services from
+ * method return types. Service ids are available as "ClassName::methodName".
+ *
+ * @author Kevin Bond
+ */
+interface TranslatableInterface
+{
+ public function trans(TranslatorInterface $translator, string $locale = null): string;
+}
diff --git a/src/vendor/symfony/translation-contracts/TranslatorInterface.php b/src/vendor/symfony/translation-contracts/TranslatorInterface.php
new file mode 100644
index 0000000..018db07
--- /dev/null
+++ b/src/vendor/symfony/translation-contracts/TranslatorInterface.php
@@ -0,0 +1,68 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Contracts\Translation;
+
+/**
+ * @author Fabien Potencier
+ *
+ *
+ * This method returns the value of the parameter for choosing the right
+ * pluralization form (in this case "choices").
+ *
+ * @return int|null The number to use to pluralize of the message
+ */
+ public function getPlural();
+
+ /**
+ * Returns the root element of the validation.
+ *
+ * @return mixed The value that was passed originally to the validator when
+ * the validation was started. Because the validator traverses
+ * the object graph, the value at which the violation occurs
+ * is not necessarily the value that was originally validated.
+ */
+ public function getRoot();
+
+ /**
+ * Returns the property path from the root element to the violation.
+ *
+ * @return string The property path indicates how the validator reached
+ * the invalid value from the root element. If the root
+ * element is a Person instance with a property
+ * "address" that contains an Address instance
+ * with an invalid property "street", the generated property
+ * path is "address.street". Property access is denoted by
+ * dots, while array access is denoted by square brackets,
+ * for example "addresses[1].street".
+ */
+ public function getPropertyPath();
+
+ /**
+ * Returns the value that caused the violation.
+ *
+ * @return mixed the invalid value that caused the validated constraint to
+ * fail
+ */
+ public function getInvalidValue();
+
+ /**
+ * Returns a machine-digestible error code for the violation.
+ *
+ * @return string|null
+ */
+ public function getCode();
+}
diff --git a/src/vendor/symfony/validator/ConstraintViolationList.php b/src/vendor/symfony/validator/ConstraintViolationList.php
new file mode 100644
index 0000000..3d459b2
--- /dev/null
+++ b/src/vendor/symfony/validator/ConstraintViolationList.php
@@ -0,0 +1,203 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Validator;
+
+/**
+ * Default implementation of {@ConstraintViolationListInterface}.
+ *
+ * @author Bernhard Schussek