Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .npmignore
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ yarn.lock
#IDE
.idea
.history
.vscode

test
bin
Expand Down
62 changes: 36 additions & 26 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -156,7 +156,7 @@ result.os.toString();

Or access parts of these properties directly:

```php
```js
result.browser.name;
// Chrome

Expand All @@ -172,7 +172,7 @@ result.engine.name;

Finally you can also query versions directly:

```php
```js
result.browser.version.is('>', 26);
// true

Expand All @@ -192,46 +192,56 @@ In some cases you may want to disable the detection of bots. This allows the bot
```js
result = new WhichBrowser(request.headers, { detectBots: false });
```
<!---

Enable result caching
---------------------

WhichBrowser supports PSR-6 compatible cache adapters for caching results between requests. Using a cache is especially useful if you use WhichBrowser on every page of your website and a user visits multiple pages. During the first visit the headers will be parsed and the result will be cached. Upon further visits, the cached results will be used, which is much faster than having to parse the headers again and again.

There are adapters available for other types of caches, such as APC, Doctrine, Memcached, MongoDB, Redis and many more. The configuration of these adapters all differ from each other, but once configured, all you have to do is pass it as an option when creating the `Parser` object, or use the `setCache()` function to set it afterwards. WhichBrowser has been tested to work with the adapters provided by [PHP Cache](http://php-cache.readthedocs.org/en/latest/). For a list of other packages that provide adapters see [Packagist](https://packagist.org/providers/psr/cache-implementation).
WhichBrowser can cache results between requests. Using a cache is especially useful if you use WhichBrowser on every page of your website and a user visits multiple pages. During the first visit the headers will be parsed and the result will be cached. Upon further visits, the cached results will be used, which is much faster than having to parse the headers again and again.

For example, if you want to enable a memcached based cache you need to install an extra composer package:
In order to enable caching you need to specify in the options object which type of cache you want use.

composer require cache/memcached-adapter
For example, if you want to enable the cache you need to construct the `WhichBrowser` object in this way:

And change the call to WhichBrowser/Parser as follows:
```js
const result = new WhichBrowser(request.header, {cache: WhichBrowser.SIMPLE_CACHE});
```
If a result is retrieved from the cache it has the `cached` property set to `true`

```php
$client = new \Memcached();
$client.addServer('localhost', 11211);
```js
if (result.cached) {
// from cache
} else {
// just parsed for the first time
}
```
You can also specify after how many seconds a cached result should be discarded. The default value is 900 seconds or 15 minutes. If you think WhichBrowser uses too much memory for caching, you should lower this value. You can do this by setting the `cacheExpires` property in the options object.

$pool = new \Cache\Adapter\Memcached\MemcachedCachePool($client);
For example, if you want that your cached results lasts for 300 seconds or 5 minutes do:

result = new WhichBrowser\Parser(getallheaders(), [ 'cache' => $pool ]);
```js
const result = new WhichBrowser(request.header, {
cache: WhichBrowser.SIMPLE_CACHE,
cacheExpires: 300
}
);
```
A value for `cacheExpires` less or equal to `0` disable the expiry for that result and it will last until you restart node or you parse the same set of headers with a `cacheExpires` greater than `0`.

or

```php
$client = new \Memcached();
$client.addServer('localhost', 11211);
Cache validity is checked at a rate of `cacheExpires / 5` so, with a `cacheExpires` of `500`, you can rest assured that your result has been reaped from the cache after `500 + 500 / 5 + 1` seconds.

$pool = new \Cache\Adapter\Memcached\MemcachedCachePool($client);
If you want to speed up the process of validity check you can set the `cacheCheckInterval` property in the options object. This property can't be smaller than `1`.

result = new WhichBrowser\Parser();
result.setCache($pool);
result.analyse(getallheaders());
```js
const result = new WhichBrowser(request.header, {
cache: WhichBrowser.SIMPLE_CACHE,
cacheExpires: 300,
cacheCheckInterval: 1
}
);
```
In this way the cache lasts for `300` seconds but is checked every `1` second.

You can also specify after how many seconds a cached result should be discarded. The default value is 900 seconds or 15 minutes. If you think WhichBrowser uses too much memory for caching, you should lower this value. You can do this by setting the `cacheExpires` option or passing it as a second parameter to the `setCache()` function.

-->
> Be aware that changing `cacheExpiries` or `cacheCheckInterval` impact the cache validity check rate for **ALL** records in the cache. For example setting `cacheExpiries` to `0` will prevent **ALL** results to expire because it will disable the cache validity check (for the sake of truth it will be done every `57085` years, `5` months, `10` days, `7` hours, `35` minutes and `48` seconds).

API reference
-------------
Expand Down
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "which-browser",
"version": "0.1.0",
"description": "Browser sniffing tool and UA parser. This library is a porting of WhichBrowser/Parser",
"version": "0.2.0",
"description": "Browser sniffing tool and UA parser. Porting to VanillaJS of the PHP library WhichBrowser/Parser",
"main": "src/Parser.js",
"scripts": {
"test": "lab -a code -v",
Expand Down
100 changes: 100 additions & 0 deletions src/Cache.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
const Analyser = require('./Analyser');
const Crypto = require('crypto');
const simpleCache = new Map();
let cacheCleanerInterval;
let actualCacheCheckInterval;

/**
* Class that enable caching of results
*/
class Cache {
/**
* Analyse the provided headers or User-Agent string
*
* @param {object|string} headers An object with all of the headers or a string with just the User-Agent header
* @param {object} options An object with configuration options
* @param {object} that An object that is the Parser this
*
* @return {boolean|object} Returns true if the cache is active but no match found, return the match if found
*/
static analyseWithCache(headers, options, that) {
if (options.cache) {
const Parser = require('./Parser');
options.cacheExpires = options.cacheExpires <= 0 ? Number.MAX_SAFE_INTEGER : options.cacheExpires || 900;
options.cacheCheckInterval = Math.max(options.cacheCheckInterval || parseInt(options.cacheExpires / 5, 10), 1);
switch (options.cache) {
case Parser.SIMPLE_CACHE: {
/* Should set the clearCache interval only if cacheExpires is > 0 and it hasn't yet initializated
or the cacheCheckInterval has changed */
if (
(!cacheCleanerInterval || actualCacheCheckInterval !== options.cacheCheckInterval) &&
options.cacheExpires
) {
clearInterval(cacheCleanerInterval);
cacheCleanerInterval = setInterval(Cache.clearCache, options.cacheCheckInterval * 1000);
}
actualCacheCheckInterval = options.cacheCheckInterval;
const cacheKey = Crypto.createHash('sha256').update(JSON.stringify(headers)).digest('hex');
if (simpleCache.has(cacheKey)) {
const hit = simpleCache.get(cacheKey);
hit.expires = Cache.getExpirationTime(options);
return hit.data;
} else {
const analyser = new Analyser(headers, options);
analyser.setData(that);
analyser.analyse();
simpleCache.set(cacheKey, {
data: {
browser: that.browser,
engine: that.engine,
os: that.os,
device: that.device,
camouflage: that.camouflage,
features: that.features,
},
expires: Cache.getExpirationTime(options),
});
}
}
}
return true;
}
return false;
}

/**
* Check if data in cache is expired
*/
static clearCache() {
simpleCache.forEach((v, k) => {
if (v.expires < +new Date()) {
simpleCache.delete(k);
}
});
}

/**
* Compute the expiration time
*
* @param {object} options An object with configuration options
*
* @return {int} The expiration time timestamp
*/
static getExpirationTime(options) {
return options.cacheExpires === Number.MAX_SAFE_INTEGER
? Number.MAX_SAFE_INTEGER
: +new Date() + options.cacheExpires * 1000;
}

/**
* Reset the class state. Used only for test purpose
*/
static resetClassState() {
simpleCache.clear();
clearInterval(cacheCleanerInterval);
cacheCleanerInterval = null;
actualCacheCheckInterval = null;
}
}

module.exports = Cache;
20 changes: 14 additions & 6 deletions src/Parser.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
const Main = require('./model/Main');
const Analyser = require('./Analyser');
const Cache = require('./Cache');

/**
* Class that parse the user-agent
*/
Expand All @@ -8,7 +10,9 @@ class Parser extends Main {
* Create a new object that contains all the detected information
*
* @param {object|string} headers Optional, an object with all of the headers or a string with just the User-Agent header
* @param {object} options Optional, an object with configuration options
* @param {object} options Optional, an object with configuration options
* @param {int} [options.cacheExpires=900] Expiry time in seconds
* @param {int} [options.cacheCheckInterval=1/5 * options.cacheExpires] Time in seconds between each cache check to remove expired records. Minimum 1
*/
constructor(headers = null, options = {}) {
super();
Expand Down Expand Up @@ -37,15 +41,19 @@ class Parser extends Main {
} else {
h = headers;
}

/* if (this.analyseWithCache(h, o)) {
return;
}*/
let data;
if ((data = Cache.analyseWithCache(h, o, this))) {
if (typeof data === 'object') {
Object.assign(this, data);
this.cached = true;
}
return;
}

const analyser = new Analyser(h, o);
analyser.setData(this);
analyser.analyse();
}
}

Parser.SIMPLE_CACHE = 'simple';
module.exports = Parser;
Loading