diff --git a/classes/mailer.php b/classes/mailer.php new file mode 100644 index 0000000..d8f4eb7 --- /dev/null +++ b/classes/mailer.php @@ -0,0 +1,175 @@ +is_utf8($to) ) { + $elements = explode('@', $to); + $domainpart = EncodePunycodeIDN(array_pop($elements)); // Convert domain part to ascii + $to = $elements[0] . '@' . $domainpart; // Reassemble tge full email address + syslog(1,"email: " .$to); + } + + // Send using PHP mailer if it is enabled + if ( PHP_MAILER ) { + require_once(PHP_MAILER_PATH .'/Exception.php'); /* Exception class. */ + require_once(PHP_MAILER_PATH .'/PHPMailer.php'); /* The main PHPMailer class. */ + + if ( PHP_MAILER_SMTP ) { + require_once(PHP_MAILER_PATH .'/SMTP.php'); /* SMTP class, needed if you want to use SMTP. */ + } + + $phpmail = new PHPMailer(false); + + $phpmail->setFrom(MAILER_ADDRESS, MAILER_NAME); + $phpmail->addReplyTo(MAILER_ADDRESS, MAILER_NAME); + //$phpmail->Debugoutput = error_log; + + // Define SMTP parameters if enabled + if ( PHP_MAILER_SMTP ) { + + $phpmail->isSMTP(); + $phpmail->Host = PHP_MAILER_HOST; + $phpmail->Port = PHP_MAILER_PORT; + $phpmail->SMTPSecure = PHP_MAILER_SECURE; + //$phpmail->SMTPDebug = 2; // Enable for debugging + + // Handle authentication for SMTP if enabled + if ( !empty(PHP_MAILER_USER) ) { + $phpmail->SMTPAuth = true; + $phpmail->Username = PHP_MAILER_USER; + $phpmail->Password = PHP_MAILER_PASS; + } + } + + $phpmail->addAddress($to); + $phpmail->Subject = $subject; + // Send HMTL mail + if ( $html ) { + $phpmail->msgHtml($message); + $phpmail->AltBody = $this->convert_html_to_plain_txt($message, false); + } else { + $phpmail->Body = $message; // Send plain text + } + + $phpmail->isHtml($html); // use htmlmail if enabled + if ( ! $phpmail->send() ) { + // TODO Log error message $phpmail->ErrorInfo; + return false; + } + return true; + + } else { + // Use standard PHP mail() function + $headers = "Content-Type: $content_type; \"charset=utf-8\" ".PHP_EOL; + $headers .= "MIME-Version: 1.0 ".PHP_EOL; + $headers .= "From: ".MAILER_NAME.' <'.MAILER_ADDRESS.'>'.PHP_EOL; + $headers .= "Reply-To: ".MAILER_NAME.' <'.MAILER_ADDRESS.'>'.PHP_EOL; + + mail($to, $subject, $message, $headers); + // TODO log error message if mail fails + return true; + } + + } + /** + * Tries to verify the domain using dns request against an MX record of the domain part + * of the passed email address. The code also handles IDN/Punycode formatted addresses which + * contains utf8 characters. + * Original code from https://stackoverflow.com/questions/19261987/how-to-check-if-an-email-address-is-real-or-valid-using-php/19262381 + * @param String $email Email address to check + * @return boolean True if MX record exits, false if otherwise + */ + public function verify_domain($email){ + // TODO - Handle idn/punycode domain names without being dependent on PHP native libs. + $domain = explode('@', $email); + $domain = EncodePunycodeIDN(array_pop($domain).'.'); // Add dot at end of domain to avoid local domain lookups + syslog(1,$domain); + return checkdnsrr($domain, 'MX'); + } + + + /** + * Check if string contains non-english characters (detect IDN/Punycode enabled domains) + * Original code from: https://stackoverflow.com/questions/13120475/detect-non-english-chars-in-a-string + * @param String $str String to check for extended characters + * @return boolean True if extended characters, false otherwise + */ + public function is_utf8($str) + { + if (strlen($str) == strlen(utf8_decode($str))) { + return false; + } else { + return true; + } + } + + /** + * Takes the input from an HTML email and convert it to plain text + * This is commonly used when sending HTML emails as a backup for email clients who can only view, or who choose to only view, + * Original code from https://github.com/DukeOfMarshall/PHP---JSON-Email-Verification/blob/master/EmailVerify.class.php + * plain text emails + * @param string $content The body part of the email to convert to plain text. + * @param boolean $remove_links Set to true if links should be removed from email + * @return String pain text version + */ + public function convert_html_to_plain_txt($content, $remove_links=false){ + // TODO does not handle unsubscribe/manage subscription text very well. + // Replace HTML line breaks with text line breaks + $plain_text = str_ireplace(array("
","
"), "\n\r", $content); + + // Remove the content between the tags that wouldn't normally get removed with the strip_tags function + $plain_text = preg_replace(array('@]*?>.*?@siu', + '@]*?>.*?@siu', + '@]*?.*?@siu', + '@]*?.*?@siu', + ), "", $plain_text); // Remove everything from between the tags that doesn't get removed with strip_tags function + + // If the user has chosen to preserve the addresses from links + if(!$remove_links){ + $plain_text = strip_tags(preg_replace('//', ' $1 ', $plain_text)); + } + + // Remove HTML spaces + $plain_text = str_replace(" ", "", $plain_text); + + // Replace multiple line breaks with a single line break + $plain_text = preg_replace("/(\s){3,}/","\r\n\r\n",trim($plain_text)); + + return $plain_text; + } + +} \ No newline at end of file diff --git a/classes/notification.php b/classes/notification.php new file mode 100644 index 0000000..51b3a3c --- /dev/null +++ b/classes/notification.php @@ -0,0 +1,139 @@ +prepare("SELECT services.id, services.name FROM services INNER JOIN services_status on services.id = services_status.service_id WHERE services_status.status_id = ?"); + $stmt->bind_param("i", $status_id); + $stmt->execute(); + $query = $stmt->get_result(); + $arrServicesNames = array(); + $arrServicesId = array(); + while ($result = $query->fetch_assoc()) { + $arrServicesNames[] = $result['name']; + $arrServicesId[] = (int) $result['id']; + } + $this->status_id = $status_id; + $this->servicenames = implode(",", $arrServicesNames); + $this->serviceids = implode(",", $arrServicesId); + return true; + } else { + return false; + } + } + + /** + * Loop over the list of subscribers to notify depending on impacted service(s) and + * call the differnet notification handles. + * @return void + */ + public function notify_subscribers() + { + global $mysqli; + // Fetch list of unique subscribers for given service + // Direct inclusion of variable withour using prepare justified by the fact that + // this->serviceids are not user submitted + $sql = "SELECT DISTINCT subscriberIDFK FROM services_subscriber WHERE serviceIDFK IN (" . $this->serviceids . ")"; + $query = $mysqli->query($sql); + + while ($subscriber = $query->fetch_assoc()) { + // Fetch list of subscriber details for already found subscriber IDs + $stmt = $mysqli->prepare("SELECT typeID, userID, firstname, token FROM subscribers WHERE subscriberID = ? AND active=1"); + $stmt->bind_param("i", $subscriber['subscriberIDFK']); + $stmt->execute(); + $subscriberQuery = $stmt->get_result(); + + while ($subscriberData = $subscriberQuery->fetch_assoc()) { + $typeID = $subscriberData['typeID']; // Telegram = 1, email = 2 + $userID = $subscriberData['userID']; + $firstname = $subscriberData['firstname']; + $token = $subscriberData['token']; + + // Handle telegram + if ($typeID == 1) { + $this->submit_telegram($userID, $firstname, $token); + } + + // Handle email + if ($typeID == 2) { + $this->submit_email($userID, $token); + } + } + } + } + + /** + * Sends Telegram notification message using their web api. + * @param string $userID The Telegram userid to send to + * @param string $firstname The users firstname + * @param string $uthkey Token used for managing subscription + * @return void + */ + public function submit_telegram($userID, $firstname, $token) + { + // TODO Handle limitations (Max 30 different subscribers per second) + // TODO Error handling + $msg = _("Hi %s!\nThere is a status update for service(s): %s\nThe new status is: %s\nTitle: %s\n\n%s\n\nView online"); + $msg = sprintf($msg, $firstname, $this->servicenames, $this->status, $this->title, $this->text, WEB_URL); + + $tg_message = urlencode($msg); + $response = json_decode(file_get_contents("https://api.telegram.org/bot" . TG_BOT_API_TOKEN . "/sendMessage?chat_id=" . $userID . "&parse_mode=HTML&text=" . $tg_message), true); + } + + /** + * Sends email notifications to a subscriber. + * Function depends on Parsedown and Mailer class being loaded. + * @param String $userID The email address to send to + * @param String $uthkey Users token for managing subscription + * @return void + */ + public function submit_email($userID, $token) + { + // TODO Error handling + $Parsedown = new Parsedown(); + $mailer = new Mailer(); + + $str_mail = file_get_contents("../libs/templates/email_status_update.html"); + $str_mail = str_replace("%name%", NAME, $str_mail); + // $smtp_mail = str_replace("%email%", $userID, $smtp_mail); + $str_mail = str_replace("%url%", WEB_URL, $str_mail); + $str_mail = str_replace("%service%", $this->servicenames, $str_mail); + $str_mail = str_replace("%status%", $this->status, $str_mail); + $str_mail = str_replace("%time%", date("c", $this->time), $str_mail); + $str_mail = str_replace("%comment%", $Parsedown->setBreaksEnabled(true)->text($this->text), $str_mail); + $str_mail = str_replace("%token%", $token, $str_mail); + + $str_mail = str_replace("%service_status_update_from%", _("Service status update from"), $str_mail); + $str_mail = str_replace("%services_impacted%", _("Service(s) Impacted"), $str_mail); + $str_mail = str_replace("%status_label%", _("Status"), $str_mail); + $str_mail = str_replace("%time_label%", _("Time"), $str_mail); + $str_mail = str_replace("%manage_subscription%", _("Manage subscription"), $str_mail); + $str_mail = str_replace("%unsubscribe%", _("Unsubscribe"), $str_mail); + $str_mail = str_replace("%powered_by%", _("Powered by"), $str_mail); + $subject = _('Status update from') . ' - ' . NAME . ' [ ' . $this->status . ' ]'; + $mailer->send_mail($userID, $subject, $str_mail); + } +} \ No newline at end of file diff --git a/classes/subscriber.php b/classes/subscriber.php new file mode 100644 index 0000000..bdc8b92 --- /dev/null +++ b/classes/subscriber.php @@ -0,0 +1,328 @@ +firstname = null; + $this->lastname = null; + $this->userID = ""; + $this->token = null; + $this->active = 0; + $this->typeID = null; + } + + /** + * Gets authentcation token for specified subscriberID + * @param Integer $subscriberID - specifies which subscriber we are looking up + * @param Integer $typeID - specifies which type of subscription we are refering (1 = telegram, 2 = email) + * @return String $token - 32 bytes HEX string + */ + public function get_token($subscriberID, $typeID) + { + global $mysqli; + $stmt = $mysqli->prepare("SELECT token FROM subscribers WHERE subscriberID = ? and typeID=? and active = 1 LIMIT 1"); + $stmt->bind_param("ii", $subscriberID, $typeID); + $stmt->execute(); + $result = $stmt->get_result(); + if ($result->num_rows > 0) { + $row = $result->fetch_assoc(); + $this->token = $row['token']; + //$this->get_subscriber_by_token($this->token); + return $row['token']; + } + return false; + + } + public function get_subscriber_by_token($token) + { + global $mysqli; + $stmt = $mysqli->prepare("SELECT subscriberID FROM subscribers WHERE token=? and typeID=?"); + $stmt->bind_param("si", $token, $this->typeID); + $stmt->execute(); + $result = $stmt->get_result(); + if ($result->num_rows > 0) { + $row = $result->fetch_assoc(); + $this->id = $row['subscriberID']; + $this->populate(); // + return true; + } + return false; + } + + public function get_subscriber_by_userid($create = false) + { + global $mysqli; + $stmt = $mysqli->prepare("SELECT subscriberID FROM subscribers WHERE userID LIKE ? AND typeID = ? LIMIT 1"); + $stmt->bind_param("si", $this->userID, $this->typeID ); + $stmt->execute(); + $result = $stmt->get_result(); + + if ($result->num_rows > 0) { + $row = $result->fetch_assoc(); + $this->id = $row['subscriberID']; + $this->populate(); + return $row['subscriberID']; + } else { + // User is not registered in DB, so add if $create = true + if ( $create ) { + $subscriber_id = $this->add($this->typeID, $this->userID, $this->active, $this->firstname, $this->lastname); + return $subscriber_id; + } + return false; + } + } + + public function populate() + { + global $mysqli; + $stmt = $mysqli->prepare("SELECT typeID, userID, firstname, lastname, token, active FROM subscribers WHERE subscriberID = ?"); + $stmt->bind_param("i", $this->id); + $stmt->execute(); + $result = $stmt->get_result(); + if ($result->num_rows > 0) { + $row = $result->fetch_assoc(); + $this->userID = $row['userID']; + $this->typeID = $row['typeID']; + $this->firstname = $row['firstname']; + $this->lastname = $row['lastname']; + $this->token = $row['token']; + $this->active = $row['active']; + return true; + } + return false; + } + + public function add($typeID, $userID, $active = null, $firstname = null, $lastname = null) + { + global $mysqli; + $expireTime = strtotime("+2 hours"); + $updateTime = strtotime("now"); + $token = $this->generate_token(); + syslog(1,"token". $token); + $stmt = $mysqli->prepare("INSERT INTO subscribers (typeID, userID, firstname, lastname, token, active, expires, create_time) VALUES (?, ?, ?, ?, ?, ?, ?, ?)"); + $stmt->bind_param("issssiii", $typeID, $userID, $firstname, $lastname, $token, $active, $expireTime, $updateTime); + $stmt->execute(); + $query = $stmt->get_result(); + + $this->id = $mysqli->insert_id; + $this->typeID = $typeID; + $this->userID = $userID; + $this->token = $token; + $this->firstname = $firstname; + $this->lastname = $lastname; + $this->active = $active; + return $this->id; + } + + public function update($subscriberID) + { + global $mysqli; + $updateTime = strtotime("now"); + $stmt = $mysqli->prepare("UPDATE subscribers SET update_time = ? WHERE subscriberID=?"); + $stmt->bind_param("ii", $updateTime, $subscriberId); + $stmt->execute(); + return true; + + } + + public function activate($subscriberID) + { + global $mysqli; + $updateTime = strtotime("now"); + + $stmt = $mysqli->prepare("UPDATE subscribers SET update_time = ?, expires = ? WHERE subscriberID = ?"); + $tmp = null; + $stmt->bind_param("iii", $updateTime, $tmp, $subscriberId); + $stmt->execute(); + return true; + } + + public function delete($id) + { + global $mysqli; + + $stmt = $mysqli->prepare("DELETE FROM services_subscriber WHERE subscriberIDFK = ?"); + $stmt->bind_param("i", $this->id); + $stmt->execute(); + $query = $stmt->get_result(); + + $stmt = $mysqli->prepare("DELETE FROM subscribers WHERE subscriberID = ?"); + $stmt->bind_param("i", $this->id); + $stmt->execute(); + $query = $stmt->get_result(); + + } + + public function check_userid_exist() + { + global $mysqli; + + $stmt = $mysqli->prepare("SELECT subscriberID, userID, token, active FROM subscribers WHERE typeID=? AND userID=? LIMIT 1"); + + $stmt->bind_param("is", $this->typeID, $this->userID); + $stmt->execute(); + $result = $stmt->get_result(); + + if($result->num_rows > 0) { + $row = $result->fetch_assoc(); + $this->id = $row['subscriberID']; + $this->populate(); + return true; + } + return false; + } + + public function is_active_subscriber($token) + { + global $mysqli; + + + $stmt = $mysqli->prepare("SELECT subscriberID, token, userID, active, expires FROM subscribers WHERE token LIKE ? LIMIT 1"); + $stmt->bind_param("s", $token ); + $stmt->execute(); + $result = $stmt->get_result(); + + if ($result->num_rows > 0) { + $row = $result->fetch_assoc(); + } else { + // No data found, fail gently... + return false; + } + + // If account is not already active, check if we are within timeframe of exipre +2h + // and active if so, otherwise,delete account and return falsev + if ( $row['active'] <> 1 ) { + + // Calculate time range for when subscription need to be validated + $time_end = $row['expires']; + $time_start = $time_end - (3600*2); // TODO - make this interval configurable via a config option + $time_now = time(); + + if ( ($time_now > $time_start) && ($time_now < $time_end) ) { + // Timefram is within range, active user.. + $stmt2 = $mysqli->prepare("UPDATE subscribers SET active=1, expires=null WHERE subscriberID = ?"); + $stmt2->bind_param("i", $row['subscriberID']); + $stmt2->execute(); + $result = $stmt2->get_result(); + $this->active = 1; + $this->id = $row['subscriberID']; + $this->userID = $row['userID']; + $this->token = $row['token']; + return true; + + } else { + // Timeframe outside of given scope -> delete account + $stmt2 = $mysqli->prepare("DELETE FROM subscribers WHERE subscriberID = ?"); + $stmt2->bind_param("i", $row['subscriberID']); + $stmt2->execute(); + $result = $stmt2->get_result(); + $this->active = 0; + return false; + } + } + + // if we get here, account should already be active + $this->active = 1; + $this->id = $row['subscriberID']; + $this->userID = $row['userID']; + $this->token = $row['token']; + return true; + } + + /** + * Generate a new 64 byte token (32 bytes converted from bin2hex = 64 bytes) + * @return string token + */ + public function generate_token() + { + global $mysqli; + + if ( function_exists('openssl_random_pseudo_bytes') ) { + $token = openssl_random_pseudo_bytes(32); //Generate a random string. + $token = bin2hex($token); //Convert the binary data into hexadecimal representation. + } else { + // Use alternative token generator if openssl isn't available... + $token = make_alt_token(32, 32); + } + + // Make sure token doesn't already exist in db + $stmt = $mysqli->prepare("SELECT subscriberID FROM subscribers WHERE token LIKE ?"); + echo $mysqli->error; + $stmt->bind_param("s", $token); + $stmt->execute(); + $result = $stmt->get_result(); + if ($result->num_rows > 0 ) { + // token already exists, call self again + $token = $this->generate_token(); + } + + return $token; + } + + /** + * Alternative token generator if openssl_random_pseudo_bytes is not available + * Original code by jsheets at shadonet dot com from http://php.net/manual/en/function.mt-rand.php + * @params int min_length Minimum length of token + * @params int max_length Maximum length of token + * @return String token + */ + public function make_alt_token($min_length = 32, $max_length = 64) + { + $key = ''; + + // build range and shuffle range using ASCII table + for ($i=0; $i<=255; $i++) { + $range[] = chr($i); + } + + // shuffle our range 3 times + for ($i=0; $i<=3; $i++) { + shuffle($range); + } + + // loop for random number generation + for ($i = 0; $i < mt_rand($min_length, $max_length); $i++) { + $key .= $range[mt_rand(0, count($range)-1)]; + } + + $return = bin2hex($key); + + if (!empty($return)) { + return $return; + } else { + return 0; + } + } + + public function set_logged_in() + { + $_SESSION['subscriber_valid'] = true; + $_SESSION['subscriber_id'] = $this->id; + $_SESSION['subscriber_userid'] = $this->userID; + $_SESSION['subscriber_typeid'] = $this->typeID; //email + $_SESSION['subscriber_token'] = $this->token; + } + + public function set_logged_off() + { + unset($_SESSION['subscriber_valid']); + unset($_SESSION['subscriber_userid']); + unset($_SESSION['subscriber_typeid']); + unset($_SESSION['subscriber_id']); + unset($_SESSION['subscriber_token']); + } + +} \ No newline at end of file diff --git a/classes/subscriptions.php b/classes/subscriptions.php new file mode 100644 index 0000000..d1cf6e4 --- /dev/null +++ b/classes/subscriptions.php @@ -0,0 +1,94 @@ +prepare("INSERT INTO services_subscriber (subscriberIDFK, serviceIDFK) VALUES (?, ?)"); + $stmt->bind_param("ii", $userID, $service); + $stmt->execute(); + $query = $stmt->get_result(); + } + + public function remove($userID, $service) + { + global $mysqli; + + $stmt = $mysqli->prepare("DELETE FROM services_subscriber WHERE subscriberIDFK = ? AND serviceIDFK = ?"); + $stmt->bind_param("ii", $userID, $service); + $stmt->execute(); + $query = $stmt->get_result(); + } + + function render_subscribed_services($typeID, $subscriberID, $userID, $token) + { + global $mysqli; + $stmt = $mysqli->prepare("SELECT services.id, services.name, subscribers.subscriberID, subscribers.userID, subscribers.token + FROM services + LEFT JOIN services_subscriber ON services_subscriber.serviceIDFK = services.id + LEFT JOIN subscribers ON services_subscriber.subscriberIDFK = subscribers.subscriberID + WHERE subscribers.typeID = ? AND subscribers.subscriberID = ?"); + $stmt->bind_param("ii", $typeID, $subscriberID); + $stmt->execute(); + $query = $stmt->get_result(); + + $timestamp = time(); + + $strNotifyType = _('E-mail Notification subscription'); + if ( $typeID == 1 ) { $strNotifyType = _('Telegram Notification subscription'); } + + ?> + + ' . _("Your subscriptions") . ""; + echo '
'; + $subs = array(); // Will be used to hold IDs of services already selected + + if ($query->num_rows){ + while($result = $query->fetch_assoc()) + { + echo ' ' . $result['name'] . ''; + $subs[] = $result['id']; + } + + } else { + echo '
'._("You do not currently subscribe to any services. Please add services from the list below.").'
'; + } + echo "
"; + + echo '

' . _("Add new subscription") . '

'; + + // Prepare to query for unselect services. If none are selected, query for all + $subsExp = null; + if (count($subs) > 0 ) { + $subsExp = 'NOT IN ('. implode(",", $subs) .')'; + } + + $query = $mysqli->query("SELECT services.id, services.name from services WHERE services.id $subsExp"); + echo '
'; + if ($query->num_rows){ + while($result = $query->fetch_assoc()){ + echo ' ' . $result['name'] . ''; + } + } else { + echo '
'._("No further services available for subscriptions.").'
'; + } + echo '
'; + } + +} \ No newline at end of file