* @category class * @package core * @subpackage alert * @copyright 2005-2017 Titan Framework * @license http://www.titanframework.com/license/ BSD License (3 Clause) * @see Instance * @link http://www.titanframework.com/docs/tutorials/alerts/ */ class Alert { static private $alert = FALSE; private $templates = array (); private $tags = array (); static private $active = NULL; private final function __construct () { $array = Instance::singleton ()->getAlert (); if (!array_key_exists ('xml-path', $array)) throw new Exception ('Not located [xml-path] attribute on <alert></alert> tag in file [configure/titan.xml]!'); $file = $array ['xml-path']; $cacheFile = Instance::singleton ()->getCachePath () .'parsed/'. fileName ($file) .'_'. md5_file ($file) .'.php'; if (file_exists ($cacheFile)) $array = include $cacheFile; else { $xml = new Xml ($file); $array = $xml->getArray (); $array = $array ['alert-mapping'][0]; xmlCache ($cacheFile, $array); } if (array_key_exists ('alert', $array)) foreach ($array ['alert'] as $trash => $alert) { if (!array_key_exists ('id', $alert)) continue; $this->templates [$alert ['id']] = $alert; } } static public function singleton () { if (self::$alert !== FALSE) return self::$alert; $class = __CLASS__; self::$alert = new $class (); return self::$alert; } public function getAlerts ($userId) { $sql = "SELECT a.*, au._read, at._name AS author, av._name AS user, CASE WHEN a._until IS NULL THEN to_char (now(), 'MM-DD-YYYY') ELSE to_char (a._until, 'MM-DD-YYYY') END AS f_until, extract (epoch from a._until) as u_until, to_char (a._create, 'HH24-MI-SS-MM-DD-YYYY') AS f_create FROM _alert_user au INNER JOIN _alert a ON a._id = au._alert LEFT JOIN _user at ON at._id = a._user INNER JOIN _user av ON av._id = au._user WHERE au._user = :user AND au._delete = B'0' AND (a._until IS NULL OR a._until > CURRENT_TIMESTAMP) ORDER BY a._update DESC"; $sth = Database::singleton ()->prepare ($sql); $sth->execute (array (':user' => $userId)); $array = array (); $dTags = array ('[SYSTEM]' => Instance::singleton ()->getName (), '[URL]' => Instance::singleton ()->getUrl ()); while ($obj = $sth->fetch (PDO::FETCH_OBJ)) { if (!array_key_exists ($obj->_template, $this->templates)) continue; $tags = $dTags; $tags ['[AUTHOR]'] = is_null ($obj->author) ? Instance::singleton ()->getName () : $obj->author; $tags ['[USER]'] = $obj->user; $tags ['[DAYS_MISSING]'] = (int) $obj->u_until <= 0 ? 0 : floor (($obj->u_until - time ()) / (60 * 60 * 24)); $u = explode ('-', $obj->f_until); $tags ['[UNTIL]'] = strftime ('%x', mktime (0, 0, 0, (int) $u [0], (int) $u [1], (int) $u [2])); $d = explode ('-', $obj->f_create); $tags ['[DATE]'] = strftime ('%c', mktime ((int) $d [0], (int) $d [1], (int) $d [2], (int) $d [3], (int) $d [4], (int) $d [5])); $uTags = unserialize ($obj->_parameters); if (is_array ($uTags)) $tags = array_merge ($tags, $uTags); $array [$obj->_id]['_MESSAGE_'] = str_replace (array_keys ($tags), $tags, $this->getFromTemplate ($obj->_template, 'message')); $array [$obj->_id]['_GO_'] = str_replace (array_keys ($tags), $tags, $this->getFromTemplate ($obj->_template, 'go')); $array [$obj->_id]['_ICON_'] = $this->getFromTemplate ($obj->_template, 'icon'); $array [$obj->_id]['_READ_'] = (int) $obj->_read ? 'true' : 'false'; } return $array; } public function getAlertMessage ($id, $user, $useUntil = TRUE) { $sql = "SELECT a.*, au._read, at._name AS author, av._name AS user, CASE WHEN a._until IS NULL THEN to_char (now(), 'MM-DD-YYYY') ELSE to_char (a._until, 'MM-DD-YYYY') END AS f_until, extract (epoch from a._until) as u_until, to_char (a._create, 'HH24-MI-SS-MM-DD-YYYY') AS f_create FROM _alert_user au INNER JOIN _alert a ON a._id = au._alert LEFT JOIN _user at ON at._id = a._user INNER JOIN _user av ON av._id = au._user WHERE au._user = :user AND a._id = :id"; if ($useUntil) $sql .= " AND (a._until IS NULL OR a._until > CURRENT_TIMESTAMP)"; $sth = Database::singleton ()->prepare ($sql); $sth->bindParam (':id', $id, PDO::PARAM_INT); $sth->bindParam (':user', $user, PDO::PARAM_INT); $sth->execute (); $array = array (); $dTags = array ('[SYSTEM]' => Instance::singleton ()->getName (), '[URL]' => Instance::singleton ()->getUrl ()); $obj = $sth->fetch (PDO::FETCH_OBJ); if (!is_object ($obj) || !array_key_exists ($obj->_template, $this->templates)) return ''; $tags = $dTags; $tags ['[AUTHOR]'] = is_null ($obj->author) ? Instance::singleton ()->getName () : $obj->author; $tags ['[USER]'] = $obj->user; $tags ['[DAYS_MISSING]'] = (int) $obj->u_until <= 0 ? 0 : floor (($obj->u_until - time ()) / (60 * 60 * 24)); $u = explode ('-', $obj->f_until); $tags ['[UNTIL]'] = strftime ('%x', mktime (0, 0, 0, (int) $u [0], (int) $u [1], (int) $u [2])); $d = explode ('-', $obj->f_create); $tags ['[DATE]'] = strftime ('%c', mktime ((int) $d [0], (int) $d [1], (int) $d [2], (int) $d [3], (int) $d [4], (int) $d [5])); $uTags = unserialize ($obj->_parameters); if (is_array ($uTags)) $tags = array_merge ($tags, $uTags); return str_replace (array_keys ($tags), $tags, $this->getFromTemplate ($obj->_template, 'message')); } private function getFromTemplate ($template, $attribute) { if (!array_key_exists ($template, $this->templates)) return 'N/A'; if (!array_key_exists ($attribute, $this->templates [$template])) return 'N/A'; return $this->templates [$template][$attribute]; } private function register ($template, $assign, $users, $tags = NULL, $until = NULL, $author = NULL, $overwrite = TRUE, $mail = NULL) { if ((!is_integer ($users) && !is_array ($users)) || (!is_null ($until) && !is_integer ($until)) || (!is_null ($until) && is_integer ($until) && $until > 0 && $until < time ())) return FALSE; if (!array_key_exists ($template, $this->templates)) return FALSE; if (!is_null ($until) && !$until) $until = NULL; if (!is_array ($tags)) $tags = array (); if (!is_array ($users)) $users = array ($users); $users = array_filter ($users); if (!sizeof ($users)) return FALSE; if ((!is_array ($mail) && !is_integer ($mail)) || (!is_array ($mail) && is_integer ($mail) && !$mail)) $mail = array (); if (is_integer ($mail) && $mail > time ()) $mail = array ($mail); if (is_null ($author)) $author = User::singleton ()->getId (); $db = Database::singleton (); $sql = "SELECT _id FROM _alert WHERE _template = :template AND _assign = :assign"; $sth = $db->prepare ($sql); $sth->execute (array (':template' => $template, ':assign' => $assign)); $obj = $sth->fetch (PDO::FETCH_OBJ); try { if (is_object ($obj)) { if (!$overwrite) return FALSE; $id = $obj->_id; $sql = "UPDATE _alert SET _until = timestamptz 'epoch' + :until * interval '1 second', _parameters = :parameters, _user = :user, _update = NOW() WHERE _id = :id"; $db->prepare ($sql)->execute (array (':until' => $until, ':parameters' => serialize ($tags), ':user' => $author, ':id' => $id)); $sql = "UPDATE _alert_user SET _read = B'0', _delete = B'0' WHERE _alert = :id"; $sth = $db->prepare ($sql); $sth->execute (array (':id' => $id)); } else { $id = Database::nextId ('_alert'); $sql = "INSERT INTO _alert (_id, _template, _assign, _user, _until, _parameters) VALUES (:id, :template, :assign, :user, timestamptz 'epoch' + :until * interval '1 second', :parameters)"; $db->prepare ($sql)->execute (array (':template' => $template, ':assign' => $assign, ':until' => $until, ':parameters' => serialize ($tags), ':user' => $author, ':id' => $id)); } } catch (PDOException $e) { toLog ($e->getMessage ()); return FALSE; } $sql = "INSERT INTO _alert_user (_alert, _user) VALUES (:id, :user)"; $sth = $db->prepare ($sql); foreach ($users as $trash => $user) { try { $sth->execute (array (':id' => $id, ':user' => $user)); } catch (PDOException $e) { continue; } } try { $db->beginTransaction (); $db->exec ("DELETE FROM _alert_mail WHERE _alert = '". $id ."'"); $sql = "INSERT INTO _alert_mail (_alert, _trigger) VALUES (:id, timestamptz 'epoch' + :trigger * interval '1 second')"; $sth = $db->prepare ($sql); $now = time (); $today = mktime (0, 0, 0, date ('m'), date ('d') + 1, date ('Y')); foreach ($mail as $trash => $trigger) if ($trigger > $today) $sth->execute (array (':id' => $id, ':trigger' => $trigger)); $sth->execute (array (':id' => $id, ':trigger' => $now)); $db->commit (); } catch (PDOException $e) { toLog ($e->getMessage ()); $db->rollBack (); } $this->sendMail ($id); return TRUE; } public function sendMail ($id = NULL) { if (!is_null ($id) && (!is_numeric ($id) || !(int) $id)) return FALSE; $today = mktime (0, 0, 0, date ('m'), date ('d') + 1, date ('Y')); $db = Database::singleton (); $sql = "SELECT a.*, at._name AS author, at._email AS a_mail, av._name AS user, av._email AS u_mail, av._id AS u_id, av._login AS u_login, to_char (a._until, 'DD/MM/YYYY') AS f_until, extract (epoch FROM a._until) AS u_until, to_char (a._create, 'DD/MM/YYYY HH24:MI:SS') AS f_create FROM _alert_user au INNER JOIN _alert a ON a._id = au._alert LEFT JOIN _user at ON at._id = a._user INNER JOIN _user av ON av._id = au._user WHERE (a._until IS NULL OR a._until > CURRENT_TIMESTAMP) AND ". (is_null ($id) ? "" : "au._alert = :id AND ") ." EXISTS (SELECT 1 FROM _alert_mail WHERE _alert = a._id AND _send = B'0' AND timestamptz 'epoch' + :today * interval '1 second' > _trigger) AND av._active = B'1' AND av._deleted = B'0'"; $sth = $db->prepare ($sql); if (is_null ($id)) $sth->execute (array (':today' => $today)); else $sth->execute (array (':id' => $id, ':today' => $today)); $dTags = array ('[SYSTEM]' => Instance::singleton ()->getName (), '[URL]' => Instance::singleton ()->getUrl ()); $flag = FALSE; while ($obj = $sth->fetch (PDO::FETCH_OBJ)) { if (!array_key_exists ($obj->_template, $this->templates) || !array_key_exists ('subject', $this->templates [$obj->_template]) || !array_key_exists (0, $this->templates [$obj->_template]) || trim ($this->templates [$obj->_template]['subject']) == '' || trim ($this->templates [$obj->_template][0]) == '') continue; try { $query = $db->query ("SELECT _alert FROM _user WHERE _id = '". $obj->u_id ."'"); $enabled = $query->fetchColumn (); if (!is_null ($enabled) && !(int) $enabled) continue; } catch (PDOException $e) {} $auth = is_null ($obj->author) ? Instance::singleton ()->getName () : $obj->author; $mail = is_null ($obj->a_mail) ? Instance::singleton ()->getEmail () : $obj->a_mail; $tags = $dTags; $tags ['[AUTHOR]'] = $auth; $tags ['[USER]'] = $obj->user; $tags ['[DAYS_MISSING]'] = (int) $obj->u_until <= 0 ? 0 : floor (($obj->u_until - time ()) / (60 * 60 * 24)); $tags ['[UNTIL]'] = $obj->f_until; $tags ['[DATE]'] = $obj->f_create; $hash = Security::singleton ()->getHash (); if (Instance::singleton ()->getFriendlyUrl ('disable-alerts') == '') $tags ['[DISABLE]'] = Instance::singleton ()->getUrl () .'titan.php?target=disableAlerts&login='. urlencode ($obj->u_login) .'&hash='. shortlyHash (md5 ($hash . $obj->user . $hash . $obj->u_id . $hash . $obj->u_mail . $hash)); else $tags ['[DISABLE]'] = Instance::singleton ()->getUrl () . Instance::singleton ()->getFriendlyUrl ('disable-alerts') .'/'. urlencode ($obj->u_login) .'/'. shortlyHash (md5 ($hash . $obj->user . $hash . $obj->u_id . $hash . $obj->u_mail . $hash)); $uTags = unserialize ($obj->_parameters); if (is_array ($uTags)) $tags = array_merge ($tags, $uTags); if (!@mail ($obj->u_mail, '=?utf-8?B?'. base64_encode (str_replace (array_keys ($tags), $tags, $this->getFromTemplate ($obj->_template, 'subject'))) .'?=', str_replace (array_keys ($tags), $tags, $this->getFromTemplate ($obj->_template, 0)), "From: ". $auth ." <". Instance::singleton ()->getEmail () .">\r\nReply-To: ". $mail ."\r\nX-Mailer: PHP/". phpversion () ."\r\nContent-Type: text/plain; charset=utf-8")) { toLog ('Impossible to send alert mail! [To: '. $obj->u_mail .'] [Subject: '. str_replace (array_keys ($tags), $tags, $this->getFromTemplate ($obj->_template, 'subject')) .']'); continue; } $flag = TRUE; } if ($flag) { try { $sql = "UPDATE _alert_mail SET _send = B'1' WHERE _send = B'0' AND timestamptz 'epoch' + :today * interval '1 second' > _trigger". (is_null ($id) ? "" : " AND _alert = :id"); $sth = $db->prepare ($sql); if (is_null ($id)) $sth->execute (array (':today' => $today)); else $sth->execute (array (':id' => $id, ':today' => $today)); } catch (PDOException $e) { toLog ($e->getMessage ()); return FALSE; } } return $flag; } private function unregister ($template, $assign) { try { $sth = Database::singleton ()->prepare ("DELETE FROM _alert WHERE _template = :template AND _assign = :assign"); $sth->bindParam (':template', $template, PDO::PARAM_STR, 64); $sth->bindParam (':assign', $assign, PDO::PARAM_STR, 64); $sth->execute (); } catch (PDOException $e) { return FALSE; } return TRUE; } public function read ($id, $user) { try { $sth = Database::singleton ()->prepare ("UPDATE _alert_user SET _read = B'1' WHERE _alert = :id AND _user = :user"); $sth->bindParam (':id', $id, PDO::PARAM_INT); $sth->bindParam (':user', $user, PDO::PARAM_INT); $sth->execute (); } catch (PDOException $e) { toLog ($e->getMessage ()); return FALSE; } return TRUE; } public function delete ($id, $user) { try { $sth = Database::singleton ()->prepare ("UPDATE _alert_user SET _delete = B'1' WHERE _alert = :id AND _user = :user"); $sth->bindParam (':id', $id, PDO::PARAM_INT); $sth->bindParam (':user', $user, PDO::PARAM_INT); $sth->execute (); } catch (PDOException $e) { toLog ($e->getMessage ()); return FALSE; } return TRUE; } public static function add ($template, $assign, $users, $tags = NULL, $until = NULL, $author = NULL, $overwrite = TRUE, $mail = NULL) { if (!self::isActive ()) return FALSE; return Alert::singleton ()->register ($template, $assign, $users, $tags, $until, $author, $overwrite, $mail); } public static function remove ($template, $assign) { if (!self::isActive ()) return FALSE; return Alert::singleton ()->unregister ($template, $assign); } public static function garbageCollector () { $db = Database::singleton (); $sql = "SELECT a._id FROM _alert a WHERE (NOT EXISTS (SELECT 1 FROM _alert_mail WHERE _alert = a._id AND _send = B'0') AND NOT EXISTS (SELECT 1 FROM _alert_user WHERE _alert = a._id AND _delete = B'0')) OR (a._until IS NOT NULL AND a._until < date_trunc ('day', now() - interval '1 day'))"; $sth = $db->prepare ($sql); $sth->execute (); $garbage = $sth->fetchAll (PDO::FETCH_COLUMN, 0); try { if (sizeof ($garbage)) { $sql = "DELETE FROM _alert WHERE _id IN (". implode (", ", $garbage) .")"; $db->exec ($sql); } } catch (Exception $e) { toLog ($e->getMessage ()); } } public static function isActive () { if (is_null (self::$active)) self::$active = Database::tableExists ('_alert'); return self::$active; } public static function sendMobileNotification () { if (!self::isActive () || !MobileDevice::isActive () || !Api::isActive ()) return FALSE; $db = Database::singleton (); $mark = $db->prepare ("UPDATE _alert_user SET _mobile = B'1' WHERE _alert = :id AND _user = :user"); $sql = "SELECT _user AS user, MIN(_alert) AS id FROM _alert_user WHERE _mobile = B'0' AND _read = B'0' AND _delete = B'0' GROUP BY _user"; $sth = $db->prepare ($sql); $sth->execute (); while ($obj = $sth->fetch (PDO::FETCH_OBJ)) { $message = self::singleton ()->getAlertMessage ($obj->id, $obj->user); if (trim ($message) == '') continue; while ($app = Api::singleton ()->getApp ()) { if (!$app->sendAlerts ()) continue; $data = array ('id' => $obj->id, 'message' => $message); $app->sendNotification ($obj->user, $data); $mark->execute (array (':id' => $obj->id, ':user' => $obj->user)); } } } }