File: /var/www/ojs/pages/article/ArticleHandler.inc.php
<?php
/**
* @file pages/article/ArticleHandler.inc.php
*
* Copyright (c) 2014-2021 Simon Fraser University
* Copyright (c) 2003-2021 John Willinsky
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
*
* @class ArticleHandler
* @ingroup pages_article
*
* @brief Handle requests for article functions.
*
*/
import('classes.handler.Handler');
use \Firebase\JWT\JWT;
class ArticleHandler extends Handler {
/** context associated with the request **/
var $context;
/** issue associated with the request **/
var $issue;
/** submission associated with the request **/
var $article;
/** category associated with the request **/
var $categories;
/** publication associated with the request **/
var $publication;
/** galley associated with the request **/
var $galley;
/** fileId associated with the request **/
var $fileId;
/**
* @copydoc PKPHandler::authorize()
*/
function authorize($request, &$args, $roleAssignments) {
// Permit the use of the Authorization header and an API key for access to unpublished/subscription content
if ($header = array_search('Authorization', array_flip(getallheaders()))) {
list($bearer, $jwt) = explode(' ', $header);
if (strcasecmp($bearer, 'Bearer') == 0 && !empty($jwt)) {
$secret = Config::getVar('security', 'api_key_secret', '');
if (!$secret) {
AppLocale::requireComponents(LOCALE_COMPONENT_PKP_API);
$templateMgr = TemplateManager::getManager($request);
$templateMgr->assign('message', 'api.500.apiSecretKeyMissing');
return $templateMgr->display('frontend/pages/message.tpl');
}
try {
$apiToken = JWT::decode($jwt, $secret, array('HS256'));
// Compatibility with old API keys
// https://github.com/pkp/pkp-lib/issues/6462
if (substr($apiToken, 0, 2) === '""') {
$apiToken = json_decode($apiToken);
}
$this->setApiToken($apiToken);
} catch (Exception $e) {
AppLocale::requireComponents(LOCALE_COMPONENT_PKP_API);
$templateMgr = TemplateManager::getManager($request);
$templateMgr->assign('message', 'api.400.invalidApiToken');
return $templateMgr->display('frontend/pages/message.tpl');
}
}
}
import('lib.pkp.classes.security.authorization.ContextRequiredPolicy');
$this->addPolicy(new ContextRequiredPolicy($request));
import('classes.security.authorization.OjsJournalMustPublishPolicy');
$this->addPolicy(new OjsJournalMustPublishPolicy($request));
return parent::authorize($request, $args, $roleAssignments);
}
/**
* @see PKPHandler::initialize()
* @param $request Request
* @param $args array Arguments list
*/
function initialize($request, $args = array()) {
$urlPath = empty($args) ? 0 : array_shift($args);
// Get the submission that matches the requested urlPath
$submission = Services::get('submission')->getByUrlPath($urlPath, $request->getContext()->getId());
if (!$submission && ctype_digit((string) $urlPath)) {
$submission = Services::get('submission')->get($urlPath);
if (!$submission || $request->getContext()->getId() != $submission->getContextId()) $submission = null;
}
import('classes.issue.IssueAction');
$issueAction = new IssueAction();
$context = $request->getContext();
$user = $request->getUser();
if (!$submission || ($submission->getData('status') !== STATUS_PUBLISHED && !$issueAction->allowedPrePublicationAccess($context, $submission, $user))) {
$request->getDispatcher()->handle404();
}
// If the urlPath does not match the urlPath of the current
// publication, redirect to the current URL
$currentUrlPath = $submission->getBestId();
if ($currentUrlPath && $currentUrlPath != $urlPath) {
$newArgs = array_merge([$currentUrlPath], $args);
$request->redirect(null, $request->getRequestedPage(), $request->getRequestedOp(), $newArgs);
}
$this->article = $submission;
// Get the requested publication or if none requested get the current publication
$subPath = empty($args) ? 0 : array_shift($args);
if ($subPath === 'version') {
$publicationId = (int) array_shift($args);
$galleyId = empty($args) ? 0 : array_shift($args);
foreach ((array) $this->article->getData('publications') as $publication) {
if ($publication->getId() === $publicationId) {
$this->publication = $publication;
}
}
if (!$this->publication) {
$request->getDispatcher()->handle404();
}
} else {
$this->publication = $this->article->getCurrentPublication();
$galleyId = $subPath;
}
if ($this->publication->getData('status') !== STATUS_PUBLISHED && !$issueAction->allowedPrePublicationAccess($context, $submission, $user)) {
$request->getDispatcher()->handle404();
}
if ($galleyId && in_array($request->getRequestedOp(), ['view', 'download'])) {
$galleys = (array) $this->publication->getData('galleys');
foreach ($galleys as $galley) {
if ($galley->getBestGalleyId() == $galleyId) {
$this->galley = $galley;
break;
}
}
// Redirect to the most recent version of the submission if the request
// points to an outdated galley but doesn't use the specific versioned
// URL. This can happen when a galley's urlPath is changed between versions.
if (!$this->galley) {
$publications = $submission->getPublishedPublications();
foreach ($publications as $publication) {
foreach ((array) $publication->getData('galleys') as $galley) {
if ($galley->getBestGalleyId() == $galleyId) {
$request->redirect(null, $request->getRequestedPage(), $request->getRequestedOp(), [$submission->getBestId()]);
// In some cases, a URL to a galley may use the ID when it should use
// the urlPath. Redirect to the galley's correct URL.
} elseif (ctype_digit($galleyId) && $galley->getId() == $galleyId) {
$request->redirect(null, $request->getRequestedPage(), $request->getRequestedOp(), [$submission->getBestId(), $galley->getBestGalleyId()]);
}
}
}
$request->getDispatcher()->handle404();
}
// Store the file id if it exists
if (!empty($args)) {
$this->fileId = array_shift($args);
}
}
if ($this->publication->getData('issueId')) {
$issueDao = DAORegistry::getDAO('IssueDAO'); /* @var $issueDao IssueDAO */
$this->issue = $issueDao->getById($this->publication->getData('issueId'), $submission->getData('contextId'), true);
}
}
/**
* View Article. (Either article landing page or galley view.)
* @param $args array
* @param $request Request
*/
function view($args, $request) {
$context = $request->getContext();
$user = $request->getUser();
$issue = $this->issue;
$article = $this->article;
$publication = $this->publication;
$templateMgr = TemplateManager::getManager($request);
$templateMgr->assign(array(
'issue' => $issue,
'article' => $article,
'publication' => $publication,
'firstPublication' => reset($article->getData('publications')),
'currentPublication' => $article->getCurrentPublication(),
'galley' => $this->galley,
'fileId' => $this->fileId,
));
$this->setupTemplate($request);
$sectionDao = DAORegistry::getDAO('SectionDAO'); /* @var $sectionDao SectionDAO */
$templateMgr->assign([
'ccLicenseBadge' => Application::get()->getCCLicenseBadge($publication->getData('licenseUrl')),
'publication' => $publication,
'section' => $sectionDao->getById($publication->getData('sectionId')),
]);
if ($this->galley && !$this->userCanViewGalley($request, $article->getId(), $this->galley->getId())) {
fatalError('Cannot view galley.');
}
$categoryDao = DAORegistry::getDAO('CategoryDAO'); /* @var $categoryDao CategoryDAO */
$templateMgr->assign([
'categories' => $categoryDao->getByPublicationId($publication->getId())->toArray()
]);
// Get galleys sorted into primary and supplementary groups
$galleys = $publication->getData('galleys');
$primaryGalleys = array();
$supplementaryGalleys = array();
if ($galleys) {
$genreDao = DAORegistry::getDAO('GenreDAO'); /* @var $genreDao GenreDAO */
$primaryGenres = $genreDao->getPrimaryByContextId($context->getId())->toArray();
$primaryGenreIds = array_map(function($genre) {
return $genre->getId();
}, $primaryGenres);
$supplementaryGenres = $genreDao->getBySupplementaryAndContextId(true, $context->getId())->toArray();
$supplementaryGenreIds = array_map(function($genre) {
return $genre->getId();
}, $supplementaryGenres);
foreach ($galleys as $galley) {
$remoteUrl = $galley->getRemoteURL();
$file = $galley->getFile();
if (!$remoteUrl && !$file) {
continue;
}
if ($remoteUrl || in_array($file->getGenreId(), $primaryGenreIds)) {
$primaryGalleys[] = $galley;
} elseif (in_array($file->getGenreId(), $supplementaryGenreIds)) {
$supplementaryGalleys[] = $galley;
}
}
}
$templateMgr->assign(array(
'primaryGalleys' => $primaryGalleys,
'supplementaryGalleys' => $supplementaryGalleys,
));
// Citations
if ($publication->getData('citationsRaw')) {
$citationDao = DAORegistry::getDAO('CitationDAO'); /* @var $citationDao CitationDAO */
$parsedCitations = $citationDao->getByPublicationId($publication->getId());
$templateMgr->assign([
'parsedCitations' => $parsedCitations->toArray(),
]);
}
// Assign deprecated values to the template manager for
// compatibility with older themes
$templateMgr->assign([
'licenseTerms' => $context->getLocalizedData('licenseTerms'),
'licenseUrl' => $publication->getData('licenseUrl'),
'copyrightHolder' => $publication->getLocalizedData('copyrightHolder'),
'copyrightYear' => $publication->getData('copyrightYear'),
'pubIdPlugins' => PluginRegistry::loadCategory('pubIds', true),
'keywords' => $publication->getData('keywords'),
]);
// Fetch and assign the galley to the template
if ($this->galley && $this->galley->getRemoteURL()) $request->redirectUrl($this->galley->getRemoteURL());
if (empty($this->galley)) {
// No galley: Prepare the article landing page.
// Ask robots not to index outdated versions and point to the canonical url for the latest version
if ($publication->getId() !== $article->getCurrentPublication()->getId()) {
$templateMgr->addHeader('noindex', '<meta name="robots" content="noindex">');
$url = $request->getDispatcher()->url($request, ROUTE_PAGE, null, 'article', 'view', $article->getBestId());
$templateMgr->addHeader('canonical', '<link rel="canonical" href="' . $url . '">');
}
// Get the subscription status if displaying the abstract;
// if access is open, we can display links to the full text.
import('classes.issue.IssueAction');
// The issue may not exist, if this is an editorial user
// and scheduling hasn't been completed yet for the article.
$issueAction = new IssueAction();
$subscriptionRequired = false;
if ($issue) {
$subscriptionRequired = $issueAction->subscriptionRequired($issue, $context);
}
$subscribedUser = $issueAction->subscribedUser($user, $context, isset($issue) ? $issue->getId() : null, isset($article) ? $article->getId() : null);
$subscribedDomain = $issueAction->subscribedDomain($request, $context, isset($issue) ? $issue->getId() : null, isset($article) ? $article->getId() : null);
$completedPaymentDao = DAORegistry::getDAO('OJSCompletedPaymentDAO'); /* @var $completedPaymentDao OJSCompletedPaymentDAO */
$templateMgr->assign('hasAccess',
!$subscriptionRequired ||
$publication->getData('accessStatus') == ARTICLE_ACCESS_OPEN ||
$subscribedUser || $subscribedDomain ||
($user && $issue && $completedPaymentDao->hasPaidPurchaseIssue($user->getId(), $issue->getId())) ||
($user && $completedPaymentDao->hasPaidPurchaseArticle($user->getId(), $article->getId()))
);
$paymentManager = Application::get()->getPaymentManager($context);
if ( $paymentManager->onlyPdfEnabled() ) {
$templateMgr->assign('restrictOnlyPdf', true);
}
if ( $paymentManager->purchaseArticleEnabled() ) {
$templateMgr->assign('purchaseArticleEnabled', true);
}
if (!HookRegistry::call('ArticleHandler::view', array(&$request, &$issue, &$article, $publication))) {
return $templateMgr->display('frontend/pages/article.tpl');
}
} else {
// Ask robots not to index outdated versions
if ($publication->getId() !== $article->getCurrentPublication()->getId()) {
$templateMgr->addHeader('noindex', '<meta name="robots" content="noindex">');
}
// Galley: Prepare the galley file download.
if (!HookRegistry::call('ArticleHandler::view::galley', array(&$request, &$issue, &$this->galley, &$article, $publication))) {
if ($this->publication->getId() !== $this->article->getCurrentPublication()->getId()) {
$redirectPath = [
$article->getBestId(),
'version',
$publication->getId(),
$this->galley->getBestGalleyId()
];
} else {
$redirectPath = [
$article->getBestId(),
$this->galley->getBestGalleyId()
];
}
$request->redirect(null, null, 'download', $redirectPath);
}
}
}
/**
* Download an article file
* For deprecated OJS 2.x URLs; see https://github.com/pkp/pkp-lib/issues/1541
* @param $args array
* @param $request PKPRequest
*/
function viewFile($args, $request) {
$articleId = isset($args[0]) ? $args[0] : 0;
$galleyId = isset($args[1]) ? $args[1] : 0;
$fileId = isset($args[2]) ? (int) $args[2] : 0;
header('HTTP/1.1 301 Moved Permanently');
$request->redirect(null, null, 'download', array($articleId, $galleyId, $fileId));
}
/**
* Download a supplementary file.
* For deprecated OJS 2.x URLs; see https://github.com/pkp/pkp-lib/issues/1541
* @param $args array
* @param $request PKPRequest
*/
function downloadSuppFile($args, $request) {
$articleId = isset($args[0]) ? $args[0] : 0;
$article = Services::get('submission')->get($articleId);
if (!$article) {
$dispatcher = $request->getDispatcher();
$dispatcher->handle404();
}
$suppId = isset($args[1]) ? $args[1] : 0;
$submissionFilesIterator = Services::get('submissionFile')->getMany([
'submissionIds' => [$article->getId()],
]);
foreach ($submissionFilesIterator as $submissionFile) {
if ($submissionFile->getData('old-supp-id') == $suppId) {
$articleGalleyDao = DAORegistry::getDAO('ArticleGalleyDAO'); /* @var $articleGalleyDao ArticleGalleyDAO */
$articleGalleys = $articleGalleyDao->getByPublicationId($article->getCurrentPublication()->getId());
while ($articleGalley = $articleGalleys->next()) {
$galleyFile = $articleGalley->getFile();
if ($galleyFile && $galleyFile->getFileId() == $submissionFile->getId()) {
header('HTTP/1.1 301 Moved Permanently');
$request->redirect(null, null, 'download', array($articleId, $articleGalley->getId(), $submissionFile->getId()));
}
}
}
}
$dispatcher = $request->getDispatcher();
$dispatcher->handle404();
}
/**
* Download an article file
* @param array $args
* @param PKPRequest $request
*/
function download($args, $request) {
if (!isset($this->galley)) $request->getDispatcher()->handle404();
if ($this->galley->getRemoteURL()) $request->redirectUrl($this->galley->getRemoteURL());
else if ($this->userCanViewGalley($request, $this->article->getId(), $this->galley->getId())) {
if (!$this->fileId) {
$this->fileId = $this->galley->getData('submissionFileId');
}
// If no file ID could be determined, treat it as a 404.
if (!$this->fileId) $request->getDispatcher()->handle404();
// If the file ID is not the galley's file ID, ensure it is a dependent file, or else 404.
if ($this->fileId != $this->galley->getData('submissionFileId')) {
import('lib.pkp.classes.submission.SubmissionFile'); // Constants
$dependentFileIds = Services::get('submissionFile')->getIds([
'assocTypes' => [ASSOC_TYPE_SUBMISSION_FILE],
'assocIds' => [$this->galley->getFileId()],
'fileStages' => [SUBMISSION_FILE_DEPENDENT],
'includeDependentFiles' => true,
]);
if (!in_array($this->fileId, $dependentFileIds)) $request->getDispatcher()->handle404();
}
if (!HookRegistry::call('ArticleHandler::download', array($this->article, &$this->galley, &$this->fileId))) {
$submissionFile = Services::get('submissionFile')->get($this->fileId);
if (!Services::get('file')->fs->has($submissionFile->getData('path'))) {
$request->getDispatcher()->handle404();
}
$filename = Services::get('file')->formatFilename($submissionFile->getData('path'), $submissionFile->getLocalizedData('name'));
$returner = true;
HookRegistry::call('FileManager::downloadFileFinished', array(&$returner));
Services::get('file')->download($submissionFile->getData('fileId'), $filename);
}
} else {
header('HTTP/1.0 403 Forbidden');
echo '403 Forbidden<br>';
}
}
/**
* Determines whether a user can view this article galley or not.
* @param $request Request
* @param $articleId string
* @param $galleyId int or string
*/
function userCanViewGalley($request, $articleId, $galleyId = null) {
import('classes.issue.IssueAction');
$issueAction = new IssueAction();
$context = $request->getContext();
$submission = $this->article;
$issue = $this->issue;
$contextId = $context->getId();
$user = $request->getUser();
$userId = $user?$user->getId():0;
// If this is an editorial user who can view unpublished/unscheduled
// articles, bypass further validation. Likewise for its author.
if ($submission && $issueAction->allowedPrePublicationAccess($context, $submission, $user)) {
return true;
}
// Make sure the reader has rights to view the article/issue.
if ($issue && $issue->getPublished() && $submission->getStatus() == STATUS_PUBLISHED) {
$subscriptionRequired = $issueAction->subscriptionRequired($issue, $context);
$isSubscribedDomain = $issueAction->subscribedDomain($request, $context, $issue->getId(), $submission->getId());
// Check if login is required for viewing.
if (!$isSubscribedDomain && !Validation::isLoggedIn() && $context->getData('restrictArticleAccess') && isset($galleyId) && $galleyId) {
Validation::redirectLogin();
}
// bypass all validation if subscription based on domain or ip is valid
// or if the user is just requesting the abstract
if ( (!$isSubscribedDomain && $subscriptionRequired) && (isset($galleyId) && $galleyId) ) {
// Subscription Access
$subscribedUser = $issueAction->subscribedUser($user, $context, $issue->getId(), $submission->getId());
import('classes.payment.ojs.OJSPaymentManager');
$paymentManager = Application::get()->getPaymentManager($context);
$purchasedIssue = false;
if (!$subscribedUser && $paymentManager->purchaseIssueEnabled()) {
$completedPaymentDao = DAORegistry::getDAO('OJSCompletedPaymentDAO'); /* @var $completedPaymentDao OJSCompletedPaymentDAO */
$purchasedIssue = $completedPaymentDao->hasPaidPurchaseIssue($userId, $issue->getId());
}
if (!(!$subscriptionRequired || $submission->getCurrentPublication()->getData('accessStatus') == ARTICLE_ACCESS_OPEN || $subscribedUser || $purchasedIssue)) {
if ( $paymentManager->purchaseArticleEnabled() || $paymentManager->membershipEnabled() ) {
/* if only pdf files are being restricted, then approve all non-pdf galleys
* and continue checking if it is a pdf galley */
if ( $paymentManager->onlyPdfEnabled() ) {
if ($this->galley && !$this->galley->isPdfGalley() ) {
$this->issue = $issue;
$this->article = $submission;
return true;
}
}
if (!Validation::isLoggedIn()) {
Validation::redirectLogin('payment.loginRequired.forArticle');
}
/* if the article has been paid for then forget about everything else
* and just let them access the article */
$completedPaymentDao = DAORegistry::getDAO('OJSCompletedPaymentDAO'); /* @var $completedPaymentDao OJSCompletedPaymentDAO */
$dateEndMembership = $user->getSetting('dateEndMembership', 0);
if ($completedPaymentDao->hasPaidPurchaseArticle($userId, $submission->getId())
|| (!is_null($dateEndMembership) && $dateEndMembership > time())) {
$this->issue = $issue;
$this->article = $submission;
return true;
} elseif ($paymentManager->purchaseArticleEnabled()) {
$queuedPayment = $paymentManager->createQueuedPayment($request, PAYMENT_TYPE_PURCHASE_ARTICLE, $user->getId(), $submission->getId(), $context->getData('purchaseArticleFee'));
$paymentManager->queuePayment($queuedPayment);
$paymentForm = $paymentManager->getPaymentForm($queuedPayment);
$paymentForm->display($request);
exit;
}
}
if (!isset($galleyId) || $galleyId) {
if (!Validation::isLoggedIn()) {
Validation::redirectLogin('reader.subscriptionRequiredLoginText');
}
$request->redirect(null, 'about', 'subscriptions');
}
}
}
} else {
$request->redirect(null, 'search');
}
return true;
}
/**
* Set up the template. (Load required locale components.)
* @param $request PKPRequest
*/
function setupTemplate($request) {
parent::setupTemplate($request);
AppLocale::requireComponents(LOCALE_COMPONENT_PKP_READER, LOCALE_COMPONENT_PKP_SUBMISSION, LOCALE_COMPONENT_APP_SUBMISSION);
}
}