46f98b3142
The core plugins whose version was attached to GS's were reseted to 2.0.0. 2.0.0 was chosen as reset version for plugins because it is higher than the one that was set by inheriting GS version. Furthermore, it's a major change from prior plugin versioning system thus it also makes semantic sense. Justification for version bump: == GS == 9a4ab31f26 1.19.0c13b935201
1.18.3c13b935201
1.18.218fc39d2cf
1.18.1c083a8bcc2
1.18.0e8783d46d0
1.17.1d9a42550ff
1.17.01536d3ef29
1.16.0c03ed457a6
1.15.0d2e6519bad
1.14.2fe411e8138
1.14.1b17e0b4169
1.14.0daa5f87fd4
1.13.0d75b5d2f4a
1.11.7f6dbf66983
1.11.66cf674f8f8
1.11.57845a09b34
1.11.4e4d432295d
1.11.3339204f1ee
1.11.2a4e679a118
1.11.17967db6ff5
1.11.0bc030da320
1.10.19cc7df51d6
1.10.0bf7f17474d
1.9.28a07edec5f
1.9.10042971d74
1.9.06b5450b7e6
1.8.05dcc98d1c6
1.7.0e6667db0cd
1.6.03290227b50
1.5.0a59c439b46
1.4.0496ab8c920
1.3.10986030060b
1.3.91d529c021a
1.3.8f89c052cf8
1.3.738f2ecefac
1.3.6e473937cb9
1.3.59a39ebe66f
1.3.4ddc3cecfc0
1.3.32b43d484eb
1.3.2e8e487187e
1.3.1 == Plugins == XMPP plugine0887220b0
bump patche186ad57d0
bump patch OStatuse186ad57d0
bump patch Nodeinfoceae66a30f
bump minor586fb5a517
bump major195296846e
bump minor
513 lines
16 KiB
PHP
513 lines
16 KiB
PHP
<?php
|
|
/**
|
|
* StatusNet, the distributed open-source microblogging tool
|
|
*
|
|
* Plugin to convert string locations to Geonames IDs and vice versa
|
|
*
|
|
* PHP version 5
|
|
*
|
|
* LICENCE: This program is free software: you can redistribute it and/or modify
|
|
* it under the terms of the GNU Affero General Public License as published by
|
|
* the Free Software Foundation, either version 3 of the License, or
|
|
* (at your option) any later version.
|
|
*
|
|
* This program is distributed in the hope that it will be useful,
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
* GNU Affero General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU Affero General Public License
|
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
*
|
|
* @category Action
|
|
* @package StatusNet
|
|
* @author Evan Prodromou <evan@status.net>
|
|
* @copyright 2009 StatusNet Inc.
|
|
* @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
|
|
* @link http://status.net/
|
|
*/
|
|
|
|
if (!defined('STATUSNET')) {
|
|
exit(1);
|
|
}
|
|
|
|
/**
|
|
* Plugin to convert string locations to Geonames IDs and vice versa
|
|
*
|
|
* This handles most of the events that Location class emits. It uses
|
|
* the geonames.org Web service to convert names like 'Montreal, Quebec, Canada'
|
|
* into IDs and lat/lon pairs.
|
|
*
|
|
* @category Plugin
|
|
* @package StatusNet
|
|
* @author Evan Prodromou <evan@status.net>
|
|
* @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
|
|
* @link http://status.net/
|
|
*
|
|
* @seeAlso Location
|
|
*/
|
|
class GeonamesPlugin extends Plugin
|
|
{
|
|
const PLUGIN_VERSION = '2.0.0';
|
|
|
|
const LOCATION_NS = 1;
|
|
|
|
public $host = 'ws.geonames.org';
|
|
public $username = null;
|
|
public $token = null;
|
|
public $expiry = 7776000; // 90-day expiry
|
|
public $timeout = 2; // Web service timeout in seconds.
|
|
public $timeoutWindow = 60; // Further lookups in this process will be disabled for N seconds after a timeout.
|
|
public $cachePrefix = null; // Optional shared memcache prefix override
|
|
// to share lookups between local instances.
|
|
|
|
protected $lastTimeout = null; // timestamp of last web service timeout
|
|
|
|
/**
|
|
* convert a name into a Location object
|
|
*
|
|
* @param string $name Name to convert
|
|
* @param string $language ISO code for anguage the name is in
|
|
* @param Location &$location Location object (may be null)
|
|
*
|
|
* @return boolean whether to continue (results in $location)
|
|
*/
|
|
function onLocationFromName($name, $language, &$location)
|
|
{
|
|
$loc = $this->getCache(array('name' => $name,
|
|
'language' => $language));
|
|
|
|
if ($loc !== false) {
|
|
$location = $loc;
|
|
return false;
|
|
}
|
|
|
|
try {
|
|
$geonames = $this->getGeonames('search',
|
|
array('maxRows' => 1,
|
|
'q' => $name,
|
|
'lang' => $language,
|
|
'type' => 'xml'));
|
|
} catch (Exception $e) {
|
|
$this->log(LOG_WARNING, "Error for $name: " . $e->getMessage());
|
|
return true;
|
|
}
|
|
|
|
if (count($geonames) == 0) {
|
|
// no results
|
|
$this->setCache(array('name' => $name,
|
|
'language' => $language),
|
|
null);
|
|
return true;
|
|
}
|
|
|
|
$n = $geonames[0];
|
|
|
|
$location = new Location();
|
|
|
|
$location->lat = $this->canonical($n->lat);
|
|
$location->lon = $this->canonical($n->lng);
|
|
$location->names[$language] = (string)$n->name;
|
|
$location->location_id = (string)$n->geonameId;
|
|
$location->location_ns = self::LOCATION_NS;
|
|
|
|
$this->setCache(array('name' => $name,
|
|
'language' => $language),
|
|
$location);
|
|
|
|
// handled, don't continue processing!
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* convert an id into a Location object
|
|
*
|
|
* @param string $id Name to convert
|
|
* @param string $ns Name to convert
|
|
* @param string $language ISO code for language for results
|
|
* @param Location &$location Location object (may be null)
|
|
*
|
|
* @return boolean whether to continue (results in $location)
|
|
*/
|
|
function onLocationFromId($id, $ns, $language, &$location)
|
|
{
|
|
if ($ns != self::LOCATION_NS) {
|
|
// It's not one of our IDs... keep processing
|
|
return true;
|
|
}
|
|
|
|
$loc = $this->getCache(array('id' => $id));
|
|
|
|
if ($loc !== false) {
|
|
$location = $loc;
|
|
return false;
|
|
}
|
|
|
|
try {
|
|
$geonames = $this->getGeonames('hierarchy',
|
|
array('geonameId' => $id,
|
|
'lang' => $language));
|
|
} catch (Exception $e) {
|
|
$this->log(LOG_WARNING, "Error for ID $id: " . $e->getMessage());
|
|
return false;
|
|
}
|
|
|
|
$parts = array();
|
|
|
|
foreach ($geonames as $level) {
|
|
if (in_array($level->fcode, array('PCLI', 'ADM1', 'PPL'))) {
|
|
$parts[] = (string)$level->name;
|
|
}
|
|
}
|
|
|
|
$last = $geonames[count($geonames)-1];
|
|
|
|
if (!in_array($level->fcode, array('PCLI', 'ADM1', 'PPL'))) {
|
|
$parts[] = (string)$last->name;
|
|
}
|
|
|
|
$location = new Location();
|
|
|
|
$location->location_id = (string)$last->geonameId;
|
|
$location->location_ns = self::LOCATION_NS;
|
|
$location->lat = $this->canonical($last->lat);
|
|
$location->lon = $this->canonical($last->lng);
|
|
|
|
$location->names[$language] = implode(', ', array_reverse($parts));
|
|
|
|
$this->setCache(array('id' => (string)$last->geonameId),
|
|
$location);
|
|
|
|
// We're responsible for this namespace; nobody else
|
|
// can resolve it
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* convert a lat/lon pair into a Location object
|
|
*
|
|
* Given a lat/lon, we try to find a Location that's around
|
|
* it or nearby. We prefer populated places (cities, towns, villages).
|
|
*
|
|
* @param string $lat Latitude
|
|
* @param string $lon Longitude
|
|
* @param string $language ISO code for language for results
|
|
* @param Location &$location Location object (may be null)
|
|
*
|
|
* @return boolean whether to continue (results in $location)
|
|
*/
|
|
function onLocationFromLatLon($lat, $lon, $language, &$location)
|
|
{
|
|
// Make sure they're canonical
|
|
|
|
$lat = $this->canonical($lat);
|
|
$lon = $this->canonical($lon);
|
|
|
|
$loc = $this->getCache(array('lat' => $lat,
|
|
'lon' => $lon));
|
|
|
|
if ($loc !== false) {
|
|
$location = $loc;
|
|
return false;
|
|
}
|
|
|
|
try {
|
|
$geonames = $this->getGeonames('findNearbyPlaceName',
|
|
array('lat' => $lat,
|
|
'lng' => $lon,
|
|
'lang' => $language));
|
|
} catch (Exception $e) {
|
|
$this->log(LOG_WARNING, "Error for coords $lat, $lon: " . $e->getMessage());
|
|
return true;
|
|
}
|
|
|
|
if (count($geonames) == 0) {
|
|
// no results
|
|
$this->setCache(array('lat' => $lat,
|
|
'lon' => $lon),
|
|
null);
|
|
return true;
|
|
}
|
|
|
|
$n = $geonames[0];
|
|
|
|
$parts = array();
|
|
|
|
$location = new Location();
|
|
|
|
$parts[] = (string)$n->name;
|
|
|
|
if (!empty($n->adminName1)) {
|
|
$parts[] = (string)$n->adminName1;
|
|
}
|
|
|
|
if (!empty($n->countryName)) {
|
|
$parts[] = (string)$n->countryName;
|
|
}
|
|
|
|
$location->location_id = (string)$n->geonameId;
|
|
$location->location_ns = self::LOCATION_NS;
|
|
$location->lat = $this->canonical($n->lat);
|
|
$location->lon = $this->canonical($n->lng);
|
|
|
|
$location->names[$language] = implode(', ', $parts);
|
|
|
|
$this->setCache(array('lat' => $lat,
|
|
'lon' => $lon),
|
|
$location);
|
|
|
|
// Success! We handled it, so no further processing
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Human-readable name for a location
|
|
*
|
|
* Given a location, we try to retrieve a human-readable name
|
|
* in the target language.
|
|
*
|
|
* @param Location $location Location to get the name for
|
|
* @param string $language ISO code for language to find name in
|
|
* @param string &$name Place to put the name
|
|
*
|
|
* @return boolean whether to continue
|
|
*/
|
|
function onLocationNameLanguage($location, $language, &$name)
|
|
{
|
|
if ($location->location_ns != self::LOCATION_NS) {
|
|
// It's not one of our IDs... keep processing
|
|
return true;
|
|
}
|
|
|
|
$id = $location->location_id;
|
|
|
|
$n = $this->getCache(array('id' => $id,
|
|
'language' => $language));
|
|
|
|
if ($n !== false) {
|
|
$name = $n;
|
|
return false;
|
|
}
|
|
|
|
try {
|
|
$geonames = $this->getGeonames('hierarchy',
|
|
array('geonameId' => $id,
|
|
'lang' => $language));
|
|
} catch (Exception $e) {
|
|
$this->log(LOG_WARNING, "Error for ID $id: " . $e->getMessage());
|
|
return false;
|
|
}
|
|
|
|
if (count($geonames) == 0) {
|
|
$this->setCache(array('id' => $id,
|
|
'language' => $language),
|
|
null);
|
|
return false;
|
|
}
|
|
|
|
$parts = array();
|
|
|
|
foreach ($geonames as $level) {
|
|
if (in_array($level->fcode, array('PCLI', 'ADM1', 'PPL'))) {
|
|
$parts[] = (string)$level->name;
|
|
}
|
|
}
|
|
|
|
$last = $geonames[count($geonames)-1];
|
|
|
|
if (!in_array($level->fcode, array('PCLI', 'ADM1', 'PPL'))) {
|
|
$parts[] = (string)$last->name;
|
|
}
|
|
|
|
if (count($parts)) {
|
|
$name = implode(', ', array_reverse($parts));
|
|
$this->setCache(array('id' => $id,
|
|
'language' => $language),
|
|
$name);
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Human-readable URL for a location
|
|
*
|
|
* Given a location, we try to retrieve a geonames.org URL.
|
|
*
|
|
* @param Location $location Location to get the url for
|
|
* @param string &$url Place to put the url
|
|
*
|
|
* @return boolean whether to continue
|
|
*/
|
|
function onLocationUrl($location, &$url)
|
|
{
|
|
if ($location->location_ns != self::LOCATION_NS) {
|
|
// It's not one of our IDs... keep processing
|
|
return true;
|
|
}
|
|
|
|
$url = 'http://www.geonames.org/' . $location->location_id;
|
|
|
|
// it's been filled, so don't process further.
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Machine-readable name for a location
|
|
*
|
|
* Given a location, we try to retrieve a geonames.org URL.
|
|
*
|
|
* @param Location $location Location to get the url for
|
|
* @param string &$url Place to put the url
|
|
*
|
|
* @return boolean whether to continue
|
|
*/
|
|
function onLocationRdfUrl($location, &$url)
|
|
{
|
|
if ($location->location_ns != self::LOCATION_NS) {
|
|
// It's not one of our IDs... keep processing
|
|
return true;
|
|
}
|
|
|
|
$url = 'http://sws.geonames.org/' . $location->location_id . '/';
|
|
|
|
// it's been filled, so don't process further.
|
|
return false;
|
|
}
|
|
|
|
function getCache($attrs)
|
|
{
|
|
$c = Cache::instance();
|
|
|
|
if (empty($c)) {
|
|
return null;
|
|
}
|
|
|
|
$key = $this->cacheKey($attrs);
|
|
|
|
$value = $c->get($key);
|
|
|
|
return $value;
|
|
}
|
|
|
|
function setCache($attrs, $loc)
|
|
{
|
|
$c = Cache::instance();
|
|
|
|
if (empty($c)) {
|
|
return null;
|
|
}
|
|
|
|
$key = $this->cacheKey($attrs);
|
|
|
|
$result = $c->set($key, $loc, 0, time() + $this->expiry);
|
|
|
|
return $result;
|
|
}
|
|
|
|
function cacheKey($attrs)
|
|
{
|
|
$key = 'geonames:' .
|
|
implode(',', array_keys($attrs)) . ':'.
|
|
Cache::keyize(implode(',', array_values($attrs)));
|
|
if ($this->cachePrefix) {
|
|
return $this->cachePrefix . ':' . $key;
|
|
} else {
|
|
return Cache::key($key);
|
|
}
|
|
}
|
|
|
|
function wsUrl($method, $params)
|
|
{
|
|
if (!empty($this->username)) {
|
|
$params['username'] = $this->username;
|
|
}
|
|
|
|
if (!empty($this->token)) {
|
|
$params['token'] = $this->token;
|
|
}
|
|
|
|
$str = http_build_query($params, null, '&');
|
|
|
|
return 'http://'.$this->host.'/'.$method.'?'.$str;
|
|
}
|
|
|
|
function getGeonames($method, $params)
|
|
{
|
|
if ($this->lastTimeout && (time() - $this->lastTimeout < $this->timeoutWindow)) {
|
|
// TRANS: Exception thrown when a geo names service is not used because of a recent timeout.
|
|
throw new Exception(_m('Skipping due to recent web service timeout.'));
|
|
}
|
|
|
|
$client = HTTPClient::start();
|
|
$client->setConfig('connect_timeout', $this->timeout);
|
|
$client->setConfig('timeout', $this->timeout);
|
|
|
|
try {
|
|
$result = $client->get($this->wsUrl($method, $params));
|
|
} catch (Exception $e) {
|
|
common_log(LOG_ERR, __METHOD__ . ": " . $e->getMessage());
|
|
$this->lastTimeout = time();
|
|
throw $e;
|
|
}
|
|
|
|
if (!$result->isOk()) {
|
|
// TRANS: Exception thrown when a geo names service does not return an expected response.
|
|
// TRANS: %s is an HTTP error code.
|
|
throw new Exception(sprintf(_m('HTTP error code %s.'),$result->getStatus()));
|
|
}
|
|
|
|
$body = $result->getBody();
|
|
|
|
if (empty($body)) {
|
|
// TRANS: Exception thrown when a geo names service returns an empty body.
|
|
throw new Exception(_m('Empty HTTP body in response.'));
|
|
}
|
|
|
|
// This will throw an exception if the XML is mal-formed
|
|
|
|
$document = new SimpleXMLElement($body);
|
|
|
|
// No children, usually no results
|
|
|
|
$children = $document->children();
|
|
|
|
if (count($children) == 0) {
|
|
return array();
|
|
}
|
|
|
|
if (isset($document->status)) {
|
|
// TRANS: Exception thrown when a geo names service return a specific error number and error text.
|
|
// TRANS: %1$s is an error code, %2$s is an error message.
|
|
throw new Exception(sprintf(_m('Error #%1$s ("%2$s").'),$document->status['value'],$document->status['message']));
|
|
}
|
|
|
|
// Array of elements, >0 elements
|
|
|
|
return $document->geoname;
|
|
}
|
|
|
|
function onPluginVersion(array &$versions)
|
|
{
|
|
$versions[] = array('name' => 'Geonames',
|
|
'version' => self::PLUGIN_VERSION,
|
|
'author' => 'Evan Prodromou',
|
|
'homepage' => 'https://git.gnu.io/gnu/gnu-social/tree/master/plugins/Geonames',
|
|
'rawdescription' =>
|
|
// TRANS: Plugin description.
|
|
_m('Uses <a href="http://geonames.org/">Geonames</a> service to get human-readable '.
|
|
'names for locations based on user-provided lat/long pairs.'));
|
|
return true;
|
|
}
|
|
|
|
function canonical($coord)
|
|
{
|
|
$coord = rtrim($coord, "0");
|
|
$coord = rtrim($coord, ".");
|
|
|
|
return $coord;
|
|
}
|
|
}
|