# User plugin configuration
plugins/*/config.php
+
+# 3rd party themes
+tpl/*
+!tpl/default
## [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
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
'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;
+
+/**
+ * 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;
+ }
+}
$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;
+ }
}
/**
$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',
$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;
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);
$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);
$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);
+ }
}
},
"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>
}
#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;
</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();"
<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('inc/user.css')"}<link type="text/css" rel="stylesheet" href="inc/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>