<?php
/**
 * This file implements the Calendar plugin.
 *
 * This file is part of the b2evolution project - {@link http://b2evolution.net/}
 *
 * @license GNU GPL v2 - {@link http://b2evolution.net/about/gnu-gpl-license}
 *
 * @copyright (c)2003-2020 by Francois Planque - {@link http://fplanque.com/}
 * Parts of this file are copyright (c)2004-2006 by Daniel HAHLER - {@link http://thequod.de/contact}.
 *
 * @package plugins
 */
if( !defined('EVO_MAIN_INIT') ) die( 'Please, do not access this page directly.' );


/**
 * Calendar Plugin
 *
 * This plugin displays
 */
class calendar_plugin extends Plugin
{
	/**
	 * Variables below MUST be overriden by plugin implementations,
	 * either in the subclass declaration or in the subclass constructor.
	 */

	var $name;
	var $code = 'evo_Calr';
	var $priority = 20;
	var $version = '7.2.3';
	var $author = 'The b2evo Group';
	var $group = 'widget';
	var $subgroup = 'navigation';
	var $widget_icon = 'calendar';


  /**
	 * @var ItemQuery
	 */
	var $ItemQuery;

	/**
	 * Init
	 */
	function PluginInit( & $params )
	{
		$this->name = T_( 'Calendar' );
		$this->short_desc = T_('This skin tag displays a navigable calendar.');
		$this->long_desc = T_('Days containing posts are highlighted.');

		$this->dbtable = 'T_items__item';
		$this->dbprefix = 'post_';
		$this->dbIDname = 'post_ID';
	}


  /**
   * Get definitions for widget specific editable params
   *
	 * @see Plugin::GetDefaultSettings()
	 * @param local params like 'for_editing' => true
	 */
	function get_widget_param_definitions( $params )
	{
		// Initialize an array for the field "Post type":
		$ItemTypeCache = & get_ItemTypeCache();
		$ItemTypeCache->clear();
		$ItemTypeCache->load_where( 'ityp_usage = "post"' );
		$item_type_options = array(
				'#' => T_('Default'),
				''  => T_('All'),
			);
		foreach( $ItemTypeCache->cache as $ItemType )
		{
			$item_type_options[ $ItemType->ID ] = $ItemType->get_name();
		}

		$r = array(
			'title' => array(
				'label' => T_('Block title'),
				'note' => T_('Title to display in your skin.'),
				'size' => 60,
				'defaultvalue' => '',
			),
			'title_link' => array(
				'label' => T_('Link to blog'),
				'note' => T_('Link the block title to the blog?'),
				'type' => 'checkbox',
				'defaultvalue' => false,
			),
			'item_visibility' => array(
				'label' => T_('Item visibility'),
				'note' => T_('What post statuses should be included in the list?'),
				'type' => 'radio',
				'field_lines' => true,
				'options' => array(
						array( 'public', T_('show public posts') ),
						array( 'all', T_('show all posts the current user is allowed to see') ) ),
				'defaultvalue' => 'all',
			),
			'item_type' => array(
				'label' => T_('Exact post type'),
				'note' => T_('What type of items do you want to list?'),
				'type' => 'select',
				'options' => $item_type_options,
				'defaultvalue' => '#',
			),
			'blog_ID' => array(
				'label' => T_( 'Collection' ),
				'note' => T_( 'ID of the collection to use, leave empty for the current collection.' ),
				'size' => 4,
				'type' => 'integer',
				'allow_empty' => true,
			),
			'cat_IDs' => array(
				'label' => T_('Categories'),
				'note' => sprintf( T_('List category IDs separated by %s.'), '<code>,</code>' ),
				'size' => 15,
				'type' => 'text',
				'valid_pattern' => array( 'pattern' => '/^(\d+(,\d+)*|-|\*)?$/',
																	'error'   => T_('Invalid list of Category IDs.') ),
			),
			'displaycaption' => array(
				'label' => T_('Display caption'),
				'note' => T_('Display caption on top of calendar'),
				'type' => 'checkbox',
				'defaultvalue' => true,
			),
			'linktomontharchive' => array(
				'label' => T_('Link caption to archives'),
				'note' => T_('The month in the caption can be clicked to see all posts for this month'),
				'type' => 'checkbox',
				'defaultvalue' => true,
			),
			'headerdisplay' => array(
				'label' => 'Column headers',
				'note' => T_('How do you want to display the days of the week in the column headers?'),
				'type' => 'select',
				'options' => array( 'e' => 'F', 'D' => 'Fri', 'l' => 'Friday', '' => T_('No header') ),
				'defaultvalue' => 'D',
			),
			'navigation' => array(
				'label' => 'Navigation arrows',
				'note' => T_('Where do you want to display the navigation arrows?'),
				'type' => 'select',
				'options' => array( 'caption' => T_('Top'), 'tfoot' => T_('Bottom'), '' => T_('No navigation') ),
				'defaultvalue' => 'tfoot',
			),
			'browseyears' => array(
				'label' => T_('Navigate years'),
				'note' => T_('Display double arrows for yearly navigation?'),
				'type' => 'checkbox',
				'defaultvalue' => true,
			),
		);
		return $r;
	}


	/**
	 * Get keys for block/widget caching
	 *
	 * Maybe be overriden by some widgets, depending on what THEY depend on..
	 *
	 * @param integer Widget ID
	 * @return array of keys this widget depends on
	 */
	function get_widget_cache_keys( $widget_ID = 0 )
	{
		global $Collection, $Blog;

		return array(
				'wi_ID'        => $widget_ID, // Have the widget settings changed ?
				'set_coll_ID'  => $Blog->ID, // Have the settings of the blog changed ? (ex: new skin)
				'cont_coll_ID' => empty( $this->disp_params['blog_ID'] ) ? $Blog->ID : $this->disp_params['blog_ID'], // Has the content of the displayed blog changed ?
			);
	}


	/**
	 * Event handler: SkinTag (widget)
	 *
	 * @param array Associative array of parameters. Valid keys are:
	 *      - 'block_start' : (Default: '<div class="bSideItem">')
	 *      - 'block_end' : (Default: '</div>')
	 *      - 'title' : (Default: T_('Calendar'))
	 *      - 'displaycaption'
	 *      - 'monthformat'
	 *      - 'linktomontharchive'
	 *      - 'tablestart'
	 *      - 'tableend'
	 *      - 'monthstart'
	 *      - 'monthend'
	 *      - 'rowstart'
	 *      - 'rowend'
	 *      - 'headerdisplay'
	 *      - 'headerrowstart'
	 *      - 'headerrowend'
	 *      - 'headercellstart'
	 *      - 'headercellend'
	 *      - 'cellstart'
	 *      - 'cellend'
	 *      - 'linkpostcellstart'
	 *      - 'linkposttodaycellstart'
	 *      - 'todaycellstart'
	 *      - 'todaycellstartpost'
	 *      - 'navigation' : Where do we want to have the navigation arrows? (Default: 'tfoot')
	 *      - 'browseyears' : boolean  Do we want arrows to move one year at a time?
	 *      - 'min_timestamp' : Minimum unix timestamp the user can browse too or 'query' (Default: 2000-01-01)
	 *      - 'max_timestamp' : Maximum unix timestamp the user can browse too or 'query' (Default: now + 1 year )
	 *      - 'postcount_month_atitle'
	 *      - 'postcount_month_atitle_one'
	 *      - 'postcount_year_atitle'
	 *      - 'postcount_year_atitle_one'
	 *      - 'link_type' : 'canonic'|'context' (default: canonic)
	 * @return boolean did we display?
	 */
	function SkinTag( & $params )
	{
		// Prefix of the ItemList object
		$itemlist_prefix = isset( $params['itemlist_prefix'] ) ? $params['itemlist_prefix'] : '';

		global $month;
		global $Collection, $Blog, $cat_array, $cat_modifier;
		global $show_statuses;
		global $author, $assgn, $status, $types;
		global ${$itemlist_prefix.'m'}, $w, $dstart;
		global $s, $sentence, $exact;

		/**
		 * Default params:
		 */
		$params = array_merge( array(
				// This is what will enclose the block in the skin:
				'block_start'       => '<div class="bSideItem">',
				'block_end'         => "</div>\n",
				// This is what will enclose the title:
				'block_title_start' => '<h3>',
				'block_title_end'   => '</h3>',
				// This is what will enclose the body:
				'block_body_start'  => '',
				'block_body_end'    => '',
			), $params );


		$Calendar = new Calendar( ${$itemlist_prefix.'m'}, $params );

		// TODO: automate with a table inside of Calendatr object. Table should also contain descriptions and default values to display in help screen.
		// Note: minbrowse and maxbrowe already work this way.
		if( isset($params['displaycaption']) ) $Calendar->set( 'displaycaption', $params['displaycaption'] );
		if( isset($params['monthformat']) ) $Calendar->set( 'monthformat', $params['monthformat'] );
		if( isset($params['linktomontharchive']) ) $Calendar->set( 'linktomontharchive', $params['linktomontharchive'] );
		if( isset($params['tablestart']) ) $Calendar->set( 'tablestart', $params['tablestart'] );
		if( isset($params['tableend']) ) $Calendar->set( 'tableend', $params['tableend'] );
		if( isset($params['monthstart']) ) $Calendar->set( 'monthstart', $params['monthstart'] );
		if( isset($params['monthend']) ) $Calendar->set( 'monthend', $params['monthend'] );
		if( isset($params['rowstart']) ) $Calendar->set( 'rowstart', $params['rowstart'] );
		if( isset($params['rowend']) ) $Calendar->set( 'rowend', $params['rowend'] );
		if( isset($params['headerdisplay']) ) $Calendar->set( 'headerdisplay', $params['headerdisplay'] );
		if( isset($params['headerrowstart']) ) $Calendar->set( 'headerrowstart', $params['headerrowstart'] );
		if( isset($params['headerrowend']) ) $Calendar->set( 'headerrowend', $params['headerrowend'] );
		if( isset($params['headercellstart']) ) $Calendar->set( 'headercellstart', $params['headercellstart'] );
		if( isset($params['headercellend']) ) $Calendar->set( 'headercellend', $params['headercellend'] );
		if( isset($params['cellstart']) ) $Calendar->set( 'cellstart', $params['cellstart'] );
		if( isset($params['cellend']) ) $Calendar->set( 'cellend', $params['cellend'] );
		if( isset($params['emptycellstart']) ) $Calendar->set( 'emptycellstart', $params['emptycellstart'] );
		if( isset($params['emptycellend']) ) $Calendar->set( 'emptycellend', $params['emptycellend'] );
		if( isset($params['emptycellcontent']) ) $Calendar->set( 'emptycellcontent', $params['emptycellcontent'] );
		if( isset($params['linkpostcellstart']) ) $Calendar->set( 'linkpostcellstart', $params['linkpostcellstart'] );
		if( isset($params['linkposttodaycellstart']) ) $Calendar->set( 'linkposttodaycellstart', $params['linkposttodaycellstart'] );
		if( isset($params['todaycellstart']) ) $Calendar->set( 'todaycellstart', $params['todaycellstart'] );
		if( isset($params['todaycellstartpost']) ) $Calendar->set( 'todaycellstartpost', $params['todaycellstartpost'] );
		if( isset($params['navigation']) ) $Calendar->set( 'navigation', $params['navigation'] );
		if( isset($params['browseyears']) ) $Calendar->set( 'browseyears', $params['browseyears'] );
		if( isset($params['postcount_month_atitle']) ) $Calendar->set( 'postcount_month_atitle', $params['postcount_month_atitle'] );
		if( isset($params['postcount_month_atitle_one']) ) $Calendar->set( 'postcount_month_atitle_one', $params['postcount_month_atitle_one'] );
		if( isset($params['postcount_year_atitle']) ) $Calendar->set( 'postcount_year_atitle', $params['postcount_year_atitle'] );
		if( isset($params['postcount_year_atitle_one']) ) $Calendar->set( 'postcount_year_atitle_one', $params['postcount_year_atitle_one'] );
		// Link type:
		if( isset($params['link_type']) ) $Calendar->set( 'link_type', $params['link_type'] );
		if( isset($params['context_isolation']) ) $Calendar->set( 'context_isolation', $params['context_isolation'] );

		echo $params['block_start'];

		if( !empty($params['title']) )
		{	// We want to display a title for the widget block:
			echo $params['block_title_start'];
			if( $params[ 'title_link' ] )
			{	// Set block title as link to current collection:
				echo '<a href="'.$Blog->gen_blogurl().'" rel="nofollow">'.$params['title'].'</a>';
			}
			else
			{	// Display a block title as simple text:
				echo $params['title'];
			}
			echo $params['block_title_end'];
		}

		echo $params['block_body_start'];

		// CONSTRUCT THE WHERE CLAUSE:

		// - - Select a specific Item:
		// $this->ItemQuery->where_ID( $p, $title );

		// Set filter by collection:
		$blog_ID = empty( $params['blog_ID'] ) ? NULL : $params['blog_ID'];


		if( empty( $params['cat_IDs'] ) )
		{	// Use default categories filter:
			$filter_cat_array = ( $Calendar->link_type == 'context' ) ? $cat_array : array();
		}
		else
		{	// Get categories filter from widget settings:
			$filter_cat_array = sanitize_id_list( $params['cat_IDs'], true );
		}

		if( $Calendar->link_type == 'context' )
		{	// We want to preserve the current context:
			// * - - Restrict to selected blog/categories:
			$Calendar->ItemQuery->where_chapter2( $Blog, $filter_cat_array, $cat_modifier, 'wide', $blog_ID );

			// Restrict to selected authors:
			$Calendar->ItemQuery->where_author( $author );

			// Restrict to selected assignees:
			$Calendar->ItemQuery->where_assignees( $assgn );

			// Restrict to selected satuses:
			$Calendar->ItemQuery->where_statuses( $status );

			// - - - + * * if a month is specified in the querystring, load that month:
			$Calendar->ItemQuery->where_datestart( /* NO m */'', /* NO w */'', $dstart, '', $Blog->get_timestamp_min(), $Blog->get_timestamp_max() );

			// Keyword search stuff:
			$Calendar->ItemQuery->where_keywords( $s, $sentence, $exact );
		}
		else
		{	// We want to preserve only the minimal context:
			// * - - Restrict to selected blog/categories:
			$Calendar->ItemQuery->where_chapter2( $Blog, $filter_cat_array, '', 'wide', $blog_ID );

			// - - - + * * if a month is specified in the querystring, load that month:
			$Calendar->ItemQuery->where_datestart( /* NO m */'', /* NO w */'', '', '', $Blog->get_timestamp_min(), $Blog->get_timestamp_max() );
		}

		if( isset( $params['item_visibility'] ) && $params['item_visibility'] == 'public' )
		{	// Get only the public items:
			$visibility_array = array( 'published' );
		}
		else
		{	// Get the current selected status items:
			$visibility_array = $show_statuses;
		}
		// * Restrict to the statuses we want to show:
		$Calendar->ItemQuery->where_visibility( $visibility_array );

		// Restrict with locale visibility by current navigation locale:
		$Calendar->ItemQuery->where_locale_visibility();

		$item_types = $types;
		if( isset( $params['item_type'] ) )
		{
			if( $params['item_type'] == '#' )
			{	// Exclude pages and intros and sidebar stuff by default:
				$item_types_usage = 'post';
			}
			elseif( $params['item_type'] != 'all' )
			{	// Filter by one selected item type:
				$item_types = $params['item_type'];
			}
		}
		if( isset( $item_types_usage ) )
		{	// Filter by item types usage:
			$Calendar->ItemQuery->where_itemtype_usage( $item_types_usage );
		}
		else
		{	// Filter by item types:
			$Calendar->ItemQuery->where_types( $item_types );
		}

		// DISPLAY:
		$Calendar->display( );

		echo $params['block_body_end'];

		echo $params['block_end'];

		return true;
	}
}


/**
 * Calendar
 *
 * @package evocore
 */
class Calendar
{
	var $year;

	/**
	 * The month to display or empty in mode 'year' with no selected month.
	 * @var string
	 */
	var $month;

	/**
	 * 'month' or 'year'
	 * @var string
	 */
	var $mode;

	var $where;
	/**
	 * SQL query string
	 * @var string
	 */
	var $request;
	/**
	 * Result set
	 */
	var $result;
	/**
	 * Number of rows in result set
	 * @var integer
	 */
	var $result_num_rows;

	var $displaycaption;
	var $monthformat;
	var $monthstart;
	var $monthend;
	var $linktomontharchive;
	/**
	 * Where to do the navigation
	 *
	 * 'caption' or 'tfoot';
	 *
	 * @var string
	 */
	var $navigation = 'tfoot';

	var $tablestart;
	var $tableend;

	var $rowstart;
	var $rowend;

	var $headerdisplay;
	var $headerrowstart;
	var $headerrowend;
	var $headercellstart;
	var $headercellend;

	var $cellstart;
	var $cellend;

	var $emptycellstart;
	var $emptycellend;

	var $emptycellcontent;

	/**
	 * Do we want to browse years in the caption? True by default for mode == year,
	 * false for mode == month (gets set in constructor).
	 * @var boolean
	 */
	var $browseyears;

	/**
	 * Is today in the displayed frame?
	 * @var boolean
	 * @access protected
	 */
	var $today_is_visible;


	var $link_type;
	var $context_isolation;

	var $params = array( );

	/**
	 * Prefix of the ItemList object
	 * @var string
	 */
	var $itemlist_prefix = '';

	/**
	 * Calendar::Calendar(-)
	 *
	 * Constructor
	 *
	 * @param string Month ('YYYYMM'), year ('YYYY'), current ('')
	 * @param array Associative array of parameters. Valid keys are:
	 *      - 'min_timestamp' : Minimum unix timestamp the user can browse too or 'query' (Default: 2000-01-01)
	 *      - 'max_timestamp' : Maximum unix timestamp the user can browse too or 'query' (Default: now + 1 year )
	 */
	function __construct( $m = '', $params = array() )
	{
		global $localtimenow;

		$this->dbtable = 'T_items__item';
		$this->dbprefix = 'post_';
		$this->dbIDname = 'post_ID';

		if( isset( $params['itemlist_prefix'] ) )
		{ // Set a prefix of the ItemList object
			$this->itemlist_prefix = $params['itemlist_prefix'];
		}

		// OBJECT THAT WILL BE USED TO CONSTRUCT THE WHERE CLAUSE:
		$this->ItemQuery = new ItemQuery( $this->dbtable, $this->dbprefix, $this->dbIDname );	// COPY!!

		$localyearnow = date( 'Y', $localtimenow );
		$localmonthnow = date( 'm', $localtimenow );

		// Find out which month to display:
		if( empty($m) )
		{ // Current month (monthly)
			$this->year = $localyearnow;
			$this->month = $localmonthnow;
			$this->mode = 'month';

			$this->today_is_visible = true;
		}
		else
		{	// We have requested a specific date
			$this->year = substr($m, 0, 4);
			if (strlen($m) < 6)
			{ // no month provided
				$this->mode = 'year';

				if( $this->year == $localyearnow )
				{ // we display current year, month gets current
					$this->month = $localmonthnow;
				}
				else
				{ // highlight no month, when not current year
					$this->month = '';
				}
			}
			else
			{
				$this->month = substr($m, 4, 2);
				$this->mode = 'month';
			}

			$this->today_is_visible = ( $this->year == $localyearnow
				&& ( empty($this->month) || $this->month == $localmonthnow ) );
		}


		// Default styling:
		$this->displaycaption = 1;	// set this to 0 if you don't want to display the month name
		$this->monthformat = 'F Y';
		$this->linktomontharchive = true;  // month displayed as link to month' archive

		$this->tablestart = '<table class="bCalendarTable" title="Monthly calendar with links to each day\'s posts">'."\n";
		$this->tableend = '</table>';

		$this->monthstart = '<caption>';
		$this->monthend = "</caption>\n";

		$this->rowstart = '<tr class="bCalendarRow">' . "\n";
		$this->rowend = "</tr>\n";

		$this->headerdisplay = 'D';	 // D => 'Fri'; e => 'F', l (lowercase l) => 'Friday'
		// These codes are twisted because they're the same as for date formats.
		// set this to 0 or '' if you don't want to display the "Mon Tue Wed..." header

		$this->headerrowstart = '<thead><tr class="bCalendarRow">' . "\n";
		$this->headerrowend = "</tr></thead>\n";
		$this->headercellstart = '<th class="bCalendarHeaderCell" scope="col" title="[abbr]">';	// please leave [abbr] there !
		$this->headercellend = "</th>\n";

		$this->cellstart = '<td class="bCalendarCell">';
		$this->cellend = "</td>\n";

		$this->emptycellstart = '<td class="bCalendarEmptyCell">';
		$this->emptycellend = "</td>\n";
		$this->emptycellcontent = '&nbsp;';

		$this->linkpostcellstart = '<td class="bCalendarLinkPost">';
		$this->linkposttodaycellstart = '<td class="bCalendarLinkPostToday">';
		$this->todaycellstart = '<td id="bCalendarToday">';
		$this->todaycellstartpost = '<td id="bCalendarToday" class="bCalendarLinkPost">';

		// Where do we want to have the navigation arrows? tfoot or caption
		$this->navigation = 'tfoot';

		// Do we want arrows to move one year at a time?
		$this->browseyears = true;

		/**#@+
		 * Display number of posts with days/months
		 *
		 * - set to '' (empty) to disable
		 * - %d gets replaced with the number of posts on that day/month
		 */
		$this->postcount_month_atitle = T_('%d posts'); 						// in archive links title tag
		$this->postcount_month_atitle_one = T_('1 post');  					//  -- " -- [for single post]
		$this->postcount_year_atitle = T_('%d posts'); 							// in archive links title tag
		$this->postcount_year_atitle_one = T_('1 post'); 						// in archive links title tag
		/**#@-*/

		// Link type:
		$this->link_type = 'canonic';
		$this->context_isolation = 'm,w,p,title,unit,dstart';

		// New style params:
		$this->params = $params;

		// Default values:
		if( empty( $this->params['min_timestamp'] ) )
		{	// 2000-01-01:
			$this->params['min_timestamp'] = mktime( 0, 0, 0, 1, 1, 2000 );
		}
		if( empty( $this->params['max_timestamp'] ) )
		{	// Now + 1 year:
			$this->params['max_timestamp'] = mktime( 23, 59, 59, date( 'm', $localtimenow  ),  date( 'd', $localtimenow ),  date( 'Y', $localtimenow )+1 );
		}

	}


	/*
	 * Calendar->set(-)
	 *
	 * set a variable
	 */
	function set( $var, $value )
	{
		$this->$var = $value;
	}


	/**
	 * Display the calendar.
	 *
	 * @todo If a specific day (mode == month) or month (mode == year) is selected, apply another class (default to some border)
	 */
	function display()
	{
		global $DB;
		global $weekday, $weekday_abbrev, $weekday_letter, $month, $month_abbrev;
		global $time_difference;

		if( $this->mode == 'month' )
		{
			$end_of_week = ((locale_startofweek() + 7) % 7);

			// fplanque>> note: I am removing the searchframe thing because 1) I don't think it's of any use
			// and 2) it's brutally inefficient! If someone needs this it should be implemented with A SINGLE
			// QUERY which gets the last available post (BTW, I think there is already a function for that somwhere)
			// walter>> As we are just counting items, the ORDER BY db_prefix . date_start doesn't matter. And a side effect
			// of that is make queries standart compatible (compatible with other databases than MySQL)

			$arc_sql = 'SELECT COUNT(DISTINCT '.$this->dbIDname.') AS item_count,
													EXTRACT(DAY FROM '.$this->dbprefix.'datestart) AS myday
									FROM ('.$this->dbtable.' INNER JOIN T_postcats ON '.$this->dbIDname.' = postcat_post_ID)
										INNER JOIN T_categories ON postcat_cat_ID = cat_ID
										LEFT JOIN T_items__type ON post_ityp_ID = ityp_ID
									WHERE EXTRACT(YEAR FROM '.$this->dbprefix.'datestart) = \''.$this->year.'\'
										AND EXTRACT(MONTH FROM '.$this->dbprefix.'datestart) = \''.$this->month.'\'
										'.$this->ItemQuery->get_where( ' AND ' ).'
									GROUP BY myday '.$this->ItemQuery->get_group_by( ', ' );
			// echo $this->ItemQuery->where;
			$arc_result = $DB->get_results( $arc_sql, ARRAY_A );

			foreach( $arc_result as $arc_row )
			{
				if( !isset( $daysinmonthwithposts[ $arc_row['myday'] ] ) )
				{
					$daysinmonthwithposts[ $arc_row['myday'] ] = 0;
				}
				// The '+' situation actually only happens when we have a complex GROUP BY above
				// (multiple categories wcombined with "ALL")
				$daysinmonthwithposts[ $arc_row['myday'] ] += $arc_row['item_count'];
			}

			$daysinmonth = intval(date('t', mktime(0, 0, 0, $this->month, 1, $this->year)));
			// echo 'days in month=', $daysinmonth;


			// caution: offset bug inside (??)
			$datestartofmonth = mktime(0, 0, 0, $this->month, 1, $this->year );
			// echo date( locale_datefmt(), $datestartofmonth );
			$calendarblah = get_weekstartend( $datestartofmonth, locale_startofweek() );
			$calendarfirst = $calendarblah['start'];


			$dateendofmonth = mktime(0, 0, 0, $this->month, $daysinmonth, $this->year);
			// echo 'end of month: '.date( 'Y-m-d H:i:s', $dateendofmonth );
			$calendarblah = get_weekstartend( $dateendofmonth, locale_startofweek() );
			$calendarlast = $calendarblah['end'];
			// echo date( ' Y-m-d H:i:s', $calendarlast );



			// here the offset bug is corrected
			if( (intval(date('d', $calendarfirst)) > 1) && (intval(date('m', $calendarfirst)) == intval($this->month)) )
			{
				#pre_dump( 'with offset bug', date('Y-m-d', $calendarfirst) );
				$calendarfirst = $calendarfirst - 604800 /* 1 week */;
				#pre_dump( 'without offset bug', date('Y-m-d', $calendarfirst) );
			}
		}
		else
		{ // mode is 'year'
			// Find months with posts
			$arc_sql = '
				SELECT COUNT(DISTINCT '.$this->dbIDname.') AS item_count, EXTRACT(MONTH FROM '.$this->dbprefix.'datestart) AS mymonth
				  FROM ('.$this->dbtable.' INNER JOIN T_postcats ON '.$this->dbIDname.' = postcat_post_ID)
				 INNER JOIN T_categories ON postcat_cat_ID = cat_ID
				 LEFT JOIN T_items__type ON post_ityp_ID = ityp_ID
				 WHERE EXTRACT(YEAR FROM '.$this->dbprefix.'datestart) = \''.$this->year.'\' '
				       .$this->ItemQuery->get_where( ' AND ' ).'
				 GROUP BY mymonth '.$this->ItemQuery->get_group_by( ', ' );
			$arc_result = $DB->get_results( $arc_sql, ARRAY_A );

			if( $DB->num_rows > 0 )
			{ // OK we have a month with posts! // fp>dh why did you removed that?
				foreach( $arc_result as $arc_row )
				{
					$monthswithposts[ $arc_row['mymonth'] ] = $arc_row['item_count'];
				}
			}
		}


		// ** display everything **

		echo $this->tablestart;

		// CAPTION :

		if( $this->displaycaption )
		{ // caption:
			echo $this->monthstart;

			if( $this->navigation == 'caption' )
			{
				echo implode( '&nbsp;', $this->getNavLinks( 'prev' ) ).'&nbsp;';
			}

			if( $this->mode == 'month' )
			{ // MONTH CAPTION:
				$text = date_i18n($this->monthformat, mktime(0, 0, 0, $this->month, 1, $this->year));

				if( $this->linktomontharchive )
				{ // chosen month with link to archives
					echo $this->archive_link( $text, T_('View monthly archive'), $this->year, $this->month );
				}
				else
				{
					echo $text;
				}
			}
			else
			{ // YEAR CAPTION:
				echo date_i18n('Y', mktime(0, 0, 0, 1, 1, $this->year)); // display year
			}

			if( $this->navigation == 'caption' )
			{
				echo '&nbsp;'.implode( '&nbsp;', $this->getNavLinks( 'next' ) );
			}

			echo $this->monthend;
		}

		// HEADER :

		if( !empty($this->headerdisplay) && ($this->mode == 'month') )
		{ // Weekdays:
			echo $this->headerrowstart;

			for( $i = locale_startofweek(), $j = $i + 7; $i < $j; $i = $i + 1)
			{
				echo str_replace('[abbr]', T_($weekday[($i % 7)]), $this->headercellstart);
				switch( $this->headerdisplay )
				{
					case 'e':
						// e => 'F'
						echo T_($weekday_letter[($i % 7)]);
						break;

					case 'l':
						// l (lowercase l) => 'Friday'
						echo T_($weekday[($i % 7)]);
						break;

					default:	// Backward compatibility: any non emty value will display this
						// D => 'Fri'
						echo T_($weekday_abbrev[($i % 7)]);
				}

				echo $this->headercellend;
			}

			echo $this->headerrowend;
		}

		// REAL TABLE DATA :

		echo '<tbody>'.$this->rowstart;

		if( $this->mode == 'year' )
		{	// DISPLAY MONTHS:

			for ($i = 1; $i < 13; $i = $i + 1)
			{	// For each month:
				if( isset($monthswithposts[ $i ]) )
				{
					if( $this->month == $i )
					{
						echo $this->todaycellstartpost;
					}
					else
					{
						echo $this->linkpostcellstart;
					}
					if( $monthswithposts[ $i ] > 1 && !empty($this->postcount_year_atitle) )
					{ // display postcount
						$title = sprintf($this->postcount_year_atitle, $monthswithposts[ $i ]);
					}
					elseif( !empty($this->postcount_year_atitle_one) )
					{ // display postcount for one post
						$title = sprintf($this->postcount_year_atitle_one, 1);
					}
					else
					{
						$title = '';
					}
					echo $this->archive_link( T_($month_abbrev[ zeroise($i, 2) ]), $title, $this->year, $i );
				}
				elseif( $this->month == $i )
				{ // current month
					echo $this->todaycellstart;
					echo T_($month_abbrev[ zeroise($i, 2) ]);
				}
				else
				{
					echo $this->cellstart;
					echo T_($month_abbrev[ zeroise($i, 2) ]);
				}
				echo $this->cellend;
				if( $i == 4 || $i == 8 )
				{ // new row
					echo $this->rowend.$this->rowstart;
				}
			}
		}
		else // mode == 'month'
		{	// DISPLAY DAYS of current month:
			$dow = 0;
			$last_day = -1;
			$dom_displayed = 0; // days of month displayed

			for( $i = $calendarfirst; $i <= $calendarlast; $i = $i + 86400 )
			{ // loop day by day (86400 seconds = 24 hours; but not on days where daylight saving changes!)
				if( $dow == 7 )
				{ // We need to start a new row:
					if( $dom_displayed >= $daysinmonth )
					{ // Last day already displayed!
						break;
					}
					echo $this->rowend;
					echo $this->rowstart;
					$dow = 0;
				}
				$dow++;

				// correct daylight saving ("last day"+86400 would lead to "last day at 23:00")
				// fp> TODO: use mkdate()
				while( date('j', $i) == $last_day )
				{
					$i += 3600;
				}
				$last_day = date('j', $i);


				if (date('m', $i) != $this->month)
				{ // empty cell
					echo $this->emptycellstart;
					echo $this->emptycellcontent;
					echo $this->emptycellend;
				}
				else
				{ // This day is in this month
					$dom_displayed++;
					$calendartoday = (date('Ymd',$i) == date('Ymd', (time() + $time_difference)));

					if( isset($daysinmonthwithposts[ date('j', $i) ]) )
					{
						if( $calendartoday )
						{
							echo $this->todaycellstartpost;
						}
						else
						{
							echo $this->linkpostcellstart;
						}
						if( $daysinmonthwithposts[ date('j', $i) ] > 1 && !empty($this->postcount_month_atitle) )
						{ // display postcount
							$title = sprintf($this->postcount_month_atitle, $daysinmonthwithposts[ date('j', $i) ]);
						}
						elseif( !empty($this->postcount_month_atitle_one) )
						{ // display postcount for one post
							$title = sprintf($this->postcount_month_atitle_one, 1);
						}
						else
						{
							$title = '';
						}
						echo $this->archive_link( date('j',$i), $title, $this->year, $this->month, date('d',$i) );
					}
					elseif ($calendartoday)
					{
						echo $this->todaycellstart;
						echo date('j',$i);
					}
					else
					{
						echo $this->cellstart;
						echo date('j',$i);
					}
					echo $this->cellend;
				}
			} // loop day by day
		} // mode == 'month'

		echo $this->rowend."</tbody>\n";

		// FOOTER :

		if( $this->navigation == 'tfoot' )
		{ // We want to display navigation in the table footer:
			echo "<tfoot>\n";
			echo "<tr>\n";
			echo '<td colspan="'.( ( $this->mode == 'month' ? 2 : 1 ) + (int)$this->today_is_visible ).'" id="prev">';
			echo implode( '&nbsp;', $this->getNavLinks( 'prev' ) );
			echo "</td>\n";

			if( $this->today_is_visible )
			{
				if( $this->mode == 'month' )
				{
					echo '<td class="pad">&nbsp;</td>'."\n";
				}
			}
			else
			{
				echo '<td colspan="'.( $this->mode == 'month' ? '3' : '2' ).'" class="center">'
							.$this->archive_link( T_('Current'), '', date('Y'), ( $this->mode == 'month' ? date('m') : NULL ) )
							.'</td>';
			}
			echo '<td colspan="'.( ( $this->mode == 'month' ? 2 : 1 ) + (int)$this->today_is_visible ).'" id="next">';
			echo implode( '&nbsp;', $this->getNavLinks( 'next' ) );
			echo "</td>\n";
			echo "</tr>\n";
			echo "</tfoot>\n";
		}

		echo $this->tableend;

	}  // display(-)


	/**
	 * Create a link to archive, using either URL params or extra path info.
	 *
	 * Can make contextual links.
	 *
	 * @param string
	 * @param string
	 * @param string year
	 * @param string month
	 * @param string day
	 */
	function archive_link( $text, $title, $year, $month = NULL, $day = NULL )
	{
		/**
		 * @var Blog
		 */
		global $Collection, $Blog;

		if( $this->link_type == 'context' )
		{	// We want to preserve context:
			$url_params = $this->itemlist_prefix.'m='.$year;
			if( !empty( $month ) )
			{
				$url_params .= zeroise($month,2);
				if( !empty( $day ) )
				{
					$url_params .= zeroise($day,2);
				}
			}
			return '<a rel="nofollow" href="'.regenerate_url( $this->context_isolation, $url_params ).'">'.format_to_output($text).'</a>';
		}
		else
		{	// We want a canonic link:
			return $Blog->gen_archive_link( $text, $title, $year, $month, $day );
		}
	}


	/**
	 * Get links to navigate between month / year.
	 *
	 * Unless min/max_timestamp='query' has been specified, this will not do any (time consuming!) queries to check where the posts are.
	 *
	 * @param string 'prev' / 'next'
	 * @return array
	 */
	function getNavLinks( $direction )
	{
		global $DB, $localtimenow;

		//pre_dump( 'get_nav_links', $direction );

		$r = array();

		if( $this->params['min_timestamp'] == 'query' || $this->params['max_timestamp'] == 'query' )
		{ // Do inits:
			// WE NEED SPECIAL QUERY PARAMS WHEN MOVING THOUGH MONTHS ( NO dstart especially! )
			$nav_ItemQuery = new ItemQuery( $this->dbtable, $this->dbprefix, $this->dbIDname );	// TEMP object
			// Restrict to selected blog/categories:
			$nav_ItemQuery->where_chapter2( $this->ItemQuery->Blog, $this->ItemQuery->cat_array, $this->ItemQuery->cat_modifier );
			// Restrict to the statuses we want to show:
			$nav_ItemQuery->where_visibility( $this->ItemQuery->show_statuses );
			// Restrict to selected authors:
			$nav_ItemQuery->where_author( $this->ItemQuery->author );
			// if a month is specified in the querystring, load that month:
			$nav_ItemQuery->where_datestart( /* NO m */'', /* NO w */'', /* NO dstart */'', '', $this->ItemQuery->timestamp_min, $this->ItemQuery->timestamp_max );
			// Keyword search stuff:
			$nav_ItemQuery->where_keywords( $this->ItemQuery->keywords, $this->ItemQuery->phrase, $this->ItemQuery->exact );
			// Exclude pages and intros:
			$nav_ItemQuery->where_types( $this->ItemQuery->types );
			// Restrict with locale visibility by current navigation locale:
			$nav_ItemQuery->where_locale_visibility();
		}

		switch( $direction )
		{
			case 'prev':
				//pre_dump( $this->params['min_timestamp'] );
				$r[] = '';

				if( empty($this->month) )
				{ // if $this->month is empty, we're in mode "year" with no selected month
					$use_range_month = 12;
					$use_range_day = 31;
				}
				else
				{
					$use_range_month = $this->month;
					$use_range_day = 1;	// Note: cannot use current day since all months do not have same number of days
				}

				/*
				 * << (PREV YEAR)
				 */
				if( $this->browseyears )
				{	// We want arrows to move one year at a time
					if( $this->params['min_timestamp'] == 'query' )
					{	// Let's query to find the correct year:
						if( $row = $DB->get_row(
								'SELECT EXTRACT(YEAR FROM '.$this->dbprefix.'datestart) AS year,
												EXTRACT(MONTH FROM '.$this->dbprefix.'datestart) AS month
									FROM ('.$this->dbtable.' INNER JOIN T_postcats ON '.$this->dbIDname.' = postcat_post_ID)
												INNER JOIN T_categories ON postcat_cat_ID = cat_ID
									WHERE EXTRACT(YEAR FROM '.$this->dbprefix.'datestart) < '.$this->year.'
												'.$nav_ItemQuery->get_where( ' AND ' )
												.$nav_ItemQuery->get_group_by( ' GROUP BY ' ).'
									ORDER BY EXTRACT(YEAR FROM '.$this->dbprefix.'datestart) DESC, ABS( '.$use_range_month.' - EXTRACT(MONTH FROM '.$this->dbprefix.'datestart) ) ASC
									LIMIT 1', OBJECT, 0, 'Calendar: find prev year with posts' )
							)
						{
							$prev_year_year = $row->year;
							$prev_year_month = $row->month;
						}
					}
					else
					{ // Let's see if the previous year is in the desired navigation range:
						$prev_year_ts = mktime( 0, 0, 0, $use_range_month, $use_range_day,  $this->year-1 );
						if( $prev_year_ts >= $this->params['min_timestamp'] )
						{
							$prev_year_year = date( 'Y', $prev_year_ts );
							$prev_year_month = date( 'm', $prev_year_ts );
						}
					}
				}

				if( !empty($prev_year_year) )
				{	// We have a link to display:
					$r[] = $this->archive_link( '&lt;&lt;', sprintf(
												( $this->mode == 'month'
														? /* Calendar link title to a month in a previous year */ T_('Previous year (%04d-%02d)')
														: /* Calendar link title to a previous year */ T_('Previous year (%04d)') ),
												$prev_year_year, $prev_year_month ), $prev_year_year, ($this->mode == 'month') ? $prev_year_month : NULL ) ;
				}


				/*
				 * < (PREV MONTH)
				 */
				if( $this->mode == 'month' )
				{ // We are browsing months, we'll display arrows to move one month at a time:
					if( $this->params['min_timestamp'] == 'query' )
					{	// Let's query to find the correct month:
						if( $row = $DB->get_row(
								'SELECT EXTRACT(MONTH FROM '.$this->dbprefix.'datestart) AS month,
												EXTRACT(YEAR FROM '.$this->dbprefix.'datestart) AS year
								FROM ('.$this->dbtable.' INNER JOIN T_postcats ON '.$this->dbIDname.' = postcat_post_ID)
									INNER JOIN T_categories ON postcat_cat_ID = cat_ID
								WHERE
								(
									EXTACT(YEAR FROM '.$this->dbprefix.'datestart) < '.($this->year).'
									OR ( EXTRACT(YEAR FROM '.$this->dbprefix.'datestart) = '.($this->year).'
												AND EXTRACT(MONTH FROM '.$this->dbprefix.'datestart) < '.($this->month).'
											)
								)
								'.$nav_ItemQuery->get_where( ' AND ' )
								 .$nav_ItemQuery->get_group_by( ' GROUP BY ' ).'
								ORDER BY EXTRACT(YEAR FROM '.$this->dbprefix.'datestart) DESC, EXTRACT(MONTH FROM '.$this->dbprefix.'datestart) DESC
								LIMIT 1',
								OBJECT,
								0,
								'Calendar: Find prev month with posts' )
							)
						{
							$prev_month_year = $row->year;
							$prev_month_month = $row->month;
						}
					}
					else
					{ // Let's see if the previous month is in the desired navigation range:
						$prev_month_ts = mktime( 0, 0, 0, $this->month-1, 1, $this->year ); // Note: cannot use current day since all months do not have same number of days
						if( $prev_month_ts >= $this->params['min_timestamp'] )
						{
							$prev_month_year = date( 'Y', $prev_month_ts );
							$prev_month_month = date( 'm', $prev_month_ts );
						}
					}
				}

				if( !empty($prev_month_year) )
				{	// We have a link to display:
					$r[] = $this->archive_link( '&lt;', sprintf( T_('Previous month (%04d-%02d)'), $prev_month_year, $prev_month_month ), $prev_month_year, $prev_month_month );
				}
				break;


			case 'next':
				//pre_dump( $this->params['max_timestamp'] );

				/*
				 * > (NEXT MONTH)
				 */
				if( $this->mode == 'month' )
				{ // We are browsing months, we'll display arrows to move one month at a time:
					if( $this->params['max_timestamp'] == 'query' )
					{	// Let's query to find the correct month:
						if( $row = $DB->get_row(
								'SELECT EXTRACT(MONTH FROM '.$this->dbprefix.'datestart) AS month,
												EXTRACT(YEAR FROM '.$this->dbprefix.'datestart) AS year
								FROM ('.$this->dbtable.' INNER JOIN T_postcats ON '.$this->dbIDname.' = postcat_post_ID)
									INNER JOIN T_categories ON postcat_cat_ID = cat_ID
								WHERE
								(
									EXTRACT(YEAR FROM '.$this->dbprefix.'datestart) > '.($this->year).'
									OR ( EXTRACT(YEAR FROM '.$this->dbprefix.'datestart) = '.($this->year).'
												AND EXTRACT(MONTH FROM '.$this->dbprefix.'datestart) > '.($this->month).'
											)
								)
								'.$nav_ItemQuery->get_where( ' AND ' )
								 .$nav_ItemQuery->get_group_by( ' GROUP BY ' ).'
								ORDER BY EXTRACT(YEAR FROM '.$this->dbprefix.'datestart), EXTRACT(MONTH FROM '.$this->dbprefix.'datestart) ASC
								LIMIT 1',
								OBJECT,
								0,
								'Calendar: Find next month with posts' )
							)
						{
							$next_month_year = $row->year;
							$next_month_month = $row->month;
						}
					}
					else
					{ // Let's see if the next month is in the desired navigation range:
						$next_month_ts = mktime( 0, 0, 0, $this->month+1, 1,  $this->year ); // Note: cannot use current day since all months do not have same number of days
						if( $next_month_ts <= $this->params['max_timestamp'] )
						{
							$next_month_year = date( 'Y', $next_month_ts );
							$next_month_month = date( 'm', $next_month_ts );
						}
					}
				}

				if( !empty($next_month_year) )
				{	// We have a link to display:
					$r[] = $this->archive_link( '&gt;', sprintf( T_('Next month (%04d-%02d)'), $next_month_year, $next_month_month ), $next_month_year, $next_month_month );
				}


				if( empty($this->month) )
				{ // if $this->month is empty, we're in mode "year" with no selected month
					$use_range_month = 12;
					$use_range_day = 31;
				}
				else
				{
					$use_range_month = $this->month;
					$use_range_day = 1;	// Note: cannot use current day since all months do not have same number of days
				}

				/*
				 * >> (NEXT YEAR)
				 */
				if( $this->browseyears )
				{ // We want arrows to move one year at a time
					if( $this->params['max_timestamp'] == 'query' )
					{	// Let's query to find the correct year:
					if( $row = $DB->get_row(
							'SELECT EXTRACT(YEAR FROM '.$this->dbprefix.'datestart) AS year,
											EXTRACT(MONTH FROM '.$this->dbprefix.'datestart) AS month
								FROM ('.$this->dbtable.' INNER JOIN T_postcats ON '.$this->dbIDname.' = postcat_post_ID)
									INNER JOIN T_categories ON postcat_cat_ID = cat_ID
								WHERE EXTRACT(YEAR FROM '.$this->dbprefix.'datestart) > '.$this->year.'
								 '.$nav_ItemQuery->get_where( ' AND ' )
								 .$nav_ItemQuery->get_group_by( ' GROUP BY ' ).'
								ORDER BY EXTRACT(YEAR FROM '.$this->dbprefix.'datestart) ASC, ABS( '.$use_range_month.' - EXTRACT(MONTH FROM '.$this->dbprefix.'datestart) ) ASC
								LIMIT 1', OBJECT, 0, 'Calendar: find next year with posts' )
						)
						{
							$next_year_year = $row->year;
							$next_year_month = $row->month;
						}
					}
					else
					{ // Let's see if the next year is in the desired navigation range:
						$next_year_ts = mktime( 0, 0, 0, $use_range_month, $use_range_day,  $this->year+1 );
						if( $next_year_ts <= $this->params['max_timestamp'] )
						{
							$next_year_year = date( 'Y', $next_year_ts );
							$next_year_month = date( 'm', $next_year_ts );
						}
					}

				if( !empty($next_year_year) )
				{	// We have a link to display:
						$r[] = $this->archive_link( '&gt;&gt;', sprintf(
																	( $this->mode == 'month'
																			? /* Calendar link title to a month in a following year */ T_('Next year (%04d-%02d)')
																			: /* Calendar link title to a following year */ T_('Next year (%04d)') ),
																	$next_year_year, $next_year_month ), $next_year_year, ($this->mode == 'month') ? $next_year_month : NULL );
					}
				}
				break;
		}

		return $r;
	}

}

?>