Files
FreshRSS/app/Models/Context.php
Alexis Degrugillier 4bb8548ecc Add a way to parse search string to extract keywords
This feature is not in use at the moment, but it will be handy to reorganize the query building process. It allows to have more than one keyword in the search box.
Full tests are available as well.

It probably needs a refactoring later, but I think this is the first step to make the application full object oriented and testable.
2015-02-08 20:53:36 -05:00

454 lines
12 KiB
PHP

<?php
/**
* The context object handles the current configuration file and different
* useful functions associated to the current view state.
*/
class FreshRSS_Context {
public static $user_conf = null;
public static $system_conf = null;
public static $categories = array();
public static $name = '';
public static $total_unread = 0;
public static $total_starred = array(
'all' => 0,
'read' => 0,
'unread' => 0,
);
public static $get_unread = 0;
public static $current_get = array(
'all' => false,
'starred' => false,
'feed' => false,
'category' => false,
);
public static $next_get = 'a';
public static $state = 0;
public static $order = 'DESC';
public static $number = 0;
public static $search = '';
public static $first_id = '';
public static $next_id = '';
public static $id_max = '';
/**
* Initialize the context.
*
* Set the correct configurations and $categories variables.
*/
public static function init() {
// Init configuration.
self::$system_conf = Minz_Configuration::get('system');
self::$user_conf = Minz_Configuration::get('user');
$catDAO = new FreshRSS_CategoryDAO();
self::$categories = $catDAO->listCategories();
}
/**
* Returns if the current state includes $state parameter.
*/
public static function isStateEnabled($state) {
return self::$state & $state;
}
/**
* Returns the current state with or without $state parameter.
*/
public static function getRevertState($state) {
if (self::$state & $state) {
return self::$state & ~$state;
} else {
return self::$state | $state;
}
}
/**
* Return the current get as a string or an array.
*
* If $array is true, the first item of the returned value is 'f' or 'c' and
* the second is the id.
*/
public static function currentGet($array = false) {
if (self::$current_get['all']) {
return 'a';
} elseif (self::$current_get['starred']) {
return 's';
} elseif (self::$current_get['feed']) {
if ($array) {
return array('f', self::$current_get['feed']);
} else {
return 'f_' . self::$current_get['feed'];
}
} elseif (self::$current_get['category']) {
if ($array) {
return array('c', self::$current_get['category']);
} else {
return 'c_' . self::$current_get['category'];
}
}
}
/**
* Return true if $get parameter correspond to the $current_get attribute.
*/
public static function isCurrentGet($get) {
$type = $get[0];
$id = substr($get, 2);
switch($type) {
case 'a':
return self::$current_get['all'];
case 's':
return self::$current_get['starred'];
case 'f':
return self::$current_get['feed'] == $id;
case 'c':
return self::$current_get['category'] == $id;
default:
return false;
}
}
/**
* Set the current $get attribute.
*
* Valid $get parameter are:
* - a
* - s
* - f_<feed id>
* - c_<category id>
*
* $name and $get_unread attributes are also updated as $next_get
* Raise an exception if id or $get is invalid.
*/
public static function _get($get) {
$type = $get[0];
$id = substr($get, 2);
$nb_unread = 0;
switch($type) {
case 'a':
self::$current_get['all'] = true;
self::$name = _t('index.feed.title');
self::$get_unread = self::$total_unread;
break;
case 's':
self::$current_get['starred'] = true;
self::$name = _t('index.feed.title_fav');
self::$get_unread = self::$total_starred['unread'];
// Update state if favorite is not yet enabled.
self::$state = self::$state | FreshRSS_Entry::STATE_FAVORITE;
break;
case 'f':
// We try to find the corresponding feed.
$feed = FreshRSS_CategoryDAO::findFeed(self::$categories, $id);
if ($feed === null) {
$feedDAO = FreshRSS_Factory::createFeedDao();
$feed = $feedDAO->searchById($id);
if (!$feed) {
throw new FreshRSS_Context_Exception('Invalid feed: ' . $id);
}
}
self::$current_get['feed'] = $id;
self::$current_get['category'] = $feed->category();
self::$name = $feed->name();
self::$get_unread = $feed->nbNotRead();
break;
case 'c':
// We try to find the corresponding category.
self::$current_get['category'] = $id;
if (!isset(self::$categories[$id])) {
$catDAO = new FreshRSS_CategoryDAO();
$cat = $catDAO->searchById($id);
if (!$cat) {
throw new FreshRSS_Context_Exception('Invalid category: ' . $id);
}
} else {
$cat = self::$categories[$id];
}
self::$name = $cat->name();
self::$get_unread = $cat->nbNotRead();
break;
default:
throw new FreshRSS_Context_Exception('Invalid getter: ' . $get);
}
self::_nextGet();
}
/**
* Set the value of $next_get attribute.
*/
public static function _nextGet() {
$get = self::currentGet();
// By default, $next_get == $get
self::$next_get = $get;
if (self::$user_conf->onread_jump_next && strlen($get) > 2) {
$another_unread_id = '';
$found_current_get = false;
switch ($get[0]) {
case 'f':
// We search the next feed with at least one unread article in
// same category as the currend feed.
foreach (self::$categories as $cat) {
if ($cat->id() != self::$current_get['category']) {
// We look into the category of the current feed!
continue;
}
foreach ($cat->feeds() as $feed) {
if ($feed->id() == self::$current_get['feed']) {
// Here is our current feed! Fine, the next one will
// be a potential candidate.
$found_current_get = true;
continue;
}
if ($feed->nbNotRead() > 0) {
$another_unread_id = $feed->id();
if ($found_current_get) {
// We have found our current feed and now we
// have an feed with unread articles. Leave the
// loop!
break;
}
}
}
break;
}
// If no feed have been found, next_get is the current category.
self::$next_get = empty($another_unread_id) ?
'c_' . self::$current_get['category'] :
'f_' . $another_unread_id;
break;
case 'c':
// We search the next category with at least one unread article.
foreach (self::$categories as $cat) {
if ($cat->id() == self::$current_get['category']) {
// Here is our current category! Next one could be our
// champion if it has unread articles.
$found_current_get = true;
continue;
}
if ($cat->nbNotRead() > 0) {
$another_unread_id = $cat->id();
if ($found_current_get) {
// Unread articles and the current category has
// already been found? Leave the loop!
break;
}
}
}
// No unread category? The main stream will be our destination!
self::$next_get = empty($another_unread_id) ?
'a' :
'c_' . $another_unread_id;
break;
}
}
}
/**
* Determine if the auto remove is available in the current context.
* This feature is available if:
* - it is activated in the configuration
* - the "read" state is not enable
* - the "unread" state is enable
*
* @return boolean
*/
public static function isAutoRemoveAvailable() {
if (!self::$user_conf->auto_remove_article) {
return false;
}
if (self::isStateEnabled(FreshRSS_Entry::STATE_READ)) {
return false;
}
if (!self::isStateEnabled(FreshRSS_Entry::STATE_NOT_READ)) {
return false;
}
return true;
}
/**
* Determine if the "sticky post" option is enabled. It can be enable
* by the user when it is selected in the configuration page or by the
* application when the context allows to auto-remove articles when they
* are read.
*
* @return boolean
*/
public static function isStickyPostEnabled() {
if (self::$user_conf->sticky_post) {
return true;
}
if (self::isAutoRemoveAvailable()) {
return true;
}
return false;
}
/**
* Parse search string to extract the different keywords.
*
* @return array
*/
public function parseSearch() {
$search = self::$search;
$intitle = $this->parseIntitleSearch($search);
$author = $this->parseAuthorSearch($intitle['string']);
$inurl = $this->parseInurlSearch($author['string']);
$pubdate = $this->parsePubdateSearch($inurl['string']);
$date = $this->parseDateSearch($pubdate['string']);
$remaining = array();
$remaining_search = trim($date['string']);
if (strcmp($remaining_search, '') != 0) {
$remaining['search'] = $remaining_search;
}
return array_merge($intitle['search'], $author['search'], $inurl['search'], $date['search'], $pubdate['search'], $remaining);
}
/**
* Parse the search string to find intitle keyword and the search related
* to it.
* The search is the first word following the keyword.
* It returns an array containing the matched string and the search.
*
* @param string $search
* @return array
*/
private function parseIntitleSearch($search) {
if (preg_match('/intitle:(?P<delim>[\'"])(?P<search>.*)(?P=delim)/U', $search, $matches)) {
return array(
'string' => str_replace($matches[0], '', $search),
'search' => array('intitle' => $matches['search']),
);
}
if (preg_match('/intitle:(?P<search>\w*)/', $search, $matches)) {
return array(
'string' => str_replace($matches[0], '', $search),
'search' => array('intitle' => $matches['search']),
);
}
return array(
'string' => $search,
'search' => array(),
);
}
/**
* Parse the search string to find author keyword and the search related
* to it.
* The search is the first word following the keyword except when using
* a delimiter. Supported delimiters are single quote (') and double
* quotes (").
* It returns an array containing the matched string and the search.
*
* @param string $search
* @return array
*/
private function parseAuthorSearch($search) {
if (preg_match('/author:(?P<delim>[\'"])(?P<search>.*)(?P=delim)/U', $search, $matches)) {
return array(
'string' => str_replace($matches[0], '', $search),
'search' => array('author' => $matches['search']),
);
}
if (preg_match('/author:(?P<search>\w*)/', $search, $matches)) {
return array(
'string' => str_replace($matches[0], '', $search),
'search' => array('author' => $matches['search']),
);
}
return array(
'string' => $search,
'search' => array(),
);
}
/**
* Parse the search string to find inurl keyword and the search related
* to it.
* The search is the first word following the keyword except.
* It returns an array containing the matched string and the search.
*
* @param string $search
* @return array
*/
private function parseInurlSearch($search) {
if (preg_match('/inurl:(?P<search>[^\s]*)/', $search, $matches)) {
return array(
'string' => str_replace($matches[0], '', $search),
'search' => array('inurl' => $matches['search']),
);
}
return array(
'string' => $search,
'search' => array(),
);
}
/**
* Parse the search string to find date keyword and the search related
* to it.
* The search is the first word following the keyword.
* It returns an array containing the matched string and the search.
*
* @param string $search
* @return array
*/
private function parseDateSearch($search) {
if (preg_match('/date:(?P<search>[^\s]*)/', $search, $matches)) {
list($min_date, $max_date) = parseDateInterval($matches['search']);
return array(
'string' => str_replace($matches[0], '', $search),
'search' => array('min_date' => $min_date, 'max_date' => $max_date),
);
}
return array(
'string' => $search,
'search' => array(),
);
}
/**
* Parse the search string to find pubdate keyword and the search related
* to it.
* The search is the first word following the keyword.
* It returns an array containing the matched string and the search.
*
* @param string $search
* @return array
*/
private function parsePubdateSearch($search) {
if (preg_match('/pubdate:(?P<search>[^\s]*)/', $search, $matches)) {
list($min_date, $max_date) = parseDateInterval($matches['search']);
return array(
'string' => str_replace($matches[0], '', $search),
'search' => array('min_pubdate' => $min_date, 'max_pubdate' => $max_date),
);
}
return array(
'string' => $search,
'search' => array(),
);
}
}