# Exclude from Git archives
.gitattributes export-ignore
+.github export-ignore
.gitignore export-ignore
.travis.yml export-ignore
doc/**/*.json export-ignore
--- /dev/null
+ArthurHoaro <arthur@hoa.ro>
+Florian Eula <eula.florian@gmail.com> feula
+Florian Eula <eula.florian@gmail.com> <mr.pikzen@gmail.com>
+Nicolas Danelon <hi@nicolasmd.com.ar> nicolasm
+Nicolas Danelon <hi@nicolasmd.com.ar> <nda@3818.com.ar>
+Nicolas Danelon <hi@nicolasmd.com.ar> <nicolasdanelon@gmail.com>
+Nicolas Danelon <hi@nicolasmd.com.ar> <nicolasdanelon@users.noreply.github.com>
+Sébastien Sauvage <sebsauvage@sebsauvage.net>
+Timo Van Neerden <fire@lehollandaisvolant.net>
+Timo Van Neerden <fire@lehollandaisvolant.net> lehollandaisvolant <levoltigeurhollandais@gmail.com>
+VirtualTam <virtualtam@flibidi.net> <tamisier.aurelien@gmail.com>
+VirtualTam <virtualtam@flibidi.net> <virtualtam+github@flibidi.net>
+VirtualTam <virtualtam@flibidi.net> <virtualtam@flibidi.org>
# User plugin configuration
plugins/*/config.php
+
+# 3rd party themes
+tpl/*
+!tpl/default
--- /dev/null
+ 327 ArthurHoaro <arthur@hoa.ro>
+ 188 VirtualTam <virtualtam@flibidi.net>
+ 132 nodiscc <nodiscc@gmail.com>
+ 56 Sébastien Sauvage <sebsauvage@sebsauvage.net>
+ 15 Florian Eula <eula.florian@gmail.com>
+ 13 Emilien Klein <emilien@klein.st>
+ 12 Nicolas Danelon <hi@nicolasmd.com.ar>
+ 7 Christophe HENRY <christophe.henry@sbgodin.fr>
+ 4 Alexandre Alapetite <alexandre@alapetite.fr>
+ 4 David Sferruzza <david.sferruzza@gmail.com>
+ 3 Teromene <teromene@teromene.fr>
+ 2 Chris Kuethe <chris.kuethe@gmail.com>
+ 2 Knah Tsaeb <Knah-Tsaeb@knah-tsaeb.org>
+ 2 Mathieu Chabanon <git@matchab.fr>
+ 2 Miloš Jovanović <mjovanovic@gmail.com>
+ 2 Qwerty <champlywood@free.fr>
+ 2 Timo Van Neerden <fire@lehollandaisvolant.net>
+ 2 julienCXX <software@chmodplusx.eu>
+ 2 kalvn <kalvnthereal@gmail.com>
+ 1 Adrien Oliva <adrien.oliva@yapbreak.fr>
+ 1 Alexis J <alexis@effingo.be>
+ 1 BoboTiG <bobotig@gmail.com>
+ 1 Bronco <bronco@warriordudimanche.net>
+ 1 D Low <daniellowtw@gmail.com>
+ 1 Dimtion <zizou.xena@gmail.com>
+ 1 Fanch <fanch-github@qth.fr>
+ 1 Felix Bartels <felix@host-consultants.de>
+ 1 Felix Kästner <github.com-fpunktk@fpunktk.de>
+ 1 Florian Voigt <flvoigt@me.com>
+ 1 Gary Marigliano <gmarigliano93@gmail.com>
+ 1 Guillaume Virlet <github@virlet.org>
+ 1 Jonathan Druart <jonathan.druart@gmail.com>
+ 1 Julien Pivotto <roidelapluie@inuits.eu>
+ 1 Kevin Canévet <kevin@streamroot.io>
+ 1 Knah Tsaeb <knah-tsaeb@knah-tsaeb.org>
+ 1 Lionel Martin <renarddesmers@gmail.com>
+ 1 Marsup <marsup@gmail.com>
+ 1 Sbgodin <Sbgodin@users.noreply.github.com>
+ 1 TsT <tst2005@gmail.com>
+ 1 dimtion <zizou.xena@gmail.com>
## [v0.9.0](https://github.com/shaarli/Shaarli/releases/tag/v0.9.0) - UNPUBLISHED
-**WARNING**: Shaarli now requires PHP 5.5+.
+**WARNING**: Shaarli now requires PHP 5.5+.
### Added
- REST API: see [Shaarli API documentation](http://shaarli.github.io/api-documentation/)
+- The theme can now be selected in the administration page.
### Changed
+- Default template files are moved to a subfolder (`default`).
+
### Fixed
Files: *
License: zlib/libpng
Copyright: (c) 2011-2015 Sébastien SAUVAGE <sebsauvage@sebsauvage.net>
- (c) 2011-2015 Alexandre Alapetite <alexandre@alapetite.fr>
- (c) 2011-2015 David Sferruzza <david.sferruzza@gmail.com>
- (c) 2011-2015 Christophe HENRY <christophe.henry@sbgodin.fr>
- (c) 2011-2015 Mathieu Chabanon <git@matchab.fr>
- (c) 2011-2015 BoboTiG <bobotig@gmail.com>
- (c) 2011-2015 Bronco <bronco@warriordudimanche.net>
- (c) 2011-2015 Emilien Klein <emilien@klein.st>
- (c) 2011-2015 Knah Tsaeb <knah-tsaeb@knah-tsaeb.org>
- (c) 2011-2015 Lionel Martin <renarddesmers@gmail.com>
- (c) 2011-2015 lehollandaisvolant <levoltigeurhollandais@gmail.com>
- (c) 2011-2015 timo van neerden <fire@lehollandaisvolant.net>
- (c) 2011-2015 nodiscc <nodiscc@gmail.com>
- (c) 2011-2015 Florian Eula <mr.pikzen@gmail.com>
- (c) 2011-2015 Arthur Hoaro <arthur@hoa.ro>
- (c) 2011-2015 Aurélien "VirtualTam" Tamisier <virtualtam@flibidi.net>
- (c) 2011-2015 qwertygc <champlywood@free.fr>
- (c) 2011-2015 idleman <idleman@idleman.fr>
- (c) 2015 Alexis Ju <alexis@effingo.be>
- (c) 2015 dimtion <zizou.xena@gmail.com>
- (c) 2015 Fanch <fanch-github@qth.fr>
- (c) 2015 Guillaume Virlet <github@virlet.org>
- (c) 2015 Felix Bartels <felix@host-consultants.de>
- (c) 2015 Marsup <marsup@gmail.com>
- (c) 2015 Miloš Jovanović <mjovanovic@gmail.com>
- (c) 2015 Nicolás Danelón <hola@nicolasdanelon.com.ar>
- (c) 2015 TsT <tst2005@gmail.com>
-
+ (c) 2011-2017 The Shaarli Community, see AUTHORS
Files: inc/reset.css
License: BSD (http://opensource.org/licenses/BSD-3-Clause)
Copyright: (c) 2014 Designmodo
Source: http://designmodo.com/linecons-free/
-Files: images/floral_left.png, images/floral_right.png, images/squiggle.png, images/squiggle2.png, images/squiggle_closing.png
+Files: images/floral_left.png, images/floral_right.png, images/squiggle.png, images/squiggle_closing.png
Licence: Public Domain
Source: https://openclipart.org/people/j4p4n/j4p4n_ornimental_bookend_-_left.svg
@git clean -df
@rm -rf sandbox
+### generate the AUTHORS file from Git commit information
+authors:
+ @cp .github/mailmap .mailmap
+ @git shortlog -sne > AUTHORS
+ @rm .mailmap
+
### generate Doxygen documentation
doxygen: clean
@rm -rf doxygen
-o doc/$$base.html $$file; \
done;
-htmldoc: doc htmlsidebar htmlpages
+htmldoc: authors doc htmlsidebar htmlpages
'inc',
'plugins',
$conf->get('resource.raintpl_tpl'),
+ $conf->get('resource.raintpl_tpl').'/'.$conf->get('resource.theme'),
) as $path) {
if (! is_readable(realpath($path))) {
$errors[] = '"'.$path.'" directory is not readable';
--- /dev/null
+<?php
+
+namespace Shaarli;
+
+
+/**
+ * URL-safe Base64 operations
+ *
+ * @see https://en.wikipedia.org/wiki/Base64#URL_applications
+ */
+class Base64Url
+{
+ /**
+ * Base64Url-encodes data
+ *
+ * @param string $data Data to encode
+ *
+ * @return string Base64Url-encoded data
+ */
+ public static function encode($data) {
+ return rtrim(strtr(base64_encode($data), '+/', '-_'), '=');
+ }
+
+ /**
+ * Decodes Base64Url-encoded data
+ *
+ * @param string $data Data to decode
+ *
+ * @return string Decoded data
+ */
+ public static function decode($data) {
+ return base64_decode(str_pad(strtr($data, '-_', '+/'), strlen($data) % 4, '=', STR_PAD_RIGHT));
+ }
+}
// Directory containing page caches
private $cacheDir;
- // Full URL of the page to cache -typically the value returned by pageUrl()
- private $url;
-
// Should this URL be cached (boolean)?
private $shouldBeCached;
{
// TODO: check write access to the cache directory
$this->cacheDir = $cacheDir;
- $this->url = $url;
$this->filename = $this->cacheDir.'/'.sha1($url).'.cache';
$this->shouldBeCached = $shouldBeCached;
}
$content = substr($response, $headSize);
$headers = array();
foreach (preg_split('~[\r\n]+~', $rawHeadersLastRedir) as $line) {
- if (empty($line) or ctype_space($line)) {
+ if (empty($line) || ctype_space($line)) {
continue;
}
$splitLine = explode(': ', $line, 2);
{
$cpt = 0;
foreach ($links as $link) {
- $cpt = $link['private'] == true ? $cpt + 1 : $cpt;
+ if ($link['private']) {
+ $cpt += 1;
+ }
}
return $cpt;
*
* @param ConfigManager $conf Configuration Manager instance (reference).
*/
- function __construct(&$conf)
+ public function __construct(&$conf)
{
$this->tpl = false;
$this->conf = $conf;
--- /dev/null
+<?php
+
+namespace Shaarli;
+
+/**
+ * Class ThemeUtils
+ *
+ * Utility functions related to theme management.
+ *
+ * @package Shaarli
+ */
+class ThemeUtils
+{
+ /**
+ * Get a list of available themes.
+ *
+ * It will return the name of any directory present in the template folder.
+ *
+ * @param string $tplDir Templates main directory.
+ *
+ * @return array List of theme names.
+ */
+ public static function getThemes($tplDir)
+ {
+ $allTheme = glob($tplDir.'/*', GLOB_ONLYDIR);
+ $themes = [];
+ foreach ($allTheme as $value) {
+ $themes[] = str_replace($tplDir.'/', '', $value);
+ }
+
+ return $themes;
+ }
+}
return $updatesRan;
}
- if ($this->methods == null) {
+ if ($this->methods === null) {
throw new UpdaterException('Couldn\'t retrieve Updater class methods.');
}
$this->conf->write($this->isLoggedIn);
return true;
}
+
+ /**
+ * New setting: theme name. If the default theme is used, nothing to do.
+ *
+ * If the user uses a custom theme, raintpl_tpl dir is updated to the parent directory,
+ * and the current theme is set as default in the theme setting.
+ *
+ * @return bool true if the update is successful, false otherwise.
+ */
+ public function updateMethodDefaultTheme()
+ {
+ // raintpl_tpl isn't the root template directory anymore.
+ // We run the update only if this folder still contains the template files.
+ $tplDir = $this->conf->get('resource.raintpl_tpl');
+ $tplFile = $tplDir . '/linklist.html';
+ if (! file_exists($tplFile)) {
+ return true;
+ }
+
+ $parent = dirname($tplDir);
+ $this->conf->set('resource.raintpl_tpl', $parent);
+ $this->conf->set('resource.theme', trim(str_replace($parent, '', $tplDir), '/'));
+ $this->conf->write($this->isLoggedIn);
+
+ // Dependency injection gore
+ RainTPL::$tpl_dir = $tplDir;
+
+ return true;
+ }
+
+ /**
+ * Move the file to inc/user.css to data/user.css.
+ *
+ * Note: Due to hardcoded paths, it's not unit testable. But one line of code should be fine.
+ *
+ * @return bool true if the update is successful, false otherwise.
+ */
+ public function updateMethodMoveUserCss()
+ {
+ if (! is_file('inc/user.css')) {
+ return true;
+ }
+
+ return rename('inc/user.css', 'data/user.css');
+ }
}
/**
* @throws ApiAuthorizationException The token couldn't be validated.
*/
protected function checkToken($request) {
- $jwt = $request->getHeaderLine('jwt');
- if (empty($jwt)) {
+ if (! $request->hasHeader('Authorization')) {
throw new ApiAuthorizationException('JWT token not provided');
}
throw new ApiAuthorizationException('Token secret must be set in Shaarli\'s administration');
}
- ApiUtils::validateJwtToken($jwt, $this->conf->get('api.secret'));
+ $authorization = $request->getHeaderLine('Authorization');
+
+ if (! preg_match('/^Bearer (.*)/i', $authorization, $matches)) {
+ throw new ApiAuthorizationException('Invalid JWT header');
+ }
+
+ ApiUtils::validateJwtToken($matches[1], $this->conf->get('api.secret'));
}
/**
<?php
-
namespace Shaarli\Api;
+use Shaarli\Base64Url;
use Shaarli\Api\Exceptions\ApiAuthorizationException;
/**
- * Class ApiUtils
- *
- * Utility functions for the API.
+ * REST API utilities
*/
class ApiUtils
{
throw new ApiAuthorizationException('Malformed JWT token');
}
- $genSign = hash_hmac('sha512', $parts[0] .'.'. $parts[1], $secret);
+ $genSign = Base64Url::encode(hash_hmac('sha512', $parts[0] .'.'. $parts[1], $secret, true));
if ($parts[2] != $genSign) {
throw new ApiAuthorizationException('Invalid JWT signature');
}
- $header = json_decode(base64_decode($parts[0]));
+ $header = json_decode(Base64Url::decode($parts[0]));
if ($header === null) {
throw new ApiAuthorizationException('Invalid JWT header');
}
- $payload = json_decode(base64_decode($parts[1]));
+ $payload = json_decode(Base64Url::decode($parts[1]));
if ($payload === null) {
throw new ApiAuthorizationException('Invalid JWT payload');
}
*
* @return array All configuration in an array.
*/
- function read($filepath);
+ public function read($filepath);
/**
* Write configuration.
* @param string $filepath Config file absolute path.
* @param array $conf All configuration in an array.
*/
- function write($filepath, $conf);
+ public function write($filepath, $conf);
/**
* Get config file extension according to config type.
*
* @return string Config file extension.
*/
- function getExtension();
+ public function getExtension();
}
/**
* @inheritdoc
*/
- function read($filepath)
+ public function read($filepath)
{
if (! is_readable($filepath)) {
return array();
/**
* @inheritdoc
*/
- function write($filepath, $conf)
+ public function write($filepath, $conf)
{
// JSON_PRETTY_PRINT is available from PHP 5.4.
$print = defined('JSON_PRETTY_PRINT') ? JSON_PRETTY_PRINT : 0;
/**
* @inheritdoc
*/
- function getExtension()
+ public function getExtension()
{
return '.json.php';
}
$this->setEmpty('resource.log', 'data/log.txt');
$this->setEmpty('resource.update_check', 'data/lastupdatecheck.txt');
$this->setEmpty('resource.raintpl_tpl', 'tpl/');
+ $this->setEmpty('resource.theme', 'default');
$this->setEmpty('resource.raintpl_tmp', 'tmp/');
$this->setEmpty('resource.thumbnails_cache', 'cache');
$this->setEmpty('resource.page_cache', 'pagecache');
'resource.log' => 'config.LOG_FILE',
'resource.update_check' => 'config.UPDATECHECK_FILENAME',
'resource.raintpl_tpl' => 'config.RAINTPL_TPL',
+ 'resource.theme' => 'config.theme',
'resource.raintpl_tmp' => 'config.RAINTPL_TMP',
'resource.thumbnails_cache' => 'config.CACHEDIR',
'resource.page_cache' => 'config.PAGECACHE',
/**
* @inheritdoc
*/
- function read($filepath)
+ public function read($filepath)
{
if (! file_exists($filepath) || ! is_readable($filepath)) {
return array();
/**
* @inheritdoc
*/
- function write($filepath, $conf)
+ public function write($filepath, $conf)
{
$configStr = '<?php '. PHP_EOL;
foreach (self::$ROOT_KEYS as $key) {
$configStr .= '$GLOBALS[\'' . $key . '\'] = ' . var_export($conf[$key], true) . ';' . PHP_EOL;
}
}
-
+
// Store all $conf['config']
foreach ($conf['config'] as $key => $value) {
$configStr .= '$GLOBALS[\'config\'][\''. $key .'\'] = '.var_export($conf['config'][$key], true).';'. PHP_EOL;
/**
* @inheritdoc
*/
- function getExtension()
+ public function getExtension()
{
return '.php';
}
},
"autoload": {
"psr-4": {
+ "Shaarli\\": "application",
"Shaarli\\Api\\": "application/api/",
"Shaarli\\Api\\Controllers\\": "application/api/controllers",
"Shaarli\\Api\\Exceptions\\": "application/api/exceptions"
alias /var/www/shaarli/images/favicon.ico;
}
+ location / {
+ # Slim - rewrite URLs
+ try_files $uri /index.php$is_args$args;
+ }
+
location ~ (index)\.php$ {
+ # Slim - split URL path into (script_filename, path_info)
+ try_files $uri =404;
+ fastcgi_split_path_info ^(.+\.php)(/.+)$;
+
# filter and proxy PHP requests to PHP-FPM
fastcgi_pass unix:/var/run/php5-fpm.sock;
fastcgi_index index.php;
alias /var/www/shaarli/images/favicon.ico;
}
+ location / {
+ # Slim - rewrite URLs
+ try_files $uri /index.php$is_args$args;
+ }
+
location ~ (index)\.php$ {
+ # Slim - split URL path into (script_filename, path_info)
+ try_files $uri =404;
+ fastcgi_split_path_info ^(.+\.php)(/.+)$;
+
# filter and proxy PHP requests to PHP-FPM
fastcgi_pass unix:/var/run/php5-fpm.sock;
fastcgi_index index.php;
alias /var/www/shaarli/images/favicon.ico;
}
+ location / {
+ # Slim - rewrite URLs
+ try_files $uri /index.php$is_args$args;
+ }
+
location ~ (index)\.php$ {
+ # Slim - split URL path into (script_filename, path_info)
+ try_files $uri =404;
+ fastcgi_split_path_info ^(.+\.php)(/.+)$;
+
# filter and proxy PHP requests to PHP-FPM
fastcgi_pass unix:/var/run/php5-fpm.sock;
fastcgi_index index.php;
require_once 'application/PluginManager.php';
require_once 'application/Router.php';
require_once 'application/Updater.php';
+use \Shaarli\ThemeUtils;
// Ensure the PHP version is supported
try {
$conf = new ConfigManager();
$conf->setEmpty('general.timezone', date_default_timezone_get());
$conf->setEmpty('general.title', 'Shared links on '. escape(index_url($_SERVER)));
-RainTPL::$tpl_dir = $conf->get('resource.raintpl_tpl'); // template directory
+RainTPL::$tpl_dir = $conf->get('resource.raintpl_tpl').'/'.$conf->get('resource.theme').'/'; // template directory
RainTPL::$cache_dir = $conf->get('resource.raintpl_tmp'); // cache directory
$pluginManager = new PluginManager($conf);
}
// If session does not exist on server side, or IP address has changed, or session has expired, logout.
if (empty($_SESSION['uid'])
- || ($conf->get('security.session_protection_disabled') == false && $_SESSION['ip'] != allIPs())
+ || ($conf->get('security.session_protection_disabled') === false && $_SESSION['ip'] != allIPs())
|| time() >= $_SESSION['expires_on'])
{
logout();
$tpl->assign('links', $links);
$tpl->assign('rssdate', escape($dayDate->format(DateTime::RSS)));
$tpl->assign('hide_timestamps', $conf->get('privacy.hide_timestamps', false));
- $html = $tpl->draw('dailyrss', $return_string=true);
+ $html = $tpl->draw('dailyrss', true);
echo $html . PHP_EOL;
}
$conf->set('general.timezone', $tz);
$conf->set('general.title', escape($_POST['title']));
$conf->set('general.header_link', escape($_POST['titleLink']));
+ $conf->set('resource.theme', escape($_POST['theme']));
$conf->set('redirector.url', escape($_POST['redirector']));
$conf->set('security.session_protection_disabled', !empty($_POST['disablesessionprotection']));
$conf->set('privacy.default_private_links', !empty($_POST['privateLinkByDefault']));
$conf->set('api.secret', escape($_POST['apiSecret']));
try {
$conf->write(isLoggedIn());
+ invalidateCaches($conf->get('resource.page_cache'));
}
catch(Exception $e) {
error_log(
else // Show the configuration form.
{
$PAGE->assign('title', $conf->get('general.title'));
+ $PAGE->assign('theme', $conf->get('resource.theme'));
+ $PAGE->assign('theme_available', ThemeUtils::getThemes($conf->get('resource.raintpl_tpl')));
$PAGE->assign('redirector', $conf->get('redirector.url'));
list($timezone_form, $timezone_js) = generateTimeZoneForm($conf->get('general.timezone'));
$PAGE->assign('timezone_form', $timezone_form);
*/
private $apiVersion;
- function __construct($instance, $version)
+ public function __construct($instance, $version)
{
if ($this->isVersionAllowed($version)) {
$this->apiVersion = self::$wallabagVersions[$version];
$conf->set('resource.page_cache', 'pagecache');
$conf->set('resource.raintpl_tmp', 'tmp');
$conf->set('resource.raintpl_tpl', 'tpl');
+ $conf->set('resource.theme', 'default');
$conf->set('resource.update_check', 'data/lastupdatecheck.txt');
$this->assertEquals(
$conf->set('resource.page_cache', 'null/pagecache');
$conf->set('resource.raintpl_tmp', 'null/tmp');
$conf->set('resource.raintpl_tpl', 'null/tpl');
+ $conf->set('resource.raintpl_theme', 'null/tpl/default');
$conf->set('resource.update_check', 'null/data/lastupdatecheck.txt');
$this->assertEquals(
array(
'"null/tpl" directory is not readable',
+ '"null/tpl/default" directory is not readable',
'"null/cache" directory is not readable',
'"null/cache" directory is not writable',
'"null/data" directory is not readable',
--- /dev/null
+<?php
+
+namespace Shaarli;
+
+/**
+ * Class ThemeUtilsTest
+ *
+ * @package Shaarli
+ */
+class ThemeUtilsTest extends \PHPUnit_Framework_TestCase
+{
+ /**
+ * Test getThemes() with existing theme directories.
+ */
+ public function testGetThemes()
+ {
+ $themes = ['theme1', 'default', 'Bl1p_- bL0p'];
+ foreach ($themes as $theme) {
+ mkdir('sandbox/tpl/'. $theme, 0755, true);
+ }
+
+ // include a file which should be ignored
+ touch('sandbox/tpl/supertheme');
+
+ $res = ThemeUtils::getThemes('sandbox/tpl/');
+ foreach ($res as $theme) {
+ $this->assertTrue(in_array($theme, $themes));
+ }
+ $this->assertFalse(in_array('supertheme', $res));
+
+ foreach ($themes as $theme) {
+ rmdir('sandbox/tpl/'. $theme);
+ }
+ unlink('sandbox/tpl/supertheme');
+ rmdir('sandbox/tpl');
+ }
+
+ /**
+ * Test getThemes() without any theme dir.
+ */
+ public function testGetThemesEmpty()
+ {
+ mkdir('sandbox/tpl/', 0755, true);
+ $this->assertEquals([], ThemeUtils::getThemes('sandbox/tpl/'));
+ rmdir('sandbox/tpl/');
+ }
+
+ /**
+ * Test getThemes() with an invalid path.
+ */
+ public function testGetThemesInvalid()
+ {
+ $this->assertEquals([], ThemeUtils::getThemes('nope'));
+ }
+}
require_once 'application/config/ConfigManager.php';
require_once 'tests/Updater/DummyUpdater.php';
+require_once 'inc/rain.tpl.class.php';
/**
* Class UpdaterTest.
$this->assertTrue($updater->updateMethodDatastoreIds());
$this->assertEquals($checksum, hash_file('sha1', self::$testDatastore));
}
+
+ /**
+ * Test defaultTheme update with default settings: nothing to do.
+ */
+ public function testDefaultThemeWithDefaultSettings()
+ {
+ $sandbox = 'sandbox/config';
+ copy(self::$configFile . '.json.php', $sandbox . '.json.php');
+ $this->conf = new ConfigManager($sandbox);
+ $updater = new Updater([], [], $this->conf, true);
+ $this->assertTrue($updater->updateMethodDefaultTheme());
+
+ $this->assertEquals('tpl/', $this->conf->get('resource.raintpl_tpl'));
+ $this->assertEquals('default', $this->conf->get('resource.theme'));
+ $this->conf = new ConfigManager($sandbox);
+ $this->assertEquals('tpl/', $this->conf->get('resource.raintpl_tpl'));
+ $this->assertEquals('default', $this->conf->get('resource.theme'));
+ unlink($sandbox . '.json.php');
+ }
+
+ /**
+ * Test defaultTheme update with a custom theme in a subfolder
+ */
+ public function testDefaultThemeWithCustomTheme()
+ {
+ $theme = 'iamanartist';
+ $sandbox = 'sandbox/config';
+ copy(self::$configFile . '.json.php', $sandbox . '.json.php');
+ $this->conf = new ConfigManager($sandbox);
+ mkdir('sandbox/'. $theme);
+ touch('sandbox/'. $theme .'/linklist.html');
+ $this->conf->set('resource.raintpl_tpl', 'sandbox/'. $theme .'/');
+ $updater = new Updater([], [], $this->conf, true);
+ $this->assertTrue($updater->updateMethodDefaultTheme());
+
+ $this->assertEquals('sandbox', $this->conf->get('resource.raintpl_tpl'));
+ $this->assertEquals($theme, $this->conf->get('resource.theme'));
+ $this->conf = new ConfigManager($sandbox);
+ $this->assertEquals('sandbox', $this->conf->get('resource.raintpl_tpl'));
+ $this->assertEquals($theme, $this->conf->get('resource.theme'));
+ unlink($sandbox . '.json.php');
+ unlink('sandbox/'. $theme .'/linklist.html');
+ rmdir('sandbox/'. $theme);
+ }
}
/**
* Test add trailing slash.
*/
- function testAddTrailingSlash()
+ public function testAddTrailingSlash()
{
$strOn = 'http://randomstr.com/test/';
$strOff = 'http://randomstr.com/test';
/**
* Test valid HTTP url.
*/
- function testUrlIsHttp()
+ public function testUrlIsHttp()
{
$url = new Url(self::$baseUrl);
$this->assertTrue($url->isHttp());
/**
* Test non HTTP url.
*/
- function testUrlIsNotHttp()
+ public function testUrlIsNotHttp()
{
$url = new Url('ftp://save.tld/mysave');
$this->assertFalse($url->isHttp());
/**
* Test International Domain Name to ASCII conversion
*/
- function testIdnToAscii()
+ public function testIdnToAscii()
{
$ind = 'http://www.académie-française.fr/';
$expected = 'http://www.xn--acadmie-franaise-npb1a.fr/';
$env = Environment::mock([
'REQUEST_METHOD' => 'GET',
'REQUEST_URI' => '/echo',
- 'HTTP_JWT'=> 'jwt',
+ 'HTTP_AUTHORIZATION'=> 'Bearer jwt',
]);
$request = Request::createFromEnvironment($env);
$response = new Response();
}
/**
- * Invoke the middleware without an invalid JWT token (debug):
+ * Invoke the middleware with an invalid JWT token header
+ */
+ public function testInvalidJwtAuthHeaderDebug()
+ {
+ $this->conf->set('dev.debug', true);
+ $mw = new ApiMiddleware($this->container);
+ $env = Environment::mock([
+ 'REQUEST_METHOD' => 'GET',
+ 'REQUEST_URI' => '/echo',
+ 'HTTP_AUTHORIZATION'=> 'PolarBearer jwt',
+ ]);
+ $request = Request::createFromEnvironment($env);
+ $response = new Response();
+ /** @var Response $response */
+ $response = $mw($request, $response, null);
+
+ $this->assertEquals(401, $response->getStatusCode());
+ $body = json_decode((string) $response->getBody());
+ $this->assertEquals('Not authorized: Invalid JWT header', $body->message);
+ $this->assertContains('ApiAuthorizationException', $body->stacktrace);
+ }
+
+ /**
+ * Invoke the middleware with an invalid JWT token (debug):
* should return a 401 error Unauthorized - with a specific message and a stacktrace.
*
* Note: specific JWT errors tests are handled in ApiUtilsTest.
$env = Environment::mock([
'REQUEST_METHOD' => 'GET',
'REQUEST_URI' => '/echo',
- 'HTTP_JWT'=> 'bad jwt',
+ 'HTTP_AUTHORIZATION'=> 'Bearer jwt',
]);
$request = Request::createFromEnvironment($env);
$response = new Response();
namespace Shaarli\Api;
+use Shaarli\Base64Url;
+
+
/**
* Class ApiUtilsTest
*/
*/
public static function generateValidJwtToken($secret)
{
- $header = base64_encode('{
+ $header = Base64Url::encode('{
"typ": "JWT",
"alg": "HS512"
}');
- $payload = base64_encode('{
+ $payload = Base64Url::encode('{
"iat": '. time() .'
}');
- $signature = hash_hmac('sha512', $header .'.'. $payload , $secret);
+ $signature = Base64Url::encode(hash_hmac('sha512', $header .'.'. $payload , $secret, true));
return $header .'.'. $payload .'.'. $signature;
}
*/
public static function generateCustomJwtToken($header, $payload, $secret)
{
- $header = base64_encode($header);
- $payload = base64_encode($payload);
- $signature = hash_hmac('sha512', $header . '.' . $payload, $secret);
+ $header = Base64Url::encode($header);
+ $payload = Base64Url::encode($payload);
+ $signature = Base64Url::encode(hash_hmac('sha512', $header . '.' . $payload, $secret, true));
return $header . '.' . $payload . '.' . $signature;
}
/**
* Reset plugin path.
*/
- function setUp()
+ public function setUp()
{
PluginManager::$PLUGINS_PATH = 'plugins';
}
/**
* Test render_header hook while logged in.
*/
- function testAddlinkHeaderLoggedIn()
+ public function testAddlinkHeaderLoggedIn()
{
$str = 'stuff';
$data = array($str => $str);
/**
* Test render_header hook while logged out.
*/
- function testAddlinkHeaderLoggedOut()
+ public function testAddlinkHeaderLoggedOut()
{
$str = 'stuff';
$data = array($str => $str);
/**
* Test render_includes hook while logged in.
*/
- function testAddlinkIncludesLoggedIn()
+ public function testAddlinkIncludesLoggedIn()
{
$str = 'stuff';
$data = array($str => $str);
* Test render_includes hook.
* Should not affect css files while logged out.
*/
- function testAddlinkIncludesLoggedOut()
+ public function testAddlinkIncludesLoggedOut()
{
$str = 'stuff';
$data = array($str => $str);
/**
* Reset plugin path
*/
- function setUp()
+ public function setUp()
{
PluginManager::$PLUGINS_PATH = 'plugins';
}
/**
* Test render_linklist hook on external links.
*/
- function testArchiveorgLinklistOnExternalLinks()
+ public function testArchiveorgLinklistOnExternalLinks()
{
$str = 'http://randomstr.com/test';
// plugin data
$this->assertEquals(1, count($link['link_plugin']));
$this->assertNotFalse(strpos($link['link_plugin'][0], $str));
-
}
/**
* Test render_linklist hook on internal links.
*/
- function testArchiveorgLinklistOnInternalLinks()
+ public function testArchiveorgLinklistOnInternalLinks()
{
$internalLink1 = 'http://shaarli.shaarli/?qvMAqg';
$internalLinkRealURL1 = '?qvMAqg';
)
);
-
$data = hook_archiveorg_render_linklist($data);
// Case n°1: first link type, public
$link = $data['links'][5];
$this->assertArrayNotHasKey('link_plugin', $link);
-
}
-
}
/**
* Reset plugin path
*/
- function setUp()
+ public function setUp()
{
PluginManager::$PLUGINS_PATH = 'plugins';
}
/**
* Test Isso init without errors.
*/
- function testWallabagInitNoError()
+ public function testWallabagInitNoError()
{
$conf = new ConfigManager('');
$conf->set('plugins.ISSO_SERVER', 'value');
/**
* Test Isso init with errors.
*/
- function testWallabagInitError()
+ public function testWallabagInitError()
{
$conf = new ConfigManager('');
$errors = isso_init($conf);
/**
* Test render_linklist hook with valid settings to display the comment form.
*/
- function testIssoDisplayed()
+ public function testIssoDisplayed()
{
$conf = new ConfigManager('');
$conf->set('plugins.ISSO_SERVER', 'value');
/**
* Test isso plugin when multiple links are displayed (shouldn't be displayed).
*/
- function testIssoMultipleLinks()
+ public function testIssoMultipleLinks()
{
$conf = new ConfigManager('');
$conf->set('plugins.ISSO_SERVER', 'value');
/**
* Test isso plugin when using search (shouldn't be displayed).
*/
- function testIssoNotDisplayedWhenSearch()
+ public function testIssoNotDisplayedWhenSearch()
{
$conf = new ConfigManager('');
$conf->set('plugins.ISSO_SERVER', 'value');
/**
* Test isso plugin without server configuration (shouldn't be displayed).
*/
- function testIssoWithoutConf()
+ public function testIssoWithoutConf()
{
$data = 'abc';
$conf = new ConfigManager('');
/**
* Reset plugin path
*/
- function setUp()
+ public function setUp()
{
PluginManager::$PLUGINS_PATH = 'plugins';
}
* Test render_linklist hook.
* Only check that there is basic markdown rendering.
*/
- function testMarkdownLinklist()
+ public function testMarkdownLinklist()
{
$markdown = '# My title' . PHP_EOL . 'Very interesting content.';
$data = array(
* Test render_daily hook.
* Only check that there is basic markdown rendering.
*/
- function testMarkdownDaily()
+ public function testMarkdownDaily()
{
$markdown = '# My title' . PHP_EOL . 'Very interesting content.';
$data = array(
/**
* Test reverse_text2clickable().
*/
- function testReverseText2clickable()
+ public function testReverseText2clickable()
{
$text = 'stuff http://hello.there/is=someone#here otherstuff';
$clickableText = text2clickable($text, '');
/**
* Test reverse_nl2br().
*/
- function testReverseNl2br()
+ public function testReverseNl2br()
{
$text = 'stuff' . PHP_EOL . 'otherstuff';
$processedText = nl2br($text);
/**
* Test reverse_space2nbsp().
*/
- function testReverseSpace2nbsp()
+ public function testReverseSpace2nbsp()
{
$text = ' stuff' . PHP_EOL . ' otherstuff and another';
$processedText = space2nbsp($text);
/**
* Test sanitize_html().
*/
- function testSanitizeHtml()
+ public function testSanitizeHtml()
{
$input = '< script src="js.js"/>';
$input .= '< script attr>alert(\'xss\');</script>';
/**
* Test the no markdown tag.
*/
- function testNoMarkdownTag()
+ public function testNoMarkdownTag()
{
$str = 'All _work_ and `no play` makes Jack a *dull* boy.';
$data = array(
/**
* Test that a close value to nomarkdown is not understand as nomarkdown (previous value `.nomarkdown`).
*/
- function testNoMarkdownNotExcactlyMatching()
+ public function testNoMarkdownNotExcactlyMatching()
{
$str = 'All _work_ and `no play` makes Jack a *dull* boy.';
$data = array(
/**
* Test hashtag links processed with markdown.
*/
- function testMarkdownHashtagLinks()
+ public function testMarkdownHashtagLinks()
{
$md = file_get_contents('tests/plugins/resources/markdown.md');
$md = format_description($md);
/**
* Reset plugin path
*/
- function setUp()
+ public function setUp()
{
PluginManager::$PLUGINS_PATH = 'plugins';
}
/**
* Test render_linklist hook.
*/
- function testPlayvideosHeader()
+ public function testPlayvideosHeader()
{
$str = 'stuff';
$data = array($str => $str);
/**
* Test render_footer hook.
*/
- function testPlayvideosFooter()
+ public function testPlayvideosFooter()
{
$str = 'stuff';
$data = array($str => $str);
/**
* Reset plugin path
*/
- function setUp()
+ public function setUp()
{
PluginManager::$PLUGINS_PATH = 'plugins';
}
/**
* Test render_feed hook with an RSS feed.
*/
- function testPubSubRssRenderFeed()
+ public function testPubSubRssRenderFeed()
{
$hub = 'http://domain.hub';
$conf = new ConfigManager(self::$configFile);
/**
* Test render_feed hook with an ATOM feed.
*/
- function testPubSubAtomRenderFeed()
+ public function testPubSubAtomRenderFeed()
{
$hub = 'http://domain.hub';
$conf = new ConfigManager(self::$configFile);
<?php
/**
- * PlugQrcodeTest.php
+ * PluginQrcodeTest.php
*/
require_once 'plugins/qrcode/qrcode.php';
require_once 'application/Router.php';
/**
- * Class PlugQrcodeTest
+ * Class PluginQrcodeTest
* Unit test for the QR-Code plugin
*/
-class PlugQrcodeTest extends PHPUnit_Framework_TestCase
+class PluginQrcodeTest extends PHPUnit_Framework_TestCase
{
/**
* Reset plugin path
*/
- function setUp() {
+ public function setUp() {
PluginManager::$PLUGINS_PATH = 'plugins';
}
/**
* Test render_linklist hook.
*/
- function testQrcodeLinklist()
+ public function testQrcodeLinklist()
{
$str = 'http://randomstr.com/test';
$data = array(
/**
* Test render_footer hook.
*/
- function testQrcodeFooter()
+ public function testQrcodeFooter()
{
$str = 'stuff';
$data = array($str => $str);
/**
* Reset plugin path
*/
- function setUp()
+ public function setUp()
{
PluginManager::$PLUGINS_PATH = 'plugins';
}
/**
* Test Readityourself init without errors.
*/
- function testReadityourselfInitNoError()
+ public function testReadityourselfInitNoError()
{
$conf = new ConfigManager('');
$conf->set('plugins.READITYOUSELF_URL', 'value');
/**
* Test Readityourself init with errors.
*/
- function testReadityourselfInitError()
+ public function testReadityourselfInitError()
{
$conf = new ConfigManager('');
$errors = readityourself_init($conf);
/**
* Test render_linklist hook.
*/
- function testReadityourselfLinklist()
+ public function testReadityourselfLinklist()
{
$conf = new ConfigManager('');
$conf->set('plugins.READITYOUSELF_URL', 'value');
/**
* Test without config: nothing should happened.
*/
- function testReadityourselfLinklistWithoutConfig()
+ public function testReadityourselfLinklistWithoutConfig()
{
$conf = new ConfigManager('');
$conf->set('plugins.READITYOUSELF_URL', null);
/**
* Reset plugin path
*/
- function setUp()
+ public function setUp()
{
PluginManager::$PLUGINS_PATH = 'plugins';
}
/**
* Test wallabag init without errors.
*/
- function testWallabagInitNoError()
+ public function testWallabagInitNoError()
{
$conf = new ConfigManager('');
$conf->set('plugins.WALLABAG_URL', 'value');
/**
* Test wallabag init with errors.
*/
- function testWallabagInitError()
+ public function testWallabagInitError()
{
$conf = new ConfigManager('');
$errors = wallabag_init($conf);
/**
* Test render_linklist hook.
*/
- function testWallabagLinklist()
+ public function testWallabagLinklist()
{
$conf = new ConfigManager('');
$conf->set('plugins.WALLABAG_URL', 'value');
/**
* Reset plugin path
*/
- function setUp()
+ public function setUp()
{
$this->instance = 'http://some.url';
}
/**
* Test WallabagInstance with API V1.
*/
- function testWallabagInstanceV1()
+ public function testWallabagInstanceV1()
{
$instance = new WallabagInstance($this->instance, 1);
$expected = $this->instance . '/?plainurl=';
/**
* Test WallabagInstance with API V2.
*/
- function testWallabagInstanceV2()
+ public function testWallabagInstanceV2()
{
$instance = new WallabagInstance($this->instance, 2);
$expected = $this->instance . '/bookmarklet?url=';
/**
* Test WallabagInstance with an invalid API version.
*/
- function testWallabagInstanceInvalidVersion()
+ public function testWallabagInstanceInvalidVersion()
{
$instance = new WallabagInstance($this->instance, false);
$expected = $this->instance . '/?plainurl=';
},
"resource": {
"datastore": "tests\/utils\/config\/datastore.php",
- "data_dir": "tests\/utils\/config"
+ "data_dir": "tests\/utils\/config",
+ "raintpl_tpl": "tpl/"
},
"plugins": {
"WALLABAG_VERSION": 1
<!DOCTYPE html>
<html>
<head>{include="includes"}
- <link type="text/css" rel="stylesheet" href="../inc/awesomplete.css" />
+ <link type="text/css" rel="stylesheet" href="inc/awesomplete.css#" />
<script src="inc/awesomplete.min.js#"></script>
</head>
<body onload="document.changetag.fromtag.focus();">
<td><input type="text" name="titleLink" id="titleLink" size="50" value="{$titleLink}"><br/><label
for="titleLink">(default value is: ?)</label></td>
</tr>
+
+ <tr>
+ <td><b>Theme:</b></td>
+ <td>
+ <select name="theme" id="theme">
+ {loop="$theme_available"}
+ <option value="{$value}" {if="$value===$theme"}selected{/if}>
+ {$value|ucfirst}
+ </option>
+ {/loop}
+ </select>
+ </td>
+ </tr>
+
<tr>
<td><b>Timezone:</b></td>
<td>{$timezone_form}</td>
}
/* Buttons */
-.bigbutton {
+.bigbutton, #pageheader a.bigbutton {
background-color: #c0c0c0;
background: -moz-linear-gradient(#c0c0c0, #ffffff) repeat scroll 0 0 transparent;
background: -webkit-gradient(linear, 0 0, 0 bottom, from(#c0c0c0), to(#ffffff));
box-shadow: 0 1px 1px rgba(0, 0, 0, 0.5);
cursor: pointer;
height: 24px;
- margin-left: 5px;
padding: 0 5px;
+ margin: 5px 5px 0 0;
color: #606060;
border-style: outset;
border-width: 1px;
+ display: inline-block;
+}
+
+a.bigbutton, #pageheader a.bigbutton {
+ height: 22px;
+ line-height: 22px;
}
.smallbutton {
}
#pageheader #logo {
- background-image: url('../images/logo.png');
+ background-image: url('../../../images/logo.png');
background-repeat: no-repeat;
float: left;
margin: 0 10px 0 10px;
height: 14px;
}
+div.dailyEntryPermalink {
+ float: right;
+}
+
div.dailyTitle {
font-weight: bold;
font-size: 44pt;
display: inline !important;
}
- .tagfilter input.bigbutton, .searchform input.bigbutton, .addform input.bigbutton {
+ .tagfilter input.bigbutton, .searchform input.bigbutton, .addform input.bigbutton, a.bigbutton {
width: 30%;
font-size: smaller;
}
</div>
<div class="dailyTitle">
- <img src="../images/floral_left.png" width="51" height="50" class="nomobile" alt="floral_left">
+ <img src="images/floral_left.png" width="51" height="50" class="nomobile" alt="floral_left">
The Daily Shaarli
- <img src="../images/floral_right.png" width="51" height="50" class="nomobile" alt="floral_right">
+ <img src="images/floral_right.png" width="51" height="50" class="nomobile" alt="floral_right">
</div>
<div class="dailyDate">
<div class="dailyEntry">
<div class="dailyEntryPermalink">
<a href="?{$value.shorturl}">
- <img src="../images/squiggle2.png" width="25" height="26" title="permalink" alt="permalink">
+ <img src="images/squiggle.png" width="25" height="26" title="permalink" alt="permalink">
</a>
</div>
{if="!$hide_timestamps || isLoggedIn()"}
{$value}
{/loop}
</div>
- <div id="closing"><img src="../images/squiggle_closing.png" width="66" height="61" alt="-"></div>
+ <div id="closing"><img src="images/squiggle_closing.png" width="66" height="61" alt="-"></div>
</div>
{include="page.footer"}
</body>
<!DOCTYPE html>
<html>
<head>{include="includes"}
- <link type="text/css" rel="stylesheet" href="../inc/awesomplete.css" />
+ <link type="text/css" rel="stylesheet" href="inc/awesomplete.css#" />
</head>
<body
{if="$link.title==''"}onload="document.linkform.lf_title.focus();"
{/if}
<input type="submit" value="Save" name="save_edit" class="bigbutton">
<input type="submit" value="Cancel" name="cancel_edit" class="bigbutton">
- {if="!$link_is_new"}<input type="submit" value="Delete" name="delete_link" class="bigbutton delete" onClick="return confirmDeleteLink();">{/if}
+ {if="!$link_is_new && isset($link.id)"}
+ <a href="?delete_link&lf_linkdate={$link.id}&token={$token}"
+ name="delete_link" class="bigbutton"
+ onClick="return confirmDeleteLink();">
+ {'Delete'|t}
+ </a>
+ {/if}
<input type="hidden" name="token" value="{$token}">
{if="$http_referer"}<input type="hidden" name="returnurl" value="{$http_referer}">{/if}
</form>
<link rel="alternate" type="application/rss+xml" href="{$feedurl}?do=rss{$searchcrits}#" title="RSS Feed" />
<link rel="alternate" type="application/atom+xml" href="{$feedurl}?do=atom{$searchcrits}#" title="ATOM Feed" />
<link href="images/favicon.ico#" rel="shortcut icon" type="image/x-icon" />
-<link type="text/css" rel="stylesheet" href="../inc/reset.css" />
-<link type="text/css" rel="stylesheet" href="../inc/shaarli.css" />
-{if="is_file('inc/user.css')"}<link type="text/css" rel="stylesheet" href="../inc/user.css" />{/if}
+<link type="text/css" rel="stylesheet" href="css/reset.css" />
+<link type="text/css" rel="stylesheet" href="css/shaarli.css" />
+{if="is_file('data/user.css')"}<link type="text/css" rel="stylesheet" href="data/user.css#" />{/if}
{loop="$plugins_includes.css_files"}
<link type="text/css" rel="stylesheet" href="{$value}#"/>
{/loop}
<!DOCTYPE html>
<html>
<head>
- <link type="text/css" rel="stylesheet" href="../inc/awesomplete.css" />
+ <link type="text/css" rel="stylesheet" href="inc/awesomplete.css#" />
{include="includes"}
</head>
<body>