Passed
Push — master ( 1adba3...c13e00 )
by smiley
02:15
created

QRCode::isByte()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
nc 1
nop 1
dl 0
loc 3
rs 10
c 0
b 0
f 0
1
<?php
2
/**
3
 * Class QRCode
4
 *
5
 * @filesource   QRCode.php
6
 * @created      26.11.2015
7
 * @package      chillerlan\QRCode
8
 * @author       Smiley <[email protected]>
9
 * @copyright    2015 Smiley
10
 * @license      MIT
11
 */
12
13
namespace chillerlan\QRCode;
14
15
use chillerlan\QRCode\Data\{
16
	AlphaNum, Byte, Kanji, MaskPatternTester, Number, QRCodeDataException, QRDataInterface, QRMatrix
17
};
18
use chillerlan\QRCode\Output\{
19
	QRCodeOutputException, QRImage, QRMarkup, QROutputInterface, QRString
20
};
21
use chillerlan\Settings\SettingsContainerInterface;
22
23
/**
24
 * Turns a text string into a Model 2 QR Code
25
 *
26
 * @link https://github.com/kazuhikoarase/qrcode-generator/tree/master/php
27
 * @link http://www.qrcode.com/en/codes/model12.html
28
 * @link http://www.thonky.com/qr-code-tutorial/
29
 */
30
class QRCode{
31
32
	/**
33
	 * API constants
34
	 */
35
	const OUTPUT_MARKUP_HTML  = 'html';
36
	const OUTPUT_MARKUP_SVG   = 'svg';
37
#	const OUTPUT_MARKUP_EPS   = 'eps';
38
#	const OUTPUT_MARKUP_XML   = 'xml'; // anyone?
39
40
	const OUTPUT_IMAGE_PNG    = 'png';
41
	const OUTPUT_IMAGE_JPG    = 'jpg';
42
	const OUTPUT_IMAGE_GIF    = 'gif';
43
44
	const OUTPUT_STRING_JSON  = 'json';
45
	const OUTPUT_STRING_TEXT  = 'text';
46
47
	const OUTPUT_CUSTOM       = 'custom';
48
49
	const VERSION_AUTO        = -1;
50
	const MASK_PATTERN_AUTO   = -1;
51
52
	const ECC_L         = 0b01; // 7%.
53
	const ECC_M         = 0b00; // 15%.
54
	const ECC_Q         = 0b11; // 25%.
55
	const ECC_H         = 0b10; // 30%.
56
57
	const DATA_NUMBER   = 0b0001;
58
	const DATA_ALPHANUM = 0b0010;
59
	const DATA_BYTE     = 0b0100;
60
	const DATA_KANJI    = 0b1000;
61
62
	const ECC_MODES = [
63
		self::ECC_L => 0,
64
		self::ECC_M => 1,
65
		self::ECC_Q => 2,
66
		self::ECC_H => 3,
67
	];
68
69
	const DATA_MODES = [
70
		self::DATA_NUMBER   => 0,
71
		self::DATA_ALPHANUM => 1,
72
		self::DATA_BYTE     => 2,
73
		self::DATA_KANJI    => 3,
74
	];
75
76
	const OUTPUT_MODES = [
77
		QRMarkup::class => [
78
			self::OUTPUT_MARKUP_SVG,
79
			self::OUTPUT_MARKUP_HTML,
80
#			self::OUTPUT_MARKUP_EPS,
81
		],
82
		QRImage::class => [
83
			self::OUTPUT_IMAGE_PNG,
84
			self::OUTPUT_IMAGE_GIF,
85
			self::OUTPUT_IMAGE_JPG,
86
		],
87
		QRString::class => [
88
			self::OUTPUT_STRING_JSON,
89
			self::OUTPUT_STRING_TEXT,
90
		]
91
	];
92
93
	/**
94
	 * @var \chillerlan\QRCode\QROptions
95
	 */
96
	protected $options;
97
98
	/**
99
	 * @var \chillerlan\QRCode\Data\QRDataInterface
100
	 */
101
	protected $dataInterface;
102
103
	/**
104
	 * QRCode constructor.
105
	 *
106
	 * @param \chillerlan\Settings\SettingsContainerInterface|null $options
107
	 */
108
	public function __construct(SettingsContainerInterface $options = null){
109
		mb_internal_encoding('UTF-8');
110
111
		$this->options = $options ?? new QROptions;
0 ignored issues
show
Documentation Bug introduced by
$options ?? new \chillerlan\QRCode\QROptions() is of type object<chillerlan\Settin...ingsContainerInterface>, but the property $options was declared to be of type object<chillerlan\QRCode\QROptions>. Are you sure that you always receive this specific sub-class here, or does it make sense to add an instanceof check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a given class or a super-class is assigned to a property that is type hinted more strictly.

Either this assignment is in error or an instanceof check should be added for that assignment.

class Alien {}

class Dalek extends Alien {}

class Plot
{
    /** @var  Dalek */
    public $villain;
}

$alien = new Alien();
$plot = new Plot();
if ($alien instanceof Dalek) {
    $plot->villain = $alien;
}
Loading history...
112
	}
113
114
	/**
115
	 * Renders a QR Code for the given $data and QROptions
116
	 *
117
	 * @param string $data
118
	 *
119
	 * @return mixed
120
	 */
121
	public function render(string $data){
122
		return $this->initOutputInterface($data)->dump();
123
	}
124
125
	/**
126
	 * Returns a QRMatrix object for the given $data and current QROptions
127
	 *
128
	 * @param string $data
129
	 *
130
	 * @return \chillerlan\QRCode\Data\QRMatrix
131
	 * @throws \chillerlan\QRCode\Data\QRCodeDataException
132
	 */
133
	public function getMatrix(string $data):QRMatrix {
134
		// https://github.com/chillerlan/php-qrcode/pull/15
135
		// NOTE: input sanitization should be done outside
136
		// $data = trim($data);
137
138
		if(empty($data)){
139
			throw new QRCodeDataException('QRCode::getMatrix() No data given.');
140
		}
141
142
		$this->dataInterface = $this->initDataInterface($data);
143
144
		$maskPattern = $this->options->maskPattern === $this::MASK_PATTERN_AUTO
145
			? $this->getBestMaskPattern()
146
			: min(7, max(0, (int)$this->options->maskPattern));
147
148
		$matrix = $this
149
			->dataInterface
150
			->initMatrix($maskPattern)
151
		;
152
153
		if((bool)$this->options->addQuietzone){
154
			$matrix->setQuietZone($this->options->quietzoneSize);
0 ignored issues
show
Bug introduced by
It seems like $this->options->quietzoneSize can also be of type boolean; however, chillerlan\QRCode\Data\QRMatrix::setQuietZone() does only seem to accept null|integer, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
155
		}
156
157
		return $matrix;
158
	}
159
160
	/**
161
	 * shoves a QRMatrix through the MaskPatternTester to find the lowest penalty mask pattern
162
	 *
163
	 * @see \chillerlan\QRCode\Data\MaskPatternTester
164
	 *
165
	 * @return int
166
	 */
167
	protected function getBestMaskPattern():int{
168
		$penalties = [];
169
170
		for($testPattern = 0; $testPattern < 8; $testPattern++){
171
			$matrix = $this
172
				->dataInterface
173
				->initMatrix($testPattern, true);
174
175
			$penalties[$testPattern] = (new MaskPatternTester($matrix))->testPattern();
176
		}
177
178
		return array_search(min($penalties), $penalties, true);
179
	}
180
181
	/**
182
	 * returns a fresh QRDataInterface for the given $data
183
	 *
184
	 * @param string                       $data
185
	 *
186
	 * @return \chillerlan\QRCode\Data\QRDataInterface
187
	 * @throws \chillerlan\QRCode\Data\QRCodeDataException
188
	 */
189
	public function initDataInterface(string $data):QRDataInterface{
190
191
		$DATA_MODES = [
192
			Number::class   => 'Number',
193
			AlphaNum::class => 'AlphaNum',
194
			Kanji::class    => 'Kanji',
195
			Byte::class     => 'Byte',
196
		];
197
198
		foreach($DATA_MODES as $dataInterface => $mode){
199
200
			if(call_user_func_array([$this, 'is'.$mode], [$data]) === true && class_exists($dataInterface)){
201
				return new $dataInterface($this->options, $data);
202
			}
203
204
		}
205
206
		throw new QRCodeDataException('invalid data type'); // @codeCoverageIgnore
207
	}
208
209
	/**
210
	 * returns a fresh (built-in) QROutputInterface
211
	 *
212
	 * @param string $data
213
	 *
214
	 * @return \chillerlan\QRCode\Output\QROutputInterface
215
	 * @throws \chillerlan\QRCode\Output\QRCodeOutputException
216
	 */
217
	protected function initOutputInterface(string $data):QROutputInterface{
218
219
		if($this->options->outputType === $this::OUTPUT_CUSTOM && class_exists($this->options->outputInterface)){
220
			return new $this->options->outputInterface($this->options, $this->getMatrix($data));
221
		}
222
223
		foreach($this::OUTPUT_MODES as $outputInterface => $modes){
224
225
			if(in_array($this->options->outputType, $modes, true) && class_exists($outputInterface)){
226
				return new $outputInterface($this->options, $this->getMatrix($data));
227
			}
228
229
		}
230
231
		throw new QRCodeOutputException('invalid output type');
232
	}
233
234
	/**
235
	 * checks if a string qualifies as numeric
236
	 *
237
	 * @param string $string
238
	 *
239
	 * @return bool
240
	 */
241
	public function isNumber(string $string):bool {
242
		return $this->checkString($string, Number::CHAR_MAP);
243
	}
244
245
	/**
246
	 * checks if a string qualifies as alphanumeric
247
	 *
248
	 * @param string $string
249
	 *
250
	 * @return bool
251
	 */
252
	public function isAlphaNum(string $string):bool {
253
		return $this->checkString($string, AlphaNum::CHAR_MAP);
254
	}
255
256
	/**
257
	 * checks is a given $string matches the characters of a given $charmap, returns false on the first invalid occurence.
258
	 *
259
	 * @param string $string
260
	 * @param array  $charmap
261
	 *
262
	 * @return bool
263
	 */
264
	protected function checkString(string $string, array $charmap):bool{
265
		$len = strlen($string);
266
267
		for($i = 0; $i < $len; $i++){
268
			if(!in_array($string[$i], $charmap, true)){
269
				return false;
270
			}
271
		}
272
273
		return true;
274
	}
275
276
	/**
277
	 * checks if a string qualifies as Kanji
278
	 *
279
	 * @param string $string
280
	 *
281
	 * @return bool
282
	 */
283
	public function isKanji(string $string):bool {
284
		$i   = 0;
285
		$len = strlen($string);
286
287
		while($i + 1 < $len){
288
			$c = ((0xff&ord($string[$i])) << 8)|(0xff&ord($string[$i + 1]));
289
290
			if(!($c >= 0x8140 && $c <= 0x9FFC) && !($c >= 0xE040 && $c <= 0xEBBF)){
291
				return false;
292
			}
293
294
			$i += 2;
295
		}
296
297
		return $i >= $len;
298
	}
299
300
	/**
301
	 * a dummy
302
	 *
303
	 * @param $data
304
	 *
305
	 * @return bool
306
	 */
307
	protected function isByte(string $data):bool{
308
		return !empty($data);
309
	}
310
311
}
312