├── .gitignore ├── README.md ├── composer.json └── src ├── LidlPlus ├── LidlPlus.php ├── barcode_generator.php └── receipt_font.ttf └── getLidlRefreshToken.js /.gitignore: -------------------------------------------------------------------------------- 1 | /vendor/ 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # lidlplus-php-client 2 | Lidl Plus PHP API client 3 | 4 | This library allows you to query your Lidl Plus receipts, coupons and stores. 5 | It even allows you to generate a JPEG receipt to use in your automation scripts, e.g. automatically add a receipt to your transactions. 6 | 7 | ## Get your refresh Token 8 | You first need to retrieve your refreshToken. This can be done with the nodeJS script getLidlRefreshToken.js. You might need to install 9 | additional node JS libraries first with npm (e.g. request or openid-client, puppeteer). 10 | ```bash 11 | npm install puppeteer openid-client 12 | ``` 13 | Run the script and enter the country and language with the country you are using your account with. 14 | 15 | ## How to use 16 | First of all you need to get your `refresh_token` from your app as described above. Afterwards the library can be used as described below. 17 | Mind that the country code is essential to retrieve your purchases. If the country code does not match the country where you purchased someting, 18 | your will not be able to receive your transactions. 19 | 20 | Tested and working countrieCodes: Netherlands (default, 'NL') and Germany ('DE'). 21 | 22 | 23 | ```php 24 | require_once __DIR__ . 'vendor/autoload.php'; 25 | 26 | $lidl = new Net\Bluewalk\LidlPlus\LidlPlus('MyRefreshToken', 'CountryCode'); 27 | $receipts = $lidl->GetReceipts(); 28 | 29 | $latest = $lidl->GetReceiptJpeg($receipts->Records[0]->Id); 30 | 31 | header('Content-type: image/jpeg'); 32 | print $latest; 33 | ``` 34 | 35 | ## Acknowledgments 36 | This script is using a stripped down version of Kreative Software's barcode.php (https://github.com/kreativekorp/barcode). 37 | 38 | Script to fetch refresh_token is based on based script of Bastiaan Steinmeier, https://github.com/basst85. 39 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "bluewalk/lidlplus", 3 | "description": "Lidl Plus PHP API client", 4 | "type": "library", 5 | "license": "MIT", 6 | "homepage": "https://github.com/bluewalk/lidlplus-php-client", 7 | "keywords": ["lidl", "plus", "api", "php", "client", "receipts"], 8 | "authors": [ 9 | { 10 | "name": "Bluewalk", 11 | "email": "noreply@bluewalk.net" 12 | } 13 | ], 14 | "require": { 15 | "php": ">=5.3.0" 16 | }, 17 | "minimum-stability": "stable", 18 | "autoload": { 19 | "psr-4": { 20 | "Net\\Bluewalk\\LidlPlus\\": ["src/LidlPlus"] 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/LidlPlus/LidlPlus.php: -------------------------------------------------------------------------------- 1 | country = strtoupper($countryShort); 18 | 19 | $this->refresh_token = $refresh_token; 20 | 21 | $this->token_file = join( 22 | DIRECTORY_SEPARATOR, 23 | [ 24 | $pathToTokenFile ?? sys_get_temp_dir(), 25 | 'lidl-token_' . base64_encode($refresh_token) . '.json' 26 | ] 27 | ); 28 | 29 | if (file_exists($this->token_file)) 30 | $this->token = json_decode(file_get_contents($this->token_file)); 31 | } 32 | 33 | private function _checkAuth() 34 | { 35 | if (!$this->token || $this->token->expires < time()) 36 | $this->RefreshToken(); 37 | } 38 | 39 | private function _request(string $endpoint, string $method = 'GET', $data = null) 40 | { 41 | $ch = curl_init(); 42 | 43 | $headers = [ 44 | 'App-Version: 999.99.9', 45 | 'Operating-System: iOS', 46 | 'App: com.lidl.eci.lidl.plus', 47 | 'Accept-Language: ' . $this->country 48 | ]; 49 | $query_params = ''; 50 | 51 | if ($this->token) 52 | $headers[] = 'Authorization: Bearer ' . $this->token->access_token; 53 | 54 | if ($method == 'POST' || $method == 'PUT') { 55 | if ($data) { 56 | $data_str = json_encode($data); 57 | curl_setopt($ch, CURLOPT_POSTFIELDS, $data_str); 58 | 59 | $headers[] = 'Content-Type: application/json'; 60 | $headers[] = 'Content-Length: ' . strlen($data_str); 61 | } 62 | } 63 | if ($method == 'GET') 64 | if ($data) 65 | $query_params = '?' . http_build_query($data); 66 | 67 | curl_setopt($ch, CURLOPT_URL, $endpoint . $query_params); 68 | 69 | curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false); 70 | curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); 71 | 72 | curl_setopt($ch, CURLOPT_CUSTOMREQUEST, $method); 73 | curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); 74 | curl_setopt($ch, CURLOPT_HTTPHEADER, $headers); 75 | 76 | //curl_setopt($ch, CURLOPT_PROXY, '127.0.0.1:1080'); 77 | 78 | $result = curl_exec($ch); 79 | 80 | $error = curl_error($ch); 81 | if ($error) 82 | throw new Exception('Lidl API: ' . $error); 83 | 84 | curl_close($ch); 85 | 86 | return json_decode($result); 87 | } 88 | 89 | private function _request_auth() 90 | { 91 | $ch = curl_init(); 92 | 93 | $request = 'refresh_token=' . $this->refresh_token . '&grant_type=refresh_token'; 94 | 95 | curl_setopt($ch, CURLOPT_URL, $this->account_url . 'connect/token'); 96 | 97 | curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false); 98 | curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); 99 | 100 | curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'POST'); 101 | curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); 102 | 103 | curl_setopt($ch, CURLOPT_POSTFIELDS, $request); 104 | curl_setopt($ch, CURLOPT_HTTPHEADER, [ 105 | 'Authorization: Basic ' . base64_encode('LidlPlusNativeClient:secret'), 106 | 'Content-Type: application/x-www-form-urlencoded', 107 | 'Content-Length: ' . strlen($request) 108 | ]); 109 | 110 | //curl_setopt($ch, CURLOPT_PROXY, '127.0.0.1:1080'); 111 | 112 | $result = curl_exec($ch); 113 | 114 | $error = curl_error($ch); 115 | if ($error) 116 | throw new Exception('Lidl API: ' . $error); 117 | 118 | curl_close($ch); 119 | 120 | return json_decode($result); 121 | } 122 | 123 | public function RefreshToken() 124 | { 125 | $result = $this->_request_auth(); 126 | $result->expires = strtotime('+ ' . round($result->expires_in) . ' seconds'); 127 | 128 | file_put_contents($this->token_file, json_encode($result)); 129 | 130 | $this->token = $result; 131 | } 132 | 133 | public function GetReceipts(int $page = 1) 134 | { 135 | $this->_checkAuth(); 136 | 137 | return $this->_request($this->ticket_url . $this->country . '/list/' . $page); 138 | } 139 | 140 | public function GetReceipt(string $id = '') 141 | { 142 | $this->_checkAuth(); 143 | 144 | return $this->_request($this->ticket_url . $this->country . '/tickets/' . $id); 145 | } 146 | 147 | public function GetStore(string $store) 148 | { 149 | return $this->_request($this->stores_url . $this->country . '/' . $store); 150 | } 151 | 152 | public function GetReceiptJpeg(string $id = '') 153 | { 154 | function strcenter($string) 155 | { 156 | return str_pad($string, 50, ' ', STR_PAD_BOTH); 157 | } 158 | 159 | $receipt = $this->GetReceipt($id); 160 | 161 | if (property_exists($receipt, 'Message')) return null; 162 | 163 | $store = $this->GetStore($receipt->storeCode); 164 | 165 | $header = strcenter($store->address) . PHP_EOL; 166 | $header .= strcenter($store->postalCode . ' ' . $store->locality) . PHP_EOL; 167 | 168 | $str = sprintf("%-45s%5s\n", "OMSCHRIJVING", "EUR"); 169 | foreach ($receipt->itemsLine as $item) { 170 | $str .= sprintf("%-45s%5s\n", $item->description, $item->originalAmount); 171 | if ($item->quantity != 1) 172 | if ($item->isWeight) 173 | $str .= " " . $item->quantity . ' kg x ' . $item->currentUnitPrice . ' EUR/kg' . PHP_EOL; 174 | else 175 | $str .= " " . $item->quantity . ' X ' . $item->currentUnitPrice . PHP_EOL; 176 | 177 | if ($item->deposit) { 178 | $str .= sprintf("%-45s%5s\n", $item->deposit->description, $item->deposit->amount); 179 | $str .= " " . $item->deposit->quantity . ' X ' . $item->deposit->unitPrice . PHP_EOL; 180 | } 181 | 182 | if ($item->discounts) 183 | foreach ($item->discounts as $discount) 184 | $str .= sprintf(" %-42s%5s\n", $discount->description, '-' . $discount->amount); 185 | } 186 | 187 | $str .= sprintf("%-38s%s", "", "------------") . PHP_EOL; 188 | $str .= sprintf("%s%-20s%-25s%5s", "", "Te betalen", $receipt->linesScannedCount . ' art.', $receipt->totalAmount) . PHP_EOL; 189 | $str .= sprintf("%-38s%s", "", "============") . PHP_EOL; 190 | 191 | $str .= sprintf("%-45s%5s\n", $receipt->payments[0]->description, $receipt->payments[0]->amount) . PHP_EOL; 192 | 193 | $str .= '--------------------------------------------------' . PHP_EOL . PHP_EOL; 194 | $str .= trim(strip_tags(preg_replace('##i', "\n", $receipt->payments[0]->rawPaymentInformationHTML))) . PHP_EOL . PHP_EOL; 195 | $str .= '% Bedr.Excl BTW Bedr.Incl' . PHP_EOL; 196 | 197 | foreach ($receipt->taxes as $tax) 198 | $str .= sprintf("%-9s%15s%12s%14s\n", (int)$tax->percentage, $tax->netAmount, $tax->amount, $tax->taxableAmount); 199 | 200 | $str .= '--------------------------------------------------' . PHP_EOL; 201 | $str .= sprintf("%-9s%15s%12s%14s\n", 'Som', $receipt->totalTaxes->totalNetAmount, $receipt->totalTaxes->totalAmount, $receipt->totalTaxes->totalTaxableAmount); 202 | 203 | $footer = sprintf("%-9s%10s%17s%14s\n", substr($receipt->storeCode, 2), $receipt->sequenceNumber . '/' . $receipt->workstation, date('d.m.y', strtotime($receipt->date)), date('H:i', strtotime($receipt->date))); 204 | 205 | // 200x71 206 | $logo = imagecreatefromstring(base64_decode("iVBORw0KGgoAAAANSUhEUgAAAMgAAABPCAMAAACZM3rMAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAMAUExURUpKSktLS0xMTE1NTU5OTk9PT1BQUFFRUVJSUlNTU1RUVFVVVVZWVldXV1hYWFlZWVpaWltbW1" . 207 | "xcXF1dXV5eXl9fX2BgYGFhYWJiYmNjY2RkZGVlZWZmZmdnZ2hoaGlpaWpqamtra2xsbG1tbW5ubm9vb3BwcHFxcXJycnNzc3R0dHV1dXZ2dnd3d3h4eHl5eXp6ent7e3x8fH19fX5+fn9/f4CAgIGBgYKCgoODg4SEhIWF" . 208 | "hYaGhoeHh4iIiImJiYqKiouLi4yMjI2NjY6Ojo+Pj5CQkJGRkZKSkpSUlJaWlpeXl5iYmJmZmZqampubm5ycnJ2dnZ6enp+fn6CgoKGhoaKioqOjo6SkpKWlpaampqenp6ioqKmpqaqqqqurq6ysrK2tra6urq+vr7CwsL" . 209 | "GxsbKysrOzs7S0tLW1tba2tre3t7i4uLm5ubq6uru7u7y8vL29vb6+vr+/v8DAwMHBwcLCwsPDw8TExMXFxcbGxsfHx8jIyMnJycrKysvLy8zMzM3Nzc7Ozs/Pz9DQ0NHR0dLS0tPT09TU1NXV1dbW1tfX19jY2NnZ2dra" . 210 | "2tvb29zc3N3d3d7e3t/f3+Dg4OHh4eLi4uPj4+Tk5OXl5ebm5ufn5+jo6Onp6erq6uvr6+zs7O3t7e7u7u/v7/Dw8PHx8fLy8vPz8/T09PX19fb29vf39/j4+Pn5+fr6+vv7+/z8/P39/f7+/v///wAAAAAAAAAAAAAAAA" . 211 | "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" . 212 | "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAKXYgk4AAAAJcEhZcwAADsMAAA7DAcdvqGQAAAjUSURBVG" . 213 | "hD1Zr5XxNHH4AzuwlnuCEERU6lgA0UwhHk7avYisqLLfaFvhYo2lIVEHhblJeKCHIpCEXk8FVQFJAUcpA9/sHO7k4g2Z0lG5Pufnh+29m5nszOzHcnq2NDh953/LmzY991UShBC0IXYaiNx611leXnGvpXHTRKVJ+QRTxr" . 214 | "HSUZiTGREVHGtLyGpw4GpatNqCKe4RqTQYcACUXt7zQyCVFk90FpFLLgIU/ceKWNSWgijsECPVJAAFOjNmMSkgi1Ui3ygCYnbrvRbVUJSWSnJwJ13wdDwWst1q5QROjZIoB670tsmxNlUJNQRHb7MQMCJ7xlG2VQk1BElh" . 215 | "pI1Hd/TDP7KIeKhCIyYcU9WTpdYo8D5VCRUESGz+BF4n/4E+VQkVBEhnLxInEtdpRDRUIakQKZEWk9ZiMyWUmgrvuT1HvM5shyI37VMj/3oBwqEorIzr1o1HU/SMsHDaKtUESY+bO4SWK8edx2dnb7NmZIDKdXj1usxTIr" . 216 | "xZLol0j/0YVuq0pIIqx74owo3CIzmnaO3/sIfLOaKvAzIUz/eX8c3xBZxvX8fCySgJDFPdsaHaRAEWr0m0sB+HYaZZey/3rggtlAcBhLf5qXPFf0QCOqRI76q003BxZ2aZ+Syx3onixX74n2Kiji6cyMDUB2P8qOY295rP" . 217 | "f7xobrtx7ObUmP6OjrZlSJHMb4xPQcy/lfFg4XickKdE+WlMuiF2pO5GYyejJkMfWg7HiYvbWll682sQeN1L/iUSVHE5FzsfeVt4YnZ1GqLFF1f4eIAH6SKxWBC8WJy49RtKmpCLWyittAlIvodNHlD+38z6GliHO2xjaC" . 218 | "Cd+DEdHpc7t5k08UuZUCOFAWEdwdIj2gCP2iNoKsmJC+rFONCfKVSyCyprkqxi1cGblC3K1ojAj137K8vLwc/LCApNy8vHzrEMouh3up2gB/0NJhSbxI3yyBtZ+KV6iiL1mBM372EiyTm4F/3YnNhDeLWkS/GRRhPU6Hw7" . 219 | "HejG0q6ruP8KYzwAsGNV3Dn2STn9+XjMk+V/vK19iTIwzG+m1o74JltgeNKM0PYJvneiRuhxPh2b6BF/leQQhIP6+KFLIbioawi/D6FaUiIGEMteh8FIfS/ADnloX7/oRDhJ68cBCmRJXfx42echEdeem9UEZ1EfphtU+4" . 220 | "FVXYJ5qGHDIiuCZBCj/f1Rehxyr9GowsGpLME7yI3pxnPviT6BB9xxZfRmWR/ccVMSgvQm8ZlKxdWJGEa4O/3bCg2XUIYV3hy6grQk/aRB4wbLI8Ep8HrV3AiJh+dTE79wvFgwJSp/kFQ1UR17wN89pOlo7uoQwCjpECzM" . 221 | "kRFIE1/HQKXR6gH9jlCqkp4n5aI3kyOECpN/zjcU5YcXsbL8KuNIrvka1v+FLqiXgWuf0cB1k2eFjOPW3DnuQJIp5+nzWPh7i6wBVTT8T5VNjPceg/E54PLtuQFZ9NEGHH89G1F6J2ii/394sIbx7UuO2IXY4s6hV2Rs9I" . 222 | "ieTMSACJzFnRtRei4gmXHowI8ykijPsjZ0KNW4/wgNFK/m/c4kMNlsplQyKL59C1FzjDuHTlIgy14w5ahHEsNLfANPpRpd+nAlIMhXfdrOd/xfxygKtcToQoG+PSlYownvVb1xaDFdlfbC7KrHOw9LBVsn+IiczudT7gNz" . 223 | "wQbcIsW0hk9gt07YWonuDSFYrQ77qt5tLZ4ESYlZ6LGURcnYMar8W24o++uKOU0wXm+grMfEciYzno2gtxYYZLVyTCbI01njGQlqBEqDf9dRlw4sbV2UcCjwcHkcgNBJHZPnMZv7PDWp3d4uWX/HaJa06JyNbE9bNwyIlg" . 224 | "RKiPky1Z/O8a9+VYRYD54Qs42byOjbUEkRdXxA2TP29wDQYW2XvZKQTeQYjQmxOt+ejxiM4/K7Og4iCyOjcYeZEPzWZ0eUDMI77NACJw1bljMwqdVizCON/0Wg46D/D/uOEBqW3wRQkrkta9tbPRl4uuDgBZ83yPjhbxbE" . 225 | "2cj/V2WamIY62vKka07IDkNCWPF0i/yx3PY0WM/2z7d1mKZDkjL6/xPTpKxLM5fiXx8LFQJuKx/14dK24OxLf+iluJRBBJd/jdEyuiI/QkIW01ckAIneVFaOdCUzLpU1KJSIt9sipV0mMQ3/7W/cQWaKqAxN5NPp7Bi2AB" . 226 | "p1eFEEhW5MXSjewov94qEImoupQTLRl9kNjxmmIdUzVHmwBTFzrZDkIkon2HLyIrUvhVSZKoXSTC0JAtvAiIEU8OCEi6+57roHvOdtTTRaT02Bmh9jXcPoLF8NkqzTJcIccwVkQXFSP5+QQReulBX19fZzVKDAyI/2FD+K" . 227 | "FdY0cEwCDrxw/QY+4+rL3j80BPIQKYuE+LPozCMl0NijcrQcTTZ8nKyjqZgBIDE13yRniKocmzarnWiJN3uA8H6PZiWPsJtOAHJPY6V2j2IixzKk36LMiARAKfxvsDYy3kAff7xxXYzx90ILuVP2gL6jReF1u7yP0HGfg0" . 228 | "3p8wiLDuoRLcmID0tnV+2IIRAUbbKP/MaiECA/ovpAEkyBTGIygRval+RvhPWBMRlh6yiscEmO68RdNIsQhpPN3mnXvaiLCeJ//wPxQCafe2UJeUiQBDTNKZpj9c3kKfKNJpjgyK1Hp/EdYzW+kbR4KEXj4u4aGb0lAxWa" . 229 | "KikwuvDmw4Dz81mChDtxQSUz4HRZi3M5NB8WxZ/HXD/tSXh/sESL936MEy/3+Giskx/fzlu027y7dO+wK6p5CpF7tQhPW4gkR62u6cq/KaEKldm76dCly7W9hdfaD30S2luGlOJBx4ZmqFaIU42aXJ50HhEmE3rvETnshu" . 230 | "Wxc/eKoQLhFHH3/eTuR2C3G76oRJxD5q4QIjIrudPzfQgPCI2AdKeY+c2xvajEeYRPZGLPz8yOjcRCnqEw4RZqScGw9g7tbig19EOEScP5vh2waRPOCzD6pOWEZk83YyIFL79zT0CM8cod61pZ7qOogTNSEsIiz9fvT3XU" . 231 | "09wiTCUnvCp29awbJ/AUCewlrlEY2aAAAAAElFTkSuQmCC")); 232 | 233 | // Replace path by your own font path 234 | $font = __DIR__ . '/receipt_font.ttf'; 235 | 236 | list(, $text_height, $text_width) = imageftbbox(12, 0, $font, $str); 237 | 238 | $height = $text_height + 40 + 181 /* logo */ + 150 /* barcode */ 239 | ; 240 | $width = $text_width + 40; 241 | $pos = 0; 242 | 243 | // Create the image 244 | $im = imagecreatetruecolor($width, $height); 245 | 246 | // Create a few colors 247 | $white = imagecolorallocate($im, 255, 255, 255); 248 | $grey = imagecolorallocate($im, 30, 30, 30); 249 | $black = imagecolorallocate($im, 0, 0, 0); 250 | 251 | // Create white background 252 | imagefilledrectangle($im, 0, 0, $width, $height, $white); 253 | 254 | // Add logo 255 | imagecopy($im, $logo, $width / 2 - 100, 10, 0, 0, 200, 79); 256 | $pos += 110; 257 | 258 | // And add header 259 | imagettftext($im, 12, 0, 20, $pos, $grey, $font, $header); 260 | $pos += 90; 261 | 262 | // And add the text 263 | imagettftext($im, 12, 0, 20, $pos, $grey, $font, $str); 264 | $pos += $text_height + 20; 265 | 266 | // Add barcode 267 | $generator = new barcode_generator(); 268 | $barcode = $generator->render_image('itf', $receipt->barCode, ['f' => 'png', 'w' => $width]); 269 | imagecopy($im, $barcode, 0, $pos, 0, 0, $width, 80); 270 | $pos += 80 + 30; 271 | 272 | // Final line 273 | imagettftext($im, 12, 0, 20, $pos, $grey, $font, $footer); 274 | 275 | // Using imagepng() results in clearer text compared with imagejpeg() 276 | ob_start(); 277 | imagepng($im); 278 | $data = ob_get_contents(); 279 | ob_end_clean(); 280 | imagedestroy($im); 281 | 282 | return $data; 283 | } 284 | } 285 | -------------------------------------------------------------------------------- /src/LidlPlus/barcode_generator.php: -------------------------------------------------------------------------------- 1 | output_image($format, $_POST['s'], $_POST['d'], $_POST); 36 | exit(0); 37 | } 38 | if (isset($_GET['s']) && isset($_GET['d'])) { 39 | $generator = new barcode_generator(); 40 | $format = (isset($_GET['f']) ? $_GET['f'] : 'png'); 41 | $generator->output_image($format, $_GET['s'], $_GET['d'], $_GET); 42 | exit(0); 43 | } 44 | } 45 | 46 | class barcode_generator { 47 | 48 | public function render_image($symbology, $data, $options) { 49 | list($code, $widths, $width, $height, $x, $y, $w, $h) = 50 | $this->encode_and_calculate_size($symbology, $data, $options); 51 | $image = imagecreatetruecolor($width, $height); 52 | imagesavealpha($image, true); 53 | $bgcolor = (isset($options['bc']) ? $options['bc'] : 'FFF'); 54 | $bgcolor = $this->allocate_color($image, $bgcolor); 55 | imagefill($image, 0, 0, $bgcolor); 56 | $colors = array( 57 | (isset($options['cs']) ? $options['cs'] : ''), 58 | (isset($options['cm']) ? $options['cm'] : '000'), 59 | (isset($options['c2']) ? $options['c2'] : 'F00'), 60 | (isset($options['c3']) ? $options['c3'] : 'FF0'), 61 | (isset($options['c4']) ? $options['c4'] : '0F0'), 62 | (isset($options['c5']) ? $options['c5'] : '0FF'), 63 | (isset($options['c6']) ? $options['c6'] : '00F'), 64 | (isset($options['c7']) ? $options['c7'] : 'F0F'), 65 | (isset($options['c8']) ? $options['c8'] : 'FFF'), 66 | (isset($options['c9']) ? $options['c9'] : '000'), 67 | ); 68 | foreach ($colors as $i => $color) { 69 | $colors[$i] = $this->allocate_color($image, $color); 70 | } 71 | $this->dispatch_render_image( 72 | $image, $code, $x, $y, $w, $h, $colors, $widths, $options 73 | ); 74 | return $image; 75 | } 76 | 77 | /* - - - - INTERNAL FUNCTIONS - - - - */ 78 | 79 | private function encode_and_calculate_size($symbology, $data, $options) { 80 | $code = $this->dispatch_encode($symbology, $data, $options); 81 | $widths = array( 82 | (isset($options['wq']) ? (int)$options['wq'] : 1), 83 | (isset($options['wm']) ? (int)$options['wm'] : 1), 84 | (isset($options['ww']) ? (int)$options['ww'] : 3), 85 | (isset($options['wn']) ? (int)$options['wn'] : 1), 86 | (isset($options['w4']) ? (int)$options['w4'] : 1), 87 | (isset($options['w5']) ? (int)$options['w5'] : 1), 88 | (isset($options['w6']) ? (int)$options['w6'] : 1), 89 | (isset($options['w7']) ? (int)$options['w7'] : 1), 90 | (isset($options['w8']) ? (int)$options['w8'] : 1), 91 | (isset($options['w9']) ? (int)$options['w9'] : 1), 92 | ); 93 | $size = $this->dispatch_calculate_size($code, $widths, $options); 94 | $dscale = ($code && isset($code['g']) && $code['g'] == 'm') ? 4 : 1; 95 | $scale = (isset($options['sf']) ? (float)$options['sf'] : $dscale); 96 | $scalex = (isset($options['sx']) ? (float)$options['sx'] : $scale); 97 | $scaley = (isset($options['sy']) ? (float)$options['sy'] : $scale); 98 | $dpadding = ($code && isset($code['g']) && $code['g'] == 'm') ? 0 : 10; 99 | $padding = (isset($options['p']) ? (int)$options['p'] : $dpadding); 100 | $vert = (isset($options['pv']) ? (int)$options['pv'] : $padding); 101 | $horiz = (isset($options['ph']) ? (int)$options['ph'] : $padding); 102 | $top = (isset($options['pt']) ? (int)$options['pt'] : $vert); 103 | $left = (isset($options['pl']) ? (int)$options['pl'] : $horiz); 104 | $right = (isset($options['pr']) ? (int)$options['pr'] : $horiz); 105 | $bottom = (isset($options['pb']) ? (int)$options['pb'] : $vert); 106 | $dwidth = ceil($size[0] * $scalex) + $left + $right; 107 | $dheight = ceil($size[1] * $scaley) + $top + $bottom; 108 | $iwidth = (isset($options['w']) ? (int)$options['w'] : $dwidth); 109 | $iheight = (isset($options['h']) ? (int)$options['h'] : $dheight); 110 | $swidth = $iwidth - $left - $right; 111 | $sheight = $iheight - $top - $bottom; 112 | return array( 113 | $code, $widths, $iwidth, $iheight, 114 | $left, $top, $swidth, $sheight 115 | ); 116 | } 117 | 118 | private function allocate_color($image, $color) { 119 | $color = preg_replace('/[^0-9A-Fa-f]/', '', $color); 120 | switch (strlen($color)) { 121 | case 1: 122 | $v = hexdec($color) * 17; 123 | return imagecolorallocate($image, $v, $v, $v); 124 | case 2: 125 | $v = hexdec($color); 126 | return imagecolorallocate($image, $v, $v, $v); 127 | case 3: 128 | $r = hexdec(substr($color, 0, 1)) * 17; 129 | $g = hexdec(substr($color, 1, 1)) * 17; 130 | $b = hexdec(substr($color, 2, 1)) * 17; 131 | return imagecolorallocate($image, $r, $g, $b); 132 | case 4: 133 | $a = hexdec(substr($color, 0, 1)) * 17; 134 | $r = hexdec(substr($color, 1, 1)) * 17; 135 | $g = hexdec(substr($color, 2, 1)) * 17; 136 | $b = hexdec(substr($color, 3, 1)) * 17; 137 | $a = round((255 - $a) * 127 / 255); 138 | return imagecolorallocatealpha($image, $r, $g, $b, $a); 139 | case 6: 140 | $r = hexdec(substr($color, 0, 2)); 141 | $g = hexdec(substr($color, 2, 2)); 142 | $b = hexdec(substr($color, 4, 2)); 143 | return imagecolorallocate($image, $r, $g, $b); 144 | case 8: 145 | $a = hexdec(substr($color, 0, 2)); 146 | $r = hexdec(substr($color, 2, 2)); 147 | $g = hexdec(substr($color, 4, 2)); 148 | $b = hexdec(substr($color, 6, 2)); 149 | $a = round((255 - $a) * 127 / 255); 150 | return imagecolorallocatealpha($image, $r, $g, $b, $a); 151 | default: 152 | return imagecolorallocatealpha($image, 0, 0, 0, 127); 153 | } 154 | } 155 | 156 | /* - - - - DISPATCH - - - - */ 157 | 158 | private function dispatch_encode($symbology, $data, $options) { 159 | switch (strtolower(preg_replace('/[^A-Za-z0-9]/', '', $symbology))) { 160 | case 'itf' : return $this->itf_encode($data); 161 | case 'itf14' : return $this->itf_encode($data); 162 | } 163 | return null; 164 | } 165 | 166 | private function dispatch_calculate_size($code, $widths, $options) { 167 | if ($code && isset($code['g']) && $code['g']) { 168 | switch ($code['g']) { 169 | case 'l': 170 | return $this->linear_calculate_size($code, $widths); 171 | } 172 | } 173 | return array(0, 0); 174 | } 175 | 176 | private function dispatch_render_image( 177 | $image, $code, $x, $y, $w, $h, $colors, $widths, $options 178 | ) { 179 | if ($code && isset($code['g']) && $code['g']) { 180 | switch ($code['g']) { 181 | case 'l': 182 | $this->linear_render_image( 183 | $image, $code, $x, $y, $w, $h, 184 | $colors, $widths, $options 185 | ); 186 | break; 187 | } 188 | } 189 | } 190 | 191 | /* - - - - LINEAR BARCODE RENDERER - - - - */ 192 | 193 | private function linear_calculate_size($code, $widths) { 194 | $width = 0; 195 | foreach ($code['b'] as $block) { 196 | foreach ($block['m'] as $module) { 197 | $width += $module[1] * $widths[$module[2]]; 198 | } 199 | } 200 | return array($width, 80); 201 | } 202 | 203 | private function linear_render_image( 204 | $image, $code, $x, $y, $w, $h, $colors, $widths, $options 205 | ) { 206 | $textheight = (isset($options['th']) ? (int)$options['th'] : 10); 207 | $textsize = (isset($options['ts']) ? (int)$options['ts'] : 1); 208 | $textcolor = (isset($options['tc']) ? $options['tc'] : '000'); 209 | $textcolor = $this->allocate_color($image, $textcolor); 210 | $width = 0; 211 | foreach ($code['b'] as $block) { 212 | foreach ($block['m'] as $module) { 213 | $width += $module[1] * $widths[$module[2]]; 214 | } 215 | } 216 | if ($width) { 217 | $scale = $w / $width; 218 | $scale = (($scale > 1) ? floor($scale) : 1); 219 | $x = floor($x + ($w - $width * $scale) / 2); 220 | } else { 221 | $scale = 1; 222 | $x = floor($x + $w / 2); 223 | } 224 | foreach ($code['b'] as $block) { 225 | if (isset($block['l'])) { 226 | $label = $block['l'][0]; 227 | $ly = (isset($block['l'][1]) ? (float)$block['l'][1] : 1); 228 | $lx = (isset($block['l'][2]) ? (float)$block['l'][2] : 0.5); 229 | $my = round($y + min($h, $h + ($ly - 1) * $textheight)); 230 | $ly = ($y + $h + $ly * $textheight); 231 | $ly = round($ly - imagefontheight($textsize)); 232 | } else { 233 | $label = null; 234 | $my = $y + $h; 235 | } 236 | $mx = $x; 237 | foreach ($block['m'] as $module) { 238 | $mc = $colors[$module[0]]; 239 | $mw = $mx + $module[1] * $widths[$module[2]] * $scale; 240 | imagefilledrectangle($image, $mx, $y, $mw - 1, $my - 1, $mc); 241 | $mx = $mw; 242 | } 243 | if (!is_null($label)) { 244 | $lx = ($x + ($mx - $x) * $lx); 245 | $lw = imagefontwidth($textsize) * strlen($label); 246 | $lx = round($lx - $lw / 2); 247 | imagestring($image, $textsize, $lx, $ly, $label, $textcolor); 248 | } 249 | $x = $mx; 250 | } 251 | } 252 | 253 | /* - - - - ITF ENCODER - - - - */ 254 | 255 | private function itf_encode($data) { 256 | $data = preg_replace('/[^0-9]/', '', $data); 257 | if (strlen($data) % 2) $data = '0' . $data; 258 | $blocks = array(); 259 | /* Quiet zone, start. */ 260 | $blocks[] = array( 261 | 'm' => array(array(0, 10, 0)) 262 | ); 263 | $blocks[] = array( 264 | 'm' => array( 265 | array(1, 1, 1), 266 | array(0, 1, 1), 267 | array(1, 1, 1), 268 | array(0, 1, 1), 269 | ) 270 | ); 271 | /* Data. */ 272 | for ($i = 0, $n = strlen($data); $i < $n; $i += 2) { 273 | $c1 = substr($data, $i, 1); 274 | $c2 = substr($data, $i+1, 1); 275 | $b1 = $this->itf_alphabet[$c1]; 276 | $b2 = $this->itf_alphabet[$c2]; 277 | $blocks[] = array( 278 | 'm' => array( 279 | array(1, 1, $b1[0]), 280 | array(0, 1, $b2[0]), 281 | array(1, 1, $b1[1]), 282 | array(0, 1, $b2[1]), 283 | array(1, 1, $b1[2]), 284 | array(0, 1, $b2[2]), 285 | array(1, 1, $b1[3]), 286 | array(0, 1, $b2[3]), 287 | array(1, 1, $b1[4]), 288 | array(0, 1, $b2[4]), 289 | ), 290 | 'l' => array($c1 . $c2) 291 | ); 292 | } 293 | /* End, quiet zone. */ 294 | $blocks[] = array( 295 | 'm' => array( 296 | array(1, 1, 2), 297 | array(0, 1, 1), 298 | array(1, 1, 1), 299 | ) 300 | ); 301 | $blocks[] = array( 302 | 'm' => array(array(0, 10, 0)) 303 | ); 304 | /* Return code. */ 305 | return array('g' => 'l', 'b' => $blocks); 306 | } 307 | 308 | private $itf_alphabet = array( 309 | '0' => array(1, 1, 2, 2, 1), 310 | '1' => array(2, 1, 1, 1, 2), 311 | '2' => array(1, 2, 1, 1, 2), 312 | '3' => array(2, 2, 1, 1, 1), 313 | '4' => array(1, 1, 2, 1, 2), 314 | '5' => array(2, 1, 2, 1, 1), 315 | '6' => array(1, 2, 2, 1, 1), 316 | '7' => array(1, 1, 1, 2, 2), 317 | '8' => array(2, 1, 1, 2, 1), 318 | '9' => array(1, 2, 1, 2, 1), 319 | ); 320 | } -------------------------------------------------------------------------------- /src/LidlPlus/receipt_font.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bluewalk/lidlplus-php-client/b551fec15ff063576d2e7f18588a67af59538e27/src/LidlPlus/receipt_font.ttf -------------------------------------------------------------------------------- /src/getLidlRefreshToken.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Get refresh_token for Lidl Plus API 3 | * 4 | * By: Bastiaan Steinmeier, https://github.com/basst85 5 | * Modified by: https://github.com/UNICodehORN 6 | * Modified again by: https://github.com/Sharknoon 7 | * 8 | */ 9 | 10 | const { Issuer, generators } = require('openid-client'); 11 | const puppeteer = require('puppeteer'); 12 | const devices = puppeteer.devices; 13 | const iPhone = devices['iPhone 5']; 14 | const urlparse = require('url'); 15 | const req = require('request'); 16 | const readline = require('readline').createInterface({ 17 | input: process.stdin, 18 | output: process.stdout 19 | }); 20 | 21 | function question(query) { 22 | return new Promise(resolve => { 23 | readline.question(query, resolve); 24 | }) 25 | } 26 | 27 | Issuer.discover('https://accounts.lidl.com') 28 | .then(function (openidIssuer) { 29 | const nonce = generators.nonce(); 30 | const code_verifier = generators.codeVerifier(); 31 | const code_challenge = generators.codeChallenge(code_verifier); 32 | 33 | const client = new openidIssuer.Client({ 34 | client_id: 'LidlPlusNativeClient', 35 | redirect_uris: ['com.lidlplus.app://callback'], 36 | response_types: ['code'] 37 | }); 38 | 39 | const loginurl = client.authorizationUrl({ 40 | scope: 'openid profile offline_access lpprofile lpapis', 41 | code_challenge, 42 | code_challenge_method: 'S256' 43 | }); 44 | 45 | (async () => { 46 | const login_country = await question('Please enter your country code (DE, NL, ...): '); 47 | const login_language = await question('Please enter your language code (DE-DE, NL-NL, ...): '); 48 | 49 | console.log('In case your refresh_token cannot be retrieved open this url once in your browser and accept the terms and conditions of the given country:\n'); 50 | console.log(loginurl + '&Country=' + login_country + '&language=' + login_language); 51 | 52 | const browser = await puppeteer.launch(); 53 | const page = await browser.newPage(); 54 | await page.emulate(iPhone); 55 | await page.goto(loginurl + '&Country=' + login_country + '&language=' + login_language); 56 | await new Promise(r => setTimeout(r, 1000)); 57 | await page.click('#button_welcome_login', { waitUntil: 'networkidle0' }); 58 | await new Promise(r => setTimeout(r, 3000)); 59 | await page.click('[name="EmailOrPhone"]', { waitUntil: 'networkidle0' }); 60 | const login_email = await question('Please enter your LIDL Plus e-mail: '); 61 | await page.keyboard.type(login_email, { waitUntil: 'networkidle0' }); 62 | await page.click('#button_btn_submit_email', { waitUntil: 'networkidle0' }); 63 | await new Promise(r => setTimeout(r, 3000)); 64 | await page.click('[name="Password"]', { waitUntil: 'networkidle0' }); 65 | const login_password = await question('Please enter your LIDL Plus password: ') 66 | await page.keyboard.type(login_password, { waitUntil: 'networkidle0' }); 67 | await page.click('#button_submit', { waitUntil: 'networkidle0' }); 68 | await new Promise(r => setTimeout(r, 3000)); 69 | await page.click('button', { waitUntil: 'networkidle0' }); 70 | await new Promise(r => setTimeout(r, 3000)); 71 | await page.click('#field_VerificationCode', { waitUntil: 'networkidle0' }); 72 | const code = await question('Please check your e-mails and enter the received code here: '); 73 | await page.keyboard.type(code, { waitUntil: 'networkidle0' }); 74 | 75 | page.on('request', request => { 76 | if (request.isNavigationRequest()) { 77 | if (request.url().includes('com.lidlplus.app://callback')) { 78 | var url_parts = urlparse.parse(request.url(), true); 79 | console.log('Query:\n', url_parts.query); 80 | var query = url_parts.query; 81 | console.log('auth-code:\n', query.code); 82 | 83 | var tokenurl = 'https://accounts.lidl.com/connect/token'; 84 | var headers = { 85 | 'Authorization': 'Basic TGlkbFBsdXNOYXRpdmVDbGllbnQ6c2VjcmV0', 86 | 'Content-Type': 'application/x-www-form-urlencoded' 87 | }; 88 | var form = { 89 | grant_type: 'authorization_code', 90 | code: query.code, 91 | redirect_uri: 'com.lidlplus.app://callback', 92 | code_verifier: code_verifier 93 | }; 94 | 95 | req.post({ url: tokenurl, form: form, headers: headers, json: true }, function (e, r, body) { 96 | console.log('BODY:\n', body, '\n'); 97 | console.log('Access token:\n', body.access_token, '\n'); 98 | console.log('Refresh token:\n', body.refresh_token); 99 | }); 100 | } else { 101 | console.log('undefined document!!!'); 102 | } 103 | } else { 104 | console.log('undefined request... ', request.url()); 105 | } 106 | request.continue().catch((err) => { 107 | }); 108 | }); 109 | 110 | console.log("submit..."); 111 | await page.click('button', { waitUntil: 'networkidle0' }); 112 | 113 | await new Promise(r => setTimeout(r, 15000)); 114 | await browser.close(); 115 | })(); 116 | 117 | }); 118 | --------------------------------------------------------------------------------