Skip to content

Commit a83fe8e

Browse files
RC for OJS 3.4
1 parent b3bde11 commit a83fe8e

File tree

7 files changed

+504
-514
lines changed

7 files changed

+504
-514
lines changed
Original file line numberDiff line numberDiff line change
@@ -1,165 +1,170 @@
1-
<?php
2-
3-
/**
4-
* @file plugins/generic/ashSecurityHeaders/SecurityHeadersPlugin.inc.php
5-
*
6-
* Copyright (c) 2021-2025 AshVisualTheme
7-
* Copyright (c) 2014-2025 Simon Fraser University
8-
* Copyright (c) 2003-2025 John Willinsky
9-
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
10-
*
11-
* @class SecurityHeadersPlugin
12-
* @brief Main class for the Security Headers plugin.
13-
*/
14-
15-
import('lib.pkp.classes.core.PKPApplication');
16-
import('lib.pkp.classes.core.JSONMessage');
17-
import('lib.pkp.classes.linkAction.LinkAction');
18-
import('lib.pkp.classes.linkAction.request.AjaxModal');
19-
import('lib.pkp.classes.plugins.GenericPlugin');
20-
import('lib.pkp.classes.plugins.HookRegistry');
21-
22-
import('plugins.generic.ashSecurityHeaders.SecurityHeadersSettingsForm');
23-
24-
class SecurityHeadersPlugin extends GenericPlugin
25-
{
26-
public function register($category, $path, $mainContextId = null)
27-
{
28-
$success = parent::register($category, $path, $mainContextId);
29-
if ($success && $this->getEnabled()) {
30-
HookRegistry::register('Dispatcher::dispatch', [$this, 'addSecurityHeaders']);
31-
}
32-
return $success;
33-
}
34-
35-
public function getDisplayName()
36-
{
37-
return __('plugins.generic.ashSecurityHeaders.displayName');
38-
}
39-
40-
public function getDescription()
41-
{
42-
return __('plugins.generic.ashSecurityHeaders.description');
43-
}
44-
45-
public function isSitePlugin()
46-
{
47-
if (!$this->getRequest()->getContext()) {
48-
return true;
49-
}
50-
return false;
51-
}
52-
53-
public function getActions($request, $actionArgs)
54-
{
55-
$actions = parent::getActions($request, $actionArgs);
56-
if (!$this->getEnabled()) {
57-
return $actions;
58-
}
59-
60-
$router = $request->getRouter();
61-
$linkAction = new LinkAction(
62-
'settings',
63-
new AjaxModal(
64-
$router->url(
65-
$request,
66-
null,
67-
null,
68-
'manage',
69-
null,
70-
['verb' => 'settings', 'plugin' => $this->getName(), 'category' => 'generic']
71-
),
72-
$this->getDisplayName()
73-
),
74-
__('manager.plugins.settings'),
75-
null
76-
);
77-
array_unshift($actions, $linkAction);
78-
return $actions;
79-
}
80-
81-
public function manage($args, $request)
82-
{
83-
switch ($request->getUserVar('verb')) {
84-
case 'settings':
85-
$form = new SecurityHeadersSettingsForm($this);
86-
87-
if (!$request->getUserVar('save')) {
88-
$form->initData();
89-
return new JSONMessage(true, $form->fetch($request));
90-
}
91-
92-
$form->readInputData();
93-
if ($form->validate()) {
94-
$form->execute();
95-
return new JSONMessage(true);
96-
}
97-
}
98-
return parent::manage($args, $request);
99-
}
100-
101-
public function getDefaultHeaders()
102-
{
103-
return [
104-
'X-Frame-Options' => 'SAMEORIGIN',
105-
'X-Content-Type-Options' => 'nosniff',
106-
'X-XSS-Protection' => '1; mode=block',
107-
'Content-Security-Policy' => "upgrade-insecure-requests;",
108-
'Cross-Origin-Embedder-Policy' => "same-origin; report-to='default'",
109-
'Cross-Origin-Opener-Policy' => 'require-corp',
110-
'Cross-Origin-Resource-Policy' => 'same-origin',
111-
'Permissions-Policy' => "accelerometer=(), camera=(), geolocation=(), gyroscope=(), magnetometer=(), microphone=(), usb=(), fullscreen=(self)",
112-
'Referrer-Policy' => 'strict-origin-when-cross-origin',
113-
'Strict-Transport-Security' => 'max-age=63072000; includeSubDomains; preload',
114-
];
115-
}
116-
117-
public function addSecurityHeaders($hookName, $params)
118-
{
119-
120-
if (defined('SESSION_DISABLE_INIT') || php_sapi_name() === 'cli' || headers_sent()) {
121-
return false;
122-
}
123-
124-
$defaultHeaders = $this->getDefaultHeaders();
125-
$request = PKPApplication::get()->getRequest();
126-
$context = $request->getContext();
127-
$contextId = $context ? $context->getId() : CONTEXT_SITE;
128-
129-
$settingMap = [
130-
'X-Frame-Options' => 'headerXfo',
131-
'X-Content-Type-Options' => 'headerXcto',
132-
'X-XSS-Protection' => 'headerXxss',
133-
'Content-Security-Policy' => 'headerCsp',
134-
'Cross-Origin-Embedder-Policy' => 'headerCoep',
135-
'Cross-Origin-Opener-Policy' => 'headerCoop',
136-
'Cross-Origin-Resource-Policy' => 'headerCorp',
137-
'Permissions-Policy' => 'headerPp',
138-
'Referrer-Policy' => 'headerRp',
139-
'Strict-Transport-Security' => 'headerHsts',
140-
];
141-
142-
$finalHeaders = [];
143-
foreach ($settingMap as $headerName => $settingKey) {
144-
$savedValue = $this->getSetting($contextId, $settingKey);
145-
146-
if ($savedValue === null) {
147-
if (isset($defaultHeaders[$headerName])) {
148-
$finalHeaders[$headerName] = $defaultHeaders[$headerName];
149-
}
150-
} elseif ($savedValue !== '') {
151-
$finalHeaders[$headerName] = $savedValue;
152-
}
153-
}
154-
155-
header_remove('X-Powered-By');
156-
157-
if (!empty($finalHeaders)) {
158-
foreach ($finalHeaders as $name => $value) {
159-
header("{$name}: {$value}");
160-
}
161-
}
162-
163-
return false;
164-
}
165-
}
1+
<?php
2+
3+
/**
4+
* @file plugins/generic/ashSecurityHeaders/AshSecurityHeadersPlugin.inc.php
5+
*
6+
* Copyright (c) 2021-2025 AshVisualTheme
7+
* Copyright (c) 2014-2025 Simon Fraser University
8+
* Copyright (c) 2003-2025 John Willinsky
9+
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
10+
*
11+
* @class AshSecurityHeadersPlugin
12+
* @brief Main class for the Security Headers plugin.
13+
*/
14+
15+
namespace APP\plugins\generic\ashSecurityHeaders;
16+
17+
use APP\core\Application;
18+
use PKP\core\JSONMessage;
19+
use PKP\linkAction\LinkAction;
20+
use PKP\linkAction\request\AjaxModal;
21+
use PKP\plugins\GenericPlugin;
22+
use PKP\plugins\Hook;
23+
use APP\plugins\generic\ashSecurityHeaders\AshSecurityHeadersSettingsForm;
24+
25+
class AshSecurityHeadersPlugin extends GenericPlugin
26+
{
27+
public function register($category, $path, $mainContextId = null)
28+
{
29+
$success = parent::register($category, $path, $mainContextId);
30+
if ($success && $this->getEnabled()) {
31+
Hook::add('Dispatcher::dispatch', [$this, 'addSecurityHeaders']);
32+
}
33+
return $success;
34+
}
35+
36+
public function getDisplayName()
37+
{
38+
return __('plugins.generic.ashSecurityHeaders.displayName');
39+
}
40+
41+
public function getDescription()
42+
{
43+
return __('plugins.generic.ashSecurityHeaders.description');
44+
}
45+
46+
public function isSitePlugin()
47+
{
48+
if (!$this->getRequest()->getContext()) {
49+
return true;
50+
}
51+
return false;
52+
}
53+
54+
public function getActions($request, $actionArgs)
55+
{
56+
$actions = parent::getActions($request, $actionArgs);
57+
if (!$this->getEnabled()) {
58+
return $actions;
59+
}
60+
61+
$router = $request->getRouter();
62+
$linkAction = new LinkAction(
63+
'settings',
64+
new AjaxModal(
65+
$router->url(
66+
$request,
67+
null,
68+
null,
69+
'manage',
70+
null,
71+
['verb' => 'settings', 'plugin' => $this->getName(), 'category' => 'generic']
72+
),
73+
$this->getDisplayName()
74+
),
75+
__('manager.plugins.settings'),
76+
null
77+
);
78+
array_unshift($actions, $linkAction);
79+
return $actions;
80+
}
81+
82+
public function manage($args, $request)
83+
{
84+
switch ($request->getUserVar('verb')) {
85+
case 'settings':
86+
$form = new AshSecurityHeadersSettingsForm($this);
87+
88+
if (!$request->getUserVar('save')) {
89+
$form->initData();
90+
return new JSONMessage(true, $form->fetch($request));
91+
}
92+
93+
$form->readInputData();
94+
if ($form->validate()) {
95+
$form->execute();
96+
return new JSONMessage(true);
97+
}
98+
}
99+
return parent::manage($args, $request);
100+
}
101+
102+
public function getDefaultHeaders()
103+
{
104+
return [
105+
'X-Frame-Options' => 'SAMEORIGIN',
106+
'X-Content-Type-Options' => 'nosniff',
107+
'X-XSS-Protection' => '1; mode=block',
108+
'Content-Security-Policy' => "upgrade-insecure-requests;",
109+
'Cross-Origin-Embedder-Policy' => "same-origin; report-to='default'",
110+
'Cross-Origin-Opener-Policy' => 'require-corp',
111+
'Cross-Origin-Resource-Policy' => 'same-origin',
112+
'Permissions-Policy' => "accelerometer=(), camera=(), geolocation=(), gyroscope=(), magnetometer=(), microphone=(), usb=(), fullscreen=(self)",
113+
'Referrer-Policy' => 'strict-origin-when-cross-origin',
114+
'Strict-Transport-Security' => 'max-age=63072000; includeSubDomains; preload',
115+
];
116+
}
117+
118+
public function addSecurityHeaders($hookName, $params)
119+
{
120+
121+
if (defined('SESSION_DISABLE_INIT') || php_sapi_name() === 'cli' || headers_sent()) {
122+
return false;
123+
}
124+
125+
$defaultHeaders = $this->getDefaultHeaders();
126+
$request = Application::get()->getRequest();
127+
$context = $request->getContext();
128+
$contextId = $context ? $context->getId() : CONTEXT_SITE;
129+
130+
$settingMap = [
131+
'X-Frame-Options' => 'headerXfo',
132+
'X-Content-Type-Options' => 'headerXcto',
133+
'X-XSS-Protection' => 'headerXxss',
134+
'Content-Security-Policy' => 'headerCsp',
135+
'Cross-Origin-Embedder-Policy' => 'headerCoep',
136+
'Cross-Origin-Opener-Policy' => 'headerCoop',
137+
'Cross-Origin-Resource-Policy' => 'headerCorp',
138+
'Permissions-Policy' => 'headerPp',
139+
'Referrer-Policy' => 'headerRp',
140+
'Strict-Transport-Security' => 'headerHsts',
141+
];
142+
143+
$finalHeaders = [];
144+
foreach ($settingMap as $headerName => $settingKey) {
145+
$savedValue = $this->getSetting($contextId, $settingKey);
146+
147+
if ($savedValue === null) {
148+
if (isset($defaultHeaders[$headerName])) {
149+
$finalHeaders[$headerName] = $defaultHeaders[$headerName];
150+
}
151+
} elseif ($savedValue !== '') {
152+
$finalHeaders[$headerName] = $savedValue;
153+
}
154+
}
155+
156+
header_remove('X-Powered-By');
157+
158+
if (!empty($finalHeaders)) {
159+
foreach ($finalHeaders as $name => $value) {
160+
header("{$name}: {$value}");
161+
}
162+
}
163+
164+
return false;
165+
}
166+
}
167+
168+
if (!PKP_STRICT_MODE) {
169+
class_alias('\APP\plugins\generic\ashSecurityHeaders\AshSecurityHeadersPlugin', '\AshSecurityHeadersPlugin');
170+
}

0 commit comments

Comments
 (0)