Index: admin/system_presets/simple/email_events_emailevents.php =================================================================== --- admin/system_presets/simple/email_events_emailevents.php (revision 15222) +++ admin/system_presets/simple/email_events_emailevents.php (working copy) @@ -29,7 +29,7 @@ /*'EventId', 'Event', 'ReplacementTags', 'AllowChangingSender', 'CustomSender', 'SenderName', 'SenderAddressType', 'SenderAddress', 'AllowChangingRecipient', 'CustomRecipient', 'Recipients', 'Subject', 'HtmlBody', 'PlainTextBody', 'Headers', - 'Enabled', 'FrontEndOnly', 'Module', 'Description', 'Type'*/ + 'Enabled', 'FrontEndOnly', 'Module', 'BindToSystemEvent', 'Description', 'Type'*/ ); // virtual fields to hide @@ -43,7 +43,7 @@ /*'EventId',*/ 'Event', /*'ReplacementTags', 'AllowChangingSender', 'CustomSender', 'SenderName', 'SenderAddressType', 'SenderAddress', 'AllowChangingRecipient', 'CustomRecipient', 'Recipients',*/ 'Subject', /*'HtmlBody', 'PlainTextBody', 'Headers',*/ - /*'Enabled', 'FrontEndOnly',*/ 'Module', /*'Description',*/ 'Type' + /*'Enabled', 'FrontEndOnly',*/ 'Module', /*'BindToSystemEvent', 'Description',*/ 'Type' ); // virtual fields to make required Index: core/admin_templates/incs/form_blocks.tpl =================================================================== --- core/admin_templates/incs/form_blocks.tpl (revision 15165) +++ core/admin_templates/incs/form_blocks.tpl (working copy) @@ -682,7 +682,7 @@ - + Index: core/admin_templates/languages/email_message_settings.tpl =================================================================== --- core/admin_templates/languages/email_message_settings.tpl (revision 15222) +++ core/admin_templates/languages/email_message_settings.tpl (working copy) @@ -57,51 +57,52 @@ - + - - - + + + - - - + + + - + - + - + - - - - + + + + - - + + - + - + - - - + + + - + +
Index: core/install/install_schema.sql =================================================================== --- core/install/install_schema.sql (revision 15246) +++ core/install/install_schema.sql (working copy) @@ -117,6 +117,7 @@ Description text, `Type` int(11) NOT NULL DEFAULT '0', LastChanged int(10) unsigned DEFAULT NULL, + BindToSystemEvent varchar(255) NOT NULL DEFAULT '', PRIMARY KEY (EventId), KEY `Type` (`Type`), KEY Enabled (Enabled), @@ -139,6 +140,19 @@ KEY l5_PlainTextBody (l5_PlainTextBody(5)) ); +CREATE TABLE EmailEventSubscribers ( + SubscriptionId int(11) NOT NULL AUTO_INCREMENT, + EmailEventId int(11) DEFAULT NULL, + SubscriberEmail varchar(255) NOT NULL DEFAULT '', + UserId int(11) DEFAULT NULL, + CategoryId int(11) DEFAULT NULL, + ItemId int(11) DEFAULT NULL, + ParentItemId int(11) DEFAULT NULL, + SubscribedOn int(11) DEFAULT NULL, + PRIMARY KEY (SubscriptionId), + KEY EmailEventId (EmailEventId) +); + CREATE TABLE IdGenerator ( lastid int(11) default NULL ); Index: core/install/remove_schema.sql =================================================================== --- core/install/remove_schema.sql (revision 15165) +++ core/install/remove_schema.sql (working copy) @@ -4,6 +4,7 @@ DROP TABLE SystemSettings; DROP TABLE EmailQueue; DROP TABLE EmailEvents; +DROP TABLE EmailEventSubscribers; DROP TABLE IdGenerator; DROP TABLE Languages; DROP TABLE Modules; Index: core/kernel/application.php =================================================================== --- core/kernel/application.php (revision 15251) +++ core/kernel/application.php (working copy) @@ -2024,6 +2024,7 @@ * @param kEvent $event * @param Array $params * @param Array $specific_params + * @return void * @access public */ public function HandleEvent($event, $params = null, $specific_params = null) @@ -2036,6 +2037,17 @@ } /** + * Notifies event subscribers, that event has occured + * + * @param kEvent $event + * @return void + */ + public function notifyEventSubscribers(kEvent $event) + { + $this->EventManager->notifySubscribers($event); + } + + /** * Allows to process any type of event * * @param kEvent $event @@ -2430,6 +2442,12 @@ $debug_mode = defined('DEBUG_MODE') && DEBUG_MODE; $skip_reporting = defined('DBG_SKIP_REPORTING') && DBG_SKIP_REPORTING; + if ( $exception instanceof kRedirectException ) { + /* @var $exception kRedirectException */ + + $exception->run(); + } + if ( !$this->exceptionHandlers || ($debug_mode && $skip_reporting) ) { // when debugger absent OR it's present, but we actually can't see it's error report (e.g. during ajax request) $this->errorDisplayFatal('' . get_class($exception) . ': ' . "{$errstr} in {$errfile} on line {$errline}"); Index: core/kernel/db/db_event_handler.php =================================================================== --- core/kernel/db/db_event_handler.php (revision 15226) +++ core/kernel/db/db_event_handler.php (working copy) @@ -581,6 +581,7 @@ $user_id = $this->Application->InitDone ? $this->Application->RecallVar('user_id') : USER_ROOT; $event->setEventParam('top_prefix', $this->Application->GetTopmostPrefix($event->Prefix, true)); $status_checked = false; + if ( $user_id == USER_ROOT || $this->CheckPermission($event) ) { // don't autoload item, when user doesn't have view permission $this->LoadItem($event); @@ -599,29 +600,7 @@ if ( !$perm_status ) { // when no permission to view item -> redirect to no permission template - if ( $this->Application->isDebugMode() ) { - $this->Application->Debugger->appendTrace(); - } - - trigger_error('ItemLoad Permission Failed for prefix [' . $event->getPrefixSpecial() . '] in ' . ($status_checked ? 'checkItemStatus' : 'CheckPermission') . '', E_USER_NOTICE); - $template = $this->Application->isAdmin ? 'no_permission' : $this->Application->ConfigValue('NoPermissionTemplate'); - - if ( $this->Application->GetVar('t') != $template ) { - // don't perform "no_permission" redirect if already on a "no_permission" template - if ( MOD_REWRITE ) { - $redirect_params = Array ( - 'm_cat_id' => 0, - 'next_template' => urlencode('external:' . $_SERVER['REQUEST_URI']), - ); - } - else { - $redirect_params = Array ( - 'next_template' => $this->Application->GetVar('t'), - ); - } - - $this->Application->Redirect($template, $redirect_params); - } + $this->_processItemLoadingError($event, $status_checked); } } @@ -634,6 +613,50 @@ } /** + * Processes case, when item wasn't loaded because of lack of permissions + * + * @param kEvent $event + * @param bool $status_checked + * @throws kNoPermissionException + * @return void + * @access protected + */ + protected function _processItemLoadingError($event, $status_checked) + { + $current_template = $this->Application->GetVar('t'); + $redirect_template = $this->Application->isAdmin ? 'no_permission' : $this->Application->ConfigValue('NoPermissionTemplate'); + $error_msg = 'ItemLoad Permission Failed for prefix [' . $event->getPrefixSpecial() . '] in ' . ($status_checked ? 'checkItemStatus' : 'CheckPermission') . ''; + + if ( $current_template == $redirect_template ) { + // don't perform "no_permission" redirect if already on a "no_permission" template + if ( $this->Application->isDebugMode() ) { + $this->Application->Debugger->appendTrace(); + } + + trigger_error($error_msg, E_USER_NOTICE); + + return; + } + + if ( MOD_REWRITE ) { + $redirect_params = Array ( + 'm_cat_id' => 0, + 'next_template' => urlencode('external:' . $_SERVER['REQUEST_URI']), + ); + } + else { + $redirect_params = Array ( + 'next_template' => $current_template, + ); + } + + $exception = new kNoPermissionException($error_msg); + $exception->setup($redirect_template, $redirect_params); + + throw $exception; + } + + /** * Build sub-tables array from configs * * @param kEvent $event @@ -2412,13 +2435,28 @@ * Occurs after deleting item, id of deleted item * is stored as 'id' param of event * + * Also deletes subscriptions to that particual item once it's deleted + * * @param kEvent $event * @return void * @access protected */ protected function OnAfterItemDelete(kEvent $event) { + $object = $event->getObject(); + /* @var $object kDBItem */ + $sql = 'SELECT EventId + FROM ' . TABLE_PREFIX . 'EmailEvents + WHERE BindToSystemEvent REGEXP "' . $this->Conn->escape($event->Prefix) . '[.]{0,1}([^:]*):(.*)"'; + $email_event_ids = $this->Conn->GetCol($sql); + + if ( $email_event_ids ) { + // e-mail events, connected to that unit prefix are found + $sql = 'DELETE FROM ' . TABLE_PREFIX . 'EmailEventSubscribers + WHERE ItemId = ' . $object->GetID() . ' AND EmailEventId IN (' . implode(',', $email_event_ids) . ')'; + $this->Conn->Query($sql); + } } /** Index: core/kernel/db/dbitem.php =================================================================== --- core/kernel/db/dbitem.php (revision 15226) +++ core/kernel/db/dbitem.php (working copy) @@ -372,14 +372,14 @@ } /** - * Loads item from the database by given id - * - * @access public - * @param mixed $id item id of keys->values hash to load item by - * @param string $id_field_name Optional parameter to load item by given Id field - * @param bool $cachable cache this query result based on it's prefix serial - * @return bool True if item has been loaded, false otherwise - */ + * Loads item from the database by given id + * + * @access public + * @param mixed $id item id of keys->values hash to load item by + * @param string $id_field_name Optional parameter to load item by given Id field + * @param bool $cachable cache this query result based on it's prefix serial + * @return bool True if item has been loaded, false otherwise + */ public function Load($id, $id_field_name = null, $cachable = false) { if ( isset($id_field_name) ) { @@ -1154,8 +1154,9 @@ * @param string $parent_prefix * @param bool $top_most return topmost parent, when used * @return int + * @access public */ - protected function getParentId($parent_prefix, $top_most = false) + public function getParentId($parent_prefix, $top_most = false) { $current_id = $this->GetID(); $current_prefix = $this->Prefix; Index: core/kernel/event_handler.php =================================================================== --- core/kernel/event_handler.php (revision 15250) +++ core/kernel/event_handler.php (working copy) @@ -209,4 +209,19 @@ } + /** + * Returns sql query to be used for event subscriber list selection + * + * @param kEvent $event + * @return string + * @access protected + */ + protected function OnGetEventSubscribersQuery(kEvent $event) + { + $sql = 'SELECT SubscriberEmail, UserId + FROM ' . TABLE_PREFIX . 'EmailEventSubscribers + WHERE (' . implode(') AND (', $event->getEventParam('where_clause')) . ')'; + $event->setEventParam('sql', $sql); + } + } \ No newline at end of file Index: core/kernel/event_manager.php =================================================================== --- core/kernel/event_manager.php (revision 15250) +++ core/kernel/event_manager.php (working copy) @@ -237,6 +237,7 @@ * Allows to process any type of event * * @param kEvent $event + * @return void * @access public */ public function HandleEvent($event) @@ -272,6 +273,136 @@ } /** + * Notifies event subscribers, that event has occured + * + * @param kEvent $event + * @return void + */ + public function notifySubscribers(kEvent $event) + { + if ( $event->status != kEvent::erSUCCESS ) { + return; + } + + $cache_key = 'email_to_event_mapping[%EmaileventsSerial%]'; + $event_mapping = $this->Application->getCache($cache_key); + + if ( $event_mapping === false ) { + $this->Conn->nextQueryCachable = true; + $sql = 'SELECT EventId, Event, Type, BindToSystemEvent + FROM ' . TABLE_PREFIX . 'EmailEvents + WHERE BindToSystemEvent <> ""'; + $event_mapping = $this->Conn->Query($sql, 'BindToSystemEvent'); + + $this->Application->setCache($cache_key, $event_mapping); + } + + $email_event = Array (); + + if ( isset($event_mapping[(string)$event]) ) { + $email_event = $event_mapping[(string)$event]; + } + elseif ( isset($event_mapping[$event->Prefix . '.*:' . $event->Name]) ) { + $email_event = $event_mapping[$event->Prefix . '.*:' . $event->Name]; + } + + if ( !$email_event ) { + return; + } + + $category_ids = $item_id = $parent_item_id = false; + $where_clause = Array (); + $where_clause['EmailEventId'] = 'EmailEventId = ' . $email_event['EventId']; + + try { + $category = $this->Application->recallObject('c'); + /* @var $category kDBItem */ + + if ( $category->isLoaded() ) { + $category_ids = trim(str_replace('|', ',', $category->GetDBField('ParentPath')), ','); + } + } + catch (Exception $e) { + } + + $where_clause['CategoryId'] = $this->_getSubscriberFilter('CategoryId', $category_ids, true); + + try { + $object = $event->getObject(); + /* @var $object kDBItem */ + + if ( $object->isLoaded() ) { + $item_id = $object->GetID(); + $parent_prefix = $this->Application->getUnitOption($event->Prefix, 'ParentPrefix'); + + if ( $parent_prefix ) { + $parent_item_id = $object->getParentId($parent_prefix); + } + } + } + catch (Exception $e) { + } + + $where_clause['ItemId'] = $this->_getSubscriberFilter('ItemId', $item_id); + $where_clause['ParentItemId'] = $this->_getSubscriberFilter('ParentItemId', $parent_item_id); + + $event_params = Array ( + 'EmailEventId' => $email_event['EventId'], + 'CategoryIds' => $category_ids, + 'ItemId' => $item_id, + 'ParentId' => $parent_item_id, + 'where_clause' => $where_clause, + ); + + $sql_event = new kEvent($event->getPrefixSpecial() . ':OnGetEventSubscribersQuery', $event_params); + $sql_event->MasterEvent = $event; + $this->HandleEvent($sql_event); + + $subscribers = $this->Conn->GetIterator($sql_event->getEventParam('sql')); + + if ( !count($subscribers) ) { + // mapping exists, but nobody has subscribed + return; + } + + $send_params = Array ( + 'Prefix' => $event->Prefix, + 'Special' => $event->Special, + 'PrefixSpecial' => $event->getPrefixSpecial(), + ); + + $send_method = $email_event['Type'] == EmailEvent::EVENT_TYPE_FRONTEND ? 'EmailEventUser' : 'EmailEventAdmin'; + + foreach ($subscribers as $subscriber_info) { + $send_params['to_name'] = $subscriber_info['SubscriberEmail']; + $send_params['to_email'] = $subscriber_info['SubscriberEmail']; + $this->Application->$send_method($email_event['Event'], $subscriber_info['UserId'], $send_params); + } + } + + /** + * Returns filter for searching event subscribers + * + * @param string $field + * @param mixed $value + * @param bool $use_in_clause + * @return string + * @access protected + */ + protected function _getSubscriberFilter($field, $value, $use_in_clause = false) + { + if ( $value ) { + // send to this item subscribers AND to subscribers to all items + $clause = $use_in_clause ? ' IN (' . $value . ')' : ' = ' . $this->Conn->qstr($value); + + return $field . $clause . ' OR ' . $field . ' IS NULL'; + } + + // send to subscribers to all items + return $field . ' IS NULL'; + } + + /** * Checks, that given event is implemented * * @param kEvent $event Index: core/kernel/kbase.php =================================================================== --- core/kernel/kbase.php (revision 15226) +++ core/kernel/kbase.php (working copy) @@ -103,6 +103,7 @@ } } + class kHelper extends kBase { /** @@ -138,6 +139,7 @@ } } + abstract class kDBBase extends kBase { /** @@ -1081,4 +1083,78 @@ * @access protected */ abstract protected function GetCol($field); -} \ No newline at end of file +} + + +/** + * Base class for exceptions, that trigger redirect action once thrown + */ +class kRedirectException extends Exception { + + /** + * Redirect template + * + * @var string + * @access protected + */ + protected $template = ''; + + /** + * Redirect params + * + * @var Array + * @access protected + */ + protected $params = Array (); + + /** + * Creates redirect exception + * + * @param string $message + * @param int $code + * @param Exception $previous + */ + public function __construct($message = '', $code = 0, $previous = NULL) + { + parent::__construct($message, $code, $previous); + } + + /** + * Initializes exception + * + * @param string $template + * @param Array $params + * @return void + * @access public + */ + public function setup($template, $params = Array ()) + { + $this->template = $template; + $this->params = $params; + } + + /** + * Display exception details in debugger (only useful, when DBG_REDIRECT is enabled) and performs redirect + * + * @return void + * @access public + */ + public function run() + { + $application =& kApplication::Instance(); + + if ( $application->isDebugMode() ) { + $application->Debugger->appendException($this); + } + + $application->Redirect($this->template, $this->params); + } +} + + +/** + * Exception, that is thrown when user don't have permission to perform requested action + */ +class kNoPermissionException extends kRedirectException { + +} Index: core/kernel/managers/request_manager.php =================================================================== --- core/kernel/managers/request_manager.php (revision 15226) +++ core/kernel/managers/request_manager.php (working copy) @@ -158,6 +158,7 @@ if ( ($this->Application->RecallVar('user_id') == USER_ROOT) || $event_handler->CheckPermission($event) ) { $this->Application->HandleEvent($event); + $this->Application->notifyEventSubscribers($event); } return $event; Index: core/kernel/managers/rewrite_url_processor.php =================================================================== --- core/kernel/managers/rewrite_url_processor.php (revision 15246) +++ core/kernel/managers/rewrite_url_processor.php (working copy) @@ -120,15 +120,17 @@ { $url = $this->Application->GetVar('_mod_rw_url_'); - if ($url) { + if ( $url ) { foreach ($this->_urlEndings as $url_ending) { - if (substr($url, strlen($url) - strlen($url_ending)) == $url_ending) { + if ( substr($url, strlen($url) - strlen($url_ending)) == $url_ending ) { $url = substr($url, 0, strlen($url) - strlen($url_ending)); $default_ending = $this->Application->ConfigValue('ModRewriteUrlEnding'); // user manually typed url with different url ending -> redirect to same url with default url ending - if (($url_ending != $default_ending) && $this->Application->ConfigValue('ForceModRewriteUrlEnding')) { + if ( ($url_ending != $default_ending) && $this->Application->ConfigValue('ForceModRewriteUrlEnding') ) { $target_url = $this->Application->BaseURL() . $url . $default_ending; + + trigger_error('Mod-rewrite url "' . $_SERVER['REQUEST_URI'] . '" without "' . $default_ending . '" line ending used', E_USER_NOTICE); $this->Application->Redirect('external:' . $target_url, Array ('response_code' => 301)); } Index: core/kernel/utility/debugger.php =================================================================== --- core/kernel/utility/debugger.php (revision 15226) +++ core/kernel/utility/debugger.php (working copy) @@ -1725,13 +1725,13 @@ } /** - * User-defined exception handler + * Adds exception details into debugger but don't cause fatal error * * @param Exception $exception * @return void * @access public */ - public function saveException($exception) + public function appendException($exception) { $this->ProfilerData['error_handling']['begins'] = memory_get_usage(); @@ -1751,7 +1751,18 @@ $this->ProfilerData['error_handling']['ends'] = memory_get_usage(); $this->profilerAddTotal('error_handling', 'error_handling'); + } + /** + * User-defined exception handler + * + * @param Exception $exception + * @return void + * @access public + */ + public function saveException($exception) + { + $this->appendException($exception); $this->IsFatalError = true; // append debugger report to data in buffer & clean buffer afterwards Index: core/kernel/utility/http_query.php =================================================================== --- core/kernel/utility/http_query.php (revision 15233) +++ core/kernel/utility/http_query.php (working copy) @@ -332,7 +332,7 @@ * * @return void * @access protected - * TODO: only uses build-in rewrite listeners, when cache is build for the first time + * @todo: only uses build-in rewrite listeners, when cache is build for the first time */ protected function AfterInit() { @@ -361,6 +361,7 @@ $url_params['pass_category'] = 1; $url_params['response_code'] = 301; // Moved Permanently + trigger_error('Non mod-rewrite url "' . $_SERVER['REQUEST_URI'] . '" used', E_USER_NOTICE); $this->Application->Redirect('', $url_params); } } Index: core/units/email_events/email_events_config.php =================================================================== --- core/units/email_events/email_events_config.php (revision 15222) +++ core/units/email_events/email_events_config.php (working copy) @@ -194,6 +194,7 @@ 'not_null' => 1, 'unique' => Array ('Event'), 'required' => 1, 'default' => 0 ), 'LastChanged' => Array ('type' => 'int', 'formatter' => 'kDateFormatter', 'default' => NULL), + 'BindToSystemEvent' => Array ('type' => 'string', 'max_len' => 255, 'not_null' => 1, 'default' => ''), ), 'VirtualFields' => Array ( Index: core/units/email_events/email_events_event_handler.php =================================================================== --- core/units/email_events/email_events_event_handler.php (revision 15225) +++ core/units/email_events/email_events_event_handler.php (working copy) @@ -577,6 +577,7 @@ } $this->_validateAddress($event, 'Recipient'); + $this->_validateBindEvent($object); } /** @@ -646,6 +647,27 @@ } /** + * Checks that bind event is specified in correct format and exists + * + * @param kDBItem $object + */ + protected function _validateBindEvent($object) + { + $event_string = $object->GetDBField('BindToSystemEvent'); + + if ( !$event_string ) { + return; + } + + try { + $this->Application->eventImplemented(new kEvent($event_string)); + } + catch (Exception $e) { + $object->SetError('BindToSystemEvent', 'invalid_event', '+' . $e->getMessage()); + } + } + + /** * Stores ids of selected phrases and redirects to export language step 1 * * @param kEvent $event @@ -672,4 +694,23 @@ ) ); } + + /** + * Deletes all subscribers to e-mail event after it was deleted + * + * @param kEvent $event + * @return void + * @access protected + */ + protected function OnAfterItemDelete(kEvent $event) + { + parent::OnAfterItemDelete($event); + + $object = $event->getObject(); + /* @var $object kDBItem */ + + $sql = 'DELETE FROM ' . TABLE_PREFIX . 'EmailEventSubscribers + WHERE EmailEventId = ' . $object->GetID(); + $this->Conn->Query($sql); + } } \ No newline at end of file Index: core/units/languages/languages_item.php =================================================================== --- core/units/languages/languages_item.php (revision 15165) +++ core/units/languages/languages_item.php (working copy) @@ -182,6 +182,16 @@ return false; } + /** + * Loads item from the database by given id + * + * @access public + * @param mixed $id item id of keys->values hash to load item by + * @param string $id_field_name Optional parameter to load item by given Id field + * @param bool $cachable cache this query result based on it's prefix serial + * @return bool True if item has been loaded, false otherwise + * @throws kRedirectException + */ public function Load($id, $id_field_name = null, $cachable = true) { $default = false; @@ -211,7 +221,10 @@ 'pass' => 'm' ); - $this->Application->Redirect('', $url_params); + $exception = new kRedirectException('Redirect into language ID = ' . $language_id . ' according to "Accepted-Language" header "' . $_SERVER['HTTP_ACCEPT_LANGUAGE'] . '"'); + $exception->setup('', $url_params); + + throw $exception; } } }