diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..7d04096 --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +/vendor/ +/report/ +composer.lock +.phplint-cache +.env \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..03bd01d --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,6 @@ +# Changelog + +All notable changes to this project will be documented in this file, in reverse chronological order by release. + +## 0.1.0 - Unreleased + diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..88a35c7 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,36 @@ +# Contributing + +## Only one feature or change per pull request + +Make pull requests only one feature or change at the time. Make pull requests from feature branch. Pull requests should not come from your master branch. + +For example you have fixed a bug. You also have optimized some code. Optimization is not related to a bug. These should be submitted as separate pull requests. This way I can easily choose what to include. It is also easier to understand the code changes. + +## Write meaningful commit messages + +Proper commit message is full sentence. It starts with capital letter but does not end with period. Headlines do not end with period. The GitHub default `Update filename.php` is not enough. When needed include also longer explanation what the commit does. + +``` +Capitalized, short (50 chars or less) summary + +More detailed explanatory text, if necessary. Wrap it to about 72 +characters or so. In some contexts, the first line is treated as the +subject of an email and the rest of the text as the body. The blank +line separating the summary from the body is critical (unless you omit +the body entirely); tools like rebase can get confused if you run the +two together. +``` + +When in doubt see Tim Pope's blogpost [A Note About Git Commit Messages](http://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html) + +## Follow the existing coding standards + +When contributing to open source project it is polite to follow the original authors coding standars. They might be different than yours. It is not a holy war. This project uses **[PSR-2 Coding Standard](https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-2-coding-style-guide.md)** + +## Running Tests + +You can run tests using composer. + +``` bash +$ composer test +``` diff --git a/LICENSE.md b/LICENSE.md new file mode 100644 index 0000000..c50cb41 --- /dev/null +++ b/LICENSE.md @@ -0,0 +1,21 @@ +# The MIT License (MIT) + +Copyright (c) 2017 Mika Tuupola + +> 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/README.md b/README.md new file mode 100644 index 0000000..7543862 --- /dev/null +++ b/README.md @@ -0,0 +1,62 @@ +# Common interface for wifi positioning services + +[![Latest Version](https://img.shields.io/packagist/v/tuupola/whereami.svg?style=flat-square)](https://packagist.org/packages/tuupola/whereami) +[![Software License](https://img.shields.io/badge/license-MIT-brightgreen.svg?style=flat-square)](LICENSE.md) +[![Build Status](https://img.shields.io/travis/tuupola/whereami/master.svg?style=flat-square)](https://travis-ci.org/tuupola/whereami) +[![HHVM Status](https://img.shields.io/hhvm/tuupola/whereami.svg?style=flat-square)](http://hhvm.h4cc.de/package/tuupola/whereami) +[![Coverage](http://img.shields.io/codecov/c/github/tuupola/whereami.svg?style=flat-square)](https://codecov.io/github/tuupola/whereami) + +## Install + +Install using [Composer](https://getcomposer.org/). + +``` bash +$ composer require tuupola/whereami:dev-master +``` + +## Usage + +```php +require __DIR__ . "/vendor/autoload.php"; + +$provider = new MozillaProvider("your-api-key-here"); +$scanner = new AirportScanner; +$locator = new Whereami($provider, $scanner); + +$location = $locator->whereami(); + +/* +Array +( + [latitude] => 1.355989 + [longitude] => 103.992365 + [accuracy] => 65 +) +*/ +``` + + +## Testing + +You can run tests either manually... + +``` bash +$ composer test +``` +... or automatically on every code change. This requires [entr](http://entrproject.org/) to work: + +``` bash +$ composer watch +``` + +## Contributing + +Please see [CONTRIBUTING](CONTRIBUTING.md) for details. + +## Security + +If you discover any security related issues, please email tuupola@appelsiini.net instead of using the issue tracker. + +## License + +The MIT License (MIT). Please see [License File](LICENSE.md) for more information. diff --git a/codecov.yml b/codecov.yml new file mode 100644 index 0000000..959972a --- /dev/null +++ b/codecov.yml @@ -0,0 +1 @@ +comment: false \ No newline at end of file diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..e96892a --- /dev/null +++ b/composer.json @@ -0,0 +1,53 @@ +{ + "name": "tuupola/whereami", + "description": "Common interface for wifi positioning services", + "keywords": [ + "wifi", + "geoposition" + ], + "homepage": "https://github.com/tuupola/whereami", + "license": "MIT", + "authors": [ + { + "name": "Mika Tuupola", + "email": "tuupola@appelsiini.net", + "homepage": "http://www.appelsiini.net/", + "role": "Developer" + } + ], + "require": { + "php": "^5.6 || ^7.0", + "php-http/client-implementation": "^1.0", + "php-http/httplug": "^1.1", + "php-http/discovery": "^1.2", + "php-http/message": "^1.5", + "php-http/client-common": "^1.5" + }, + "require-dev": { + "phpunit/phpunit": "^5.7", + "squizlabs/php_codesniffer": "^2.5", + "overtrue/phplint": "^0.2.0", + "php-http/mock-client": "^1.0", + "php-http/curl-client": "^1.7", + "zendframework/zend-diactoros": "^1.4", + "php-http/logger-plugin": "^1.0", + "monolog/monolog": "^1.22" + }, + "autoload": { + "psr-4": { + "Whereami\\": "src" + } + }, + "autoload-dev": { + "psr-4": { + "Whereami\\": "tests" + } + }, + "scripts": { + "test": "composer phplint && composer phpcs && composer phpunit", + "phpcs": "vendor/bin/phpcs --standard=PSR2 --extensions=php --encoding=utf-8 --ignore=*/vendor/* . -p", + "phpunit": "vendor/bin/phpunit --coverage-text --coverage-html ./report/", + "phplint": "vendor/bin/phplint . --exclude=vendor/", + "watch": "find . -name \"*.php\" -not -path \"./vendor/*\" -o -name \"*.json\" -not -path \"./vendor/*\"| entr -c composer test" + } +} diff --git a/phpunit.xml.dist b/phpunit.xml.dist new file mode 100644 index 0000000..bacd1b0 --- /dev/null +++ b/phpunit.xml.dist @@ -0,0 +1,16 @@ + + + + + tests/ + + + + + src/ + + + vendor/ + + + \ No newline at end of file diff --git a/src/HttpClientFactory.php b/src/HttpClientFactory.php new file mode 100644 index 0000000..4c4976a --- /dev/null +++ b/src/HttpClientFactory.php @@ -0,0 +1,60 @@ +httpClient = $httpClient ?: HttpClientDiscovery::find(); + $this->options = $options; + } + + public function create() + { + $plugins = [ + //new ErrorPlugin, + new HeaderSetPlugin([ + "User-Agent" => "whereami/" . Whereami::VERSION, + "Accept" => "application/json", + "Content-Type" => "application/json; charset=utf-8", + ]) + ]; + + $plugins = array_merge($plugins, $this->plugins); + + return new PluginClient($this->httpClient, $plugins); + } + + public function addPlugin(Plugin $plugin) + { + $this->plugins[] = $plugin; + return $this; + } +} diff --git a/src/Provider.php b/src/Provider.php new file mode 100644 index 0000000..e51da9c --- /dev/null +++ b/src/Provider.php @@ -0,0 +1,27 @@ +apikey = $apikey; + $this->httpClient = $httpClient ?: (new HttpClientFactory)->create(); + $this->requestFactory = $requestFactory ?: MessageFactoryDiscovery::find(); + } + + public function process(array $data, array $options = []) + { + $endpoint = $this->endpoint(); + $headers = []; + $body = $this->transform($data); + $request = $this->requestFactory->createRequest("POST", $endpoint, $headers, $body); + $response = $this->httpClient->sendRequest($request); + return $this->parse((string) $response->getBody()); + } + + private function endpoint() + { + return $this->endpoint .= "?" . http_build_query(["key" => $this->apikey]); + } + + private function transform($data = []) + { + $json["wifiAccessPoints"] = array_map(function ($entry) { + return [ + "ssid" => $entry["name"], + "macAddress" => $entry["address"], + "signalStrength" => $entry["signal"], + "channel" => $entry["channel"], + ]; + }, $data); + + return json_encode($json, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT); + } + + private function parse($json) + { + $data = json_decode($json, true); + + return [ + "latitude" => $data["location"]["lat"], + "longitude" => $data["location"]["lng"], + "accuracy" => $data["accuracy"], + ]; + } +} diff --git a/src/Provider/MozillaProvider.php b/src/Provider/MozillaProvider.php new file mode 100644 index 0000000..4c494f8 --- /dev/null +++ b/src/Provider/MozillaProvider.php @@ -0,0 +1,80 @@ +apikey = $apikey; + $this->httpClient = $httpClient ?: (new HttpClientFactory)->create(); + $this->requestFactory = $requestFactory ?: MessageFactoryDiscovery::find(); + } + + public function process(array $data, array $options = []) + { + $endpoint = $this->endpoint(); + $headers = []; + $body = $this->transform($data); + $request = $this->requestFactory->createRequest("POST", $endpoint, $headers, $body); + $response = $this->httpClient->sendRequest($request); + return $this->parse((string) $response->getBody()); + } + + private function endpoint() + { + return $this->endpoint .= "?" . http_build_query(["key" => $this->apikey]); + } + + private function transform($data = []) + { + $json["wifiAccessPoints"] = array_map(function ($entry) { + return [ + "ssid" => $entry["name"], + "macAddress" => $entry["address"], + "signalStrength" => $entry["signal"], + "channel" => $entry["channel"], + ]; + }, $data); + + return json_encode($json, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT); + } + + private function parse($json) + { + $data = json_decode($json, true); + + return [ + "latitude" => $data["location"]["lat"], + "longitude" => $data["location"]["lng"], + "accuracy" => $data["accuracy"], + ]; + } +} diff --git a/src/Provider/UnwiredProvider.php b/src/Provider/UnwiredProvider.php new file mode 100644 index 0000000..f61cbec --- /dev/null +++ b/src/Provider/UnwiredProvider.php @@ -0,0 +1,80 @@ +apikey = $apikey; + $this->httpClient = $httpClient ?: (new HttpClientFactory)->create(); + $this->requestFactory = $requestFactory ?: MessageFactoryDiscovery::find(); + } + + public function process(array $data, array $options = []) + { + $endpoint = $this->endpoint(); + $headers = []; + $body = $this->transform($data); + $request = $this->requestFactory->createRequest("POST", $endpoint, $headers, $body); + $response = $this->httpClient->sendRequest($request); + return $this->parse((string) $response->getBody()); + } + + private function endpoint() + { + return $this->endpoint; + } + + private function transform($data = []) + { + $json["token"] = $this->apikey; + $json["address"] = 0; + $json["wifi"] = array_map(function ($entry) { + return [ + "bssid" => $entry["address"], + "signal" => $entry["signal"], + "channel" => $entry["channel"], + ]; + }, $data); + + return json_encode($json, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT); + } + + private function parse($json) + { + $data = json_decode($json, true); + return [ + "latitude" => $data["lat"], + "longitude" => $data["lon"], + "accuracy" => $data["accuracy"], + ]; + } +} diff --git a/src/Scanner.php b/src/Scanner.php new file mode 100644 index 0000000..604b66a --- /dev/null +++ b/src/Scanner.php @@ -0,0 +1,26 @@ +&1" + ) { + $this->command = $command; + } + + public function scan() + { + exec($this->command, $output); + return $this->parse($output); + } + + private function parse(array $output) + { + array_shift($output); + return array_map(function ($line) { + $data = preg_split("/\s+/", $line); + /* Airport sometimes return 157,+1 style channels. */ + preg_match("/^(\d+)/", $data[4], $matches); + $channel = $matches[0]; + + return [ + "name" => $data[1], // SSID + "address" => $data[2], // BSSID + "signal" => $data[3], // RSSI + "channel" => $channel, + ]; + }, $output); + } +} diff --git a/src/Scanner/IwlistScanner.php b/src/Scanner/IwlistScanner.php new file mode 100644 index 0000000..20d8206 --- /dev/null +++ b/src/Scanner/IwlistScanner.php @@ -0,0 +1,67 @@ +&1") + { + $this->command = $command; + } + + public function scan() + { + exec($this->command, $output); + return $this->parse($output); + } + + private function parse(array $output) + { + $current = -1; + $networks = []; + foreach ($output as $line) { + if (preg_match(self::REGEXP_BSSID, $line, $matches)) { + $networks[++$current] = [ + "name" => null, + "address" => null, + "signal" => null, + "channel" => null, + ]; + $networks[$current]["address"] = $matches[1]; + } + if (preg_match(self::REGEXP_ESSID, $line, $matches)) { + $networks[$current]["name"] = $matches[1]; + } + if (preg_match(self::REGEXP_SIGNAL, $line, $matches)) { + $networks[$current]["signal"] = (integer) $matches[1]; + } + if (preg_match(self::REGEXP_CHANNEL, $line, $matches)) { + $networks[$current]["channel"] = (integer) $matches[1]; + } + } + + return $networks; + } +} diff --git a/src/Whereami.php b/src/Whereami.php new file mode 100644 index 0000000..15134d8 --- /dev/null +++ b/src/Whereami.php @@ -0,0 +1,43 @@ +provider = $provider; + $this->scanner = $scanner; + } + + public function whereami() + { + $networks = $this->scanner->scan(); + return $this->provider->process($networks); + } + + public function whereis(array $networks) + { + return $this->provider->process($networks); + } +} diff --git a/tests/AirportScannerTest.php b/tests/AirportScannerTest.php new file mode 100644 index 0000000..e9a3db8 --- /dev/null +++ b/tests/AirportScannerTest.php @@ -0,0 +1,33 @@ +assertTrue(true); + } + + public function testShouldScan() + { + $data = (new AirportScanner())->scan(); + print_r($data); + } +} diff --git a/tests/GoogleProviderTest.php b/tests/GoogleProviderTest.php new file mode 100644 index 0000000..9761853 --- /dev/null +++ b/tests/GoogleProviderTest.php @@ -0,0 +1,58 @@ +assertTrue(true); + } + + public function testShouldConstruct() + { + $provider = new GoogleProvider("fakekey"); + $this->assertInstanceOf(GoogleProvider::class, $provider); + } + + public function testShouldProcess() + { + if (false === $key = getenv("WHEREAMI_GOOGLE_KEY")) { + $this->markTestSkipped("Google API key not set."); + } + + $location = (new GoogleProvider($key))->process([ + [ + "name" => "CrownePlaza", + "address" => "54:3d:37:2e:60:88", + "signal" => -71, + "channel" => 1, + ], + [ + "name" => "Boingo", + "address" => "54:3d:37:ae:60:88", + "signal" => -73, + "channel" => 1, + ], + ]); + $this->assertArrayHasKey("latitude", $location); + $this->assertArrayHasKey("longitude", $location); + $this->assertArrayHasKey("accuracy", $location); + } +} diff --git a/tests/HttpClientFactoryTest.php b/tests/HttpClientFactoryTest.php new file mode 100644 index 0000000..c4fda67 --- /dev/null +++ b/tests/HttpClientFactoryTest.php @@ -0,0 +1,34 @@ +assertTrue(true); + } + + public function testShouldCreate() + { + $client = (new HttpClientFactory())->create(); + $this->assertInstanceOf(PluginClient::class, $client); + } +} diff --git a/tests/IwListScannerTest.php b/tests/IwListScannerTest.php new file mode 100644 index 0000000..2b91c14 --- /dev/null +++ b/tests/IwListScannerTest.php @@ -0,0 +1,33 @@ +assertTrue(true); + } + + public function testShouldScan() + { + $data = (new IwListScanner())->scan(); + print_r($data); + } +} diff --git a/tests/MozillaProviderTest.php b/tests/MozillaProviderTest.php new file mode 100644 index 0000000..16d685b --- /dev/null +++ b/tests/MozillaProviderTest.php @@ -0,0 +1,58 @@ +assertTrue(true); + } + + public function testShouldConstruct() + { + $provider = new MozillaProvider("fakekey"); + $this->assertInstanceOf(MozillaProvider::class, $provider); + } + + public function testShouldProcess() + { + if (false === $key = getenv("WHEREAMI_MOZILLA_KEY")) { + $this->markTestSkipped("Mozilla API key not set."); + } + + $location = (new MozillaProvider($key))->process([ + [ + "name" => "CrownePlaza", + "address" => "54:3d:37:2e:60:88", + "signal" => -71, + "channel" => 1, + ], + [ + "name" => "Boingo", + "address" => "54:3d:37:ae:60:88", + "signal" => -73, + "channel" => 1, + ], + ]); + $this->assertArrayHasKey("latitude", $location); + $this->assertArrayHasKey("longitude", $location); + $this->assertArrayHasKey("accuracy", $location); + } +} diff --git a/tests/UnwiredProviderTest.php b/tests/UnwiredProviderTest.php new file mode 100644 index 0000000..26edf0c --- /dev/null +++ b/tests/UnwiredProviderTest.php @@ -0,0 +1,58 @@ +assertTrue(true); + } + + public function testShouldConstruct() + { + $provider = new UnwiredProvider("fakekey"); + $this->assertInstanceOf(UnwiredProvider::class, $provider); + } + + public function testShouldProcess() + { + if (false === $key = getenv("WHEREAMI_UNWIRED_KEY")) { + $this->markTestSkipped("Unwired API key not set."); + } + + $location = (new UnwiredProvider($key))->process([ + [ + "name" => "CrownePlaza", + "address" => "54:3d:37:2e:60:88", + "signal" => -71, + "channel" => 1, + ], + [ + "name" => "Boingo", + "address" => "54:3d:37:ae:60:88", + "signal" => -73, + "channel" => 1, + ], + ]); + $this->assertArrayHasKey("latitude", $location); + $this->assertArrayHasKey("longitude", $location); + $this->assertArrayHasKey("accuracy", $location); + } +}