|
|
@ -517,149 +517,228 @@ async def get_table_configs(query_db, page_object, user, role_id_list, table_nam |
|
|
"user_row_list": user_row_list, |
|
|
"user_row_list": user_row_list, |
|
|
"isHave":isHave |
|
|
"isHave":isHave |
|
|
} |
|
|
} |
|
|
|
|
|
# async def generate_sql(tablesRowCol:dict, table_columns:dict): |
|
|
|
|
|
# sql_queries = {} |
|
|
|
|
|
|
|
|
|
|
|
# # 1. 列控制 |
|
|
|
|
|
# # 遍历每个表 |
|
|
|
|
|
# no_configTable_name="" |
|
|
|
|
|
# for table_name, table_configs in tablesRowCol.items(): |
|
|
|
|
|
# if not table_configs.get("isHave", False): |
|
|
|
|
|
# no_configTable_name += table_name + "," |
|
|
|
|
|
# if no_configTable_name: |
|
|
|
|
|
# no_configTable_name = no_configTable_name.rstrip(',') |
|
|
|
|
|
# raise ValueError(f"表:{no_configTable_name}均未配置行列数据安全") |
|
|
|
|
|
# for table_name, config in tablesRowCol.items(): |
|
|
|
|
|
# # 获取该表的字段名 |
|
|
|
|
|
# columns = {col.lower(): col for col in table_columns[table_name]} # 将字段名转为小写 |
|
|
|
|
|
# # 初始化 SELECT 部分:用字典存储字段名,值是 null 字段名 |
|
|
|
|
|
# select_columns = {col: f"null as {col}" for col in columns} |
|
|
|
|
|
|
|
|
|
|
|
# # 处理角色列配置 |
|
|
|
|
|
# for col in config["role_col_list"]: |
|
|
|
|
|
# # If dbCName is "ALL", handle it as a special case |
|
|
|
|
|
# if col.dbCName == "ALL": |
|
|
|
|
|
# if col.ctrl_type == '0': # If ctrl_type is '0', prefix all columns with null |
|
|
|
|
|
# for db_column in columns: # Assuming 'user' is the table name |
|
|
|
|
|
# select_columns[db_column] = f"null as {db_column}" # 仍然保留 null 前缀 |
|
|
|
|
|
# elif col.ctrl_type == '1': # If ctrl_type is '1', use actual column names |
|
|
|
|
|
# for db_column in columns: |
|
|
|
|
|
# select_columns[db_column] = db_column # 使用实际字段名 |
|
|
|
|
|
# else: |
|
|
|
|
|
# # Handle specific columns listed in dbCName |
|
|
|
|
|
# db_columns = [db_column.strip().lower() for db_column in col.dbCName.split(",")] |
|
|
|
|
|
# for db_column in db_columns: |
|
|
|
|
|
# db_column = db_column.strip() |
|
|
|
|
|
# if db_column in columns: # Check if the column exists in the table |
|
|
|
|
|
# if col.ctrl_type == '0': # If ctrl_type is '0', prefix with null |
|
|
|
|
|
# select_columns[db_column] = f"null as {db_column}" # 仍然保留 null 前缀 |
|
|
|
|
|
# elif col.ctrl_type == '1': # If ctrl_type is '1', use actual column name |
|
|
|
|
|
# select_columns[db_column] = db_column # 使用实际字段名 |
|
|
|
|
|
# # 处理用户列配置 |
|
|
|
|
|
# for col in config["user_col_list"]: |
|
|
|
|
|
# if col.dbCName == "ALL": # 如果 dbCName 为 "ALL" |
|
|
|
|
|
# if col.ctrl_type == "0": # ctrlType 为 0,字符串字段 |
|
|
|
|
|
# for db_column in columns: # 对所有字段加上 null |
|
|
|
|
|
# select_columns[db_column] = f"null as {db_column}" # 仍然保留 null 前缀 |
|
|
|
|
|
# elif col.ctrl_type == "1": # ctrlType 为 1,实际数据库字段 |
|
|
|
|
|
# for db_column in columns: # 使用实际字段名,不加 null |
|
|
|
|
|
# select_columns[db_column] = db_column # 使用实际字段名 |
|
|
|
|
|
# else: # 处理 dbCName 不为 "ALL" 的情况 |
|
|
|
|
|
# db_columns = [db_column.strip().lower() for db_column in col.dbCName.split(",")] |
|
|
|
|
|
# for db_column in db_columns: |
|
|
|
|
|
# db_column = db_column.strip() |
|
|
|
|
|
# if db_column in columns: |
|
|
|
|
|
# if col.ctrl_type == "0": |
|
|
|
|
|
# select_columns[db_column] = f"null as {db_column}" # 仍然保留 null 前缀 |
|
|
|
|
|
# elif col.ctrl_type == "1": |
|
|
|
|
|
# select_columns[db_column] = db_column # 使用实际字段名 |
|
|
|
|
|
# # 生成 SQL 查询 |
|
|
|
|
|
# sql_queries[table_name] = f"SELECT {', '.join(select_columns.values())} FROM {table_name}" |
|
|
|
|
|
# # 2.行控制 |
|
|
|
|
|
# select_rows={} |
|
|
|
|
|
# # 处理角色行配置 |
|
|
|
|
|
# for row in config["role_row_list"]: |
|
|
|
|
|
# # 仅仅对固定值有效,不加行限制 |
|
|
|
|
|
# if row.ctrl_value == "ALL" and row.ctrl_type == '0': |
|
|
|
|
|
# # 控制方式 --固定值 |
|
|
|
|
|
# select_rows[row.dbCName] = "" |
|
|
|
|
|
# else: |
|
|
|
|
|
# if row.ctrl_type == '0': |
|
|
|
|
|
# # row.ctrl_value 是逗号分隔的字符串时,改为 IN 语句 |
|
|
|
|
|
# if "," in row.ctrl_value: |
|
|
|
|
|
# # 将 ctrl_value 按逗号分割,并用单引号包裹每个值 |
|
|
|
|
|
# values = [f"'{value.strip()}'" for value in row.ctrl_value.split(",")] |
|
|
|
|
|
# select_rows[row.dbCName] = f"{row.dbCName} IN ({', '.join(values)})" |
|
|
|
|
|
# else: |
|
|
|
|
|
# select_rows[row.dbCName] = f"{row.dbCName} = '{row.ctrl_value}'" |
|
|
|
|
|
# if row.ctrl_type == '1': |
|
|
|
|
|
# tab_col_value=row.ctrl_value.split(".") |
|
|
|
|
|
# if len(tab_col_value) != 2: |
|
|
|
|
|
# raise RuntimeError(f"{row.dbCName}字段控制类型为表字段,未维护正确的值") |
|
|
|
|
|
# select_rows[row.dbCName] = f"{row.dbCName} in (select {tab_col_value[1]} from {row.dbSName}.{tab_col_value[0]} where user_id = '1')" |
|
|
|
|
|
# # 处理用户行配置 |
|
|
|
|
|
# for row in config["user_row_list"]: |
|
|
|
|
|
# # 仅仅对固定值有效,不加行限制 |
|
|
|
|
|
# if row.ctrl_value == "ALL" and row.ctrl_type == '0': |
|
|
|
|
|
# # 控制方式 --固定值 |
|
|
|
|
|
# select_rows[row.dbCName] = "" |
|
|
|
|
|
# else: |
|
|
|
|
|
# if row.ctrl_type == '0': |
|
|
|
|
|
# # row.obj_value 是逗号分隔的字符串时,改为 IN 语句 |
|
|
|
|
|
# if "," in row.ctrl_value: |
|
|
|
|
|
# # 将 obj_value 按逗号分割,并用单引号包裹每个值 |
|
|
|
|
|
# values = [f"'{value.strip()}'" for value in row.ctrl_value.split(",")] |
|
|
|
|
|
# select_rows[row.dbCName] = f"{row.dbCName} IN ({', '.join(values)})" |
|
|
|
|
|
# else: |
|
|
|
|
|
# select_rows[row.dbCName] = f"{row.dbCName} = '{row.ctrl_value}'" |
|
|
|
|
|
# if row.ctrl_type == '1': |
|
|
|
|
|
# tab_col_value=row.ctrl_value.split(".") |
|
|
|
|
|
# if len(tab_col_value) != 2: |
|
|
|
|
|
# raise RuntimeError(f"{row.dbCName}字段控制类型为表字段,未维护正确的值") |
|
|
|
|
|
# select_rows[row.dbCName] = f"{row.dbCName} in (select {tab_col_value[1]} from {row.dbSName}.{tab_col_value[0]} where user_id = '1')" |
|
|
|
|
|
# if select_rows.values(): |
|
|
|
|
|
# where_conditions = " AND ".join(select_rows.values()) |
|
|
|
|
|
# if where_conditions: |
|
|
|
|
|
# sql_queries[table_name] += " WHERE " + where_conditions |
|
|
|
|
|
# else: |
|
|
|
|
|
# sql_queries[table_name] += " WHERE 1 = 0" |
|
|
|
|
|
# return sql_queries |
|
|
async def generate_sql(tablesRowCol: dict, table_columns: dict): |
|
|
async def generate_sql(tablesRowCol: dict, table_columns: dict): |
|
|
sql_queries = {} |
|
|
sql_queries = {} |
|
|
|
|
|
|
|
|
# 1. 列控制 |
|
|
# ========= 0. 校验是否存在未配置安全策略的表 ========= |
|
|
# 遍历每个表 |
|
|
no_config_tables = [ |
|
|
no_configTable_name="" |
|
|
table_name |
|
|
for table_name, table_configs in tablesRowCol.items(): |
|
|
for table_name, cfg in tablesRowCol.items() |
|
|
if not table_configs.get("isHave", False): |
|
|
if not cfg.get("isHave", False) |
|
|
no_configTable_name += table_name + "," |
|
|
] |
|
|
if no_configTable_name: |
|
|
if no_config_tables: |
|
|
no_configTable_name = no_configTable_name.rstrip(',') |
|
|
raise ValueError(f"表:{','.join(no_config_tables)} 均未配置行列数据安全") |
|
|
raise ValueError(f"表:{no_configTable_name}均未配置行列数据安全") |
|
|
|
|
|
|
|
|
# ========= 1. 遍历每个表 ========= |
|
|
for table_name, config in tablesRowCol.items(): |
|
|
for table_name, config in tablesRowCol.items(): |
|
|
# 获取该表的字段名 |
|
|
# 字段映射:小写 → 原始字段名 |
|
|
columns = {col.lower(): col for col in table_columns[table_name]} # 将字段名转为小写 |
|
|
columns = {col.lower(): col for col in table_columns[table_name]} |
|
|
# 初始化 SELECT 部分:用字典存储字段名,值是 null 字段名 |
|
|
|
|
|
select_columns = {col: f"null as {col}" for col in columns} |
|
|
# ==================================================== |
|
|
|
|
|
# 2. 列控制(不可见优先) |
|
|
# 处理角色列配置 |
|
|
# ==================================================== |
|
|
for col in config["role_col_list"]: |
|
|
|
|
|
# If dbCName is "ALL", handle it as a special case |
|
|
# 0 = 不可见,1 = 可见,None = 未配置(默认不可见) |
|
|
|
|
|
column_visibility = {col: None for col in columns} |
|
|
|
|
|
|
|
|
|
|
|
def set_visibility(col_name: str, ctrl_type: str): |
|
|
|
|
|
""" |
|
|
|
|
|
不可见(ctrl_type=0) 优先级最高 |
|
|
|
|
|
""" |
|
|
|
|
|
if ctrl_type == '0': |
|
|
|
|
|
column_visibility[col_name] = '0' |
|
|
|
|
|
elif ctrl_type == '1': |
|
|
|
|
|
if column_visibility[col_name] != '0': |
|
|
|
|
|
column_visibility[col_name] = '1' |
|
|
|
|
|
|
|
|
|
|
|
def handle_col_config(col_cfg_list): |
|
|
|
|
|
for col in col_cfg_list: |
|
|
if col.dbCName == "ALL": |
|
|
if col.dbCName == "ALL": |
|
|
if col.ctrl_type == '0': # If ctrl_type is '0', prefix all columns with null |
|
|
for db_col in columns: |
|
|
for db_column in columns: # Assuming 'user' is the table name |
|
|
set_visibility(db_col, col.ctrl_type) |
|
|
select_columns[db_column] = f"null as {db_column}" # 仍然保留 null 前缀 |
|
|
|
|
|
elif col.ctrl_type == '1': # If ctrl_type is '1', use actual column names |
|
|
|
|
|
for db_column in columns: |
|
|
|
|
|
select_columns[db_column] = db_column # 使用实际字段名 |
|
|
|
|
|
else: |
|
|
else: |
|
|
# Handle specific columns listed in dbCName |
|
|
db_cols = [c.strip().lower() for c in col.dbCName.split(",")] |
|
|
db_columns = [db_column.strip().lower() for db_column in col.dbCName.split(",")] |
|
|
for db_col in db_cols: |
|
|
for db_column in db_columns: |
|
|
if db_col in columns: |
|
|
db_column = db_column.strip() |
|
|
set_visibility(db_col, col.ctrl_type) |
|
|
if db_column in columns: # Check if the column exists in the table |
|
|
|
|
|
if col.ctrl_type == '0': # If ctrl_type is '0', prefix with null |
|
|
# 角色列 + 用户列 |
|
|
select_columns[db_column] = f"null as {db_column}" # 仍然保留 null 前缀 |
|
|
handle_col_config(config.get("role_col_list", [])) |
|
|
elif col.ctrl_type == '1': # If ctrl_type is '1', use actual column name |
|
|
handle_col_config(config.get("user_col_list", [])) |
|
|
select_columns[db_column] = db_column # 使用实际字段名 |
|
|
|
|
|
# 处理用户列配置 |
|
|
# 生成 SELECT 字段 |
|
|
for col in config["user_col_list"]: |
|
|
select_columns = [] |
|
|
if col.dbCName == "ALL": # 如果 dbCName 为 "ALL" |
|
|
for col in columns: |
|
|
if col.ctrl_type == "0": # ctrlType 为 0,字符串字段 |
|
|
if column_visibility[col] == '1': |
|
|
for db_column in columns: # 对所有字段加上 null |
|
|
select_columns.append(col) |
|
|
select_columns[db_column] = f"null as {db_column}" # 仍然保留 null 前缀 |
|
|
|
|
|
elif col.ctrl_type == "1": # ctrlType 为 1,实际数据库字段 |
|
|
|
|
|
for db_column in columns: # 使用实际字段名,不加 null |
|
|
|
|
|
select_columns[db_column] = db_column # 使用实际字段名 |
|
|
|
|
|
else: # 处理 dbCName 不为 "ALL" 的情况 |
|
|
|
|
|
db_columns = [db_column.strip().lower() for db_column in col.dbCName.split(",")] |
|
|
|
|
|
for db_column in db_columns: |
|
|
|
|
|
db_column = db_column.strip() |
|
|
|
|
|
if db_column in columns: |
|
|
|
|
|
if col.ctrl_type == "0": |
|
|
|
|
|
select_columns[db_column] = f"null as {db_column}" # 仍然保留 null 前缀 |
|
|
|
|
|
elif col.ctrl_type == "1": |
|
|
|
|
|
select_columns[db_column] = db_column # 使用实际字段名 |
|
|
|
|
|
# 生成 SQL 查询 |
|
|
|
|
|
sql_queries[table_name] = f"SELECT {', '.join(select_columns.values())} FROM {table_name}" |
|
|
|
|
|
# 2.行控制 |
|
|
|
|
|
select_rows={} |
|
|
|
|
|
# 处理角色行配置 |
|
|
|
|
|
for row in config["role_row_list"]: |
|
|
|
|
|
# 仅仅对固定值有效,不加行限制 |
|
|
|
|
|
if row.ctrl_value == "ALL" and row.ctrl_type == '0': |
|
|
|
|
|
# 控制方式 --固定值 |
|
|
|
|
|
select_rows[row.dbCName] = "" |
|
|
|
|
|
else: |
|
|
|
|
|
if row.ctrl_type == '0': |
|
|
|
|
|
# row.ctrl_value 是逗号分隔的字符串时,改为 IN 语句 |
|
|
|
|
|
if "," in row.ctrl_value: |
|
|
|
|
|
# 将 ctrl_value 按逗号分割,并用单引号包裹每个值 |
|
|
|
|
|
values = [f"'{value.strip()}'" for value in row.ctrl_value.split(",")] |
|
|
|
|
|
select_rows[row.dbCName] = f"{row.dbCName} IN ({', '.join(values)})" |
|
|
|
|
|
else: |
|
|
|
|
|
select_rows[row.dbCName] = f"{row.dbCName} = '{row.ctrl_value}'" |
|
|
|
|
|
if row.ctrl_type == '1': |
|
|
|
|
|
tab_col_value=row.ctrl_value.split(".") |
|
|
|
|
|
if len(tab_col_value) != 2: |
|
|
|
|
|
raise RuntimeError(f"{row.dbCName}字段控制类型为表字段,未维护正确的值") |
|
|
|
|
|
select_rows[row.dbCName] = f"{row.dbCName} in (select {tab_col_value[1]} from {row.dbSName}.{tab_col_value[0]} where user_id = '1')" |
|
|
|
|
|
# 处理用户行配置 |
|
|
|
|
|
for row in config["user_row_list"]: |
|
|
|
|
|
# 仅仅对固定值有效,不加行限制 |
|
|
|
|
|
if row.ctrl_value == "ALL" and row.ctrl_type == '0': |
|
|
|
|
|
# 控制方式 --固定值 |
|
|
|
|
|
select_rows[row.dbCName] = "" |
|
|
|
|
|
else: |
|
|
else: |
|
|
|
|
|
select_columns.append(f"null as {col}") |
|
|
|
|
|
|
|
|
|
|
|
sql = f"SELECT {', '.join(select_columns)} FROM {table_name}" |
|
|
|
|
|
|
|
|
|
|
|
# ==================================================== |
|
|
|
|
|
# 3. 行控制 |
|
|
|
|
|
# ==================================================== |
|
|
|
|
|
|
|
|
|
|
|
where_conditions = [] |
|
|
|
|
|
|
|
|
|
|
|
def build_row_condition(row): |
|
|
|
|
|
# 固定值 & ALL → 不加限制 |
|
|
|
|
|
if row.ctrl_type == '0' and row.ctrl_value == "ALL": |
|
|
|
|
|
return None |
|
|
|
|
|
|
|
|
|
|
|
# 固定值 |
|
|
if row.ctrl_type == '0': |
|
|
if row.ctrl_type == '0': |
|
|
# row.obj_value 是逗号分隔的字符串时,改为 IN 语句 |
|
|
|
|
|
if "," in row.ctrl_value: |
|
|
if "," in row.ctrl_value: |
|
|
# 将 obj_value 按逗号分割,并用单引号包裹每个值 |
|
|
values = [f"'{v.strip()}'" for v in row.ctrl_value.split(",")] |
|
|
values = [f"'{value.strip()}'" for value in row.ctrl_value.split(",")] |
|
|
return f"{row.dbCName} IN ({', '.join(values)})" |
|
|
select_rows[row.dbCName] = f"{row.dbCName} IN ({', '.join(values)})" |
|
|
return f"{row.dbCName} = '{row.ctrl_value}'" |
|
|
else: |
|
|
|
|
|
select_rows[row.dbCName] = f"{row.dbCName} = '{row.ctrl_value}'" |
|
|
# 表字段 |
|
|
if row.ctrl_type == '1': |
|
|
if row.ctrl_type == '1': |
|
|
tab_col_value=row.ctrl_value.split(".") |
|
|
tab_col = row.ctrl_value.split(".") |
|
|
if len(tab_col_value) != 2: |
|
|
if len(tab_col) != 2: |
|
|
raise RuntimeError(f"{row.dbCName}字段控制类型为表字段,未维护正确的值") |
|
|
raise RuntimeError( |
|
|
select_rows[row.dbCName] = f"{row.dbCName} in (select {tab_col_value[1]} from {row.dbSName}.{tab_col_value[0]} where user_id = '1')" |
|
|
f"{row.dbCName} 字段控制类型为表字段,但未维护正确的值" |
|
|
if select_rows.values(): |
|
|
) |
|
|
where_conditions = " AND ".join(select_rows.values()) |
|
|
table, column = tab_col |
|
|
|
|
|
return ( |
|
|
|
|
|
f"{row.dbCName} IN (" |
|
|
|
|
|
f"SELECT {column} FROM {row.dbSName}.{table} " |
|
|
|
|
|
f"WHERE user_id = '1')" |
|
|
|
|
|
) |
|
|
|
|
|
return None |
|
|
|
|
|
|
|
|
|
|
|
def handle_row_config(row_cfg_list): |
|
|
|
|
|
for row in row_cfg_list: |
|
|
|
|
|
condition = build_row_condition(row) |
|
|
|
|
|
if condition: |
|
|
|
|
|
where_conditions.append(condition) |
|
|
|
|
|
|
|
|
|
|
|
# 角色行 + 用户行 |
|
|
|
|
|
handle_row_config(config.get("role_row_list", [])) |
|
|
|
|
|
handle_row_config(config.get("user_row_list", [])) |
|
|
|
|
|
|
|
|
|
|
|
# ==================================================== |
|
|
|
|
|
# 4. WHERE 拼接(无行权限则拒绝访问) |
|
|
|
|
|
# ==================================================== |
|
|
|
|
|
|
|
|
if where_conditions: |
|
|
if where_conditions: |
|
|
sql_queries[table_name] += " WHERE " + where_conditions |
|
|
sql += " WHERE " + " AND ".join(where_conditions) |
|
|
else: |
|
|
else: |
|
|
sql_queries[table_name] += " WHERE 1 = 0" |
|
|
sql += " WHERE 1 = 0" |
|
|
|
|
|
|
|
|
|
|
|
sql_queries[table_name] = sql |
|
|
|
|
|
|
|
|
return sql_queries |
|
|
return sql_queries |
|
|
# async def replace_table_with_subquery(ctrSqlDict, oldStrSql): |
|
|
|
|
|
# table_alias_map = {} # 存储表名和别名的映射 |
|
|
|
|
|
# for table_name, subquery in ctrSqlDict.items(): |
|
|
|
|
|
# # 构建正则表达式,匹配表名及可能的别名 |
|
|
|
|
|
# pattern = ( |
|
|
|
|
|
# r'(\b(?:[a-zA-Z_][a-zA-Z0-9_]*\.)?' # 匹配模式名(可选) |
|
|
|
|
|
# + re.escape(table_name) # 转义表名 |
|
|
|
|
|
# + r'\b)' # 结束表名 |
|
|
|
|
|
# r'(\s+(?:AS\s+)?(\w+))?' # 捕获别名部分(含 AS 或直接别名) |
|
|
|
|
|
# r'(?=\s*[\w\(\)]*)' # 确保后面是合法 SQL 语法,不是 SQL 关键字 |
|
|
|
|
|
# ) |
|
|
|
|
|
# def replace(match): |
|
|
|
|
|
# original_table = match.group(1) # 原始表名(可能含模式名) |
|
|
|
|
|
# alias_part = match.group(2) # 别名部分(含空格、AS 或直接别名) |
|
|
|
|
|
# alias_name = match.group(3) # 别名名称(无 AS 前缀) |
|
|
|
|
|
# if original_table not in table_alias_map: |
|
|
|
|
|
# # 处理表名后直接跟着 SQL 关键字的情况 |
|
|
|
|
|
# sql_keywords = {"LIMIT", "WHERE", "ORDER", "GROUP", "HAVING", "JOIN", "ON", "USING", "UNION", |
|
|
|
|
|
# "EXCEPT", "INTERSECT", "FETCH", "OFFSET"} |
|
|
|
|
|
# if alias_name and alias_name.upper().split()[0] not in sql_keywords: |
|
|
|
|
|
# # 已存在别名,且别名后没有紧跟 SQL 关键字,保留原别名 |
|
|
|
|
|
# replaced = f"({subquery}) {alias_part}" |
|
|
|
|
|
# table_alias_map[original_table] = alias_part |
|
|
|
|
|
# else: |
|
|
|
|
|
# # 无别名时,或者别名无效(如 LIMIT),添加默认别名 |
|
|
|
|
|
# alias = original_table.split('.')[-1] |
|
|
|
|
|
# replaced = f"({subquery}) AS {alias}{alias_part}" |
|
|
|
|
|
# table_alias_map[original_table] = alias |
|
|
|
|
|
# else: |
|
|
|
|
|
# alias = table_alias_map[original_table] |
|
|
|
|
|
# replaced = f"{alias}" # 使用别名 |
|
|
|
|
|
# return replaced |
|
|
|
|
|
# # 执行替换(忽略大小写) |
|
|
|
|
|
# oldStrSql = re.sub(pattern, replace, oldStrSql, flags=re.IGNORECASE) |
|
|
|
|
|
|
|
|
|
|
|
# return oldStrSql |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|