1<?php
2/**
3 * Copyright 1999-2016 Horde LLC (http://www.horde.org/)
4 *
5 * See the enclosed file COPYING for license information (LGPL). If you
6 * did not receive this file, see http://www.horde.org/licenses/lgpl21.
7 *
8 * @author   Jon Parise <jon@horde.org>
9 * @author   Chuck Hagenbuch <chuck@horde.org>
10 * @author   Jan Schneider <jan@horde.org>
11 * @author   Michael Slusarz <slusarz@horde.org>
12 * @category Horde
13 * @license  http://www.horde.org/licenses/lgpl21 LGPL 2.1
14 * @package  Nls
15 */
16
17/**
18 * The Horde_Nls class provides Native Language Support.
19 *
20 * This includes common methods for handling language data, timezones, and
21 * hostname->country lookups.
22 *
23 * @author   Jon Parise <jon@horde.org>
24 * @author   Chuck Hagenbuch <chuck@horde.org>
25 * @author   Jan Schneider <jan@horde.org>
26 * @author   Michael Slusarz <slusarz@horde.org>
27 * @category Horde
28 * @license  http://www.horde.org/licenses/lgpl21 LGPL 2.1
29 * @package  Nls
30 */
31class Horde_Nls
32{
33    /**
34     * DNS resolver.
35     *
36     * @var Net_DNS2_Resolver
37     */
38    public static $dnsResolver;
39
40    /**
41     * Cached values.
42     *
43     * @var array
44     */
45    protected static $_cache = array();
46
47    /**
48     * Check to see if character set is valid for htmlspecialchars() calls.
49     *
50     * @param string $charset  The character set to check.
51     *
52     * @return boolean  Is charset valid for the current system?
53     */
54    public static function checkCharset($charset)
55    {
56        if (is_null($charset) || empty($charset)) {
57            return false;
58        }
59
60        $valid = true;
61
62        ini_set('track_errors', 1);
63        @htmlspecialchars('', ENT_COMPAT, $charset);
64        if (isset($php_errormsg)) {
65            $valid = false;
66        }
67        ini_restore('track_errors');
68
69        return $valid;
70    }
71
72    /**
73     * Returns a list of available timezones.
74     *
75     * @return array  List of timezones.
76     */
77    public static function getTimezones()
78    {
79        $timezones = DateTimeZone::listIdentifiers();
80        return array_combine($timezones, $timezones);
81    }
82
83    /**
84     * Returns a list of available timezones, including timezone abbreviations.
85     *
86     * Contrary to getTimezones() the timezone IDs are values and the timezone
87     * labels are the keys, to allow multiple labels for the same timezone.
88     *
89     * @since 2.2.0
90     *
91     * @return array  List of timezones.
92     */
93    public static function getTimezonesWithAbbreviations()
94    {
95        $timezones = array_flip(self::getTimezones());
96        foreach (DateTimeZone::listAbbreviations() as $abbreviation => $timezone) {
97            $abbreviation = Horde_String::upper($abbreviation);
98            if ($abbreviation == 'UTC' || strlen($abbreviation) < 2) {
99                continue;
100            }
101            $timezones[$abbreviation] = $timezone[0]['timezone_id'];
102        }
103        return $timezones;
104    }
105
106    /**
107     * Get the locale info returned by localeconv(), but cache it, to
108     * avoid repeated calls.
109     *
110     * @return array  The results of localeconv().
111     */
112    public static function getLocaleInfo()
113    {
114        if (!isset(self::$_cache['lc_info'])) {
115            self::$_cache['lc_info'] = localeconv();
116        }
117
118        return self::$_cache['lc_info'];
119    }
120
121    /**
122     * Get the language info returned by nl_langinfo(), but cache it, to
123     * avoid repeated calls.
124     *
125     * @param const $item  The langinfo item to return.
126     *
127     * @return array  The results of nl_langinfo().
128     */
129    public static function getLangInfo($item)
130    {
131        if (!function_exists('nl_langinfo')) {
132            return false;
133        }
134
135        if (!isset(self::$_cache['nl_info'])) {
136            self::$_cache['nl_info'] = array();
137        }
138
139        if (!isset(self::$_cache['nl_info'][$item])) {
140            self::$_cache['nl_info'][$item] = nl_langinfo($item);
141        }
142
143        return self::$_cache['nl_info'][$item];
144    }
145
146    /**
147     * Get country information from a hostname or IP address.
148     *
149     * @param string $host      The hostname or IP address.
150     * @param string $datafile  The datafile for the GeoIP lookup. If not set,
151     *                          will skip this lookup.
152     *
153     * @return mixed  On success, return an array with the following entries:
154     *                'code'  =>  Country Code
155     *                'name'  =>  Country Name
156     *                On failure, return false.
157     */
158    public static function getCountryByHost($host, $datafile = null)
159    {
160        /* List of generic domains that we know is not in the country TLD
161           list. See: http://www.iana.org/gtld/gtld.htm */
162        $generic = array(
163            'aero', 'biz', 'com', 'coop', 'edu', 'gov', 'info', 'int', 'mil',
164            'museum', 'name', 'net', 'org', 'pro'
165        );
166
167        $checkHost = null;
168        if (preg_match('/^\d+\.\d+\.\d+\.\d+$/', $host)) {
169            if (isset(self::$dnsResolver)) {
170                try {
171                    $response = self::$dnsResolver->query($host, 'PTR');
172                    foreach ($response->answer as $val) {
173                        if (isset($val->ptrdname)) {
174                            $checkHost = $val->ptrdname;
175                            break;
176                        }
177                    }
178                } catch (Net_DNS2_Exception $e) {}
179            }
180            if (is_null($checkHost)) {
181                $checkHost = @gethostbyaddr($host);
182            }
183        } else {
184            $checkHost = $host;
185        }
186
187        /* Get the TLD of the hostname. */
188        $pos = strrpos($checkHost, '.');
189        if ($pos === false) {
190            return false;
191        }
192        $domain = Horde_String::lower(substr($checkHost, $pos + 1));
193
194        /* Try lookup via TLD first. */
195        if (!in_array($domain, $generic)) {
196            $name = self::tldLookup($domain);
197            if ($name) {
198                return array(
199                    'code' => $domain,
200                    'name' => $name
201                );
202            }
203        }
204
205        /* Try GeoIP lookup next. */
206        $geoip = new Horde_Nls_Geoip($datafile);
207        return $geoip->getCountryInfo($checkHost);
208    }
209
210    /**
211     * Do a top level domain (TLD) lookup.
212     *
213     * @param string $code  A 2-letter country code.
214     *
215     * @return mixed  The localized country name, or null if not found.
216     */
217    public static function tldLookup($code)
218    {
219        if (!isset(self::$_cache['tld'])) {
220            include __DIR__ . '/Nls/Tld.php';
221            self::$_cache['tld'] = $tld;
222        }
223
224        $code = Horde_String::lower($code);
225
226        return isset(self::$_cache['tld'][$code])
227            ? self::$_cache['tld'][$code]
228            : null;
229    }
230
231    /**
232     * Returns either a specific or all ISO-3166 country names.
233     *
234     * @param string $code  The ISO 3166 country code.
235     *
236     * @return mixed  If a country code has been requested will return the
237     *                corresponding country name. If empty will return an
238     *                array of all the country codes and their names.
239     */
240    public static function getCountryISO($code = null)
241    {
242        if (!isset(self::$_cache['iso3166'])) {
243            include __DIR__ . '/Nls/Countries.php';
244            self::$_cache['iso3166'] = $countries;
245        }
246
247        if (empty($code)) {
248            return self::$_cache['iso3166'];
249        }
250
251        $code = Horde_String::upper($code);
252
253        return isset(self::$_cache['iso3166'][$code])
254            ? self::$_cache['iso3166'][$code]
255            : null;
256    }
257
258    /**
259     * Returns either a specific or all ISO-639 language names.
260     *
261     * @param string $code  The ISO 639 language code.
262     *
263     * @return mixed  If a language code has been requested will return the
264     *                corresponding language name. If empty will return an
265     *                array of all the language codes (keys) and their names
266     *                (values).
267     */
268    public static function getLanguageISO($code = null)
269    {
270        if (!isset(self::$_cache['iso639'])) {
271            include __DIR__ . '/Nls/Languages.php';
272            self::$_cache['iso639'] = $languages;
273        }
274
275        if (empty($code)) {
276            return self::$_cache['iso639'];
277        }
278
279        $code = substr(Horde_String::lower(trim($code)), 0, 2);
280
281        return isset(self::$_cache['iso639'][$code])
282            ? self::$_cache['iso639'][$code]
283            : null;
284    }
285
286}
287