Files
objecttothisandTravis Garrison 5c9b1b81e6 fix(xss): remove redundant escaping that double-encoded item attribute values (#4628)
* fix(xss): remove redundant escaping that double-encoded item attribute values

- Remove esc()/html_entity_decode() calls now that output is escaped
  at render time by the framework, preventing double-encoding of
  special characters in attribute names, units, and definition values
- Fix employee_name form_input value fields to stop pre-escaping
  before form_input applies its own escaping
- Reorder Items.php use statements and add missing BaseConnection import
- Change items/manage.php start_date from let to plain assignment for
  proper reassignment scope

Signed-off-by: Travis Garrison <travis@chiraqbookstore.com>

* test(sales): add regression tests for permission checks on sales endpoints

- Ensure role-based permissions correctly restrict access to sensitive actions like price edits, receipt/invoice views, and report generation.
- Add tests for both granted and restricted user scenarios to validate the behavior.

Signed-off-by: Travis Garrison <travis@chiraqbookstore.com>

* fix(attributes): validate `attribute_value` before processing

- Add checks to ensure `attribute_value` is a non-empty string in `postSaveAttributeValue` and `postDeleteDropdownAttributeValue` methods.
- Return error response if validation fails to prevent invalid data handling.

Signed-off-by: Travis Garrison <travis@chiraqbookstore.com>

* fix(attributes): improve error handling and optimize affected items processing

- Use `array_column` for extracting item IDs to streamline logic.
- Add JSON validation with `JSON_THROW_ON_ERROR` and return proper error response for invalid `definition_values`.

Signed-off-by: Travis Garrison <travis@chiraqbookstore.com>

* test(sales): enable database refresh for consistent test state

Signed-off-by: Travis Garrison <travis@chiraqbookstore.com>

* test(sales): assert unauthorized message is displayed on restricted access

Signed-off-by: Travis Garrison <travis@chiraqbookstore.com>

* refactor(attributes): use camelCase for `attributeValue` in controller methods

- Standardize variable naming in `postSaveAttributeValue` and `postDeleteDropdownAttributeValue` methods by switching to camelCase.

Signed-off-by: Travis Garrison <travis@chiraqbookstore.com>

---------

Signed-off-by: Travis Garrison <travis@chiraqbookstore.com>
Co-authored-by: Travis Garrison <travis@chiraqbookstore.com>
2026-08-09 11:37:38 +04:00

168 lines
7.6 KiB
PHP

<?php
/**
* @var array $definition_names
* @var array $definition_values
* @var int $item_id
* @var array $config
*/
?>
<div class="form-group form-group-sm">
<?= form_label(lang('Attributes.definition_name'), 'definition_name_label', ['class' => 'control-label col-xs-3']) ?>
<div class="col-xs-8">
<?= form_dropdown([
'name' => 'definition_name',
'options' => $definition_names,
'selected' => -1,
'class' => 'form-control',
'id' => 'definition_name'
]) ?>
</div>
</div>
<?php foreach ($definition_values as $definition_id => $definition_value) { ?>
<div class="form-group form-group-sm">
<?= form_label(esc($definition_value['definition_name']), esc($definition_value['definition_name']), ['class' => 'control-label col-xs-3']) ?>
<div class="col-xs-8">
<div class="input-group">
<?php
echo form_hidden("attribute_ids[$definition_id]", strval($definition_value['attribute_id']));
$attribute_value = $definition_value['attribute_value'];
switch ($definition_value['definition_type']) {
case DATE:
$value = (empty($attribute_value) || empty($attribute_value->attribute_date)) ? NOW : strtotime($attribute_value->attribute_date);
echo form_input([
'name' => "attribute_links[$definition_id]",
'value' => to_date($value),
'class' => 'form-control input-sm datetime',
'data-definition-id' => $definition_id,
'readonly' => 'true'
]);
break;
case DROPDOWN:
$selected_value = $definition_value['selected_value'];
echo form_dropdown([
'name' => "attribute_links[$definition_id]",
'options' => $definition_value['values'],
'selected' => $selected_value,
'class' => 'form-control',
'data-definition-id' => $definition_id
]);
break;
case TEXT:
$value = (empty($attribute_value) || empty($attribute_value->attribute_value)) ? $definition_value['selected_value'] : $attribute_value->attribute_value;
echo form_input([
'name' => "attribute_links[$definition_id]",
'value' => $value,
'class' => 'form-control valid_chars',
'data-definition-id' => $definition_id
]);
break;
case DECIMAL:
$value = (empty($attribute_value) || empty($attribute_value->attribute_decimal)) ? $definition_value['selected_value'] : $attribute_value->attribute_decimal;
echo form_input([
'name' => "attribute_links[$definition_id]",
'value' => to_decimals((float)$value),
'class' => 'form-control valid_chars',
'data-definition-id' => $definition_id
]);
break;
case CHECKBOX:
$value = (empty($attribute_value) || empty($attribute_value->attribute_value)) ? $definition_value['selected_value'] : $attribute_value->attribute_value;
// Sends 0 if the box is unchecked instead of not sending anything.
echo form_input([
'type' => 'hidden',
'name' => "attribute_links[$definition_id]",
'id' => "attribute_links[$definition_id]",
'value' => 0,
'data-definition-id' => $definition_id
]);
echo form_checkbox([
'name' => "attribute_links[$definition_id]",
'id' => "attribute_links[$definition_id]",
'value' => 1,
'checked' => $value == 1,
'class' => 'checkbox-inline',
'data-definition-id' => $definition_id
]);
break;
}
?>
<span class="input-group-addon input-sm btn btn-default remove_attribute_btn">
<span class="glyphicon glyphicon-trash"></span>
</span>
</div>
</div>
</div>
<?php } ?>
<script type="text/javascript">
(function() {
<?= view('partial/datepicker_locale', ['format' => dateformat_bootstrap($config['dateformat'])]) ?>
const enable_delete = function() {
$('.remove_attribute_btn').click(function() {
$(this).parents('.form-group').remove();
});
};
enable_delete();
$("input[name*='attribute_links']").change(function() {
const definition_id = $(this).data('definition-id');
$("input[name='attribute_ids[" + definition_id + "]']").val('');
}).autocomplete({
source: function(request, response) {
$.get('<?= 'attributes/suggestAttribute/' ?>' + this.element.data('definition-id') + '?term=' + request.term, function(data) {
return response(data);
}, 'json');
},
appendTo: '.modal-content',
select: function(event, ui) {
event.preventDefault();
$(this).val(ui.item.label);
},
delay: 10
});
const definition_values = function() {
const result = {};
$("[name*='attribute_links'").each(function() {
const definition_id = $(this).data('definition-id');
const element = $(this);
// For checkboxes, use the visible checkbox, not the hidden input
if (element.attr('type') === 'hidden' && element.siblings('input[type="checkbox"]').length > 0) {
// Skip hidden inputs that have a corresponding checkbox
return;
}
// For checkboxes, get the checked state
if (element.attr('type') === 'checkbox') {
result[definition_id] = element.prop('checked') ? '1' : '0';
} else {
result[definition_id] = element.val();
}
});
return result;
};
const refresh = function() {
const definition_id = $("#definition_name option:selected").val();
let attribute_values = definition_values();
attribute_values[definition_id] = '';
$('#attributes').load('<?= "items/attributes/$item_id" ?>', {
'definition_ids': JSON.stringify(attribute_values)
}, enable_delete);
};
$('#definition_name').change(function() {
refresh();
});
})();
</script>