diff --git a/include/application.php b/include/application.php index d9ee999..b8bc19c 100644 --- a/include/application.php +++ b/include/application.php @@ -11,6 +11,7 @@ require_once(BASE."include/util.php"); require_once(BASE."include/mail.php"); require_once(BASE."include/maintainer.php"); require_once(BASE."include/tableve.php"); +require_once(BASE."include/db_filter_ui.php"); define("PLATINUM_RATING", "Platinum"); define("GOLD_RATING", "Gold"); @@ -939,11 +940,18 @@ class Application { return 'appId'; } - public static function objectGetEntries($sState, $iRows = 0, $iStart = 0, $sOrderBy = "appId", $bAscending = TRUE) + public static function objectGetEntries($sState, $iRows = 0, $iStart = 0, $sOrderBy = "appId", $bAscending = TRUE, $oFilters = null) { $sLimit = ""; $sOrdering = $bAscending ? "ASC" : "DESC"; + $sExtraTables = ''; + $sWhereFilter = $oFilters ? $oFilters->getWhereClause() : ''; + if($sWhereFilter) + { + $sExtraTables = ',appVersion'; + $sWhereFilter = " AND appVersion.appId = appFamily.appId AND $sWhereFilter"; + } /* Should we add a limit clause to the query? */ if($iRows || $iStart) { @@ -955,10 +963,10 @@ class Application { $iRows = application::objectGetEntriesCount($sState); } - $sQuery = "SELECT appFamily.*, vendor.vendorName AS vendorName FROM appFamily, vendor WHERE + $sQuery = "SELECT DISTINCT(appFamily.appId), appFamily.*, vendor.vendorName AS vendorName FROM appFamily, vendor$sExtraTables WHERE appFamily.vendorId = vendor.vendorId AND - appFamily.state = '?'"; + appFamily.state = '?'$sWhereFilter"; if($sState != 'accepted' && !application::canEdit()) { @@ -997,6 +1005,14 @@ class Application { return $hResult; } + public static function objectGetFilterInfo() + { + $oFilter = new FilterInterface(); + + $oFilter->AddFilterInfo('appVersion.rating', 'Rating', array(FILTER_EQUALS, FILTER_LESS_THAN, FILTER_GREATER_THAN), FILTER_VALUES_ENUM, array('Platinum', 'Gold', 'Silver', 'Bronze', 'Garbage')); + return $oFilter; + } + public static function objectGetSortableFields() { return array('submitTime', 'appName', 'appId', 'userName', 'vendorName'); @@ -1112,23 +1128,31 @@ class Application { } } - public static function objectGetEntriesCount($sState) + public static function objectGetEntriesCount($sState, $oFilters = null) { + $sExtraTables = ''; + $sWhereFilter = $oFilters ? $oFilters->getWhereClause() : ''; + if($sWhereFilter) + { + $sExtraTables = ',appVersion'; + $sWhereFilter = " AND appVersion.appId = appFamily.appId AND $sWhereFilter"; + } + if($sState != 'accepted' && !application::canEdit()) { /* Without edit rights users can only resubmit their rejected entries */ if(!$bRejected) return FALSE; - $sQuery = "SELECT COUNT(appId) as count FROM appFamily WHERE + $sQuery = "SELECT COUNT(DISTINCT(appFamily.appId)) as count FROM appFamily$sExtraTables WHERE submitterId = '?' AND - state = '?'"; + appFamily.state = '?'$sWhereFilter"; $hResult = query_parameters($sQuery, $_SESSION['current']->iUserId, $sState); } else { - $sQuery = "SELECT COUNT(appId) as count FROM appFamily WHERE state = '?'"; + $sQuery = "SELECT COUNT(DISTINCT(appFamily.appId)) as count FROM appFamily$sExtraTables WHERE appFamily.state = '?'$sWhereFilter"; $hResult = query_parameters($sQuery, $sState); } diff --git a/include/db_filter.php b/include/db_filter.php index 2ae001a..e5a556d 100644 --- a/include/db_filter.php +++ b/include/db_filter.php @@ -14,6 +14,7 @@ define('FILTER_GREATER_THAN', 3); define('FILTER_LESS_THAN', 4); define('FILTER_NOT_EQUALS', 5); define('FILTER_NOT_LIKE', 6); +define('FILTER_ENUM', 7); /* A filter as part of an SQL query, such as something = 'somevalue' */ class Filter diff --git a/include/db_filter_ui.php b/include/db_filter_ui.php index 71c90c9..d928b84 100644 --- a/include/db_filter_ui.php +++ b/include/db_filter_ui.php @@ -10,6 +10,10 @@ require_once('db_filter.php'); +define(FILTER_VALUES_NORMAL, 1); +define(FILTER_VALUES_ENUM, 2); +define(FILTER_VALUES_BOOL, 3); + /* Info describing an available filter: what column it applies to, and what comparison options are available */ class FilterInfo @@ -17,12 +21,22 @@ class FilterInfo private $sColumn; private $sDisplayName; private $aTypes; // Available filters for this column + private $iValueType; // Normal, enum ... + private $aValueTypeData; // List of enums + private $aValueTypeDataDisplay; // Optional display names for enums - public function FilterInfo($sColumn, $sDisplayName, $aTypes) + public function FilterInfo($sColumn, $sDisplayName, $aTypes, $iValueType = FILTER_VALUES_NORMAL, $aValueTypeData = array(), $aValueTypeDisplay = array()) { $this->sColumn = $sColumn; $this->sDisplayName = $sDisplayName; $this->aTypes = $aTypes; + $this->iValueType = $iValueType; + $this->aValueTypeData = $aValueTypeData; + + if(sizeof($aValueTypeData) && !sizeof($aValueTypeDisplay)) + $this->aValueTypeDataDisplay = $aValueTypeData; + else + $this->aValueTypeDataDisplay = $aValueTypeDisplay; } public function getColumn() @@ -35,6 +49,21 @@ class FilterInfo return $this->sDisplayName; } + public function getValueType() + { + return $this->iValueType; + } + + public function getValueTypeData() + { + return $this->aValueTypeData; + } + + public function getValueTypeDataDisplay() + { + return $this->aValueTypeDataDisplay; + } + public function getTypes() { return $this->aTypes; @@ -65,11 +94,15 @@ class FilterInterface { private $aFilterInfo; private $oFilterSet; + private $aEscapeChars; + private $aEscapeCharsWith; public function FilterInterface($sTableName = '') { $this->aFilterInfo = array(); - $this->oFilterSet = new FilterSet(mysql_real_escape_string($sTableName)); + $this->oFilterSet = new FilterSet(query_escape_string($sTableName)); + $this->aEscapeChars = array('.'); + $this->aEscapeCharsWith = array('-'); } public function AddFilterObject(Filter $oFilter) @@ -83,15 +116,53 @@ class FilterInterface } /* Convenience function to add a filter option */ - public function AddFilterInfo($sColumn, $sDisplayName, $aTypes) + public function AddFilterInfo($sColumn, $sDisplayName, $aTypes, $iValueType = VALUE_TYPE_NORMAL, $aValueTypeData = array(), $aValueTypeDisplay = array()) { - $this->aFilterInfo[$sColumn] = new FilterInfo($sColumn, $sDisplayName, $aTypes); + $this->aFilterInfo[$sColumn] = new FilterInfo($sColumn, $sDisplayName, $aTypes, $iValueType, $aValueTypeData, $aValueTypeDisplay); } + /* We can't use some special chars in variable names, such as '.' */ + public function escapeChars($sIn) + { + return str_replace($this->aEscapeChars, $this->aEscapeCharsWith, $sIn); + } + + public function unescapeChars($sIn) + { + return str_replace($this->aEscapeWith, $this->aEscape, $sIn); + } + + public function getUrlElement($iId, Filter $oFilter) + { + $sColumn = $this->escapeChars($oFilter->getColumn()); + $oColumn = $this->aFilterInfo[$sColumn]; + + $sId = $iId; + + $shEditor = "&i{$sColumn}Op$sId={$oFilter->getOperatorId()}"; + $shEditor .= "&s{$sColumn}Data$sId={$oFilter->getData()}"; + + return $shEditor; + } + + public function getHiddenInputTag($iId, Filter $oFilter) + { + $sColumn = $this->escapeChars($oFilter->getColumn()); + $oColumn = $this->aFilterInfo[$sColumn]; + + $sId = $iId; + + $shEditor = "getOperatorId()}\">"; + $shEditor .= "getData()}\" />"; + + return $shEditor; + } + + public function getItemEditor($iId, Filter $oFilter) { - $sColumn = $oFilter->getColumn(); - $oColumn = $this->aFilterInfo[$sColumn]; + $sColumn = $this->escapeChars($oFilter->getColumn()); + $oColumn = $this->aFilterInfo[$oFilter->getColumn()]; $sId = ($iId == -1) ? '' : $iId; $shEditor = $oColumn->getDisplayName(); @@ -121,7 +192,90 @@ class FilterInterface $shEditor .= ' '; - $shEditor .= "getData()}\" name='s{$sColumn}Data$sId' size='30' />"; + switch($oColumn->getValueType()) + { + case FILTER_VALUES_NORMAL: + $shEditor .= "getData()}\" name='s{$sColumn}Data$sId' size='30' />"; + break; + case FILTER_VALUES_ENUM: + $shEditor .= $this->getEnumEditor($oColumn, $oFilter, $sId); + break; + } + + return $shEditor; + } + + public function getEnumEditor($oColumn, $oFilter, $sId) + { + $sColumn = $this->escapeChars($oFilter->getColumn()); + $aOptions = $oColumn->getValueTypeData(); + $aOptionNames = $oColumn->getValueTypeDataDisplay(); + + $sData = $oFilter->getData(); + + $shEditor .= ""; + + return $shEditor; + } + + /* Get filter data formatted to fit in a URL */ + public function getUrlData() + { + $shEditor = ''; + $aCounts = array(); + + foreach($this->oFilterSet->getFilters() as $oFilter) + { + $sColumn = $oFilter->getColumn(); + + if(!array_key_exists($sColumn, $aCounts)) + $aCounts[$sColumn] = 0; + + $shEditor .= $this->getUrlElement($aCounts[$sColumn], $oFilter); + + $shEditor .= '
'; + + $aCounts[$sColumn]++; + } + + return $shEditor; + } + + /* Get a list of hidden input tags to preserve form data */ + public function getHiddenFormData() + { + $shEditor = ''; + $aCounts = array(); + + foreach($this->oFilterSet->getFilters() as $oFilter) + { + $sColumn = $oFilter->getColumn(); + + if(!array_key_exists($sColumn, $aCounts)) + $aCounts[$sColumn] = 0; + + $shEditor .= $this->getHiddenInputTag($aCounts[$sColumn], $oFilter); + + $shEditor .= '
'; + + $aCounts[$sColumn]++; + } return $shEditor; } @@ -131,16 +285,18 @@ class FilterInterface $shEditor = ''; $aCounts = array(); - $shEditor .= 'Add new filter
'; + $shEditor .= 'Add new filter (You don’t have to fill out all rows.)
'; foreach($this->aFilterInfo as $oOption) { $oDummyFilter = new Filter($oOption->getColumn(), 0, ''); - $shEditor .= $this->getItemEditor(-1, $oDummyFilter); + $aTypes = $oOption->getTypes(); + + $shEditor .= $this->getItemEditor(-1, $oDummyFilter); $shEditor .= '
'; } if(sizeof($this->oFilterSet->getFilters())) - $shEditor .= '
Active filters
'; + $shEditor .= '
Active filters
'; foreach($this->oFilterSet->getFilters() as $oFilter) { $sColumn = $oFilter->getColumn(); @@ -167,23 +323,26 @@ class FilterInterface { $aReturn = array(); - for($i = 0; array_key_exists('i'.$oOption->getColumn()."Op$i", $aClean); $i++) + for($i = 0; array_key_exists('i'.$this->escapeChars($oOption->getColumn())."Op$i", $aClean); $i++) { - $sData = mysql_real_escape_string($aClean["s{$oOption->getColumn()}Data$i"]); - $iOp = $aClean["i{$oOption->getColumn()}Op$i"]; + $sColumn = $this->escapeChars($oOption->getColumn()); + $sData = query_escape_string($aClean["s{$sColumn}Data$i"]); + $iOp = $aClean["i{$sColumn}Op$i"]; if(!$iOp) continue; $oFilter = new Filter($oOption->getColumn(), $iOp, $sData); + $aReturn[] = $oFilter; } - if(array_key_exists('i'.$oOption->getColumn()."Op", $aClean)) + if(array_key_exists('i'.$this->escapeChars($oOption->getColumn())."Op", $aClean)) { + $sColumn = $this->escapeChars($oOption->getColumn()); $i = sizeof($aReturn); - $sData = $aClean["s{$oOption->getColumn()}Data"]; - $iOp = $aClean["i{$oOption->getColumn()}Op"]; + $sData = $aClean["s{$sColumn}Data"]; + $iOp = $aClean["i{$sColumn}Op"]; if($iOp) { @@ -208,12 +367,12 @@ class FilterInterface public function loadTable($sTableName) { - $this->oFilterSet->loadTable(mysql_real_escape_string($sTableName)); + $this->oFilterSet->loadTable($sTableName); } public function saveTable($sTableName) { - $this->oFilterSet->saveTable(mysql_real_escape_string($sTableName)); + $this->oFilterSet->saveTable($sTableName); } public function getFilterCount() @@ -221,6 +380,11 @@ class FilterInterface return $this->oFilterSet->getFilterCount(); } + public function getWhereClause() + { + return $this->oFilterSet->getWhereClause(); + } + public function getTable($sTable, $iLimit = 0) { $hResult = $this->oFilterSet->getMatchedItems($sTable, $iLimit); diff --git a/include/objectManager.php b/include/objectManager.php index 32c48fb..cec6fac 100644 --- a/include/objectManager.php +++ b/include/objectManager.php @@ -14,6 +14,7 @@ class ObjectManager private $sReturnTo; private $sReturnToTitle; /* Used to preserve the title when processing entries from a queue list, for instance */ private $oMultiPage; + private $oFilters; /* Currently active filters in table view */ private $oTableRow; private $oSortInfo; /* Contains sort info used when displaying tables */ private $oObject; /* Store an instance of the object of the class @@ -63,6 +64,12 @@ class ObjectManager return $this->sState; } + public function getFilterInfoFromInput($aClean) + { + if($this->oFilters) + $this->oFilters->readInput($aClean); + } + public function setState($sState) { /* Ensure that the given state is valid */ @@ -126,6 +133,7 @@ class ObjectManager $this->oMultiPage = new MultiPage(FALSE); $this->oTableRow = new OMTableRow(null); $this->sState = 'accepted'; + $this->oFilters = $this->getOptionalSetting('objectGetFilterInfo', FALSE); // initialize the common responses array $this->aCommonResponses = array(); @@ -278,6 +286,8 @@ class ObjectManager if(!$this->oSortInfo->sCurrentSort) $this->oSortInfo->sCurrentSort = $this->getOptionalSetting('objectGetDefaultSort', ''); + $this->handleFilterControls($aClean); + /* query the class for its entries */ /* We pass in queue states to tell the object */ /* if we are requesting a list of its queued objects or */ @@ -286,23 +296,30 @@ class ObjectManager $this->oMultiPage->iItemsPerPage, $this->oMultiPage->iLowerLimit, $this->oSortInfo->sCurrentSort, - $this->oSortInfo->bAscending); + $this->oSortInfo->bAscending, + $this->oFilters); /* did we get any entries? */ if(!$hResult || query_num_rows($hResult) == 0) { - switch($this->getQueueString($this->getIsQueue(), $this->sState == 'rejected')) + if($this->oFilters->getFilterCount()) { - case "true": - echo "
The queue for '$this->sClass' is empty
"; - break; - case "false": - echo "
No entries of '$this->sClass' are present
"; - break; - case "rejected": - echo "
No rejected entries of '$this->sClass' are ". - "present
"; - break; + echo '
No matches found
'; + } else + { + switch($this->getQueueString($this->getIsQueue(), $this->sState == 'rejected')) + { + case "true": + echo "
The queue for '$this->sClass' is empty
"; + break; + case "false": + echo "
No entries of '$this->sClass' are present
"; + break; + case "rejected": + echo "
No rejected entries of '$this->sClass' are ". + "present
"; + break; + } } if($this->GetOptionalSetting("objectShowAddEntry", FALSE)) @@ -1236,6 +1253,9 @@ class ObjectManager $sUrl .= "&iPage=".$this->oMultiPage->iPage; } + if($this->oFilters) + $sUrl .= $this->oFilters->getUrlData(); + if($this->oSortInfo && $this->oSortInfo->sCurrentSort) { $sUrl .= "&sOrderBy={$this->oSortInfo->sCurrentSort}"; @@ -1267,6 +1287,9 @@ class ObjectManager $this->oMultiPage->iPage."\">\n"; } + if($this->oFilters) + $sReturn .= $this->oFilters->getHiddenFormData(); + if($this->sReturnToTitle) $sReturn .= "sReturnToTitle."\">\n"; @@ -1310,6 +1333,20 @@ class ObjectManager echo $oTableRow->GetString(); } + private function handleFilterControls($aClean) + { + /* Show filter info */ + if($this->oFilters) + { + echo "
makeUrl()."\" >"; + + echo $this->oFilters->getEditor(); + + echo "
"; + echo "
"; + } + } + private function handleMultiPageControls($aClean, $bItemsPerPageSelector = TRUE) { /* Display multi-page browsing controls (prev, next etc.) if applicable. @@ -1361,7 +1398,7 @@ class ObjectManager $sControls .= ""; } - $iTotalEntries = $oObject->objectGetEntriesCount($this->sState); + $iTotalEntries = $oObject->objectGetEntriesCount($this->sState, $this->oFilters); $iNumPages = ceil($iTotalEntries / $iItemsPerPage); if($iNumPages == 0) $iNumPages = 1; diff --git a/objectManager.php b/objectManager.php index 29fb539..30ad2f3 100644 --- a/objectManager.php +++ b/objectManager.php @@ -23,6 +23,7 @@ require_once(BASE.'include/application_queue.php'); require_once(BASE.'include/version_queue.php'); require_once(BASE.'include/testData_queue.php'); require_once(BASE.'include/bugs.php'); +require_once(BASE.'include/db_filter_ui.php'); /* if we have no valid class name we should abort */ if(!isset($aClean['sClass'])) @@ -59,6 +60,7 @@ if(isset($aClean['sReturnTo'])) $oObject->getMultiPageDataFromInput($aClean); $oObject->setSortInfo($aClean); +$oObject->getFilterInfoFromInput($aClean); $sClass = $oObject->getClass(); $oOtherObject = new $sClass($oObject->getId());