Index: branches/5.2.x/units/articles/articles_event_handler.php
===================================================================
--- branches/5.2.x/units/articles/articles_event_handler.php	(revision 16025)
+++ branches/5.2.x/units/articles/articles_event_handler.php	(revision 16026)
@@ -1,575 +1,575 @@
 <?php
 /**
 * @version	$Id$
 * @package	In-News
 * @copyright	Copyright (C) 1997 - 2009 Intechnic. All rights reserved.
 * @license      GNU/GPL
 * In-Portal is Open Source software.
 * This means that this software may have been modified pursuant
 * the GNU General Public License, and as distributed it includes
 * or is derivative of works licensed under the GNU General Public License
 * or other free or open source software licenses.
 * See http://www.in-portal.org/license for copyright notices and details.
 */
 
 	defined('FULL_PATH') or die('restricted access!');
 
 	class ArticlesEventHandler extends kCatDBEventHandler {
 
 		/**
 		 * Filters out archived articles
 		 *
 		 * @param kEvent $event
 		 * @return void
 		 * @access protected
 		 * @see kDBEventHandler::OnListBuild()
 		 */
 		protected function SetCustomQuery(kEvent $event)
 		{
 			parent::SetCustomQuery($event);
 
 			$object = $event->getObject();
 			/* @var $object kDBList */
 
 			if ( !$this->Application->isAdminUser ) {
 				$where_clause = '(Archived = 0) AND (StartDate < ' . adodb_mktime() . ' OR StartDate = 0) AND (EndOn > ' . adodb_mktime() . ' OR EndOn IS NULL)';
 				$object->addFilter('archived_filter', $where_clause);
 			}
 		}
 
 		/**
 		 * Return type clauses for list bulding on front
 		 *
 		 * @param kEvent $event
 		 * @return Array
 		 */
 		function getTypeClauses($event)
 		{
 			$type_clauses = parent::getTypeClauses($event);
 
 			$type_clauses['site_lead']['include']='%1$s.LeadStory = 1 AND '.TABLE_PREFIX.'CategoryItems.PrimaryCat = 1';
 			$type_clauses['site_lead']['except']='%1$s.LeadStory <> 1 AND '.TABLE_PREFIX.'CategoryItems.PrimaryCat = 1';
 			$type_clauses['site_lead']['having_filter'] = false;
 
 			$type_clauses['cat_lead']['include']='%1$s.LeadCatStory = 1 AND '.TABLE_PREFIX.'CategoryItems.PrimaryCat = 1';
 			$type_clauses['cat_lead']['except']='%1$s.LeadCatStory <> 1 AND '.TABLE_PREFIX.'CategoryItems.PrimaryCat = 1';
 			$type_clauses['cat_lead']['having_filter'] = false;
 
 			return $type_clauses;
 		}
 
 
 		/**
 		 * [CRON] Deletes expired articles + update existing articles from rss feed with new data (key - article url)
 		 *
 		 * @param kEvent $event
 		 */
 		function OnUpdateRSSArticles($event)
 		{
 			if ( defined('IS_INSTALL') && IS_INSTALL ) {
 				return;
 			}
 
 			$category_table = $this->Application->getUnitOption('c', 'TableName');
 			$custom_table = $this->Application->getUnitOption('c-cdata', 'TableName');
 
 			$category_custom_fields = $this->getCustomColumns('c');
 			$article_custom_fields = $this->getCustomColumns($event->Prefix);
 
 
 			// update categories which sould be updated
 			$sql = 'SELECT cd.*, c.CategoryId
 					FROM '.$category_table.' c
 					LEFT JOIN '.$custom_table.' cd ON c.ResourceId = cd.ResourceId
 					WHERE 	(IF(cd.'.$category_custom_fields['RssLastUpdated'].' IS NULL, 0, cd.'.$category_custom_fields['RssLastUpdated'].') +
 							cd.'.$category_custom_fields['RssUpdateInterval'].' * cd.'.$category_custom_fields['RssUpdateIntervalType'].' <=
 							'.adodb_mktime().') AND (LENGTH('.$category_custom_fields['RssSource'].') > 0)';
 			$categories = $this->Conn->Query($sql, 'CategoryId');
 			if ($categories) {
 				$resource_ids = Array();
 				foreach ($categories as $category_id => $category_data) {
 					$resource_ids[] = $category_data['ResourceId'];
 					$event->setEventParam('source_url', $category_data[ $category_custom_fields['RssSource'] ]);
 					$event->setEventParam('category_id', $category_id);
 					$event->setEventParam('custom_fields', $article_custom_fields);
 					$event->setEventParam('life_time',  $category_data[ $category_custom_fields['RssDefaultExpiration'] ] * $category_data[ $category_custom_fields['RssDefaultExpirationType'] ]);
 					$this->parseFeed($event);
 				}
 
 				$sql = 'UPDATE '.$custom_table.'
 						SET '.$category_custom_fields['RssLastUpdated'].' = '.adodb_mktime().'
 						WHERE ResourceId IN ('.implode(',', $resource_ids).')';
 				$this->Conn->Query($sql);
 			}
 
 			// delete expired articles from feed categories
 			$sql = 'SELECT c.CategoryId, c.ResourceId
 					FROM '.$category_table.' c
 					LEFT JOIN '.$custom_table.' cd ON c.ResourceId = cd.ResourceId
 					WHERE (	IF(cd.'.$category_custom_fields['RssLastExpired'].' IS NULL, 0, cd.'.$category_custom_fields['RssLastExpired'].') +
 							cd.'.$category_custom_fields['RssExpireInterval'].' * cd.'.$category_custom_fields['RssExpireIntervalType'].' <=
 							'.adodb_mktime().') AND (cd.'.$category_custom_fields['RssDeleteExpired'].' = 1)';
 
 			$categories = $this->Conn->GetCol($sql, 'ResourceId');
 
 			$id_field = $this->Application->getUnitOption($event->Prefix, 'IDField');
 			$table = $this->Application->getUnitOption($event->Prefix, 'TableName');
 			$ci_table = $this->Application->getUnitOption($event->Prefix.'-ci', 'TableName');
 
 			if ($categories) {
 				$article_custom_table = $this->Application->getUnitOption($event->Prefix.'-cdata', 'TableName');
 
 				$sql = 'SELECT main_table.'.$id_field.'
 						FROM '.$table.' main_table
 						LEFT JOIN '.$ci_table.' ci ON main_table.ResourceId = ci.ItemResourceId
 						LEFT JOIN '.$article_custom_table.' cd ON main_table.ResourceId = cd.ResourceId
 						WHERE 	(ci.PrimaryCat = 1) AND
 								(ci.CategoryId IN ('.implode(',', $categories).')) AND
 								(main_table.EndOn < '.adodb_mktime().' AND main_table.EndOn IS NOT NULL) AND
 								(LENGTH(cd.'.$article_custom_fields['RssOriginalURL'].') > 0)';
 				$article_ids = $this->Conn->GetCol($sql);
 				if ($article_ids) {
 					$temp_handler = $this->Application->recallObject($event->getPrefixSpecial().'_TempHandler', 'kTempTablesHandler');
 					/* @var $temp_handler kTempTablesHandler */
 
 					$temp_handler->DeleteItems($event->Prefix, $event->Special, $article_ids);
 				}
 
 				$sql = 'UPDATE '.$custom_table.'
 						SET '.$category_custom_fields['RssLastExpired'].' = '.adodb_mktime().'
 						WHERE ResourceId IN ('.implode(',', array_keys($categories)).')';
 				$this->Conn->Query($sql);
 			}
 		}
 
 		/**
 		 * Returns article ids & crc, that are created during feed import
 		 *
 		 * @param kEvent $event
 		 * @return Array
 		 */
 		function getFeedArticles($event)
 		{
 			$id_field = $this->Application->getUnitOption($event->Prefix, 'IDField');
 			$table = $this->Application->getUnitOption($event->Prefix, 'TableName');
 			$custom_table = $this->Application->getUnitOption($event->Prefix.'-cdata', 'TableName');
 
 			$crc_field = $event->getEventParam('custom_fields', 'RssArticleCRC');
 
 			$sql = 'SELECT main_table.'.$id_field.', cd.'.$crc_field.'
 					FROM '.$table.' main_table
 					LEFT JOIN '.$custom_table.' cd ON cd.ResourceId = main_table.ResourceId
 					WHERE LENGTH(cd.'.$crc_field.') > 0';
 			return $this->Conn->GetCol($sql, $crc_field);
 		}
 
 		/**
 		 * Creates new, updates existing articles from feed url specified
 		 *
 		 * @param kEvent $event
 		 */
 		function parseFeed($event)
 		{
 			$source_urls = explode(',', $event->getEventParam('source_url'));
 			if (count($source_urls) > 1) {
 				foreach ($source_urls as $source_url) {
 					$event->setEventParam('source_url', $source_url);
 					$this->parseFeed($event);
 				}
 				return true;
 			}
 
 			$curl_helper = $this->Application->recallObject('CurlHelper');
 			/* @var $curl_helper kCurlHelper */
 
 			$curl_helper->followLocation = true;
 			$curl_helper->setOptions( Array (CURLOPT_USERAGENT => 'Mozilla/5.0 (Windows; U; Windows NT 5.2; ru; rv:1.9.1.3) Gecko/20090824 Firefox/3.5.3 (.NET CLR 3.5.30729)') ); // otherwise FeedBurner will return HTML
 
 			$xml_data = $curl_helper->Send($event->getEventParam('source_url'));
 
 			if (!$xml_data) {
 				return false;
 			}
 
 			$xml_helper = $this->Application->recallObject('kXMLHelper');
 			/* @var $xml_helper kXMLHelper */
 
 			$root_node =& $xml_helper->Parse($xml_data, kXMLHelper::XML_WITH_TEXT_NODES);
 
 			$feed_types = Array (
 				'rss_2.0' => 'channel', 'atom' => 'feed',
 			);
 
 			foreach ($feed_types as $feed_type => $node_name) {
 				$article_node =& $root_node->FindChild($node_name);
 				if (is_object($article_node)) {
 					break;
 				}
 			}
 
 			if (!$article_node) {
 				return false;
 			}
 
 			$category_id = $event->getEventParam('category_id');
 			$backup_category_id = $this->Application->GetVar('m_cat_id');
 			$this->Application->SetVar('m_cat_id', $category_id);
 
 			switch ($feed_type) {
 				case 'rss_2.0':
 					$this->parseRssFeed($article_node, $event);
 					break;
 
 				case 'atom':
 					$this->parseAtomFeed($article_node, $event);
 					break;
 			}
 
 			$this->Application->SetVar('m_cat_id', $backup_category_id);
 		}
 
 		/**
 		 * Returns ML field names for article record
 		 *
 		 * @param kCatDBItem $object
 		 * @return Array
 		 */
 		function _getMLFields(&$object)
 		{
 			$ml_formatter = $this->Application->recallObject('kMultiLanguage');
 			/* @var $ml_formatter kMultiLanguage */
 
 			$title_field = 'Title';
 			$title_formatter = $object->GetFieldOption($title_field, 'formatter');
 
 			if ( $title_formatter == 'kMultiLanguage' ) {
 				$title_field = $ml_formatter->LangFieldName($title_field);
 			}
 
 			$body_field = 'Body';
 			$body_formatter = $object->GetFieldOption($body_field, 'formatter');
 
 			if ( $body_formatter == 'kMultiLanguage' ) {
 				$body_field = $ml_formatter->LangFieldName($body_field);
 			}
 
 			return Array ($title_field, $body_field);
 		}
 
 		/**
 		 * Parses RSS 2.0 feed
 		 *
 		 * @param kXMLNode $root_node
 		 * @param kEvent $event
 		 */
 		function parseRssFeed(&$root_node, $event)
 		{
 			$current_node = $root_node->firstChild;
 			$feed_articles = $this->getFeedArticles($event);
 
 			$object = $this->Application->recallObject($event->Prefix.'.-item', null, Array('skip_autoload' => true));
 			/* @var $object kDBItem */
 
 			list ($title_field, $body_field) = $this->_getMLFields($object);
 
 			do {
 				// IMAGE is information about channel and is not useful here
 				if ($current_node->Name != 'ITEM') continue;
 				// collect item data
 				$data = Array();
 				$sub_node =& $current_node->firstChild;
 				/* @var $sub_node kXMLNode */
 
 				do {
 					if ($sub_node->Name == 'ATOM:SUMMARY') {
 						$data[$sub_node->Name] = $this->getNodeContent($sub_node);
 					} else {
 						$data[$sub_node->Name] = $this->getNodeContent($sub_node, 'xhtml'); // $sub_node->firstChild->Data; // was  $sub_node->Data;
 					}
 
 				} while ( ($sub_node =& $sub_node->NextSibling()) );
 
 				// create/update article
 				$article_crc = kUtil::crc32($data['LINK'].$data['TITLE']);
 				$article_id = getArrayValue($feed_articles, $article_crc);
 				if ($article_id) {
 					$object->Load($article_id);
 				}
 				else {
 					$object->Clear();
 				}
 
 				$object->SetDBField($title_field, $data['TITLE']);
 				$object->SetDBField('cust_RssOriginalURL', $data['LINK']);
 				$object->SetDBField('cust_RssArticleCRC', $article_crc);
 				$object->SetDBField($body_field, !array_key_exists('DESCRIPTION', $data) ? $data['ATOM:SUMMARY'] : $data['DESCRIPTION']);
 				$expiration_time = adodb_mktime() + $event->getEventParam('life_time');
 				$object->SetDBField('EndOn_date', $expiration_time);
 				$object->SetDBField('EndOn_time', $expiration_time);
 				$object->SetDBField('Status', STATUS_ACTIVE);
 				$object->SetDBField('Author', 'root');
 				$object->SetDBField('CreatedById', USER_ROOT);
 
 				$status = $object->isLoaded() ? $object->Update() : $object->Create();
 			} while (($current_node =& $current_node->NextSibling()));
 		}
 
 		/**
 		 * Returns parsed node content
 		 *
 		 * @param kXMLNode $node
 		 * @param string $content_type
 		 * @return string
 		 */
 		function getNodeContent(&$node, $content_type = null)
 		{
 			if ( !isset($content_type) ) {
 				$content_type = $node->GetAttribute('TYPE');
 			}
 
 			switch ($content_type) {
 				case 'xhtml':
 					$data = $node->GetXML(true);
 					break;
 
 				case 'html':
-					$data = htmlspecialchars_decode( $node->GetXML(true) ); // $node->firstChild->Data // $node->Data
+					$data = kUtil::unescape($node->GetXML(true), kUtil::ESCAPE_HTML); // $node->firstChild->Data // $node->Data
 					break;
 
 				default:
 					$data = $node->GetXML(true); // $node->firstChild->Data; // $node->Data; also for 'text'
 					break;
 			}
 
 			return trim($data);
 		}
 
 		/**
 		 * Parses ATOM feed
 		 *
 		 * @param kXMLNode $root_node
 		 * @param kEvent $event
 		 */
 		function parseAtomFeed(&$root_node, $event)
 		{
 			$current_node = $root_node->firstChild;
 			$feed_articles = $this->getFeedArticles($event);
 
 			$object = $this->Application->recallObject($event->Prefix.'.-item', null, Array('skip_autoload' => true));
 			/* @var $object kDBItem */
 
 			list ($title_field, $body_field) = $this->_getMLFields($object);
 
 			do {
 				if ($current_node->Name != 'ENTRY') continue;
 				// collect item data
 				$data = Array();
 				$sub_node =& $current_node->firstChild;
 				/* @var $sub_node kXMLNode */
 
 				do {
 					if ($sub_node->Name == 'LINK') {
 						if ($sub_node->GetAttribute('REL') === false || $sub_node->GetAttribute('REL') == 'alternate') {
 							$data[$sub_node->Name] = $sub_node->GetAttribute('HREF');
 						}
 					}
 					elseif ($sub_node->Name == 'CONTENT' || $sub_node->Name == 'SUMMARY' || $sub_node->Name == 'TITLE') {
 						$data[$sub_node->Name] = $this->getNodeContent($sub_node);
 					}
 					else {
 						$data[$sub_node->Name] = $sub_node->GetXML(true); // firstChild->Data; // $sub_node->Data
 					}
 				} while ( ($sub_node =& $sub_node->NextSibling()) );
 
 				// create/update article
 				$article_crc = kUtil::crc32($data['LINK'].$data['TITLE']);
 				$article_id = getArrayValue($feed_articles, $article_crc);
 				if ($article_id) {
 					$object->Load($article_id);
 				}
 				else {
 					$object->Clear();
 				}
 
 				$object->SetDBField($title_field, $data['TITLE']);
 				$object->SetDBField('cust_RssOriginalURL', $data['LINK']);
 				$object->SetDBField('cust_RssArticleCRC', $article_crc);
 				$object->SetDBField($body_field, !array_key_exists('CONTENT', $data) ? $data['SUMMARY'] : $data['CONTENT']);
 				$expiration_time = adodb_mktime() + $event->getEventParam('life_time');
 				$object->SetDBField('EndOn_date', $expiration_time);
 				$object->SetDBField('EndOn_time', $expiration_time);
 				$object->SetDBField('Status', STATUS_ACTIVE);
 				$object->SetDBField('Author', 'root');
 				$object->SetDBField('CreatedById', USER_ROOT);
 
 				$status = $object->isLoaded() ? $object->Update() : $object->Create();
 			} while (($current_node =& $current_node->NextSibling()));
 		}
 
 		function getCustomColumns($prefix)
 		{
 			$ml_formatter = $this->Application->recallObject('kMultiLanguage');
 			/* @var $ml_formatter kMultiLanguage */
 
 			$custom_fields = array_flip($this->Application->getUnitOption($prefix, 'CustomFields'));
 			foreach ($custom_fields as $custom_name => $custom_id) {
 				$custom_fields[$custom_name] = $ml_formatter->LangFieldName('cust_'.$custom_id);
 			}
 
 			return $custom_fields;
 		}
 
 		/**
 		 * Create missing excerpt
 		 *
 		 * @param kEvent $event
 		 * @return void
 		 * @access protected
 		 */
 		protected function OnBeforeItemUpdate(kEvent $event)
 		{
 			parent::OnBeforeItemUpdate($event);
 
 			$this->createExcerpt($event);
 			$this->cacheItemOwner($event, 'CreatedById', 'Author');
 		}
 
 		/**
 		 * Create missing excerpt
 		 *
 		 * @param kEvent $event
 		 * @return void
 		 * @access protected
 		 */
 		protected function OnBeforeItemCreate(kEvent $event)
 		{
 			parent::OnBeforeItemCreate($event);
 
 			$this->createExcerpt($event);
 			$this->cacheItemOwner($event, 'CreatedById', 'Author');
 		}
 
 		/**
 		 * Create excerpt if missing
 		 *
 		 * @param kEvent $event
 		 */
 		function createExcerpt($event)
 		{
 			$object = $event->getObject();
 			/* @var $object kDBItem */
 
 			if ( !$object->GetField('Excerpt') || $this->Application->GetVar('generate_excerpt') ) {
 				$excerpt = strip_tags($object->GetField('Body'));
 
 
 				$length = mb_strlen($excerpt);
 				if ( $length > 100 ) {
 					$excerpt = mb_substr(strip_tags($excerpt), 0, 100);
 
 					if ( mb_substr($excerpt, -1) != ' ' ) {
 						$pos = mb_strrpos($excerpt, ' ');
 
 						if ( $pos ) {
 							$excerpt = mb_substr($excerpt, 0, $pos);
 						}
 					}
 
 					$excerpt .= '...';
 				}
 
 				$excerpt_field = 'Excerpt';
 				$excerpt_formatter = $object->GetFieldOption('Excerpt', 'formatter');
 
 				if ( $excerpt_formatter == 'kMultiLanguage' ) {
 					$ml_formatter = $this->Application->recallObject('kMultiLanguage');
 					/* @var $ml_formatter kMultiLanguage */
 
 					$excerpt_field = $ml_formatter->LangFieldName($excerpt_field);
 				}
 
 				$object->SetDBField($excerpt_field, $excerpt);
 			}
 		}
 
 		/**
 		 * [HOOK] Updates category custom fields options in config
 		 *
 		 * @param kEvent $event
 		 */
 		function OnUpdateCategoryCustomFields($event)
 		{
 			$new_virtual_fields =	Array(
 											'cust_RssSource'				=>	Array('type' => 'string', 'default' => ''),
 											'cust_RssDefaultExpiration'		=>	Array('type' => 'int', 'default' => ''),
 											'cust_RssDefaultExpirationType'	=>	Array('type' => 'int', 'formatter' => 'kOptionsFormatter', 'use_phrases' => 1, 'options' => Array(60 => 'la_opt_min', 3600 => 'la_opt_hour', 86400 => 'la_opt_day', 2419200 => 'la_opt_month', 29030400 => 'la_opt_year'), 'default' => 60),
 											'cust_RssExpireInterval'		=>	Array('type' => 'int', 'default' => ''),
 											'cust_RssExpireIntervalType'	=>	Array('type' => 'int', 'formatter' => 'kOptionsFormatter', 'use_phrases' => 1, 'options' => Array(60 => 'la_opt_min', 3600 => 'la_opt_hour', 86400 => 'la_opt_day', 2419200 => 'la_opt_month'), 'default' => 60),
 											'cust_RssDeleteExpired'			=>	Array('type' => 'int', 'formatter' => 'kOptionsFormatter', 'use_phrases' => 1, 'options' => Array(1 => 'la_Yes', 0 => 'la_No'), 'default' => 0),
 											'cust_RssUpdateInterval'		=>	Array('type' => 'int', 'default' => ''),
 											'cust_RssUpdateIntervalType'	=>	Array('type' => 'int', 'formatter' => 'kOptionsFormatter', 'use_phrases' => 1, 'options' => Array(60 => 'la_opt_min', 3600 => 'la_opt_hour', 86400 => 'la_opt_day', 2419200 => 'la_opt_month'), 'default' => 60),
 											'cust_RssLastUpdated'			=>	Array('type' => 'int', 'formatter' => 'kDateFormatter', 'default' => ''),
 											'cust_RssLastExpired'			=>	Array('type' => 'int', 'formatter' => 'kDateFormatter', 'default' => ''),
 									);
 			$virtual_fields = $this->Application->getUnitOption('c', 'VirtualFields');
 			$virtual_fields = kUtil::array_merge_recursive($virtual_fields, $new_virtual_fields);
 			$this->Application->setUnitOption('c', 'VirtualFields', $virtual_fields);
 		}
 
 		/**
 		 * Sets default expiration based on module setting
 		 *
 		 * @param kEvent $event
 		 * @return void
 		 * @access protected
 		 */
 		protected function OnPreCreate(kEvent $event)
 		{
 			parent::OnPreCreate($event);
 
 			if ( $event->status != kEvent::erSUCCESS ) {
 				return ;
 			}
 
 			$object = $event->getObject();
 			/* @var $object kDBItem */
 
 			$archive_days = $this->Application->ConfigValue('News_Archive');
 			if ( $archive_days ) {
 				$expire_date = adodb_mktime() + $archive_days * 3600 * 24;
 				$object->SetDBField('EndOn_date', $expire_date);
 				$object->SetDBField('EndOn_time', $expire_date);
 			}
 		}
 
 		/**
 		 * [HOOK] Allows to add cloned subitem to given prefix
 		 *
 		 * @param kEvent $event
 		 * @return void
 		 * @access protected
 		 */
 		protected function OnCloneSubItem(kEvent $event)
 		{
 			parent::OnCloneSubItem($event);
 
 			if ( $event->MasterEvent->Prefix == 'rev' ) {
 				$clones = $this->Application->getUnitOption($event->MasterEvent->Prefix, 'Clones');
 				$subitem_prefix = $event->Prefix . '-' . $event->MasterEvent->Prefix;
 
 				$clones[$subitem_prefix]['ConfigMapping'] = Array (
 					'PerPage'				=>	'Perpage_NewsReviews',
 					'ShortListPerPage'		=>	'Perpage_NewsReviews_Short',
 					'DefaultSorting1Field'	=>	'News_SortReviews',
 					'DefaultSorting2Field'	=>	'News_SortReviews2',
 					'DefaultSorting1Dir'	=>	'News_SortReviewsOrder',
 					'DefaultSorting2Dir'	=>	'News_SortReviewsOrder2',
 
 					'ReviewDelayInterval'	=>	'News_ReviewDelay_Interval',
 					'ReviewDelayValue'		=>	'News_ReviewDelay_Value',
 				);
 
 				$this->Application->setUnitOption($event->MasterEvent->Prefix, 'Clones', $clones);
 			}
 		}
-	}
\ No newline at end of file
+	}