File: /var/www/ojs/plugins/generic/citationStyleLanguage/CitationStyleLanguagePlugin.inc.php
<?php
/**
* @file plugins/generic/citationStyleLanguage/CitationStyleLanguagePlugin.inc.php
*
* Copyright (c) 2017-2020 Simon Fraser University
* Copyright (c) 2017-2020 John Willinsky
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
*
* @class CitationStyleLanguagePlugin
* @ingroup plugins_generic_citationStyleLanguage
*
* @brief Citation Style Language plugin class.
*/
import('lib.pkp.classes.plugins.GenericPlugin');
require_once(__DIR__ . '/lib/vendor/autoload.php');
use Seboettg\CiteProc\CiteProc;
class CitationStyleLanguagePlugin extends GenericPlugin {
/** @var array List of citation styles available */
public $_citationStyles = array();
/** @var array List of citation download formats available */
public $_citationDownloads = array();
/**
* @copydoc Plugin::getDisplayName()
*/
public function getDisplayName() {
return __('plugins.generic.citationStyleLanguage.displayName');
}
/**
* @copydoc Plugin::getDescription()
*/
public function getDescription() {
return __('plugins.generic.citationStyleLanguage.description');
}
/**
* @copydoc Plugin::register()
*/
public function register($category, $path, $mainContextId = null) {
$success = parent::register($category, $path, $mainContextId);
if (!Config::getVar('general', 'installed') || defined('RUNNING_UPGRADE')) return $success;
if ($success && $this->getEnabled($mainContextId)) {
HookRegistry::register('ArticleHandler::view', array($this, 'getArticleTemplateData'));
HookRegistry::register('LoadHandler', array($this, 'setPageHandler'));
}
return $success;
}
/**
* Get list of citation styles available
*
* @return array
*/
public function getCitationStyles() {
if (!empty($this->_citationStyles)) {
return $this->_citationStyles;
}
$defaults = array(
array(
'id' => 'acm-sig-proceedings',
'title' => __('plugins.generic.citationStyleLanguage.style.acm-sig-proceedings'),
'isEnabled' => true,
),
array(
'id' => 'acs-nano',
'title' => __('plugins.generic.citationStyleLanguage.style.acs-nano'),
'isEnabled' => true,
),
array(
'id' => 'apa',
'title' => __('plugins.generic.citationStyleLanguage.style.apa'),
'isEnabled' => true,
'isPrimary' => true,
),
array(
'id' => 'associacao-brasileira-de-normas-tecnicas',
'title' => __('plugins.generic.citationStyleLanguage.style.associacao-brasileira-de-normas-tecnicas'),
'isEnabled' => true,
),
array(
'id' => 'chicago-author-date',
'title' => __('plugins.generic.citationStyleLanguage.style.chicago-author-date'),
'isEnabled' => true,
),
array(
'id' => 'harvard-cite-them-right',
'title' => __('plugins.generic.citationStyleLanguage.style.harvard-cite-them-right'),
'isEnabled' => true,
),
array(
'id' => 'ieee',
'title' => __('plugins.generic.citationStyleLanguage.style.ieee'),
'isEnabled' => true,
),
array(
'id' => 'modern-language-association',
'title' => __('plugins.generic.citationStyleLanguage.style.modern-language-association'),
'isEnabled' => true,
),
array(
'id' => 'turabian-fullnote-bibliography',
'title' => __('plugins.generic.citationStyleLanguage.style.turabian-fullnote-bibliography'),
'isEnabled' => true,
),
array(
'id' => 'vancouver',
'title' => __('plugins.generic.citationStyleLanguage.style.vancouver'),
'isEnabled' => true,
),
);
// If hooking in to add a custom .csl file, add a `useCsl` key to your
// style definition with the path to the file.
HookRegistry::call('CitationStyleLanguage::citationStyleDefaults', array(&$defaults, $this));
$this->_citationStyles = $defaults;
return $this->_citationStyles;
}
/**
* Get the primary style name or default to the first available style
*
* @param $contextId integer Journal ID
* @return string
*/
public function getPrimaryStyleName($contextId = 0) {
$primaryStyleName = $this->getSetting($contextId, 'primaryCitationStyle');
if ($primaryStyleName) {
return $primaryStyleName;
}
$styles = $this->getCitationStyles();
$primaryStyles = array_filter($styles, function($style) {
return !empty($style['isPrimary']);
});
$primaryStyle = count($primaryStyles) ? array_shift($primaryStyles) : array_shift($styles);
return $primaryStyle['id'];
}
/**
* Get enabled citation styles
*
* @param $contextId integer Journal ID
* @return array
*/
public function getEnabledCitationStyles($contextId = 0) {
$styles = $this->getCitationStyles();
$enabled = $this->getSetting($contextId, 'enabledCitationStyles');
if (!is_array($enabled)) {
return array_filter($styles, function($style) {
return !empty($style['isEnabled']);
});
} else {
return array_filter($styles, function($style) use ($enabled) {
return in_array($style['id'], $enabled);
});
}
}
/**
* Get list of citation download formats available
*
* @return array
*/
public function getCitationDownloads() {
if (!empty($this->_citationDownloads)) {
return $this->_citationDownloads;
}
$defaults = array(
array(
'id' => 'ris',
'title' => __('plugins.generic.citationStyleLanguage.download.ris'),
'isEnabled' => true,
'useTemplate' => $this->getTemplateResource('citation-styles/ris.tpl'),
'fileExtension' => 'ris',
'contentType' => 'application/x-Research-Info-Systems',
),
array(
'id' => 'bibtex',
'title' => __('plugins.generic.citationStyleLanguage.download.bibtex'),
'isEnabled' => true,
'fileExtension' => 'bib',
'contentType' => 'application/x-bibtex',
),
);
// If hooking in to add a custom .csl file, add a `useCsl` key to your
// style definition with the path to the file.
HookRegistry::call('CitationStyleLanguage::citationDownloadDefaults', array(&$defaults, $this));
$this->_citationDownloads = $defaults;
return $this->_citationDownloads;
}
/**
* Get enabled citation styles
*
* @param $contextId integer Journal ID
* @return array
*/
public function getEnabledCitationDownloads($contextId = 0) {
$downloads = $this->getCitationDownloads();
$enabled = $this->getSetting($contextId, 'enabledCitationDownloads');
if (!is_array($enabled)) {
return array_filter($downloads, function($style) {
return !empty($style['isEnabled']);
});
} else {
return array_filter($downloads, function($style) use ($enabled) {
return in_array($style['id'], $enabled);
});
}
}
/**
* Pluck citation IDs from array of citations
*
* @param $citations array See getCitationStyles()
* @return array
*/
public function mapCitationIds($citations) {
return array_values(array_map(function($citation) { return $citation['id']; }, $citations));
}
/**
* Get citation config for a citation ID
*
* @param $styleId string Example: 'apa'
* @return array
*/
public function getCitationStyleConfig($styleId) {
$styleConfigs = array_merge($this->getCitationStyles(), $this->getCitationDownloads());
$styleConfig = array_filter($styleConfigs, function($styleConfig) use ($styleId) {
return $styleConfig['id'] === $styleId;
});
return array_shift($styleConfig);
}
/**
* Retrieve citation information for the article details template. This
* method is hooked in before a template displays.
*
* @see ArticleHandler::view()
* @param $hookname string
* @param $args array
* @return false
*/
public function getArticleTemplateData($hookName, $args) {
$request = $args[0];
$issue = $args[1];
$article = $args[2];
$publication = $args[3];
$context = $request->getContext();
$contextId = $context ? $context->getId() : 0;
$templateMgr = TemplateManager::getManager();
$citationArgs = array(
'submissionId' => $article->getId(),
'publicationId' => $publication->getId(),
);
$citationArgsJson = $citationArgs;
$citationArgsJson['return'] = 'json';
$templateMgr->assign(array(
'citation' => $this->getCitation($request, $article, $this->getPrimaryStyleName($contextId), $issue, $publication),
'citationArgs' => $citationArgs,
'citationArgsJson' => $citationArgsJson,
'citationStyles' => $this->getEnabledCitationStyles($contextId),
'citationDownloads' => $this->getEnabledCitationDownloads($contextId),
));
$templateMgr->addJavaScript(
'citationStyleLanguage',
$request->getBaseUrl() . '/' . $this->getPluginPath() . '/js/articleCitation.js'
);
return false;
}
/**
* Get a specified citation for a given article
*
* This citation format follows the csl-json schema and takes some direction
* from existing CSL mappings documented by Zotero and Mendeley.
*
* @see CSL-json schema https://github.com/citation-style-language/schema#csl-json-schema
* @see Zotero's mappings https://aurimasv.github.io/z2csl/typeMap.xml#map-journalArticle
* @see Mendeley's mappings http://support.mendeley.com/customer/portal/articles/364144-csl-type-mapping
* @param $request Request
* @param $article Submission
* @param $citationStyle string Name of the citation style to use.
* @param $issue Issue Optional. Will fetch from db if not passed.
* @param $publication Publication Optional. A particular version
* @return string
*/
public function getCitation($request, $article, $citationStyle = 'apa', $issue = null, $publication = null) {
$publication = $publication ?? $article->getCurrentPublication();
$issueDao = DAORegistry::getDAO('IssueDAO'); /* @var $issueDao IssueDAO */
$issue = $issue ?? $issueDao->getById($publication->getData('issueId'));
$context = $request->getContext();
import('lib.pkp.classes.core.PKPString');
$citationData = new stdClass();
$citationData->type = 'article-journal';
$citationData->id = $article->getId();
$citationData->title = $publication->getLocalizedFullTitle();
$citationData->{'container-title'} = $context->getLocalizedName();
$citationData->{'publisher-place'} = $this->getSetting($context->getId(), 'publisherLocation');
$citationData->abstract = htmlspecialchars($publication->getLocalizedData('abstract'));
$abbreviation = $context->getData('abbreviation', $context->getPrimaryLocale()) ?? $context->getData('acronym', $context->getPrimaryLocale());
if ($abbreviation) $citationData->{'container-title-short'} = htmlspecialchars($abbreviation);
if ($issue) {
$citationData->volume = htmlspecialchars($issue->getData('volume'));
// Zotero prefers issue and Mendeley uses `number` to store revisions
$citationData->issue = htmlspecialchars($issue->getData('number'));
}
$sectionDao = DAORegistry::getDAO('SectionDAO'); /** @var $sectionDao SectionDAO */
if ($sectionId = $publication->getData('sectionId')) {
$section = $sectionDao->getById($sectionId);
if ($section && !$section->getHideTitle()) $citationData->section = htmlspecialchars($section->getTitle($context->getPrimaryLocale()));
}
$citationData->URL = $request->getDispatcher()->url(
$request,
ROUTE_PAGE,
null,
'article',
'view',
$article->getBestId()
);
$citationData->accessed = new stdClass();
$citationData->accessed->raw = date('Y-m-d');
$authors = $publication->getData('authors');
if (count($authors)) {
$citationData->author = array();
foreach ($authors as $author) {
$currentAuthor = new stdClass();
if (empty($author->getLocalizedFamilyName())) {
$currentAuthor->family = htmlspecialchars($author->getLocalizedGivenName());
} else {
$currentAuthor->family = htmlspecialchars($author->getLocalizedFamilyName());
$currentAuthor->given = htmlspecialchars($author->getLocalizedGivenName());
}
$citationData->author[] = $currentAuthor;
}
}
if ($publication->getData('datePublished')) {
$citationData->issued = new stdClass();
$citationData->issued->raw = htmlspecialchars($publication->getData('datePublished'));
$publishedPublications = $article->getPublishedPublications();
if (count($publishedPublications) > 1) {
$originalPublication = array_reduce($publishedPublications, function($a, $b) {
return $a && $a->getId() < $b->getId() ? $a : $b;
});
$originalDate = $originalPublication->getData('datePublished');
if ($originalDate && $originalDate !== $publication->getData('datePublished')) {
$citationData->{'original-date'} = new stdClass();
$citationData->{'original-date'}->raw = htmlspecialchars($originalPublication->getData('datePublished'));
}
}
} elseif ($issue && $issue->getPublished()) {
$citationData->issued = new stdClass();
$citationData->issued->raw = htmlspecialchars($issue->getDatePublished());
}
if ($publication->getData('pages')) {
$citationData->page = htmlspecialchars($publication->getData('pages'));
}
HookRegistry::call('CitationStyleLanguage::citation', array(&$citationData, &$citationStyle, $article, $issue, $context, $publication));
$citation = '';
// Determine whether to use citeproc-php or a custom template to render
// the citation
$styleConfig = $this->getCitationStyleConfig($citationStyle);
if (!empty($styleConfig)) {
if (!empty($styleConfig['useTemplate'])) {
$templateMgr = TemplateManager::getManager($request);
$templateMgr->assign(array(
'citationData' => $citationData,
'citationStyle' => $citationStyle,
'article' => $article,
'publication' => $publication,
'issue' => $issue,
'journal' => $context,
));
$citation = $templateMgr->fetch($styleConfig['useTemplate']);
} else {
$style = $this->loadStyle($styleConfig);
if ($style) {
// Determine what locale to use. Try in order:
// - xx_YY
// - xx
// Fall back English if none found.
$tryLocale = null;
foreach (array(
str_replace('_', '-', substr(AppLocale::getLocale(), 0, 5)),
substr(AppLocale::getLocale(), 0, 2),
'en-US'
) as $tryLocale) {
if (file_exists(dirname(__FILE__) . '/lib/vendor/citation-style-language/locales/locales-' . $tryLocale . '.xml')) break;
}
$citeProc = new CiteProc($style, $tryLocale);
$citation = $citeProc->render(array($citationData), 'bibliography');
}
}
}
return $citation;
}
/**
* Load a CSL style and return the contents as a string
*
* @param $styleConfig array CSL configuration to load
*/
public function loadStyle($styleConfig) {
if (!empty($styleConfig['useCsl'])) {
return file_get_contents($styleConfig['useCsl']);
} else {
return file_get_contents($this->getPluginPath() . '/citation-styles/' . $styleConfig['id'] . '.csl');
}
}
/**
* Download a citation format
*
* Downloadable citation formats can be used to import into third-party
* software.
*
* @param $request Request
* @param $article Submission
* @param $citationStyle string Name of the citation style to use.
* @param $issue Issue Optional. Will fetch from db if not passed.
* @return string
*/
public function downloadCitation($request, $article, $citationStyle = 'ris', $issue = null) {
$journal = $request->getContext();
if (empty($issue)) {
$issueDao = DAORegistry::getDAO('IssueDAO'); /* @var $issueDao IssueDAO */
$issue = $issueDao->getById($article->getCurrentPublication()->getData('issueId'));
}
$styleConfig = $this->getCitationStyleConfig($citationStyle);
if (empty($styleConfig)) {
return false;
}
$citation = trim(strip_tags($this->getCitation($request, $article, $citationStyle, $issue)));
// TODO this is likely going to cause an error in a citation some day,
// but is necessary to get the .ris downloadable format working. The
// CSL language doesn't seem to offer a way to indicate a line break.
// See: https://github.com/citation-style-language/styles/issues/2831
$citation = str_replace('\n', "\n", $citation);
$encodedFilename = urlencode(substr($article->getLocalizedTitle(), 0, 60)) . '.' . $styleConfig['fileExtension'];
header("Content-Disposition: attachment; filename*=UTF-8''\"$encodedFilename\"");
header('Content-Type: ' . $styleConfig['contentType']);
echo $citation;
exit;
}
/**
* @see Plugin::getActions()
*/
public function getActions($request, $actionArgs) {
$actions = parent::getActions($request, $actionArgs);
if (!$this->getEnabled()) {
return $actions;
}
$router = $request->getRouter();
import('lib.pkp.classes.linkAction.request.AjaxModal');
$linkAction = new LinkAction(
'settings',
new AjaxModal(
$router->url(
$request,
null,
null,
'manage',
null,
array(
'verb' => 'settings',
'plugin' => $this->getName(),
'category' => 'generic'
)
),
$this->getDisplayName()
),
__('manager.plugins.settings'),
null
);
array_unshift($actions, $linkAction);
return $actions;
}
/**
* @see Plugin::manage()
*/
public function manage($args, $request) {
switch ($request->getUserVar('verb')) {
case 'settings':
$this->import('CitationStyleLanguageSettingsForm');
$form = new CitationStyleLanguageSettingsForm($this);
if ($request->getUserVar('save')) {
$form->readInputData();
if ($form->validate()) {
$form->execute();
return new JSONMessage(true);
}
}
$form->initData();
return new JSONMessage(true, $form->fetch($request));
}
return parent::manage($args, $request);
}
/**
* Route requests for the citation styles to custom page handler
*
* @see PKPPageRouter::route()
* @param $hookName string
* @param $params array
*/
public function setPageHandler($hookName, $params) {
$page = $params[0];
if ($this->getEnabled() && $page === 'citationstylelanguage') {
$this->import('pages/CitationStyleLanguageHandler');
define('HANDLER_CLASS', 'CitationStyleLanguageHandler');
return true;
}
return false;
}
}