├── README.md ├── index.php └── lib ├── Tinify.php ├── Tinify ├── Client.php ├── Exception.php ├── Result.php ├── ResultMeta.php └── Source.php └── data └── cacert.pem /README.md: -------------------------------------------------------------------------------- 1 | # Kirby Upload Extended 2 | 3 | More options when uploading files like name changes, resizing via Kirby or compression and optional resizing via TinyPNG. Thanks at this point also to @medienbaecker for Parts of the code for pure resize with Kirby. It is the best alternative when only pure resizing is needed during upload. https://github.com/medienbaecker/kirby-autoresize 4 | 5 | **The individual components of the plugin:** 6 | 7 | - Name change and replacement 8 | - Kirby size change 9 | - Optimization of images by TinyPNG 10 | - Resizing with TinyPNG 11 | - Upload control for other file types 12 | 13 | **Note:** this is my first plugin for Kirby. Small bugs here and there are possible. 14 | 15 | ## Installation 16 | 17 | Download and copy this repository to `/site/plugins/upload-extended`. Sorry, currently no Composer 18 | 19 | ## Configuration and options 20 | 21 | There are a few options for the plugin. Every single function can be enabled or disabled. Below are the default settings for the `config.php`. 22 | 23 | ```php 24 | return [ 25 | 'werbschaft.uploadExtended.rename' => true, 26 | 'werbschaft.uploadExtended.excludeCharacters' => ['_','__','___','--','---'], 27 | 'werbschaft.uploadExtended.kirbyResize' => true, 28 | 'werbschaft.uploadExtended.maxWidth' => 2000, 29 | 'werbschaft.uploadExtended.maxHeight' => 2000, 30 | 'werbschaft.uploadExtended.quality' => 100, 31 | 'werbschaft.uploadExtended.debug' => false, 32 | 'werbschaft.uploadExtended.tinyPng' => true, 33 | 'werbschaft.uploadExtended.tinyPngKey' => 'insert-here', 34 | 'werbschaft.uploadExtended.tinyPngResize' => false, 35 | 'werbschaft.uploadExtended.tinyPngResizeMethod' => 'thumb', 36 | 'werbschaft.uploadExtended.excludeTemplates' => [], 37 | 'werbschaft.uploadExtended.excludePages' => [], 38 | 'werbschaft.uploadExtended.uploadLimit' => true, 39 | 'werbschaft.uploadExtended.uploadLimitMegabyte' => 5, 40 | ]; 41 | ``` 42 | 43 | ## Options in detail 44 | 45 | Option | Type | Function 46 | ------------ | ------------- | ------------- 47 | rename | Bool | Should the files be renamed during upload 48 | excludeCharacters | Array | Which strings should be replaced with a - 49 | kirbyResize | Bool | Should images be checked for size when uploaded in Kirby and scaled down if necessary 50 | maxWidth | Int | Maximum width of an image in pixels 51 | maxHeight | Int | Maximum height of an image in pixels 52 | quality | Int | Quality of the uploaded image 53 | debug | Bool | If active, various details are output with each upload what was changed by the plugin 54 | tinyPng | Bool | Should the images be optimized by the TinyPNG service during upload? Requires a valid API key 55 | tinyPngKey | String | The valid API key of your account 56 | tinyPngResize | Bool | Images can also be resized by TinyPNG. Attention: per resize this uses one credit extra 57 | tinyPngResizeMethod | String | If the TinyPNG Resize is used, which method should be applied: https://tinypng.com/developers/reference/php#resizing-images 58 | excludeTemplates | Array | Array of page templates to exclude 59 | excludePages | Array | Array of pages to exclude 60 | uploadLimit | Bool | Should other files, except images, be checked for size when uploaded 61 | uploadLimitMegabyte | Int | The Maximum Upload Limit. Files that are larger will be deleted immediately 62 | 63 | ## Known problems and future 64 | 65 | - [ ] Replace files in combination with the Change name option makes problems 66 | - [ ] next version with individual Search and Replace for the name option 67 | - [ ] Display at TinyPNG how many KB were saved 68 | - [ ] Solve problems when uploaded in site instead of page 69 | -------------------------------------------------------------------------------- /index.php: -------------------------------------------------------------------------------- 1 | page()->intendedTemplate(), $excludeTemplates ) || in_array( $file->page()->uid(), $excludePages ); 34 | 35 | } 36 | 37 | if ( $rename == true ) { 38 | 39 | $fileName = str_replace($excludeCharacters,'-',$file->name(),$count); 40 | 41 | if ( $count > 0 ) { 42 | 43 | $file = $file->changeName($fileName); 44 | $message .= ' The file was renamed according to the specifications. '; 45 | 46 | } 47 | 48 | 49 | } 50 | 51 | if ( !$excluded ) { 52 | 53 | if ( $file->isResizable() ) { 54 | 55 | // RESIZE THE IMAGE 56 | 57 | if ( $kirbyResize == true ) { 58 | 59 | if( $file->width() > $maxWidth || $file->height() > $maxHeight ) { 60 | 61 | if ( $file->width() > $maxWidth ) { 62 | $message .= ' The file is with ' . $file->width() . ' pixels wider than the allowed ' . $maxWidth . ' pixels and was reduced accordingly. '; 63 | } 64 | 65 | if ( $file->height() > $maxHeight ) { 66 | $message .= ' The file is with ' . $file->height() . ' pixels higher than the allowed ' . $maxHeight . ' pixels and was reduced accordingly. '; 67 | } 68 | 69 | try { 70 | 71 | kirby()->thumb($file->root(), $file->root(), [ 72 | 'width' => $maxWidth, 73 | 'height' => $maxHeight, 74 | 'quality' => $quality 75 | ]); 76 | 77 | } catch (Exception $e) { 78 | 79 | $message .= $e->getMessage(); 80 | 81 | } 82 | } 83 | 84 | } 85 | 86 | // CHECK FOR TINY PNG 87 | 88 | if ( $tinyPng == true ) { 89 | 90 | try { 91 | 92 | \Tinify\setKey($tinyPngKey); 93 | \Tinify\validate(); 94 | $compressionsCount = \Tinify\compressionCount() + 1; 95 | $message .= ' There have already been ' . $compressionsCount . ' compressions made with the TinyPNG API key this month. 500 are free per Key. '; 96 | 97 | } catch(\Tinify\Exception $e) { 98 | 99 | $message .= ' The Tiny PNG Api Key is not correct. Disable Tiny PNG or enter a valid key. '; 100 | 101 | } 102 | 103 | // UPLOAD AND MINIFY 104 | 105 | $fileName = $file->name(); 106 | 107 | if ( $file->page() ) { 108 | 109 | $path = $file->page()->root(); 110 | 111 | } else { 112 | 113 | $path = site()->root(); 114 | 115 | } 116 | 117 | try { 118 | 119 | $source = \Tinify\fromFile($file->root()); 120 | 121 | // RESIZE THROUGH TINY PNG 122 | 123 | if ( $tinyPngResize == true ) { 124 | 125 | $source = $source->resize([ 126 | "method" => $tinyPngResizeMethod, 127 | "width" => $maxWidth, 128 | "height" => $maxHeight 129 | ]); 130 | 131 | } 132 | 133 | $source->toFile( $path . '/' . $fileName . '.' . $file->extension() ); 134 | $message .= ' The file was optimized by TinyPNG. '; 135 | 136 | } catch(\Tinify\Exception $e) { 137 | 138 | $message .= $e->getMessage(); 139 | 140 | } 141 | 142 | } 143 | 144 | } else { 145 | 146 | if ( $uploadLimit == true ) { 147 | 148 | $uploadLimitBytes = $uploadLimitMegabyte * 1048576; 149 | 150 | $fileSizeBytes = $file->size(); 151 | $fileSizeNice = $file->niceSize(); 152 | 153 | if ( $fileSizeBytes > $uploadLimitBytes ) { 154 | 155 | $file->delete(); 156 | 157 | $message .= ' The file is with ' . $fileSizeNice . ' too big for the upload and was deleted because the limit for files is ' . $uploadLimitMegabyte . ' MB. '; 158 | 159 | } 160 | 161 | } 162 | 163 | } 164 | 165 | } 166 | 167 | if ( $debug == true ) { 168 | 169 | throw new Exception($message); 170 | 171 | } 172 | 173 | } 174 | 175 | Kirby::plugin('werbschaft/uploadExtended', [ 176 | 'options' => [ 177 | 'rename' => true, 178 | 'excludeCharacters' => ['_','__','___','--','---'], 179 | 'kirbyResize' => true, 180 | 'maxWidth' => 2000, 181 | 'maxHeight' => 2000, 182 | 'quality' => 100, 183 | 'debug' => false, 184 | 'tinyPng' => true, 185 | 'tinyPngKey' => "insert-here", 186 | 'tinyPngResize' => false, 187 | 'tinyPngResizeMethod' => 'thumb', 188 | 'excludeTemplates' => [], 189 | 'excludePages' => [], 190 | 'uploadLimit' => true, 191 | 'uploadLimitMegabyte' => 5 192 | ], 193 | 'hooks' => [ 194 | 'file.create:after' => function ($file) { 195 | uploadExtended($file); 196 | }, 197 | 'file.replace:after' => function ($newFile, $oldFile) { 198 | uploadExtended($newFile); 199 | } 200 | ] 201 | ]); 202 | -------------------------------------------------------------------------------- /lib/Tinify.php: -------------------------------------------------------------------------------- 1 | request("post", "/shrink"); 90 | } catch (AccountException $err) { 91 | if ($err->status == 429) return true; 92 | throw $err; 93 | } catch (ClientException $err) { 94 | return true; 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /lib/Tinify/Client.php: -------------------------------------------------------------------------------- 1 | options = array( 35 | CURLOPT_BINARYTRANSFER => true, 36 | CURLOPT_RETURNTRANSFER => true, 37 | CURLOPT_HEADER => true, 38 | CURLOPT_USERPWD => "api:" . $key, 39 | CURLOPT_CAINFO => self::caBundle(), 40 | CURLOPT_SSL_VERIFYPEER => true, 41 | CURLOPT_USERAGENT => join(" ", array_filter(array(self::userAgent(), $app_identifier))), 42 | ); 43 | 44 | if ($proxy) { 45 | $parts = parse_url($proxy); 46 | if (isset($parts["host"])) { 47 | $this->options[CURLOPT_PROXYTYPE] = CURLPROXY_HTTP; 48 | $this->options[CURLOPT_PROXY] = $parts["host"]; 49 | } else { 50 | throw new ConnectionException("Invalid proxy"); 51 | } 52 | 53 | if (isset($parts["port"])) { 54 | $this->options[CURLOPT_PROXYPORT] = $parts["port"]; 55 | } 56 | 57 | $creds = ""; 58 | if (isset($parts["user"])) $creds .= $parts["user"]; 59 | if (isset($parts["pass"])) $creds .= ":" . $parts["pass"]; 60 | 61 | if ($creds) { 62 | $this->options[CURLOPT_PROXYAUTH] = CURLAUTH_ANY; 63 | $this->options[CURLOPT_PROXYUSERPWD] = $creds; 64 | } 65 | } 66 | } 67 | 68 | function request($method, $url, $body = NULL) { 69 | $header = array(); 70 | if (is_array($body)) { 71 | if (!empty($body)) { 72 | $body = json_encode($body); 73 | array_push($header, "Content-Type: application/json"); 74 | } else { 75 | $body = NULL; 76 | } 77 | } 78 | 79 | for ($retries = self::RETRY_COUNT; $retries >= 0; $retries--) { 80 | if ($retries < self::RETRY_COUNT) { 81 | usleep(self::RETRY_DELAY * 1000); 82 | } 83 | 84 | $request = curl_init(); 85 | if ($request === false || $request === null) { 86 | throw new ConnectionException( 87 | "Error while connecting: curl extension is not functional or disabled." 88 | ); 89 | } 90 | 91 | curl_setopt_array($request, $this->options); 92 | 93 | $url = strtolower(substr($url, 0, 6)) == "https:" ? $url : self::API_ENDPOINT . $url; 94 | curl_setopt($request, CURLOPT_URL, $url); 95 | curl_setopt($request, CURLOPT_CUSTOMREQUEST, strtoupper($method)); 96 | 97 | if (count($header) > 0) { 98 | curl_setopt($request, CURLOPT_HTTPHEADER, $header); 99 | } 100 | 101 | if ($body) { 102 | curl_setopt($request, CURLOPT_POSTFIELDS, $body); 103 | } 104 | 105 | $response = curl_exec($request); 106 | 107 | if (is_string($response)) { 108 | $status = curl_getinfo($request, CURLINFO_HTTP_CODE); 109 | $headerSize = curl_getinfo($request, CURLINFO_HEADER_SIZE); 110 | curl_close($request); 111 | 112 | $headers = self::parseHeaders(substr($response, 0, $headerSize)); 113 | $body = substr($response, $headerSize); 114 | 115 | if (isset($headers["compression-count"])) { 116 | Tinify::setCompressionCount(intval($headers["compression-count"])); 117 | } 118 | 119 | if ($status >= 200 && $status <= 299) { 120 | return (object) array("body" => $body, "headers" => $headers); 121 | } 122 | 123 | $details = json_decode($body); 124 | if (!$details) { 125 | $message = sprintf("Error while parsing response: %s (#%d)", 126 | PHP_VERSION_ID >= 50500 ? json_last_error_msg() : "Error", 127 | json_last_error()); 128 | $details = (object) array( 129 | "message" => $message, 130 | "error" => "ParseError" 131 | ); 132 | } 133 | 134 | if ($retries > 0 && $status >= 500) continue; 135 | throw Exception::create($details->message, $details->error, $status); 136 | } else { 137 | $message = sprintf("%s (#%d)", curl_error($request), curl_errno($request)); 138 | curl_close($request); 139 | if ($retries > 0) continue; 140 | throw new ConnectionException("Error while connecting: " . $message); 141 | } 142 | } 143 | } 144 | 145 | protected static function parseHeaders($headers) { 146 | if (!is_array($headers)) { 147 | $headers = explode("\r\n", $headers); 148 | } 149 | 150 | $res = array(); 151 | foreach ($headers as $header) { 152 | if (empty($header)) continue; 153 | $split = explode(":", $header, 2); 154 | if (count($split) === 2) { 155 | $res[strtolower($split[0])] = trim($split[1]); 156 | } 157 | } 158 | return $res; 159 | } 160 | } 161 | -------------------------------------------------------------------------------- /lib/Tinify/Exception.php: -------------------------------------------------------------------------------- 1 | = 400 && $status <= 499) { 12 | $klass = "Tinify\ClientException"; 13 | } else if($status >= 500 && $status <= 599) { 14 | $klass = "Tinify\ServerException"; 15 | } else { 16 | $klass = "Tinify\Exception"; 17 | } 18 | 19 | if (empty($message)) $message = "No message was provided"; 20 | return new $klass($message, $type, $status); 21 | } 22 | 23 | function __construct($message, $type = NULL, $status = NULL) { 24 | $this->status = $status; 25 | if ($status) { 26 | parent::__construct($message . " (HTTP " . $status . "/" . $type . ")"); 27 | } else { 28 | parent::__construct($message); 29 | } 30 | } 31 | } 32 | 33 | class AccountException extends Exception {} 34 | class ClientException extends Exception {} 35 | class ServerException extends Exception {} 36 | class ConnectionException extends Exception {} 37 | -------------------------------------------------------------------------------- /lib/Tinify/Result.php: -------------------------------------------------------------------------------- 1 | meta = $meta; 10 | $this->data = $data; 11 | } 12 | 13 | public function data() { 14 | return $this->data; 15 | } 16 | 17 | public function toBuffer() { 18 | return $this->data; 19 | } 20 | 21 | public function toFile($path) { 22 | return file_put_contents($path, $this->toBuffer()); 23 | } 24 | 25 | public function size() { 26 | return intval($this->meta["content-length"]); 27 | } 28 | 29 | public function mediaType() { 30 | return $this->meta["content-type"]; 31 | } 32 | 33 | public function contentType() { 34 | return $this->mediaType(); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /lib/Tinify/ResultMeta.php: -------------------------------------------------------------------------------- 1 | meta = $meta; 10 | } 11 | 12 | public function width() { 13 | return intval($this->meta["image-width"]); 14 | } 15 | 16 | public function height() { 17 | return intval($this->meta["image-height"]); 18 | } 19 | 20 | public function location() { 21 | return isset($this->meta["location"]) ? $this->meta["location"] : null; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /lib/Tinify/Source.php: -------------------------------------------------------------------------------- 1 | request("post", "/shrink", $string); 14 | return new self($response->headers["location"]); 15 | } 16 | 17 | public static function fromUrl($url) { 18 | $body = array("source" => array("url" => $url)); 19 | $response = Tinify::getClient()->request("post", "/shrink", $body); 20 | return new self($response->headers["location"]); 21 | } 22 | 23 | public function __construct($url, $commands = array()) { 24 | $this->url = $url; 25 | $this->commands = $commands; 26 | } 27 | 28 | public function preserve() { 29 | $options = $this->flatten(func_get_args()); 30 | $commands = array_merge($this->commands, array("preserve" => $options)); 31 | return new self($this->url, $commands); 32 | } 33 | 34 | public function resize($options) { 35 | $commands = array_merge($this->commands, array("resize" => $options)); 36 | return new self($this->url, $commands); 37 | } 38 | 39 | public function store($options) { 40 | $response = Tinify::getClient()->request("post", $this->url, 41 | array_merge($this->commands, array("store" => $options))); 42 | return new Result($response->headers, $response->body); 43 | } 44 | 45 | public function result() { 46 | $response = Tinify::getClient()->request("get", $this->url, $this->commands); 47 | return new Result($response->headers, $response->body); 48 | } 49 | 50 | public function toFile($path) { 51 | return $this->result()->toFile($path); 52 | } 53 | 54 | public function toBuffer() { 55 | return $this->result()->toBuffer(); 56 | } 57 | 58 | private static function flatten($options) { 59 | $flattened = array(); 60 | foreach ($options as $option) { 61 | if (is_array($option)) { 62 | $flattened = array_merge($flattened, $option); 63 | } else { 64 | array_push($flattened, $option); 65 | } 66 | } 67 | return $flattened; 68 | } 69 | } 70 | --------------------------------------------------------------------------------