Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
# Passwords, Tokens, API keys
.env

# CakePHP 3

/vendor/*
Expand Down
339 changes: 339 additions & 0 deletions CertificateRetrievingAndCreation.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,339 @@
<?php

require __DIR__ . '/vendor/autoload.php';

use Github\Client;
use Dotenv\Dotenv;

// Load .env variables
$dotenv = Dotenv::createImmutable(__DIR__);
$dotenv->load();

class Set
{
protected $arraySet = [];

public function insert($element): void
{
foreach ($this->arraySet as $listElement) {
if($listElement->toStr() == $element->toStr()) {
return;
}
}
$this->arraySet[] = $element;
}

public function insertArray(array $array): void
{
foreach ($array as $element) {
$this->insert($element);
}
}

public function getArray(): array
{
return $this->arraySet;
}
}

enum CodecheckType
{
case checkNL;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We might want to add different options here without a new release of the plugin, so this cannot be a fixed enum.

Note that you are mixing types and venues here, too.

The best way to get all types is probably to look at the property type in register.json: https://codecheck.org.uk/register/register.json

case community;
case conference_workshop;
case institution;
case journal;
case lifecycleJournal;

public function labels(): array
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should not be a fixed list, but retrieved from the CODECHECK register (https://codecheck.org.uk/register/venues/index.json) or from the GitHub API (https://api.github.com/repos/codecheckers/register/labels).

The problem with the latter is, that there are labels that are irrelevant for the OJS plugin.

So, we probably need a configuration option that, for each journal, allows to select labels that are set to checks of that journal. Can you please create that one? There should be a selection of existing labels from GitHub.

{
return match($this) {
self::checkNL => ['check-nl', 'community'],
self::community => ['community'],
self::conference_workshop => ['conference/workshop'],
self::institution => ['institution'],
self::journal => ['journal'],
self::lifecycleJournal => ['lifecycle journal'],
};
}
}

class CertificateIdentifier
{
private $year;
private $id;

public function setYear(int $year): void
{
$this->year = $year;
}

public function setId(int $id): void
{
$this->id = $id;
}

public function getYear(): int
{
return $this->year;
}

public function getId(): int
{
return $this->id;
}

// Factory Method for Certificate Identifier
static function fromStr(string $identifier_str): CertificateIdentifier
{
// split Identifier String at '-'
list($year, $id) = explode('-', $identifier_str);
// create new instance of $certificateIdentifier
$certificateIdentifier = new CertificateIdentifier();
// set year and id (cast to int from str)
$certificateIdentifier->setYear((int) $year);
$certificateIdentifier->setId((int) $id);
// return new instance of $certificateIdentifier
return $certificateIdentifier;
}

// Factory Method for new unique Identifier
static function newUniqueIdentifier(CodecheckRegister $codecheckRegister): CertificateIdentifier
{
$latest_identifier = $codecheckRegister->getNewestIdentifier();
$current_year = (int) date("Y");

$new_identifier = new CertificateIdentifier();

// different year, so this is the first CODECHECK certificate of the year -> id 001
if($current_year != $latest_identifier->getYear()) {
// configure new Identifier
$new_identifier->setYear($current_year);
$new_identifier->setId(1);
return $new_identifier;
}

// get the latest id
$latest_id = (int) $latest_identifier->getId();
// increment the latest id by one to get a new unique one
$latest_id++;
// configure new Identifier
$new_identifier->setYear($latest_identifier->getYear());
$new_identifier->setId($latest_id);
return $new_identifier;
}

public function toStr(): string
{
// pad with leading zeros (3 digits) in case number doesn't have 3 digits already
return $this->year . '-' . str_pad($this->id, 3, '0', STR_PAD_LEFT);;
}
}

class CodecheckRegister extends Set
Copy link
Member

@nuest nuest Sep 17, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not so happy with the name here, because this set is only about identifiers of the register. Please rename.

If you want to create a PHP representation of the whole register, then you should base it on register.csv or register.json.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should I maybe just call it something like CertificateList or CertificateSet because in the end it is just that.
My goal was not to create a PHP representation of the whole register, so something along the lines of names I just listed should work just fine.

{
// Factory Method to create a new CodecheckRegister from a GitHub API fetch
static function fromApi(
CodecheckRegisterGithubIssuesApiParser $apiParser
): CodecheckRegister {
$newCodecheckRegister = new CodecheckRegister();

// fetch API
$apiParser->fetchApi();

foreach ($apiParser->getIssues() as $issue) {
// raw identifier (can still have ranges of identifiers);
$rawIdentifier = getRawIdentifier($issue['title']);

// append to all identifiers in new Register
$newCodecheckRegister->appendToCertificateIdList($rawIdentifier);
}

// return the new Register
return $newCodecheckRegister;
}

public function appendToCertificateIdList(string $rawIdentifier): void
{
// list of certificate identifiers in range
$idRange = [];

// if it is a range
if(strpos($rawIdentifier, '/')) {
// split into "fromIdStr" and "toIdStr"
list($fromIdStr, $toIdStr) = explode('/', $rawIdentifier);

$from_identifier = CertificateIdentifier::fromStr($fromIdStr);
$to_identifier = CertificateIdentifier::fromStr($toIdStr);

// append to $idRange list
for ($id_count = $from_identifier->getId(); $id_count <= $to_identifier->getId(); $id_count++) {
$new_identifier = new CertificateIdentifier();
$new_identifier->setYear($from_identifier->getYear());
$new_identifier->setId($id_count);
// append new identifier
$idRange[] = $new_identifier;
}
}
// if it isn't a list then just append on identifier
else {
$new_identifier = CertificateIdentifier::fromStr($rawIdentifier);
$idRange[] = $new_identifier;
}

// append to all certificate identifiers
$this->insertArray($idRange);
}

// sort ascending Certificate Identifiers
public function sortAsc(): void
{
usort($this->arraySet, function($a, $b) {
// First, compare year
if ($a->getYear() !== $b->getYear()) {
return $a->getYear() <=> $b->getYear();
}
// If years are equal, compare ID
return $a->getId() <=> $b->getId();
});
}

public function sortDesc(): void
{
usort($this->arraySet, function($a, $b) {
// First, compare year descending
if ($a->getYear() !== $b->getYear()) {
return $b->getYear() <=> $a->getYear();
}
// If years are equal, compare ID descending
return $b->getId() <=> $a->getId();
});
}

public function getNumberOfIdentifiers(): int
{
return count($this->arraySet);
}

// return the latest identifier
public function getNewestIdentifier(): CertificateIdentifier
{
$this->sortDesc();
// get first element of sort descending -> newest element
return $this->arraySet[0];
}

public function toStr(): string
{
$return_str = "Certificate Identifiers:\n";
foreach ($this->arraySet as $id) {
$return_str = $return_str . $id->toStr() . "\n";
}
return $return_str;
}
}

// get the certificate ID from the issue description
function getRawIdentifier(string $title): string
{
$title = strtolower($title); // convert whole title to lowercase

//$title = "Arabsheibani, Winter, Tomko | 2025-026/2025-029";

if (strpos($title, '|') !== false) {
// find the last "|"
$seperator = strrpos($title, '|');
// move one position forwards (so we get character after '|')
$seperator++;

// Find where the next line break occurs after "certificate"
$rawIdentifier = substr($title, $seperator);
// remove white spaces
$rawIdentifier = preg_replace('/[\s]+/', '', $rawIdentifier);
}

return $rawIdentifier;
}

// api call
class CodecheckRegisterGithubIssuesApiParser
{
private $issues = [];
private $client;

function __construct()
{
$this->client = new Client();
}

public function fetchApi(): void
{
$allissues = $this->client->api('issue')->all('codecheckers', 'register', [
'state' => 'all', // 'open', 'closed', or 'all'
'labels' => 'id assigned', // label
'sort' => 'updated',
'direction' => 'desc',
'per_page' => 100, // get all issues in page
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's start with a lower number of issues, say... 20.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What if there is by any chance no issue with the correct format inside these 20? The chance of that happening are very slim, but theoretically it would be possible

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could just return an error and require editors to create the issue manually, but I do not like that.
I am a bit careful with how many request the plugin shall send to the GitHub API, but then the size of the response probably is not a reason for throttling.

Having a iterative approach here (retrieve 20, if no clear data then retrieve 20 more if need be, etc.) should work, and in most cases we can give a quick response in the UI then.

]);

foreach ($allissues as $issue) {
// check if this issue has the certificate identifier in the title
if(strpos($issue['title'], '|') !== false) {
$this->issues[] = $issue;
}
}
}

public function addIssue(
CertificateIdentifier $certificateIdentifier,
CodecheckType $codecheckType
): void {
$token = $_ENV['CODECHECK_REGISTER_GITHUB_TOKEN'];

$this->client->authenticate($token, null, Client::AUTH_ACCESS_TOKEN);

$repositoryOwner = 'dxL1nus';
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can use this repo for your testing: https://github.com/codecheckers/testing-dev-register

You can also create the existing labels etc. there.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How does it work with the GitHub Token there? Can I use my own one or do I need one especially provided to me to work for that repository?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can use your own for your local development, you're not sharing that in the repository anyway.

For our demo server I can create one using a CODECHECK user, and other users of the plugin will probably have to add their own token.

$repositoryName = 'dxL1nus';
$issueTitle = 'New CODECHECK | ' . $certificateIdentifier->toStr();
$issueBody = '';
$labels = ['id assigned'];
Comment on lines +297 to +299
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So this is information that will have to be provided by the plugin and the journal configuration, right?

Please think about a useful function/internal API or how a class could look like that provides all information that can be configured by a journal or provided from the submission (author name, unless anonymous; repo URL for the body; assigned codechecker, if already known; ...)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This would only work if we keep the reserve certificate button at the very bottom of the form though correct? Because otherwise this information (like: repo URL, assigned codechecker) is most likely not yet known

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ha, good point.

So let's go with "minimal information for reservation" first. Please create an issue about posting relevant information for the check in the issue. Probably we need something like an "Update information in register issue" workflow ?


$labels = array_merge($labels, $codecheckType->labels());

$issue = $this->client->api('issue')->create(
$repositoryOwner,
$repositoryName,
[
'title' => $issueTitle,
'body' => $issueBody,
'labels' => $labels
]
);
}

public function getIssues(): array
{
return $this->issues;
}
}


// CODECHECK GitHub Issue Register API parser
$apiParser = new CodecheckRegisterGithubIssuesApiParser();

// CODECHECK Register with list of all identifiers in range
$codecheckRegister = CodecheckRegister::fromApi($apiParser);

// print Certificate Identifier list
$codecheckRegister->sortDesc();
echo $codecheckRegister->toStr();

echo $codecheckRegister->getNewestIdentifier()->toStr() . "\n";

$new_identifier = CertificateIdentifier::newUniqueIdentifier($codecheckRegister);

$apiParser->addIssue($new_identifier, CodecheckType::checkNL);

echo "Added new issue with identifier: " . $new_identifier->toStr() . "\n";

//echo "{$num_of_issues}";
13 changes: 13 additions & 0 deletions composer.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"require": {
"knplabs/github-api": "^3.0",
"guzzlehttp/guzzle": "^7.0.1",
"http-interop/http-factory-guzzle": "^1.0",
"vlucas/phpdotenv": "^5.6"
},
"config": {
"allow-plugins": {
"php-http/discovery": true
}
}
}
Loading