Merge pull request #732 from ArthurHoaro/feature/theme-manager

Theme manager: improvements
This commit is contained in:
Arthur 2017-01-06 11:40:54 +01:00 committed by GitHub
commit 7418f7cb60
52 changed files with 215 additions and 16 deletions

4
.gitignore vendored
View file

@ -28,3 +28,7 @@ phpmd.html
# User plugin configuration # User plugin configuration
plugins/*/config.php plugins/*/config.php
# 3rd party themes
tpl/*
!tpl/default

View file

@ -7,14 +7,17 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
## [v0.9.0](https://github.com/shaarli/Shaarli/releases/tag/v0.9.0) - UNPUBLISHED ## [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 ### Added
- REST API: see [Shaarli API documentation](http://shaarli.github.io/api-documentation/) - REST API: see [Shaarli API documentation](http://shaarli.github.io/api-documentation/)
- The theme can now be selected in the administration page.
### Changed ### Changed
- Default template files are moved to a subfolder (`default`).
### Fixed ### Fixed

View file

@ -43,7 +43,7 @@ License: CC-BY (http://creativecommons.org/licenses/by/3.0/)
Copyright: (c) 2014 Designmodo Copyright: (c) 2014 Designmodo
Source: http://designmodo.com/linecons-free/ 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 Licence: Public Domain
Source: https://openclipart.org/people/j4p4n/j4p4n_ornimental_bookend_-_left.svg Source: https://openclipart.org/people/j4p4n/j4p4n_ornimental_bookend_-_left.svg

View file

@ -150,6 +150,7 @@ public static function checkResourcePermissions($conf)
'inc', 'inc',
'plugins', 'plugins',
$conf->get('resource.raintpl_tpl'), $conf->get('resource.raintpl_tpl'),
$conf->get('resource.raintpl_tpl').'/'.$conf->get('resource.theme'),
) as $path) { ) as $path) {
if (! is_readable(realpath($path))) { if (! is_readable(realpath($path))) {
$errors[] = '"'.$path.'" directory is not readable'; $errors[] = '"'.$path.'" directory is not readable';

View file

@ -0,0 +1,33 @@
<?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;
}
}

View file

@ -279,6 +279,35 @@ public function updateMethodApiSettings()
$this->conf->write($this->isLoggedIn); $this->conf->write($this->isLoggedIn);
return true; 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;
}
} }
/** /**

View file

@ -299,6 +299,7 @@ protected function setDefaultValues()
$this->setEmpty('resource.log', 'data/log.txt'); $this->setEmpty('resource.log', 'data/log.txt');
$this->setEmpty('resource.update_check', 'data/lastupdatecheck.txt'); $this->setEmpty('resource.update_check', 'data/lastupdatecheck.txt');
$this->setEmpty('resource.raintpl_tpl', 'tpl/'); $this->setEmpty('resource.raintpl_tpl', 'tpl/');
$this->setEmpty('resource.theme', 'default');
$this->setEmpty('resource.raintpl_tmp', 'tmp/'); $this->setEmpty('resource.raintpl_tmp', 'tmp/');
$this->setEmpty('resource.thumbnails_cache', 'cache'); $this->setEmpty('resource.thumbnails_cache', 'cache');
$this->setEmpty('resource.page_cache', 'pagecache'); $this->setEmpty('resource.page_cache', 'pagecache');

View file

@ -41,6 +41,7 @@ class ConfigPhp implements ConfigIO
'resource.log' => 'config.LOG_FILE', 'resource.log' => 'config.LOG_FILE',
'resource.update_check' => 'config.UPDATECHECK_FILENAME', 'resource.update_check' => 'config.UPDATECHECK_FILENAME',
'resource.raintpl_tpl' => 'config.RAINTPL_TPL', 'resource.raintpl_tpl' => 'config.RAINTPL_TPL',
'resource.theme' => 'config.theme',
'resource.raintpl_tmp' => 'config.RAINTPL_TMP', 'resource.raintpl_tmp' => 'config.RAINTPL_TMP',
'resource.thumbnails_cache' => 'config.CACHEDIR', 'resource.thumbnails_cache' => 'config.CACHEDIR',
'resource.page_cache' => 'config.PAGECACHE', 'resource.page_cache' => 'config.PAGECACHE',
@ -99,7 +100,7 @@ public function write($filepath, $conf)
$configStr .= '$GLOBALS[\'' . $key . '\'] = ' . var_export($conf[$key], true) . ';' . PHP_EOL; $configStr .= '$GLOBALS[\'' . $key . '\'] = ' . var_export($conf[$key], true) . ';' . PHP_EOL;
} }
} }
// Store all $conf['config'] // Store all $conf['config']
foreach ($conf['config'] as $key => $value) { foreach ($conf['config'] as $key => $value) {
$configStr .= '$GLOBALS[\'config\'][\''. $key .'\'] = '.var_export($conf['config'][$key], true).';'. PHP_EOL; $configStr .= '$GLOBALS[\'config\'][\''. $key .'\'] = '.var_export($conf['config'][$key], true).';'. PHP_EOL;

Binary file not shown.

Before

Width:  |  Height:  |  Size: 684 B

View file

@ -79,6 +79,7 @@
require_once 'application/PluginManager.php'; require_once 'application/PluginManager.php';
require_once 'application/Router.php'; require_once 'application/Router.php';
require_once 'application/Updater.php'; require_once 'application/Updater.php';
use \Shaarli\ThemeUtils;
// Ensure the PHP version is supported // Ensure the PHP version is supported
try { try {
@ -122,7 +123,7 @@
$conf = new ConfigManager(); $conf = new ConfigManager();
$conf->setEmpty('general.timezone', date_default_timezone_get()); $conf->setEmpty('general.timezone', date_default_timezone_get());
$conf->setEmpty('general.title', 'Shared links on '. escape(index_url($_SERVER))); $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 RainTPL::$cache_dir = $conf->get('resource.raintpl_tmp'); // cache directory
$pluginManager = new PluginManager($conf); $pluginManager = new PluginManager($conf);
@ -1124,6 +1125,7 @@ function renderPage($conf, $pluginManager, $LINKSDB)
$conf->set('general.timezone', $tz); $conf->set('general.timezone', $tz);
$conf->set('general.title', escape($_POST['title'])); $conf->set('general.title', escape($_POST['title']));
$conf->set('general.header_link', escape($_POST['titleLink'])); $conf->set('general.header_link', escape($_POST['titleLink']));
$conf->set('resource.theme', escape($_POST['theme']));
$conf->set('redirector.url', escape($_POST['redirector'])); $conf->set('redirector.url', escape($_POST['redirector']));
$conf->set('security.session_protection_disabled', !empty($_POST['disablesessionprotection'])); $conf->set('security.session_protection_disabled', !empty($_POST['disablesessionprotection']));
$conf->set('privacy.default_private_links', !empty($_POST['privateLinkByDefault'])); $conf->set('privacy.default_private_links', !empty($_POST['privateLinkByDefault']));
@ -1134,6 +1136,7 @@ function renderPage($conf, $pluginManager, $LINKSDB)
$conf->set('api.secret', escape($_POST['apiSecret'])); $conf->set('api.secret', escape($_POST['apiSecret']));
try { try {
$conf->write(isLoggedIn()); $conf->write(isLoggedIn());
invalidateCaches($conf->get('resource.page_cache'));
} }
catch(Exception $e) { catch(Exception $e) {
error_log( error_log(
@ -1151,6 +1154,8 @@ function renderPage($conf, $pluginManager, $LINKSDB)
else // Show the configuration form. else // Show the configuration form.
{ {
$PAGE->assign('title', $conf->get('general.title')); $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')); $PAGE->assign('redirector', $conf->get('redirector.url'));
list($timezone_form, $timezone_js) = generateTimeZoneForm($conf->get('general.timezone')); list($timezone_form, $timezone_js) = generateTimeZoneForm($conf->get('general.timezone'));
$PAGE->assign('timezone_form', $timezone_form); $PAGE->assign('timezone_form', $timezone_form);

View file

@ -289,6 +289,7 @@ public function testCheckCurrentResourcePermissions()
$conf->set('resource.page_cache', 'pagecache'); $conf->set('resource.page_cache', 'pagecache');
$conf->set('resource.raintpl_tmp', 'tmp'); $conf->set('resource.raintpl_tmp', 'tmp');
$conf->set('resource.raintpl_tpl', 'tpl'); $conf->set('resource.raintpl_tpl', 'tpl');
$conf->set('resource.theme', 'default');
$conf->set('resource.update_check', 'data/lastupdatecheck.txt'); $conf->set('resource.update_check', 'data/lastupdatecheck.txt');
$this->assertEquals( $this->assertEquals(
@ -312,10 +313,12 @@ public function testCheckCurrentResourcePermissionsErrors()
$conf->set('resource.page_cache', 'null/pagecache'); $conf->set('resource.page_cache', 'null/pagecache');
$conf->set('resource.raintpl_tmp', 'null/tmp'); $conf->set('resource.raintpl_tmp', 'null/tmp');
$conf->set('resource.raintpl_tpl', 'null/tpl'); $conf->set('resource.raintpl_tpl', 'null/tpl');
$conf->set('resource.raintpl_theme', 'null/tpl/default');
$conf->set('resource.update_check', 'null/data/lastupdatecheck.txt'); $conf->set('resource.update_check', 'null/data/lastupdatecheck.txt');
$this->assertEquals( $this->assertEquals(
array( array(
'"null/tpl" directory is not readable', '"null/tpl" directory is not readable',
'"null/tpl/default" directory is not readable',
'"null/cache" directory is not readable', '"null/cache" directory is not readable',
'"null/cache" directory is not writable', '"null/cache" directory is not writable',
'"null/data" directory is not readable', '"null/data" directory is not readable',

55
tests/ThemeUtilsTest.php Normal file
View file

@ -0,0 +1,55 @@
<?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'));
}
}

View file

@ -2,6 +2,7 @@
require_once 'application/config/ConfigManager.php'; require_once 'application/config/ConfigManager.php';
require_once 'tests/Updater/DummyUpdater.php'; require_once 'tests/Updater/DummyUpdater.php';
require_once 'inc/rain.tpl.class.php';
/** /**
* Class UpdaterTest. * Class UpdaterTest.
@ -421,4 +422,48 @@ public function testDatastoreIdsNothingToDo()
$this->assertTrue($updater->updateMethodDatastoreIds()); $this->assertTrue($updater->updateMethodDatastoreIds());
$this->assertEquals($checksum, hash_file('sha1', self::$testDatastore)); $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);
}
} }

View file

@ -24,7 +24,8 @@
}, },
"resource": { "resource": {
"datastore": "tests\/utils\/config\/datastore.php", "datastore": "tests\/utils\/config\/datastore.php",
"data_dir": "tests\/utils\/config" "data_dir": "tests\/utils\/config",
"raintpl_tpl": "tpl/"
}, },
"plugins": { "plugins": {
"WALLABAG_VERSION": 1 "WALLABAG_VERSION": 1

View file

@ -1,7 +1,7 @@
<!DOCTYPE html> <!DOCTYPE html>
<html> <html>
<head>{include="includes"} <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> <script src="inc/awesomplete.min.js#"></script>
</head> </head>
<body onload="document.changetag.fromtag.focus();"> <body onload="document.changetag.fromtag.focus();">

View file

@ -19,6 +19,20 @@
<td><input type="text" name="titleLink" id="titleLink" size="50" value="{$titleLink}"><br/><label <td><input type="text" name="titleLink" id="titleLink" size="50" value="{$titleLink}"><br/><label
for="titleLink">(default value is: ?)</label></td> for="titleLink">(default value is: ?)</label></td>
</tr> </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> <tr>
<td><b>Timezone:</b></td> <td><b>Timezone:</b></td>
<td>{$timezone_form}</td> <td>{$timezone_form}</td>

View file

@ -103,7 +103,7 @@ strong {
} }
#pageheader #logo { #pageheader #logo {
background-image: url('../images/logo.png'); background-image: url('../../../images/logo.png');
background-repeat: no-repeat; background-repeat: no-repeat;
float: left; float: left;
margin: 0 10px 0 10px; margin: 0 10px 0 10px;
@ -803,6 +803,10 @@ div.dailyAbout img {
height: 14px; height: 14px;
} }
div.dailyEntryPermalink {
float: right;
}
div.dailyTitle { div.dailyTitle {
font-weight: bold; font-weight: bold;
font-size: 44pt; font-size: 44pt;

View file

@ -28,9 +28,9 @@
</div> </div>
<div class="dailyTitle"> <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 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>
<div class="dailyDate"> <div class="dailyDate">
@ -50,7 +50,7 @@
<div class="dailyEntry"> <div class="dailyEntry">
<div class="dailyEntryPermalink"> <div class="dailyEntryPermalink">
<a href="?{$value.shorturl}"> <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> </a>
</div> </div>
{if="!$hide_timestamps || isLoggedIn()"} {if="!$hide_timestamps || isLoggedIn()"}
@ -94,7 +94,7 @@
{$value} {$value}
{/loop} {/loop}
</div> </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> </div>
{include="page.footer"} {include="page.footer"}
</body> </body>

View file

@ -1,7 +1,7 @@
<!DOCTYPE html> <!DOCTYPE html>
<html> <html>
<head>{include="includes"} <head>{include="includes"}
<link type="text/css" rel="stylesheet" href="../inc/awesomplete.css" /> <link type="text/css" rel="stylesheet" href="inc/awesomplete.css#" />
</head> </head>
<body <body
{if="$link.title==''"}onload="document.linkform.lf_title.focus();" {if="$link.title==''"}onload="document.linkform.lf_title.focus();"

View file

Before

Width:  |  Height:  |  Size: 599 B

After

Width:  |  Height:  |  Size: 599 B

View file

Before

Width:  |  Height:  |  Size: 124 KiB

After

Width:  |  Height:  |  Size: 124 KiB

View file

Before

Width:  |  Height:  |  Size: 650 B

After

Width:  |  Height:  |  Size: 650 B

View file

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

View file

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

View file

Before

Width:  |  Height:  |  Size: 813 B

After

Width:  |  Height:  |  Size: 813 B

View file

Before

Width:  |  Height:  |  Size: 720 B

After

Width:  |  Height:  |  Size: 720 B

View file

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

View file

Before

Width:  |  Height:  |  Size: 714 B

After

Width:  |  Height:  |  Size: 714 B

View file

@ -6,9 +6,9 @@
<link rel="alternate" type="application/rss+xml" href="{$feedurl}?do=rss{$searchcrits}#" title="RSS Feed" /> <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 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 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="css/reset.css" />
<link type="text/css" rel="stylesheet" href="../inc/shaarli.css" /> <link type="text/css" rel="stylesheet" href="css/shaarli.css" />
{if="is_file('inc/user.css')"}<link type="text/css" rel="stylesheet" href="../inc/user.css" />{/if} {if="is_file('inc/user.css')"}<link type="text/css" rel="stylesheet" href="inc/user.css#" />{/if}
{loop="$plugins_includes.css_files"} {loop="$plugins_includes.css_files"}
<link type="text/css" rel="stylesheet" href="{$value}#"/> <link type="text/css" rel="stylesheet" href="{$value}#"/>
{/loop} {/loop}

View file

@ -1,7 +1,7 @@
<!DOCTYPE html> <!DOCTYPE html>
<html> <html>
<head> <head>
<link type="text/css" rel="stylesheet" href="../inc/awesomplete.css" /> <link type="text/css" rel="stylesheet" href="inc/awesomplete.css#" />
{include="includes"} {include="includes"}
</head> </head>
<body> <body>