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
194 changes: 123 additions & 71 deletions CodecheckPlugin.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@
use APP\plugins\generic\codecheck\classes\migration\CodecheckSchemaMigration;
use APP\plugins\generic\codecheck\classes\Submission\Schema;
use APP\plugins\generic\codecheck\classes\Submission\SubmissionWizardHandler;
use APP\plugins\generic\codecheck\classes\Orcid\OrcidAuthHandler;
use APP\plugins\generic\codecheck\classes\Orcid\OrcidDepositService;
use APP\plugins\generic\codecheck\classes\Log\CodecheckLogger;
use PKP\plugins\GenericPlugin;
use PKP\plugins\Hook;
Expand All @@ -33,8 +35,6 @@ class CodecheckPlugin extends GenericPlugin

public function register($category, $path, $mainContextId = null): bool
{
CodecheckLogger::debug('register() called, path=' . $path);

$success = parent::register($category, $path);

if ($success && $this->getEnabled()) {
Expand Down Expand Up @@ -75,6 +75,9 @@ public function register($category, $path, $mainContextId = null): bool
Hook::add('Template::SubmissionWizard::Section::Review', function($hookName, $params) use ($codecheckWizard) {
return $codecheckWizard->addToSubmissionWizardReviewTemplate($hookName, $params);
});

// ORCID: automatically deposit when an article is published
Hook::add('Publication::publish', $this->onPublicationPublish(...));

// Test if we can hook into the publication to block it if codecheck failed
Hook::add('Publication::validatePublish', $this->validateCodecheckStatus(...));
Expand Down Expand Up @@ -131,66 +134,75 @@ public function addCodecheckStatusLocalizations($hookName, $args) {
}

/**
* Setup the `CodecheckApiHandler`
*
* @param string $hookname The name of the hook
* @param array $args The arguments passed by the hook
*
* @return void
* Triggered when an editor publishes an article.
*/
public function onPublicationPublish(string $hookName, array $args): bool
Comment thread
nuest marked this conversation as resolved.
{
$publication = $args[0];
$submission = Repo::submission()->get($publication->getData('submissionId'));

if (!$submission) return false;
if (!$submission->getData('codecheckOptIn')) return false;

$context = Application::get()->getRequest()->getContext();
if (!$this->getSetting($context->getId(), Constants::ORCID_ENABLED)) return false;

try {
$depositService = new OrcidDepositService($this);
$results = $depositService->depositForSubmission($submission->getId());
foreach ($results as $result) {
if ($result['status'] === 'success') {
Comment thread
nuest marked this conversation as resolved.
CodecheckLogger::info('ORCID deposited for ' . $result['orcidId'] . ' put-code=' . $result['putCode']);
} else {
CodecheckLogger::error('ORCID deposit failed for ' . $result['orcidId'] . ': ' . ($result['error'] ?? 'unknown'));
}
}
} catch (\Throwable $e) {
CodecheckLogger::error('ORCID deposit exception on publish: ' . $e->getMessage());
}

return false;
}

/**
* Setup the CodecheckApiHandler.
* The constructor handles the request and exits — no need to set a router handler.
*/
public function setupAPIHandler(string $hookName, array $args): void
{
$request = $args[0];
$router = $request->getRouter();
$router = $request->getRouter();

if (!($router instanceof \PKP\core\APIRouter)) {
return;
}
if (!($router instanceof \PKP\core\APIRouter)) return;

if (str_contains($request->getRequestPath(), 'api/v1/codecheck')) {
CodecheckLogger::debug('Instantiating the CODECHECK APIHandler');

$adminRoles = new CodecheckRoleArray([Role::ROLE_ID_MANAGER, Role::ROLE_ID_SITE_ADMIN]);
$editRoles = new CodecheckRoleArray([$adminRoles, Role::ROLE_ID_SUB_EDITOR, Role::ROLE_ID_ASSISTANT, Role::ROLE_ID_MANAGER]);
$editRoles = new CodecheckRoleArray([$adminRoles, Role::ROLE_ID_SUB_EDITOR, Role::ROLE_ID_ASSISTANT, Role::ROLE_ID_MANAGER, Role::ROLE_ID_REVIEWER]);
$readRoles = new CodecheckRoleArray([$editRoles, Role::ROLE_ID_READER, Role::ROLE_ID_AUTHOR]);

$roles = new CodecheckRoleManager(
readMetadata: $readRoles,
editMetadata: $editRoles,
admin: $adminRoles,
editMetadata: $editRoles,
admin: $adminRoles,
);

$apiHandler = new CodecheckApiHandler($this, $request, $roles);
CodecheckLogger::debug('API request: ' . $request->getRequestPath());
}

if (!isset($apiHandler)) {
return;
new CodecheckApiHandler($this, $request, $roles);
}

$router->setHandler($apiHandler);
exit;
}

/**
* Declare the handler function to process the actual page PATH
*
* @param string $hookName The name of the invoked hook
* @param array $args Hook parameters
*
* @return bool Hook handling status
* Declare the handler function to process the actual page PATH.
*/
public function setCodecheckPageHandler($hookName, $args)
{
$request = Application::get()->getRequest();
$templateMgr = TemplateManager::getManager($request);

$page = &$args[0];
$op = &$args[1];
$page = &$args[0];
$op = &$args[1];
$handler = &$args[3];


// Construct a path to look for
$path = $page;
if ($op !== 'index') {
$path .= "/{$op}";
Expand All @@ -199,13 +211,19 @@ public function setCodecheckPageHandler($hookName, $args)
$path .= '/' . implode('/', $ops);
}

// Check if this is a request for a static page or preview.
// ORCID OAuth routes
if ($page === 'codecheck' && $op === 'orcid') {
$subOp = $request->getRequestedArgs()[0] ?? '';
if (in_array($subOp, ['startAuth', 'callback'], true)) {
$handler = new OrcidAuthHandler($this);
$args[1] = $subOp;
return true;
}
}

if ($page = 'codecheck' && $op == 'info') {
// Trick the handler into dealing with it normally
$page = 'pages';
$op = 'view';

// It is -- attach the static pages handler.
$handler = new CodecheckPageHandler($this);
return true;
}
Expand Down Expand Up @@ -270,14 +288,33 @@ public function callbackTemplateManagerDisplay($hookName, $args): bool
'priority' => TemplateManager::STYLE_SEQUENCE_LAST,
]
);

$orcidAuthUrl = $request->getBaseUrl() . '/index.php/' . $context->getPath() . '/codecheck/orcid/startAuth';

$orcidConfig = json_encode([
'enabled' => (bool) $this->getSetting($contextId, Constants::ORCID_ENABLED),
'authUrl' => $orcidAuthUrl,
'apiType' => $this->getSetting($contextId, Constants::ORCID_API_TYPE)
?? Constants::ORCID_API_TYPE_SANDBOX,
'apiBaseUrl' => $request->getBaseUrl() . '/index.php/' . $context->getPath(),
]);

$templateMgr->addJavaScript(
'codecheck-orcid-config',
'window.codecheckOrcidConfig = ' . $orcidConfig . ';',
[
'inline' => true,
'contexts' => ['backend'],
'priority' => TemplateManager::STYLE_SEQUENCE_LAST
]
);
}

// ----------------------------------------------------------------
// Workflow page — inject submission data for the CODECHECK tab
// ----------------------------------------------------------------
if ($request->getRequestedOp() == 'workflow') {
$submission = $request->getRouter()->getHandler()->getAuthorizedContextObject(ASSOC_TYPE_SUBMISSION);

if ($submission) {
$templateMgr->setState([
'codecheckSubmission' => [
Expand All @@ -288,23 +325,57 @@ public function callbackTemplateManagerDisplay($hookName, $args): bool
'dataRepository' => $submission->getData('dataRepository'),
'manifestFiles' => $submission->getData('manifestFiles'),
'dataAvailabilityStatement' => $submission->getData('dataAvailabilityStatement'),
]
],
]);
}
}


// ----------------------------------------------------------------
// Reviewer page — inject submission data + ORCID config for Vue
// ----------------------------------------------------------------
if ($request->getRequestedPage() == 'reviewer' && $request->getRequestedOp() == 'submission') {
$requestArgs = $request->getRequestedArgs();
$submissionId = (int) ($requestArgs[0] ?? 0);

if ($submissionId) {
$context = $request->getContext();
$contextId = $context->getId();
$submission = Repo::submission()->get($submissionId);

if ($submission && $submission->getData('codecheckOptIn')) {
$orcidAuthUrl = $request->getBaseUrl() . '/index.php/' . $context->getPath() . '/codecheck/orcid/startAuth';

$reviewerData = json_encode([
'submissionId' => $submission->getId(),
'codecheckOptIn' => true,
'orcid' => [
'enabled' => (bool) $this->getSetting($contextId, Constants::ORCID_ENABLED),
'authUrl' => $orcidAuthUrl,
'apiType' => $this->getSetting($contextId, Constants::ORCID_API_TYPE) ?? Constants::ORCID_API_TYPE_SANDBOX,
'apiBaseUrl' => $request->getBaseUrl() . '/index.php/' . $context->getPath(),
],
]);

$templateMgr->addJavaScript(
'codecheck-reviewer-data',
'window.codecheckReviewerData = ' . $reviewerData . ';',
[
'inline' => true,
'contexts' => ['backend'],
'priority' => TemplateManager::STYLE_SEQUENCE_LAST,
]
);
}
}
}

return false;
}

public function getUrlPageRoute(string $page): string
{
$request = Application::get()->getRequest();
return $request->getDispatcher()->url(
$request,
ROUTE_PAGE,
null,
$page
);
return $request->getDispatcher()->url($request, ROUTE_PAGE, null, $page);
}

public function addOptInToSchema(string $hookName, array $args): bool
Expand Down Expand Up @@ -339,7 +410,7 @@ public function addOptInCheckbox(string $hookName, \PKP\components\forms\FormCom
'codecheckLink' => "<a href='{$this->getUrlPageRoute("codecheck")}/info' target='_blank'>" . __('plugins.generic.codecheck.displayName') . "</a>"
]);

if($codecheckMode == 'opt-out') {
if ($codecheckMode == 'opt-out') {
$checkboxValue = true;
} elseif ($codecheckMode == 'mandatory') {
$checkboxValue = true;
Expand All @@ -360,7 +431,7 @@ public function addOptInCheckbox(string $hookName, \PKP\components\forms\FormCom
'disabled' => $codecheckMandatory,
]
],
'value' => $checkboxValue,
'value' => $checkboxValue,
'groupId' => 'default'
]));
}
Expand All @@ -383,10 +454,7 @@ public function saveOptIn(string $hookName, array $params): bool
public function saveWizardFieldsFromRequest(string $hookName, array $params): bool
{
$submission = $params[1];

if (!$submission) {
return false;
}
if (!$submission) return false;

$request = Application::get()->getRequest();

Expand Down Expand Up @@ -415,9 +483,6 @@ public function saveWizardFieldsFromRequest(string $hookName, array $params): bo

/**
* Provide a name for this plugin
*
* The name will appear in the Plugin Gallery where editors can
* install, enable and disable plugins.
*/
public function getDisplayName(): string
{
Expand All @@ -426,34 +491,21 @@ public function getDisplayName(): string

/**
* Provide a description for this plugin
*
* The description will appear in the Plugin Gallery where editors can
* install, enable and disable plugins.
*/
public function getDescription(): string
{
return __('plugins.generic.codecheck.description');
}

/**
* Add a settings action to the plugin's entry in the CODECHECK plugins list.
*
* @param Request $request
* @param array $actionArgs
* Add a settings action to the plugin's entry in the plugins list.
*/
public function getActions($request, $actionArgs): array
{
$actions = new Actions($this);
return $actions->execute($request, $actionArgs, parent::getActions($request, $actionArgs));
}

/**
* Load a form when the `settings` button is clicked and
* save the form when the user saves it.
*
* @param array $args
* @param Request $request
*/
public function manage($args, $request): JSONMessage
{
$manage = new Manage($this);
Expand Down
26 changes: 24 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ The ojs-codecheck plugin development was started as part of the [CHECK-PUB](http
-->
- **Certificate display**: Automatically display CODECHECK certificates for verified submissions
- **Customizable settings**: Configure CODECHECK workflow and display preferences
- **ORCID deposition**: Automatically deposit CODECHECK peer-review activity to codecheckers' ORCID profiles using the ORCID Member API

## Installation

Expand Down Expand Up @@ -131,6 +132,27 @@ function getStatus() {
}
```

## ORCID Peer-Review Deposition

The plugin deposits CODECHECK activity as a [peer-review item](https://support.orcid.org/hc/en-us/articles/360006971333-Peer-Reviews) to the codechecker's ORCID record. The following fields are set (see `classes/Orcid/PeerReviewPayloadBuilder.php`):

| Field | Value |
|---|---|
| Role | `reviewer` |
| Reviewer identifier | Codechecker's ORCID iD |
| Convening organization | Publisher name (Journal Settings → Masthead → Publisher) |
| Country | Journal Settings → Masthead → Country |
| City | Not set — OJS has no publisher city field |
| Group | `issn:{ISSN}` or `orcid-generated:codecheck-ojs` if no ISSN configured |
| Type | `review` |
| Date | Check completion date (`codecheck_metadata.check_time`) |
| Review container name | Journal name |
| Review identifier | Certificate DOI or source work ID (`codecheck_metadata.certificate`) |
| Review URL | `https://doi.org/{certificate}` or CODECHECK register URL |
| Subject name | Submission title |
| Subject type | `journal-article` |
| Subject identifier | Article DOI |

## Development

### Requirements
Expand Down Expand Up @@ -242,7 +264,7 @@ codecheck/
├── composer.lock # composer lock-file
├── css/* # CODECHECK CSS stylesheets
├── cypress/tests/functional/ # End-to-end testing
   └── CodecheckPlugin.cy.js # Cypress test suite
└── CodecheckPlugin.cy.js # Cypress test suite
├── locale/* # Internationalization (language localization strings)
├── package-lock.json
├── package.json
Expand Down Expand Up @@ -414,4 +436,4 @@ The [CHECK-PUB](https://codecheck.org.uk/pub/) project (2025-2026) is empored by
## Related Projects

- [CODECHECK](https://codecheck.org.uk/)
- [OJS](https://pkp.sfu.ca/software/ojs/) by [PKP](https://pkp.sfu.ca/)
- [OJS](https://pkp.sfu.ca/software/ojs/) by [PKP](https://pkp.sfu.ca/)
Loading
Loading