|
30 | 30 | router = APIRouter() |
31 | 31 |
|
32 | 32 | _MAX_TAG_FILTERS = 50 |
| 33 | +_MAX_TAG_OR_TERMS = 20 |
| 34 | +_MAX_TAG_TOTAL_TERMS = 200 |
33 | 35 |
|
34 | 36 |
|
35 | 37 | def _as_nonneg_int(value: Any) -> int: |
@@ -241,22 +243,49 @@ async def random_image( |
241 | 243 | if not min_explicit and min_width_i == 0 and min_height_i == 0 and min_pixels_i == 0: |
242 | 244 | min_pixels_i = 1_000_000 if is_mobile else 2_000_000 |
243 | 245 |
|
244 | | - def _parse_tags(values: list[str] | None) -> list[str]: |
| 246 | + def _parse_tag_filters(values: list[str] | None) -> list[str]: |
| 247 | + """ |
| 248 | + Tag filters support "AND of groups" where each query param is one group. |
| 249 | +
|
| 250 | + Examples: |
| 251 | + - included_tags=girl&included_tags=boy -> girl AND boy |
| 252 | + - included_tags=girl|boy -> girl OR boy |
| 253 | + - included_tags=girl|boy&included_tags=white|black -> (girl OR boy) AND (white OR black) |
| 254 | + """ |
245 | 255 | out: list[str] = [] |
246 | 256 | seen: set[str] = set() |
247 | 257 | for raw in values or []: |
248 | | - for part in str(raw).split("|"): |
249 | | - name = part.strip() |
250 | | - if not name or name in seen: |
251 | | - continue |
252 | | - seen.add(name) |
253 | | - out.append(name) |
| 258 | + expr = str(raw or "").strip() |
| 259 | + if not expr or expr in seen: |
| 260 | + continue |
| 261 | + seen.add(expr) |
| 262 | + out.append(expr) |
254 | 263 | return out |
255 | 264 |
|
256 | | - included = _parse_tags(included_tags) |
257 | | - excluded = _parse_tags(excluded_tags) |
| 265 | + included = _parse_tag_filters(included_tags) |
| 266 | + excluded = _parse_tag_filters(excluded_tags) |
| 267 | + |
| 268 | + def _validate_tag_filters(values: list[str]) -> None: |
| 269 | + total_terms = 0 |
| 270 | + for expr in values: |
| 271 | + parts: list[str] = [] |
| 272 | + seen_terms: set[str] = set() |
| 273 | + for part in str(expr).split("|"): |
| 274 | + term = part.strip() |
| 275 | + if not term or term in seen_terms: |
| 276 | + continue |
| 277 | + seen_terms.add(term) |
| 278 | + parts.append(term) |
| 279 | + if len(parts) > _MAX_TAG_OR_TERMS: |
| 280 | + raise ApiError(code=ErrorCode.BAD_REQUEST, message="Too many tag terms in a group", status_code=400) |
| 281 | + total_terms += len(parts) |
| 282 | + if total_terms > _MAX_TAG_TOTAL_TERMS: |
| 283 | + raise ApiError(code=ErrorCode.BAD_REQUEST, message="Too many tag terms", status_code=400) |
| 284 | + |
258 | 285 | if len(included) > _MAX_TAG_FILTERS or len(excluded) > _MAX_TAG_FILTERS: |
259 | 286 | raise ApiError(code=ErrorCode.BAD_REQUEST, message="Too many tag filters", status_code=400) |
| 287 | + _validate_tag_filters(included) |
| 288 | + _validate_tag_filters(excluded) |
260 | 289 | if user_id is not None and int(user_id) <= 0: |
261 | 290 | raise ApiError(code=ErrorCode.BAD_REQUEST, message="Unsupported user_id", status_code=400) |
262 | 291 | if illust_id is not None and int(illust_id) <= 0: |
|
0 commit comments