new: First implementation of the feed analysis system

pull/2211/head
iglocska 2017-05-08 14:22:27 +02:00
parent 17984f4845
commit 96574ec335
12 changed files with 495 additions and 8 deletions

View File

@ -87,7 +87,34 @@ class ServerShell extends AppShell
'message' => $message,
'progress' => 0,
'status' => 3
));
));
} else {
$message = 'Job done.';
$this->Job->save(array(
'id' => $jobId,
'message' => $message,
'progress' => 100,
'status' => 4
));
}
}
public function cacheFeeds() {
$userId = $this->args[0];
$jobId = $this->args[1];
$scope = $this->args[2];
$this->Job->read(null, $jobId);
$user = $this->User->getAuthUser($userId);
$result = $this->Feed->cacheFeedInitiator($user, $jobId, $scope);
$this->Job->id = $jobId;
if ($result !== true) {
$message = 'Job Failed. Reason: ';
$this->Job->save(array(
'id' => $jobId,
'message' => $message . $result,
'progress' => 0,
'status' => 3
));
} else {
$message = 'Job done.';
$this->Job->save(array(

View File

@ -135,6 +135,8 @@ class ACLComponent extends Component {
),
'feeds' => array(
'add' => array(),
'cacheFeeds' => array(),
'compareFeeds' => array(),
'delete' => array(),
'disable' => array(),
'edit' => array(),
@ -233,11 +235,13 @@ class ACLComponent extends Component {
),
'servers' => array(
'add' => array(),
'checkout' => array(),
'delete' => array(),
'deleteFile' => array(),
'edit' => array(),
'fetchServersForSG' => array('*'),
'filterEventIndex' => array(),
'getGit' => array(),
'getPyMISPVersion' => array('*'),
'getVersion' => array('*'),
'index' => array('OR' => array('perm_sync', 'perm_admin')),
@ -257,6 +261,7 @@ class ACLComponent extends Component {
'stopWorker' => array(),
'stopZeroMQServer' => array(),
'testConnection' => array('perm_sync'),
'update' => array(),
'uploadFile' => array(),
'clearWorkerQueue' => array()
),
@ -318,6 +323,7 @@ class ACLComponent extends Component {
'addTag' => array(),
'delete' => array(),
'disable' => array(),
'disableTag' => array(),
'enable' => array(),
'index' => array('*'),
'taxonomyMassConfirmation' => array('perm_tagger'),

View File

@ -926,6 +926,7 @@ class EventsController extends AppController {
if (isset($this->params['named']['public']) && $this->params['named']['public']) {
$conditions['distribution'] = array(3, 5);
}
$conditions['includeFeedCorrelations'] = true;
$results = $this->Event->fetchEvent($this->Auth->user(), $conditions);
if (empty($results)) throw new NotFoundException('Invalid event');
//if the current user is an org admin AND event belongs to his/her org, fetch also the event creator info

View File

@ -27,9 +27,15 @@ class FeedsController extends AppController {
public function index() {
$scope = isset($this->passedArgs['scope']) ? $this->passedArgs['scope'] : 'all';
if ($scope !== 'all') {
$this->paginate['conditions'][] = array(
'Feed.default' => $scope == 'custom' ? 0 : 1
);
if ($scope == 'enabled') {
$this->paginate['conditions'][] = array(
'Feed.enabled' => 1
);
} else {
$this->paginate['conditions'][] = array(
'Feed.default' => $scope == 'custom' ? 0 : 1
);
}
}
$data = $this->paginate();
$this->loadModel('Event');
@ -41,6 +47,9 @@ class FeedsController extends AppController {
}
}
}
if ($this->_isSiteAdmin()) {
$data = $this->Feed->attachFeedCacheTimestamps($data);
}
if ($this->_isRest()) {
foreach ($data as $k => $v) {
unset($data[$k]['SharingGroup']);
@ -394,7 +403,7 @@ class FeedsController extends AppController {
$resultArray = array();
}
$this->params->params['paging'] = array($this->modelClass => $params);
$resultArray = $this->Feed->getFreetextFeedCorrelations($resultArray);
$resultArray = $this->Feed->getFreetextFeedCorrelations($resultArray, $feed['Feed']['id']);
// remove all duplicates
foreach ($resultArray as $k => $v) {
for ($i = 0; $i < $k; $i++) {
@ -421,7 +430,8 @@ class FeedsController extends AppController {
if ($resultArray == false) {
$resultArray = array();
}
$resultArray = $this->Feed->getFreetextFeedCorrelations($resultArray);
$resultArray = $this->Feed->getFreetextFeedCorrelations($resultArray, $feed['Feed']['id']);
$resultArray = $this->Feed->getFreetextFeed2FeedCorrelations($resultArray);
// remove all duplicates
foreach ($resultArray as $k => $v) {
for ($i = 0; $i < $k; $i++) {
@ -535,4 +545,52 @@ class FeedsController extends AppController {
}
$this->redirect(array('controller' => 'feeds', 'action' => 'index'));
}
public function cacheFeeds($scope = 'freetext') {
if (Configure::read('MISP.background_jobs')) {
$this->loadModel('Job');
$this->Job->create();
$data = array(
'worker' => 'default',
'job_type' => 'cache_feeds',
'job_input' => $scope,
'status' => 0,
'retries' => 0,
'org' => $this->Auth->user('Organisation')['name'],
'message' => 'Starting feed caching.',
);
$this->Job->save($data);
$jobId = $this->Job->id;
$process_id = CakeResque::enqueue(
'default',
'ServerShell',
array('cacheFeeds', $this->Auth->user('id'), $jobId, $scope),
true
);
$this->Job->saveField('process_id', $process_id);
$message = 'Feed caching job initiated.';
} else {
$result = $this->Feed->cacheFeedInitiator($this->Auth->user(), false, $scope);
if (!$result) {
$this->Session->setFlash('Caching the feeds has failed.');
$this->redirect(array('action' => 'index'));
}
$message = 'Caching the feeds has successfuly completed.';
}
if ($this->_isRest()) {
return $this->RestResponse->saveSuccessResponse('Feed', 'cacheFeeds', false, $this->response->type(), $message);
} else {
$this->Session->setFlash($message);
$this->redirect(array('controller' => 'feeds', 'action' => 'index'));
}
}
public function compareFeeds($id = false) {
$feeds = $this->Feed->compareFeeds($id);
if ($this->_isRest()) {
return $this->RestResponse->viewData($feeds, $this->response->type());
} else {
$this->set('feeds', $feeds);
}
}
}

View File

@ -996,4 +996,16 @@ class AppModel extends Model {
public function checkFilename($filename) {
return preg_match('@^([a-z0-9_.]+[a-z0-9_.\- ]*[a-z0-9_.\-]|[a-z0-9_.])+$@i', $filename);
}
public function setupRedis() {
$redis = new Redis();
$host = Configure::read('MISP.redis_host') ? Configure::read('MISP.redis_host') : '127.0.0.1';
$port = Configure::read('MISP.redis_port') ? Configure::read('MISP.redis_port') : 6379;
$database = Configure::read('MISP.redis_database') ? Configure::read('MISP.redis_database') : 13;
if (!$redis->connect($host, $port)) {
return false;
}
$redis->select($database);
return $redis;
}
}

View File

@ -1460,6 +1460,10 @@ class Event extends AppModel {
$this->Warninglist = ClassRegistry::init('Warninglist');
$warninglists = $this->Warninglist->fetchForEventView();
}
if ($isSiteAdmin && isset($options['includeFeedCorrelations']) && $options['includeFeedCorrelations']) {
$this->Feed = ClassRegistry::init('Feed');
$event['Attribute'] = $this->Feed->attachFeedCorrelations($event['Attribute']);
}
foreach ($event['Attribute'] as $key => $attribute) {
if ($options['enforceWarninglist'] && !$this->Warninglist->filterWarninglistAttributes($warninglists, $attribute, $this->Warninglist)) {
unset($event['Attribute'][$key]);
@ -1492,6 +1496,9 @@ class Event extends AppModel {
// This is to differentiate between proposals that were made to an attribute for modification and between proposals for new attributes
if (isset($event['ShadowAttribute'])) {
if (isset($options['includeFeedCorrelations']) && $options['includeFeedCorrelations']) {
$event['ShadowAttribute'] = $this->Feed->attachFeedCorrelations($event['ShadowAttribute']);
}
foreach ($event['ShadowAttribute'] as $k => $sa) {
if (!empty($sa['old_id'])) {
if ($event['ShadowAttribute'][$k]['old_id'] == $attribute['id']) {

View File

@ -194,16 +194,30 @@ class Feed extends AppModel {
}
$resultArray = array_slice($resultArray, $start, $limit);
}
return $resultArray;
}
public function getFreetextFeedCorrelations($data) {
public function getFreetextFeedCorrelations($data, $feedId) {
$values = array();
foreach ($data as $key => $value) {
$values[] = $value['value'];
}
$this->Attribute = ClassRegistry::init('Attribute');
$redis = $this->setupRedis();
if ($redis !== false) {
$feeds = $this->find('all', array(
'recursive' => -1,
'conditions' => array('Feed.id !=' => $feedId),
'fields' => array('id', 'name', 'url', 'provider', 'source_format')
));
foreach ($feeds as $k => $v) {
if (!$redis->exists('misp:feed_cache:' . $v['Feed']['id'])) {
unset($feeds[$k]);
}
}
} else {
return array();
}
// Adding a 3rd parameter to a list find seems to allow grouping several results into a key. If we ran a normal list with value => event_id we'd only get exactly one entry for each value
// The cost of this method is orders of magnitude lower than getting all id - event_id - value triplets and then doing a double loop comparison
$correlations = $this->Attribute->find('list', array('conditions' => array('Attribute.value1' => $values, 'Attribute.deleted' => 0), 'fields' => array('Attribute.event_id', 'Attribute.event_id', 'Attribute.value1')));
@ -213,10 +227,35 @@ class Feed extends AppModel {
if (isset($correlations[$value['value']])) {
$data[$key]['correlations'] = array_values($correlations[$value['value']]);
}
if ($redis) {
foreach ($feeds as $k => $v) {
if ($redis->sismember('misp:feed_cache:' . $v['Feed']['id'], md5($value['value']))) {
$data[$key]['feed_correlations'][] = array($v);
}
}
}
}
return $data;
}
public function attachFeedCorrelations($objects) {
$redis = $this->setupRedis();
if ($redis !== false) {
$feeds = $this->find('all', array(
'recursive' => -1,
'fields' => array('id', 'name', 'url', 'provider', 'source_format')
));
foreach ($objects as $k => $object) {
foreach ($feeds as $k2 => $feed) {
if ($redis->sismember('misp:feed_cache:' . $feed['Feed']['id'], md5($object['value']))) {
$objects[$k]['Feed'][] = $feed['Feed'];
}
}
}
}
return $objects;
}
public function downloadFromFeed($actions, $feed, $HttpSocket, $user, $jobId = false) {
if ($jobId) {
$job = ClassRegistry::init('Job');
@ -691,4 +730,170 @@ class Feed extends AppModel {
}
return true;
}
public function cacheFeedInitiator($user, $jobId = false, $scope = 'freetext') {
$params = array(
'conditions' => array('enabled' => 1),
'recursive' => -1,
'fields' => array('source_format', 'input_source', 'url', 'id')
);
if ($scope !== 'all') {
if (is_numeric($scope)) {
$params['conditions']['id'] = $scope;
} else if ($scope == 'freetext' || $scope == 'csv') {
$params['conditions']['source_format'] = array('csv', 'freetext');
} else if ($scope == 'misp') {
$params['conditions']['source_format'] = 'misp';
}
}
$feeds = $this->find('all', $params);
if ($jobId) {
$job = ClassRegistry::init('Job');
$job->id = $jobId;
if (!$job->exists()) {
$jobId = false;
}
}
$redis = $this->setupRedis();
if ($redis === false) {
return 'Redis not reachable.';
}
foreach ($feeds as $k => $feed) {
$redis->del('misp:feed_cache:' . $feed['Feed']['id']);
$this->__cacheFeed($feed, $redis, $jobId);
if ($jobId) {
$job->saveField('progress', 100 * $k / count($feeds));
$job->saveField('message', 'Feed ' . $feed['Feed']['id'] . ' cached.');
}
}
return true;
}
public function attachFeedCacheTimestamps($data) {
$redis = $this->setupRedis();
if ($redis === false) {
return $data;
}
foreach ($data as $k => $v) {
$data[$k]['Feed']['cache_timestamp'] = $redis->get('misp:feed_cache_timestamp:' . $data[$k]['Feed']['id']);
}
return $data;
}
private function __cacheFeed($feed, $redis, $jobId = false) {
if ($feed['Feed']['input_source'] == 'local') {
$HttpSocket = false;
} else {
$HttpSocket = $this->__setupHttpSocket($feed);
}
if ($feed['Feed']['source_format'] == 'misp') {
return $this->__cacheMISPFeed($feed, $redis, $HttpSocket, $jobId);
} else {
return $this->__cacheFreetextFeed($feed, $redis, $HttpSocket, $jobId);
}
}
private function __cacheFreetextFeed($feed, $redis, $HttpSocket, $jobId = false) {
if ($jobId) {
$job = ClassRegistry::init('Job');
$job->id = $jobId;
if (!$job->exists()) {
$jobId = false;
}
}
$values = $this->getFreetextFeed($feed, $HttpSocket, $feed['Feed']['source_format'], 'all');
foreach ($values as $k => $value) {
$redis->sAdd('misp:feed_cache:' . $feed['Feed']['id'], md5($value['value']));
if ($jobId && ($k % 1000 == 0)) {
$job->saveField('message', 'Feed ' . $feed['Feed']['id'] . ': ' . $k . ' values cached.');
}
}
$redis->set('misp:feed_cache_timestamp:' . $feed['Feed']['id'], time());
return true;
}
private function __cacheMISPFeed($feed, $redis, $HttpSocket, $jobId = false) {
if ($jobId) {
$job = ClassRegistry::init('Job');
$job->id = $jobId;
if (!$job->exists()) {
$jobId = false;
}
}
$this->Attribute = ClassRegistry::init('Attribute');
$manifest = $this->getManifest($feed, $HttpSocket);
$k = 0;
foreach ($manifest as $uuid => $event) {
$data = false;
$path = $feed['Feed']['url'] . '/' . $uuid . '.json';
if (isset($feed['Feed']['input_source']) && $feed['Feed']['input_source'] == 'local') {
if (file_exists($path)) {
$data = file_get_contents($path);
}
} else {
$HttpSocket = $this->__setupHttpSocket($feed);
$request = $this->__createFeedRequest();
$response = $HttpSocket->get($path, '', $request);
if ($response->code != 200) {
return false;
}
$data = $response->body;
}
if ($data) {
$event = json_decode($data, true);
foreach ($event['Event']['Attribute'] as $attribute) {
if (!in_array($attribute['type'], $this->Attribute->nonCorrelatingTypes)) {
if (in_array($attribute['type'], $this->Attribute->getCompositeTypes())) {
$value = explode('|', $attribute['value']);
$redis->sAdd('misp:feed_cache:' . $feed['Feed']['id'], md5($value[0]));
$redis->sAdd('misp:feed_cache:' . $feed['Feed']['id'], md5($value[1]));
} else {
$redis->sAdd('misp:feed_cache:' . $feed['Feed']['id'], md5($attribute['value']));
}
}
}
}
$k++;
if ($jobId && ($k % 10 == 0)) {
$job->saveField('message', 'Feed ' . $feed['Feed']['id'] . ': ' . $k . ' events cached.');
}
}
$redis->set('misp:feed_cache_timestamp:' . $feed['Feed']['id'], time());
return true;
}
public function compareFeeds($id = false) {
$redis = $this->setupRedis();
if ($redis === false) {
return array();
}
$fields = array('id', 'input_source', 'source_format', 'url', 'provider', 'name', 'default');
$feeds = $this->find('all', array(
'recursive' => -1,
'fields' => $fields
));
// we'll use this later for the intersect
$fields[] = 'values';
$fields = array_flip($fields);
// Get all of the feed cache cardinalities for all feeds - if a feed is not cached remove it from the list
foreach ($feeds as $k => $feed) {
if (!$redis->exists('misp:feed_cache:' . $feed['Feed']['id'])) {
unset($feeds[$k]);
continue;
}
$feeds[$k]['Feed']['values'] = $redis->sCard('misp:feed_cache:' . $feed['Feed']['id']);
}
$feeds = array_values($feeds);
foreach ($feeds as $k => $feed) {
foreach ($feeds as $k2 => $feed2) {
if ($k == $k2) continue;
$intersect = $redis->sInter('misp:feed_cache:' . $feed['Feed']['id'], 'misp:feed_cache:' . $feed2['Feed']['id']);
$feeds[$k]['Feed']['ComparedFeed'][] = array_merge(array_intersect_key($feed2['Feed'], $fields), array(
'overlap_count' => count($intersect),
'overlap_percentage' => round(100 * count($intersect) / $feeds[$k]['Feed']['values']),
));
}
}
return $feeds;
}
}

View File

@ -734,6 +734,30 @@ class Server extends AppModel {
'type' => 'boolean',
'null' => true
),
'redis_host' => array(
'level' => 0,
'description' => 'The host running the redis server to be used for generic MISP tasks such as caching. This is not to be confused by the redis server used by the background processing.',
'value' => '127.0.0.1',
'errorMessage' => '',
'test' => 'testForEmpty',
'type' => 'string'
),
'redis_port' => array(
'level' => 0,
'description' => 'The port used by the redis server to be used for generic MISP tasks such as caching. This is not to be confused by the redis server used by the background processing.',
'value' => 6379,
'errorMessage' => '',
'test' => 'testForNumeric',
'type' => 'numeric'
),
'redis_database' => array(
'level' => 0,
'description' => 'The database on the redis server to be used for generic MISP tasks. If you run more than one MISP instance, please make sure to use a different database on each instance.',
'value' => 13,
'errorMessage' => '',
'test' => 'testForNumeric',
'type' => 'numeric'
)
),
'GnuPG' => array(
'branch' => 1,

View File

@ -152,6 +152,13 @@
endif;
?>
<th>Related Events</th>
<?php
if ($isSiteAdmin):
?>
<th>Feed hits</th>
<?php
endif;
?>
<th title="<?php echo $attrDescriptions['signature']['desc'];?>"><?php echo $this->Paginator->sort('to_ids', 'IDS');?></th>
<th title="<?php echo $attrDescriptions['distribution']['desc'];?>"><?php echo $this->Paginator->sort('distribution');?></th>
<?php if (Configure::read('Plugin.Sightings_enable') !== false): ?>
@ -403,6 +410,34 @@
?>
</ul>
</td>
<?php
if ($isSiteAdmin):
?>
<td class="shortish <?php echo $extra; ?>">
<ul class="inline" style="margin:0px;">
<?php
if (!empty($object['Feed'])):
foreach ($object['Feed'] as $feed):
$popover = '';
foreach ($feed as $k => $v):
if ($k == 'id') continue;
$popover .= '<span class=\'bold black\'>' . Inflector::humanize(h($k)) . '</span>: <span class="blue">' . h($v) . '</span><br />';
endforeach;
?>
<li style="padding-right: 0px; padding-left:0px;" data-toggle="popover" data-content="<?php echo h($popover);?>" data-trigger="hover"><span>
<?php
echo $this->Html->link($feed['id'], array('controller' => 'feeds', 'action' => 'previewIndex', $feed['id']));
endforeach;
?>
</li>
<?php
endif;
?>
</ul>
</td>
<?php
endif;
?>
<td class="short <?php echo $extra; ?>">
<div id = "<?php echo $currentType . '_' . $object['id'] . '_to_ids_placeholder'; ?>" class = "inline-field-placeholder"></div>
<div id = "<?php echo $currentType . '_' . $object['id'] . '_to_ids_solid'; ?>" class="inline-field-solid" ondblclick="activateField('<?php echo $currentType; ?>', '<?php echo $object['id']; ?>', 'to_ids', <?php echo $event['Event']['id'];?>);">

View File

@ -311,6 +311,7 @@
<li id='liindex'><a href="<?php echo $baseurl;?>/feeds/index">List Feeds</a></li>
<li id='liadd'><a href="<?php echo $baseurl;?>/feeds/add">Add Feed</a></li>
<li id='liadd'><a href="<?php echo $baseurl;?>/feeds/importFeeds">Import Feeds from JSON</a></li>
<li id='licompare'><a href="<?php echo $baseurl;?>/feeds/compareFeeds">Compare feed overlap</a></li>
<?php if ($menuItem === 'edit'): ?>
<li class="active"><a href="#">Edit Feed</a></li>
<?php elseif ($menuItem === 'previewIndex'): ?>

View File

@ -0,0 +1,79 @@
<?php
$feedTemplate = array(
'id', 'name', 'provider', 'url'
);
?>
<div class="feed index">
<h2>Feed overlap analysis</h2>
<div>
<table class="table table-striped table-hover table-condensed" style="width:100px;">
<tr>
<th>&nbsp;</th>
<?php
foreach ($feeds as $item):
$popover = '';
foreach ($feedTemplate as $element):
$popover .= '<span class=\'bold\'>' . Inflector::humanize($element) . '</span>: <span class=\'bold blue\'>' . h($item['Feed'][$element]) . '</span><br />';
endforeach;
?>
<th>
<div data-toggle="popover" data-content="<?php echo $popover; ?>" data-trigger="hover">
<?php echo h($item['Feed']['id']); ?>
</div>
</th>
<?php
endforeach;
?>
</tr>
<?php
foreach ($feeds as $item):
$popover = '';
foreach ($feedTemplate as $element):
$popover .= '<span class=\'bold\'>' . Inflector::humanize($element) . '</span>: <span class=\'bold blue\'>' . h($item['Feed'][$element]) . '</span><br />';
endforeach;
?>
<tr>
<td class="short">
<div data-toggle="popover" data-content="<?php echo $popover;?>" data-trigger="hover">
<?php echo h($item['Feed']['id']) . ' ' . h($item['Feed']['name']); ?>&nbsp;
</div>
</td>
<?php
foreach ($feeds as $item2):
$percentage = -1;
$class = 'bold';
foreach ($item['Feed']['ComparedFeed'] as $k => $v):
if ($item2['Feed']['id'] == $v['id']):
$percentage = $v['overlap_percentage'];
if ($percentage <= 5) $class .= ' green';
else if ($percentage <= 50) $class .= ' orange';
else $class .= ' red';
endif;
endforeach;
$title = '';
if ($percentage == 0) $popover = 'None or less than 1% of the data of ' . $item['Feed']['name'] . ' is contained in ' . $item2['Feed']['name'];
else if ($percentage > 0) $popover = $percentage . '% of the data of ' . $item['Feed']['name'] . ' is contained in ' . $item2['Feed']['name'];
?>
<td style="width:50px;" class="<?php echo h($class); ?>">
<div data-toggle="popover" data-content="<?php echo h($popover);?>" data-trigger="hover">
<?php echo (($percentage == -1) ? '-' : h($percentage) . '%');?>
</div>
</td>
<?php
endforeach;
?>
</tr>
<?php
endforeach;
?>
</table>
</div>
</div>
<script type="text/javascript">
$(document).ready(function(){
popoverStartup();
});
</script>
<?php
echo $this->element('side_menu', array('menuList' => 'feeds', 'menuItem' => 'compare'));
?>

View File

@ -1,5 +1,11 @@
<div class="feed index">
<h2><?php echo __('Feeds');?></h2>
<h4>Generate feed lookup caches</h4>
<div class="toggleButtons">
<a href="<?php echo $baseurl; ?>/feeds/cacheFeeds/all" class="toggle-left qet btn btn-inverse">All</a>
<a href="<?php echo $baseurl; ?>/feeds/cacheFeeds/freetext" class="toggle qet btn btn-inverse">Freetext/CSV</a>
<a href="<?php echo $baseurl; ?>/feeds/cacheFeeds/misp" class="toggle-right qet btn btn-inverse">MISP</a>
</div><br />
<div class="pagination">
<ul>
<?php
@ -20,6 +26,7 @@
<span role="button" tabindex="0" aria-label="Default feeds filter" title="Default feeds" class="tabMenuFixed tabMenuFixedCenter tabMenuSides useCursorPointer <?php echo $scope == 'default' ? 'tabMenuActive' : ''; ?>" onclick="window.location='/feeds/index/scope:default'">Default feeds</span>
<span role="button" tabindex="0" aria-label="Custom feeds filter" title="Custom feeds" class="tabMenuFixed tabMenuFixedCenter tabMenuSides useCursorPointer <?php echo $scope == 'custom' ? 'tabMenuActive' : ''; ?> " onclick="window.location='/feeds/index/scope:custom'">Custom Feeds</span>
<span role="button" tabindex="0" aria-label="All feeds" title="All feeds" class="tabMenuFixed tabMenuFixedCenter tabMenuSides useCursorPointer <?php echo $scope == 'all' ? 'tabMenuActive' : ''; ?> " onclick="window.location='/feeds/index/scope:all'">All Feeds</span>
<span role="button" tabindex="0" aria-label="Enabled feeds" title="Enabled feeds" class="tabMenuFixed tabMenuFixedCenter tabMenuSides useCursorPointer <?php echo $scope == 'enabled' ? 'tabMenuActive' : ''; ?> " onclick="window.location='/feeds/index/scope:enabled'">Enabled Feeds</span>
</div>
<table class="table table-striped table-hover table-condensed">
<tr>
@ -36,6 +43,7 @@
<th><?php echo $this->Paginator->sort('distribution');?></th>
<th><?php echo $this->Paginator->sort('tag');?></th>
<th><?php echo $this->Paginator->sort('enabled');?></th>
<th class="actions"><?php echo __('Caching');?></th>
<th class="actions"><?php echo __('Actions');?></th>
</tr><?php
foreach ($feeds as $item):
@ -127,6 +135,30 @@ foreach ($feeds as $item):
<?php endif;?>
</td>
<td class="short"><span class="<?php echo ($item['Feed']['enabled'] ? 'icon-ok' : 'icon-remove'); ?>"></span><span class="short <?php if (!$item['Feed']['enabled'] || empty($ruleDescription)) echo "hidden"; ?>" data-toggle="popover" title="Filter rules" data-content="<?php echo $ruleDescription; ?>"> (Rules)</span>
<td class="short action-links <?php echo $item['Feed']['cache_timestamp'] ? 'bold' : 'bold red';?>">
<?php
if ($item['Feed']['cache_timestamp']):
$units = array('m', 'h', 'd');
$intervals = array(60, 60, 24);
$unit = 's';
$last = time() - $item['Feed']['cache_timestamp'];
foreach ($units as $k => $v) {
if ($last > $intervals[$k]) {
$unit = $v;
$last = floor($last / $intervals[$k]);
}
}
echo 'Age: ' . $last . $unit;
else:
echo 'Not cached';
endif;
if ($item['Feed']['enabled']):
?>
<a href="<?php echo $baseurl;?>/feeds/cacheFeeds/<?php echo h($item['Feed']['id']); ?>" title="Cache feed"><span class="icon-download-alt"></span></a>
<?php
endif;
?>
</td>
<td class="short action-links">
<?php
if (!isset($item['Feed']['event_error'])) {