Revision control
++ // Copyright (C) 2025 The Qt Company Ltd.
++ // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
++ // Qt-Security score:significant reason:default
++
++ #include "qqmlabstractcolumnmodel_p.h"
++
++ #include <QtCore/qloggingcategory.h>
++
++ #include <QtQml/qqmlinfo.h>
++ #include <QtQml/qqmlengine.h>
++
++ QT_BEGIN_NAMESPACE
++
++ using namespace Qt::StringLiterals;
++
++ Q_STATIC_LOGGING_CATEGORY(lcColumnModel, "qt.qml.columnmodel")
++
++ QQmlAbstractColumnModel::QQmlAbstractColumnModel(QObject *parent)
++ : QAbstractItemModel(parent)
++ {
++ }
++
++ QQmlListProperty<QQmlTableModelColumn> QQmlAbstractColumnModel::columns()
++ {
++ return {this, nullptr,
++ &QQmlAbstractColumnModel::columns_append,
++ &QQmlAbstractColumnModel::columns_count,
++ &QQmlAbstractColumnModel::columns_at,
++ &QQmlAbstractColumnModel::columns_clear,
++ &QQmlAbstractColumnModel::columns_replace,
++ &QQmlAbstractColumnModel::columns_removeLast};
++ }
++
++ void QQmlAbstractColumnModel::columns_append(QQmlListProperty<QQmlTableModelColumn> *property,
++ QQmlTableModelColumn *value)
++ {
++ auto *model = static_cast<QQmlAbstractColumnModel *>(property->object);
++ Q_ASSERT(value);
++ Q_ASSERT(model);
++ auto *column = qobject_cast<QQmlTableModelColumn *>(value);
++ if (column)
++ model->mColumns.append(column);
++ }
++
++ qsizetype QQmlAbstractColumnModel::columns_count(QQmlListProperty<QQmlTableModelColumn> *property)
++ {
++ auto *model = static_cast<QQmlAbstractColumnModel*>(property->object);
++ Q_ASSERT(model);
++ return model->mColumns.size();
++ }
++
++ QQmlTableModelColumn *QQmlAbstractColumnModel::columns_at(QQmlListProperty<QQmlTableModelColumn> *property, qsizetype index)
++ {
++ auto *model = static_cast<QQmlAbstractColumnModel*>(property->object);
++ Q_ASSERT(model);
++ return model->mColumns.at(index);
++ }
++
++ void QQmlAbstractColumnModel::columns_clear(QQmlListProperty<QQmlTableModelColumn> *property)
++ {
++ auto *model = static_cast<QQmlAbstractColumnModel *>(property->object);
++ Q_ASSERT(model);
++ model->mColumns.clear();
++ }
++
++ void QQmlAbstractColumnModel::columns_replace(QQmlListProperty<QQmlTableModelColumn> *property, qsizetype index, QQmlTableModelColumn *value)
++ {
++ auto *model = static_cast<QQmlAbstractColumnModel *>(property->object);
++ Q_ASSERT(model);
++ if (auto *column = qobject_cast<QQmlTableModelColumn *>(value))
++ model->mColumns.replace(index, column);
++ }
++
++ void QQmlAbstractColumnModel::columns_removeLast(QQmlListProperty<QQmlTableModelColumn> *property)
++ {
++ auto *model = static_cast<QQmlAbstractColumnModel *>(property->object);
++ Q_ASSERT(model);
++ model->mColumns.removeLast();
++ }
++
++ QVariant QQmlAbstractColumnModel::data(const QModelIndex &index, const QString &role) const
++ {
++ const int iRole = mRoleNames.key(role.toUtf8(), -1);
++ if (iRole >= 0)
++ return data(index, iRole);
++ return {};
++ }
++
++ QVariant QQmlAbstractColumnModel::data(const QModelIndex &index, int role) const
++ {
++ if (!index.isValid()) {
++ qmlWarning(this) << "data(): invalid QModelIndex";
++ return {};
++ }
++
++ const int row = index.row();
++ if (row < 0 || row >= rowCount(parent(index))) {
++ qmlWarning(this) << "data(): invalid row specified in QModelIndex";
++ return {};
++ }
++
++ const int column = index.column();
++ if (column < 0 || column >= columnCount(parent(index))) {
++ qmlWarning(this) << "data(): invalid column specified in QModelIndex";
++ return {};
++ }
++
++ const ColumnMetadata columnMetadata = mColumnMetadata.at(column);
++ const QString roleName = QString::fromUtf8(mRoleNames.value(role));
++ if (!columnMetadata.roles.contains(roleName)) {
++ qmlWarning(this) << "data(): no role named " << roleName
++ << " at column index " << column << ". The available roles for that column are: "
++ << columnMetadata.roles.keys();
++ return {};
++ }
++
++ const ColumnRoleMetadata roleData = columnMetadata.roles.value(roleName);
++ if (roleData.columnRole == ColumnRole::StringRole) {
++ // We know the data structure, so we can get the data for the user.
++ return dataPrivate(index, roleName);
++ }
++
++ // We don't know the data structure, so the user has to modify their data themselves.
++ // First, find the getter for this column and role.
++ QJSValue getter = mColumns.at(column)->getterAtRole(roleName);
++
++ // Then, call it and return what it returned.
++ const auto args = QJSValueList() << qmlEngine(this)->toScriptValue(index);
++ return getter.call(args).toVariant();
++ }
++
++ bool QQmlAbstractColumnModel::setData(const QModelIndex &index, const QVariant &value, const QString &role)
++ {
++ const int intRole = mRoleNames.key(role.toUtf8(), -1);
++ if (intRole >= 0)
++ return setData(index, value, intRole);
++ return false;
++ }
++
++ bool QQmlAbstractColumnModel::setData(const QModelIndex &index, const QVariant &value, int role)
++ {
++ Q_ASSERT(index.isValid());
++
++ const int row = index.row();
++ if (row < 0 || row >= rowCount(parent(index)))
++ return false;
++
++ const int column = index.column();
++ if (column < 0 || column >= columnCount(parent(index)))
++ return false;
++
++ const QString roleName = QString::fromUtf8(mRoleNames.value(role));
++
++ qCDebug(lcColumnModel).nospace() << "setData() called with index "
++ << index << ", value " << value << " and role " << roleName;
++
++ // Verify that the role exists for this column.
++ const ColumnMetadata columnMetadata = mColumnMetadata.at(index.column());
++ if (!columnMetadata.roles.contains(roleName)) {
++ qmlWarning(this) << "setData(): no role named \"" << roleName
++ << "\" at column index " << column << ". The available roles for that column are: "
++ << columnMetadata.roles.keys();
++ return false;
++ }
++
++ // Verify that the type of the value is what we expect.
++ // If the value set is not of the expected type, we can try to convert it automatically.
++ const ColumnRoleMetadata roleData = columnMetadata.roles.value(roleName);
++ QVariant effectiveValue = value;
++ if (value.userType() != roleData.type) {
++ if (!value.canConvert(QMetaType(roleData.type))) {
++ qmlWarning(this).nospace() << "setData(): the value " << value
++ << " set at row " << row << " column " << column << " with role " << roleName
++ << " cannot be converted to " << roleData.typeName;
++ return false;
++ }
++
++ if (!effectiveValue.convert(QMetaType(roleData.type))) {
++ qmlWarning(this).nospace() << "setData(): failed converting value " << value
++ << " set at row " << row << " column " << column << " with role " << roleName
++ << " to " << roleData.typeName;
++ return false;
++ }
++ }
++
++ if (roleData.columnRole == ColumnRole::StringRole) {
++ // We know the data structure, so we can set it for the user.
++ setDataPrivate(index, roleData.name, value);
++ } else {
++ qmlWarning(this).nospace() << "setData(): manipulation of complex row "
++ << "structures is not supported";
++ return false;
++ }
++
++ QList<int> rolesChanged;
++ rolesChanged.append(role);
++ emit dataChanged(index, index, rolesChanged);
++ emit rowsChanged();
++
++ return true;
++ }
++
++ QHash<int, QByteArray> QQmlAbstractColumnModel::roleNames() const
++ {
++ return mRoleNames;
++ }
++
++ Qt::ItemFlags QQmlAbstractColumnModel::flags(const QModelIndex &index) const
++ {
++ Q_UNUSED(index)
++ return Qt::ItemIsSelectable | Qt::ItemIsEnabled | Qt::ItemIsEditable;
++ }
++
++ void QQmlAbstractColumnModel::classBegin()
++ {
++ }
++
++ void QQmlAbstractColumnModel::componentComplete()
++ {
++ mComponentCompleted = true;
++
++ mColumnCount = mColumns.size();
++ if (mColumnCount > 0)
++ emit columnCountChanged();
++
++ setInitialRows();
++ }
++
++
++ QQmlAbstractColumnModel::ColumnRoleMetadata::ColumnRoleMetadata()
++ = default;
++
++ QQmlAbstractColumnModel::ColumnRoleMetadata::ColumnRoleMetadata(
++ ColumnRole role, QString name, int type, QString typeName) :
++ columnRole(role),
++ name(std::move(name)),
++ type(type),
++ typeName(std::move(typeName))
++ {
++ }
++
++ bool QQmlAbstractColumnModel::ColumnRoleMetadata::isValid() const
++ {
++ return !name.isEmpty();
++ }
++
++ QQmlAbstractColumnModel::ColumnRoleMetadata QQmlAbstractColumnModel::fetchColumnRoleData(const QString &roleNameKey,
++ QQmlTableModelColumn *tableModelColumn, int columnIndex) const
++ {
++ const QVariant row = firstRow();
++ ColumnRoleMetadata roleData;
++
++ QJSValue columnRoleGetter = tableModelColumn->getterAtRole(roleNameKey);
++ if (columnRoleGetter.isUndefined()) {
++ // This role is not defined, which is fine; just skip it.
++ return roleData;
++ }
++
++ if (columnRoleGetter.isString()) {
++ // The role is set as a string, so we assume the row is a simple object.
++ if (row.userType() != QMetaType::QVariantMap) {
++ qmlWarning(this).quote() << "expected row for role "
++ << roleNameKey << " of TableModelColumn at index "
++ << columnIndex << " to be a simple object, but it's "
++ << row.typeName() << " instead: " << row;
++ return roleData;
++ }
++
++ QString rolePropertyName = columnRoleGetter.toString();
++ const QVariant roleProperty = row.toMap().value(rolePropertyName);
++
++ roleData.columnRole = ColumnRole::StringRole;
++ roleData.type = roleProperty.userType();
++ roleData.typeName = QString::fromLatin1(roleProperty.typeName());
++ roleData.name = std::move(rolePropertyName);
++ } else if (columnRoleGetter.isCallable()) {
++ // The role is provided via a function, which means the row is complex and
++ // the user needs to provide the data for it.
++ const auto modelIndex = index(0, columnIndex);
++ const auto args = QJSValueList() << qmlEngine(this)->toScriptValue(modelIndex);
++ const QVariant cellData = columnRoleGetter.call(args).toVariant();
++
++ // We don't know the property name since it's provided through the function.
++ // roleData.name = ???
++ roleData.columnRole = ColumnRole::FunctionRole;
++ roleData.type = cellData.userType();
++ roleData.typeName = QString::fromLatin1(cellData.typeName());
++ } else {
++ // Invalid role.
++ qmlWarning(this) << "TableModelColumn role for column at index "
++ << columnIndex << " must be either a string or a function; actual type is: "
++ << columnRoleGetter.toString();
++ }
++
++ return roleData;
++ }
++
++ void QQmlAbstractColumnModel::fetchColumnMetadata()
++ {
++ qCDebug(lcColumnModel) << "gathering metadata for" << mColumnCount << "columns from first row:";
++
++ static const auto supportedRoleNames = QQmlTableModelColumn::supportedRoleNames();
++
++ // Since we support different data structures at the row level, we require that there
++ // is a TableModelColumn for each column.
++ // Collect and cache metadata for each column. This makes data lookup faster.
++ for (int columnIndex = 0; columnIndex < mColumns.size(); ++columnIndex) {
++ QQmlTableModelColumn *column = mColumns.at(columnIndex);
++ qCDebug(lcColumnModel).nospace() << "- column " << columnIndex << ":";
++
++ ColumnMetadata metaData;
++ const auto builtInRoleKeys = supportedRoleNames.keys();
++ for (const int builtInRoleKey : builtInRoleKeys) {
++ const QString builtInRoleName = supportedRoleNames.value(builtInRoleKey);
++ ColumnRoleMetadata roleData = fetchColumnRoleData(builtInRoleName, column, columnIndex);
++ if (roleData.type == QMetaType::UnknownType) {
++ // This built-in role was not specified in this column.
++ continue;
++ }
++
++ qCDebug(lcColumnModel).nospace() << " - added metadata for built-in role "
++ << builtInRoleName << " at column index " << columnIndex
++ << ": name=" << roleData.name << " typeName=" << roleData.typeName
++ << " type=" << roleData.type;
++
++ // This column now supports this specific built-in role.
++ metaData.roles.insert(builtInRoleName, roleData);
++ // Add it if it doesn't already exist.
++ mRoleNames[builtInRoleKey] = builtInRoleName.toLatin1();
++ }
++ mColumnMetadata.insert(columnIndex, metaData);
++ }
++ }
++
++ bool QQmlAbstractColumnModel::validateRowType(QLatin1StringView functionName, const QVariant &row) const
++ {
++ if (!row.canConvert<QJSValue>()) {
++ qmlWarning(this) << functionName << ": expected \"row\" argument to be a QJSValue,"
++ << " but got " << row.typeName() << " instead:\n" << row;
++ return false;
++ }
++
++ const auto rowAsJSValue = row.value<QJSValue>();
++ if (!rowAsJSValue.isObject() && !rowAsJSValue.isArray()) {
++ qmlWarning(this) << functionName << ": expected \"row\" argument "
++ << "to be an object or array, but got:\n" << rowAsJSValue.toString();
++ return false;
++ }
++
++ return true;
++ }
++
++ bool QQmlAbstractColumnModel::validateNewRow(QLatin1StringView functionName, const QVariant &row,
++ NewRowOperationFlag operation) const
++ {
++ if (mColumnMetadata.isEmpty()) {
++ // There is no column metadata, so we have nothing to validate the row against.
++ // Rows have to be added before we can gather metadata from them, so just this
++ // once we'll return true to allow the rows to be added.
++ return true;
++ }
++
++ const bool isVariantMap = (row.userType() == QMetaType::QVariantMap);
++ const bool isVariantList = (row.userType() == QMetaType::QVariantList);
++
++ // Don't require each row to be a QJSValue when setting all rows,
++ // as they won't be; they'll be QVariantMap if given as object.
++ // Passing a QVariantList counts as attempted complex row manipulation.
++ if (operation != SetRowsOperation
++ && !isVariantMap
++ && !isVariantList
++ && !validateRowType(functionName, row)) {
++ return false;
++ }
++
++ const QVariant rowAsVariant = operation == SetRowsOperation || isVariantMap
++ ? row : row.value<QJSValue>().toVariant();
++ if (rowAsVariant.userType() != QMetaType::QVariantMap) {
++ qmlWarning(this) << functionName << ": row manipulation functions "
++ << "do not support complex rows";
++ return false;
++ }
++
++ const QVariantMap rowAsMap = rowAsVariant.toMap();
++ const int columnCount = rowAsMap.size();
++ if (columnCount < mColumnCount) {
++ qmlWarning(this) << functionName << ": expected " << mColumnCount
++ << " columns, but only got " << columnCount;
++ return false;
++ }
++
++ // We can't validate complex structures, but we can make sure that
++ // each simple string-based role in each column is correct.
++ for (int columnIndex = 0; columnIndex < mColumns.size(); ++columnIndex) {
++ QQmlTableModelColumn *column = mColumns.at(columnIndex);
++ const QHash<QString, QJSValue> getters = column->getters();
++ const auto roleNames = getters.keys();
++ const ColumnMetadata columnMetadata = mColumnMetadata.at(columnIndex);
++ for (const QString &roleName : roleNames) {
++ const ColumnRoleMetadata roleData = columnMetadata.roles.value(roleName);
++ if (roleData.columnRole == ColumnRole::FunctionRole)
++ continue;
++
++ if (!rowAsMap.contains(roleData.name)) {
++ qmlWarning(this).noquote() << functionName << ": expected a property named \""
++ << roleData.name << "\" in row";
++ return false;
++ }
++
++ const QVariant rolePropertyValue = rowAsMap.value(roleData.name);
++
++ if (rolePropertyValue.userType() != roleData.type) {
++ if (!rolePropertyValue.canConvert(QMetaType(roleData.type))) {
++ qmlWarning(this).noquote() << functionName << ": expected the property named \""
++ << roleData.name << "\" to be of type \"" << roleData.typeName
++ << "\", but got \"" << QString::fromLatin1(rolePropertyValue.typeName())
++ << "\" instead";
++ return false;
++ }
++
++ QVariant effectiveValue = rolePropertyValue;
++ if (!effectiveValue.convert(QMetaType(roleData.type))) {
++ qmlWarning(this).noquote() << functionName << ": failed converting value \""
++ << rolePropertyValue << "\" set at column " << columnIndex << " with role \""
++ << QString::fromLatin1(rolePropertyValue.typeName()) << "\" to \""
++ << roleData.typeName << "\"";
++ return false;
++ }
++ }
++ }
++ }
++
++ return true;
++ }
++
++ QLatin1StringView QQmlAbstractColumnModel::jsTypeName(const QJSValue &v)
++ {
++ if (v.isArray()) return "array"_L1;
++ if (v.isObject()) return "object"_L1;
++ if (v.isString()) return "string"_L1;
++ if (v.isNumber()) return "number"_L1;
++ if (v.isBool()) return "boolean"_L1;
++ if (v.isNull()) return "null"_L1;
++ if (v.isUndefined()) return "undefined"_L1;
++ return "unknown"_L1;
++ }
++
++ std::optional<QVariantList> QQmlAbstractColumnModel::validateRowsArgument(const QVariant &rows) const
++ {
++ if (rows.userType() != qMetaTypeId<QJSValue>()) {
++ qmlWarning(this)
++ << "setRows(): \"rows\" must be an array; actual type is"
++ << rows.typeName();
++ return std::nullopt;
++ }
++
++ const auto rowsAsJSValue = rows.value<QJSValue>();
++
++ if (!rowsAsJSValue.isArray()) {
++ qmlWarning(this)
++ << "setRows(): the type of \"rows\" is"
++ << jsTypeName(rowsAsJSValue)
++ << "but an array is expected";
++ return std::nullopt;
++ }
++
++ return rowsAsJSValue.toVariant().toList();
++ }
++
++ QT_END_NAMESPACE
++
++ #include "moc_qqmlabstractcolumnmodel_p.cpp"