--- /dev/null
+# EditorConfig: http://EditorConfig.org
+
+root = true
+
+[*]
+charset = utf-8
+end_of_line = lf
+insert_final_newline = true
+trim_trailing_whitespace = true
+indent_style = space
+indent_size = 4
+
+[*.{htaccess,html,xml}]
+indent_size = 2
+
+[*.php]
+max_line_length = 100
+
+[Dockerfile]
+max_line_length = 80
+
+[Makefile]
+indent_style = tab
*.min.js binary
# Exclude from Git archives
+.editorconfig export-ignore
.gitattributes export-ignore
.github export-ignore
.gitignore export-ignore
VirtualTam <virtualtam@flibidi.net> <tamisier.aurelien@gmail.com>
VirtualTam <virtualtam@flibidi.net> <virtualtam+github@flibidi.net>
VirtualTam <virtualtam@flibidi.net> <virtualtam@flibidi.org>
+Willi Eggeling <thewilli@gmail.com> <mail@wje-online.de>
+Willi Eggeling <thewilli@gmail.com> <thewilli@users.noreply.github.com>
- 518 ArthurHoaro <arthur@hoa.ro>
- 231 VirtualTam <virtualtam@flibidi.net>
- 147 nodiscc <nodiscc@gmail.com>
+ 537 ArthurHoaro <arthur@hoa.ro>
+ 252 VirtualTam <virtualtam@flibidi.net>
+ 148 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>
+ 9 Willi Eggeling <thewilli@gmail.com>
8 Christophe HENRY <christophe.henry@sbgodin.fr>
+ 6 B. van Berkum <dev@dotmpe.com>
5 Lucas Cimon <lucas.cimon@gmail.com>
4 Alexandre Alapetite <alexandre@alapetite.fr>
4 David Sferruzza <david.sferruzza@gmail.com>
1 Kevin Canévet <kevin@streamroot.io>
1 Knah Tsaeb <knah-tsaeb@knah-tsaeb.org>
1 Lionel Martin <renarddesmers@gmail.com>
+ 1 Mark Gerarts <mark.gerarts@gmail.com>
1 Marsup <marsup@gmail.com>
1 Sbgodin <Sbgodin@users.noreply.github.com>
1 TsT <tst2005@gmail.com>
The format is based on [Keep a Changelog](http://keepachangelog.com/)
and this project adheres to [Semantic Versioning](http://semver.org/).
+## [v0.9.2](https://github.com/shaarli/Shaarli/releases/tag/v0.9.2) - 2017-10-07
+
+**Major security issue fixed. Please update.**
+
+### Added
+- Tag search now supports wildcards `*`
+- New setting `privacy.force_login` which can be used with `privacy.hide_public_links` to redirect anonymous users to the login page.
+- New setting `general.default_note_title` used to override default `Note:` title prefix for notes.
+- Add a version hash for asset loading to prevent browser's cache issue
+
+### Changed
+- The "Remember me" checkbox is unchecked by default
+- The default value of the "Remember me" checkbox can be configured under `data/config.json.php`
+
+### Removed
+- Remove obsolete PHP magic quote support
+
+### Fixed
+- Generates a permalink URL if the URL is set to blank
+- Replace links to the old GitHub wiki with ReadTheDocs URIs
+- Use single quotes in the note bookmarklet
+- Daily page if there is no link
+- Bulk link deletion with a single link
+- HTTPS detection behind a reverse proxy
+- Travis tests environment and localization
+- Improve template paths robustness (trailing slash)
+- Robustness: safer gzinflate/zlib usage
+- Description links parsing with parenthesis (without Markdown)
+- Templates:
+ - Sort the tag cloud alphabetically
+ - Firefox social title
+ - Improved visited link color
+ - Fix jumpy textarea with long content in post edit
+
+### Security
+
+- Vulnerability introduced in v0.9.1 fixed.
+
## [v0.9.1](https://github.com/shaarli/Shaarli/releases/tag/v0.9.1) - 2017-08-23
The documentation has been migrated to ReadTheDocs:
[![](https://img.shields.io/badge/stable-v0.8.4-blue.svg)](https://github.com/shaarli/Shaarli/releases/tag/v0.8.4)
[![](https://img.shields.io/travis/shaarli/Shaarli/stable.svg?label=stable)](https://travis-ci.org/shaarli/Shaarli)
•
-[![](https://img.shields.io/badge/latest-v0.9.1-blue.svg)](https://github.com/shaarli/Shaarli/releases/tag/v0.9.1)
+[![](https://img.shields.io/badge/latest-v0.9.2-blue.svg)](https://github.com/shaarli/Shaarli/releases/tag/v0.9.2)
[![](https://img.shields.io/travis/shaarli/Shaarli/latest.svg?label=latest)](https://travis-ci.org/shaarli/Shaarli)
•
[![](https://img.shields.io/badge/master-v0.9.x-blue.svg)](https://github.com/shaarli/Shaarli)
return $errors;
}
+
+ /**
+ * Returns a salted hash representing the current Shaarli version.
+ *
+ * Useful for assets browser cache.
+ *
+ * @param string $currentVersion of Shaarli
+ * @param string $salt User personal salt, also used for the authentication
+ *
+ * @return string version hash
+ */
+ public static function getVersionHash($currentVersion, $salt)
+ {
+ return hash_hmac('sha256', $currentVersion, $salt);
+ }
}
* - UPDATED: link updated
* - DELETED: link deleted
* - SETTINGS: the settings have been updated through the UI.
+ * - IMPORT: bulk links import
*
* Note: new events are put at the beginning of the file and history array.
*/
*/
const SETTINGS = 'SETTINGS';
+ /**
+ * @var string Action key: a bulk import has been processed.
+ */
+ const IMPORT = 'IMPORT';
+
/**
* @var string History file path.
*/
$this->addEvent(self::SETTINGS);
}
+ /**
+ * Add Event: bulk import.
+ *
+ * Note: we don't store links add/update one by one since it can have a huge impact on performances.
+ */
+ public function importLinks()
+ {
+ $this->addEvent(self::IMPORT);
+ }
+
/**
* Save a new event and write it in the history file.
*
* @param int $importCount how many links were imported
* @param int $overwriteCount how many links were overwritten
* @param int $skipCount how many links were skipped
+ * @param int $duration how many seconds did the import take
*
* @return string Summary of the bookmark import status
*/
$filesize,
$importCount=0,
$overwriteCount=0,
- $skipCount=0
+ $skipCount=0,
+ $duration=0
)
{
$status = 'File '.$filename.' ('.$filesize.' bytes) ';
if ($importCount == 0 && $overwriteCount == 0 && $skipCount == 0) {
$status .= 'has an unknown file format. Nothing was imported.';
} else {
- $status .= 'was successfully processed: '.$importCount.' links imported, ';
+ $status .= 'was successfully processed in '. $duration .' seconds: ';
+ $status .= $importCount.' links imported, ';
$status .= $overwriteCount.' links overwritten, ';
$status .= $skipCount.' links skipped.';
}
*/
public static function import($post, $files, $linkDb, $conf, $history)
{
+ $start = time();
$filename = $files['filetoupload']['name'];
$filesize = $files['filetoupload']['size'];
$data = file_get_contents($files['filetoupload']['tmp_name']);
$linkDb[$existingLink['id']] = $newLink;
$importCount++;
$overwriteCount++;
- $history->updateLink($newLink);
continue;
}
$newLink['shorturl'] = link_small_hash($newLink['created'], $newLink['id']);
$linkDb[$newLink['id']] = $newLink;
$importCount++;
- $history->addLink($newLink);
}
$linkDb->save($conf->get('resource.page_cache'));
+ $history->importLinks();
+
+ $duration = time() - $start;
return self::importStatus(
$filename,
$filesize,
$importCount,
$overwriteCount,
- $skipCount
+ $skipCount,
+ $duration
);
}
}
try {
$version = ApplicationUtils::checkUpdate(
- shaarli_version,
+ SHAARLI_VERSION,
$this->conf->get('resource.update_check'),
$this->conf->get('updates.check_updates_interval'),
$this->conf->get('updates.check_updates'),
}
$this->tpl->assign('searchcrits', $searchcrits);
$this->tpl->assign('source', index_url($_SERVER));
- $this->tpl->assign('version', shaarli_version);
+ $this->tpl->assign('version', SHAARLI_VERSION);
+ $this->tpl->assign(
+ 'version_hash',
+ ApplicationUtils::getVersionHash(SHAARLI_VERSION, $this->conf->get('credentials.salt'))
+ );
$this->tpl->assign('scripturl', index_url($_SERVER));
$this->tpl->assign('privateonly', !empty($_SESSION['privateonly'])); // Show only private links?
$this->tpl->assign('untaggedonly', !empty($_SESSION['untaggedonly']));
$this->tpl->assign('feed_type', $this->conf->get('feed.show_atom', true) !== false ? 'atom' : 'rss');
$this->tpl->assign('hide_timestamps', $this->conf->get('privacy.hide_timestamps', false));
$this->tpl->assign('token', getToken($this->conf));
+
if ($this->linkDB !== null) {
$this->tpl->assign('tags', $this->linkDB->linksCountPerTag());
}
*/
public function updateMethodCheckUpdateRemoteBranch()
{
- if (shaarli_version === 'dev' || $this->conf->get('updates.check_updates_branch') === 'latest') {
+ if (SHAARLI_VERSION === 'dev' || $this->conf->get('updates.check_updates_branch') === 'latest') {
return true;
}
$latestMajor = $matches[1];
// Get current major version digit
- preg_match('/(\d+)\.\d+$/', shaarli_version, $matches);
+ preg_match('/(\d+)\.\d+$/', SHAARLI_VERSION, $matches);
$currentMajor = $matches[1];
if ($currentMajor === $latestMajor) {
<IfModule version_module>
<IfVersion >= 2.4>
- Require all denied
+ Require all denied
+ <Files "user.css">
+ Require all granted
+ </Files>
</IfVersion>
<IfVersion < 2.4>
- Allow from none
- Deny from all
+ Allow from none
+ Deny from all
+ <Files "user.css">
+ Allow from all
+ </Files>
</IfVersion>
</IfModule>
- `data/ipbans.php` - banned IP addresses
- `data/updates.txt` - contains all automatic update to the configuration and datastore files already run
-See [Shaarli configuration](Shaarli configuration) for more information about Shaarli resources.
+See [Shaarli configuration](Shaarli-configuration) for more information about Shaarli resources.
It is recommended to backup this repository _before_ starting updating/upgrading Shaarli:
- backup the `data` directory
- install or update Shaarli:
- - fresh installation - see [Download and installation](Download and installation)
+ - fresh installation - see [Download and installation](Download-and-installation)
- update - see the following sections
- check or restore the `data` directory
All tagged revisions can be downloaded as tarballs or ZIP archives from the [releases](https://github.com/shaarli/Shaarli/releases) page.
-We recommend that you use the latest release tarball with the `-full` suffix. It contains the dependencies, please read [Download and installation](Download and installation) for `git` complete instructions.
+We recommend that you use the latest release tarball with the `-full` suffix. It contains the dependencies, please read [Download and installation](Download-and-installation) for `git` complete instructions.
Once downloaded, extract the archive locally and update your remote installation (e.g. via FTP) -be sure you keep the content of the `data` directory!
-After upgrading, access your fresh Shaarli installation from a web browser; the configuration and data store will then be automatically updated, and new settings added to `data/config.json.php` (see [Shaarli configuration](Shaarli configuration) for more details).
+After upgrading, access your fresh Shaarli installation from a web browser; the configuration and data store will then be automatically updated, and new settings added to `data/config.json.php` (see [Shaarli configuration](Shaarli-configuration) for more details).
## Upgrading with Git
#### Step 3: configuration
-After migrating, access your fresh Shaarli installation from a web browser; the configuration will then be automatically updated, and new settings added to `data/config.php` (see [Shaarli configuration](Shaarli configuration) for more details).
+After migrating, access your fresh Shaarli installation from a web browser; the configuration will then be automatically updated, and new settings added to `data/config.php` (see [Shaarli configuration](Shaarli-configuration) for more details).
## Troubleshooting
Digest: sha256:c584131da2ac1948aa3e66468a4424b6aea2f33acba7cec0b631bdb56254c4fe
Status: Downloaded newer image for debian:wheezy
```
+
+Docker re-uses layers already downloaded. In other words if you have images based on Alpine or some Ubuntu version for example, those can share disk space.
+
+### Start a container
+A container is an instance created from an image, that can be run and that keeps running until its main process exits. Or until the user stops the container.
+
+The simplest way to start a container from image is ``docker run``. It also pulls the image for you if it is not locally available. For more advanced use, refer to ``docker create``.
+
+Stopped containers are not destroyed, unless you specify ``--rm``. To view all created, running and stopped containers, enter:
+```bash
+$ docker ps -a
+```
+
+Some containers may be designed or configured to be restarted, others are not. Also remember both network ports and volumes of a container are created on start, and not editable later.
+
+### Access a running container
+A running container is accessible using ``docker exec``, or ``docker copy``. You can use ``exec`` to start a root shell in the Shaarli container:
+```bash
+$ docker exec -ti <container-name-or-id> bash
+```
+Note the names and ID's of containers are listed in ``docker ps``. You can even type only one or two letters of the ID, given they are unique.
+
+Access can also be through one or more network ports, or disk volumes. Both are specified on and fixed on ``docker create`` or ``run``.
+
+You can view the console output of the main container process too:
+```bash
+$ docker logs -f <container-name-or-id>
+```
+
+### Docker disk use
+Trying out different images can fill some gigabytes of disk quickly. Besides images, the docker volumes usually take up most disk space.
+
+If you care only about trying out docker and not about what is running or saved, the following commands should help you out quickly if you run low on disk space:
+
+```bash
+$ docker rmi -f $(docker images -aq) # remove or mark all images for disposal
+$ docker volume rm $(docker volume ls -q) # remove all volumes
+```
+
+### Systemd config
+Systemd is the process manager of choice on Debian-based distributions. Once you have a ``docker`` service installed, you can use the following steps to set up Shaarli to run on system start.
+
+```bash
+systemctl enable /etc/systemd/system/docker.shaarli.service
+systemctl start docker.shaarli
+systemctl status docker.*
+journalctl -f # inspect system log if needed
+```
+
+You will need sudo or a root terminal to perform some or all of the steps above. Here are the contents for the service file:
+```
+[Unit]
+Description=Shaarli Bookmark Manager Container
+After=docker.service
+Requires=docker.service
+
+
+[Service]
+Restart=always
+
+# Put any environment you want in an included file, like $host- or $domainname in this example
+EnvironmentFile=/etc/sysconfig/box-environment
+
+# It's just an example..
+ExecStart=/usr/bin/docker run \
+ -p 28010:80 \
+ --name ${hostname}-shaarli \
+ --hostname shaarli.${domainname} \
+ -v /srv/docker-volumes-local/shaarli-data:/var/www/shaarli/data:rw \
+ -v /etc/localtime:/etc/localtime:ro \
+ shaarli/shaarli:latest
+
+ExecStop=/usr/bin/docker rm -f ${hostname}-shaarli
+
+
+[Install]
+WantedBy=multi-user.target
+```
Login: `demo`; Password: `demo`
+Docker users can start a personal instance from an [autobuild image](https://hub.docker.com/r/shaarli/shaarli/). For example to start a temporary Shaarli at ``localhost:8000``, and keep session data (config, storage):
+```
+MY_SHAARLI_VOLUME=$(cd /path/to/shaarli/data/ && pwd -P)
+docker run -ti --rm \
+ -p 8000:80 \
+ -v $MY_SHAARLI_VOLUME:/var/www/shaarli/data \
+ shaarli/shaarli
+```
+
+A brief guide on getting starting using docker is given in [Docker 101](docker/docker-101).
+To learn more about user data and how to keep it across versions, please see [Upgrade and Migration](Upgrade-and-migration) documentation.
## Features
exit;
}
-define('shaarli_version', ApplicationUtils::getVersion(__DIR__ .'/'. ApplicationUtils::$VERSION_FILE));
+define('SHAARLI_VERSION', ApplicationUtils::getVersion(__DIR__ .'/'. ApplicationUtils::$VERSION_FILE));
// Force cookie path (but do not change lifetime)
$cookie = session_get_cookie_params();
}
$data = array(
- 'search_tags' => implode(' ', $filteringTags),
+ 'search_tags' => implode(' ', escape($filteringTags)),
'tags' => $tagList,
);
$pluginManager->executeHooks('render_tagcloud', $data, array('loggedin' => isLoggedIn()));
}
$data = [
- 'search_tags' => implode(' ', $filteringTags),
+ 'search_tags' => implode(' ', escape($filteringTags)),
'tags' => $tags,
];
$pluginManager->executeHooks('render_taglist', $data, ['loggedin' => isLoggedIn()]);
public function testImportInternetExplorerEncoding()
{
$files = file2array('internet_explorer_encoding.htm');
- $this->assertEquals(
- 'File internet_explorer_encoding.htm (356 bytes) was successfully processed:'
+ $this->assertStringMatchesFormat(
+ 'File internet_explorer_encoding.htm (356 bytes) was successfully processed in %d seconds:'
.' 1 links imported, 0 links overwritten, 0 links skipped.',
NetscapeBookmarkUtils::import([], $files, $this->linkDb, $this->conf, $this->history)
);
public function testImportNested()
{
$files = file2array('netscape_nested.htm');
- $this->assertEquals(
- 'File netscape_nested.htm (1337 bytes) was successfully processed:'
+ $this->assertStringMatchesFormat(
+ 'File netscape_nested.htm (1337 bytes) was successfully processed in %d seconds:'
.' 8 links imported, 0 links overwritten, 0 links skipped.',
NetscapeBookmarkUtils::import([], $files, $this->linkDb, $this->conf, $this->history)
);
public function testImportDefaultPrivacyNoPost()
{
$files = file2array('netscape_basic.htm');
- $this->assertEquals(
- 'File netscape_basic.htm (482 bytes) was successfully processed:'
+ $this->assertStringMatchesFormat(
+ 'File netscape_basic.htm (482 bytes) was successfully processed in %d seconds:'
.' 2 links imported, 0 links overwritten, 0 links skipped.',
NetscapeBookmarkUtils::import([], $files, $this->linkDb, $this->conf, $this->history)
);
{
$post = array('privacy' => 'default');
$files = file2array('netscape_basic.htm');
- $this->assertEquals(
- 'File netscape_basic.htm (482 bytes) was successfully processed:'
+ $this->assertStringMatchesFormat(
+ 'File netscape_basic.htm (482 bytes) was successfully processed in %d seconds:'
.' 2 links imported, 0 links overwritten, 0 links skipped.',
NetscapeBookmarkUtils::import($post, $files, $this->linkDb, $this->conf, $this->history)
);
{
$post = array('privacy' => 'public');
$files = file2array('netscape_basic.htm');
- $this->assertEquals(
- 'File netscape_basic.htm (482 bytes) was successfully processed:'
+ $this->assertStringMatchesFormat(
+ 'File netscape_basic.htm (482 bytes) was successfully processed in %d seconds:'
.' 2 links imported, 0 links overwritten, 0 links skipped.',
NetscapeBookmarkUtils::import($post, $files, $this->linkDb, $this->conf, $this->history)
);
{
$post = array('privacy' => 'private');
$files = file2array('netscape_basic.htm');
- $this->assertEquals(
- 'File netscape_basic.htm (482 bytes) was successfully processed:'
+ $this->assertStringMatchesFormat(
+ 'File netscape_basic.htm (482 bytes) was successfully processed in %d seconds:'
.' 2 links imported, 0 links overwritten, 0 links skipped.',
NetscapeBookmarkUtils::import($post, $files, $this->linkDb, $this->conf, $this->history)
);
// import links as private
$post = array('privacy' => 'private');
- $this->assertEquals(
- 'File netscape_basic.htm (482 bytes) was successfully processed:'
+ $this->assertStringMatchesFormat(
+ 'File netscape_basic.htm (482 bytes) was successfully processed in %d seconds:'
.' 2 links imported, 0 links overwritten, 0 links skipped.',
NetscapeBookmarkUtils::import($post, $files, $this->linkDb, $this->conf, $this->history)
);
'privacy' => 'public',
'overwrite' => 'true'
);
- $this->assertEquals(
- 'File netscape_basic.htm (482 bytes) was successfully processed:'
+ $this->assertStringMatchesFormat(
+ 'File netscape_basic.htm (482 bytes) was successfully processed in %d seconds:'
.' 2 links imported, 2 links overwritten, 0 links skipped.',
NetscapeBookmarkUtils::import($post, $files, $this->linkDb, $this->conf, $this->history)
);
// import links as public
$post = array('privacy' => 'public');
- $this->assertEquals(
- 'File netscape_basic.htm (482 bytes) was successfully processed:'
+ $this->assertStringMatchesFormat(
+ 'File netscape_basic.htm (482 bytes) was successfully processed in %d seconds:'
.' 2 links imported, 0 links overwritten, 0 links skipped.',
NetscapeBookmarkUtils::import($post, $files, $this->linkDb, $this->conf, $this->history)
);
'privacy' => 'private',
'overwrite' => 'true'
);
- $this->assertEquals(
- 'File netscape_basic.htm (482 bytes) was successfully processed:'
+ $this->assertStringMatchesFormat(
+ 'File netscape_basic.htm (482 bytes) was successfully processed in %d seconds:'
.' 2 links imported, 2 links overwritten, 0 links skipped.',
NetscapeBookmarkUtils::import($post, $files, $this->linkDb, $this->conf, $this->history)
);
{
$post = array('privacy' => 'public');
$files = file2array('netscape_basic.htm');
- $this->assertEquals(
- 'File netscape_basic.htm (482 bytes) was successfully processed:'
+ $this->assertStringMatchesFormat(
+ 'File netscape_basic.htm (482 bytes) was successfully processed in %d seconds:'
.' 2 links imported, 0 links overwritten, 0 links skipped.',
NetscapeBookmarkUtils::import($post, $files, $this->linkDb, $this->conf, $this->history)
);
// re-import as private, DO NOT enable overwriting
$post = array('privacy' => 'private');
- $this->assertEquals(
- 'File netscape_basic.htm (482 bytes) was successfully processed:'
+ $this->assertStringMatchesFormat(
+ 'File netscape_basic.htm (482 bytes) was successfully processed in %d seconds:'
.' 0 links imported, 0 links overwritten, 2 links skipped.',
NetscapeBookmarkUtils::import($post, $files, $this->linkDb, $this->conf, $this->history)
);
'default_tags' => 'tag1,tag2 tag3'
);
$files = file2array('netscape_basic.htm');
- $this->assertEquals(
- 'File netscape_basic.htm (482 bytes) was successfully processed:'
+ $this->assertStringMatchesFormat(
+ 'File netscape_basic.htm (482 bytes) was successfully processed in %d seconds:'
.' 2 links imported, 0 links overwritten, 0 links skipped.',
NetscapeBookmarkUtils::import($post, $files, $this->linkDb, $this->conf, $this->history)
);
'default_tags' => 'tag1&,tag2 "tag3"'
);
$files = file2array('netscape_basic.htm');
- $this->assertEquals(
- 'File netscape_basic.htm (482 bytes) was successfully processed:'
+ $this->assertStringMatchesFormat(
+ 'File netscape_basic.htm (482 bytes) was successfully processed in %d seconds:'
.' 2 links imported, 0 links overwritten, 0 links skipped.',
NetscapeBookmarkUtils::import($post, $files, $this->linkDb, $this->conf, $this->history)
);
public function testImportSameDate()
{
$files = file2array('same_date.htm');
- $this->assertEquals(
- 'File same_date.htm (453 bytes) was successfully processed:'
+ $this->assertStringMatchesFormat(
+ 'File same_date.htm (453 bytes) was successfully processed in %d seconds:'
.' 3 links imported, 0 links overwritten, 0 links skipped.',
NetscapeBookmarkUtils::import(array(), $files, $this->linkDb, $this->conf, $this->history)
);
'overwrite' => 'true',
];
$files = file2array('netscape_basic.htm');
- $nbLinks = 2;
NetscapeBookmarkUtils::import($post, $files, $this->linkDb, $this->conf, $this->history);
$history = $this->history->getHistory();
- $this->assertEquals($nbLinks, count($history));
- foreach ($history as $value) {
- $this->assertEquals(History::CREATED, $value['event']);
- $this->assertTrue(new DateTime('-5 seconds') < $value['datetime']);
- $this->assertTrue(is_int($value['id']));
- }
+ $this->assertEquals(1, count($history));
+ $this->assertEquals(History::IMPORT, $history[0]['event']);
+ $this->assertTrue(new DateTime('-5 seconds') < $history[0]['datetime']);
// re-import as private, enable overwriting
NetscapeBookmarkUtils::import($post, $files, $this->linkDb, $this->conf, $this->history);
$history = $this->history->getHistory();
- $this->assertEquals($nbLinks * 2, count($history));
- for ($i = 0 ; $i < $nbLinks ; $i++) {
- $this->assertEquals(History::UPDATED, $history[$i]['event']);
- $this->assertTrue(new DateTime('-5 seconds') < $history[$i]['datetime']);
- $this->assertTrue(is_int($history[$i]['id']));
- }
+ $this->assertEquals(2, count($history));
+ $this->assertEquals(History::IMPORT, $history[0]['event']);
+ $this->assertTrue(new DateTime('-5 seconds') < $history[0]['datetime']);
+ $this->assertEquals(History::IMPORT, $history[1]['event']);
+ $this->assertTrue(new DateTime('-5 seconds') < $history[1]['datetime']);
}
}
<link rel="alternate" type="application/atom+xml" href="{$feedurl}?do=atom{$searchcrits}#" title="ATOM Feed" />
<link rel="alternate" type="application/rss+xml" href="{$feedurl}?do=rss{$searchcrits}#" title="RSS Feed" />
<link href="img/favicon.png" rel="shortcut icon" type="image/png" />
-<link type="text/css" rel="stylesheet" href="css/pure.min.css" />
-<link type="text/css" rel="stylesheet" href="css/grids-responsive.min.css">
-<link type="text/css" rel="stylesheet" href="css/pure-extras.css">
-<link type="text/css" rel="stylesheet" href="css/font-awesome.min.css" />
-<link type="text/css" rel="stylesheet" href="inc/awesomplete.css#" />
-<link type="text/css" rel="stylesheet" href="css/shaarli.css" />
+<link type="text/css" rel="stylesheet" href="css/pure.min.css?v={$version_hash}" />
+<link type="text/css" rel="stylesheet" href="css/grids-responsive.min.css?v={$version_hash}">
+<link type="text/css" rel="stylesheet" href="css/pure-extras.css?v={$version_hash}">
+<link type="text/css" rel="stylesheet" href="css/font-awesome.min.css?v={$version_hash}" />
+<link type="text/css" rel="stylesheet" href="inc/awesomplete.css?v={$version_hash}#" />
+<link type="text/css" rel="stylesheet" href="css/shaarli.css?v={$version_hash}" />
{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}#"/>
+ <link type="text/css" rel="stylesheet" href="{$value}?v={$version_hash}#"/>
{/loop}
<link rel="search" type="application/opensearchdescription+xml" href="?do=opensearch#" title="Shaarli search - {$shaarlititle}"/>
\ No newline at end of file
};
function init () {
function resize () {
+ /* Fix jumpy resizing: https://stackoverflow.com/a/18262927/1484919 */
+ var scrollTop = window.pageYOffset ||
+ (document.documentElement || document.body.parentNode || document.body).scrollTop;
+
description.style.height = 'auto';
description.style.height = description.scrollHeight+10+'px';
+
+ window.scrollTo(0, scrollTop);
}
/* 0-timeout to get the already changed text */
function delayedResize () {
<script src="{$value}#"></script>
{/loop}
-<script src="js/shaarli.js"></script>
-<script src="inc/awesomplete.js#"></script>
-<script src="inc/awesomplete-multiple-tags.js#"></script>
+<script src="js/shaarli.js?v={$version_hash}"></script>
+<script src="inc/awesomplete.js?v={$version_hash}#"></script>
+<script src="inc/awesomplete-multiple-tags.js?v={$version_hash}#"></script>
<input type="hidden" name="do" value="tagcloud">
<input type="text" name="searchtags" placeholder="{'Filter by tag'|t}"
{if="!empty($search_tags)"}
- value="{$search_tags}"
+ value="{$search_tags}"
{/if}
autocomplete="off" data-multiple data-autofirst data-minChars="1"
data-list="{loop="$tags"}{$key}, {/loop}"