<?php
/*

 Purpose:       outputs a bulleted list of most recent
                items residing in a category, or a union
                of several categories.


 Contributors: 
 n:en:User:IlyaHaykinson n:en:User:Amgine w:de:Benutzer:Unendlich
 http://en.wikinews.org/wiki/User:Amgine
 http://en.wikinews.org/wiki/User:IlyaHaykinson
 http://de.wikipedia.org/wiki/Benutzer:Unendlich

 This program is free software; you can redistribute it and/or modify
 it under the terms of the GNU General Public License as published by
 the Free Software Foundation; either version 2 of the License, or 
 (at your option) any later version.
 
 This program is distributed in the hope that it will be useful,
 but WITHOUT ANY WARRANTY; without even the implied warranty of
 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 GNU General Public License for more details.
 
 You should have received a copy of the GNU General Public License along
 with this program; if not, write to the Free Software Foundation, Inc.,
 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
 http://www.gnu.org/copyleft/gpl.html

 Current feature request list
     1. RSS feed output?

 To install, add following to LocalSettings.php
   include("extensions/intersection/DynamicPageList.php");
*
@module DynamicPageList
@version $ID 2.0$

*/

$wgDLPminCategories = 1;                // Minimum number of categories to look for
$wgDLPmaxCategories = 5;                // Maximum number of categories to look for
$wgDLPMinResultCount = 1;               // Minimum number of results to allow
$wgDLPMaxResultCount = 50;              // Maximum number of results to allow
$wgDLPAllowUnlimitedResults = true;     // Allow unlimited results
$wgDLPAllowUnlimitedCategories = false; // Allow unlimited categories


$wgExtensionFunctions[] = "wfDynamicPageList";
$wgExtensionCredits['parserhook'][] = array(
	'name' => 'DynamicPageList',
	'description' => 'outputs a bulleted list of the most recent items residing in a category, or a union of several categories',
	'url' => 'http://meta.wikimedia.org/wiki/DynamicPageList'
);

 
function wfDynamicPageList() {
    global $wgParser, $wgMessageCache;
   
    $wgMessageCache->addMessages( array(
					'dynamicpagelist_toomanycats' 					=> 'DynamicPageList: Too many categories!',
					'dynamicpagelist_toofewcats' 					=> 'DynamicPageList: Too few categories!',
					'dynamicpagelist_noresults'						=> 'DynamicPageList: No results!',
					'dynamicpagelist_noincludecats'					=> 'DynamicPageList: You need to include at least one category, or specify a namespace!',
					'dynamicpagelist_noincludedcatsbutcatdate' 		=> 'DynamicPageList: You need to include at least one category to use \'addfirstcategorydate=true\' or \'ordermethod=categoryadd\'!',
					'dynamicpagelist_morethanonecatbutcatdate'		=> 'DynamicPageList: If you include more than one category you cannot use \'addfirstcategorydate=true\' or \'ordermethod=categoryadd\'!',
					'dynamicpagelist_catoutputwithwrongordermethod'	=> 'DynamicPageList: You need to use \'ordermethod=title\' when using category-style output!',

					)
				  );

    $wgParser->setHook( "DynamicPageList", "DynamicPageList" );
}

	/* **
	 * function: setParams
	 * **
	 * Purpose: constructor, parse parameters
	 * parameters
	 *	aCategories				ARRAY	category names
	 *	aExcludeCategories		ARRAY	notcategory names
	 *	bShowNamespace			BOOL	true = show namespace in link
	 *	bNamespace				BOOL	true = only from namespace
	 *	iNamspace				INTEGER	namespace id
	 *	sOrder					STRING	sorting direction
	 *	sOrderMethod			STRING	method of sorting to use
	 *	bSuppressErrors			BOOL	true = do not return errors
	 *	sRedirects				STRING	show, show only, or exclude redirects
	 *	bAddFirstCategoryDate	BOOL	display date added to 1st category
	 *	sStartList				STRING	HTML list type opening tag
	 *	sEndList				STRING	HTML list type closing tag
	 *	sStartItem				STRING	HTML list item opening tag
	 *	sEndItem				STRING	HTML list item closing tag
	 *	iCatCount				INTEGER count of include categories
	 *	iExcludeCatCount		INTEGER count of exclued categories
	 *	iTotalCatCount			INTEGER	count of both included and excluded
	 *	bCountSet				BOOL	true = final category count determined
	 *	iCount					INTEGER	total categories
	 * **
	 * @param STRING $input
	 * @return ARRAY $aParameters
	 * ** */
function setParams ( $input ) {
	// Globals
	global $wgContLang, $wgDLPminCategories;
	global $wgDLPmaxCategories, $wgDLPMinResultCount, $wgDLPMaxResultCount;
	global $wgDLPAllowUnlimitedResults, $wgDLPAllowUnlimitedCategories;

	$aParameters = array(
		'aCategories' => array(),
		'aExcludeCategories' => array(),
		'bShowNamespace' => true,
		'aNamespace' => array(),
		'abNamespace' => array(),
		'sOrder' => 'descending',
		'sOrderMethod' => 'lastedit',
		'bSuppressErrors' => false,
		'sRedirects' => 'exclude',
		'bAddFirstCategoryDate' => false,
		'sStartList' => '<ul>',
		'sEndList' => '</ul>',
		'sStartItem' => '<li>',
		'sEndItem' => '</li>'
		);

	$aParams = explode("\n", $input);

	// Loop through passed parameters
	foreach ($aParams as $sParam) {
		$aParam = explode('=', $sParam);
	
		// if improper, skip
		if (  count( $aParam ) < 2 ) continue;

		$sType = trim($aParam[0]);
		$sArg = trim($aParam[1]);
	
		switch ( $sType ) {
			case 'category':
				$aCats = explode(',', $sArg);
				$aaCats = array();
				foreach($aCats as $sParam) {
					$sParam=trim($sParam);
					$title = Title::newFromText( $sParam );
					if( $title != NULL ) $aaCats[] = $title;
				}
				if (!empty($aaCats)) $aParameters['aCategories'][] = $aaCats;
				break;
			case 'notcategory':
				$title = Title::newFromText( $sArg );
				// if not a real category, skip
				if( is_null( $title ) ) continue;
				$aParameters['aExcludeCategories'][] = $title;
				break;
			case 'namespace':
				$aNS = explode(',', $sArg);
				if (count($aNS)) {
					// If second namespace assignment, reset the array
					$aParameters['aNamespace'] = array();
					$aParameters['abNamespace'] = array();
					foreach ($aNS as $key => $sNS) {
						$sNS = trim($sNS);
						// check valid namespace value
						$ns = $wgContLang->getNsIndex($sNS);
						if (NULL != $ns) {
							$aParameters['aNamespace'][$key] = $ns;
							$aParameters['abNamespace'][$key] = true;
						} else {
							$aParameters['aNamespace'][$key] = intval($sNS);
							if ($aParameters['aNamespace'][$key] >= 0) {
								$aParameters['abNamespace'][$key] = true;
							} else {
								$aParameters['abNamespace'][$key] = false;
							}
						}
					}
				}
				break;
			case 'shownamespace':
				if ('false' == $sArg) $aParameters['bShowNamespace'] = false;
				else $aParameters['bShowNamespace'] = true;
				break;
			case 'mode':
				switch ($sArg) {
					case 'none':
						$aParameters['sStartList'] = '';
						$aParameters['sEndList'] = '';
						$aParameters['sStartItem'] = '';
						$aParameters['sEndItem'] = '<br/>';
						$aParameters['bLastEndItem'] = false;
						break;
					case 'inline':
						$aParameters['sStartList'] = '';
						$aParameters['sEndList'] = '';
						$aParameters['sStartItem'] = '';
						$aParameters['sEndItem'] = ' - ';
						$aParameters['bLastEndItem'] = false;
						break;
					case 'ordered':
						$aParameters['sStartList'] = '<ol>';
						$aParameters['sEndList'] = '</ol>';
						$aParameters['sStartItem'] = '<li>';
						$aParameters['sEndItem'] = '</li>';
						$aParameters['bLastEndItem'] = true;
						break;
					case 'unordered':
					default:
						$aParameters['sStartList'] = '<ul>';
						$aParameters['sEndList'] = '</ul>';
						$aParameters['sStartItem'] = '<li>';
						$aParameters['sEndItem'] = '</li>';
						$aParameters['bLastEndItem'] = true;
				}
				break;
			case 'order':
				switch ($sArg) {
					case 'ascending':
						$aParameters['sOrder'] = 'ascending';
						break;
					case 'descending':
					default:
						$aParameters['sOrder'] = 'descending';
				}
				break;
			case 'ordermethod':
				switch ($sArg) {
					case 'lastedit':
						$aParameters['sOrderMethod'] = 'lastedit';
						break;
					case 'categoryadd':
					default:
						$aParameters['sOrderMethod'] = 'categoryadd';
				}
				break;
			case 'count':
				$aParameters['iCount'] = IntVal( $sArg );
				$aParameters['bCountSet'] = true;
				break;
			case 'suppresserrors':
				if ('true' == $sArg) $aParameters['bSuppressErrors'] = true;
				else $aParameters['bSuppressErrors'] = false;
				break;
			case 'redirects':
				switch ($sArg) {
					case 'include':
						$aParameters['sRedirects'] = 'include';
						break;
					case 'only':
						$aParameters['sRedirects'] = 'only';
						break;
					case 'exclude':
					default:
						$aParameters['sRedirects'] = 'exclude';
				}
				break;
			case 'addfirstcategorydate':
				if ('true' == $sArg) $aParameters['bAddFirstCategoryDate'] = true;
				else $aParameters['bAddFirstCategoryDate'] = false;
				break;
			default:
		}
	}

	// Calculate and check category counts against configuration limits
	
	$aParameters['iCatCount'] = count($aParameters['aCategories']);
	$aParameters['iTotalIncludeCatCount'] = count($aParameters['aCategories'], COUNT_RECURSIVE) - $aParameters['iCatCount'];
	$aParameters['iExcludeCatCount'] = count($aParameters['aExcludeCategories']);
	$aParameters['iTotalCatCount'] = $aParameters['iCatCount'] + $aParameters['iExcludeCatCount'];
	
	// 'DynamicPageList: You need to include at least one category, or specify a namespace!'
	if ($aParameters['iCatCount'] < 1 && !count($aParameters['abNamespace'])) {
		if (false == $aParameters['bSuppressErrors']) {
			return htmlspecialchars( wfMsg( 'dynamicpagelist_noincludecats' ) ); 
		} else return '';
	}
	
	// 'DynamicPageList: Too few categories!'
	if ($aParameters['iTotalIncludeCatCount'] < $wgDLPminCategories && !count($aParameters['abNamespace'])) {
		if (false == $aParameters['bSuppressErrors']) {
			echo $aParameters['iTotalIncludeCatCount'], $wgDLPminCategories;
			return htmlspecialchars( wfMsg( 'dynamicpagelist_toofewcats' ) );
		} else return '';
	}
	// 'DynamicPageList: Too many categories!'
	if ( $aParameters['iTotalCatCount'] > $wgDLPmaxCategories && !$wgDLPAllowUnlimitedCategories ) {
		if (false == $aParameters['bSuppressErrors'])
			return htmlspecialchars( wfMsg( 'dynamicpagelist_toomanycats' ) ); 
		else return '';
	}
	// DynamicPageList: You need to include at least one category to use \'addfirstcategorydate=true\' or \'ordermethod=categoryadd\'!
	if ( $aParameters['iCatCount'] == 0 && ($aParameters['sOrderMethod'] == 'categoryadd' || $aParameters['bAddFirstCategoryDate'] == true) ) {
		if (false == $aParameters['bSuppressErrors'])
			return htmlspecialchars( wfMsg( 'dynamicpagelist_noincludedcatsbutcatdate' ) );
		else return '';
	}
	// DynamicPageList: If you include more than one category you cannot use \'addfirstcategorydate=true\' or \'ordermethod=categoryadd\'!
	if ($aParameters['iTotalCatCount'] > 1 && ($aParameters['sOrderMethod'] == 'categoryadd' || $aParameters['bAddFirstCategoryDate'] == true) ) {
		if (false == $aParameters['bSuppressErrors'])
			return htmlspecialchars( wfMsg( 'dynamicpagelist_morethanonecatbutcatdate' ) );
		else return '';
	}
	// DynamicPageList: You need to use \'ordermethod=title\' when using category-style output!
	if ($aParameters['sOutputMode'] == 'category' && $aParameters['sOrderMethod'] != 'title') {
		if (false == $aParameters['bSuppressErrors'])
			return htmlspecialchars( wfMsg( 'dpl2_catoutputwithwrongordermethod' ) );	
		else return '';
	}
		
	if ($aParameters['bCountSet']) {
		if ($aParameters['iCount'] < $wgDLPMinResultCount) $aParameters['iCount'] = $wgDLPMinResultCount;
		if ($aParameters['iCount'] > $wgDLPMaxResultCount) $aParameters['iCount'] = $wgDLPMaxResultCount;
	} else {
		if (!$wgDLPAllowUnlimitedResults) {
			$aParameters['iCount'] = $wgDLPMaxResultCount;
			$aParameters['bCountSet'] = true;
		}
	}
	
	// prevent display of date when no include category defined
	if ($aParameters['iCatCount'] < 1) $aParameters['bAddFirstCategoryDate'] = false;
	
	return $aParameters;
}
	
/* **
 * function: BuildSQL
 * **
 * Purpose: build the necessary SQL statement for the given parameters
 * **
 * @return STRING SQL query
 * ** */
function BuildSQL ( $aParameters ) {
	// Globals
	
	$dbr =& wfGetDB( DB_SLAVE );

	//build the SQL query
	$sPageTable = $dbr->tableName( 'page' );
	$categorylinks = $dbr->tableName( 'categorylinks' );
	if ($aParameters['iTotalIncludeCatCount'] == 1) 
		$sSqlSelectFrom = "SELECT DISTINCT page_namespace, page_touched, page_title, c1.cl_timestamp FROM $sPageTable";
	else
		$sSqlSelectFrom = "SELECT DISTINCT page_namespace, page_touched, page_title FROM $sPageTable";
		
	// Set the selectfrom
	$iCurrentTableNumber = 0;

    for ($i = 0; $i < $aParameters['iCatCount']; $i++) {
		$sSqlSelectFrom .= " INNER JOIN $categorylinks AS c" . ($iCurrentTableNumber+1);
		$sSqlSelectFrom .= ' ON page_id = c'.($iCurrentTableNumber+1).'.cl_from';
		$sSqlSelectFrom .= ' AND (c'.($iCurrentTableNumber+1).'.cl_to='. $dbr->addQuotes( $aParameters['aCategories'][$i][0]->getDbKey() );
		for ($j = 1; $j < count($aParameters['aCategories'][$i]); $j++) {
			$sSqlSelectFrom .= ' OR c' . ($iCurrentTableNumber+1) . '.cl_to=' . $dbr->addQuotes( $aParameters['aCategories'][$i][$j]->getDbKey() );
		}
		$sSqlSelectFrom .= ') ';
		$iCurrentTableNumber++;
    }
	
	// if namespace
	$i = count($aParameters['abNamespace']);
	if ($i > 0) {
		$sSqlWhere = ' WHERE (page_namespace=' . $aParameters['aNamespace'][0] . ' ';
		if ($i > 1) {
			for ($i = 1; $i < count($aParameters['aNamespace']); $i++ ) {
				if (true == $aParameters['abNamespace'][$i])
					$sSqlWhere .= ' OR page_namespace=' . $aParameters['aNamespace'][$i];
			}
		}
		$sSqlWhere .= ') ';
	}
	// if only or exclude redirects...
    switch ($aParameters['sRedirects']) {
		case 'only':
			if (isset($sSqlWhere)) $sSqlWhere .= ' AND page_is_redirect = 1 ';
			else $sSqlWhere = ' WHERE page_is_redirect = 1 ';
			break;
		case 'exclude':
			if (isset($sSqlWhere)) $sSqlWhere .= ' AND page_is_redirect = 0 ';
			else $sSqlWhere = ' WHERE page_is_redirect = 0 ';
			break;
	}	
	if (!isset($sSqlWhere)) $sSqlWhere = ' WHERE 1=1 ';
	
	for ($i = 0; $i < $aParameters['iExcludeCatCount']; $i++) {
		$sSqlSelectFrom .= " LEFT OUTER JOIN $categorylinks AS c" . ($iCurrentTableNumber+1);
		$sSqlSelectFrom .= ' ON page_id = c'.($iCurrentTableNumber+1).'.cl_from';
		$sSqlSelectFrom .= ' AND c'.($iCurrentTableNumber+1).'.cl_to='.
        $dbr->addQuotes( $aParameters['aExcludeCategories'][$i]->getDbKey() );
		
		$sSqlWhere .= ' AND c'.($iCurrentTableNumber+1).'.cl_to IS NULL';
		
		$iCurrentTableNumber++;
	}
	
	// Set the sort method, direction
	switch ($aParameters['sOrderMethod']) {
		case 'lastedit':
			$sSqlWhere .= ' ORDER BY page_touched ';
			break;
		case 'categoryadd':
			$sSqlWhere .= ' ORDER BY c1.cl_timestamp ';
			break;
		case 'title':
		default:
			$sSqlWhere .= ' ORDER BY page_title ';
			break;
	}
	
	if ('descending' == $aParameters['sOrder']) $sSqlWhere .= 'DESC';
	else $sSqlWhere .= 'ASC';
	
	// if count parameter set
	if ($aParameters['bCountSet']) $sSqlWhere .= ' LIMIT ' . $aParameters['iCount'];


	return $sSqlSelectFrom . $sSqlWhere;
}

function processQuery ( $sSql, $aParameters ) {
	// Globals
	global $wgUser, $wgLang;
	
	$dbr =& wfGetDB( DB_SLAVE );
	
	//DEBUG: output SQL query 
	$output .= "QUERY: [{$sSql}]<br /><br />";    

    // process the query
    $res = $dbr->query($sSql);
	
    $sk =& $wgUser->getSkin();

	// if no results returned
    if ($dbr->numRows( $res ) == 0) {
		if (false == $aParameters['bSuppressErrors'])
			return $output . htmlspecialchars( wfMsg( 'dynamicpagelist_noresults' ) );
		else return '';
    }

    
	// Display the output
    //start list
    $output .= $aParameters['sStartList'] . "\n";
	
    //process results of query, outputing equivalent of <li>[[Article]]</li> for each result,
    //or something similar if the list uses other startlist/endlist
    while ($row = $dbr->fetchObject( $res ) ) {
		$title = Title::makeTitle( $row->page_namespace, $row->page_title);
		$output .= $aParameters['sStartItem'];
		if (true == $aParameters['bAddFirstCategoryDate'])
			$output .= $wgLang->date($row->cl_timestamp) . ': ';
		
		if (true == $aParameters['bShowNamespace'])
			$output .= $sk->makeKnownLinkObj($title);
		else 
			$output .= $sk->makeKnownLinkObj($title, htmlspecialchars($title->getText()));
		$output .= $aParameters['sEndItem'] . "\n";
    }

    //end unordered list
    $output .= $aParameters['sEndList'] . "\n";                                                              
	
	return $output;

}
 
// The callback function for converting the input text to HTML output
function DynamicPageList( $input ) {

    // Disable page cache
    global $wgTitle;
    $dbw =& wfGetDB( DB_MASTER );
    $dbw->update( 'page', array( 'page_touched' => $dbw->timestamp( time() + 120 )), 
    	array( 
        	'page_namespace' => $wgTitle->getNamespace(), 
		'page_title' => $wgTitle->getDBkey() 
		), 'DynamicPageList'
	);

	// parse the parameters
	$aParameters = setParams( $input );
	if (!is_array($aParameters)) return $aParameters;
	
	// build the query
	$sSql = BuildSql( $aParameters );
	
	// Get output
	$output = processQuery( $sSql, $aParameters );
	
	return $output;
}
?>
