Completed
Push — master ( b8d8de...2f6d07 )
by smiley
05:15
created

QRCode::checkString()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 11
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 3
eloc 6
nc 3
nop 2
dl 0
loc 11
rs 9.4285
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\Traits\{
22
	ClassLoader, ContainerInterface
23
};
24
25
/**
26
 * Turns a text string into a Model 2 QR Code
27
 *
28
 * @link https://github.com/kazuhikoarase/qrcode-generator/tree/master/php
29
 * @link http://www.qrcode.com/en/codes/model12.html
30
 * @link http://www.thonky.com/qr-code-tutorial/
31
 */
32
class QRCode{
33
	use ClassLoader;
34
35
	/**
36
	 * API constants
37
	 */
38
	const OUTPUT_MARKUP_HTML  = 'html';
39
	const OUTPUT_MARKUP_SVG   = 'svg';
40
#	const OUTPUT_MARKUP_EPS   = 'eps';
41
#	const OUTPUT_MARKUP_XML   = 'xml'; // anyone?
42
43
	const OUTPUT_IMAGE_PNG    = 'png';
44
	const OUTPUT_IMAGE_JPG    = 'jpg';
45
	const OUTPUT_IMAGE_GIF    = 'gif';
46
47
	const OUTPUT_STRING_JSON  = 'json';
48
	const OUTPUT_STRING_TEXT  = 'text';
49
50
	const OUTPUT_CUSTOM       = 'custom';
51
52
	const VERSION_AUTO        = -1;
53
	const MASK_PATTERN_AUTO   = -1;
54
55
	const ECC_L         = 0b01; // 7%.
56
	const ECC_M         = 0b00; // 15%.
57
	const ECC_Q         = 0b11; // 25%.
58
	const ECC_H         = 0b10; // 30%.
59
60
	const DATA_NUMBER   = 0b0001;
61
	const DATA_ALPHANUM = 0b0010;
62
	const DATA_BYTE     = 0b0100;
63
	const DATA_KANJI    = 0b1000;
64
65
	const ECC_MODES = [
66
		self::ECC_L => 0,
67
		self::ECC_M => 1,
68
		self::ECC_Q => 2,
69
		self::ECC_H => 3,
70
	];
71
72
	const DATA_MODES = [
73
		self::DATA_NUMBER   => 0,
74
		self::DATA_ALPHANUM => 1,
75
		self::DATA_BYTE     => 2,
76
		self::DATA_KANJI    => 3,
77
	];
78
79
	const OUTPUT_MODES = [
80
		QRMarkup::class => [
81
			self::OUTPUT_MARKUP_SVG,
82
			self::OUTPUT_MARKUP_HTML,
83
#			self::OUTPUT_MARKUP_EPS,
84
		],
85
		QRImage::class => [
86
			self::OUTPUT_IMAGE_PNG,
87
			self::OUTPUT_IMAGE_GIF,
88
			self::OUTPUT_IMAGE_JPG,
89
		],
90
		QRString::class => [
91
			self::OUTPUT_STRING_JSON,
92
			self::OUTPUT_STRING_TEXT,
93
		]
94
	];
95
96
	/**
97
	 * @var \chillerlan\QRCode\QROptions
98
	 */
99
	protected $options;
100
101
	/**
102
	 * @var \chillerlan\QRCode\Data\QRDataInterface
103
	 */
104
	protected $dataInterface;
105
106
	/**
107
	 * QRCode constructor.
108
	 *
109
	 * @param \chillerlan\Traits\ContainerInterface|null $options
110
	 */
111
	public function __construct(ContainerInterface $options = null){
112
		mb_internal_encoding('UTF-8');
113
114
		$this->setOptions($options ?? new QROptions);
115
	}
116
117
	/**
118
	 * Sets the options, called internally by the constructor
119
	 *
120
	 * @param \chillerlan\Traits\ContainerInterface $options
121
	 *
122
	 * @return \chillerlan\QRCode\QRCode
123
	 * @throws \chillerlan\QRCode\QRCodeException
124
	 */
125
	public function setOptions(ContainerInterface $options):QRCode{
126
127
		if(!array_key_exists($options->eccLevel, $this::ECC_MODES)){
0 ignored issues
show
Bug introduced by
Accessing eccLevel on the interface chillerlan\Traits\ContainerInterface suggest that you code against a concrete implementation. How about adding an instanceof check?

If you access a property on an interface, you most likely code against a concrete implementation of the interface.

Available Fixes

  1. Adding an additional type check:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeInterface $object) {
        if ($object instanceof SomeClass) {
            $a = $object->a;
        }
    }
    
  2. Changing the type hint:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeClass $object) {
        $a = $object->a;
    }
    
Loading history...
128
			throw new QRCodeException('Invalid error correct level: '.$options->eccLevel);
0 ignored issues
show
Bug introduced by
Accessing eccLevel on the interface chillerlan\Traits\ContainerInterface suggest that you code against a concrete implementation. How about adding an instanceof check?

If you access a property on an interface, you most likely code against a concrete implementation of the interface.

Available Fixes

  1. Adding an additional type check:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeInterface $object) {
        if ($object instanceof SomeClass) {
            $a = $object->a;
        }
    }
    
  2. Changing the type hint:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeClass $object) {
        $a = $object->a;
    }
    
Loading history...
129
		}
130
131
		if(!is_array($options->imageTransparencyBG) || count($options->imageTransparencyBG) < 3){
0 ignored issues
show
Bug introduced by
Accessing imageTransparencyBG on the interface chillerlan\Traits\ContainerInterface suggest that you code against a concrete implementation. How about adding an instanceof check?

If you access a property on an interface, you most likely code against a concrete implementation of the interface.

Available Fixes

  1. Adding an additional type check:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeInterface $object) {
        if ($object instanceof SomeClass) {
            $a = $object->a;
        }
    }
    
  2. Changing the type hint:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeClass $object) {
        $a = $object->a;
    }
    
Loading history...
132
			$options->imageTransparencyBG = [255, 255, 255];
0 ignored issues
show
Bug introduced by
Accessing imageTransparencyBG on the interface chillerlan\Traits\ContainerInterface suggest that you code against a concrete implementation. How about adding an instanceof check?

If you access a property on an interface, you most likely code against a concrete implementation of the interface.

Available Fixes

  1. Adding an additional type check:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeInterface $object) {
        if ($object instanceof SomeClass) {
            $a = $object->a;
        }
    }
    
  2. Changing the type hint:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeClass $object) {
        $a = $object->a;
    }
    
Loading history...
133
		}
134
135
		$options->version = (int)$options->version;
0 ignored issues
show
Bug introduced by
Accessing version on the interface chillerlan\Traits\ContainerInterface suggest that you code against a concrete implementation. How about adding an instanceof check?

If you access a property on an interface, you most likely code against a concrete implementation of the interface.

Available Fixes

  1. Adding an additional type check:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeInterface $object) {
        if ($object instanceof SomeClass) {
            $a = $object->a;
        }
    }
    
  2. Changing the type hint:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeClass $object) {
        $a = $object->a;
    }
    
Loading history...
136
137
		// clamp min/max version number
138
		$options->versionMin = (int)min($options->versionMin, $options->versionMax);
0 ignored issues
show
Bug introduced by
Accessing versionMin on the interface chillerlan\Traits\ContainerInterface suggest that you code against a concrete implementation. How about adding an instanceof check?

If you access a property on an interface, you most likely code against a concrete implementation of the interface.

Available Fixes

  1. Adding an additional type check:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeInterface $object) {
        if ($object instanceof SomeClass) {
            $a = $object->a;
        }
    }
    
  2. Changing the type hint:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeClass $object) {
        $a = $object->a;
    }
    
Loading history...
Bug introduced by
Accessing versionMax on the interface chillerlan\Traits\ContainerInterface suggest that you code against a concrete implementation. How about adding an instanceof check?

If you access a property on an interface, you most likely code against a concrete implementation of the interface.

Available Fixes

  1. Adding an additional type check:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeInterface $object) {
        if ($object instanceof SomeClass) {
            $a = $object->a;
        }
    }
    
  2. Changing the type hint:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeClass $object) {
        $a = $object->a;
    }
    
Loading history...
139
		$options->versionMax = (int)max($options->versionMin, $options->versionMax);
0 ignored issues
show
Bug introduced by
Accessing versionMax on the interface chillerlan\Traits\ContainerInterface suggest that you code against a concrete implementation. How about adding an instanceof check?

If you access a property on an interface, you most likely code against a concrete implementation of the interface.

Available Fixes

  1. Adding an additional type check:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeInterface $object) {
        if ($object instanceof SomeClass) {
            $a = $object->a;
        }
    }
    
  2. Changing the type hint:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeClass $object) {
        $a = $object->a;
    }
    
Loading history...
Bug introduced by
Accessing versionMin on the interface chillerlan\Traits\ContainerInterface suggest that you code against a concrete implementation. How about adding an instanceof check?

If you access a property on an interface, you most likely code against a concrete implementation of the interface.

Available Fixes

  1. Adding an additional type check:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeInterface $object) {
        if ($object instanceof SomeClass) {
            $a = $object->a;
        }
    }
    
  2. Changing the type hint:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeClass $object) {
        $a = $object->a;
    }
    
Loading history...
140
141
		$this->options = $options;
0 ignored issues
show
Documentation Bug introduced by
$options is of type object<chillerlan\Traits\ContainerInterface>, 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...
142
143
		return $this;
144
	}
145
146
	/**
147
	 * Renders a QR Code for the given $data and QROptions
148
	 *
149
	 * @param string $data
150
	 *
151
	 * @return mixed
152
	 */
153
	public function render(string $data){
154
		return $this->initOutputInterface($data)->dump();
155
	}
156
157
	/**
158
	 * Returns a QRMatrix object for the given $data and current QROptions
159
	 *
160
	 * @param string $data
161
	 *
162
	 * @return \chillerlan\QRCode\Data\QRMatrix
163
	 * @throws \chillerlan\QRCode\Data\QRCodeDataException
164
	 */
165
	public function getMatrix(string $data):QRMatrix {
166
		// https://github.com/chillerlan/php-qrcode/pull/15
167
		// NOTE: input sanitization should be done outside
168
		// $data = trim($data);
169
170
		if(empty($data)){
171
			throw new QRCodeDataException('QRCode::getMatrix() No data given.');
172
		}
173
174
		$this->dataInterface = $this->initDataInterface($data);
175
176
		$maskPattern = $this->options->maskPattern === $this::MASK_PATTERN_AUTO
177
			? $this->getBestMaskPattern()
178
			: min(7, max(0, (int)$this->options->maskPattern));
179
180
		$matrix = $this
181
			->dataInterface
182
			->initMatrix($maskPattern)
183
		;
184
185
		if((bool)$this->options->addQuietzone){
186
			$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...
187
		}
188
189
		return $matrix;
190
	}
191
192
	/**
193
	 * shoves a QRMatrix through the MaskPatternTester to find the lowest penalty mask pattern
194
	 *
195
	 * @see \chillerlan\QRCode\Data\MaskPatternTester
196
	 *
197
	 * @return int
198
	 */
199
	protected function getBestMaskPattern():int{
200
		$penalties = [];
201
202
		for($testPattern = 0; $testPattern < 8; $testPattern++){
203
			$matrix = $this
204
				->dataInterface
205
				->initMatrix($testPattern, true);
206
207
			$penalties[$testPattern] = (new MaskPatternTester($matrix))->testPattern();
208
		}
209
210
		return array_search(min($penalties), $penalties, true);
211
	}
212
213
	/**
214
	 * returns a fresh QRDataInterface for the given $data
215
	 *
216
	 * @param string                       $data
217
	 *
218
	 * @return \chillerlan\QRCode\Data\QRDataInterface
219
	 * @throws \chillerlan\QRCode\Data\QRCodeDataException
220
	 */
221
	public function initDataInterface(string $data):QRDataInterface{
222
223
		$DATA_MODES = [
224
			Number::class   => 'Number',
225
			AlphaNum::class => 'AlphaNum',
226
			Kanji::class    => 'Kanji',
227
			Byte::class     => 'Byte',
228
		];
229
230
		foreach($DATA_MODES as $dataInterface => $mode){
231
232
			if(call_user_func_array([$this, 'is'.$mode], [$data]) === true){
233
				return $this->loadClass($dataInterface, QRDataInterface::class, $this->options, $data);
234
			}
235
236
		}
237
238
		throw new QRCodeDataException('invalid data type'); // @codeCoverageIgnore
239
	}
240
241
	/**
242
	 * returns a fresh (built-in) QROutputInterface
243
	 *
244
	 * @param string $data
245
	 *
246
	 * @return \chillerlan\QRCode\Output\QROutputInterface
247
	 * @throws \chillerlan\QRCode\Output\QRCodeOutputException
248
	 */
249
	protected function initOutputInterface(string $data):QROutputInterface{
250
251
		if($this->options->outputType === $this::OUTPUT_CUSTOM && $this->options->outputInterface !== null){
252
			return $this->loadClass($this->options->outputInterface, QROutputInterface::class, $this->options, $this->getMatrix($data));
253
		}
254
255
		foreach($this::OUTPUT_MODES as $outputInterface => $modes){
256
257
			if(in_array($this->options->outputType, $modes, true)){
258
				return $this->loadClass($outputInterface, QROutputInterface::class, $this->options, $this->getMatrix($data));
259
			}
260
261
		}
262
263
		throw new QRCodeOutputException('invalid output type');
264
	}
265
266
	/**
267
	 * checks if a string qualifies as numeric
268
	 *
269
	 * @param string $string
270
	 *
271
	 * @return bool
272
	 */
273
	public function isNumber(string $string):bool {
274
		return $this->checkString($string, Number::CHAR_MAP);
275
	}
276
277
	/**
278
	 * checks if a string qualifies as alphanumeric
279
	 *
280
	 * @param string $string
281
	 *
282
	 * @return bool
283
	 */
284
	public function isAlphaNum(string $string):bool {
285
		return $this->checkString($string, AlphaNum::CHAR_MAP);
286
	}
287
288
	/**
289
	 * checks is a given $string matches the characters of a given $charmap, returns false on the first invalid occurence.
290
	 *
291
	 * @param string $string
292
	 * @param array  $charmap
293
	 *
294
	 * @return bool
295
	 */
296
	protected function checkString(string $string, array $charmap):bool{
297
		$len = strlen($string);
298
299
		for($i = 0; $i < $len; $i++){
300
			if(!in_array($string[$i], $charmap, true)){
301
				return false;
302
			}
303
		}
304
305
		return true;
306
	}
307
308
	/**
309
	 * checks if a string qualifies as Kanji
310
	 *
311
	 * @param string $string
312
	 *
313
	 * @return bool
314
	 */
315
	public function isKanji(string $string):bool {
316
		$i   = 0;
317
		$len = strlen($string);
318
319
		while($i + 1 < $len){
320
			$c = ((0xff&ord($string[$i])) << 8)|(0xff&ord($string[$i + 1]));
321
322
			if(!($c >= 0x8140 && $c <= 0x9FFC) && !($c >= 0xE040 && $c <= 0xEBBF)){
323
				return false;
324
			}
325
326
			$i += 2;
327
		}
328
329
		return $i >= $len;
330
	}
331
332
	/**
333
	 * a dummy
334
	 *
335
	 * @param $data
336
	 *
337
	 * @return bool
338
	 */
339
	protected function isByte(string $data):bool{
340
		return !empty($data);
341
	}
342
343
}
344