diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..d4de78f
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1 @@
+module.sig
diff --git a/Api/Gql/Gateway.php b/Api/Gql/Gateway.php
new file mode 100644
index 0000000..592297f
--- /dev/null
+++ b/Api/Gql/Gateway.php
@@ -0,0 +1,429 @@
+ [
+ 'description' => _('Read Gateways'),
+ ],
+ 'write:gateway' => [
+ 'description' => _('Write Gateways'),
+ ]
+ ];
+ }
+
+ /**
+ * mutationCallback
+ *
+ * @return callable|void
+ */
+ public function mutationCallback() {
+ if($this->checkWriteScope("gateway")) {
+ return function() {
+ return [
+ 'addGateway' => Relay::mutationWithClientMutationId([
+ 'name' => 'addGateway',
+ 'description' => _('Add a new gateway'),
+ 'inputFields' => $this->getAddInputFields(),
+ 'outputFields' => $this->getMutationOutputFields(),
+ 'mutateAndGetPayload' => function ($input) {
+ $data = $this->resolveGatewayInput($input, null);
+ $result = $this->freepbx->Gateway->addGateway($data);
+ return [
+ 'gateway' => $result['gateway'],
+ 'status' => $result['status'],
+ 'message' => $result['message'],
+ ];
+ }
+ ]),
+ 'updateGateway' => Relay::mutationWithClientMutationId([
+ 'name' => 'updateGateway',
+ 'description' => _('Update an existing gateway. Fields left out keep their current value.'),
+ 'inputFields' => $this->getUpdateInputFields(),
+ 'outputFields' => $this->getMutationOutputFields(),
+ 'mutateAndGetPayload' => function ($input) {
+ $extension = (string)($input['extension'] ?? '');
+ $existing = $extension !== '' ? $this->freepbx->Gateway->getGateway($extension) : null;
+ if(empty($existing)){
+ return ['gateway' => null, 'status' => false, 'message' => _('Gateway not found!')];
+ }
+ $data = $this->resolveGatewayInput($input, $existing);
+ $result = $this->freepbx->Gateway->updateGateway($data);
+ return [
+ 'gateway' => $result['gateway'],
+ 'status' => $result['status'],
+ 'message' => $result['message'],
+ ];
+ }
+ ]),
+ 'removeGateway' => Relay::mutationWithClientMutationId([
+ 'name' => 'removeGateway',
+ 'description' => _('Remove an existing gateway'),
+ 'inputFields' => [
+ 'extension' => [
+ 'type' => Type::nonNull(Type::id()),
+ 'description' => _('Extension of the gateway to remove'),
+ ]
+ ],
+ 'outputFields' => [
+ 'deletedId' => [
+ 'type' => Type::id(),
+ 'description' => _('Extension of the gateway that was removed'),
+ 'resolve' => function ($payload) {
+ return $payload['extension'];
+ }
+ ],
+ 'status' => [
+ 'type' => Type::boolean(),
+ 'description' => _('Status of the request'),
+ ],
+ 'message' => [
+ 'type' => Type::string(),
+ 'description' => _('Message for the request'),
+ ],
+ ],
+ 'mutateAndGetPayload' => function ($input) {
+ $result = $this->freepbx->Gateway->deleteGateway($input['extension']);
+ return [
+ 'extension' => $input['extension'],
+ 'status' => $result['status'],
+ 'message' => $result['message'],
+ ];
+ }
+ ]),
+ ];
+ };
+ }
+ }
+
+ /**
+ * queryCallback
+ *
+ * @return callable|void
+ */
+ public function queryCallback() {
+ if($this->checkReadScope("gateway")) {
+ return function() {
+ return [
+ 'allGateways' => [
+ 'type' => $this->typeContainer->get('gateway')->getConnectionType(),
+ 'description' => $this->description,
+ 'args' => Relay::connectionArgs(),
+ 'resolve' => function($root, $args) {
+ return Relay::connectionFromArray($this->freepbx->Gateway->getAllGateway(), $args);
+ },
+ ],
+ 'gateway' => [
+ 'type' => $this->typeContainer->get('gateway')->getObject(),
+ 'description' => _('Fetch a single gateway by extension'),
+ 'args' => [
+ 'id' => [
+ 'type' => Type::nonNull(Type::id()),
+ 'description' => _('Extension of the gateway'),
+ ]
+ ],
+ 'resolve' => function($root, $args) {
+ $row = $this->freepbx->Gateway->getGateway($args['id']);
+ return !empty($row) ? $row : null;
+ }
+ ],
+ ];
+ };
+ }
+ }
+
+ /**
+ * initializeTypes
+ *
+ * @return void
+ */
+ public function initializeTypes() {
+ $gateway = $this->typeContainer->create('gateway');
+ $gateway->setDescription($this->description);
+
+ $gateway->addInterfaceCallback(function() {
+ return [$this->getNodeDefinition()['nodeInterface']];
+ });
+
+ $gateway->setGetNodeCallback(function($id) {
+ $row = $this->freepbx->Gateway->getGateway($id);
+ return !empty($row) ? $row : null;
+ });
+
+ $gateway->addFieldCallback(function() {
+ return [
+ 'id' => [
+ 'type' => Type::nonNull(Type::id()),
+ 'description' => _('Extension used as the gateway id'),
+ 'resolve' => function($row) {
+ return $row['extension'];
+ }
+ ],
+ 'extension' => [
+ 'type' => Type::nonNull(Type::string()),
+ 'description' => _('Extension as Gateway'),
+ ],
+ 'contact' => [
+ 'type' => Type::string(),
+ 'description' => _('Name of the contact'),
+ ],
+ 'description' => [
+ 'type' => Type::string(),
+ 'description' => _('Description of the gateway'),
+ ],
+ 'address' => [
+ 'type' => Type::string(),
+ 'description' => _('Address of the contact'),
+ ],
+ 'city' => [
+ 'type' => Type::string(),
+ 'description' => _('City of the contact'),
+ ],
+ 'zipCode' => [
+ 'type' => Type::string(),
+ 'description' => _('ZIP code of the contact'),
+ 'resolve' => function($row) {
+ return $row['zip_code'] ?? '';
+ }
+ ],
+ 'country' => [
+ 'type' => Type::string(),
+ 'description' => _('Country of the contact'),
+ ],
+ 'email' => [
+ 'type' => Type::string(),
+ 'description' => _('Email of the contact'),
+ ],
+ 'gatewayAddress' => [
+ 'type' => Type::string(),
+ 'description' => _('IP address (with optional :port) of the remote gateway'),
+ 'resolve' => function($row) {
+ return $row['gateway'] ?? '';
+ }
+ ],
+ 'accountcode' => [
+ 'type' => Type::string(),
+ 'description' => _('Account code applied to calls through this gateway'),
+ ],
+ 'callLimit' => [
+ 'type' => Type::int(),
+ 'description' => _('Maximum number of simultaneous calls (0 = unlimited)'),
+ 'resolve' => function($row) {
+ return intval($row['call_limit'] ?? 0);
+ }
+ ],
+ 'dids' => [
+ 'type' => Type::listOf(Type::string()),
+ 'description' => _('DIDs attached to this gateway (first one is the DID base)'),
+ 'resolve' => function($row) {
+ $dids = json_decode($row['dids'] ?? '[]', true);
+ return is_array($dids) ? array_values($dids) : [];
+ }
+ ],
+ 'status' => [
+ 'type' => Type::string(),
+ 'description' => _('Live PJSIP connectivity status: online, offline or unknown'),
+ 'resolve' => function($row) {
+ return $this->freepbx->Gateway->getEndpointStatus($row['extension']);
+ }
+ ],
+ ];
+ });
+
+ $gateway->setConnectionResolveNode(function ($edge) {
+ return $edge['node'];
+ });
+
+ $gateway->setConnectionFields(function() {
+ return [
+ 'totalCount' => [
+ 'type' => Type::int(),
+ 'resolve' => function($value) {
+ return count($this->freepbx->Gateway->getAllGateway());
+ }
+ ],
+ 'gateways' => [
+ 'type' => Type::listOf($this->typeContainer->get('gateway')->getObject()),
+ 'description' => $this->description,
+ 'resolve' => function($root, $args) {
+ return array_map(function($row) {
+ return $row['node'];
+ }, $root['edges']);
+ }
+ ]
+ ];
+ });
+ }
+
+ /**
+ * getMutationOutputFields
+ *
+ * Shared output shape for addGateway/updateGateway mutations.
+ *
+ * @return array
+ */
+ private function getMutationOutputFields() {
+ return [
+ 'gateway' => [
+ 'type' => $this->typeContainer->get('gateway')->getObject(),
+ 'description' => _('The gateway after the change'),
+ 'resolve' => function ($payload) {
+ return $payload['gateway'];
+ }
+ ],
+ 'status' => [
+ 'type' => Type::boolean(),
+ 'description' => _('Status of the request'),
+ ],
+ 'message' => [
+ 'type' => Type::string(),
+ 'description' => _('Message for the request'),
+ ],
+ ];
+ }
+
+ /**
+ * getAddInputFields
+ *
+ * @return array
+ */
+ private function getAddInputFields() {
+ return [
+ 'extension' => [
+ 'type' => Type::nonNull(Type::id()),
+ 'description' => _('Extension to use as the gateway. Must be an existing PJSIP extension not already used by another gateway.'),
+ ],
+ 'contact' => [
+ 'type' => Type::nonNull(Type::string()),
+ 'description' => _('Name of the contact'),
+ ],
+ 'description' => [
+ 'type' => Type::string(),
+ 'description' => _('Description of the gateway'),
+ ],
+ 'address' => [
+ 'type' => Type::string(),
+ 'description' => _('Address of the contact'),
+ ],
+ 'city' => [
+ 'type' => Type::string(),
+ 'description' => _('City of the contact'),
+ ],
+ 'zip' => [
+ 'type' => Type::string(),
+ 'description' => _('ZIP code of the contact (digits only, 3 to 6 characters)'),
+ ],
+ 'country' => [
+ 'type' => Type::string(),
+ 'description' => _('Country of the contact'),
+ ],
+ 'email' => [
+ 'type' => Type::string(),
+ 'description' => _('Email of the contact'),
+ ],
+ 'gatewayAddress' => [
+ 'type' => Type::nonNull(Type::string()),
+ 'description' => _('IPv4 address of the remote gateway, with an optional :port (e.g. 200.25.46.30 or 200.25.46.30:5061)'),
+ ],
+ 'accountcode' => [
+ 'type' => Type::string(),
+ 'description' => _('Account code applied to calls through this gateway (letters, digits, "_" and "-" only)'),
+ ],
+ 'callLimit' => [
+ 'type' => Type::int(),
+ 'description' => _('Maximum number of simultaneous calls (0 = unlimited)'),
+ ],
+ 'dids' => [
+ 'type' => Type::nonNull(Type::listOf(Type::nonNull(Type::string()))),
+ 'description' => _('DIDs attached to this gateway. The first entry is the DID base (usually the same as the extension).'),
+ ],
+ ];
+ }
+
+ /**
+ * getUpdateInputFields
+ *
+ * Same as getAddInputFields() but nothing besides "extension" is
+ * required - any field left out keeps its current stored value.
+ *
+ * @return array
+ */
+ private function getUpdateInputFields() {
+ $fields = $this->getAddInputFields();
+ $fields['extension'] = [
+ 'type' => Type::nonNull(Type::id()),
+ 'description' => _('Extension of the gateway to update'),
+ ];
+ $fields['gatewayAddress'] = [
+ 'type' => Type::string(),
+ 'description' => _('IPv4 address of the remote gateway, with an optional :port (e.g. 200.25.46.30 or 200.25.46.30:5061)'),
+ ];
+ $fields['dids'] = [
+ 'type' => Type::listOf(Type::nonNull(Type::string())),
+ 'description' => _('DIDs attached to this gateway. The first entry is the DID base. Omit to keep the current list.'),
+ ];
+ return $fields;
+ }
+
+ /**
+ * resolveGatewayInput
+ *
+ * Maps the camelCase GraphQL input onto the internal array shape
+ * expected by Gateway::addGateway()/updateGateway() (itself shared
+ * with the web form), falling back to the existing DB row for any
+ * field omitted from the input - so updateGateway is a real partial
+ * update instead of blanking out everything that wasn't passed.
+ *
+ * @param array $input the GraphQL mutation input
+ * @param array|null $existing the current DB row (null when adding)
+ * @return array
+ */
+ private function resolveGatewayInput($input, $existing) {
+ $existing = $existing ?? [];
+ $existingDids = isset($existing['dids']) ? json_decode($existing['dids'], true) : [];
+ $existingDids = is_array($existingDids) ? $existingDids : [];
+
+ return [
+ 'extension' => $input['extension'] ?? ($existing['extension'] ?? ''),
+ 'contact' => $input['contact'] ?? ($existing['contact'] ?? ''),
+ 'description' => $input['description'] ?? ($existing['description'] ?? ''),
+ 'address' => $input['address'] ?? ($existing['address'] ?? ''),
+ 'city' => $input['city'] ?? ($existing['city'] ?? ''),
+ 'zip' => $input['zip'] ?? ($existing['zip_code'] ?? ''),
+ 'country' => $input['country'] ?? ($existing['country'] ?? ''),
+ 'email' => $input['email'] ?? ($existing['email'] ?? ''),
+ 'gateway' => $input['gatewayAddress'] ?? ($existing['gateway'] ?? ''),
+ 'accountcode' => $input['accountcode'] ?? ($existing['accountcode'] ?? ''),
+ 'call_limit' => $input['callLimit'] ?? ($existing['call_limit'] ?? 0),
+ 'dids' => $input['dids'] ?? $existingDids,
+ ];
+ }
+}
diff --git a/Backup.php b/Backup.php
new file mode 100644
index 0000000..1d8e3d6
--- /dev/null
+++ b/Backup.php
@@ -0,0 +1,10 @@
+FreePBX->Gateway->getAllGateway();
+ $this->addConfigs($config);
+ }
+}
diff --git a/Gateway.class.php b/Gateway.class.php
new file mode 100644
index 0000000..80a599c
--- /dev/null
+++ b/Gateway.class.php
@@ -0,0 +1,713 @@
+FreePBX = $freepbx;
+ $this->db = $freepbx->Database;
+ $this->astman = $this->FreePBX->astman;
+ }
+
+ //Install method. use this or install.php using both may cause weird behavior
+ //Runs on both fresh install and module upgrade: re-applies the PJSIP settings
+ //on every existing gateway so extensions created by older module versions
+ //(missing e.g. user_eq_phone) get backfilled instead of staying stale.
+ public function install() {
+ $gateways = $this->getAllGateway();
+ foreach ($gateways as $gateway) {
+ $this->applyGatewaySipConfig($gateway["extension"], $gateway["gateway"], $gateway["accountcode"]);
+ }
+ if (!empty($gateways)) {
+ needreload();
+ }
+ }
+
+ //Uninstall method. use this or install.php using both may cause weird behavior
+ public function uninstall() {
+ $gateways = $this->getAllGateway();
+ foreach($gateways as $gateway){
+ $sql = "UPDATE sip SET data = 'from-internal' WHERE id = :extension AND keyword = 'context'";
+ $stm = $this->db->prepare($sql);
+ $stm->execute(array(":extension" => $gateway["extension"]));
+ }
+
+ $sql = "TRUNCATE TABLE gateway;";
+ $this->db->prepare($sql)->execute();
+ }
+
+ //Not yet implemented
+ public function backup() {}
+
+ //not yet implimented
+ public function restore($backup) {}
+
+ //process form
+ public function doConfigPageInit($page) {}
+
+ // Set by showPage() when it redisplays add_gateway.php/edit_gateway.php
+ // after a failed validate() (e.g. duplicate DID). That POST-back has no
+ // "action" in the request (the form posts to plain "?display=gateway"),
+ // so getActionBar() below has no way to know it should still show the
+ // Submit/Cancel buttons unless we tell it explicitly.
+ private $lastFailedFormAction = null;
+
+ //This shows the submit buttons
+ public function getActionBar($request) {
+ $buttons = [];
+ $action = !empty($request["action"]) ? $request["action"] : $this->lastFailedFormAction;
+ if(!empty($action)){
+ if($action === "help"){
+ return $buttons;
+ }
+ }
+ else{
+ return $buttons;
+ }
+ switch($_GET['display']) {
+ case 'gateway':
+ $buttons = array(
+ 'submit' => array(
+ 'name' => 'submit',
+ 'id' => 'submit',
+ 'value' => _('Submit')
+ ),
+ 'Cancel' => array(
+ 'name' => 'cancel',
+ 'id' => 'cancel',
+ 'value' => _('Cancel')
+ ),
+ );
+ break;
+ }
+ return $buttons;
+ }
+
+ public function showPage(){
+ $request = freepbxGetSanitizedRequest();
+ $lang = $_COOKIE["lang"];
+ $jsloc = "[]";
+ if( file_exists(__DIR__."/i18n/$lang/LC_MESSAGES/gateway.json")){
+ $jsloc = file_get_contents(__DIR__."/i18n/$lang/LC_MESSAGES/gateway.json");
+ }
+ $request["action"] = empty($request["action"]) ? "" : $request["action"];
+ switch($request["action"]){
+ case "add_gateway":
+ $vars = array('title' => _("Add Gateway"));
+ $vars["users"] = $this->getUsers();
+ $vars["jsloc"] = $this->cleanJSLoc($jsloc);
+ return load_view(__DIR__.'/views/add_gateway.php',$vars);
+ case "edit_gateway":
+ $vars = array('title' => _("Edit Gateway"));
+ $vars["users"] = $this->getUsers();
+ $vars["gateway"] = $this->getGateway($request["gateway"]);
+ $vars["accountcode"]= $vars["gateway"]["accountcode"] ?? '';
+ $vars["jsloc"] = $this->cleanJSLoc($jsloc);
+ return load_view(__DIR__.'/views/edit_gateway.php',$vars);
+ case "help":
+ $vars = ["img" => "/admin/modules/gateway/"];
+ $vars["jsloc"] = $this->cleanJSLoc($jsloc);
+ return load_view(__DIR__.'/views/description.php', $vars);
+ default:
+ $vars = ['title' => _("Gateway List")];
+ $vars["jsloc"] = $this->cleanJSLoc($jsloc);
+ if(!empty($request["edit"]) && $request["edit"] === "no" ){
+ $result = $this->addGateway($request);
+ if(!$result["status"]){
+ // Stay on the "Add" form and keep what was typed instead
+ // of dropping the user back on the grid.
+ $addVars = array('title' => _("Add Gateway"));
+ $addVars["users"] = $this->getUsers();
+ $addVars["jsloc"] = $this->cleanJSLoc($jsloc);
+ $addVars["message"] = $result["message"];
+ $addVars["formdata"] = $request;
+ $this->lastFailedFormAction = "add_gateway";
+ return load_view(__DIR__.'/views/add_gateway.php',$addVars);
+ }
+ }
+
+ if(!empty($request["edit"]) && $request["edit"] === "yes" ){
+ $result = $this->updateGateway($request);
+ if(!$result["status"]){
+ // Stay on the "Edit" form and redisplay what was typed
+ // (not the stale DB row) instead of dropping the user
+ // back on the grid.
+ $editVars = array('title' => _("Edit Gateway"));
+ $editVars["users"] = $this->getUsers();
+ $editVars["jsloc"] = $this->cleanJSLoc($jsloc);
+ $editVars["message"] = $result["message"];
+ $editVars["accountcode"] = $request["accountcode"] ?? '';
+ $editVars["gateway"] = array(
+ "extension" => $request["extension"] ?? '',
+ "contact" => $request["contact"] ?? '',
+ "description" => $request["description"] ?? '',
+ "address" => $request["address"] ?? '',
+ "city" => $request["city"] ?? '',
+ "zip_code" => $request["zip"] ?? '',
+ "country" => $request["country"] ?? '',
+ "email" => $request["email"] ?? '',
+ "gateway" => $request["gateway"] ?? '',
+ "dids" => json_encode($request["dids"] ?? []),
+ "call_limit" => $request["call_limit"] ?? 0,
+ "accountcode" => $request["accountcode"] ?? '',
+ );
+ $this->lastFailedFormAction = "edit_gateway";
+ return load_view(__DIR__.'/views/edit_gateway.php',$editVars);
+ }
+ }
+ return load_view(__DIR__.'/views/grid.php',$vars);
+ }
+
+ }
+
+ /**
+ * cleanJSLoc
+ *
+ * @param string $jsloc
+ * @return string
+ */
+ public function cleanJSLoc($jsloc){
+ $jsloc_array = json_decode($jsloc, true);
+
+ if (isset($jsloc_array['locale_data']['gateway'])) {
+ foreach ($jsloc_array['locale_data']['gateway'] as $key => &$translation) {
+ if (is_array($translation)) {
+ if (empty($translation[0]) && !empty($translation[1])) {
+ array_shift($translation);
+ } elseif (count($translation) === 1) {
+ $translation[0] = $translation[0];
+ }
+ }
+ }
+ }
+ return json_encode($jsloc_array, JSON_UNESCAPED_UNICODE);
+ }
+
+ /**
+ * getUsers
+ *
+ * @return array
+ */
+ public function getUsers(){
+ $allGateway = $this->getAllGateway();
+ $sql = "SELECT users.extension, users.name FROM users INNER JOIN sip ON (sip.id = users.extension AND sip.data LIKE 'PJSIP/%' AND users.extension NOT LIKE '99%' AND users.extension NOT LIKE '98%') ORDER BY users.extension;";
+ $stm = $this->db->prepare($sql);
+ $stm->execute();
+ $ret = $stm->fetchAll(\PDO::FETCH_ASSOC);
+ $extGateway = array_column($allGateway, 'extension');
+ $result = array_filter($ret, function($item) use ($extGateway) {
+ return !in_array($item['extension'], $extGateway);
+ });
+ return array_values($result);
+ }
+
+ private $aorsData = null;
+
+ private function getAorsData() {
+ if ($this->aorsData === null) {
+ $response = $this->astman->send_request('Command', ['Command' => 'pjsip show aors']);
+ $this->aorsData = (is_array($response) && !empty($response['data'])) ? $response['data'] : '';
+ }
+ return $this->aorsData;
+ }
+
+ public function getEndpointStatus($extension) {
+ if (!$this->astman) {
+ return 'unknown';
+ }
+ $data = $this->getAorsData();
+ if (empty($data)) {
+ return 'offline';
+ }
+ $pattern = '/Contact:\s+' . preg_quote($extension, '/') . '\/.*Avail/i';
+ return preg_match($pattern, $data) ? 'online' : 'offline';
+ }
+
+ public function ajaxRequest($req, &$setting) {
+ switch ($req) {
+ case 'gatewayList':
+ case 'delete':
+ return true;
+ default:
+ return false;
+ break;
+ }
+ }
+
+ public function ajaxHandler(){
+ $request = freepbxGetSanitizedRequest();
+ switch ($request['command']) {
+ case 'gatewayList':
+ $gateways = $this->getAllGateway();
+ foreach ($gateways as &$gw) {
+ $gw['status'] = $this->getEndpointStatus($gw['extension']);
+ }
+ return $gateways;
+ case 'delete':
+ $result = $this->deleteGateway($request['gateway']);
+ return $result['status'];
+ default:
+ return false;
+ break;
+ }
+ }
+
+ /**
+ * validate
+ *
+ * Validates format/charset of every field AND checks for duplicates
+ * (Gateway IP, Account Code, DID Base / DIDs) against the other gateways
+ * already stored, so two gateways can never silently share the same
+ * accountcode, IP, or DID.
+ *
+ * @param array $data the submitted request
+ * @param string|null $excludeExtension when editing, the extension of the
+ * gateway being edited, so it isn't
+ * compared against itself
+ * @return array
+ */
+ public function validate($data, $excludeExtension = null){
+ if( !preg_match('/^\d+$/', trim((string)($data["extension"] ?? '')))){
+ return ["status" => "false", "message" => _("The extension is not numeric!")];
+ }
+
+ if( !empty($data["contact"]) && !preg_match('/^[a-zA-Z0-9éèêëàâùûüîïôçœæÉÈÊËÀÂÙÛÜÎÏÔÇŒÆ_\s\-]+$/u', trim($data["contact"]))){
+ return ["status" => "false", "message" => _("Contact Error: Wrong characters detected!")];
+ }
+
+ if( !empty($data["description"]) && !preg_match('/^[a-zA-Z0-9éèêëàâùûüîïôçœæÉÈÊËÀÂÙÛÜÎÏÔÇŒÆ_\s\-]+$/u', trim($data["description"]))){
+ return ["status" => "false", "message" => _("Description Error: Wrong characters detected!")];
+ }
+
+ if( !empty($data["address"]) && !preg_match('/^[a-zA-Z0-9éèêëàâùûüîïôçœæÉÈÊËÀÂÙÛÜÎÏÔÇŒÆ_\s\-]+$/u', trim($data["address"]))){
+ return ["status" => "false", "message" => _("Address Error: Wrong characters detected!")];
+ }
+
+ if( !empty($data["city"]) && !preg_match('/^[a-zA-Z0-9éèêëàâùûüîïôçœæÉÈÊËÀÂÙÛÜÎÏÔÇŒÆ_\s\-]+$/u', trim($data["city"]))){
+ return ["status" => "false", "message" => _("City Error: Wrong characters detected!")];
+ }
+
+ if( !empty($data["country"]) && !preg_match('/^[a-zA-ZéèêëàâùûüîïôçœæÉÈÊËÀÂÙÛÜÎÏÔÇŒÆ_\s\-]+$/u', trim($data["country"]))){
+ return ["status" => "false", "message" => _("Country Error: Wrong characters detected!")];
+ }
+
+ if( !empty($data["zip"]) && !preg_match('/^\d{3,6}$/', trim($data["zip"]))){
+ return ["status" => "false", "message" => _("ZIP Code must contain digits only (3 to 6, column is limited to 6 characters)!")];
+ }
+
+ // FILTER_VALIDATE_EMAIL alone accepts RFC5321 quoted local-parts
+ // (e.g. "
diff --git a/views/description.php b/views/description.php
new file mode 100644
index 0000000..47d232d
--- /dev/null
+++ b/views/description.php
@@ -0,0 +1,118 @@
+
= _("Document to configure the gateway") ?>
+
+
+ = _("When you go to the gateway page, you've got a grid showing all gateways configured like this.") ?>
+ = _("This grid shows some useful things regarding this gateway.") ?>
+
+
+ = _("To add a gateway, click on the + Add button above the grid.")?>
+ = _("A page is displayed, you just have to fill in the desired fields.") ?>
+ = _("The required fields are:") ?>
+ - = _("Extension as gateway") ?>
+ - = _("Gateway") ?>
+ - = _("Primary DID") ?>
+ = _("Start by selecting an extension to convert to a gateway.") ?>
+ = _("Enter the IP address of the gateway that will be connected to the extension. Example: 200.25.46.30 or 200.25.46.30:5061") ?>
+ = _("If you do not specify the port in the IP address of your gateway, the port will point to 5060.") ?>
+ = _("When you choose an extension to convert to a gateway, the Primary DID fields will be filled in automatically. The first DID will therefore be the head phone number.") ?>
+ = _("You may add any DID number associated to the gateway clicking on DID + button. If the field DID is added, fill it, otherwise, you can delete it clicking on the trash button.") ?>
+ = _("Fill in the other fields to identify the gateway. Then click on the Submit button.") ?>
+
+ + = _("On the grid, click on the Pencil icon on the right of row. Same type of fields used above. You can change everything excepted the field: Extension as Gateway and Primary DID.") ?> +
+ +
+ + = _("You can delete a gateway clicking on the trash icon.") ?> +
+ +
+ flowchart TD
+ A[Trunk PJSIP] <--> B(Context: \n from-trunk-gateway)
+ B<--> C{Calls}
+ C<--> D[Context: \n from-internal-gateway\nfa:fa-server\nGW 1]
+ C<--> E[Context: \n from-internal-gateway\nfa:fa-server\nGW 2]
+ C<--> F[Context: \n from-internal-gateway\nfa:fa-server\nPBX 1]
+ D<--> G[fa:fa-phone\nExt 1]
+ D<--> h[fa:fa-phone\nExt 2]
+ E<--> i[fa:fa-phone\nExt 1]
+ E<--> j[fa:fa-phone\nExt 2]
+ F<--> k[fa:fa-phone\nExt 1]
+ F<--> L[fa:fa-phone\nExt 2]
+
+
+ = _("Once extensions (PJSIP) are used as a gateway, their context will change to:") ?> from-internal-gateway.
+ = _("Regarding the trunk (PJSIP) used to receive calls to gateways, set the context manually to:") ?> from-trunk-gateway.
+ = _("Look the schema above.") ?>
+
| = _("Phone Number") ?> | += _("Account Code") ?> | += _("Description") ?> | += _("Contact") ?> | += _("City") ?> | += _("Gateway") ?> | += _("Status") ?> | += _("DIDs") ?> | += _("Action") ?> | +
|---|