Browse Source

[enh] Bootstrap PHP interface

kload 10 years ago
parent
commit
0cc34124bd
33 changed files with 13802 additions and 0 deletions
  1. 56 0
      sources/config.php
  2. 26 0
      sources/controller.php
  3. 3 0
      sources/i18n/README
  4. 11 0
      sources/index.php
  5. 536 0
      sources/lib/gettext.inc
  6. 2707 0
      sources/lib/limonade.php
  7. 193 0
      sources/lib/limonade/abstract.php
  8. 173 0
      sources/lib/limonade/assertions.php
  9. 220 0
      sources/lib/limonade/public/css/screen.css
  10. BIN
      sources/lib/limonade/public/img/bg_header.png
  11. 350 0
      sources/lib/limonade/tests.php
  12. 37 0
      sources/lib/limonade/views/_debug.html.php
  13. 15 0
      sources/lib/limonade/views/_notices.html.php
  14. 22 0
      sources/lib/limonade/views/default_layout.php
  15. 6 0
      sources/lib/limonade/views/error.html.php
  16. 49 0
      sources/lib/unix_func.php
  17. 457 0
      sources/public/bootstrap/css/bootstrap-theme.css
  18. 1 0
      sources/public/bootstrap/css/bootstrap-theme.css.map
  19. 5 0
      sources/public/bootstrap/css/bootstrap-theme.min.css
  20. 6358 0
      sources/public/bootstrap/css/bootstrap.css
  21. 1 0
      sources/public/bootstrap/css/bootstrap.css.map
  22. 5 0
      sources/public/bootstrap/css/bootstrap.min.css
  23. BIN
      sources/public/bootstrap/fonts/glyphicons-halflings-regular.eot
  24. 229 0
      sources/public/bootstrap/fonts/glyphicons-halflings-regular.svg
  25. BIN
      sources/public/bootstrap/fonts/glyphicons-halflings-regular.ttf
  26. BIN
      sources/public/bootstrap/fonts/glyphicons-halflings-regular.woff
  27. 2276 0
      sources/public/bootstrap/js/bootstrap.js
  28. 7 0
      sources/public/bootstrap/js/bootstrap.min.js
  29. 13 0
      sources/public/bootstrap/js/npm.js
  30. 0 0
      sources/public/css/style.css
  31. 4 0
      sources/public/jquery/jquery-2.1.1.min.js
  32. 3 0
      sources/views/homepage.html.php
  33. 39 0
      sources/views/layout.html.php

+ 56 - 0
sources/config.php

@@ -0,0 +1,56 @@
+<?php
+
+// Limonade configuration
+function configure() {
+    option('env', ENV_PRODUCTION);
+    option('debug', false);
+    option('base_uri', '/');
+    layout("layout.html.php");
+    define('PUBLIC_DIR', '/public');
+}
+
+// Not found page
+function not_found($errno, $errstr, $errfile=null, $errline=null) {
+    $msg = h(rawurldecode($errstr));
+    return render($msg, 'error_layout.html.php');
+}
+
+function T_($string) {
+    return gettext($string);
+}
+
+// Before routing
+function before($route) {
+     /**
+     * * Locale
+     * */
+    if (!isset($_SESSION['locale'])) {
+        $locale = explode(',',$_SERVER['HTTP_ACCEPT_LANGUAGE']);
+        $_SESSION['locale'] = strtolower(substr(chop($locale[0]),0,2));
+    }
+    $textdomain="localization";
+    putenv('LANGUAGE='.$_SESSION['locale']);
+    putenv('LANG='.$_SESSION['locale']);
+    putenv('LC_ALL='.$_SESSION['locale']);
+    putenv('LC_MESSAGES='.$_SESSION['locale']);
+    setlocale(LC_ALL,$_SESSION['locale']);
+    setlocale(LC_CTYPE,$_SESSION['locale']);
+    $locales_dir = dirname(__FILE__).'/../i18n';
+    bindtextdomain($textdomain,$locales_dir);
+    bind_textdomain_codeset($textdomain, 'UTF-8');
+    textdomain($textdomain);
+    // Set the $locale variable in template
+    set('locale', $_SESSION['locale']);
+}
+
+// After routing
+function after($output, $route) {
+    /*
+    $time = number_format( (float)substr(microtime(), 0, 10) - LIM_START_MICROTIME, 6);
+    $output .= "\n<!-- page rendered in $time sec., on ".date(DATE_RFC822)." -->\n";
+    $output .= "<!-- for route\n";
+    $output .= print_r($route, true);
+    $output .= "-->";
+    */
+    return $output;
+}

+ 26 - 0
sources/controller.php

@@ -0,0 +1,26 @@
+<?php
+
+
+dispatch('/', 'hello_world');
+function hello_world() {
+    flash('success', T_('This is a notification'));
+    set('title', T_('Hello World !'));
+    return render('homepage.html.php');
+}
+
+dispatch('/lang/:locale', 'changeLocale');
+function changeLocale ($locale = 'en') {
+    switch ($locale) {
+        case 'fr':
+            $_SESSION['locale'] = 'fr';
+            break;
+        default:
+            $_SESSION['locale'] = 'en';
+            break;
+    }
+    if(!empty($_GET['redirect_to']))
+        redirect_to($_GET['redirect_to']);
+    else
+        redirect_to('/');
+}
+

+ 3 - 0
sources/i18n/README

@@ -0,0 +1,3 @@
+Add a "localization.pot" to this directory, and one folder per translated language, containing the po a LC_MESSAGE directory (with the .mo)
+
+See: https://github.com/YunoHost/admin_v1/tree/master/i18n

+ 11 - 0
sources/index.php

@@ -0,0 +1,11 @@
+<?php
+
+require dirname(__FILE__).'/lib/limonade.php';
+require dirname(__FILE__).'/controller.php';
+
+require dirname(__FILE__).'/lib/unix_func.php';
+//require '/usr/share/php/php-gettext/gettext.inc';
+
+require dirname(__FILE__).'/config.php';
+
+run();

+ 536 - 0
sources/lib/gettext.inc

@@ -0,0 +1,536 @@
+<?php
+/*
+   Copyright (c) 2005 Steven Armstrong <sa at c-area dot ch>
+   Copyright (c) 2009 Danilo Segan <danilo@kvota.net>
+
+   Drop in replacement for native gettext.
+
+   This file is part of PHP-gettext.
+
+   PHP-gettext is free software; you can redistribute it and/or modify
+   it under the terms of the GNU General Public License as published by
+   the Free Software Foundation; either version 2 of the License, or
+   (at your option) any later version.
+
+   PHP-gettext is distributed in the hope that it will be useful,
+   but WITHOUT ANY WARRANTY; without even the implied warranty of
+   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+   GNU General Public License for more details.
+
+   You should have received a copy of the GNU General Public License
+   along with PHP-gettext; if not, write to the Free Software
+   Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+
+*/
+/*
+LC_CTYPE        0
+LC_NUMERIC      1
+LC_TIME         2
+LC_COLLATE      3
+LC_MONETARY     4
+LC_MESSAGES     5
+LC_ALL          6
+*/
+
+// LC_MESSAGES is not available if php-gettext is not loaded
+// while the other constants are already available from session extension.
+if (!defined('LC_MESSAGES')) {
+  define('LC_MESSAGES', 5);
+}
+
+require('streams.php');
+require('gettext.php');
+
+
+// Variables
+
+global $text_domains, $default_domain, $LC_CATEGORIES, $EMULATEGETTEXT, $CURRENTLOCALE;
+$text_domains = array();
+$default_domain = 'messages';
+$LC_CATEGORIES = array('LC_CTYPE', 'LC_NUMERIC', 'LC_TIME', 'LC_COLLATE', 'LC_MONETARY', 'LC_MESSAGES', 'LC_ALL');
+$EMULATEGETTEXT = 0;
+$CURRENTLOCALE = '';
+
+/* Class to hold a single domain included in $text_domains. */
+class domain {
+  var $l10n;
+  var $path;
+  var $codeset;
+}
+
+// Utility functions
+
+/**
+ * Return a list of locales to try for any POSIX-style locale specification.
+ */
+function get_list_of_locales($locale) {
+  /* Figure out all possible locale names and start with the most
+   * specific ones.  I.e. for sr_CS.UTF-8@latin, look through all of
+   * sr_CS.UTF-8@latin, sr_CS@latin, sr@latin, sr_CS.UTF-8, sr_CS, sr.
+   */
+  $locale_names = array();
+  $lang = NULL;
+  $country = NULL;
+  $charset = NULL;
+  $modifier = NULL;
+  if ($locale) {
+    if (preg_match("/^(?P<lang>[a-z]{2,3})"              // language code
+                   ."(?:_(?P<country>[A-Z]{2}))?"           // country code
+                   ."(?:\.(?P<charset>[-A-Za-z0-9_]+))?"    // charset
+                   ."(?:@(?P<modifier>[-A-Za-z0-9_]+))?$/",  // @ modifier
+                   $locale, $matches)) {
+
+      if (isset($matches["lang"])) $lang = $matches["lang"];
+      if (isset($matches["country"])) $country = $matches["country"];
+      if (isset($matches["charset"])) $charset = $matches["charset"];
+      if (isset($matches["modifier"])) $modifier = $matches["modifier"];
+
+      if ($modifier) {
+        if ($country) {
+          if ($charset)
+            array_push($locale_names, "${lang}_$country.$charset@$modifier");
+          array_push($locale_names, "${lang}_$country@$modifier");
+        } elseif ($charset)
+            array_push($locale_names, "${lang}.$charset@$modifier");
+        array_push($locale_names, "$lang@$modifier");
+      }
+      if ($country) {
+        if ($charset)
+          array_push($locale_names, "${lang}_$country.$charset");
+        array_push($locale_names, "${lang}_$country");
+      } elseif ($charset)
+          array_push($locale_names, "${lang}.$charset");
+      array_push($locale_names, $lang);
+    }
+
+    // If the locale name doesn't match POSIX style, just include it as-is.
+    if (!in_array($locale, $locale_names))
+      array_push($locale_names, $locale);
+  }
+  return $locale_names;
+}
+
+/**
+ * Utility function to get a StreamReader for the given text domain.
+ */
+function _get_reader($domain=null, $category=5, $enable_cache=true) {
+    global $text_domains, $default_domain, $LC_CATEGORIES;
+    if (!isset($domain)) $domain = $default_domain;
+    if (!isset($text_domains[$domain]->l10n)) {
+        // get the current locale
+        $locale = _setlocale(LC_MESSAGES, 0);
+        $bound_path = isset($text_domains[$domain]->path) ?
+          $text_domains[$domain]->path : './';
+        $subpath = $LC_CATEGORIES[$category] ."/$domain.mo";
+
+        $locale_names = get_list_of_locales($locale);
+        $input = null;
+        foreach ($locale_names as $locale) {
+          $full_path = $bound_path . $locale . "/" . $subpath;
+          if (file_exists($full_path)) {
+            $input = new FileReader($full_path);
+            break;
+          }
+        }
+
+        if (!array_key_exists($domain, $text_domains)) {
+          // Initialize an empty domain object.
+          $text_domains[$domain] = new domain();
+        }
+        $text_domains[$domain]->l10n = new gettext_reader($input,
+                                                          $enable_cache);
+    }
+    return $text_domains[$domain]->l10n;
+}
+
+/**
+ * Returns whether we are using our emulated gettext API or PHP built-in one.
+ */
+function locale_emulation() {
+    global $EMULATEGETTEXT;
+    return $EMULATEGETTEXT;
+}
+
+/**
+ * Checks if the current locale is supported on this system.
+ */
+function _check_locale_and_function($function=false) {
+    global $EMULATEGETTEXT;
+    if ($function and !function_exists($function))
+        return false;
+    return !$EMULATEGETTEXT;
+}
+
+/**
+ * Get the codeset for the given domain.
+ */
+function _get_codeset($domain=null) {
+    global $text_domains, $default_domain, $LC_CATEGORIES;
+    if (!isset($domain)) $domain = $default_domain;
+    return (isset($text_domains[$domain]->codeset))? $text_domains[$domain]->codeset : ini_get('mbstring.internal_encoding');
+}
+
+/**
+ * Convert the given string to the encoding set by bind_textdomain_codeset.
+ */
+function _encode($text) {
+    $source_encoding = mb_detect_encoding($text);
+    $target_encoding = _get_codeset();
+    if ($source_encoding != $target_encoding) {
+        return mb_convert_encoding($text, $target_encoding, $source_encoding);
+    }
+    else {
+        return $text;
+    }
+}
+
+
+// Custom implementation of the standard gettext related functions
+
+/**
+ * Returns passed in $locale, or environment variable $LANG if $locale == ''.
+ */
+function _get_default_locale($locale) {
+  if ($locale == '') // emulate variable support
+    return getenv('LANG');
+  else
+    return $locale;
+}
+
+/**
+ * Sets a requested locale, if needed emulates it.
+ */
+function _setlocale($category, $locale) {
+    global $CURRENTLOCALE, $EMULATEGETTEXT;
+    if ($locale === 0) { // use === to differentiate between string "0"
+        if ($CURRENTLOCALE != '')
+            return $CURRENTLOCALE;
+        else
+            // obey LANG variable, maybe extend to support all of LC_* vars
+            // even if we tried to read locale without setting it first
+            return _setlocale($category, $CURRENTLOCALE);
+    } else {
+        if (function_exists('setlocale')) {
+          $ret = setlocale($category, $locale);
+          if (($locale == '' and !$ret) or // failed setting it by env
+              ($locale != '' and $ret != $locale)) { // failed setting it
+            // Failed setting it according to environment.
+            $CURRENTLOCALE = _get_default_locale($locale);
+            $EMULATEGETTEXT = 1;
+          } else {
+            $CURRENTLOCALE = $ret;
+            $EMULATEGETTEXT = 0;
+          }
+        } else {
+          // No function setlocale(), emulate it all.
+          $CURRENTLOCALE = _get_default_locale($locale);
+          $EMULATEGETTEXT = 1;
+        }
+        // Allow locale to be changed on the go for one translation domain.
+        global $text_domains, $default_domain;
+        if (array_key_exists($default_domain, $text_domains)) {
+            unset($text_domains[$default_domain]->l10n);
+        }
+        return $CURRENTLOCALE;
+    }
+}
+
+/**
+ * Sets the path for a domain.
+ */
+function _bindtextdomain($domain, $path) {
+    global $text_domains;
+    // ensure $path ends with a slash ('/' should work for both, but lets still play nice)
+    if (substr(php_uname(), 0, 7) == "Windows") {
+      if ($path[strlen($path)-1] != '\\' and $path[strlen($path)-1] != '/')
+        $path .= '\\';
+    } else {
+      if ($path[strlen($path)-1] != '/')
+        $path .= '/';
+    }
+    if (!array_key_exists($domain, $text_domains)) {
+      // Initialize an empty domain object.
+      $text_domains[$domain] = new domain();
+    }
+    $text_domains[$domain]->path = $path;
+}
+
+/**
+ * Specify the character encoding in which the messages from the DOMAIN message catalog will be returned.
+ */
+function _bind_textdomain_codeset($domain, $codeset) {
+    global $text_domains;
+    $text_domains[$domain]->codeset = $codeset;
+}
+
+/**
+ * Sets the default domain.
+ */
+function _textdomain($domain) {
+    global $default_domain;
+    $default_domain = $domain;
+}
+
+/**
+ * Lookup a message in the current domain.
+ */
+function _gettext($msgid) {
+    $l10n = _get_reader();
+    return _encode($l10n->translate($msgid));
+}
+
+/**
+ * Alias for gettext.
+ */
+function __($msgid) {
+    return _gettext($msgid);
+}
+
+/**
+ * Plural version of gettext.
+ */
+function _ngettext($singular, $plural, $number) {
+    $l10n = _get_reader();
+    return _encode($l10n->ngettext($singular, $plural, $number));
+}
+
+/**
+ * Override the current domain.
+ */
+function _dgettext($domain, $msgid) {
+    $l10n = _get_reader($domain);
+    return _encode($l10n->translate($msgid));
+}
+
+/**
+ * Plural version of dgettext.
+ */
+function _dngettext($domain, $singular, $plural, $number) {
+    $l10n = _get_reader($domain);
+    return _encode($l10n->ngettext($singular, $plural, $number));
+}
+
+/**
+ * Overrides the domain and category for a single lookup.
+ */
+function _dcgettext($domain, $msgid, $category) {
+    $l10n = _get_reader($domain, $category);
+    return _encode($l10n->translate($msgid));
+}
+/**
+ * Plural version of dcgettext.
+ */
+function _dcngettext($domain, $singular, $plural, $number, $category) {
+    $l10n = _get_reader($domain, $category);
+    return _encode($l10n->ngettext($singular, $plural, $number));
+}
+
+/**
+ * Context version of gettext.
+ */
+function _pgettext($context, $msgid) {
+    $l10n = _get_reader();
+    return _encode($l10n->pgettext($context, $msgid));
+}
+
+/**
+ * Override the current domain in a context gettext call.
+ */
+function _dpgettext($domain, $context, $msgid) {
+    $l10n = _get_reader($domain);
+    return _encode($l10n->pgettext($context, $msgid));
+}
+
+/**
+ * Overrides the domain and category for a single context-based lookup.
+ */
+function _dcpgettext($domain, $context, $msgid, $category) {
+    $l10n = _get_reader($domain, $category);
+    return _encode($l10n->pgettext($context, $msgid));
+}
+
+/**
+ * Context version of ngettext.
+ */
+function _npgettext($context, $singular, $plural) {
+    $l10n = _get_reader();
+    return _encode($l10n->npgettext($context, $singular, $plural));
+}
+
+/**
+ * Override the current domain in a context ngettext call.
+ */
+function _dnpgettext($domain, $context, $singular, $plural) {
+    $l10n = _get_reader($domain);
+    return _encode($l10n->npgettext($context, $singular, $plural));
+}
+
+/**
+ * Overrides the domain and category for a plural context-based lookup.
+ */
+function _dcnpgettext($domain, $context, $singular, $plural, $category) {
+    $l10n = _get_reader($domain, $category);
+    return _encode($l10n->npgettext($context, $singular, $plural));
+}
+
+
+
+// Wrappers to use if the standard gettext functions are available,
+// but the current locale is not supported by the system.
+// Use the standard impl if the current locale is supported, use the
+// custom impl otherwise.
+
+function T_setlocale($category, $locale) {
+    return _setlocale($category, $locale);
+}
+
+function T_bindtextdomain($domain, $path) {
+    if (_check_locale_and_function()) return bindtextdomain($domain, $path);
+    else return _bindtextdomain($domain, $path);
+}
+function T_bind_textdomain_codeset($domain, $codeset) {
+    // bind_textdomain_codeset is available only in PHP 4.2.0+
+    if (_check_locale_and_function('bind_textdomain_codeset'))
+        return bind_textdomain_codeset($domain, $codeset);
+    else return _bind_textdomain_codeset($domain, $codeset);
+}
+function T_textdomain($domain) {
+    if (_check_locale_and_function()) return textdomain($domain);
+    else return _textdomain($domain);
+}
+function T_gettext($msgid) {
+    if (_check_locale_and_function()) return gettext($msgid);
+    else return _gettext($msgid);
+}
+function T_($msgid) {
+    if (_check_locale_and_function()) return _($msgid);
+    return __($msgid);
+}
+function T_ngettext($singular, $plural, $number) {
+    if (_check_locale_and_function())
+        return ngettext($singular, $plural, $number);
+    else return _ngettext($singular, $plural, $number);
+}
+function T_dgettext($domain, $msgid) {
+    if (_check_locale_and_function()) return dgettext($domain, $msgid);
+    else return _dgettext($domain, $msgid);
+}
+function T_dngettext($domain, $singular, $plural, $number) {
+    if (_check_locale_and_function())
+        return dngettext($domain, $singular, $plural, $number);
+    else return _dngettext($domain, $singular, $plural, $number);
+}
+function T_dcgettext($domain, $msgid, $category) {
+    if (_check_locale_and_function())
+        return dcgettext($domain, $msgid, $category);
+    else return _dcgettext($domain, $msgid, $category);
+}
+function T_dcngettext($domain, $singular, $plural, $number, $category) {
+    if (_check_locale_and_function())
+      return dcngettext($domain, $singular, $plural, $number, $category);
+    else return _dcngettext($domain, $singular, $plural, $number, $category);
+}
+
+function T_pgettext($context, $msgid) {
+  if (_check_locale_and_function('pgettext'))
+      return pgettext($context, $msgid);
+  else
+      return _pgettext($context, $msgid);
+}
+
+function T_dpgettext($domain, $context, $msgid) {
+  if (_check_locale_and_function('dpgettext'))
+      return dpgettext($domain, $context, $msgid);
+  else
+      return _dpgettext($domain, $context, $msgid);
+}
+
+function T_dcpgettext($domain, $context, $msgid, $category) {
+  if (_check_locale_and_function('dcpgettext'))
+      return dcpgettext($domain, $context, $msgid, $category);
+  else
+      return _dcpgettext($domain, $context, $msgid, $category);
+}
+
+function T_npgettext($context, $singular, $plural, $number) {
+    if (_check_locale_and_function('npgettext'))
+        return npgettext($context, $singular, $plural, $number);
+    else
+        return _npgettext($context, $singular, $plural, $number);
+}
+
+function T_dnpgettext($domain, $context, $singular, $plural, $number) {
+  if (_check_locale_and_function('dnpgettext'))
+      return dnpgettext($domain, $context, $singular, $plural, $number);
+  else
+      return _dnpgettext($domain, $context, $singular, $plural, $number);
+}
+
+function T_dcnpgettext($domain, $context, $singular, $plural,
+                       $number, $category) {
+    if (_check_locale_and_function('dcnpgettext'))
+        return dcnpgettext($domain, $context, $singular,
+                           $plural, $number, $category);
+    else
+        return _dcnpgettext($domain, $context, $singular,
+                            $plural, $number, $category);
+}
+
+
+
+// Wrappers used as a drop in replacement for the standard gettext functions
+
+if (!function_exists('gettext')) {
+    function bindtextdomain($domain, $path) {
+        return _bindtextdomain($domain, $path);
+    }
+    function bind_textdomain_codeset($domain, $codeset) {
+        return _bind_textdomain_codeset($domain, $codeset);
+    }
+    function textdomain($domain) {
+        return _textdomain($domain);
+    }
+    function gettext($msgid) {
+        return _gettext($msgid);
+    }
+    function _($msgid) {
+        return __($msgid);
+    }
+    function ngettext($singular, $plural, $number) {
+        return _ngettext($singular, $plural, $number);
+    }
+    function dgettext($domain, $msgid) {
+        return _dgettext($domain, $msgid);
+    }
+    function dngettext($domain, $singular, $plural, $number) {
+        return _dngettext($domain, $singular, $plural, $number);
+    }
+    function dcgettext($domain, $msgid, $category) {
+        return _dcgettext($domain, $msgid, $category);
+    }
+    function dcngettext($domain, $singular, $plural, $number, $category) {
+        return _dcngettext($domain, $singular, $plural, $number, $category);
+    }
+    function pgettext($context, $msgid) {
+        return _pgettext($context, $msgid);
+    }
+    function npgettext($context, $singular, $plural, $number) {
+        return _npgettext($context, $singular, $plural, $number);
+    }
+    function dpgettext($domain, $context, $msgid) {
+        return _dpgettext($domain, $context, $msgid);
+    }
+    function dnpgettext($domain, $context, $singular, $plural, $number) {
+        return _dnpgettext($domain, $context, $singular, $plural, $number);
+    }
+    function dcpgettext($domain, $context, $msgid, $category) {
+        return _dcpgettext($domain, $context, $msgid, $category);
+    }
+    function dcnpgettext($domain, $context, $singular, $plural,
+                         $number, $category) {
+      return _dcnpgettext($domain, $context, $singular, $plural,
+                          $number, $category);
+    }
+}
+
+?>

File diff suppressed because it is too large
+ 2707 - 0
sources/lib/limonade.php


+ 193 - 0
sources/lib/limonade/abstract.php

@@ -0,0 +1,193 @@
+<?php
+/**
+ * Abstract methods that might be redefined by user
+ * Do not include this file in your app: it only aims to provide documentation
+ * about those functions.
+ * 
+ * @package limonade
+ * @subpackage abstract
+ */
+ 
+/**
+ * It will be called when app is launched (at the begining of the run function).
+ * You can define options inside it, a connection to a database ...
+ *
+ * @abstract this function might be redefined by user
+ * @return void 
+ */
+function configure()
+{
+  return;
+}
+
+/**
+ * Called in run() just after session start, and before checking request method
+ * and output buffer start.  
+ *
+ * @abstract this function might be redefined by user
+ * @return void 
+ */
+function initialize()
+{
+  return;
+}
+
+/**
+ * Called in run() just after the route matching, in order to load controllers. 
+ * If not specfied, the default function is called:
+ * 
+ * <code>
+ * function autoload_controller($callback)
+ * {
+ *   require_once_dir(option('controllers_dir'));
+ * }
+ * </code>
+ * 
+ *
+ * @param string $callback the callback defined in matching route
+ * @return void
+ */
+function autoload_controller($callback)
+{
+  return;
+}
+ 
+/**
+ * Called before each request.
+ * This is very useful to define a default layout or passing common variables
+ * to the templates.
+ *
+ * @abstract this function might be redefined by user
+ * @param array() $route array (like returned by {@link route_build()},
+ *   with keys "method", "pattern", "names", "callback", "options")
+ * @return void 
+ */
+function before($route)
+{
+  
+}
+ 
+/**
+ * An `after` output filter
+ * 
+ * Called after each request and can apply a transformation to the output
+ * (except for `render_file` outputs  which are sent directly to the output buffer).
+ *
+ * @abstract this function might be redefined by user
+ * @param string $output 
+ * @param array() $route array (like returned by {@link route_find()},
+ *   with keys "method", "pattern", "names", "callback", "params", "options")
+ * @return string 
+ */
+function after($output, $route)
+{
+  # Call functions...
+  # .. modifies $output...
+  return $output;
+}
+ 
+/**
+ * Not found error output
+ *
+ * @abstract this function might be redefined by user
+ * @param string $errno 
+ * @param string $errstr 
+ * @param string $errfile 
+ * @param string $errline 
+ * @return string "not found" output string
+ */
+function not_found($errno, $errstr, $errfile=null, $errline=null)
+{
+ 
+}
+ 
+/**
+ * Server error output
+ *
+ * @abstract this function might be redefined by user
+ * @param string $errno 
+ * @param string $errstr 
+ * @param string $errfile 
+ * @param string $errline 
+ * @return string "server error" output string
+ */
+function server_error($errno, $errstr, $errfile=null, $errline=null)
+{
+  
+}
+ 
+/**
+ * Called when a route is not found.
+ * 
+ * 
+ * @abstract this function might be redefined by user
+ * @param string $request_method 
+ * @param string $request_uri 
+ * @return void 
+ */
+function route_missing($request_method, $request_uri)
+{
+  halt(NOT_FOUND, "($request_method) $request_uri"); # by default
+}
+
+/**
+ * Called before stoppping and exiting application.
+ *
+ * @abstract this function might be redefined by user
+ * @param boolean exit or not
+ * @return void 
+ */
+function before_exit($exit)
+{
+  
+}
+
+/**
+ * Rendering prefilter.
+ * Useful if you want to transform your views before rendering.
+ * The first three parameters are the same as those provided 
+ * to the `render` function.
+ *
+ * @abstract this function might be redefined by user
+ * @param string $content_or_func a function, a file in current views dir or a string
+ * @param string $layout 
+ * @param array $locals 
+ * @param array $view_path (by default <code>file_path(option('views_dir'),$content_or_func);</code>)
+ * @return array with, in order, $content_or_func, $layout, $locals vars
+ *  and the calculated $view_path
+ */
+function before_render($content_or_func, $layout, $locals, $view_path)
+{
+  # transform $content_or_func, $layout, $locals or $view_path…
+  return array($content_or_func, $layout, $locals, $view_path);
+}
+
+
+/**
+ * Called only if rendering $output is_null,
+ * like in a controller with no return statement.
+ *
+ * @abstract this function might be defined by user
+ * @param array() $route array (like returned by {@link route_build()},
+ *   with keys "method", "pattern", "names", "callback", "options")
+ * @return string
+ */
+function autorender($route)
+{
+  # process output depending on $route
+  return $output;
+}
+
+/**
+ * Called if a header is about to be sent
+ *
+ * @abstract this function might be defined by user
+ * @param string the headers that limonade will send
+ * @return void
+ */
+function before_sending_header($header)
+{
+
+}
+
+

+ 173 - 0
sources/lib/limonade/assertions.php

@@ -0,0 +1,173 @@
+<?php
+/**
+ * @package tests
+ * @subpackage assertions
+ */
+# ============================================================================ #
+#    ASSERTIONS                                                                #
+# ============================================================================ #
+
+/**
+ * assert_true
+ *
+ * @param string $value 
+ * @param string $message 
+ * @return boolean
+ */
+function assert_true($value, $message = '<1> should be TRUE')
+{
+   test_run_assertion();
+   return assert('$value === TRUE; //'.$message);
+}
+
+function assert_false($value, $message = '<1> should be FALSE')
+{
+   test_run_assertion();
+   return assert('$value === FALSE; //'.$message);
+}
+
+function assert_null($value, $message = '<1> should be NULL')
+{
+   test_run_assertion();
+   return assert('$value === NULL; //'.$message);
+}
+
+function assert_not_null($value, $message = '<1> should not be NULL')
+{
+   test_run_assertion();
+   return assert('$value !== NULL; //'.$message);
+}
+
+function assert_empty($value, $message = '<1> should be empty')
+{
+   test_run_assertion();
+   return assert('empty($value); //'.$message);
+}
+
+function assert_not_empty($value, $message = '<1> should not be empty')
+{
+   test_run_assertion();
+   return assert('!empty($value); //'.$message);
+}
+
+function assert_equal($expected, $value, $message = '<1> should be equal to <2>')
+{
+   test_run_assertion();
+   return assert('$expected == $value; //'.$message);
+}
+
+function assert_not_equal($expected, $value, $message = '<1> should not equal to <2>')
+{
+   test_run_assertion();
+   return assert('$expected != $value; //'.$message);
+}
+
+function assert_identical($expected, $value, $message = '<1> should be identical to <2>')
+{
+   test_run_assertion();
+   return assert('$expected === $value; //'.$message);
+}
+
+function assert_not_identical($expected, $value, $message = '<1> should not be identical to <2>')
+{
+   test_run_assertion();
+   return assert('$expected !== $value; //'.$message);
+}
+
+function assert_match($pattern, $string, $message = '<2> expected to match regular expression <1>') {
+   test_run_assertion();
+   return assert('preg_match($pattern, $string); //'.$message);
+}
+ 
+function assert_no_match($pattern, $string, $message = '<2> expected to not match regular expression <1>') {
+   test_run_assertion();
+   return assert('!preg_match($pattern, $string); //'.$message);
+}
+
+function assert_type($type, $value, $message = '<1> is not of type <2>') {
+  test_run_assertion();
+  $predicate = 'is_' . strtolower(is_string($type) ? $type : gettype($type));
+  return assert('$predicate($value); //'.$message);
+}
+ 
+function assert_instance_of($class, $object, $message = '<2> is not an instance of class <1>') {
+   test_run_assertion();
+   return assert('$object instanceof $class; //'.$message);
+}
+ 
+function assert_length_of($value, $length, $message = '<1> expected to be of length <2>') {
+   test_run_assertion();
+   $count = is_string($value) ? 'strlen' : 'count';
+   return assert('$count($value) == $length; //'.$message);
+}
+
+function assert_trigger_error($callable, $args = array(), $message = '<1> should trigger an error') {
+  test_run_assertion();
+  $trigger_errors = count($GLOBALS["limonade"]["test_errors"]);
+  set_error_handler("test_error_handler");
+  $result = call_user_func_array($callable, $args);
+  restore_error_handler();
+  return assert('$trigger_errors < count($GLOBALS["limonade"]["test_errors"]); //'.$message);
+}
+
+# TODO add web browser assertions assert_http_get, assert_http_response... as in SimpleTest (http://www.simpletest.org/en/web_tester_documentation.html)
+
+function assert_header($response, $expected_name, $expected_value = null, $message = "expected header '%s' to be equal to '%s' but received '%s: %s'")
+{
+  test_run_assertion();
+  # see assert_header in http://github.com/fnando/voodoo-test/blob/f3b0994ef138a6ba94d5e7cef6c1fb1720797a86/lib/assertions.php
+  $headers = preg_split("/^\s*$/ms", $response);
+  //var_dump($headers);    
+  $headers = preg_replace("/\s*$/sm", "", $headers[0]);
+  //var_dump($headers);   
+  
+  $regex_header = str_replace("/", "\\/", $expected_name);
+  $regex_header = str_replace(".", "\\.", $regex_header);
+  
+  $header = $expected_name;
+  
+  # from http://www.faqs.org/rfcs/rfc2616
+  # Field names are case-insensitive
+  if ($expected_value) {
+      $regex = "/^{$regex_header}:(.*?)$/ism";
+      $header .= ": {$expected_value}";
+  } else {
+      $regex = "/^{$regex_header}(:.*?)?$/ism";
+  }
+  
+  $has_header = preg_match($regex, $headers, $matches);    
+  $sent_header = trim((string)$matches[1]);
+
+  
+  if(empty($sent_header))
+  {
+    if(is_null($expected_value))
+    {
+      $message = "expected header '%s' but header has not been sent";
+    }
+    else
+    {
+      $message = "expected header '%s' to be equal to '%s' but header has not been sent";
+    }
+    
+    $message = sprintf($message, $expected_name, $expected_value);
+    return assert("false; //".$message);
+  }
+  else if($expected_value)
+  {
+    $message = sprintf($message, $expected_name, $expected_value, $expected_name, $sent_header);
+    return assert('$expected_value && $sent_header == $expected_value; //'.$message);
+  }
+  return assert("true; //");
+}
+
+function assert_status($response, $expected_status, $message = "expected status code to be equal to '%s' but received '%s'")
+{
+  $lines = explode('\n', trim($response));
+  if (preg_match('/HTTP\/(\d+\.\d+)\s+(\d+)/i', $lines[0], $matches))
+  {
+      $status = $matches[2];
+      return assert('$expected_status == $status; //'.sprintf($message, $expected_status, $status));
+  }
+  return assert("false; //no status code returned in this response string");
+}

+ 220 - 0
sources/lib/limonade/public/css/screen.css

@@ -0,0 +1,220 @@
+/* -----------------------------------------------------------------------
+
+
+ Blueprint CSS Framework 0.8
+ http://blueprintcss.org
+
+   * Copyright (c) 2007-Present. See LICENSE for more info.
+   * See README for instructions on how to use Blueprint.
+   * For credits and origins, see AUTHORS.
+   * This is a compressed file. See the sources in the 'src' directory.
+
+----------------------------------------------------------------------- */
+
+/* reset.css */
+html, body, div, span, object, iframe, h1, h2, h3, h4, h5, h6, p, blockquote, pre, a, abbr, acronym, address, code, del, dfn, em, img, q, dl, dt, dd, ol, ul, li, fieldset, form, label, legend, table, caption, tbody, tfoot, thead, tr, th, td {margin:0;padding:0;border:0;font-weight:inherit;font-style:inherit;font-size:100%;font-family:inherit;vertical-align:baseline;}
+body {line-height:1.5;}
+table {border-collapse:separate;border-spacing:0;}
+caption, th, td {text-align:left;font-weight:normal;}
+table, td, th {vertical-align:middle;}
+blockquote:before, blockquote:after, q:before, q:after {content:"";}
+blockquote, q {quotes:"" "";}
+a img {border:none;}
+
+/* typography.css */
+body {font-size:75%;color:#222;background:#fff;font-family:"Helvetica Neue", Arial, Helvetica, sans-serif;}
+h1, h2, h3, h4, h5, h6 {font-weight:normal;color:#111;}
+h1 {font-size:3em;line-height:1;margin-bottom:0.5em;}
+h2 {font-size:2em;margin-bottom:0.75em;}
+h3 {font-size:1.5em;line-height:1;margin-bottom:1em;}
+h4 {font-size:1.2em;line-height:1.25;margin-bottom:1.25em;}
+h5 {font-size:1em;font-weight:bold;margin-bottom:1.5em;}
+h6 {font-size:1em;font-weight:bold;}
+h1 img, h2 img, h3 img, h4 img, h5 img, h6 img {margin:0;}
+p {margin:0 0 1.5em;}
+p img.left {float:left;margin:1.5em 1.5em 1.5em 0;padding:0;}
+p img.right {float:right;margin:1.5em 0 1.5em 1.5em;}
+a:focus, a:hover {color:#000;}
+a {color:#009;text-decoration:underline;}
+blockquote {margin:1.5em;color:#666;font-style:italic;}
+strong {font-weight:bold;}
+em, dfn {font-style:italic;}
+dfn {font-weight:bold;}
+sup, sub {line-height:0;}
+abbr, acronym {border-bottom:1px dotted #666;}
+address {margin:0 0 1.5em;font-style:italic;}
+del {color:#666;}
+pre {margin:1.5em 0;white-space:pre;}
+pre, code, tt {font:1em 'andale mono', 'lucida console', monospace;line-height:1.5;}
+li ul, li ol {margin:0 1.5em;}
+ul, ol {margin:0 1.5em 1.5em 1.5em;}
+ul {list-style-type:disc;}
+ol {list-style-type:decimal;}
+dl {margin:0 0 1.5em 0;}
+dl dt {font-weight:bold;}
+dd {margin-left:1.5em;}
+table {margin-bottom:1.4em;width:100%;}
+th {font-weight:bold;}
+thead th {background:#c3d9ff;}
+th, td, caption {padding:4px 10px 4px 5px;}
+tr.even td {background:#e5ecf9;}
+tfoot {font-style:italic;}
+caption {background:#eee;}
+.small {font-size:.8em;margin-bottom:1.875em;line-height:1.875em;}
+.large {font-size:1.2em;line-height:2.5em;margin-bottom:1.25em;}
+.hide {display:none;}
+.quiet {color:#666;}
+.loud {color:#000;}
+.highlight {background:#ff0;}
+.added {background:#060;color:#fff;}
+.removed {background:#900;color:#fff;}
+.first {margin-left:0;padding-left:0;}
+.last {margin-right:0;padding-right:0;}
+.top {margin-top:0;padding-top:0;}
+.bottom {margin-bottom:0;padding-bottom:0;}
+
+/* forms.css */
+label {font-weight:bold;}
+fieldset {padding:1.4em;margin:0 0 1.5em 0;border:1px solid #ccc;}
+legend {font-weight:bold;font-size:1.2em;}
+input.text, input.title, textarea, select {margin:0.5em 0;border:1px solid #bbb;}
+input.text:focus, input.title:focus, textarea:focus, select:focus {border:1px solid #666;}
+input.text, input.title {width:300px;padding:5px;}
+input.title {font-size:1.5em;}
+textarea {width:390px;height:250px;padding:5px;}
+.error, .notice, .success {padding:.8em;margin-bottom:1em;border:2px solid #ddd;}
+.error {background:#FBE3E4;color:#8a1f11;border-color:#FBC2C4;}
+.notice {background:#FFF6BF;color:#514721;border-color:#FFD324;}
+.success {background:#E6EFC2;color:#264409;border-color:#C6D880;}
+.error a {color:#8a1f11;}
+.notice a {color:#514721;}
+.success a {color:#264409;}
+
+.box {padding:1.5em;margin-bottom:1.5em;background:#E5ECF9;}
+hr {background:#ddd;color:#ddd;clear:both;float:none;width:100%;height:.1em;margin:0 0 1.45em;border:none;}
+hr.space {background:#fff;color:#fff;}
+.clearfix:after, .container:after {content:"\0020";display:block;height:0;clear:both;visibility:hidden;overflow:hidden;}
+.clearfix, .container {display:block;}
+.clear {clear:both;}
+
+/* -----------------------------------------------------------------------
+
+ LIMONADE
+ 
+ Design and logo by <a href="http://www.minimau.com">Minimau</a>
+
+----------------------------------------------------------------------- */
+
+html {
+ 	background-color: #b4a689;
+ 	text-align: center;
+ 	}
+ 	body {
+ 		width: auto; /* 16 col */
+ 		margin: 0 20px;
+ 		text-align: left;
+ 		/*background: #fff url(blueprint/src/grid.png);*/
+ 		font-family: Verdana, Geneva, sans-serif;
+ 		color: #303030;
+ 		}
+ 		a, a:hover, a:visited {
+ 			color: #4596cd;
+ 			}
+ 		a:hover {
+ 			text-decoration: none;
+ 			}
+ 		#header, h1, h2, h3, h4 {
+ 			font-family: "Arial Black", Gadget, sans-serif;
+ 			}
+ 		h1 {
+ 			color: #b4a689;
+ 			font-size: 2em;
+ 			line-height: 2;
+ 			border-bottom: 7px solid #000;
+ 			margin-bottom: 0.75em;
+ 			margin-top:0;
+ 			}
+ 		h2 {
+ 			font-size: 1.5em;
+ 			color: #cddb12;
+ 			margin-top: 2em;
+ 			line-height: 0.8;
+ 			margin-bottom: 1.2em;
+ 			}
+ 		h3 {
+ 			font-size: 1.33em;
+ 			line-height: 0.89;
+ 			margin-top: 2.66em;
+ 			margin-bottom: 1.33em;
+ 			color:#95b383;
+ 			}
+ 		pre {
+ 			background-color: #cddb12;
+ 			font-size: 12px;
+ 			padding: 1.5em;
+ 			margin-bottom: 1.5em;
+ 			overflow: auto;
+ 			}
+ 		code {
+ 			font: 10px Monaco, "Courier New", Courier, monospace;
+ 			color: #0d562a;
+ 			}
+ 		#header {
+ 			/*position:relative;*/
+ 			height: 187px; /* 12 lines */
+ 			background: #fff url(<?=url_for('/_lim_public/img/bg_header.png')?>) no-repeat 0 0;
+ 			margin-top: 10px;
+      padding:0;
+ 			}
+ 			#header h1 {
+ 				position: absolute;
+ 				left:-9999px;
+ 				   			margin:0;
+ 				}
+ 		#footer {
+ 			padding: 0 55px;
+ 			background-color: #1a1818;
+ 			color: #999;
+ 			height: 2em;
+ 			line-height: 2em;
+ 			text-align: right;
+ 			}
+ 			#footer a {
+ 				color: #a3d8de;
+ 				text-decoration: none;
+ 				}
+ 			#footer a:hover {
+ 				color: #fff;
+ 				}
+ 		#content {
+ 		  background-color: #fff;
+ 			padding: 0 55px;
+ 			min-height: 400px;
+ 			}
+    		p.bt {
+    		   text-align:right;
+    		   }
+       		p.bt a {
+       		   text-decoration:none;
+       		   padding: 2px 4px;
+       		   }
+       		p.bt a:hover {
+      	      background-color: #4596cd;
+      	      color: #fff;
+      	      }
+      	#debug-menu {
+      	   position: fixed;
+      	   top: 0px;
+      	   right:20px;
+      	   background-color: rgba(100,100,100, 0.7);
+      	   padding:5px;
+      	}
+      	#debug-menu a {
+      	   color: #fff;
+      	   font-size: 90%;
+      	   text-decoration:none;
+   	   }
+   	   #debug-menu a:hover {
+   	      text-decoration: underline;
+	      }
+   	

BIN
sources/lib/limonade/public/img/bg_header.png


+ 350 - 0
sources/lib/limonade/tests.php

@@ -0,0 +1,350 @@
+<?php
+/**
+ * @package tests
+ */
+ 
+# ============================================================================ #
+#    TESTS                                                                     #
+# ============================================================================ #
+
+/**
+ * load assertions
+ */
+require_once dirname(__FILE__)."/assertions.php";
+
+
+ 
+/**
+ * Constants and globals
+ */
+if(!defined('DS')) define("DS", DIRECTORY_SEPARATOR);
+
+if(!array_key_exists("limonade", $GLOBALS))
+   $GLOBALS["limonade"] = array();
+if(!array_key_exists("test_cases", $GLOBALS["limonade"]))
+   $GLOBALS["limonade"]["test_cases"] = array();
+if(!array_key_exists("test_errors", $GLOBALS["limonade"]))
+   $GLOBALS["limonade"]["test_errors"] = array();
+if(!array_key_exists("test_case_current", $GLOBALS["limonade"]))
+   $GLOBALS["limonade"]["test_case_current"] = NULL;
+if(!array_key_exists("test_suites", $GLOBALS["limonade"]))
+   $GLOBALS["limonade"]["test_suites"] = NULL;
+
+ini_set("display_errors", true);
+error_reporting(E_ALL ^ (E_USER_WARNING | E_NOTICE | E_USER_NOTICE));
+// error_reporting(E_ALL | E_STRICT);
+assert_options(ASSERT_ACTIVE, 1);
+assert_options(ASSERT_WARNING, 0);
+assert_options(ASSERT_BAIL, 0);
+assert_options(ASSERT_QUIET_EVAL, 0);
+assert_options(ASSERT_CALLBACK, 'test_assert_failure');
+
+# TODO: separate display from logic
+# TODO: clean results output
+# TODO: add all tests results
+
+/**
+ * Starts a test suite
+ *
+ * @param string $name 
+ * @return void
+ */
+function test_suite($name)
+{
+  $GLOBALS["limonade"]["test_suites"] = $name;
+  echo test_cli_format("===========================================================\n", 'white');
+  echo test_cli_format(">>>> START $name tests suites\n", 'white');
+  echo test_cli_format("-----------------------------------------------------------\n", 'white');
+}
+
+/**
+ * Ends the last group of test suites
+ *
+ * @return void
+ */
+function end_test_suite()
+{
+  $name         = $GLOBALS["limonade"]["test_suites"];
+  $failures     = 0;
+  $tests        = 0;
+  $passed_tests = 0;
+  $assertions   = 0;
+
+  foreach($GLOBALS["limonade"]["test_cases"] as $test)
+  {
+    $failures += $test['failures'];
+    $assertions += $test['assertions'];
+    if(empty($test['failures'])) $passed_tests++;
+    $tests++;
+  }
+  echo ">> ENDING $name tests suites\n  ";
+  echo $failures > 0 ? test_cli_format("|FAILED!|", "red") : test_cli_format("|PASSED|", "green");;
+  echo " Passes ".$passed_tests."/".$tests.", ";
+  echo " {$failures} failures for {$assertions} assertions.\n";
+  echo test_cli_format("===========================================================\n", 'white');
+}
+
+/**
+ * Starting a new test case
+ *
+ * @param string $name 
+ * @return void
+ */
+function test_case($name)
+{
+   $name = strtolower($name); // TODO: normalize name
+   
+   if(!array_key_exists($name, $GLOBALS["limonade"]["test_cases"]))
+   {
+      $GLOBALS["limonade"]["test_cases"][$name] = array( 
+                                                      "name" => $name,
+                                                      "assertions" => 0, 
+                                                      "failures" => 0,
+                                                      "description" => NULL
+                                                   );      
+      $GLOBALS["limonade"]["test_case_current"] = $name;
+   }
+   else
+   {
+      
+   }
+}
+
+/**
+ * Displays and ending the current tests suite
+ * 
+ * @return void
+ */
+function end_test_case()
+{
+   $name = $GLOBALS["limonade"]["test_case_current"];
+   echo "## ".strtoupper($name)."\n";
+      
+   $desc = test_case_describe();
+   if(!is_null($desc)) echo $desc."\n";
+
+   echo "- - - - - - - - - - - - - - - - - - - - - - - - - - - - - -\n";
+   
+   test_case_execute_current();
+   
+   
+   if(!is_null($name))
+   {
+      $test = $GLOBALS["limonade"]["test_cases"][$name];
+      // closing previous test
+      echo "\n- - - - - - - - - - - - - - - - - - - - - - - - - - - - - -\n";
+      echo $test['failures'] > 0 ? test_cli_format("|FAILED!|", "red") : test_cli_format("|PASSED|", "green");
+      echo " Test case '$name' finished: ";
+      echo count(test_case_all_func())." tests, ";
+      echo " {$test['failures']} failures for {$test['assertions']} assertions.\n";
+      
+      echo "-----------------------------------------------------------\n";
+   }
+   $GLOBALS["limonade"]["test_case_current"] = null;
+}
+
+/**
+ * Describes the current tests suite
+ *
+ * @param string $msg 
+ * @return string tests description
+ */
+function test_case_describe($msg = NULL)
+{
+   $test =& test_case_current();
+   if(!is_null($msg))
+   {
+      $test["description"] = $msg;
+   }
+   //var_dump($test["description"]);
+   return $test["description"];
+}
+
+/**
+ * Returns all user test case functions
+ *
+ * @access private
+ * @return void
+ */
+function test_case_all_func()
+{
+   $functions = get_defined_functions();
+   $functions = $functions['user'];
+   $tests = array();
+   $name = $GLOBALS["limonade"]["test_case_current"];
+   while ($func = array_shift($functions)) {
+      $regexp = "/^test_{$name}_(.*)$/";
+      if(!preg_match($regexp, $func)) continue;
+      if($func == test_before_func_name()) continue;
+      // TODO: adding break for all test api methods
+      
+      $tests[] = $func;
+   }
+   return $tests;
+}
+
+/**
+ * Execute current test case
+ *
+ * @access private
+ * @return void
+ */
+function test_case_execute_current()
+{
+   $tests = test_case_all_func();
+   while($func = array_shift($tests))
+   {
+      test_call_func(test_before_func_name());
+      call_user_func($func);
+   }
+}
+
+
+function &test_case_current()
+{
+   $name = $GLOBALS["limonade"]["test_case_current"];
+   return $GLOBALS["limonade"]["test_cases"][$name];
+}
+
+
+
+function test_before_func_name()
+{
+   $test = test_case_current();
+   $func = "before_each_test_in_".$test["name"];
+   return $func;
+}
+
+function test_before_assert_func_name()
+{
+   $test = test_case_current();
+   $func = "before_each_assert_in_$name".$test["name"];
+   return $func;
+}
+
+function test_run_assertion()
+{
+   $name = $GLOBALS["limonade"]["test_case_current"];
+   $GLOBALS["limonade"]["test_cases"][$name]['assertions']++;
+   test_call_func(test_before_assert_func_name());
+}
+
+/**
+ * Calls a function if exists
+ *
+ * @param string $func the function name
+ * @param mixed $arg,.. (optional)
+ * @return mixed
+ */
+function test_call_func($func)
+{
+  if(empty($func)) return;
+  $args = func_get_args();
+  $func = array_shift($args);
+  if(function_exists($func)) return call_user_func_array($func, $args);
+  return;
+}
+
+/**
+ * Error handler 
+ * 
+ * @access private
+ * @return boolean true
+ */
+function test_error_handler($errno, $errstr, $errfile, $errline)
+{
+	if($errno < E_USER_ERROR || $errno > E_USER_NOTICE) 
+	   echo test_cli_format("!!! ERROR", "red") . " [$errno], $errstr in $errfile at line $errline\n";
+	$GLOBALS["limonade"]["test_errors"][] = array($errno, $errstr, $errfile, $errline);
+   return true;
+}
+
+/**
+ * Assert callback
+ * 
+ * @access private
+ * @param string $script 
+ * @param string $line 
+ * @param string $message 
+ * @return void
+ */
+function test_assert_failure($script, $line, $message)
+{
+   // Using the stack trace, find the outermost assert*() call
+   $stacktrace = array_slice(debug_backtrace(), 1); // skip self
+   $assertion = reset($stacktrace);
+   while ($stackframe = array_shift($stacktrace)) {
+    if (!preg_match('/^assert/', $stackframe['function']))
+      break;
+    $assertion = $stackframe;
+   }
+
+   extract($assertion, EXTR_PREFIX_ALL, 'assert');
+   $code = explode("\n", file_get_contents($assert_file));
+   $code = trim($code[$assert_line - 1]);
+   
+   list($assert_code, $message) = explode("//", $message);
+   echo test_cli_format("Assertion failed", "yellow");
+   echo " in script *{$assert_file}* (line {$assert_line}):\n";
+   echo "   * assertion: $code\n";
+   echo "   * message:   $message\n";
+   $name = $GLOBALS["limonade"]["test_case_current"];
+   $GLOBALS["limonade"]["test_cases"][$name]['failures']++;
+}
+
+function test_cli_format($text, $format) {
+    $formats = array(
+        "blue"       => 34,
+        "bold"       => 1,
+        "green"      => 32,
+        "highlight"  => 7,
+        "light_blue" => 36,
+        "purple"     => 35,
+        "red"        => 31,
+        "underline"  => 4,
+        "white"      => 37,
+        "yellow"     => 33
+    );
+
+    if (array_key_exists($format, $formats)) $format = $formats[$format];
+    return chr(27) . "[01;{$format} m{$text}" . chr(27) . "[00m";
+}
+
+/**
+ * Do HTTP request and return the response content.
+ * 
+ * @param string $url
+ * @param string $method
+ * @param bool $include_header
+ * @return string
+ * @author Nando Vieira
+ */
+function test_request($url, $method="GET", $include_header=false, $post_data=array(), $http_header=array()) {
+    $method = strtoupper($method);
+    $allowed_methods = array("GET", "PUT", "POST", "DELETE", "HEAD");
+    if(!in_array($method, $allowed_methods))
+    {
+      $message = "The requested method '$method' is not allowed";
+      return assert('false; //'.$message);
+    }
+    
+    $curl = curl_init($url);
+    curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1);
+    curl_setopt($curl, CURLOPT_HEADER, $include_header);
+    curl_setopt($curl, CURLOPT_HTTPHEADER, $http_header);
+    curl_setopt($curl, CURLOPT_CUSTOMREQUEST, $method);
+    curl_setopt($curl, CURLOPT_FOLLOWLOCATION, 1); 
+    if($method == 'POST' || $method == 'PUT')
+    {
+      curl_setopt($curl, CURLOPT_POST, 1);
+      curl_setopt($curl, CURLOPT_POSTFIELDS, $post_data);
+    }
+    if($method == 'HEAD')
+    {
+      curl_setopt($curl, CURLOPT_NOBODY, true);
+    }
+    $response = curl_exec($curl);
+    curl_close($curl);
+
+    return $response;
+}

+ 37 - 0
sources/lib/limonade/views/_debug.html.php

@@ -0,0 +1,37 @@
+<?php if(option('env') > ENV_PRODUCTION && option('debug')): ?>
+  <?php if(!$is_http_error): ?>
+  <p>[<?php echo error_type($errno)?>]
+  	<?php echo $errstr?> (in <strong><?php echo $errfile?></strong> line <strong><?php echo $errline?></strong>)
+  	</p>
+  	<?php endif; ?>
+
+
+  <?php if($debug_args = set('_lim_err_debug_args')): ?>
+  <h2 id="debug-arguments">Debug arguments</h2>
+  	<pre><code><?php echo h(print_r($debug_args, true))?></code></pre>
+  <?php endif; ?>
+  
+  <h2 id="limonade-options">Options</strong></h2>
+  <pre><code><?php echo h(print_r(option(), true))?></code></pre>
+  <p class="bt top"><a href="#header">[ &#x2191; ]</a></p>
+  
+  <h2 id="environment">Environment</h2>
+  <pre><code><?php echo h(print_r(env(), true))?></code></pre>
+  <p class="bt top"><a href="#header">[ &#x2191; ]</a></p>
+  
+  <h2 id="debug-backtrace">Backtrace</h2>
+  <pre><code><?php echo h(print_r(debug_backtrace(), true))?></code></pre>
+  <p class="bt top"><a href="#header">[ &#x2191; ]</a></p>
+
+  <div id="debug-menu">
+    
+    <?php if($debug_args = set('_lim_err_debug_args')): ?>
+    <a href="#debug-arguments">Debug arguments</a> |
+    <?php endif; ?>
+    <a href="#limonade-options">Options</a> |
+    <a href="#environment">Environment</a> |
+    <a href="#debug-backtrace">Backtrace</a> |
+    <a href="#header">[ &#x2191; ]</a>
+  </div>
+  
+<?php endif; ?>

+ 15 - 0
sources/lib/limonade/views/_notices.html.php

@@ -0,0 +1,15 @@
+<?php if(!empty($notices)): ?>
+<div class="lim-debug lim-notices">
+  <h4> &#x2192; Notices and warnings</h4>
+  <dl>
+  <?php $cpt = 1; foreach($notices as $notice): ?>
+    <dt>[<?php echo $cpt.'. '.error_type($notice['errno'])?>]</dt>
+    <dd>
+    <?php echo $notice['errstr']?> in <strong><code><?php echo $notice['errfile']?></code></strong> 
+    line <strong><code><?php echo $notice['errline']?></code></strong>
+    </dd>
+  <?php $cpt++; endforeach; ?>
+  </dl>
+  <hr>
+</div>
+<?php endif; ?>

+ 22 - 0
sources/lib/limonade/views/default_layout.php

@@ -0,0 +1,22 @@
+<!DOCTYPE html>
+<html>
+<head>
+	<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
+	<title>Limonade, the fizzy PHP micro-framework</title>
+	<link rel="stylesheet" href="<?php echo url_for('/_lim_css/screen.css');?>" type="text/css" media="screen">
+</head>
+<body>
+  <div id="header">
+    <h1>Limonade</h1>
+  </div>
+  
+  <div id="content">
+    <?php echo error_notices_render(); ?>
+    <div id="main">
+      <?php echo $content;?>
+      <hr class="space">
+    </div>
+  </div>
+
+</body>
+</html>

+ 6 - 0
sources/lib/limonade/views/error.html.php

@@ -0,0 +1,6 @@
+  <h1><?php echo h(error_http_status($errno));?></h1>
+  <?php if($is_http_error): ?>
+  <p><?php echo h($errstr)?></p>
+  <?php endif; ?>
+  
+  <?php echo  render('_debug.html.php', null, $vars); ?>

+ 49 - 0
sources/lib/unix_func.php

@@ -0,0 +1,49 @@
+<?php 
+
+ /**
+  *  YunoHost - Self-hosting for all
+  *  Copyright (C) 2012  Kload <kload@kload.fr>
+  *
+  *  This program is free software: you can redistribute it and/or modify
+  *  it under the terms of the GNU Affero General Public License as
+  *  published by the Free Software Foundation, either version 3 of the
+  *  License, or (at your option) any later version.
+  *
+  *  This program is distributed in the hope that it will be useful,
+  *  but WITHOUT ANY WARRANTY; without even the implied warranty of
+  *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+  *  GNU Affero General Public License for more details.
+  *
+  *  You should have received a copy of the GNU Affero General Public License
+  *  along with this program.  If not, see <http://www.gnu.org/licenses/>.
+  */
+  
+function read_file($file, $lines = 20) {
+
+    if (!$handle = fopen($file, "r"))
+        return false;
+    $linecounter = $lines;
+    $pos = -2;
+    $beginning = false;
+    $text = array();
+    while ($linecounter > 0) {
+        $t = " ";
+        while ($t != "\n") {
+            if(fseek($handle, $pos, SEEK_END) == -1) {
+                $beginning = true;
+                break;
+            }
+            $t = fgetc($handle);
+            $pos --;
+        }
+        $linecounter --;
+        if ($beginning) {
+            rewind($handle);
+        }
+        $text[$lines-$linecounter-1] = fgets($handle);
+        if ($beginning) break;
+    }
+    fclose ($handle);
+    return array_reverse($text);
+}
+

+ 457 - 0
sources/public/bootstrap/css/bootstrap-theme.css

@@ -0,0 +1,457 @@
+/*!
+ * Bootstrap v3.3.0 (http://getbootstrap.com)
+ * Copyright 2011-2014 Twitter, Inc.
+ * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
+ */
+
+.btn-default,
+.btn-primary,
+.btn-success,
+.btn-info,
+.btn-warning,
+.btn-danger {
+  text-shadow: 0 -1px 0 rgba(0, 0, 0, .2);
+  -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, .15), 0 1px 1px rgba(0, 0, 0, .075);
+          box-shadow: inset 0 1px 0 rgba(255, 255, 255, .15), 0 1px 1px rgba(0, 0, 0, .075);
+}
+.btn-default:active,
+.btn-primary:active,
+.btn-success:active,
+.btn-info:active,
+.btn-warning:active,
+.btn-danger:active,
+.btn-default.active,
+.btn-primary.active,
+.btn-success.active,
+.btn-info.active,
+.btn-warning.active,
+.btn-danger.active {
+  -webkit-box-shadow: inset 0 3px 5px rgba(0, 0, 0, .125);
+          box-shadow: inset 0 3px 5px rgba(0, 0, 0, .125);
+}
+.btn-default .badge,
+.btn-primary .badge,
+.btn-success .badge,
+.btn-info .badge,
+.btn-warning .badge,
+.btn-danger .badge {
+  text-shadow: none;
+}
+.btn:active,
+.btn.active {
+  background-image: none;
+}
+.btn-default {
+  text-shadow: 0 1px 0 #fff;
+  background-image: -webkit-linear-gradient(top, #fff 0%, #e0e0e0 100%);
+  background-image:      -o-linear-gradient(top, #fff 0%, #e0e0e0 100%);
+  background-image: -webkit-gradient(linear, left top, left bottom, from(#fff), to(#e0e0e0));
+  background-image:         linear-gradient(to bottom, #fff 0%, #e0e0e0 100%);
+  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffffff', endColorstr='#ffe0e0e0', GradientType=0);
+  filter: progid:DXImageTransform.Microsoft.gradient(enabled = false);
+  background-repeat: repeat-x;
+  border-color: #dbdbdb;
+  border-color: #ccc;
+}
+.btn-default:hover,
+.btn-default:focus {
+  background-color: #e0e0e0;
+  background-position: 0 -15px;
+}
+.btn-default:active,
+.btn-default.active {
+  background-color: #e0e0e0;
+  border-color: #dbdbdb;
+}
+.btn-default:disabled,
+.btn-default[disabled] {
+  background-color: #e0e0e0;
+  background-image: none;
+}
+.btn-primary {
+  background-image: -webkit-linear-gradient(top, #428bca 0%, #2d6ca2 100%);
+  background-image:      -o-linear-gradient(top, #428bca 0%, #2d6ca2 100%);
+  background-image: -webkit-gradient(linear, left top, left bottom, from(#428bca), to(#2d6ca2));
+  background-image:         linear-gradient(to bottom, #428bca 0%, #2d6ca2 100%);
+  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff428bca', endColorstr='#ff2d6ca2', GradientType=0);
+  filter: progid:DXImageTransform.Microsoft.gradient(enabled = false);
+  background-repeat: repeat-x;
+  border-color: #2b669a;
+}
+.btn-primary:hover,
+.btn-primary:focus {
+  background-color: #2d6ca2;
+  background-position: 0 -15px;
+}
+.btn-primary:active,
+.btn-primary.active {
+  background-color: #2d6ca2;
+  border-color: #2b669a;
+}
+.btn-primary:disabled,
+.btn-primary[disabled] {
+  background-color: #2d6ca2;
+  background-image: none;
+}
+.btn-success {
+  background-image: -webkit-linear-gradient(top, #5cb85c 0%, #419641 100%);
+  background-image:      -o-linear-gradient(top, #5cb85c 0%, #419641 100%);
+  background-image: -webkit-gradient(linear, left top, left bottom, from(#5cb85c), to(#419641));
+  background-image:         linear-gradient(to bottom, #5cb85c 0%, #419641 100%);
+  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5cb85c', endColorstr='#ff419641', GradientType=0);
+  filter: progid:DXImageTransform.Microsoft.gradient(enabled = false);
+  background-repeat: repeat-x;
+  border-color: #3e8f3e;
+}
+.btn-success:hover,
+.btn-success:focus {
+  background-color: #419641;
+  background-position: 0 -15px;
+}
+.btn-success:active,
+.btn-success.active {
+  background-color: #419641;
+  border-color: #3e8f3e;
+}
+.btn-success:disabled,
+.btn-success[disabled] {
+  background-color: #419641;
+  background-image: none;
+}
+.btn-info {
+  background-image: -webkit-linear-gradient(top, #5bc0de 0%, #2aabd2 100%);
+  background-image:      -o-linear-gradient(top, #5bc0de 0%, #2aabd2 100%);
+  background-image: -webkit-gradient(linear, left top, left bottom, from(#5bc0de), to(#2aabd2));
+  background-image:         linear-gradient(to bottom, #5bc0de 0%, #2aabd2 100%);
+  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5bc0de', endColorstr='#ff2aabd2', GradientType=0);
+  filter: progid:DXImageTransform.Microsoft.gradient(enabled = false);
+  background-repeat: repeat-x;
+  border-color: #28a4c9;
+}
+.btn-info:hover,
+.btn-info:focus {
+  background-color: #2aabd2;
+  background-position: 0 -15px;
+}
+.btn-info:active,
+.btn-info.active {
+  background-color: #2aabd2;
+  border-color: #28a4c9;
+}
+.btn-info:disabled,
+.btn-info[disabled] {
+  background-color: #2aabd2;
+  background-image: none;
+}
+.btn-warning {
+  background-image: -webkit-linear-gradient(top, #f0ad4e 0%, #eb9316 100%);
+  background-image:      -o-linear-gradient(top, #f0ad4e 0%, #eb9316 100%);
+  background-image: -webkit-gradient(linear, left top, left bottom, from(#f0ad4e), to(#eb9316));
+  background-image:         linear-gradient(to bottom, #f0ad4e 0%, #eb9316 100%);
+  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff0ad4e', endColorstr='#ffeb9316', GradientType=0);
+  filter: progid:DXImageTransform.Microsoft.gradient(enabled = false);
+  background-repeat: repeat-x;
+  border-color: #e38d13;
+}
+.btn-warning:hover,
+.btn-warning:focus {
+  background-color: #eb9316;
+  background-position: 0 -15px;
+}
+.btn-warning:active,
+.btn-warning.active {
+  background-color: #eb9316;
+  border-color: #e38d13;
+}
+.btn-warning:disabled,
+.btn-warning[disabled] {
+  background-color: #eb9316;
+  background-image: none;
+}
+.btn-danger {
+  background-image: -webkit-linear-gradient(top, #d9534f 0%, #c12e2a 100%);
+  background-image:      -o-linear-gradient(top, #d9534f 0%, #c12e2a 100%);
+  background-image: -webkit-gradient(linear, left top, left bottom, from(#d9534f), to(#c12e2a));
+  background-image:         linear-gradient(to bottom, #d9534f 0%, #c12e2a 100%);
+  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9534f', endColorstr='#ffc12e2a', GradientType=0);
+  filter: progid:DXImageTransform.Microsoft.gradient(enabled = false);
+  background-repeat: repeat-x;
+  border-color: #b92c28;
+}
+.btn-danger:hover,
+.btn-danger:focus {
+  background-color: #c12e2a;
+  background-position: 0 -15px;
+}
+.btn-danger:active,
+.btn-danger.active {
+  background-color: #c12e2a;
+  border-color: #b92c28;
+}
+.btn-danger:disabled,
+.btn-danger[disabled] {
+  background-color: #c12e2a;
+  background-image: none;
+}
+.thumbnail,
+.img-thumbnail {
+  -webkit-box-shadow: 0 1px 2px rgba(0, 0, 0, .075);
+          box-shadow: 0 1px 2px rgba(0, 0, 0, .075);
+}
+.dropdown-menu > li > a:hover,
+.dropdown-menu > li > a:focus {
+  background-color: #e8e8e8;
+  background-image: -webkit-linear-gradient(top, #f5f5f5 0%, #e8e8e8 100%);
+  background-image:      -o-linear-gradient(top, #f5f5f5 0%, #e8e8e8 100%);
+  background-image: -webkit-gradient(linear, left top, left bottom, from(#f5f5f5), to(#e8e8e8));
+  background-image:         linear-gradient(to bottom, #f5f5f5 0%, #e8e8e8 100%);
+  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff5f5f5', endColorstr='#ffe8e8e8', GradientType=0);
+  background-repeat: repeat-x;
+}
+.dropdown-menu > .active > a,
+.dropdown-menu > .active > a:hover,
+.dropdown-menu > .active > a:focus {
+  background-color: #357ebd;
+  background-image: -webkit-linear-gradient(top, #428bca 0%, #357ebd 100%);
+  background-image:      -o-linear-gradient(top, #428bca 0%, #357ebd 100%);
+  background-image: -webkit-gradient(linear, left top, left bottom, from(#428bca), to(#357ebd));
+  background-image:         linear-gradient(to bottom, #428bca 0%, #357ebd 100%);
+  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff428bca', endColorstr='#ff357ebd', GradientType=0);
+  background-repeat: repeat-x;
+}
+.navbar-default {
+  background-image: -webkit-linear-gradient(top, #fff 0%, #f8f8f8 100%);
+  background-image:      -o-linear-gradient(top, #fff 0%, #f8f8f8 100%);
+  background-image: -webkit-gradient(linear, left top, left bottom, from(#fff), to(#f8f8f8));
+  background-image:         linear-gradient(to bottom, #fff 0%, #f8f8f8 100%);
+  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffffff', endColorstr='#fff8f8f8', GradientType=0);
+  filter: progid:DXImageTransform.Microsoft.gradient(enabled = false);
+  background-repeat: repeat-x;
+  border-radius: 4px;
+  -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, .15), 0 1px 5px rgba(0, 0, 0, .075);
+          box-shadow: inset 0 1px 0 rgba(255, 255, 255, .15), 0 1px 5px rgba(0, 0, 0, .075);
+}
+.navbar-default .navbar-nav > .open > a,
+.navbar-default .navbar-nav > .active > a {
+  background-image: -webkit-linear-gradient(top, #dbdbdb 0%, #e2e2e2 100%);
+  background-image:      -o-linear-gradient(top, #dbdbdb 0%, #e2e2e2 100%);
+  background-image: -webkit-gradient(linear, left top, left bottom, from(#dbdbdb), to(#e2e2e2));
+  background-image:         linear-gradient(to bottom, #dbdbdb 0%, #e2e2e2 100%);
+  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffdbdbdb', endColorstr='#ffe2e2e2', GradientType=0);
+  background-repeat: repeat-x;
+  -webkit-box-shadow: inset 0 3px 9px rgba(0, 0, 0, .075);
+          box-shadow: inset 0 3px 9px rgba(0, 0, 0, .075);
+}
+.navbar-brand,
+.navbar-nav > li > a {
+  text-shadow: 0 1px 0 rgba(255, 255, 255, .25);
+}
+.navbar-inverse {
+  background-image: -webkit-linear-gradient(top, #3c3c3c 0%, #222 100%);
+  background-image:      -o-linear-gradient(top, #3c3c3c 0%, #222 100%);
+  background-image: -webkit-gradient(linear, left top, left bottom, from(#3c3c3c), to(#222));
+  background-image:         linear-gradient(to bottom, #3c3c3c 0%, #222 100%);
+  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff3c3c3c', endColorstr='#ff222222', GradientType=0);
+  filter: progid:DXImageTransform.Microsoft.gradient(enabled = false);
+  background-repeat: repeat-x;
+}
+.navbar-inverse .navbar-nav > .open > a,
+.navbar-inverse .navbar-nav > .active > a {
+  background-image: -webkit-linear-gradient(top, #080808 0%, #0f0f0f 100%);
+  background-image:      -o-linear-gradient(top, #080808 0%, #0f0f0f 100%);
+  background-image: -webkit-gradient(linear, left top, left bottom, from(#080808), to(#0f0f0f));
+  background-image:         linear-gradient(to bottom, #080808 0%, #0f0f0f 100%);
+  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff080808', endColorstr='#ff0f0f0f', GradientType=0);
+  background-repeat: repeat-x;
+  -webkit-box-shadow: inset 0 3px 9px rgba(0, 0, 0, .25);
+          box-shadow: inset 0 3px 9px rgba(0, 0, 0, .25);
+}
+.navbar-inverse .navbar-brand,
+.navbar-inverse .navbar-nav > li > a {
+  text-shadow: 0 -1px 0 rgba(0, 0, 0, .25);
+}
+.navbar-static-top,
+.navbar-fixed-top,
+.navbar-fixed-bottom {
+  border-radius: 0;
+}
+.alert {
+  text-shadow: 0 1px 0 rgba(255, 255, 255, .2);
+  -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, .25), 0 1px 2px rgba(0, 0, 0, .05);
+          box-shadow: inset 0 1px 0 rgba(255, 255, 255, .25), 0 1px 2px rgba(0, 0, 0, .05);
+}
+.alert-success {
+  background-image: -webkit-linear-gradient(top, #dff0d8 0%, #c8e5bc 100%);
+  background-image:      -o-linear-gradient(top, #dff0d8 0%, #c8e5bc 100%);
+  background-image: -webkit-gradient(linear, left top, left bottom, from(#dff0d8), to(#c8e5bc));
+  background-image:         linear-gradient(to bottom, #dff0d8 0%, #c8e5bc 100%);
+  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffdff0d8', endColorstr='#ffc8e5bc', GradientType=0);
+  background-repeat: repeat-x;
+  border-color: #b2dba1;
+}
+.alert-info {
+  background-image: -webkit-linear-gradient(top, #d9edf7 0%, #b9def0 100%);
+  background-image:      -o-linear-gradient(top, #d9edf7 0%, #b9def0 100%);
+  background-image: -webkit-gradient(linear, left top, left bottom, from(#d9edf7), to(#b9def0));
+  background-image:         linear-gradient(to bottom, #d9edf7 0%, #b9def0 100%);
+  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9edf7', endColorstr='#ffb9def0', GradientType=0);
+  background-repeat: repeat-x;
+  border-color: #9acfea;
+}
+.alert-warning {
+  background-image: -webkit-linear-gradient(top, #fcf8e3 0%, #f8efc0 100%);
+  background-image:      -o-linear-gradient(top, #fcf8e3 0%, #f8efc0 100%);
+  background-image: -webkit-gradient(linear, left top, left bottom, from(#fcf8e3), to(#f8efc0));
+  background-image:         linear-gradient(to bottom, #fcf8e3 0%, #f8efc0 100%);
+  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fffcf8e3', endColorstr='#fff8efc0', GradientType=0);
+  background-repeat: repeat-x;
+  border-color: #f5e79e;
+}
+.alert-danger {
+  background-image: -webkit-linear-gradient(top, #f2dede 0%, #e7c3c3 100%);
+  background-image:      -o-linear-gradient(top, #f2dede 0%, #e7c3c3 100%);
+  background-image: -webkit-gradient(linear, left top, left bottom, from(#f2dede), to(#e7c3c3));
+  background-image:         linear-gradient(to bottom, #f2dede 0%, #e7c3c3 100%);
+  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff2dede', endColorstr='#ffe7c3c3', GradientType=0);
+  background-repeat: repeat-x;
+  border-color: #dca7a7;
+}
+.progress {
+  background-image: -webkit-linear-gradient(top, #ebebeb 0%, #f5f5f5 100%);
+  background-image:      -o-linear-gradient(top, #ebebeb 0%, #f5f5f5 100%);
+  background-image: -webkit-gradient(linear, left top, left bottom, from(#ebebeb), to(#f5f5f5));
+  background-image:         linear-gradient(to bottom, #ebebeb 0%, #f5f5f5 100%);
+  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffebebeb', endColorstr='#fff5f5f5', GradientType=0);
+  background-repeat: repeat-x;
+}
+.progress-bar {
+  background-image: -webkit-linear-gradient(top, #428bca 0%, #3071a9 100%);
+  background-image:      -o-linear-gradient(top, #428bca 0%, #3071a9 100%);
+  background-image: -webkit-gradient(linear, left top, left bottom, from(#428bca), to(#3071a9));
+  background-image:         linear-gradient(to bottom, #428bca 0%, #3071a9 100%);
+  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff428bca', endColorstr='#ff3071a9', GradientType=0);
+  background-repeat: repeat-x;
+}
+.progress-bar-success {
+  background-image: -webkit-linear-gradient(top, #5cb85c 0%, #449d44 100%);
+  background-image:      -o-linear-gradient(top, #5cb85c 0%, #449d44 100%);
+  background-image: -webkit-gradient(linear, left top, left bottom, from(#5cb85c), to(#449d44));
+  background-image:         linear-gradient(to bottom, #5cb85c 0%, #449d44 100%);
+  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5cb85c', endColorstr='#ff449d44', GradientType=0);
+  background-repeat: repeat-x;
+}
+.progress-bar-info {
+  background-image: -webkit-linear-gradient(top, #5bc0de 0%, #31b0d5 100%);
+  background-image:      -o-linear-gradient(top, #5bc0de 0%, #31b0d5 100%);
+  background-image: -webkit-gradient(linear, left top, left bottom, from(#5bc0de), to(#31b0d5));
+  background-image:         linear-gradient(to bottom, #5bc0de 0%, #31b0d5 100%);
+  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5bc0de', endColorstr='#ff31b0d5', GradientType=0);
+  background-repeat: repeat-x;
+}
+.progress-bar-warning {
+  background-image: -webkit-linear-gradient(top, #f0ad4e 0%, #ec971f 100%);
+  background-image:      -o-linear-gradient(top, #f0ad4e 0%, #ec971f 100%);
+  background-image: -webkit-gradient(linear, left top, left bottom, from(#f0ad4e), to(#ec971f));
+  background-image:         linear-gradient(to bottom, #f0ad4e 0%, #ec971f 100%);
+  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff0ad4e', endColorstr='#ffec971f', GradientType=0);
+  background-repeat: repeat-x;
+}
+.progress-bar-danger {
+  background-image: -webkit-linear-gradient(top, #d9534f 0%, #c9302c 100%);
+  background-image:      -o-linear-gradient(top, #d9534f 0%, #c9302c 100%);
+  background-image: -webkit-gradient(linear, left top, left bottom, from(#d9534f), to(#c9302c));
+  background-image:         linear-gradient(to bottom, #d9534f 0%, #c9302c 100%);
+  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9534f', endColorstr='#ffc9302c', GradientType=0);
+  background-repeat: repeat-x;
+}
+.progress-bar-striped {
+  background-image: -webkit-linear-gradient(45deg, rgba(255, 255, 255, .15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .15) 50%, rgba(255, 255, 255, .15) 75%, transparent 75%, transparent);
+  background-image:      -o-linear-gradient(45deg, rgba(255, 255, 255, .15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .15) 50%, rgba(255, 255, 255, .15) 75%, transparent 75%, transparent);
+  background-image:         linear-gradient(45deg, rgba(255, 255, 255, .15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .15) 50%, rgba(255, 255, 255, .15) 75%, transparent 75%, transparent);
+}
+.list-group {
+  border-radius: 4px;
+  -webkit-box-shadow: 0 1px 2px rgba(0, 0, 0, .075);
+          box-shadow: 0 1px 2px rgba(0, 0, 0, .075);
+}
+.list-group-item.active,
+.list-group-item.active:hover,
+.list-group-item.active:focus {
+  text-shadow: 0 -1px 0 #3071a9;
+  background-image: -webkit-linear-gradient(top, #428bca 0%, #3278b3 100%);
+  background-image:      -o-linear-gradient(top, #428bca 0%, #3278b3 100%);
+  background-image: -webkit-gradient(linear, left top, left bottom, from(#428bca), to(#3278b3));
+  background-image:         linear-gradient(to bottom, #428bca 0%, #3278b3 100%);
+  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff428bca', endColorstr='#ff3278b3', GradientType=0);
+  background-repeat: repeat-x;
+  border-color: #3278b3;
+}
+.list-group-item.active .badge,
+.list-group-item.active:hover .badge,
+.list-group-item.active:focus .badge {
+  text-shadow: none;
+}
+.panel {
+  -webkit-box-shadow: 0 1px 2px rgba(0, 0, 0, .05);
+          box-shadow: 0 1px 2px rgba(0, 0, 0, .05);
+}
+.panel-default > .panel-heading {
+  background-image: -webkit-linear-gradient(top, #f5f5f5 0%, #e8e8e8 100%);
+  background-image:      -o-linear-gradient(top, #f5f5f5 0%, #e8e8e8 100%);
+  background-image: -webkit-gradient(linear, left top, left bottom, from(#f5f5f5), to(#e8e8e8));
+  background-image:         linear-gradient(to bottom, #f5f5f5 0%, #e8e8e8 100%);
+  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff5f5f5', endColorstr='#ffe8e8e8', GradientType=0);
+  background-repeat: repeat-x;
+}
+.panel-primary > .panel-heading {
+  background-image: -webkit-linear-gradient(top, #428bca 0%, #357ebd 100%);
+  background-image:      -o-linear-gradient(top, #428bca 0%, #357ebd 100%);
+  background-image: -webkit-gradient(linear, left top, left bottom, from(#428bca), to(#357ebd));
+  background-image:         linear-gradient(to bottom, #428bca 0%, #357ebd 100%);
+  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff428bca', endColorstr='#ff357ebd', GradientType=0);
+  background-repeat: repeat-x;
+}
+.panel-success > .panel-heading {
+  background-image: -webkit-linear-gradient(top, #dff0d8 0%, #d0e9c6 100%);
+  background-image:      -o-linear-gradient(top, #dff0d8 0%, #d0e9c6 100%);
+  background-image: -webkit-gradient(linear, left top, left bottom, from(#dff0d8), to(#d0e9c6));
+  background-image:         linear-gradient(to bottom, #dff0d8 0%, #d0e9c6 100%);
+  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffdff0d8', endColorstr='#ffd0e9c6', GradientType=0);
+  background-repeat: repeat-x;
+}
+.panel-info > .panel-heading {
+  background-image: -webkit-linear-gradient(top, #d9edf7 0%, #c4e3f3 100%);
+  background-image:      -o-linear-gradient(top, #d9edf7 0%, #c4e3f3 100%);
+  background-image: -webkit-gradient(linear, left top, left bottom, from(#d9edf7), to(#c4e3f3));
+  background-image:         linear-gradient(to bottom, #d9edf7 0%, #c4e3f3 100%);
+  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9edf7', endColorstr='#ffc4e3f3', GradientType=0);
+  background-repeat: repeat-x;
+}
+.panel-warning > .panel-heading {
+  background-image: -webkit-linear-gradient(top, #fcf8e3 0%, #faf2cc 100%);
+  background-image:      -o-linear-gradient(top, #fcf8e3 0%, #faf2cc 100%);
+  background-image: -webkit-gradient(linear, left top, left bottom, from(#fcf8e3), to(#faf2cc));
+  background-image:         linear-gradient(to bottom, #fcf8e3 0%, #faf2cc 100%);
+  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fffcf8e3', endColorstr='#fffaf2cc', GradientType=0);
+  background-repeat: repeat-x;
+}
+.panel-danger > .panel-heading {
+  background-image: -webkit-linear-gradient(top, #f2dede 0%, #ebcccc 100%);
+  background-image:      -o-linear-gradient(top, #f2dede 0%, #ebcccc 100%);
+  background-image: -webkit-gradient(linear, left top, left bottom, from(#f2dede), to(#ebcccc));
+  background-image:         linear-gradient(to bottom, #f2dede 0%, #ebcccc 100%);
+  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff2dede', endColorstr='#ffebcccc', GradientType=0);
+  background-repeat: repeat-x;
+}
+.well {
+  background-image: -webkit-linear-gradient(top, #e8e8e8 0%, #f5f5f5 100%);
+  background-image:      -o-linear-gradient(top, #e8e8e8 0%, #f5f5f5 100%);
+  background-image: -webkit-gradient(linear, left top, left bottom, from(#e8e8e8), to(#f5f5f5));
+  background-image:         linear-gradient(to bottom, #e8e8e8 0%, #f5f5f5 100%);
+  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffe8e8e8', endColorstr='#fff5f5f5', GradientType=0);
+  background-repeat: repeat-x;
+  border-color: #dcdcdc;
+  -webkit-box-shadow: inset 0 1px 3px rgba(0, 0, 0, .05), 0 1px 0 rgba(255, 255, 255, .1);
+          box-shadow: inset 0 1px 3px rgba(0, 0, 0, .05), 0 1px 0 rgba(255, 255, 255, .1);
+}
+/*# sourceMappingURL=bootstrap-theme.css.map */

File diff suppressed because it is too large
+ 1 - 0
sources/public/bootstrap/css/bootstrap-theme.css.map


File diff suppressed because it is too large
+ 5 - 0
sources/public/bootstrap/css/bootstrap-theme.min.css


File diff suppressed because it is too large
+ 6358 - 0
sources/public/bootstrap/css/bootstrap.css


File diff suppressed because it is too large
+ 1 - 0
sources/public/bootstrap/css/bootstrap.css.map


File diff suppressed because it is too large
+ 5 - 0
sources/public/bootstrap/css/bootstrap.min.css


BIN
sources/public/bootstrap/fonts/glyphicons-halflings-regular.eot


File diff suppressed because it is too large
+ 229 - 0
sources/public/bootstrap/fonts/glyphicons-halflings-regular.svg


BIN
sources/public/bootstrap/fonts/glyphicons-halflings-regular.ttf


BIN
sources/public/bootstrap/fonts/glyphicons-halflings-regular.woff


File diff suppressed because it is too large
+ 2276 - 0
sources/public/bootstrap/js/bootstrap.js


File diff suppressed because it is too large
+ 7 - 0
sources/public/bootstrap/js/bootstrap.min.js


+ 13 - 0
sources/public/bootstrap/js/npm.js

@@ -0,0 +1,13 @@
+// This file is autogenerated via the `commonjs` Grunt task. You can require() this file in a CommonJS environment.
+require('../../js/transition.js')
+require('../../js/alert.js')
+require('../../js/button.js')
+require('../../js/carousel.js')
+require('../../js/collapse.js')
+require('../../js/dropdown.js')
+require('../../js/modal.js')
+require('../../js/tooltip.js')
+require('../../js/popover.js')
+require('../../js/scrollspy.js')
+require('../../js/tab.js')
+require('../../js/affix.js')

+ 0 - 0
sources/public/css/style.css


File diff suppressed because it is too large
+ 4 - 0
sources/public/jquery/jquery-2.1.1.min.js


+ 3 - 0
sources/views/homepage.html.php

@@ -0,0 +1,3 @@
+<div class="hero">
+    <h1>Hello world !</h1>
+</div>

+ 39 - 0
sources/views/layout.html.php

@@ -0,0 +1,39 @@
+<!doctype html>
+<!--[if lt IE 7]> <html class="no-js lt-ie9 lt-ie8 lt-ie7" lang="en"> <![endif]-->
+<!--[if IE 7]> <html class="no-js lt-ie9 lt-ie8" lang="en"> <![endif]-->
+<!--[if IE 8]> <html class="no-js lt-ie9" lang="en"> <![endif]-->
+<!--[if gt IE 8]><!--> <html class="no-js" lang="<?= $locale ?>"> <!--<![endif]-->
+<head>
+    <meta charset="utf-8">
+    <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
+    <title>YunoHost <? echo (isset($title)) ? "| ".$title : "" ?></title>
+    <meta name="viewport" content="width=device-width">
+    <link media="all" type="text/css" href="<?= PUBLIC_DIR ?>/bootstrap/css/bootstrap.min.css" rel="stylesheet">
+    <link media="all" type="text/css" href="<?= PUBLIC_DIR ?>/bootstrap/css/bootstrap-theme.min.css" rel="stylesheet">
+    <link media="all" type="text/css" href="<?= PUBLIC_DIR ?>/css/style.css" rel="stylesheet">
+    <script src="<?= PUBLIC_DIR ?>/jquery/jquery-2.1.1.min.js"></script>
+    <script src="<?= PUBLIC_DIR ?>/bootstrap/js/bootstrap.min.js"></script>
+</head>
+<body>
+    <div class="container">
+        <? if (isset($flash['error'])) { ?>
+            <div class="alert alert-dismissible alert-error fade in" role="alert">
+                <button type="button" class="close" data-dismiss="alert"><span aria-hidden="true">&times;</span><span class="sr-only">Close</span></button>
+                <strong><?= T_('Error') ?>:</strong> <?= $flash['error'] ?>
+            </div>
+        <? } elseif (isset($flash['notice'])) { ?>
+            <div class="alert alert-dismissible alert-info fade in" role="alert">
+                <button type="button" class="close" data-dismiss="alert"><span aria-hidden="true">&times;</span><span class="sr-only">Close</span></button>
+                <strong><?= T_('Notice') ?>:</strong> <?= $flash['notice'] ?>
+            </div>
+        <? } elseif (isset($flash['success'])) { ?>
+            <div class="alert alert-dismissible alert-success fade in" role="alert">
+                <button type="button" class="close" data-dismiss="alert"><span aria-hidden="true">&times;</span><span class="sr-only">Close</span></button>
+                <?= $flash['success'] ?>
+            </div>
+        <? } ?>
+
+        <?= $content?>
+
+</body>
+</html>