Enclosed is a fix for multipart form handling.
As noted earlier, it checks for content-length.
This change set also makes multipart form handling an HttpRequest duty
(which is similar to the way it is done in swiki.net, but not quite).
And now it handles form types other than file (text, etc.)

Since this is a developer change set, not much is to be said here.
(Note that it's not the full Comanche 4.7 package)
Feel free to test, though!

Bolot

'From Squeak2.8alpha of 8 February 2000 [latest update: #2005] on 9 November 2000 at 
6:59:34 pm'!
"Change Set:          Comanche47-multipart2
Date:                     9 
November 2000
Author:                 Bolot Kerimbaev

This is a second pass at fixing 
multipart stuff.
Now, all multipart form handling is done in the HttpRequest (see 
section 'multipart - bolot'). See the example in MultipartFormModule >> 
processUsingRequestStuff:.

This will eventually be added to Comanche 4.7"!

NetworkRequest subclass: #HttpRequest
  instanceVariableNames: 'url stream 
responseHeader operation header '
   classVariableNames: 'EndOfRequestMarker 
HttpStatusCodeDict '
   poolDictionaries: ''
   category: 'Comanche-HTTP Support'!
Smalltalk renameClassNamed: #MultipartChunkHeader as: #MultipartChunk!
Object 
subclass: #MultipartChunk
     instanceVariableNames: 'header properties 
multipartBoundary request '
  classVariableNames: ''
 poolDictionaries: ''
   
category: 'Comanche-Server'!

!MultipartChunk commentStamp: 'bolot 10/25/2000 17:18' 
prior: 0!
!
!
MultipartChunkHeader
- this is a hack (bolot 10/25/2000 17:17)
- store a part's 
(from a multipart message) header information:
-- header (raw)
-- properties 
(extracted and converted info, such as file-name, content-length, etc.)!

!BufferStream methodsFor: 'as yet unclassified' stamp: 'bolot 10/25/2000 16:58'!
contents
     "return contents, non-destructive"
     ^buffer copyFrom: startPos to: 
endPos! !

!BufferStream methodsFor: 'as yet unclassified' stamp: 'bolot 10/25/2000 
17:02'!
includes: aChar
     "answer whether buffer includes aChar within bounds"
   | 
tmp |
        tmp _ buffer indexOf: aChar startingAt: startPos.
      ^(tmp >= 
startPos) and: [tmp <= endPos]! !

!BufferStream methodsFor: 'as yet unclassified' 
stamp: 'bolot 10/25/2000 17:00'!
includesAll: aString
      "answer whether the buffer 
contains aString"
   | tmp |
        tmp _ buffer findString: aString startingAt: 
startPos.
 ^(tmp >= startPos) and: [tmp <= endPos]! !

!BufferStream methodsFor: 'as 
yet unclassified' stamp: 'bolot 10/25/2000 16!
!
:59'!
next
     "return the next character"
    "consume it"
   ^buffer at: (startPos 
:_ startPos + 1) - 1! !

!BufferStream methodsFor: 'as yet unclassified' stamp: 'bolot 
:10/25/2000 16:58'!
next: anInteger
 "return next anInteger characters"
     "consumes 
:them"
        ^buffer copyFrom: startPos to: (startPos _ startPos + anInteger) - 1! !
:
!BufferStream methodsFor: 'as yet unclassified' stamp: 'bolot 10/25/2000 14:59'!
:printOn: aStream
     super printOn: aStream.
        aStream nextPutAll: '(', self 
:size asString, ')'! !

!BufferStream methodsFor: 'as yet unclassified' stamp: 'bolot 
:10/25/2000 16:58'!
upTo: aChar
      "return String up to aChar, but not including"
 
:"consumes the returned string"
 | i |
  i _ buffer indexOf: aChar startingAt: 
:startPos.
        (i <= 0 or: [i > endPos])
              ifTrue: [^self upToEnd].
   
:    ^self next: (i-startPos)! !

!BufferStream methodsFor: 'as yet unclassified' 
:stamp: 'bolot 10/25/2000 17:01'!
upToAll: aString
 "return a string before the 
:occurence of aString, if any"
      "consume it"
   | i !
!
|
      i _ buffer findString: aString startingAt: startPos.
   (i <= 0 or: [i > 
|endPos])
              ifTrue: [^self upToEnd].
       ^self next: (i-startPos)! !

|!BufferStream methodsFor: 'as yet unclassified' stamp: 'bolot 10/25/2000 17:03'!
|upToEnd
  "return buffer contents,
       consume all my buffer"
 | string |
     
|string _ self contents.
        self reset.
    ^string! !


!HttpRequest methodsFor: 
|'multipart - bolot' stamp: 'bolot 11/9/2000 18:47'!
multipartFormFieldsDo: aBlock
    
|    "USAGE: request multipartFormFieldsDo: [:chunk |
               chunk 
|saveToStream: aStream]."
 "NOTE: if the chunk is not saved, save it after aBlock"

   
|    | mChunk aFieldValue |
 (self method = 'POST' and: [self contentType = 
|MIMEDocument contentTypeMultipart])
             ifFalse: [^nil].

      [mChunk _ 
|self nextChunkHeader.
        aBlock value: mChunk.
  mChunk flag: #todo.
    mChunk 
|flagIsSaved
             ifFalse: [
                     mChunk fileName isEmptyOrNil
|                           ifFalse: [mChunk saveToStream: (DummyStream on: String 
|new)]
                           ifTrue: [
                                      
|aFieldValue _ String streamContents: [:aStream!
!
 |
                                             mChunk saveToStream: aStream].
         
 |                        self postFields at: mChunk fieldName put: aFieldValue
        
 |                  ]
              ].
     stream inDataCount - (self propertyAt: 
 |#headerBytes ifAbsent: [0]) >= self contentLength]
      whileFalse! !

!HttpRequest 
 |methodsFor: 'multipart - bolot' stamp: 'bolot 11/9/2000 18:32'!
nextChunkHeader
    
 |"Read the next multipart data chunk's header"

 | string dict mChunk |
 string _ 
 |stream upToAll: String crlf, String crlf.
self flag: #todo.
Transcript show: 'HEADER: 
 |', string; cr.
  dict _ HttpRequest fastParseHeader: string.
    stream next: 4. 
 |"ignore CRLFCRLF"
      mChunk _ MultipartChunk from: dict.
    mChunk request: self.
 |  mChunk multipartBoundary: self multipartBoundary.
      ^mChunk! !

!HttpRequest 
 |methodsFor: 'private' stamp: 'bolot 11/9/2000 17:46'!
pvtReadRequestFrom: aStream
    
 | | headerString statusString statusLine i |

    headerString _ aStream upToAll: self 
 |endOfRequestMarker.
       i _ headerString findString: String crlf.
      i = 0
     
 |     ifFalse:
                       [statusString _ headerString cop!
!
yFrom: 1 to: i-1.
                      headerString _ headerString copyFrom: i+2 to: 
headerString size]
               ifTrue:
                        [statusString _ 
headerString.
                  headerString _ ''].
    statusLine _ statusString 
findTokens: ' '.

    "skip CRLFCRLF"  aStream next: (self endOfRequestMarker size).

 
       statusLine size = 3
            ifTrue: [self pvtMethod: statusLine first.
     
                url _ statusLine second.
                       self pvtProtocol: 
statusLine third]
            ifFalse: [self error: 'Bad request'].

 self pvtHeader: 
(HttpAdaptor parseHttpHeader: headerString).
self flag: #todo. "remember the size of 
the header"
       self propertyAt: #headerBytes put: stream inDataCount.

        
self postFields. "force reading/parsing post fields"! !


!MultipartChunk methodsFor: 
'as yet unclassified' stamp: 'bolot 10/25/2000 17:23'!
contentType
       ^properties 
at: #contentType! !

!MultipartChunk methodsFor: 'as yet unclassified' stamp: 'bolot 
10/25/2000 17:23'!
fieldName
  ^properties at: #fieldName! !

!MultipartChunk 
methodsFor: 'as yet unclassified' stamp: 'bolot 10/25/2!
!
000 17:23'!
fileName
   ^properties at: #fileName! !

!MultipartChunk methodsFor: 'as 
yet unclassified' stamp: 'bolot 10/25/2000 17:34'!
fileName: aString
     properties 
at: #fileName put: aString! !

!MultipartChunk methodsFor: 'as yet unclassified' 
stamp: 'bolot 11/9/2000 18:03'!
flagIsSaved
        ^(properties at: #isSaved 
ifAbsent: [false]) == true! !

!MultipartChunk methodsFor: 'as yet unclassified' 
stamp: 'bolot 11/9/2000 18:03'!
flagSetIsSaved
      properties at: #isSaved put: 
true! !

!MultipartChunk methodsFor: 'as yet unclassified' stamp: 'bolot 10/25/2000 
17:22'!
initializeFrom: aDictionary
   | str fileName fieldName idx contentType |
     
"PRE: all keys in aDictionary are low-case"
    header _ aDictionary.
  properties _ 
Dictionary new.
self flag: #todo.
 "parse the header, pick out: filename, etc."


 str 
_ (header at: 'content-disposition' ifAbsent: ['']).

      fileName _ fieldName _ ''.

    idx _ str findString: 'filename='.
     idx > 0 ifTrue: [fileName _ str copyFrom: 
idx + 10 to: (str ind!
!
exOf: $" startingAt: idx+10) - 1].
     properties at: #fileName put: fileName.

      
 idx _ str findString: 'name='.
 idx > 0 ifTrue: [fieldName _ str copyFrom: idx + 6 
to: (str indexOf: $" startingAt: idx+6) - 1].
       properties at: #fieldName put: 
fieldName.

     contentType _ header at: 'content-type' ifAbsent: [MIMEDocument 
defaultContentType].
   properties at: #contentType put: contentType.

Transcript 
show: 'field=', fieldName; cr; show: 'file=', fileName; cr;
  show: 'content-type=', 
contentType; cr.

! !

!MultipartChunk methodsFor: 'as yet unclassified' stamp: 'bolot 
10/25/2000 17:45'!
multipartBoundary
     ^multipartBoundary! !

!MultipartChunk 
methodsFor: 'as yet unclassified' stamp: 'bolot 10/25/2000 17:45'!
multipartBoundary: 
aString
   multipartBoundary _ aString! !

!MultipartChunk methodsFor: 'as yet 
unclassified' stamp: 'bolot 11/9/2000 18:01'!
request: aRequest
    request _ 
aRequest! !

!MultipartChunk methodsFor: 'as yet unclassified' stamp: 'bolot 11/9/2000 
18:03'!
saveToStream: !
!
outStream
      "Save this chunk's body in outStream"
  "the caller is responsible for 
closing outStream (if applicable)"
      | body origFileName idx str |

 self 
flagSetIsSaved.

  origFileName _ self fileName.

 body _ request stream nextChunk.
   
    "IE4 for Mac appends 128 bytes of Mac file system info - must remove"
  "this fix 
assumes that body size >= 128"
       ((body at: 1) asciiValue = 0
                   
and: [(body at: 2) asciiValue = origFileName size
                              and: 
[(body copyFrom: 3 to: origFileName size + 2) = origFileName]])
           ifTrue: 
[body _ body copyFrom: 129 to: body size].
     [(idx _ body findString: request 
multipartBoundary) = 0]
               whileTrue:
                     [idx _ body 
size - request multipartBoundary size + 1 max: 1.
                  str _ request 
stream nextChunk.
                        outStream nextPutAll: (body copyFrom: 1 to: 
idx - 1).
                  body _ (body copyFrom: idx to: body size) , str].
      " 
save the last chunk in the file "
    outStream nextPutAll: (body copyFrom: 1 to: 
idx-5). " idx-1 - String crlf size "
       request stream putDataBack: (bo!
!
dy copyFrom: idx-4 to: body size).! !


!MultipartChunk class methodsFor: 'instance 
creation' stamp: 'bolot 10/25/2000 17:26'!
from: aDictionary
       aDictionary 
isEmptyOrNil
               ifTrue: [^nil].
        ^self new initializeFrom: 
aDictionary! !


!MultipartPostModule methodsFor: 'processing' stamp: 'bolot 
10/25/2000 17:36'!
openFileForChunk: mChunk
     "return a stream"
      | fileName 
file |
      fileName _ mChunk fileName.

   (fileName size > 0 and: [Comanche 
allowUploads])
               ifTrue: [fileName _ (fileName findTokens: ':/\') last.
  
               fileName _ (FileDirectory checkName: fileName fixErrors: true) 
unescapePercents.
                       Comanche caseSensitiveFilenames
              
                  ifTrue: [fileName _ fileName asLowercase].
                     
workDir deleteFileNamed: fileName ifAbsent: [].
                        file _ workDir 
fileNamed: fileName]
            ifFalse: [file _ DummyStream on: String new. fileName 
_ nil].

 mChunk fileName: fileName.
     ^file! !

!MultipartPostModule methodsFor: 
'processing' stamp: 'bolot 11/9/2000 18:47'!
process: request
       " process 
multipart for!
!
m data "
       " save files in the work directory "
" TODO: save other form data "
   
 | mChunk headerBytes fieldName contentType formDict doc |
self flag: #todo. "allow 
saving elsewhere first"
     (request method = 'POST' and: [request contentType = 
MIMEDocument contentTypeMultipart])
               ifFalse: [^nil].

      (self 
processUsingRequestStuff: request)
               ifNotNil: [^nil].

     headerBytes 
_ request stream inDataCount.
      formDict _ Dictionary new.
"Transcript show: 
'Boundary = ', boundary; cr."
     [
Transcript show: 'got ', request stream 
inDataCount asString, '/', request contentLength asString, ' bytes'; cr.
     mChunk _ 
self readChunkHeaderFrom: request stream.
     mChunk ifNil: [self flag: #todo. "this 
should never happen"
            Transcript show: 'upload socket read over-run'; cr. 
request privPostFields: formDict. ^nil].

  mChunk multipartBoundary: request 
multipartBoundary.

  fieldName _ mChunk fieldName.
  contentType _ mChunk 
contentType.

self flag: #todo. "parse regular form data "
self flag: #todo. "cr!
!
eate IgnoreStream "
    self readChunkBodyFrom: request stream forChunk: mChunk.

     
 mChunk fileName
                ifNotNil:
                      [doc _ MIMEDocument 
contentType: contentType content: ''
                               url: (String 
streamContents: [:stream |
                                        stream nextPutAll: 
'file://'.
                                  (workDir pathName findTokens: 
FileDirectory slash)
                                             do: [:piece |
       
                                           stream nextPutAll: piece; nextPut: $/].
    
                                    stream nextPutAll: mChunk fileName]).
             
     formDict at: fieldName put: doc]
               ifNil: [mChunk inspect].

      
"request stream atEnd or: [mChunk isNil]"
      request stream inDataCount - 
headerBytes >= request contentLength
      ] whileFalse.

 request privPostFields: 
formDict.

     ^nil! !

!MultipartPostModule methodsFor: 'processing' stamp: 'bolot 
11/9/2000 18:45'!
processUsingRequestStuff: request
       | file |
       "hahaha"

  
    request multipartFormFieldsDo: [:mChunk |
              mChunk fileName 
isEmptyOrNil ifFalse: [
                        file _ self openFileForChunk: mChunk.
 
                 mChunk saveToStream: file.
                     file close]]! !

!MultipartPostModule methodsFor: !
!
'processing' stamp: 'bolot 11/9/2000 16:11'!
readChunkBodyFrom: aStream forChunk: 
mChunk
       | body origFileName idx str file |
     origFileName _ mChunk fileName. 
"save the file name from request"
      file _ self openFileForChunk: mChunk. "this 
method may change file name in mChunk"

Transcript show: 
'readChunkBodyFrom:forChunk:'; cr.
        body _ aStream nextChunk.
body inspect.
    
    "IE4 for Mac appends 128 bytes of Mac file system info - must remove"
  "this fix 
assumes that body size >= 128"
       ((body at: 1) asciiValue = 0
                   
and: [(body at: 2) asciiValue = origFileName size
                              and: 
[(body copyFrom: 3 to: origFileName size + 2) = origFileName]])
           ifTrue: 
[body _ body copyFrom: 129 to: body size].
"Transcript show: 'init body=', body; cr."
 
 [(idx _ body findString: mChunk multipartBoundary) = 0]
                whileTrue:
   
                  [idx _ body size - mChunk multipartBoundary size + 1 max: 1.
" 
Transcript show: 'idx=', idx asString; cr. "
                    str _ aStream 
nextChunk.
" Transcript show: 'str=', str; cr;
   show: 'fil!
!
e=', (body copyFrom: 1 to: idx); cr. "
                 file nextPutAll: (body 
copyFrom: 1 to: idx - 1).
                       body _ (body copyFrom: idx to: body 
size) , str].
      " save the last chunk in the file "
    file nextPutAll: (body 
copyFrom: 1 to: idx-5). " idx-1 - String crlf size "
    file close.
    aStream 
putDataBack: (body copyFrom: idx-4 to: body size).! !

!MultipartPostModule 
methodsFor: 'processing' stamp: 'bolot 10/25/2000 17:50'!
readChunkHeaderFrom: aStream
     | string dict |
        string _ aStream upToAll: String crlf, String crlf.
self 
flag: #todo.
Transcript show: 'HEADER: ', string; cr.
 dict _ HttpRequest 
fastParseHeader: string.
    aStream next: 4. "ignore CRLFCRLF"
     ^(MultipartChunk 
from: dict)! !


!SocketStream methodsFor: 'http' stamp: 'bolot 10/25/2000 17:13'!
getSubHeader
       " return a MIME section header "
       | i result string dict |
self flag: #todo.
"START deprecating this method"
"RATIONALE: SocketStream should be 
stateless, just simple stream stuff"
     string _ self upToAll: String crlf, Strin!
!
g crlf.
        dict _ HttpRequest fastParseHeader: string.
    self next: 4. "ignore 
CRLFCRLF"
self halt.
string inspect.
     1 = 0 ifTrue: [string _ inStream contents.
  
   self resetInStream.
    [(i _ string findString: String crlf, String crlf) > 0
     
    ifTrue: [result _ string copyFrom: 1 to: i-1.
                  inStream 
nextPutAll: (string copyFrom: i to: string size).
                     self next: 4. " 
ignore CRLFCRLF "
                      ^HttpRequest parseHeader: result].
     socket 
isConnected "and: [socket dataAvailable]"]
              whileTrue:
     [string _ 
string, socket getData].
     result _ string.
       dict _ HttpRequest parseHeader: 
result.
        dict isEmptyOrNil ifFalse: [self next: 4]. " ignore CRLFCRLF "].
      
 ^dict! !

MultipartPostModule removeSelector: #readSubHeaderFrom:!

Reply via email to