Skip to content

Commit

Permalink
Merge branch 'release/v0.21.1'
Browse files Browse the repository at this point in the history
  • Loading branch information
rubenarslan committed Jun 1, 2024
2 parents e68b959 + 85326ee commit 138931b
Show file tree
Hide file tree
Showing 17 changed files with 284 additions and 57 deletions.
23 changes: 23 additions & 0 deletions application/Controller/AdminAdvancedController.php
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
10 changes: 8 additions & 2 deletions application/Model/EmailAccount.php
Original file line number Diff line number Diff line change
Expand Up @@ -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,
);

Expand All @@ -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);
Expand Down Expand Up @@ -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'))) {
Expand Down
2 changes: 1 addition & 1 deletion application/Model/Item/Calculate.php
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
2 changes: 1 addition & 1 deletion application/Model/Item/Hidden.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down
107 changes: 78 additions & 29 deletions application/Model/Run.php
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
65 changes: 52 additions & 13 deletions application/Model/User.php
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand Down Expand Up @@ -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) {
Expand Down
4 changes: 2 additions & 2 deletions bin/add_user.php
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
34 changes: 33 additions & 1 deletion config-dist/settings.php
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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;
Expand All @@ -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';
Expand Down
4 changes: 4 additions & 0 deletions sql/patches/037_mediumtexts.sql
Original file line number Diff line number Diff line change
@@ -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;
2 changes: 2 additions & 0 deletions templates/admin/account/login.php
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,8 @@
<button type="submit" class="btn btn-sup btn-material-pink btn-raised">Sign In</button>
<p>&nbsp;</p>
<a href="<?php echo admin_url('account/forgot-password'); ?>"><strong>Forgot password?</strong></a>
<p>&nbsp;</p>
<a href="<?php echo admin_url('account/register'); ?>" class="btn btn-sup btn-material-pink btn-raised">Sign Up</a>
</form>
</div>

Expand Down
Loading

0 comments on commit 138931b

Please sign in to comment.