diff options
author | Simon Rettberg | 2024-05-27 15:37:55 +0200 |
---|---|---|
committer | Simon Rettberg | 2024-05-27 15:37:55 +0200 |
commit | 43ddb14693e4a4830f471dd7c90f6257d95b7b29 (patch) | |
tree | 8c960b53e0f7ba1c660d16abf8bf044411b96080 | |
parent | Merge branch 'master' of git.openslx.org:bwlp/bwlp-webadmin (diff) | |
download | bwlp-webadmin-master.tar.gz bwlp-webadmin-master.tar.xz bwlp-webadmin-master.zip |
-rw-r--r-- | inc/qrcode.inc.php | 1860 | ||||
-rw-r--r-- | inc/shibauth.inc.php | 32 | ||||
-rw-r--r-- | inc/user.inc.php | 10 | ||||
-rw-r--r-- | inc/util.inc.php | 18 | ||||
-rw-r--r-- | modules/register.inc.php | 8 | ||||
-rw-r--r-- | pam.php | 44 | ||||
-rw-r--r-- | shib/api.php | 1 | ||||
-rw-r--r-- | shib/client_auth.php | 142 |
8 files changed, 2085 insertions, 30 deletions
diff --git a/inc/qrcode.inc.php b/inc/qrcode.inc.php new file mode 100644 index 0000000..a4c7a87 --- /dev/null +++ b/inc/qrcode.inc.php @@ -0,0 +1,1860 @@ +<?php + +//--------------------------------------------------------------- +// QRCode for PHP5 +// +// Copyright (c) 2009 Kazuhiko Arase +// +// URL: http://www.d-project.com/ +// +// Licensed under the MIT license: +// http://www.opensource.org/licenses/mit-license.php +// +// The word "QR Code" is registered trademark of +// DENSO WAVE INCORPORATED +// http://www.denso-wave.com/qrcode/faqpatent-e.html +// +//--------------------------------------------------------------------- + +//--------------------------------------------------------------- +// QRCode +//--------------------------------------------------------------- + +const QR_PAD0 = 0xEC; +const QR_PAD1 = 0x11; + +//--------------------------------------------------------------- +// Mode +//--------------------------------------------------------------- + +const QR_MODE_NUMBER = 1 << 0; +const QR_MODE_ALPHA_NUM = 1 << 1; +const QR_MODE_8BIT_BYTE = 1 << 2; +const QR_MODE_KANJI = 1 << 3; + +//--------------------------------------------------------------- +// MaskPattern +//--------------------------------------------------------------- + +const QR_MASK_PATTERN000 = 0; +const QR_MASK_PATTERN001 = 1; +const QR_MASK_PATTERN010 = 2; +const QR_MASK_PATTERN011 = 3; +const QR_MASK_PATTERN100 = 4; +const QR_MASK_PATTERN101 = 5; +const QR_MASK_PATTERN110 = 6; +const QR_MASK_PATTERN111 = 7; + +//--------------------------------------------------------------- +// ErrorCorrectLevel + +// 7%. +const QR_ERROR_CORRECT_LEVEL_L = 1; +// 15%. +const QR_ERROR_CORRECT_LEVEL_M = 0; +// 25%. +const QR_ERROR_CORRECT_LEVEL_Q = 3; +// 30%. +const QR_ERROR_CORRECT_LEVEL_H = 2; + +class QRCode +{ + + private int $typeNumber; + + private $modules; + + private int $moduleCount; + + private int $errorCorrectLevel; + + /** @var QRData[] */ + private array $qrDataList; + + public function __construct() + { + $this->typeNumber = 1; + $this->errorCorrectLevel = QR_ERROR_CORRECT_LEVEL_H; + $this->qrDataList = array(); + } + + public function getTypeNumber(): int + { + return $this->typeNumber; + } + + public function setTypeNumber(int $typeNumber): void + { + $this->typeNumber = $typeNumber; + } + + public function getErrorCorrectLevel(): int + { + return $this->errorCorrectLevel; + } + + public function setErrorCorrectLevel(int $errorCorrectLevel): void + { + $this->errorCorrectLevel = $errorCorrectLevel; + } + + public function addData($data, int $mode = 0): void + { + + if ($mode == 0) { + $mode = QRUtil::getMode($data); + } + + switch ($mode) { + + case QR_MODE_NUMBER : + $this->addDataImpl(new QRNumber($data)); + break; + + case QR_MODE_ALPHA_NUM : + $this->addDataImpl(new QRAlphaNum($data)); + break; + + case QR_MODE_8BIT_BYTE : + $this->addDataImpl(new QR8BitByte($data)); + break; + + case QR_MODE_KANJI : + $this->addDataImpl(new QRKanji($data)); + break; + + default : + trigger_error("mode:$mode", E_USER_ERROR); + } + } + + public function clearData(): void + { + $this->qrDataList = array(); + } + + private function addDataImpl(QRData $qrData): void + { + $this->qrDataList[] = $qrData; + } + + public function getDataCount(): int + { + return count($this->qrDataList); + } + + public function getData(int $index): QRData + { + return $this->qrDataList[$index]; + } + + public function isDark(int $row, int $col): bool + { + if ($this->modules[$row][$col] !== null) { + return $this->modules[$row][$col]; + } else { + return false; + } + } + + public function getModuleCount(): int + { + return $this->moduleCount; + } + + // used for converting fg/bg colors (e.g. #0000ff = 0x0000FF) + // added 2015.07.27 ~ DoktorJ + private function hex2rgb(int $hex = 0x0): array + { + return array( + 'r' => floor($hex / 65536), + 'g' => floor($hex / 256) % 256, + 'b' => $hex % 256 + ); + } + + public function make(): void + { + $this->makeImpl(false, $this->getBestMaskPattern()); + } + + private function getBestMaskPattern(): int + { + + $minLostPoint = 0; + $pattern = 0; + + for ($i = 0; $i < 8; $i++) { + + $this->makeImpl(true, $i); + + $lostPoint = QRUtil::getLostPoint($this); + + if ($i == 0 || $minLostPoint > $lostPoint) { + $minLostPoint = $lostPoint; + $pattern = $i; + } + } + + return $pattern; + } + + private function createNullArray($length): array + { + $nullArray = array(); + for ($i = 0; $i < $length; $i++) { + $nullArray[] = null; + } + return $nullArray; + } + + function makeImpl($test, $maskPattern): void + { + + $this->moduleCount = $this->typeNumber * 4 + 17; + + $this->modules = array(); + for ($i = 0; $i < $this->moduleCount; $i++) { + $this->modules[] = QRCode::createNullArray($this->moduleCount); + } + + $this->setupPositionProbePattern(0, 0); + $this->setupPositionProbePattern($this->moduleCount - 7, 0); + $this->setupPositionProbePattern(0, $this->moduleCount - 7); + + $this->setupPositionAdjustPattern(); + $this->setupTimingPattern(); + + $this->setupTypeInfo($test, $maskPattern); + + if ($this->typeNumber >= 7) { + $this->setupTypeNumber($test); + } + + $dataArray = $this->qrDataList; + + $data = QRCode::createData($this->typeNumber, $this->errorCorrectLevel, $dataArray); + + $this->mapData($data, $maskPattern); + } + + function mapData($data, $maskPattern): void + { + + $inc = -1; + $row = $this->moduleCount - 1; + $bitIndex = 7; + $byteIndex = 0; + + for ($col = $this->moduleCount - 1; $col > 0; $col -= 2) { + + if ($col == 6) + $col--; + + while (true) { + + for ($c = 0; $c < 2; $c++) { + + if ($this->modules[$row][$col - $c] === null) { + + $dark = false; + + if ($byteIndex < count($data)) { + $dark = ((($data[$byteIndex] >> $bitIndex) & 1) == 1); + } + + if (QRUtil::getMask($maskPattern, $row, $col - $c)) { + $dark = !$dark; + } + + $this->modules[$row][$col - $c] = $dark; + $bitIndex--; + + if ($bitIndex == -1) { + $byteIndex++; + $bitIndex = 7; + } + } + } + + $row += $inc; + + if ($row < 0 || $this->moduleCount <= $row) { + $row -= $inc; + $inc = -$inc; + break; + } + } + } + } + + private function setupPositionAdjustPattern(): void + { + + $pos = QRUtil::getPatternPosition($this->typeNumber); + + for ($i = 0; $i < count($pos); $i++) { + + for ($j = 0; $j < count($pos); $j++) { + + $row = $pos[$i]; + $col = $pos[$j]; + + if ($this->modules[$row][$col] !== null) { + continue; + } + + for ($r = -2; $r <= 2; $r++) { + + for ($c = -2; $c <= 2; $c++) { + $this->modules[$row + $r][$col + $c] = + $r == -2 || $r == 2 || $c == -2 || $c == 2 || ($r == 0 && $c == 0); + } + } + } + } + } + + private function setupPositionProbePattern(int $row, int $col): void + { + + for ($r = -1; $r <= 7; $r++) { + + for ($c = -1; $c <= 7; $c++) { + + if ($row + $r <= -1 || $this->moduleCount <= $row + $r + || $col + $c <= -1 || $this->moduleCount <= $col + $c) { + continue; + } + + $this->modules[$row + $r][$col + $c] = + (0 <= $r && $r <= 6 && ($c == 0 || $c == 6)) + || (0 <= $c && $c <= 6 && ($r == 0 || $r == 6)) + || (2 <= $r && $r <= 4 && 2 <= $c && $c <= 4); + } + } + } + + private function setupTimingPattern(): void + { + + for ($i = 8; $i < $this->moduleCount - 8; $i++) { + + if ($this->modules[$i][6] !== null || $this->modules[6][$i] !== null) { + continue; + } + + $this->modules[$i][6] = ($i % 2 == 0); + $this->modules[6][$i] = ($i % 2 == 0); + } + } + + private function setupTypeNumber($test): void + { + + $bits = QRUtil::getBCHTypeNumber($this->typeNumber); + + for ($i = 0; $i < 18; $i++) { + $mod = (!$test && (($bits >> $i) & 1) == 1); + $this->modules[(int)floor($i / 3)][$i % 3 + $this->moduleCount - 8 - 3] = $mod; + $this->modules[$i % 3 + $this->moduleCount - 8 - 3][floor($i / 3)] = $mod; + } + } + + function setupTypeInfo($test, int $maskPattern): void + { + + $data = ($this->errorCorrectLevel << 3) | $maskPattern; + $bits = QRUtil::getBCHTypeInfo($data); + + for ($i = 0; $i < 15; $i++) { + + $mod = (!$test && (($bits >> $i) & 1) == 1); + + if ($i < 6) { + $this->modules[$i][8] = $mod; + } else if ($i < 8) { + $this->modules[$i + 1][8] = $mod; + } else { + $this->modules[$this->moduleCount - 15 + $i][8] = $mod; + } + + if ($i < 8) { + $this->modules[8][$this->moduleCount - $i - 1] = $mod; + } else if ($i < 9) { + $this->modules[8][15 - $i - 1 + 1] = $mod; + } else { + $this->modules[8][15 - $i - 1] = $mod; + } + } + + $this->modules[$this->moduleCount - 8][8] = !$test; + } + + private function createData(int $typeNumber, int $errorCorrectLevel, array $dataArray): array + { + + $rsBlocks = QRRSBlock::getRSBlocks($typeNumber, $errorCorrectLevel); + + $buffer = new QRBitBuffer(); + + for ($i = 0; $i < count($dataArray); $i++) { + /** @var \QRData $data */ + $data = $dataArray[$i]; + $buffer->put($data->getMode(), 4); + $buffer->put($data->getLength(), $data->getLengthInBits($typeNumber)); + $data->write($buffer); + } + + $totalDataCount = 0; + for ($i = 0; $i < count($rsBlocks); $i++) { + $totalDataCount += $rsBlocks[$i]->getDataCount(); + } + + if ($buffer->getLengthInBits() > $totalDataCount * 8) { + trigger_error("code length overflow. (" + . $buffer->getLengthInBits() + . ">" + . $totalDataCount * 8 + . ")", E_USER_ERROR); + } + + // end code. + if ($buffer->getLengthInBits() + 4 <= $totalDataCount * 8) { + $buffer->put(0, 4); + } + + // padding + while ($buffer->getLengthInBits() % 8 != 0) { + $buffer->putBit(false); + } + + // padding + while (true) { + + if ($buffer->getLengthInBits() >= $totalDataCount * 8) { + break; + } + $buffer->put(QR_PAD0, 8); + + if ($buffer->getLengthInBits() >= $totalDataCount * 8) { + break; + } + $buffer->put(QR_PAD1, 8); + } + + return QRCode::createBytes($buffer, $rsBlocks); + } + + /** + * @param QRRSBlock[] $rsBlocks + */ + private function createBytes(QRBitBuffer $buffer, array $rsBlocks): array + { + + $offset = 0; + + $maxDcCount = 0; + $maxEcCount = 0; + + $dcdata = self::createNullArray(count($rsBlocks)); + $ecdata = self::createNullArray(count($rsBlocks)); + + $rsBlockCount = count($rsBlocks); + for ($r = 0; $r < $rsBlockCount; $r++) { + + $dcCount = $rsBlocks[$r]->getDataCount(); + $ecCount = $rsBlocks[$r]->getTotalCount() - $dcCount; + + $maxDcCount = max($maxDcCount, $dcCount); + $maxEcCount = max($maxEcCount, $ecCount); + + $dcdata[$r] = QRCode::createNullArray($dcCount); + $dcDataCount = count($dcdata[$r]); + for ($i = 0; $i < $dcDataCount; $i++) { + $bdata = $buffer->getBuffer(); + $dcdata[$r][$i] = 0xff & $bdata[$i + $offset]; + } + $offset += $dcCount; + + $rsPoly = QRUtil::getErrorCorrectPolynomial($ecCount); + $rawPoly = new QRPolynomial($dcdata[$r], $rsPoly->getLength() - 1); + + $modPoly = $rawPoly->mod($rsPoly); + $ecdata[$r] = QRCode::createNullArray($rsPoly->getLength() - 1); + + $ecDataCount = count($ecdata[$r]); + for ($i = 0; $i < $ecDataCount; $i++) { + $modIndex = $i + $modPoly->getLength() - count($ecdata[$r]); + $ecdata[$r][$i] = ($modIndex >= 0) ? $modPoly->get($modIndex) : 0; + } + } + + $totalCodeCount = 0; + for ($i = 0; $i < $rsBlockCount; $i++) { + $totalCodeCount += $rsBlocks[$i]->getTotalCount(); + } + + $data = QRCode::createNullArray($totalCodeCount); + + $index = 0; + + for ($i = 0; $i < $maxDcCount; $i++) { + for ($r = 0; $r < $rsBlockCount; $r++) { + if ($i < count($dcdata[$r])) { + $data[$index++] = $dcdata[$r][$i]; + } + } + } + + for ($i = 0; $i < $maxEcCount; $i++) { + for ($r = 0; $r < $rsBlockCount; $r++) { + if ($i < count($ecdata[$r])) { + $data[$index++] = $ecdata[$r][$i]; + } + } + } + + return $data; + } + + public static function getMinimumQRCode($data, int $errorCorrectLevel): QRCode + { + + $mode = QRUtil::getMode($data); + + $qr = new QRCode(); + $qr->setErrorCorrectLevel($errorCorrectLevel); + $qr->addData($data, $mode); + + $qrData = $qr->getData(0); + $length = $qrData->getLength(); + + for ($typeNumber = 1; $typeNumber <= 40; $typeNumber++) { + if ($length <= QRUtil::getMaxLength($typeNumber, $mode, $errorCorrectLevel)) { + $qr->setTypeNumber($typeNumber); + break; + } + } + + $qr->make(); + + return $qr; + } + + // added $fg (foreground), $bg (background), and $bgtrans (use transparent bg) parameters + // also added some simple error checking on parameters + // updated 2015.07.27 ~ DoktorJ + public function createImage(int $size = 2, int $margin = 2, int $fg = 0x000000, int $bg = 0xFFFFFF, bool $bgtrans = false) + { + + // size/margin EC + if (!is_numeric($size)) + $size = 2; + if (!is_numeric($margin)) + $margin = 2; + if ($size < 1) + $size = 1; + if ($margin < 0) + $margin = 0; + + $image_size = $this->getModuleCount() * $size + $margin * 2; + + $image = imagecreatetruecolor($image_size, $image_size); + + // fg/bg EC + if ($fg < 0 || $fg > 0xFFFFFF) + $fg = 0x0; + if ($bg < 0 || $bg > 0xFFFFFF) + $bg = 0xFFFFFF; + + // convert hexadecimal RGB to arrays for imagecolorallocate + $fgrgb = $this->hex2rgb($fg); + $bgrgb = $this->hex2rgb($bg); + + // replace $black and $white with $fgc and $bgc + $fgc = imagecolorallocate($image, $fgrgb['r'], $fgrgb['g'], $fgrgb['b']); + $bgc = imagecolorallocate($image, $bgrgb['r'], $bgrgb['g'], $bgrgb['b']); + if ($bgtrans) + imagecolortransparent($image, $bgc); + + // update $white to $bgc + imagefilledrectangle($image, 0, 0, $image_size, $image_size, $bgc); + + for ($r = 0; $r < $this->getModuleCount(); $r++) { + for ($c = 0; $c < $this->getModuleCount(); $c++) { + if ($this->isDark($r, $c)) { + + // update $black to $fgc + imagefilledrectangle($image, + $margin + $c * $size, + $margin + $r * $size, + $margin + ($c + 1) * $size - 1, + $margin + ($r + 1) * $size - 1, + $fgc); + } + } + } + + return $image; + } + + public function printHTML($size = "2px"): void + { + + $style = "border-style:none;border-collapse:collapse;margin:0px;padding:0px;"; + + print("<table style='$style'>"); + + for ($r = 0; $r < $this->getModuleCount(); $r++) { + + print("<tr style='$style'>"); + + for ($c = 0; $c < $this->getModuleCount(); $c++) { + $color = $this->isDark($r, $c) ? "#000000" : "#ffffff"; + print("<td style='$style;width:$size;height:$size;background-color:$color'></td>"); + } + + print("</tr>"); + } + + print("</table>"); + } + + public function printSVG(int $scale = 2): void + { + $width = $this->getModuleCount() * $scale; + $height = $width; + print('<svg width="' . $width . '" height="' . $height . '" viewBox="0 0 ' . $width . ' ' . $height . '" xmlns="http://www.w3.org/2000/svg">'); + print('<rect x="0" y="0" width="' . $width . '" height="' . $height . '" fill="#fff" shape-rendering="crispEdges"/>'); + + $modSize = $this->getModuleCount(); + for ($r = 0; $r < $modSize; $r++) { + for ($c = 0; $c < $modSize; $c++) { + if ($this->isDark($r, $c)) { + print('<rect x="' . ($c * $scale) . '" y="' . ($r * $scale) . '" width="' . ($scale + 1) . '" height="' . ($scale + 1) . '" fill="#000" shape-rendering="crispEdges"/>'); + } + } + } + + print("</svg>"); + } +} + +//--------------------------------------------------------------- +// QRUtil +//--------------------------------------------------------------- + +const QR_G15 = (1 << 10) | (1 << 8) | (1 << 5) + | (1 << 4) | (1 << 2) | (1 << 1) | (1 << 0); + +const QR_G18 = (1 << 12) | (1 << 11) | (1 << 10) + | (1 << 9) | (1 << 8) | (1 << 5) | (1 << 2) | (1 << 0); + +const QR_G15_MASK = (1 << 14) | (1 << 12) | (1 << 10) + | (1 << 4) | (1 << 1); + +class QRUtil +{ + + static array $QR_MAX_LENGTH = array( + array(array(41, 25, 17, 10), array(34, 20, 14, 8), array(27, 16, 11, 7), array(17, 10, 7, 4)), + array(array(77, 47, 32, 20), array(63, 38, 26, 16), array(48, 29, 20, 12), array(34, 20, 14, 8)), + array(array(127, 77, 53, 32), array(101, 61, 42, 26), array(77, 47, 32, 20), array(58, 35, 24, 15)), + array(array(187, 114, 78, 48), array(149, 90, 62, 38), array(111, 67, 46, 28), array(82, 50, 34, 21)), + array(array(255, 154, 106, 65), array(202, 122, 84, 52), array(144, 87, 60, 37), array(106, 64, 44, 27)), + array(array(322, 195, 134, 82), array(255, 154, 106, 65), array(178, 108, 74, 45), array(139, 84, 58, 36)), + array(array(370, 224, 154, 95), array(293, 178, 122, 75), array(207, 125, 86, 53), array(154, 93, 64, 39)), + array(array(461, 279, 192, 118), array(365, 221, 152, 93), array(259, 157, 108, 66), array(202, 122, 84, 52)), + array(array(552, 335, 230, 141), array(432, 262, 180, 111), array(312, 189, 130, 80), array(235, 143, 98, 60)), + array(array(652, 395, 271, 167), array(513, 311, 213, 131), array(364, 221, 151, 93), array(288, 174, 119, 74)) + ); + + static array $QR_PATTERN_POSITION_TABLE = array( + array(), + array(6, 18), + array(6, 22), + array(6, 26), + array(6, 30), + array(6, 34), + array(6, 22, 38), + array(6, 24, 42), + array(6, 26, 46), + array(6, 28, 50), + array(6, 30, 54), + array(6, 32, 58), + array(6, 34, 62), + array(6, 26, 46, 66), + array(6, 26, 48, 70), + array(6, 26, 50, 74), + array(6, 30, 54, 78), + array(6, 30, 56, 82), + array(6, 30, 58, 86), + array(6, 34, 62, 90), + array(6, 28, 50, 72, 94), + array(6, 26, 50, 74, 98), + array(6, 30, 54, 78, 102), + array(6, 28, 54, 80, 106), + array(6, 32, 58, 84, 110), + array(6, 30, 58, 86, 114), + array(6, 34, 62, 90, 118), + array(6, 26, 50, 74, 98, 122), + array(6, 30, 54, 78, 102, 126), + array(6, 26, 52, 78, 104, 130), + array(6, 30, 56, 82, 108, 134), + array(6, 34, 60, 86, 112, 138), + array(6, 30, 58, 86, 114, 142), + array(6, 34, 62, 90, 118, 146), + array(6, 30, 54, 78, 102, 126, 150), + array(6, 24, 50, 76, 102, 128, 154), + array(6, 28, 54, 80, 106, 132, 158), + array(6, 32, 58, 84, 110, 136, 162), + array(6, 26, 54, 82, 110, 138, 166), + array(6, 30, 58, 86, 114, 142, 170) + ); + + public static function getPatternPosition(int $typeNumber): array + { + return self::$QR_PATTERN_POSITION_TABLE[$typeNumber - 1]; + } + + public static function getMaxLength(int $typeNumber, int $mode, int $errorCorrectLevel): int + { + + $t = $typeNumber - 1; + $e = 0; + $m = 0; + + switch ($errorCorrectLevel) { + case QR_ERROR_CORRECT_LEVEL_L : + $e = 0; + break; + case QR_ERROR_CORRECT_LEVEL_M : + $e = 1; + break; + case QR_ERROR_CORRECT_LEVEL_Q : + $e = 2; + break; + case QR_ERROR_CORRECT_LEVEL_H : + $e = 3; + break; + default : + trigger_error("e:$errorCorrectLevel", E_USER_ERROR); + } + + switch ($mode) { + case QR_MODE_NUMBER : + $m = 0; + break; + case QR_MODE_ALPHA_NUM : + $m = 1; + break; + case QR_MODE_8BIT_BYTE : + $m = 2; + break; + case QR_MODE_KANJI : + $m = 3; + break; + default : + trigger_error("m:$mode", E_USER_ERROR); + } + + return self::$QR_MAX_LENGTH[$t][$e][$m]; + } + + public static function getErrorCorrectPolynomial($errorCorrectLength): QRPolynomial + { + + $a = new QRPolynomial(array(1)); + + for ($i = 0; $i < $errorCorrectLength; $i++) { + $a = $a->multiply(new QRPolynomial(array(1, QRMath::gexp($i)))); + } + + return $a; + } + + public static function getMask(int $maskPattern, int $i, int $j): bool + { + + switch ($maskPattern) { + + case QR_MASK_PATTERN000 : + return ($i + $j) % 2 == 0; + case QR_MASK_PATTERN001 : + return $i % 2 == 0; + case QR_MASK_PATTERN010 : + return $j % 3 == 0; + case QR_MASK_PATTERN011 : + return ($i + $j) % 3 == 0; + case QR_MASK_PATTERN100 : + return (floor($i / 2) + floor($j / 3)) % 2 == 0; + case QR_MASK_PATTERN101 : + return ($i * $j) % 2 + ($i * $j) % 3 == 0; + case QR_MASK_PATTERN110 : + return (($i * $j) % 2 + ($i * $j) % 3) % 2 == 0; + case QR_MASK_PATTERN111 : + return (($i * $j) % 3 + ($i + $j) % 2) % 2 == 0; + + default : + trigger_error("mask:$maskPattern", E_USER_ERROR); + } + } + + static function getLostPoint(QRCode $qrCode): float + { + + $moduleCount = $qrCode->getModuleCount(); + + $lostPoint = 0; + + + // LEVEL1 + + for ($row = 0; $row < $moduleCount; $row++) { + + for ($col = 0; $col < $moduleCount; $col++) { + + $sameCount = 0; + $dark = $qrCode->isDark($row, $col); + + for ($r = -1; $r <= 1; $r++) { + + if ($row + $r < 0 || $moduleCount <= $row + $r) { + continue; + } + + for ($c = -1; $c <= 1; $c++) { + + if (($col + $c < 0 || $moduleCount <= $col + $c) || ($r == 0 && $c == 0)) { + continue; + } + + if ($dark == $qrCode->isDark($row + $r, $col + $c)) { + $sameCount++; + } + } + } + + if ($sameCount > 5) { + $lostPoint += (3 + $sameCount - 5); + } + } + } + + // LEVEL2 + + for ($row = 0; $row < $moduleCount - 1; $row++) { + for ($col = 0; $col < $moduleCount - 1; $col++) { + $count = 0; + if ($qrCode->isDark($row, $col)) + $count++; + if ($qrCode->isDark($row + 1, $col)) + $count++; + if ($qrCode->isDark($row, $col + 1)) + $count++; + if ($qrCode->isDark($row + 1, $col + 1)) + $count++; + if ($count == 0 || $count == 4) { + $lostPoint += 3; + } + } + } + + // LEVEL3 + + for ($row = 0; $row < $moduleCount; $row++) { + for ($col = 0; $col < $moduleCount - 6; $col++) { + if ($qrCode->isDark($row, $col) + && !$qrCode->isDark($row, $col + 1) + && $qrCode->isDark($row, $col + 2) + && $qrCode->isDark($row, $col + 3) + && $qrCode->isDark($row, $col + 4) + && !$qrCode->isDark($row, $col + 5) + && $qrCode->isDark($row, $col + 6)) { + $lostPoint += 40; + } + } + } + + for ($col = 0; $col < $moduleCount; $col++) { + for ($row = 0; $row < $moduleCount - 6; $row++) { + if ($qrCode->isDark($row, $col) + && !$qrCode->isDark($row + 1, $col) + && $qrCode->isDark($row + 2, $col) + && $qrCode->isDark($row + 3, $col) + && $qrCode->isDark($row + 4, $col) + && !$qrCode->isDark($row + 5, $col) + && $qrCode->isDark($row + 6, $col)) { + $lostPoint += 40; + } + } + } + + // LEVEL4 + + $darkCount = 0; + + for ($col = 0; $col < $moduleCount; $col++) { + for ($row = 0; $row < $moduleCount; $row++) { + if ($qrCode->isDark($row, $col)) { + $darkCount++; + } + } + } + + $ratio = abs(100 * $darkCount / $moduleCount / $moduleCount - 50) / 5; + $lostPoint += $ratio * 10; + + return $lostPoint; + } + + public static function getMode($s): int + { + if (QRUtil::isAlphaNum($s)) { + if (QRUtil::isNumber($s)) { + return QR_MODE_NUMBER; + } + return QR_MODE_ALPHA_NUM; + } else if (QRUtil::isKanji($s)) { + return QR_MODE_KANJI; + } else { + return QR_MODE_8BIT_BYTE; + } + } + + public static function isNumber($s): bool + { + for ($i = 0; $i < strlen($s); $i++) { + $c = ord($s[$i]); + if (!(QRUtil::toCharCode('0') <= $c && $c <= QRUtil::toCharCode('9'))) { + return false; + } + } + return true; + } + + public static function isAlphaNum($s): bool + { + for ($i = 0; $i < strlen($s); $i++) { + $c = ord($s[$i]); + if (!(QRUtil::toCharCode('0') <= $c && $c <= QRUtil::toCharCode('9')) + && !(QRUtil::toCharCode('A') <= $c && $c <= QRUtil::toCharCode('Z')) + && strpos(" $%*+-./:", $s[$i]) === false) { + return false; + } + } + return true; + } + + public static function isKanji($s): bool + { + + $data = $s; + + $i = 0; + + while ($i + 1 < strlen($data)) { + + $c = ((0xff & ord($data[$i])) << 8) | (0xff & ord($data[$i + 1])); + + if (!(0x8140 <= $c && $c <= 0x9FFC) && !(0xE040 <= $c && $c <= 0xEBBF)) { + return false; + } + + $i += 2; + } + + if ($i < strlen($data)) { + return false; + } + + return true; + } + + public static function toCharCode($s): int + { + return ord($s[0]); + } + + public static function getBCHTypeInfo(int $data): int + { + $d = $data << 10; + while (QRUtil::getBCHDigit($d) - QRUtil::getBCHDigit(QR_G15) >= 0) { + $d ^= (QR_G15 << (QRUtil::getBCHDigit($d) - QRUtil::getBCHDigit(QR_G15))); + } + return (($data << 10) | $d) ^ QR_G15_MASK; + } + + public static function getBCHTypeNumber(int $data): int + { + $d = $data << 12; + while (QRUtil::getBCHDigit($d) - QRUtil::getBCHDigit(QR_G18) >= 0) { + $d ^= (QR_G18 << (QRUtil::getBCHDigit($d) - QRUtil::getBCHDigit(QR_G18))); + } + return ($data << 12) | $d; + } + + public static function getBCHDigit(int $data): int + { + + $digit = 0; + + while ($data != 0) { + $digit++; + $data >>= 1; + } + + return $digit; + } +} + +//--------------------------------------------------------------- +// QRRSBlock +//--------------------------------------------------------------- + +class QRRSBlock +{ + + private int $totalCount; + private int $dataCount; + + private static array $QR_RS_BLOCK_TABLE = array( + + // L + // M + // Q + // H + + // 1 + array(1, 26, 19), + array(1, 26, 16), + array(1, 26, 13), + array(1, 26, 9), + + // 2 + array(1, 44, 34), + array(1, 44, 28), + array(1, 44, 22), + array(1, 44, 16), + + // 3 + array(1, 70, 55), + array(1, 70, 44), + array(2, 35, 17), + array(2, 35, 13), + + // 4 + array(1, 100, 80), + array(2, 50, 32), + array(2, 50, 24), + array(4, 25, 9), + + // 5 + array(1, 134, 108), + array(2, 67, 43), + array(2, 33, 15, 2, 34, 16), + array(2, 33, 11, 2, 34, 12), + + // 6 + array(2, 86, 68), + array(4, 43, 27), + array(4, 43, 19), + array(4, 43, 15), + + // 7 + array(2, 98, 78), + array(4, 49, 31), + array(2, 32, 14, 4, 33, 15), + array(4, 39, 13, 1, 40, 14), + + // 8 + array(2, 121, 97), + array(2, 60, 38, 2, 61, 39), + array(4, 40, 18, 2, 41, 19), + array(4, 40, 14, 2, 41, 15), + + // 9 + array(2, 146, 116), + array(3, 58, 36, 2, 59, 37), + array(4, 36, 16, 4, 37, 17), + array(4, 36, 12, 4, 37, 13), + + // 10 + array(2, 86, 68, 2, 87, 69), + array(4, 69, 43, 1, 70, 44), + array(6, 43, 19, 2, 44, 20), + array(6, 43, 15, 2, 44, 16), + + // 11 + array(4, 101, 81), + array(1, 80, 50, 4, 81, 51), + array(4, 50, 22, 4, 51, 23), + array(3, 36, 12, 8, 37, 13), + + // 12 + array(2, 116, 92, 2, 117, 93), + array(6, 58, 36, 2, 59, 37), + array(4, 46, 20, 6, 47, 21), + array(7, 42, 14, 4, 43, 15), + + // 13 + array(4, 133, 107), + array(8, 59, 37, 1, 60, 38), + array(8, 44, 20, 4, 45, 21), + array(12, 33, 11, 4, 34, 12), + + // 14 + array(3, 145, 115, 1, 146, 116), + array(4, 64, 40, 5, 65, 41), + array(11, 36, 16, 5, 37, 17), + array(11, 36, 12, 5, 37, 13), + + // 15 + array(5, 109, 87, 1, 110, 88), + array(5, 65, 41, 5, 66, 42), + array(5, 54, 24, 7, 55, 25), + array(11, 36, 12, 7, 37, 13), + + // 16 + array(5, 122, 98, 1, 123, 99), + array(7, 73, 45, 3, 74, 46), + array(15, 43, 19, 2, 44, 20), + array(3, 45, 15, 13, 46, 16), + + // 17 + array(1, 135, 107, 5, 136, 108), + array(10, 74, 46, 1, 75, 47), + array(1, 50, 22, 15, 51, 23), + array(2, 42, 14, 17, 43, 15), + + // 18 + array(5, 150, 120, 1, 151, 121), + array(9, 69, 43, 4, 70, 44), + array(17, 50, 22, 1, 51, 23), + array(2, 42, 14, 19, 43, 15), + + // 19 + array(3, 141, 113, 4, 142, 114), + array(3, 70, 44, 11, 71, 45), + array(17, 47, 21, 4, 48, 22), + array(9, 39, 13, 16, 40, 14), + + // 20 + array(3, 135, 107, 5, 136, 108), + array(3, 67, 41, 13, 68, 42), + array(15, 54, 24, 5, 55, 25), + array(15, 43, 15, 10, 44, 16), + + // 21 + array(4, 144, 116, 4, 145, 117), + array(17, 68, 42), + array(17, 50, 22, 6, 51, 23), + array(19, 46, 16, 6, 47, 17), + + // 22 + array(2, 139, 111, 7, 140, 112), + array(17, 74, 46), + array(7, 54, 24, 16, 55, 25), + array(34, 37, 13), + + // 23 + array(4, 151, 121, 5, 152, 122), + array(4, 75, 47, 14, 76, 48), + array(11, 54, 24, 14, 55, 25), + array(16, 45, 15, 14, 46, 16), + + // 24 + array(6, 147, 117, 4, 148, 118), + array(6, 73, 45, 14, 74, 46), + array(11, 54, 24, 16, 55, 25), + array(30, 46, 16, 2, 47, 17), + + // 25 + array(8, 132, 106, 4, 133, 107), + array(8, 75, 47, 13, 76, 48), + array(7, 54, 24, 22, 55, 25), + array(22, 45, 15, 13, 46, 16), + + // 26 + array(10, 142, 114, 2, 143, 115), + array(19, 74, 46, 4, 75, 47), + array(28, 50, 22, 6, 51, 23), + array(33, 46, 16, 4, 47, 17), + + // 27 + array(8, 152, 122, 4, 153, 123), + array(22, 73, 45, 3, 74, 46), + array(8, 53, 23, 26, 54, 24), + array(12, 45, 15, 28, 46, 16), + + // 28 + array(3, 147, 117, 10, 148, 118), + array(3, 73, 45, 23, 74, 46), + array(4, 54, 24, 31, 55, 25), + array(11, 45, 15, 31, 46, 16), + + // 29 + array(7, 146, 116, 7, 147, 117), + array(21, 73, 45, 7, 74, 46), + array(1, 53, 23, 37, 54, 24), + array(19, 45, 15, 26, 46, 16), + + // 30 + array(5, 145, 115, 10, 146, 116), + array(19, 75, 47, 10, 76, 48), + array(15, 54, 24, 25, 55, 25), + array(23, 45, 15, 25, 46, 16), + + // 31 + array(13, 145, 115, 3, 146, 116), + array(2, 74, 46, 29, 75, 47), + array(42, 54, 24, 1, 55, 25), + array(23, 45, 15, 28, 46, 16), + + // 32 + array(17, 145, 115), + array(10, 74, 46, 23, 75, 47), + array(10, 54, 24, 35, 55, 25), + array(19, 45, 15, 35, 46, 16), + + // 33 + array(17, 145, 115, 1, 146, 116), + array(14, 74, 46, 21, 75, 47), + array(29, 54, 24, 19, 55, 25), + array(11, 45, 15, 46, 46, 16), + + // 34 + array(13, 145, 115, 6, 146, 116), + array(14, 74, 46, 23, 75, 47), + array(44, 54, 24, 7, 55, 25), + array(59, 46, 16, 1, 47, 17), + + // 35 + array(12, 151, 121, 7, 152, 122), + array(12, 75, 47, 26, 76, 48), + array(39, 54, 24, 14, 55, 25), + array(22, 45, 15, 41, 46, 16), + + // 36 + array(6, 151, 121, 14, 152, 122), + array(6, 75, 47, 34, 76, 48), + array(46, 54, 24, 10, 55, 25), + array(2, 45, 15, 64, 46, 16), + + // 37 + array(17, 152, 122, 4, 153, 123), + array(29, 74, 46, 14, 75, 47), + array(49, 54, 24, 10, 55, 25), + array(24, 45, 15, 46, 46, 16), + + // 38 + array(4, 152, 122, 18, 153, 123), + array(13, 74, 46, 32, 75, 47), + array(48, 54, 24, 14, 55, 25), + array(42, 45, 15, 32, 46, 16), + + // 39 + array(20, 147, 117, 4, 148, 118), + array(40, 75, 47, 7, 76, 48), + array(43, 54, 24, 22, 55, 25), + array(10, 45, 15, 67, 46, 16), + + // 40 + array(19, 148, 118, 6, 149, 119), + array(18, 75, 47, 31, 76, 48), + array(34, 54, 24, 34, 55, 25), + array(20, 45, 15, 61, 46, 16) + + ); + + public function __construct(int $totalCount, int $dataCount) + { + $this->totalCount = $totalCount; + $this->dataCount = $dataCount; + } + + public function getDataCount(): int + { + return $this->dataCount; + } + + public function getTotalCount(): int + { + return $this->totalCount; + } + + public static function getRSBlocks(int $typeNumber, int $errorCorrectLevel): array + { + + $rsBlock = QRRSBlock::getRsBlockTable($typeNumber, $errorCorrectLevel); + $length = count($rsBlock) / 3; + + $list = array(); + + for ($i = 0; $i < $length; $i++) { + + $count = $rsBlock[$i * 3 + 0]; + $totalCount = $rsBlock[$i * 3 + 1]; + $dataCount = $rsBlock[$i * 3 + 2]; + + for ($j = 0; $j < $count; $j++) { + $list[] = new QRRSBlock($totalCount, $dataCount); + } + } + + return $list; + } + + public static function getRsBlockTable(int $typeNumber, int $errorCorrectLevel): array + { + + switch ($errorCorrectLevel) { + case QR_ERROR_CORRECT_LEVEL_L : + return self::$QR_RS_BLOCK_TABLE[($typeNumber - 1) * 4 + 0]; + case QR_ERROR_CORRECT_LEVEL_M : + return self::$QR_RS_BLOCK_TABLE[($typeNumber - 1) * 4 + 1]; + case QR_ERROR_CORRECT_LEVEL_Q : + return self::$QR_RS_BLOCK_TABLE[($typeNumber - 1) * 4 + 2]; + case QR_ERROR_CORRECT_LEVEL_H : + return self::$QR_RS_BLOCK_TABLE[($typeNumber - 1) * 4 + 3]; + default : + trigger_error("tn:$typeNumber/ecl:$errorCorrectLevel", E_USER_ERROR); + } + } +} + +//--------------------------------------------------------------- +// QRNumber +//--------------------------------------------------------------- + +class QRNumber extends QRData +{ + + public function __construct($data) + { + parent::__construct(QR_MODE_NUMBER, $data); + } + + function write(QRBitBuffer $buffer): void + { + + $data = $this->getData(); + + $i = 0; + + while ($i + 2 < strlen($data)) { + $num = QRNumber::parseInt(substr($data, $i, 3)); + $buffer->put($num, 10); + $i += 3; + } + + if ($i < strlen($data)) { + + if (strlen($data) - $i == 1) { + $num = QRNumber::parseInt(substr($data, $i, $i + 1)); + $buffer->put($num, 4); + } else if (strlen($data) - $i == 2) { + $num = QRNumber::parseInt(substr($data, $i, $i + 2)); + $buffer->put($num, 7); + } + } + } + + private static function parseInt(string $s): int + { + + $num = 0; + for ($i = 0; $i < strlen($s); $i++) { + $num = $num * 10 + QRNumber::parseIntAt(ord($s[$i])); + } + return $num; + } + + private static function parseIntAt(int $c): int + { + + if (QRUtil::toCharCode('0') <= $c && $c <= QRUtil::toCharCode('9')) { + return $c - QRUtil::toCharCode('0'); + } + + trigger_error("illegal char : $c", E_USER_ERROR); + } +} + +//--------------------------------------------------------------- +// QRKanji +//--------------------------------------------------------------- + +class QRKanji extends QRData +{ + + public function __construct($data) + { + parent::__construct(QR_MODE_KANJI, $data); + } + + public function write(QRBitBuffer $buffer): void + { + + $data = $this->getData(); + + $i = 0; + + while ($i + 1 < strlen($data)) { + + $c = ((0xff & ord($data[$i])) << 8) | (0xff & ord($data[$i + 1])); + + if (0x8140 <= $c && $c <= 0x9FFC) { + $c -= 0x8140; + } else if (0xE040 <= $c && $c <= 0xEBBF) { + $c -= 0xC140; + } else { + trigger_error("illegal char at " . ($i + 1) . "/$c", E_USER_ERROR); + } + + $c = (($c >> 8) & 0xff) * 0xC0 + ($c & 0xff); + + $buffer->put($c, 13); + + $i += 2; + } + + if ($i < strlen($data)) { + trigger_error("illegal char at " . ($i + 1), E_USER_ERROR); + } + } + + public function getLength(): int + { + return floor(strlen($this->getData()) / 2); + } +} + +//--------------------------------------------------------------- +// QRAlphaNum +//--------------------------------------------------------------- + +class QRAlphaNum extends QRData +{ + + function __construct($data) + { + parent::__construct(QR_MODE_ALPHA_NUM, $data); + } + + public function write(QRBitBuffer $buffer): void + { + + $i = 0; + $c = $this->getData(); + + while ($i + 1 < strlen($c)) { + $buffer->put(QRAlphaNum::getCode(ord($c[$i])) * 45 + + QRAlphaNum::getCode(ord($c[$i + 1])), 11); + $i += 2; + } + + if ($i < strlen($c)) { + $buffer->put(QRAlphaNum::getCode(ord($c[$i])), 6); + } + } + + private static function getCode(int $c): int + { + + if (QRUtil::toCharCode('0') <= $c + && $c <= QRUtil::toCharCode('9')) { + return $c - QRUtil::toCharCode('0'); + } else if (QRUtil::toCharCode('A') <= $c + && $c <= QRUtil::toCharCode('Z')) { + return $c - QRUtil::toCharCode('A') + 10; + } else { + switch ($c) { + case QRUtil::toCharCode(' ') : + return 36; + case QRUtil::toCharCode('$') : + return 37; + case QRUtil::toCharCode('%') : + return 38; + case QRUtil::toCharCode('*') : + return 39; + case QRUtil::toCharCode('+') : + return 40; + case QRUtil::toCharCode('-') : + return 41; + case QRUtil::toCharCode('.') : + return 42; + case QRUtil::toCharCode('/') : + return 43; + case QRUtil::toCharCode(':') : + return 44; + default : + trigger_error("illegal char : $c", E_USER_ERROR); + } + } + + } +} + +//--------------------------------------------------------------- +// QR8BitByte +//--------------------------------------------------------------- + +class QR8BitByte extends QRData +{ + + function __construct($data) + { + parent::__construct(QR_MODE_8BIT_BYTE, $data); + } + + public function write(QRBitBuffer $buffer): void + { + + $data = $this->getData(); + for ($i = 0; $i < strlen($data); $i++) { + $buffer->put(ord($data[$i]), 8); + } + } + +} + +//--------------------------------------------------------------- +// QRData +//--------------------------------------------------------------- + +abstract class QRData +{ + + private int $mode; + + private string $data; + + function __construct(int $mode, string $data) + { + $this->mode = $mode; + $this->data = $data; + } + + public function getMode(): int + { + return $this->mode; + } + + public function getData(): string + { + return $this->data; + } + + public function getLength(): int + { + return strlen($this->getData()); + } + + public abstract function write(QRBitBuffer $buffer): void; + + public function getLengthInBits(int $type): int + { + + if (1 <= $type && $type < 10) { + + // 1 - 9 + + switch ($this->mode) { + case QR_MODE_NUMBER : + return 10; + case QR_MODE_ALPHA_NUM : + return 9; + case QR_MODE_8BIT_BYTE : + return 8; + case QR_MODE_KANJI : + return 8; + default : + trigger_error("mode:$this->mode", E_USER_ERROR); + } + + } else if ($type < 27) { + + // 10 - 26 + + switch ($this->mode) { + case QR_MODE_NUMBER : + return 12; + case QR_MODE_ALPHA_NUM : + return 11; + case QR_MODE_8BIT_BYTE : + return 16; + case QR_MODE_KANJI : + return 10; + default : + trigger_error("mode:$this->mode", E_USER_ERROR); + } + + } else if ($type < 41) { + + // 27 - 40 + + switch ($this->mode) { + case QR_MODE_NUMBER : + return 14; + case QR_MODE_ALPHA_NUM : + return 13; + case QR_MODE_8BIT_BYTE : + return 16; + case QR_MODE_KANJI : + return 12; + default : + trigger_error("mode:$this->mode", E_USER_ERROR); + } + + } else { + trigger_error("mode:$this->mode", E_USER_ERROR); + } + } + +} + +//--------------------------------------------------------------- +// QRMath +//--------------------------------------------------------------- + +class QRMath +{ + + private static array $QR_MATH_EXP_TABLE; + private static array $QR_MATH_LOG_TABLE; + + static function init(): void + { + + self::$QR_MATH_EXP_TABLE = QRMath::createNumArray(256); + + for ($i = 0; $i < 8; $i++) { + self::$QR_MATH_EXP_TABLE[$i] = 1 << $i; + } + + for ($i = 8; $i < 256; $i++) { + self::$QR_MATH_EXP_TABLE[$i] = self::$QR_MATH_EXP_TABLE[$i - 4] + ^ self::$QR_MATH_EXP_TABLE[$i - 5] + ^ self::$QR_MATH_EXP_TABLE[$i - 6] + ^ self::$QR_MATH_EXP_TABLE[$i - 8]; + } + + self::$QR_MATH_LOG_TABLE = QRMath::createNumArray(256); + + for ($i = 0; $i < 255; $i++) { + self::$QR_MATH_LOG_TABLE[self::$QR_MATH_EXP_TABLE[$i]] = $i; + } + } + + public static function createNumArray(int $length): array + { + $num_array = array(); + for ($i = 0; $i < $length; $i++) { + $num_array[] = 0; + } + return $num_array; + } + + public static function glog(int $n): int + { + + if ($n < 1) { + trigger_error("log($n)", E_USER_ERROR); + } + + return self::$QR_MATH_LOG_TABLE[$n]; + } + + static function gexp(int $n): int + { + + while ($n < 0) { + $n += 255; + } + + while ($n >= 256) { + $n -= 255; + } + + return self::$QR_MATH_EXP_TABLE[$n]; + } +} + +// init static table +QRMath::init(); + +//--------------------------------------------------------------- +// QRPolynomial +//--------------------------------------------------------------- + +class QRPolynomial +{ + + private array $num; + + public function __construct(array $num, int $shift = 0) + { + + $offset = 0; + + while ($offset < count($num) && $num[$offset] == 0) { + $offset++; + } + + $this->num = QRMath::createNumArray(count($num) - $offset + $shift); + for ($i = 0; $i < count($num) - $offset; $i++) { + $this->num[$i] = $num[$i + $offset]; + } + } + + public function get(int $index): int + { + return $this->num[$index]; + } + + public function getLength(): int + { + return count($this->num); + } + + public function __toString(): string + { + return $this->toString(); + } + + public function toString(): string + { + + $buffer = ""; + + for ($i = 0; $i < $this->getLength(); $i++) { + if ($i > 0) { + $buffer .= ","; + } + $buffer .= $this->get($i); + } + + return $buffer; + } + + public function toLogString(): string + { + + $buffer = ""; + + for ($i = 0; $i < $this->getLength(); $i++) { + if ($i > 0) { + $buffer .= ","; + } + $buffer .= QRMath::glog($this->get($i)); + } + + return $buffer; + } + + public function multiply(QRPolynomial $e): QRPolynomial + { + + $num = QRMath::createNumArray($this->getLength() + $e->getLength() - 1); + + for ($i = 0; $i < $this->getLength(); $i++) { + $vi = QRMath::glog($this->get($i)); + + for ($j = 0; $j < $e->getLength(); $j++) { + $num[$i + $j] ^= QRMath::gexp($vi + QRMath::glog($e->get($j))); + } + } + + return new QRPolynomial($num); + } + + function mod(QRPolynomial $e): QRPolynomial + { + + if ($this->getLength() - $e->getLength() < 0) { + return $this; + } + + $ratio = QRMath::glog($this->get(0)) - QRMath::glog($e->get(0)); + + $num = QRMath::createNumArray($this->getLength()); + for ($i = 0; $i < $this->getLength(); $i++) { + $num[$i] = $this->get($i); + } + + for ($i = 0; $i < $e->getLength(); $i++) { + $num[$i] ^= QRMath::gexp(QRMath::glog($e->get($i)) + $ratio); + } + + $newPolynomial = new QRPolynomial($num); + return $newPolynomial->mod($e); + } +} + + +//--------------------------------------------------------------- +// QRBitBuffer +//--------------------------------------------------------------- + +class QRBitBuffer +{ + + private array $buffer; + private int $length; + + public function __construct() + { + $this->buffer = array(); + $this->length = 0; + } + + public function getBuffer(): array + { + return $this->buffer; + } + + public function getLengthInBits(): int + { + return $this->length; + } + + public function __toString(): string + { + $buffer = ""; + for ($i = 0; $i < $this->getLengthInBits(); $i++) { + $buffer .= $this->get($i) ? '1' : '0'; + } + return $buffer; + } + + public function get(int $index): bool + { + $bufIndex = (int)floor($index / 8); + return (($this->buffer[$bufIndex] >> (7 - $index % 8)) & 1) === 1; + } + + public function put(int $num, int $length): void + { + + for ($i = 0; $i < $length; $i++) { + $this->putBit((($num >> ($length - $i - 1)) & 1) == 1); + } + } + + public function putBit(bool $bit): void + { + + $bufIndex = (int)floor($this->length / 8); + if (count($this->buffer) <= $bufIndex) { + $this->buffer[] = 0; + } + + if ($bit) { + $this->buffer[$bufIndex] |= (0x80 >> ($this->length % 8)); + } + + $this->length++; + } +}
\ No newline at end of file diff --git a/inc/shibauth.inc.php b/inc/shibauth.inc.php index 6ae3a89..d0e7800 100644 --- a/inc/shibauth.inc.php +++ b/inc/shibauth.inc.php @@ -3,12 +3,8 @@ class ShibAuth { - /** - * Log user into master-server using the data provided by the current shibboleth session - * @param ?string $accessCode optional one-time access code to retreive session data via thrift - * @return array{status: string, firstName: string, lastName: string, mail: string, token: string, sessionId: string, userId: string, organizationId: string, url: string, error: string} - */ - private static function loginInternal(?string $accessCode = null): array + + private static function loginInternal(?string $accessCode, int $sessionTimeout): array { if ($accessCode !== null) { $entrop = strlen(count_chars($accessCode, 3)); @@ -34,15 +30,8 @@ class ShibAuth } } // Figure out role - if (strpos(";{$_SERVER['entitlement']};", CONFIG_ENTITLEMENT) !== false) { - $role = 'TUTOR'; - } else if (strpos(";{$_SERVER[CONFIG_SCOPED_AFFILIATION]};", ';employee@') !== false - || strpos(";{$_SERVER[CONFIG_SCOPED_AFFILIATION]};", ';staff@') !== false - || strpos(";{$_SERVER[CONFIG_SCOPED_AFFILIATION]};", ';faculty@') !== false) { - $role = 'TUTOR'; - } else { - file_put_contents('/tmp/shib-student-' . time() . '-' . $_SERVER['REMOTE_ADDR'] . '.txt', print_r($_SERVER, true)); - $role = 'STUDENT'; + $role = Util::getRole('shibauth-login'); + if ($role === 'STUDENT') { // NEW: Ignore students for now return [ 'status' => 'error', @@ -146,6 +135,7 @@ class ShibAuth if ($accessCode !== null) { $rpc['accessCode'] = $accessCode; } + $rpc['timeoutSeconds'] = $sessionTimeout; $reply = RPC::submit($rpc); if (preg_match('/^TOKEN:(\w+) SESSIONID:(\w+)$/', $reply, $out)) { // For talking to the sat server, also referred to as userToken in Java @@ -172,9 +162,15 @@ class ShibAuth return $response; } - public static function login(?string $accessCode = null): array + /** + * Log user into master-server using the data provided by the current shibboleth session + * @param string $accessCode one-time access code to retreive session data via thrift + * @param int $sessionTimeout how long the created session stays valid on master server + * @return array{status: string, firstName: string, lastName: string, mail: string, token: string, sessionId: string, userId: string, organizationId: string, url: string, error: string} + */ + public static function login(?string $accessCode = null, int $sessionTimeout = 0): array { - $res = self::loginInternal($accessCode); + $res = self::loginInternal($accessCode, $sessionTimeout); if ($res['status'] !== 'ok' && isset($res['error']) && $accessCode !== null) { RPC::submit(['status' => 'error', 'error' => $res['error'], 'accessCode' => $accessCode]); } @@ -199,4 +195,4 @@ class ShibAuth return $sat2; } -}
\ No newline at end of file +} diff --git a/inc/user.inc.php b/inc/user.inc.php index 9520640..84e517d 100644 --- a/inc/user.inc.php +++ b/inc/user.inc.php @@ -161,6 +161,7 @@ class User . ' an den {{1}}-SP zu übermitteln. Bitte wenden Sie sich an den Support.', CONFIG_IDM, CONFIG_SUITE); } Session::delete(); + file_put_contents('/tmp/shib-load-' . time() . '-' . $_SERVER['REMOTE_ADDR'] . '.txt', print_r($_SERVER, true)); return false; } // Try user from local DB @@ -209,14 +210,7 @@ class User 'email' => $_SERVER['mail'], ); // Figure out whether the user should be considered a tutor - if (isset($_SERVER[CONFIG_SCOPED_AFFILIATION]) && (strpos(";{$_SERVER[CONFIG_SCOPED_AFFILIATION]}", ';employee@') !== false - || strpos(";{$_SERVER[CONFIG_SCOPED_AFFILIATION]}", ';staff@') !== false - || strpos(";{$_SERVER[CONFIG_SCOPED_AFFILIATION]}", ';faculty@') !== false)) - self::$user['role'] = 'TUTOR'; - elseif (isset($_SERVER['entitlement']) && strpos(";{$_SERVER['entitlement']};", CONFIG_ENTITLEMENT) !== false) - self::$user['role'] = 'TUTOR'; - else - self::$user['role'] = 'STUDENT'; + self::$user['role'] = Util::getRole(); // Try to figure out organization if (isset($_SERVER[CONFIG_EPPN]) && preg_match('/@([0-9a-zA-Z\-._]+)$/', $_SERVER[CONFIG_EPPN], $out)) { self::$user['organization'] = $out[1]; diff --git a/inc/util.inc.php b/inc/util.inc.php index 0e06e6a..4904588 100644 --- a/inc/util.inc.php +++ b/inc/util.inc.php @@ -286,4 +286,22 @@ SADFACE; exit(0); } + public static function getRole(?string $debugKey = null): string + { + if (strpos(";{$_SERVER['entitlement']};", CONFIG_ENTITLEMENT) !== false) { + $role = 'TUTOR'; + } else if (strpos(";{$_SERVER[CONFIG_SCOPED_AFFILIATION]};", ';employee@') !== false + || strpos(";{$_SERVER[CONFIG_SCOPED_AFFILIATION]};", ';staff@') !== false + || strpos(";{$_SERVER[CONFIG_SCOPED_AFFILIATION]};", ';faculty@') !== false) { + $role = 'TUTOR'; + } else { + if ($debugKey !== null) { + file_put_contents('/tmp/shib-student-' . $debugKey . '-' + . time() . '-' . $_SERVER['REMOTE_ADDR'] . '.txt', print_r($_SERVER, true)); + } + $role = 'STUDENT'; + } + return $role; + } + } diff --git a/modules/register.inc.php b/modules/register.inc.php index 98e8763..c4322f3 100644 --- a/modules/register.inc.php +++ b/modules/register.inc.php @@ -13,13 +13,10 @@ class Page_Register extends Page if (!User::isShibbolethAuth()) Util::redirect(CONFIG_PREFIX . 'shib/?do=Main'); - if (!User::isTutor()) { - return; - } - if (User::getOrganization() === false) { Message::addError('Ihre Einrichtung {{0}} scheint kein {{1}} zu unterstützen. Bitte wenden Sie sich an den Support.', User::getOrganizationId(), CONFIG_IDM); + file_put_contents('/tmp/shib-reg-noorg-' . time() . '-' . $_SERVER['REMOTE_ADDR'] . '.txt', print_r($_SERVER, true)); Util::redirect('?do=Main'); } @@ -63,10 +60,13 @@ class Page_Register extends Page if (!User::isTutor()) { Message::addError('Sie sind weder Mitglied einer Gruppe, die als Zugriffsberechtigt eingestuft wird, noch tragen Sie das {{0}}-Entitlement." . " Bitte kontaktieren Sie Ihren lokalen {{0}}-Support.', CONFIG_SUITE); + file_put_contents('/tmp/shib-reg-tut-' . time() . '-' . $_SERVER['REMOTE_ADDR'] . '.txt', print_r($_SERVER, true)); } elseif (empty(User::getMail())) { Message::addError('Ihr Identity Provider hat keine E-Mail-Adresse zu Ihrem Account geliefert. Registrierung nicht möglich.'); + file_put_contents('/tmp/shib-reg-mail-' . time() . '-' . $_SERVER['REMOTE_ADDR'] . '.txt', print_r($_SERVER, true)); } elseif (!User::hasFullName()) { Message::addError('Ihr Identity Provider hat keinen Namen zu Ihrem Account geliefert. Registrierung nicht möglich.'); + file_put_contents('/tmp/shib-reg-name-' . time() . '-' . $_SERVER['REMOTE_ADDR'] . '.txt', print_r($_SERVER, true)); // Put stuff in DB } elseif (User::deploy($anonymous, $testLogin)) { Message::addSuccess('Ihr Konto wurde freigeschaltet'); @@ -13,6 +13,48 @@ require_once 'config.php'; $action = Request::any('action'); // +// Even newer version - QR code based +// +if ($action === 'qrgen') { + // Generate new QR code + $token = Request::get('token'); + if (strlen($token) !== 16) { + http_response_code(400); + die('Wrong token length'); + } + Database::exec("DELETE FROM client_token WHERE dateline < UNIX_TIMESTAMP() - 300"); + $ret = Database::exec("INSERT INTO client_token (username, token, dateline, qrtoken) + VALUES ('', '', UNIX_TIMESTAMP(), :token)", ['token' => $token], true); + if ($ret === false) { + http_response_code(400); + die('Token already in use'); + } + $code = QRCode::getMinimumQRCode('https://' . CONFIG_FORCE_DOMAIN . '/?qr=' . $token, QR_ERROR_CORRECT_LEVEL_L); + Header('Content-Type: image/svg+xml; charset=utf-8'); + $code->printSVG(16); + exit; +} +if ($action === 'qrpoll') { + $token = Request::get('token'); + $ret = Database::queryFirst("SELECT username, token, dmsdsession FROM client_token WHERE qrtoken = :qrtoken LIMIT 1", + ['qrtoken' => $token]); + if ($ret === false) { + http_response_code(404); + exit; + } + if ($ret['username'] === '') { + http_response_code(204); + exit; + } + // Successful, send reply to lightdm + $retval = $ret['username'] . "\n" . $ret['token']; + if (!empty($ret['dmsdsession'])) { + $retval .= "\n" . $ret['dmsdsession']; + } + die ($retval); +} + +// // New version - browser based // if ($action === 'browser') { @@ -29,6 +71,8 @@ if ($action === 'verify') { if ($row === false) { die("ERROR=Invalid token"); } + Database::exec("DELETE FROM client_token WHERE token = :token LIMIT 1", + ['token' => (string)Request::any('token')]); die("USER={$row['username']}"); } diff --git a/shib/api.php b/shib/api.php index eec1e3d..a05c301 100644 --- a/shib/api.php +++ b/shib/api.php @@ -19,6 +19,7 @@ spl_autoload_register(function ($class) require_once $file; }); +// DMSD login via web $response = ShibAuth::login(); Header('Content-Type: text/plain; charset=utf-8'); diff --git a/shib/client_auth.php b/shib/client_auth.php new file mode 100644 index 0000000..0af34de --- /dev/null +++ b/shib/client_auth.php @@ -0,0 +1,142 @@ +<?php + +use JetBrains\PhpStorm\NoReturn; + +chdir('..'); + +require_once 'config.php'; + +/* +Header('Content-Type: text/plain; charset=utf-8'); +die( json_encode($_SERVER, JSON_PRETTY_PRINT) ); + +// */ + +// Autoload classes from ./inc which adhere to naming scheme <lowercasename>.inc.php +spl_autoload_register(function ($class) { + $file = 'inc/' . preg_replace('/[^a-z0-9]/', '', mb_strtolower($class)) . '.inc.php'; + if (!file_exists($file)) + return; + require_once $file; +}); + +#[NoReturn] +function error(string $msg): void +{ + die('<!doctype html><html lang="en"><body><div id="bwlp-error">' . htmlspecialchars($msg) . '</div></body></html>'); +} + + + +Header('X-Frame-Options: DENY'); +Header("Content-Security-Policy: frame-ancestors 'none';"); +Header('Content-Type: text/html; charset=utf-8'); + +$token = Request::get('token'); +$qrtoken = !empty(Request::get('qr')); +if (strlen($token) <= 8) { + error('Unknown request'); +} + +// Got a token + +$user = ''; +// Get username from eppn is preferred +if (isset($_SERVER[CONFIG_EPPN])) { + $user = preg_replace('/[^a-z0-9_@\-.]/', '', strtolower($_SERVER[CONFIG_EPPN])); + if (!empty($user) && is_numeric($user[0])) { + $user = 'x' . $user; // First character must not be a digit... + } +} +if (empty($user)) { + // No user - generate random username from persistent-id + $user = 'x' . substr(md5($_SERVER['persistent-id']), 0, 6); +} +if (isset($_SERVER[CONFIG_SCOPED_AFFILIATION]) && !str_contains($user, '@') && preg_match('/@[^@;\s]+/', $_SERVER[CONFIG_SCOPED_AFFILIATION], $out)) { + // We do not have an affiliation in the form of @something.com - try to extract from scoped affiliation + $user .= $out[0]; +} +if (!str_contains($user, '@')) { + // *shrug* + $user .= '@unknown'; +} +if (strlen($user) > 31) { + $user = substr($user, 0, 31); +} +/* +Database::exec("CREATE TABLE IF NOT EXISTS client_token ( + username varchar(128) not null, + token char(32) not null, + dateline int(10) unsigned not null, + KEY (username), + KEY (token), + KEY (dateline) +) DEFAULT CHARSET=ascii COLLATE=ascii_general_ci"); + */ +// Prevent too many attempts (max 20 in 5 mins) +Database::exec("DELETE FROM client_token WHERE dateline < UNIX_TIMESTAMP() - 300"); +$num = Database::queryFirst("SELECT Count(*) AS num FROM client_token WHERE username = :user", ['user' => $user]); +if ($num['num'] > 200) { + error('DoS detected'); +} + +$token2 = substr(md5(mt_rand() . ',' . microtime(true) . ',' . posix_getpid() . ',' + . $_SERVER['REMOTE_ADDR'] . $_SERVER['REMOTE_PORT']), 0, 10); + +// For edit mode via CoW +$dmsd = null; +if (Util::getRole() === 'TUTOR') { + $shibSess = ShibAuth::login(null, 300); + if (!empty($shibSess['token'])) { + $dmsd = $shibSess['token']; + } +} + +// See if valid QR code request +if ($qrtoken) { + $f = Database::queryFirst("SELECT username FROM client_token WHERE qrtoken = :token LIMIT 1", ['token' => $token]); + if ($f === false) { + error('Unknown QR code login token'); + } + if ($f['username'] !== '') { + error('Token already used'); + } + $num = Database::exec("UPDATE client_token SET username = :user, token = :pass, dmsdsession = :dmsd + WHERE username = '' AND qrtoken = :token", + ['user' => $user, 'pass' => $token . $token2, 'token' => $token, 'dmsd' => $dmsd]); + if ($num !== 1) { + error('Token already used'); + } + error('Login erfolgreich. Sie können diese Seite jetzt schließen.'); +} + +// Non-QRCode +Database::exec("INSERT INTO client_token (username, token, dmsdsession, dateline) + VALUES (:user, :pass, :dmsd, UNIX_TIMESTAMP())", + ['user' => $user, 'pass' => $token . $token2, 'dmsd' => $dmsd]); +$hash = md5($token); + +if (!empty($dmsd)) { + $dmsd = "<div id=\"bwlp-cow-token\">$dmsd</div>"; +} + +echo <<<BLUBB +<!doctype html> +<html lang="en"> +<body> + <div style="display:none"> + <div id="bwlp-username">$user</div> + <div id="bwlp-password">$token2</div> + <div id="bwlp-hash">$hash</div> + $dmsd +BLUBB; +// For debugging +/* +foreach ($_SERVER as $k => $v) { + if (preg_match('/[a-z]/', $k)) { + echo '<div id="' . htmlspecialchars($k) . '">' . htmlspecialchars($v) . '</div>'; + } +} + */ +echo '</div>Please wait...</body></html>'; +exit; |