| File: | d/form.c |
| Warning: | line 617, column 22 Access out-of-bound array element (buffer overflow) |
Press '?' to see keyboard shortcuts
Keyboard shortcuts:
| 1 | /* | |||
| 2 | * The contents of this file are subject to the Mozilla Public License | |||
| 3 | * Version 1.1 (the "License"); you may not use this file except in | |||
| 4 | * compliance with the License. You may obtain a copy of the License at | |||
| 5 | * http://mozilla.org/. | |||
| 6 | * | |||
| 7 | * Software distributed under the License is distributed on an "AS IS" | |||
| 8 | * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See | |||
| 9 | * the License for the specific language governing rights and limitations | |||
| 10 | * under the License. | |||
| 11 | * | |||
| 12 | * The Original Code is AOLserver Code and related documentation | |||
| 13 | * distributed by AOL. | |||
| 14 | * | |||
| 15 | * The Initial Developer of the Original Code is America Online, | |||
| 16 | * Inc. Portions created by AOL are Copyright (C) 1999 America Online, | |||
| 17 | * Inc. All Rights Reserved. | |||
| 18 | * | |||
| 19 | * Alternatively, the contents of this file may be used under the terms | |||
| 20 | * of the GNU General Public License (the "GPL"), in which case the | |||
| 21 | * provisions of GPL are applicable instead of those above. If you wish | |||
| 22 | * to allow use of your version of this file only under the terms of the | |||
| 23 | * GPL and not to allow others to use your version of this file under the | |||
| 24 | * License, indicate your decision by deleting the provisions above and | |||
| 25 | * replace them with the notice and other provisions required by the GPL. | |||
| 26 | * If you do not delete the provisions above, a recipient may use your | |||
| 27 | * version of this file under either the License or the GPL. | |||
| 28 | */ | |||
| 29 | ||||
| 30 | /* | |||
| 31 | * form.c -- | |||
| 32 | * | |||
| 33 | * Routines for dealing with HTML FORM's. | |||
| 34 | */ | |||
| 35 | ||||
| 36 | #include "nsd.h" | |||
| 37 | ||||
| 38 | /* | |||
| 39 | * Local functions defined in this file. | |||
| 40 | */ | |||
| 41 | ||||
| 42 | static Ns_ReturnCode ParseQuery(char *form, Ns_Set *set, Tcl_Encoding encoding, bool_Bool translate) | |||
| 43 | NS_GNUC_NONNULL(1)__attribute__((__nonnull__(1))) NS_GNUC_NONNULL(2)__attribute__((__nonnull__(2))); | |||
| 44 | ||||
| 45 | static Ns_ReturnCode ParseQueryWithFallback(Tcl_Interp *interp, NsServer *servPtr, | |||
| 46 | char *toParse, Ns_Set *set, | |||
| 47 | Tcl_Encoding encoding, bool_Bool translate, | |||
| 48 | Tcl_Obj *fallbackCharsetObj) | |||
| 49 | NS_GNUC_NONNULL(1)__attribute__((__nonnull__(1))) NS_GNUC_NONNULL(3)__attribute__((__nonnull__(3))) NS_GNUC_NONNULL(4)__attribute__((__nonnull__(4))); | |||
| 50 | ||||
| 51 | static Ns_ReturnCode ParseMultipartEntry(Conn *connPtr, Tcl_Encoding valueEncoding, const char *start, char *end) | |||
| 52 | NS_GNUC_NONNULL(1)__attribute__((__nonnull__(1))) NS_GNUC_NONNULL(3)__attribute__((__nonnull__(3))) NS_GNUC_NONNULL(4)__attribute__((__nonnull__(4))); | |||
| 53 | ||||
| 54 | static char *Ext2utf(Tcl_DString *dsPtr, const char *start, size_t len, Tcl_Encoding encoding, char unescape) | |||
| 55 | NS_GNUC_NONNULL(1)__attribute__((__nonnull__(1))) NS_GNUC_NONNULL(2)__attribute__((__nonnull__(2))); | |||
| 56 | ||||
| 57 | static bool_Bool GetBoundary(Tcl_DString *dsPtr, const char *contentType) | |||
| 58 | NS_GNUC_NONNULL(1)__attribute__((__nonnull__(1))) NS_GNUC_NONNULL(2)__attribute__((__nonnull__(2))); | |||
| 59 | ||||
| 60 | static char *NextBoundary(const Tcl_DString *dsPtr, char *s, const char *e) | |||
| 61 | NS_GNUC_NONNULL(1)__attribute__((__nonnull__(1))) NS_GNUC_NONNULL(2)__attribute__((__nonnull__(2))) NS_GNUC_PURE; | |||
| 62 | ||||
| 63 | static bool_Bool GetValue(const char *hdr, const char *att, const char **vsPtr, const char **vePtr, char *uPtr) | |||
| 64 | NS_GNUC_NONNULL(1)__attribute__((__nonnull__(1))) NS_GNUC_NONNULL(2)__attribute__((__nonnull__(2))) NS_GNUC_NONNULL(3)__attribute__((__nonnull__(3))) NS_GNUC_NONNULL(4)__attribute__((__nonnull__(4))) NS_GNUC_NONNULL(5)__attribute__((__nonnull__(5))); | |||
| 65 | ||||
| 66 | ||||
| 67 | ||||
| 68 | /* | |||
| 69 | *---------------------------------------------------------------------- | |||
| 70 | * | |||
| 71 | * Ns_ConnGetQuery -- | |||
| 72 | * | |||
| 73 | * Return the connection query data in form of an Ns_Set. This function | |||
| 74 | * parses the either the query (of the request URL) or the form content | |||
| 75 | * (in POST requests with content type "www-form-urlencoded" or | |||
| 76 | * "multipart/form-data"). In case the Ns_Set for the query is already | |||
| 77 | * set, it is treated as cached result and is returned untouched. | |||
| 78 | * | |||
| 79 | * Results: | |||
| 80 | * Query data or NULL if no form data is available. | |||
| 81 | * | |||
| 82 | * Side effects: | |||
| 83 | * None. | |||
| 84 | * | |||
| 85 | *---------------------------------------------------------------------- | |||
| 86 | */ | |||
| 87 | ||||
| 88 | Ns_Set * | |||
| 89 | Ns_ConnGetQuery(Tcl_Interp *interp, Ns_Conn *conn, Tcl_Obj *fallbackCharsetObj, Ns_ReturnCode *rcPtr) | |||
| 90 | { | |||
| 91 | Conn *connPtr; | |||
| 92 | ||||
| 93 | NS_NONNULL_ASSERT(conn != NULL)((void) (0)); | |||
| 94 | connPtr = (Conn *) conn; | |||
| 95 | ||||
| 96 | /* | |||
| 97 | * connPtr->query is used to cache the result, in case this function is | |||
| 98 | * called multiple times during a single request. | |||
| 99 | */ | |||
| 100 | if (connPtr->query == NULL((void*)0)) { | |||
| ||||
| 101 | const char *contentType, *charset = NULL((void*)0); | |||
| 102 | char *content = NULL((void*)0), *toParse = NULL((void*)0); | |||
| 103 | size_t charsetOffset; | |||
| 104 | bool_Bool haveFormData = NS_FALSE0; | |||
| 105 | Ns_ReturnCode status = NS_OK; | |||
| 106 | ||||
| 107 | /* | |||
| 108 | * We are called the first time, so create an ns_set named (slightly | |||
| 109 | * misleading "connPtr->query") | |||
| 110 | */ | |||
| 111 | if (connPtr->formData == NULL((void*)0)) { | |||
| 112 | connPtr->formData = Ns_SetCreate(NS_SET_NAME_QUERY"query"); | |||
| 113 | } | |||
| 114 | connPtr->query = connPtr->formData; | |||
| 115 | contentType = Ns_SetIGet(connPtr->headers, "content-type"); | |||
| 116 | ||||
| 117 | if (contentType != NULL((void*)0)) { | |||
| 118 | charset = NsFindCharset(contentType, &charsetOffset); | |||
| 119 | if (strncmp(contentType, "application/x-www-form-urlencoded", 33u) == 0) { | |||
| 120 | haveFormData = NS_TRUE1; | |||
| 121 | } else if (strncmp(contentType, "multipart/form-data", 19u) == 0) { | |||
| 122 | haveFormData = NS_TRUE1; | |||
| 123 | } | |||
| 124 | } | |||
| 125 | ||||
| 126 | if (haveFormData
| |||
| 127 | /* | |||
| 128 | * It is unsafe to access the content when the | |||
| 129 | * connection is already closed due to potentially | |||
| 130 | * unmmapped memory. | |||
| 131 | */ | |||
| 132 | if ((connPtr->flags & NS_CONN_CLOSED0x001u) == 0u) { | |||
| 133 | content = connPtr->reqPtr->content; | |||
| 134 | } else { | |||
| 135 | /* | |||
| 136 | * Formdata is unavailable, but do not fall back to the | |||
| 137 | * query-as-formdata tradition. We should keep a consistent | |||
| 138 | * behavior. | |||
| 139 | */ | |||
| 140 | } | |||
| 141 | } else if (connPtr->request.query != NULL((void*)0)) { | |||
| 142 | /* | |||
| 143 | * The content has none of the "FORM" content types, so get it | |||
| 144 | * in good old AOLserver tradition from the query variables. | |||
| 145 | */ | |||
| 146 | toParse = connPtr->request.query; | |||
| 147 | status = ParseQueryWithFallback(interp, connPtr->poolPtr->servPtr, | |||
| 148 | toParse, connPtr->query, connPtr->urlEncoding, | |||
| 149 | NS_FALSE0, fallbackCharsetObj); | |||
| 150 | } | |||
| 151 | ||||
| 152 | if (content != NULL((void*)0)) { | |||
| 153 | Tcl_DString boundaryDs; | |||
| 154 | /* | |||
| 155 | * We have one of the accepted content types AND the data is | |||
| 156 | * provided via content string. | |||
| 157 | */ | |||
| 158 | Tcl_DStringInit(&boundaryDs); | |||
| 159 | ||||
| 160 | if (*contentType == 'a') { | |||
| 161 | /* | |||
| 162 | * The content-type is "application/x-www-form-urlencoded" | |||
| 163 | */ | |||
| 164 | bool_Bool translate; | |||
| 165 | Tcl_Encoding encoding; | |||
| 166 | #ifdef _WIN32 | |||
| 167 | /* | |||
| 168 | * Keep CRLF | |||
| 169 | */ | |||
| 170 | translate = NS_FALSE0; | |||
| 171 | #else | |||
| 172 | /* | |||
| 173 | * Translate CRLF -> LF, since browsers translate all | |||
| 174 | * LF to CRLF in the body of POST requests. | |||
| 175 | */ | |||
| 176 | translate = NS_TRUE1; | |||
| 177 | #endif | |||
| 178 | if (charset != NULL((void*)0)) { | |||
| 179 | encoding = Ns_GetCharsetEncoding(charset); | |||
| 180 | } else { | |||
| 181 | encoding = connPtr->urlEncoding; | |||
| 182 | } | |||
| 183 | toParse = content; | |||
| 184 | status = ParseQueryWithFallback(interp, connPtr->poolPtr->servPtr, | |||
| 185 | content, connPtr->query, encoding, | |||
| 186 | translate, fallbackCharsetObj); | |||
| 187 | ||||
| 188 | } else if (GetBoundary(&boundaryDs, contentType)) { | |||
| 189 | /* | |||
| 190 | * GetBoundary cares for "multipart/form-data; boundary=...". | |||
| 191 | */ | |||
| 192 | const char *formEndPtr = content + connPtr->reqPtr->length; | |||
| 193 | char *firstBoundary = NextBoundary(&boundaryDs, content, formEndPtr), *s; | |||
| 194 | Tcl_Encoding valueEncoding = connPtr->urlEncoding; | |||
| 195 | ||||
| 196 | s = firstBoundary; | |||
| 197 | for (;;) { | |||
| 198 | const char *defaultCharset; | |||
| 199 | ||||
| 200 | while (s
| |||
| 201 | char *e; | |||
| 202 | ||||
| 203 | s += boundaryDs.length; | |||
| 204 | if (*s == '\r') { | |||
| 205 | ++s; | |||
| 206 | } | |||
| 207 | if (*s == '\n') { | |||
| 208 | ++s; | |||
| 209 | } | |||
| 210 | e = NextBoundary(&boundaryDs, s, formEndPtr); | |||
| 211 | if (e
| |||
| 212 | status = ParseMultipartEntry(connPtr, valueEncoding, s, e); | |||
| 213 | if (status == NS_ERROR) { | |||
| 214 | toParse = s; | |||
| 215 | } | |||
| 216 | } | |||
| 217 | s = e; | |||
| 218 | } | |||
| 219 | /* | |||
| 220 | * We have now parsed all form fields into | |||
| 221 | * connPtr->query. According to the HTML5 standard, we | |||
| 222 | * have to check for a form entry named "_charset_" | |||
| 223 | * specifying the "default charset". | |||
| 224 | * https://datatracker.ietf.org/doc/html/rfc7578#section-4.6 | |||
| 225 | */ | |||
| 226 | defaultCharset = Ns_SetGet(connPtr->query, "_charset_"); | |||
| 227 | if (defaultCharset != NULL((void*)0) && strcmp(defaultCharset, "utf-8") != 0) { | |||
| 228 | /* | |||
| 229 | * We got an explicit charset different from UTF-8. We | |||
| 230 | * have to reparse the input data. | |||
| 231 | */ | |||
| 232 | Tcl_Encoding defaultEncoding = Ns_GetCharsetEncoding(defaultCharset); | |||
| 233 | ||||
| 234 | if (valueEncoding != NULL((void*)0)) { | |||
| 235 | if (valueEncoding != defaultEncoding) { | |||
| 236 | valueEncoding = defaultEncoding; | |||
| 237 | s = firstBoundary; | |||
| 238 | Ns_SetTrunc(connPtr->query, 0u); | |||
| 239 | Ns_Log(Debug, "form: retry with default charset %s", defaultCharset); | |||
| 240 | continue; | |||
| 241 | } | |||
| 242 | } else { | |||
| 243 | Ns_Log(Error, "multipart form: invalid charset specified" | |||
| 244 | " inside of form '%s'", defaultCharset); | |||
| 245 | status = NS_ERROR; | |||
| 246 | } | |||
| 247 | } | |||
| 248 | break; | |||
| 249 | } | |||
| 250 | } | |||
| 251 | Tcl_DStringFree(&boundaryDs); | |||
| 252 | } | |||
| 253 | ||||
| 254 | if (status == NS_ERROR) { | |||
| 255 | Ns_Log(Warning, "formdata: could not parse '%s'", toParse); | |||
| 256 | Ns_ConnClearQuery(conn); | |||
| 257 | if (rcPtr != NULL((void*)0)) { | |||
| 258 | *rcPtr = status; | |||
| 259 | if (interp != NULL((void*)0)) { | |||
| 260 | Ns_TclPrintfResult(interp, | |||
| 261 | "cannot decode '%s'; contains invalid UTF-8", | |||
| 262 | toParse); | |||
| 263 | Tcl_SetErrorCode(interp, "NS_INVALID_UTF8", NULL((void*)0)); | |||
| 264 | } | |||
| 265 | } | |||
| 266 | return NULL((void*)0); | |||
| 267 | } | |||
| 268 | } | |||
| 269 | ||||
| 270 | return connPtr->query; | |||
| 271 | } | |||
| 272 | ||||
| 273 | ||||
| 274 | ||||
| 275 | /* | |||
| 276 | *---------------------------------------------------------------------- | |||
| 277 | * | |||
| 278 | * Ns_ConnClearQuery -- | |||
| 279 | * | |||
| 280 | * Release the any query set cached up from a previous call | |||
| 281 | * to Ns_ConnGetQuery. Useful if the query data requires | |||
| 282 | * reparsing, as when the encoding changes. | |||
| 283 | * | |||
| 284 | * Results: | |||
| 285 | * None | |||
| 286 | * | |||
| 287 | * Side effects: | |||
| 288 | * None. | |||
| 289 | * | |||
| 290 | *---------------------------------------------------------------------- | |||
| 291 | */ | |||
| 292 | ||||
| 293 | void | |||
| 294 | Ns_ConnClearQuery(Ns_Conn *conn) | |||
| 295 | { | |||
| 296 | Conn *connPtr; | |||
| 297 | ||||
| 298 | NS_NONNULL_ASSERT(conn != NULL)((void) (0)); | |||
| 299 | connPtr = (Conn *) conn; | |||
| 300 | ||||
| 301 | if (connPtr->query != NULL((void*)0)) { | |||
| 302 | const Tcl_HashEntry *hPtr; | |||
| 303 | Tcl_HashSearch search; | |||
| 304 | ||||
| 305 | Ns_SetTrunc(connPtr->query, 0); | |||
| 306 | connPtr->query = NULL((void*)0); | |||
| 307 | ||||
| 308 | hPtr = Tcl_FirstHashEntry(&connPtr->files, &search); | |||
| 309 | while (hPtr != NULL((void*)0)) { | |||
| 310 | FormFile *filePtr = Tcl_GetHashValue(hPtr)((hPtr)->clientData); | |||
| 311 | ||||
| 312 | if (filePtr->hdrObj != NULL((void*)0)) { | |||
| 313 | Tcl_DecrRefCount(filePtr->hdrObj)do { Tcl_Obj *_objPtr = (filePtr->hdrObj); if (_objPtr-> refCount-- <= 1) { TclFreeObj(_objPtr); } } while(0); | |||
| 314 | } | |||
| 315 | if (filePtr->offObj != NULL((void*)0)) { | |||
| 316 | Tcl_DecrRefCount(filePtr->offObj)do { Tcl_Obj *_objPtr = (filePtr->offObj); if (_objPtr-> refCount-- <= 1) { TclFreeObj(_objPtr); } } while(0); | |||
| 317 | } | |||
| 318 | if (filePtr->sizeObj != NULL((void*)0)) { | |||
| 319 | Tcl_DecrRefCount(filePtr->sizeObj)do { Tcl_Obj *_objPtr = (filePtr->sizeObj); if (_objPtr-> refCount-- <= 1) { TclFreeObj(_objPtr); } } while(0); | |||
| 320 | } | |||
| 321 | ns_free(filePtr); | |||
| 322 | ||||
| 323 | hPtr = Tcl_NextHashEntry(&search); | |||
| 324 | } | |||
| 325 | Tcl_DeleteHashTable(&connPtr->files); | |||
| 326 | Tcl_InitHashTable(&connPtr->files, TCL_STRING_KEYS(0)); | |||
| 327 | } | |||
| 328 | } | |||
| 329 | ||||
| 330 | ||||
| 331 | /* | |||
| 332 | *---------------------------------------------------------------------- | |||
| 333 | * | |||
| 334 | * Ns_QueryToSet -- | |||
| 335 | * | |||
| 336 | * Parse query data into a given Ns_Set. | |||
| 337 | * | |||
| 338 | * Results: | |||
| 339 | * NS_OK. | |||
| 340 | * | |||
| 341 | * Side effects: | |||
| 342 | * Will add data to set. | |||
| 343 | * | |||
| 344 | *---------------------------------------------------------------------- | |||
| 345 | */ | |||
| 346 | ||||
| 347 | Ns_ReturnCode | |||
| 348 | Ns_QueryToSet(char *query, Ns_Set *set, Tcl_Encoding encoding) | |||
| 349 | { | |||
| 350 | NS_NONNULL_ASSERT(query != NULL)((void) (0)); | |||
| 351 | NS_NONNULL_ASSERT(set != NULL)((void) (0)); | |||
| 352 | ||||
| 353 | return ParseQuery(query, set, encoding, NS_FALSE0); | |||
| 354 | } | |||
| 355 | ||||
| 356 | ||||
| 357 | /* | |||
| 358 | *---------------------------------------------------------------------- | |||
| 359 | * | |||
| 360 | * NsTclParseQueryObjCmd -- | |||
| 361 | * | |||
| 362 | * Implements "ns_parsequery". | |||
| 363 | * | |||
| 364 | * Results: | |||
| 365 | * The Tcl result is a Tcl set with the parsed name-value pairs from | |||
| 366 | * the querystring argument | |||
| 367 | * | |||
| 368 | * Side effects: | |||
| 369 | * None. | |||
| 370 | * | |||
| 371 | *---------------------------------------------------------------------- | |||
| 372 | */ | |||
| 373 | ||||
| 374 | int | |||
| 375 | NsTclParseQueryObjCmd(ClientData clientData, Tcl_Interp *interp, int objc, Tcl_Obj *const* objv) | |||
| 376 | { | |||
| 377 | int result; | |||
| 378 | NsInterp *itPtr = clientData; | |||
| 379 | char *charset = NULL((void*)0), *chars = (char *)NS_EMPTY_STRING; | |||
| 380 | Tcl_Obj *fallbackCharsetObj = NULL((void*)0); | |||
| 381 | Ns_ObjvSpec lopts[] = { | |||
| 382 | {"-charset", Ns_ObjvString, &charset, NULL((void*)0)}, | |||
| 383 | {"-fallbackcharset", Ns_ObjvObj, &fallbackCharsetObj, NULL((void*)0)}, | |||
| 384 | {"--", Ns_ObjvBreak, NULL((void*)0), NULL((void*)0)}, | |||
| 385 | {NULL((void*)0), NULL((void*)0), NULL((void*)0), NULL((void*)0)} | |||
| 386 | }; | |||
| 387 | Ns_ObjvSpec args[] = { | |||
| 388 | {"querystring", Ns_ObjvString, &chars, NULL((void*)0)}, | |||
| 389 | {NULL((void*)0), NULL((void*)0), NULL((void*)0), NULL((void*)0)} | |||
| 390 | }; | |||
| 391 | ||||
| 392 | if (Ns_ParseObjv(lopts, args, interp, 1, objc, objv) != NS_OK) { | |||
| 393 | result = TCL_ERROR1; | |||
| 394 | ||||
| 395 | } else { | |||
| 396 | Tcl_Encoding encoding; | |||
| 397 | Ns_Set *set = Ns_SetCreate(NS_SET_NAME_PARSEQ"parseq"); | |||
| 398 | ||||
| 399 | if (charset != NULL((void*)0)) { | |||
| 400 | encoding = Ns_GetCharsetEncoding(charset); | |||
| 401 | } else { | |||
| 402 | encoding = Ns_GetUrlEncoding(NULL((void*)0)); | |||
| 403 | } | |||
| 404 | ||||
| 405 | if (ParseQueryWithFallback(interp, itPtr->servPtr, | |||
| 406 | chars, set, encoding, | |||
| 407 | NS_FALSE0, fallbackCharsetObj)) { | |||
| 408 | Ns_TclPrintfResult(interp, "could not parse query: \"%s\"", chars); | |||
| 409 | Tcl_SetErrorCode(interp, "NS_INVALID_UTF8", NULL((void*)0)); | |||
| 410 | Ns_SetFree(set); | |||
| 411 | result = TCL_ERROR1; | |||
| 412 | } else { | |||
| 413 | result = Ns_TclEnterSet(interp, set, NS_TCL_SET_DYNAMIC); | |||
| 414 | } | |||
| 415 | } | |||
| 416 | return result; | |||
| 417 | } | |||
| 418 | ||||
| 419 | ||||
| 420 | /* | |||
| 421 | *---------------------------------------------------------------------- | |||
| 422 | * | |||
| 423 | * ParseQuery -- | |||
| 424 | * | |||
| 425 | * Parse the given form string for URL encoded key=value pairs, | |||
| 426 | * converting to UTF8 if given encoding is not NULL. | |||
| 427 | * | |||
| 428 | * Results: | |||
| 429 | * TCL_OK or TCL_ERROR in case parsing was not possible. | |||
| 430 | * | |||
| 431 | * Side effects: | |||
| 432 | * None. | |||
| 433 | * | |||
| 434 | *---------------------------------------------------------------------- | |||
| 435 | */ | |||
| 436 | ||||
| 437 | static Ns_ReturnCode | |||
| 438 | ParseQuery(char *form, Ns_Set *set, Tcl_Encoding encoding, bool_Bool translate) | |||
| 439 | { | |||
| 440 | Tcl_DString kds, vds, vds2; | |||
| 441 | char *p; | |||
| 442 | Ns_ReturnCode result = NS_OK; | |||
| 443 | ||||
| 444 | NS_NONNULL_ASSERT(form != NULL)((void) (0)); | |||
| 445 | NS_NONNULL_ASSERT(set != NULL)((void) (0)); | |||
| 446 | ||||
| 447 | Tcl_DStringInit(&kds); | |||
| 448 | Tcl_DStringInit(&vds); | |||
| 449 | Tcl_DStringInit(&vds2); | |||
| 450 | p = form; | |||
| 451 | ||||
| 452 | while (p != NULL((void*)0)) { | |||
| 453 | char *v; | |||
| 454 | const char *k; | |||
| 455 | ||||
| 456 | k = p; | |||
| 457 | p = strchr(p, INTCHAR('&')((int)((unsigned char)(('&'))))); | |||
| 458 | if (p != NULL((void*)0)) { | |||
| 459 | *p = '\0'; | |||
| 460 | } | |||
| 461 | v = strchr(k, INTCHAR('=')((int)((unsigned char)(('='))))); | |||
| 462 | if (v != NULL((void*)0)) { | |||
| 463 | *v = '\0'; | |||
| 464 | } | |||
| 465 | Ns_DStringSetLengthTcl_DStringSetLength(&kds, 0); | |||
| 466 | k = Ns_UrlQueryDecode(&kds, k, encoding, &result); | |||
| 467 | if (v != NULL((void*)0)) { | |||
| 468 | Ns_DStringSetLengthTcl_DStringSetLength(&vds, 0); | |||
| 469 | ||||
| 470 | (void) Ns_UrlQueryDecode(&vds, v+1, encoding, &result); | |||
| 471 | *v = '='; | |||
| 472 | v = vds.string; | |||
| 473 | if (translate) { | |||
| 474 | char *q = strchr(v, INTCHAR('\r')((int)((unsigned char)(('\r'))))); | |||
| 475 | ||||
| 476 | if (q != NULL((void*)0)) { | |||
| 477 | /* | |||
| 478 | * We have one or more CR in the field content. | |||
| 479 | * Remove these. | |||
| 480 | */ | |||
| 481 | Ns_DStringSetLengthTcl_DStringSetLength(&vds2, 0); | |||
| 482 | do { | |||
| 483 | Tcl_DStringAppend(&vds2, v, (int)(q - v)); | |||
| 484 | v = q +1; | |||
| 485 | q = strchr(v, INTCHAR('\r')((int)((unsigned char)(('\r'))))); | |||
| 486 | } while (q != NULL((void*)0)); | |||
| 487 | /* | |||
| 488 | * Append the remaining string. | |||
| 489 | */ | |||
| 490 | Tcl_DStringAppend(&vds2, v, -1); | |||
| 491 | v = vds2.string; | |||
| 492 | } | |||
| 493 | } | |||
| 494 | } | |||
| 495 | if (result == TCL_OK0) { | |||
| 496 | (void) Ns_SetPut(set, k, v); | |||
| 497 | } | |||
| 498 | if (p != NULL((void*)0)) { | |||
| 499 | *p++ = '&'; | |||
| 500 | } | |||
| 501 | } | |||
| 502 | Tcl_DStringFree(&kds); | |||
| 503 | Tcl_DStringFree(&vds); | |||
| 504 | Tcl_DStringFree(&vds2); | |||
| 505 | ||||
| 506 | return result; | |||
| 507 | } | |||
| 508 | ||||
| 509 | /* | |||
| 510 | *---------------------------------------------------------------------- | |||
| 511 | * | |||
| 512 | * ParseQueryWithFallback -- | |||
| 513 | * | |||
| 514 | * Helper function for ParseQuery(), which handles fallback charset for | |||
| 515 | * cases, where converting to UTF8 fails (due to invalid UTF-8). | |||
| 516 | * | |||
| 517 | * Results: | |||
| 518 | * TCL_OK or TCL_ERROR in case parsing was not possible. | |||
| 519 | * | |||
| 520 | * Side effects: | |||
| 521 | * None. | |||
| 522 | * | |||
| 523 | *---------------------------------------------------------------------- | |||
| 524 | */ | |||
| 525 | static Ns_ReturnCode | |||
| 526 | ParseQueryWithFallback(Tcl_Interp *interp, NsServer *servPtr, char *toParse, | |||
| 527 | Ns_Set *set, Tcl_Encoding encoding, | |||
| 528 | bool_Bool translate, Tcl_Obj *fallbackCharsetObj) | |||
| 529 | { | |||
| 530 | Ns_ReturnCode status; | |||
| 531 | ||||
| 532 | NS_NONNULL_ASSERT(interp != NULL)((void) (0)); | |||
| 533 | NS_NONNULL_ASSERT(toParse != NULL)((void) (0)); | |||
| 534 | NS_NONNULL_ASSERT(set != NULL)((void) (0)); | |||
| 535 | ||||
| 536 | status = ParseQuery(toParse, set, encoding, translate); | |||
| 537 | if (status == NS_ERROR) { | |||
| 538 | Tcl_Encoding fallbackEncoding = NULL((void*)0); | |||
| 539 | Ns_ReturnCode rc; | |||
| 540 | ||||
| 541 | /* | |||
| 542 | * ParseQuery failed. This might be due to invalid UTF-8. Retry with | |||
| 543 | * fallbackCharset if specified. | |||
| 544 | */ | |||
| 545 | rc = NsGetFallbackEncoding(interp, servPtr, fallbackCharsetObj, NS_TRUE1, &fallbackEncoding); | |||
| 546 | if (rc == NS_OK && fallbackEncoding != NULL((void*)0) && fallbackEncoding != encoding) { | |||
| 547 | Ns_Log(Notice, "Retry ParseQuery with encoding %s", | |||
| 548 | Ns_GetEncodingCharset(fallbackEncoding)); | |||
| 549 | Ns_SetTrunc(set, 0u); | |||
| 550 | status = ParseQuery(toParse, set, fallbackEncoding, translate); | |||
| 551 | } | |||
| 552 | } | |||
| 553 | return status; | |||
| 554 | } | |||
| 555 | ||||
| 556 | ||||
| 557 | /* | |||
| 558 | *---------------------------------------------------------------------- | |||
| 559 | * | |||
| 560 | * ParseMultipartEntry -- | |||
| 561 | * | |||
| 562 | * Parse a single part of a multipart form. | |||
| 563 | * | |||
| 564 | * Results: | |||
| 565 | * Ns_ReturnCode (NS_OK or NS_ERROR). | |||
| 566 | * | |||
| 567 | * Side effects: | |||
| 568 | * Records offset, lengths for files. After execution, connPtr->query | |||
| 569 | * contains the parsed form in form of an Ns_Set. | |||
| 570 | * | |||
| 571 | *---------------------------------------------------------------------- | |||
| 572 | */ | |||
| 573 | ||||
| 574 | static Ns_ReturnCode | |||
| 575 | ParseMultipartEntry(Conn *connPtr, Tcl_Encoding valueEncoding, const char *start, char *end) | |||
| 576 | { | |||
| 577 | Tcl_Encoding encoding; | |||
| 578 | Tcl_DString kds, vds; | |||
| 579 | char *e, saveend, unescape; | |||
| 580 | const char *ks = NULL((void*)0), *ke, *disp; | |||
| 581 | Ns_Set *set; | |||
| 582 | int isNew; | |||
| 583 | Ns_ReturnCode status = NS_OK; | |||
| 584 | ||||
| 585 | NS_NONNULL_ASSERT(connPtr != NULL)((void) (0)); | |||
| 586 | NS_NONNULL_ASSERT(start != NULL)((void) (0)); | |||
| 587 | NS_NONNULL_ASSERT(end != NULL)((void) (0)); | |||
| 588 | ||||
| 589 | encoding = connPtr->urlEncoding; | |||
| 590 | ||||
| 591 | Tcl_DStringInit(&kds); | |||
| 592 | Tcl_DStringInit(&vds); | |||
| 593 | set = Ns_SetCreate(NS_SET_NAME_MP"mp"); | |||
| 594 | ||||
| 595 | /* | |||
| 596 | * Trim off the trailing \r\n and null terminate the input. | |||
| 597 | */ | |||
| 598 | ||||
| 599 | if (end
| |||
| 600 | --end; | |||
| 601 | } | |||
| 602 | if (end
| |||
| 603 | --end; | |||
| 604 | } | |||
| 605 | saveend = *end; | |||
| 606 | *end = '\0'; | |||
| 607 | ||||
| 608 | /* | |||
| 609 | * Parse header lines | |||
| 610 | */ | |||
| 611 | ||||
| 612 | while ((e = strchr(start, INTCHAR('\n')((int)((unsigned char)(('\n')))))) != NULL((void*)0)) { | |||
| 613 | const char *s = start; | |||
| 614 | char save; | |||
| 615 | ||||
| 616 | start = e + 1; | |||
| 617 | if (e > s && *(e-1) == '\r') { | |||
| ||||
| 618 | --e; | |||
| 619 | } | |||
| 620 | if (s == e) { | |||
| 621 | /* | |||
| 622 | * Reached empty line, end of header. | |||
| 623 | */ | |||
| 624 | break; | |||
| 625 | } | |||
| 626 | save = *e; | |||
| 627 | *e = '\0'; | |||
| 628 | (void) Ns_ParseHeader(set, s, NULL((void*)0), ToLower, NULL((void*)0)); | |||
| 629 | *e = save; | |||
| 630 | } | |||
| 631 | ||||
| 632 | /* | |||
| 633 | * Look for valid disposition header. | |||
| 634 | */ | |||
| 635 | ||||
| 636 | disp = Ns_SetGet(set, "content-disposition"); | |||
| 637 | if (disp != NULL((void*)0) && GetValue(disp, "name=", &ks, &ke, &unescape) == NS_TRUE1) { | |||
| 638 | const char *key = Ext2utf(&kds, ks, (size_t)(ke - ks), encoding, unescape); | |||
| 639 | const char *value, *fs = NULL((void*)0), *fe = NULL((void*)0); | |||
| 640 | ||||
| 641 | if (key == NULL((void*)0)) { | |||
| 642 | status = NS_ERROR; | |||
| 643 | goto bailout; | |||
| 644 | } | |||
| 645 | Ns_Log(Debug, "ParseMultipartEntry disp '%s'", disp); | |||
| 646 | ||||
| 647 | if (GetValue(disp, "filename=", &fs, &fe, &unescape) == NS_FALSE0) { | |||
| 648 | /* | |||
| 649 | * Plain (non-file) entry. | |||
| 650 | */ | |||
| 651 | if (valueEncoding == NULL((void*)0)) { | |||
| 652 | valueEncoding = encoding; | |||
| 653 | } | |||
| 654 | Ns_Log(Debug, "ParseMultipartEntry LINE '%s'", start); | |||
| 655 | value = Ext2utf(&vds, start, (size_t)(end - start), valueEncoding, unescape); | |||
| 656 | if (value == NULL((void*)0)) { | |||
| 657 | status = NS_ERROR; | |||
| 658 | goto bailout; | |||
| 659 | } | |||
| 660 | } else { | |||
| 661 | Tcl_HashEntry *hPtr; | |||
| 662 | FormFile *filePtr; | |||
| 663 | Tcl_Interp *interp = connPtr->itPtr->interp; | |||
| 664 | ||||
| 665 | assert(fs != NULL)((void) (0)); | |||
| 666 | value = Ext2utf(&vds, fs, (size_t)(fe - fs), encoding, unescape); | |||
| 667 | if (value == NULL((void*)0)) { | |||
| 668 | status = NS_ERROR; | |||
| 669 | goto bailout; | |||
| 670 | } | |||
| 671 | ||||
| 672 | hPtr = Tcl_CreateHashEntry(&connPtr->files, key, &isNew)(*((&connPtr->files)->createProc))(&connPtr-> files, (const char *)(key), &isNew); | |||
| 673 | if (isNew != 0) { | |||
| 674 | ||||
| 675 | filePtr = ns_malloc(sizeof(FormFile)); | |||
| 676 | Tcl_SetHashValue(hPtr, filePtr)((hPtr)->clientData = (ClientData) (filePtr)); | |||
| 677 | ||||
| 678 | filePtr->hdrObj = Tcl_NewListObj(0, NULL((void*)0)); | |||
| 679 | filePtr->offObj = Tcl_NewListObj(0, NULL((void*)0)); | |||
| 680 | filePtr->sizeObj = Tcl_NewListObj(0, NULL((void*)0)); | |||
| 681 | ||||
| 682 | Tcl_IncrRefCount(filePtr->hdrObj)++(filePtr->hdrObj)->refCount; | |||
| 683 | Tcl_IncrRefCount(filePtr->offObj)++(filePtr->offObj)->refCount; | |||
| 684 | Tcl_IncrRefCount(filePtr->sizeObj)++(filePtr->sizeObj)->refCount; | |||
| 685 | } else { | |||
| 686 | filePtr = Tcl_GetHashValue(hPtr)((hPtr)->clientData); | |||
| 687 | } | |||
| 688 | ||||
| 689 | (void) Ns_TclEnterSet(interp, set, NS_TCL_SET_DYNAMIC); | |||
| 690 | (void) Tcl_ListObjAppendElement(interp, filePtr->hdrObj, | |||
| 691 | Tcl_GetObjResult(interp)); | |||
| 692 | Tcl_ResetResult(connPtr->itPtr->interp); | |||
| 693 | ||||
| 694 | (void) Tcl_ListObjAppendElement(interp, filePtr->offObj, | |||
| 695 | Tcl_NewIntObj((int)(start - connPtr->reqPtr->content))); | |||
| 696 | ||||
| 697 | (void) Tcl_ListObjAppendElement(interp, filePtr->sizeObj, | |||
| 698 | Tcl_NewWideIntObj((Tcl_WideInt)(end - start))); | |||
| 699 | set = NULL((void*)0); | |||
| 700 | } | |||
| 701 | Ns_Log(Debug, "ParseMultipartEntry sets '%s': '%s'", key, value); | |||
| 702 | (void) Ns_SetPut(connPtr->query, key, value); | |||
| 703 | } | |||
| 704 | ||||
| 705 | /* | |||
| 706 | * Restore the end marker. | |||
| 707 | */ | |||
| 708 | bailout: | |||
| 709 | *end = saveend; | |||
| 710 | Tcl_DStringFree(&kds); | |||
| 711 | Tcl_DStringFree(&vds); | |||
| 712 | if (set != NULL((void*)0)) { | |||
| 713 | Ns_SetFree(set); | |||
| 714 | } | |||
| 715 | ||||
| 716 | return status; | |||
| 717 | } | |||
| 718 | ||||
| 719 | ||||
| 720 | /* | |||
| 721 | *---------------------------------------------------------------------- | |||
| 722 | * | |||
| 723 | * GetBoundary -- | |||
| 724 | * | |||
| 725 | * Copy multipart/form-data boundary string, if any. | |||
| 726 | * | |||
| 727 | * Results: | |||
| 728 | * NS_TRUE if boundary copied, NS_FALSE otherwise. | |||
| 729 | * | |||
| 730 | * Side effects: | |||
| 731 | * Copies boundary string to given dstring. | |||
| 732 | * | |||
| 733 | *---------------------------------------------------------------------- | |||
| 734 | */ | |||
| 735 | ||||
| 736 | static bool_Bool | |||
| 737 | GetBoundary(Tcl_DString *dsPtr, const char *contentType) | |||
| 738 | { | |||
| 739 | const char *bs; | |||
| 740 | bool_Bool success = NS_FALSE0; | |||
| 741 | ||||
| 742 | NS_NONNULL_ASSERT(dsPtr != NULL)((void) (0)); | |||
| 743 | NS_NONNULL_ASSERT(contentType != NULL)((void) (0)); | |||
| 744 | ||||
| 745 | if ((Ns_StrCaseFind(contentType, "multipart/form-data") != NULL((void*)0)) | |||
| 746 | && ((bs = Ns_StrCaseFind(contentType, "boundary=")) != NULL((void*)0))) { | |||
| 747 | const char *be; | |||
| 748 | ||||
| 749 | bs += 9; | |||
| 750 | be = bs; | |||
| 751 | while ((*be != '\0') && (CHARTYPE(space, *be)(((*__ctype_b_loc ())[(int) (((int)((unsigned char)(*be))))] & (unsigned short int) _ISspace)) == 0)) { | |||
| 752 | ++be; | |||
| 753 | } | |||
| 754 | Tcl_DStringAppend(dsPtr, "--", 2); | |||
| 755 | Tcl_DStringAppend(dsPtr, bs, (int)(be - bs)); | |||
| 756 | success = NS_TRUE1; | |||
| 757 | } | |||
| 758 | return success; | |||
| 759 | } | |||
| 760 | ||||
| 761 | ||||
| 762 | /* | |||
| 763 | *---------------------------------------------------------------------- | |||
| 764 | * | |||
| 765 | * NextBoundary -- | |||
| 766 | * | |||
| 767 | * Locate the next form boundary. | |||
| 768 | * | |||
| 769 | * Results: | |||
| 770 | * Pointer to start of next input field or NULL on end of fields. | |||
| 771 | * | |||
| 772 | * Side effects: | |||
| 773 | * None. | |||
| 774 | * | |||
| 775 | *---------------------------------------------------------------------- | |||
| 776 | */ | |||
| 777 | ||||
| 778 | static char * | |||
| 779 | NextBoundary(const Tcl_DString *dsPtr, char *s, const char *e) | |||
| 780 | { | |||
| 781 | char c, sc; | |||
| 782 | const char *find; | |||
| 783 | size_t len; | |||
| 784 | ||||
| 785 | NS_NONNULL_ASSERT(dsPtr != NULL)((void) (0)); | |||
| 786 | NS_NONNULL_ASSERT(s != NULL)((void) (0)); | |||
| 787 | NS_NONNULL_ASSERT(e != NULL)((void) (0)); | |||
| 788 | ||||
| 789 | find = dsPtr->string; | |||
| 790 | c = *find++; | |||
| 791 | len = (size_t)(dsPtr->length - 1); | |||
| 792 | e -= len; | |||
| 793 | do { | |||
| 794 | do { | |||
| 795 | sc = *s++; | |||
| 796 | if (s > e) { | |||
| 797 | return NULL((void*)0); | |||
| 798 | } | |||
| 799 | } while (sc != c); | |||
| 800 | } while (strncmp(s, find, len) != 0); | |||
| 801 | s--; | |||
| 802 | ||||
| 803 | return s; | |||
| 804 | } | |||
| 805 | ||||
| 806 | ||||
| 807 | /* | |||
| 808 | *---------------------------------------------------------------------- | |||
| 809 | * | |||
| 810 | * GetValue -- | |||
| 811 | * | |||
| 812 | * Determine start and end of a multipart form input value. | |||
| 813 | * | |||
| 814 | * Results: | |||
| 815 | * NS_TRUE if attribute found and value parsed, NS_FALSE otherwise. | |||
| 816 | * | |||
| 817 | * Side effects: | |||
| 818 | * Start and end are stored in given pointers, quoted character, | |||
| 819 | * when it was preceded by a backslash. | |||
| 820 | * | |||
| 821 | *---------------------------------------------------------------------- | |||
| 822 | */ | |||
| 823 | ||||
| 824 | static bool_Bool | |||
| 825 | GetValue(const char *hdr, const char *att, const char **vsPtr, const char **vePtr, char *uPtr) | |||
| 826 | { | |||
| 827 | const char *s; | |||
| 828 | bool_Bool success = NS_TRUE1; | |||
| 829 | ||||
| 830 | NS_NONNULL_ASSERT(hdr != NULL)((void) (0)); | |||
| 831 | NS_NONNULL_ASSERT(att != NULL)((void) (0)); | |||
| 832 | NS_NONNULL_ASSERT(vsPtr != NULL)((void) (0)); | |||
| 833 | NS_NONNULL_ASSERT(vePtr != NULL)((void) (0)); | |||
| 834 | NS_NONNULL_ASSERT(uPtr != NULL)((void) (0)); | |||
| 835 | ||||
| 836 | s = Ns_StrCaseFind(hdr, att); | |||
| 837 | if (s == NULL((void*)0)) { | |||
| 838 | success = NS_FALSE0; | |||
| 839 | } else { | |||
| 840 | const char *e; | |||
| 841 | ||||
| 842 | s += strlen(att); | |||
| 843 | e = s; | |||
| 844 | if (*s != '"' && *s != '\'') { | |||
| 845 | /* | |||
| 846 | * End of unquoted att=value is next space. | |||
| 847 | */ | |||
| 848 | while (*e != '\0' && CHARTYPE(space, *e)(((*__ctype_b_loc ())[(int) (((int)((unsigned char)(*e))))] & (unsigned short int) _ISspace)) == 0) { | |||
| 849 | ++e; | |||
| 850 | } | |||
| 851 | *uPtr = '\0'; | |||
| 852 | } else { | |||
| 853 | bool_Bool escaped = NS_FALSE0; | |||
| 854 | ||||
| 855 | *uPtr = '\0'; | |||
| 856 | /* | |||
| 857 | * End of quoted att="value" is next quote. A quote within | |||
| 858 | * the quoted string could be escaped with a backslash. In | |||
| 859 | * case, an escaped quote was detected, report the quote | |||
| 860 | * character as result. | |||
| 861 | */ | |||
| 862 | ++e; | |||
| 863 | while (*e != '\0' && (escaped || *e != *s)) { | |||
| 864 | if (escaped) { | |||
| 865 | escaped = NS_FALSE0; | |||
| 866 | } else if (*e == '\\') { | |||
| 867 | *uPtr = *s; | |||
| 868 | escaped = NS_TRUE1; | |||
| 869 | } | |||
| 870 | ++e; | |||
| 871 | } | |||
| 872 | ++s; | |||
| 873 | } | |||
| 874 | *vsPtr = s; | |||
| 875 | *vePtr = e; | |||
| 876 | } | |||
| 877 | ||||
| 878 | return success; | |||
| 879 | } | |||
| 880 | ||||
| 881 | ||||
| 882 | /* | |||
| 883 | *---------------------------------------------------------------------- | |||
| 884 | * | |||
| 885 | * Ext2utf -- | |||
| 886 | * | |||
| 887 | * Convert input string to UTF. | |||
| 888 | * | |||
| 889 | * Results: | |||
| 890 | * Pointer to converted string or NULL, when conversion to UTF-8 fails. | |||
| 891 | * | |||
| 892 | * Side effects: | |||
| 893 | * Converted string is copied to given dString, overwriting | |||
| 894 | * any previous content. | |||
| 895 | * | |||
| 896 | *---------------------------------------------------------------------- | |||
| 897 | */ | |||
| 898 | ||||
| 899 | static char * | |||
| 900 | Ext2utf(Tcl_DString *dsPtr, const char *start, size_t len, Tcl_Encoding encoding, char unescape) | |||
| 901 | { | |||
| 902 | char *buffer; | |||
| 903 | ||||
| 904 | NS_NONNULL_ASSERT(dsPtr != NULL)((void) (0)); | |||
| 905 | NS_NONNULL_ASSERT(start != NULL)((void) (0)); | |||
| 906 | ||||
| 907 | if (encoding == NULL((void*)0)) { | |||
| 908 | Tcl_DStringSetLength(dsPtr, 0); | |||
| 909 | Tcl_DStringAppend(dsPtr, start, (int)len); | |||
| 910 | buffer = dsPtr->string; | |||
| 911 | } else { | |||
| 912 | Tcl_DString ds; | |||
| 913 | /* | |||
| 914 | * Actual to UTF conversion. | |||
| 915 | */ | |||
| 916 | if (NsEncodingIsUtf8(encoding) && !Ns_Valid_UTF8((const unsigned char *)start, len, &ds)) { | |||
| 917 | Ns_Log(Warning, "form: multipart contains invalid UTF8: %s", ds.string); | |||
| 918 | Tcl_DStringFree(&ds); | |||
| 919 | buffer = NULL((void*)0); | |||
| 920 | } else { | |||
| 921 | /* | |||
| 922 | * ExternalToUtfDString will re-init dstring. | |||
| 923 | */ | |||
| 924 | Tcl_DStringFree(dsPtr); | |||
| 925 | (void) Tcl_ExternalToUtfDString(encoding, start, (int)len, dsPtr); | |||
| 926 | buffer = dsPtr->string; | |||
| 927 | } | |||
| 928 | } | |||
| 929 | ||||
| 930 | /* | |||
| 931 | * In case the string contains backslash escaped characters, the | |||
| 932 | * backslashes have to be removed. This will shorten the resulting | |||
| 933 | * string. | |||
| 934 | */ | |||
| 935 | if (buffer != NULL((void*)0) && unescape != '\0') { | |||
| 936 | int i, j, l = (int)len; | |||
| 937 | ||||
| 938 | for (i = 0; i<l; i++) { | |||
| 939 | if (buffer[i] == '\\' && buffer[i+1] == unescape) { | |||
| 940 | for (j = i; j < l; j++) { | |||
| 941 | buffer[j] = buffer[j+1]; | |||
| 942 | } | |||
| 943 | l --; | |||
| 944 | } | |||
| 945 | } | |||
| 946 | Tcl_DStringSetLength(dsPtr, l); | |||
| 947 | buffer = dsPtr->string; | |||
| 948 | } | |||
| 949 | ||||
| 950 | return buffer; | |||
| 951 | } | |||
| 952 | ||||
| 953 | /* | |||
| 954 | * Local Variables: | |||
| 955 | * mode: c | |||
| 956 | * c-basic-offset: 4 | |||
| 957 | * fill-column: 78 | |||
| 958 | * indent-tabs-mode: nil | |||
| 959 | * End: | |||
| 960 | */ |