diff --git a/application/Controller/AdminAdvancedController.php b/application/Controller/AdminAdvancedController.php index 75cef84e..8e57c8fc 100644 --- a/application/Controller/AdminAdvancedController.php +++ b/application/Controller/AdminAdvancedController.php @@ -70,6 +70,29 @@ public function ajaxAdminAction() { $this->response->setJsonContent(['success' => true]); return $this->sendResponse(); } + + if ($request->verify_email_manually) { + $user = (new User())->refresh(['id' => (int)$request->user_id, 'email' => $request->user_email]); + + $user->setVerified($request->user_email); + + alert('User email address has been manually set to verified.', 'alert-success'); + $this->response->setContentType('application/json'); + $this->response->setJsonContent(['success' => true]); + return $this->sendResponse(); + } + + if ($request->add_default_email_access) { + $user = (new User())->refresh(['id' => (int)$request->user_id, 'email' => $request->user_email]); + + $user->enableDefaultEmailaccount(); + + alert('User has been given access to the default email sending account.', 'alert-success'); + $this->response->setContentType('application/json'); + $this->response->setJsonContent(['success' => true]); + return $this->sendResponse(); + } + } private function setAdminLevel($user_id, $level) { diff --git a/application/Model/EmailAccount.php b/application/Model/EmailAccount.php index a56c95d7..907110b8 100644 --- a/application/Model/EmailAccount.php +++ b/application/Model/EmailAccount.php @@ -58,6 +58,7 @@ public function changeSettings($posted) { 'port' => $this->account['port'], 'tls' => $this->account['tls'], 'username' => $this->account['username'], + 'reply_to' => $this->account['reply_to'], 'password' => $old_password, ); @@ -72,7 +73,7 @@ public function changeSettings($posted) { $params['password'] = ''; $query = "UPDATE `survey_email_accounts` - SET `from` = :fromm, `from_name` = :from_name, `host` = :host, `port` = :port, `tls` = :tls, `username` = :username, `password` = :password, `auth_key` = :auth_key, `status` = 0 + SET `from` = :fromm, `from_name` = :from_name, `host` = :host, `port` = :port, `tls` = :tls, `username` = :username, `reply_to` = :reply_to, `password` = :password, `auth_key` = :auth_key, `status` = 0 WHERE id = :id LIMIT 1"; $this->db->exec($query, $params); @@ -122,7 +123,12 @@ public function makeMailer() { } $mail->From = $this->account['from']; $mail->FromName = $this->account['from_name']; - $mail->AddReplyTo($this->account['from'], $this->account['from_name']); + if(!empty($this->account['reply_to'])) { + $mail->AddReplyTo($this->account['reply_to'], $this->account['from_name']); + } else { + $mail->AddReplyTo($this->account['from'], $this->account['from_name']); + } + $mail->CharSet = "utf-8"; $mail->WordWrap = 65; // set word wrap to 65 characters if (is_array(Config::get('email.smtp_options'))) { diff --git a/application/Model/Item/Calculate.php b/application/Model/Item/Calculate.php index 8833027b..02f79589 100644 --- a/application/Model/Item/Calculate.php +++ b/application/Model/Item/Calculate.php @@ -5,7 +5,7 @@ class Calculate_Item extends Item { public $type = 'calculate'; public $input_attributes = array('type' => 'hidden'); public $no_user_input_required = true; - public $mysql_field = 'TEXT DEFAULT NULL'; + public $mysql_field = 'MEDIUMTEXT DEFAULT NULL'; public function render() { return $this->render_input(); diff --git a/application/Model/Item/Hidden.php b/application/Model/Item/Hidden.php index b3ab6b79..1598cb2f 100644 --- a/application/Model/Item/Hidden.php +++ b/application/Model/Item/Hidden.php @@ -3,7 +3,7 @@ class Hidden_Item extends Item { public $type = 'hidden'; - public $mysql_field = 'TEXT DEFAULT NULL'; + public $mysql_field = 'MEDIUMTEXT DEFAULT NULL'; public $input_attributes = array('type' => 'hidden'); public $optional = 1; diff --git a/application/Model/Run.php b/application/Model/Run.php index 4f592b35..ec2dce65 100644 --- a/application/Model/Run.php +++ b/application/Model/Run.php @@ -222,74 +222,123 @@ public function getUploadedFiles() { ->fetchAll(); } - public $file_endings = array( - 'image/jpeg' => '.jpg', 'image/png' => '.png', 'image/gif' => '.gif', 'image/tiff' => '.tif', - 'video/mpeg' => '.mpg', 'video/quicktime' => '.mov', 'video/x-flv' => '.flv', 'video/x-f4v' => '.f4v', 'video/x-msvideo' => '.avi', - 'audio/mpeg' => '.mp3', - 'application/pdf' => '.pdf', - 'text/csv' => '.csv', 'text/css' => '.css', 'text/tab-separated-values' => '.tsv', 'text/plain' => '.txt' - ); - public function uploadFiles($files) { $max_size_upload = Config::get('admin_maximum_size_of_uploaded_files'); + $allowed_file_endings = Config::get('allowed_file_endings_for_run_upload'); + // make lookup array $existing_files = $this->getUploadedFiles(); $files_by_names = array(); foreach ($existing_files as $existing_file) { $files_by_names[$existing_file['original_file_name']] = $existing_file['new_file_path']; } - + + // Generate a random directory name for this batch + $batch_directory = 'assets/tmp/admin/' . crypto_token(15, true) . '/'; + + // Ensure the batch directory exists + $destination_dir = APPLICATION_ROOT . 'webroot/' . $batch_directory; + if (!is_dir($destination_dir)) { + mkdir($destination_dir, 0777, true); + } + // loop through files and modify them if necessary for ($i = 0; $i < count($files['tmp_name']); $i++) { - // validate if any error occured on upload + // validate if any error occurred on upload if ($files['error'][$i]) { - $this->errors[] = __("An error occured uploading file '%s'. ERROR CODE: PFUL-%d", $files['name'][$i], $files['error'][$i]); + $this->errors[] = __("An error occurred uploading file '%s'. ERROR CODE: PFUL-%d", $files['name'][$i], $files['error'][$i]); continue; } - + // validate file size $size = (int) $files['size'][$i]; if (!$size || ($size > $max_size_upload * 1048576)) { $this->errors[] = __("The file '%s' is too big or the size could not be determined. The allowed maximum size is %d megabytes.", $files['name'][$i], round($max_size_upload, 2)); continue; } - - // validate mime type + + // validate mime type and file ending $finfo = new finfo(FILEINFO_MIME_TYPE); $mime = $finfo->file($files['tmp_name'][$i]); - if (!isset($this->file_endings[$mime])) { - $this->errors[] = __('The file "%s" has the MIME type %s and is not allowed to be uploaded.', $files['name'][$i], $mime); + $original_file_name = $files['name'][$i]; + $file_extension = pathinfo($original_file_name, PATHINFO_EXTENSION); + + // Adjust validation for ambiguous types + if ($mime == 'text/plain' || $mime == "text/x-asm") { + // Add additional cases for other ambiguous MIME types + switch ($file_extension) { + case 'css': + $mime = 'text/css'; + break; + case 'js': + $mime = 'text/javascript'; + break; + case 'svg': + $mime = 'image/svg+xml'; + break; + case 'html': + $mime = 'text/html'; + break; + case 'xml': + $mime = 'application/xml'; + break; + case 'md': + $mime = 'text/markdown'; + break; + case 'yaml': + case 'yml': + $mime = 'application/x-yaml'; + break; + case 'json': + $mime = 'application/json'; + break; + case 'rtf': + $mime = 'application/rtf'; + break; + case 'php': + $mime = 'application/x-httpd-php'; + break; + case 'sh': + $mime = 'application/x-sh'; + break; + } + } + + if (!isset($allowed_file_endings[$mime]) || $allowed_file_endings[$mime] !== $file_extension) { + $this->errors[] = __('The file "%s" has an invalid file extension %s. Expected %s for MIME type %s.', $original_file_name, $file_extension, $allowed_file_endings[$mime], $mime); continue; } - - // validation was OK - $original_file_name = $files['name'][$i]; - if (isset($files_by_names[$original_file_name])) { - // override existing path - $new_file_path = $files_by_names[$original_file_name]; - $this->messages[] = __('The file "%s" was overriden.', $original_file_name); - } else { - $new_file_path = 'assets/tmp/admin/' . crypto_token(33, true) . $this->file_endings[$mime]; + + // Sanitize file name to remove control characters + $sanitized_file_name = preg_replace('/[\x00-\x1F\x7F]/u', '', $original_file_name); // Remove control characters + $new_file_path = $batch_directory . $sanitized_file_name; + + // Ensure the destination path is within the intended directory + $intended_path = $destination_dir . $sanitized_file_name; + if (strpos(realpath(dirname($intended_path)), realpath($destination_dir)) !== 0) { + $this->errors[] = __("The file '%s' could not be uploaded due to an invalid file path.", $sanitized_file_name); + continue; } // save file - $destination_dir = APPLICATION_ROOT . 'webroot/' . $new_file_path; - if (move_uploaded_file($files['tmp_name'][$i], $destination_dir)) { + if (move_uploaded_file($files['tmp_name'][$i], $destination_dir . $original_file_name)) { $this->db->insert_update('survey_uploaded_files', array( 'run_id' => $this->id, 'created' => mysql_now(), 'original_file_name' => $original_file_name, 'new_file_path' => $new_file_path, - ), array( + ), array( 'modified' => mysql_now() )); + $this->messages[] = __('The file "%s" was successfully uploaded.', $original_file_name); } else { $this->errors[] = __("Unable to move uploaded file '%s' to storage location.", $files['name'][$i]); } } - + return empty($this->errors); } + public function deleteFile($id, $filename) { $where = array('id' => (int) $id, 'original_file_name' => $filename); diff --git a/application/Model/User.php b/application/Model/User.php index 9d17bfd0..694b089e 100644 --- a/application/Model/User.php +++ b/application/Model/User.php @@ -295,6 +295,36 @@ public function changeData($password, $data) { return true; } + public function enableDefaultEmailaccount() { + $default_email = Config::get('default_admin_email'); + + if ($default_email !== null && $default_email['host'] !== null) { + // Create a new EmailAccount instance + $emailAccount = new EmailAccount(null, $this->id); + + // Prepare the settings array + $settings = array( + 'from' => $default_email['from'], + 'from_name' => $default_email['from_name'], + 'host' => $default_email['host'], + 'port' => $default_email['port'], + 'tls' => $default_email['tls'], + 'username' => $default_email['username'], + 'password' => $default_email['password'], + 'reply_to' => $this->email + ); + + // Create the email account + $emailAccount->create(); + + // Change settings to the new default email configuration + $emailAccount->changeSettings($settings); + $emailAccount->validate(); + alert("You have been set up with an email account to programmatically send emails from {$default_email['from']}!", 'alert-success'); + } + } + + public function resetPassword($info) { $email = array_val($info, 'email'); $token = array_val($info, 'reset_token'); @@ -328,26 +358,35 @@ public function resetPassword($info) { return false; } - public function verifyEmail($email, $token) { + public function setVerified($email) { + $this->db->update('survey_users', array('email_verification_hash' => null, 'email_verified' => 1), array('email' => $email), array('int', 'int')); + alert('Your email was successfully verified!', 'alert-success'); + $verify_data = $this->db->findRow('survey_users', array('email' => $email), array('email_verification_hash', 'referrer_code')); - if (!$verify_data) { - alert('Incorrect token or email address.', 'alert-danger'); - return false; - } - if (password_verify($token, (string)$verify_data['email_verification_hash'])) { - $this->db->update('survey_users', array('email_verification_hash' => null, 'email_verified' => 1), array('email' => $email), array('int', 'int')); - alert('Your email was successfully verified!', 'alert-success'); + if (in_array($verify_data['referrer_code'], Config::get('referrer_codes'))) { + $this->db->update('survey_users', array('admin' => 1), array('email' => $email)); + $this->id = (int) $this->db->findValue('survey_users', array('email' => $email), array('id')); + $this->load(); + $this->enableDefaultEmailaccount(); - if (in_array($verify_data['referrer_code'], Config::get('referrer_codes'))) { - $this->db->update('survey_users', array('admin' => 1), array('email' => $email)); - alert('You now have the rights to create your own studies!', 'alert-success'); - } - return true; + alert('You now have the rights to create your own studies!', 'alert-success'); } else { alert('Your email verification token was invalid or oudated. Please try copy-pasting the link in your email and removing any spaces.', 'alert-danger'); return false; } + return true; + } + + public function verifyEmail($email, $token) { + $verify_data = $this->db->findRow('survey_users', array('email' => $email), array('email_verification_hash')); + if (!$verify_data) { + alert('Incorrect token or email address.', 'alert-danger'); + return false; + } + if (password_verify($token, (string)$verify_data['email_verification_hash'])) { + return $this->setVerified($email); + } } public function resendVerificationEmail($verificationHash) { diff --git a/bin/add_user.php b/bin/add_user.php index 8f7fda6d..eabcc358 100644 --- a/bin/add_user.php +++ b/bin/add_user.php @@ -27,8 +27,8 @@ print("$users exist already"); -if($config['level'] > 0 && $users > 0) { - throw new Exception("Cannot create admins when users already exist"); +if($config['level'] > 1 && $users > 0) { + print("Cannot create superadmins when users already exist"); } $inserted = $db->insert('survey_users', array( diff --git a/config-dist/settings.php b/config-dist/settings.php index 70f2ce45..6b2e8f54 100644 --- a/config-dist/settings.php +++ b/config-dist/settings.php @@ -43,7 +43,8 @@ 'r_lib_path' => '/usr/local/lib/R/site-library' ); -// email SMTP and queueing configuration +// email SMTP and queueing configuration for emails sent by the formr app itself +// for example for email confirmation and password reset $settings['email'] = array( 'host' => 'smtp.example.com', 'port' => 587, @@ -66,6 +67,18 @@ 'smtp_options' => array(), ); +// email SMTP and queueing configuration for emails sent by formr admins in studies +// maybe not be the same as the formr app email +$settings['default_admin_email'] = array( + 'host' => NULL, + 'port' => NULL, + 'tls' => true, + 'from' => NULL, + 'from_name' => NULL, + 'username' => NULL, + 'password' => NULL +); + // should PHP and MySQL errors be displayed to the users when formr is not running locally? If 0, they are only logged $settings['display_errors_when_live'] = 0; $settings['display_errors'] = 0; @@ -83,6 +96,25 @@ // Maximum size allowed for uploaded files in MB $settings['admin_maximum_size_of_uploaded_files'] = 50; +$settings['allowed_file_endings_for_run_upload'] = array( + 'image/jpeg' => 'jpg', + 'image/png' => 'png', + 'image/gif' => 'gif', + 'image/tiff' => 'tif', + 'video/mpeg' => 'mpg', + 'video/quicktime' => 'mov', + 'video/x-flv' => 'flv', + 'video/x-f4v' => 'f4v', + 'video/x-msvideo' => 'avi', + 'audio/mpeg' => 'mp3', + 'application/pdf' => 'pdf', + 'text/csv' => 'csv', + 'text/javascript' => 'js', + 'text/css' => 'css', + 'text/tab-separated-values' => 'tsv', + 'text/plain' => 'txt', + 'text/html' => 'html' +); // Directory for exported runs $settings['run_exports_dir'] = APPLICATION_ROOT . 'documentation/run_components'; diff --git a/sql/patches/037_mediumtexts.sql b/sql/patches/037_mediumtexts.sql new file mode 100644 index 00000000..b47e4992 --- /dev/null +++ b/sql/patches/037_mediumtexts.sql @@ -0,0 +1,4 @@ +ALTER TABLE `survey_items` CHANGE `label` `label` mediumtext COLLATE utf8mb4_unicode_ci; +ALTER TABLE `survey_run_sessions` CHANGE `session` `session` varchar(64) COLLATE utf8mb4_unicode_ci NOT NULL; +ALTER TABLE `survey_users` CHANGE `user_code` `user_code` varchar(64) COLLATE utf8mb4_unicode_ci NOT NULL; +ALTER TABLE `survey_email_accounts` ADD `reply_to` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL; diff --git a/templates/admin/account/login.php b/templates/admin/account/login.php index fe1b4f24..dc3c8c39 100755 --- a/templates/admin/account/login.php +++ b/templates/admin/account/login.php @@ -57,6 +57,8 @@
Forgot password? +
+ Sign Up diff --git a/templates/admin/account/register.php b/templates/admin/account/register.php index 003f3e69..74fef428 100755 --- a/templates/admin/account/register.php +++ b/templates/admin/account/register.php @@ -34,13 +34,13 @@
If you don't have a referral token, sign up first and then write us an &body== rawurlencode(Template::get('email/reg-account.ftpl')); ?>">email to ask for the right to create studies. If you have a token, you'll be able to create studies once you confirm your email address.
+If you have a valid token, you'll be able to create studies once you confirm your email address. If you don't have a valid token, sign up first and then write an email to this instance's administrator at = $support_email ?>.
+
Already signed up?
+ +Users would send requests to this email for admin accounts.
![image description for blind users](image link)
, so in a concrete example ![Picture of a guitar](https://formr.org/assets/tmp/admin/mkWpDTv5Um2ijGs1SJbH1uw9Bn2ctysD8N3tbkuwalOM.png)
. You can embed images anywhere you can use Markdown (e.g. in item and choice labels, feedback, emails).