Skip to content

Commit

Permalink
Merge branch 'develop' into fix/207-fix
Browse files Browse the repository at this point in the history
  • Loading branch information
chris-bateman authored Jul 10, 2024
2 parents 14bf4f4 + 4a98a87 commit 12d64b6
Show file tree
Hide file tree
Showing 13 changed files with 177 additions and 78 deletions.
7 changes: 6 additions & 1 deletion system/classes/HtmlBootstrap5.php
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,12 @@ public static function multiColForm($data, $action = null, $method = "POST", $su
break;
}
if ((property_exists($field, "type") && $field->type !== "hidden") || !property_exists($field, "type")) {
$buffer .= '<div class="col"><label class="' . $label_class . '"' . (property_exists($field, 'id') && !empty($field->id) ? ' for="' . $field->id . '"' : '') . '>' . $field->label . ($field->required ? " <small>Required</small>" : "") . "</label>"
$buffer .= '<div class="col"><label class="' . $label_class . '"'
. (property_exists($field, 'id') && !empty($field->id) ? ' for="' . $field->id . '"' : '')
. '>'
. $field->label
. (property_exists($field, "required") && $field->required ? " <small>Required</small>" : "")
. "</label>"
. $field->__toString() . '</div>';
} else {
$buffer .= $field->__toString();
Expand Down
5 changes: 0 additions & 5 deletions system/classes/MenuLinkStruct.php
Original file line number Diff line number Diff line change
@@ -1,10 +1,5 @@
<?php

enum MenuLinkType {
case Link;
case Modal;
}

class MenuLinkStruct {
public function __construct(
public string $title,
Expand Down
6 changes: 6 additions & 0 deletions system/classes/MenuLinkType.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
<?php

enum MenuLinkType {
case Link;
case Modal;
}
16 changes: 11 additions & 5 deletions system/modules/admin/actions/userdel.php
Original file line number Diff line number Diff line change
@@ -1,12 +1,18 @@
<?php
function userdel_GET(Web $w) {
$w->pathMatch("id");
$user = AuthService::getInstance($w)->getObject("User",$w->ctx("id"));
$user = AuthService::getInstance($w)->getObject("User", $w->ctx("id"));
if ($user) {
$user->delete();
$w->msg("User ".$user->login." deleted.","/admin/users");

if ($w->session('user_id') == $w->ctx("id")) {
// We deleted our own user, force logout
$w->sessionDestroy();
$w->redirect($w->localUrl("/auth/login"));
} else {
$w->msg("User " . $user->login . " deleted.", "/admin/users");
}
} else {
$w->error("User ".$w->ctx("id")." does not exist.","/admin/users");
$w->error("User " . $w->ctx("id") . " does not exist.", "/admin/users");
}

}
}
114 changes: 61 additions & 53 deletions system/modules/search/templates/index.tpl.php
Original file line number Diff line number Diff line change
@@ -1,74 +1,82 @@
<div>
<h3 class="subheading columns large-6">Search</h3>
<span class="columns large-6" style="text-align: right;">
<p style="font-size: 12px;">
<strong>Note:</strong> Search terms must contain minimum 3 characters.
<br>
<strong>Tip:</strong> To search by Id, use 'id##' eg. id5.
</p>
</span>
<h3 class="subheading columns large-6">Search</h3>
<span class="columns large-6" style="text-align: right;">
<p style="font-size: 12px;">
<strong>Note:</strong> Search terms must contain minimum 3 characters.
<br>
<strong>Tip:</strong> To search by Id, use 'id##' eg. id5.
</p>
</span>
</div>
<hr>
<div class="row-fluid">
<!-- <form action="<?php // echo $webroot; ?>/search/results" method="GET">-->
<form id="search_form" class="clearfix">
<input type="hidden" name="<?php echo CSRF::getTokenID(); ?>" value="<?php echo CSRF::getTokenValue(); ?>" />
<div class="row-fluid">
<div class="small-12 medium-6 columns">
<input class="input-large" type="text" name="q" id="q" autofocus/>
</div>
<!-- <form action="<?php // echo $webroot;
?>/search/results" method="GET">-->
<form id="search_form" class="clearfix">
<input type="hidden" name="<?php echo CSRF::getTokenID(); ?>" value="<?php echo CSRF::getTokenValue(); ?>" />
<div class="row-fluid">
<div class="small-12 medium-6 columns">
<input class="input-large" type="text" name="q" id="q" autofocus />
</div>
<div class="small-12 medium-2 columns">
<?php echo Html::select("idx", $indexes); ?>
</div>
<div class="small-12 medium-2 columns">
<?php echo Html::select("tags", $tags); ?>
</div>
<div class="small-12 medium-2 columns">
<button class="button tiny small-12" type="submit">Go</button>
</div>
</div>
</form>
<?php echo Html::select("idx", $indexes); ?>
</div>
<div class="small-12 medium-2 columns">
<?php echo Html::select("tags", $tags); ?>
</div>
<div class="small-12 medium-2 columns">
<button class="button tiny small-12" type="submit">Go</button>
</div>
</div>
</form>


</div>

<div id="search_message" class="row hide">
<div data-alert class="alert-box warning" id="message_box"></div>
<div id="search_message" class="row">
<div data-alert style="margin-top: 1rem" class="alert-box warning" id="message_box"></div>
</div>

<div id="result" class="row" style="display: none;">

</div>

<script>
$("#search_form").submit(function(event) {
event.preventDefault();
$("#search_message").hide();
$("#result").hide();
const setError = (str) => {
if (!str) {
document.querySelector("#search_message").style.display = "none";
document.querySelector("#result").style.display = "block";
return;
}

document.querySelector("#message_box").innerText = str;
document.querySelector("#search_message").style.display = "block";
document.querySelector("#result").style.display = "none";
}

document.querySelector("#search_form").addEventListener("submit", async function(event) {
event.preventDefault();

setError(false);

const form = new FormData(event.target);
const body = new URLSearchParams(form);

try {
const response = await fetch(`/search/results?` + body.toString());

var data = $("#search_form").serialize();
const json = await response.json();

$.getJSON("/search/results", data,
function(response) {
if (response.success === false) {
$("#message_box").html(response.data);
$("#search_message").show();
} else {
var text_data = "<span style='padding-left: 20px;'>No results found</span>";
if (response.data) {
text_data = response.data;
}
$("#result").html(text_data).delay(100).fadeIn();
}
},
function(response) {
$("#message_box").html("Failed to receive a response from search");
$("#search_message").show();
}
);
if (!json.success)
return setError(json.data);

return false;
});
document.querySelector("#result").innerHTML =
json.data || `<span style="padding-left: 20px;">No results found</span>`;
} catch (e) {
setError(`Failed to receive a response from search`);
}

return false;
});
</script>
<br>
<br>
22 changes: 21 additions & 1 deletion system/modules/timelog/actions/edit.php
Original file line number Diff line number Diff line change
Expand Up @@ -119,11 +119,31 @@ function edit_POST(Web $w)
$timelog->user_id = !empty($_POST['user_id']) ? intval($_POST['user_id']) : AuthService::getInstance($w)->user()->id;
}

// Check to see if timelog with same starting time already exists, and remove if duplicate
$timelogs_for_task = TimelogService::getInstance($w)->getTimelogsForObject($timelog->getLinkedObject());
$timelogs_for_task_and_user = [];
foreach ($timelogs_for_task as $existing_timelog) {
if ($existing_timelog->user_id == $timelog->user_id) {
$timelogs_for_task_and_user[] = $existing_timelog;
}
}

$dedupeInfo = "current( id( $timelog->user_id ) dt_start( " . substr($timelog->dt_start, 0, 10) . " " . substr($timelog->dt_start, 11, 5) . " ) )";

foreach ($timelogs_for_task_and_user as $existing_timelog_for_task_and_user) {

$dedupeInfo .= " existing( id( $existing_timelog_for_task_and_user->user_id ) dt_start( " . gmdate('Y-m-d', strtotime($existing_timelog_for_task_and_user->getDateStart() . ' ' . $existing_timelog_for_task_and_user->getTimeStart())) . " " . gmdate('H:i', strtotime($existing_timelog_for_task_and_user->getTimeStart())) . " ) )";

if (gmdate('Y-m-d', strtotime($existing_timelog_for_task_and_user->getDateStart() . ' ' . $existing_timelog_for_task_and_user->getTimeStart())) == substr($timelog->dt_start, 0, 10) && gmdate('H:i', strtotime($existing_timelog_for_task_and_user->getTimeStart())) == substr($timelog->dt_start, 11, 5)) {
$w->error('Duplicate Timelog Removed', $redirect ?: '/timelog');
}
}

// Timelog user_id handled in insert/update
$timelog->insertOrUpdate();

// Save comment
$timelog->setComment($_POST['description']);

$w->msg("<div id='saved_record_id' data-id='" . $timelog->id . "' >Timelog saved</div>", (!empty($redirect) ? $redirect . "#timelog" : "/timelog"));
$w->msg("<div id='saved_record_id' data-id='" . $timelog->id . "' >Timelog saved<!--$dedupeInfo--></div>", (!empty($redirect) ? $redirect . "#timelog" : "/timelog"));
}
Original file line number Diff line number Diff line change
Expand Up @@ -56,4 +56,45 @@ test("You can create a Timelog using Add Timelog" , async ({page}) => {
await TimelogHelper.deleteTimelog(page, timelog, task, taskID);
await TaskHelper.deleteTask(page, task, taskID);
await TaskHelper.deleteTaskGroup(page, taskgroup, taskgroupID);
});
});

test("Test that duplicate timelogs are deleted" , async ({page}) => {
test.setTimeout(GLOBAL_TIMEOUT);
CmfiveHelper.acceptDialog(page);

await CmfiveHelper.login(page, "admin", "admin");

const taskgroup = CmfiveHelper.randomID("taskgroup_");
const taskgroupID = await TaskHelper.createTaskGroup(page, taskgroup, "Software Development", "OWNER", "OWNER", "OWNER");
await TaskHelper.addMemberToTaskgroup(page, taskgroup, taskgroupID, "admin admin", "OWNER");
await TaskHelper.setDefaultAssignee(page, taskgroup, taskgroupID, "admin admin");

const task = CmfiveHelper.randomID("task_");
const taskID = await TaskHelper.createTask(page, task, taskgroup, "Software Development");

const timelog1 = CmfiveHelper.randomID("timelog_");
await TimelogHelper.createTimelog(
page,
timelog1,
task,
taskID,
DateTime.fromFormat("6/6/2024", "d/M/yyyy"),
"1:00",
"2:00",
);
const timelog2 = CmfiveHelper.randomID("timelog_");
await TimelogHelper.createTimelog(
page,
timelog2,
task,
taskID,
DateTime.fromFormat("6/6/2024", "d/M/yyyy"),
"1:00",
"2:00",
true
);

await TimelogHelper.deleteTimelog(page, timelog1, task, taskID);
await TaskHelper.deleteTask(page, task, taskID);
await TaskHelper.deleteTaskGroup(page, taskgroup, taskgroupID);
});
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ export class TimelogHelper {
await expect(page.getByText(timelog)).toBeVisible();
}

static async createTimelog(page: Page, timelog: string, taskName: string, taskID: string, date: DateTime, start_time: string, end_time: string)
static async createTimelog(page: Page, timelog: string, taskName: string, taskID: string, date: DateTime, start_time: string, end_time: string, check_duplicate: boolean = false)
{
if(page.url() != HOST + "/task/edit/" + taskID + "#details") {
if(page.url() != HOST + "/task/tasklist")
Expand All @@ -54,10 +54,17 @@ export class TimelogHelper {
await page.getByLabel("Description", {exact: true}).fill(timelog);
await page.locator("#timelog_edit_form").getByRole("button", { name: "Save" }).click();

await page.getByRole("link", {name: taskName, exact: true}).first().click();
if(await page.$("#saved_record_id") != null)
console.log(await page.$eval("#saved_record_id", el => el.innerHTML));

await page.getByRole("link", {name: taskName, exact: false}).first().click();
await page.getByRole("link", {name: "Time Log"}).click();
await page.reload();
await expect(page.getByText(timelog)).toBeVisible();

if (check_duplicate)
await expect(page.getByText(timelog)).toBeHidden();
else
await expect(page.getByText(timelog)).toBeVisible();
}

static async editTimelog(page: Page, timelog: string, taskName: string, taskID: string, date: DateTime, start_time: string, end_time: string)
Expand Down Expand Up @@ -97,4 +104,4 @@ export class TimelogHelper {
await page.reload();
await expect(page.getByText(timelog)).not.toBeVisible();
}
}
}
7 changes: 3 additions & 4 deletions system/modules/tokens/models/ApiOutputService.php
Original file line number Diff line number Diff line change
Expand Up @@ -36,10 +36,9 @@ public function apiReturnJsonResponse($response)
{
$this->w->setLayout(null);
http_response_code($response['status']);
// mark header for return content type JSON
if (substr($response['status'], 0, 1) == "2") {
header('Content-Type: application/json');
} else {
header('Content-Type: application/json');

if (substr($response['status'], 0, 1) != "2") {
LogService::getInstance($this->w)->info("API request rejected: " . $response['referer']);
// Don't need, already have set response code!
// header($_SERVER["SERVER_PROTOCOL"] . " " . $response['status'] . " " . $response['payload'][0]);
Expand Down
9 changes: 8 additions & 1 deletion system/templates/base/src/js/app.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
// src/app.ts
import { AlertAdaptation, DropdownAdaptation, FavouritesAdaptation, TabAdaptation, TableAdaptation } from './adaptations';
import { QuillEditor, InputWithOther, MultiFileUpload, MultiSelect, Overlay, CodeMirror } from './components';
import { CodeMirror, InputWithOther, MultiFileUpload, MultiSelect, Overlay, QuillEditor } from './components';

import { Modal, Toast, Tooltip } from 'bootstrap';

Expand Down Expand Up @@ -97,6 +97,13 @@ class Cmfive {
return response.text()
}).then((content) => {
modalContent.innerHTML = content + modalContent.innerHTML;

// https://developer.mozilla.org/en-US/docs/Web/API/Element/innerHTML#security_considerations
// Appending scripts to the DOM via innerHTML is not meant to execute them for security purposes
// Unfortunately, various modals however contian script tags we need to execute
modalContent.querySelectorAll("script").forEach(x => {
eval(x.innerHTML);
})

// Rebind elements for modal
Cmfive.ready(modalContent);
Expand Down
4 changes: 4 additions & 0 deletions system/templates/base/src/scss/cmfive/_forms.scss
Original file line number Diff line number Diff line change
Expand Up @@ -50,4 +50,8 @@ form {
width: 100%;
}
}

input.input-large {
width: 100%;
}
}
2 changes: 1 addition & 1 deletion system/templates/layout-bootstrap-5.tpl.php
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@
if (is_string($module_nav)) : ?>
<li class="nav-item"><?php echo $module_nav; ?></li>
<?php else: ?>
<li class="nav-item"><a <?php echo $module_nav->type == MenuLinkType::Modal ? 'data-modal-target' : 'link'; ?>="<?php echo $module_nav->url; ?>"><?php echo $module_nav->title; ?></a></li>
<li class="nav-item"><a <?php echo $module_nav->type == MenuLinkType::Modal ? 'data-modal-target' : 'href'; ?>="<?php echo $module_nav->url; ?>"><?php echo $module_nav->title; ?></a></li>
<?php endif;
endforeach; ?>
</ul>
Expand Down
5 changes: 3 additions & 2 deletions system/templates/vue-components/profile-security.vue.js
Original file line number Diff line number Diff line change
Expand Up @@ -122,8 +122,9 @@ Vue.component('profile-security', {
return;
}

_this.mfa_qr_code_url = response.data.qr_code;
_this.mfa_secret = response.data.mfa_secret;
const { data } = response.data;
_this.mfa_qr_code_url = data.qr_code;
_this.mfa_secret = data.mfa_secret;
}).catch(function (error) {
(new Toast("Failed to fetch QR Code")).show();
console.log(error);
Expand Down

0 comments on commit 12d64b6

Please sign in to comment.