From f07457ddb48a7ce7c735492a98cee7096a794d15 Mon Sep 17 00:00:00 2001 From: sbenlevy Date: Sun, 14 Dec 2025 16:52:49 -0500 Subject: [PATCH] Fix Google Contacts caller ID lookup failing for US numbers unless NANP format is used Fix Google Contacts caller ID lookup failing for US numbers unless NANP format is used --- Superfecta.class.php | 108 ++-- .../Google_Service_ReadContacts.php | 490 +++++++++--------- 2 files changed, 322 insertions(+), 276 deletions(-) diff --git a/Superfecta.class.php b/Superfecta.class.php index a4adc425..49c8e139 100644 --- a/Superfecta.class.php +++ b/Superfecta.class.php @@ -39,6 +39,52 @@ function __construct($options=array()) { private $destination = null; private $agi = null; + /* -------------------- Added: safe logging controls (non-breaking) -------------------- */ + /** @var int $logLevel Current log threshold. 1=default (all existing out() calls map here) */ + private $logLevel = 1; + + /** @var bool $suppressWebEcho When true, out() won’t echo to web (keeps AGI verbose only) */ + private $suppressWebEcho = false; + + /** + * Change log level (for future use). Existing out() calls use level=1, + * so behavior remains identical unless you raise this threshold. + */ + public function setLogLevel($level) { + $lvl = intval($level); + if ($lvl < 0) { $lvl = 0; } + $this->logLevel = $lvl; + } + + /** Suppress web echo (still logs via AGI->verbose when available). */ + public function setSuppressWebEcho($bool) { + $this->suppressWebEcho = (bool)$bool; + } + + /** + * Safe logger used by out(). Honors logLevel and suppressWebEcho. + * Non-breaking: if you never call setLogLevel/setSuppressWebEcho, + * behavior is identical to the original out(). + */ + private function safeLog($message, $level = 1) { + if ($level > $this->logLevel) { + return; + } + // Prefer AGI verbose when available (typical at call time) + if (is_object($this->agi)) { + $this->agi->verbose($message); + return; + } + // Otherwise echo to web/CLI unless suppressed for web + if (php_sapi_name() != "cli") { + if ($this->suppressWebEcho) { return; } + echo "".$message."
"; + } else { + echo $message."\n"; + } + } + /* ------------------ End: safe logging controls (non-breaking) ------------------ */ + public function install() { $sql = "SELECT * FROM superfectaconfig LIMIT 1;"; $res = $this->Database->query($sql); @@ -83,14 +129,9 @@ public function chownFreepbx() { ]; } + /** Non-breaking: route legacy out() via safeLog(level=1) */ private function out($message) { - if(is_object($this->agi)) { - $this->agi->verbose($message); - } elseif (php_sapi_name() != "cli") { - echo "".$message."
"; - } else { - echo $message."\n"; - } + $this->safeLog($message, 1); } public function setAgi($agi) { @@ -135,7 +176,7 @@ public function execute($scheme='ALL', $request=[], $debug=0, $keepGoing=false) ); foreach ($schemes as $s) { - try{ + try{ $this->out(""); $this->out(sprintf(_("Starting scheme %s"),$s['name'])); //reset these each time @@ -160,7 +201,7 @@ public function execute($scheme='ALL', $request=[], $debug=0, $keepGoing=false) $superfecta = NEW \superfecta_single($options); break; } - + $superfecta->setDebug($debug); $superfecta->setCLI(true); $superfecta->setDID($did); @@ -174,7 +215,7 @@ public function execute($scheme='ALL', $request=[], $debug=0, $keepGoing=false) $this->out(_("No matching DID rules. Skipping scheme")); continue; } - + // Determine if the CID matches any patterns defined for this scheme $rule_match = $superfecta->match_pattern_all((isset($options['scheme_settings']['CID_rules'])) ? $options['scheme_settings']['CID_rules'] : '', $cnum); if ($rule_match['number']) { @@ -222,39 +263,36 @@ public function execute($scheme='ALL', $request=[], $debug=0, $keepGoing=false) if (!empty($callerid)) { $found = true; - $this->out(sprintf(_("Caller ID before strip_tags: %s, length: %s"), $callerid,strlen($callerid)) . ((function_exists('mb_strlen')) ? (sprintf(_(", mb_strlen: %s"),mb_strlen($callerid))) : '')); + // $this->out(sprintf(_("Caller ID before strip_tags: %s, length: %s"), $callerid,strlen($callerid)) . ((function_exists('mb_strlen')) ? (sprintf(_(", mb_strlen: %s"),mb_strlen($callerid))) : '')); $callerid = trim(strip_tags($callerid)); - $this->out(sprintf(_("Caller ID after strip_tags: %s, length: %s"), $callerid,strlen($callerid)) . ((function_exists('mb_strlen')) ? (sprintf(_(", mb_strlen: %s"),mb_strlen($callerid))) : '')); - $stripAccentsCharacters = (isset($options['scheme_settings']['Strip_Accent_Characters'])) ? $options['scheme_settings']['Strip_Accent_Characters'] : 'Y'; // Default to stripping accent character for backward compatibility - $this->out("Strip_Accent_Characters: " . $stripAccentsCharacters); + // $this->out(sprintf(_("Caller ID after strip_tags: %s, length: %s"), $callerid,strlen($callerid)) . ((function_exists('mb_strlen')) ? (sprintf(_(", mb_strlen: %s"),mb_strlen($callerid))) : '')); + $stripAccentsCharacters = (isset($options['scheme_settings']['Strip_Accent_Characters'])) ? $options['scheme_settings']['Strip_Accent_Characters'] : 'Y'; + // $this->out("Strip_Accent_Characters: " . $stripAccentsCharacters); if ($superfecta->isCharSetIA5() && $stripAccentsCharacters == 'Y') { - $this->out(sprintf(_("Caller ID before stripAccents: %s, length: %s"), $callerid,strlen($callerid)) . ((function_exists('mb_strlen')) ? (sprintf(_(", mb_strlen: %s"),mb_strlen($callerid))) : '')); + // $this->out(sprintf(_("Caller ID before stripAccents: %s, length: %s"), $callerid,strlen($callerid)) . ((function_exists('mb_strlen')) ? (sprintf(_(", mb_strlen: %s"),mb_strlen($callerid))) : '')); $callerid = $superfecta->stripAccents($callerid); - $this->out(sprintf(_("Caller ID after stripAccents: %s, length: %s"), $callerid,strlen($callerid)) . ((function_exists('mb_strlen')) ? (sprintf(_(", mb_strlen: %s"),mb_strlen($callerid))) : '')); + // $this->out(sprintf(_("Caller ID after stripAccents: %s, length: %s"), $callerid,strlen($callerid)) . ((function_exists('mb_strlen')) ? (sprintf(_(", mb_strlen: %s"),mb_strlen($callerid))) : '')); } - //Why? - $this->out(sprintf(_("Caller ID before preg_replace: %s, length: %s"), $callerid,strlen($callerid)) . ((function_exists('mb_strlen')) ? (sprintf(_(", mb_strlen: %s"),mb_strlen($callerid))) : '')); + // $this->out(sprintf(_("Caller ID before preg_replace: %s, length: %s"), $callerid,strlen($callerid)) . ((function_exists('mb_strlen')) ? (sprintf(_(", mb_strlen: %s"),mb_strlen($callerid))) : '')); $callerid = preg_replace("/[\";']/", "", $callerid); - $this->out(sprintf(_("Caller ID after preg_replace: %s, length: %s"), $callerid,strlen($callerid)) . ((function_exists('mb_strlen')) ? (sprintf(_(", mb_strlen: %s"),mb_strlen($callerid))) : '')); - - // Display issues on phones and CDR with special characters - // convert CNAM to UTF-8 to fix + // $this->out(sprintf(_("Caller ID after preg_replace: %s, length: %s"), $callerid,strlen($callerid)) . ((function_exists('mb_strlen')) ? (sprintf(_(", mb_strlen: %s"),mb_strlen($callerid))) : '')); + $character_encodings = (isset($options['scheme_settings']['Character_Encodings'])) ? $options['scheme_settings']['Character_Encodings'] : self::DEFAULT_CHARACTER_ENCODINGS; - $this->out(sprintf(_("Character Encodings: '%s'"), $character_encodings)); + // $this->out(sprintf(_("Character Encodings: '%s'"), $character_encodings)); if (in_array('pass',explode(',', $character_encodings))){ - $this->out(_("Bypassing character conversion.")); + $this->out(_("Bypassing character conversion.")); } elseif(!function_exists('mb_convert_encoding')) { $this->out(_("Function mb_convert_encoding does not exist.")); } else{ - $this->out(_("Converting result to UTF-8")); + // $this->out(_("Converting result to UTF-8")); try{ - $this->out(sprintf(_("Caller ID before mb_convert_encoding: %s, length: %s"), $callerid,strlen($callerid)) . ((function_exists('mb_strlen')) ? (sprintf(_(", mb_strlen: %s"),mb_strlen($callerid))) : '')); + // $this->out(sprintf(_("Caller ID before mb_convert_encoding: %s, length: %s"), $callerid,strlen($callerid)) . ((function_exists('mb_strlen')) ? (sprintf(_(", mb_strlen: %s"),mb_strlen($callerid))) : '')); $callerid = mb_convert_encoding($callerid, "UTF-8", $character_encodings); - $this->out(sprintf(_("Caller ID after mb_convert_encoding: %s, length: %s"), $callerid,strlen($callerid)) . ((function_exists('mb_strlen')) ? (sprintf(_(", mb_strlen: %s"),mb_strlen($callerid))) : '')); + // $this->out(sprintf(_("Caller ID after mb_convert_encoding: %s, length: %s"), $callerid,strlen($callerid)) . ((function_exists('mb_strlen')) ? (sprintf(_(", mb_strlen: %s"),mb_strlen($callerid))) : '')); } catch(Exception $e) { $this->out(sprintf(_('Caught exception calling mb_convert_encoding: %s'), $e->getMessage())); @@ -262,11 +300,11 @@ public function execute($scheme='ALL', $request=[], $debug=0, $keepGoing=false) } - $this->out(sprintf(_("Caller_Id_Max_Length: '%s'"),$Caller_Id_Max_Length)); + // $this->out(sprintf(_("Caller_Id_Max_Length: '%s'"),$Caller_Id_Max_Length)); if ($Caller_Id_Max_Length != -1){ - $this->out(sprintf(_("Caller ID before %s: %s, length: %s"), ((function_exists('mb_substr')) ? 'mb_substr' : 'substr'), $callerid,strlen($callerid)) . ((function_exists('mb_strlen')) ? (sprintf(_(", mb_strlen: %s"),mb_strlen($callerid))) : '')); + // $this->out(sprintf(_("Caller ID before %s: %s, length: %s"), ((function_exists('mb_substr')) ? 'mb_substr' : 'substr'), $callerid,strlen($callerid)) . ((function_exists('mb_strlen')) ? (sprintf(_(", mb_strlen: %s"),mb_strlen($callerid))) : '')); $callerid = (function_exists('mb_substr')) ? mb_substr($callerid, 0, $Caller_Id_Max_Length) : substr($callerid, 0, $Caller_Id_Max_Length); - $this->out(sprintf(_("Caller ID after %s: %s, length: %s"), ((function_exists('mb_substr')) ? 'mb_substr' : 'substr'), $callerid,strlen($callerid)) . ((function_exists('mb_strlen')) ? (sprintf(_(", mb_strlen: %s"),mb_strlen($callerid))) : '')); + // $this->out(sprintf(_("Caller ID after %s: %s, length: %s"), ((function_exists('mb_substr')) ? 'mb_substr' : 'substr'), $callerid,strlen($callerid)) . ((function_exists('mb_strlen')) ? (sprintf(_(", mb_strlen: %s"),mb_strlen($callerid))) : '')); } else{ $this->out(sprintf(_("Caller ID string length is not limited"))); @@ -285,9 +323,9 @@ public function execute($scheme='ALL', $request=[], $debug=0, $keepGoing=false) } if ($Caller_Id_Max_Length != -1){ - $this->out(sprintf(_("Caller ID before %s: %s, length: %s"), ((function_exists('mb_substr')) ? 'mb_substr' : 'substr'), $callerid,strlen($callerid)) . ((function_exists('mb_strlen')) ? (sprintf(_(", mb_strlen: %s"),mb_strlen($callerid))) : '')); + // $this->out(sprintf(_("Caller ID before %s: %s, length: %s"), ((function_exists('mb_substr')) ? 'mb_substr' : 'substr'), $callerid,strlen($callerid)) . ((function_exists('mb_strlen')) ? (sprintf(_(", mb_strlen: %s"),mb_strlen($callerid))) : '')); $callerid = (function_exists('mb_substr')) ? mb_substr($callerid, 0, $Caller_Id_Max_Length) : substr($callerid, 0, $Caller_Id_Max_Length); - $this->out(sprintf(_("Caller ID after %s: %s, length: %s"), ((function_exists('mb_substr')) ? 'mb_substr' : 'substr'), $callerid,strlen($callerid)) . ((function_exists('mb_strlen')) ? (sprintf(_(", mb_strlen: %s"),mb_strlen($callerid))) : '')); + // $this->out(sprintf(_("Caller ID after %s: %s, length: %s"), ((function_exists('mb_substr')) ? 'mb_substr' : 'substr'), $callerid,strlen($callerid)) . ((function_exists('mb_strlen')) ? (sprintf(_(", mb_strlen: %s"),mb_strlen($callerid))) : '')); } //Set Spam Destination @@ -319,7 +357,7 @@ public function execute($scheme='ALL', $request=[], $debug=0, $keepGoing=false) $this->out(sprintf(_('Caught exception: %s
Skipping scheme %s
'), $e->getMessage(), $s['name'])); } } - + if(empty($callerid) && !$keepGoing) { //No callerid so I guess? return $trunk_info['calleridname']; @@ -390,7 +428,7 @@ public function ajaxCustomHandler() { $schem = htmlEntities($_REQUEST['scheme']); $thedid = htmlEntities($_REQUEST['thedid']); if (empty($thedid)){ - $thedid = '5555555555'; + $thedid = '5555555555'; } echo ""._('Debug is on and set at level:')." ". $level."
"; echo ""._('The DID:')." ".$thedid."
"; @@ -868,4 +906,4 @@ public function didList($id = false){ $results = $stmt->fetchAll(PDO::FETCH_ASSOC); return is_array($results) ? $results : array(); } -} +} \ No newline at end of file diff --git a/includes/oauth-google/Google_Service_ReadContacts.php b/includes/oauth-google/Google_Service_ReadContacts.php index 416bad44..c66d3328 100644 --- a/includes/oauth-google/Google_Service_ReadContacts.php +++ b/includes/oauth-google/Google_Service_ReadContacts.php @@ -2,25 +2,23 @@ /* * Copyright 2010 Google Inc. * -* Licensed under the Apache License, Version 2.0 ( the "License" ); -you may not -* use this file except in compliance with the License. You may obtain a copy of -* the License at +* Licensed under the Apache License, Version 2.0 (the "License" ); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software -* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -* License for the specific language governing permissions and limitations under -* the License. +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. */ -// based on original work from the PHP Laravel framework -if ( !function_exists( 'str_contains' ) ) { - function str_contains( $haystack, $needle ) { - return $needle !== '' && mb_strpos( $haystack, $needle ) !== false; - } +if (!function_exists('str_contains')) { + function str_contains($haystack, $needle) { + return $needle !== '' && mb_strpos($haystack, $needle) !== false; + } } require_once 'Google/Service.php'; @@ -28,255 +26,265 @@ function str_contains( $haystack, $needle ) { #[AllowDynamicProperties] class Google_Service_ReadContacts { - const SCOPE_CONTACTS_READONLY = "https://www.googleapis.com/auth/contacts.readonly"; - const BASE_URL = "https://people.googleapis.com/v1/people:searchContacts"; - - private $query; - private $gam; - - /** - * Constructs the internal representation of the Admin service. - * - * @param Google_Client $client - */ - - public function __construct( GoogleAuthManager $authManager ) { - $this->gam = $authManager; - } - - public function setAccessToken( $at ) { - $this->access_token = $at; - } - - public function getContactsForNumberStarting( $query ) { - $appendPhoneTypes = true; - $useNickNames = true; - $displayLastNameFirstName = false; - try { - // echo '



in getContactsForNumberStarting: '.json_encode( $this ).'


'; - $append_phone_types = $this->gam->append_phone_types; - $use_nicknames = $this->gam->use_nicknames; - - if ( $append_phone_types === 'on' ) { - $appendPhoneTypes = true; - } else { - $appendPhoneTypes = false; - } - if ( $this->gam->use_nicknames === 'on' ) { - $useNickNames = true; - } else { - $useNickNames = false; - } - } catch( Exception $e ) { - //echo 'Unable to get phone Type Flag: '.$e->getMessage().'
'; - } + // Expected by Superfecta + const SCOPE_CONTACTS = "https://www.googleapis.com/auth/contacts"; + const SCOPE_CONTACTS_READONLY = "https://www.googleapis.com/auth/contacts.readonly"; - try { - if ( $this->gam->display_lastname_firstname === 'on' ) { - $displayLastNameFirstName = true; - } else { - $displayLastNameFirstName = false; - } - } catch( Exception $e ) { - echo 'Unable to get Last Name First Name Flag Flag: '.$e->getMessage().'
'; - } + const BASE_URL = "https://people.googleapis.com/v1/people:searchContacts"; - if ( $appendPhoneTypes ) { - echo 'Appending Phone Types to names.
'; - } else { - echo 'Not Appending Phone Types to names.
'; - } + private $query; + private $gam; + private $access_token; - if ( $useNickNames ) { - echo 'Using Nicknames when available.
'; - } else { - echo 'Not using Nicknames.
'; - } - if ( $displayLastNameFirstName ) { - echo 'Displaying Last Name, First Name format
'; - } else { - echo 'Displaying Default Name format.
'; + // Behavior flags + private $stopAfterFirstMatch = false; + + // Debug control + private $debugEnabled = true; + private $last_http_code = null; + + public function __construct(GoogleAuthManager $authManager) { + $this->gam = $authManager; + + try { + if (isset($this->gam->debug_level)) { + $lvl = intval($this->gam->debug_level); + $this->debugEnabled = ($lvl > 0); + } elseif (isset($this->gam->debug)) { + $this->debugEnabled = ($this->gam->debug === 'on' || $this->gam->debug === 1 || intval($this->gam->debug) > 0); + } + } catch (\Throwable $e) {} + + try { + if (isset($this->gam->stop_after_first_match) && $this->gam->stop_after_first_match === 'on') { + $this->stopAfterFirstMatch = true; + } + } catch (\Throwable $e) {} } - $counter = 0; - $output = array(); - - $this->setAccessToken( $this->gam->getAccessToken() ); - $query_number = $this->cleanNumber( $query ); - $len_query = strlen( $query_number ); - $this->query = $query_number; - - $result = $this->curl_file_get_contents( $this->constructFinalUrl() ); - //echo 'result:
'.$result.'
'; - $result_json = json_decode( $result ); - dbug( $result_json ); - - if ( isset( $result_json->error ) ) { - echo 'Error getting result:
'.$result.'
'; - $results = array( 'success' => 'no', 'data' => json_encode( $result_json->error ) ); - return $results; + + public function setAccessToken($at) { + $this->access_token = $at; } - try { - $count = 0; - if ( isset( $result_json->results ) ) { - foreach ( $result_json->results as $entry ) { - $count++; - $name = ""; - try { - if ( $useNickNames and isset( $entry->person->nicknames ) ) { - $name = $entry->person->nicknames[0]->value; - dbug( $name ); - echo '
Nickname: '.$name; - } elseif ( isset( $entry->person->names ) ) { - if ($displayLastNameFirstName){ - $name = $entry->person->names[0]->displayNameLastFirst; - } - else{ - $name = $entry->person->names[0]->displayName; - } - dbug( $name ); - echo '
Name: '.$name; - } elseif ( isset( $entry->person->organizations ) ) { - $name = $entry->person->organizations[0]->name; - dbug( $name ); - echo '
Organization: '.$name; - } - echo '
'; - - if ( !empty( $name ) ) { - if ( isset( $entry->person->phoneNumbers ) ) { - foreach ( $entry->person->phoneNumbers as $phoneNumber ) { - $nameWithType = $name; - try { - if ( $appendPhoneTypes and isset( $phoneNumber->formattedType ) ) { - $nameWithType = $name.' ('.$phoneNumber->formattedType.')'; - } - } catch( Exception $e ) { - echo 'Unable to get phone number type for '.$name.': '.$e->getMessage().'
'; - } - - try { - if ( isset( $phoneNumber->canonicalForm ) ) { - $no = $phoneNumber->canonicalForm; - echo $nameWithType.', Canonical Form: '.$no.'
'; - $score = $this->subStringScore( $no, $query_number ); - if ( $score > 0 ) { - $output[$counter] = $option = array( 'name' => $nameWithType, 'number' => $no, 'score' => $score ); - $counter++; - echo $counter.'. '.$nameWithType.', '.$no.', Score:'.$score.'
'; - } + public function getContactsForNumberStarting($query) { + $appendPhoneTypes = true; + $useNickNames = true; + $displayLastNameFirstName = false; + + try { $appendPhoneTypes = ($this->gam->append_phone_types === 'on'); } catch (\Throwable $e) {} + try { $useNickNames = ($this->gam->use_nicknames === 'on'); } catch (\Throwable $e) {} + try { $displayLastNameFirstName = ($this->gam->display_lastname_firstname === 'on'); } catch (\Throwable $e) {} + + $this->debugEcho($appendPhoneTypes ? "Appending Phone Types to names.
" : "Not Appending Phone Types to names.
"); + $this->debugEcho($useNickNames ? "Using Nicknames when available.
" : "Not using Nicknames.
"); + $this->debugEcho($displayLastNameFirstName ? "Displaying Last Name, First Name format
" : "Displaying Default Name format.
"); + + $this->setAccessToken($this->gam->getAccessToken()); + + // 1) E.164 first + $queryE164 = $this->normalizeToE164($query); + $this->debugEcho("Normalized query number (E.164): {$queryE164}
"); + $result_json = $this->performQueryAndDecode($queryE164); + + // 2) NANP format + if (!$this->hasResults($result_json)) { + $this->debugEcho("No results with E.164, retrying with NANP format
"); + $queryNANP = $this->formatNANP($query); + $result_json = $this->performQueryAndDecode($queryNANP); + } + + // 3) Raw digits + if (!$this->hasResults($result_json)) { + $this->debugEcho("No results with NANP, retrying with raw digits
"); + $queryDigits = preg_replace('/\D+/', '', $query); + $result_json = $this->performQueryAndDecode($queryDigits); + } + + if (isset($result_json->error)) { + $this->debugEcho('Error getting result:
' . htmlspecialchars(json_encode($result_json->error)) . '
'); + return ['success' => 'no', 'data' => json_encode($result_json->error)]; + } + + $output = []; + $counter = 0; + + if (isset($result_json->results)) { + foreach ($result_json->results as $entry) { + try { + $name = ""; + if ($useNickNames && isset($entry->person->nicknames)) { + $name = $entry->person->nicknames[0]->value; + $this->debugEcho("
Nickname: " . htmlspecialchars($name)); + } elseif (isset($entry->person->names)) { + $name = $displayLastNameFirstName ? + $entry->person->names[0]->displayNameLastFirst : + $entry->person->names[0]->displayName; + $this->debugEcho("
Name: " . htmlspecialchars($name)); + } elseif (isset($entry->person->organizations)) { + $name = $entry->person->organizations[0]->name; + $this->debugEcho("
Organization: " . htmlspecialchars($name)); } - } catch( Exception $e ) { - echo 'Error Parsing Canonical phone number for '.$nameWithType.': ' .$e->getMessage().'
'; - } - - try { - if ( isset( $phoneNumber->value ) ) { - $no = $this->cleanNumber( $phoneNumber->value ); - echo $nameWithType.', Value: '.$no.'
'; - $score = $this->subStringScore( $no, $query_number ); - if ( $score > 0 ) { - $counter++; - $output[$counter] = $option = array( 'name' => $nameWithType, 'number' => $no, 'score' => $score ); - echo $counter.'. '.$nameWithType.', '.$no.', Score:'.$score.'
'; - } + + if (!empty($name) && isset($entry->person->phoneNumbers)) { + foreach ($entry->person->phoneNumbers as $phoneNumber) { + $nameWithType = $name; + if ($appendPhoneTypes && isset($phoneNumber->formattedType)) { + $nameWithType = $name . ' (' . $phoneNumber->formattedType . ')'; + } + + if (isset($phoneNumber->canonicalForm)) { + $no = $phoneNumber->canonicalForm; + $this->debugEcho($this->h("$nameWithType, Canonical Form: $no") . "
"); + $score = $this->subStringScore($no, $queryE164); + if ($score > 0) { + $output[$counter] = ['name' => $nameWithType, 'number' => $no, 'score' => $score]; + $counter++; + $this->debugEcho($this->h("$counter. $nameWithType, $no, Score:$score") . "
"); + if ($this->stopAfterFirstMatch) { + $this->debugEcho("Stopping after first match (flag enabled)
"); + return ['success' => 'yes', 'data' => $output]; + } + } + } + + if (isset($phoneNumber->value)) { + $noRaw = $this->cleanNumber($phoneNumber->value); + $this->debugEcho($this->h("$nameWithType, Value: $noRaw") . "
"); + $score = $this->subStringScore($noRaw, $queryE164); + if ($score > 0) { + $output[$counter] = ['name' => $nameWithType, 'number' => $noRaw, 'score' => $score]; + $counter++; + $this->debugEcho($this->h("$counter. $nameWithType, $noRaw, Score:$score") . "
"); + if ($this->stopAfterFirstMatch) { + $this->debugEcho("Stopping after first match (flag enabled)
"); + return ['success' => 'yes', 'data' => $output]; + } + } + } + } } - } catch( Exception $e ) { - echo 'Error Parsing phone number for '.$nameWithType.': ' .$e->getMessage().'
'; - } + } catch (\Throwable $e) { + $this->debugEcho('Message: ' . $this->h($e->getMessage()) . '
'); } - } } - } catch( Exception $e ) { - echo 'Message 2: ' .$e->getMessage().'
'; - } } - } - } catch( Exception $e ) { - echo 'Message 3: ' .$e->getMessage().'
'; + + $this->debugEcho('Found ' . $counter . ' matches
'); + return ['success' => 'yes', 'data' => $output]; } - echo 'found '.$counter.' matches
'; - $results = array( 'success' => 'yes', 'data' => $output ); - if ( sizeof( $results['data'] ) > 0 ) { - return $results; + /* -------------------- Helpers -------------------- */ + + private function performQueryAndDecode($queryString) { + $this->query = $queryString; + $url = $this->constructFinalUrl(); + $this->debugEcho('URL: ' . $this->h($url) . '
'); + + $result = $this->curl_file_get_contents($url); + + $this->debugEcho("cURL HTTP Code: " . $this->last_http_code . "
"); + $this->debugEcho("Raw API result:
" . $this->prettyOrRaw($result) . "
"); + + $decoded = json_decode($result); + return $decoded ?: (object)[]; } - if ( substr( $query, 0, 1 ) == '+' ) { - // No matches - return $results; + private function hasResults($result_json) { + return (isset($result_json->results) && !empty($result_json->results)); } - // For US Numbers try to prefix number with 1 - if ( substr( $query, 0, 1 ) != '1' ) { - $query1 = '1'.$query; - echo '
Searching Google Contacts for number with a 1 appended: '.$query1.'
'; - $results = $this->getContactsForNumberStarting( $query1 ); - if ( sizeof( $results['data'] ) > 0 ) { - //we have some matches - return $results; - } - // We didn't find a match with a number starting with 1 + private function normalizeToE164($number) { + // Always strip all non-digits except leading '+' + $clean = preg_replace('/[^\d+]/', '', $number); + + if (substr($clean, 0, 1) === '+') { + return $clean; + } + if (strlen($clean) === 10) { + return '+1' . $clean; + } elseif (strlen($clean) === 11 && substr($clean, 0, 1) === '1') { + return '+' . $clean; + } + return '+' . $clean; } - // Try to prefix + sign - $query1 = '+'.$query; - echo '
Searching Google Contacts for number with a + sign appended: '.$query1.'
'; - $results = $this->getContactsForNumberStarting($query1); - //echo sizeof($results['data']).'
'; - if (sizeof($results['data']) > 0){ - //we have some matches - return $results; + private function formatNANP($number) { + $digits = preg_replace('/\D+/', '', $number); + if (strlen($digits) == 10) { + return '(' . substr($digits, 0, 3) . ') ' . substr($digits, 3, 3) . '-' . substr($digits, 6); + } elseif (strlen($digits) == 11 && substr($digits, 0, 1) == '1') { + return '(' . substr($digits, 1, 3) . ') ' . substr($digits, 4, 3) . '-' . substr($digits, 7); + } + return $number; } - // No Matches - return $results; - } - - private function cleanNumber($number) { - $result = preg_replace('/[^0-9+]*/', '', $number); - return $result; - } - - private function subStringScore($number, $prefix) { - if (str_contains($number, $prefix)){ - $result = strlen($prefix); + + private function cleanNumber($number) { + return preg_replace('/[^0-9+]*/', '', $number); } - elseif (str_contains($prefix, $number)){ - $result = strlen($number); + + private function subStringScore($number, $prefix) { + if (str_contains($number, $prefix)) return strlen($prefix); + if (str_contains($prefix, $number)) return strlen($number); + return 0; } - else{ - $result = 0; + + private function constructFinalUrl() { + $url = self::BASE_URL; + $url .= '?readMask=names,nicknames,organizations,phoneNumbers'; + $url .= '&access_token=' . $this->access_token; + if (isset($this->query)) $url .= '&query=' . urlencode($this->query); + return $url; } - return $result; - } - - private function constructFinalUrl() { - $result = Google_Service_ReadContacts::BASE_URL; - $result .= '?readMask=names,nicknames,organizations,phoneNumbers'; - $result .= '&access_token='.$this->access_token; - if (isset($this->query)) $result .= '&query='.$this->query; - dbug($result); - echo 'url: '.$result.'
'; - return $result; - } - - private function curl_file_get_contents($url) { - $curl = curl_init(); - $userAgent = 'Mozilla/4.0 ( compatible;MSIE 6.0;Windows NT 5.1;.NET CLR 1.1.4322 )'; - - curl_setopt($curl, CURLOPT_URL, $url); //The URL to fetch. This can also be set when initializing a session with curl_init(). - curl_setopt($curl, CURLOPT_RETURNTRANSFER, TRUE); //TRUE to return the transfer as a string of the return value of curl_exec() instead of outputting it out directly. - curl_setopt($curl, CURLOPT_CONNECTTIMEOUT, 5); //The number of seconds to wait while trying to connect. - - curl_setopt($curl, CURLOPT_USERAGENT, $userAgent); //The contents of the "User-Agent: " header to be used in a HTTP request. - curl_setopt($curl, CURLOPT_FOLLOWLOCATION, TRUE); //To follow any "Location: " header that the server sends as part of the HTTP header. - curl_setopt($curl, CURLOPT_AUTOREFERER, TRUE); //To automatically set the Referer: field in requests where it follows a Location: redirect. - curl_setopt($curl, CURLOPT_TIMEOUT, 10); //The maximum number of seconds to allow cURL functions to execute. - curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, 0); //To stop cURL from verifying the peer's certificate. - curl_setopt( $curl, CURLOPT_SSL_VERIFYHOST, 0 ); - - $contents = curl_exec( $curl ); - curl_close( $curl ); - return $contents; + + private function curl_file_get_contents($url) { + $curl = curl_init(); + $userAgent = 'Mozilla/5.0 (Superfecta GoogleContacts)'; + + curl_setopt($curl, CURLOPT_URL, $url); + curl_setopt($curl, CURLOPT_RETURNTRANSFER, true); + curl_setopt($curl, CURLOPT_CONNECTTIMEOUT, 5); + curl_setopt($curl, CURLOPT_TIMEOUT, 10); + curl_setopt($curl, CURLOPT_USERAGENT, $userAgent); + curl_setopt($curl, CURLOPT_FOLLOWLOCATION, true); + curl_setopt($curl, CURLOPT_AUTOREFERER, true); + curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, 0); + curl_setopt($curl, CURLOPT_SSL_VERIFYHOST, 0); + curl_setopt($curl, CURLOPT_IPRESOLVE, CURL_IPRESOLVE_V4); + + $contents = curl_exec($curl); + $this->last_http_code = curl_getinfo($curl, CURLINFO_HTTP_CODE); + + if ($contents === false) { + $err = curl_error($curl); + $this->debugEcho('cURL Error: ' . $this->h($err) . '
'); + } + + curl_close($curl); + return $contents ?: ''; } - } + + private function prettyOrRaw($result) { + if (!$this->debugEnabled) return ''; + $decodedAssoc = json_decode($result, true); + if (json_last_error() === JSON_ERROR_NONE) { + $pretty = json_encode($decodedAssoc, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES); + $pretty = preg_replace_callback('/^( +)/m', function($m) { + $spaces = strlen($m[1]); + $groups = intdiv($spaces, 4); + return str_repeat(' ', max(1, $groups)); + }, $pretty); + return '
' . htmlspecialchars($pretty) . '
'; + } + return '
' . htmlspecialchars($result) . '
'; + } + + private function debugEcho($msg) { + if ($this->debugEnabled) { + echo $msg; + } + } + + private function h($s) { + return htmlspecialchars($s, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8'); + } +}