supercookie-8.x-1.x-dev/src/SupercookieManager.php
src/SupercookieManager.php
<?php
namespace Drupal\supercookie;
use Drupal\Core\Site\Settings;
use Drupal\Core\Config\ConfigFactory;
use Drupal\Core\Database\Connection;
use Drupal\Core\Database\Query\Condition;
use Drupal\Core\Session\AccountProxy;
use Drupal\node\Entity\Node;
use Drupal\taxonomy\Entity\Term;
use Drupal\user\Entity\User;
/**
* The Supercookie manager class.
*/
class SupercookieManager {
public $dnt = FALSE;
public $scid;
public $uid;
public $created;
public $modified;
public $expires = 0;
public $data = NULL;
public $tid = [];
public $nid = [];
public $custom = [];
public $config;
private $connection;
private $user;
private $mongodb;
private $mongodbConn;
/**
* Constructor method.
*/
public function __construct(Settings $settings, ConfigFactory $config_factory, Connection $connection, AccountProxy $user) {
$this->connection = $connection;
$this->user = $user;
$this->config = $config_factory
->get('supercookie.settings')
->get();
$this->mongodb = $this->config['supercookie_mongodb'] && class_exists('\MongoDB\Client');
$this->mongodbConn = $this->mongodb ?? $settings->get('mongodb')['default'];
return $this;
}
/**
* Gets an instance of the MongoDB collection per config.
*/
public function getMongoCollection() {
$collection = NULL;
if ($this->mongodb) {
$client = new \MongoDB\Client($this->mongodbConn['host'] . '/' . $this->mongodbConn['db']);
$collection = $client->selectCollection($this->mongodbConn['db'], 'supercookie');
}
return $collection;
}
/**
* Get the custom HTTP header set by supercookie.
*/
private function getHeader($all = FALSE) {
if (function_exists('apache_request_headers')) {
$request_headers = getallheaders();
}
else {
// Nginx equivalent.
foreach ($_SERVER as $key => $value) {
if (substr($key, 0, 5) == 'HTTP_') {
$key = str_replace(' ', '-', ucwords(strtolower(str_replace('_', ' ', substr($key, 5)))));
$request_headers[$key] = $value;
}
else {
$request_headers[$key] = $value;
}
}
}
if ($all) {
return $request_headers;
}
return (!empty($request_headers[$this->config['supercookie_name_header']]) ? $request_headers[$this->config['supercookie_name_header']] : NULL);
}
/**
* TODO.
*
* @see http://php.net/manual/en/function.array-walk-recursive.php#114574
*/
private function walkRecursiveRemoveNulls(array $array) {
foreach ($array as $k => $v) {
if (is_array($v)) {
$array[$k] = $this->walkRecursiveRemoveNulls($v);
}
else {
if ($v === NULL) {
unset($array[$k]);
}
}
}
return $array;
}
/**
* TODO.
*/
private function init($matcher = NULL, $data = NULL) {
// Get client's specified hash.
$hash_client = 0;
$header = $this->getHeader();
$headers = $this->getHeader(TRUE);
// Flag to honor user's (and site owners's acknowledgemnt of) DNT requests.
$this->dnt = $this->config['supercookie_honor_dnt'] && !empty($headers['DNT']) && $headers['DNT'] == 1;
// Check custom HTTP header for cookie value.
if (empty($hash_client) && !empty($header) && $header !== '""') {
$hash_client = $header;
}
// Check Cookie HTTP header for cookie value.
if (empty($hash_client) && !empty($headers['Cookie'])) {
$cookies = explode(';', $headers['Cookie']);
foreach ($cookies as $pair) {
$parts = explode('=', $pair);
if ($parts[0] == $this->config['supercookie_name_server']) {
$hash_client = $parts[1];
}
}
}
// Check HTTP cookie for cookie value.
if (empty($hash_client) && !empty($_COOKIE[$this->config['supercookie_name_server']])) {
$hash_client = $_COOKIE[$this->config['supercookie_name_server']];
}
// Expire user's db record and cookies if client hash does not match hash
// lookup result.
if (!empty($data) && !empty($hash_client)) {
if ($data->data !== $hash_client) {
// TODO: bring back delete op?
}
}
$this->scid = (!empty($data->scid) ? $data->scid : 0);
$this->data = (!empty($data->data) ? $data->data : $matcher);
if (empty($this->data)) {
$this->data = $hash_client;
}
return $this->read();
}
/**
* TODO.
*/
private function read() {
$or = new Condition('OR');
$or = $or
->condition('scid', $this->scid)
->condition('data', $this->data);
// Honor user's DNT header and delete any previously collected data.
if ($this->dnt === TRUE) {
if ($this->mongodb) {
$this
->getMongoCollection()
->deleteOne(array('data' => $this->data));
}
else {
$this
->connection
->delete('supercookie')
->condition($or)
->execute();
}
$this->scid = 0;
$this->uid = 0;
$this->created = 0;
$this->modified = 0;
$this->expires = 0;
$this->data = NULL;
$this->tid = [];
$this->nid = [];
$this->custom = [];
return $this;
}
$row = NULL;
if ($this->mongodb) {
// Check for mongodb storage.
if (!empty($this->scid)) {
$row = (object) $this
->getMongoCollection()
->findOne(array('_id' => new \MongoDB\BSON\ObjectID($this->scid)));
}
else {
$row = (object) $this
->getMongoCollection()
->findOne(array(
'$or' => array(
array('data' => $this->data),
array('scid' => $this->scid),
),
));
}
if (!empty($row->_id)) {
$row->scid = $row->_id->__toString();
}
}
else {
// Default to standard rdbms.
$row = $this
->connection
->select('supercookie', 'sc')
->fields('sc', array(
'scid',
'uid',
'created',
'modified',
'expires',
'data',
'tid',
'nid',
'custom',
))
->condition($or)
->range(0, 1)
->orderBy('sc.created', 'DESC')
->execute()
->fetchObject();
}
if (!empty($row) && !empty($row->scid)) {
$this->scid = (!$this->mongodb ? intval($row->scid) : $row->scid);
$this->uid = intval($row->uid);
$this->created = intval($row->created);
$this->modified = intval($row->modified);
$this->expires = intval($row->expires);
$this->data = $row->data;
$this->tid = (!$this->mongodb ? unserialize($row->tid) : (array) $row->tid);
$this->nid = (!$this->mongodb ? unserialize($row->nid) : (array) $row->nid);
$this->custom = (!$this->mongodb ? unserialize($row->custom) : (array) $row->custom);
}
else {
$this->scid = 0;
$this->uid = 0;
$this->created = 0;
$this->modified = 0;
$this->expires = 0;
// Don't even think about bringing back $this->data = NULL; doing so will
// cause infinite XHR requests from JS when calls to $this->init() are
// made with no args.
$this->tid = [];
$this->nid = [];
$this->custom = [];
}
return $this;
}
/**
* TODO.
*/
private function write($timestamp) {
if ($this->dnt === TRUE) {
return;
}
$row = array(
'uid' => $this->user->id(),
'modified' => $timestamp,
'data' => $this->data,
'tid' => (is_array($this->tid) ? $this->tid : []),
'nid' => (is_array($this->nid) ? $this->nid : []),
'custom' => (is_array($this->custom) ? $this->custom : []),
);
if (!empty($this->scid)) {
$row['scid'] = $this->scid;
}
else {
$row['created'] = $timestamp;
$row['expires'] = $this->expires;
}
if ($this->mongodb) {
// Check for mongodb storage.
if (empty($row['scid'])) {
$this->scid = $this
->getMongoCollection()
->insertOne($row)
->getInsertedId()
->__toString();
$row['scid'] = $this->scid;
}
$this
->getMongoCollection()
->updateOne(array('_id' => new \MongoDB\BSON\ObjectID($row['scid'])), array('$set' => $row));
}
elseif (!empty($row['data']) && $row['data'] !== '""') {
// Default to standard rdbms.
$row['tid'] = serialize($row['tid']);
$row['nid'] = serialize($row['nid']);
$row['custom'] = serialize($row['custom']);
$this
->connection
->upsert('supercookie')
->key('scid')
->fields($row)
->execute();
}
}
/**
* TODO.
*/
private function delete($timestamp) {
if ($this->mongodb) {
$result = $this
->getMongoCollection()
->deleteOne(array(
'expired' => $timestamp,
'scid' => $this->scid,
));
}
else {
$result = $this
->connection
->delete('supercookie')
->condition('expires', $timestamp, '<')
->condition('scid', $this->scid)
->execute();
}
return $result;
}
/**
* TODO.
*/
public function match(&$hash) {
// Check db for fingerprint match on data.
if ($this->mongodb) {
$data = $this
->getMongoCollection()
->findOne(array(
'data' => $hash,
));
if (!empty($data['scid'])) {
$data = (object) array(
'scid' => $data['scid'],
'data' => $data['data'],
);
}
else {
$data = NULL;
}
}
else {
$data = $this
->connection
->select('supercookie', 'sc')
->fields('sc', array(
'scid',
'data',
))
->condition('data', $hash)
->range(0, 1)
->orderBy('sc.created', 'DESC')
->execute()
->fetchObject();
}
return $this->init($hash, $data);
}
/**
* TODO.
*/
public function save($timestamp) {
// Ignore client time; use server time exclusively.
$timestamp = \Drupal::time()->getRequestTime();
$expires = $this->config['supercookie_expire'];
if ($expires == 'calendar_day') {
$expires = strtotime(date('Y-m-d', $timestamp) . ' + 1 day');
}
else {
$expires = ($timestamp + $expires);
}
if (empty($this->expires)) {
$this->expires = $expires;
}
// Clean up expired sessions.
if ($this->expires < $timestamp) {
$expired = $this->delete($timestamp);
// Reset object and set new expiration.
if (!empty($expired)) {
$this->read();
$this->expires = $expires;
}
}
// Upsert fingerprint record.
$this->write($timestamp);
// Return populated supercookie.
return $this->read();
}
/**
* Update record's target field.
*/
private function mergeField($field_name, $data) {
// Merge $data to target field.
if (empty($this->{$field_name})) {
$this->{$field_name} = [];
}
if (empty($data)) {
return $this;
}
if (in_array($field_name, array('tid', 'nid'))) {
$data = array_fill_keys($data, 1);
}
if (!empty($this->{$field_name})) {
// Increment value counters for nid and tid fields.
if (in_array($field_name, array('tid', 'nid'))) {
foreach ($this->{$field_name} as $key => &$count) {
if (array_key_exists($key, $data)) {
$this->{$field_name}[$key] = ($this->{$field_name}[$key] + 1);
unset($data[$key]);
}
}
foreach ($data as $key => &$count) {
$this->{$field_name}[$key] = $data[$key];
}
}
elseif ($field_name == 'custom') {
// Deep merge existing value with input leaves.
$this->{$field_name} = array_replace_recursive($this->{$field_name}, $data);
// Now prune NULL leaves from value.
$this->{$field_name} = $this->walkRecursiveRemoveNulls($this->{$field_name});
}
arsort($this->{$field_name});
}
else {
$this->{$field_name} = $data;
}
// Return populated supercookie.
return $this->save($this->created);
}
/**
* Update custom field. To remove a leaf from the array set its value to NULL.
*/
public function mergeCustom(array $data) {
return $this->mergeField('custom', $data);
}
/**
* Update record's nid field.
*/
public function trackNodes(array $data) {
if ($this->config['supercookie_track_nid']) {
return $this->mergeField('nid', $data);
}
return $this;
}
/**
* Update record's tid field.
*/
public function trackTerms(array $data) {
if ($this->config['supercookie_track_tid']) {
return $this->mergeField('tid', $data);
}
return $this;
}
/**
* Gets a human-readable array from raw supercookie values.
*/
private function reportFormat($object) {
$account = User::load($object->uid);
$uname = t('anonymous');
if ($account->isAuthenticated()) {
$uname = $account->getAccountName();
}
$human = array(
'user' => $uname,
'cookie' => $object->scid,
'hash' => $object->data,
'created' => \Drupal::service('date.formatter')->format($object->created, 'e'),
'modified' => \Drupal::service('date.formatter')->format($object->modified, 'e'),
'expires' => \Drupal::service('date.formatter')->format($object->expires, 'e'),
);
// Try to unserialize row blobs.
if (!$this->mongodb) {
$tid = unserialize($object->tid);
$nid = unserialize($object->nid);
$custom = unserialize($object->custom);
if ($tid !== FALSE) {
$object->tid = $tid;
}
if ($nid !== FALSE) {
$object->nid = $nid;
}
if ($custom !== FALSE) {
$object->custom = unserialize($object->custom);
$human['custom'] = $object->custom;
}
}
// Add term names + counts to response.
if (!empty($object->tid)) {
$human['terms'] = Term::loadMultiple(array_keys($object->tid));
foreach ($human['terms'] as &$term) {
$term = (object) array(
$term->getName() => $object->tid[$term->id()],
);
}
$human['terms'] = array_values($human['terms']);
}
// Add node titles + counts to response.
if (!empty($object->nid)) {
$human['nodes'] = Node::loadMultiple(array_keys($object->nid));
foreach ($human['nodes'] as &$node) {
$node = (object) array(
$node->getTitle() => $object->nid[$node->id()],
);
}
$human['nodes'] = array_values($human['nodes']);
}
return $human;
}
/**
* Dump a JSON blob of all current, transformed supercookie data.
*/
public function report() {
$data = [];
if ($this->mongodb) {
$results = $this
->getMongoCollection()
->find();
$iterator = new \IteratorIterator($results);
$iterator->rewind();
while ($row = $iterator->current()) {
$row = $row->bsonSerialize();
$row->tid = (array) $row->tid->bsonSerialize();
$row->nid = (array) $row->nid->bsonSerialize();
$row->custom = (array) $row->custom->bsonSerialize();
$data[] = $row;
$iterator->next();
}
foreach ($data as &$human) {
$human = $this->reportFormat($human);
// Allow other modules to customize each row as needed (e.g. per its own
// usage of the "custom" field).
\Drupal::moduleHandler()->alter('supercookie.admin_report', $human);
}
}
else {
$results = $this
->connection
->select('supercookie', 'sc')
->fields('sc', array(
'scid',
'uid',
'created',
'modified',
'expires',
'data',
'tid',
'nid',
'custom',
))
->execute();
while ($row = $results->fetch()) {
$human = $this->reportFormat($row);
// Allow other modules to customize each row as needed (e.g. per its own
// usage of the "custom" field).
\Drupal::moduleHandler()->alter('supercookie.admin_report', $human);
$data[] = $human;
}
}
return $data;
}
}
