<?php
/**
* @file classes/doi/DoiGenerator.php
*
* Copyright (c) 2014-2022 Simon Fraser University
* Copyright (c) 2000-2022 John Willinsky
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
*
* @class DoiGenerator
*
* @brief A utility class for generating DOI suffixes using the Cool DOIs pattern, which generates opaque, but easy to read suffixes.
*
* @see https://blog.datacite.org/cool-dois/
*/
namespace PKP\doi;
use Dflydev\Base32\Crockford\Crockford;
class DoiGenerator
{
// 32 by the factor of 6
protected const UPPER_LIMIT = 1073741823;
/**
* Constructs a DOI with an 8-character suffix, using a Crockford Base 32 algorithm
* to generate the suffix.
*
*/
public static function encodeSuffix(): string
{
$number = random_int(1, self::UPPER_LIMIT);
return self::base32EncodeSuffix($number);
}
/**
* Returns the decoded int used to generate the suffix after validating the two-digit checksum
*
* @return int|null Returns null if checksum is invalid
*/
public static function decodeSuffix(string $suffix): ?int
{
$suffix = strtoupper($suffix);
$suffixParts = str_split($suffix, 6);
$encodedString = $suffixParts[0];
$checksum = $suffixParts[1];
$decodedSuffix = Crockford::decode($encodedString);
$isSuffixValid = self::verifySuffixChecksum($decodedSuffix, $checksum);
return $isSuffixValid ? $decodedSuffix : null;
}
/**
* Encodes suffix as 8-digit base32 encoded string where the final two numbers are a checksum.
*
* E.g. DDDDDDYY where 'D' is a base32 encoded character and 'YY' is the checksum.
*
* @param int $number A random number between 1 and 1073741823 (UPPER_LIMIT). Used as seed for encoding suffix.
*/
protected static function base32EncodeSuffix(int $number): string
{
// Initial base32 encoded string (up to 6 characters max)
$encodedNumber = strtolower(Crockford::encode($number));
// Add checksum at end of string, calculated as modulo 97-10 (ISO 7064)
$remainder = self::calculateChecksum($number);
$payload = $encodedNumber . sprintf('%02d', $remainder);
return str_pad($payload, 8, '0', STR_PAD_LEFT);
}
/**
* Verifies the provided checksum was generated from the number provided.
*
* @param int $number The integer decoded form the base 32 suffix. Original number used to generate suffix.
* @param int $checksum The two-digit checksum (last two digits of suffix)
*/
protected static function verifySuffixChecksum(int $number, int $checksum): bool
{
return $checksum === self::calculateChecksum($number);
}
/**
* Checksum calculated as modulo 97-10 (ISO 7064).
*/
protected static function calculateChecksum(int $number): int
{
return 98 - (($number * 100) % 97);
}
}
|