Skip to content

Commit 0f4d4f5

Browse files
committed
structured citations (development)
1 parent 0f108ea commit 0f4d4f5

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

51 files changed

+2676
-531
lines changed
+220
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,220 @@
1+
<?php
2+
3+
/**
4+
* @file api/v1/citations/PKPCitationController.php
5+
*
6+
* Copyright (c) 2025 Simon Fraser University
7+
* Copyright (c) 2025 John Willinsky
8+
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
9+
*
10+
* @class PKPCitationController
11+
*
12+
* @ingroup api_v1_citations
13+
*
14+
* @brief Controller class to handle API requests for citation operations.
15+
*
16+
*/
17+
18+
namespace pkp\api\v1\citations;
19+
20+
use APP\core\Application;
21+
use APP\facades\Repo;
22+
use Illuminate\Http\JsonResponse;
23+
use Illuminate\Http\Request;
24+
use Illuminate\Http\Response;
25+
use Illuminate\Support\Facades\Route;
26+
use PKP\components\forms\citation\PKPCitationEditForm;
27+
use PKP\core\PKPBaseController;
28+
use PKP\core\PKPRequest;
29+
use PKP\plugins\Hook;
30+
use PKP\security\authorization\ContextRequiredPolicy;
31+
use PKP\security\authorization\PolicySet;
32+
use PKP\security\authorization\RoleBasedHandlerOperationPolicy;
33+
use PKP\security\authorization\UserRolesRequiredPolicy;
34+
use PKP\security\Role;
35+
use PKP\services\PKPSchemaService;
36+
37+
class PKPCitationController extends PKPBaseController
38+
{
39+
/** @var int The default number of citations to return in one request */
40+
public const DEFAULT_COUNT = 30;
41+
42+
/** @var int The maximum number of citations to return in one request */
43+
public const MAX_COUNT = 100;
44+
45+
/**
46+
* @copydoc \PKP\core\PKPBaseController::getHandlerPath()
47+
*/
48+
public function getHandlerPath(): string
49+
{
50+
return 'citations';
51+
}
52+
53+
/**
54+
* @copydoc \PKP\core\PKPBaseController::getRouteGroupMiddleware()
55+
*/
56+
public function getRouteGroupMiddleware(): array
57+
{
58+
return [
59+
'has.user',
60+
'has.context',
61+
self::roleAuthorizer([
62+
Role::ROLE_ID_MANAGER,
63+
Role::ROLE_ID_SUB_EDITOR,
64+
Role::ROLE_ID_ASSISTANT,
65+
Role::ROLE_ID_AUTHOR,
66+
]),
67+
];
68+
}
69+
70+
/**
71+
* @copydoc \PKP\core\PKPBaseController::getGroupRoutes()
72+
*/
73+
public function getGroupRoutes(): void
74+
{
75+
Route::get('{citationId}', $this->get(...))
76+
->name('citation.getCitation')
77+
->whereNumber('citationId');
78+
79+
Route::get('', $this->getMany(...))
80+
->name('citation.getMany');
81+
82+
Route::post('', $this->edit(...))
83+
->name('citation.edit');
84+
85+
Route::get('{citationId}/_components/citationForm', $this->getCitationForm(...))
86+
->name('citation._components.citationForm');
87+
}
88+
89+
/**
90+
* @copydoc \PKP\core\PKPBaseController::authorize()
91+
*/
92+
public function authorize(PKPRequest $request, array &$args, array $roleAssignments): bool
93+
{
94+
$this->addPolicy(new UserRolesRequiredPolicy($request), true);
95+
96+
$rolePolicy = new PolicySet(PolicySet::COMBINING_PERMIT_OVERRIDES);
97+
98+
$this->addPolicy(new ContextRequiredPolicy($request));
99+
100+
foreach ($roleAssignments as $role => $operations) {
101+
$rolePolicy->addPolicy(new RoleBasedHandlerOperationPolicy($request, $role, $operations));
102+
}
103+
104+
$this->addPolicy($rolePolicy);
105+
106+
return parent::authorize($request, $args, $roleAssignments);
107+
}
108+
109+
/**
110+
* Get a single citation
111+
*/
112+
public function get(Request $illuminateRequest): JsonResponse
113+
{
114+
if (!Repo::citation()->exists((int)$illuminateRequest->route('citationId'))) {
115+
return response()->json([
116+
'error' => __('api.citations.404.citationNotFound')
117+
], Response::HTTP_OK);
118+
}
119+
120+
$citation = Repo::citation()->get((int)$illuminateRequest->route('citationId'));
121+
122+
return response()->json(Repo::citation()->getSchemaMap()->map($citation), Response::HTTP_OK);
123+
}
124+
125+
/**
126+
* Get a collection of citations
127+
*
128+
* @hook API::citations::params [[$collector, $illuminateRequest]]
129+
*/
130+
public function getMany(Request $illuminateRequest): JsonResponse
131+
{
132+
$collector = Repo::citation()->getCollector()
133+
->limit(self::DEFAULT_COUNT)
134+
->offset(0);
135+
136+
foreach ($illuminateRequest->query() as $param => $val) {
137+
switch ($param) {
138+
case 'count':
139+
$collector->limit(min((int)$val, self::MAX_COUNT));
140+
break;
141+
case 'offset':
142+
$collector->offset((int)$val);
143+
break;
144+
}
145+
}
146+
147+
Hook::call('API::citations::params', [$collector, $illuminateRequest]);
148+
149+
$citations = $collector->getMany();
150+
151+
return response()->json([
152+
'itemsMax' => $collector->getCount(),
153+
'items' => Repo::citation()->getSchemaMap()->summarizeMany($citations->values())->values(),
154+
], Response::HTTP_OK);
155+
}
156+
157+
158+
/**
159+
* Add or edit a citation
160+
*/
161+
public function edit(Request $illuminateRequest): JsonResponse
162+
{
163+
$params = $this->convertStringsToSchema(PKPSchemaService::SCHEMA_CITATION, $illuminateRequest->input());
164+
165+
$errors = Repo::citation()->validate(null, $params);
166+
167+
if (!empty($errors)) {
168+
return response()->json($errors, Response::HTTP_BAD_REQUEST);
169+
}
170+
171+
$citation = Repo::citation()->newDataObject($params);
172+
$id = Repo::citation()->updateOrInsert($citation);
173+
$citation = Repo::citation()->get($id);
174+
175+
return response()->json(
176+
Repo::citation()->getSchemaMap()->map($citation), Response::HTTP_OK
177+
);
178+
}
179+
180+
/**
181+
* Get Publication Reference/Citation Form component
182+
*/
183+
protected function getCitationForm(Request $illuminateRequest): JsonResponse
184+
{
185+
$citation = Repo::citation()->get((int)$illuminateRequest->route('citationId'));
186+
$publication = Repo::publication()->get($citation->getData('publicationId'));
187+
188+
if (!$citation) {
189+
return response()->json(
190+
[
191+
'error' => __('api.404.resourceNotFound')
192+
],
193+
Response::HTTP_NOT_FOUND
194+
);
195+
}
196+
197+
$publicationApiUrl = $this->getCitationApiUrl(
198+
$this->getRequest(),
199+
(int)$illuminateRequest->route('citationId'));
200+
201+
$citationForm = new PKPCitationEditForm($publicationApiUrl, (int)$illuminateRequest->route('citationId'));
202+
203+
return response()->json($citationForm->getConfig(), Response::HTTP_OK);
204+
}
205+
206+
/**
207+
* Get the url to the citation's API endpoint
208+
*/
209+
protected function getCitationApiUrl(PKPRequest $request, int $citationId): string
210+
{
211+
return $request
212+
->getDispatcher()
213+
->url(
214+
$request,
215+
Application::ROUTE_API,
216+
$request->getContext()->getPath(),
217+
'citations/' . $citationId
218+
);
219+
}
220+
}

api/v1/submissions/PKPSubmissionController.php

+1-1
Original file line numberDiff line numberDiff line change
@@ -34,8 +34,8 @@
3434
use Illuminate\Support\Facades\Route;
3535
use Illuminate\Support\LazyCollection;
3636
use PKP\affiliation\Affiliation;
37+
use PKP\components\forms\citation\PKPCitationsForm;
3738
use PKP\components\forms\FormComponent;
38-
use PKP\components\forms\publication\PKPCitationsForm;
3939
use PKP\components\forms\publication\PKPMetadataForm;
4040
use PKP\components\forms\publication\PKPPublicationIdentifiersForm;
4141
use PKP\components\forms\publication\PKPPublicationLicenseForm;

classes/citation/Citation.php

+34-67
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,10 @@
11
<?php
22

3-
/**
4-
* @defgroup citation Citation
5-
*/
6-
73
/**
84
* @file classes/citation/Citation.php
95
*
10-
* Copyright (c) 2014-2021 Simon Fraser University
11-
* Copyright (c) 2000-2021 John Willinsky
6+
* Copyright (c) 2014-2025 Simon Fraser University
7+
* Copyright (c) 2000-2025 John Willinsky
128
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
139
*
1410
* @class Citation
@@ -20,99 +16,70 @@
2016

2117
namespace PKP\citation;
2218

23-
class Citation extends \PKP\core\DataObject
24-
{
25-
/**
26-
* Constructor.
27-
*
28-
* @param string $rawCitation an unparsed citation string
29-
*/
30-
public function __construct($rawCitation = null)
31-
{
32-
parent::__construct();
33-
$this->setRawCitation($rawCitation);
34-
}
35-
36-
//
37-
// Getters and Setters
38-
//
39-
40-
/**
41-
* Replace URLs through HTML links, if the citation does not already contain HTML links
42-
*
43-
* @return string
44-
*/
45-
public function getCitationWithLinks()
46-
{
47-
$citation = $this->getRawCitation();
48-
if (stripos($citation, '<a href=') === false) {
49-
$citation = preg_replace_callback(
50-
'#(http|https|ftp)://[\d\w\.-]+\.[\w\.]{2,6}[^\s\]\[\<\>]*/?#',
51-
function ($matches) {
52-
$trailingDot = in_array($char = substr($matches[0], -1), ['.', ',']);
53-
$url = rtrim($matches[0], '.,');
54-
return "<a href=\"{$url}\">{$url}</a>" . ($trailingDot ? $char : '');
55-
},
56-
$citation
57-
);
58-
}
59-
return $citation;
60-
}
19+
use PKP\core\DataObject;
6120

21+
class Citation extends DataObject
22+
{
6223
/**
63-
* Get the rawCitation
64-
*
65-
* @return string
24+
* Get the rawCitation.
6625
*/
67-
public function getRawCitation()
26+
public function getRawCitation(): string
6827
{
6928
return $this->getData('rawCitation');
7029
}
7130

7231
/**
73-
* Set the rawCitation
32+
* Set the rawCitation.
7433
*/
75-
public function setRawCitation(?string $rawCitation)
34+
public function setRawCitation(?string $rawCitation): void
7635
{
77-
$rawCitation = $this->_cleanCitationString($rawCitation ?? '');
36+
$rawCitation = $this->cleanCitationString($rawCitation ?? '');
7837
$this->setData('rawCitation', $rawCitation);
7938
}
8039

8140
/**
8241
* Get the sequence number
83-
*
84-
* @return int
8542
*/
86-
public function getSequence()
43+
public function getSequence(): int
8744
{
8845
return $this->getData('seq');
8946
}
9047

9148
/**
9249
* Set the sequence number
93-
*
94-
* @param int $seq
9550
*/
96-
public function setSequence($seq)
51+
public function setSequence(int $seq): void
9752
{
9853
$this->setData('seq', $seq);
9954
}
10055

101-
//
102-
// Private methods
103-
//
56+
/**
57+
* Replace URLs through HTML links, if the citation does not already contain HTML links.
58+
*/
59+
public function getRawCitationWithLinks(): string
60+
{
61+
$rawCitationWithLinks = $this->getRawCitation();
62+
if (stripos($rawCitationWithLinks, '<a href=') === false) {
63+
$rawCitationWithLinks = preg_replace_callback(
64+
'#(http|https|ftp)://[\d\w\.-]+\.[\w\.]{2,6}[^\s\]\[\<\>]*/?#',
65+
function ($matches) {
66+
$trailingDot = in_array($char = substr($matches[0], -1), ['.', ',']);
67+
$url = rtrim($matches[0], '.,');
68+
return "<a href=\"{$url}\">{$url}</a>" . ($trailingDot ? $char : '');
69+
},
70+
$rawCitationWithLinks
71+
);
72+
}
73+
return $rawCitationWithLinks;
74+
}
75+
10476
/**
10577
* Take a citation string and clean/normalize it
10678
*/
107-
public function _cleanCitationString(string $citationString) : string
79+
public function cleanCitationString(?string $citationString = null): string|null
10880
{
109-
// 1) Strip slashes and whitespace
11081
$citationString = trim(stripslashes($citationString));
111-
112-
// 2) Normalize whitespace
113-
$citationString = preg_replace('/[\s]+/u', ' ', $citationString);
114-
115-
return $citationString;
82+
return preg_replace('/[\s]+/u', ' ', $citationString);
11683
}
11784
}
11885

0 commit comments

Comments
 (0)