diff options
Diffstat (limited to 'vendor/blueimp/jquery-file-upload/server')
12 files changed, 2134 insertions, 0 deletions
diff --git a/vendor/blueimp/jquery-file-upload/server/gae-go/app.yaml b/vendor/blueimp/jquery-file-upload/server/gae-go/app.yaml new file mode 100644 index 000000000..b5ac1a2e1 --- /dev/null +++ b/vendor/blueimp/jquery-file-upload/server/gae-go/app.yaml @@ -0,0 +1,10 @@ +runtime: go +api_version: go1 + +handlers: +- url: /(favicon\.ico|robots\.txt) + static_files: static/\1 + upload: static/(.*) + expiration: '1d' +- url: /.* + script: _go_app diff --git a/vendor/blueimp/jquery-file-upload/server/gae-go/main.go b/vendor/blueimp/jquery-file-upload/server/gae-go/main.go new file mode 100644 index 000000000..a92d128c0 --- /dev/null +++ b/vendor/blueimp/jquery-file-upload/server/gae-go/main.go @@ -0,0 +1,361 @@ +/* + * 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: + * https://opensource.org/licenses/MIT + */ + +package app + +import ( + "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" +) + +const ( + 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 + // 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 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) { + if acceptFileTypes.MatchString(fi.Type) { + return true + } + fi.Error = "Filetype not allowed" + return false +} + +func (fi *FileInfo) ValidateSize() (valid bool) { + if fi.Size < MIN_FILE_SIZE { + fi.Error = "File is too small" + } else if fi.Size > MAX_FILE_SIZE { + fi.Error = "File is too big" + } else { + return true + } + return false +} + +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 + fi.Key + fi.DeleteUrl = fi.Url + fi.DeleteType = "DELETE" + if fi.ThumbnailKey != "" { + fi.ThumbnailUrl = uString + fi.ThumbnailKey + } +} + +func (fi *FileInfo) SetKey(checksum uint32) { + fi.Key = escape(string(fi.Type)) + "/" + + escape(fmt.Sprint(checksum)) + "/" + + escape(string(fi.Name)) +} + +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(), + } + err = memcache.Set(c, item) + check(err) + fi.ThumbnailKey = thumbnailKey + } +} + +func handleUpload(r *http.Request, p *multipart.Part) (fi *FileInfo) { + fi = &FileInfo{ + Name: p.FileName(), + Type: p.Header.Get("Content-Type"), + } + if !fi.ValidateType() { + return + } + defer func() { + if rec := recover(); rec != nil { + log.Println(rec) + 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) + err = memcache.Set(context, item) + check(err) + fi.createThumb(&buffer, context) + fi.CreateUrls(r, context) + return +} + +func getFormValue(p *multipart.Part) string { + var b bytes.Buffer + io.CopyN(&b, p, int64(1<<20)) // Copy max: 1 MiB + return b.String() +} + +func handleUploads(r *http.Request) (fileInfos []*FileInfo) { + fileInfos = make([]*FileInfo, 0) + mr, err := r.MultipartReader() + check(err) + r.Form, err = url.ParseQuery(r.URL.RawQuery) + check(err) + part, err := mr.NextPart() + for err == nil { + if name := part.FormName(); name != "" { + if part.FileName() != "" { + fileInfos = append(fileInfos, handleUpload(r, part)) + } else { + r.Form[name] = append(r.Form[name], getFormValue(part)) + } + } + part, err = mr.NextPart() + } + 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 + } + // Use RequestURI instead of r.URL.Path, as we need the encoded form: + key := extractKey(r) + parts := strings.Split(key, "/") + if len(parts) == 3 { + 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) + b, err := json.Marshal(result) + check(err) + 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 + } + w.Header().Set("Cache-Control", "no-cache") + jsonType := "application/json" + if strings.Index(r.Header.Get("Accept"), jsonType) != -1 { + w.Header().Set("Content-Type", jsonType) + } + fmt.Fprintln(w, string(b)) +} + +func delete(w http.ResponseWriter, r *http.Request) { + 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) + fmt.Fprintln(w, string(b)) + } else { + http.Error(w, "405 Method not allowed", http.StatusMethodNotAllowed) + } +} + +func handle(w http.ResponseWriter, r *http.Request) { + params, err := url.ParseQuery(r.URL.RawQuery) + check(err) + w.Header().Add("Access-Control-Allow-Origin", "*") + w.Header().Add( + "Access-Control-Allow-Methods", + "OPTIONS, HEAD, GET, POST, DELETE", + ) + w.Header().Add( + "Access-Control-Allow-Headers", + "Content-Type, Content-Range, Content-Disposition", + ) + switch r.Method { + case "OPTIONS", "HEAD": + return + case "GET": + get(w, r) + case "POST": + if len(params["_method"]) > 0 && params["_method"][0] == "DELETE" { + delete(w, r) + } else { + post(w, r) + } + case "DELETE": + delete(w, r) + default: + http.Error(w, "501 Not Implemented", http.StatusNotImplemented) + } +} + +func init() { + http.HandleFunc("/", handle) +} diff --git a/vendor/blueimp/jquery-file-upload/server/gae-go/static/robots.txt b/vendor/blueimp/jquery-file-upload/server/gae-go/static/robots.txt new file mode 100644 index 000000000..eb0536286 --- /dev/null +++ b/vendor/blueimp/jquery-file-upload/server/gae-go/static/robots.txt @@ -0,0 +1,2 @@ +User-agent: * +Disallow: diff --git a/vendor/blueimp/jquery-file-upload/server/gae-python/app.yaml b/vendor/blueimp/jquery-file-upload/server/gae-python/app.yaml new file mode 100644 index 000000000..0c49462fa --- /dev/null +++ b/vendor/blueimp/jquery-file-upload/server/gae-python/app.yaml @@ -0,0 +1,15 @@ +runtime: python27 +api_version: 1 +threadsafe: true + +libraries: +- name: PIL + version: latest + +handlers: +- url: /(favicon\.ico|robots\.txt) + static_files: static/\1 + upload: static/(.*) + expiration: '1d' +- url: /.* + script: main.app diff --git a/vendor/blueimp/jquery-file-upload/server/gae-python/main.py b/vendor/blueimp/jquery-file-upload/server/gae-python/main.py new file mode 100644 index 000000000..1955ac00a --- /dev/null +++ b/vendor/blueimp/jquery-file-upload/server/gae-python/main.py @@ -0,0 +1,204 @@ +# -*- coding: utf-8 -*- +# +# 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: +# https://opensource.org/licenses/MIT +# + +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 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 +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 json_stringify(self, obj): + return json.dumps(obj, separators=(',', ':')) + + 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' + elif file['size'] > MAX_FILE_SIZE: + file['error'] = 'File is too big' + elif not ACCEPT_FILE_TYPES.match(file['type']): + file['error'] = 'Filetype not allowed' + else: + 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 + file.seek(0) # Reset the file position to the beginning + return size + + def write_blob(self, data, info): + 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 = [] + for name, fieldStorage in self.request.POST.items(): + if type(fieldStorage) is unicode: + continue + result = {} + result['name'] = urllib.unquote(fieldStorage.filename) + result['type'] = fieldStorage.type + result['size'] = self.get_file_size(fieldStorage.file) + if self.validate(result): + key, thumbnail_key = self.write_blob( + fieldStorage.value, + result + ) + 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) + return results + + def head(self): + pass + + def get(self): + self.redirect(WEBSITE) + + def post(self): + if (self.request.get('_method') == 'DELETE'): + return self.delete() + result = {'files': self.handle_upload()} + s = self.json_stringify(result) + redirect = self.request.get('redirect') + if self.validate_redirect(redirect): + return self.redirect(str( + redirect.replace('%s', urllib.quote(s, ''), 1) + )) + if 'application/json' in self.request.headers.get('Accept'): + self.response.headers['Content-Type'] = 'application/json' + self.response.write(s) + +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) + +app = webapp2.WSGIApplication( + [ + ('/', UploadHandler), + ('/(.+)/([^/]+)/([^/]+)', FileHandler) + ], + debug=DEBUG +) diff --git a/vendor/blueimp/jquery-file-upload/server/gae-python/static/robots.txt b/vendor/blueimp/jquery-file-upload/server/gae-python/static/robots.txt new file mode 100644 index 000000000..eb0536286 --- /dev/null +++ b/vendor/blueimp/jquery-file-upload/server/gae-python/static/robots.txt @@ -0,0 +1,2 @@ +User-agent: * +Disallow: diff --git a/vendor/blueimp/jquery-file-upload/server/php/Dockerfile b/vendor/blueimp/jquery-file-upload/server/php/Dockerfile new file mode 100644 index 000000000..8633fee74 --- /dev/null +++ b/vendor/blueimp/jquery-file-upload/server/php/Dockerfile @@ -0,0 +1,38 @@ +FROM php:7-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/vendor/blueimp/jquery-file-upload/server/php/UploadHandler.php b/vendor/blueimp/jquery-file-upload/server/php/UploadHandler.php new file mode 100755 index 000000000..e44004395 --- /dev/null +++ b/vendor/blueimp/jquery-file-upload/server/php/UploadHandler.php @@ -0,0 +1,1449 @@ +<?php +/* + * jQuery File Upload Plugin PHP Class + * https://github.com/blueimp/jQuery-File-Upload + * + * Copyright 2010, Sebastian Tschan + * https://blueimp.net + * + * Licensed under the MIT license: + * https://opensource.org/licenses/MIT + */ + +class UploadHandler +{ + + protected $options; + + // PHP File Upload error message codes: + // http://php.net/manual/en/features.file-upload.errors.php + protected $error_messages = array( + 1 => 'The uploaded file exceeds the upload_max_filesize directive in php.ini', + 2 => 'The uploaded file exceeds the MAX_FILE_SIZE directive that was specified in the HTML form', + 3 => 'The uploaded file was only partially uploaded', + 4 => 'No file was uploaded', + 6 => 'Missing a temporary folder', + 7 => 'Failed to write file to disk', + 8 => 'A PHP extension stopped the file upload', + 'post_max_size' => 'The uploaded file exceeds the post_max_size directive in php.ini', + 'max_file_size' => 'File is too big', + 'min_file_size' => 'File is too small', + 'accept_file_types' => 'Filetype not allowed', + 'max_number_of_files' => 'Maximum number of files exceeded', + 'max_width' => 'Image exceeds maximum width', + 'min_width' => 'Image requires a minimum width', + 'max_height' => 'Image exceeds maximum height', + 'min_height' => 'Image requires a minimum height', + 'abort' => 'File upload aborted', + 'image_resize' => 'Failed to resize image' + ); + + protected const IMAGETYPE_GIF = 1; + protected const IMAGETYPE_JPEG = 2; + protected const IMAGETYPE_PNG = 3; + + protected $image_objects = array(); + + public function __construct($options = null, $initialize = true, $error_messages = null) { + $this->response = array(); + $this->options = array( + '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', + // Set the following option to 'POST', if your server does not support + // DELETE requests. This is a parameter sent to the client: + 'delete_type' => 'DELETE', + 'access_control_allow_origin' => '*', + 'access_control_allow_credentials' => false, + 'access_control_allow_methods' => array( + 'OPTIONS', + 'HEAD', + 'GET', + 'POST', + 'PUT', + 'PATCH', + 'DELETE' + ), + 'access_control_allow_headers' => array( + 'Content-Type', + '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 + // 3. Set to 3 to send a X-Accel-Redirect header for nginx + // If set to 2 or 3, adjust the upload_url option to the base path of + // the redirect parameter, e.g. '/files/'. + 'download_via_php' => false, + // Read files in chunks to avoid memory limits when download_via_php + // is enabled, set to 0 to disable chunked reading of files: + 'readfile_chunk_size' => 10 * 1024 * 1024, // 10 MiB + // Defines which files can be displayed inline when downloaded: + 'inline_file_types' => '/\.(gif|jpe?g|png)$/i', + // Defines which files (based on their names) are accepted for upload. + // By default, only allows file uploads with image file extensions. + // Only change this setting after making sure that any allowed file + // types cannot be executed by the webserver in the files directory, + // e.g. PHP scripts, nor executed by the browser when downloaded, + // e.g. HTML files with embedded JavaScript code. + // Please also read the SECURITY.md document in this repository. + 'accept_file_types' => '/\.(gif|jpe?g|png)$/i', + // Replaces dots in filenames with the given string. + // Can be disabled by setting it to false or an empty string. + // Note that this is a security feature for servers that support + // multiple file extensions, e.g. the Apache AddHandler Directive: + // https://httpd.apache.org/docs/current/mod/mod_mime.html#addhandler + // Before disabling it, make sure that files uploaded with multiple + // extensions cannot be executed by the webserver, e.g. + // "example.php.png" with embedded PHP code, nor executed by the + // browser when downloaded, e.g. "example.html.gif" with embedded + // JavaScript code. + 'replace_dots_in_filenames' => '-', + // The php.ini settings upload_max_filesize and post_max_size + // take precedence over the following max_file_size setting: + 'max_file_size' => null, + 'min_file_size' => 1, + // The maximum number of files for the upload directory: + 'max_number_of_files' => null, + // Reads first file bytes to identify and correct file extensions: + 'correct_image_extensions' => false, + // Image resolution restrictions: + 'max_width' => null, + 'max_height' => null, + 'min_width' => 1, + 'min_height' => 1, + // Set the following option to false to enable resumable uploads: + 'discard_aborted_uploads' => true, + // Set to 0 to use the GD library to scale and orient images, + // set to 1 to use imagick (if installed, falls back to GD), + // set to 2 to use the ImageMagick convert binary directly: + 'image_library' => 1, + // Uncomment the following to define an array of resource limits + // for imagick: + /* + 'imagick_resource_limits' => array( + imagick::RESOURCETYPE_MAP => 32, + imagick::RESOURCETYPE_MEMORY => 32 + ), + */ + // Command or path for to the ImageMagick convert binary: + 'convert_bin' => 'convert', + // Uncomment the following to add parameters in front of each + // ImageMagick convert call (the limit constraints seem only + // to have an effect if put in front): + /* + 'convert_params' => '-limit memory 32MiB -limit map 32MiB', + */ + // Command or path for to the ImageMagick identify binary: + 'identify_bin' => 'identify', + 'image_versions' => array( + // The empty image version key defines options for the original image. + // Keep in mind: these image manipulations are inherited by all other image versions from this point onwards. + // Also note that the property 'no_cache' is not inherited, since it's not a manipulation. + '' => array( + // Automatically rotate images based on EXIF meta data: + 'auto_orient' => true + ), + // You can add arrays to generate different versions. + // The name of the key is the name of the version (example: 'medium'). + // the array contains the options to apply. + /* + 'medium' => array( + 'max_width' => 800, + 'max_height' => 600 + ), + */ + 'thumbnail' => array( + // Uncomment the following to use a defined directory for the thumbnails + // instead of a subdirectory based on the version identifier. + // Make sure that this directory doesn't allow execution of files if you + // don't pose any restrictions on the type of uploaded files, e.g. by + // copying the .htaccess file from the files directory for Apache: + //'upload_dir' => dirname($this->get_server_var('SCRIPT_FILENAME')).'/thumb/', + //'upload_url' => $this->get_full_url().'/thumb/', + // Uncomment the following to force the max + // dimensions and e.g. create square thumbnails: + // 'auto_orient' => true, + // 'crop' => true, + // 'jpeg_quality' => 70, + // 'no_cache' => true, (there's a caching option, but this remembers thumbnail sizes from a previous action!) + // 'strip' => true, (this strips EXIF tags, such as geolocation) + 'max_width' => 80, // either specify width, or set to 0. Then width is automatically adjusted - keeping aspect ratio to a specified max_height. + 'max_height' => 80 // either specify height, or set to 0. Then height is automatically adjusted - keeping aspect ratio to a specified max_width. + ) + ), + 'print_response' => true + ); + if ($options) { + $this->options = $options + $this->options; + } + if ($error_messages) { + $this->error_messages = $error_messages + $this->error_messages; + } + if ($initialize) { + $this->initialize(); + } + } + + protected function initialize() { + switch ($this->get_server_var('REQUEST_METHOD')) { + case 'OPTIONS': + case 'HEAD': + $this->head(); + break; + case 'GET': + $this->get($this->options['print_response']); + break; + case 'PATCH': + case 'PUT': + case 'POST': + $this->post($this->options['print_response']); + break; + case 'DELETE': + $this->delete($this->options['print_response']); + break; + default: + $this->header('HTTP/1.1 405 Method Not Allowed'); + } + } + + protected function get_full_url() { + $https = !empty($_SERVER['HTTPS']) && strcasecmp($_SERVER['HTTPS'], 'on') === 0 || + !empty($_SERVER['HTTP_X_FORWARDED_PROTO']) && + strcasecmp($_SERVER['HTTP_X_FORWARDED_PROTO'], 'https') === 0; + return + ($https ? 'https://' : 'http://'). + (!empty($_SERVER['REMOTE_USER']) ? $_SERVER['REMOTE_USER'].'@' : ''). + (isset($_SERVER['HTTP_HOST']) ? $_SERVER['HTTP_HOST'] : ($_SERVER['SERVER_NAME']. + ($https && $_SERVER['SERVER_PORT'] === 443 || + $_SERVER['SERVER_PORT'] === 80 ? '' : ':'.$_SERVER['SERVER_PORT']))). + substr($_SERVER['SCRIPT_NAME'],0, strrpos($_SERVER['SCRIPT_NAME'], '/')); + } + + protected function get_user_id() { + @session_start(); + return session_id(); + } + + protected function get_user_path() { + if ($this->options['user_dirs']) { + return $this->get_user_id().'/'; + } + return ''; + } + + protected function get_upload_path($file_name = null, $version = null) { + $file_name = $file_name ? $file_name : ''; + if (empty($version)) { + $version_path = ''; + } else { + $version_dir = @$this->options['image_versions'][$version]['upload_dir']; + if ($version_dir) { + return $version_dir.$this->get_user_path().$file_name; + } + $version_path = $version.'/'; + } + return $this->options['upload_dir'].$this->get_user_path() + .$version_path.$file_name; + } + + protected function get_query_separator($url) { + return strpos($url, '?') === false ? '?' : '&'; + } + + protected function get_download_url($file_name, $version = null, $direct = false) { + if (!$direct && $this->options['download_via_php']) { + $url = $this->options['script_url'] + .$this->get_query_separator($this->options['script_url']) + .$this->get_singular_param_name() + .'='.rawurlencode($file_name); + if ($version) { + $url .= '&version='.rawurlencode($version); + } + return $url.'&download=1'; + } + if (empty($version)) { + $version_path = ''; + } else { + $version_url = @$this->options['image_versions'][$version]['upload_url']; + if ($version_url) { + return $version_url.$this->get_user_path().rawurlencode($file_name); + } + $version_path = rawurlencode($version).'/'; + } + return $this->options['upload_url'].$this->get_user_path() + .$version_path.rawurlencode($file_name); + } + + protected function set_additional_file_properties($file) { + $file->deleteUrl = $this->options['script_url'] + .$this->get_query_separator($this->options['script_url']) + .$this->get_singular_param_name() + .'='.rawurlencode($file->name); + $file->deleteType = $this->options['delete_type']; + if ($file->deleteType !== 'DELETE') { + $file->deleteUrl .= '&_method=DELETE'; + } + if ($this->options['access_control_allow_credentials']) { + $file->deleteWithCredentials = true; + } + } + + // Fix for overflowing signed 32 bit integers, + // works for sizes up to 2^32-1 bytes (4 GiB - 1): + protected function fix_integer_overflow($size) { + if ($size < 0) { + $size += 2.0 * (PHP_INT_MAX + 1); + } + return $size; + } + + protected function get_file_size($file_path, $clear_stat_cache = false) { + if ($clear_stat_cache) { + if (version_compare(PHP_VERSION, '5.3.0') >= 0) { + clearstatcache(true, $file_path); + } else { + clearstatcache(); + } + } + return $this->fix_integer_overflow(filesize($file_path)); + } + + protected function is_valid_file_object($file_name) { + $file_path = $this->get_upload_path($file_name); + if (is_file($file_path) && $file_name[0] !== '.') { + return true; + } + return false; + } + + protected function get_file_object($file_name) { + if ($this->is_valid_file_object($file_name)) { + $file = new \stdClass(); + $file->name = $file_name; + $file->size = $this->get_file_size( + $this->get_upload_path($file_name) + ); + $file->url = $this->get_download_url($file->name); + 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( + $file->name, + $version + ); + } + } + } + $this->set_additional_file_properties($file); + return $file; + } + return null; + } + + protected function get_file_objects($iteration_method = 'get_file_object') { + $upload_dir = $this->get_upload_path(); + if (!is_dir($upload_dir)) { + return array(); + } + return array_values(array_filter(array_map( + array($this, $iteration_method), + scandir($upload_dir) + ))); + } + + protected function count_file_objects() { + return count($this->get_file_objects('is_valid_file_object')); + } + + protected function get_error_message($error) { + return isset($this->error_messages[$error]) ? + $this->error_messages[$error] : $error; + } + + public function get_config_bytes($val) { + $val = trim($val); + $last = strtolower($val[strlen($val)-1]); + $val = (int)$val; + switch ($last) { + case 'g': + $val *= 1024; + case 'm': + $val *= 1024; + case 'k': + $val *= 1024; + } + return $this->fix_integer_overflow($val); + } + + protected function validate($uploaded_file, $file, $error, $index) { + if ($error) { + $file->error = $this->get_error_message($error); + return false; + } + $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'); + return false; + } + if (!preg_match($this->options['accept_file_types'], $file->name)) { + $file->error = $this->get_error_message('accept_file_types'); + return false; + } + if ($uploaded_file && is_uploaded_file($uploaded_file)) { + $file_size = $this->get_file_size($uploaded_file); + } else { + $file_size = $content_length; + } + if ($this->options['max_file_size'] && ( + $file_size > $this->options['max_file_size'] || + $file->size > $this->options['max_file_size']) + ) { + $file->error = $this->get_error_message('max_file_size'); + return false; + } + if ($this->options['min_file_size'] && + $file_size < $this->options['min_file_size']) { + $file->error = $this->get_error_message('min_file_size'); + return false; + } + if (is_int($this->options['max_number_of_files']) && + ($this->count_file_objects() >= $this->options['max_number_of_files']) && + // Ignore additional chunks of existing files: + !is_file($this->get_upload_path($file->name))) { + $file->error = $this->get_error_message('max_number_of_files'); + return false; + } + $max_width = @$this->options['max_width']; + $max_height = @$this->options['max_height']; + $min_width = @$this->options['min_width']; + $min_height = @$this->options['min_height']; + if (($max_width || $max_height || $min_width || $min_height) + && $this->is_valid_image_file($uploaded_file)) { + 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) { + $file->error = $this->get_error_message('max_width'); + return false; + } + if ($max_height && $img_height > $max_height) { + $file->error = $this->get_error_message('max_height'); + return false; + } + if ($min_width && $img_width < $min_width) { + $file->error = $this->get_error_message('min_width'); + return false; + } + if ($min_height && $img_height < $min_height) { + $file->error = $this->get_error_message('min_height'); + return false; + } + } + return true; + } + + protected function upcount_name_callback($matches) { + $index = isset($matches[1]) ? ((int)$matches[1]) + 1 : 1; + $ext = isset($matches[2]) ? $matches[2] : ''; + return ' ('.$index.')'.$ext; + } + + protected function upcount_name($name) { + return preg_replace_callback( + '/(?:(?: \(([\d]+)\))?(\.[^.]+))?$/', + array($this, 'upcount_name_callback'), + $name, + 1 + ); + } + + protected function get_unique_filename($file_path, $name, $size, $type, $error, + $index, $content_range) { + while(is_dir($this->get_upload_path($name))) { + $name = $this->upcount_name($name); + } + // Keep an existing filename if this is part of a chunked upload: + $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; + } + $name = $this->upcount_name($name); + } + return $name; + } + + protected function fix_file_extension($file_path, $name, $size, $type, $error, + $index, $content_range) { + // Add missing file extension for known image types: + if (strpos($name, '.') === false && + preg_match('/^image\/(gif|jpe?g|png)/', $type, $matches)) { + $name .= '.'.$matches[1]; + } + if ($this->options['correct_image_extensions']) { + switch ($this->imagetype($file_path)) { + case self::IMAGETYPE_JPEG: + $extensions = array('jpg', 'jpeg'); + break; + case self::IMAGETYPE_PNG: + $extensions = array('png'); + break; + case self::IMAGETYPE_GIF: + $extensions = array('gif'); + break; + } + // Adjust incorrect image file extensions: + if (!empty($extensions)) { + $parts = explode('.', $name); + $extIndex = count($parts) - 1; + $ext = strtolower(@$parts[$extIndex]); + if (!in_array($ext, $extensions)) { + $parts[$extIndex] = $extensions[0]; + $name = implode('.', $parts); + } + } + } + return $name; + } + + protected function trim_file_name($file_path, $name, $size, $type, $error, + $index, $content_range) { + // 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($this->basename(stripslashes($name)), ".\x00..\x20"); + // Replace dots in filenames to avoid security issues with servers + // that interpret multiple file extensions, e.g. "example.php.png": + $replacement = $this->options['replace_dots_in_filenames']; + if (!empty($replacement)) { + $parts = explode('.', $name); + if (count($parts) > 2) { + $ext = array_pop($parts); + $name = implode($replacement, $parts).'.'.$ext; + } + } + // Use a timestamp for empty filenames: + if (!$name) { + $name = str_replace('.', '-', microtime(true)); + } + return $name; + } + + protected function get_file_name($file_path, $name, $size, $type, $error, + $index, $content_range) { + $name = $this->trim_file_name($file_path, $name, $size, $type, $error, + $index, $content_range); + return $this->get_unique_filename( + $file_path, + $this->fix_file_extension($file_path, $name, $size, $type, $error, + $index, $content_range), + $size, + $type, + $error, + $index, + $content_range + ); + } + + protected function get_scaled_image_file_paths($file_name, $version) { + $file_path = $this->get_upload_path($file_name); + if (!empty($version)) { + $version_dir = $this->get_upload_path(null, $version); + if (!is_dir($version_dir)) { + mkdir($version_dir, $this->options['mkdir_mode'], true); + } + $new_file_path = $version_dir.'/'.$file_name; + } else { + $new_file_path = $file_path; + } + return array($file_path, $new_file_path); + } + + protected function gd_get_image_object($file_path, $func, $no_cache = false) { + if (empty($this->image_objects[$file_path]) || $no_cache) { + $this->gd_destroy_image_object($file_path); + $this->image_objects[$file_path] = $func($file_path); + } + return $this->image_objects[$file_path]; + } + + protected function gd_set_image_object($file_path, $image) { + $this->gd_destroy_image_object($file_path); + $this->image_objects[$file_path] = $image; + } + + protected function gd_destroy_image_object($file_path) { + $image = (isset($this->image_objects[$file_path])) ? $this->image_objects[$file_path] : null ; + return $image && imagedestroy($image); + } + + protected function gd_imageflip($image, $mode) { + if (function_exists('imageflip')) { + return imageflip($image, $mode); + } + $new_width = $src_width = imagesx($image); + $new_height = $src_height = imagesy($image); + $new_img = imagecreatetruecolor($new_width, $new_height); + $src_x = 0; + $src_y = 0; + switch ($mode) { + case '1': // flip on the horizontal axis + $src_y = $new_height - 1; + $src_height = -$new_height; + break; + case '2': // flip on the vertical axis + $src_x = $new_width - 1; + $src_width = -$new_width; + break; + case '3': // flip on both axes + $src_y = $new_height - 1; + $src_height = -$new_height; + $src_x = $new_width - 1; + $src_width = -$new_width; + break; + default: + return $image; + } + imagecopyresampled( + $new_img, + $image, + 0, + 0, + $src_x, + $src_y, + $new_width, + $new_height, + $src_width, + $src_height + ); + return $new_img; + } + + protected function gd_orient_image($file_path, $src_img) { + if (!function_exists('exif_read_data')) { + return false; + } + $exif = @exif_read_data($file_path); + if ($exif === false) { + return false; + } + $orientation = (int)@$exif['Orientation']; + if ($orientation < 2 || $orientation > 8) { + return false; + } + switch ($orientation) { + case 2: + $new_img = $this->gd_imageflip( + $src_img, + defined('IMG_FLIP_VERTICAL') ? IMG_FLIP_VERTICAL : 2 + ); + break; + case 3: + $new_img = imagerotate($src_img, 180, 0); + break; + case 4: + $new_img = $this->gd_imageflip( + $src_img, + defined('IMG_FLIP_HORIZONTAL') ? IMG_FLIP_HORIZONTAL : 1 + ); + break; + case 5: + $tmp_img = $this->gd_imageflip( + $src_img, + defined('IMG_FLIP_HORIZONTAL') ? IMG_FLIP_HORIZONTAL : 1 + ); + $new_img = imagerotate($tmp_img, 270, 0); + imagedestroy($tmp_img); + break; + case 6: + $new_img = imagerotate($src_img, 270, 0); + break; + case 7: + $tmp_img = $this->gd_imageflip( + $src_img, + defined('IMG_FLIP_VERTICAL') ? IMG_FLIP_VERTICAL : 2 + ); + $new_img = imagerotate($tmp_img, 270, 0); + imagedestroy($tmp_img); + break; + case 8: + $new_img = imagerotate($src_img, 90, 0); + break; + default: + return false; + } + $this->gd_set_image_object($file_path, $new_img); + return true; + } + + protected function gd_create_scaled_image($file_name, $version, $options) { + if (!function_exists('imagecreatetruecolor')) { + error_log('Function not found: imagecreatetruecolor'); + return false; + } + list($file_path, $new_file_path) = + $this->get_scaled_image_file_paths($file_name, $version); + $type = strtolower(substr(strrchr($file_name, '.'), 1)); + switch ($type) { + case 'jpg': + case 'jpeg': + $src_func = 'imagecreatefromjpeg'; + $write_func = 'imagejpeg'; + $image_quality = isset($options['jpeg_quality']) ? + $options['jpeg_quality'] : 75; + break; + case 'gif': + $src_func = 'imagecreatefromgif'; + $write_func = 'imagegif'; + $image_quality = null; + break; + case 'png': + $src_func = 'imagecreatefrompng'; + $write_func = 'imagepng'; + $image_quality = isset($options['png_quality']) ? + $options['png_quality'] : 9; + break; + default: + return false; + } + $src_img = $this->gd_get_image_object( + $file_path, + $src_func, + !empty($options['no_cache']) + ); + $image_oriented = false; + if (!empty($options['auto_orient']) && $this->gd_orient_image( + $file_path, + $src_img + )) { + $image_oriented = true; + $src_img = $this->gd_get_image_object( + $file_path, + $src_func + ); + } + $max_width = $img_width = imagesx($src_img); + $max_height = $img_height = imagesy($src_img); + if (!empty($options['max_width'])) { + $max_width = $options['max_width']; + } + if (!empty($options['max_height'])) { + $max_height = $options['max_height']; + } + $scale = min( + $max_width / $img_width, + $max_height / $img_height + ); + if ($scale >= 1) { + if ($image_oriented) { + return $write_func($src_img, $new_file_path, $image_quality); + } + if ($file_path !== $new_file_path) { + return copy($file_path, $new_file_path); + } + return true; + } + if (empty($options['crop'])) { + $new_width = $img_width * $scale; + $new_height = $img_height * $scale; + $dst_x = 0; + $dst_y = 0; + $new_img = imagecreatetruecolor($new_width, $new_height); + } else { + if (($img_width / $img_height) >= ($max_width / $max_height)) { + $new_width = $img_width / ($img_height / $max_height); + $new_height = $max_height; + } else { + $new_width = $max_width; + $new_height = $img_height / ($img_width / $max_width); + } + $dst_x = 0 - ($new_width - $max_width) / 2; + $dst_y = 0 - ($new_height - $max_height) / 2; + $new_img = imagecreatetruecolor($max_width, $max_height); + } + // Handle transparency in GIF and PNG images: + switch ($type) { + case 'gif': + case 'png': + imagecolortransparent($new_img, imagecolorallocate($new_img, 0, 0, 0)); + case 'png': + imagealphablending($new_img, false); + imagesavealpha($new_img, true); + break; + } + $success = imagecopyresampled( + $new_img, + $src_img, + $dst_x, + $dst_y, + 0, + 0, + $new_width, + $new_height, + $img_width, + $img_height + ) && $write_func($new_img, $new_file_path, $image_quality); + $this->gd_set_image_object($file_path, $new_img); + return $success; + } + + protected function imagick_get_image_object($file_path, $no_cache = false) { + if (empty($this->image_objects[$file_path]) || $no_cache) { + $this->imagick_destroy_image_object($file_path); + $image = new \Imagick(); + if (!empty($this->options['imagick_resource_limits'])) { + foreach ($this->options['imagick_resource_limits'] as $type => $limit) { + $image->setResourceLimit($type, $limit); + } + } + $image->readImage($file_path); + $this->image_objects[$file_path] = $image; + } + return $this->image_objects[$file_path]; + } + + protected function imagick_set_image_object($file_path, $image) { + $this->imagick_destroy_image_object($file_path); + $this->image_objects[$file_path] = $image; + } + + protected function imagick_destroy_image_object($file_path) { + $image = (isset($this->image_objects[$file_path])) ? $this->image_objects[$file_path] : null ; + return $image && $image->destroy(); + } + + protected function imagick_orient_image($image) { + $orientation = $image->getImageOrientation(); + $background = new \ImagickPixel('none'); + switch ($orientation) { + case \imagick::ORIENTATION_TOPRIGHT: // 2 + $image->flopImage(); // horizontal flop around y-axis + break; + case \imagick::ORIENTATION_BOTTOMRIGHT: // 3 + $image->rotateImage($background, 180); + break; + case \imagick::ORIENTATION_BOTTOMLEFT: // 4 + $image->flipImage(); // vertical flip around x-axis + break; + case \imagick::ORIENTATION_LEFTTOP: // 5 + $image->flopImage(); // horizontal flop around y-axis + $image->rotateImage($background, 270); + break; + case \imagick::ORIENTATION_RIGHTTOP: // 6 + $image->rotateImage($background, 90); + break; + case \imagick::ORIENTATION_RIGHTBOTTOM: // 7 + $image->flipImage(); // vertical flip around x-axis + $image->rotateImage($background, 270); + break; + case \imagick::ORIENTATION_LEFTBOTTOM: // 8 + $image->rotateImage($background, 270); + break; + default: + return false; + } + $image->setImageOrientation(\imagick::ORIENTATION_TOPLEFT); // 1 + return true; + } + + protected function imagick_create_scaled_image($file_name, $version, $options) { + list($file_path, $new_file_path) = + $this->get_scaled_image_file_paths($file_name, $version); + $image = $this->imagick_get_image_object( + $file_path, + !empty($options['crop']) || !empty($options['no_cache']) + ); + if ($image->getImageFormat() === 'GIF') { + // Handle animated GIFs: + $images = $image->coalesceImages(); + foreach ($images as $frame) { + $image = $frame; + $this->imagick_set_image_object($file_name, $image); + break; + } + } + $image_oriented = false; + if (!empty($options['auto_orient'])) { + $image_oriented = $this->imagick_orient_image($image); + } + + $image_resize = false; + $new_width = $max_width = $img_width = $image->getImageWidth(); + $new_height = $max_height = $img_height = $image->getImageHeight(); + + // use isset(). User might be setting max_width = 0 (auto in regular resizing). Value 0 would be considered empty when you use empty() + if (isset($options['max_width'])) { + $image_resize = true; + $new_width = $max_width = $options['max_width']; + } + if (isset($options['max_height'])) { + $image_resize = true; + $new_height = $max_height = $options['max_height']; + } + + $image_strip = (isset($options['strip']) ? $options['strip'] : false); + + if ( !$image_oriented && ($max_width >= $img_width) && ($max_height >= $img_height) && !$image_strip && empty($options["jpeg_quality"]) ) { + if ($file_path !== $new_file_path) { + return copy($file_path, $new_file_path); + } + return true; + } + $crop = (isset($options['crop']) ? $options['crop'] : false); + + if ($crop) { + $x = 0; + $y = 0; + if (($img_width / $img_height) >= ($max_width / $max_height)) { + $new_width = 0; // Enables proportional scaling based on max_height + $x = ($img_width / ($img_height / $max_height) - $max_width) / 2; + } else { + $new_height = 0; // Enables proportional scaling based on max_width + $y = ($img_height / ($img_width / $max_width) - $max_height) / 2; + } + } + $success = $image->resizeImage( + $new_width, + $new_height, + isset($options['filter']) ? $options['filter'] : \imagick::FILTER_LANCZOS, + isset($options['blur']) ? $options['blur'] : 1, + $new_width && $new_height // fit image into constraints if not to be cropped + ); + if ($success && $crop) { + $success = $image->cropImage( + $max_width, + $max_height, + $x, + $y + ); + if ($success) { + $success = $image->setImagePage($max_width, $max_height, 0, 0); + } + } + $type = strtolower(substr(strrchr($file_name, '.'), 1)); + switch ($type) { + case 'jpg': + case 'jpeg': + if (!empty($options['jpeg_quality'])) { + $image->setImageCompression(\imagick::COMPRESSION_JPEG); + $image->setImageCompressionQuality($options['jpeg_quality']); + } + break; + } + if ( $image_strip ) { + $image->stripImage(); + } + return $success && $image->writeImage($new_file_path); + } + + protected function imagemagick_create_scaled_image($file_name, $version, $options) { + list($file_path, $new_file_path) = + $this->get_scaled_image_file_paths($file_name, $version); + $resize = @$options['max_width'] + .(empty($options['max_height']) ? '' : 'X'.$options['max_height']); + if (!$resize && empty($options['auto_orient'])) { + if ($file_path !== $new_file_path) { + return copy($file_path, $new_file_path); + } + return true; + } + $cmd = $this->options['convert_bin']; + if (!empty($this->options['convert_params'])) { + $cmd .= ' '.$this->options['convert_params']; + } + $cmd .= ' '.escapeshellarg($file_path); + if (!empty($options['auto_orient'])) { + $cmd .= ' -auto-orient'; + } + if ($resize) { + // Handle animated GIFs: + $cmd .= ' -coalesce'; + if (empty($options['crop'])) { + $cmd .= ' -resize '.escapeshellarg($resize.'>'); + } else { + $cmd .= ' -resize '.escapeshellarg($resize.'^'); + $cmd .= ' -gravity center'; + $cmd .= ' -crop '.escapeshellarg($resize.'+0+0'); + } + // Make sure the page dimensions are correct (fixes offsets of animated GIFs): + $cmd .= ' +repage'; + } + if (!empty($options['convert_params'])) { + $cmd .= ' '.$options['convert_params']; + } + $cmd .= ' '.escapeshellarg($new_file_path); + exec($cmd, $output, $error); + if ($error) { + error_log(implode('\n', $output)); + return false; + } + return true; + } + + protected function get_image_size($file_path) { + if ($this->options['image_library']) { + if (extension_loaded('imagick')) { + $image = new \Imagick(); + try { + if (@$image->pingImage($file_path)) { + $dimensions = array($image->getImageWidth(), $image->getImageHeight()); + $image->destroy(); + return $dimensions; + } + return false; + } catch (\Exception $e) { + error_log($e->getMessage()); + } + } + if ($this->options['image_library'] === 2) { + $cmd = $this->options['identify_bin']; + $cmd .= ' -ping '.escapeshellarg($file_path); + 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+/', substr($output[0], strlen($file_path))); + $dimensions = preg_split('/x/', $infos[2]); + return $dimensions; + } + return false; + } + } + if (!function_exists('getimagesize')) { + error_log('Function not found: getimagesize'); + return false; + } + return @getimagesize($file_path); + } + + protected function create_scaled_image($file_name, $version, $options) { + if ($this->options['image_library'] === 2) { + return $this->imagemagick_create_scaled_image($file_name, $version, $options); + } + if ($this->options['image_library'] && extension_loaded('imagick')) { + return $this->imagick_create_scaled_image($file_name, $version, $options); + } + return $this->gd_create_scaled_image($file_name, $version, $options); + } + + protected function destroy_image_object($file_path) { + if ($this->options['image_library'] && extension_loaded('imagick')) { + return $this->imagick_destroy_image_object($file_path); + } + } + + protected function imagetype($file_path) { + $fp = fopen($file_path, 'r'); + $data = fread($fp, 4); + fclose($fp); + // GIF: 47 49 46 + if (substr($data, 0, 3) === 'GIF') { + return self::IMAGETYPE_GIF; + } + // JPG: FF D8 + if (bin2hex(substr($data, 0, 2)) === 'ffd8') { + return self::IMAGETYPE_JPEG; + } + // PNG: 89 50 4E 47 + if (bin2hex(@$data[0]).substr($data, 1, 4) === '89PNG') { + return self::IMAGETYPE_PNG; + } + return false; + } + + protected function is_valid_image_file($file_path) { + return !!$this->imagetype($file_path); + } + + protected function handle_image_file($file_path, $file) { + $failed_versions = array(); + 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( + $file->name, + $version + ); + } else { + $file->size = $this->get_file_size($file_path, true); + } + } else { + $failed_versions[] = $version ? $version : 'original'; + } + } + if (count($failed_versions)) { + $file->error = $this->get_error_message('image_resize') + .' ('.implode($failed_versions, ', ').')'; + } + // Free memory: + $this->destroy_image_object($file_path); + } + + protected function handle_file_upload($uploaded_file, $name, $size, $type, $error, + $index = null, $content_range = null) { + $file = new \stdClass(); + $file->name = $this->get_file_name($uploaded_file, $name, $size, $type, $error, + $index, $content_range); + $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); + $upload_dir = $this->get_upload_path(); + if (!is_dir($upload_dir)) { + mkdir($upload_dir, $this->options['mkdir_mode'], true); + } + $file_path = $this->get_upload_path($file->name); + $append_file = $content_range && is_file($file_path) && + $file->size > $this->get_file_size($file_path); + if ($uploaded_file && is_uploaded_file($uploaded_file)) { + // multipart/formdata uploads (POST method uploads) + if ($append_file) { + file_put_contents( + $file_path, + fopen($uploaded_file, 'r'), + FILE_APPEND + ); + } else { + move_uploaded_file($uploaded_file, $file_path); + } + } else { + // Non-multipart uploads (PUT method support) + file_put_contents( + $file_path, + fopen($this->options['input_stream'], 'r'), + $append_file ? FILE_APPEND : 0 + ); + } + $file_size = $this->get_file_size($file_path, $append_file); + if ($file_size === $file->size) { + $file->url = $this->get_download_url($file->name); + if ($this->is_valid_image_file($file_path)) { + $this->handle_image_file($file_path, $file); + } + } else { + $file->size = $file_size; + if (!$content_range && $this->options['discard_aborted_uploads']) { + unlink($file_path); + $file->error = $this->get_error_message('abort'); + } + } + $this->set_additional_file_properties($file); + } + return $file; + } + + protected function readfile($file_path) { + $file_size = $this->get_file_size($file_path); + $chunk_size = $this->options['readfile_chunk_size']; + if ($chunk_size && $file_size > $chunk_size) { + $handle = fopen($file_path, 'rb'); + while (!feof($handle)) { + echo fread($handle, $chunk_size); + @ob_flush(); + @flush(); + } + fclose($handle); + return $file_size; + } + return readfile($file_path); + } + + 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 @$_SERVER[$id]; + } + + protected function handle_form_data($file, $index) { + // Handle form data, e.g. $_POST['description'][$index] + } + + protected function get_version_param() { + return $this->basename(stripslashes($this->get_query_param('version'))); + } + + protected function get_singular_param_name() { + return substr($this->options['param_name'], 0, -1); + } + + protected function get_file_name_param() { + $name = $this->get_singular_param_name(); + return $this->basename(stripslashes($this->get_query_param($name))); + } + + protected function get_file_names_params() { + $params = $this->get_query_param($this->options['param_name']); + if (!$params) { + return null; + } + foreach ($params as $key => $value) { + $params[$key] = $this->basename(stripslashes($value)); + } + return $params; + } + + protected function get_file_type($file_path) { + switch (strtolower(pathinfo($file_path, PATHINFO_EXTENSION))) { + case 'jpeg': + case 'jpg': + return 'image/jpeg'; + case 'png': + return 'image/png'; + case 'gif': + return 'image/gif'; + default: + return ''; + } + } + + protected function download() { + switch ($this->options['download_via_php']) { + case 1: + $redirect_header = null; + break; + case 2: + $redirect_header = 'X-Sendfile'; + break; + case 3: + $redirect_header = 'X-Accel-Redirect'; + break; + default: + return $this->header('HTTP/1.1 403 Forbidden'); + } + $file_name = $this->get_file_name_param(); + if (!$this->is_valid_file_object($file_name)) { + return $this->header('HTTP/1.1 404 Not Found'); + } + if ($redirect_header) { + return $this->header( + $redirect_header.': '.$this->get_download_url( + $file_name, + $this->get_version_param(), + true + ) + ); + } + $file_path = $this->get_upload_path($file_name, $this->get_version_param()); + // Prevent browsers from MIME-sniffing the content-type: + $this->header('X-Content-Type-Options: nosniff'); + if (!preg_match($this->options['inline_file_types'], $file_name)) { + $this->header('Content-Type: application/octet-stream'); + $this->header('Content-Disposition: attachment; filename="'.$file_name.'"'); + } else { + $this->header('Content-Type: '.$this->get_file_type($file_path)); + $this->header('Content-Disposition: inline; filename="'.$file_name.'"'); + } + $this->header('Content-Length: '.$this->get_file_size($file_path)); + $this->header('Last-Modified: '.gmdate('D, d M Y H:i:s T', filemtime($file_path))); + $this->readfile($file_path); + } + + protected function send_content_type_header() { + $this->header('Vary: Accept'); + if (strpos($this->get_server_var('HTTP_ACCEPT'), 'application/json') !== false) { + $this->header('Content-type: application/json'); + } else { + $this->header('Content-type: text/plain'); + } + } + + protected function send_access_control_headers() { + $this->header('Access-Control-Allow-Origin: '.$this->options['access_control_allow_origin']); + $this->header('Access-Control-Allow-Credentials: ' + .($this->options['access_control_allow_credentials'] ? 'true' : 'false')); + $this->header('Access-Control-Allow-Methods: ' + .implode(', ', $this->options['access_control_allow_methods'])); + $this->header('Access-Control-Allow-Headers: ' + .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'); + $this->header('Content-Disposition: inline; filename="files.json"'); + // Prevent Internet Explorer from MIME-sniffing the content-type: + $this->header('X-Content-Type-Options: nosniff'); + if ($this->options['access_control_allow_origin']) { + $this->send_access_control_headers(); + } + $this->send_content_type_header(); + } + + public function get($print_response = true) { + if ($print_response && $this->get_query_param('download')) { + return $this->download(); + } + $file_name = $this->get_file_name_param(); + if ($file_name) { + $response = array( + $this->get_singular_param_name() => $this->get_file_object($file_name) + ); + } else { + $response = array( + $this->options['param_name'] => $this->get_file_objects() + ); + } + return $this->generate_response($response, $print_response); + } + + public function post($print_response = true) { + if ($this->get_query_param('_method') === 'DELETE') { + return $this->delete($print_response); + } + $upload = $this->get_upload_data($this->options['param_name']); + // Parse the Content-Disposition header, if available: + $content_disposition_header = $this->get_server_var('HTTP_CONTENT_DISPOSITION'); + $file_name = $content_disposition_header ? + rawurldecode(preg_replace( + '/(^[^"]+")|("$)/', + '', + $content_disposition_header + )) : null; + // Parse the Content-Range header, which has the following form: + // Content-Range: bytes 0-524287/2000000 + $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) { + 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( + 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 + ); + } + } + $response = array($this->options['param_name'] => $files); + return $this->generate_response($response, $print_response); + } + + public function delete($print_response = true) { + $file_names = $this->get_file_names_params(); + if (empty($file_names)) { + $file_names = array($this->get_file_name_param()); + } + $response = array(); + 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) { + if (!empty($version)) { + $file = $this->get_upload_path($file_name, $version); + if (is_file($file)) { + unlink($file); + } + } + } + } + $response[$file_name] = $success; + } + 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/vendor/blueimp/jquery-file-upload/server/php/docker-compose.yml b/vendor/blueimp/jquery-file-upload/server/php/docker-compose.yml new file mode 100644 index 000000000..74eabf7dc --- /dev/null +++ b/vendor/blueimp/jquery-file-upload/server/php/docker-compose.yml @@ -0,0 +1,9 @@ +version: '2.3' +services: + apache: + build: ./ + network_mode: bridge + ports: + - "80:80" + volumes: + - "../../:/var/www/html" diff --git a/vendor/blueimp/jquery-file-upload/server/php/files/.gitignore b/vendor/blueimp/jquery-file-upload/server/php/files/.gitignore new file mode 100644 index 000000000..e24a60fae --- /dev/null +++ b/vendor/blueimp/jquery-file-upload/server/php/files/.gitignore @@ -0,0 +1,3 @@ +* +!.gitignore +!.htaccess diff --git a/vendor/blueimp/jquery-file-upload/server/php/files/.htaccess b/vendor/blueimp/jquery-file-upload/server/php/files/.htaccess new file mode 100644 index 000000000..6f454afb9 --- /dev/null +++ b/vendor/blueimp/jquery-file-upload/server/php/files/.htaccess @@ -0,0 +1,26 @@ +# 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: +<FilesMatch "(?i)\.(gif|jpe?g|png)$"> + ForceType none + Header unset Content-Disposition +</FilesMatch> + +# The following directive prevents browsers from MIME-sniffing the content-type. +# This is an important complement to the ForceType directive above: +Header set X-Content-Type-Options nosniff + +# Uncomment the following lines to prevent unauthorized download of files: +#AuthName "Authorization required" +#AuthType Basic +#require valid-user diff --git a/vendor/blueimp/jquery-file-upload/server/php/index.php b/vendor/blueimp/jquery-file-upload/server/php/index.php new file mode 100644 index 000000000..9c2cfb8eb --- /dev/null +++ b/vendor/blueimp/jquery-file-upload/server/php/index.php @@ -0,0 +1,15 @@ +<?php +/* + * jQuery File Upload Plugin PHP Example + * https://github.com/blueimp/jQuery-File-Upload + * + * Copyright 2010, Sebastian Tschan + * https://blueimp.net + * + * Licensed under the MIT license: + * https://opensource.org/licenses/MIT + */ +exit; +error_reporting(E_ALL | E_STRICT); +require('UploadHandler.php'); +$upload_handler = new UploadHandler(); |