AutorÃa | Ultima modificación | Ver Log |
<?php
// This file is part of the Contact Form plugin for Moodle - https://moodle.org/
//
// Contact Form is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Contact Form is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Contact Form. If not, see <https://www.gnu.org/licenses/>.
/**
* This plugin for Moodle is used to send emails through a web form.
*
* @package local_contact
* @copyright 2016-2024 TNG Consulting Inc. - www.tngconsulting.ca
* @author Michael Milette
* @license https://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
/**
* local_contact class. Handles processing of information submitted from a web form.
* @copyright 2016-2024 TNG Consulting Inc. - www.tngconsulting.ca
* @license https://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class local_contact {
/**
* The name of the sender for the message.
*
* @var string
*/
public $fromname;
/**
* The email address of the sender for the message.
*
* @var string
*/
public $fromemail;
/**
* True if the information submitted is considered to have been sent from a spambot.
*
* @var bool
*/
public $isspambot;
/**
* Error message in case there are any issues.
*
* @var string
*/
public $errmsg;
/**
* Class constructor. Receives and validates information received through a
* web form submission.
*
* @return boolean True if the information received passes our spambot detection. False if it fails.
*/
public function __construct() {
global $CFG;
if (isloggedin() && !isguestuser()) {
// If logged-in as non guest, use their registered fullname and email address.
global $USER;
$this->fromname = get_string('fullnamedisplay', null, $USER);
$this->fromemail = $USER->email;
// Insert name and email address at first position in $_POST array.
if (!empty($_POST['email'])) {
unset($_POST['email']);
}
if (!empty($_POST['name'])) {
unset($_POST['name']);
}
$_POST = array_merge(['email' => $this->fromemail], $_POST);
$_POST = array_merge(['name' => $this->fromname], $_POST);
} else {
// If not logged-in as a user or logged in a guest, the name and email fields are required.
if (empty($this->fromname = trim(optional_param(get_string('field-name', 'local_contact'), '', PARAM_TEXT)))) {
$this->fromname = required_param('name', PARAM_TEXT);
}
if (empty($this->fromemail = trim(optional_param(get_string('field-email', 'local_contact'), '', PARAM_EMAIL)))) {
$this->fromemail = required_param('email', PARAM_TEXT);
}
}
$this->fromname = trim($this->fromname ?? '');
$this->fromemail = trim($this->fromemail ?? '');
$this->isspambot = false;
$this->errmsg = '';
if ($CFG->branch >= 32) {
// As of Moodle 3.2, $CFG->emailonlyfromnoreplyaddress has been deprecated.
$CFG->emailonlyfromnoreplyaddress = !empty($CFG->noreplyaddress);
}
// Did someone forget to configure Moodle properly?
// Validate Moodle's no-reply email address.
if (!empty($CFG->emailonlyfromnoreplyaddress)) {
if (
!$this->isspambot
&& !empty($CFG->emailonlyfromnoreplyaddress)
&& $this->isspambot = !validate_email($CFG->noreplyaddress)
) {
$this->errmsg = 'Moodle no-reply email address is invalid.';
if ($CFG->branch >= 32) {
$this->errmsg .= ' (<a href="../../admin/settings.php?section=outgoingmailconfig">change</a>)';
} else {
$this->errmsg .= ' (<a href="../../admin/settings.php?section=messagesettingemail">change</a>)';
}
}
}
// Use primary administrators name and email address if support name and email are not defined.
$primaryadmin = get_admin();
$CFG->supportemail = empty($CFG->supportemail) ? $primaryadmin->email : $CFG->supportemail;
$CFG->supportname = empty($CFG->supportname) ? fullname($primaryadmin, true) : $CFG->supportname;
// Validate Moodle's support email address.
if (!$this->isspambot && $this->isspambot = !validate_email($CFG->supportemail)) {
$this->errmsg = 'Moodle support email address is invalid.';
$this->errmsg .= ' (<a href="../../admin/settings.php?section=supportcontact">change</a>)';
}
// START: Spambot detection.
// File attachments not supported.
$supportattachments = !empty(get_config('local_contact', 'attachment'));
if (!$supportattachments && !$this->isspambot && $this->isspambot = !empty($_FILES)) {
$this->errmsg = 'File attachments not enabled.';
}
// Validate submit button.
if (!$this->isspambot && $this->isspambot = !isset($_POST['submit'])) {
$this->errmsg = 'Missing submit button.';
}
// Limit maximum number of form $_POST fields to 1024.
if (!$this->isspambot) {
$postsize = @count($_POST);
if ($this->isspambot = ($postsize > 1024)) {
$this->errmsg = 'Form cannot contain more than 1024 fields.';
} else if ($this->isspambot = ($postsize == 0)) {
$this->errmsg = 'Form must be submitted using POST method.';
}
}
// Limit maximum size of allowed form $_POST submission to 256 KB.
if (!$this->isspambot) {
$postsize = (int) @$_SERVER['CONTENT_LENGTH'];
if ($this->isspambot = ($postsize > 262144)) {
$this->errmsg = 'Form cannot contain more than 256 KB of data.';
}
}
// Validate if "sesskey" field contains the correct value.
if (!$this->isspambot && $this->isspambot = (optional_param('sesskey', '3.1415', PARAM_RAW) != sesskey())) {
$this->errmsg = '"sesskey" field is missing or contains an incorrect value.';
}
// Validate referrer URL.
if (!$this->isspambot && $this->isspambot = !isset($_SERVER['HTTP_REFERER'])) {
$this->errmsg = 'Missing referrer.';
}
if (!$this->isspambot && $this->isspambot = (stripos($_SERVER['HTTP_REFERER'], $CFG->wwwroot) != 0)) {
$this->errmsg = 'Unknown referrer - must come from this site: ' . $CFG->wwwroot;
}
// Validate sender's email address.
if (!$this->isspambot && $this->isspambot = !validate_email($this->fromemail)) {
$this->errmsg = 'Unknown sender - invalid email address or the form field name is incorrect.';
}
// Validate sender's name.
if (!$this->isspambot && $this->isspambot = empty($this->fromname)) {
$this->errmsg = 'Missing sender - invalid name or the form field name is incorrect';
}
// Validate against email address whitelist and blacklist.
$skipdomaintest = false;
// TODO: MDL-0 - Create a plugin setting for this list.
$whitelist = ''; // Future code: $config->whitelistemails .
$whitelist = ',' . $whitelist . ',';
// TODO: MDL-0 - Create a plugin blacklistemails setting.
$blacklist = ''; // Future code: $config->blacklistemails .
$blacklist = ',' . $blacklist . ',';
if (!$this->isspambot && stripos($whitelist, ',' . $this->fromemail . ',') != false) {
$skipdomaintest = true; // Skip the upcoming domain test.
} else {
if (
!$this->isspambot
&& $blacklist != ',,'
&& $this->isspambot = ($blacklist == '*' || stripos($blacklist, ',' . $this->fromemail . ',') == false)
) {
// Nice try. We know who you are.
$this->errmsg = 'Bad sender - Email address is blacklisted.';
}
}
// Validate against domain whitelist and blacklist... except for the nice people.
if (!$skipdomaintest && !$this->isspambot) {
// TODO: MDL-0 - Create a plugin whitelistdomains setting.
$whitelist = ''; // Future code: $config->whitelistdomains .
$whitelist = ',' . $whitelist . ',';
$domain = substr(strrchr($this->fromemail, '@'), 1);
if (stripos($whitelist, ',' . $domain . ',') != false) {
// Ya, you check out. This email domain is gold here!
$blacklist = '';
} else {
// TODO: MDL-0 - Create a plugin blacklistdomains setting.
$blacklist = 'example.com,example.net,sample.com,test.com,specified.com'; // Future code:$config->blacklistdomains .
$blacklist = ',' . $blacklist . ',';
if (
$blacklist != ',,'
&& $this->isspambot = ($blacklist == '*'
|| stripos($blacklist, ',' . $domain . ',') != false)
) {
// Naughty naughty. We know all about your kind.
$this->errmsg = 'Bad sender - Email domain is blacklisted.';
}
}
}
// TODO: MDL-0 - Test IP address against blacklist.
// END: Spambot detection... Wait, got some photo ID on you? ;-) .
}
/**
* Creates a user info object based on provided parameters.
*
* @param string $email email address.
* @param string $name (optional) Plain text real name.
* @param int $id (optional) Moodle user ID.
*
* @return object Moodle userinfo.
*/
private function makeemailuser($email, $name = '', $id = -99) {
$emailuser = new stdClass();
$emailuser->email = trim(filter_var($email, FILTER_SANITIZE_EMAIL) ?? '');
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
$emailuser->email = '';
}
$emailuser->firstname = format_text($name, FORMAT_PLAIN, ['trusted' => false]);
$emailuser->lastname = '';
$emailuser->maildisplay = true;
$emailuser->mailformat = 1; // 0 (zero) text-only emails, 1 (one) for HTML emails.
$emailuser->id = $id;
$emailuser->firstnamephonetic = '';
$emailuser->lastnamephonetic = '';
$emailuser->middlename = '';
$emailuser->alternatename = '';
$emailuser->username = '';
return $emailuser;
}
/**
* Send email message and optionally autorespond.
*
* @param string $email Recipient's Email address.
* @param string $name Recipient's real name in plain text.
* @param boolean $sendconfirmationemail Set to true to also send an autorespond confirmation email back to user (TODO).
*
* @return boolean $status - True if message was successfully sent, false if not.
*/
public function sendmessage($email, $name, $sendconfirmationemail = false) {
global $USER, $CFG, $SITE;
$systemcontext = context_system::instance();
// Create the sender from the submitted name and email address.
$from = $this->makeemailuser($this->fromemail, $this->fromname);
// Create the recipient.
$to = $this->makeemailuser($email, $name);
// Create the Subject for message.
$subject = '';
if (empty(get_config('local_contact', 'nosubjectsitename'))) { // Not checked.
// Include site name in subject field.
$subject .= '[' . format_string($SITE->shortname, true, ['escape' => false, 'context' => $systemcontext]) . '] ';
}
$subject .= optional_param(
get_string('field-subject', 'local_contact'),
get_string('defaultsubject', 'local_contact'),
PARAM_TEXT
);
// Build the body of the email using user-entered information.
// Note: Name of message field is defined in the language pack.
$fieldmessage = get_string('field-message', 'local_contact');
$htmlmessage = '';
/**
* Callback function for array_filter.
*
* @param string $string Text to be chekced.
* @return boolean true if string is not empty, otherwise false.
*/
function filterempty($string) {
$string = trim($string ?? '');
return ($string !== null && $string !== false && $string !== '');
}
foreach ($_POST as $key => $value) {
// Only process key conforming to valid form field ID/Name token specifications.
if (preg_match('/^[A-Za-z][A-Za-z0-9_:\.-]*/', $key)) {
if (is_array($value)) {
// Join array of values. Example: <select multiple>.
$value = array_filter($value, "filterempty");
$value = join(', ', $value);
}
$value = (!empty($value) ? trim($value) : '');
// Exclude fields we don't want in the message and empty fields.
if (!in_array($key, ['sesskey', 'submit']) && $value != '') {
// Apply minor formatting of the key by replacing underscores with spaces.
$key = str_replace('_', ' ', $key);
// Make custom alterations.
switch ($key) {
case 'message':
// Message field - use translated value from language file.
$key = $fieldmessage;
// Continue checking for more issues to fix.
case strpos($value, "\n") !== false:
// Field contains linefeeds.
case $fieldmessage: // Message field.
// Strip out excessive empty lines.
$value = preg_replace('/\n(\s*\n){2,}/', "\n\n", $value);
// Sanitize the text.
$value = format_text($value, FORMAT_PLAIN, ['trusted' => false]);
// Add to email message.
$htmlmessage .= '<p><strong>' . ucfirst($key) . ' :</strong></p><p>' . $value . '</p>';
break;
// Don't include the following fields in the body of the message.
case 'recipient':
// Recipient field.
case 'recaptcha challenge field':
// ReCAPTCHA related field.
case 'recaptcha response field':
// ReCAPTCHA related field.
case 'g-recaptcha-response':
// ReCAPTCHA related field.
break;
// Use language translations for the labels of the following fields.
case 'name':
// Name field.
case 'email':
// Email field.
case 'subject':
// Subject field.
$key = get_string('field-' . $key, 'local_contact');
// Continue processing.
default:
// All other fields.
// Sanitize the text.
$value = format_text($value, FORMAT_PLAIN, ['trusted' => false]);
if (filter_var($value, FILTER_VALIDATE_URL)) {
// Convert URL into clickable link.
$value = '<a href="' . $value . '">' . $value . '</a>';
}
// Add to email message.
$htmlmessage .= '<strong>' . ucfirst($key) . ' :</strong> ' . $value . '<br>' . PHP_EOL;
}
}
}
}
$attachname = '';
$attachpath = '';
// If support for an attachement is enabled.
$supportattachments = !empty(get_config('local_contact', 'attachment'));
if ($supportattachments) {
// Take the first file as the attachment.
foreach ($_FILES as $value) {
$attachname = $value['name'];
$path = $CFG->tempdir . '/local_contact/';
if (!is_dir($path)) {
mkdir($path); // Create temp directory if it does not exist.
}
$attachpath = tempnam($path, 'attachment_');
move_uploaded_file($value['tmp_name'], $attachpath);
break;
}
}
// Sanitize user agent and referer.
$httpuseragent = format_text($_SERVER['HTTP_USER_AGENT'], FORMAT_PLAIN, ['trusted' => false]);
$httpreferer = format_text($_SERVER['HTTP_REFERER'], FORMAT_PLAIN, ['trusted' => false]);
// Prepare arrays to handle substitution of embedded tags in the footer.
$tags = [
'[fromname]',
'[fromemail]',
'[supportname]',
'[supportemail]',
'[lang]',
'[userip]',
'[userstatus]',
'[sitefullname]',
'[siteshortname]',
'[siteurl]',
'[http_user_agent]',
'[http_referer]',
];
$info = [
$from->firstname,
$from->email,
$CFG->supportname,
$CFG->supportemail,
current_language(),
getremoteaddr(),
$this->moodleuserstatus($from->email),
format_text($SITE->fullname, FORMAT_HTML, ['context' => $systemcontext, 'escape' => false]) . ': ',
format_text($SITE->shortname, FORMAT_HTML, ['context' => $systemcontext, 'escape' => false]),
$CFG->wwwroot,
$httpuseragent,
$httpreferer,
];
// Create the footer - Add some system information.
$footmessage = get_string('extrainfo', 'local_contact');
$footmessage = format_text($footmessage, FORMAT_HTML, ['trusted' => true, 'noclean' => true, 'para' => false]);
$htmlmessage .= str_replace($tags, $info, $footmessage);
// Override "from" email address if one was specified in the plugin's settings.
$noreplyaddress = $CFG->noreplyaddress;
if (!empty($customfrom = get_config('local_contact', 'senderaddress'))) {
$CFG->noreplyaddress = $customfrom;
}
// Send email message to recipient and set replyto to the sender's email address and name.
if (empty(get_config('local_contact', 'noreplyto'))) { // Not checked.
$status = email_to_user(
$to,
$from,
$subject,
html_to_text($htmlmessage),
$htmlmessage,
$attachpath,
$attachname,
true,
$from->email,
$from->firstname
);
} else { // Checked.
$status = email_to_user($to, $from, $subject, html_to_text($htmlmessage), $htmlmessage, $attachpath, $attachname, true);
}
$CFG->noreplyaddress = $noreplyaddress;
// If successful and a confirmation email is desired, send it the original sender.
if ($status && $sendconfirmationemail) {
// Substitute embedded tags for some information.
$htmlmessage = str_replace($tags, $info, get_string('confirmationemail', 'local_contact'));
$htmlmessage = format_text($htmlmessage, FORMAT_HTML, ['trusted' => true, 'noclean' => true, 'para' => false]);
$replyname = empty($CFG->emailonlyfromnoreplyaddress) ? $CFG->supportname : get_string('noreplyname');
$replyemail = empty($CFG->emailonlyfromnoreplyaddress) ? $CFG->supportemail : $CFG->noreplyaddress;
$to = $this->makeemailuser($replyemail, $replyname);
// Send confirmation email message to the sender.
email_to_user($from, $to, $subject, html_to_text($htmlmessage), $htmlmessage, '', '', true);
}
return $status;
}
/**
* Builds a one line status report on the user. Uses their Moodle info, if
* logged in, or their email address to look up the information if they are
* not.
*
* @param string $emailaddress Plain text email address.
*
* @return string Contains what we know about the Moodle user including whether they are logged in or out.
*/
private function moodleuserstatus($emailaddress) {
if (isloggedin() && !isguestuser()) {
global $USER;
$info = get_string('fullnamedisplay', null, $USER) . ' / ' . $USER->email . ' (' . $USER->username .
' / ' . get_string('eventuserloggedin', 'auth') . ')';
} else {
global $DB;
$usercount = $DB->count_records('user', ['email' => $emailaddress, 'deleted' => 0]);
switch ($usercount) {
case 0: // We don't know this email address.
$info = get_string('emailnotfound');
break;
case 1: // We found exactly one match.
$user = get_complete_user_data('email', $emailaddress);
$extrainfo = '';
// Is user locked out?
if ($lockedout = get_user_preferences('login_lockout', 0, $user)) {
$extrainfo .= ' / ' . get_string('lockedout', 'local_contact');
}
// Has user responded to confirmation email?
if (empty($user->confirmed)) {
$extrainfo .= ' / ' . get_string('notconfirmed', 'local_contact');
}
$info = get_string('fullnamedisplay', null, $user) . ' / ' . $user->email . ' (' . $user->username .
' / ' . get_string('eventuserloggedout') . $extrainfo . ')';
break;
default: // We found multiple users with this email address.
$info = get_string('duplicateemailaddresses', 'local_contact');
}
}
return $info;
}
}