Skip to content

structured citations #11270

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: stable-3_5_0
Choose a base branch
from
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
220 changes: 220 additions & 0 deletions api/v1/citations/PKPCitationController.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,220 @@
<?php

/**
* @file api/v1/citations/PKPCitationController.php
*
* Copyright (c) 2025 Simon Fraser University
* Copyright (c) 2025 John Willinsky
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
*
* @class PKPCitationController
*
* @ingroup api_v1_citations
*
* @brief Controller class to handle API requests for citation operations.
*
*/

namespace pkp\api\v1\citations;

use APP\core\Application;
use APP\facades\Repo;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
use Illuminate\Support\Facades\Route;
use PKP\components\forms\citation\PKPCitationEditForm;
use PKP\core\PKPBaseController;
use PKP\core\PKPRequest;
use PKP\plugins\Hook;
use PKP\security\authorization\ContextRequiredPolicy;
use PKP\security\authorization\PolicySet;
use PKP\security\authorization\RoleBasedHandlerOperationPolicy;
use PKP\security\authorization\UserRolesRequiredPolicy;
use PKP\security\Role;
use PKP\services\PKPSchemaService;

class PKPCitationController extends PKPBaseController
{
/** @var int The default number of citations to return in one request */
public const DEFAULT_COUNT = 30;

/** @var int The maximum number of citations to return in one request */
public const MAX_COUNT = 100;

/**
* @copydoc \PKP\core\PKPBaseController::getHandlerPath()
*/
public function getHandlerPath(): string
{
return 'citations';
}

/**
* @copydoc \PKP\core\PKPBaseController::getRouteGroupMiddleware()
*/
public function getRouteGroupMiddleware(): array
{
return [
'has.user',
'has.context',
self::roleAuthorizer([
Role::ROLE_ID_MANAGER,
Role::ROLE_ID_SUB_EDITOR,
Role::ROLE_ID_ASSISTANT,
Role::ROLE_ID_AUTHOR,
]),
];
}

/**
* @copydoc \PKP\core\PKPBaseController::getGroupRoutes()
*/
public function getGroupRoutes(): void
{
Route::get('{citationId}', $this->get(...))
->name('citation.getCitation')
->whereNumber('citationId');

Route::get('', $this->getMany(...))
->name('citation.getMany');

Route::post('', $this->edit(...))
->name('citation.edit');

Route::get('{citationId}/_components/citationForm', $this->getCitationForm(...))
->name('citation._components.citationForm');
}

/**
* @copydoc \PKP\core\PKPBaseController::authorize()
*/
public function authorize(PKPRequest $request, array &$args, array $roleAssignments): bool
{
$this->addPolicy(new UserRolesRequiredPolicy($request), true);

$rolePolicy = new PolicySet(PolicySet::COMBINING_PERMIT_OVERRIDES);

$this->addPolicy(new ContextRequiredPolicy($request));

foreach ($roleAssignments as $role => $operations) {
$rolePolicy->addPolicy(new RoleBasedHandlerOperationPolicy($request, $role, $operations));
}

$this->addPolicy($rolePolicy);

return parent::authorize($request, $args, $roleAssignments);
}

/**
* Get a single citation
*/
public function get(Request $illuminateRequest): JsonResponse
{
if (!Repo::citation()->exists((int)$illuminateRequest->route('citationId'))) {
return response()->json([
'error' => __('api.citations.404.citationNotFound')
], Response::HTTP_OK);
}

$citation = Repo::citation()->get((int)$illuminateRequest->route('citationId'));

return response()->json(Repo::citation()->getSchemaMap()->map($citation), Response::HTTP_OK);
}

/**
* Get a collection of citations
*
* @hook API::citations::params [[$collector, $illuminateRequest]]
*/
public function getMany(Request $illuminateRequest): JsonResponse
{
$collector = Repo::citation()->getCollector()
->limit(self::DEFAULT_COUNT)
->offset(0);

foreach ($illuminateRequest->query() as $param => $val) {
switch ($param) {
case 'count':
$collector->limit(min((int)$val, self::MAX_COUNT));
break;
case 'offset':
$collector->offset((int)$val);
break;
}
}

Hook::call('API::citations::params', [$collector, $illuminateRequest]);

$citations = $collector->getMany();

return response()->json([
'itemsMax' => $collector->getCount(),
'items' => Repo::citation()->getSchemaMap()->summarizeMany($citations->values())->values(),
], Response::HTTP_OK);
}


/**
* Add or edit a citation
*/
public function edit(Request $illuminateRequest): JsonResponse
{
$params = $this->convertStringsToSchema(PKPSchemaService::SCHEMA_CITATION, $illuminateRequest->input());

$errors = Repo::citation()->validate(null, $params);

if (!empty($errors)) {
return response()->json($errors, Response::HTTP_BAD_REQUEST);
}

$citation = Repo::citation()->newDataObject($params);
$id = Repo::citation()->updateOrInsert($citation);
$citation = Repo::citation()->get($id);

return response()->json(
Repo::citation()->getSchemaMap()->map($citation), Response::HTTP_OK
);
}

/**
* Get Publication Reference/Citation Form component
*/
protected function getCitationForm(Request $illuminateRequest): JsonResponse
{
$citation = Repo::citation()->get((int)$illuminateRequest->route('citationId'));
$publication = Repo::publication()->get($citation->getData('publicationId'));

if (!$citation) {
return response()->json(
[
'error' => __('api.404.resourceNotFound')
],
Response::HTTP_NOT_FOUND
);
}

$publicationApiUrl = $this->getCitationApiUrl(
$this->getRequest(),
(int)$illuminateRequest->route('citationId'));

$citationForm = new PKPCitationEditForm($publicationApiUrl, (int)$illuminateRequest->route('citationId'));

return response()->json($citationForm->getConfig(), Response::HTTP_OK);
}

/**
* Get the url to the citation's API endpoint
*/
protected function getCitationApiUrl(PKPRequest $request, int $citationId): string
{
return $request
->getDispatcher()
->url(
$request,
Application::ROUTE_API,
$request->getContext()->getPath(),
'citations/' . $citationId
);
}
}
2 changes: 1 addition & 1 deletion api/v1/submissions/PKPSubmissionController.php
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,8 @@
use Illuminate\Support\Facades\Route;
use Illuminate\Support\LazyCollection;
use PKP\affiliation\Affiliation;
use PKP\components\forms\citation\PKPCitationsForm;
use PKP\components\forms\FormComponent;
use PKP\components\forms\publication\PKPCitationsForm;
use PKP\components\forms\publication\PKPMetadataForm;
use PKP\components\forms\publication\PKPPublicationIdentifiersForm;
use PKP\components\forms\publication\PKPPublicationLicenseForm;
Expand Down
101 changes: 34 additions & 67 deletions classes/citation/Citation.php
Original file line number Diff line number Diff line change
@@ -1,14 +1,10 @@
<?php

/**
* @defgroup citation Citation
*/

/**
* @file classes/citation/Citation.php
*
* Copyright (c) 2014-2021 Simon Fraser University
* Copyright (c) 2000-2021 John Willinsky
* Copyright (c) 2014-2025 Simon Fraser University
* Copyright (c) 2000-2025 John Willinsky
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
*
* @class Citation
Expand All @@ -20,99 +16,70 @@

namespace PKP\citation;

class Citation extends \PKP\core\DataObject
{
/**
* Constructor.
*
* @param string $rawCitation an unparsed citation string
*/
public function __construct($rawCitation = null)
{
parent::__construct();
$this->setRawCitation($rawCitation);
}

//
// Getters and Setters
//

/**
* Replace URLs through HTML links, if the citation does not already contain HTML links
*
* @return string
*/
public function getCitationWithLinks()
{
$citation = $this->getRawCitation();
if (stripos($citation, '<a href=') === false) {
$citation = preg_replace_callback(
'#(http|https|ftp)://[\d\w\.-]+\.[\w\.]{2,6}[^\s\]\[\<\>]*/?#',
function ($matches) {
$trailingDot = in_array($char = substr($matches[0], -1), ['.', ',']);
$url = rtrim($matches[0], '.,');
return "<a href=\"{$url}\">{$url}</a>" . ($trailingDot ? $char : '');
},
$citation
);
}
return $citation;
}
use PKP\core\DataObject;

class Citation extends DataObject
{
/**
* Get the rawCitation
*
* @return string
* Get the rawCitation.
*/
public function getRawCitation()
public function getRawCitation(): string
{
return $this->getData('rawCitation');
}

/**
* Set the rawCitation
* Set the rawCitation.
*/
public function setRawCitation(?string $rawCitation)
public function setRawCitation(?string $rawCitation): void
{
$rawCitation = $this->_cleanCitationString($rawCitation ?? '');
$rawCitation = $this->cleanCitationString($rawCitation ?? '');
$this->setData('rawCitation', $rawCitation);
}

/**
* Get the sequence number
*
* @return int
*/
public function getSequence()
public function getSequence(): int
{
return $this->getData('seq');
}

/**
* Set the sequence number
*
* @param int $seq
*/
public function setSequence($seq)
public function setSequence(int $seq): void
{
$this->setData('seq', $seq);
}

//
// Private methods
//
/**
* Replace URLs through HTML links, if the citation does not already contain HTML links.
*/
public function getRawCitationWithLinks(): string
{
$rawCitationWithLinks = $this->getRawCitation();
if (stripos($rawCitationWithLinks, '<a href=') === false) {
$rawCitationWithLinks = preg_replace_callback(
'#(http|https|ftp)://[\d\w\.-]+\.[\w\.]{2,6}[^\s\]\[\<\>]*/?#',
function ($matches) {
$trailingDot = in_array($char = substr($matches[0], -1), ['.', ',']);
$url = rtrim($matches[0], '.,');
return "<a href=\"{$url}\">{$url}</a>" . ($trailingDot ? $char : '');
},
$rawCitationWithLinks
);
}
return $rawCitationWithLinks;
}

/**
* Take a citation string and clean/normalize it
*/
public function _cleanCitationString(string $citationString) : string
public function cleanCitationString(?string $citationString = null): string|null
{
// 1) Strip slashes and whitespace
$citationString = trim(stripslashes($citationString));

// 2) Normalize whitespace
$citationString = preg_replace('/[\s]+/u', ' ', $citationString);

return $citationString;
return preg_replace('/[\s]+/u', ' ', $citationString);
}
}

Expand Down
Loading