From f121530ecb0d7582f13c114cd29b233a1c7f3494 Mon Sep 17 00:00:00 2001 From: Mika Tuupola Date: Sat, 8 Apr 2017 17:33:31 +0800 Subject: [PATCH] First working concept --- .gitignore | 5 ++ CHANGELOG.md | 6 +++ CONTRIBUTING.md | 36 ++++++++++++++ LICENSE.md | 21 +++++++++ README.md | 62 +++++++++++++++++++++++++ codecov.yml | 1 + composer.json | 53 +++++++++++++++++++++ phpunit.xml.dist | 16 +++++++ src/HttpClientFactory.php | 60 ++++++++++++++++++++++++ src/Provider.php | 27 +++++++++++ src/Provider/GoogleProvider.php | 80 ++++++++++++++++++++++++++++++++ src/Provider/MozillaProvider.php | 80 ++++++++++++++++++++++++++++++++ src/Provider/UnwiredProvider.php | 80 ++++++++++++++++++++++++++++++++ src/Scanner.php | 26 +++++++++++ src/Scanner/AirportScanner.php | 55 ++++++++++++++++++++++ src/Scanner/IwlistScanner.php | 67 ++++++++++++++++++++++++++ src/Whereami.php | 43 +++++++++++++++++ tests/AirportScannerTest.php | 33 +++++++++++++ tests/GoogleProviderTest.php | 58 +++++++++++++++++++++++ tests/HttpClientFactoryTest.php | 34 ++++++++++++++ tests/IwListScannerTest.php | 33 +++++++++++++ tests/MozillaProviderTest.php | 58 +++++++++++++++++++++++ tests/UnwiredProviderTest.php | 58 +++++++++++++++++++++++ 23 files changed, 992 insertions(+) create mode 100644 .gitignore create mode 100644 CHANGELOG.md create mode 100644 CONTRIBUTING.md create mode 100644 LICENSE.md create mode 100644 README.md create mode 100644 codecov.yml create mode 100644 composer.json create mode 100644 phpunit.xml.dist create mode 100644 src/HttpClientFactory.php create mode 100644 src/Provider.php create mode 100644 src/Provider/GoogleProvider.php create mode 100644 src/Provider/MozillaProvider.php create mode 100644 src/Provider/UnwiredProvider.php create mode 100644 src/Scanner.php create mode 100644 src/Scanner/AirportScanner.php create mode 100644 src/Scanner/IwlistScanner.php create mode 100644 src/Whereami.php create mode 100644 tests/AirportScannerTest.php create mode 100644 tests/GoogleProviderTest.php create mode 100644 tests/HttpClientFactoryTest.php create mode 100644 tests/IwListScannerTest.php create mode 100644 tests/MozillaProviderTest.php create mode 100644 tests/UnwiredProviderTest.php 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); + } +}