From a8374b25f15c1c7545748bae26dda5e3c190573c Mon Sep 17 00:00:00 2001 From: zotlabs Date: Mon, 26 Jun 2017 17:32:38 -0700 Subject: upgrade blueimp from 9.8 to 9.18 --- library/blueimp_upload/server/gae-go/app/main.go | 297 +++++++++++++-------- library/blueimp_upload/server/gae-python/app.yaml | 5 +- library/blueimp_upload/server/gae-python/main.py | 208 +++++++++------ library/blueimp_upload/server/node/.gitignore | 2 - library/blueimp_upload/server/node/package.json | 41 --- .../server/node/public/files/.gitignore | 2 - library/blueimp_upload/server/node/server.js | 292 -------------------- library/blueimp_upload/server/node/tmp/.gitignore | 0 library/blueimp_upload/server/php/Dockerfile | 38 +++ .../blueimp_upload/server/php/UploadHandler.php | 250 ++++++++++------- .../blueimp_upload/server/php/docker-compose.yml | 6 + library/blueimp_upload/server/php/files/.htaccess | 14 +- library/blueimp_upload/server/php/index.php | 4 +- 13 files changed, 512 insertions(+), 647 deletions(-) delete mode 100644 library/blueimp_upload/server/node/.gitignore delete mode 100644 library/blueimp_upload/server/node/package.json delete mode 100644 library/blueimp_upload/server/node/public/files/.gitignore delete mode 100755 library/blueimp_upload/server/node/server.js delete mode 100644 library/blueimp_upload/server/node/tmp/.gitignore create mode 100644 library/blueimp_upload/server/php/Dockerfile create mode 100644 library/blueimp_upload/server/php/docker-compose.yml (limited to 'library/blueimp_upload/server') diff --git a/library/blueimp_upload/server/gae-go/app/main.go b/library/blueimp_upload/server/gae-go/app/main.go index 03af0b1d2..a92d128c0 100644 --- a/library/blueimp_upload/server/gae-go/app/main.go +++ b/library/blueimp_upload/server/gae-go/app/main.go @@ -1,59 +1,90 @@ /* - * jQuery File Upload Plugin GAE Go Example 3.2.0 + * jQuery File Upload Plugin GAE Go Example * https://github.com/blueimp/jQuery-File-Upload * * Copyright 2011, Sebastian Tschan * https://blueimp.net * * Licensed under the MIT license: - * http://www.opensource.org/licenses/MIT + * https://opensource.org/licenses/MIT */ package app import ( - "appengine" - "appengine/blobstore" - "appengine/image" - "appengine/taskqueue" + "bufio" "bytes" "encoding/json" "fmt" + "github.com/disintegration/gift" + "golang.org/x/net/context" + "google.golang.org/appengine" + "google.golang.org/appengine/memcache" + "hash/crc32" + "image" + "image/gif" + "image/jpeg" + "image/png" "io" "log" "mime/multipart" "net/http" "net/url" + "path/filepath" "regexp" "strings" - "time" ) const ( - WEBSITE = "https://blueimp.github.io/jQuery-File-Upload/" - MIN_FILE_SIZE = 1 // bytes - MAX_FILE_SIZE = 5000000 // bytes + WEBSITE = "https://blueimp.github.io/jQuery-File-Upload/" + MIN_FILE_SIZE = 1 // bytes + // Max file size is memcache limit (1MB) minus key size minus overhead: + MAX_FILE_SIZE = 999000 // bytes IMAGE_TYPES = "image/(gif|p?jpeg|(x-)?png)" ACCEPT_FILE_TYPES = IMAGE_TYPES + THUMB_MAX_WIDTH = 80 + THUMB_MAX_HEIGHT = 80 EXPIRATION_TIME = 300 // seconds - THUMBNAIL_PARAM = "=s80" + // If empty, only allow redirects to the referer protocol+host. + // Set to a regexp string for custom pattern matching: + REDIRECT_ALLOW_TARGET = "" ) var ( imageTypes = regexp.MustCompile(IMAGE_TYPES) acceptFileTypes = regexp.MustCompile(ACCEPT_FILE_TYPES) + thumbSuffix = "." + fmt.Sprint(THUMB_MAX_WIDTH) + "x" + + fmt.Sprint(THUMB_MAX_HEIGHT) ) +func escape(s string) string { + return strings.Replace(url.QueryEscape(s), "+", "%20", -1) +} + +func extractKey(r *http.Request) string { + // Use RequestURI instead of r.URL.Path, as we need the encoded form: + path := strings.Split(r.RequestURI, "?")[0] + // Also adjust double encoded slashes: + return strings.Replace(path[1:], "%252F", "%2F", -1) +} + +func check(err error) { + if err != nil { + panic(err) + } +} + type FileInfo struct { - Key appengine.BlobKey `json:"-"` - Url string `json:"url,omitempty"` - ThumbnailUrl string `json:"thumbnailUrl,omitempty"` - Name string `json:"name"` - Type string `json:"type"` - Size int64 `json:"size"` - Error string `json:"error,omitempty"` - DeleteUrl string `json:"deleteUrl,omitempty"` - DeleteType string `json:"deleteType,omitempty"` + Key string `json:"-"` + ThumbnailKey string `json:"-"` + Url string `json:"url,omitempty"` + ThumbnailUrl string `json:"thumbnailUrl,omitempty"` + Name string `json:"name"` + Type string `json:"type"` + Size int64 `json:"size"` + Error string `json:"error,omitempty"` + DeleteUrl string `json:"deleteUrl,omitempty"` + DeleteType string `json:"deleteType,omitempty"` } func (fi *FileInfo) ValidateType() (valid bool) { @@ -75,50 +106,58 @@ func (fi *FileInfo) ValidateSize() (valid bool) { return false } -func (fi *FileInfo) CreateUrls(r *http.Request, c appengine.Context) { +func (fi *FileInfo) CreateUrls(r *http.Request, c context.Context) { u := &url.URL{ Scheme: r.URL.Scheme, Host: appengine.DefaultVersionHostname(c), Path: "/", } uString := u.String() - fi.Url = uString + escape(string(fi.Key)) + "/" + - escape(string(fi.Name)) - fi.DeleteUrl = fi.Url + "?delete=true" + fi.Url = uString + fi.Key + fi.DeleteUrl = fi.Url fi.DeleteType = "DELETE" - if imageTypes.MatchString(fi.Type) { - servingUrl, err := image.ServingURL( - c, - fi.Key, - &image.ServingURLOptions{ - Secure: strings.HasSuffix(u.Scheme, "s"), - Size: 0, - Crop: false, - }, - ) - check(err) - fi.ThumbnailUrl = servingUrl.String() + THUMBNAIL_PARAM + if fi.ThumbnailKey != "" { + fi.ThumbnailUrl = uString + fi.ThumbnailKey } } -func check(err error) { - if err != nil { - panic(err) - } -} - -func escape(s string) string { - return strings.Replace(url.QueryEscape(s), "+", "%20", -1) +func (fi *FileInfo) SetKey(checksum uint32) { + fi.Key = escape(string(fi.Type)) + "/" + + escape(fmt.Sprint(checksum)) + "/" + + escape(string(fi.Name)) } -func delayedDelete(c appengine.Context, fi *FileInfo) { - if key := string(fi.Key); key != "" { - task := &taskqueue.Task{ - Path: "/" + escape(key) + "/-", - Method: "DELETE", - Delay: time.Duration(EXPIRATION_TIME) * time.Second, +func (fi *FileInfo) createThumb(buffer *bytes.Buffer, c context.Context) { + if imageTypes.MatchString(fi.Type) { + src, _, err := image.Decode(bytes.NewReader(buffer.Bytes())) + check(err) + filter := gift.New(gift.ResizeToFit( + THUMB_MAX_WIDTH, + THUMB_MAX_HEIGHT, + gift.LanczosResampling, + )) + dst := image.NewNRGBA(filter.Bounds(src.Bounds())) + filter.Draw(dst, src) + buffer.Reset() + bWriter := bufio.NewWriter(buffer) + switch fi.Type { + case "image/jpeg", "image/pjpeg": + err = jpeg.Encode(bWriter, dst, nil) + case "image/gif": + err = gif.Encode(bWriter, dst, nil) + default: + err = png.Encode(bWriter, dst) + } + check(err) + bWriter.Flush() + thumbnailKey := fi.Key + thumbSuffix + filepath.Ext(fi.Name) + item := &memcache.Item{ + Key: thumbnailKey, + Value: buffer.Bytes(), } - taskqueue.Add(c, task, "") + err = memcache.Set(c, item) + check(err) + fi.ThumbnailKey = thumbnailKey } } @@ -136,24 +175,26 @@ func handleUpload(r *http.Request, p *multipart.Part) (fi *FileInfo) { fi.Error = rec.(error).Error() } }() + var buffer bytes.Buffer + hash := crc32.NewIEEE() + mw := io.MultiWriter(&buffer, hash) lr := &io.LimitedReader{R: p, N: MAX_FILE_SIZE + 1} + _, err := io.Copy(mw, lr) + check(err) + fi.Size = MAX_FILE_SIZE + 1 - lr.N + if !fi.ValidateSize() { + return + } + fi.SetKey(hash.Sum32()) + item := &memcache.Item{ + Key: fi.Key, + Value: buffer.Bytes(), + } context := appengine.NewContext(r) - w, err := blobstore.Create(context, fi.Type) - defer func() { - w.Close() - fi.Size = MAX_FILE_SIZE + 1 - lr.N - fi.Key, err = w.Key() - check(err) - if !fi.ValidateSize() { - err := blobstore.Delete(context, fi.Key) - check(err) - return - } - delayedDelete(context, fi) - fi.CreateUrls(r, context) - }() + err = memcache.Set(context, item) check(err) - _, err = io.Copy(w, lr) + fi.createThumb(&buffer, context) + fi.CreateUrls(r, context) return } @@ -183,49 +224,70 @@ func handleUploads(r *http.Request) (fileInfos []*FileInfo) { return } +func validateRedirect(r *http.Request, redirect string) bool { + if redirect != "" { + var redirectAllowTarget *regexp.Regexp + if REDIRECT_ALLOW_TARGET != "" { + redirectAllowTarget = regexp.MustCompile(REDIRECT_ALLOW_TARGET) + } else { + referer := r.Referer() + if referer == "" { + return false + } + refererUrl, err := url.Parse(referer) + if err != nil { + return false + } + redirectAllowTarget = regexp.MustCompile("^" + regexp.QuoteMeta( + refererUrl.Scheme+"://"+refererUrl.Host+"/", + )) + } + return redirectAllowTarget.MatchString(redirect) + } + return false +} + func get(w http.ResponseWriter, r *http.Request) { if r.URL.Path == "/" { http.Redirect(w, r, WEBSITE, http.StatusFound) return } - parts := strings.Split(r.URL.Path, "/") + // Use RequestURI instead of r.URL.Path, as we need the encoded form: + key := extractKey(r) + parts := strings.Split(key, "/") if len(parts) == 3 { - if key := parts[1]; key != "" { - blobKey := appengine.BlobKey(key) - bi, err := blobstore.Stat(appengine.NewContext(r), blobKey) - if err == nil { - w.Header().Add("X-Content-Type-Options", "nosniff") - if !imageTypes.MatchString(bi.ContentType) { - w.Header().Add("Content-Type", "application/octet-stream") - w.Header().Add( - "Content-Disposition", - fmt.Sprintf("attachment; filename=\"%s\"", parts[2]), - ) - } - w.Header().Add( - "Cache-Control", - fmt.Sprintf("public,max-age=%d", EXPIRATION_TIME), - ) - blobstore.Send(w, blobKey) - return + context := appengine.NewContext(r) + item, err := memcache.Get(context, key) + if err == nil { + w.Header().Add("X-Content-Type-Options", "nosniff") + contentType, _ := url.QueryUnescape(parts[0]) + if !imageTypes.MatchString(contentType) { + contentType = "application/octet-stream" } + w.Header().Add("Content-Type", contentType) + w.Header().Add( + "Cache-Control", + fmt.Sprintf("public,max-age=%d", EXPIRATION_TIME), + ) + w.Write(item.Value) + return } } http.Error(w, "404 Not Found", http.StatusNotFound) } func post(w http.ResponseWriter, r *http.Request) { - result := make(map[string][]*FileInfo, 1) - result["files"] = handleUploads(r) + result := make(map[string][]*FileInfo, 1) + result["files"] = handleUploads(r) b, err := json.Marshal(result) check(err) - if redirect := r.FormValue("redirect"); redirect != "" { - if strings.Contains(redirect, "%s") { - redirect = fmt.Sprintf( - redirect, - escape(string(b)), - ) - } + if redirect := r.FormValue("redirect"); validateRedirect(r, redirect) { + if strings.Contains(redirect, "%s") { + redirect = fmt.Sprintf( + redirect, + escape(string(b)), + ) + } http.Redirect(w, r, redirect, http.StatusFound) return } @@ -238,27 +300,30 @@ func post(w http.ResponseWriter, r *http.Request) { } func delete(w http.ResponseWriter, r *http.Request) { - parts := strings.Split(r.URL.Path, "/") - if len(parts) != 3 { - return - } - result := make(map[string]bool, 1) - if key := parts[1]; key != "" { - c := appengine.NewContext(r) - blobKey := appengine.BlobKey(key) - err := blobstore.Delete(c, blobKey) - check(err) - err = image.DeleteServingURL(c, blobKey) + key := extractKey(r) + parts := strings.Split(key, "/") + if len(parts) == 3 { + result := make(map[string]bool, 1) + context := appengine.NewContext(r) + err := memcache.Delete(context, key) + if err == nil { + result[key] = true + contentType, _ := url.QueryUnescape(parts[0]) + if imageTypes.MatchString(contentType) { + thumbnailKey := key + thumbSuffix + filepath.Ext(parts[2]) + err := memcache.Delete(context, thumbnailKey) + if err == nil { + result[thumbnailKey] = true + } + } + } + w.Header().Set("Content-Type", "application/json") + b, err := json.Marshal(result) check(err) - result[key] = true - } - jsonType := "application/json" - if strings.Index(r.Header.Get("Accept"), jsonType) != -1 { - w.Header().Set("Content-Type", jsonType) + fmt.Fprintln(w, string(b)) + } else { + http.Error(w, "405 Method not allowed", http.StatusMethodNotAllowed) } - b, err := json.Marshal(result) - check(err) - fmt.Fprintln(w, string(b)) } func handle(w http.ResponseWriter, r *http.Request) { @@ -267,15 +332,15 @@ func handle(w http.ResponseWriter, r *http.Request) { w.Header().Add("Access-Control-Allow-Origin", "*") w.Header().Add( "Access-Control-Allow-Methods", - "OPTIONS, HEAD, GET, POST, PUT, DELETE", + "OPTIONS, HEAD, GET, POST, DELETE", ) w.Header().Add( "Access-Control-Allow-Headers", "Content-Type, Content-Range, Content-Disposition", ) switch r.Method { - case "OPTIONS": - case "HEAD": + case "OPTIONS", "HEAD": + return case "GET": get(w, r) case "POST": diff --git a/library/blueimp_upload/server/gae-python/app.yaml b/library/blueimp_upload/server/gae-python/app.yaml index 5fe123f59..764449b74 100644 --- a/library/blueimp_upload/server/gae-python/app.yaml +++ b/library/blueimp_upload/server/gae-python/app.yaml @@ -4,8 +4,9 @@ runtime: python27 api_version: 1 threadsafe: true -builtins: -- deferred: on +libraries: +- name: PIL + version: latest handlers: - url: /(favicon\.ico|robots\.txt) diff --git a/library/blueimp_upload/server/gae-python/main.py b/library/blueimp_upload/server/gae-python/main.py index 6276be6a0..1955ac00a 100644 --- a/library/blueimp_upload/server/gae-python/main.py +++ b/library/blueimp_upload/server/gae-python/main.py @@ -1,49 +1,57 @@ # -*- coding: utf-8 -*- # -# jQuery File Upload Plugin GAE Python Example 2.2.0 +# jQuery File Upload Plugin GAE Python Example # https://github.com/blueimp/jQuery-File-Upload # # Copyright 2011, Sebastian Tschan # https://blueimp.net # # Licensed under the MIT license: -# http://www.opensource.org/licenses/MIT +# https://opensource.org/licenses/MIT # -from __future__ import with_statement -from google.appengine.api import files, images -from google.appengine.ext import blobstore, deferred -from google.appengine.ext.webapp import blobstore_handlers +from google.appengine.api import memcache, images import json +import os import re import urllib import webapp2 +DEBUG=os.environ.get('SERVER_SOFTWARE', '').startswith('Dev') WEBSITE = 'https://blueimp.github.io/jQuery-File-Upload/' MIN_FILE_SIZE = 1 # bytes -MAX_FILE_SIZE = 5000000 # bytes +# Max file size is memcache limit (1MB) minus key size minus overhead: +MAX_FILE_SIZE = 999000 # bytes IMAGE_TYPES = re.compile('image/(gif|p?jpeg|(x-)?png)') ACCEPT_FILE_TYPES = IMAGE_TYPES -THUMBNAIL_MODIFICATOR = '=s80' # max width / height +THUMB_MAX_WIDTH = 80 +THUMB_MAX_HEIGHT = 80 +THUMB_SUFFIX = '.'+str(THUMB_MAX_WIDTH)+'x'+str(THUMB_MAX_HEIGHT)+'.png' EXPIRATION_TIME = 300 # seconds +# If set to None, only allow redirects to the referer protocol+host. +# Set to a regexp for custom pattern matching against the redirect value: +REDIRECT_ALLOW_TARGET = None + +class CORSHandler(webapp2.RequestHandler): + def cors(self): + headers = self.response.headers + headers['Access-Control-Allow-Origin'] = '*' + headers['Access-Control-Allow-Methods'] =\ + 'OPTIONS, HEAD, GET, POST, DELETE' + headers['Access-Control-Allow-Headers'] =\ + 'Content-Type, Content-Range, Content-Disposition' + def initialize(self, request, response): + super(CORSHandler, self).initialize(request, response) + self.cors() -def cleanup(blob_keys): - blobstore.delete(blob_keys) - - -class UploadHandler(webapp2.RequestHandler): + def json_stringify(self, obj): + return json.dumps(obj, separators=(',', ':')) - def initialize(self, request, response): - super(UploadHandler, self).initialize(request, response) - self.response.headers['Access-Control-Allow-Origin'] = '*' - self.response.headers[ - 'Access-Control-Allow-Methods' - ] = 'OPTIONS, HEAD, GET, POST, PUT, DELETE' - self.response.headers[ - 'Access-Control-Allow-Headers' - ] = 'Content-Type, Content-Range, Content-Disposition' + def options(self, *args, **kwargs): + pass +class UploadHandler(CORSHandler): def validate(self, file): if file['size'] < MIN_FILE_SIZE: file['error'] = 'File is too small' @@ -55,6 +63,20 @@ class UploadHandler(webapp2.RequestHandler): return True return False + def validate_redirect(self, redirect): + if redirect: + if REDIRECT_ALLOW_TARGET: + return REDIRECT_ALLOW_TARGET.match(redirect) + referer = self.request.headers['referer'] + if referer: + from urlparse import urlparse + parts = urlparse(referer) + redirect_allow_target = '^' + re.escape( + parts.scheme + '://' + parts.netloc + '/' + ) + return re.match(redirect_allow_target, redirect) + return False + def get_file_size(self, file): file.seek(0, 2) # Seek to the end of the file size = file.tell() # Get the position of EOF @@ -62,64 +84,58 @@ class UploadHandler(webapp2.RequestHandler): return size def write_blob(self, data, info): - blob = files.blobstore.create( - mime_type=info['type'], - _blobinfo_uploaded_filename=info['name'] - ) - with files.open(blob, 'a') as f: - f.write(data) - files.finalize(blob) - return files.blobstore.get_blob_key(blob) + key = urllib.quote(info['type'].encode('utf-8'), '') +\ + '/' + str(hash(data)) +\ + '/' + urllib.quote(info['name'].encode('utf-8'), '') + try: + memcache.set(key, data, time=EXPIRATION_TIME) + except: #Failed to add to memcache + return (None, None) + thumbnail_key = None + if IMAGE_TYPES.match(info['type']): + try: + img = images.Image(image_data=data) + img.resize( + width=THUMB_MAX_WIDTH, + height=THUMB_MAX_HEIGHT + ) + thumbnail_data = img.execute_transforms() + thumbnail_key = key + THUMB_SUFFIX + memcache.set( + thumbnail_key, + thumbnail_data, + time=EXPIRATION_TIME + ) + except: #Failed to resize Image or add to memcache + thumbnail_key = None + return (key, thumbnail_key) def handle_upload(self): results = [] - blob_keys = [] for name, fieldStorage in self.request.POST.items(): if type(fieldStorage) is unicode: continue result = {} - result['name'] = re.sub( - r'^.*\\', - '', - fieldStorage.filename - ) + result['name'] = urllib.unquote(fieldStorage.filename) result['type'] = fieldStorage.type result['size'] = self.get_file_size(fieldStorage.file) if self.validate(result): - blob_key = str( - self.write_blob(fieldStorage.value, result) + key, thumbnail_key = self.write_blob( + fieldStorage.value, + result ) - blob_keys.append(blob_key) - result['deleteType'] = 'DELETE' - result['deleteUrl'] = self.request.host_url +\ - '/?key=' + urllib.quote(blob_key, '') - if (IMAGE_TYPES.match(result['type'])): - try: - result['url'] = images.get_serving_url( - blob_key, - secure_url=self.request.host_url.startswith( - 'https' - ) - ) - result['thumbnailUrl'] = result['url'] +\ - THUMBNAIL_MODIFICATOR - except: # Could not get an image serving url - pass - if not 'url' in result: - result['url'] = self.request.host_url +\ - '/' + blob_key + '/' + urllib.quote( - result['name'].encode('utf-8'), '') + if key is not None: + result['url'] = self.request.host_url + '/' + key + result['deleteUrl'] = result['url'] + result['deleteType'] = 'DELETE' + if thumbnail_key is not None: + result['thumbnailUrl'] = self.request.host_url +\ + '/' + thumbnail_key + else: + result['error'] = 'Failed to store uploaded file.' results.append(result) - deferred.defer( - cleanup, - blob_keys, - _countdown=EXPIRATION_TIME - ) return results - def options(self): - pass - def head(self): pass @@ -130,9 +146,9 @@ class UploadHandler(webapp2.RequestHandler): if (self.request.get('_method') == 'DELETE'): return self.delete() result = {'files': self.handle_upload()} - s = json.dumps(result, separators=(',', ':')) + s = self.json_stringify(result) redirect = self.request.get('redirect') - if redirect: + if self.validate_redirect(redirect): return self.redirect(str( redirect.replace('%s', urllib.quote(s, ''), 1) )) @@ -140,31 +156,49 @@ class UploadHandler(webapp2.RequestHandler): self.response.headers['Content-Type'] = 'application/json' self.response.write(s) - def delete(self): - key = self.request.get('key') or '' - blobstore.delete(key) - s = json.dumps({key: True}, separators=(',', ':')) +class FileHandler(CORSHandler): + def normalize(self, str): + return urllib.quote(urllib.unquote(str), '') + + def get(self, content_type, data_hash, file_name): + content_type = self.normalize(content_type) + file_name = self.normalize(file_name) + key = content_type + '/' + data_hash + '/' + file_name + data = memcache.get(key) + if data is None: + return self.error(404) + # Prevent browsers from MIME-sniffing the content-type: + self.response.headers['X-Content-Type-Options'] = 'nosniff' + content_type = urllib.unquote(content_type) + if not IMAGE_TYPES.match(content_type): + # Force a download dialog for non-image types: + content_type = 'application/octet-stream' + elif file_name.endswith(THUMB_SUFFIX): + content_type = 'image/png' + self.response.headers['Content-Type'] = content_type + # Cache for the expiration time: + self.response.headers['Cache-Control'] = 'public,max-age=%d' \ + % EXPIRATION_TIME + self.response.write(data) + + def delete(self, content_type, data_hash, file_name): + content_type = self.normalize(content_type) + file_name = self.normalize(file_name) + key = content_type + '/' + data_hash + '/' + file_name + result = {key: memcache.delete(key)} + content_type = urllib.unquote(content_type) + if IMAGE_TYPES.match(content_type): + thumbnail_key = key + THUMB_SUFFIX + result[thumbnail_key] = memcache.delete(thumbnail_key) if 'application/json' in self.request.headers.get('Accept'): self.response.headers['Content-Type'] = 'application/json' + s = self.json_stringify(result) self.response.write(s) - -class DownloadHandler(blobstore_handlers.BlobstoreDownloadHandler): - def get(self, key, filename): - if not blobstore.get(key): - self.error(404) - else: - # Prevent browsers from MIME-sniffing the content-type: - self.response.headers['X-Content-Type-Options'] = 'nosniff' - # Cache for the expiration time: - self.response.headers['Cache-Control'] = 'public,max-age=%d' % EXPIRATION_TIME - # Send the file forcing a download dialog: - self.send_blob(key, save_as=filename, content_type='application/octet-stream') - app = webapp2.WSGIApplication( [ ('/', UploadHandler), - ('/([^/]+)/([^/]+)', DownloadHandler) + ('/(.+)/([^/]+)/([^/]+)', FileHandler) ], - debug=True + debug=DEBUG ) diff --git a/library/blueimp_upload/server/node/.gitignore b/library/blueimp_upload/server/node/.gitignore deleted file mode 100644 index 9daa8247d..000000000 --- a/library/blueimp_upload/server/node/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -.DS_Store -node_modules diff --git a/library/blueimp_upload/server/node/package.json b/library/blueimp_upload/server/node/package.json deleted file mode 100644 index dd38c50ca..000000000 --- a/library/blueimp_upload/server/node/package.json +++ /dev/null @@ -1,41 +0,0 @@ -{ - "name": "blueimp-file-upload-node", - "version": "2.1.0", - "title": "jQuery File Upload Node.js example", - "description": "Node.js implementation example of a file upload handler for jQuery File Upload.", - "keywords": [ - "file", - "upload", - "cross-domain", - "cross-site", - "node" - ], - "homepage": "https://github.com/blueimp/jQuery-File-Upload", - "author": { - "name": "Sebastian Tschan", - "url": "https://blueimp.net" - }, - "maintainers": [ - { - "name": "Sebastian Tschan", - "url": "https://blueimp.net" - } - ], - "repository": { - "type": "git", - "url": "git://github.com/blueimp/jQuery-File-Upload.git" - }, - "bugs": "https://github.com/blueimp/jQuery-File-Upload/issues", - "licenses": [ - { - "type": "MIT", - "url": "http://www.opensource.org/licenses/MIT" - } - ], - "dependencies": { - "formidable": ">=1.0.11", - "node-static": ">=0.6.5", - "imagemagick": ">=0.1.3" - }, - "main": "server.js" -} diff --git a/library/blueimp_upload/server/node/public/files/.gitignore b/library/blueimp_upload/server/node/public/files/.gitignore deleted file mode 100644 index d6b7ef32c..000000000 --- a/library/blueimp_upload/server/node/public/files/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -* -!.gitignore diff --git a/library/blueimp_upload/server/node/server.js b/library/blueimp_upload/server/node/server.js deleted file mode 100755 index 808d6ffe1..000000000 --- a/library/blueimp_upload/server/node/server.js +++ /dev/null @@ -1,292 +0,0 @@ -#!/usr/bin/nodejs -/* - * jQuery File Upload Plugin Node.js Example 2.1.2 - * https://github.com/blueimp/jQuery-File-Upload - * - * Copyright 2012, Sebastian Tschan - * https://blueimp.net - * - * Licensed under the MIT license: - * http://www.opensource.org/licenses/MIT - */ - -/* jshint nomen:false */ -/* global require, __dirname, unescape, console */ - -(function (port) { - 'use strict'; - var path = require('path'), - fs = require('fs'), - // Since Node 0.8, .existsSync() moved from path to fs: - _existsSync = fs.existsSync || path.existsSync, - formidable = require('formidable'), - nodeStatic = require('node-static'), - imageMagick = require('imagemagick'), - options = { - tmpDir: __dirname + '/tmp', - publicDir: __dirname + '/public', - uploadDir: __dirname + '/public/files', - uploadUrl: '/files/', - maxPostSize: 11000000000, // 11 GB - minFileSize: 1, - maxFileSize: 10000000000, // 10 GB - acceptFileTypes: /.+/i, - // Files not matched by this regular expression force a download dialog, - // to prevent executing any scripts in the context of the service domain: - inlineFileTypes: /\.(gif|jpe?g|png)$/i, - imageTypes: /\.(gif|jpe?g|png)$/i, - imageVersions: { - 'thumbnail': { - width: 80, - height: 80 - } - }, - accessControl: { - allowOrigin: '*', - allowMethods: 'OPTIONS, HEAD, GET, POST, PUT, DELETE', - allowHeaders: 'Content-Type, Content-Range, Content-Disposition' - }, - /* Uncomment and edit this section to provide the service via HTTPS: - ssl: { - key: fs.readFileSync('/Applications/XAMPP/etc/ssl.key/server.key'), - cert: fs.readFileSync('/Applications/XAMPP/etc/ssl.crt/server.crt') - }, - */ - nodeStatic: { - cache: 3600 // seconds to cache served files - } - }, - utf8encode = function (str) { - return unescape(encodeURIComponent(str)); - }, - fileServer = new nodeStatic.Server(options.publicDir, options.nodeStatic), - nameCountRegexp = /(?:(?: \(([\d]+)\))?(\.[^.]+))?$/, - nameCountFunc = function (s, index, ext) { - return ' (' + ((parseInt(index, 10) || 0) + 1) + ')' + (ext || ''); - }, - FileInfo = function (file) { - this.name = file.name; - this.size = file.size; - this.type = file.type; - this.deleteType = 'DELETE'; - }, - UploadHandler = function (req, res, callback) { - this.req = req; - this.res = res; - this.callback = callback; - }, - serve = function (req, res) { - res.setHeader( - 'Access-Control-Allow-Origin', - options.accessControl.allowOrigin - ); - res.setHeader( - 'Access-Control-Allow-Methods', - options.accessControl.allowMethods - ); - res.setHeader( - 'Access-Control-Allow-Headers', - options.accessControl.allowHeaders - ); - var handleResult = function (result, redirect) { - if (redirect) { - res.writeHead(302, { - 'Location': redirect.replace( - /%s/, - encodeURIComponent(JSON.stringify(result)) - ) - }); - res.end(); - } else { - res.writeHead(200, { - 'Content-Type': req.headers.accept - .indexOf('application/json') !== -1 ? - 'application/json' : 'text/plain' - }); - res.end(JSON.stringify(result)); - } - }, - setNoCacheHeaders = function () { - res.setHeader('Pragma', 'no-cache'); - res.setHeader('Cache-Control', 'no-store, no-cache, must-revalidate'); - res.setHeader('Content-Disposition', 'inline; filename="files.json"'); - }, - handler = new UploadHandler(req, res, handleResult); - switch (req.method) { - case 'OPTIONS': - res.end(); - break; - case 'HEAD': - case 'GET': - if (req.url === '/') { - setNoCacheHeaders(); - if (req.method === 'GET') { - handler.get(); - } else { - res.end(); - } - } else { - fileServer.serve(req, res); - } - break; - case 'POST': - setNoCacheHeaders(); - handler.post(); - break; - case 'DELETE': - handler.destroy(); - break; - default: - res.statusCode = 405; - res.end(); - } - }; - fileServer.respond = function (pathname, status, _headers, files, stat, req, res, finish) { - // Prevent browsers from MIME-sniffing the content-type: - _headers['X-Content-Type-Options'] = 'nosniff'; - if (!options.inlineFileTypes.test(files[0])) { - // Force a download dialog for unsafe file extensions: - _headers['Content-Type'] = 'application/octet-stream'; - _headers['Content-Disposition'] = 'attachment; filename="' + - utf8encode(path.basename(files[0])) + '"'; - } - nodeStatic.Server.prototype.respond - .call(this, pathname, status, _headers, files, stat, req, res, finish); - }; - FileInfo.prototype.validate = function () { - if (options.minFileSize && options.minFileSize > this.size) { - this.error = 'File is too small'; - } else if (options.maxFileSize && options.maxFileSize < this.size) { - this.error = 'File is too big'; - } else if (!options.acceptFileTypes.test(this.name)) { - this.error = 'Filetype not allowed'; - } - return !this.error; - }; - FileInfo.prototype.safeName = function () { - // Prevent directory traversal and creating hidden system files: - this.name = path.basename(this.name).replace(/^\.+/, ''); - // Prevent overwriting existing files: - while (_existsSync(options.uploadDir + '/' + this.name)) { - this.name = this.name.replace(nameCountRegexp, nameCountFunc); - } - }; - FileInfo.prototype.initUrls = function (req) { - if (!this.error) { - var that = this, - baseUrl = (options.ssl ? 'https:' : 'http:') + - '//' + req.headers.host + options.uploadUrl; - this.url = this.deleteUrl = baseUrl + encodeURIComponent(this.name); - Object.keys(options.imageVersions).forEach(function (version) { - if (_existsSync( - options.uploadDir + '/' + version + '/' + that.name - )) { - that[version + 'Url'] = baseUrl + version + '/' + - encodeURIComponent(that.name); - } - }); - } - }; - UploadHandler.prototype.get = function () { - var handler = this, - files = []; - fs.readdir(options.uploadDir, function (err, list) { - list.forEach(function (name) { - var stats = fs.statSync(options.uploadDir + '/' + name), - fileInfo; - if (stats.isFile() && name[0] !== '.') { - fileInfo = new FileInfo({ - name: name, - size: stats.size - }); - fileInfo.initUrls(handler.req); - files.push(fileInfo); - } - }); - handler.callback({files: files}); - }); - }; - UploadHandler.prototype.post = function () { - var handler = this, - form = new formidable.IncomingForm(), - tmpFiles = [], - files = [], - map = {}, - counter = 1, - redirect, - finish = function () { - counter -= 1; - if (!counter) { - files.forEach(function (fileInfo) { - fileInfo.initUrls(handler.req); - }); - handler.callback({files: files}, redirect); - } - }; - form.uploadDir = options.tmpDir; - form.on('fileBegin', function (name, file) { - tmpFiles.push(file.path); - var fileInfo = new FileInfo(file); - fileInfo.safeName(); - map[path.basename(file.path)] = fileInfo; - files.push(fileInfo); - }).on('field', function (name, value) { - if (name === 'redirect') { - redirect = value; - } - }).on('file', function (name, file) { - var fileInfo = map[path.basename(file.path)]; - fileInfo.size = file.size; - if (!fileInfo.validate()) { - fs.unlink(file.path); - return; - } - fs.renameSync(file.path, options.uploadDir + '/' + fileInfo.name); - if (options.imageTypes.test(fileInfo.name)) { - Object.keys(options.imageVersions).forEach(function (version) { - counter += 1; - var opts = options.imageVersions[version]; - imageMagick.resize({ - width: opts.width, - height: opts.height, - srcPath: options.uploadDir + '/' + fileInfo.name, - dstPath: options.uploadDir + '/' + version + '/' + - fileInfo.name - }, finish); - }); - } - }).on('aborted', function () { - tmpFiles.forEach(function (file) { - fs.unlink(file); - }); - }).on('error', function (e) { - console.log(e); - }).on('progress', function (bytesReceived) { - if (bytesReceived > options.maxPostSize) { - handler.req.connection.destroy(); - } - }).on('end', finish).parse(handler.req); - }; - UploadHandler.prototype.destroy = function () { - var handler = this, - fileName; - if (handler.req.url.slice(0, options.uploadUrl.length) === options.uploadUrl) { - fileName = path.basename(decodeURIComponent(handler.req.url)); - if (fileName[0] !== '.') { - fs.unlink(options.uploadDir + '/' + fileName, function (ex) { - Object.keys(options.imageVersions).forEach(function (version) { - fs.unlink(options.uploadDir + '/' + version + '/' + fileName); - }); - handler.callback({success: !ex}); - }); - return; - } - } - handler.callback({success: false}); - }; - if (options.ssl) { - require('https').createServer(options.ssl, serve).listen(port); - } else { - require('http').createServer(serve).listen(port); - } -}(8888)); diff --git a/library/blueimp_upload/server/node/tmp/.gitignore b/library/blueimp_upload/server/node/tmp/.gitignore deleted file mode 100644 index e69de29bb..000000000 diff --git a/library/blueimp_upload/server/php/Dockerfile b/library/blueimp_upload/server/php/Dockerfile new file mode 100644 index 000000000..ca88d3d0d --- /dev/null +++ b/library/blueimp_upload/server/php/Dockerfile @@ -0,0 +1,38 @@ +FROM php:7.0-apache + +# Enable the Apache Headers module: +RUN ln -s /etc/apache2/mods-available/headers.load \ + /etc/apache2/mods-enabled/headers.load + +# Enable the Apache Rewrite module: +RUN ln -s /etc/apache2/mods-available/rewrite.load \ + /etc/apache2/mods-enabled/rewrite.load + +# Install GD, Imagick and ImageMagick as image conversion options: +RUN DEBIAN_FRONTEND=noninteractive \ + apt-get update && apt-get install -y --no-install-recommends \ + libpng-dev \ + libjpeg-dev \ + libmagickwand-dev \ + imagemagick \ + && pecl install \ + imagick \ + && docker-php-ext-enable \ + imagick \ + && docker-php-ext-configure \ + gd --with-jpeg-dir=/usr/include/ \ + && docker-php-ext-install \ + gd \ + # Uninstall obsolete packages: + && apt-get autoremove -y \ + libpng-dev \ + libjpeg-dev \ + libmagickwand-dev \ + # Remove obsolete files: + && apt-get clean \ + && rm -rf \ + /tmp/* \ + /usr/share/doc/* \ + /var/cache/* \ + /var/lib/apt/lists/* \ + /var/tmp/* diff --git a/library/blueimp_upload/server/php/UploadHandler.php b/library/blueimp_upload/server/php/UploadHandler.php index fb77be1d0..1380d4739 100755 --- a/library/blueimp_upload/server/php/UploadHandler.php +++ b/library/blueimp_upload/server/php/UploadHandler.php @@ -1,13 +1,13 @@ response = array(); $this->options = array( - 'script_url' => $this->get_full_url().'/', + 'script_url' => $this->get_full_url().'/'.$this->basename($this->get_server_var('SCRIPT_NAME')), 'upload_dir' => dirname($this->get_server_var('SCRIPT_FILENAME')).'/files/', 'upload_url' => $this->get_full_url().'/files/', + 'input_stream' => 'php://input', 'user_dirs' => false, 'mkdir_mode' => 0755, 'param_name' => 'files', @@ -67,6 +69,14 @@ class UploadHandler 'Content-Range', 'Content-Disposition' ), + // By default, allow redirects to the referer protocol+host: + 'redirect_allow_target' => '/^'.preg_quote( + parse_url($this->get_server_var('HTTP_REFERER'), PHP_URL_SCHEME) + .'://' + .parse_url($this->get_server_var('HTTP_REFERER'), PHP_URL_HOST) + .'/', // Trailing slash to not match subdomains by mistake + '/' // preg_quote delimiter param + ).'/', // Enable to provide file downloads via GET requests to the PHP script: // 1. Set to 1 to download files via readfile method through PHP // 2. Set to 2 to send a X-Sendfile header for lighttpd/Apache @@ -147,7 +157,8 @@ class UploadHandler 'max_width' => 80, 'max_height' => 80 ) - ) + ), + 'print_response' => true ); if ($options) { $this->options = $options + $this->options; @@ -167,15 +178,15 @@ class UploadHandler $this->head(); break; case 'GET': - $this->get(); + $this->get($this->options['print_response']); break; case 'PATCH': case 'PUT': case 'POST': - $this->post(); + $this->post($this->options['print_response']); break; case 'DELETE': - $this->delete(); + $this->delete($this->options['print_response']); break; default: $this->header('HTTP/1.1 405 Method Not Allowed'); @@ -300,7 +311,7 @@ class UploadHandler $this->get_upload_path($file_name) ); $file->url = $this->get_download_url($file->name); - foreach($this->options['image_versions'] as $version => $options) { + foreach ($this->options['image_versions'] as $version => $options) { if (!empty($version)) { if (is_file($this->get_upload_path($file_name, $version))) { $file->{$version.'Url'} = $this->get_download_url( @@ -332,14 +343,15 @@ class UploadHandler } protected function get_error_message($error) { - return array_key_exists($error, $this->error_messages) ? + return isset($this->error_messages[$error]) ? $this->error_messages[$error] : $error; } - function get_config_bytes($val) { + public function get_config_bytes($val) { $val = trim($val); $last = strtolower($val[strlen($val)-1]); - switch($last) { + $val = (int)$val; + switch ($last) { case 'g': $val *= 1024; case 'm': @@ -355,9 +367,9 @@ class UploadHandler $file->error = $this->get_error_message($error); return false; } - $content_length = $this->fix_integer_overflow(intval( - $this->get_server_var('CONTENT_LENGTH') - )); + $content_length = $this->fix_integer_overflow( + (int)$this->get_server_var('CONTENT_LENGTH') + ); $post_max_size = $this->get_config_bytes(ini_get('post_max_size')); if ($post_max_size && ($content_length > $post_max_size)) { $file->error = $this->get_error_message('post_max_size'); @@ -398,6 +410,21 @@ class UploadHandler if (($max_width || $max_height || $min_width || $min_height) && preg_match($this->options['image_file_types'], $file->name)) { list($img_width, $img_height) = $this->get_image_size($uploaded_file); + + // If we are auto rotating the image by default, do the checks on + // the correct orientation + if ( + @$this->options['image_versions']['']['auto_orient'] && + function_exists('exif_read_data') && + ($exif = @exif_read_data($uploaded_file)) && + (((int) @$exif['Orientation']) >= 5) + ) { + $tmp = $img_width; + $img_width = $img_height; + $img_height = $tmp; + unset($tmp); + } + } if (!empty($img_width)) { if ($max_width && $img_width > $max_width) { @@ -421,7 +448,7 @@ class UploadHandler } protected function upcount_name_callback($matches) { - $index = isset($matches[1]) ? intval($matches[1]) + 1 : 1; + $index = isset($matches[1]) ? ((int)$matches[1]) + 1 : 1; $ext = isset($matches[2]) ? $matches[2] : ''; return ' ('.$index.')'.$ext; } @@ -441,8 +468,8 @@ class UploadHandler $name = $this->upcount_name($name); } // Keep an existing filename if this is part of a chunked upload: - $uploaded_bytes = $this->fix_integer_overflow(intval($content_range[1])); - while(is_file($this->get_upload_path($name))) { + $uploaded_bytes = $this->fix_integer_overflow((int)$content_range[1]); + while (is_file($this->get_upload_path($name))) { if ($uploaded_bytes === $this->get_file_size( $this->get_upload_path($name))) { break; @@ -461,7 +488,7 @@ class UploadHandler } if ($this->options['correct_image_extensions'] && function_exists('exif_imagetype')) { - switch(@exif_imagetype($file_path)){ + switch (@exif_imagetype($file_path)){ case IMAGETYPE_JPEG: $extensions = array('jpg', 'jpeg'); break; @@ -491,7 +518,7 @@ class UploadHandler // Remove path information and dots around the filename, to prevent uploading // into different directories or replacing hidden system files. // Also remove control characters and spaces (\x00..\x20) around the filename: - $name = trim(basename(stripslashes($name)), ".\x00..\x20"); + $name = trim($this->basename(stripslashes($name)), ".\x00..\x20"); // Use a timestamp for empty filenames: if (!$name) { $name = str_replace('.', '-', microtime(true)); @@ -515,10 +542,6 @@ class UploadHandler ); } - protected function handle_form_data($file, $index) { - // Handle form data, e.g. $_REQUEST['description'][$index] - } - protected function get_scaled_image_file_paths($file_name, $version) { $file_path = $this->get_upload_path($file_name); if (!empty($version)) { @@ -601,7 +624,7 @@ class UploadHandler if ($exif === false) { return false; } - $orientation = intval(@$exif['Orientation']); + $orientation = (int)@$exif['Orientation']; if ($orientation < 2 || $orientation > 8) { return false; } @@ -825,7 +848,7 @@ class UploadHandler $this->get_scaled_image_file_paths($file_name, $version); $image = $this->imagick_get_image_object( $file_path, - !empty($options['no_cache']) + !empty($options['crop']) || !empty($options['no_cache']) ); if ($image->getImageFormat() === 'GIF') { // Handle animated GIFs: @@ -955,7 +978,7 @@ class UploadHandler return $dimensions; } return false; - } catch (Exception $e) { + } catch (\Exception $e) { error_log($e->getMessage()); } } @@ -965,7 +988,7 @@ class UploadHandler exec($cmd, $output, $error); if (!$error && !empty($output)) { // image.jpg JPEG 1920x1080 1920x1080+0+0 8-bit sRGB 465KB 0.000u 0:00.000 - $infos = preg_split('/\s+/', $output[0]); + $infos = preg_split('/\s+/', substr($output[0], strlen($file_path))); $dimensions = preg_split('/x/', $infos[2]); return $dimensions; } @@ -1008,7 +1031,7 @@ class UploadHandler protected function handle_image_file($file_path, $file) { $failed_versions = array(); - foreach($this->options['image_versions'] as $version => $options) { + foreach ($this->options['image_versions'] as $version => $options) { if ($this->create_scaled_image($file->name, $version, $options)) { if (!empty($version)) { $file->{$version.'Url'} = $this->get_download_url( @@ -1024,7 +1047,7 @@ class UploadHandler } if (count($failed_versions)) { $file->error = $this->get_error_message('image_resize') - .' ('.implode($failed_versions,', ').')'; + .' ('.implode($failed_versions, ', ').')'; } // Free memory: $this->destroy_image_object($file_path); @@ -1035,7 +1058,7 @@ class UploadHandler $file = new \stdClass(); $file->name = $this->get_file_name($uploaded_file, $name, $size, $type, $error, $index, $content_range); - $file->size = $this->fix_integer_overflow(intval($size)); + $file->size = $this->fix_integer_overflow((int)$size); $file->type = $type; if ($this->validate($uploaded_file, $file, $error, $index)) { $this->handle_form_data($file, $index); @@ -1061,7 +1084,7 @@ class UploadHandler // Non-multipart uploads (PUT method support) file_put_contents( $file_path, - fopen('php://input', 'r'), + fopen($this->options['input_stream'], 'r'), $append_file ? FILE_APPEND : 0 ); } @@ -1102,41 +1125,33 @@ class UploadHandler protected function body($str) { echo $str; } - + protected function header($str) { header($str); } + protected function get_upload_data($id) { + return @$_FILES[$id]; + } + + protected function get_post_param($id) { + return @$_POST[$id]; + } + + protected function get_query_param($id) { + return @$_GET[$id]; + } + protected function get_server_var($id) { - return isset($_SERVER[$id]) ? $_SERVER[$id] : ''; + return @$_SERVER[$id]; } - protected function generate_response($content, $print_response = true) { - if ($print_response) { - $json = json_encode($content); - $redirect = isset($_REQUEST['redirect']) ? - stripslashes($_REQUEST['redirect']) : null; - if ($redirect) { - $this->header('Location: '.sprintf($redirect, rawurlencode($json))); - return; - } - $this->head(); - if ($this->get_server_var('HTTP_CONTENT_RANGE')) { - $files = isset($content[$this->options['param_name']]) ? - $content[$this->options['param_name']] : null; - if ($files && is_array($files) && is_object($files[0]) && $files[0]->size) { - $this->header('Range: 0-'.( - $this->fix_integer_overflow(intval($files[0]->size)) - 1 - )); - } - } - $this->body($json); - } - return $content; + protected function handle_form_data($file, $index) { + // Handle form data, e.g. $_POST['description'][$index] } protected function get_version_param() { - return isset($_GET['version']) ? basename(stripslashes($_GET['version'])) : null; + return $this->basename(stripslashes($this->get_query_param('version'))); } protected function get_singular_param_name() { @@ -1145,14 +1160,16 @@ class UploadHandler protected function get_file_name_param() { $name = $this->get_singular_param_name(); - return isset($_REQUEST[$name]) ? basename(stripslashes($_REQUEST[$name])) : null; + return $this->basename(stripslashes($this->get_query_param($name))); } protected function get_file_names_params() { - $params = isset($_REQUEST[$this->options['param_name']]) ? - $_REQUEST[$this->options['param_name']] : array(); + $params = $this->get_query_param($this->options['param_name']); + if (!$params) { + return null; + } foreach ($params as $key => $value) { - $params[$key] = basename(stripslashes($value)); + $params[$key] = $this->basename(stripslashes($value)); } return $params; } @@ -1232,6 +1249,34 @@ class UploadHandler .implode(', ', $this->options['access_control_allow_headers'])); } + public function generate_response($content, $print_response = true) { + $this->response = $content; + if ($print_response) { + $json = json_encode($content); + $redirect = stripslashes($this->get_post_param('redirect')); + if ($redirect && preg_match($this->options['redirect_allow_target'], $redirect)) { + $this->header('Location: '.sprintf($redirect, rawurlencode($json))); + return; + } + $this->head(); + if ($this->get_server_var('HTTP_CONTENT_RANGE')) { + $files = isset($content[$this->options['param_name']]) ? + $content[$this->options['param_name']] : null; + if ($files && is_array($files) && is_object($files[0]) && $files[0]->size) { + $this->header('Range: 0-'.( + $this->fix_integer_overflow((int)$files[0]->size) - 1 + )); + } + } + $this->body($json); + } + return $content; + } + + public function get_response () { + return $this->response; + } + public function head() { $this->header('Pragma: no-cache'); $this->header('Cache-Control: no-store, no-cache, must-revalidate'); @@ -1245,7 +1290,7 @@ class UploadHandler } public function get($print_response = true) { - if ($print_response && isset($_GET['download'])) { + if ($print_response && $this->get_query_param('download')) { return $this->download(); } $file_name = $this->get_file_name_param(); @@ -1262,58 +1307,59 @@ class UploadHandler } public function post($print_response = true) { - if (isset($_REQUEST['_method']) && $_REQUEST['_method'] === 'DELETE') { + if ($this->get_query_param('_method') === 'DELETE') { return $this->delete($print_response); } - $upload = isset($_FILES[$this->options['param_name']]) ? - $_FILES[$this->options['param_name']] : null; + $upload = $this->get_upload_data($this->options['param_name']); // Parse the Content-Disposition header, if available: - $file_name = $this->get_server_var('HTTP_CONTENT_DISPOSITION') ? + $content_disposition_header = $this->get_server_var('HTTP_CONTENT_DISPOSITION'); + $file_name = $content_disposition_header ? rawurldecode(preg_replace( '/(^[^"]+")|("$)/', '', - $this->get_server_var('HTTP_CONTENT_DISPOSITION') + $content_disposition_header )) : null; // Parse the Content-Range header, which has the following form: // Content-Range: bytes 0-524287/2000000 - $content_range = $this->get_server_var('HTTP_CONTENT_RANGE') ? - preg_split('/[^0-9]+/', $this->get_server_var('HTTP_CONTENT_RANGE')) : null; + $content_range_header = $this->get_server_var('HTTP_CONTENT_RANGE'); + $content_range = $content_range_header ? + preg_split('/[^0-9]+/', $content_range_header) : null; $size = $content_range ? $content_range[3] : null; $files = array(); - if ($upload && is_array($upload['tmp_name'])) { - // param_name is an array identifier like "files[]", - // $_FILES is a multi-dimensional array: - foreach ($upload['tmp_name'] as $index => $value) { + if ($upload) { + if (is_array($upload['tmp_name'])) { + // param_name is an array identifier like "files[]", + // $upload is a multi-dimensional array: + foreach ($upload['tmp_name'] as $index => $value) { + $files[] = $this->handle_file_upload( + $upload['tmp_name'][$index], + $file_name ? $file_name : $upload['name'][$index], + $size ? $size : $upload['size'][$index], + $upload['type'][$index], + $upload['error'][$index], + $index, + $content_range + ); + } + } else { + // param_name is a single object identifier like "file", + // $upload is a one-dimensional array: $files[] = $this->handle_file_upload( - $upload['tmp_name'][$index], - $file_name ? $file_name : $upload['name'][$index], - $size ? $size : $upload['size'][$index], - $upload['type'][$index], - $upload['error'][$index], - $index, + isset($upload['tmp_name']) ? $upload['tmp_name'] : null, + $file_name ? $file_name : (isset($upload['name']) ? + $upload['name'] : null), + $size ? $size : (isset($upload['size']) ? + $upload['size'] : $this->get_server_var('CONTENT_LENGTH')), + isset($upload['type']) ? + $upload['type'] : $this->get_server_var('CONTENT_TYPE'), + isset($upload['error']) ? $upload['error'] : null, + null, $content_range ); } - } else { - // param_name is a single object identifier like "file", - // $_FILES is a one-dimensional array: - $files[] = $this->handle_file_upload( - isset($upload['tmp_name']) ? $upload['tmp_name'] : null, - $file_name ? $file_name : (isset($upload['name']) ? - $upload['name'] : null), - $size ? $size : (isset($upload['size']) ? - $upload['size'] : $this->get_server_var('CONTENT_LENGTH')), - isset($upload['type']) ? - $upload['type'] : $this->get_server_var('CONTENT_TYPE'), - isset($upload['error']) ? $upload['error'] : null, - null, - $content_range - ); } - return $this->generate_response( - array($this->options['param_name'] => $files), - $print_response - ); + $response = array($this->options['param_name'] => $files); + return $this->generate_response($response, $print_response); } public function delete($print_response = true) { @@ -1322,11 +1368,11 @@ class UploadHandler $file_names = array($this->get_file_name_param()); } $response = array(); - foreach($file_names as $file_name) { + foreach ($file_names as $file_name) { $file_path = $this->get_upload_path($file_name); $success = is_file($file_path) && $file_name[0] !== '.' && unlink($file_path); if ($success) { - foreach($this->options['image_versions'] as $version => $options) { + foreach ($this->options['image_versions'] as $version => $options) { if (!empty($version)) { $file = $this->get_upload_path($file_name, $version); if (is_file($file)) { @@ -1340,4 +1386,8 @@ class UploadHandler return $this->generate_response($response, $print_response); } + protected function basename($filepath, $suffix = null) { + $splited = preg_split('/\//', rtrim ($filepath, '/ ')); + return substr(basename('X'.$splited[count($splited)-1], $suffix), 1); + } } diff --git a/library/blueimp_upload/server/php/docker-compose.yml b/library/blueimp_upload/server/php/docker-compose.yml new file mode 100644 index 000000000..691ea9caa --- /dev/null +++ b/library/blueimp_upload/server/php/docker-compose.yml @@ -0,0 +1,6 @@ +apache: + build: ./ + ports: + - "80:80" + volumes: + - "../../:/var/www/html" diff --git a/library/blueimp_upload/server/php/files/.htaccess b/library/blueimp_upload/server/php/files/.htaccess index 56689f0bb..6f454afb9 100644 --- a/library/blueimp_upload/server/php/files/.htaccess +++ b/library/blueimp_upload/server/php/files/.htaccess @@ -1,8 +1,16 @@ -# The following directives force the content-type application/octet-stream -# and force browsers to display a download dialog for non-image files. -# This prevents the execution of script files in the context of the website: +# To enable the Headers module, execute the following command and reload Apache: +# sudo a2enmod headers + +# The following directives prevent the execution of script files +# in the context of the website. +# They also force the content-type application/octet-stream and +# force browsers to display a download dialog for non-image files. +SetHandler default-handler ForceType application/octet-stream Header set Content-Disposition attachment + +# The following unsets the forced type and Content-Disposition headers +# for known image files: ForceType none Header unset Content-Disposition diff --git a/library/blueimp_upload/server/php/index.php b/library/blueimp_upload/server/php/index.php index 3ae1295ef..6caabb710 100644 --- a/library/blueimp_upload/server/php/index.php +++ b/library/blueimp_upload/server/php/index.php @@ -1,13 +1,13 @@