Process password change controller through Slim
This commit is contained in:
parent
ba43064ddb
commit
ef00f9d203
24 changed files with 450 additions and 182 deletions
100
application/front/controller/admin/PasswordController.php
Normal file
100
application/front/controller/admin/PasswordController.php
Normal file
|
@ -0,0 +1,100 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Shaarli\Front\Controller\Admin;
|
||||
|
||||
use Shaarli\Container\ShaarliContainer;
|
||||
use Shaarli\Front\Exception\OpenShaarliPasswordException;
|
||||
use Shaarli\Front\Exception\ShaarliFrontException;
|
||||
use Slim\Http\Request;
|
||||
use Slim\Http\Response;
|
||||
use Throwable;
|
||||
|
||||
/**
|
||||
* Class PasswordController
|
||||
*
|
||||
* Slim controller used to handle passwords update.
|
||||
*/
|
||||
class PasswordController extends ShaarliAdminController
|
||||
{
|
||||
public function __construct(ShaarliContainer $container)
|
||||
{
|
||||
parent::__construct($container);
|
||||
|
||||
$this->assignView(
|
||||
'pagetitle',
|
||||
t('Change password') .' - '. $this->container->conf->get('general.title', 'Shaarli')
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /password - Displays the change password template
|
||||
*/
|
||||
public function index(Request $request, Response $response): Response
|
||||
{
|
||||
return $response->write($this->render('changepassword'));
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /password - Change admin password - existing and new passwords need to be provided.
|
||||
*/
|
||||
public function change(Request $request, Response $response): Response
|
||||
{
|
||||
$this->checkToken($request);
|
||||
|
||||
if ($this->container->conf->get('security.open_shaarli', false)) {
|
||||
throw new OpenShaarliPasswordException();
|
||||
}
|
||||
|
||||
$oldPassword = $request->getParam('oldpassword');
|
||||
$newPassword = $request->getParam('setpassword');
|
||||
|
||||
if (empty($newPassword) || empty($oldPassword)) {
|
||||
$this->saveErrorMessage(t('You must provide the current and new password to change it.'));
|
||||
|
||||
return $response
|
||||
->withStatus(400)
|
||||
->write($this->render('changepassword'))
|
||||
;
|
||||
}
|
||||
|
||||
// Make sure old password is correct.
|
||||
$oldHash = sha1(
|
||||
$oldPassword .
|
||||
$this->container->conf->get('credentials.login') .
|
||||
$this->container->conf->get('credentials.salt')
|
||||
);
|
||||
|
||||
if ($oldHash !== $this->container->conf->get('credentials.hash')) {
|
||||
$this->saveErrorMessage(t('The old password is not correct.'));
|
||||
|
||||
return $response
|
||||
->withStatus(400)
|
||||
->write($this->render('changepassword'))
|
||||
;
|
||||
}
|
||||
|
||||
// Save new password
|
||||
// Salt renders rainbow-tables attacks useless.
|
||||
$this->container->conf->set('credentials.salt', sha1(uniqid('', true) .'_'. mt_rand()));
|
||||
$this->container->conf->set(
|
||||
'credentials.hash',
|
||||
sha1(
|
||||
$newPassword
|
||||
. $this->container->conf->get('credentials.login')
|
||||
. $this->container->conf->get('credentials.salt')
|
||||
)
|
||||
);
|
||||
|
||||
try {
|
||||
$this->container->conf->write($this->container->loginManager->isLoggedIn());
|
||||
} catch (Throwable $e) {
|
||||
throw new ShaarliFrontException($e->getMessage(), 500, $e);
|
||||
}
|
||||
|
||||
$this->saveSuccessMessage(t('Your password has been changed'));
|
||||
|
||||
return $response->write($this->render('changepassword'));
|
||||
}
|
||||
}
|
|
@ -7,7 +7,19 @@
|
|||
use Shaarli\Container\ShaarliContainer;
|
||||
use Shaarli\Front\Controller\Visitor\ShaarliVisitorController;
|
||||
use Shaarli\Front\Exception\UnauthorizedException;
|
||||
use Shaarli\Front\Exception\WrongTokenException;
|
||||
use Shaarli\Security\SessionManager;
|
||||
use Slim\Http\Request;
|
||||
|
||||
/**
|
||||
* Class ShaarliAdminController
|
||||
*
|
||||
* All admin controllers (for logged in users) MUST extend this abstract class.
|
||||
* It makes sure that the user is properly logged in, and otherwise throw an exception
|
||||
* which will redirect to the login page.
|
||||
*
|
||||
* @package Shaarli\Front\Controller\Admin
|
||||
*/
|
||||
abstract class ShaarliAdminController extends ShaarliVisitorController
|
||||
{
|
||||
public function __construct(ShaarliContainer $container)
|
||||
|
@ -18,4 +30,51 @@ public function __construct(ShaarliContainer $container)
|
|||
throw new UnauthorizedException();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Any persistent action to the config or data store must check the XSRF token validity.
|
||||
*/
|
||||
protected function checkToken(Request $request): void
|
||||
{
|
||||
if (!$this->container->sessionManager->checkToken($request->getParam('token'))) {
|
||||
throw new WrongTokenException();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Save a SUCCESS message in user session, which will be displayed on any template page.
|
||||
*/
|
||||
protected function saveSuccessMessage(string $message): void
|
||||
{
|
||||
$this->saveMessage(SessionManager::KEY_SUCCESS_MESSAGES, $message);
|
||||
}
|
||||
|
||||
/**
|
||||
* Save a WARNING message in user session, which will be displayed on any template page.
|
||||
*/
|
||||
protected function saveWarningMessage(string $message): void
|
||||
{
|
||||
$this->saveMessage(SessionManager::KEY_WARNING_MESSAGES, $message);
|
||||
}
|
||||
|
||||
/**
|
||||
* Save an ERROR message in user session, which will be displayed on any template page.
|
||||
*/
|
||||
protected function saveErrorMessage(string $message): void
|
||||
{
|
||||
$this->saveMessage(SessionManager::KEY_ERROR_MESSAGES, $message);
|
||||
}
|
||||
|
||||
/**
|
||||
* Use the sessionManager to save the provided message using the proper type.
|
||||
*
|
||||
* @param string $type successed/warnings/errors
|
||||
*/
|
||||
protected function saveMessage(string $type, string $message): void
|
||||
{
|
||||
$messages = $this->container->sessionManager->getSessionParameter($type) ?? [];
|
||||
$messages[] = $message;
|
||||
|
||||
$this->container->sessionManager->setSessionParameter($type, $messages);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -9,6 +9,14 @@
|
|||
use Slim\Http\Request;
|
||||
use Slim\Http\Response;
|
||||
|
||||
/**
|
||||
* Class ShaarliVisitorController
|
||||
*
|
||||
* All controllers accessible by visitors (non logged in users) should extend this abstract class.
|
||||
* Contains a few helper function for template rendering, plugins, etc.
|
||||
*
|
||||
* @package Shaarli\Front\Controller\Visitor
|
||||
*/
|
||||
abstract class ShaarliVisitorController
|
||||
{
|
||||
/** @var ShaarliContainer */
|
||||
|
|
|
@ -0,0 +1,18 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Shaarli\Front\Exception;
|
||||
|
||||
/**
|
||||
* Class OpenShaarliPasswordException
|
||||
*
|
||||
* Raised if the user tries to change the admin password on an open shaarli instance.
|
||||
*/
|
||||
class OpenShaarliPasswordException extends ShaarliFrontException
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct(t('You are not supposed to change a password on an Open Shaarli.'), 403);
|
||||
}
|
||||
}
|
|
@ -9,11 +9,11 @@
|
|||
/**
|
||||
* Class ShaarliException
|
||||
*
|
||||
* Abstract exception class used to defined any custom exception thrown during front rendering.
|
||||
* Exception class used to defined any custom exception thrown during front rendering.
|
||||
*
|
||||
* @package Front\Exception
|
||||
*/
|
||||
abstract class ShaarliFrontException extends \Exception
|
||||
class ShaarliFrontException extends \Exception
|
||||
{
|
||||
/** Override parent constructor to force $message and $httpCode parameters to be set. */
|
||||
public function __construct(string $message, int $httpCode, Throwable $previous = null)
|
||||
|
|
18
application/front/exceptions/WrongTokenException.php
Normal file
18
application/front/exceptions/WrongTokenException.php
Normal file
|
@ -0,0 +1,18 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Shaarli\Front\Exception;
|
||||
|
||||
/**
|
||||
* Class OpenShaarliPasswordException
|
||||
*
|
||||
* Raised if the user tries to perform an action with an invalid XSRF token.
|
||||
*/
|
||||
class WrongTokenException extends ShaarliFrontException
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct(t('Wrong token.'), 403);
|
||||
}
|
||||
}
|
|
@ -7,6 +7,7 @@
|
|||
use Shaarli\ApplicationUtils;
|
||||
use Shaarli\Bookmark\BookmarkServiceInterface;
|
||||
use Shaarli\Config\ConfigManager;
|
||||
use Shaarli\Security\SessionManager;
|
||||
use Shaarli\Thumbnailer;
|
||||
|
||||
/**
|
||||
|
@ -136,17 +137,28 @@ private function initialize()
|
|||
$this->tpl->assign('thumbnails_width', $this->conf->get('thumbnails.width'));
|
||||
$this->tpl->assign('thumbnails_height', $this->conf->get('thumbnails.height'));
|
||||
|
||||
if (!empty($_SESSION['warnings'])) {
|
||||
$this->tpl->assign('global_warnings', $_SESSION['warnings']);
|
||||
unset($_SESSION['warnings']);
|
||||
}
|
||||
|
||||
$this->tpl->assign('formatter', $this->conf->get('formatter', 'default'));
|
||||
|
||||
// To be removed with a proper theme configuration.
|
||||
$this->tpl->assign('conf', $this->conf);
|
||||
}
|
||||
|
||||
protected function finalize(): void
|
||||
{
|
||||
// TODO: use the SessionManager
|
||||
$messageKeys = [
|
||||
SessionManager::KEY_SUCCESS_MESSAGES,
|
||||
SessionManager::KEY_WARNING_MESSAGES,
|
||||
SessionManager::KEY_ERROR_MESSAGES
|
||||
];
|
||||
foreach ($messageKeys as $messageKey) {
|
||||
if (!empty($_SESSION[$messageKey])) {
|
||||
$this->tpl->assign('global_' . $messageKey, $_SESSION[$messageKey]);
|
||||
unset($_SESSION[$messageKey]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The following assign() method is basically the same as RainTPL (except lazy loading)
|
||||
*
|
||||
|
@ -196,6 +208,8 @@ public function renderPage($page)
|
|||
$this->initialize();
|
||||
}
|
||||
|
||||
$this->finalize();
|
||||
|
||||
$this->tpl->draw($page);
|
||||
}
|
||||
|
||||
|
@ -213,6 +227,8 @@ public function render(string $page): string
|
|||
$this->initialize();
|
||||
}
|
||||
|
||||
$this->finalize();
|
||||
|
||||
return $this->tpl->draw($page, true);
|
||||
}
|
||||
|
||||
|
|
|
@ -12,6 +12,10 @@ class SessionManager
|
|||
public const KEY_VISIBILITY = 'visibility';
|
||||
public const KEY_UNTAGGED_ONLY = 'untaggedonly';
|
||||
|
||||
public const KEY_SUCCESS_MESSAGES = 'successes';
|
||||
public const KEY_WARNING_MESSAGES = 'warnings';
|
||||
public const KEY_ERROR_MESSAGES = 'errors';
|
||||
|
||||
/** @var int Session expiration timeout, in seconds */
|
||||
public static $SHORT_TIMEOUT = 3600; // 1 hour
|
||||
|
||||
|
|
52
index.php
52
index.php
|
@ -507,57 +507,9 @@ function renderPage($conf, $pluginManager, $bookmarkService, $history, $sessionM
|
|||
|
||||
// -------- User wants to change his/her password.
|
||||
if ($targetPage == Router::$PAGE_CHANGEPASSWORD) {
|
||||
if ($conf->get('security.open_shaarli')) {
|
||||
die(t('You are not supposed to change a password on an Open Shaarli.'));
|
||||
}
|
||||
|
||||
if (!empty($_POST['setpassword']) && !empty($_POST['oldpassword'])) {
|
||||
if (!$sessionManager->checkToken($_POST['token'])) {
|
||||
die(t('Wrong token.')); // Go away!
|
||||
}
|
||||
|
||||
// Make sure old password is correct.
|
||||
$oldhash = sha1(
|
||||
$_POST['oldpassword'].$conf->get('credentials.login').$conf->get('credentials.salt')
|
||||
);
|
||||
if ($oldhash != $conf->get('credentials.hash')) {
|
||||
echo '<script>alert("'
|
||||
. t('The old password is not correct.')
|
||||
.'");document.location=\'./?do=changepasswd\';</script>';
|
||||
header('Location: ./password');
|
||||
exit;
|
||||
}
|
||||
// Save new password
|
||||
// Salt renders rainbow-tables attacks useless.
|
||||
$conf->set('credentials.salt', sha1(uniqid('', true) .'_'. mt_rand()));
|
||||
$conf->set(
|
||||
'credentials.hash',
|
||||
sha1(
|
||||
$_POST['setpassword']
|
||||
. $conf->get('credentials.login')
|
||||
. $conf->get('credentials.salt')
|
||||
)
|
||||
);
|
||||
try {
|
||||
$conf->write($loginManager->isLoggedIn());
|
||||
} catch (Exception $e) {
|
||||
error_log(
|
||||
'ERROR while writing config file after changing password.' . PHP_EOL .
|
||||
$e->getMessage()
|
||||
);
|
||||
|
||||
// TODO: do not handle exceptions/errors in JS.
|
||||
echo '<script>alert("'. $e->getMessage() .'");document.location=\'./tools\';</script>';
|
||||
exit;
|
||||
}
|
||||
echo '<script>alert("'. t('Your password has been changed') .'");document.location=\'./tools\';</script>';
|
||||
exit;
|
||||
} else {
|
||||
// show the change password form.
|
||||
$PAGE->assign('pagetitle', t('Change password') .' - '. $conf->get('general.title', 'Shaarli'));
|
||||
$PAGE->renderPage('changepassword');
|
||||
exit;
|
||||
}
|
||||
}
|
||||
|
||||
// -------- User wants to change configuration
|
||||
if ($targetPage == Router::$PAGE_CONFIGURE) {
|
||||
|
@ -1504,6 +1456,8 @@ function install($conf, $sessionManager, $loginManager)
|
|||
/* -- LOGGED IN -- */
|
||||
$this->get('/logout', '\Shaarli\Front\Controller\Admin\LogoutController:index')->setName('logout');
|
||||
$this->get('/tools', '\Shaarli\Front\Controller\Admin\ToolsController:index')->setName('tools');
|
||||
$this->get('/password', '\Shaarli\Front\Controller\Admin\PasswordController:index')->setName('password');
|
||||
$this->post('/password', '\Shaarli\Front\Controller\Admin\PasswordController:change')->setName('changePassword');
|
||||
|
||||
$this
|
||||
->get('/links-per-page', '\Shaarli\Front\Controller\Admin\SessionFilterController:linksPerPage')
|
||||
|
|
|
@ -6,7 +6,6 @@
|
|||
|
||||
use Shaarli\Container\ShaarliTestContainer;
|
||||
use Shaarli\Front\Controller\Visitor\FrontControllerMockHelper;
|
||||
use Shaarli\Security\LoginManager;
|
||||
|
||||
/**
|
||||
* Trait FrontControllerMockHelper
|
||||
|
@ -28,7 +27,7 @@ protected function createContainer(): void
|
|||
{
|
||||
$this->parentCreateContainer();
|
||||
|
||||
$this->container->loginManager = $this->createMock(LoginManager::class);
|
||||
$this->container->loginManager->method('isLoggedIn')->willReturn(true);
|
||||
$this->container->sessionManager->method('checkToken')->willReturn(true);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -35,8 +35,6 @@ public function setUp(): void
|
|||
|
||||
public function testValidControllerInvoke(): void
|
||||
{
|
||||
$this->createValidContainerMockSet();
|
||||
|
||||
$request = $this->createMock(Request::class);
|
||||
$response = new Response();
|
||||
|
||||
|
|
186
tests/front/controller/admin/PasswordControllerTest.php
Normal file
186
tests/front/controller/admin/PasswordControllerTest.php
Normal file
|
@ -0,0 +1,186 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Shaarli\Front\Controller\Admin;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Shaarli\Config\ConfigManager;
|
||||
use Shaarli\Front\Exception\WrongTokenException;
|
||||
use Shaarli\Security\SessionManager;
|
||||
use Slim\Http\Request;
|
||||
use Slim\Http\Response;
|
||||
|
||||
class PasswordControllerTest extends TestCase
|
||||
{
|
||||
use FrontAdminControllerMockHelper;
|
||||
|
||||
/** @var PasswordController */
|
||||
protected $controller;
|
||||
|
||||
/** @var mixed[] Variables assigned to the template */
|
||||
protected $assignedVariables = [];
|
||||
|
||||
public function setUp(): void
|
||||
{
|
||||
$this->createContainer();
|
||||
$this->assignTemplateVars($this->assignedVariables);
|
||||
|
||||
$this->controller = new PasswordController($this->container);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test displaying the change password page.
|
||||
*/
|
||||
public function testGetPage(): void
|
||||
{
|
||||
$request = $this->createMock(Request::class);
|
||||
$response = new Response();
|
||||
|
||||
$result = $this->controller->index($request, $response);
|
||||
|
||||
static::assertSame(200, $result->getStatusCode());
|
||||
static::assertSame('changepassword', (string) $result->getBody());
|
||||
static::assertSame('Change password - Shaarli', $this->assignedVariables['pagetitle']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Change the password with valid parameters
|
||||
*/
|
||||
public function testPostNewPasswordDefault(): void
|
||||
{
|
||||
$request = $this->createMock(Request::class);
|
||||
$request->method('getParam')->willReturnCallback(function (string $key): string {
|
||||
if ('oldpassword' === $key) {
|
||||
return 'old';
|
||||
}
|
||||
if ('setpassword' === $key) {
|
||||
return 'new';
|
||||
}
|
||||
|
||||
return $key;
|
||||
});
|
||||
$response = new Response();
|
||||
|
||||
$this->container->conf = $this->createMock(ConfigManager::class);
|
||||
$this->container->conf->method('get')->willReturnCallback(function (string $key, $default) {
|
||||
if ('credentials.hash' === $key) {
|
||||
return sha1('old' . 'credentials.login' . 'credentials.salt');
|
||||
}
|
||||
|
||||
return strpos($key, 'credentials') !== false ? $key : $default;
|
||||
});
|
||||
$this->container->conf->expects(static::once())->method('write')->with(true);
|
||||
|
||||
$this->container->conf
|
||||
->method('set')
|
||||
->willReturnCallback(function (string $key, string $value) {
|
||||
if ('credentials.hash' === $key) {
|
||||
static::assertSame(sha1('new' . 'credentials.login' . 'credentials.salt'), $value);
|
||||
}
|
||||
})
|
||||
;
|
||||
|
||||
$result = $this->controller->change($request, $response);
|
||||
|
||||
static::assertSame(200, $result->getStatusCode());
|
||||
static::assertSame('changepassword', (string) $result->getBody());
|
||||
static::assertSame('Change password - Shaarli', $this->assignedVariables['pagetitle']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Change the password with a wrong existing password
|
||||
*/
|
||||
public function testPostNewPasswordWrongOldPassword(): void
|
||||
{
|
||||
$request = $this->createMock(Request::class);
|
||||
$request->method('getParam')->willReturnCallback(function (string $key): string {
|
||||
if ('oldpassword' === $key) {
|
||||
return 'wrong';
|
||||
}
|
||||
if ('setpassword' === $key) {
|
||||
return 'new';
|
||||
}
|
||||
|
||||
return $key;
|
||||
});
|
||||
$response = new Response();
|
||||
|
||||
$this->container->conf = $this->createMock(ConfigManager::class);
|
||||
$this->container->conf->method('get')->willReturnCallback(function (string $key, $default) {
|
||||
if ('credentials.hash' === $key) {
|
||||
return sha1('old' . 'credentials.login' . 'credentials.salt');
|
||||
}
|
||||
|
||||
return strpos($key, 'credentials') !== false ? $key : $default;
|
||||
});
|
||||
|
||||
$this->container->conf->expects(static::never())->method('set');
|
||||
$this->container->conf->expects(static::never())->method('write');
|
||||
|
||||
$this->container->sessionManager
|
||||
->expects(static::once())
|
||||
->method('setSessionParameter')
|
||||
->with(SessionManager::KEY_ERROR_MESSAGES, ['The old password is not correct.'])
|
||||
;
|
||||
|
||||
$result = $this->controller->change($request, $response);
|
||||
|
||||
static::assertSame(400, $result->getStatusCode());
|
||||
static::assertSame('changepassword', (string) $result->getBody());
|
||||
static::assertSame('Change password - Shaarli', $this->assignedVariables['pagetitle']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Change the password with a wrong existing password
|
||||
*/
|
||||
public function testPostNewPasswordWrongToken(): void
|
||||
{
|
||||
$this->container->sessionManager = $this->createMock(SessionManager::class);
|
||||
$this->container->sessionManager->method('checkToken')->willReturn(false);
|
||||
|
||||
$this->container->conf->expects(static::never())->method('set');
|
||||
$this->container->conf->expects(static::never())->method('write');
|
||||
|
||||
$request = $this->createMock(Request::class);
|
||||
$response = new Response();
|
||||
|
||||
$this->expectException(WrongTokenException::class);
|
||||
|
||||
$this->controller->change($request, $response);
|
||||
}
|
||||
|
||||
/**
|
||||
* Change the password with an empty new password
|
||||
*/
|
||||
public function testPostNewEmptyPassword(): void
|
||||
{
|
||||
$this->container->sessionManager
|
||||
->expects(static::once())
|
||||
->method('setSessionParameter')
|
||||
->with(SessionManager::KEY_ERROR_MESSAGES, ['You must provide the current and new password to change it.'])
|
||||
;
|
||||
|
||||
$this->container->conf->expects(static::never())->method('set');
|
||||
$this->container->conf->expects(static::never())->method('write');
|
||||
|
||||
$request = $this->createMock(Request::class);
|
||||
$request->method('getParam')->willReturnCallback(function (string $key): string {
|
||||
if ('oldpassword' === $key) {
|
||||
return 'old';
|
||||
}
|
||||
if ('setpassword' === $key) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return $key;
|
||||
});
|
||||
$response = new Response();
|
||||
|
||||
$result = $this->controller->change($request, $response);
|
||||
|
||||
static::assertSame(400, $result->getStatusCode());
|
||||
static::assertSame('changepassword', (string) $result->getBody());
|
||||
static::assertSame('Change password - Shaarli', $this->assignedVariables['pagetitle']);
|
||||
}
|
||||
}
|
|
@ -30,8 +30,6 @@ public function setUp(): void
|
|||
*/
|
||||
public function testLinksPerPage(): void
|
||||
{
|
||||
$this->createValidContainerMockSet();
|
||||
|
||||
$this->container->environment = ['HTTP_REFERER' => 'http://shaarli/subfolder/controller/?searchtag=abc'];
|
||||
|
||||
$request = $this->createMock(Request::class);
|
||||
|
@ -62,8 +60,6 @@ public function testLinksPerPage(): void
|
|||
*/
|
||||
public function testLinksPerPageNotValid(): void
|
||||
{
|
||||
$this->createValidContainerMockSet();
|
||||
|
||||
$request = $this->createMock(Request::class);
|
||||
$request->method('getUri')->willReturnCallback(function (): Uri {
|
||||
$uri = $this->createMock(Uri::class);
|
||||
|
@ -92,8 +88,6 @@ public function testLinksPerPageNotValid(): void
|
|||
*/
|
||||
public function testVisibility(): void
|
||||
{
|
||||
$this->createValidContainerMockSet();
|
||||
|
||||
$arg = ['visibility' => 'private'];
|
||||
|
||||
$this->container->environment = ['HTTP_REFERER' => 'http://shaarli/subfolder/controller/?searchtag=abc'];
|
||||
|
@ -126,8 +120,6 @@ public function testVisibility(): void
|
|||
*/
|
||||
public function testVisibilityToggleOff(): void
|
||||
{
|
||||
$this->createValidContainerMockSet();
|
||||
|
||||
$arg = ['visibility' => 'private'];
|
||||
|
||||
$this->container->environment = ['HTTP_REFERER' => 'http://shaarli/subfolder/controller/?searchtag=abc'];
|
||||
|
@ -169,8 +161,6 @@ public function testVisibilityToggleOff(): void
|
|||
*/
|
||||
public function testVisibilitySwitch(): void
|
||||
{
|
||||
$this->createValidContainerMockSet();
|
||||
|
||||
$arg = ['visibility' => 'private'];
|
||||
|
||||
$this->container->loginManager->method('isLoggedIn')->willReturn(true);
|
||||
|
@ -206,8 +196,6 @@ public function testVisibilitySwitch(): void
|
|||
*/
|
||||
public function testVisibilityInvalidValue(): void
|
||||
{
|
||||
$this->createValidContainerMockSet();
|
||||
|
||||
$arg = ['visibility' => 'test'];
|
||||
|
||||
$this->container->environment = ['HTTP_REFERER' => 'http://shaarli/subfolder/controller/?searchtag=abc'];
|
||||
|
@ -244,8 +232,6 @@ public function testVisibilityInvalidValue(): void
|
|||
*/
|
||||
public function testVisibilityLoggedOut(): void
|
||||
{
|
||||
$this->createValidContainerMockSet();
|
||||
|
||||
$arg = ['visibility' => 'test'];
|
||||
|
||||
$this->container->environment = ['HTTP_REFERER' => 'http://shaarli/subfolder/controller/?searchtag=abc'];
|
||||
|
@ -283,8 +269,6 @@ public function testVisibilityLoggedOut(): void
|
|||
*/
|
||||
public function testUntaggedOnly(): void
|
||||
{
|
||||
$this->createValidContainerMockSet();
|
||||
|
||||
$this->container->environment = ['HTTP_REFERER' => 'http://shaarli/subfolder/controller/?searchtag=abc'];
|
||||
|
||||
$request = $this->createMock(Request::class);
|
||||
|
@ -314,8 +298,6 @@ public function testUntaggedOnly(): void
|
|||
*/
|
||||
public function testUntaggedOnlyToggleOff(): void
|
||||
{
|
||||
$this->createValidContainerMockSet();
|
||||
|
||||
$this->container->environment = ['HTTP_REFERER' => 'http://shaarli/subfolder/controller/?searchtag=abc'];
|
||||
|
||||
$request = $this->createMock(Request::class);
|
||||
|
|
|
@ -24,8 +24,6 @@ public function setUp(): void
|
|||
|
||||
public function testDefaultInvokeWithHttps(): void
|
||||
{
|
||||
$this->createValidContainerMockSet();
|
||||
|
||||
$request = $this->createMock(Request::class);
|
||||
$response = new Response();
|
||||
|
||||
|
@ -49,8 +47,6 @@ public function testDefaultInvokeWithHttps(): void
|
|||
|
||||
public function testDefaultInvokeWithoutHttps(): void
|
||||
{
|
||||
$this->createValidContainerMockSet();
|
||||
|
||||
$request = $this->createMock(Request::class);
|
||||
$response = new Response();
|
||||
|
||||
|
|
|
@ -27,8 +27,6 @@ public function setUp(): void
|
|||
|
||||
public function testValidIndexControllerInvokeDefault(): void
|
||||
{
|
||||
$this->createValidContainerMockSet();
|
||||
|
||||
$currentDay = new \DateTimeImmutable('2020-05-13');
|
||||
|
||||
$request = $this->createMock(Request::class);
|
||||
|
@ -169,8 +167,6 @@ public function testValidIndexControllerInvokeDefault(): void
|
|||
*/
|
||||
public function testValidIndexControllerInvokeNoFutureOrPast(): void
|
||||
{
|
||||
$this->createValidContainerMockSet();
|
||||
|
||||
$currentDay = new \DateTimeImmutable('2020-05-13');
|
||||
|
||||
$request = $this->createMock(Request::class);
|
||||
|
@ -243,8 +239,6 @@ public function testValidIndexControllerInvokeNoFutureOrPast(): void
|
|||
*/
|
||||
public function testValidIndexControllerInvokeHeightAdjustment(): void
|
||||
{
|
||||
$this->createValidContainerMockSet();
|
||||
|
||||
$currentDay = new \DateTimeImmutable('2020-05-13');
|
||||
|
||||
$request = $this->createMock(Request::class);
|
||||
|
@ -314,8 +308,6 @@ public function testValidIndexControllerInvokeHeightAdjustment(): void
|
|||
*/
|
||||
public function testValidIndexControllerInvokeNoBookmark(): void
|
||||
{
|
||||
$this->createValidContainerMockSet();
|
||||
|
||||
$request = $this->createMock(Request::class);
|
||||
$response = new Response();
|
||||
|
||||
|
@ -363,8 +355,6 @@ public function testValidIndexControllerInvokeNoBookmark(): void
|
|||
*/
|
||||
public function testValidRssControllerInvokeDefault(): void
|
||||
{
|
||||
$this->createValidContainerMockSet();
|
||||
|
||||
$dates = [
|
||||
new \DateTimeImmutable('2020-05-17'),
|
||||
new \DateTimeImmutable('2020-05-15'),
|
||||
|
@ -439,8 +429,6 @@ public function testValidRssControllerInvokeDefault(): void
|
|||
*/
|
||||
public function testValidRssControllerInvokeTriggerCache(): void
|
||||
{
|
||||
$this->createValidContainerMockSet();
|
||||
|
||||
$request = $this->createMock(Request::class);
|
||||
$response = new Response();
|
||||
|
||||
|
@ -465,8 +453,6 @@ public function testValidRssControllerInvokeTriggerCache(): void
|
|||
*/
|
||||
public function testValidRssControllerInvokeNoBookmark(): void
|
||||
{
|
||||
$this->createValidContainerMockSet();
|
||||
|
||||
$request = $this->createMock(Request::class);
|
||||
$response = new Response();
|
||||
|
||||
|
|
|
@ -30,8 +30,6 @@ public function setUp(): void
|
|||
*/
|
||||
public function testDefaultRssController(): void
|
||||
{
|
||||
$this->createValidContainerMockSet();
|
||||
|
||||
$request = $this->createMock(Request::class);
|
||||
$response = new Response();
|
||||
|
||||
|
@ -71,8 +69,6 @@ public function testDefaultRssController(): void
|
|||
*/
|
||||
public function testDefaultAtomController(): void
|
||||
{
|
||||
$this->createValidContainerMockSet();
|
||||
|
||||
$request = $this->createMock(Request::class);
|
||||
$response = new Response();
|
||||
|
||||
|
@ -112,8 +108,6 @@ public function testDefaultAtomController(): void
|
|||
*/
|
||||
public function testAtomControllerWithParameters(): void
|
||||
{
|
||||
$this->createValidContainerMockSet();
|
||||
|
||||
$request = $this->createMock(Request::class);
|
||||
$request->method('getParams')->willReturn(['parameter' => 'value']);
|
||||
$response = new Response();
|
||||
|
|
|
@ -31,18 +31,12 @@ trait FrontControllerMockHelper
|
|||
protected $container;
|
||||
|
||||
/**
|
||||
* Mock the container instance
|
||||
* Mock the container instance and initialize container's services used by tests
|
||||
*/
|
||||
protected function createContainer(): void
|
||||
{
|
||||
$this->container = $this->createMock(ShaarliTestContainer::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize container's services used by tests
|
||||
*/
|
||||
protected function createValidContainerMockSet(): void
|
||||
{
|
||||
$this->container->loginManager = $this->createMock(LoginManager::class);
|
||||
|
||||
// Config
|
||||
|
|
|
@ -26,8 +26,6 @@ public function setUp(): void
|
|||
|
||||
public function testValidControllerInvoke(): void
|
||||
{
|
||||
$this->createValidContainerMockSet();
|
||||
|
||||
$request = $this->createMock(Request::class);
|
||||
$request->expects(static::once())->method('getServerParam')->willReturn('> referer');
|
||||
$response = new Response();
|
||||
|
@ -57,8 +55,6 @@ public function testValidControllerInvoke(): void
|
|||
|
||||
public function testValidControllerInvokeWithUserName(): void
|
||||
{
|
||||
$this->createValidContainerMockSet();
|
||||
|
||||
$request = $this->createMock(Request::class);
|
||||
$request->expects(static::once())->method('getServerParam')->willReturn('> referer');
|
||||
$request->expects(static::exactly(2))->method('getParam')->willReturn('myUser>');
|
||||
|
@ -90,8 +86,6 @@ public function testValidControllerInvokeWithUserName(): void
|
|||
|
||||
public function testLoginControllerWhileLoggedIn(): void
|
||||
{
|
||||
$this->createValidContainerMockSet();
|
||||
|
||||
$request = $this->createMock(Request::class);
|
||||
$response = new Response();
|
||||
|
||||
|
@ -106,8 +100,6 @@ public function testLoginControllerWhileLoggedIn(): void
|
|||
|
||||
public function testLoginControllerOpenShaarli(): void
|
||||
{
|
||||
$this->createValidContainerMockSet();
|
||||
|
||||
$request = $this->createMock(Request::class);
|
||||
$response = new Response();
|
||||
|
||||
|
@ -129,8 +121,6 @@ public function testLoginControllerOpenShaarli(): void
|
|||
|
||||
public function testLoginControllerWhileBanned(): void
|
||||
{
|
||||
$this->createValidContainerMockSet();
|
||||
|
||||
$request = $this->createMock(Request::class);
|
||||
$response = new Response();
|
||||
|
||||
|
|
|
@ -24,8 +24,6 @@ public function setUp(): void
|
|||
|
||||
public function testOpenSearchController(): void
|
||||
{
|
||||
$this->createValidContainerMockSet();
|
||||
|
||||
$request = $this->createMock(Request::class);
|
||||
$response = new Response();
|
||||
|
||||
|
|
|
@ -28,8 +28,6 @@ public function setUp(): void
|
|||
|
||||
public function testValidControllerInvokeDefault(): void
|
||||
{
|
||||
$this->createValidContainerMockSet();
|
||||
|
||||
$request = $this->createMock(Request::class);
|
||||
$request->expects(static::once())->method('getQueryParams')->willReturn([]);
|
||||
$response = new Response();
|
||||
|
@ -106,8 +104,6 @@ public function testControllerWithThumbnailsDisabled(): void
|
|||
{
|
||||
$this->expectException(ThumbnailsDisabledException::class);
|
||||
|
||||
$this->createValidContainerMockSet();
|
||||
|
||||
$request = $this->createMock(Request::class);
|
||||
$response = new Response();
|
||||
|
||||
|
|
|
@ -67,8 +67,6 @@ public function redirectFromReferer(
|
|||
|
||||
public function testAssignView(): void
|
||||
{
|
||||
$this->createValidContainerMockSet();
|
||||
|
||||
$this->assignTemplateVars($this->assignedValues);
|
||||
|
||||
$self = $this->controller->assignView('variableName', 'variableValue');
|
||||
|
@ -79,8 +77,6 @@ public function testAssignView(): void
|
|||
|
||||
public function testRender(): void
|
||||
{
|
||||
$this->createValidContainerMockSet();
|
||||
|
||||
$this->assignTemplateVars($this->assignedValues);
|
||||
|
||||
$this->container->bookmarkService
|
||||
|
@ -120,8 +116,6 @@ public function testRender(): void
|
|||
*/
|
||||
public function testRedirectFromRefererDefault(): void
|
||||
{
|
||||
$this->createValidContainerMockSet();
|
||||
|
||||
$this->container->environment['HTTP_REFERER'] = 'http://shaarli.tld/subfolder/controller?query=param&other=2';
|
||||
|
||||
$response = new Response();
|
||||
|
@ -137,8 +131,6 @@ public function testRedirectFromRefererDefault(): void
|
|||
*/
|
||||
public function testRedirectFromRefererWithUnmatchedLoopTerm(): void
|
||||
{
|
||||
$this->createValidContainerMockSet();
|
||||
|
||||
$this->container->environment['HTTP_REFERER'] = 'http://shaarli.tld/subfolder/controller?query=param&other=2';
|
||||
|
||||
$response = new Response();
|
||||
|
@ -154,8 +146,6 @@ public function testRedirectFromRefererWithUnmatchedLoopTerm(): void
|
|||
*/
|
||||
public function testRedirectFromRefererWithMatchingLoopTermInPath(): void
|
||||
{
|
||||
$this->createValidContainerMockSet();
|
||||
|
||||
$this->container->environment['HTTP_REFERER'] = 'http://shaarli.tld/subfolder/controller?query=param&other=2';
|
||||
|
||||
$response = new Response();
|
||||
|
@ -171,8 +161,6 @@ public function testRedirectFromRefererWithMatchingLoopTermInPath(): void
|
|||
*/
|
||||
public function testRedirectFromRefererWithMatchingLoopTermInQueryParam(): void
|
||||
{
|
||||
$this->createValidContainerMockSet();
|
||||
|
||||
$this->container->environment['HTTP_REFERER'] = 'http://shaarli.tld/subfolder/controller?query=param&other=2';
|
||||
|
||||
$response = new Response();
|
||||
|
@ -189,8 +177,6 @@ public function testRedirectFromRefererWithMatchingLoopTermInQueryParam(): void
|
|||
*/
|
||||
public function testRedirectFromRefererWithMatchingLoopTermInQueryValue(): void
|
||||
{
|
||||
$this->createValidContainerMockSet();
|
||||
|
||||
$this->container->environment['HTTP_REFERER'] = 'http://shaarli.tld/subfolder/controller?query=param&other=2';
|
||||
|
||||
$response = new Response();
|
||||
|
@ -207,8 +193,6 @@ public function testRedirectFromRefererWithMatchingLoopTermInQueryValue(): void
|
|||
*/
|
||||
public function testRedirectFromRefererWithLoopTermInDomain(): void
|
||||
{
|
||||
$this->createValidContainerMockSet();
|
||||
|
||||
$this->container->environment['HTTP_REFERER'] = 'http://shaarli.tld/subfolder/controller?query=param&other=2';
|
||||
|
||||
$response = new Response();
|
||||
|
@ -225,8 +209,6 @@ public function testRedirectFromRefererWithLoopTermInDomain(): void
|
|||
*/
|
||||
public function testRedirectFromRefererWithMatchingClearedParam(): void
|
||||
{
|
||||
$this->createValidContainerMockSet();
|
||||
|
||||
$this->container->environment['HTTP_REFERER'] = 'http://shaarli.tld/subfolder/controller?query=param&other=2';
|
||||
|
||||
$response = new Response();
|
||||
|
|
|
@ -28,8 +28,6 @@ public function setUp(): void
|
|||
*/
|
||||
public function testValidCloudControllerInvokeDefault(): void
|
||||
{
|
||||
$this->createValidContainerMockSet();
|
||||
|
||||
$allTags = [
|
||||
'ghi' => 1,
|
||||
'abc' => 3,
|
||||
|
@ -94,8 +92,6 @@ public function testValidCloudControllerInvokeDefault(): void
|
|||
*/
|
||||
public function testValidCloudControllerInvokeWithParameters(): void
|
||||
{
|
||||
$this->createValidContainerMockSet();
|
||||
|
||||
$request = $this->createMock(Request::class);
|
||||
$request
|
||||
->method('getQueryParam')
|
||||
|
@ -161,8 +157,6 @@ public function testValidCloudControllerInvokeWithParameters(): void
|
|||
*/
|
||||
public function testEmptyCloud(): void
|
||||
{
|
||||
$this->createValidContainerMockSet();
|
||||
|
||||
$request = $this->createMock(Request::class);
|
||||
$response = new Response();
|
||||
|
||||
|
@ -209,8 +203,6 @@ public function testEmptyCloud(): void
|
|||
*/
|
||||
public function testValidListControllerInvokeDefault(): void
|
||||
{
|
||||
$this->createValidContainerMockSet();
|
||||
|
||||
$allTags = [
|
||||
'def' => 12,
|
||||
'abc' => 3,
|
||||
|
@ -271,8 +263,6 @@ public function testValidListControllerInvokeDefault(): void
|
|||
*/
|
||||
public function testValidListControllerInvokeWithParameters(): void
|
||||
{
|
||||
$this->createValidContainerMockSet();
|
||||
|
||||
$request = $this->createMock(Request::class);
|
||||
$request
|
||||
->method('getQueryParam')
|
||||
|
@ -336,8 +326,6 @@ public function testValidListControllerInvokeWithParameters(): void
|
|||
*/
|
||||
public function testEmptyList(): void
|
||||
{
|
||||
$this->createValidContainerMockSet();
|
||||
|
||||
$request = $this->createMock(Request::class);
|
||||
$response = new Response();
|
||||
|
||||
|
|
|
@ -23,8 +23,6 @@ public function setUp(): void
|
|||
|
||||
public function testAddTagWithReferer(): void
|
||||
{
|
||||
$this->createValidContainerMockSet();
|
||||
|
||||
$this->container->environment = ['HTTP_REFERER' => 'http://shaarli/controller/'];
|
||||
|
||||
$request = $this->createMock(Request::class);
|
||||
|
@ -41,8 +39,6 @@ public function testAddTagWithReferer(): void
|
|||
|
||||
public function testAddTagWithRefererAndExistingSearch(): void
|
||||
{
|
||||
$this->createValidContainerMockSet();
|
||||
|
||||
$this->container->environment = ['HTTP_REFERER' => 'http://shaarli/controller/?searchtags=def'];
|
||||
|
||||
$request = $this->createMock(Request::class);
|
||||
|
@ -59,8 +55,6 @@ public function testAddTagWithRefererAndExistingSearch(): void
|
|||
|
||||
public function testAddTagWithoutRefererAndExistingSearch(): void
|
||||
{
|
||||
$this->createValidContainerMockSet();
|
||||
|
||||
$request = $this->createMock(Request::class);
|
||||
$response = new Response();
|
||||
|
||||
|
@ -75,8 +69,6 @@ public function testAddTagWithoutRefererAndExistingSearch(): void
|
|||
|
||||
public function testAddTagRemoveLegacyQueryParam(): void
|
||||
{
|
||||
$this->createValidContainerMockSet();
|
||||
|
||||
$this->container->environment = ['HTTP_REFERER' => 'http://shaarli/controller/?searchtags=def&addtag=abc'];
|
||||
|
||||
$request = $this->createMock(Request::class);
|
||||
|
@ -93,8 +85,6 @@ public function testAddTagRemoveLegacyQueryParam(): void
|
|||
|
||||
public function testAddTagResetPagination(): void
|
||||
{
|
||||
$this->createValidContainerMockSet();
|
||||
|
||||
$this->container->environment = ['HTTP_REFERER' => 'http://shaarli/controller/?searchtags=def&page=12'];
|
||||
|
||||
$request = $this->createMock(Request::class);
|
||||
|
@ -111,8 +101,6 @@ public function testAddTagResetPagination(): void
|
|||
|
||||
public function testAddTagWithRefererAndEmptySearch(): void
|
||||
{
|
||||
$this->createValidContainerMockSet();
|
||||
|
||||
$this->container->environment = ['HTTP_REFERER' => 'http://shaarli/controller/?searchtags='];
|
||||
|
||||
$request = $this->createMock(Request::class);
|
||||
|
@ -129,8 +117,6 @@ public function testAddTagWithRefererAndEmptySearch(): void
|
|||
|
||||
public function testAddTagWithoutNewTagWithReferer(): void
|
||||
{
|
||||
$this->createValidContainerMockSet();
|
||||
|
||||
$this->container->environment = ['HTTP_REFERER' => 'http://shaarli/controller/?searchtags=def'];
|
||||
|
||||
$request = $this->createMock(Request::class);
|
||||
|
@ -145,8 +131,6 @@ public function testAddTagWithoutNewTagWithReferer(): void
|
|||
|
||||
public function testAddTagWithoutNewTagWithoutReferer(): void
|
||||
{
|
||||
$this->createValidContainerMockSet();
|
||||
|
||||
$request = $this->createMock(Request::class);
|
||||
$response = new Response();
|
||||
|
||||
|
@ -159,8 +143,6 @@ public function testAddTagWithoutNewTagWithoutReferer(): void
|
|||
|
||||
public function testRemoveTagWithoutMatchingTag(): void
|
||||
{
|
||||
$this->createValidContainerMockSet();
|
||||
|
||||
$this->container->environment = ['HTTP_REFERER' => 'http://shaarli/controller/?searchtags=def'];
|
||||
|
||||
$request = $this->createMock(Request::class);
|
||||
|
@ -177,8 +159,6 @@ public function testRemoveTagWithoutMatchingTag(): void
|
|||
|
||||
public function testRemoveTagWithoutTagsearch(): void
|
||||
{
|
||||
$this->createValidContainerMockSet();
|
||||
|
||||
$this->container->environment = ['HTTP_REFERER' => 'http://shaarli/controller/'];
|
||||
|
||||
$request = $this->createMock(Request::class);
|
||||
|
@ -195,8 +175,6 @@ public function testRemoveTagWithoutTagsearch(): void
|
|||
|
||||
public function testRemoveTagWithoutReferer(): void
|
||||
{
|
||||
$this->createValidContainerMockSet();
|
||||
|
||||
$request = $this->createMock(Request::class);
|
||||
$response = new Response();
|
||||
|
||||
|
@ -211,8 +189,6 @@ public function testRemoveTagWithoutReferer(): void
|
|||
|
||||
public function testRemoveTagWithoutTag(): void
|
||||
{
|
||||
$this->createValidContainerMockSet();
|
||||
|
||||
$this->container->environment = ['HTTP_REFERER' => 'http://shaarli/controller/?searchtag=abc'];
|
||||
|
||||
$request = $this->createMock(Request::class);
|
||||
|
@ -227,8 +203,6 @@ public function testRemoveTagWithoutTag(): void
|
|||
|
||||
public function testRemoveTagWithoutTagWithoutReferer(): void
|
||||
{
|
||||
$this->createValidContainerMockSet();
|
||||
|
||||
$request = $this->createMock(Request::class);
|
||||
$response = new Response();
|
||||
|
||||
|
|
|
@ -184,6 +184,20 @@
|
|||
</div>
|
||||
{/if}
|
||||
|
||||
{if="!empty($global_errors) && $is_logged_in"}
|
||||
<div class="pure-g new-version-message pure-alert pure-alert-error pure-alert-closable" id="shaarli-errors-alert">
|
||||
<div class="pure-u-2-24"></div>
|
||||
<div class="pure-u-20-24">
|
||||
{loop="$global_errors"}
|
||||
<p>{$value}</p>
|
||||
{/loop}
|
||||
</div>
|
||||
<div class="pure-u-2-24">
|
||||
<i class="fa fa-times pure-alert-close"></i>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{if="!empty($global_warnings) && $is_logged_in"}
|
||||
<div class="pure-g pure-alert pure-alert-warning pure-alert-closable" id="shaarli-warnings-alert">
|
||||
<div class="pure-u-2-24"></div>
|
||||
|
@ -198,4 +212,18 @@
|
|||
</div>
|
||||
{/if}
|
||||
|
||||
{if="!empty($global_successes) && $is_logged_in"}
|
||||
<div class="pure-g new-version-message pure-alert pure-alert-success pure-alert-closable" id="shaarli-success-alert">
|
||||
<div class="pure-u-2-24"></div>
|
||||
<div class="pure-u-20-24">
|
||||
{loop="$global_successes"}
|
||||
<p>{$value}</p>
|
||||
{/loop}
|
||||
</div>
|
||||
<div class="pure-u-2-24">
|
||||
<i class="fa fa-times pure-alert-close"></i>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="clear"></div>
|
||||
|
|
Loading…
Reference in a new issue