sudo: false
-dist: precise
+dist: trusty
language: php
-addons:
- apt:
- packages:
- - locales
- - language-pack-de
- - language-pack-fr
cache:
directories:
- $HOME/.composer/cache
install:
- composer self-update
- composer install --prefer-dist
+ - locale -a
script:
- make clean
- make check_permissions
all: static_analysis_summary check_permissions test
+##
+# Docker test adapter
+#
+# Shaarli sources and vendored libraries are copied from a shared volume
+# to a user-owned directory to enable running tests as a non-root user.
+##
+docker_%:
+ rsync -az /shaarli/ ~/shaarli/
+ cd ~/shaarli && make $*
+
##
# Concise status of the project
# These targets are non-blocking: || exit 0
find vendor/ -name ".git" -type d -exec rm -rf {} +
### generate a release tarball and include 3rd-party dependencies
-release_tar: composer_dependencies doc_html
+release_tar: composer_dependencies htmldoc
git archive --prefix=$(ARCHIVE_PREFIX) -o $(ARCHIVE_VERSION).tar HEAD
tar rvf $(ARCHIVE_VERSION).tar --transform "s|^vendor|$(ARCHIVE_PREFIX)vendor|" vendor/
tar rvf $(ARCHIVE_VERSION).tar --transform "s|^doc/html|$(ARCHIVE_PREFIX)doc/html|" doc/html/
gzip $(ARCHIVE_VERSION).tar
### generate a release zip and include 3rd-party dependencies
-release_zip: composer_dependencies doc_html
+release_zip: composer_dependencies htmldoc
git archive --prefix=$(ARCHIVE_PREFIX) -o $(ARCHIVE_VERSION).zip -9 HEAD
mkdir -p $(ARCHIVE_PREFIX)/{doc,vendor}
rsync -a doc/html/ $(ARCHIVE_PREFIX)doc/html/
@rm -rf doxygen
@( cat Doxyfile ; echo "PROJECT_NUMBER=`git describe`" ) | doxygen -
-### Convert local markdown documentation to HTML
-#
-# For all pages:
-# - convert GitHub-flavoured relative links to standard Markdown
-# - generate html documentation with mkdocs
-htmlpages:
+### generate HTML documentation from Markdown pages with MkDocs
+htmldoc:
python3 -m venv venv/
bash -c 'source venv/bin/activate; \
pip install mkdocs; \
mkdocs build'
find doc/html/ -type f -exec chmod a-x '{}' \;
rm -r venv
-
-doc_html: authors htmlpages
return array_pop($ips);
}
+
+/**
+ * Returns true if Shaarli's currently browsed in HTTPS.
+ * Supports reverse proxies (if the headers are correctly set).
+ *
+ * @param array $server $_SERVER.
+ *
+ * @return bool true if HTTPS, false otherwise.
+ */
+function is_https($server)
+{
+
+ if (isset($server['HTTP_X_FORWARDED_PORT'])) {
+ // Keep forwarded port
+ if (strpos($server['HTTP_X_FORWARDED_PORT'], ',') !== false) {
+ $ports = explode(',', $server['HTTP_X_FORWARDED_PORT']);
+ $port = trim($ports[0]);
+ } else {
+ $port = $server['HTTP_X_FORWARDED_PORT'];
+ }
+
+ if ($port == '443') {
+ return true;
+ }
+ }
+
+ return ! empty($server['HTTPS']);
+}
$this->setEmpty('privacy.default_private_links', false);
$this->setEmpty('privacy.hide_public_links', false);
+ $this->setEmpty('privacy.force_login', false);
$this->setEmpty('privacy.hide_timestamps', false);
+ // default state of the 'remember me' checkbox of the login form
+ $this->setEmpty('privacy.remember_user_default', true);
$this->setEmpty('thumbnail.enable_thumbnails', true);
$this->setEmpty('thumbnail.enable_localcache', true);
## Increment the version code, update docs, create and push a signed tag
+### Update the list of Git contributors
+```bash
+$ make authors
+$ git commit -s -m "Update AUTHORS"
+```
+
### Create and merge a Pull Request
This one is pretty straightforward ;-)
- **default_private_links**: Check the private checkbox by default for every new link.
- **hide_public_links**: All links are hidden while logged out.
+- **force_login**: if **hide_public_links** and this are set to `true`, all anonymous users are redirected to the login page.
- **hide_timestamps**: Timestamps are hidden.
+- **remember_user_default**: Default state of the login page's *remember me* checkbox
+ - `true`: checked by default, `false`: unchecked by default
### Feed
"privacy": {
"default_private_links": true,
"hide_public_links": false,
- "hide_timestamps": false
+ "force_login": false,
+ "hide_timestamps": false,
+ "remember_user_default": true
},
"thumbnail": {
"enable_thumbnails": true,
--- /dev/null
+## Running tests inside Docker containers
+
+Read first:
+
+- [Docker 101](docker/docker-101.md)
+- [Docker resources](docker/resources.md)
+- [Unit tests](Unit-tests.md)
+
+### Docker test images
+
+Test Dockerfiles are located under `docker/tests/<distribution>/Dockerfile`,
+and can be used to build Docker images to run Shaarli test suites under common
+Linux environments.
+
+Dockerfiles are provided for the following environments:
+
+- `alpine36` - [Alpine 3.6](https://www.alpinelinux.org/downloads/)
+- `debian8` - [Debian 8 Jessie](https://www.debian.org/DebianJessie) (oldstable)
+- `debian9` - [Debian 9 Stretch](https://wiki.debian.org/DebianStretch) (stable)
+- `ubuntu16` - [Ubuntu 16.04 Xenial Xerus](http://releases.ubuntu.com/16.04/) (LTS)
+
+What's behind the curtains:
+
+- each image provides:
+ - a base Linux OS
+ - Shaarli PHP dependencies (OS packages)
+ - test PHP dependencies (OS packages)
+ - Composer
+- the local workspace is mapped to the container's `/shaarli/` directory,
+- the files are rsync'd to so tests are run using a standard Linux user account
+ (running tests as `root` would bypass permission checks and may hide issues)
+- the tests are run inside the container.
+
+### Building test images
+
+```bash
+# build the Debian 9 Docker image
+$ cd /path/to/shaarli
+$ cd docker/test/debian9
+$ docker build -t shaarli-test:debian9 .
+```
+
+### Running tests
+
+```bash
+$ cd /path/to/shaarli
+
+# install/update 3rd-party test dependencies
+$ composer install --prefer-dist
+
+# run tests using the freshly built image
+$ docker run -v $PWD:/shaarli shaarli-test:debian9 docker_test
+
+# run the full test campaign
+$ docker run -v $PWD:/shaarli shaarli-test:debian9 docker_all_tests
+```
--- /dev/null
+FROM alpine:3.6
+MAINTAINER Shaarli Community
+
+RUN apk --update --no-cache add \
+ ca-certificates \
+ curl \
+ make \
+ php7 \
+ php7-ctype \
+ php7-curl \
+ php7-dom \
+ php7-gd \
+ php7-iconv \
+ php7-intl \
+ php7-json \
+ php7-mbstring \
+ php7-openssl \
+ php7-phar \
+ php7-session \
+ php7-simplexml \
+ php7-tokenizer \
+ php7-xdebug \
+ php7-xml \
+ php7-zlib \
+ rsync
+
+RUN curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer
+
+RUN mkdir /shaarli
+WORKDIR /shaarli
+VOLUME /shaarli
+
+ENTRYPOINT ["make"]
+CMD []
--- /dev/null
+FROM debian:jessie
+MAINTAINER Shaarli Community
+
+ENV TERM dumb
+ENV DEBIAN_FRONTEND noninteractive
+ENV LANG en_US.UTF-8
+ENV LANGUAGE en_US:en
+
+RUN apt-get update \
+ && apt-get install --no-install-recommends -y \
+ ca-certificates \
+ curl \
+ locales \
+ make \
+ php5 \
+ php5-curl \
+ php5-gd \
+ php5-intl \
+ php5-xdebug \
+ rsync \
+ && apt-get clean
+
+RUN locale-gen en_US.UTF-8 \
+ && locale-gen de_DE.UTF-8 \
+ && locale-gen fr_FR.UTF-8
+
+ADD https://getcomposer.org/composer.phar /usr/local/bin/composer
+RUN chmod 755 /usr/local/bin/composer
+
+RUN mkdir /shaarli
+WORKDIR /shaarli
+VOLUME /shaarli
+
+ENTRYPOINT ["make"]
+CMD []
--- /dev/null
+FROM debian:stretch
+MAINTAINER Shaarli Community
+
+ENV TERM dumb
+ENV DEBIAN_FRONTEND noninteractive
+ENV LANG en_US.UTF-8
+ENV LANGUAGE en_US:en
+
+RUN apt-get update \
+ && apt-get install --no-install-recommends -y \
+ ca-certificates \
+ curl \
+ locales \
+ make \
+ php7.0 \
+ php7.0-curl \
+ php7.0-gd \
+ php7.0-intl \
+ php7.0-xml \
+ php-xdebug \
+ rsync \
+ && apt-get clean
+
+RUN locale-gen en_US.UTF-8 \
+ && locale-gen de_DE.UTF-8 \
+ && locale-gen fr_FR.UTF-8
+
+ADD https://getcomposer.org/composer.phar /usr/local/bin/composer
+RUN chmod 755 /usr/local/bin/composer
+
+RUN mkdir /shaarli
+WORKDIR /shaarli
+VOLUME /shaarli
+
+ENTRYPOINT ["make"]
+CMD []
--- /dev/null
+FROM ubuntu:16.04
+MAINTAINER Shaarli Community
+
+ENV TERM dumb
+ENV DEBIAN_FRONTEND noninteractive
+ENV LANG en_US.UTF-8
+ENV LANGUAGE en_US:en
+
+RUN apt-get update \
+ && apt-get install --no-install-recommends -y \
+ ca-certificates \
+ curl \
+ language-pack-de \
+ language-pack-en \
+ language-pack-fr \
+ locales \
+ make \
+ php7.0 \
+ php7.0-curl \
+ php7.0-gd \
+ php7.0-intl \
+ php7.0-xml \
+ php-xdebug \
+ rsync \
+ && apt-get clean
+
+ADD https://getcomposer.org/composer.phar /usr/local/bin/composer
+RUN chmod 755 /usr/local/bin/composer
+
+RUN useradd -m dev \
+ && mkdir /shaarli
+USER dev
+WORKDIR /shaarli
+
+ENTRYPOINT ["make"]
+CMD []
*/
function showDaily($pageBuilder, $LINKSDB, $conf, $pluginManager)
{
- $day=date('Ymd',strtotime('-1 day')); // Yesterday, in format YYYYMMDD.
- if (isset($_GET['day'])) $day=$_GET['day'];
+ $day = date('Ymd', strtotime('-1 day')); // Yesterday, in format YYYYMMDD.
+ if (isset($_GET['day'])) {
+ $day = $_GET['day'];
+ }
$days = $LINKSDB->days();
- $i = array_search($day,$days);
- if ($i===false) { $i=count($days)-1; $day=$days[$i]; }
- $previousday='';
- $nextday='';
- if ($i!==false)
- {
- if ($i>=1) $previousday=$days[$i-1];
- if ($i<count($days)-1) $nextday=$days[$i+1];
+ $i = array_search($day, $days);
+ if ($i === false && count($days)) {
+ // no links for day, but at least one day with links
+ $i = count($days) - 1;
+ $day = $days[$i];
}
+ $previousday = '';
+ $nextday = '';
+ if ($i !== false) {
+ if ($i >= 1) {
+ $previousday=$days[$i - 1];
+ }
+ if ($i < count($days) - 1) {
+ $nextday = $days[$i + 1];
+ }
+ }
try {
$linksToDisplay = $LINKSDB->filterDay($day);
} catch (Exception $exc) {
}
// We pre-format some fields for proper output.
- foreach($linksToDisplay as $key=>$link)
- {
-
+ foreach($linksToDisplay as $key => $link) {
$taglist = explode(' ',$link['tags']);
uasort($taglist, 'strcasecmp');
$linksToDisplay[$key]['taglist']=$taglist;
so I manually spread entries with a simple method: I roughly evaluate the
height of a div according to title and description length.
*/
- $columns=array(array(),array(),array()); // Entries to display, for each column.
- $fill=array(0,0,0); // Rough estimate of columns fill.
- foreach($linksToDisplay as $key=>$link)
- {
+ $columns = array(array(), array(), array()); // Entries to display, for each column.
+ $fill = array(0, 0, 0); // Rough estimate of columns fill.
+ foreach($linksToDisplay as $key => $link) {
// Roughly estimate length of entry (by counting characters)
// Title: 30 chars = 1 line. 1 line is 30 pixels height.
// Description: 836 characters gives roughly 342 pixel height.
// This is not perfect, but it's usually OK.
- $length=strlen($link['title'])+(342*strlen($link['description']))/836;
- if ($link['thumbnail']) $length +=100; // 1 thumbnails roughly takes 100 pixels height.
+ $length = strlen($link['title']) + (342 * strlen($link['description'])) / 836;
+ if ($link['thumbnail']) {
+ $length += 100; // 1 thumbnails roughly takes 100 pixels height.
+ }
// Then put in column which is the less filled:
- $smallest=min($fill); // find smallest value in array.
- $index=array_search($smallest,$fill); // find index of this smallest value.
- array_push($columns[$index],$link); // Put entry in this column.
- $fill[$index]+=$length;
+ $smallest = min($fill); // find smallest value in array.
+ $index = array_search($smallest, $fill); // find index of this smallest value.
+ array_push($columns[$index], $link); // Put entry in this column.
+ $fill[$index] += $length;
}
$dayDate = DateTime::createFromFormat(LinkDB::LINK_DATE_FORMAT, $day.'_000000');
$query = (isset($_SERVER['QUERY_STRING'])) ? $_SERVER['QUERY_STRING'] : '';
$targetPage = Router::findPage($query, $_GET, isLoggedIn());
+ if (
+ // if the user isn't logged in
+ !isLoggedIn() &&
+ // and Shaarli doesn't have public content...
+ $conf->get('privacy.hide_public_links') &&
+ // and is configured to enforce the login
+ $conf->get('privacy.force_login') &&
+ // and the current page isn't already the login page
+ $targetPage !== Router::$PAGE_LOGIN &&
+ // and the user is not requesting a feed (which would lead to a different content-type as expected)
+ $targetPage !== Router::$PAGE_FEED_ATOM &&
+ $targetPage !== Router::$PAGE_FEED_RSS
+ ) {
+ // force current page to be the login page
+ $targetPage = Router::$PAGE_LOGIN;
+ }
+
// Call plugin hooks for header, footer and includes, specifying which page will be rendered.
// Then assign generated data to RainTPL.
$common_hooks = array(
$PAGE->assign('username', escape($_GET['username']));
}
$PAGE->assign('returnurl',(isset($_SERVER['HTTP_REFERER']) ? escape($_SERVER['HTTP_REFERER']):''));
+ // add default state of the 'remember me' checkbox
+ $PAGE->assign('remember_user_default', $conf->get('privacy.remember_user_default'));
$PAGE->renderPage('loginform');
exit;
}
// -------- Display the Tools menu if requested (import/export/bookmarklet...)
if ($targetPage == Router::$PAGE_TOOLS)
{
- $data = array(
+ $data = [
'pageabsaddr' => index_url($_SERVER),
- 'sslenabled' => !empty($_SERVER['HTTPS'])
- );
+ 'sslenabled' => is_https($_SERVER),
+ ];
$pluginManager->executeHooks('render_tools', $data);
foreach ($data as $key => $value) {
die('Wrong token.');
}
- if (strpos($_GET['lf_linkdate'], ' ') !== false) {
- $ids = array_values(array_filter(preg_split('/\s+/', escape($_GET['lf_linkdate']))));
+ $ids = trim($_GET['lf_linkdate']);
+ if (strpos($ids, ' ') !== false) {
+ // multiple, space-separated ids provided
+ $ids = array_values(array_filter(preg_split('/\s+/', escape($ids))));
} else {
- $ids = [$_GET['lf_linkdate']];
+ // only a single id provided
+ $ids = [$ids];
+ }
+ // assert at least one id is given
+ if(!count($ids)){
+ die('no id provided');
}
foreach ($ids as $id) {
$id = (int) escape($id);
- Static analysis: Static-analysis.md
- Theming: Theming.md
- Unit tests: Unit-tests.md
+ - Unit tests inside Docker: Unit-tests-Docker.md
- About:
- FAQ: FAQ.md
- Community & Related software: Community-&-Related-software.md
--- /dev/null
+<?php
+
+
+/**
+ * Class IsHttpsTest
+ *
+ * Test class for is_https() function.
+ */
+class IsHttpsTest extends PHPUnit_Framework_TestCase
+{
+
+ /**
+ * Test is_https with HTTPS values.
+ */
+ public function testIsHttpsTrue()
+ {
+ $this->assertTrue(is_https(['HTTPS' => true]));
+ $this->assertTrue(is_https(['HTTPS' => '1']));
+ $this->assertTrue(is_https(['HTTPS' => false, 'HTTP_X_FORWARDED_PORT' => 443]));
+ $this->assertTrue(is_https(['HTTPS' => false, 'HTTP_X_FORWARDED_PORT' => '443']));
+ $this->assertTrue(is_https(['HTTPS' => false, 'HTTP_X_FORWARDED_PORT' => '443,123,456,']));
+ }
+
+ /**
+ * Test is_https with HTTP values.
+ */
+ public function testIsHttpsFalse()
+ {
+ $this->assertFalse(is_https([]));
+ $this->assertFalse(is_https(['HTTPS' => false]));
+ $this->assertFalse(is_https(['HTTPS' => '0']));
+ $this->assertFalse(is_https(['HTTPS' => false, 'HTTP_X_FORWARDED_PORT' => 123]));
+ $this->assertFalse(is_https(['HTTPS' => false, 'HTTP_X_FORWARDED_PORT' => '123']));
+ $this->assertFalse(is_https(['HTTPS' => false, 'HTTP_X_FORWARDED_PORT' => ',123,456,']));
+ }
+}
}
/**
- * Test autoLocale with multiples value, the second one is valid
+ * Test autoLocale with multiples value, the second one is available
*/
- public function testAutoLocaleMultipleSecondValid()
+ public function testAutoLocaleMultipleSecondAvailable()
{
$current = setlocale(LC_ALL, 0);
- $header = 'pt_BR,fr-fr';
+ $header = 'mag_IN,fr-fr';
autoLocale($header);
$this->assertEquals('fr_FR.utf8', setlocale(LC_ALL, 0));
}
/**
- * Test autoLocale with an invalid value: defaults to en_US.
+ * Test autoLocale with an unavailable value: defaults to en_US.
*/
- public function testAutoLocaleInvalid()
+ public function testAutoLocaleUnavailable()
{
$current = setlocale(LC_ALL, 0);
- autoLocale('pt_BR');
+ autoLocale('mag_IN');
$this->assertEquals('en_US.utf8', setlocale(LC_ALL, 0));
setlocale(LC_ALL, $current);
}
/**
- * Test autoLocale with multiples value, the second one is valid
+ * Test autoLocale with multiples value, the second one is available
*/
- public function testAutoLocaleMultipleSecondValid()
+ public function testAutoLocaleMultipleSecondAvailable()
{
$current = setlocale(LC_ALL, 0);
- $header = 'pt_BR,fr-fr';
+ $header = 'mag_IN,fr-fr';
autoLocale($header);
$this->assertEquals('fr_FR.utf8', setlocale(LC_ALL, 0));
}
/**
- * Test autoLocale with an invalid value: defaults to en_US.
+ * Test autoLocale with an unavailable value: defaults to en_US.
*/
- public function testAutoLocaleInvalid()
+ public function testAutoLocaleUnavailable()
{
$current = setlocale(LC_ALL, 0);
- autoLocale('pt_BR');
+ autoLocale('mag_IN');
$this->assertEquals('en_US.utf8', setlocale(LC_ALL, 0));
setlocale(LC_ALL, $current);
}
/**
- * Test autoLocale with multiples value, the second one is valid
+ * Test autoLocale with multiples value, the second one is available
*/
- public function testAutoLocaleMultipleSecondValid()
+ public function testAutoLocaleMultipleSecondAvailable()
{
$current = setlocale(LC_ALL, 0);
- $header = 'pt_BR,de-de';
+ $header = 'mag_IN,de-de';
autoLocale($header);
$this->assertEquals('de_DE.utf8', setlocale(LC_ALL, 0));
}
/**
- * Test autoLocale with an invalid value: defaults to en_US.
+ * Test autoLocale with an unavailable value: defaults to en_US.
*/
- public function testAutoLocaleInvalid()
+ public function testAutoLocaleUnavailable()
{
$current = setlocale(LC_ALL, 0);
- autoLocale('pt_BR');
+ autoLocale('mag_IN');
$this->assertEquals('en_US.utf8', setlocale(LC_ALL, 0));
setlocale(LC_ALL, $current);
}
.linklist-item-title a:visited .linklist-link {
- color: #555555;
+ color: #2a4c41;
}
.linklist-item-title a:hover, .linklist-item-title .linklist-link:hover{
var message = 'Are you sure you want to delete '+ links.length +' links?\n';
message += 'This action is IRREVERSIBLE!\n\nTitles:\n';
- var ids = '';
+ var ids = [];
links.forEach(function(item) {
message += ' - '+ item['title'] +'\n';
- ids += item['id'] +'+';
+ ids.push(item['id']);
});
if (window.confirm(message)) {
- window.location = '?delete_link&lf_linkdate='+ ids +'&token='+ token.value;
+ window.location = '?delete_link&lf_linkdate='+ ids.join('+') +'&token='+ token.value;
}
});
}
function activateFirefoxSocial(node) {
var loc = location.href;
var baseURL = loc.substring(0, loc.lastIndexOf("/") + 1);
+ var title = document.title;
// Keeping the data separated (ie. not in the DOM) so that it's maintainable and diffable.
var data = {
- name: "{$shaarlititle}",
+ name: title,
description: "The personal, minimalist, super-fast, database free, bookmarking service by the Shaarli community.",
author: "Shaarli",
version: "1.0.0",
</div>
<div class="remember-me">
<input type="checkbox" name="longlastingsession" id="longlastingsessionform"
- checked="checked" tabindex="22">
+ {if="$remember_user_default"}checked="checked"{/if}
+ tabindex="22">
<label for="longlastingsessionform">{'Remember me'|t}</label>
</div>
<div>
</label>
<input type="submit" value="Login" class="bigbutton" tabindex="4">
<label for="longlastingsession">
- <input type="checkbox" name="longlastingsession" id="longlastingsession" tabindex="3">
+ <input type="checkbox" name="longlastingsession"
+ id="longlastingsession" tabindex="3"
+ {if="$remember_user_default"}checked="checked"{/if}>
Stay signed in (Do not check on public computers)</label>
<input type="hidden" name="token" value="{$token}">
{if="$returnurl"}<input type="hidden" name="returnurl" value="{$returnurl}">{/if}