Skip to content
Draft
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
37 changes: 37 additions & 0 deletions CitationsPlugin.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,10 @@
use PKP\core\JSONMessage;
use PKP\linkAction\LinkAction;
use PKP\linkAction\request\AjaxModal;
use PKP\linkAction\request\RedirectAction;
use PKP\plugins\GenericPlugin;
use PKP\plugins\Hook;
use PKP\security\Role;


class CitationsPlugin extends GenericPlugin
Expand Down Expand Up @@ -95,6 +97,40 @@ public function setPageHandler($hookName, $params): bool
public function getActions($request, $actionArgs): array
{
$router = $request->getRouter();
$exportActions = [];
if ($this->getEnabled()) {
$context = $request->getContext();
$user = $request->getUser();

if ($context && $user) {
$contextId = $context->getId();
$isManager = $user->hasRole([Role::ROLE_ID_MANAGER], $contextId);
$isSiteAdmin = $user->hasRole([Role::ROLE_ID_SITE_ADMIN], \PKP\core\PKPApplication::SITE_CONTEXT_ID);

$dispatcher = $request->getDispatcher();
if ($isManager || $isSiteAdmin) {
$exportActions[] = new LinkAction(
'exportCitations',
new RedirectAction(
$dispatcher->url($request, Application::ROUTE_PAGE, null, 'citations', 'export')
),
__('plugins.generic.citations.export'),
null
);
}

if ($isSiteAdmin) {
$exportActions[] = new LinkAction(
'exportAllCitations',
new RedirectAction(
$dispatcher->url($request, Application::ROUTE_PAGE, null, 'citations', 'export', null, ['scope' => 'all'])
),
__('plugins.generic.citations.export.all'),
null
);
}
}
}
return array_merge(
$this->getEnabled() ? array(
new LinkAction(
Expand All @@ -116,6 +152,7 @@ public function getActions($request, $actionArgs): array
null
),
) : array(),
$exportActions,
parent::getActions($request, $actionArgs)
);
}
Expand Down
221 changes: 221 additions & 0 deletions classes/CitationsHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,19 @@

namespace APP\plugins\generic\citations\classes;

use APP\facades\Repo;
use APP\handler\Handler;
use PKP\db\DAORegistry;

use APP\plugins\generic\citations\classes\processor\CrossrefProcessor;
use APP\plugins\generic\citations\classes\processor\EuropePmcProcessor;
use APP\plugins\generic\citations\classes\processor\ScopusProcessor;
use PKP\core\JSONMessage;
use PKP\core\PKPApplication;
use PKP\core\PKPRequest;
use PKP\plugins\PluginRegistry;
use PKP\security\Role;
use PKP\submission\PKPSubmission;

class CitationsHandler extends Handler
{
Expand Down Expand Up @@ -49,6 +54,206 @@ public function get(array $args, PKPRequest $request): JSONMessage
return new JSONMessage(!empty($result), !empty($result) ? $result : null);
}

/**
* Export citation data for a journal (or all journals) as a CSV download.
*
* Accessible at /index.php/<journal>/citations/export
*
* Site admins may append ?scope=all to export across every journal in the
* installation. Journal managers (and site admins) without that parameter
* receive a report for the current journal only.
*
* @param array $args
* @param PKPRequest $request
*/
public function export(array $args, PKPRequest $request): void
{
$user = $request->getUser();
if ($user === null) {
header('HTTP/1.0 403 Forbidden');
exit;
}

$context = $request->getContext();
$contextId = $context ? $context->getId() : null;

$isSiteAdmin = $user->hasRole([Role::ROLE_ID_SITE_ADMIN], PKPApplication::SITE_CONTEXT_ID);
$isManager = $contextId && $user->hasRole([Role::ROLE_ID_MANAGER], $contextId);

if (!$isSiteAdmin && !$isManager) {
header('HTTP/1.0 403 Forbidden');
exit;
}

$exportAll = $isSiteAdmin && $request->getUserVar('scope') === 'all';

if ($exportAll) {
$contextMap = $this->getAllContextMap();
} else {
if ($contextId === null) {
header('HTTP/1.0 400 Bad Request');
exit;
}
$journalName = $context ? ($context->getLocalizedName() ?: $context->getPath()) : (string) $contextId;
$contextMap = [$contextId => $journalName];
}

$rawPath = $exportAll ? 'all-journals' : ($context ? $context->getPath() : 'journal');
$safePath = preg_replace('/[^A-Za-z0-9_\-]/', '_', $rawPath);
$filename = 'citations-' . $safePath . '.csv';

header('Content-Type: text/csv; charset=UTF-8');
header('Content-Disposition: attachment; filename="' . $filename . '"');
header('Cache-Control: no-cache, no-store, must-revalidate');
header('Pragma: no-cache');
header('Expires: 0');

$output = fopen('php://output', 'w');

// UTF-8 BOM for Excel compatibility
fputs($output, "\xEF\xBB\xBF");

fputcsv($output, [
'Journal',
'Article DOI',
'Article Title',
'Article Year',
'Citation Source',
'Citation DOI',
'Citation Title',
'Citation Authors',
'Citation Journal',
'Citation Year',
'Citation Volume',
'Citation Issue',
'Citation Pages',
'Citation Type',
]);

foreach ($contextMap as $ctxId => $journalName) {
$settings = $this->loadSettingsForContext($ctxId);
if (empty($settings)) {
continue;
}
$this->writeContextRows($output, $ctxId, $journalName, $settings);
}

fclose($output);
exit;
}

/**
* Write CSV rows for all published submissions in one journal context.
*
* @param resource $output
* @param int $contextId
* @param string $journalName
* @param array $settings
*/
private function writeContextRows($output, int $contextId, string $journalName, array $settings): void
{
$submissions = Repo::submission()->getCollector()
->filterByContextIds([$contextId])
->filterByStatus([PKPSubmission::STATUS_PUBLISHED])
->getMany();

foreach ($submissions as $submission) {
$doi = $submission->getStoredPubId('doi');
if (empty($doi)) {
continue;
}

$publication = $submission->getCurrentPublication();
$articleTitle = $publication ? $publication->getLocalizedTitle() : '';
$articleYear = $publication
? substr((string) ($publication->getData('datePublished') ?? ''), 0, 4)
: '';

// Force showList=true so we always retrieve citation details for the export
$exportSettings = array_merge($settings, ['showList' => true]);
$citations = $this->fetchAllCitations($doi, $exportSettings);

if (empty($citations)) {
fputcsv($output, [
$journalName, $doi, $articleTitle, $articleYear,
'', '', '', '', '', '', '', '', '', '',
]);
continue;
}

foreach ($citations as $citation) {
fputcsv($output, [
$journalName,
$doi,
$articleTitle,
$articleYear,
$citation['source'] ?? '',
$citation['doi'] ?? '',
$citation['title'] ?? '',
$citation['authors'] ?? '',
$citation['journal'] ?? '',
$citation['year'] ?? '',
$citation['volume'] ?? '',
$citation['issue'] ?? '',
$citation['pages'] ?? '',
$citation['type'] ?? '',
]);
}
}
}

/**
* Collect citations from all configured providers for a given DOI.
*
* @param string $doi
* @param array $settings Plugin settings (showList must already be true)
* @return array Flat list of citation arrays
*/
private function fetchAllCitations(string $doi, array $settings): array
{
$citations = [];

if ('all' === ($settings['provider'] ?? '') || 'crossref' === ($settings['provider'] ?? '')) {
$result = (new CrossrefProcessor())->process($doi, $settings);
if (!empty($result['citations'])) {
$citations = array_merge($citations, $result['citations']);
}
}

if ('all' === ($settings['provider'] ?? '') || 'scopus' === ($settings['provider'] ?? '')) {
$result = (new ScopusProcessor())->process($doi, $settings);
if (!empty($result['citations'])) {
$citations = array_merge($citations, $result['citations']);
}
}

if (!empty($settings['showPmc'])) {
$result = (new EuropePmcProcessor())->process($doi, $settings);
if (!empty($result['citations'])) {
$citations = array_merge($citations, $result['citations']);
}
}

return $citations;
}

/**
* Return a map of context ID => display name for all enabled contexts.
*
* @return array<int, string>
*/
private function getAllContextMap(): array
{
/** @var \APP\journal\JournalDAO $journalDao */
$journalDao = DAORegistry::getDAO('JournalDAO');
$map = [];
$journalIterator = $journalDao->getAll(true);
while ($journal = $journalIterator->next()) {
$map[$journal->getId()] = $journal->getLocalizedName() ?: $journal->getPath();
}
return $map;
}

/** Loads the plugin settings
* @param PKPRequest $request The request
* @return array The settings
Expand All @@ -64,6 +269,22 @@ private function loadSettings(PKPRequest $request): array
}
}

/**
* Load plugin settings for a specific context ID.
*
* @param int $contextId
* @return array
*/
private function loadSettingsForContext(int $contextId): array
{
$plugin = PluginRegistry::getPlugin('generic', 'citationsplugin');
if ($plugin === null) {
return [];
}
$raw = $plugin->getSetting($contextId, 'settings');
return $raw ? (json_decode($raw, true) ?? []) : [];
}

/** checks if the doi of a scopus citation is already in the crossref citations and removes it if so
* @param array $crossrefCitations The Crossref citations
* @param array $scopusCitations The Scopus citations
Expand Down
12 changes: 12 additions & 0 deletions locale/en/locale.po
Original file line number Diff line number Diff line change
Expand Up @@ -100,3 +100,15 @@ msgstr "The link to the Europe PMC search can be de-/activated with this option.

msgid "plugins.generic.citations.show.pmc.check"
msgstr "Enable Europe PMC"

msgid "plugins.generic.citations.export"
msgstr "Export Citations (CSV)"

msgid "plugins.generic.citations.export.all"
msgstr "Export All Journals (CSV)"

msgid "plugins.generic.citations.export.description"
msgstr "Download a CSV report of all citation data for published articles in this journal."

msgid "plugins.generic.citations.export.all.description"
msgstr "Download a CSV report of all citation data for published articles across all journals in this installation. Only available to site administrators."