<?php
namespace Croogo\Core\Utility;
use Cake\Collection\CollectionInterface;
use Cake\Datasource\ResultSetInterface;
use Cake\Http\ServerRequest;
use Cake\Log\LogTrait;
use Cake\ORM\Entity;
use Cake\Routing\Router;
use Cake\Utility\Hash;
use Psr\Log\LogLevel;
use Traversable;
/**
* VisibilityFilter
*
* @author Rachman Chavik <rchavik@xintesa.com>
* @package Croogo.Croogo.Lib.Utility
* @license http://www.opensource.org/licenses/mit-license.php The MIT License
* @link http://www.croogo.org
*/
class VisibilityFilter
{
use LogTrait;
/**
* StringConverter instance
*/
protected $_converter = null;
/**
* Known url keys
*/
protected $_urlKeys = [
'admin' => false,
'plugin' => false,
'controller' => false,
'action' => false,
'named' => false,
'pass' => false,
];
/**
* Constructor
*
* @param \Cake\Http\ServerRequest $request
*/
public function __construct(ServerRequest $request = null)
{
if ($request) {
$this->_request = $request;
} else {
$this->_request = new Request();
}
$this->_converter = new StringConverter();
}
/**
* Check that request (passed in the constructor) is visible based on list of
* specified rules. The rules can specified in link string format or just a
* plain URL fragment. Whenever possible, use link string formatted rule since
* a URL fragment can be expensive.
*
* The current request is checked against negative rules first (implicitly
* hidden), then against positive rules (implicitly visible).
* If there's no positive rule, defaults to visible.
*
* @param array $rules Array of rules in link string format
* @return bool True if the rules are satisfied
* @see StringConverter::linkStringToArray()
*/
protected function _isVisible($rules)
{
$negativeRules = array_filter($rules, function ($value) {
if ($value[0] === '-') {
return true;
}
return false;
});
foreach ($negativeRules as $rule) {
if ($this->_ruleMatch(substr($rule, 1))) {
return false;
}
}
$positiveRules = array_diff($rules, $negativeRules);
if (empty($positiveRules)) {
return true;
}
foreach ($positiveRules as $rule) {
if ($rule[0] == '+') {
$rule = substr($rule, 1);
}
if ($this->_ruleMatch($rule)) {
return true;
}
}
return false;
}
/**
* Check that request matches a single rule
*
* @param string $rule Rule in link string or plain URL fragment
* @return bool True if request satisfies the rule
*/
protected function _ruleMatch($rule)
{
if (strpos($rule, ':') !== false) {
$url = array_filter($this->_converter->linkStringToArray($rule));
if (isset($url['?'])) {
$queryString = $url['?'];
unset($url['?']);
}
} else {
$url = Router::parse($rule);
$named = array_diff_key($url, $this->_urlKeys);
$url['named'] = $named;
}
$intersect = array_intersect_key($this->_request->params, $url);
$matched = $intersect == $url;
if ($matched && isset($queryString)) {
$matched = $this->_request->query == $queryString;
}
return $matched;
}
/**
* Remove values based on rules in visibility_path field.
*
* Options:
* - field Field name containing the visibility path rules
*
* @param \Traversable $traversable
* @param array $options
* @return \Cake\Collection\Collection
*/
public function remove(Traversable $traversable, $options = [])
{
$options = Hash::merge([
'field' => null,
], $options);
$field = $options['field'];
return collection($traversable)->filter(function (Entity $entity) use ($field) {
$rules = $entity->get($field);
if (empty($rules)) {
return true;
}
if (!is_array($rules)) {
$this->log('Invalid visibility_path rule', LogLevel::ERROR);
}
return $this->_isVisible($rules);
});
}
}