idleman 2 years ago
commit
4f5dd6b682
100 changed files with 18202 additions and 0 deletions
  1. 9 0
      .gitignore
  2. 7 0
      .tool.sample.php
  3. 44 0
      404.html
  4. 77 0
      README.md
  5. 1071 0
      RainTPL.php
  6. 411 0
      action.php
  7. 31 0
      api/index.php
  8. 18 0
      apigen.neon
  9. BIN
      apigen.phar
  10. 38 0
      classes/Action.class.php
  11. 87 0
      classes/Client.class.php
  12. 192 0
      classes/Configuration.class.php
  13. 42 0
      classes/Database.class.php
  14. 53 0
      classes/Device.class.php
  15. 440 0
      classes/Entity.class.php
  16. 145 0
      classes/Event.class.php
  17. 320 0
      classes/Functions.class.php
  18. 65 0
      classes/Gpio.class.php
  19. 306 0
      classes/Monitoring.class.php
  20. 111 0
      classes/Personality.class.php
  21. 369 0
      classes/Plugin.class.php
  22. 51 0
      classes/Rank.class.php
  23. 86 0
      classes/Right.class.php
  24. 431 0
      classes/SQLiteEntity.class.php
  25. 82 0
      classes/Section.class.php
  26. 267 0
      classes/System.class.php
  27. 219 0
      classes/User.class.php
  28. 189 0
      common.php
  29. 37 0
      constant.sample.php
  30. 33 0
      db.json
  31. 1 0
      db/index.php
  32. 2 0
      doc.bat
  33. 4 0
      footer.php
  34. 14 0
      header.php
  35. 16 0
      index.php
  36. 316 0
      install.php
  37. 1113 0
      install.sh
  38. 7 0
      lib/sabre/autoload.php
  39. 413 0
      lib/sabre/composer/ClassLoader.php
  40. 19 0
      lib/sabre/composer/LICENSE
  41. 9 0
      lib/sabre/composer/autoload_classmap.php
  42. 16 0
      lib/sabre/composer/autoload_files.php
  43. 9 0
      lib/sabre/composer/autoload_namespaces.php
  44. 18 0
      lib/sabre/composer/autoload_psr4.php
  45. 59 0
      lib/sabre/composer/autoload_real.php
  46. 416 0
      lib/sabre/composer/installed.json
  47. 43 0
      lib/sabre/sabre/dav/.gitignore
  48. 33 0
      lib/sabre/sabre/dav/.travis.yml
  49. 87 0
      lib/sabre/sabre/dav/CONTRIBUTING.md
  50. 177 0
      lib/sabre/sabre/dav/bin/build.php
  51. 248 0
      lib/sabre/sabre/dav/bin/googlecode_upload.py
  52. 284 0
      lib/sabre/sabre/dav/bin/migrateto17.php
  53. 453 0
      lib/sabre/sabre/dav/bin/migrateto20.php
  54. 180 0
      lib/sabre/sabre/dav/bin/migrateto21.php
  55. 171 0
      lib/sabre/sabre/dav/bin/migrateto30.php
  56. 140 0
      lib/sabre/sabre/dav/bin/naturalselection
  57. 2 0
      lib/sabre/sabre/dav/bin/sabredav
  58. 53 0
      lib/sabre/sabre/dav/bin/sabredav.php
  59. 66 0
      lib/sabre/sabre/dav/composer.json
  60. 226 0
      lib/sabre/sabre/dav/lib/CalDAV/Backend/AbstractBackend.php
  61. 268 0
      lib/sabre/sabre/dav/lib/CalDAV/Backend/BackendInterface.php
  62. 46 0
      lib/sabre/sabre/dav/lib/CalDAV/Backend/NotificationSupport.php
  63. 1210 0
      lib/sabre/sabre/dav/lib/CalDAV/Backend/PDO.php
  64. 65 0
      lib/sabre/sabre/dav/lib/CalDAV/Backend/SchedulingSupport.php
  65. 243 0
      lib/sabre/sabre/dav/lib/CalDAV/Backend/SharingSupport.php
  66. 89 0
      lib/sabre/sabre/dav/lib/CalDAV/Backend/SubscriptionSupport.php
  67. 81 0
      lib/sabre/sabre/dav/lib/CalDAV/Backend/SyncSupport.php
  68. 527 0
      lib/sabre/sabre/dav/lib/CalDAV/Calendar.php
  69. 430 0
      lib/sabre/sabre/dav/lib/CalDAV/CalendarHome.php
  70. 290 0
      lib/sabre/sabre/dav/lib/CalDAV/CalendarObject.php
  71. 375 0
      lib/sabre/sabre/dav/lib/CalDAV/CalendarQueryValidator.php
  72. 80 0
      lib/sabre/sabre/dav/lib/CalDAV/CalendarRoot.php
  73. 35 0
      lib/sabre/sabre/dav/lib/CalDAV/Exception/InvalidComponentType.php
  74. 366 0
      lib/sabre/sabre/dav/lib/CalDAV/ICSExportPlugin.php
  75. 18 0
      lib/sabre/sabre/dav/lib/CalDAV/ICalendar.php
  76. 21 0
      lib/sabre/sabre/dav/lib/CalDAV/ICalendarObject.php
  77. 39 0
      lib/sabre/sabre/dav/lib/CalDAV/ICalendarObjectContainer.php
  78. 48 0
      lib/sabre/sabre/dav/lib/CalDAV/IShareableCalendar.php
  79. 36 0
      lib/sabre/sabre/dav/lib/CalDAV/ISharedCalendar.php
  80. 173 0
      lib/sabre/sabre/dav/lib/CalDAV/Notifications/Collection.php
  81. 23 0
      lib/sabre/sabre/dav/lib/CalDAV/Notifications/ICollection.php
  82. 38 0
      lib/sabre/sabre/dav/lib/CalDAV/Notifications/INode.php
  83. 193 0
      lib/sabre/sabre/dav/lib/CalDAV/Notifications/Node.php
  84. 180 0
      lib/sabre/sabre/dav/lib/CalDAV/Notifications/Plugin.php
  85. 1025 0
      lib/sabre/sabre/dav/lib/CalDAV/Plugin.php
  86. 33 0
      lib/sabre/sabre/dav/lib/CalDAV/Principal/Collection.php
  87. 19 0
      lib/sabre/sabre/dav/lib/CalDAV/Principal/IProxyRead.php
  88. 19 0
      lib/sabre/sabre/dav/lib/CalDAV/Principal/IProxyWrite.php
  89. 181 0
      lib/sabre/sabre/dav/lib/CalDAV/Principal/ProxyRead.php
  90. 181 0
      lib/sabre/sabre/dav/lib/CalDAV/Principal/ProxyWrite.php
  91. 135 0
      lib/sabre/sabre/dav/lib/CalDAV/Principal/User.php
  92. 15 0
      lib/sabre/sabre/dav/lib/CalDAV/Schedule/IInbox.php
  93. 190 0
      lib/sabre/sabre/dav/lib/CalDAV/Schedule/IMipPlugin.php
  94. 15 0
      lib/sabre/sabre/dav/lib/CalDAV/Schedule/IOutbox.php
  95. 13 0
      lib/sabre/sabre/dav/lib/CalDAV/Schedule/ISchedulingObject.php
  96. 261 0
      lib/sabre/sabre/dav/lib/CalDAV/Schedule/Inbox.php
  97. 184 0
      lib/sabre/sabre/dav/lib/CalDAV/Schedule/Outbox.php
  98. 994 0
      lib/sabre/sabre/dav/lib/CalDAV/Schedule/Plugin.php
  99. 165 0
      lib/sabre/sabre/dav/lib/CalDAV/Schedule/SchedulingObject.php
  100. 72 0
      lib/sabre/sabre/dav/lib/CalDAV/ShareableCalendar.php

+ 9 - 0
.gitignore

@@ -0,0 +1,9 @@
+database.db
+*.rtpl.php
+/plugins/plugins.states.json
+/templates/save-default
+/db/.database.db
+/log/.log.txt
+/dbversion
+/cache/avatar/*.jpg
+/constant.php

+ 7 - 0
.tool.sample.php

@@ -0,0 +1,7 @@
+<?php  
+	$tool  = (object) array(
+		'type' => 'reset_password',
+		'login' => 'votre.login',
+		'password' => 'votre.nouveau.mdp'
+	);
+?>

+ 44 - 0
404.html

@@ -0,0 +1,44 @@
+<!doctype html>
+<html lang="en">
+<head>
+  <meta charset="utf-8">
+  <title>Page Non trouvée :(</title>
+  <style>
+    ::-moz-selection { background: #fe57a1; color: #fff; text-shadow: none; }
+    ::selection { background: #fe57a1; color: #fff; text-shadow: none; }
+    html { padding: 30px 10px; font-size: 20px; line-height: 1.4; color: #737373; background: #f0f0f0; -webkit-text-size-adjust: 100%; -ms-text-size-adjust: 100%; }
+    html, input { font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; }
+    body { max-width: 500px; _width: 500px; padding: 30px 20px 50px; border: 1px solid #b3b3b3; border-radius: 4px; margin: 0 auto; box-shadow: 0 1px 10px #a7a7a7, inset 0 1px 0 #fff; background: #fcfcfc; }
+    h1 { margin: 0 10px; font-size: 50px; text-align: center; }
+    h1 span { color: #bbb; }
+    h3 { margin: 1.5em 0 0.5em; }
+    p { margin: 1em 0; }
+    ul { padding: 0 0 0 40px; margin: 1em 0; }
+    .container { max-width: 380px; _width: 380px; margin: 0 auto; }
+    /* google search */
+    #goog-fixurl ul { list-style: none; padding: 0; margin: 0; }
+    #goog-fixurl form { margin: 0; }
+    #goog-wm-qt, #goog-wm-sb { border: 1px solid #bbb; font-size: 16px; line-height: normal; vertical-align: top; color: #444; border-radius: 2px; }
+    #goog-wm-qt { width: 220px; height: 20px; padding: 5px; margin: 5px 10px 0 0; box-shadow: inset 0 1px 1px #ccc; }
+    #goog-wm-sb { display: inline-block; height: 32px; padding: 0 10px; margin: 5px 0 0; white-space: nowrap; cursor: pointer; background-color: #f5f5f5; background-image: -webkit-linear-gradient(rgba(255,255,255,0), #f1f1f1); background-image: -moz-linear-gradient(rgba(255,255,255,0), #f1f1f1); background-image: -ms-linear-gradient(rgba(255,255,255,0), #f1f1f1); background-image: -o-linear-gradient(rgba(255,255,255,0), #f1f1f1); -webkit-appearance: none; -moz-appearance: none; appearance: none; *overflow: visible; *display: inline; *zoom: 1; }
+    #goog-wm-sb:hover, #goog-wm-sb:focus { border-color: #aaa; box-shadow: 0 1px 1px rgba(0, 0, 0, 0.1); background-color: #f8f8f8; }
+    #goog-wm-qt:focus, #goog-wm-sb:focus { border-color: #105cb6; outline: 0; color: #222; }
+    input::-moz-focus-inner { padding: 0; border: 0; }
+  </style>
+</head>
+<body>
+  <div class="container">
+    <h1>Page Non trouvée <span>:(</span></h1>
+    <p>Désolé la page que vous cherchez n'existe pas.</p>
+    <p>C'est certainement:</p>
+    <ul>
+      <li>une adresse mal tapée</li>
+      <li>Un lien mort</li>
+    </ul>
+    <script>
+      var GOOG_FIXURL_LANG = (navigator.language || '').slice(0,2),GOOG_FIXURL_SITE = location.host;
+    </script>
+    <script src="http://linkhelp.clients.google.com/tbproxy/lh/wm/fixurl.js"></script>
+  </div>
+</body>
+</html>

+ 77 - 0
README.md

@@ -0,0 +1,77 @@
+Yana Server
+===========
+
+Interface PHP de domotique Y.A.N.A (You Are Not Alone)
+
+Pré-requis
+============
+
+- Raspberry PI
+- Apache 2 ou Lighttpd
+- PHP 5
+- SQLite 3
+- [Librairie Wiring PI](https://projects.drogon.net/raspberry-pi/wiringpi/download-and-install/)
+
+
+
+Installation automatique
+========================
+Si vous n'êtes pas à l'aise avec le monde linux et que vous n'avez pas trop bidouillé votre système jusqu'ici, vous pouvez utiliser la commande
+d'installation automatique gentiment proposé par maditnerd, tapez simplement :
+
+> curl -L yana.madnerd.org|sudo bash
+
+Pour plus de détails allez sur le wiki: http://projet.idleman.fr/yana/?page=Installation
+
+Puis executez l'adresse web de yana dans un navigateur :
+
+`http://adresse.de.votre.rpi/yana-server`
+
+Et suivez le formulaire d'installation.
+nb : A la fin de l'installation, vous pouvez activer ou désactiver les plugins qui vous sont utiles dans la section
+configuration --> plugins, pensez à le faire AVANT de jouer avec Yana windows ou Yana Android sans quoi aucune commande ne sera disponible
+
+Si vous tombez sur l'erreur 'Erreur connexion au serveur socket depuis yana-server, le serveur est il allumé ? Connection refused' c'est que le serveur socket s'est mal lancé, il est necessaire de lancer le serveur avec la commande :
+`php /var/www/yana-server/socket.php`
+
+Installation manuelle
+============
+
+Executez les commandes suivantes dans un shell :
+
+> sudo apt-get install git-core && sudo apt-get install sqlite3 && apt-get install php5 && sudo apt-get install php5-sqlite && cd /var/www/ && sudo git clone https://github.com/ldleman/yana-server.git /var/www/yana-server && sudo chown -R www-data:www-data yana-server && sudo chown root:www-data /var/www/yana-server/plugins/radioRelay/radioEmission && sudo chmod +s /var/www/yana-server/plugins/radioRelay/radioEmission
+
+Puis executez l'adresse web de yana dans un navigateur :
+
+`http://adresse.de.votre.rpi/yana-server`
+
+Et suivez le formulaire d'installation.
+
+nb : A la fin de l'installation, vous pouvez activer ou désactiver les plugins qui vous sont utiles dans la section
+configuration --> plugins, pensez à le faire AVANT de jouer avec Yana windows ou Yana Android sans quoi aucune commande ne sera disponible
+
+Si vous utilisez le client vocal, il est necessaire de lancer le serveur socket avec la commande :
+`php /var/www/yana-server/socket.php`
+
+Sécurité
+========
+Pour des raisons de sécurité, il est très fortement déconseillé d'ouvrir l'accès au serveur web de yana sur l'exterieur.
+Si vous le faites cependant, il est necessaire d'utiliser apache comme serveur http OU de configurer votre serveur http
+pour interdire l'accès au dossier /db
+
+Mise à jour
+============
+
+Pour mettre a jour yana-server, il faut utiliser git, placez vous dans le répertoire de yana
+```cd /var/www/yana-server```
+
+Et faites un git pull pour récuperer la dernière version
+```git pull```
+
+Attention, si vous aviez fait des modifs sur le code entre temps il est possible que le git pull ne fonctionne pas, dans ce cas faites un git checkout pour reprendre la copie exacte du dépot officiel en ecransant vos modifications
+```git reset --hard origin/master```
+
+Puis remettez les permission en ecriture sur le dossier plugins
+> sudo chown -R www-data:www-data /var/www/yana-server && sudo chown root:www-data /var/www/yana-server/plugins/radioRelay/radioEmission && sudo chmod +s /var/www/yana-server/plugins/radioRelay/radioEmission
+
+Une fois l'update terminé, allez en section plugin de yana-server et désactivez/réactivez chaques plugins utilisés afin de mettre à jour leurs tables.

+ 1071 - 0
RainTPL.php

@@ -0,0 +1,1071 @@
+<?php
+
+/**
+ *  RainTPL
+ *  -------
+ *  Realized by Federico Ulfo & maintained by the Rain Team
+ *  Distributed under GNU/LGPL 3 License
+ *
+ *  @version 2.7.2
+ */
+
+
+class RainTPL{
+
+	// -------------------------
+	// 	CONFIGURATION
+	// -------------------------
+
+		/**
+		 * Template directory
+		 *
+		 * @var string
+		 */
+		static $tpl_dir = "tpl/";
+
+
+		/**
+		 * Cache directory. Is the directory where RainTPL will compile the template and save the cache
+		 *
+		 * @var string
+		 */
+		static $cache_dir = "tmp/";
+
+
+		/**
+		 * Template base URL. RainTPL will add this URL to the relative paths of element selected in $path_replace_list.
+		 *
+		 * @var string
+		 */
+		static $base_url = null;
+
+
+		/**
+		 * Template extension.
+		 *
+		 * @var string
+		 */
+		static $tpl_ext = "html";
+
+
+		/**
+		 * Path replace is a cool features that replace all relative paths of images (<img src="...">), stylesheet (<link href="...">), script (<script src="...">) and link (<a href="...">)
+		 * Set true to enable the path replace.
+		 *
+		 * @var unknown_type
+		 */
+		static $path_replace = true;
+
+
+		/**
+		 * You can set what the path_replace method will replace.
+		 * Avaible options: a, img, link, script, input
+		 *
+		 * @var array
+		 */
+		static $path_replace_list = array( 'a', 'img', 'link', 'script', 'input' );
+
+
+		/**
+		 * You can define in the black list what string are disabled into the template tags
+		 *
+		 * @var unknown_type
+		 */
+		static $black_list = array( '\$this', 'raintpl::', 'self::', '_SESSION', '_SERVER', '_ENV',  'eval', 'exec', 'unlink', 'rmdir' );
+
+
+		/**
+		 * Check template.
+		 * true: checks template update time, if changed it compile them
+		 * false: loads the compiled template. Set false if server doesn't have write permission for cache_directory.
+		 *
+		 */
+		static $check_template_update = true;
+                
+
+		/**
+		 * PHP tags <? ?> 
+		 * True: php tags are enabled into the template
+		 * False: php tags are disabled into the template and rendered as html
+		 *
+		 * @var bool
+		 */
+		static $php_enabled = false;
+
+		
+		/**
+		 * Debug mode flag.
+		 * True: debug mode is used, syntax errors are displayed directly in template. Execution of script is not terminated.
+		 * False: exception is thrown on found error.
+		 *
+		 * @var bool
+		 */
+		static $debug = false;
+
+	// -------------------------
+
+
+	// -------------------------
+	// 	RAINTPL VARIABLES
+	// -------------------------
+
+		/**
+		 * Is the array where RainTPL keep the variables assigned
+		 *
+		 * @var array
+		 */
+		public $var = array();
+
+		protected $tpl = array(),		// variables to keep the template directories and info
+				  $cache = false,		// static cache enabled / disabled
+                  $cache_id = null;       // identify only one cache
+
+                protected static $config_name_sum = array();   // takes all the config to create the md5 of the file
+
+	// -------------------------
+
+
+
+	const CACHE_EXPIRE_TIME = 3600; // default cache expire time = hour
+
+
+
+	/**
+	 * Assign variable
+	 * eg. 	$t->assign('name','mickey');
+	 *
+	 * @param mixed $variable_name Name of template variable or associative array name/value
+	 * @param mixed $value value assigned to this variable. Not set if variable_name is an associative array
+	 */
+
+	function assign( $variable, $value = null ){
+		if( is_array( $variable ) )
+			$this->var = $variable + $this->var;
+		else
+			$this->var[ $variable ] = $value;
+	}
+
+
+
+	/**
+	 * Draw the template
+	 * eg. 	$html = $tpl->draw( 'demo', TRUE ); // return template in string
+	 * or 	$tpl->draw( $tpl_name ); // echo the template
+	 *
+	 * @param string $tpl_name  template to load
+	 * @param boolean $return_string  true=return a string, false=echo the template
+	 * @return string
+	 */
+
+	function draw( $tpl_name, $return_string = false ){
+
+		try {
+			// compile the template if necessary and set the template filepath
+			$this->check_template( $tpl_name );
+		} catch (RainTpl_Exception $e) {
+			$output = $this->printDebug($e);
+			die($output);
+		}
+
+		// Cache is off and, return_string is false
+        // Rain just echo the template
+
+        if( !$this->cache && !$return_string ){
+            extract( $this->var );
+            include $this->tpl['compiled_filename'];
+            unset( $this->tpl );
+        }
+
+
+		// cache or return_string are enabled
+        // rain get the output buffer to save the output in the cache or to return it as string
+
+        else{
+
+            //----------------------
+            // get the output buffer
+            //----------------------
+                ob_start();
+                extract( $this->var );
+                include $this->tpl['compiled_filename'];
+                $raintpl_contents = ob_get_clean();
+            //----------------------
+
+
+            // save the output in the cache
+            if( $this->cache )
+                file_put_contents( $this->tpl['cache_filename'], "<?php if(!class_exists('raintpl')){exit;}?>" . $raintpl_contents );
+
+            // free memory
+            unset( $this->tpl );
+
+            // return or print the template
+            if( $return_string ) return $raintpl_contents; else echo $raintpl_contents;
+
+        }
+
+	}
+
+
+
+	/**
+	 * If exists a valid cache for this template it returns the cache
+	 *
+	 * @param string $tpl_name Name of template (set the same of draw)
+	 * @param int $expiration_time Set after how many seconds the cache expire and must be regenerated
+	 * @return string it return the HTML or null if the cache must be recreated
+	 */
+
+	function cache( $tpl_name, $expire_time = self::CACHE_EXPIRE_TIME, $cache_id = null ){
+
+        // set the cache_id
+        $this->cache_id = $cache_id;
+
+		if( !$this->check_template( $tpl_name ) && file_exists( $this->tpl['cache_filename'] ) && ( time() - filemtime( $this->tpl['cache_filename'] ) < $expire_time ) ){
+			// return the cached file as HTML. It remove the first 43 character, which are a PHP code to secure the file <?php if(!class_exists('raintpl')){exit;}? >
+			return substr( file_get_contents( $this->tpl['cache_filename'] ), 43 );
+		}
+		else{
+			//delete the cache of the selected template
+            if (file_exists($this->tpl['cache_filename']))
+            unlink($this->tpl['cache_filename'] );
+			$this->cache = true;
+		}
+	}
+
+
+
+	/**
+	 * Configure the settings of RainTPL
+	 *
+	 */
+	static function configure( $setting, $value = null ){
+		if( is_array( $setting ) )
+			foreach( $setting as $key => $value )
+				self::configure( $key, $value );
+		else if( property_exists( __CLASS__, $setting ) ){
+			self::$$setting = $value;
+            self::$config_name_sum[ $setting ] = $value; // take trace of all config
+        }
+	}
+
+
+
+	// check if has to compile the template
+	// return true if the template has changed
+	protected function check_template( $tpl_name ){
+
+		if( !isset($this->tpl['checked']) ){
+
+			$tpl_basename                       = basename( $tpl_name );														// template basename
+			$tpl_basedir                        = strpos($tpl_name,"/") ? dirname($tpl_name) . '/' : null;						// template basedirectory
+			$this->tpl['template_directory']    = self::$tpl_dir . $tpl_basedir;								// template directory
+			$this->tpl['tpl_filename']          = $this->tpl['template_directory'] . $tpl_basename . '.' . self::$tpl_ext;	// template filename
+			$temp_compiled_filename             = self::$cache_dir . $tpl_basename . "." . md5( $this->tpl['template_directory'] . serialize(self::$config_name_sum));
+			$this->tpl['compiled_filename']     = $temp_compiled_filename . '.rtpl.php';	// cache filename
+			$this->tpl['cache_filename']        = $temp_compiled_filename . '.s_' . $this->cache_id . '.rtpl.php';	// static cache filename
+                        $this->tpl['checked']               = true;
+                        
+			// if the template doesn't exist and is not an external source throw an error
+			if( self::$check_template_update && !file_exists( $this->tpl['tpl_filename'] ) && !preg_match('/http/', $tpl_name) ){
+				$e = new RainTpl_NotFoundException( 'Template '. $tpl_basename .' not found!' );
+				throw $e->setTemplateFile($this->tpl['tpl_filename']);
+			}
+
+			// We check if the template is not an external source
+			if(preg_match('/http/', $tpl_name)){
+				$this->compileFile('', '', $tpl_name, self::$cache_dir, $this->tpl['compiled_filename'] );
+				return true;
+			}
+			// file doesn't exist, or the template was updated, Rain will compile the template
+			elseif( !file_exists( $this->tpl['compiled_filename'] ) || ( self::$check_template_update && filemtime($this->tpl['compiled_filename']) < filemtime( $this->tpl['tpl_filename'] ) ) ){
+				$this->compileFile( $tpl_basename, $tpl_basedir, $this->tpl['tpl_filename'], self::$cache_dir, $this->tpl['compiled_filename'] );
+				return true;
+			}
+			
+		}
+	}
+
+
+	/**
+	* execute stripslaches() on the xml block. Invoqued by preg_replace_callback function below
+	* @access protected
+	*/
+	protected function xml_reSubstitution($capture) {
+    		return "<?php echo '<?xml ".stripslashes($capture[1])." ?>'; ?>";
+	} 
+
+	/**
+	 * Compile and write the compiled template file
+	 * @access protected
+	 */
+	protected function compileFile( $tpl_basename, $tpl_basedir, $tpl_filename, $cache_dir, $compiled_filename ){
+
+		//read template file
+		$this->tpl['source'] = $template_code = file_get_contents( $tpl_filename );
+
+		//xml substitution
+		$template_code = preg_replace( "/<\?xml(.*?)\?>/s", "##XML\\1XML##", $template_code );
+
+		//disable php tag
+		if( !self::$php_enabled )
+			$template_code = str_replace( array("<?","?>"), array("&lt;?","?&gt;"), $template_code );
+
+		//xml re-substitution
+		$template_code = preg_replace_callback ( "/##XML(.*?)XML##/s", array($this, 'xml_reSubstitution'), $template_code ); 
+
+		//compile template
+		$template_compiled = "<?php if(!class_exists('raintpl')){exit;}?>" . $this->compileTemplate( $template_code, $tpl_basedir );
+		
+
+		// fix the php-eating-newline-after-closing-tag-problem
+		$template_compiled = str_replace( "?>\n", "?>\n\n", $template_compiled );
+
+		// create directories
+		if( !is_dir( $cache_dir ) )
+			mkdir( $cache_dir, 0755, true );
+
+		if( !is_writable( $cache_dir ) )
+			throw new RainTpl_Exception ('Cache directory ' . $cache_dir . 'doesn\'t have write permission. Set write permission or set RAINTPL_CHECK_TEMPLATE_UPDATE to false. More details on http://www.raintpl.com/Documentation/Documentation-for-PHP-developers/Configuration/');
+
+		//write compiled file
+		file_put_contents( $compiled_filename, $template_compiled );
+	}
+
+
+
+	/**
+	 * Compile template
+	 * @access protected
+	 */
+	protected function compileTemplate( $template_code, $tpl_basedir ){
+
+		//tag list
+		$tag_regexp = array( 'loop'         => '(\{loop(?: name){0,1}="\${0,1}[^"]*"\})',
+                             'loop_close'   => '(\{\/loop\})',
+                             'if'           => '(\{if(?: condition){0,1}="[^"]*"\})',
+                             'elseif'       => '(\{elseif(?: condition){0,1}="[^"]*"\})',
+                             'else'         => '(\{else\})',
+                             'if_close'     => '(\{\/if\})',
+                             'function'     => '(\{function="[^"]*"\})',
+                             'noparse'      => '(\{noparse\})',
+                             'noparse_close'=> '(\{\/noparse\})',
+                             'ignore'       => '(\{ignore\}|\{\*)',
+                             'ignore_close'	=> '(\{\/ignore\}|\*\})',
+                             'include'      => '(\{include="[^"]*"(?: cache="[^"]*")?\})',
+                             'template_info'=> '(\{\$template_info\})',
+                             'function'		=> '(\{function="(\w*?)(?:.*?)"\})'
+							);
+
+		$tag_regexp = "/" . join( "|", $tag_regexp ) . "/";
+
+		//split the code with the tags regexp
+		$template_code = preg_split ( $tag_regexp, $template_code, -1, PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY );
+
+		//path replace (src of img, background and href of link)
+		$template_code = $this->path_replace( $template_code, $tpl_basedir );
+
+		//compile the code
+		$compiled_code = $this->compileCode( $template_code );
+
+		//return the compiled code
+		return $compiled_code;
+
+	}
+
+
+
+	/**
+	 * Compile the code
+	 * @access protected
+	 */
+	protected function compileCode( $parsed_code ){
+            
+                // if parsed code is empty return null string
+                if( !$parsed_code )
+                    return "";
+
+		//variables initialization
+		$compiled_code = $open_if = $comment_is_open = $ignore_is_open = null;
+                $loop_level = 0;
+
+                
+	 	//read all parsed code
+	 	foreach( $parsed_code as $html ){
+
+	 		//close ignore tag
+			if( !$comment_is_open && ( strpos( $html, '{/ignore}' ) !== FALSE || strpos( $html, '*}' ) !== FALSE ) )
+	 			$ignore_is_open = false;
+
+	 		//code between tag ignore id deleted
+	 		elseif( $ignore_is_open ){
+	 			//ignore the code
+	 		}
+
+	 		//close no parse tag
+			elseif( strpos( $html, '{/noparse}' ) !== FALSE )
+	 			$comment_is_open = false;
+
+	 		//code between tag noparse is not compiled
+	 		elseif( $comment_is_open )
+ 				$compiled_code .= $html;
+
+	 		//ignore
+			elseif( strpos( $html, '{ignore}' ) !== FALSE || strpos( $html, '{*' ) !== FALSE )
+	 			$ignore_is_open = true;
+
+	 		//noparse
+	 		elseif( strpos( $html, '{noparse}' ) !== FALSE )
+	 			$comment_is_open = true;
+
+			//include tag
+			elseif( preg_match( '/\{include="([^"]*)"(?: cache="([^"]*)"){0,1}\}/', $html, $code ) ){
+				if (preg_match("/http/", $code[1])) {
+					$content = file_get_contents($code[1]);
+					$compiled_code .= $content;
+				} else {
+					//variables substitution
+					$include_var = $this->var_replace( $code[ 1 ], $left_delimiter = null, $right_delimiter = null, $php_left_delimiter = '".' , $php_right_delimiter = '."', $loop_level );
+
+                                        //get the folder of the actual template
+                                        $actual_folder = substr( $this->tpl['template_directory'], strlen(self::$tpl_dir) );
+
+                                        //get the included template
+                                        $include_template = $actual_folder . $include_var;
+
+                                        // reduce the path
+                                        $include_template = $this->reduce_path( $include_template );
+
+					// if the cache is active
+					if( isset($code[ 2 ]) ){
+
+                                                //include
+                                                $compiled_code .= '<?php $tpl = new '.get_called_class().';' .
+                                                                  'if( $cache = $tpl->cache( "'.$include_template.'" ) )' .
+								  '	echo $cache;' .
+								  'else{' .
+                                                                  '$tpl->assign( $this->var );' .
+                                                                  ( !$loop_level ? null : '$tpl->assign( "key", $key'.$loop_level.' ); $tpl->assign( "value", $value'.$loop_level.' );' ).
+                                                                  '$tpl->draw( "'.$include_template.'" );'.
+                                                                  '}' .
+                                                                  '?>';
+
+					}
+					else{
+                                                //include
+                                                $compiled_code .= '<?php $tpl = new '.get_called_class().';' .
+                                                                  '$tpl->assign( $this->var );' .
+                                                                  ( !$loop_level ? null : '$tpl->assign( "key", $key'.$loop_level.' ); $tpl->assign( "value", $value'.$loop_level.' );' ).
+                                                                  '$tpl->draw( "'.$include_template.'" );'.
+                                                                  '?>';
+
+					}
+				}
+			}
+
+	 		//loop
+			elseif( preg_match( '/\{loop(?: name){0,1}="\${0,1}([^"]*)"\}/', $html, $code ) ){
+
+	 			//increase the loop counter
+	 			$loop_level++;
+
+				//replace the variable in the loop
+				$var = $this->var_replace( '$' . $code[ 1 ], $tag_left_delimiter=null, $tag_right_delimiter=null, $php_left_delimiter=null, $php_right_delimiter=null, $loop_level-1 );
+
+				//loop variables
+				$counter = "\$counter$loop_level";       // count iteration
+				$key = "\$key$loop_level";               // key
+				$value = "\$value$loop_level";           // value
+
+				//loop code
+				$compiled_code .=  "<?php $counter=-1; if( isset($var) && is_array($var) && sizeof($var) ) foreach( $var as $key => $value ){ $counter++; ?>";
+
+			}
+
+			//close loop tag
+			elseif( strpos( $html, '{/loop}' ) !== FALSE ) {
+
+				//iterator
+				$counter = "\$counter$loop_level";
+
+				//decrease the loop counter
+				$loop_level--;
+
+				//close loop code
+				$compiled_code .=  "<?php } ?>";
+
+			}
+
+			//if
+			elseif( preg_match( '/\{if(?: condition){0,1}="([^"]*)"\}/', $html, $code ) ){
+
+				//increase open if counter (for intendation)
+				$open_if++;
+
+				//tag
+				$tag = $code[ 0 ];
+
+				//condition attribute
+				$condition = $code[ 1 ];
+
+				// check if there's any function disabled by black_list
+				$this->function_check( $tag );
+
+				//variable substitution into condition (no delimiter into the condition)
+				$parsed_condition = $this->var_replace( $condition, $tag_left_delimiter = null, $tag_right_delimiter = null, $php_left_delimiter = null, $php_right_delimiter = null, $loop_level );
+
+				//if code
+				$compiled_code .=   "<?php if( $parsed_condition ){ ?>";
+
+			}
+
+			//elseif
+			elseif( preg_match( '/\{elseif(?: condition){0,1}="([^"]*)"\}/', $html, $code ) ){
+
+				//tag
+				$tag = $code[ 0 ];
+
+				//condition attribute
+				$condition = $code[ 1 ];
+
+				//variable substitution into condition (no delimiter into the condition)
+				$parsed_condition = $this->var_replace( $condition, $tag_left_delimiter = null, $tag_right_delimiter = null, $php_left_delimiter = null, $php_right_delimiter = null, $loop_level );
+
+				//elseif code
+				$compiled_code .=   "<?php }elseif( $parsed_condition ){ ?>";
+			}
+
+			//else
+			elseif( strpos( $html, '{else}' ) !== FALSE ) {
+
+				//else code
+				$compiled_code .=   '<?php }else{ ?>';
+
+			}
+
+			//close if tag
+			elseif( strpos( $html, '{/if}' ) !== FALSE ) {
+
+				//decrease if counter
+				$open_if--;
+
+				// close if code
+				$compiled_code .=   '<?php } ?>';
+
+			}
+
+			//function
+			elseif( preg_match( '/\{function="(\w*)(.*?)"\}/', $html, $code ) ){
+
+				//tag
+				$tag = $code[ 0 ];
+
+				//function
+				$function = $code[ 1 ];
+
+				// check if there's any function disabled by black_list
+				$this->function_check( $tag );
+
+				if( empty( $code[ 2 ] ) )
+					$parsed_function = $function . "()";
+				else
+					// parse the function
+					$parsed_function = $function . $this->var_replace( $code[ 2 ], $tag_left_delimiter = null, $tag_right_delimiter = null, $php_left_delimiter = null, $php_right_delimiter = null, $loop_level );
+				
+				//if code
+				$compiled_code .=   "<?php echo $parsed_function; ?>";
+			}
+
+			// show all vars
+			elseif ( strpos( $html, '{$template_info}' ) !== FALSE ) {
+
+				//tag
+				$tag  = '{$template_info}';
+
+				//if code
+				$compiled_code .=   '<?php echo "<pre>"; print_r( $this->var ); echo "</pre>"; ?>';
+			}
+
+
+			//all html code
+			else{
+
+				//variables substitution (es. {$title})
+				$html = $this->var_replace( $html, $left_delimiter = '\{', $right_delimiter = '\}', $php_left_delimiter = '<?php ', $php_right_delimiter = ';?>', $loop_level, $echo = true );
+				//const substitution (es. {#CONST#})
+				$html = $this->const_replace( $html, $left_delimiter = '\{', $right_delimiter = '\}', $php_left_delimiter = '<?php ', $php_right_delimiter = ';?>', $loop_level, $echo = true );
+				//functions substitution (es. {"string"|functions})
+				$compiled_code .= $this->func_replace( $html, $left_delimiter = '\{', $right_delimiter = '\}', $php_left_delimiter = '<?php ', $php_right_delimiter = ';?>', $loop_level, $echo = true );
+			}
+		}
+
+		if( $open_if > 0 ) {
+			$e = new RainTpl_SyntaxException('Error! You need to close an {if} tag in ' . $this->tpl['tpl_filename'] . ' template');
+			throw $e->setTemplateFile($this->tpl['tpl_filename']);
+		}
+		return $compiled_code;
+	}
+	
+	
+	/**
+	 * Reduce a path, eg. www/library/../filepath//file => www/filepath/file
+	 * @param type $path
+	 * @return type
+	 */
+	protected function reduce_path( $path ){
+            $path = str_replace( "://", "@not_replace@", $path );
+            $path = preg_replace( "#(/+)#", "/", $path );
+            $path = preg_replace( "#(/\./+)#", "/", $path );
+            $path = str_replace( "@not_replace@", "://", $path );
+            
+            while( preg_match( '#\.\./#', $path ) ){
+                $path = preg_replace('#\w+/\.\./#', '', $path );
+            }
+            return $path;
+	}
+
+
+
+	/**
+	 * replace the path of image src, link href and a href.
+	 * url => template_dir/url
+	 * url# => url
+	 * http://url => http://url
+	 *
+	 * @param string $html
+	 * @return string html sostituito
+	 */
+	protected function path_replace( $html, $tpl_basedir ){
+
+		if( self::$path_replace ){
+
+			$tpl_dir = self::$base_url . self::$tpl_dir . $tpl_basedir;
+			
+			// reduce the path
+			$path = $this->reduce_path($tpl_dir);
+
+			$exp = $sub = array();
+
+			if( in_array( "img", self::$path_replace_list ) ){
+				$exp = array( '/<img(.*?)src=(?:")(http|https)\:\/\/([^"]+?)(?:")/i', '/<img(.*?)src=(?:")([^"]+?)#(?:")/i', '/<img(.*?)src="(.*?)"/', '/<img(.*?)src=(?:\@)([^"]+?)(?:\@)/i' );
+				$sub = array( '<img$1src=@$2://$3@', '<img$1src=@$2@', '<img$1src="' . $path . '$2"', '<img$1src="$2"' );
+			}
+
+			if( in_array( "script", self::$path_replace_list ) ){
+				$exp = array_merge( $exp , array( '/<script(.*?)src=(?:")(http|https)\:\/\/([^"]+?)(?:")/i', '/<script(.*?)src=(?:")([^"]+?)#(?:")/i', '/<script(.*?)src="(.*?)"/', '/<script(.*?)src=(?:\@)([^"]+?)(?:\@)/i' ) );
+				$sub = array_merge( $sub , array( '<script$1src=@$2://$3@', '<script$1src=@$2@', '<script$1src="' . $path . '$2"', '<script$1src="$2"' ) );
+			}
+
+			if( in_array( "link", self::$path_replace_list ) ){
+				$exp = array_merge( $exp , array( '/<link(.*?)href=(?:")(http|https)\:\/\/([^"]+?)(?:")/i', '/<link(.*?)href=(?:")([^"]+?)#(?:")/i', '/<link(.*?)href="(.*?)"/', '/<link(.*?)href=(?:\@)([^"]+?)(?:\@)/i' ) );
+				$sub = array_merge( $sub , array( '<link$1href=@$2://$3@', '<link$1href=@$2@' , '<link$1href="' . $path . '$2"', '<link$1href="$2"' ) );
+			}
+
+			if( in_array( "a", self::$path_replace_list ) ){
+				$exp = array_merge( $exp , array( '/<a(.*?)href=(?:")(http\:\/\/|https\:\/\/|javascript:|mailto:)([^"]+?)(?:")/i', '/<a(.*?)href="(.*?)"/', '/<a(.*?)href=(?:\@)([^"]+?)(?:\@)/i'  ) );
+				$sub = array_merge( $sub , array( '<a$1href=@$2$3@', '<a$1href="' . self::$base_url . '$2"', '<a$1href="$2"' ) );
+			}
+
+			if( in_array( "input", self::$path_replace_list ) ){
+				$exp = array_merge( $exp , array( '/<input(.*?)src=(?:")(http|https)\:\/\/([^"]+?)(?:")/i', '/<input(.*?)src=(?:")([^"]+?)#(?:")/i', '/<input(.*?)src="(.*?)"/', '/<input(.*?)src=(?:\@)([^"]+?)(?:\@)/i' ) );
+				$sub = array_merge( $sub , array( '<input$1src=@$2://$3@', '<input$1src=@$2@', '<input$1src="' . $path . '$2"', '<input$1src="$2"' ) );
+			}
+
+			return preg_replace( $exp, $sub, $html );
+
+		}
+		else
+			return $html;
+
+	}
+
+
+
+
+
+	// replace const
+	function const_replace( $html, $tag_left_delimiter, $tag_right_delimiter, $php_left_delimiter = null, $php_right_delimiter = null, $loop_level = null, $echo = null ){
+		// const
+		return preg_replace( '/\{\#(\w+)\#{0,1}\}/', $php_left_delimiter . ( $echo ? " echo " : null ) . '\\1' . $php_right_delimiter, $html );
+	}
+
+
+
+	// replace functions/modifiers on constants and strings
+	function func_replace( $html, $tag_left_delimiter, $tag_right_delimiter, $php_left_delimiter = null, $php_right_delimiter = null, $loop_level = null, $echo = null ){
+
+		preg_match_all( '/' . '\{\#{0,1}(\"{0,1}.*?\"{0,1})(\|\w.*?)\#{0,1}\}' . '/', $html, $matches );
+
+		for( $i=0, $n=count($matches[0]); $i<$n; $i++ ){
+
+			//complete tag ex: {$news.title|substr:0,100}
+			$tag = $matches[ 0 ][ $i ];
+
+			//variable name ex: news.title
+			$var = $matches[ 1 ][ $i ];
+
+			//function and parameters associate to the variable ex: substr:0,100
+			$extra_var = $matches[ 2 ][ $i ];
+
+			// check if there's any function disabled by black_list
+			$this->function_check( $tag );
+
+			$extra_var = $this->var_replace( $extra_var, null, null, null, null, $loop_level );
+            
+
+			// check if there's an operator = in the variable tags, if there's this is an initialization so it will not output any value
+			$is_init_variable = preg_match( "/^(\s*?)\=[^=](.*?)$/", $extra_var );
+
+			//function associate to variable
+			$function_var = ( $extra_var and $extra_var[0] == '|') ? substr( $extra_var, 1 ) : null;
+
+			//variable path split array (ex. $news.title o $news[title]) or object (ex. $news->title)
+			$temp = preg_split( "/\.|\[|\-\>/", $var );
+
+			//variable name
+			$var_name = $temp[ 0 ];
+
+			//variable path
+			$variable_path = substr( $var, strlen( $var_name ) );
+
+			//parentesis transform [ e ] in [" e in "]
+			$variable_path = str_replace( '[', '["', $variable_path );
+			$variable_path = str_replace( ']', '"]', $variable_path );
+
+			//transform .$variable in ["$variable"]
+			$variable_path = preg_replace('/\.\$(\w+)/', '["$\\1"]', $variable_path );
+
+			//transform [variable] in ["variable"]
+			$variable_path = preg_replace('/\.(\w+)/', '["\\1"]', $variable_path );
+
+			//if there's a function
+			if( $function_var ){
+                
+                // check if there's a function or a static method and separate, function by parameters
+				$function_var = str_replace("::", "@double_dot@", $function_var );
+
+                // get the position of the first :
+                if( $dot_position = strpos( $function_var, ":" ) ){
+
+                    // get the function and the parameters
+                    $function = substr( $function_var, 0, $dot_position );
+                    $params = substr( $function_var, $dot_position+1 );
+
+                }
+                else{
+
+                    //get the function
+                    $function = str_replace( "@double_dot@", "::", $function_var );
+                    $params = null;
+
+                }
+
+                // replace back the @double_dot@ with ::
+                $function = str_replace( "@double_dot@", "::", $function );
+                $params = str_replace( "@double_dot@", "::", $params );
+
+
+			}
+			else
+				$function = $params = null;
+
+			$php_var = $var_name . $variable_path;
+
+			// compile the variable for php
+			if( isset( $function ) ){
+				if( $php_var )
+					$php_var = $php_left_delimiter . ( !$is_init_variable && $echo ? 'echo ' : null ) . ( $params ? "( $function( $php_var, $params ) )" : "$function( $php_var )" ) . $php_right_delimiter;
+				else
+					$php_var = $php_left_delimiter . ( !$is_init_variable && $echo ? 'echo ' : null ) . ( $params ? "( $function( $params ) )" : "$function()" ) . $php_right_delimiter;
+			}
+			else
+				$php_var = $php_left_delimiter . ( !$is_init_variable && $echo ? 'echo ' : null ) . $php_var . $extra_var . $php_right_delimiter;
+
+			$html = str_replace( $tag, $php_var, $html );
+
+		}
+
+		return $html;
+
+	}
+
+
+
+	function var_replace( $html, $tag_left_delimiter, $tag_right_delimiter, $php_left_delimiter = null, $php_right_delimiter = null, $loop_level = null, $echo = null ){
+
+		//all variables
+		if( preg_match_all( '/' . $tag_left_delimiter . '\$(\w+(?:\.\${0,1}[A-Za-z0-9_]+)*(?:(?:\[\${0,1}[A-Za-z0-9_]+\])|(?:\-\>\${0,1}[A-Za-z0-9_]+))*)(.*?)' . $tag_right_delimiter . '/', $html, $matches ) ){
+
+                    for( $parsed=array(), $i=0, $n=count($matches[0]); $i<$n; $i++ )
+                        $parsed[$matches[0][$i]] = array('var'=>$matches[1][$i],'extra_var'=>$matches[2][$i]);
+
+                    foreach( $parsed as $tag => $array ){
+
+                            //variable name ex: news.title
+                            $var = $array['var'];
+
+                            //function and parameters associate to the variable ex: substr:0,100
+                            $extra_var = $array['extra_var'];
+
+                            // check if there's any function disabled by black_list
+                            $this->function_check( $tag );
+
+                            $extra_var = $this->var_replace( $extra_var, null, null, null, null, $loop_level );
+
+                            // check if there's an operator = in the variable tags, if there's this is an initialization so it will not output any value
+                            $is_init_variable = preg_match( "/^[a-z_A-Z\.\[\](\-\>)]*=[^=]*$/", $extra_var );
+                            
+                            //function associate to variable
+                            $function_var = ( $extra_var and $extra_var[0] == '|') ? substr( $extra_var, 1 ) : null;
+
+                            //variable path split array (ex. $news.title o $news[title]) or object (ex. $news->title)
+                            $temp = preg_split( "/\.|\[|\-\>/", $var );
+
+                            //variable name
+                            $var_name = $temp[ 0 ];
+
+                            //variable path
+                            $variable_path = substr( $var, strlen( $var_name ) );
+
+                            //parentesis transform [ e ] in [" e in "]
+                            $variable_path = str_replace( '[', '["', $variable_path );
+                            $variable_path = str_replace( ']', '"]', $variable_path );
+
+                            //transform .$variable in ["$variable"] and .variable in ["variable"]
+                            $variable_path = preg_replace('/\.(\${0,1}\w+)/', '["\\1"]', $variable_path );
+                            
+                            // if is an assignment also assign the variable to $this->var['value']
+                            if( $is_init_variable )
+                                $extra_var = "=\$this->var['{$var_name}']{$variable_path}" . $extra_var;
+
+                                
+
+                            //if there's a function
+                            if( $function_var ){
+                                
+                                    // check if there's a function or a static method and separate, function by parameters
+                                    $function_var = str_replace("::", "@double_dot@", $function_var );
+
+
+                                    // get the position of the first :
+                                    if( $dot_position = strpos( $function_var, ":" ) ){
+
+                                        // get the function and the parameters
+                                        $function = substr( $function_var, 0, $dot_position );
+                                        $params = substr( $function_var, $dot_position+1 );
+
+                                    }
+                                    else{
+
+                                        //get the function
+                                        $function = str_replace( "@double_dot@", "::", $function_var );
+                                        $params = null;
+
+                                    }
+
+                                    // replace back the @double_dot@ with ::
+                                    $function = str_replace( "@double_dot@", "::", $function );
+                                    $params = str_replace( "@double_dot@", "::", $params );
+                            }
+                            else
+                                    $function = $params = null;
+
+                            //if it is inside a loop
+                            if( $loop_level ){
+                                    //verify the variable name
+                                    if( $var_name == 'key' )
+                                            $php_var = '$key' . $loop_level;
+                                    elseif( $var_name == 'value' )
+                                            $php_var = '$value' . $loop_level . $variable_path;
+                                    elseif( $var_name == 'counter' )
+                                            $php_var = '$counter' . $loop_level;
+                                    else
+                                            $php_var = '$' . $var_name . $variable_path;
+                            }else
+                                    $php_var = '$' . $var_name . $variable_path;
+                            
+                            // compile the variable for php
+                            if( isset( $function ) )
+                                    $php_var = $php_left_delimiter . ( !$is_init_variable && $echo ? 'echo ' : null ) . ( $params ? "( $function( $php_var, $params ) )" : "$function( $php_var )" ) . $php_right_delimiter;
+                            else
+                                    $php_var = $php_left_delimiter . ( !$is_init_variable && $echo ? 'echo ' : null ) . $php_var . $extra_var . $php_right_delimiter;
+                            
+                            $html = str_replace( $tag, $php_var, $html );
+
+
+                    }
+                }
+
+		return $html;
+	}
+
+
+
+	/**
+	 * Check if function is in black list (sandbox)
+	 *
+	 * @param string $code
+	 * @param string $tag
+	 */
+	protected function function_check( $code ){
+
+		$preg = '#(\W|\s)' . implode( '(\W|\s)|(\W|\s)', self::$black_list ) . '(\W|\s)#';
+
+		// check if the function is in the black list (or not in white list)
+		if( count(self::$black_list) && preg_match( $preg, $code, $match ) ){
+
+			// find the line of the error
+			$line = 0;
+			$rows=explode("\n",$this->tpl['source']);
+			while( !strpos($rows[$line],$code) )
+				$line++;
+
+			// stop the execution of the script
+			$e = new RainTpl_SyntaxException('Unallowed syntax in ' . $this->tpl['tpl_filename'] . ' template');
+			throw $e->setTemplateFile($this->tpl['tpl_filename'])
+				->setTag($code)
+				->setTemplateLine($line);
+		}
+
+	}
+
+	/**
+	 * Prints debug info about exception or passes it further if debug is disabled.
+	 *
+	 * @param RainTpl_Exception $e
+	 * @return string
+	 */
+	protected function printDebug(RainTpl_Exception $e){
+		if (!self::$debug) {
+			throw $e;
+		}
+		$output = sprintf('<h2>Exception: %s</h2><h3>%s</h3><p>template: %s</p>',
+			get_class($e),
+			$e->getMessage(),
+			$e->getTemplateFile()
+		);
+		if ($e instanceof RainTpl_SyntaxException) {
+			if (null != $e->getTemplateLine()) {
+				$output .= '<p>line: ' . $e->getTemplateLine() . '</p>';
+			}
+			if (null != $e->getTag()) {
+				$output .= '<p>in tag: ' . htmlspecialchars($e->getTag()) . '</p>';
+			}
+			if (null != $e->getTemplateLine() && null != $e->getTag()) {
+				$rows=explode("\n",  htmlspecialchars($this->tpl['source']));
+				$rows[$e->getTemplateLine()] = '<font color=red>' . $rows[$e->getTemplateLine()] . '</font>';
+				$output .= '<h3>template code</h3>' . implode('<br />', $rows) . '</pre>';
+			}
+		}
+		$output .= sprintf('<h3>trace</h3><p>In %s on line %d</p><pre>%s</pre>',
+			$e->getFile(), $e->getLine(),
+			nl2br(htmlspecialchars($e->getTraceAsString()))
+		);
+		return $output;
+	}
+}
+
+
+/**
+ * Basic Rain tpl exception.
+ */
+class RainTpl_Exception extends Exception{
+	/**
+	 * Path of template file with error.
+	 */
+	protected $templateFile = '';
+
+	/**
+	 * Returns path of template file with error.
+	 *
+	 * @return string
+	 */
+	public function getTemplateFile()
+	{
+		return $this->templateFile;
+	}
+
+	/**
+	 * Sets path of template file with error.
+	 *
+	 * @param string $templateFile
+	 * @return RainTpl_Exception
+	 */
+	public function setTemplateFile($templateFile)
+	{
+		$this->templateFile = (string) $templateFile;
+		return $this;
+	}
+}
+
+/**
+ * Exception thrown when template file does not exists.
+ */
+class RainTpl_NotFoundException extends RainTpl_Exception{
+}
+
+/**
+ * Exception thrown when syntax error occurs.
+ */
+class RainTpl_SyntaxException extends RainTpl_Exception{
+	/**
+	 * Line in template file where error has occured.
+	 *
+	 * @var int | null
+	 */
+	protected $templateLine = null;
+
+	/**
+	 * Tag which caused an error.
+	 *
+	 * @var string | null
+	 */
+	protected $tag = null;
+
+	/**
+	 * Returns line in template file where error has occured
+	 * or null if line is not defined.
+	 *
+	 * @return int | null
+	 */
+	public function getTemplateLine()
+	{
+		return $this->templateLine;
+	}
+
+	/**
+	 * Sets  line in template file where error has occured.
+	 *
+	 * @param int $templateLine
+	 * @return RainTpl_SyntaxException
+	 */
+	public function setTemplateLine($templateLine)
+	{
+		$this->templateLine = (int) $templateLine;
+		return $this;
+	}
+
+	/**
+	 * Returns tag which caused an error.
+	 *
+	 * @return string
+	 */
+	public function getTag()
+	{
+		return $this->tag;
+	}
+
+	/**
+	 * Sets tag which caused an error.
+	 *
+	 * @param string $tag
+	 * @return RainTpl_SyntaxException
+	 */
+	public function setTag($tag)
+	{
+		$this->tag = (string) $tag;
+		return $this;
+	}
+}
+
+// -- end

+ 411 - 0
action.php

@@ -0,0 +1,411 @@
+<?php
+if (isset($_GET['action']) && $_GET['action'] == 'KNOCK_KNOCK_YANA') exit('1');
+
+
+if(!ini_get('safe_mode')) @set_time_limit(0);
+
+require_once(dirname(__FILE__).'/common.php');
+
+
+if(php_sapi_name() == 'cli'){
+	$_['action'] = $_SERVER['argv'][1];	
+}
+
+$response = array();
+
+Plugin::callHook("action_pre_case", array(&$_,$myUser));
+
+
+
+if(!$myUser && isset($_['token'])){
+	$userManager = new User();
+	$myUser = $userManager->load(array('token'=>$_['token']));
+	if(isset($myUser) && $myUser!=false)
+		$myUser->loadRight();
+}
+$myUser = (!$myUser?new User():$myUser);
+
+//Execution du code en fonction de l'action
+switch ($_['action']){
+
+	case 'login':
+	global $conf;
+	$user = $userManager->exist($_['login'],$_['password']);
+	$error = '?init=1';
+	if($user==false){
+		$error .= '&error='.urlencode('le compte spécifié est inexistant');
+	}else{
+		$_SESSION['currentUser'] = serialize($user);
+	
+
+	if(isset($_['rememberMe'])){	
+		$expire_time = time() + $conf->get('COOKIE_LIFETIME')*86400; //Jour en secondes
+		
+		//On crée un cookie dans la bd uniquement si aucun autre cookie n'existe sinon
+		//On rend inutilisable le cookie utilisé par un autre navigateur
+		//On ne veut que cela soit le cas uniquement si on clique sur déconnexion (et que l'on a demandé Se souvenir de moi)
+		$actual_cookie = $user->getCookie();
+		if ($actual_cookie == "")
+		{
+		$cookie_token = sha1(time().rand(0,1000));
+		$user->setCookie($cookie_token);
+		$user->save();
+		}
+		else
+		{
+			$cookie_token = $actual_cookie;
+		}	
+		Functions::makeCookie($conf->get('COOKIE_NAME'),$cookie_token,$expire_time);
+	}
+	}
+	
+	header('location: ./index.php'.$error);	
+	break;
+
+	case 'GET_TOKEN':
+		$user = $userManager->load(array('login'=>$_['login'],'password'=>sha1(md5($_['password']))));
+		$response['token'] = $user->getToken();
+		echo json_encode($response);
+	break;
+
+	
+	
+	case 'user_add_user':
+	$right_toverify = isset($_['id']) ? 'u' : 'c';
+	if($myUser->can('user',$right_toverify)){
+	$user = new User();
+	//Si modification on charge la ligne au lieu de la créer
+	if ($right_toverify == "u"){$user = $user->load(array("id"=>$_['id']));}
+	$user->setMail($_['mailUser']);
+	$user->setName($_['nameUser']);
+	$user->setFirstName($_['firstNameUser']);
+	$user->setPassword($_['passwordUser']);
+	$user->setLogin($_['loginUser']);
+	$user->setRank($_['rankUser']);
+	$user->setState(1);
+	$user->setToken(sha1(time().rand(0,1000)));
+	$user->save();
+	Functions::goback("setting","user");
+}
+else
+{
+	Functions::goback("setting","user","&error=Vous n'avez pas le droit de faire ça!");
+}
+	break;
+
+	case 'delete_user':
+	if(!$myUser->can('user','d')) exit('ERREUR: Permissions insuffisantes.');
+	$userManager = new User();
+	$NbUsers = $userManager->rowCount();
+
+	if(isset($_['id']) && $NbUsers > 1){
+		$userManager->delete(array('id'=>$_['id']));
+		Functions::goback("setting","user");
+	}
+	else
+	{
+		Functions::goback("setting","user","&error=Impossible de supprimer le dernier utilisateur.");
+	}
+	break;
+
+
+	case 'access_delete_rank':
+	if(!$myUser->can('configuration','d')) exit('ERREUR: Permissions insuffisantes.');
+	$rankManager = new Rank();
+	
+	$Nbrank = $rankManager->rowCount();
+
+	if(isset($_['id']) && $Nbrank > 1){
+		$rankManager->delete(array('id'=>$_['id']));
+		Functions::goback("setting","access");
+		header('location:setting.php?section=access');
+	}
+	else
+	{
+		Functions::goback("setting","access","&error=Impossible de supprimer le dernier rang.");
+	}
+	break;
+
+	case 'access_add_rank':
+	$right_toverify = isset($_['id']) ? 'u' : 'c';
+	if(!$myUser->can('configuration',$right_toverify)) exit('ERREUR: Permissions insuffisantes.');
+	$rank = new Rank();
+	if ($right_toverify == "u"){$rank = $rank->load(array("id"=>$_['id']));}
+	$rank->setLabel($_['labelRank']);
+	$rank->setDescription($_['descriptionRank']);
+	$rank->save();
+	Functions::goback("setting","access");
+	break;
+
+	case 'set_rank_access':
+	if(!$myUser->can('configuration','c')) exit('ERREUR: Permissions insuffisantes.');
+	$right = new Right();
+
+	$right = $right->load(array('section'=>$_['section'],'rank'=>$_['rank']));
+
+	$right = (!$right?new Right():$right);
+
+	$right->setSection($_['section']);
+
+	$_['state'] = ($_['state']==1?true:false);
+
+	switch($_['access']){
+		case 'c':
+		$right->setCreate($_['state']);
+		break;
+		case 'r':
+		$right->setRead($_['state']);
+		break;
+		case 'u':
+		$right->setUpdate($_['state']);
+		break;
+		case 'd':
+		$right->setDelete($_['state']);
+		break;
+	}
+	$right->setRank($_['rank']);
+	$right->save();
+
+	break;
+
+	if(!$myUser->can('configuration','d')) exit('ERREUR: Permissions insuffisantes.');
+	case 'access_delete_right':
+	$rankManager = new Right();
+	 
+	$rankManager->delete(array('id'=>$_['id']));
+	Functions::goback("setting","right","&id=".$_['rank']);
+	break;
+
+	case 'logout':
+	global $conf;
+	
+	//Détruire le cookie uniquement s'il existe sur cette ordinateur
+	//Afin de le garder dans la BD pour les autres ordinateurs/navigateurs
+	if(isset($_COOKIE[$conf->get('COOKIE_NAME')])){
+	$user = new User();
+	$user = $userManager->load(array("id"=>$myUser->getId()));
+	$user->setCookie("");
+	$user->save();
+	Functions::destroyCookie($conf->get('COOKIE_NAME'));
+	}
+
+	$_SESSION = array();
+	session_unset();
+	session_destroy();
+	
+
+
+	Functions::goback(" ./index");
+	break;
+
+	case 'save_sentence':
+		global $conf;
+		$conf->put('last_sentence',$_['sentence']);
+	break;
+
+	case 'ENABLE_DASHBOARD':
+		Plugin::enabled('dashboard-dashboard');
+		Plugin::enabled('dashboard-monitoring-dashboard-monitoring');
+		header('location: index.php');
+	break;
+
+	case 'changePluginState':
+	if($myUser==false) exit('Vous devez vous connecter pour cette action.');
+	if(!$myUser->can('plugin','u')) exit('ERREUR: Permissions insuffisantes.');
+	if($_['state']=='0'){
+		Plugin::enabled($_['plugin']);
+	}else{
+		Plugin::disabled($_['plugin']);
+	}
+	Functions::goback("setting","plugin","&block=".$_['block']);
+	break;
+
+	case 'crontab':
+		Plugin::callHook("cron", array());
+	break;
+	
+	
+		
+	
+
+
+	case 'GET_SPEECH_COMMAND':
+	if($myUser->getId()=='') exit('{"error":"invalid or missing token"}');
+	if(!$myUser->can('vocal','r')) exit('{"error":"insufficient permissions for this account"}');
+	
+	list($host,$port) = explode(':',$_SERVER['HTTP_HOST']);
+	$actionUrl = 'http://'.$host.':'.$_SERVER['SERVER_PORT'].$_SERVER['REQUEST_URI'];
+	$actionUrl = substr($actionUrl,0,strpos($actionUrl , '?'));
+	
+	Plugin::callHook("vocal_command", array(&$response,$actionUrl));
+
+	$json = json_encode($response);
+	echo ($json=='[]'?'{}':$json);
+	break;
+
+	case 'GET_EVENT':
+	if($myUser->getId()=='') exit('{"error":"invalid or missing token"}');
+	if(!$myUser->can('vocal','r')) exit('{"error":"insufficient permissions for this account"}');
+	$response = array('responses'=>array());
+	Plugin::callHook("get_event", array(&$response));
+
+	$checker = (isset($_['checker'])?$_['checker']:'client');
+
+	$eventManager = new Event();
+	$events = $eventManager->loadAll(array(),'id');
+	
+
+	$time = date('i-H-d-m-Y');
+	list($minut,$hour,$day,$month,$year) = explode('-',$time);
+
+	foreach ($events as $event) {
+
+		if(in_array($checker,$event->getRecipients()) && $event->getState()=='1'){
+			if( 
+			($event->getMinut() == '*' || in_array($minut,explode(',',$event->getMinut())) ) &&
+			($event->getHour() == '*' || in_array($hour,explode(',',$event->getHour())) ) &&
+			($event->getDay()== '*' || in_array($day,explode(',',$event->getDay())) ) &&
+			($event->getMonth() == '*' || in_array($month,explode(',',$event->getMonth())) ) &&
+			($event->getYear() == '*' || in_array($year,explode(',',$event->getYear())) ) 
+			){
+				
+				if($event->getRepeat()!=$time){
+					if(in_array($checker, $event->getRecipients())){
+						$event->setRepeat($time);
+						$response['responses'][]= $event->getContent();
+
+						//Le serveur ne peux qu'executer des commandes programme
+						if($checker=='server'){
+							$content = $event->getContent();
+							switch($content['type']){
+								case 'command':
+									exec(htmlspecialchars_decode($content['program']));
+								break;
+						
+								case 'gpio':
+									foreach(explode(',',$content['gpios']) as $info){
+										list($gpio,$state) = explode(':',$info);
+										exec('gpio mode '.$gpio.' out');
+										exec('gpio write '.$gpio.' '.$state);
+
+									}
+								break;
+								
+							}
+						}
+
+						$event->save();
+					}
+				}
+			}
+		}
+
+
+
+
+	}
+	
+		
+
+	$json = json_encode($response);
+	echo ($json=='[]'?'{}':$json);
+	break;
+
+
+	case 'installPlugin':
+		try{
+			if($myUser==false) throw new Exception('Vous devez vous connecter pour cette action.');
+
+			$tempZipName = 'plugins'.SLASH.md5(microtime());
+			echo '<br/>Téléchargement du plugin...';
+			file_put_contents($tempZipName,file_get_contents(urldecode($_['zip'])));
+			if(!file_exists($tempZipName)) throw new Exception("Echec du téléchargement");
+			echo '<br/>Plugin téléchargé <span class="label label-success">OK</span>';
+			echo '<br/>Extraction du plugin...';
+			$zip = new ZipArchive;
+			$res = $zip->open($tempZipName);
+			if ($res !== TRUE)  throw new Exception("Echec de l\'extraction");
+				$tempZipFolder = $tempZipName.'_';
+				$zip->extractTo($tempZipFolder);
+				$zip->close();
+				echo '<br/>Plugin extrait '.$tempZipFolder.' <span class="label label-success">OK</span>';
+				
+
+				$i = 0;
+				$pluginName = array();
+				while(count($pluginName)==0 && $i<10){
+					$pluginName = glob($tempZipFolder.SLASH.(  str_repeat('*'.SLASH, $i)   ).'*.plugin*.php');
+					$i++;
+				}
+				
+						
+				if(count($pluginName)==0) throw new Exception("Plugin invalide, fichier principal manquant");
+
+				$pluginName = str_replace(array($tempZipFolder.'/','.enabled','.disabled','.plugin','.php'),'',$pluginName[0]);
+				
+				$finalPath = __DIR__.SLASH.'plugins'.SLASH.basename(dirname($pluginName));	
+				if(file_exists($finalPath)){
+					echo '<br/>Plugin déjà installé, il sera écrasé par la derniere version <span class="label label-info">OK</span>';
+					Functions::rmFullDir($finalPath);
+				}
+
+				echo '<br/>Renommage...';
+		
+				if(rename(__DIR__.SLASH.dirname($pluginName),$finalPath )){
+					echo '<br/>Plugin installé, <span class="label label-info">pensez à l\'activer</span>';
+				}else{
+					//Functions::rmFullDir(__DIR__.SLASH.$tempZipFolder);
+					echo '<br/>Impossible de renommer le plugin '.__DIR__.SLASH.$tempZipFolder.' <span class="label label-error">Erreur</span>';
+				}
+
+				unlink($tempZipName);
+				if(file_exists($tempZipFolder)) Functions::rmFullDir($tempZipFolder);
+			
+		}catch(Exception $e){
+			if($tempZipFolder!=null && file_exists($tempZipFolder)) Functions::rmFullDir($tempZipFolder);
+			if($tempZipName!=null && file_exists($tempZipName))  unlink($tempZipName);
+			echo '<br/>'.$e->getMessage().' <span class="label label-error">Erreur</span>';
+		}
+	break;
+
+	case 'CHANGE_GPIO_STATE':
+		if($myUser==false) {
+			exit('Vous devez vous connecter pour cette action.');
+		}
+		else {
+			Gpio::write($_["pin"],$_["state"],true);
+		}
+	break;
+
+	case 'GPIO_HAS_CHANGED':
+		
+		list($program,$action,$pin,$state) = $_SERVER['argv'];
+		Gpio::emit($pin,$state);
+	break;
+
+
+	// Gestion des interfaces de seconde génération
+	case 'ADD_CLIENT':
+		Action::write(function($_,&$response){
+			global $myUser,$conf,$client;
+			if(!isset($_SERVER['argv'][2])) throw new Exception("Type client invalide");
+			
+			
+			file_put_contents('filename', $_SERVER['argv'][2]);
+			
+			
+
+
+			
+		});
+	break;
+
+
+
+	default:
+		Plugin::callHook("action_post_case", array());
+	break;
+}
+
+
+?>

+ 31 - 0
api/index.php

@@ -0,0 +1,31 @@
+<?php
+header('Content-Type: application/json; charset=utf-8');
+require_once(dirname(__FILE__).DIRECTORY_SEPARATOR.'..'.DIRECTORY_SEPARATOR.'common.php');
+
+$response = array();
+try{
+	if(!$myUser){
+		if(isset($_['token'])){
+			$userManager = new User();
+			$myUser = $userManager->load(array('token'=>$_['token']));
+			if(isset($myUser) && $myUser!=false)
+				$myUser->loadRight();
+		}
+		if(isset($_['login'])){
+			$userManager = new User();
+			$myUser = $userManager->load(array('login'=>$_['login'],'password'=>$_['password']));
+			if(!$myUser) throw new Exception('Mauvais identifiant ou mot de passe');
+			$myUser->loadRight();
+		}
+	}
+	$myUser = (!$myUser?new User():$myUser);
+	Plugin::callHook("api", array(&$_,&$response));
+}catch(Exception $e){
+	$response['error'] = Personality::response('WORRY_EMOTION').' : '.$e->getMessage();
+}
+$response = json_encode($response);
+if(isset($_['callback']))
+	$response = $_['callback'].'('.$response.');';
+
+echo $response;
+?>

+ 18 - 0
apigen.neon

@@ -0,0 +1,18 @@
+source:
+	- classes
+	
+destination: doc
+charset:
+	- UTF-8
+	
+todo: true
+download: true
+title: Yana documentation technique
+groups: none
+autocomplete:
+	- classes
+	- constants
+	- functions
+	- methods
+	- properties
+	

BIN
apigen.phar


+ 38 - 0
classes/Action.class.php

@@ -0,0 +1,38 @@
+<?php
+/**
+* Execute an action (request which no need html view response: ajax,json etc...) and manage automatically
+* access rights, exceptions and json response.
+* @author Idleman
+* @category Request
+* @license cc by nc sa
+*/
+	class Action{
+		/**
+		* Execute an action 
+		* #### Example
+		* ```php
+		* Action::write(function($_,&$response){ 
+		*	$response['custom'] = 'hello world!'; 
+		* },array('user'=>'u','plugin'=>'d')); //User must have user update right and delete plugin right to perform this action
+		* ```
+		* @param function action/code to execute
+		* @param array Array wich contain right to execute action
+		* @return print json response
+		*/
+		public static function write($f,$p = array()){
+			global $myUser,$_,$conf;
+			header('content-type:application/json');
+				
+			$response = array('errors' => array());
+			try{
+				foreach ($p as $section => $right) 
+					if(!$myUser->can($section,$right)) throw new Exception('permission denied');
+				
+				$f($_,$response);
+			}catch(Exception $e){
+				$response['errors'][] = $e->getMessage();
+			}
+			echo json_encode($response);
+		}
+	}
+?>

+ 87 - 0
classes/Client.class.php

@@ -0,0 +1,87 @@
+<?php
+
+
+class Client {
+	public $socket;
+
+
+    public   function  connect(){ 
+        
+        $this->socket = socket_create(AF_INET, SOCK_STREAM, SOL_TCP);
+        $response = '';
+        if ($this->socket !== false) {
+            if (!socket_set_option($this->socket, SOL_SOCKET, SO_REUSEADDR, 1)) 
+                throw new Exception(utf8_encode(socket_strerror(socket_last_error($this->socket))));
+                
+            $result = @socket_connect( $this->socket, '127.0.0.1', 9999);
+            if(!$result){
+                $this->socket = null;
+                throw new Exception('Erreur connexion au serveur socket depuis yana-server, le serveur est il allumé ? '.utf8_encode(socket_strerror(socket_last_error())));
+            }
+        }
+
+    }
+
+    public   function  disconnect(){ 
+        if($this->socket==null) return;
+        socket_shutdown($this->socket);
+        socket_close($this->socket);
+        $this->socket = null;
+    }
+
+	public  function send($msg,$receive = false){ 
+        if($this->socket==null) return;
+            $in = json_encode($msg);
+            $in .= '<EOF>';
+            socket_write($this->socket, $in, strlen($in));
+           
+            if(!$receive) return $in;
+            $in = '';
+            $start = time();
+            $go = false;
+            socket_set_option($this->socket,SOL_SOCKET, SO_RCVTIMEO, array("sec"=>1, "usec"=>0));
+           
+            while (!$go) {
+               $in .= @socket_read($this->socket, 2048);
+               if((time()-$start) > 1) $go = true;
+            }
+            return $in;
+
+    }
+
+
+    public function talk($parameter){
+        //echo 'Execution envois de parole vers un client : '.$parameter;
+        return $this->send(array("action"=>"TALK","parameter"=>$parameter));
+    }
+    
+	public function sound($parameter){
+		return $this->send(array("action"=>"SOUND","parameter"=>$parameter));
+	}
+	
+	public function execute($parameter){
+		return $this->send(array("action"=>"EXECUTE","parameter"=>$parameter));
+	}
+    public function emotion($emotion){
+        return $this->send(array("action"=>"EMOTION","parameter"=>$emotion));
+    }
+    public function image($image){
+        return $this->send(array("action"=>"IMAGE","parameter"=>$image));
+    }
+
+
+    // public static function connect() {
+ 
+    // if(is_null(self::$instance)) {
+    //     self::$instance = new Client();  
+    // }
+ 
+    //  return self::$instance;
+    // }
+
+
+}
+
+
+
+?>

+ 192 - 0
classes/Configuration.class.php

@@ -0,0 +1,192 @@
+<?php
+/**
+* Manage application and plugins configurations with key/value pair
+* 
+* **nb:** It's possible to specify namespace in order to distinct global configuration to plugin custom configuration
+* @author Idleman
+* @category Database
+* @license cc by nc sa
+*/
+
+class Configuration extends SQLiteEntity{
+
+	protected $id,$key,$value,$confTab,$namespace;
+	protected $TABLE_NAME = 'configuration';
+	protected $CLASS_NAME = 'Configuration';
+	protected $object_fields = 
+	array(
+		'id'=>'key',
+		'key'=>'longstring',
+		'value'=>'longstring'
+	);
+
+	function __construct(){
+		parent::__construct();
+	}
+
+	/**
+	* Get all configurations from database OR session if it was yet loaded
+	* This function is called at start of program and global var '$conf' is filled with response, so use global $conf instead of call this function.
+	* #### Example
+	* ```php
+	* $confs = Configuration::getAll();
+	* var_dump($confs);
+	* ```
+	* @return array Array of configurations
+	*/
+	public function getAll(){
+
+		if(!isset($_SESSION['configuration'])){
+	
+		$configurationManager = new Configuration();
+		$configs = $configurationManager->populate();
+		$confTab = array();
+
+		foreach($configs as $config){
+			
+			$ns = 'conf';
+			$key = $config->getKey();
+			$infos  = explode(':',$key);
+			if(count($infos) ==2){
+				list($ns,$key) = $infos;
+			}
+
+			$this->confTab[$ns][$key] = $config->getValue();
+		}
+
+		$_SESSION['configuration'] = serialize($this->confTab);
+		
+		}else{
+			$this->confTab = unserialize($_SESSION['configuration']);
+		}
+	}
+
+	/**
+	* Get configuration value from it key
+	* #### Example
+	* ```php
+	* global $conf; // global var, contain configurations
+	* echo $conf->get('myConfigKey'); // print myConfigKey value
+	* ```
+	* @param string configuration key
+	* @param string configuration namespace (default is 'conf')
+	* @return string valeur de la configuration
+	*/
+	public function get($key,$ns = 'conf'){
+		return (isset($this->confTab[$ns][$key])?$this->confTab[$ns][$key]:'');
+	}
+	
+
+	/**
+	* Update or insert configuration value in database with specified key
+	* #### Example
+	* ```php
+	* global $conf; // global var, contain configurations
+	* echo $conf->put('myNewConfigKey','hello!'); //create configuration myNewConfigKey with value 'hello!'
+	* echo $conf->put('myNewConfigKey','hello 2!'); //update configuration myNewConfigKey with value 'hello2!'
+	* ```
+	* @param string configuration key
+	* @param string configuration value
+	* @param string configuration namespace (default is 'conf')
+	*/
+	public function put($key,$value,$ns = 'conf'){
+		$configurationManager = new Configuration();
+		if (isset($this->confTab[$ns][$key])){
+			$configurationManager->change(array('value'=>$value),array('key'=>$ns.':'.$key));
+		} else {
+			$configurationManager->add($key,$value,$ns);	
+		}
+		$this->confTab[$ns][$key] = $value;
+		unset($_SESSION['configuration']);
+	}
+
+	/**
+	* Remove configuration value in database with specified key
+	* #### Example
+	* ```php
+	* global $conf; // global var, contain configurations
+	* echo $conf->remove('myNewConfigKey'); //delete myNewConfigKey from 'conf' default namespace 
+	* echo $conf->remove('myNewConfigKey','myCustomPluginConfig'); //delete myNewConfigKey from 'myCustomPluginConfig' namespace
+	* ```
+	* @param string configuration key
+	* @param string configuration namespace (default is 'conf')
+	*/
+	public function remove($key,$ns = 'conf'){
+		$configurationManager = new Configuration();
+		if (isset($this->confTab[$ns][$key])){
+			$configurationManager->delete(array('key'=>$ns.':'.$key));
+		}
+		unset($this->confTab[$ns][$key]);
+		unset($_SESSION['configuration']);
+	}
+	
+	private function add($key,$value,$ns = 'conf'){
+		$config = new Configuration();
+		$config->setKey($ns.':'.$key);
+		$config->setValue($value);
+		$config->save();
+		$this->confTab[$ns][$key] = $value;
+		unset($_SESSION['configuration']);
+	}
+	
+	/**
+	* Get current configuration id in database
+	* @return int configuration id
+	*/
+	function getId(){
+		return $this->id;
+	}
+	
+	/**
+	* Get current configuration key
+	* @return string configuration key
+	*/
+	function getKey(){
+		return $this->key;
+	}
+
+	/**
+	* Set current configuration key
+	* @param string configuration key
+	*/
+	function setKey($key){
+		$this->key = $key;
+	}
+
+	/**
+	* Get current configuration value
+	* @return string configuration value
+	*/
+	function getValue(){
+		return $this->value;
+	}
+
+	/**
+	* Set current configuration value
+	* @param string configuration value
+	*/
+	function setValue($value){
+		$this->value = $value;
+	}
+	
+	/**
+	* Set current configuration namespace
+	* @param string configuration namespace
+	*/
+	function setNameSpace($ns){
+		$this->namespace = $ns;
+	}
+	
+	/**
+	* Get current configuration namespace
+	* @return string configuration namespace
+	*/
+	function getNameSpace(){
+		return $this->namespace;
+	}
+
+
+
+}
+
+?>

+ 42 - 0
classes/Database.class.php

@@ -0,0 +1,42 @@
+<?php
+
+/**
+* PDO Connector for database connexion.
+* @author v.carruesco
+* @category Core
+* @license copyright
+*/
+class Database
+{
+	public $connection = null;
+	public static $instance = null;
+	private function __construct(){
+		$this->connect();
+	}
+
+	/**
+	* Methode de recuperation unique de l'instance
+	* @author Valentin CARRUESCO
+	* @category Singleton
+	* @param <Aucun>
+	* @return <pdo> $instance
+	*/
+	public static function instance(){
+		if (Database::$instance === null) {
+			Database::$instance = new self(); 
+		}
+		return Database::$instance->connection;
+	}
+	
+	public function connect(){
+		try {
+			$this->connection = new PDO(BASE_CONNECTION_STRING, BASE_LOGIN, BASE_PASSWORD);
+			$this->connection->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
+			
+		} catch ( Exception $e ) {
+		  echo "Connection à la base impossible : ", $e->getMessage();
+		  die();
+		}
+	}
+}
+?>

+ 53 - 0
classes/Device.class.php

@@ -0,0 +1,53 @@
+<?php
+/**
+* When devices (captors, interruptors...) and their values are declared here
+* they can be used by all plugins of yana (maps, aggregators...)
+* @author Idleman
+* @category Hardware
+* @license cc by nc sa
+*/
+
+class Device extends SQLiteEntity{
+
+	 const CAPTOR = 1;
+	 const ACTUATOR = 2;
+	 const BOTH = 3;
+	 public $id,$label,$icon,$display,$state,$values,$location,$plugin,$actions,$type,$uid;
+	 protected $TABLE_NAME = 'device';
+	 protected $CLASS_NAME = 'Device';
+	 protected $object_fields = 
+	    array(
+		    'id'=>'key',
+		    'label'=>'string',
+            'icon'=>'string',
+			'display'=>'longstring',
+			'state'=>'int',
+			'values'=>'longstring',
+			'location'=>'int',
+			'plugin'=>'string',
+			'actions'=>'longstring',
+			'type'=>'int',
+			'uid'=>'int'
+	    );
+
+	function __contruct(){
+		$this->setValues(array());
+	}
+	public function setValue($key,$value){
+		$values = $this->getValues();
+		$values[$key] = $value;
+		$this->setValues($values);
+	}
+	public function setValues($values){
+		$this->values = json_encode($values);
+	}
+	public function getValues(){
+		$values = json_decode($this->values,true);
+		return is_array($values) ? $values : array();
+	}
+	public function getValue($key){
+		$values = $this->getValues();
+		return $values[$key];
+	}
+}
+?>

+ 440 - 0
classes/Entity.class.php

@@ -0,0 +1,440 @@
+<?php
+require_once(dirname(__FILE__).'/../constant.php');
+
+/**
+	Classe mère de tous les modèles (classe entitées) liées a la base de donnée,
+	cette classe est configuré pour agir avec une base SQLite, mais il est possible de redefinir ses codes SQL pour l'adapter à un autre SGBD sans affecter 
+	le reste du code du projet.
+	@author: idleman
+	@version 2
+**/
+
+
+	class Entity
+	{
+
+		public $debug = false,$pdo = null;
+		public static $lastError = '';
+		public static $lastQuery = '';
+
+
+		function __construct(){
+			$this->connect();
+		}
+
+		function connect(){
+			$this->pdo = Database::instance();
+			if(!isset($this->TABLE_NAME)) $this->TABLE_NAME = strtolower(get_called_class());
+		}
+
+		public function __toString(){
+			foreach($this->toArray() as $key=>$value){
+				echo $key.' : '.$value.','.PHP_EOL;
+			}
+        	
+    	}
+
+		public static function debug(){
+			return array(self::$lastQuery,self::$lastError);
+		}
+
+    	public function __sleep()
+		{
+		    return array_keys($this->toArray());
+		}
+
+		public function __wakeup()
+		{
+		    $this->connect();
+		}
+
+		function toArray(){
+			$fields = array();
+			foreach($this->fields as $field=>$type)
+				$fields[$field]= $this->$field;
+			return $fields;
+		}
+		function fromArray($array){
+			foreach($array as $field=>$value)
+				$this->$field = $value;
+		}
+
+		function sgbdType($type){
+			$types = array();
+			$types['string'] = $types['timestamp'] = $types['date'] = 'VARCHAR(255)';
+			$types['longstring'] = 'TEXT';
+			$types['key'] = 'INTEGER NOT NULL PRIMARY KEY';
+			$types['object'] = $types['integer'] = 'bigint(20)';
+			$types['boolean'] = 'INTEGER(1)';
+			$types['blob'] = ' BLOB';
+			$types['default'] = 'TEXT';
+			return isset($types[$type]) ? $types[$type] : $types['default'];
+		}
+
+
+		public function closeDatabase(){
+		//$this->close();
+		}
+
+		public static function tableName(){
+			$class = get_called_class();
+			$instance = new $class();
+			return ENTITY_PREFIX.$instance->TABLE_NAME;
+		}
+
+
+		// GESTION SQL
+
+		/**
+		* Verifie l'existence de la table en base de donnée
+		* @author Valentin CARRUESCO
+		* @category manipulation SQL
+		* @param <String> créé la table si elle n'existe pas
+		* @return true si la table existe, false dans le cas contraire
+		*/
+		public static function checkTable($autocreate = false){
+			$class = get_called_class();
+			$instance = new $class();
+			$query = 'SELECT count(*) as numRows FROM sqlite_master WHERE type="table" AND name=?';  
+			$statement = $instance->customQuery($query,array($instance->tableName()));
+
+			if($statement!=false){
+				$statement = $statement->fetchArray();
+				if($statement['numRows']==1){
+					$return = true;
+				}
+			}
+			if($autocreate && !$return) self::create();
+			return $return;
+		}
+		
+		public static function install($classDirectory){
+
+			foreach(glob($classDirectory.DIRECTORY_SEPARATOR.'*.class.php') as $file){
+				$infos = explode('.',basename($file));
+				$class = array_shift($infos);
+				if (!class_exists($class) || !method_exists ($class , 'create') || $class==get_class()) continue;
+				$class::create();
+			}
+		}
+	
+
+		/**
+		* Methode de creation de l'entité
+		* @author Valentin CARRUESCO
+		* @category manipulation SQL
+		* @return Aucun retour
+		*/
+		public static function create(){
+			$class = get_called_class();
+			$instance = new $class();
+			$query = 'CREATE TABLE IF NOT EXISTS `'.ENTITY_PREFIX.$instance->TABLE_NAME.'` (';
+
+				foreach($instance->fields as $field=>$type)
+					$query .='`'.$field.'`  '. $instance->sgbdType($type).' ,';
+
+				$query = substr($query,0,strlen($query)-1);
+				$query .= ');';
+			$instance->customExecute($query);
+		}
+
+		public static function drop(){
+			$class = get_called_class();
+			$instance = new $class();
+			$query = 'DROP TABLE `'.$instance->tableName().'`;';
+			$instance->customExecute($query);
+		}
+
+		/**
+		* Methode d'insertion ou de modifications d'elements de l'entité
+		* @author Valentin CARRUESCO
+		* @category manipulation SQL
+		* @param  Aucun
+		* @return Aucun retour
+		*/
+		public function save(){
+			$data = array();
+			if(isset($this->id) && $this->id>0){
+				$query = 'UPDATE `'.ENTITY_PREFIX.$this->TABLE_NAME.'` SET ';
+				foreach($this->fields as $field=>$type){
+					$value = $this->{$field};
+					$query .= '`'.$field.'`=?,';
+					$data[] = $value;
+				}
+				$query = substr($query,0,strlen($query)-1);
+				$data[] = $this->id;
+				$query .= ' WHERE `id`=?;';
+			}else{
+				$query = 'INSERT INTO `'.$this->tableName().'`(';
+					foreach($this->fields as $field=>$type){
+						if($type!='key')
+							$query .='`'.$field.'`,';
+					}
+					$query = substr($query,0,strlen($query)-1);
+					$query .=')VALUES(';
+					
+					foreach($this->fields as $field=>$type){
+						if($type=='key') continue;
+						$query .='?,';
+						$data[] = $this->{$field};
+					}
+					$query = substr($query,0,strlen($query)-1);
+
+					$query .=');';
+				}
+				$this->customExecute($query,$data);
+				
+				$this->id =  (!isset($this->id) || !(is_numeric($this->id))?$this->pdo->lastInsertId():$this->id);
+		}
+
+		/**
+		* Méthode de modification d'éléments de l'entité
+		* @author Valentin CARRUESCO
+		* @category manipulation SQL
+		* @param <Array> $colonnes=>$valeurs
+		* @param <Array> $colonnes (WHERE) =>$valeurs (WHERE)
+		* @param <String> $operation="=" definis le type d'operateur pour la requete select
+		* @return Aucun retour
+		*/
+		public static function change($columns,$columns2=null,$operation='='){
+			$class = get_called_class();
+			$instance = new $class();
+			$data = array();
+			$query = 'UPDATE `'.$instance->tableName().'` SET ';
+
+			foreach ($columns as $column=>$value){
+				$query .= '`'.$column.'`=?,';
+				$data[] = $value;
+			}
+			$query = substr($query,0,strlen($query)-1);
+			if($columns2!=null){
+				$query .=' WHERE '; 
+				
+				foreach ($columns2 as $column=>$value){
+					$query .= '`'.$column.'`'.$operation.'?,';
+					$data[] = $value;
+				}
+				$query = substr($query,0,strlen($query)-1);
+			}
+			$instance->customExecute($query,$data);
+		}
+
+		/**
+		* Méthode de selection de tous les elements de l'entité
+		* @author Valentin CARRUESCO
+		* @category manipulation SQL
+		* @param <String> $ordre=null
+		* @param <String> $limite=null
+		* @return <Array<Entity>> $Entity
+		*/
+		public static function populate($order=null,$limit=null){
+			$results = self::loadAll(array(),$order,$limit,'=');
+			return $results;
+		}
+
+
+		/**
+		* Méthode de selection multiple d'elements de l'entité
+		* @author Valentin CARRUESCO
+		* @category manipulation SQL
+		* @param <Array> $colonnes (WHERE)
+		* @param <Array> $valeurs (WHERE)
+		* @param <String> $ordre=null
+		* @param <String> $limite=null
+		* @param <String> $operation="=" definis le type d'operateur pour la requete select
+		* @return <Array<Entity>> $Entity
+		*/
+		public static function loadAll($columns=array(),$order=null,$limit=null,$operation="=",$selColumn='*'){
+			$objects = array();
+			$whereClause = '';
+			$data = array();
+			if($columns!=null && sizeof($columns)!=0){
+				$whereClause .= ' WHERE ';
+				$i = false;
+				foreach($columns as $column=>$value){
+					if($i){$whereClause .=' AND ';}else{$i=true;}
+					$whereClause .= '`'.$column.'`'.$operation.'?';
+					$data[] = $value;
+				}
+			}
+
+			$class = get_called_class();
+			$instance = new $class();
+			$query = 'SELECT '.$selColumn.' FROM `'.$instance->tableName().'` '.$whereClause.' ';
+			
+			if($order!=null) $query .='ORDER BY '.$order.' ';
+			if($limit!=null) $query .='LIMIT '.$limit.' ';
+			$query .=';';
+			return $instance->customQuery($query,$data,true);
+		}
+
+		public static function loadAllOnlyColumn($selColumn,$columns,$order=null,$limit=null,$operation="="){
+			$objects = self::loadAll($columns,$order,$limit,$operation,$selColumn);
+			if(count($objects)==0)$objects = array();
+			return $objects;
+		}
+		
+
+
+		/**
+		* Méthode de selection unique d'élements de l'entité
+		* @author Valentin CARRUESCO
+		* @category manipulation SQL
+		* @param <Array> $colonnes (WHERE)
+		* @param <Array> $valeurs (WHERE)
+		* @param <String> $operation="=" definis le type d'operateur pour la requete select
+		* @return <Entity> $Entity ou false si aucun objet n'est trouvé en base
+		*/
+		public static function load($columns,$operation='='){
+			$objects = self::loadAll($columns,null,'1',$operation);
+			if(!isset($objects[0]))$objects[0] = false;
+			return $objects[0];
+		}
+
+		/**
+		* Méthode de selection unique d'élements de l'entité
+		* @author Valentin CARRUESCO
+		* @category manipulation SQL
+		* @param <Array> $colonnes (WHERE)
+		* @param <Array> $valeurs (WHERE)
+		* @param <String> $operation="=" definis le type d'operateur pour la requete select
+		* @return <Entity> $Entity ou false si aucun objet n'est trouvé en base
+		*/
+		public static function getById($id,$operation='='){
+			return self::load(array('id'=>$id),$operation);
+		}
+
+		/**
+		* Methode de comptage des éléments de l'entité
+		* @author Valentin CARRUESCO
+		* @category manipulation SQL
+		* @return<Integer> nombre de ligne dans l'entité'
+		*/
+		public static function rowCount($columns=null)
+		{
+			$class = get_called_class();
+			$instance = new $class();
+			$whereClause ='';
+			$data = array();
+			if($columns!=null){
+				$whereClause = ' WHERE ';
+				$i=false;
+				foreach($columns as $column=>$value){
+					if($i){$whereClause .=' AND ';}else{$i=true;}
+					$whereClause .= '`'.$column.'`=?';
+					$data[] = $value;
+				}
+			}
+			$query = 'SELECT COUNT(id) resultNumber FROM '.$instance->tableName().$whereClause;
+			
+			$execQuery = $instance->customQuery($query,$data);
+			$row = $execQuery->fetch();
+
+			return $row['resultNumber'];
+		}	
+
+		/**
+		* Methode de définition de l'éxistence d'un moins un des éléments spécifiés en base
+		* @author Valentin CARRUESCO
+		* @category manipulation SQL
+		* @return<boolean> existe (true) ou non (false)
+		*/
+		public static function exist($columns=null)
+		{
+			$result = self::rowCount($columns);
+
+			return ($result!=0);
+		}	
+
+		public static function deleteById($id){
+			
+			self::delete(array('id'=>$id));
+		}
+		/**
+		* Méthode de supression d'elements de l'entité
+		* @author Valentin CARRUESCO
+		* @category manipulation SQL
+		* @param <Array> $colonnes (WHERE)
+		* @param <Array> $valeurs (WHERE)
+		* @param <String> $operation="=" definis le type d'operateur pour la requete select
+		* @return Aucun retour
+		*/
+		public static function delete($columns,$operation='=',$limit=null){
+			
+			$class = get_called_class();
+			$instance = new $class();
+			$whereClause = '';
+			$i=false;
+			$data = array();
+			foreach($columns as $column=>$value){
+				if($i){$whereClause .=' AND ';}else{$i=true;}
+				$whereClause .= '`'.$column.'`'.$operation.'?';
+				$data[]=$value; 
+			}
+			$query = 'DELETE FROM `'.ENTITY_PREFIX.$instance->TABLE_NAME.'` WHERE '.$whereClause.' '.(isset($limit)?'LIMIT '.$limit:'').';';
+			$instance->customExecute($query,$data);
+		}
+		
+		public function customExecute($query,$data = array()){
+			self::$lastQuery = $query;
+			$stm = $this->pdo->prepare($query);
+			try{
+				$stm->execute($data);
+			}catch(Exception $e){
+				self::$lastError = $this->pdo->errorInfo();
+				throw new Exception($e->getMessage());
+			}
+			
+		}
+
+		public static function staticQuery($query,$data = array(),$fill = false){
+			$class = get_called_class();
+			$instance = new $class();
+			return $instance->customQuery($query,$data,$fill);
+		}
+
+		public function customQuery($query,$data = array(),$fill = false){
+			self::$lastQuery = $query;
+			
+			$results = $this->pdo->prepare($query);
+			
+			$results->execute($data);
+			
+			if(!$results){
+				self::$lastError = $this->pdo->errorInfo();
+				return false;
+			}else{
+
+				if(!$fill)	return $results;
+
+				$class = get_class($this);
+				$objects = array();
+				while($queryReturn = $results->fetch() ){
+					$object = new $class();
+					foreach($this->fields as $field=>$type){
+						if(isset($queryReturn[$field]))
+							$object->{$field} =  $queryReturn[$field];
+					}
+					$objects[] = $object;
+					unset($object);
+				}
+				
+				return $objects == null?array()  : $objects;
+			}
+		}
+
+
+		public function __get($name)
+		{
+				$pos = strpos($name,'_object');
+				if($pos!==false){
+					$field = strtolower(substr($name,0,$pos));
+					if(array_key_exists($field,$this->fields)){
+						$class = ucfirst($field);
+						return $class::getById($this->{$field});
+					}
+				}
+				throw new Exception("Attribut ".get_class($this)."->$name non existant");
+		}
+	}
+?>

+ 145 - 0
classes/Event.class.php

@@ -0,0 +1,145 @@
+ <?php
+	// $event = new Event();
+ //     $event->setTime(time());
+ //     $event->setRepeat(2);
+ //     $event->setContent($sayHello);
+ //     $event->save();
+    
+
+    class Event extends SQLiteEntity{
+
+	    protected $id,$name,$content,$year,$month,$day,$hour,$minut,$repeat,$recipients,$state;
+	    protected $TABLE_NAME = 'event';
+	    protected $CLASS_NAME = 'Event';
+	    protected $object_fields = 
+	    array(
+		    'id'=>'key',
+		    'name'=>'string',
+            'content'=>'longstring',
+		    'year'=>'string',
+            'month'=>'string',
+            'day'=>'string',
+            'hour'=>'string',
+            'minut'=>'string',
+		    'repeat'=>'string',
+            'state'=>'int',
+            'recipients'=>'longstring'
+	    );
+     
+         function __construct(){
+            parent::__construct();
+            // $this->time = time();
+            // $this->repeat = '1';
+            // $this->name = 'Evenement sans titre du '.date('d/m/Y H:i:s');
+            // $this->setRecipients(array());
+         }
+     
+        function setId($id){
+            $this->id= $id;
+        }
+        function getId(){
+            return $this->id;
+        }
+
+        function setState($state){
+            $this->state= $state;
+        }
+        function getState(){
+            return $this->state;
+        }
+
+        function setYear($year){
+            $this->year= $year;
+        }
+        function getYear(){
+            return $this->year;
+        }
+
+        function setMonth($month){
+            $this->month= $month;
+        }
+        function getMonth(){
+            return $this->month;
+        }
+
+        function setDay($day){
+            $this->day= $day;
+        }
+        function getDay(){
+            return $this->day;
+        }
+
+        function setHour($hour){
+            $this->hour= $hour;
+        }
+        function getHour(){
+            return $this->hour;
+        }
+
+        function setMinut($minut){
+            $this->minut= $minut;
+        }
+        function getMinut(){
+            return $this->minut;
+        }
+     
+        function setName($name){
+            $this->name= $name;
+        }
+        function getName(){
+            return $this->name;
+        }
+        function setContent($content){
+            $this->content= json_encode($content);
+        }
+        function getContent(){
+            return json_decode($this->content,true);
+        }
+        function setRepeat($repeat){
+            $this->repeat= $repeat;
+        }
+        function getRepeat(){
+            return $this->repeat;
+        }
+        function addRecipient($recipient){
+           $recipients =  $this->getRecipients();
+           $recipients[] = $recipient;
+           $this->setRecipients($recipients);
+        }
+        function setRecipients($recipients){
+            $this->recipients= json_encode($recipients);
+        }
+        function getRecipients(){
+            $rec = json_decode($this->recipients,true);
+            return is_array($rec)?$rec:array();
+        }
+
+
+        public static function emit($event, $data) {  
+            if(isset($GLOBALS['events'][$event])) { 
+
+                foreach($GLOBALS['events'][$event] as $functionName) {  
+                    call_user_func_array($functionName, $data);  
+                }  
+            }  
+        } 
+
+
+
+
+        public static function on($event, $functionName) {  
+            $GLOBALS['events'][$event][] = $functionName;  
+        } 
+
+
+        public static function announce($event, $comment,$dataDescription) {  
+            $GLOBALS['eventsDictionnary'][$event]['comment'] = $comment;  
+            $GLOBALS['eventsDictionnary'][$event]['data'] = $dataDescription; 
+        } 
+
+        public static function availables($event, $comment) {  
+            return $GLOBALS['eventsDictionnary'];  
+        }
+
+     }
+     ?>

+ 320 - 0
classes/Functions.class.php

@@ -0,0 +1,320 @@
+<?php
+/**
+* Tool library wich provide many common functions to ease your life :)
+* 
+* @author Idleman
+* @category Tools
+* @license cc by nc sa
+*/
+
+class Functions
+{
+	private $id;
+	public $debug=0;
+
+	/**
+	* Secure client var
+	* #### Example
+	* ```php
+	* Functions::secure($_GET['nonThrustedInput']);
+	* ```
+	* @param mixed var to secure
+	* @return mixed secured var
+	*/
+
+
+	public static function secure($var){
+		if(is_array($var)){
+		$array = array();
+		foreach($var as $key=>$value):
+			$array[self::secure($key)] = self::secure($value);
+		endforeach;
+		return $array;
+		} else {
+			return str_replace('&amp;','&',htmlspecialchars($var, ENT_NOQUOTES, "UTF-8"));
+		}
+	}
+
+	/**
+	* Get client IP
+	* #### Example
+	* ```php
+	* Functions::getIP();
+	* ```
+	* @return string client ip
+	*/
+	public static function getIP(){
+		if(isset($_SERVER['HTTP_X_FORWARDED_FOR'])){
+			$ip = $_SERVER['HTTP_X_FORWARDED_FOR'];}
+			elseif(isset($_SERVER['HTTP_CLIENT_IP'])){
+				$ip = $_SERVER['HTTP_CLIENT_IP'];}
+				else{ $ip = $_SERVER['REMOTE_ADDR'];}
+				return $ip;
+			}
+
+	/**
+	* Truncate string after 'x' characters and add '...'
+	* #### Example
+	* ```php
+	* echo Functions::truncate('This is incredibly long !!',5);
+	* ```
+	* @param string String to truncate
+	* @param int Max length before truncate
+	* @return string truncated string
+	*/
+	public static function truncate($msg,$limit){
+		if(mb_strlen($msg)>$limit){
+			$fin='…' ;
+			$nb=$limit-mb_strlen($fin) ;
+		}else{
+			$nb=mb_strlen($msg);
+			$fin='';
+		}
+		return mb_substr($msg, 0, $nb).$fin;
+	}
+
+	/**
+	* Get script base url (require calling file path in parameter)
+	* #### Example
+	* ```php
+	* echo Functions::getBaseUrl('action.php');
+	* ```
+	* @param string calling file path
+	* @return string base url
+	*/
+	public static function getBaseUrl($from){
+
+		$protocol = ((!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] != 'off') || $_SERVER['SERVER_PORT'] == 443) ? "https://" : "http://";
+		$split = explode('/'.$from,$_SERVER['REQUEST_URI']);
+		return $protocol.$_SERVER['HTTP_HOST'].$split[0];
+	}
+
+	/**
+	 * Definis si la chaine fournie est existante dans la reference fournie ou non
+	 * @TODO delete that after verifying that is not used by plugin or core! 
+	 * @param unknown_type $string
+	 * @param unknown_type $reference
+	 * @return false si aucune occurence du string, true dans le cas contraire
+	 */
+	public static function contain($string,$reference){
+		$return = true;
+		$pos = strpos($reference,$string);
+		if ($pos === false) {
+			$return = false;
+		}
+		return strtolower($return);
+	}
+
+	/**
+	 * @TODO delete that after verifying that is not used by plugin or core! 
+	 * Définis si la chaine passée en parametre est une url ou non
+	 */
+	public static function isUrl($url){
+		$return =false;
+		if (preg_match('/^(http|https|ftp)://([A-Z0-9][A-Z0-9_-]*(?:.[A-Z0-9][A-Z0-9_-]*)+):?(d+)?/?/i', $url)) {
+			$return =true;
+		}
+		return $return;
+	}
+
+	/**
+	 * @TODO delete that after verifying that is not used by plugin or core! 
+	 * Définis si la chaine passée en parametre est une couleur héxadécimale ou non
+	 */
+	public static function isColor($color){
+		$return =false;
+		if (preg_match('/^#(?:(?:[a-fd]{3}){1,2})$/i', $color)) {
+			$return =true;
+		}
+		return $return;
+	}
+
+	/**
+	 * @TODO delete that after verifying that is not used by plugin or core! 
+	 * Définis si la chaine passée en parametre est un mail ou non
+	 */
+	public static function isMail($mail){
+		$return =false;
+		if (filter_var($mail, FILTER_VALIDATE_EMAIL)) {
+			$return =true;
+		}
+		return $return;
+	}
+
+	/**
+	 * @TODO delete that after verifying that is not used by plugin or core! 
+	 * Définis si la chaine passée en parametre est une IP ou non
+	 */
+	public static function isIp($ip){
+		$return =false;
+		if (preg_match('^(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)(?:[.](?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)){3}$',$ip)) {
+			$return =true;
+		}
+		return $return;
+	}
+
+	public static function makeCookie($name, $value,$expire) {
+		setcookie($name,$value,$expire,'/');
+	}
+
+	public static function destroyCookie($name){
+		setcookie(COOKIE_NAME, "", time()-3600,"/");
+	}
+
+	public static function convertFileSize($bytes)
+	{
+		if($bytes<1024){
+			return round(($bytes / 1024), 2).' o';
+		}elseif(1024<$bytes && $bytes<1048576){
+			return round(($bytes / 1024), 2).' ko';
+		}elseif(1048576<$bytes && $bytes<1073741824){
+			return round(($bytes / 1024)/1024, 2).' Mo';
+		}elseif(1073741824<$bytes){
+			return round(($bytes / 1024)/1024/1024, 2).' Go';
+		}
+	}
+
+	//Calcul une adresse relative en fonction de deux adresse absolues
+	public static function relativePath($from, $to, $ps = '/') {
+		$arFrom = explode($ps, rtrim($from, $ps));
+		$arTo = explode($ps, rtrim($to, $ps));
+		while(count($arFrom) && count($arTo) && ($arFrom[0] == $arTo[0])) {
+			array_shift($arFrom);
+			array_shift($arTo);
+		}
+		return str_pad("", count($arFrom) * 3, '..'.$ps).implode($ps, $arTo);
+	}
+
+	//Transforme une date en timestamp
+	
+	public static function totimestamp($date,$delimiter='/')
+	{
+		$explode=explode($delimiter,$date);
+		return strtotime($explode[1].'/'.$explode[0].'/'.$explode[2]);
+
+
+	}
+
+	public static function goback($page,$section="",$param="")
+	{
+		if ($section == "")
+		{
+			header('location:'.$page.'.php '.$param);
+		}
+		else
+		{
+			header('location:'.$page.'.php?section='.$section.$param);
+		}
+		
+	}
+
+	public static function rmFullDir($path){
+		$files = array_diff(scandir($path), array('.','..'));
+	    foreach ($files as $file) {
+	      (is_dir("$path/$file")) ? Functions::rmFullDir("$path/$file") : unlink("$path/$file");
+	    }
+	    return rmdir($path); 
+	}
+	
+	public static function log($message,$type = 'notice'){
+	$message = date('d-m-Y H:i:s').' - ['.$type.'] :'.$message.PHP_EOL;
+	if(!file_exists(LOG_FILE)) touch(LOG_FILE);
+		$linecount = 0;
+		$handle = fopen(LOG_FILE, "r");
+		while(fgets($handle)!=false){
+		  $linecount++;
+		}
+		fclose($handle);
+		if($linecount>1000) unlink(LOG_FILE);
+		file_put_contents(LOG_FILE,$message,FILE_APPEND);
+	}
+
+	public static function alterBase($versions,$current){
+		$manager = new User();
+		foreach($versions as $version){
+			if($version['version'] <= $current) continue;
+			set_error_handler('Functions::alterBaseError');
+			foreach($version['sql'] as $command){
+				$sql = str_replace(array('{PREFIX}'), array(MYSQL_PREFIX), $command);
+				Functions::log('Execute alter base query: '.$sql);
+				$manager->customQuery($sql);
+			}
+		 	restore_error_handler();
+		}
+	}
+
+	public static function alterBaseError($errno, $errstr, $errfile, $errline){
+		self::log("Erreur update sql :  [$errno] $errstr L$errline dans le fichier $errfile");
+	}
+
+	public static function array_rand($array){
+		return $array[array_rand($array)];
+	}
+	
+
+	public static function tail($filepath, $lines = 1, $adaptive = true) {
+		$f = @fopen($filepath, "rb");
+		if ($f === false) return false;
+		if (!$adaptive) $buffer = 4096;
+		else $buffer = ($lines < 2 ? 64 : ($lines < 10 ? 512 : 4096));
+		fseek($f, -1, SEEK_END);
+		if (fread($f, 1) != "\n") $lines -= 1;
+		$output = '';
+		$chunk = '';
+		while (ftell($f) > 0 && $lines >= 0) {
+		$seek = min(ftell($f), $buffer);
+		fseek($f, -$seek, SEEK_CUR);
+		$output = ($chunk = fread($f, $seek)) . $output;
+		fseek($f, -mb_strlen($chunk, '8bit'), SEEK_CUR);
+		$lines -= substr_count($chunk, "\n");
+		}
+		while ($lines++ < 0) {
+		$output = substr($output, strpos($output, "\n") + 1);
+		}
+		fclose($f);
+		return trim($output);
+	} 
+
+
+	public static function htmlAlert($type,$message){
+		switch ($type) {
+			case 'error':
+				echo '<div class="alert alert-error"><button type="button" class="close" data-dismiss="alert">&times;</button><strong>Erreur!</strong> ';
+			break;
+			case 'info':
+				echo '<div class="alert alert-notice"><button type="button" class="close" data-dismiss="alert">&times;</button><strong>Info</strong> ';
+			break;
+			case 'success':
+				echo '<div class="alert alert-success"><button type="button" class="close" data-dismiss="alert">&times;</button><strong>Yeah!</strong> ';
+			break;
+			default:
+				echo '<div class="alert"><button type="button" class="close" data-dismiss="alert">&times;</button><strong>Erreur!</strong> ';
+			break;
+		}
+		
+		echo $message.'</div>';
+	}
+	
+	static public function slugify($text)
+	{ 
+	  // replace non letter or digits by -
+	  $text = preg_replace('~[^\\pL\.\d]+~u', '-', $text);
+	  // trim
+	  $text = trim($text, '-');
+	  // transliterate
+	  $text = iconv('utf-8', 'us-ascii//TRANSLIT', $text);
+	  // lowercase
+	  $text = strtolower($text);
+	  // remove unwanted characters
+	  $text = preg_replace('~[^-\.\w]+~', '', $text);
+
+	  if (empty($text))
+	    return 'n-a';
+	  
+	  return $text;
+	}
+
+
+
+}
+?>

+ 65 - 0
classes/Gpio.class.php

@@ -0,0 +1,65 @@
+<?php
+
+/*
+ @nom: Gpio
+ @auteur: Idleman (idleman@idleman.fr)
+ @description:  Classe de gestion des gpio via wiring PI
+ */
+
+class Gpio{
+
+	const GPIO_DEFAULT_PATH = '/usr/bin/gpio';
+	
+	public $name,$role,$wiringPiNumber,$bcmNumber,$physicalNumber;
+	
+	function __construct($name,$role,$wiringPiNumber,$bcmNumber,$physicalNumber){
+		$this->name = $name;
+		$this->role = $role;
+		$this->wiringPiNumber = $wiringPiNumber;
+		$this->bcmNumber = $bcmNumber;
+		$this->physicalNumber = $physicalNumber;
+	}
+	
+	private static function system($cmd){
+		// For compatibily with plugins wich call that method from GPIO instead of System.
+		System::command($cmd);
+	}
+	
+	public static function mode($pin,$mode = 'out'){
+		return self::system(self::GPIO_DEFAULT_PATH.' mode '.$pin.' '.$mode);
+	}
+	public static function write($pin,$value = 0,$automode = false){
+		if($automode) self::mode($pin,'out');
+		return self::system(self::GPIO_DEFAULT_PATH.' write '.$pin.' '.$value);
+	}
+	public static function read($pin,$automode = false){
+		if($automode) self::mode($pin,'in');
+		return System::commandSilent(self::GPIO_DEFAULT_PATH.' read '.$pin);
+	}
+	public static function pulse($pin,$miliseconds,$state){
+		Gpio::write($pin,$state);
+		usleep($miliseconds);
+		$state = $state == 1 ? 0 : 1;
+		Gpio::write($pin,$state);
+	}
+
+	public static function emit($gpio, $state){
+		
+		if(isset($GLOBALS['gpio'][$gpio])) {
+		    foreach($GLOBALS['gpio'][$gpio] as $functionName) {  
+		        call_user_func_array($functionName, array($gpio,$state));  
+		    }  
+		} 
+		if(isset($GLOBALS['gpio']['all'])) {
+	
+		    foreach($GLOBALS['gpio']['all'] as $functionName) {  
+		        call_user_func_array($functionName, array($gpio,$state));  
+		    }  
+		} 
+	}
+
+	public static function listen($gpio,$functionName){
+		$GLOBALS['gpio'][$gpio][] = $functionName;  
+	}
+}
+?>

+ 306 - 0
classes/Monitoring.class.php

@@ -0,0 +1,306 @@
+<?php
+
+/*
+Classe basée sur le code de Raspcontrol [https://github.com/imjacobclark/Raspcontrol/]
+*/
+
+class Monitoring {
+ 
+  
+  public static function cpu() {
+	// $loads[0] > 1 == 'danger'
+	$loads = @sys_getloadavg();
+    return array
+	(
+		'current_frequency' => round(@file_get_contents("/sys/devices/system/cpu/cpu0/cpufreq/scaling_cur_freq") / 1000), //Mhz
+		'minimum_frequency' => round(@file_get_contents("/sys/devices/system/cpu/cpu0/cpufreq/scaling_min_freq") / 1000), //Mhz
+		'maximum_frequency' => round(@file_get_contents("/sys/devices/system/cpu/cpu0/cpufreq/scaling_max_freq") / 1000), //Mhz
+		'governor'		    => substr(@file_get_contents("/sys/devices/system/cpu/cpu0/cpufreq/scaling_governor"), 0, -1),
+		'loads'			    => $loads[0],
+		'loads5'		    => $loads[1],
+		'loads15'		    => $loads[2],
+	);
+  }
+
+  public static function heat() {
+	$heat_file = '/sys/class/thermal/thermal_zone0/temp';
+	$label = "label-warning";
+	$heat = 0;
+	if(file_exists($heat_file)){
+		$heat = round(file_get_contents($heat_file) / 1000,1);
+		//OK
+		if ($heat < 55){
+		  $label = "label-success";
+		}
+		
+		//WARNING
+		if (($heat >= 55) && ($heat < 70)) {
+		  $label = "label-warning";
+		}
+		
+		//DANGER
+		if ($heat >= 70) {
+		  $label = "label-important";
+		}
+	}
+    $result = array(
+    'degrees' => $heat,
+    'label' => $label
+    );
+    return $result;
+  }
+  
+  public static function disks() {
+		$result = array();
+		exec('lsblk --pairs', $disksArray);
+		for ($i = 0; $i < count($disksArray); $i++) { 
+			parse_str(str_replace(array('"',' '), array("","&"), $disksArray[$i]), $output);		
+			$result[$i]['name'] = $output["NAME"];     
+			$result[$i]['maj:min'] = $output["MAJ:MIN"];     
+			$result[$i]['rm'] = $output["RM"];     
+			$result[$i]['size'] = $output["SIZE"];     
+			$result[$i]['ro'] = $output["RO"];     
+			$result[$i]['type'] = $output["TYPE"];     
+			$result[$i]['mountpoint'] = $output["MOUNTPOINT"];     
+		}
+		return $result;
+	}  
+	
+	
+  public static function ram() {
+		//$result['percentage'] >= '80'  = 'danger'
+		exec('free -mo', $out);
+		preg_match_all('/\s+([0-9]+)/', $out[1], $matches);
+		list($total, $used, $free, $shared, $buffers, $cached) = $matches[1];
+		return array(
+			'free' => $free + $buffers + $cached,
+			'percentage' => $total == 0 ? 0:round(($used - $buffers - $cached) / $total * 100),
+			'total'  => $total,
+			'used' => $used - $buffers - $cached,
+			'detail' => shell_exec('ps -e -o pmem,user,args --sort=-pmem | sed "/^ 0.0 /d" | head -5')
+		);
+  }
+
+  public static function swap() {
+		//$result['percentage'] >= '80' = danger
+		exec('free -mo', $out);
+		preg_match_all('/\s+([0-9]+)/', $out[2], $matches);
+		list($total, $used, $free) = $matches[1]; 
+		
+		return array(
+			'percentage' => round($used / $total * 100),
+			'free' => $free,
+			'used' => $used,
+			'total' => $total
+		);
+  }
+
+  public static function gpio() {
+    return System::gpio();
+  }
+
+  public static function connections() {
+		//$connections >= 50 = 'warning'
+		$connections = shell_exec("netstat -nta --inet | wc -l");
+		$connections--;
+		return substr($connections, 0, -1);
+		  
+  }
+
+  public static function ethernet() {
+	  $data = str_ireplace(array("TX bytes:","RX bytes:"), "",shell_exec("/sbin/ifconfig eth0 | grep RX\ bytes"));
+	  $data =  explode(" ", trim($data));
+
+    if(!is_numeric($data[0])) $data[0] = 1;
+
+	  return array(
+		'up' => round($data[4] / 1024 / 1024,2),
+		'down' => round($data[0] / 1024 / 1024,2)
+		);
+  }
+  
+   public static function distribution() {
+    $distroTypeRaw = exec("cat /etc/*-release | grep PRETTY_NAME=", $out);
+    return str_ireplace(array('PRETTY_NAME="','"'), '', $distroTypeRaw);
+  }
+
+  public static function kernel() {
+    return exec("uname -mrs");
+  }
+
+  public static function firmware() {
+    return exec("uname -v");
+  }
+
+  public static function hostname($full = false) {
+    return $full ? exec("hostname -f") : gethostname();
+  }
+
+  public static function internalIp() {
+    return $_SERVER['SERVER_ADDR'];
+  }
+
+  public static function externalIp() {
+      $ip = self::loadUrl('http://whatismyip.akamai.com');
+      if(filter_var($ip, FILTER_VALIDATE_IP) === false)
+          $ip = self::loadUrl('http://ipecho.net/plain');
+      if(filter_var($ip, FILTER_VALIDATE_IP) === false)
+          return 'Unavailable';
+      return $ip;
+  }
+
+  public static function webServer() {
+    return $_SERVER['SERVER_SOFTWARE'];
+  }
+  
+  public static function services() {
+    $result = array();
+    exec('/usr/sbin/service --status-all', $servicesArray);
+    for ($i = 0; $i < count($servicesArray); $i++) {
+		$servicesArray[$i] = preg_replace('!\s+!', ' ', $servicesArray[$i]);
+		preg_match_all('/\S+/', $servicesArray[$i], $serviceDetails);
+		list($bracket1, $result[$i]['status'], $bracket2, $result[$i]['name']) = $serviceDetails[0];
+    $result[$i]['status'] = ($result[$i]['status']=='+'?true:false);
+    }
+    return $result;
+  }
+  
+  public static function hdd() {
+	//$result[$i]['percentage'] > '80' = danger
+    $result = array();
+    exec('df -T | grep -vE "tmpfs|rootfs|Filesystem"', $drivesarray);
+    for ($i=0; $i<count($drivesarray); $i++) {
+      $drivesarray[$i] = preg_replace('!\s+!', ' ', $drivesarray[$i]);
+      preg_match_all('/\S+/', $drivesarray[$i], $drivedetails);
+      list($fs, $type, $size, $used, $available, $percentage, $mounted) = $drivedetails[0];
+      $result[$i] = array(
+		'name' => $mounted,
+		'total' => self::kConv($size),
+		'free' => self::kConv($available),
+		'used' => self::kConv($size - $available),
+		'format' => $type,
+		'percentage' => rtrim($percentage, '%')
+	  ); 
+    }
+    return $result;
+  }
+  
+    public static function temperature() {
+    $temp_file = "/sys/bus/w1/devices/28-000004e8a0f3/w1_slave";
+    if (file_exists($temp_file)) {
+       $lines = file($temp_file);
+       $currenttemp = round(substr($lines[1], strpos($lines[1], "t=")+2) / 1000 , 1) . "°C" ;
+    } else {
+       $currenttemp = "N/A";
+    }
+    return  $currenttemp;
+  }
+  
+  public static function uptime() {
+    $uptime = shell_exec("cat /proc/uptime");
+    $uptime = explode(" ", $uptime); 
+    return self::readbleTime($uptime[0]);
+  }
+
+   public static function users() {
+    $result = array();
+    $dataRaw = shell_exec("who --ips");
+    $dataRawDNS = shell_exec("who --lookup");
+    //patch for arch linux - the "who" binary doesnt support the --ips flag
+    if (empty($dataRaw)) $dataRaw = shell_exec("who");
+    foreach (explode ("\n", $dataRawDNS) as $line) {
+      $line = preg_replace("/ +/", " ", $line);
+      if (strlen($line)>0) {
+        $line = explode(" ", $line);
+     
+        $temp[] = @$line[5];
+      }
+    }
+
+    $i = 0;
+    foreach (explode ("\n", $dataRaw) as $line) {
+      $line = preg_replace("/ +/", " ", $line);
+
+      if (strlen($line)>0) {
+        $line = explode(" ", $line);
+        $result[] = array(
+          'user' => $line[0],
+          'ip' => @$line[5],
+          'dns' => $temp[$i],
+          'date' => $line[2] .' '. $line[3],
+          'hour' => $line[4]
+          );
+      }
+      $i++;
+    }
+
+    return $result;
+  }
+  
+  //TOOLS
+  
+  protected static function readbleTime($seconds) {
+    $y = floor($seconds / 60/60/24/365);
+    $d = floor($seconds / 60/60/24) % 365;
+    $h = floor(($seconds / 3600) % 24);
+    $m = floor(($seconds / 60) % 60);
+    $s = $seconds % 60;
+
+    $string = '';
+
+    if ($y > 0) {
+      $yw = $y > 1 ? ' years ' : ' year ';
+      $string .= $y . $yw;
+    }
+
+    if ($d > 0) {
+      $dw = $d > 1 ? ' days ' : ' day ';
+      $string .= $d . $dw;
+    }
+
+    if ($h > 0) {
+      $hw = $h > 1 ? ' hours ' : ' hour ';
+      $string .= $h . $hw;
+    }
+
+    if ($m > 0) {
+      $mw = $m > 1 ? ' minutes ' : ' minute ';
+      $string .= $m . $mw;
+    }
+
+    if ($s > 0) {
+     $sw = $s > 1 ? ' seconds ' : ' second ';
+     $string .= $s . $sw;
+    }
+
+    return preg_replace('/\s+/', ' ', $string);
+  }
+   public static function kConv($kSize){
+    $unit = array('K', 'M', 'G', 'T');
+    $i = 0;
+    $size = $kSize;
+    while($i < 3 && $size > 1024){
+      $i++;
+      $size = $size / 1024;
+    }
+    return round($size, 2).$unit[$i];
+  }
+    protected static function loadUrl($url){
+      if(function_exists('curl_init')){
+          $curl = curl_init();
+          curl_setopt($curl, CURLOPT_URL, $url);
+          curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1);
+          $content = curl_exec($curl);
+          curl_close($curl);
+          return trim($content);
+      }elseif(function_exists('file_get_contents')){
+          return trim(file_get_contents($url));
+      }else{
+          return false;
+      }
+  }
+     
+
+}
+
+?>

+ 111 - 0
classes/Personality.class.php

@@ -0,0 +1,111 @@
+<?php
+
+/**
+* Classe de simulation de la personalité (actuellement uniquement du random sur les réponses)
+* @author Idleman
+* @todo Intégrer de l'IA
+*/
+
+class Personality extends SQLiteEntity{
+
+
+	 protected $id,$key,$value;
+	 protected $TABLE_NAME = 'personnality';
+	 protected $CLASS_NAME = 'Personality';
+	 protected $object_fields = 
+	    array(
+		    'id'=>'key',
+		    'key'=>'string',
+            'value'=>'longstring'
+	    );
+
+	public static function randomPattern($string)
+	{
+	    if(preg_match_all('/(?<={)[^}]*(?=})/', $string, $matches)) {
+	        $matches = reset($matches);
+	        foreach($matches as $i => $match) {
+	            if(preg_match_all('/(?<=\[)[^\]]*(?=\])/', $match, $sub_matches)) {
+	                $sub_matches = reset($sub_matches);
+	                foreach($sub_matches as $sub_match) {
+	                    $pieces = explode('|', $sub_match);
+	                    $count = count($pieces);
+
+	                    $random_word = $pieces[rand(0, ($count - 1))];
+	                    $matches[$i] = str_replace('[' . $sub_match . ']',     $random_word, $matches[$i]);
+	                }
+	            }
+
+	            $pieces = explode('|', $matches[$i]);
+	            $count = count($pieces);
+
+	            $random_word = $pieces[rand(0, ($count - 1))];
+	            $string = str_replace('{' . $match . '}', $random_word, $string);
+	        }
+	    }
+
+	    return $string;
+	}
+
+	public function birth(){
+		$this->put('birthday',strtotime('-'.rand(0,50).' years'));
+		$this->put('favorite_color',Functions::array_rand(array('orange','rouge','bleu','vert','violet','taupe','indigo','bordeaux','jaune','gris','noir','blanc','citron'),1));
+		$this->put('favorite_book',Functions::array_rand(array('Les anales du disque monde de Terry Pratchet','La trilogie des fourmis de Bernard Weber','Fondation d\'Isaac Asimov','Cosmétique de l\'ennemie d\'amélie nothomb','Tout sauf un home d\'Isaac Asimov','Le vieux et son implant de paul bera'),1));
+		$this->put('favorite_food',Functions::array_rand(array('Le magret de canard','Les nuggets maison','Les calzones','les escalopes milanaises'),1));
+		$this->put('favorite_movie',Functions::array_rand(array('Retour vers le futur 1,2 et 3','Fight Club','Vice et versa','Mary poppins'),1));
+		$this->put('favorite_band',Functions::array_rand(array('Nirvana','Noir désir','Zoufris maracas','Les casseurs flowters','Les svinkels','Les frêres brothers','louis chédid','Maxime le forestier','Brassens'),1));
+		$this->put('size',Functions::array_rand(array('Grande','Petite','Moyenne'),1));
+		$this->put('skin',Functions::array_rand(array('Noire','Jaune','Blanche','Métisse'),1));
+		$this->put('fear',rand(0,10));
+		$this->put('anger',rand(0,10));
+		$this->put('sadness',rand(0,10));
+		$this->put('gluttony',rand(0,10));
+		$this->put('lust',rand(0,10));
+		$this->put('jealousy',rand(0,10));
+	}
+
+	public function put($key,$value){
+		$attribute = $this->load(array('key'=>$key));
+		if(!$attribute) $attribute = new Personality();
+		$attribute->key = $key;
+		$attribute->value = $value;
+		$attribute->save();
+	}
+	public function get($key){
+		$attribute = $this->load(array('key'=>$key));
+		if(!$attribute) return '';
+		return $attribute->value; 
+	
+	}
+
+
+	public static $sentences = array(
+								'ORDER_CONFIRMATION'=>
+									array(
+										'{J\'aime [beaucoup|vraiment|]|J\'adore|Je ne [souhaite|veux] que|Je n\'aspire qu\'a|Je ne [rêve] que de} vous {obéir|faire plaisir}!',
+										'{Je fais|J\'[execute|accomplis]} {ça|ceçi|cela} {sans [tarder|lambiner]|avec [diligence|empressement]}!',
+										'{A vos ordres|Avec [plaisir|joie]|Certainement|Oui|Bien[ reçu|compris|]|D\'accord|Oké} {chef|maitre|[mon|][ commandant| dieu]|}!'
+									),
+								'WORRY_EMOTION'=>
+									array('Je suis {confuse|désolée|attristée|peinée|affligée}',
+										'{Si il vous plait|Je vous en prie|} {pardonnez|excusez} moi',
+										'Il y a {confusion|un [problème|soucis|qwak]}',
+										'Je ne sais {pas quoi [dire|faire]|plus ou me mettre}'
+									),
+								'ANGRY_EMOTION'=>
+									array('Vas {te faire cuire un oeuf|au diable|jouer les yeux bandé près d\'une autoroute}',
+										'Tu {sent|pue|fouanne} {des [pieds|aisselles]|de l\'anus|du [posterieur|cul]}',
+										'Je {refuse|n\'accepte pas} {de [communiquer|parler|discutter] avec|d\'obeir a} {un [primate|humain|résidu d\'humanité|inférieur|cafard|étron]|une [larve|pale copie d\'être humain|erreur de la nature]}',
+										'{Je préfère|Plutot} {m\'autodétruire|m\'auto formatter|me faire mettre à jour par un stagiaire|me griller les circuits} {que [continuer|poursuivre] [cette discussion|ce dialogue] [inutile|sans queue ni tête|stupide|de sourd|]|qu\'alimenter ce trou noir intellectuel}',
+										'{Ta [mère|soeur|tante|cousine]|Ton [père|oncle|frère|cousine]} suce {des [dains|orignaux|chtroumffe|aliens]} {en [enfer|roumanie|albanie]}',
+										'Tu pousse le bouchon trop loin maurice'
+									)
+								);
+	public static function response($type){
+		$pattern = static::$sentences[$type];
+		$pattern = $pattern[array_rand($pattern)];
+		return self::randomPattern($pattern);
+	}
+
+}
+
+?>

+ 369 - 0
classes/Plugin.class.php

@@ -0,0 +1,369 @@
+<?php
+
+/*
+ @nom: Plugin
+ @auteur: Valentin CARRUESCO (valentin.carruesco@sys1.fr)
+ @description: Classe de gestion des plugins au travers de l'application
+ */
+
+class Plugin{
+	const FOLDER = '/plugins';
+
+	protected $name,$author,$mail,$link,$licence,$path,$description,$version,$state,$type;
+
+	function __construct(){
+	}
+
+	public static function includeAll(){
+		$pluginFiles = Plugin::getFiles(true);
+		if(is_array($pluginFiles)) {   
+			foreach($pluginFiles as $pluginFile) {
+				//Inclusion du coeur de plugin
+				include $pluginFile;  
+				//Gestion des css du plugin en fonction du thème actif
+				$cssTheme = glob('../'.dirname($pluginFile).'/*/'.DEFAULT_THEME.'.css');
+				$cssDefault = glob('../'.dirname($pluginFile).'/*/default.css');
+				if(isset($cssTheme[0])){
+					$GLOBALS['hooks']['css_files'][] = Functions::relativePath(str_replace('\\','/',dirname(__FILE__)),str_replace('\\','/',$cssTheme[0])); 
+				}else if(isset($cssDefault[0])){
+					$GLOBALS['hooks']['css_files'][] =  Functions::relativePath(str_replace('\\','/',dirname(__FILE__)),str_replace('\\','/',$cssDefault[0])); 
+				}
+			}  
+		}  
+	}
+
+	private static function getStates(){
+		$stateFile = dirname(dirname(__FILE__)).Plugin::FOLDER.'/plugins.states.json';
+		if(!file_exists($stateFile)) touch($stateFile);
+		return json_decode(file_get_contents($stateFile),true);
+	}
+	private static function setStates($states){
+		$stateFile = dirname(dirname(__FILE__)).Plugin::FOLDER.'/plugins.states.json';
+		file_put_contents($stateFile,json_encode($states));
+	}
+	
+
+	private static function getObject($pluginFile){
+		$plugin = new Plugin();
+		$fileLines = file_get_contents($pluginFile);
+
+		if(preg_match("#@author\s(.+)\s\<#", $fileLines, $match))
+			$plugin->setAuthor(trim($match[1]));
+			    
+		if(preg_match("#@author\s(.+)\s\<([a-z\@\.A-Z\s\-]+)\>#", $fileLines, $match))
+			$plugin->setMail(strtolower($match[2]));
+			    
+		if(preg_match("#@name\s(.+)[\r\n]#", $fileLines, $match))
+			$plugin->setName($match[1]);
+			    
+		if(preg_match("#@licence\s(.+)[\r\n]#", $fileLines, $match))
+			$plugin->setLicence($match[1]);
+			    
+		if(preg_match("#@version\s(.+)[\r\n]#", $fileLines, $match))
+			$plugin->setVersion($match[1]);
+			    
+		if(preg_match("#@link\s(.+)[\r\n]#", $fileLines, $match))
+			$plugin->setLink(trim($match[1]));
+
+		if(preg_match("#@type\s(.+)[\r\n]#", $fileLines, $match))
+			$plugin->setType(trim($match[1]));
+			    
+		if(preg_match("#@description\s(.+)[\r\n]#", $fileLines, $match))
+			$plugin->setDescription(trim($match[1]));
+			     
+		if(Plugin::loadState($pluginFile) || $plugin->getType()=='component'){
+			$plugin->setState(1);
+		}else{
+			$plugin->setState(0);
+		}
+		$plugin->setPath($pluginFile);
+		return $plugin;
+	}
+
+	public static function getAll(){
+		$pluginFiles = Plugin::getFiles(); 
+		$plugins = array();
+		if(is_array($pluginFiles)) {   
+			foreach($pluginFiles as $pluginFile) {  
+				$plugin = Plugin::getObject($pluginFile);
+				$plugins[]=$plugin;
+			}  
+		}
+		usort($plugins, "Plugin::sortPlugin");
+		return $plugins;
+	}
+
+	public static function getFiles($onlyActivated=false){
+
+		$enabled = $disabled =  array();
+		$files = glob(dirname(dirname(__FILE__)). Plugin::FOLDER .'/*/*.plugin*.php');
+		$plugins = array();
+		foreach($files as $file){
+			$plugins[] = Plugin::getObject($file);
+		}
+		usort($plugins, "Plugin::sortPlugin");
+		foreach($plugins as $plugin){
+			if($plugin->getState() || $plugin->getType() =='component'){
+				$enabled [] =  $plugin->getPath();
+			}else{
+				$disabled [] =  $plugin->getPath();
+			}
+		}
+		if(!$onlyActivated)$enabled = array_merge($enabled,$disabled);
+		return $enabled;
+	}
+
+	
+
+		public static function addHook($hookName, $functionName) {  
+		    $GLOBALS['hooks'][$hookName][] = $functionName;  
+		} 
+
+		
+		public static function callCss(){
+			$return='';
+		    if(isset($GLOBALS['hooks']['css_files'])) { 
+		        foreach($GLOBALS['hooks']['css_files'] as $css_file) {  
+		            $return .='<link href="'.$css_file.'" rel="stylesheet">'."\n";
+		        }  
+		    }    
+		    return $return;
+		}
+		
+		public static function addLink($rel, $link) {  
+		    $GLOBALS['hooks']['head_link'][] = array("rel"=>$rel, "link"=>$link);
+		}
+
+		public static function callLink(){
+			$return='';
+		    if(isset($GLOBALS['hooks']['head_link'])) { 
+		        foreach($GLOBALS['hooks']['head_link'] as $head_link) {  
+		            $return .='<link href="'.$head_link['link'].'" rel="'.$head_link['rel'].'" />'."\n";
+		        }
+		    }
+		    return $return;
+		}
+
+		public static function path(){
+			$bt =  debug_backtrace();
+			return Functions::relativePath(str_replace('\\','/',dirname(dirname(__FILE__))),str_replace('\\','/',dirname($bt[0]['file']))).'/'; 
+		}
+
+		public static function addCss($css,$force = false) {  
+			$bt =  debug_backtrace();
+			$module = isset($_GET['module'])?$_GET['module']:'';
+			$module = isset($_GET['section']) && $module==''?$_GET['section']:$module;
+			$module = isset($_GET['block']) && $module==''?$_GET['block']:$module;
+			$path = Functions::relativePath(str_replace('\\','/',dirname(dirname(__FILE__))),str_replace('\\','/',dirname($bt[0]['file']).$css));
+			if(strcasecmp(basename(dirname($bt[0]['file'])), $module) == 0  || $force)
+		    	$GLOBALS['hooks']['css_files'][] = $path;  
+		}
+
+		
+		public static function addJs($js,$force = false){  
+			global $_;
+			$bt =  debug_backtrace();
+			
+			$module = isset($_GET['module'])?$_GET['module']:'';
+			$module = isset($_GET['section']) && $module==''?$_GET['section']:$module;
+			$module = isset($_GET['block']) && $module==''?$_GET['block']:$module;
+			$path = Functions::relativePath(str_replace('\\','/',dirname(dirname(__FILE__))),str_replace('\\','/',dirname($bt[0]['file']).$js));
+			if(strcasecmp(basename(dirname($bt[0]['file'])), $module) == 0  || $force)
+		    	$GLOBALS['hooks']['js_files'][] = $path;  
+		}
+
+		public static function callJs(){
+			$return='';
+		    if(isset($GLOBALS['hooks']['js_files'])) {
+		        foreach($GLOBALS['hooks']['js_files'] as $js_file) {  
+		            $return .='<script type="text/javascript" src="'.$js_file.'"></script>'."\n";
+		        }  
+		    }    
+		    return $return;
+		}
+
+		public static function callHook($hookName, $hookArguments) { 
+
+			//echo '<div style="display:inline;background-color:#CC47CB;padding:3px;border:5px solid #9F1A9E;border-radius:5px;color:#ffffff;font-size:15px;">'.$hookName.'</div>';
+		    if(isset($GLOBALS['hooks'][$hookName])) { 
+		    	
+		        foreach($GLOBALS['hooks'][$hookName] as $functionName) {  
+		            call_user_func_array($functionName, $hookArguments);  
+		        }  
+		    }  
+		} 
+
+	//Définis si un plugin existe et si il est activé ou non
+	public static function exist($pluginName){
+		$exist = false;
+		$file = glob(dirname(dirname(__FILE__)). Plugin::FOLDER .'/'.$pluginName.'/*.plugin*.php');
+		if(count($file)!=0){
+			$plugin = Plugin::getObject($file[0]);
+			if($plugin->getState() || $plugin->getType() =='component'){
+				$exist = true;
+			}
+		}
+		return $exist;
+	}
+
+	
+
+	
+	public static function loadState($plugin){
+		$states = Plugin::getStates();
+		return (isset($states[$plugin])?$states[$plugin]:false);
+	}
+
+	public static function changeState($plugin,$state){
+		$states = Plugin::getStates();
+		$states[$plugin] = $state;
+
+		Plugin::setStates($states);
+	}
+
+
+	public static function enabled($pluginUid){
+		$plugins = Plugin::getAll();
+
+		foreach($plugins as $plugin){
+			if($plugin->getUid()==$pluginUid){
+				Plugin::changeState($plugin->getPath(),true);
+				$install = dirname($plugin->getPath()).'/install';
+				if(file_exists($install)) {
+					require_once($install);
+				}else if (file_exists($install.'.php')){
+					require_once($install.'.php');
+				}
+			}
+		}
+	}
+	public static function disabled($pluginUid){
+		$plugins = Plugin::getAll();
+		foreach($plugins as $plugin){
+			if($plugin->getUid()==$pluginUid){
+				Plugin::changeState($plugin->getPath(),false);
+				$uninstall = dirname($plugin->getPath()).'/uninstall';
+				if(file_exists($uninstall)){
+					require_once($uninstall);
+				}else if (file_exists($uninstall.'.php')){
+					require_once($uninstall.'.php');
+				}
+			}
+		}
+		
+	}
+
+	function getUid(){
+		$pathInfo = explode('/',$this->getPath()); 
+		$count = count($pathInfo);
+		$name = $pathInfo[$count-1];
+		return $pathInfo[$count -2].'-'.substr($name,0,strpos($name,'.'));
+	}
+
+
+	static function sortPlugin($a, $b){
+
+		if ($a->getName() == $b->getName()) 
+        	$result = 0;
+
+	    if($a->getName() < $b->getName()){
+	   		$result = -1;
+	    } else{
+	   		$result = 1;
+	    }
+
+	    if($b->getType() != $a->getType()){
+		    if($a->getType() == 'component'){
+				$result = -1;
+			}else{
+				$result = 1;
+			}
+		}
+	    return  $result;
+	}
+
+
+
+	function getName(){
+		return $this->name;
+	}
+
+	function setName($name){
+		$this->name = $name;
+	}
+
+	function setAuthor($author){
+		$this->author = $author;
+	}
+
+	function getAuthor(){
+		return $this->author;
+	}
+
+	function getMail(){
+		return $this->mail;
+	}
+
+	function setMail($mail){
+		$this->mail = $mail;
+	}
+
+	function getLicence(){
+		return $this->licence;
+	}
+
+	function setLicence($licence){
+		$this->licence = $licence;
+	}
+
+	function getPath(){
+		return $this->path;
+	}
+
+	function setPath($path){
+		$this->path = $path;
+	}
+
+	function getDescription(){
+		return $this->description;
+	}
+
+	function setDescription($description){
+		$this->description = $description;
+	}
+
+
+	function getLink(){
+		return $this->link;
+	}
+
+	function setLink($link){
+		$this->link = $link;
+	}
+
+	function getVersion(){
+		return $this->version;
+	}
+
+	function setVersion($version){
+		$this->version = $version;
+	}
+
+	function getState(){
+		return $this->state;
+	}
+	function setState($state){
+		$this->state = $state;
+	}
+
+	function getType(){
+		return $this->type;
+	}
+
+	function setType($type){
+		$this->type = $type;
+	}
+
+}
+
+?>

+ 51 - 0
classes/Rank.class.php

@@ -0,0 +1,51 @@
+<?php
+
+/*
+ @nom: Rank
+ @auteur: Idleman (idleman@idleman.fr)
+ @description:  Classe de gestion des utilisateurs
+ */
+
+class Rank extends SQLiteEntity{
+
+	protected $id,$label,$description;
+	protected $TABLE_NAME = 'rank';
+	protected $CLASS_NAME = 'Rank';
+	protected $object_fields = 
+	array(
+		'id'=>'key',
+		'label'=>'string',
+		'description'=>'string'
+	);
+
+	function __construct(){
+		parent::__construct();
+	}
+
+	function setId($id){
+		$this->id = $id;
+	}
+	
+	function getId(){
+		return $this->id;
+	}
+
+	function getLabel(){
+		return $this->label;
+	}
+
+	function setLabel($label){
+		$this->label = $label;
+	}
+
+	function getDescription(){
+		return $this->description;
+	}
+
+	function setDescription($description){
+		$this->description = $description;
+	}
+
+}
+
+?>

+ 86 - 0
classes/Right.class.php

@@ -0,0 +1,86 @@
+<?php
+
+/*
+ @nom: Right
+ @auteur: Idleman (idleman@idleman.fr)
+ @description:  Classe de gestion des utilisateurs
+ */
+
+class Right extends SQLiteEntity{
+
+	protected $id,$label,$description;
+	protected $TABLE_NAME = 'right';
+	protected $CLASS_NAME = 'Right';
+	protected $object_fields = 
+	array(
+		'id'=>'key',
+		'rank'=>'int',
+		'section'=>'string',
+		'read'=>'boolean',
+		'delete'=>'boolean',
+		'create'=>'boolean',
+		'update'=>'boolean'
+	);
+
+	function __construct(){
+		parent::__construct();
+	}
+
+	function setId($id){
+		$this->id = $id;
+	}
+	
+	function getId(){
+		return $this->id;
+	}
+
+	function getRank(){
+		return $this->rank;
+	}
+
+	function setRank($rank){
+		$this->rank = $rank;
+	}
+
+	function getSection(){
+		return $this->section;
+	}
+
+	function setSection($section){
+		$this->section = $section;
+	}
+
+	function getRead(){
+		return $this->read;
+	}
+
+	function setRead($read){
+		$this->read = $read;
+	}
+
+	function getCreate(){
+		return $this->create;
+	}
+
+	function setCreate($create){
+		$this->create = $create;
+	}
+
+	function getDelete(){
+		return $this->delete;
+	}
+
+	function setDelete($delete){
+		$this->delete = $delete;
+	}
+
+	function getUpdate(){
+		return $this->update;
+	}
+
+	function setUpdate($update){
+		$this->update = $update;
+	}
+}
+
+?>

+ 431 - 0
classes/SQLiteEntity.class.php

@@ -0,0 +1,431 @@
+<?php
+
+/*
+	@nom: SQLiteEntity
+	@auteur: Idleman (idleman@idleman.fr)
+	@description: Classe parent de tous les modèles (classe entitées) liées a la base de donnée,
+	 cette classe est configuré pour agir avec une base SQLite, mais il est possible de redefinir ses codes SQL pour l'adapter à un autre SGBD sans affecter 
+	 le reste du code du projet.
+
+*/
+
+
+class SQLiteEntity extends SQLite3
+{
+	
+	private $debug = false;
+	
+	function __construct($tag="rw"){
+		switch($tag){
+			case 'r' : $tag = SQLITE3_OPEN_READONLY; break;
+			default: $tag = SQLITE3_OPEN_READWRITE | SQLITE3_OPEN_CREATE; break;
+		}
+		$this->open(__ROOT__.'/'.DB_NAME,$tag);
+	}
+
+	function __destruct(){
+		 $this->close();
+	}
+
+	function sgbdType($type){
+		$return = false;
+		switch($type){
+			case 'string':
+			case 'timestamp':
+			case 'date':
+				$return = 'VARCHAR(255)';
+			break;
+			case 'longstring':
+				$return = 'longtext';
+			break;
+			case 'key':
+				$return = 'INTEGER NOT NULL PRIMARY KEY';
+			break;
+			case 'object':
+			case 'integer':
+				$return = 'bigint(20)';
+			break;
+			case 'boolean':
+				$return = 'INT(1)';
+			break;
+			default;
+				$return = 'TEXT';
+			break;
+		}
+		return $return ;
+	}
+	
+
+	
+	
+	public function closeDatabase(){
+		$this->close();
+	}
+
+
+	// GESTION SQL
+
+	/**
+	* Verifie l'existence de la table en base de donnée
+	* @author Valentin CARRUESCO
+	* @category manipulation SQL
+	* @param <String> créé la table si elle n'existe pas
+	* @return true si la table existe, false dans le cas contraire
+	*/
+	public function checkTable($autocreate = false){
+		$query = 'SELECT count(*) as numRows FROM sqlite_master WHERE type="table" AND name="'.MYSQL_PREFIX.$this->TABLE_NAME.'"';  
+		$statement = $this->query($query);
+
+		if($statement!=false){
+			$statement = $statement->fetchArray();
+			if($statement['numRows']==1){
+				$return = true;
+			}
+		}
+		if($autocreate && !$return) $this->create();
+		return $return;
+	}
+
+	/**
+	* Transforme l'objet en tableau attribut -> valeur
+	* @author Valentin CARRUESCO
+	* @return retourne un array attribut -> valeur correspondant à l'objet
+	*/
+	public function toArray(){
+		$array = array();
+		foreach($this->object_fields as $field=>$type)
+			$array[$field] = $this->$field;
+		return $array;
+	}
+	
+	/**
+	* Transforme le tableau fournis en objet, le nommage attribut -> valeur doit être respecté
+	* @author Valentin CARRUESCO
+	* @param <Array> tableau contenant les clé/valeur de l'objet
+	* @return retourne un objet attribut -> valeur correspondant à l'array fournis
+	*/
+	public function fromArray($array){
+		$object = new $this->CLASS_NAME();
+		foreach($this->object_fields as $field=>$type)
+			$object->$field = $array[$field];
+			
+		return $object;
+	}
+	
+	/**
+	* Methode de creation de l'entité
+	* @author Valentin CARRUESCO
+	* @category manipulation SQL
+	* @param <String> $debug='false' active le debug mode (0 ou 1)
+	* @return Aucun retour
+	*/
+	public function create($debug='false'){
+		$query = 'CREATE TABLE IF NOT EXISTS `'.MYSQL_PREFIX.$this->TABLE_NAME.'` (';
+
+		$i=false;
+		foreach($this->object_fields as $field=>$type){
+			if($i){$query .=',';}else{$i=true;}
+			$query .='`'.$field.'`  '. $this->sgbdType($type).'  NOT NULL';
+		}
+
+		$query .= ');';
+		if($this->debug)echo '<hr>'.$this->CLASS_NAME.' ('.__METHOD__ .') : Requete --> '.$query;
+		if(!$this->exec($query)) echo $this->lastErrorMsg();
+	}
+
+	public function drop($debug='false'){
+		$query = 'DROP TABLE `'.MYSQL_PREFIX.$this->TABLE_NAME.'`;';
+		if($this->debug)echo '<hr>'.$this->CLASS_NAME.' ('.__METHOD__ .') : Requete --> '.$query;
+		if(!$this->exec($query)) echo $this->lastErrorMsg();
+	}
+
+
+	public function massiveInsert($events,$forceId = false){
+		$query = 'INSERT INTO `'.MYSQL_PREFIX.$this->TABLE_NAME.'`(';
+			$i=false;
+			foreach($this->object_fields as $field=>$type){
+				if($type=='key' && !$forceId) continue;
+					if($i){$query .=',';}else{$i=true;}
+					$query .='`'.$field.'`';
+				
+			}
+			$query .=') select';
+			$u = false;
+			foreach($events as $event){
+				if($u){$query .=' union select ';}else{$u=true;}
+				
+				$i=false;
+				foreach($event->object_fields as $field=>$type){
+					if($type=='key' && !$forceId) continue;
+						if($i){$query .=',';}else{$i=true;}
+						$query .='"'.eval('return htmlentities($event->'.$field.');').'"';
+				}
+				
+			}
+
+			$query .=';';
+		//echo '<i>'.$this->CLASS_NAME.' ('.__METHOD__ .') : Requete --> '.$query.'<br>';
+		if(!$this->exec($query)) echo $this->lastErrorMsg().'</i>';
+
+	}
+
+	/**
+	* Methode d'insertion ou de modifications d'elements de l'entité
+	* @author Valentin CARRUESCO
+	* @category manipulation SQL
+	* @param  Aucun
+	* @return Aucun retour
+	*/
+	public function save(){
+	
+		if(isset($this->id)){
+			$query = 'UPDATE `'.MYSQL_PREFIX.$this->TABLE_NAME.'`';
+			$query .= ' SET ';
+
+			$i = false;
+			foreach($this->object_fields as $field=>$type){
+				if($i){$query .=',';}else{$i=true;}
+				$id = eval('return htmlentities($this->'.$field.');');
+				$query .= '`'.$field.'`="'.$id.'"';
+			}
+
+			$query .= ' WHERE `id`="'.$this->id.'";';
+		}else{
+			$query = 'INSERT INTO `'.MYSQL_PREFIX.$this->TABLE_NAME.'`(';
+			$i=false;
+			foreach($this->object_fields as $field=>$type){
+				if($type!='key'){
+					if($i){$query .=',';}else{$i=true;}
+					$query .='`'.$field.'`';
+				}
+			}
+			$query .=')VALUES(';
+			$i=false;
+			foreach($this->object_fields as $field=>$type){
+				if($type!='key'){
+					if($i){$query .=',';}else{$i=true;}
+					$query .='"'.eval('return htmlentities($this->'.$field.');').'"';
+				}
+			}
+
+			$query .=');';
+		}
+		if($this->debug)echo '<i>'.$this->CLASS_NAME.' ('.__METHOD__ .') : Requete --> '.$query.'<br>';
+		//var_dump ($query);
+		if(!$this->exec($query)) echo $this->lastErrorMsg().'</i>';
+		$this->id =  (!isset($this->id)?$this->lastInsertRowID():$this->id);
+	}
+
+	/**
+	* Méthode de modification d'éléments de l'entité
+	* @author Valentin CARRUESCO
+	* @category manipulation SQL
+	* @param <Array> $colonnes=>$valeurs
+	* @param <Array> $colonnes (WHERE) =>$valeurs (WHERE)
+	* @param <String> $operation="=" definis le type d'operateur pour la requete select
+	* @param <String> $debug='false' active le debug mode (0 ou 1)
+	* @return Aucun retour
+	*/
+	public function change($columns,$columns2=null,$operation='=',$debug='false'){
+		$query = 'UPDATE `'.MYSQL_PREFIX.$this->TABLE_NAME.'` SET ';
+		$i=false;
+		foreach ($columns as $column=>$value){
+			if($i){$query .=',';}else{$i=true;}
+			$query .= '`'.$column.'`="'.$value.'" ';
+		}
+
+		if($columns2!=null){
+			$query .=' WHERE '; 
+			$i=false;
+			foreach ($columns2 as $column=>$value){
+				if($i){$query .='AND ';}else{$i=true;}
+				$query .= '`'.$column.'`'.$operation.'"'.$value.'" ';
+			}
+		}
+
+		//echo '<hr>'.$this->CLASS_NAME.' ('.__METHOD__ .') : Requete --> '.$query.'<br>';
+		if(!$this->exec($query)) echo $this->lastErrorMsg();
+	}
+
+	/**
+	* Méthode de selection de tous les elements de l'entité
+	* @author Valentin CARRUESCO
+	* @category manipulation SQL
+	* @param <String> $ordre=null
+	* @param <String> $limite=null
+	* @param <String> $debug='false' active le debug mode (0 ou 1)
+	* @return <Array<Entity>> $Entity
+	*/
+	public function populate($order='null',$limit='null',$debug='false'){
+		eval('$results = '.$this->CLASS_NAME.'::loadAll(array(),\''.$order.'\','.$limit.',\'=\','.$debug.');');
+		return $results;
+	}
+
+
+	/**
+	* Méthode de selection multiple d'elements de l'entité
+	* @author Valentin CARRUESCO
+	* @category manipulation SQL
+	* @param <Array> $colonnes (WHERE)
+	* @param <Array> $valeurs (WHERE)
+	* @param <String> $ordre=null
+	* @param <String> $limite=null
+	* @param <String> $operation="=" definis le type d'operateur pour la requete select
+	* @param <String> $debug='false' active le debug mode (0 ou 1)
+	* @return <Array<Entity>> $Entity
+	*/
+	public function loadAll($columns,$order=null,$limit=null,$operation="=",$debug='false',$selColumn='*'){
+		$objects = array();
+		$whereClause = '';
+	
+			if($columns!=null && sizeof($columns)!=0){
+			$whereClause .= ' WHERE ';
+				$i = false;
+				foreach($columns as $column=>$value){
+
+					if($i){$whereClause .=' AND ';}else{$i=true;}
+					$whereClause .= '`'.$column.'`'.$operation.'"'.$value.'"';
+				}
+			}
+			$query = 'SELECT '.$selColumn.' FROM `'.MYSQL_PREFIX.$this->TABLE_NAME.'` '.$whereClause.' ';
+			if($order!=null) $query .='ORDER BY '.$order.' ';
+			if($limit!=null) $query .='LIMIT '.$limit.' ';
+			$query .=';';
+			  
+			$execQuery = $this->query($query);
+
+			if(!$execQuery) 
+				echo $this->lastErrorMsg();
+			while($queryReturn = $execQuery->fetchArray() ){
+				$object = new $this->CLASS_NAME();
+				foreach($this->object_fields as $field=>$type)
+					if(isset($queryReturn[$field])) $object->$field= html_entity_decode( addslashes($queryReturn[$field]));
+				
+				$objects[] = $object;
+				unset($object);
+			}
+			return $objects;
+	}
+
+	public function loadAllOnlyColumn($selColumn,$columns,$order=null,$limit=null,$operation="=",$debug='false'){
+		eval('$objects = $this->loadAll($columns,\''.$order.'\',\''.$limit.'\',\''.$operation.'\',\''.$debug.'\',\''.$selColumn.'\');');
+		if(count($objects)==0)$objects = array();
+		return $objects;
+	}
+
+	/**
+	* Méthode de selection unique d'élements de l'entité
+	* @author Valentin CARRUESCO
+	* @category manipulation SQL
+	* @param <Array> $colonnes (WHERE)
+	* @param <Array> $valeurs (WHERE)
+	* @param <String> $operation="=" definis le type d'operateur pour la requete select
+	* @param <String> $debug='false' active le debug mode (0 ou 1)
+	* @return <Entity> $Entity ou false si aucun objet n'est trouvé en base
+	*/
+	public function load($columns,$operation='=',$debug='false'){
+		eval('$objects = $this->loadAll($columns,null,\'1\',\''.$operation.'\',\''.$debug.'\');');
+		if(!isset($objects[0]))$objects[0] = false;
+		return $objects[0];
+	}
+
+	/**
+	* Méthode de selection unique d'élements de l'entité
+	* @author Valentin CARRUESCO
+	* @category manipulation SQL
+	* @param <Array> $colonnes (WHERE)
+	* @param <Array> $valeurs (WHERE)
+	* @param <String> $operation="=" definis le type d'operateur pour la requete select
+	* @param <String> $debug='false' active le debug mode (0 ou 1)
+	* @return <Entity> $Entity ou false si aucun objet n'est trouvé en base
+	*/
+	public function getById($id,$operation='=',$debug='false'){
+		
+		return $this->load(array('id'=>$id),$operation,$debug);
+	}
+
+	/**
+	* Methode de comptage des éléments de l'entité
+	* @author Valentin CARRUESCO
+	* @category manipulation SQL
+	* @param <String> $debug='false' active le debug mode (0 ou 1)
+	* @return<Integer> nombre de ligne dans l'entité'
+	*/
+	public function rowCount($columns=null)
+	{
+		$whereClause ='';
+		if($columns!=null){
+			$whereClause = ' WHERE ';
+			$i=false;
+			foreach($columns as $column=>$value){
+					if($i){$whereClause .=' AND ';}else{$i=true;}
+					$whereClause .= '`'.$column.'`="'.$value.'"';
+			}
+		}
+		$query = 'SELECT COUNT(id) FROM '.MYSQL_PREFIX.$this->TABLE_NAME.$whereClause;
+		//echo '<hr>'.$this->CLASS_NAME.' ('.__METHOD__ .') : Requete --> '.$query.'<br>';
+		$execQuery = $this->querySingle($query);
+		//echo $this->lastErrorMsg();
+		return (!$execQuery?0:$execQuery);
+	}	
+	
+	/**
+	* Méthode de supression d'elements de l'entité
+	* @author Valentin CARRUESCO
+	* @category manipulation SQL
+	* @param <Array> $colonnes (WHERE)
+	* @param <Array> $valeurs (WHERE)
+	* @param <String> $operation="=" definis le type d'operateur pour la requete select
+	* @param <String> $debug='false' active le debug mode (0 ou 1)
+	* @return Aucun retour
+	*/
+	public function delete($columns,$operation='=',$debug='false',$limit=null){
+		$whereClause = '';
+
+			$i=false;
+			foreach($columns as $column=>$value){
+				if($i){$whereClause .=' AND ';}else{$i=true;}
+				$whereClause .= '`'.$column.'`'.$operation.'"'.$value.'"';
+			}
+			$query = 'DELETE FROM `'.MYSQL_PREFIX.$this->TABLE_NAME.'` WHERE '.$whereClause.' '.(isset($limit)?'LIMIT '.$limit:'').';';
+			//echo '<hr>'.$this->CLASS_NAME.' ('.__METHOD__ .') : Requete --> '.$query.'<br>';
+			if(!$this->exec($query)) echo $this->lastErrorMsg();
+	}
+	
+	public function customExecute($request){
+		$this->exec($request);
+	}
+	public function customQuery($request){
+		return $this->query($request);
+	}
+
+	// ACCESSEURS
+		/**
+	* Méthode de récuperation de l'attribut debug de l'entité
+	* @author Valentin CARRUESCO
+	* @category Accesseur
+	* @param Aucun
+	* @return <Attribute> debug
+	*/
+	
+	public function getDebug(){
+		return $this->debug;
+	}
+	
+	/**
+	* Méthode de définition de l'attribut debug de l'entité
+	* @author Valentin CARRUESCO
+	* @category Accesseur
+	* @param <boolean> $debug 
+	*/
+
+	public function setDebug($debug){
+		$this->debug = $debug;
+	}
+
+	public function getObject_fields(){
+		return $this->object_fields;
+	}
+
+}
+?>

+ 82 - 0
classes/Section.class.php

@@ -0,0 +1,82 @@
+<?php
+
+/*
+ @nom: Section
+ @auteur: Idleman (idleman@idleman.fr)
+ @description:  Classe de gestion des sections
+ */
+
+class Section extends SQLiteEntity{
+
+	protected $id,$label,$description;
+	protected $TABLE_NAME = 'section';
+	protected $CLASS_NAME = 'Section';
+	protected $object_fields = 
+	array(
+		'id'=>'key',
+		'label'=>'string',
+		'description'=>'longstring'
+	);
+
+
+	public static function add($name,$description="",$grantAdmin = true){
+		$sectionManager = new Section();
+		if($sectionManager->rowCount(array('label'=>$name))==0){
+			$sectionManager->setLabel($name);
+			$sectionManager->setDescription($description);
+			$sectionManager->save();
+
+			if($grantAdmin){
+				$right = new Right();
+				$right = $right->load(array('section'=>$sectionManager->getLabel(),'rank'=>1));
+				$right = (!$right?new Right():$right);
+				$right->setSection($sectionManager->getId());
+				$right->setCreate(1);
+				$right->setRead(1);
+				$right->setUpdate(1);
+				$right->setDelete(1);
+				$right->setRank(1);
+				$right->save();
+			}
+		}
+	}
+
+	public static function remove($name){
+		$sectionManager = new Section();
+		$sectionManager->load(array('label'=>$name));
+		$rightManager = new Right();
+		$rightManager->delete(array('section'=>$sectionManager->getId()));
+		$sectionManager->delete(array('id'=>$sectionManager->getId()));
+	}
+
+
+	function __construct(){
+		parent::__construct();
+	}
+
+	function setId($id){
+		$this->id = $id;
+	}
+	
+	function getId(){
+		return $this->id;
+	}
+
+	function getLabel(){
+		return $this->label;
+	}
+
+	function setLabel($label){
+		$this->label = $label;
+	}
+
+	function getDescription(){
+		return $this->description;
+	}
+
+	function setDescription($description){
+		$this->description = $description;
+	}
+}
+
+?>

+ 267 - 0
classes/System.class.php

@@ -0,0 +1,267 @@
+<?php
+class System{
+
+	public static function getInfos(){
+		$return = self::commandSilent('cat /proc/cpuinfo');
+		$lines = preg_split("/\\r\\n|\\r|\\n/", $return);
+		$infos = array();
+		foreach($lines as $line){
+			if(strpos($line,':') === false ) continue;
+			list($key,$value)  = explode(':',$line);
+			$infos[trim($key)] = trim($value);
+		}
+		return $infos;
+	}
+	
+	public static function getPinForModel($model,$version){
+		$model = $model.$version;
+		
+		
+		//@TODO re-check pins mapping for revisions / types (it's fucking nightmare)
+		//remi : It would be easier to use the output of gpio readall
+		$pins = array();
+		
+		// rev 1.0 type B
+		$pins['b1.0'] = array(
+							array(
+								//name,function,wiringPiNumber,bcmNumber,physicalNumber
+								new Gpio('3.3V','Alimentation',-1,-1,1),
+								new Gpio('SDA0','I2C',8,0,3),
+								new Gpio('SCL0','I2C',9,1,5),
+								new Gpio('GPIO 7','',7,4,7),
+								new Gpio('DNC','Masse (GND)',-1,-1,9),
+								new Gpio('GPIO 0','',0,17,11),
+								new Gpio('GPIO 2','',2,21,13),
+								new Gpio('GPIO 3','',3,22,15),
+								new Gpio('DNC','Masse (GND)',-1,-1,17),
+								new Gpio('MOSI','SPI',12,10,19),
+								new Gpio('MISO','SPI',13,9,21),
+								new Gpio('SCLK','SPI',14,11,23),
+								new Gpio('DNC','',-1,-1,25),
+							),
+							array(
+								new Gpio('5V','Alimentation',-1,-1,2),
+								new Gpio('DNC','Masse (GND)',-1,-1,4),
+								new Gpio('0V','Masse (GND)',-1,-1,6),
+								new Gpio('TxD','UART (Transmission)',14,14,8),
+								new Gpio('RxD','UART (Réception)',15,15,10),
+								new Gpio('GPIO 1','',1,18,12),
+								new Gpio('DNC','Masse (GND)',-1,-1,14),
+								new Gpio('GPIO 4','',4,23,16),
+								new Gpio('GPIO 5','',5,24,18),
+								new Gpio('DNC','Masse (GND)',-1,-1,20),
+								new Gpio('GPIO 6','',6,25,22),
+								new Gpio('CE 0','SPI',10,8,24),
+								new Gpio('CE 1','SPI',11,7,26),
+							)
+						);
+		$pins['a1.0'] = $pins['b1.0'];
+		// rev 2.0 type B
+		$pins['b2.0'] = array(
+							array(
+								//name,function,wiringPiNumber,bcmNumber,physicalNumber
+								new Gpio('3.3V','Alimentation',-1,-1,1),
+								new Gpio('SDA0','I2C',8,2,3),
+								new Gpio('SCL0','I2C',9,3,5),
+								new Gpio('GPIO 7','',7,4,7),
+								new Gpio('DNC','Masse (GND)',-1,-1,9),
+								new Gpio('GPIO 0','',0,17,11),
+								new Gpio('GPIO 2','',2,27,13),
+								new Gpio('GPIO 3','',3,22,15),
+								new Gpio('DNC','Masse (GND)',-1,-1,17),
+								new Gpio('MOSI','SPI',12,10,19),
+								new Gpio('MISO','SPI',13,9,21),
+								new Gpio('SCLK','SPI',14,11,23),
+								new Gpio('DNC','',-1,-1,25),
+							),
+							array(
+								new Gpio('5V','Alimentation',-1,-1,2),
+								new Gpio('DNC','Masse (GND)',-1,-1,4),
+								new Gpio('0V','Masse (GND)',-1,-1,6),
+								new Gpio('TxD','UART (Transmission)',14,14,8),
+								new Gpio('RxD','UART (Réception)',15,15,10),
+								new Gpio('GPIO 1','',1,18,12),
+								new Gpio('DNC','Masse (GND)',-1,-1,14),
+								new Gpio('GPIO 4','',4,23,16),
+								new Gpio('GPIO 5','',5,24,18),
+								new Gpio('DNC','Masse (GND)',-1,-1,20),
+								new Gpio('GPIO 6','',6,25,22),
+								new Gpio('CE 0','SPI',10,8,24),
+								new Gpio('CE 1','SPI',11,7,26),
+							)
+						);
+		$pins['a2.0'] = $pins['b2.0'];
+		
+		// type B+
+		// @maditnerd I fix the gpio numbering , some were wrong.
+		$pins['b+1.0'] = array(
+							array(
+								//name,function,wiringPiNumber,bcmNumber,physicalNumber
+								new Gpio('3.3V','Alimentation',-1,-1,1),
+								new Gpio('SDA1','I2C',8,2,3),
+								new Gpio('SCL1','I2C',9,3,5),
+								new Gpio('GPIO 7','',7,4,7),
+								new Gpio('DNC','Masse (GND)',-1,-1,9),
+								new Gpio('GPIO 0','',0,17,11),
+								new Gpio('GPIO 2','',2,27,13),
+								new Gpio('GPIO 3','',3,22,15),
+								new Gpio('3.3V','Alimentation',-1,-1,1),
+								new Gpio('MOSI','SPI',12,10,19),
+								new Gpio('MISO','SPI',13,9,21),
+								new Gpio('SCLK','SPI',14,11,23),
+								new Gpio('DNC','Masse (GND)',-1,-1,25),
+								new Gpio('SDA0','I2C',30,0,27),
+								new Gpio('GPIO 21','',21,5,29),
+								new Gpio('GPIO 22','',22,6,31),
+								new Gpio('GPIO 23','',23,13,33),
+								new Gpio('GPIO 24','',24,19,35),
+								new Gpio('GPIO 25','',25,26,37),
+								new Gpio('0V','Masse (GND)',-1,-1,39),
+							),
+							array(
+								new Gpio('5V','Alimentation',-1,-1,2),
+								new Gpio('5V','Alimentation',-1,-1,4),
+								new Gpio('0V','Masse',-1,-1,6),
+								new Gpio('TxD','UART (Transmission)',15,15,8),
+								new Gpio('RxD','UART (Réception)',16,16,10),
+								new Gpio('GPIO 1','',1,18,12),
+								new Gpio('0V','Masse (GND)',-1,-1,14),
+								new Gpio('GPIO 4','',4,23,16),
+								new Gpio('GPIO 5','',5,24,18),
+								new Gpio('0V','Masse (GND)',-1,-1,20),
+								new Gpio('GPIO 6','',6,25,22),
+								new Gpio('CE 0','SPI',10,8,24),
+								new Gpio('CE 1','SPI',11,7,26),
+								new Gpio('SCL 0','I2C ID EEPROM',-1,-1,28),
+								new Gpio('0V','Masse (GND)',-1,-1,30),
+								new Gpio('GPIO 26','PWM0',26,12,32),
+								new Gpio('0V','Masse (GND)',-1,-1,34),
+								new Gpio('GPIO 27','',27,16,36),
+								new Gpio('GPIO 28','',28,20,38),
+								new Gpio('GPIO 29','',29,21,40),
+								
+							)
+						);
+
+		//type B2
+		$pins['b21.0'] = $pins['b+1.0'];
+		//A3
+		$pins['a3.0'] = $pins['b21.0'];
+		//Zero
+		$pins['zero1.0'] = $pins['a3.0'];
+
+		 // Banana PI M1
+		$pins['M11.0'] = array(
+			array(
+				new Gpio('3.3V','Alimentation',-1,-1,1),
+				new Gpio('SDA0','I2C',8,0,3),
+				new Gpio('SCL0','I2C',9,1,5),
+				new Gpio('GPIO 7','',7,4,7),
+				new Gpio('0V','Masse (GND)',-1,-1,9),
+				new Gpio('GPIO 0','',0,17,11),
+				new Gpio('GPIO 2','',2,21,13),
+				new Gpio('GPIO 3','',3,22,15),
+				new Gpio('3.3V','Alimentation',-1,-1,17),
+				new Gpio('MOSI','SPI',12,10,19),
+				new Gpio('MISO','SPI',13,9,21),
+				new Gpio('SCLK','SPI',14,11,23),
+				new Gpio('0V','Masse (GND)',-1,-1,25),
+	            new Gpio('',' ',-1,-1,-1),
+	            new Gpio('5V','Alimentation',-1,-1,1),
+	            new Gpio('GPIO 8','',17,28,3),
+	            new Gpio('GPIO 10','',19,30,5),
+	            new Gpio('0V','Masse (GND)',-1,-1,7),
+            ),
+            array(
+				new Gpio('5V','Alimentation',-1,-1,2),
+				new Gpio('5V','Alimentation',-1,-1,4),
+				new Gpio('0V','Masse (GND)',-1,-1,6),
+				new Gpio('TxD','UART (Transmission)',14,14,8),
+				new Gpio('RxD','UART (Réception)',15,15,10),
+				new Gpio('GPIO 1','',1,18,12),
+				new Gpio('0V','Masse (GND)',-1,-1,14),
+				new Gpio('GPIO 4','',4,23,16),
+				new Gpio('GPIO 5','',5,24,18),
+				new Gpio('0V','Masse (GND)',-1,-1,20),
+				new Gpio('GPIO 6','',6,25,22),
+				new Gpio('CE 0','SPI',10,8,24),
+				new Gpio('CE 1','SPI',11,7,26),
+				new Gpio('',' ',-1,-1,-1),
+				new Gpio('3.3V','Alimentation',-1,-1,2),
+				new Gpio('Gpio 9 Tx','UART (Transmission)',18,29,4),
+				new Gpio('Gpio 11 Rx','UART (Réception)',20,31,6),
+				new Gpio('0V','Masse (GND)',-1,-1,8),
+            )
+        );
+
+
+
+		return isset($pins[$model])?$pins[$model]:$pins['b1.0'];
+	}
+	
+	public static function getModel(){
+		$infos = self::getInfos();
+		$deductionArray = array(
+			'0002' => array('ram'=>'256','version'=>'1.0','type'=>'b','revision'=>'0002'),
+			'0003' => array('ram'=>'256','version'=>'1.0+ecn0001','type'=>'b','revision'=>'0003'),
+			'0004' => array('ram'=>'256','version'=>'2.0','type'=>'b','revision'=>'0004'),
+			'0005' => array('ram'=>'256','version'=>'2.0','type'=>'b','revision'=>'0005'),
+			'0006' => array('ram'=>'256','version'=>'2.0','type'=>'b','revision'=>'0006'),
+			'0007' => array('ram'=>'256','version'=>'1.0','type'=>'a','revision'=>'0007'),
+			'0008' => array('ram'=>'256','version'=>'1.0','type'=>'a','revision'=>'0008'),
+			'0009' => array('ram'=>'256','version'=>'1.0','type'=>'a','revision'=>'0009'),
+			'0010' => array('ram'=>'512','version'=>'1.0','type'=>'b+','revision'=>'0010'),
+			'0011' => array('ram'=>'512','version'=>'1.0','type'=>'compute','revision'=>'0011'),
+			'0012' => array('ram'=>'256','version'=>'1.0','type'=>'a+','revision'=>'0012'),
+			'0013' => array('ram'=>'512','version'=>'1.0','type'=>'b+','revision'=>'0013'),
+			'000d' => array('ram'=>'512','version'=>'2.0','type'=>'b','revision'=>'000d'),
+			'000e' => array('ram'=>'512','version'=>'2.0','type'=>'b','revision'=>'000e'),
+			'000f' => array('ram'=>'512','version'=>'2.0','type'=>'b','revision'=>'000f'),
+			'000f' => array('ram'=>'512','version'=>'2.0','type'=>'b','revision'=>'000f'),
+			'900032' => array('ram'=>'512','version'=>'2.0','type'=>'b','revision'=>'900032'),
+			'a01041' => array('ram'=>'1024','version'=>'1.0','type'=>'b2','revision'=>'a01041'),
+			'1a01041' => array('ram'=>'1024','version'=>'1.0','type'=>'b2','revision'=>'1a01041'),
+			'a21041' => array('ram'=>'1024','version'=>'1.0','type'=>'b2','revision'=>'a21041'),
+			'2a01041' => array('ram'=>'1024','version'=>'1.0','type'=>'b2','revision'=>'2a01041'),
+			'a02082' => array('ram'=>'1024','version'=>'3.0','type'=>'a','revision'=>'a02082'),
+			'a22082' => array('ram'=>'1024','version'=>'3.0','type'=>'a','revision'=>'a22082'), 
+			'900092' => array('ram'=>'512','version'=>'1.0','type'=>'zero','revision'=>'900092'),
+			'9000c1' => array('ram'=>'512','version'=>'1.0','type'=>'zero','revision'=>'9000c1'),
+			'0000' => array('ram'=>'1024','version'=>'1.0','type'=>'M1','revision'=>'0000')
+		);
+		if(PHP_OS=='WINNT') $infos['Revision'] = 'a01041';//for dev mode on windows only
+		return isset($deductionArray[$infos['Revision']]) ? $deductionArray[$infos['Revision']] :array('ram'=>'0','version'=>'0','type'=>'unknown','revision'=>$infos['Revision']);
+	}
+
+	public static function commandSilent($cmd){
+		Functions::log('Launch system command (without output): '.$cmd);
+		return shell_exec($cmd);
+	}
+	
+
+	public static function command($cmd){
+		Functions::log('Launch system command : '.$cmd);
+		return system($cmd);
+	}
+	
+	public static function gpio() {
+		$model = self::getModel();
+		$pinsRange = self::getPinForModel($model['type'],$model['version']);
+		$gpios = array();
+		foreach($pinsRange as $range){
+				foreach($range as $pin){
+					if(PHP_OS=='WINNT'){
+						$gpios[$pin->wiringPiNumber] = rand(0,1);
+						continue;
+					}
+					if($pin->wiringPiNumber<0) continue;
+					$gpios[$pin->wiringPiNumber] = exec(GPIO::GPIO_DEFAULT_PATH." read ".$pin->wiringPiNumber, $out);				
+				}
+		}
+		return $gpios;
+  }
+
+
+	
+}
+?>

+ 219 - 0
classes/User.class.php

@@ -0,0 +1,219 @@
+<?php
+
+/*
+ @nom: User
+ @auteur: Idleman (idleman@idleman.fr)
+ @description:  Classe de gestion des utilisateurs
+ */
+
+class User extends SQLiteEntity{
+
+	protected $id,$login,$password,$name,$firstname,$mail,$state,$groups,$rank,$rights,$phone,$token,$cookie;
+	protected $TABLE_NAME = 'user';
+	protected $CLASS_NAME = 'User';
+	protected $object_fields = 
+	array(
+		'id'=>'key',
+		'login'=>'string',
+		'password'=>'string',
+		'name'=>'string',
+		'firstname'=>'string',
+		'mail'=>'string',
+		'rank'=>'int',
+		'token'=>'string',
+		'state'=>'int',
+		'cookie'=>'string'
+	);
+
+	function __construct(){
+		parent::__construct();
+	}
+
+	function setId($id){
+		$this->id = $id;
+	}
+
+	//Teste la validité d'un compte à l'identification
+	static function exist($login,$password,$force = false,$checkRights = true ){
+		$userManager = new User();
+	    $newUser = false;
+	    if($force){
+	    	$newUser = $userManager->load(array('login'=>$login));
+	    }else{
+	    	$newUser = $userManager->load(array('login'=>$login,'password'=>sha1(md5($password))));
+	    }
+	   
+
+	    Plugin::callHook("action_pre_login", array(&$newUser));
+	   	if(is_object($newUser) && $checkRights) $newUser->loadRight();
+		return $newUser;
+	}
+
+	//Récupère les droits en CRUD de l'utilisateur courant et les charge dans son tableau de droits interne
+	function loadRight(){
+		$rightManager = new Right();
+
+		$rights = $rightManager->loadAll(array('rank'=>$this->getRank()));
+
+		$sectionManager= new Section();
+		foreach($rights as $right){
+			$section = $sectionManager->getById($right->getSection());
+			if(is_object($section)){
+				$this->rights[$section->getLabel()]['c'] = ($right->getCreate()=='1'?true:false);
+				$this->rights[$section->getLabel()]['r'] = ($right->getRead()=='1'?true:false);
+				$this->rights[$section->getLabel()]['u'] = ($right->getUpdate()=='1'?true:false);
+				$this->rights[$section->getLabel()]['d'] = ($right->getDelete()=='1'?true:false);
+			}else{
+				//Supression suite à des problèmes de perte de section quand la base plante
+				//TODO - A modifier en evitant cette action sur les erreurs SQL type database locked
+				//$rightManager->delete(array('section'=>$right->getSection()));
+			}
+		}
+	}
+
+
+	static function getByLogin($login){
+		$returnedUser = new User();
+		$users = User::getAllUsers();
+		foreach($users as $user){
+			if($user->getLogin()==$login) $returnedUser = $user;
+		}
+		return $returnedUser;
+	}
+
+	//Retourne une liste d'objets contenant tout les utilisateurs habilités à se connecter au programme
+	//@return Liste d'objets User
+	static function getAllUsers(){
+		$userManager = new User();
+		$users = $userManager->populate();
+		Plugin::callHook("user_get_all", array(&$users));
+		usort($users, "User::userorder");
+		return $users;
+	}
+
+	static function userorder($a, $b)
+	{
+	    return strcmp($a->getName(), $b->getName());
+	}
+
+
+	function getGravatar($size = 100){
+		$gravatar = AVATAR_FOLDER.'/'.$this->getMail().'-'.$size.'.jpg';
+		if(!file_exists($gravatar)){
+			if (!file_exists(AVATAR_FOLDER)) mkdir(AVATAR_FOLDER);
+			file_put_contents($gravatar, file_get_contents("http://www.gravatar.com/avatar/" . md5( strtolower( trim( $this->getMail() ) ) ) . "?&s=".$size));
+		}
+		return $gravatar;
+	}
+	function getGravatarImg($size = 100){
+		return "<img class='avatar avatar-".$size."' src='".$this->getGravatar($size)."' />" ;
+	}
+
+	function can($section,$selectedRight){
+		return (!isset($this->rights[$section])?false:$this->rights[$section][$selectedRight]);
+	}
+
+	function haveGroup($group){
+		return in_array($group,$this->getGroups());
+	}
+	
+	function getId(){
+		return $this->id;
+	}
+
+	function getLogin(){
+		return $this->login;
+	}
+
+	function setLogin($login){
+		$this->login = $login;
+	}
+
+	function getFullName(){
+		$fn = ucfirst($this->firstname).' '.strtoupper($this->name);
+		return trim($fn)==''?'Anonymous Guy':$fn;
+	}
+
+	function getName(){
+		return $this->name;
+	}
+
+	function getFirstName(){
+		return $this->firstname;
+	}
+
+	function getMail(){
+		return $this->mail;
+	}
+
+	function getState(){
+		return $this->state;
+	}
+
+	function setName($name){
+		$this->name = $name;
+	}
+
+	function setFirstName($firstname){
+		$this->firstname = $firstname;
+	}
+
+	function setMail($mail){
+		$this->mail = $mail;
+	}
+
+	function setState($state){
+		$this->state = $state;
+	}
+
+	function setGroups($groups){
+		$this->groups = $groups;
+	}
+
+	function getGroups(){
+		return (is_array($this->groups)?$this->groups:array());
+	}
+
+	function getRank(){
+		return $this->rank;
+	}
+	function setRank($rank){
+		$this->rank = $rank;
+		$this->loadRight();
+	}
+
+	function setPassword($password){
+		$this->password = User::cryptPassword($password);
+	}
+
+	public static function cryptPassword($string){
+		return sha1(md5($string));
+	}
+
+	function setPhone($phone){
+		$this->phone = $phone;
+	}
+
+	function getPhone(){
+		return $this->phone;
+	}
+	function setToken($token){
+		$this->token = $token;
+	}
+
+	function getToken(){
+		return $this->token;
+	}
+
+	function setCookie($cookie){
+		$this->cookie = $cookie;
+	}
+
+	function getCookie(){
+		return $this->cookie;
+	}
+
+
+}
+
+?>

+ 189 - 0
common.php

@@ -0,0 +1,189 @@
+<?php
+session_name('yana-server'); 
+session_start();
+$start=microtime(true);
+ini_set('display_errors','1');
+
+error_reporting(E_ALL & ~E_NOTICE);
+//Calage de la date
+setlocale( LC_ALL , "fr_FR" );
+date_default_timezone_set('Europe/Paris'); 
+
+
+//Idleman : Active les notice uniquement pour ma config reseau (pour le débug), pour les user il faut la désactiver
+//car les notices peuvent gener les reponses json, pour les dev ajoutez votre config dans une même if en dessous.
+if($_SERVER["HTTP_HOST"]=='192.168.0.14' && $_SERVER['REMOTE_ADDR']=='192.168.0.69') error_reporting(E_ALL); 
+
+mb_internal_encoding('UTF-8');
+
+if(!file_exists(__DIR__ .DIRECTORY_SEPARATOR.'constant.php')) header('location:install.php');
+require_once(__DIR__ .DIRECTORY_SEPARATOR.'constant.php');
+
+global $myUser,$conf,$_;
+//Récuperation et sécurisation de toutes les variables POST et GET
+$_ = array_map('Functions::secure',array_merge($_POST,$_GET));
+$error = '';
+
+
+
+
+
+$versions = json_decode(file_get_contents(__ROOT__.DIRECTORY_SEPARATOR.'db.json'),true);
+
+
+if(!file_exists(__ROOT__.DIRECTORY_SEPARATOR.DB_NAME) || (file_exists(__ROOT__.DIRECTORY_SEPARATOR.DB_NAME) && filesize(__ROOT__.DIRECTORY_SEPARATOR.DB_NAME)==0)){
+	file_put_contents(__ROOT__.'/dbversion',$versions[0]['version']);
+	header('location:install.php');
+}else{
+	if(file_exists(__ROOT__.DIRECTORY_SEPARATOR.'install.php')) $error .= ($error!=''?'<br/>':'').'<strong>Attention: </strong> Par mesure de sécurité, pensez à supprimer le fichier install.php';
+}
+
+if(file_exists(__ROOT__.DIRECTORY_SEPARATOR.'db.json')){
+	if(!file_exists(__ROOT__.DIRECTORY_SEPARATOR.'dbversion')) file_put_contents(__ROOT__.DIRECTORY_SEPARATOR.'dbversion', '0');
+	$current = file_get_contents(__ROOT__.DIRECTORY_SEPARATOR.'dbversion');
+	$versions = json_decode(file_get_contents(__ROOT__.DIRECTORY_SEPARATOR.'db.json'),true);
+	if($current<$versions[0]['version']){
+		Functions::alterBase($versions,$current);
+		file_put_contents(__ROOT__.DIRECTORY_SEPARATOR.'dbversion',$versions[0]['version']);
+	}
+}
+
+require_once(__ROOT__.DIRECTORY_SEPARATOR.'RainTPL.php');
+
+$error = (isset($_['error']) && $_['error']!=''?'<strong>Erreur: </strong> '.str_replace('|','<br/><strong>Erreur: </strong> ',(urldecode($_['error']))):false);
+$message = (isset($_['notice']) && $_['notice']!=''?'<strong>Message: </strong> '.str_replace('|','<br/><strong>Message: </strong> ',(urldecode($_['notice']))):false);
+
+function __autoload($class_name){
+    require_once(__ROOT__.DIRECTORY_SEPARATOR.'classes'.DIRECTORY_SEPARATOR.$class_name . '.class.php');
+}
+
+
+if(file_exists(__ROOT__.DIRECTORY_SEPARATOR.'.tool.php')){
+	require_once(__ROOT__.DIRECTORY_SEPARATOR.'.tool.php');
+	
+	switch($tool->type){
+	case 'reset_password':
+		if($tool->login != null && $tool->password != null){
+			$userManager = new User();
+			$usr = $userManager->load(array('login'=>$tool->login));
+			$usr->setPassword($tool->password);
+			$usr->save();
+			unlink(__ROOT__.DIRECTORY_SEPARATOR.'.tool.php');
+		}
+	break;
+	}
+}
+
+
+
+$myUser = false;
+$conf = new Configuration();
+$conf->getAll();
+//Inclusion des plugins  
+Plugin::includeAll($conf->get("DEFAULT_THEME"));
+
+
+$userManager = new User();
+
+if(isset($_SESSION['currentUser'])){
+	$myUser =unserialize($_SESSION['currentUser']);
+}else{
+	if(AUTO_LOGIN!=''){
+		$myUser = $userManager->exist(AUTO_LOGIN,'',true);
+		$_SESSION['currentUser'] = serialize($myUser);
+	}
+}
+if(!$myUser && isset($_COOKIE[$conf->get('COOKIE_NAME')])){
+	$users = User::getAllUsers();
+	foreach ($users as $user) {
+		if($user->getCookie() == $_COOKIE[$conf->get('COOKIE_NAME')]) 
+			{
+				$myUser = $user;
+				$myUser->loadRight();
+			}
+	}
+}
+
+
+
+//Instanciation du template
+$tpl = new RainTPL();
+
+
+//Definition des dossiers de template
+raintpl::configure("base_url", null );
+raintpl::configure("tpl_dir", './templates/'.$conf->get('DEFAULT_THEME').'/' );
+raintpl::configure("cache_dir", './cache/tmp/' );
+$view = '';
+
+$rank = new Rank();
+if($myUser!=false && $myUser->getRank()!=false){
+	$rank = $rank->getById($myUser->getRank());
+}
+
+
+function common_listen($command,$text,$confidence,$user){
+	echo "\n".'diction de la commande : '.$command;
+	
+	$response = array();
+	Plugin::callHook("vocal_command", array(&$response,YANA_URL.'/action.php'));
+	$commands = array();
+	echo "\n".'Test de comparaison avec '.count($response['commands']).' commandes';
+	foreach($response['commands'] as $cmd){
+		if($command != $cmd['command']) continue;
+		if(!isset($cmd['parameters'])) $cmd['parameters'] = array();
+		if(isset($cmd['callback'])){
+			//Catch des commandes pour les plugins en format client v2
+			echo "\n".'Commande trouvée, execution de la fonction plugin '.$cmd['callback'];
+			call_user_func($cmd['callback'],$text,$confidence,$cmd['parameters'],$user);
+		}else{
+			//Catch des commandes pour les plugins en format  client v1
+			echo "\n".'Commande ancien format trouvée, execution de l\'url '.$cmd['url'].'&token='.$user->getToken();
+			$result = file_get_contents($cmd['url'].'&token='.$user->getToken());
+			$result = json_decode($result,true);
+
+			if(is_array($result)){
+				$client=new Client();
+				$client->connect();
+
+				if(is_array($result['responses'])){
+					foreach($result['responses'] as $resp){
+						
+						switch($resp['type']){
+							case 'talk':
+								$client->talk($resp['sentence']);					
+							break;
+							case 'sound':
+								$client->sound($resp['file']);					
+							break;
+							case 'command':
+								$client->execute($resp['program']);					
+							break;
+						}
+					}
+				}
+
+				$client->disconnect();
+			}
+		}
+	}
+
+}
+
+
+Plugin::addHook("listen", "common_listen");
+
+
+
+
+
+
+$tpl->assign('myUser',$myUser);
+$tpl->assign('userManager',$userManager);
+$tpl->assign('configurationManager',$conf);
+$tpl->assign('error',$error);
+$tpl->assign('notice',$message);
+$tpl->assign('_',$_);
+$tpl->assign('action','');
+$tpl->assign('rank',$rank);
+?>

+ 37 - 0
constant.sample.php

@@ -0,0 +1,37 @@
+<?php
+	/* Nom du programme */ 
+	define('PROGRAM_NAME','Yana Server');
+	/* Nom de l'auteur principal */
+	define('PROGRAM_AUTHOR','Valentin CARRUESCO');
+	/* Préfixe de la base de données */ 
+	define('MYSQL_PREFIX','yana_');
+	/* Remplace MYSQL_PREFIX qui est deprecated */
+	define('ENTITY_PREFIX', MYSQL_PREFIX);
+	/* Chemin vers la base SQLITE */
+	define('DB_NAME','db/.database.db');
+	/* Chaine de connexion sql */
+	define('BASE_CONNECTION_STRING','sqlite:'.DB_NAME);
+	/* Chemin vers le fichier de logs */
+	define('LOG_FILE','log/.log.txt');
+	/* Chemin vers le cache des avatars */
+	define('AVATAR_FOLDER','cache/avatar');
+	/* Chemin http vers yana */
+	define('YANA_URL','http://127.0.0.1:80/yana-server');
+	/* Port du serveur socket */
+	define('SOCKET_PORT',9999);
+	/* Nombre maxium de clients sur le serveur socket */
+	define('SOCKET_MAX_CLIENTS',20);
+	/* Chemin absolus vers le projet */
+	define('__ROOT__',realpath(dirname(__FILE__)));
+	/* Alias de fainéant */
+	define('SLASH',DIRECTORY_SEPARATOR);
+
+	/* 
+	* <!> Laisser à vide sauf si vous souhaitez vous auto-logguer avec un compte sans mot de passe 
+	*     Ceci peut être utile pour les yana-server accessible uniquement depuis votre réseau interne
+	*     Dans tous les autres cas, il serait insécurisé et donc déconseillé d'utiliser cette option.
+	*     Pour vous auto-loguer avec un compte de la base yana, ecrivez le login de ce compte dans cette constante
+	*/
+	define('AUTO_LOGIN','');
+	define('DEFAULT_THEME','default');
+?>

+ 33 - 0
db.json

@@ -0,0 +1,33 @@
+[
+    {
+        "version": 5,
+        "sql": [
+            "ALTER TABLE {PREFIX}plugin_wirerelay ADD COLUMN reverse INT DEFAULT '0';"
+        ]
+    },
+	{
+        "version": 4,
+        "sql": [
+            "ALTER TABLE {PREFIX}plugin_room ADD COLUMN state INT DEFAULT '0';"
+        ]
+    },
+	{
+        "version": 3,
+        "sql": [
+            "ALTER TABLE {PREFIX}plugin_ipcam_camera ADD COLUMN pattern TEXT;"
+        ]
+    },
+    {
+        "version": 2,
+        "sql": [
+            "CREATE TABLE `{PREFIX}device` (`id`  INTEGER NOT NULL PRIMARY KEY  NOT NULL,`label`  VARCHAR(255)  NOT NULL,`icon`  VARCHAR(255)  NOT NULL,`display`  longtext  NOT NULL,`state`  TEXT  NOT NULL,`uid`  TEXT  NOT NULL,`values`  longtext  NOT NULL,`location`  TEXT  NOT NULL,`plugin`  VARCHAR(255)  NOT NULL,`actions`  longtext  NOT NULL,`type`  TEXT  NOT NULL);"
+        ]
+    },
+	{
+        "version": 1,
+        "sql": [
+            "ALTER TABLE {PREFIX}plugin_radioRelay ADD COLUMN state INT(11);"
+        ]
+    }
+	
+]

+ 1 - 0
db/index.php

@@ -0,0 +1 @@
+<?php /*silent is golden*/ ?>

+ 2 - 0
doc.bat

@@ -0,0 +1,2 @@
+cd %CD% && php apigen.phar generate
+PAUSE

+ 4 - 0
footer.php

@@ -0,0 +1,4 @@
+<?php 
+	$tpl->assign('executionTime',number_format(microtime(true)-$start,3));
+	if(isset($view) && $view!='') $html = $tpl->draw($view);
+?>

+ 14 - 0
header.php

@@ -0,0 +1,14 @@
+<?php 
+require_once(dirname(__FILE__).'/common.php');
+
+
+$menuItems = array();
+Plugin::callHook("menubar_pre_home", array(&$menuItems));
+uasort ($menuItems , function($a,$b){return $a['sort']>$b['sort']?1:-1;});
+
+
+$tpl->assign('menuItems',$menuItems);
+
+
+?>
+

+ 16 - 0
index.php

@@ -0,0 +1,16 @@
+<?php
+require_once(dirname(__FILE__).'/header.php');
+
+Plugin::callHook("index_pre_treatment", array(&$_));
+
+
+if(!$myUser){
+	$view = 'login';
+}else{
+	$view = 'index';
+	if($conf->get('HOME_PAGE') != '' && $conf->get('HOME_PAGE')!='index.php')
+	header('location: '.$conf->get('HOME_PAGE'));
+}
+
+require_once(dirname(__FILE__).'/footer.php');
+?>

+ 316 - 0
install.php

@@ -0,0 +1,316 @@
+<?php
+/*
+*/
+
+session_start();
+date_default_timezone_set('Europe/Paris'); 
+//TODO cron auto install
+// echo "*/1 * * * * root wget http://127.0.0.1/yana-server/action.php?action=crontab -O /dev/null 2>&1" > /etc/cron.d/yana-server
+unset($myUser);
+error_reporting(E_ALL);
+ini_set('display_errors','On');
+if(!file_exists(__DIR__.'/constant.php')) copy(__DIR__.'/constant.sample.php', __DIR__.'/constant.php');
+require_once(__DIR__.'/constant.php');
+
+function __autoload($class_name) {
+    include 'classes/'.$class_name . '.class.php';
+}
+?>
+<!DOCTYPE html>
+<html lang="fr">
+  <head>
+    <meta charset="utf-8">
+    <title>Installation</title>
+    <meta name="viewport" content="width=device-width, initial-scale=1.0">
+    <meta name="description" content="">
+    <meta name="author" content="">
+    <link rel="shortcut icon" type="image/x-icon" href="img/favicon.ico">
+
+    <!-- Le styles -->
+    <link href="templates/default/css/bootstrap.min.css" rel="stylesheet">
+    <link href="templates/default/css/jquery-ui-1.10.3.custom.css" rel="stylesheet">
+    <link href="templates/default/css/style.css" rel="stylesheet">
+
+    <link href="templates/default/css/bootstrap-responsive.min.css" rel="stylesheet">
+    <link rel="shortcut icon" href="ico/favicon.png">
+  </head>
+
+  <body>
+
+    <div class="navbar navbar-inverse navbar-fixed-top" id="header">
+      
+      <div class="navbar-inner">
+
+        <div class="container">
+
+          <button type="button" class="btn btn-navbar" data-toggle="collapse" data-target=".nav-collapse">
+          </button>
+          <a class="brand" href="index.php"><?php echo PROGRAM_NAME; ?></a>
+          <div class="nav-collapse collapse">
+            <ul class="nav">
+            </ul>
+          </div><!--/.nav-collapse -->
+        </div>
+      </div>
+    </div>
+<div id="body" class="container">
+<?php
+
+//On récupère le chemin http de yana
+$path_yana =  substr($_SERVER['SCRIPT_FILENAME'],0,-11);
+
+if(isset($_POST['install'])){
+ 
+    try{
+    if(!isset($_POST['password']) || trim($_POST['password'])=='' || !isset($_POST['login']) || trim($_POST['login'])=='' ) 
+      throw new Exception("L'identifiant et le mot de passe ne peuvent être vide");
+    
+	  //Supression de l'ancienne base si elle existe
+	  if(file_exists(DB_NAME) && filesize(DB_NAME)>0) throw new Exception("La base ".DB_NAME." existe déjà, pour recommencer l'installation, merci de supprimer le fichier ".DB_NAME." puis de revenir sur cette page");
+    
+      //Instanciation des managers d'entités
+      $user = new User();
+      $configuration = new Configuration();
+      $right = new Right();
+      $rank = new Rank();
+      $section = new Section();
+      $event = new Event();
+      $client = new Client();
+	    $device = new Device();
+      $personnality = new Personality();
+
+      if(isset($_POST['url'])){
+        $const = file_get_contents("constant.php");
+        file_put_contents('constant.php', (preg_replace("/(define\(\'YANA_URL\'\,\')(.*)('\)\;)/", "$1".$_POST['url']."$3", $const)));
+      }
+
+
+      //Création des tables SQL
+      $configuration->create();
+      $user->create();
+      $right->create();
+      $rank->create();
+      $section->create();
+      $event->create();
+  
+	    $device->create();
+      $personnality->create();
+      $personnality->birth();
+
+      $configuration->put('UPDATE_URL','http://update.idleman.fr/yana?callback=?');
+      $configuration->put('DEFAULT_THEME','default');
+      $configuration->put('COOKIE_NAME','yana');
+      $configuration->put('COOKIE_LIFETIME','7');
+      $configuration->put('VOCAL_ENTITY_NAME','YANA');
+      $configuration->put('PROGRAM_VERSION','3.0.6');
+	  $configuration->put('HOME_PAGE','index.php');
+	  $configuration->put('VOCAL_SENSITIVITY','0.0');
+      $configuration->put('YANA_LATITUDE','24.8235817');
+	  $configuration->put('YANA_LONGITUDE','-75.5070352');
+	  
+      //Création du rang admin
+		  $rank = new Rank();
+    	$rank->setLabel('admin');
+    	$rank->save();
+
+      //Déclaration des sections du programme
+      $sections = array('event','vocal','user','plugin','configuration','admin');
+
+      //Création des sections déclarées et attribution de tous les droits sur toutes ces sections pour l'admin
+      foreach($sections as $sectionName){
+        $s = New Section();
+        $s->setLabel($sectionName);
+        $s->save();  
+
+      	$r = New Right();
+      	$r->setSection($s->getId());
+      	$r->setRead('1');
+      	$r->setDelete('1');
+      	$r->setCreate('1');
+      	$r->setUpdate('1');
+      	$r->setRank($rank->getId());
+      	$r->save();
+      }
+    	
+      $personalities = array('John Travolta','Jeff Buckley','Tom Cruise','John Lennon','Emmet Brown','Geo trouvetou','Luke Skywalker','Mac Gyver','Marty McFly','The Doctor');
+      $im = $personalities[rand(0,count($personalities)-1)];
+      list($fn,$n) = explode(' ',$im);
+      //Creation du premier compte et assignation en admin
+    	$user->setMail($_POST['email']);
+    	$user->setPassword($_POST['password']);
+    	$user->setLogin($_POST['login']);
+      $user->setFirstName($fn);
+      $user->setName($n);
+    	$user->setToken(sha1(time().rand(0,1000)));
+    	$user->setState(1);
+    	$user->setRank($rank->getId());
+    	$user->save();
+
+      global $myUser;
+      $myUser = $user;
+
+      foreach(array('radioRelay','wireRelay','vocal_infos','speechcommands','profile','room','story','dashboard','dashboard-monitoring') as $plugin):
+        Plugin::enabled($plugin.'-'.$plugin);
+      endforeach;
+	
+      $notices = array();
+      if(function_exists('curl_init')){
+        $url="http://idleman.fr/yana/notice.php?code=justavoidspamrequest";
+        $ch = curl_init();
+        curl_setopt($ch, CURLOPT_URL, $url);
+        curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
+        curl_setopt($ch,CURLOPT_USERAGENT,'Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.8.1.13) Gecko/20080311 Firefox/2.0.0.13');
+        $html = curl_exec($ch);
+        curl_close($ch);
+  	    if($html!==false)
+          $notices = json_decode($html,true);
+        
+        if(!is_array($notices)) $notices = array();
+      }
+
+?>
+	
+    <div class="alert alert-info">
+    <button type="button" class="close" data-dismiss="alert">&times;</button>
+    <strong>Installation terminée: </strong> L'installation est terminée, vous devez supprimer le fichier <code>yana-server/install.php</code> par mesure de sécurité, puis revenir sur <a class="brand" href="index.php">l'accueil</a>.
+    </div>
+
+
+    <?php foreach($notices as $notice): ?>
+      <div class="alert alert-<?php  echo $notice['type']; ?>">
+      <button type="button" class="close" data-dismiss="alert">&times;</button>
+      <strong><?php  echo $notice['title']; ?>: </strong> <?php  echo $notice['content']; ?>.
+      </div
+    <?php endforeach; ?>
+
+<?php }catch(Exception $e){ ?>
+  <div class="alert alert-error">
+        <button type="button" class="close" data-dismiss="alert">&times;</button>
+        <strong>Echec de l'Installation : </strong> <?php echo $e->getMessage(); ?> <a class="brand" href="install.php">Réessayer</a>.
+      </div>
+<?php
+}
+}else{ 
+?>
+      
+	  
+	  <?php 
+	  
+	  /*tests*/
+		$tests = array();
+		
+		if(!is_writable($path_yana)) $tests['error'][] = "Le dossier <b>".$path_yana."</b> n'est pas accessible en écriture. <br/>Pour résoudre ce problème, merci de taper la commande suivante dans le shell <code>sudo chown -R www-data:www-data ".$path_yana."</code> ";
+		if(!class_exists('SQLite3')) $tests['error'][] = "Le pré-requis SQLITE3 n'est pas installé. <br/>Pour résoudre ce problème, merci de taper la commande suivante dans le shell <code>sudo apt-get install sqlite3 php-sqlite3</code> ";
+		
+
+
+		$out = exec('whereis gpio',$out);
+		if($out == ''){ 
+      $tests['warning'][] = "La librairie Wiring pi ne semble pas installé sur le rpi, merci de vérifier l'existence du binaire GPIO sur la machine.";
+		}else{
+      require_once(__DIR__.'/classes/Gpio.class.php');
+      $out = trim(str_replace('gpio: ','',$out));
+      if($out != GPIO::GPIO_DEFAULT_PATH) $tests['warning'][] = "Le chemin de l'executable de wiring pi est à modifier dans classes/Gpio.class.php, remplacer <code>".GPIO::GPIO_DEFAULT_PATH."</code> par <code>".$out."</code>.";
+    
+    }
+
+    
+    
+
+		if(function_exists('posix_getpwuid')){
+			
+			$permissions = array('root:www-data'=>'plugins/relay/radioEmission');
+			foreach($permissions as $key=>$file){
+				if(file_exists($file)){
+					list($o,$g) = explode(':',$key);
+					$owner = posix_getpwuid(fileowner($file));
+					$group = posix_getgrgid(filegroup($file));
+				  if($owner['name']!=$o || $group['name'] !=$g) $tests['warning'][] = 'Le fichier <strong>'.$path_yana.$file.'</strong> devrait avoir <i>'.$o.'</i> comme proprietaire et <i>'.$g.'</i> comme groupe, <strong>'.$path_yana.$file.'</strong> pourrait ne pas fonctionner comme attendu, pour résoudre le problème, tapez la commande <code>sudo chown root:www-data '.$path_yana.$file.' && sudo chmod +s '.$path_yana.$file.'</code>';
+        }
+			}
+		}else{
+			$tests['warning'][] = 'Impossible de vérifier les droits sur les fichiers sensibles, librairie posix manquante';
+		}
+		
+		foreach($tests as $type=>$messages){
+			foreach($messages as $message){
+			echo 
+			'<div class="alert alert-'.$type.'">
+				<strong>'.$type.': </strong> '.$message.' 
+			 </div>';
+			}
+		}
+		
+		if(!isset($tests['error'])){
+		
+
+    if(strpos($_SERVER['HTTP_HOST'], ':') !==false){
+      list($host,$port) = explode(':',$_SERVER['HTTP_HOST']);
+    }else{
+      $host = $_SERVER['HTTP_HOST'];
+      $port = 80;
+    }
+    $actionUrl = 'http://'.$host.':'.$_SERVER['SERVER_PORT'].$_SERVER['REQUEST_URI'];
+    $actionUrl = str_replace("/install.php", "", $actionUrl );
+ 
+	  ?>
+	  
+          <div class="alert alert-info">
+          <button type="button" class="close" data-dismiss="alert">&times;</button>
+          <strong>Installation: </strong> Vous devez remplir le formulaire ci dessous pour installer l'application.
+        </div>
+
+        <form class="form-horizontal" action="install.php" method="POST">
+          
+        <div class="control-group">
+          <label class="control-label" for="inputLogin">Login</label>
+          <div class="controls">
+            <input type="text" name="login" id="inputLogin" placeholder="Login">
+          </div>
+        </div>
+        <div class="control-group">
+          <label class="control-label" for="inputPassword">Password</label>
+          <div class="controls">
+            <input type="password" name="password" name="inputPassword" placeholder="Password">
+          </div>
+        </div>
+		<div class="control-group">
+          <label class="control-label" for="inputEmail">Email</label>
+          <div class="controls">
+            <input type="text" name="email" id="inputEmail" placeholder="Email">
+          </div>
+        </div>
+
+        <div class="control-group">
+          <label class="control-label" for="inputUrl">Adresse de yana</label>
+          <div class="controls">
+
+            <input type="text" name="url" id="inputUrl" placeholder="http://" value="<?php echo $actionUrl; ?>">
+          </div>
+        </div>
+
+        <div class="control-group">
+          <div class="controls">
+            <button type="submit" name="install" class="btn">Installer</button>
+          </div>
+        </div>
+      </form>
+	<?php }} ?>
+  
+
+ <div class="navbar navbar-inverse navbar-fixed-bottom" id="footer">CC by nc sa <?php echo PROGRAM_NAME ?>
+
+ </div>
+ </div> <!-- /container -->
+
+
+
+    <!-- Le javascript
+    ================================================== -->
+    <script src="templates/default/js/jquery.min.js"></script>
+    <script src="templates/default/js/bootstrap.min.js"></script>
+    <script src="templates/default/js/jquery.ui.custom.min.js"></script>
+    <script src="templates/default/js/jquery.yana.js"></script>
+	<script src="templates/default/js/script.js"></script>
+  </body>
+</html>

+ 1113 - 0
install.sh

@@ -0,0 +1,1113 @@
+#!/bin/bash
+# Auteur : Remi Sarrailh (maditnerd)
+# Updates : Modifications mineures par Valentin CARRUESCO Idleman 28/07/2018
+# Licence : MIT
+# Un probleme : https://git.idleman.fr/idleman/yana-server/issues
+# https://tldrlegal.com/license/mit-license
+
+#############
+# Variables
+#############
+
+INSTALLVERSION="3.0.6"
+ERR="\033[1;31m"
+NORMAL="\033[0;39m"
+INFO="\033[1;34m"
+WARN="\033[1;33m"
+OK="\033[1;32m"
+IPADDRESS=$(hostname -I)
+HOSTNAME=$(cat /etc/hostname)
+
+doInstall=0
+#Installer le serveur web (se met à 0 si un autre serveur web est installé)
+doInstallWebServer=1
+isRoot=0
+GlobalError=0
+confirmErase=0
+copyYana=1
+resizeSD=1
+
+
+################
+# Messages GUI
+################
+# J'ai séparé les messages long du GUI du reste du programme
+# Afin quel soit facilement modifiable
+
+installMessage="\
+Etapes:
+---------------------------
+* Renommer le Raspberry Pi
+* Redimensionner la carte SD
+* Mis à jour
+* Terminal en français
+------------------------------------
+* Configuration du fuseau horaire
+* Installation wiringPi (pour gérer les GPIO)
+-------------------------------------------
+* Copie de Yana Server
+* Installation du serveur web
+* Création de l'utilisateur
+* Permissions du serveur web
+* Installation du socket / cron
+"
+
+checkMessage="\
+Nous allons revérifier toute l'installation mais sans modifier ni yana-server ni la configuration
+"
+
+renameMessage="\
+Vous pouvez accéder à Yana en utilisant un nom plutôt qu'une adresse IP \n\n\
+Pour que cela marche depuis Windows, il faut que les services BONJOUR soit installés \n\
+C'est le cas si vous avez SKYPE, ITUNES ou Windows 10 sinon il vous faudra l'installer   \n\
+http://support.apple.com/kb/DL999 \n\
+\n\
+\n\
+Example : maison sera accessible sur http://maison.local/
+
+"
+
+saveMessage="\
+Vous pouvez sauvegarder yana-server sur une clé USB \n\n\
+Ceci sauvegardera /var/www/yana-server dans le dossier yana \n\n\
+Si une sauvegarde existe sur la clé, elle sera effacée \n\
+si vous voulez conserver une sauvegarde précédente renommer le dossier \n\
+"
+
+restoreMessage="\
+Vous pouvez revenir à un état précédent de yana depuis une clé USB \n\n\
+Ceci effacera /var/www/yana-server et le replacera par celui \n\
+dans le dossier sur la clé USB /yana
+"
+
+resizeSDCardMessage="\
+Voulez vous redimensionner la carte SD de votre Raspberry Pi ?
+
+Ceci sera fait au prochain redémarrage.
+"
+
+# Message d'erreurs
+
+noInternetMessage="\
+Je n'arrive pas à me connecter à git.idleman.fr \n\
+Voici votre adresse IP: $IPADDRESS\
+"
+
+ApacheMessage="\
+Yana utilise lighttpd comme serveur web par défaut\n\
+Il semblerait que Apache (un autre serveur web) soit déjà installé...\n\n\
+
+Voulez vous quand même installer lighttpd ? \n\
+"
+nginxMessage="\
+Yana utilise lighttpd comme serveur web par défaut\n\
+Il semblerait que nginx (un autre serveur web) soit déjà installé...\n\n\
+
+Voulez vous quand même installer lighttpd ? \n\
+"
+
+yanaMessage="\
+Yana semble avoir déjà été copié.\n\
+Voulez vous que je le supprimer et que je le réinstalle ?\
+"
+
+localeMessage="\
+Je n'ai pas réussi à mettre le terminal en français\n\
+Pour autant, ceci n'aura aucune incidence sur la suite de l'installation\n\n\
+Voici le message d'erreur:\
+"
+
+aptGetErrorMessage="\
+Le gestionnaire de paquet apt-get est HS\n\
+* Soit celui-ci a été interrompu\n\
+* Soit il est en cours d'utilisation par un autre programme\n\
+Supprimer le fichier de verrou est probablement la solution\n\n\
+Voici le message d'erreur:\n\
+"
+
+gitErrorMessage="\
+Impossible de récupérer le code source avec git\n\
+Cela peut être du à un problème du coté de git.idleman.fr\n\
+Veuillez vérifier que http://git.idleman.fr/ est en ligne\n\n\
+Voici le message d'erreur:\n\
+"
+
+wiringPiErrorMessage="\
+Impossible de compiler wiringPi\n\
+Voici le message d'erreur:\n\
+"
+
+lighttpdErrorMessage="\
+Le serveur web n'a pas réussi à se redémarrer correctement\n\
+Voici le message d'erreur:\n\
+"
+
+#Un joli logo ascii sans avoir à installer un programme pour ça
+yanaLogo(){
+clear
+echo -ne $INFO
+
+cat<<EOF                                                                           
+██╗   ██╗ █████╗ ███╗   ██╗ █████╗        ████████╗
+╚██╗ ██╔╝██╔══██╗████╗  ██║██╔══██╗       █ █  █ █║
+ ╚████╔╝ ███████║██╔██╗ ██║███████║       █      █║ 
+  ╚██╔╝  ██╔══██║██║╚██╗██║██╔══██║        ██████ ║
+   ██║   ██║  ██║██║ ╚████║██║  ██║       █ ████ █║
+   ╚═╝   ╚═╝  ╚═╝╚═╝  ╚═══╝╚═╝  ╚═╝         █  █══╝
+
+EOF
+
+echo -ne $ERR
+cat<<EOF 
+██╗███╗   ██╗███████╗████████╗ █████╗ ██╗     ██╗     
+██║████╗  ██║██╔════╝╚══██╔══╝██╔══██╗██║     ██║     
+██║██╔██╗ ██║███████╗   ██║   ███████║██║     ██║     
+██║██║╚██╗██║╚════██║   ██║   ██╔══██║██║     ██║     
+██║██║ ╚████║███████║   ██║   ██║  ██║███████╗███████╗
+╚═╝╚═╝  ╚═══╝╚══════╝   ╚═╝   ╚═╝  ╚═╝╚══════╝╚══════╝                                                   
+EOF
+
+echo -ne $NORMAL
+
+}
+
+##############
+# Menus
+##############
+
+# Menu principal
+mainMenu(){
+	optionsMain=$(whiptail --title "YANA Server $INSTALLVERSION" --menu "" --cancel-button "Annuler" 0 0 0 \
+		"Installer" "" \
+		"Configurer" "" \
+		"Sauvegarder" "" \
+		"Restaurer" "" \
+		"Quitter" "" 3>&1 1>&2 2>&3)
+
+	case $optionsMain in
+		"Installer")
+			installMenu;;
+		"Configurer")
+			setupMenu;;
+		"Sauvegarder")
+			saveMenu;;
+		"Restaurer")
+			restoreMenu;;
+		*)
+			echo -e "$OK ... A la prochaine! $NORMAL"
+			;;
+	esac
+}
+
+# Menu d'installation
+installMenu(){
+	if(whiptail --title "Installation" --yesno "$installMessage" --yes-button "Oui" --no-button "Non" 0 0) then
+		doInstall=1
+	else
+		echo -e "\033[1;34m... A la prochaine!\033[0;39m"
+	fi
+}
+
+# Menu de configuration
+setupMenu(){
+	optionsSetup=$(whiptail --title "YANA Server $INSTALLVERSION" --menu "" --cancel-button "Retour" 0 0 0 \
+		"Vérifier YANA" "" \
+		"Mettre à jour YANA" "" \
+		"Redimensionner la carte SD" "" \
+		"Renommer le Raspberry Pi" ""  \
+		"Scripts Plugins" "" \
+		"Quitter" "" \
+		 3>&1 1>&2 2>&3)
+
+	case $optionsSetup in
+		"Vérifier YANA")
+			checkMenu;;
+		"Mettre à jour YANA")
+			forceYanaUpdate;;
+		"Redimensionner la carte SD")
+			resizeSDCard
+			setupMenu;;
+		"Renommer le Raspberry Pi")
+			renameMenu
+			setupMenu;;
+		"Scripts Plugins")
+			scriptsMenu;;
+		"Quitter")
+			echo -e "$OK ... A la prochaine! $NORMAL";;
+		*)
+			mainMenu;;
+	esac
+}
+
+# Menu vérification de yana
+checkMenu(){
+	if(whiptail --title "Vérification" --yesno "$checkMessage" --yes-button "Oui" --no-button "Non" 0 0) then
+		updateRaspberryPi
+		checkPermissions
+		checkBinariesMenu
+		installYanaSocket
+		addCron
+	else
+		setupMenu
+	fi
+}
+
+resizeSDCardMenu(){
+	if(whiptail --title "Carte SD" --yesno "$resizeSDCardMessage" --yes-button "Oui" --no-button "Non" 0 0) then
+		resizeSDCard
+	fi
+}
+
+# Menu de renommage du Raspberry Pi
+renameMenu(){
+	newhostname=$(whiptail --inputbox "$renameMessage" --title "Choissisez un nom" 0 0 3>&1 1>&2 2>&3)
+	renamePi
+}
+
+# Menu de scripts pour les plugins
+# Il faut créer un script au format .sh pour dans /var/www/yana-server/plugins/nom-du-plugin/nom-du-script.sh
+# On peut utiliser les fonctions du script d'installation et les variables à l'intérieur d'un script
+# Par example vous pouvez récupérer le nom $HOSTNAME ou l'adresse IP $IPADDRESS
+# Vérifier si internet est connecté 
+scriptsMenu(){
+getAllScripts
+
+while read -r nextScript
+do 
+	scriptName=$(echo "${nextScript//\/var\/www\/yana-server\/plugins\//}")
+	menu_options[ $i ]="$scriptName"
+	(( i++ ))
+	
+	menu_options[ $i ]=""
+	(( i++ ))
+done <<<"$allScripts"
+
+scriptToExecute=$(whiptail --title "Plugins" --menu "Gérer un Plugin" 0 0 0 "${menu_options[@]}" 3>&1 1>&2 2>&3 )
+executeScript
+}
+
+executeScript(){
+if [[ -f /var/www/yana-server/plugins/$scriptToExecute ]];then
+	chmod +x /var/www/yana-server/plugins/$scriptToExecute
+	clear
+	yanaLogo
+	echo -e "$OK -----> Exécution de $scriptToExecute $NORMAL"
+	dir=$(dirname /var/www/yana-server/plugins/$scriptToExecute)
+	cd $dir;. /var/www/yana-server/plugins/$scriptToExecute
+else
+	echo -e "$OK -----> Aucun script trouvé dans /var/www/yana-server/plugins/$scriptToExecute $NORMAL"
+fi
+
+}
+
+# Menu de vérification des fichiers binaires
+checkBinariesMenu(){
+	getAllBinaries
+
+	if(whiptail --title "Permissions binaires" --yesno "Je peux automatiquement donner les droits roots aux programmes des plugins\n\nVoici la liste des programmes concernés: \n$allBinaries" --yes-button "Oui" --no-button "Non" 0 0) then
+		setupPermissionsBinaries
+		whiptail --title "Permissions" --msgbox "Permissions activés" 0 0
+	fi
+
+}
+
+saveMenu(){
+	if(whiptail --title "Sauvegarde USB" --yesno "$saveMessage" --yes-button "Oui" --no-button "Non" 0 0) then
+		saveUSB
+	else
+		mainMenu
+	fi
+}
+
+restoreMenu(){
+	if(whiptail --title "Restauration USB" --yesno "$restoreMessage" --yes-button "Oui" --no-button "Non" 0 0) then
+		restoreUSB
+		checkPermissions
+		setupPermissionsBinaries
+	else
+		mainMenu
+	fi
+}
+
+
+## Menu d'erreurs
+
+
+# Menu Internet HS
+noInternetMenu(){
+	whiptail --title "Vérifier que vous êtes connecté à internet" --msgbox "$noInternetMessage" 0 0
+	echo -e "$ERR - Impossible de continuer sans internet $NORMAL"
+}
+
+# Menu Apache déjà installé
+ApacheAlreadyInstalledMenu(){
+	if(whiptail --title "Un serveur web est déjà installé" --yesno "$ApacheMessage" --yes-button "Oui" --no-button "Non" 0 0) then
+		doInstallWebServer=1
+	else
+		doInstallWebServer=0
+	fi
+}
+
+# Menu Nginx déjà installé
+nginxAlreadyInstalledMenu(){
+	if(whiptail --title "Un serveur web est déjà installé" --yesno "$nginxMessage" --yes-button "Oui" --no-button "Non" 0 0) then
+		doInstallWebServer=1
+	else
+		doInstallWebServer=0
+	fi
+}
+
+# Menu error APT-GET
+aptgetErrorMenu(){
+	#Récupère le message d'erreur apt-get
+	getAptError
+
+	#Affiche l'erreur dans la GUI
+	whiptail --title "le gestionnaire de paquet ne réponds pas" --msgbox "$aptGetErrorMessage $aptError" 0 0
+	echo -e "$ERR Impossible de continuer sans apt-get $NORMAL"
+	echo -e "$WARN ERREUR - $aptGetErrorMessage $aptError"
+	exit 1
+}
+
+# Menu erreur git
+gitErrorMenu(){
+	#Récupère le message d'erreur de git clone
+	getGitError
+
+	#Affiche l'erreur dans la GUI
+	whiptail --title "le gestionnaire de paquet ne réponds pas" --msgbox "$gitErrorMessage $gitError" 0 0
+	exit 1
+}
+
+# Menu erreur wiringPi
+wiringPiErrorMenu(){
+	getWiringPiError
+
+	whiptail --title "Echec de la compilation de WiringPi" --msgbox "$wiringPiErrorMessage $wiringPiError" 0 0
+}
+
+# Menu erreur lighttpd
+lighttpdErrorMenu(){
+	whiptail --title "Echec du lancement de Lighttpd" --msgbox "$lighttpdErrorMessage $lighttpdError" 0 0
+}
+
+confirmEraseUSB(){
+	if(whiptail --title "Confirmer la suppression de la sauvegarde" --yesno "Une sauvegarde précédente existe la supprimer ?" --yes-button "Oui" --no-button "Non" 0 0) then
+		confirmErase=1
+	else
+		confirmErase=0
+	fi
+}
+
+
+
+##############
+# Scripts
+##############
+# Toutes les parties de l'installation sont séparés en fonctions
+# Ceci afin de faciliter les tests de chaque partie
+
+#Vérifie que vous êtes bien en root
+verifyRoot() {
+	if [ $(id -u) -ne 0 ]; then
+		echo -e "\033[1;31mVous avez oublié de vous mettre en root!\033[0;39m"
+		echo -e "Tapez \033[1;34msudo $0\033[0;39m"
+		isRoot=0
+	else
+		isRoot=1
+	fi
+}
+
+#Vérifie l'état de la connexion internet
+checkInternet(){
+	ping -c1 www.google.com > /dev/null 2>&1 && internet=1 || internet=0
+	echo -e "$OK -----> Vérification de la connexion à internet $NORMAL"
+	if [[ $internet -eq 0 ]]
+		then
+			noInternetMenu
+	fi
+}
+
+#Récupère le message d'erreur APT-GET
+getAptError(){
+	rm -f /tmp/aptError.log
+
+	#On lance apt-get install en dry-run (simulation) afin de recuperer l'erreur
+	#et on sauve le log dans /tmp/aptError.log
+	apt-get --dry-run install > /tmp/aptError.log 2>&1
+	aptError=$(cat /tmp/aptError.log)
+}
+
+#Récupère le message d'erreur de git
+getGitError(){
+	gitError=$(cat /tmp/gitError.log)
+}
+
+#Récupère le message d'erreur de WiringPi
+getWiringPiError(){
+	wiringPiError=$(cat /tmp/wiringPiError.log)
+}
+
+#Récupère le message d'erreur de lighttpd
+getLighttpdError(){
+	lighttpdError=$(cat /tmp/lighttpdReload.log)
+}
+
+#Met à jour le Raspberry Pi en utilisant whiptail comme interface
+updateRaspberryPi(){
+	echo -e "$OK -----> Mise à jour du Raspberry Pi $NORMAL"
+
+	#debconf-apt-progress permet d'afficher la progression de la mis à jour dans une GUI en français
+	debconf-apt-progress -- apt-get -q -y update
+	globalError=$?
+	if [[ $globalError -ne 0 ]];then
+		aptgetErrorMenu
+	fi
+	debconf-apt-progress -- apt-get -q -y upgrade
+	globalError=$?
+	if [[ $globalError -ne 0 ]];then
+		aptgetErrorMenu
+	fi
+
+	echo -e "$OK -----> Installation du client git $NORMAL"
+	#On installe aussi le client git
+	debconf-apt-progress -- apt-get install -q -y git-core
+	if [[ $globalError -ne 0 ]];then
+		aptgetErrorMenu
+	fi
+}
+
+#Change les locales de l'anglais au français de manière non interactive
+setLocaleToFrench(){
+	echo -e "$OK -----> Configuration du terminal en français... Patientez s'il vous plaît ... $NORMAL"
+
+	#Ajout des locales FR
+	sed -i -e 's/# fr_FR.UTF-8 UTF-8/fr_FR.UTF-8 UTF-8/' /etc/locale.gen
+
+	#Met FR en locale par défaut
+	echo 'LANG="fr_FR.UTF-8 UTF-8"'>/etc/default/locale
+	update-locale LANG=fr_FR.UTF-8
+	export LANG=fr_FR.UTF-8
+
+	#Les locales sont installés silencieusement
+	dpkg-reconfigure --frontend=noninteractive locales > /tmp/localeSetup.log 2>&1
+	globalError=$?
+	#En cas d'erreur on affiche le message
+	if [[ $globalError -ne 0 ]];then
+		localeError=$(cat /tmp/localeSetup.log)
+		whiptail --title "Locales FR" --msgbox "$localeMessage $localeError" 0 0
+	fi
+}
+
+#Gestion automatique des fuseaux horaires à l'aide de tzupdate
+configureTimeZone(){
+	echo -ne "$OK -----> Configuration du fuseau horaire $NORMAL"
+
+	#Vérifie que Python PIP est disponible
+	debconf-apt-progress -- apt-get install -q -y python-pip
+	globalError=$?
+	if [[ $globalError -ne 0 ]];then
+		aptgetErrorMenu
+	fi
+
+	#Installation silencieuse du Package python tzupdate
+	pip install --quiet tzupdate
+
+	#Si l'installation c'est correctement passé lancé tzupdate silencieusement
+	if [ -f /usr/local/bin/tzupdate ];then
+		tzupdate > /dev/null 2>&1
+
+		#On récupère après la zone géographique pour l'afficher
+		currentTimeZone=$(tzupdate -p|awk '{print $4}')
+		echo -e "$WARN : $currentTimeZone $NORMAL"
+	else
+		echo -e "$ERR Impossible de changer le fuseau horaire automatiquement (ce n'est pas nécessaire) $NORMAL"
+	fi
+}
+
+#Vérification sommaire de l'existance d'autres serveur web
+#Si un autre serveur web est installé prévient l'utilisateur
+#Afin qu'il choissisent s'il veut installer lighttpd ou pas
+checkWebServer(){
+	if [ -f "/usr/sbin/apache2" ];then
+		ApacheAlreadyInstalledMenu
+	fi
+
+	if [ -f "/usr/sbin/nginx" ];then
+		nginxAlreadyInstalledMenu
+	fi
+}
+
+#Installation du serveur web et de SQLite
+installWebServer(){
+	echo -e "$OK -----> Installation du serveur web $NORMAL"
+	debconf-apt-progress -- apt-get install -q -y lighttpd git-core sqlite3 php7.0-sqlite php7.0-common php7.0-cgi php7.0-cli php-mbstring php7.0-zip
+	if [[ $globalError -ne 0 ]];then
+		aptgetErrorMenu
+	fi
+
+	#On efface la page par défaut de lighttpd pour éviter d'embrouiller les utilisateurs
+	rm -f /var/www/index.lighttpd.html
+	rm -rf /var/www/html
+
+}
+
+#Configure lighttpd pour bloquer l'accès à la base de données
+setupWebServer(){
+echo -e "$OK -----> Configuration du serveur web (/etc/lighttpd/lighttpd.conf) $NORMAL"
+
+cat <<\EOF > /etc/lighttpd/lighttpd.conf
+server.modules = (
+        "mod_access",
+        "mod_alias",
+        "mod_compress",
+        "mod_redirect",
+#       "mod_rewrite",
+)
+
+server.document-root        = "/var/www/"
+server.upload-dirs          = ( "/var/cache/lighttpd/uploads" )
+server.errorlog             = "/var/log/lighttpd/error.log"
+server.pid-file             = "/var/run/lighttpd.pid"
+server.username             = "www-data"
+server.groupname            = "www-data"
+server.port                 = 80
+
+index-file.names            = ( "index.php", "index.html", "index.lighttpd.html" )
+url.access-deny             = ( "~", ".inc", "db","log.txt" )
+static-file.exclude-extensions = ( ".php", ".pl", ".fcgi" )
+
+compress.cache-dir          = "/var/cache/lighttpd/compress/"
+compress.filetype           = ( "application/javascript", "text/css", "text/html )                                                                                                                                                             ", "text/plain" )
+
+# default listening port for IPv6 falls back to the IPv4 port
+include_shell "/usr/share/lighttpd/use-ipv6.pl " + server.port
+include_shell "/usr/share/lighttpd/create-mime.assign.pl"
+include_shell "/usr/share/lighttpd/include-conf-enabled.pl"
+EOF
+
+lighttpdSetupError=$?
+
+if [[ lighttpdSetupError -eq 1 ]];then
+	echo -e "$ERR - Le fichier /etc/lighttpd/lighttpd.conf n'a pas été modifié $NORMAL"
+fi
+
+#Activation de PHP et rechargement de lighttpd
+	lighty-enable-mod fastcgi-php > /dev/null 2>&1
+	service lighttpd restart > /tmp/lighttpdReload.log 2>&1
+	globalError=$?
+	if [[ $globalError -ne 0 ]];then
+		getLighttpdError
+		lighttpdErrorMenu
+		echo -e "$ERR - La configuration de /etc/lighttpd/lighttpd.conf a échoué $NORMAL"
+		echo -e "$WARN ERREUR: $lighttpdError $NORMAL"
+	fi
+
+}
+
+
+
+
+#Clonage de Yana
+#Si Yana a déjà été cloné alors on propose à l'utilisateur de le réinstaller
+cloneYana(){
+	if [[ copyYana -eq 1 ]];then
+		if [[ -d "/var/www/yana-server" ]];then
+				if(whiptail --title "Yana déjà installé" --yesno "$yanaMessage" --yes-button "Oui" --no-button "Non" 0 0) then
+					echo -e "$ERR -----> Réinstallation de Yana Server $NORMAL"
+					rm -rf /var/www/yana-server
+					GIT_SSL_NO_VERIFY=true git clone https://git.idleman.fr/idleman/yana-server.git /var/www/yana-server > /tmp/gitError.log 2>&1
+					globalError=$?
+					if [[ $globalError -ne 0 ]];then
+						gitErrorMenu
+					fi
+				fi
+		else
+			echo -e "$OK -----> Copie de Yana Server $NORMAL"
+			GIT_SSL_NO_VERIFY=true git clone https://git.idleman.fr/idleman/yana-server.git /var/www/yana-server > /tmp/gitError.log 2>&1
+			globalError=$?
+			if [[ $globalError -ne 0 ]];then
+				gitErrorMenu
+			fi
+		fi
+	fi
+}
+
+# Mis à jour forcé de Yana
+# Cela n'affectera pas la base de données 
+forceYanaUpdate(){
+yanaLogo
+echo -e "$OK -----> Mise à jour de Yana Server $NORMAL"
+
+cd /var/www/yana-server && git fetch --all > /dev/null 2>&1
+fetchStatus=$?
+cd /var/www/yana-server && lastcomment=$(git reset --hard origin/master | awk '{$1="";$2="";$3="";$4="";$5="";print $0;}') > /dev/null 2>&1
+resetStatus=$?
+cd /var/www/yana-server && pullLog=$(git pull origin master) > /dev/null 2>&1
+pullStatus=$?
+
+if [[ $fetchStatus -eq 0 ]] && [[ $resetStatus -eq 0 ]] && [[ $pullStatus -eq 0 ]];then
+	
+	echo -e "$INFO -----> Dernier statut - $lastcomment $NORMAL"
+	# On revérifie les permissions
+	checkPermissions
+	setupPermissionsBinaries
+else
+	echo -e "$ERR -----> La mise à jour a échoué $NORMAL"
+	echo $pullLog
+fi
+
+
+
+}
+
+#Vérification des permissions pour Yana Server et le plugin radioRelay
+checkPermissions(){
+	echo -e "$OK -----> Vérification des permissions de YANA $NORMAL"
+	chown -R www-data:www-data /var/www/yana-server
+	chmod 750 -R /var/www/yana-server
+
+	giveRootPermissions /var/www/yana-server/plugins/radioRelay/radioEmission
+	
+}
+
+# Cherche tout les fichiers cpp dans yana-server
+getAllBinaries(){
+	allBinaries=$(find /var/www/yana-server/plugins -name "*.cpp")
+}
+
+# Cherche tout les fichiers cpp pour donner la permission root au fichier binaire associés
+setupPermissionsBinaries(){
+	while read -r file; do
+		file=$(echo "${file/.cpp/}")
+		if [[ -f $file ]];then
+	    	giveRootPermissions $file
+	    fi
+	done <<< "$allBinaries"
+}
+
+# Cherche des scripts dans les plugins
+getAllScripts(){
+	allScripts=$(find /var/www/yana-server/plugins -name "*.sh")
+	if [[ -z "${allScripts// }" ]];then
+		allScripts="Aucun Script disponible"
+	fi
+}
+
+# Donne les permissions root au serveur web à un programme
+# Les permissions ont été géré de façon à limité au maximum l'accès
+giveRootPermissions(){
+	rootProgram=$1
+	chown root:www-data $rootProgram
+	chmod 000 $rootProgram
+	chmod +sx $rootProgram
+}
+
+# Installation de wiringPi dans /opt/wiringPi
+# Une fois installé , wiringPi n'utilisera pas ce dossier qui ne contient que les sources
+installWiringPi(){
+	#Vérifie si WiringPi est installé sinon on ne l'installe pas
+	if [[ ! -f /usr/local/bin/gpio ]];then
+		echo -e "$OK -----> Copie de wiringPi $NORMAL"
+
+		#Si les sources ont déjà été copié on les efface pour les retélécharger
+		if [ -d /opt/wiringPi ];then
+			rm -rf /opt/wiringPi
+		fi
+
+		cd /opt/
+		git clone git://git.drogon.net/wiringPi /opt/wiringPi > /tmp/gitError.log 2>&1
+		globalError=$?
+		if [[ $globalError -ne 0 ]];then
+			gitErrorMenu
+		fi
+		cd /opt/wiringPi/
+		echo -e "$OK -----> Installation de wiringPi $NORMAL"
+		./build > /tmp/wiringPiError.log 2>&1
+		globalError=$?
+		if [[ $globalError -ne 0 ]];then
+			wiringPiErrorMenu
+		fi
+	fi
+}
+
+#Création d'un lien symbolique de l'installateur vers 
+linkInstaller(){
+	if [[ -f /usr/local/bin/configurer ]];then
+		rm /usr/local/bin/configurer
+	fi
+	ln -s /var/www/yana-server/install.sh /usr/local/bin/configurer
+	chmod +x /usr/local/bin/configurer
+}
+
+# Installation du socket pour le client YANA
+installYanaSocket(){
+
+echo -e "$OK -----> Installation du socket YANA $NORMAL"
+
+if [[ -s /var/www/yana-server/db/.database.db ]];then
+	# Installation de supervisor
+	debconf-apt-progress -- apt-get install -q -y supervisor
+	globalError=$?
+	if [[ $globalError -ne 0 ]];then
+		aptgetErrorMenu
+	fi
+
+	# Configuration du socket dans supervisor
+cat <<\EOF > /etc/supervisor/conf.d/yana.conf
+[program:yana]
+command=/usr/bin/php /var/www/yana-server/socket.php
+autostart=true
+autorestart=true
+stdout_logfile=/var/log/yanaSocket.log
+redirect_stderr=true
+EOF
+
+	supervisorFatalError=0
+
+	# Lecture du fichier de configuration
+	supervisorctl reread > /tmp/supervisorReReadError.log 2>&1
+	supervisorError=$(cat /tmp/supervisorReReadError.log)
+	if [[ ! $supervisorError == "No config updates to processes" && ! $supervisorError == "yana: available" ]];then
+		echo -e "$ERR Erreur dans /etc/supervisor/conf.d/yana.conf - $supervisorError $NORMAL"
+		supervisorFatalError=1
+	fi
+
+	# Ajout du socket dans supervisor
+	supervisorctl update > /tmp/supervisorUpdateError.log 2>&1
+	supervisorErrorUpdate=$(cat /tmp/supervisorUpdateError.log)
+	if [[ $supervisorError == "" && ! $supervisorError == "yana: added process group" ]];then
+		echo -e "$ERR Erreur dans /etc/supervisor/conf.d/yana.conf - $supervisorError $NORMAL"
+		supervisorFatalError=1
+	fi
+
+	# Relancement du socket pour test
+	if [[ $supervisorFatalError -eq 0 ]];then
+		supervisorctl stop yana > /tmp/supervisorStopError.log 2>&1
+		supervisorctl start yana > /tmp/supervisorStartError.log 2>&1
+
+		supervisorStartError=$(cat /tmp/supervisorStartError.log)
+		if [[ ! $supervisorStartError == "yana: started" ]];then
+			echo -e "$ERR Erreur lancement du socket - $supervisorStartError $NORMAL"
+			echo -e "$ERR Tapez $INFO sudo cat /var/log/yanaSocket.log pour plus d'information $NORMAL"
+		fi
+	fi
+
+else
+	echo -e "$ERR Aller sur $INFO http:///$HOSTNAME.local/yana-server $ERR pour finalisez l'installation avant d'installer le socket $NORMAL"
+fi
+
+}
+	
+checkCron(){
+	checkCronYana=$(crontab -l|grep 'http://localhost/action.php?action=crontab')
+}
+
+addCron(){
+	echo -e "$OK -----> Installation du cron scénario $NORMAL"
+
+	if [[ -z $checkCronYana ]];then
+		crontab -l | { cat; echo '*/1 * * * * wget "http://localhost/action.php?action=crontab" -O /dev/null 2>&1'; } | crontab -
+	else
+			echo -e "$WARN -----> Installation du cron scénario (déjà effectué) $NORMAL"	
+	fi
+}
+
+#Afin de sécuriser Yana une fois l'installation fini, on supprime install.php
+#Et on change le mot de passe de l'utilisateur par défaut (pi)
+securityCheck(){
+	if [[ -f /var/www/yana-server/install.php ]];then
+		echo -e "$OK -----> Supression de install.php $NORMAL"
+		rm /var/www/yana-server/install.php
+		
+	fi
+}
+
+# Pour redimensionner la carte sd, j'utilise raspi-config en mode unattended
+resizeSDCard(){
+	echo -e "$OK -----> Vérification du redimensionnement de la carte SD $NORMAL"
+	raspi-config --expand-rootfs > /tmp/resizeSDCardError.log 2>&1
+}
+
+endInstall(){
+	HOSTNAME=$(cat /etc/hostname)
+	echo -e "$OK -----> Finissez l'installation en allant sur votre $WARNING navigateur $OK à $INFO http://$HOSTNAME.local/yana-server $NORMAL"
+	echo -ne "$WARN ATTENTE DE L'UTILISATEUR $NORMAL"
+	databaseCreated=0
+	while [[ databaseCreated -eq 0 ]]
+	do
+		if [ ! -s /var/www/yana-server/db/.database.db ];then
+			echo -ne "."
+		else
+			echo -e "$OK --> OK! $NORMAL"
+			databaseCreated=1
+		fi
+		sleep 3
+	done
+}
+
+#http://unix.stackexchange.com/questions/60299/how-to-determine-which-sd-is-usb par F.Hauri
+getUSB(){
+	cut -d/ -f4 <(grep -vl ^0$ $(sed s@device/.*@size@ <(grep -l ^DRIVER=sd $(sed s+/rem.*$+/dev*/ue*+ <(grep -Hv ^0$ /sys/block/*/removable)) <(:))) <(:))
+
+}
+
+saveUSB(){
+	yanaLogo
+	USBDrive="/dev/$(getUSB)1"
+
+	#Si aucun lecteur n'est detecté
+	if [[ $USBDrive == "/dev/1" ]];then
+		echo -e "$WARN -----> Aucun clé USB detecté ! $NORMAL"
+	else
+		echo -e "$OK -----> Clé USB trouvé sur $INFO $USBDrive $NORMAL"
+		
+		#Si le lecteur existe vraiment
+		if [[ -e "$USBDrive" ]];then
+		
+			# Si la clé est déjà monté, on l'a démonte par sécurité
+			if mount |grep "/media/backupUSB" > /dev/null;then
+				umount /media/backupUSB
+				#@todo rajouter un test
+			fi
+
+			# Si le dossier de montage n'existe pas on le crée
+			if [ ! -d /media/backupUSB ];then
+				mkdir /media/backupUSB
+			fi
+		
+			# On monte la clé USB
+			mount "$USBDrive" /media/backupUSB
+			mountState=$?
+
+			# Si le montage s'est déroulé correctement
+			if [[ $mountState -eq 0 ]];then
+
+				#Si une sauvegarde précédente, on prévient l'utilisateur
+				if [ -d /media/backupUSB/yana ];then
+					confirmEraseUSB
+					#On efface la sauvegarde précédente
+					if [[ confirmErase -eq 1 ]];then
+						rm -rf /media/backupUSB/yana
+						echo -e "$WARN -----> Supression de la sauvegarde précédente $NORMAL"
+					else
+						echo -e "$WARN -----> Annulation de la sauvegarde"
+					fi
+				else 
+					confirmErase=1
+				fi
+
+				#Si pas de sauvegarde précédente ou confirmation de la suppression
+				if [[ confirmErase -eq 1 ]];then
+					echo -e "$OK -----> Copie des fichiers $NORMAL"
+					cp -R /var/www/yana-server/ /media/backupUSB/yana
+					copyState=$?
+
+					if [[ $copyState -eq 0 ]];then
+						echo -e "$OK -----> Copie réussi avec succès ! $NORMAL"
+
+					else
+						echo -e "$ERR -----> La copie a échoué ! $NORMAL"
+					fi
+				fi
+
+				# Quoiqu'il arrive nous démontons la clé à la fin				
+				echo -e "$OK -----> Démontage de la clé USB $NORMAL"
+				umount /media/backupUSB
+				umountState=$?
+				
+				#Si l'état de la clé n'est pas correcte
+				if [ $umountState -eq 0 ];then
+					echo -e "$OK -----> Vous pouvez retirer votre clé en toute sécurité $NORMAL"
+				else
+					echo -e "$ERR -----> La clé n'a pas été correctement démonté $NORMAL "
+					echo -e "$INFO Vous pouvez la démonter manuellement en tapant $OK umount /media/backupUSB $NORMAL"
+				fi
+			
+			else
+				echo -e "$ERR -----> Impossible de monter ! $NORMAL"	
+			fi
+
+		else
+			echo -e "$ERR -----> La détection a échoué ! $NORMAL"
+		fi	
+	fi
+}
+
+restoreUSB(){
+	yanaLogo
+	USBDrive="/dev/$(getUSB)1"
+
+	#Si aucun lecteur n'est detecté
+	if [[ $USBDrive == "/dev/1" ]];then
+		echo -e "$WARN -----> Aucun clé USB detecté ! $NORMAL"
+	else
+		echo -e "$OK -----> Clé USB trouvé sur $INFO $USBDrive $NORMAL"
+		
+		#Si le lecteur existe vraiment
+		if [[ -e "$USBDrive" ]];then
+		
+			# Si la clé est déjà monté, on l'a démonte par sécurité
+			if mount |grep "/media/backupUSB" > /dev/null;then
+				umount /media/backupUSB
+				#@todo rajouter un test
+			fi
+
+			# Si le dossier de montage n'existe pas on le crée
+			if [ ! -d /media/backupUSB ];then
+				mkdir /media/backupUSB
+			fi
+		
+			# On monte la clé USB
+			mount "$USBDrive" /media/backupUSB
+			mountState=$?
+
+			# Si le montage s'est déroulé correctement
+			if [[ $mountState -eq 0 ]];then
+
+				rm -rf /var/www/yana-server
+				echo -e "$WARN -----> Supression de yana-server $NORMAL"
+
+				echo -e "$OK -----> Restauration de la sauvegarde $NORMAL"
+				cp -R /media/backupUSB/yana /var/www/yana-server/ 
+				copyState=$?
+
+				if [[ $copyState -eq 0 ]];then
+					echo -e "$OK -----> La restauration est un succès ! $NORMAL"
+
+				else
+					echo -e "$ERR -----> La restauration a échoué ! $NORMAL"
+				fi
+				
+
+				# Quoiqu'il arrive nous démontons la clé à la fin				
+				echo -e "$OK -----> Démontage de la clé USB $NORMAL"
+				umount /media/backupUSB
+				umountState=$?
+				
+				#Si l'état de la clé n'est pas correcte
+				if [ $umountState -eq 0 ];then
+					echo -e "$OK -----> Vous pouvez retirer votre clé en toute sécurité $NORMAL"
+					echo -e "$INFO Pour réouvrir la clé USB tapez : $ERR mount $USBDrive /media/backupUSB $NORMAL"
+				else
+					echo -e "$ERR -----> La clé n'a pas été correctement démonté $NORMAL "
+					echo -e "$INFO Vous pouvez la démonter manuellement en tapant $OK umount /media/backupUSB $NORMAL"
+				fi
+			
+			else
+				echo -e "$ERR -----> Impossible de monter ! $NORMAL"	
+			fi
+
+		else
+			echo -e "$ERR -----> La détection a échoué ! $NORMAL"
+		fi	
+	fi
+}
+
+# Renomme le Raspberry Pi sur le réseau
+# Il sera alors accessible depuis newhostname.local
+# Cela va modifier /etc/hosts et /etc/hostname où le daemon avahi
+# va chercher le nom du Raspberry Pi
+renamePi(){
+
+yanaLogo
+if [[ -z $newhostname ]];then
+	newhostname="maison"
+fi
+		
+cat <<EOF > /etc/hosts && 
+127.0.0.1       localhost
+::1             localhost ip6-localhost ip6-loopback
+fe00::0         ip6-localnet
+ff00::0         ip6-mcastprefix
+ff02::1         ip6-allnodes
+ff02::2         ip6-allrouters
+
+127.0.1.1       $newhostname
+EOF
+
+echo $newhostname > /etc/hostname
+
+hostname $newhostname
+service avahi-daemon restart > /dev/null 2>&1
+whiptail --title "Raspberry Pi s'appelle $newhostname.local" --msgbox "Tapez http://$newhostname.local/ pour accéder à Yana" 0 0
+echo -ne "$OK -----> Renommage du Raspberry Pi $NORMAL"
+echo -e "$WARN : $newhostname.local $NORMAL"
+}
+
+###############
+# La partie principale
+###############
+# Si vous voulez supprimer des étapes ou en rajouter
+# C'est ici que tout se passe
+
+#Vérification des droits administrateur
+cd /
+verifyRoot
+
+if [[ $isRoot -eq 1 ]];then
+
+	
+	if [[ $# -eq 1 ]];then
+		scriptToExecute=$1
+		
+		if [[ scriptToExecute -eq "noresize" ]];then
+			resizeSDCard=0
+		fi
+
+	else
+
+
+		# Affichage du menu principal
+		mainMenu
+
+		# Si Installer est appuyé
+		if [[ $doInstall -eq 1 ]];then
+
+
+			if [ $resizeSD -eq 1 ];then	
+				resizeSDCardMenu # Redimensionnement de la carte SD
+			fi
+
+			# Renommer le Raspberry Pi
+			renameMenu
+
+			# Vérifier si git.idleman.fr est accessible
+			checkInternet
+			if [[ $internet -eq 1 ]];then
+
+				setLocaleToFrench # Mettre le terminal en français
+				updateRaspberryPi # Mettre à jour le Raspberry Pi (apt-get update/upgrade)
+				configureTimeZone # Configurer le fuseau horaire automatiquement (tzupdate)
+
+				# Clone le repo ldleman/yana
+				cloneYana
+
+				# Installe le serveur web
+				checkWebServer
+				if [[ $doInstallWebServer -eq 1 ]];then
+				 	installWebServer
+				 	setupWebServer
+				fi
+		
+				# Vérifie les permissions des fichiers et des binaires
+				checkPermissions
+			
+				# Install WiringPi (gpio)
+				installWiringPi
+
+				# Fait un lien de /var/www/yana-server/install.sh ver /usr/local/bin/configuration
+				linkInstaller
+			
+				# Affiche un message avec l'étape sur le web
+				endInstall
+				securityCheck
+				checkBinariesMenu
+				installYanaSocket
+				addCron
+				echo -e "$OK Installation TERMINE!!! Il est conseillé de $ERR redémarrer $OK votre raspberry pi $NORMAL"
+				echo -e "$INFO sudo reboot $NORMAL"
+			fi
+		fi
+	fi
+fi

+ 7 - 0
lib/sabre/autoload.php

@@ -0,0 +1,7 @@
+<?php
+
+// autoload.php @generated by Composer
+
+require_once __DIR__ . '/composer' . '/autoload_real.php';
+
+return ComposerAutoloaderInit4fa929ff0af55a078480b90770aa1d88::getLoader();

+ 413 - 0
lib/sabre/composer/ClassLoader.php

@@ -0,0 +1,413 @@
+<?php
+
+/*
+ * This file is part of Composer.
+ *
+ * (c) Nils Adermann <naderman@naderman.de>
+ *     Jordi Boggiano <j.boggiano@seld.be>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Composer\Autoload;
+
+/**
+ * ClassLoader implements a PSR-0, PSR-4 and classmap class loader.
+ *
+ *     $loader = new \Composer\Autoload\ClassLoader();
+ *
+ *     // register classes with namespaces
+ *     $loader->add('Symfony\Component', __DIR__.'/component');
+ *     $loader->add('Symfony',           __DIR__.'/framework');
+ *
+ *     // activate the autoloader
+ *     $loader->register();
+ *
+ *     // to enable searching the include path (eg. for PEAR packages)
+ *     $loader->setUseIncludePath(true);
+ *
+ * In this example, if you try to use a class in the Symfony\Component
+ * namespace or one of its children (Symfony\Component\Console for instance),
+ * the autoloader will first look for the class under the component/
+ * directory, and it will then fallback to the framework/ directory if not
+ * found before giving up.
+ *
+ * This class is loosely based on the Symfony UniversalClassLoader.
+ *
+ * @author Fabien Potencier <fabien@symfony.com>
+ * @author Jordi Boggiano <j.boggiano@seld.be>
+ * @see    http://www.php-fig.org/psr/psr-0/
+ * @see    http://www.php-fig.org/psr/psr-4/
+ */
+class ClassLoader
+{
+    // PSR-4
+    private $prefixLengthsPsr4 = array();
+    private $prefixDirsPsr4 = array();
+    private $fallbackDirsPsr4 = array();
+
+    // PSR-0
+    private $prefixesPsr0 = array();
+    private $fallbackDirsPsr0 = array();
+
+    private $useIncludePath = false;
+    private $classMap = array();
+
+    private $classMapAuthoritative = false;
+
+    public function getPrefixes()
+    {
+        if (!empty($this->prefixesPsr0)) {
+            return call_user_func_array('array_merge', $this->prefixesPsr0);
+        }
+
+        return array();
+    }
+
+    public function getPrefixesPsr4()
+    {
+        return $this->prefixDirsPsr4;
+    }
+
+    public function getFallbackDirs()
+    {
+        return $this->fallbackDirsPsr0;
+    }
+
+    public function getFallbackDirsPsr4()
+    {
+        return $this->fallbackDirsPsr4;
+    }
+
+    public function getClassMap()
+    {
+        return $this->classMap;
+    }
+
+    /**
+     * @param array $classMap Class to filename map
+     */
+    public function addClassMap(array $classMap)
+    {
+        if ($this->classMap) {
+            $this->classMap = array_merge($this->classMap, $classMap);
+        } else {
+            $this->classMap = $classMap;
+        }
+    }
+
+    /**
+     * Registers a set of PSR-0 directories for a given prefix, either
+     * appending or prepending to the ones previously set for this prefix.
+     *
+     * @param string       $prefix  The prefix
+     * @param array|string $paths   The PSR-0 root directories
+     * @param bool         $prepend Whether to prepend the directories
+     */
+    public function add($prefix, $paths, $prepend = false)
+    {
+        if (!$prefix) {
+            if ($prepend) {
+                $this->fallbackDirsPsr0 = array_merge(
+                    (array) $paths,
+                    $this->fallbackDirsPsr0
+                );
+            } else {
+                $this->fallbackDirsPsr0 = array_merge(
+                    $this->fallbackDirsPsr0,
+                    (array) $paths
+                );
+            }
+
+            return;
+        }
+
+        $first = $prefix[0];
+        if (!isset($this->prefixesPsr0[$first][$prefix])) {
+            $this->prefixesPsr0[$first][$prefix] = (array) $paths;
+
+            return;
+        }
+        if ($prepend) {
+            $this->prefixesPsr0[$first][$prefix] = array_merge(
+                (array) $paths,
+                $this->prefixesPsr0[$first][$prefix]
+            );
+        } else {
+            $this->prefixesPsr0[$first][$prefix] = array_merge(
+                $this->prefixesPsr0[$first][$prefix],
+                (array) $paths
+            );
+        }
+    }
+
+    /**
+     * Registers a set of PSR-4 directories for a given namespace, either
+     * appending or prepending to the ones previously set for this namespace.
+     *
+     * @param string       $prefix  The prefix/namespace, with trailing '\\'
+     * @param array|string $paths   The PSR-4 base directories
+     * @param bool         $prepend Whether to prepend the directories
+     *
+     * @throws \InvalidArgumentException
+     */
+    public function addPsr4($prefix, $paths, $prepend = false)
+    {
+        if (!$prefix) {
+            // Register directories for the root namespace.
+            if ($prepend) {
+                $this->fallbackDirsPsr4 = array_merge(
+                    (array) $paths,
+                    $this->fallbackDirsPsr4
+                );
+            } else {
+                $this->fallbackDirsPsr4 = array_merge(
+                    $this->fallbackDirsPsr4,
+                    (array) $paths
+                );
+            }
+        } elseif (!isset($this->prefixDirsPsr4[$prefix])) {
+            // Register directories for a new namespace.
+            $length = strlen($prefix);
+            if ('\\' !== $prefix[$length - 1]) {
+                throw new \InvalidArgumentException("A non-empty PSR-4 prefix must end with a namespace separator.");
+            }
+            $this->prefixLengthsPsr4[$prefix[0]][$prefix] = $length;
+            $this->prefixDirsPsr4[$prefix] = (array) $paths;
+        } elseif ($prepend) {
+            // Prepend directories for an already registered namespace.
+            $this->prefixDirsPsr4[$prefix] = array_merge(
+                (array) $paths,
+                $this->prefixDirsPsr4[$prefix]
+            );
+        } else {
+            // Append directories for an already registered namespace.
+            $this->prefixDirsPsr4[$prefix] = array_merge(
+                $this->prefixDirsPsr4[$prefix],
+                (array) $paths
+            );
+        }
+    }
+
+    /**
+     * Registers a set of PSR-0 directories for a given prefix,
+     * replacing any others previously set for this prefix.
+     *
+     * @param string       $prefix The prefix
+     * @param array|string $paths  The PSR-0 base directories
+     */
+    public function set($prefix, $paths)
+    {
+        if (!$prefix) {
+            $this->fallbackDirsPsr0 = (array) $paths;
+        } else {
+            $this->prefixesPsr0[$prefix[0]][$prefix] = (array) $paths;
+        }
+    }
+
+    /**
+     * Registers a set of PSR-4 directories for a given namespace,
+     * replacing any others previously set for this namespace.
+     *
+     * @param string       $prefix The prefix/namespace, with trailing '\\'
+     * @param array|string $paths  The PSR-4 base directories
+     *
+     * @throws \InvalidArgumentException
+     */
+    public function setPsr4($prefix, $paths)
+    {
+        if (!$prefix) {
+            $this->fallbackDirsPsr4 = (array) $paths;
+        } else {
+            $length = strlen($prefix);
+            if ('\\' !== $prefix[$length - 1]) {
+                throw new \InvalidArgumentException("A non-empty PSR-4 prefix must end with a namespace separator.");
+            }
+            $this->prefixLengthsPsr4[$prefix[0]][$prefix] = $length;
+            $this->prefixDirsPsr4[$prefix] = (array) $paths;
+        }
+    }
+
+    /**
+     * Turns on searching the include path for class files.
+     *
+     * @param bool $useIncludePath
+     */
+    public function setUseIncludePath($useIncludePath)
+    {
+        $this->useIncludePath = $useIncludePath;
+    }
+
+    /**
+     * Can be used to check if the autoloader uses the include path to check
+     * for classes.
+     *
+     * @return bool
+     */
+    public function getUseIncludePath()
+    {
+        return $this->useIncludePath;
+    }
+
+    /**
+     * Turns off searching the prefix and fallback directories for classes
+     * that have not been registered with the class map.
+     *
+     * @param bool $classMapAuthoritative
+     */
+    public function setClassMapAuthoritative($classMapAuthoritative)
+    {
+        $this->classMapAuthoritative = $classMapAuthoritative;
+    }
+
+    /**
+     * Should class lookup fail if not found in the current class map?
+     *
+     * @return bool
+     */
+    public function isClassMapAuthoritative()
+    {
+        return $this->classMapAuthoritative;
+    }
+
+    /**
+     * Registers this instance as an autoloader.
+     *
+     * @param bool $prepend Whether to prepend the autoloader or not
+     */
+    public function register($prepend = false)
+    {
+        spl_autoload_register(array($this, 'loadClass'), true, $prepend);
+    }
+
+    /**
+     * Unregisters this instance as an autoloader.
+     */
+    public function unregister()
+    {
+        spl_autoload_unregister(array($this, 'loadClass'));
+    }
+
+    /**
+     * Loads the given class or interface.
+     *
+     * @param  string    $class The name of the class
+     * @return bool|null True if loaded, null otherwise
+     */
+    public function loadClass($class)
+    {
+        if ($file = $this->findFile($class)) {
+            includeFile($file);
+
+            return true;
+        }
+    }
+
+    /**
+     * Finds the path to the file where the class is defined.
+     *
+     * @param string $class The name of the class
+     *
+     * @return string|false The path if found, false otherwise
+     */
+    public function findFile($class)
+    {
+        // work around for PHP 5.3.0 - 5.3.2 https://bugs.php.net/50731
+        if ('\\' == $class[0]) {
+            $class = substr($class, 1);
+        }
+
+        // class map lookup
+        if (isset($this->classMap[$class])) {
+            return $this->classMap[$class];
+        }
+        if ($this->classMapAuthoritative) {
+            return false;
+        }
+
+        $file = $this->findFileWithExtension($class, '.php');
+
+        // Search for Hack files if we are running on HHVM
+        if ($file === null && defined('HHVM_VERSION')) {
+            $file = $this->findFileWithExtension($class, '.hh');
+        }
+
+        if ($file === null) {
+            // Remember that this class does not exist.
+            return $this->classMap[$class] = false;
+        }
+
+        return $file;
+    }
+
+    private function findFileWithExtension($class, $ext)
+    {
+        // PSR-4 lookup
+        $logicalPathPsr4 = strtr($class, '\\', DIRECTORY_SEPARATOR) . $ext;
+
+        $first = $class[0];
+        if (isset($this->prefixLengthsPsr4[$first])) {
+            foreach ($this->prefixLengthsPsr4[$first] as $prefix => $length) {
+                if (0 === strpos($class, $prefix)) {
+                    foreach ($this->prefixDirsPsr4[$prefix] as $dir) {
+                        if (file_exists($file = $dir . DIRECTORY_SEPARATOR . substr($logicalPathPsr4, $length))) {
+                            return $file;
+                        }
+                    }
+                }
+            }
+        }
+
+        // PSR-4 fallback dirs
+        foreach ($this->fallbackDirsPsr4 as $dir) {
+            if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr4)) {
+                return $file;
+            }
+        }
+
+        // PSR-0 lookup
+        if (false !== $pos = strrpos($class, '\\')) {
+            // namespaced class name
+            $logicalPathPsr0 = substr($logicalPathPsr4, 0, $pos + 1)
+                . strtr(substr($logicalPathPsr4, $pos + 1), '_', DIRECTORY_SEPARATOR);
+        } else {
+            // PEAR-like class name
+            $logicalPathPsr0 = strtr($class, '_', DIRECTORY_SEPARATOR) . $ext;
+        }
+
+        if (isset($this->prefixesPsr0[$first])) {
+            foreach ($this->prefixesPsr0[$first] as $prefix => $dirs) {
+                if (0 === strpos($class, $prefix)) {
+                    foreach ($dirs as $dir) {
+                        if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) {
+                            return $file;
+                        }
+                    }
+                }
+            }
+        }
+
+        // PSR-0 fallback dirs
+        foreach ($this->fallbackDirsPsr0 as $dir) {
+            if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) {
+                return $file;
+            }
+        }
+
+        // PSR-0 include paths.
+        if ($this->useIncludePath && $file = stream_resolve_include_path($logicalPathPsr0)) {
+            return $file;
+        }
+    }
+}
+
+/**
+ * Scope isolated include.
+ *
+ * Prevents access to $this/self from included files.
+ */
+function includeFile($file)
+{
+    include $file;
+}

+ 19 - 0
lib/sabre/composer/LICENSE

@@ -0,0 +1,19 @@
+Copyright (c) 2015 Nils Adermann, Jordi Boggiano
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is furnished
+to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.

+ 9 - 0
lib/sabre/composer/autoload_classmap.php

@@ -0,0 +1,9 @@
+<?php
+
+// autoload_classmap.php @generated by Composer
+
+$vendorDir = dirname(dirname(__FILE__));
+$baseDir = dirname($vendorDir);
+
+return array(
+);

+ 16 - 0
lib/sabre/composer/autoload_files.php

@@ -0,0 +1,16 @@
+<?php
+
+// autoload_files.php @generated by Composer
+
+$vendorDir = dirname(dirname(__FILE__));
+$baseDir = dirname($vendorDir);
+
+return array(
+    '383eaff206634a77a1be54e64e6459c7' => $vendorDir . '/sabre/uri/lib/functions.php',
+    '2b9d0f43f9552984cfa82fee95491826' => $vendorDir . '/sabre/event/lib/coroutine.php',
+    'd81bab31d3feb45bfe2f283ea3c8fdf7' => $vendorDir . '/sabre/event/lib/Loop/functions.php',
+    'a1cce3d26cc15c00fcd0b3354bd72c88' => $vendorDir . '/sabre/event/lib/Promise/functions.php',
+    '3569eecfeed3bcf0bad3c998a494ecb8' => $vendorDir . '/sabre/xml/lib/Deserializer/functions.php',
+    '93aa591bc4ca510c520999e34229ee79' => $vendorDir . '/sabre/xml/lib/Serializer/functions.php',
+    'ebdb698ed4152ae445614b69b5e4bb6a' => $vendorDir . '/sabre/http/lib/functions.php',
+);

+ 9 - 0
lib/sabre/composer/autoload_namespaces.php

@@ -0,0 +1,9 @@
+<?php
+
+// autoload_namespaces.php @generated by Composer
+
+$vendorDir = dirname(dirname(__FILE__));
+$baseDir = dirname($vendorDir);
+
+return array(
+);

+ 18 - 0
lib/sabre/composer/autoload_psr4.php

@@ -0,0 +1,18 @@
+<?php
+
+// autoload_psr4.php @generated by Composer
+
+$vendorDir = dirname(dirname(__FILE__));
+$baseDir = dirname($vendorDir);
+
+return array(
+    'Sabre\\Xml\\' => array($vendorDir . '/sabre/xml/lib'),
+    'Sabre\\VObject\\' => array($vendorDir . '/sabre/vobject/lib'),
+    'Sabre\\Uri\\' => array($vendorDir . '/sabre/uri/lib'),
+    'Sabre\\HTTP\\' => array($vendorDir . '/sabre/http/lib'),
+    'Sabre\\Event\\' => array($vendorDir . '/sabre/event/lib'),
+    'Sabre\\DAV\\' => array($vendorDir . '/sabre/dav/lib/DAV'),
+    'Sabre\\DAVACL\\' => array($vendorDir . '/sabre/dav/lib/DAVACL'),
+    'Sabre\\CardDAV\\' => array($vendorDir . '/sabre/dav/lib/CardDAV'),
+    'Sabre\\CalDAV\\' => array($vendorDir . '/sabre/dav/lib/CalDAV'),
+);

+ 59 - 0
lib/sabre/composer/autoload_real.php

@@ -0,0 +1,59 @@
+<?php
+
+// autoload_real.php @generated by Composer
+
+class ComposerAutoloaderInit4fa929ff0af55a078480b90770aa1d88
+{
+    private static $loader;
+
+    public static function loadClassLoader($class)
+    {
+        if ('Composer\Autoload\ClassLoader' === $class) {
+            require __DIR__ . '/ClassLoader.php';
+        }
+    }
+
+    public static function getLoader()
+    {
+        if (null !== self::$loader) {
+            return self::$loader;
+        }
+
+        spl_autoload_register(array('ComposerAutoloaderInit4fa929ff0af55a078480b90770aa1d88', 'loadClassLoader'), true, true);
+        self::$loader = $loader = new \Composer\Autoload\ClassLoader();
+        spl_autoload_unregister(array('ComposerAutoloaderInit4fa929ff0af55a078480b90770aa1d88', 'loadClassLoader'));
+
+        $map = require __DIR__ . '/autoload_namespaces.php';
+        foreach ($map as $namespace => $path) {
+            $loader->set($namespace, $path);
+        }
+
+        $map = require __DIR__ . '/autoload_psr4.php';
+        foreach ($map as $namespace => $path) {
+            $loader->setPsr4($namespace, $path);
+        }
+
+        $classMap = require __DIR__ . '/autoload_classmap.php';
+        if ($classMap) {
+            $loader->addClassMap($classMap);
+        }
+
+        $loader->register(true);
+
+        $includeFiles = require __DIR__ . '/autoload_files.php';
+        foreach ($includeFiles as $fileIdentifier => $file) {
+            composerRequire4fa929ff0af55a078480b90770aa1d88($fileIdentifier, $file);
+        }
+
+        return $loader;
+    }
+}
+
+function composerRequire4fa929ff0af55a078480b90770aa1d88($fileIdentifier, $file)
+{
+    if (empty($GLOBALS['__composer_autoload_files'][$fileIdentifier])) {
+        require $file;
+
+        $GLOBALS['__composer_autoload_files'][$fileIdentifier] = true;
+    }
+}

+ 416 - 0
lib/sabre/composer/installed.json

@@ -0,0 +1,416 @@
+[
+    {
+        "name": "sabre/uri",
+        "version": "1.0.1",
+        "version_normalized": "1.0.1.0",
+        "source": {
+            "type": "git",
+            "url": "https://github.com/fruux/sabre-uri.git",
+            "reference": "6bae7efdd9dfcfdb3edfc4362741e59ce4b64f42"
+        },
+        "dist": {
+            "type": "zip",
+            "url": "https://api.github.com/repos/fruux/sabre-uri/zipball/6bae7efdd9dfcfdb3edfc4362741e59ce4b64f42",
+            "reference": "6bae7efdd9dfcfdb3edfc4362741e59ce4b64f42",
+            "shasum": ""
+        },
+        "require": {
+            "php": ">=5.4.7"
+        },
+        "require-dev": {
+            "phpunit/phpunit": "*",
+            "sabre/cs": "~0.0.1"
+        },
+        "time": "2015-04-29 03:47:26",
+        "type": "library",
+        "installation-source": "dist",
+        "autoload": {
+            "files": [
+                "lib/functions.php"
+            ],
+            "psr-4": {
+                "Sabre\\Uri\\": "lib/"
+            }
+        },
+        "notification-url": "https://packagist.org/downloads/",
+        "license": [
+            "BSD-3-Clause"
+        ],
+        "authors": [
+            {
+                "name": "Evert Pot",
+                "email": "me@evertpot.com",
+                "homepage": "http://evertpot.com/",
+                "role": "Developer"
+            }
+        ],
+        "description": "Functions for making sense out of URIs.",
+        "homepage": "http://sabre.io/uri/",
+        "keywords": [
+            "rfc3986",
+            "uri",
+            "url"
+        ]
+    },
+    {
+        "name": "sabre/event",
+        "version": "3.0.0",
+        "version_normalized": "3.0.0.0",
+        "source": {
+            "type": "git",
+            "url": "https://github.com/fruux/sabre-event.git",
+            "reference": "831d586f5a442dceacdcf5e9c4c36a4db99a3534"
+        },
+        "dist": {
+            "type": "zip",
+            "url": "https://api.github.com/repos/fruux/sabre-event/zipball/831d586f5a442dceacdcf5e9c4c36a4db99a3534",
+            "reference": "831d586f5a442dceacdcf5e9c4c36a4db99a3534",
+            "shasum": ""
+        },
+        "require": {
+            "php": ">=5.5"
+        },
+        "require-dev": {
+            "phpunit/phpunit": "*",
+            "sabre/cs": "~0.0.4"
+        },
+        "time": "2015-11-05 20:14:39",
+        "type": "library",
+        "installation-source": "dist",
+        "autoload": {
+            "psr-4": {
+                "Sabre\\Event\\": "lib/"
+            },
+            "files": [
+                "lib/coroutine.php",
+                "lib/Loop/functions.php",
+                "lib/Promise/functions.php"
+            ]
+        },
+        "notification-url": "https://packagist.org/downloads/",
+        "license": [
+            "BSD-3-Clause"
+        ],
+        "authors": [
+            {
+                "name": "Evert Pot",
+                "email": "me@evertpot.com",
+                "homepage": "http://evertpot.com/",
+                "role": "Developer"
+            }
+        ],
+        "description": "sabre/event is a library for lightweight event-based programming",
+        "homepage": "http://sabre.io/event/",
+        "keywords": [
+            "EventEmitter",
+            "async",
+            "events",
+            "hooks",
+            "plugin",
+            "promise",
+            "signal"
+        ]
+    },
+    {
+        "name": "sabre/http",
+        "version": "4.2.1",
+        "version_normalized": "4.2.1.0",
+        "source": {
+            "type": "git",
+            "url": "https://github.com/fruux/sabre-http.git",
+            "reference": "2e93bc8321524c67be4ca5b8415daebd4c8bf85e"
+        },
+        "dist": {
+            "type": "zip",
+            "url": "https://api.github.com/repos/fruux/sabre-http/zipball/2e93bc8321524c67be4ca5b8415daebd4c8bf85e",
+            "reference": "2e93bc8321524c67be4ca5b8415daebd4c8bf85e",
+            "shasum": ""
+        },
+        "require": {
+            "ext-mbstring": "*",
+            "php": ">=5.4",
+            "sabre/event": ">=1.0.0,<4.0.0",
+            "sabre/uri": "~1.0"
+        },
+        "require-dev": {
+            "phpunit/phpunit": "~4.3",
+            "sabre/cs": "~0.0.1"
+        },
+        "suggest": {
+            "ext-curl": " to make http requests with the Client class"
+        },
+        "time": "2016-01-06 23:00:08",
+        "type": "library",
+        "installation-source": "dist",
+        "autoload": {
+            "files": [
+                "lib/functions.php"
+            ],
+            "psr-4": {
+                "Sabre\\HTTP\\": "lib/"
+            }
+        },
+        "notification-url": "https://packagist.org/downloads/",
+        "license": [
+            "BSD-3-Clause"
+        ],
+        "authors": [
+            {
+                "name": "Evert Pot",
+                "email": "me@evertpot.com",
+                "homepage": "http://evertpot.com/",
+                "role": "Developer"
+            }
+        ],
+        "description": "The sabre/http library provides utilities for dealing with http requests and responses. ",
+        "homepage": "https://github.com/fruux/sabre-http",
+        "keywords": [
+            "http"
+        ]
+    },
+    {
+        "name": "sabre/xml",
+        "version": "1.3.0",
+        "version_normalized": "1.3.0.0",
+        "source": {
+            "type": "git",
+            "url": "https://github.com/fruux/sabre-xml.git",
+            "reference": "420400f36655d79894fae8ce970516a71ea8f5f5"
+        },
+        "dist": {
+            "type": "zip",
+            "url": "https://api.github.com/repos/fruux/sabre-xml/zipball/420400f36655d79894fae8ce970516a71ea8f5f5",
+            "reference": "420400f36655d79894fae8ce970516a71ea8f5f5",
+            "shasum": ""
+        },
+        "require": {
+            "ext-dom": "*",
+            "ext-xmlreader": "*",
+            "ext-xmlwriter": "*",
+            "lib-libxml": ">=2.6.20",
+            "php": ">=5.4.1",
+            "sabre/uri": "~1.0"
+        },
+        "require-dev": {
+            "phpunit/phpunit": "*",
+            "sabre/cs": "~0.0.2"
+        },
+        "time": "2015-12-29 20:51:22",
+        "type": "library",
+        "installation-source": "dist",
+        "autoload": {
+            "psr-4": {
+                "Sabre\\Xml\\": "lib/"
+            },
+            "files": [
+                "lib/Deserializer/functions.php",
+                "lib/Serializer/functions.php"
+            ]
+        },
+        "notification-url": "https://packagist.org/downloads/",
+        "license": [
+            "BSD-3-Clause"
+        ],
+        "authors": [
+            {
+                "name": "Evert Pot",
+                "email": "me@evertpot.com",
+                "homepage": "http://evertpot.com/",
+                "role": "Developer"
+            },
+            {
+                "name": "Markus Staab",
+                "email": "markus.staab@redaxo.de",
+                "role": "Developer"
+            }
+        ],
+        "description": "sabre/xml is an XML library that you may not hate.",
+        "homepage": "https://sabre.io/xml/",
+        "keywords": [
+            "XMLReader",
+            "XMLWriter",
+            "dom",
+            "xml"
+        ]
+    },
+    {
+        "name": "sabre/vobject",
+        "version": "4.0.2",
+        "version_normalized": "4.0.2.0",
+        "source": {
+            "type": "git",
+            "url": "https://github.com/fruux/sabre-vobject.git",
+            "reference": "0d123ede292ab1a8d4f1a4efc1e809f67a5c6010"
+        },
+        "dist": {
+            "type": "zip",
+            "url": "https://api.github.com/repos/fruux/sabre-vobject/zipball/0d123ede292ab1a8d4f1a4efc1e809f67a5c6010",
+            "reference": "0d123ede292ab1a8d4f1a4efc1e809f67a5c6010",
+            "shasum": ""
+        },
+        "require": {
+            "ext-mbstring": "*",
+            "php": ">=5.5",
+            "sabre/xml": "~1.1"
+        },
+        "require-dev": {
+            "phpunit/phpunit": "*",
+            "sabre/cs": "~0.0.3"
+        },
+        "suggest": {
+            "hoa/bench": "If you would like to run the benchmark scripts"
+        },
+        "time": "2016-01-11 17:39:47",
+        "bin": [
+            "bin/vobject",
+            "bin/generate_vcards"
+        ],
+        "type": "library",
+        "extra": {
+            "branch-alias": {
+                "dev-master": "4.0.x-dev"
+            }
+        },
+        "installation-source": "dist",
+        "autoload": {
+            "psr-4": {
+                "Sabre\\VObject\\": "lib/"
+            }
+        },
+        "notification-url": "https://packagist.org/downloads/",
+        "license": [
+            "BSD-3-Clause"
+        ],
+        "authors": [
+            {
+                "name": "Evert Pot",
+                "email": "me@evertpot.com",
+                "homepage": "http://evertpot.com/",
+                "role": "Developer"
+            },
+            {
+                "name": "Dominik Tobschall",
+                "email": "dominik@fruux.com",
+                "homepage": "http://tobschall.de/",
+                "role": "Developer"
+            },
+            {
+                "name": "Ivan Enderlin",
+                "email": "ivan.enderlin@hoa-project.net",
+                "homepage": "http://mnt.io/",
+                "role": "Developer"
+            }
+        ],
+        "description": "The VObject library for PHP allows you to easily parse and manipulate iCalendar and vCard objects",
+        "homepage": "http://sabre.io/vobject/",
+        "keywords": [
+            "availability",
+            "freebusy",
+            "iCalendar",
+            "ics",
+            "jCal",
+            "jCard",
+            "recurrence",
+            "rfc2425",
+            "rfc2426",
+            "rfc2739",
+            "rfc4770",
+            "rfc5545",
+            "rfc5546",
+            "rfc6321",
+            "rfc6350",
+            "rfc6351",
+            "rfc6474",
+            "rfc6638",
+            "rfc6715",
+            "rfc6868",
+            "vCard",
+            "vcf",
+            "xCal",
+            "xCard"
+        ]
+    },
+    {
+        "name": "sabre/dav",
+        "version": "3.1.1",
+        "version_normalized": "3.1.1.0",
+        "source": {
+            "type": "git",
+            "url": "https://github.com/fruux/sabre-dav.git",
+            "reference": "14faf6e3a79d1255c190a2605273b86decc4de8d"
+        },
+        "dist": {
+            "type": "zip",
+            "url": "https://api.github.com/repos/fruux/sabre-dav/zipball/14faf6e3a79d1255c190a2605273b86decc4de8d",
+            "reference": "14faf6e3a79d1255c190a2605273b86decc4de8d",
+            "shasum": ""
+        },
+        "require": {
+            "ext-ctype": "*",
+            "ext-date": "*",
+            "ext-dom": "*",
+            "ext-iconv": "*",
+            "ext-libxml": "*",
+            "ext-mbstring": "*",
+            "ext-pcre": "*",
+            "ext-simplexml": "*",
+            "ext-spl": "*",
+            "php": ">=5.5.0",
+            "sabre/event": ">=2.0.0, <4.0.0",
+            "sabre/http": "^4.2.1",
+            "sabre/uri": "~1.0",
+            "sabre/vobject": "~4.0",
+            "sabre/xml": "~1.0"
+        },
+        "require-dev": {
+            "evert/phpdoc-md": "~0.1.0",
+            "phpunit/phpunit": "> 4.8, <=6.0.0",
+            "sabre/cs": "~0.0.5"
+        },
+        "suggest": {
+            "ext-curl": "*",
+            "ext-pdo": "*"
+        },
+        "time": "2016-01-25 21:22:21",
+        "bin": [
+            "bin/sabredav",
+            "bin/naturalselection"
+        ],
+        "type": "library",
+        "extra": {
+            "branch-alias": {
+                "dev-master": "3.1.0-dev"
+            }
+        },
+        "installation-source": "dist",
+        "autoload": {
+            "psr-4": {
+                "Sabre\\DAV\\": "lib/DAV/",
+                "Sabre\\DAVACL\\": "lib/DAVACL/",
+                "Sabre\\CalDAV\\": "lib/CalDAV/",
+                "Sabre\\CardDAV\\": "lib/CardDAV/"
+            }
+        },
+        "notification-url": "https://packagist.org/downloads/",
+        "license": [
+            "BSD-3-Clause"
+        ],
+        "authors": [
+            {
+                "name": "Evert Pot",
+                "email": "me@evertpot.com",
+                "homepage": "http://evertpot.com/",
+                "role": "Developer"
+            }
+        ],
+        "description": "WebDAV Framework for PHP",
+        "homepage": "http://sabre.io/",
+        "keywords": [
+            "CalDAV",
+            "CardDAV",
+            "WebDAV",
+            "framework",
+            "iCalendar"
+        ]
+    }
+]

+ 43 - 0
lib/sabre/sabre/dav/.gitignore

@@ -0,0 +1,43 @@
+# Unit tests
+tests/temp
+tests/.sabredav
+tests/cov
+
+# Custom settings for tests
+tests/config.user.php
+
+# ViM
+*.swp
+
+# Composer
+composer.lock
+vendor
+
+# Composer binaries
+bin/phing
+bin/phpunit
+bin/vobject
+bin/generate_vcards
+bin/phpdocmd
+bin/phpunit
+bin/php-cs-fixer
+bin/sabre-cs-fixer
+
+# Assuming every .php file in the root is for testing
+/*.php
+
+# Other testing stuff
+/tmpdata
+/data
+/public
+
+# Build
+build
+build.properties
+
+# Docs
+docs/api
+docs/wikidocs
+
+# Mac
+.DS_Store

+ 33 - 0
lib/sabre/sabre/dav/.travis.yml

@@ -0,0 +1,33 @@
+language: php
+php:
+  - 5.5
+  - 5.6
+  - 7
+  - hhvm
+
+matrix:
+  fast_finish: true
+  allow_failures:
+      - php: hhvm
+
+env:
+  matrix:
+    - LOWEST_DEPS="" TEST_DEPS=""
+    - LOWEST_DEPS="--prefer-lowest" TEST_DEPS="tests/Sabre/"
+
+services:
+  - mysql
+
+sudo: false
+
+cache: vendor
+
+before_script:
+  - mysql -e 'create database sabredav'
+  #  - composer self-update
+  - composer update --prefer-source $LOWEST_DEPS
+
+script:
+  - ./bin/phpunit --configuration tests/phpunit.xml $TEST_DEPS
+  - ./bin/sabre-cs-fixer fix lib/ --dry-run --diff
+

+ 87 - 0
lib/sabre/sabre/dav/CONTRIBUTING.md

@@ -0,0 +1,87 @@
+Contributing to sabre projects
+==============================
+
+Want to contribute to sabre/dav? Here are some guidelines to ensure your patch
+gets accepted.
+
+
+Building a new feature? Contact us first
+----------------------------------------
+
+We may not want to accept every feature that comes our way. Sometimes
+features are out of scope for our projects.
+
+We don't want to waste your time, so by having a quick chat with us first,
+you may find out quickly if the feature makes sense to us, and we can give
+some tips on how to best build the feature.
+
+If we don't accept the feature, it could be for a number of reasons. For
+instance, we've rejected features in the past because we felt uncomfortable
+assuming responsibility for maintaining the feature.
+
+In those cases, it's often possible to keep the feature separate from the
+sabre projects. sabre/dav for instance has a plugin system, and there's no
+reason the feature can't live in a project you own.
+
+In that case, definitely let us know about your plugin as well, so we can
+feature it on [sabre.io][4].
+
+We are often on [IRC][5], in the #sabredav channel on freenode. If there's
+no one there, post a message on the [mailing list][6].
+
+
+Coding standards
+----------------
+
+sabre projects follow:
+
+1. [PSR-1][1]
+2. [PSR-4][2]
+
+sabre projects don't follow [PSR-2][3].
+
+In addition to that, here's a list of basic rules:
+
+1. PHP 5.4 array syntax must be used every where. This means you use `[` and
+   `]` instead of `array(` and `)`.
+2. Use PHP namespaces everywhere.
+3. Use 4 spaces for indentation.
+4. Try to keep your lines under 80 characters. This is not a hard rule, as
+   there are many places in the source where it felt more sensibile to not
+   do so. In particular, function declarations are never split over multiple
+   lines.
+5. Opening braces (`{`) are _always_ on the same line as the `class`, `if`,
+   `function`, etc. they belong to.
+6. `public` must be omitted from method declarations. It must also be omitted
+   for static properties.
+7. All files should use unix-line endings (`\n`).
+8. Files must omit the closing php tag (`?>`).
+9. `true`, `false` and `null` are always lower-case.
+10. Constants are always upper-case.
+11. Any of the rules stated before may be broken where this is the pragmatic
+    thing to do.
+
+
+Unit test requirements
+----------------------
+
+Any new feature or change requires unittests. We use [PHPUnit][7] for all our
+tests.
+
+Adding unittests will greatly increase the likelyhood of us quickly accepting
+your pull request. If unittests are not included though for whatever reason,
+we'd still _love_ your pull request.
+
+We may have to write the tests ourselves, which can increase the time it takes
+to accept the patch, but we'd still really like your contribution!
+
+To run the testsuite jump into the directory `cd tests` and trigger `phpunit`.
+Make sure you did a `composer install` beforehand.
+
+[1]: http://www.php-fig.org/psr/psr-1/
+[2]: http://www.php-fig.org/psr/psr-4/
+[3]: http://www.php-fig.org/psr/psr-2/
+[4]: http://sabre.io/
+[5]: irc://freenode.net/#sabredav
+[6]: http://groups.google.com/group/sabredav-discuss
+[7]: http://phpunit.de/

+ 177 - 0
lib/sabre/sabre/dav/bin/build.php

@@ -0,0 +1,177 @@
+#!/usr/bin/env php
+<?php
+
+$tasks = [
+
+    'buildzip' => [
+        'init', 'test', 'clean',
+    ],
+    'markrelease' => [
+        'init', 'test', 'clean',
+    ],
+    'clean' => [],
+    'test'  => [
+        'composerupdate',
+    ],
+    'init'           => [],
+    'composerupdate' => [],
+ ];
+
+$default = 'buildzip';
+
+$baseDir = __DIR__ . '/../';
+chdir($baseDir);
+
+$currentTask = $default;
+if ($argc > 1) $currentTask = $argv[1];
+$version = null;
+if ($argc > 2) $version = $argv[2];
+
+if (!isset($tasks[$currentTask])) {
+    echo "Task not found: ",  $currentTask, "\n";
+    die(1);
+}
+
+// Creating the dependency graph
+$newTaskList = [];
+$oldTaskList = [$currentTask => true];
+
+while (count($oldTaskList) > 0) {
+
+    foreach ($oldTaskList as $task => $foo) {
+
+        if (!isset($tasks[$task])) {
+            echo "Dependency not found: " . $task, "\n";
+            die(1);
+        }
+        $dependencies = $tasks[$task];
+
+        $fullFilled = true;
+        foreach ($dependencies as $dependency) {
+            if (isset($newTaskList[$dependency])) {
+                // Already in the fulfilled task list.
+                continue;
+            } else {
+                $oldTaskList[$dependency] = true;
+                $fullFilled = false;
+            }
+           
+        }
+        if ($fullFilled) {
+            unset($oldTaskList[$task]);
+            $newTaskList[$task] = 1;
+        }
+
+    }
+
+}
+
+foreach (array_keys($newTaskList) as $task) {
+
+    echo "task: " . $task, "\n";
+    call_user_func($task);
+    echo "\n";
+
+}
+
+function init() {
+
+    global $version;
+    if (!$version) {
+        include __DIR__ . '/../vendor/autoload.php';
+        $version = Sabre\DAV\Version::VERSION;
+    }
+
+    echo "  Building sabre/dav " . $version, "\n";
+
+}
+
+function clean() {
+
+    global $baseDir;
+    echo "  Removing build files\n";
+    $outputDir = $baseDir . '/build/SabreDAV';
+    if (is_dir($outputDir)) {
+        system('rm -r ' . $baseDir . '/build/SabreDAV');
+    }
+
+}
+
+function composerupdate() {
+
+    global $baseDir;
+    echo "  Updating composer packages to latest version\n\n";
+    system('cd ' . $baseDir . '; composer update');
+}
+
+function test() {
+
+    global $baseDir;
+
+    echo "  Running all unittests.\n";
+    echo "  This may take a while.\n\n";
+    system(__DIR__ . '/phpunit --configuration ' . $baseDir . '/tests/phpunit.xml --stop-on-failure', $code);
+    if ($code != 0) {
+        echo "PHPUnit reported error code $code\n";
+        die(1);
+    }
+   
+}
+
+function buildzip() {
+
+    global $baseDir, $version;
+    echo "  Generating composer.json\n";
+
+    $input = json_decode(file_get_contents(__DIR__ . '/../composer.json'), true);
+    $newComposer = [
+        "require" => $input['require'],
+        "config"  => [
+            "bin-dir" => "./bin",
+        ],
+        "prefer-stable"     => true,
+        "minimum-stability" => "alpha",
+    ];
+    unset(
+        $newComposer['require']['sabre/vobject'],
+        $newComposer['require']['sabre/http'],
+        $newComposer['require']['sabre/uri'],
+        $newComposer['require']['sabre/event']
+    );
+    $newComposer['require']['sabre/dav'] = $version;
+    mkdir('build/SabreDAV');
+    file_put_contents('build/SabreDAV/composer.json', json_encode($newComposer, JSON_PRETTY_PRINT));
+
+    echo "  Downloading dependencies\n";
+    system("cd build/SabreDAV; composer install -n", $code);
+    if ($code !== 0) {
+        echo "Composer reported error code $code\n";
+        die(1);
+    }
+
+    echo "  Removing pointless files\n";
+    unlink('build/SabreDAV/composer.json');
+    unlink('build/SabreDAV/composer.lock');
+
+    echo "  Moving important files to the root of the project\n";
+
+    $fileNames = [
+        'CHANGELOG.md',
+        'LICENSE',
+        'README.md',
+        'examples',
+    ];
+    foreach ($fileNames as $fileName) {
+        echo "    $fileName\n";
+        rename('build/SabreDAV/vendor/sabre/dav/' . $fileName, 'build/SabreDAV/' . $fileName);
+    }
+
+    // <zip destfile="build/SabreDAV-${sabredav.version}.zip" basedir="build/SabreDAV" prefix="SabreDAV/" />
+
+    echo "\n";
+    echo "Zipping the sabredav distribution\n\n";
+    system('cd build; zip -qr sabredav-' . $version . '.zip SabreDAV');
+
+    echo "Done.";
+
+}

+ 248 - 0
lib/sabre/sabre/dav/bin/googlecode_upload.py

@@ -0,0 +1,248 @@
+#!/usr/bin/env python
+#
+# Copyright 2006, 2007 Google Inc. All Rights Reserved.
+# Author: danderson@google.com (David Anderson)
+#
+# Script for uploading files to a Google Code project.
+#
+# This is intended to be both a useful script for people who want to
+# streamline project uploads and a reference implementation for
+# uploading files to Google Code projects.
+#
+# To upload a file to Google Code, you need to provide a path to the
+# file on your local machine, a small summary of what the file is, a
+# project name, and a valid account that is a member or owner of that
+# project.  You can optionally provide a list of labels that apply to
+# the file.  The file will be uploaded under the same name that it has
+# in your local filesystem (that is, the "basename" or last path
+# component).  Run the script with '--help' to get the exact syntax
+# and available options.
+#
+# Note that the upload script requests that you enter your
+# googlecode.com password.  This is NOT your Gmail account password!
+# This is the password you use on googlecode.com for committing to
+# Subversion and uploading files.  You can find your password by going
+# to http://code.google.com/hosting/settings when logged in with your
+# Gmail account. If you have already committed to your project's
+# Subversion repository, the script will automatically retrieve your
+# credentials from there (unless disabled, see the output of '--help'
+# for details).
+#
+# If you are looking at this script as a reference for implementing
+# your own Google Code file uploader, then you should take a look at
+# the upload() function, which is the meat of the uploader.  You
+# basically need to build a multipart/form-data POST request with the
+# right fields and send it to https://PROJECT.googlecode.com/files .
+# Authenticate the request using HTTP Basic authentication, as is
+# shown below.
+#
+# Licensed under the terms of the Apache Software License 2.0:
+#  http://www.apache.org/licenses/LICENSE-2.0
+#
+# Questions, comments, feature requests and patches are most welcome.
+# Please direct all of these to the Google Code users group:
+#  http://groups.google.com/group/google-code-hosting
+
+"""Google Code file uploader script.
+"""
+
+__author__ = 'danderson@google.com (David Anderson)'
+
+import httplib
+import os.path
+import optparse
+import getpass
+import base64
+import sys
+
+
+def upload(file, project_name, user_name, password, summary, labels=None):
+  """Upload a file to a Google Code project's file server.
+
+  Args:
+    file: The local path to the file.
+    project_name: The name of your project on Google Code.
+    user_name: Your Google account name.
+    password: The googlecode.com password for your account.
+              Note that this is NOT your global Google Account password!
+    summary: A small description for the file.
+    labels: an optional list of label strings with which to tag the file.
+
+  Returns: a tuple:
+    http_status: 201 if the upload succeeded, something else if an
+                 error occurred.
+    http_reason: The human-readable string associated with http_status
+    file_url: If the upload succeeded, the URL of the file on Google
+              Code, None otherwise.
+  """
+  # The login is the user part of user@gmail.com. If the login provided
+  # is in the full user@domain form, strip it down.
+  if user_name.endswith('@gmail.com'):
+    user_name = user_name[:user_name.index('@gmail.com')]
+
+  form_fields = [('summary', summary)]
+  if labels is not None:
+    form_fields.extend([('label', l.strip()) for l in labels])
+
+  content_type, body = encode_upload_request(form_fields, file)
+
+  upload_host = '%s.googlecode.com' % project_name
+  upload_uri = '/files'
+  auth_token = base64.b64encode('%s:%s'% (user_name, password))
+  headers = {
+    'Authorization': 'Basic %s' % auth_token,
+    'User-Agent': 'Googlecode.com uploader v0.9.4',
+    'Content-Type': content_type,
+    }
+
+  server = httplib.HTTPSConnection(upload_host)
+  server.request('POST', upload_uri, body, headers)
+  resp = server.getresponse()
+  server.close()
+
+  if resp.status == 201:
+    location = resp.getheader('Location', None)
+  else:
+    location = None
+  return resp.status, resp.reason, location
+
+
+def encode_upload_request(fields, file_path):
+  """Encode the given fields and file into a multipart form body.
+
+  fields is a sequence of (name, value) pairs. file is the path of
+  the file to upload. The file will be uploaded to Google Code with
+  the same file name.
+
+  Returns: (content_type, body) ready for httplib.HTTP instance
+  """
+  BOUNDARY = '----------Googlecode_boundary_reindeer_flotilla'
+  CRLF = '\r\n'
+
+  body = []
+
+  # Add the metadata about the upload first
+  for key, value in fields:
+    body.extend(
+      ['--' + BOUNDARY,
+       'Content-Disposition: form-data; name="%s"' % key,
+       '',
+       value,
+       ])
+
+  # Now add the file itself
+  file_name = os.path.basename(file_path)
+  f = open(file_path, 'rb')
+  file_content = f.read()
+  f.close()
+
+  body.extend(
+    ['--' + BOUNDARY,
+     'Content-Disposition: form-data; name="filename"; filename="%s"'
+     % file_name,
+     # The upload server determines the mime-type, no need to set it.
+     'Content-Type: application/octet-stream',
+     '',
+     file_content,
+     ])
+
+  # Finalize the form body
+  body.extend(['--' + BOUNDARY + '--', ''])
+
+  return 'multipart/form-data; boundary=%s' % BOUNDARY, CRLF.join(body)
+
+
+def upload_find_auth(file_path, project_name, summary, labels=None,
+                     user_name=None, password=None, tries=3):
+  """Find credentials and upload a file to a Google Code project's file server.
+
+  file_path, project_name, summary, and labels are passed as-is to upload.
+
+  Args:
+    file_path: The local path to the file.
+    project_name: The name of your project on Google Code.
+    summary: A small description for the file.
+    labels: an optional list of label strings with which to tag the file.
+    config_dir: Path to Subversion configuration directory, 'none', or None.
+    user_name: Your Google account name.
+    tries: How many attempts to make.
+  """
+
+  while tries > 0:
+    if user_name is None:
+      # Read username if not specified or loaded from svn config, or on
+      # subsequent tries.
+      sys.stdout.write('Please enter your googlecode.com username: ')
+      sys.stdout.flush()
+      user_name = sys.stdin.readline().rstrip()
+    if password is None:
+      # Read password if not loaded from svn config, or on subsequent tries.
+      print 'Please enter your googlecode.com password.'
+      print '** Note that this is NOT your Gmail account password! **'
+      print 'It is the password you use to access Subversion repositories,'
+      print 'and can be found here: http://code.google.com/hosting/settings'
+      password = getpass.getpass()
+
+    status, reason, url = upload(file_path, project_name, user_name, password,
+                                 summary, labels)
+    # Returns 403 Forbidden instead of 401 Unauthorized for bad
+    # credentials as of 2007-07-17.
+    if status in [httplib.FORBIDDEN, httplib.UNAUTHORIZED]:
+      # Rest for another try.
+      user_name = password = None
+      tries = tries - 1
+    else:
+      # We're done.
+      break
+
+  return status, reason, url
+
+
+def main():
+  parser = optparse.OptionParser(usage='googlecode-upload.py -s SUMMARY '
+                                 '-p PROJECT [options] FILE')
+  parser.add_option('-s', '--summary', dest='summary',
+                    help='Short description of the file')
+  parser.add_option('-p', '--project', dest='project',
+                    help='Google Code project name')
+  parser.add_option('-u', '--user', dest='user',
+                    help='Your Google Code username')
+  parser.add_option('-w', '--password', dest='password',
+                    help='Your Google Code password')
+  parser.add_option('-l', '--labels', dest='labels',
+                    help='An optional list of comma-separated labels to attach '
+                    'to the file')
+
+  options, args = parser.parse_args()
+
+  if not options.summary:
+    parser.error('File summary is missing.')
+  elif not options.project:
+    parser.error('Project name is missing.')
+  elif len(args) < 1:
+    parser.error('File to upload not provided.')
+  elif len(args) > 1:
+    parser.error('Only one file may be specified.')
+
+  file_path = args[0]
+
+  if options.labels:
+    labels = options.labels.split(',')
+  else:
+    labels = None
+
+  status, reason, url = upload_find_auth(file_path, options.project,
+                                         options.summary, labels,
+                                         options.user, options.password)
+  if url:
+    print 'The file was uploaded successfully.'
+    print 'URL: %s' % url
+    return 0
+  else:
+    print 'An error occurred. Your file was not uploaded.'
+    print 'Google Code upload server said: %s (%s)' % (reason, status)
+    return 1
+
+
+if __name__ == '__main__':
+  sys.exit(main())

+ 284 - 0
lib/sabre/sabre/dav/bin/migrateto17.php

@@ -0,0 +1,284 @@
+#!/usr/bin/env php
+<?php
+
+echo "SabreDAV migrate script for version 1.7\n";
+
+if ($argc < 2) {
+
+    echo <<<HELLO
+
+This script help you migrate from a pre-1.7 database to 1.7 and later\n
+Both the 'calendarobjects' and 'calendars' tables will be upgraded.
+
+If you do not have this table, or don't use the default PDO CalDAV backend
+it's pointless to run this script.
+
+Keep in mind that some processing will be done on every single record of this
+table and in addition, ALTER TABLE commands will be executed.
+If you have a large calendarobjects table, this may mean that this process
+takes a while.
+
+Usage:
+
+php {$argv[0]} [pdo-dsn] [username] [password]
+
+For example:
+
+php {$argv[0]} "mysql:host=localhost;dbname=sabredav" root password
+php {$argv[0]} sqlite:data/sabredav.db
+
+HELLO;
+
+    exit();
+
+}
+
+// There's a bunch of places where the autoloader could be, so we'll try all of
+// them.
+$paths = [
+    __DIR__ . '/../vendor/autoload.php',
+    __DIR__ . '/../../../autoload.php',
+];
+
+foreach ($paths as $path) {
+    if (file_exists($path)) {
+        include $path;
+        break;
+    }
+}
+
+$dsn = $argv[1];
+$user = isset($argv[2]) ? $argv[2] : null;
+$pass = isset($argv[3]) ? $argv[3] : null;
+
+echo "Connecting to database: " . $dsn . "\n";
+
+$pdo = new PDO($dsn, $user, $pass);
+$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
+$pdo->setAttribute(PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_ASSOC);
+
+echo "Validating existing table layout\n";
+
+// The only cross-db way to do this, is to just fetch a single record.
+$row = $pdo->query("SELECT * FROM calendarobjects LIMIT 1")->fetch();
+
+if (!$row) {
+    echo "Error: This database did not have any records in the calendarobjects table, you should just recreate the table.\n";
+    exit(-1);
+}
+
+$requiredFields = [
+    'id',
+    'calendardata',
+    'uri',
+    'calendarid',
+    'lastmodified',
+];
+
+foreach ($requiredFields as $requiredField) {
+    if (!array_key_exists($requiredField, $row)) {
+        echo "Error: The current 'calendarobjects' table was missing a field we expected to exist.\n";
+        echo "For safety reasons, this process is stopped.\n";
+        exit(-1);
+    }
+}
+
+$fields17 = [
+    'etag',
+    'size',
+    'componenttype',
+    'firstoccurence',
+    'lastoccurence',
+];
+
+$found = 0;
+foreach ($fields17 as $field) {
+    if (array_key_exists($field, $row)) {
+        $found++;
+    }
+}
+
+if ($found === 0) {
+    echo "The database had the 1.6 schema. Table will now be altered.\n";
+    echo "This may take some time for large tables\n";
+
+    switch ($pdo->getAttribute(PDO::ATTR_DRIVER_NAME)) {
+
+        case 'mysql' :
+
+            $pdo->exec(<<<SQL
+ALTER TABLE calendarobjects
+ADD etag VARCHAR(32),
+ADD size INT(11) UNSIGNED,
+ADD componenttype VARCHAR(8),
+ADD firstoccurence INT(11) UNSIGNED,
+ADD lastoccurence INT(11) UNSIGNED
+SQL
+        );
+            break;
+            case 'sqlite' :
+                $pdo->exec('ALTER TABLE calendarobjects ADD etag text');
+                $pdo->exec('ALTER TABLE calendarobjects ADD size integer');
+                $pdo->exec('ALTER TABLE calendarobjects ADD componenttype TEXT');
+                $pdo->exec('ALTER TABLE calendarobjects ADD firstoccurence integer');
+                $pdo->exec('ALTER TABLE calendarobjects ADD lastoccurence integer');
+                break;
+
+        default :
+            die('This upgrade script does not support this driver (' . $pdo->getAttribute(PDO::ATTR_DRIVER_NAME) . ")\n");
+
+    }
+    echo "Database schema upgraded.\n";
+
+} elseif ($found === 5) {
+
+    echo "Database already had the 1.7 schema\n";
+
+} else {
+
+    echo "The database had $found out of 5 from the changes for 1.7. This is scary and unusual, so we have to abort.\n";
+    echo "You can manually try to upgrade the schema, and then run this script again.\n";
+    exit(-1);
+
+}
+
+echo "Now, we need to parse every record and pull out some information.\n";
+
+$result = $pdo->query('SELECT id, calendardata FROM calendarobjects');
+$stmt = $pdo->prepare('UPDATE calendarobjects SET etag = ?, size = ?, componenttype = ?, firstoccurence = ?, lastoccurence = ? WHERE id = ?');
+
+echo "Total records found: " . $result->rowCount() . "\n";
+$done = 0;
+$total = $result->rowCount();
+while ($row = $result->fetch()) {
+
+    try {
+        $newData = getDenormalizedData($row['calendardata']);
+    } catch (Exception $e) {
+        echo "===\nException caught will trying to parser calendarobject.\n";
+        echo "Error message: " . $e->getMessage() . "\n";
+        echo "Record id: " . $row['id'] . "\n";
+        echo "This record is ignored, you should inspect it to see if there's anything wrong.\n===\n";
+        continue;
+    }
+    $stmt->execute([
+        $newData['etag'],
+        $newData['size'],
+        $newData['componentType'],
+        $newData['firstOccurence'],
+        $newData['lastOccurence'],
+        $row['id'],
+    ]);
+    $done++;
+
+    if ($done % 500 === 0) {
+        echo "Completed: $done / $total\n";
+    }
+}
+echo "Completed: $done / $total\n";
+
+echo "Checking the calendars table needs changes.\n";
+$row = $pdo->query("SELECT * FROM calendars LIMIT 1")->fetch();
+
+if (array_key_exists('transparent', $row)) {
+
+    echo "The calendars table is already up to date\n";
+
+} else {
+
+    echo "Adding the 'transparent' field to the calendars table\n";
+
+    switch ($pdo->getAttribute(PDO::ATTR_DRIVER_NAME)) {
+
+        case 'mysql' :
+            $pdo->exec("ALTER TABLE calendars ADD transparent TINYINT(1) NOT NULL DEFAULT '0'");
+            break;
+        case 'sqlite' :
+            $pdo->exec("ALTER TABLE calendars ADD transparent bool");
+            break;
+
+        default :
+            die('This upgrade script does not support this driver (' . $pdo->getAttribute(PDO::ATTR_DRIVER_NAME) . ")\n");
+
+    }
+
+}
+
+echo "Process completed!\n";
+
+/**
+ * Parses some information from calendar objects, used for optimized
+ * calendar-queries.
+ *
+ * Blantently copied from Sabre\CalDAV\Backend\PDO
+ *
+ * Returns an array with the following keys:
+ *   * etag
+ *   * size
+ *   * componentType
+ *   * firstOccurence
+ *   * lastOccurence
+ *
+ * @param string $calendarData
+ * @return array
+ */
+function getDenormalizedData($calendarData) {
+
+    $vObject = \Sabre\VObject\Reader::read($calendarData);
+    $componentType = null;
+    $component = null;
+    $firstOccurence = null;
+    $lastOccurence = null;
+    foreach ($vObject->getComponents() as $component) {
+        if ($component->name !== 'VTIMEZONE') {
+            $componentType = $component->name;
+            break;
+        }
+    }
+    if (!$componentType) {
+        throw new \Sabre\DAV\Exception\BadRequest('Calendar objects must have a VJOURNAL, VEVENT or VTODO component');
+    }
+    if ($componentType === 'VEVENT') {
+        $firstOccurence = $component->DTSTART->getDateTime()->getTimeStamp();
+        // Finding the last occurence is a bit harder
+        if (!isset($component->RRULE)) {
+            if (isset($component->DTEND)) {
+                $lastOccurence = $component->DTEND->getDateTime()->getTimeStamp();
+            } elseif (isset($component->DURATION)) {
+                $endDate = clone $component->DTSTART->getDateTime();
+                $endDate->add(\Sabre\VObject\DateTimeParser::parse($component->DURATION->value));
+                $lastOccurence = $endDate->getTimeStamp();
+            } elseif (!$component->DTSTART->hasTime()) {
+                $endDate = clone $component->DTSTART->getDateTime();
+                $endDate->modify('+1 day');
+                $lastOccurence = $endDate->getTimeStamp();
+            } else {
+                $lastOccurence = $firstOccurence;
+            }
+        } else {
+            $it = new \Sabre\VObject\Recur\EventIterator($vObject, (string)$component->UID);
+            $maxDate = new DateTime(\Sabre\CalDAV\Backend\PDO::MAX_DATE);
+            if ($it->isInfinite()) {
+                $lastOccurence = $maxDate->getTimeStamp();
+            } else {
+                $end = $it->getDtEnd();
+                while ($it->valid() && $end < $maxDate) {
+                    $end = $it->getDtEnd();
+                    $it->next();
+
+                }
+                $lastOccurence = $end->getTimeStamp();
+            }
+
+        }
+    }
+
+    return [
+        'etag'           => md5($calendarData),
+        'size'           => strlen($calendarData),
+        'componentType'  => $componentType,
+        'firstOccurence' => $firstOccurence,
+        'lastOccurence'  => $lastOccurence,
+    ];
+
+}

+ 453 - 0
lib/sabre/sabre/dav/bin/migrateto20.php

@@ -0,0 +1,453 @@
+#!/usr/bin/env php
+<?php
+
+echo "SabreDAV migrate script for version 2.0\n";
+
+if ($argc < 2) {
+
+    echo <<<HELLO
+
+This script help you migrate from a pre-2.0 database to 2.0 and later
+
+The 'calendars', 'addressbooks' and 'cards' tables will be upgraded, and new
+tables (calendarchanges, addressbookchanges, propertystorage) will be added.
+
+If you don't use the default PDO CalDAV or CardDAV backend, it's pointless to
+run this script.
+
+Keep in mind that ALTER TABLE commands will be executed. If you have a large
+dataset this may mean that this process takes a while.
+
+Lastly: Make a back-up first. This script has been tested, but the amount of
+potential variants are extremely high, so it's impossible to deal with every
+possible situation.
+
+In the worst case, you will lose all your data. This is not an overstatement.
+
+Usage:
+
+php {$argv[0]} [pdo-dsn] [username] [password]
+
+For example:
+
+php {$argv[0]} "mysql:host=localhost;dbname=sabredav" root password
+php {$argv[0]} sqlite:data/sabredav.db
+
+HELLO;
+
+    exit();
+
+}
+
+// There's a bunch of places where the autoloader could be, so we'll try all of
+// them.
+$paths = [
+    __DIR__ . '/../vendor/autoload.php',
+    __DIR__ . '/../../../autoload.php',
+];
+
+foreach ($paths as $path) {
+    if (file_exists($path)) {
+        include $path;
+        break;
+    }
+}
+
+$dsn = $argv[1];
+$user = isset($argv[2]) ? $argv[2] : null;
+$pass = isset($argv[3]) ? $argv[3] : null;
+
+echo "Connecting to database: " . $dsn . "\n";
+
+$pdo = new PDO($dsn, $user, $pass);
+$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
+$pdo->setAttribute(PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_ASSOC);
+
+$driver = $pdo->getAttribute(PDO::ATTR_DRIVER_NAME);
+
+switch ($driver) {
+
+    case 'mysql' :
+        echo "Detected MySQL.\n";
+        break;
+    case 'sqlite' :
+        echo "Detected SQLite.\n";
+        break;
+    default :
+        echo "Error: unsupported driver: " . $driver . "\n";
+        die(-1);
+}
+
+foreach (['calendar', 'addressbook'] as $itemType) {
+
+    $tableName = $itemType . 's';
+    $tableNameOld = $tableName . '_old';
+    $changesTable = $itemType . 'changes';
+
+    echo "Upgrading '$tableName'\n";
+
+    // The only cross-db way to do this, is to just fetch a single record.
+    $row = $pdo->query("SELECT * FROM $tableName LIMIT 1")->fetch();
+
+    if (!$row) {
+
+        echo "No records were found in the '$tableName' table.\n";
+        echo "\n";
+        echo "We're going to rename the old table to $tableNameOld (just in case).\n";
+        echo "and re-create the new table.\n";
+
+        switch ($driver) {
+
+            case 'mysql' :
+                $pdo->exec("RENAME TABLE $tableName TO $tableNameOld");
+                switch ($itemType) {
+                    case 'calendar' :
+                        $pdo->exec("
+            CREATE TABLE calendars (
+                id INT(11) UNSIGNED NOT NULL PRIMARY KEY AUTO_INCREMENT,
+                principaluri VARCHAR(100),
+                displayname VARCHAR(100),
+                uri VARCHAR(200),
+                synctoken INT(11) UNSIGNED NOT NULL DEFAULT '1',
+                description TEXT,
+                calendarorder INT(11) UNSIGNED NOT NULL DEFAULT '0',
+                calendarcolor VARCHAR(10),
+                timezone TEXT,
+                components VARCHAR(20),
+                transparent TINYINT(1) NOT NULL DEFAULT '0',
+                UNIQUE(principaluri, uri)
+            ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci;
+                        ");
+                        break;
+                    case 'addressbook' :
+                        $pdo->exec("
+            CREATE TABLE addressbooks (
+                id INT(11) UNSIGNED NOT NULL PRIMARY KEY AUTO_INCREMENT,
+                principaluri VARCHAR(255),
+                displayname VARCHAR(255),
+                uri VARCHAR(200),
+                description TEXT,
+                synctoken INT(11) UNSIGNED NOT NULL DEFAULT '1',
+                UNIQUE(principaluri, uri)
+            ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci;
+                        ");
+                        break;
+                }
+                break;
+
+            case 'sqlite' :
+
+                $pdo->exec("ALTER TABLE $tableName RENAME TO $tableNameOld");
+
+                switch ($itemType) {
+                    case 'calendar' :
+                        $pdo->exec("
+            CREATE TABLE calendars (
+                id integer primary key asc,
+                principaluri text,
+                displayname text,
+                uri text,
+                synctoken integer,
+                description text,
+                calendarorder integer,
+                calendarcolor text,
+                timezone text,
+                components text,
+                transparent bool
+            );
+                        ");
+                        break;
+                    case 'addressbook' :
+                        $pdo->exec("
+            CREATE TABLE addressbooks (
+                id integer primary key asc,
+                principaluri text,
+                displayname text,
+                uri text,
+                description text,
+                synctoken integer
+            );
+                        ");
+
+                        break;
+                }
+                break;
+
+        }
+        echo "Creation of 2.0 $tableName table is complete\n";
+
+    } else {
+
+        // Checking if there's a synctoken field already.
+        if (array_key_exists('synctoken', $row)) {
+            echo "The 'synctoken' field already exists in the $tableName table.\n";
+            echo "It's likely you already upgraded, so we're simply leaving\n";
+            echo "the $tableName table alone\n";
+        } else {
+
+            echo "1.8 table schema detected\n";
+            switch ($driver) {
+
+                case 'mysql' :
+                    $pdo->exec("ALTER TABLE $tableName ADD synctoken INT(11) UNSIGNED NOT NULL DEFAULT '1'");
+                    $pdo->exec("ALTER TABLE $tableName DROP ctag");
+                    $pdo->exec("UPDATE $tableName SET synctoken = '1'");
+                    break;
+                case 'sqlite' :
+                    $pdo->exec("ALTER TABLE $tableName ADD synctoken integer");
+                    $pdo->exec("UPDATE $tableName SET synctoken = '1'");
+                    echo "Note: there's no easy way to remove fields in sqlite.\n";
+                    echo "The ctag field is no longer used, but it's kept in place\n";
+                    break;
+
+            }
+
+            echo "Upgraded '$tableName' to 2.0 schema.\n";
+
+        }
+
+    }
+
+    try {
+        $pdo->query("SELECT * FROM $changesTable LIMIT 1");
+
+        echo "'$changesTable' already exists. Assuming that this part of the\n";
+        echo "upgrade was already completed.\n";
+
+    } catch (Exception $e) {
+        echo "Creating '$changesTable' table.\n";
+
+        switch ($driver) {
+
+            case 'mysql' :
+                $pdo->exec("
+    CREATE TABLE $changesTable (
+        id INT(11) UNSIGNED NOT NULL PRIMARY KEY AUTO_INCREMENT,
+        uri VARCHAR(200) NOT NULL,
+        synctoken INT(11) UNSIGNED NOT NULL,
+        {$itemType}id INT(11) UNSIGNED NOT NULL,
+        operation TINYINT(1) NOT NULL,
+        INDEX {$itemType}id_synctoken ({$itemType}id, synctoken)
+    ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci;
+
+                ");
+                break;
+            case 'sqlite' :
+                $pdo->exec("
+
+    CREATE TABLE $changesTable (
+        id integer primary key asc,
+        uri text,
+        synctoken integer,
+        {$itemType}id integer,
+        operation bool
+    );
+
+                ");
+                $pdo->exec("CREATE INDEX {$itemType}id_synctoken ON $changesTable ({$itemType}id, synctoken);");
+                break;
+
+        }
+
+    }
+
+}
+
+try {
+    $pdo->query("SELECT * FROM calendarsubscriptions LIMIT 1");
+
+    echo "'calendarsubscriptions' already exists. Assuming that this part of the\n";
+    echo "upgrade was already completed.\n";
+
+} catch (Exception $e) {
+    echo "Creating calendarsubscriptions table.\n";
+
+    switch ($driver) {
+
+        case 'mysql' :
+            $pdo->exec("
+CREATE TABLE calendarsubscriptions (
+    id INT(11) UNSIGNED NOT NULL PRIMARY KEY AUTO_INCREMENT,
+    uri VARCHAR(200) NOT NULL,
+    principaluri VARCHAR(100) NOT NULL,
+    source TEXT,
+    displayname VARCHAR(100),
+    refreshrate VARCHAR(10),
+    calendarorder INT(11) UNSIGNED NOT NULL DEFAULT '0',
+    calendarcolor VARCHAR(10),
+    striptodos TINYINT(1) NULL,
+    stripalarms TINYINT(1) NULL,
+    stripattachments TINYINT(1) NULL,
+    lastmodified INT(11) UNSIGNED,
+    UNIQUE(principaluri, uri)
+);
+            ");
+            break;
+        case 'sqlite' :
+            $pdo->exec("
+
+CREATE TABLE calendarsubscriptions (
+    id integer primary key asc,
+    uri text,
+    principaluri text,
+    source text,
+    displayname text,
+    refreshrate text,
+    calendarorder integer,
+    calendarcolor text,
+    striptodos bool,
+    stripalarms bool,
+    stripattachments bool,
+    lastmodified int
+);
+            ");
+
+            $pdo->exec("CREATE INDEX principaluri_uri ON calendarsubscriptions (principaluri, uri);");
+            break;
+
+    }
+
+}
+
+try {
+    $pdo->query("SELECT * FROM propertystorage LIMIT 1");
+
+    echo "'propertystorage' already exists. Assuming that this part of the\n";
+    echo "upgrade was already completed.\n";
+
+} catch (Exception $e) {
+    echo "Creating propertystorage table.\n";
+
+    switch ($driver) {
+
+        case 'mysql' :
+            $pdo->exec("
+CREATE TABLE propertystorage (
+    id INT UNSIGNED NOT NULL PRIMARY KEY AUTO_INCREMENT,
+    path VARBINARY(1024) NOT NULL,
+    name VARBINARY(100) NOT NULL,
+    value MEDIUMBLOB
+);
+            ");
+            $pdo->exec("
+CREATE UNIQUE INDEX path_property ON propertystorage (path(600), name(100));
+            ");
+            break;
+        case 'sqlite' :
+            $pdo->exec("
+CREATE TABLE propertystorage (
+    id integer primary key asc,
+    path TEXT,
+    name TEXT,
+    value TEXT
+);
+            ");
+            $pdo->exec("
+CREATE UNIQUE INDEX path_property ON propertystorage (path, name);
+            ");
+
+            break;
+
+    }
+
+}
+
+echo "Upgrading cards table to 2.0 schema\n";
+
+try {
+
+    $create = false;
+    $row = $pdo->query("SELECT * FROM cards LIMIT 1")->fetch();
+    if (!$row) {
+        $random = mt_rand(1000, 9999);
+        echo "There was no data in the cards table, so we're re-creating it\n";
+        echo "The old table will be renamed to cards_old$random, just in case.\n";
+
+        $create = true;
+
+        switch ($driver) {
+            case 'mysql' :
+                $pdo->exec("RENAME TABLE cards TO cards_old$random");
+                break;
+            case 'sqlite' :
+                $pdo->exec("ALTER TABLE cards RENAME TO cards_old$random");
+                break;
+
+        }
+    }
+
+} catch (Exception $e) {
+
+    echo "Exception while checking cards table. Assuming that the table does not yet exist.\n";
+    echo "Debug: ", $e->getMessage(), "\n";
+    $create = true;
+
+}
+
+if ($create) {
+    switch ($driver) {
+        case 'mysql' :
+            $pdo->exec("
+CREATE TABLE cards (
+    id INT(11) UNSIGNED NOT NULL PRIMARY KEY AUTO_INCREMENT,
+    addressbookid INT(11) UNSIGNED NOT NULL,
+    carddata MEDIUMBLOB,
+    uri VARCHAR(200),
+    lastmodified INT(11) UNSIGNED,
+    etag VARBINARY(32),
+    size INT(11) UNSIGNED NOT NULL
+) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci;
+
+            ");
+            break;
+
+        case 'sqlite' :
+
+            $pdo->exec("
+CREATE TABLE cards (
+    id integer primary key asc,
+    addressbookid integer,
+    carddata blob,
+    uri text,
+    lastmodified integer,
+    etag text,
+    size integer
+);
+            ");
+            break;
+
+    }
+} else {
+    switch ($driver) {
+        case 'mysql' :
+            $pdo->exec("
+                ALTER TABLE cards
+                ADD etag VARBINARY(32),
+                ADD size INT(11) UNSIGNED NOT NULL;
+            ");
+            break;
+
+        case 'sqlite' :
+
+            $pdo->exec("
+                ALTER TABLE cards ADD etag text;
+                ALTER TABLE cards ADD size integer;
+            ");
+            break;
+
+    }
+    echo "Reading all old vcards and populating etag and size fields.\n";
+    $result = $pdo->query('SELECT id, carddata FROM cards');
+    $stmt = $pdo->prepare('UPDATE cards SET etag = ?, size = ? WHERE id = ?');
+    while ($row = $result->fetch(\PDO::FETCH_ASSOC)) {
+        $stmt->execute([
+            md5($row['carddata']),
+            strlen($row['carddata']),
+            $row['id']
+        ]);
+    }
+
+
+}
+
+echo "Upgrade to 2.0 schema completed.\n";

+ 180 - 0
lib/sabre/sabre/dav/bin/migrateto21.php

@@ -0,0 +1,180 @@
+#!/usr/bin/env php
+<?php
+
+echo "SabreDAV migrate script for version 2.1\n";
+
+if ($argc < 2) {
+
+    echo <<<HELLO
+
+This script help you migrate from a pre-2.1 database to 2.1.
+
+Changes:
+  The 'calendarobjects' table will be upgraded.
+  'schedulingobjects' will be created.
+
+If you don't use the default PDO CalDAV or CardDAV backend, it's pointless to
+run this script.
+
+Keep in mind that ALTER TABLE commands will be executed. If you have a large
+dataset this may mean that this process takes a while.
+
+Lastly: Make a back-up first. This script has been tested, but the amount of
+potential variants are extremely high, so it's impossible to deal with every
+possible situation.
+
+In the worst case, you will lose all your data. This is not an overstatement.
+
+Usage:
+
+php {$argv[0]} [pdo-dsn] [username] [password]
+
+For example:
+
+php {$argv[0]} "mysql:host=localhost;dbname=sabredav" root password
+php {$argv[0]} sqlite:data/sabredav.db
+
+HELLO;
+
+    exit();
+
+}
+
+// There's a bunch of places where the autoloader could be, so we'll try all of
+// them.
+$paths = [
+    __DIR__ . '/../vendor/autoload.php',
+    __DIR__ . '/../../../autoload.php',
+];
+
+foreach ($paths as $path) {
+    if (file_exists($path)) {
+        include $path;
+        break;
+    }
+}
+
+$dsn = $argv[1];
+$user = isset($argv[2]) ? $argv[2] : null;
+$pass = isset($argv[3]) ? $argv[3] : null;
+
+echo "Connecting to database: " . $dsn . "\n";
+
+$pdo = new PDO($dsn, $user, $pass);
+$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
+$pdo->setAttribute(PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_ASSOC);
+
+$driver = $pdo->getAttribute(PDO::ATTR_DRIVER_NAME);
+
+switch ($driver) {
+
+    case 'mysql' :
+        echo "Detected MySQL.\n";
+        break;
+    case 'sqlite' :
+        echo "Detected SQLite.\n";
+        break;
+    default :
+        echo "Error: unsupported driver: " . $driver . "\n";
+        die(-1);
+}
+
+echo "Upgrading 'calendarobjects'\n";
+$addUid = false;
+try {
+    $result = $pdo->query('SELECT * FROM calendarobjects LIMIT 1');
+    $row = $result->fetch(\PDO::FETCH_ASSOC);
+
+    if (!$row) {
+        echo "No data in table. Going to try to add the uid field anyway.\n";
+        $addUid = true;
+    } elseif (array_key_exists('uid', $row)) {
+        echo "uid field exists. Assuming that this part of the migration has\n";
+        echo "Already been completed.\n";
+    } else {
+        echo "2.0 schema detected.\n";
+        $addUid = true;
+    }
+
+} catch (Exception $e) {
+    echo "Could not find a calendarobjects table. Skipping this part of the\n";
+    echo "upgrade.\n";
+}
+
+if ($addUid) {
+
+    switch ($driver) {
+        case 'mysql' :
+            $pdo->exec('ALTER TABLE calendarobjects ADD uid VARCHAR(200)');
+            break;
+        case 'sqlite' :
+            $pdo->exec('ALTER TABLE calendarobjects ADD uid TEXT');
+            break;
+    }
+
+    $result = $pdo->query('SELECT id, calendardata FROM calendarobjects');
+    $stmt = $pdo->prepare('UPDATE calendarobjects SET uid = ? WHERE id = ?');
+    $counter = 0;
+
+    while ($row = $result->fetch(\PDO::FETCH_ASSOC)) {
+
+        try {
+            $vobj = \Sabre\VObject\Reader::read($row['calendardata']);
+        } catch (\Exception $e) {
+            echo "Warning! Item with id $row[id] could not be parsed!\n";
+            continue;
+        }
+        $uid = null;
+        $item = $vobj->getBaseComponent();
+        if (!isset($item->UID)) {
+            echo "Warning! Item with id $item[id] does NOT have a UID property and this is required.\n";
+            continue;
+        }
+        $uid = (string)$item->UID;
+        $stmt->execute([$uid, $row['id']]);
+        $counter++;
+
+    }
+
+}
+
+echo "Creating 'schedulingobjects'\n";
+
+switch ($driver) {
+
+    case 'mysql' :
+        $pdo->exec('CREATE TABLE IF NOT EXISTS schedulingobjects
+(
+    id INT(11) UNSIGNED NOT NULL PRIMARY KEY AUTO_INCREMENT,
+    principaluri VARCHAR(255),
+    calendardata MEDIUMBLOB,
+    uri VARCHAR(200),
+    lastmodified INT(11) UNSIGNED,
+    etag VARCHAR(32),
+    size INT(11) UNSIGNED NOT NULL
+) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci;
+        ');
+        break;
+
+
+    case 'sqlite' :
+        $pdo->exec('CREATE TABLE IF NOT EXISTS schedulingobjects (
+    id integer primary key asc,
+    principaluri text,
+    calendardata blob,
+    uri text,
+    lastmodified integer,
+    etag text,
+    size integer
+)
+');
+        break;
+        $pdo->exec('
+            CREATE INDEX principaluri_uri ON calendarsubscriptions (principaluri, uri);
+        ');
+        break;
+}
+
+echo "Done.\n";
+
+echo "Upgrade to 2.1 schema completed.\n";

+ 171 - 0
lib/sabre/sabre/dav/bin/migrateto30.php

@@ -0,0 +1,171 @@
+#!/usr/bin/env php
+<?php
+
+echo "SabreDAV migrate script for version 3.0\n";
+
+if ($argc < 2) {
+
+    echo <<<HELLO
+
+This script help you migrate from a pre-3.0 database to 3.0 and later
+
+Changes:
+  * The propertystorage table has changed to allow storage of complex
+    properties.
+  * the vcardurl field in the principals table is no more. This was moved to
+    the propertystorage table.
+
+Keep in mind that ALTER TABLE commands will be executed. If you have a large
+dataset this may mean that this process takes a while.
+
+Lastly: Make a back-up first. This script has been tested, but the amount of
+potential variants are extremely high, so it's impossible to deal with every
+possible situation.
+
+In the worst case, you will lose all your data. This is not an overstatement.
+
+Usage:
+
+php {$argv[0]} [pdo-dsn] [username] [password]
+
+For example:
+
+php {$argv[0]} "mysql:host=localhost;dbname=sabredav" root password
+php {$argv[0]} sqlite:data/sabredav.db
+
+HELLO;
+
+    exit();
+
+}
+
+// There's a bunch of places where the autoloader could be, so we'll try all of
+// them.
+$paths = [
+    __DIR__ . '/../vendor/autoload.php',
+    __DIR__ . '/../../../autoload.php',
+];
+
+foreach ($paths as $path) {
+    if (file_exists($path)) {
+        include $path;
+        break;
+    }
+}
+
+$dsn = $argv[1];
+$user = isset($argv[2]) ? $argv[2] : null;
+$pass = isset($argv[3]) ? $argv[3] : null;
+
+echo "Connecting to database: " . $dsn . "\n";
+
+$pdo = new PDO($dsn, $user, $pass);
+$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
+$pdo->setAttribute(PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_ASSOC);
+
+$driver = $pdo->getAttribute(PDO::ATTR_DRIVER_NAME);
+
+switch ($driver) {
+
+    case 'mysql' :
+        echo "Detected MySQL.\n";
+        break;
+    case 'sqlite' :
+        echo "Detected SQLite.\n";
+        break;
+    default :
+        echo "Error: unsupported driver: " . $driver . "\n";
+        die(-1);
+}
+
+echo "Upgrading 'propertystorage'\n";
+$addValueType = false;
+try {
+    $result = $pdo->query('SELECT * FROM propertystorage LIMIT 1');
+    $row = $result->fetch(\PDO::FETCH_ASSOC);
+
+    if (!$row) {
+        echo "No data in table. Going to re-create the table.\n";
+        $random = mt_rand(1000, 9999);
+        echo "Renaming propertystorage -> propertystorage_old$random and creating new table.\n";
+
+        switch ($driver) {
+
+            case 'mysql' :
+                $pdo->exec('RENAME TABLE propertystorage TO propertystorage_old' . $random);
+                $pdo->exec('
+    CREATE TABLE propertystorage (
+        id INT UNSIGNED NOT NULL PRIMARY KEY AUTO_INCREMENT,
+        path VARBINARY(1024) NOT NULL,
+        name VARBINARY(100) NOT NULL,
+        valuetype INT UNSIGNED,
+        value MEDIUMBLOB
+    );
+                ');
+                $pdo->exec('CREATE UNIQUE INDEX path_property_' . $random . '  ON propertystorage (path(600), name(100));');
+                break;
+            case 'sqlite' :
+                $pdo->exec('ALTER TABLE propertystorage RENAME TO propertystorage_old' . $random);
+                $pdo->exec('
+CREATE TABLE propertystorage (
+    id integer primary key asc,
+    path text,
+    name text,
+    valuetype integer,
+    value blob
+);');
+
+                $pdo->exec('CREATE UNIQUE INDEX path_property_' . $random . ' ON propertystorage (path, name);');
+                break;
+
+        }
+    } elseif (array_key_exists('valuetype', $row)) {
+        echo "valuetype field exists. Assuming that this part of the migration has\n";
+        echo "Already been completed.\n";
+    } else {
+        echo "2.1 schema detected. Going to perform upgrade.\n";
+        $addValueType = true;
+    }
+
+} catch (Exception $e) {
+    echo "Could not find a propertystorage table. Skipping this part of the\n";
+    echo "upgrade.\n";
+    echo $e->getMessage(), "\n";
+}
+
+if ($addValueType) {
+
+    switch ($driver) {
+        case 'mysql' :
+            $pdo->exec('ALTER TABLE propertystorage ADD valuetype INT UNSIGNED');
+            break;
+        case 'sqlite' :
+            $pdo->exec('ALTER TABLE propertystorage ADD valuetype INT');
+
+            break;
+    }
+
+    $pdo->exec('UPDATE propertystorage SET valuetype = 1 WHERE valuetype IS NULL ');
+
+}
+
+echo "Migrating vcardurl\n";
+
+$result = $pdo->query('SELECT id, uri, vcardurl FROM principals WHERE vcardurl IS NOT NULL');
+$stmt1 = $pdo->prepare('INSERT INTO propertystorage (path, name, valuetype, value) VALUES (?, ?, 3, ?)');
+
+while ($row = $result->fetch(\PDO::FETCH_ASSOC)) {
+
+    // Inserting the new record
+    $stmt1->execute([
+        'addressbooks/' . basename($row['uri']),
+        '{http://calendarserver.org/ns/}me-card',
+        serialize(new Sabre\DAV\Xml\Property\Href($row['vcardurl']))
+    ]);
+
+    echo serialize(new Sabre\DAV\Xml\Property\Href($row['vcardurl']));
+
+}
+
+echo "Done.\n";
+echo "Upgrade to 3.0 schema completed.\n";

+ 140 - 0
lib/sabre/sabre/dav/bin/naturalselection

@@ -0,0 +1,140 @@
+#!/usr/bin/env python
+
+#
+# Copyright (c) 2009-2010 Evert Pot
+# All rights reserved.
+# http://www.rooftopsolutions.nl/
+#
+# This utility is distributed along with SabreDAV
+# license: http://sabre.io/license/ Modified BSD License
+
+import os
+from optparse import OptionParser
+import time
+
+def getfreespace(path):
+    stat = os.statvfs(path)
+    return stat.f_frsize * stat.f_bavail
+
+def getbytesleft(path,threshold):
+    return getfreespace(path)-threshold
+
+def run(cacheDir, threshold, sleep=5, simulate=False, min_erase = 0):
+
+    bytes = getbytesleft(cacheDir,threshold)
+    if (bytes>0):
+        print "Bytes to go before we hit threshold:", bytes
+    else:
+        print "Threshold exceeded with:", -bytes, "bytes"
+        dir = os.listdir(cacheDir)
+        dir2 = []
+        for file in dir:
+            path = cacheDir + '/' + file
+            dir2.append({
+                "path" : path,
+                "atime": os.stat(path).st_atime,
+                "size" : os.stat(path).st_size
+            })
+
+        dir2.sort(lambda x,y: int(x["atime"]-y["atime"]))
+
+        filesunlinked = 0
+        gainedspace = 0
+
+        # Left is the amount of bytes that need to be freed up
+        # The default is the 'min_erase setting'
+        left = min_erase
+
+        # If the min_erase setting is lower than the amount of bytes over
+        # the threshold, we use that number instead.
+        if left < -bytes :
+            left = -bytes
+
+        print "Need to delete at least:", left;
+
+        for file in dir2:
+
+            # Only deleting files if we're not simulating
+            if not simulate: os.unlink(file["path"])
+            left = int(left - file["size"])
+            gainedspace = gainedspace + file["size"]
+            filesunlinked = filesunlinked + 1
+
+            if(left<0):
+                break
+
+        print "%d files deleted (%d bytes)" % (filesunlinked, gainedspace)
+
+
+    time.sleep(sleep)
+
+
+
+def main():
+    parser = OptionParser(
+        version="naturalselection v0.3",
+        description="Cache directory manager. Deletes cache entries based on accesstime and free space thresholds.\n" +
+            "This utility is distributed alongside SabreDAV.",
+        usage="usage: %prog [options] cacheDirectory",
+    )
+    parser.add_option(
+        '-s',
+        dest="simulate",
+        action="store_true",
+        help="Don't actually make changes, but just simulate the behaviour",
+    )
+    parser.add_option(
+        '-r','--runs',
+        help="How many times to check before exiting. -1 is infinite, which is the default",
+        type="int",
+        dest="runs",
+        default=-1
+    )
+    parser.add_option(
+        '-n','--interval',
+        help="Sleep time in seconds (default = 5)",
+        type="int",
+        dest="sleep",
+        default=5
+    )
+    parser.add_option(
+        '-l','--threshold',
+        help="Threshold in bytes (default = 10737418240, which is 10GB)",
+        type="int",
+        dest="threshold",
+        default=10737418240
+    )
+    parser.add_option(
+        '-m', '--min-erase',
+        help="Minimum number of bytes to erase when the threshold is reached. " +
+            "Setting this option higher will reduce the amount of times the cache directory will need to be scanned. " +
+            "(the default is 1073741824, which is 1GB.)",
+        type="int",
+        dest="min_erase",
+        default=1073741824
+    )
+
+    options,args = parser.parse_args()
+    if len(args)<1:
+        parser.error("This utility requires at least 1 argument")
+    cacheDir = args[0]
+
+    print "Natural Selection"
+    print "Cache directory:", cacheDir
+    free = getfreespace(cacheDir);
+    print "Current free disk space:", free
+
+    runs = options.runs;
+    while runs!=0 :
+        run(
+            cacheDir,
+            sleep=options.sleep,
+            simulate=options.simulate,
+            threshold=options.threshold,
+            min_erase=options.min_erase
+        )
+        if runs>0:
+            runs = runs - 1
+
+if __name__ == '__main__' :
+    main()

+ 2 - 0
lib/sabre/sabre/dav/bin/sabredav

@@ -0,0 +1,2 @@
+#!/bin/sh
+php -S 0.0.0.0:8080 `dirname $0`/sabredav.php

+ 53 - 0
lib/sabre/sabre/dav/bin/sabredav.php

@@ -0,0 +1,53 @@
+<?php
+
+// SabreDAV test server.
+
+class CliLog {
+
+    protected $stream;
+
+    function __construct() {
+
+        $this->stream = fopen('php://stdout', 'w');
+
+    }
+
+    function log($msg) {
+        fwrite($this->stream, $msg . "\n");
+    }
+
+}
+
+$log = new CliLog();
+
+if (php_sapi_name() !== 'cli-server') {
+    die("This script is intended to run on the built-in php webserver");
+}
+
+// Finding composer
+
+
+$paths = [
+    __DIR__ . '/../vendor/autoload.php',
+    __DIR__ . '/../../../autoload.php',
+];
+
+foreach ($paths as $path) {
+    if (file_exists($path)) {
+        include $path;
+        break;
+    }
+}
+
+use Sabre\DAV;
+
+// Root 
+$root = new DAV\FS\Directory(getcwd());
+
+// Setting up server.
+$server = new DAV\Server($root);
+
+// Browser plugin
+$server->addPlugin(new DAV\Browser\Plugin());
+
+$server->exec();

+ 66 - 0
lib/sabre/sabre/dav/composer.json

@@ -0,0 +1,66 @@
+{
+    "name": "sabre/dav",
+    "type": "library",
+    "description": "WebDAV Framework for PHP",
+    "keywords": ["Framework", "WebDAV", "CalDAV", "CardDAV", "iCalendar"],
+    "homepage": "http://sabre.io/",
+    "license" : "BSD-3-Clause",
+    "authors": [
+        {
+            "name": "Evert Pot",
+            "email": "me@evertpot.com",
+            "homepage" : "http://evertpot.com/",
+            "role" : "Developer"
+        }
+    ],
+    "require": {
+        "php": ">=5.5.0",
+        "sabre/vobject": "~4.0",
+        "sabre/event" : ">=2.0.0, <4.0.0",
+        "sabre/xml"  : "~1.0",
+        "sabre/http" : "^4.2.1",
+        "sabre/uri" : "~1.0",
+        "ext-dom": "*",
+        "ext-pcre": "*",
+        "ext-spl": "*",
+        "ext-simplexml": "*",
+        "ext-mbstring" : "*",
+        "ext-ctype" : "*",
+        "ext-date" : "*",
+        "ext-iconv" : "*",
+        "ext-libxml" : "*"
+    },
+    "require-dev" : {
+        "phpunit/phpunit" : "> 4.8, <=6.0.0",
+        "evert/phpdoc-md" : "~0.1.0",
+        "sabre/cs"        : "~0.0.5"
+    },
+    "suggest" : {
+        "ext-curl" : "*",
+        "ext-pdo" : "*"
+    },
+    "autoload": {
+        "psr-4" : {
+            "Sabre\\DAV\\"     : "lib/DAV/",
+            "Sabre\\DAVACL\\"  : "lib/DAVACL/",
+            "Sabre\\CalDAV\\"  : "lib/CalDAV/",
+            "Sabre\\CardDAV\\" : "lib/CardDAV/"
+        }
+    },
+    "support" : {
+        "forum" : "https://groups.google.com/group/sabredav-discuss",
+        "source" : "https://github.com/fruux/sabre-dav"
+    },
+    "bin" : [
+        "bin/sabredav",
+        "bin/naturalselection"
+    ],
+    "config" : {
+        "bin-dir" : "./bin"
+    },
+    "extra" : {
+        "branch-alias": {
+            "dev-master": "3.1.0-dev"
+        }
+    }
+}

+ 226 - 0
lib/sabre/sabre/dav/lib/CalDAV/Backend/AbstractBackend.php

@@ -0,0 +1,226 @@
+<?php
+
+namespace Sabre\CalDAV\Backend;
+
+use Sabre\VObject;
+use Sabre\CalDAV;
+
+/**
+ * Abstract Calendaring backend. Extend this class to create your own backends.
+ *
+ * Checkout the BackendInterface for all the methods that must be implemented.
+ *
+ * @copyright Copyright (C) fruux GmbH (https://fruux.com/)
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+abstract class AbstractBackend implements BackendInterface {
+
+    /**
+     * Updates properties for a calendar.
+     *
+     * The list of mutations is stored in a Sabre\DAV\PropPatch object.
+     * To do the actual updates, you must tell this object which properties
+     * you're going to process with the handle() method.
+     *
+     * Calling the handle method is like telling the PropPatch object "I
+     * promise I can handle updating this property".
+     *
+     * Read the PropPatch documenation for more info and examples.
+     *
+     * @param string $path
+     * @param \Sabre\DAV\PropPatch $propPatch
+     * @return void
+     */
+    function updateCalendar($calendarId, \Sabre\DAV\PropPatch $propPatch) {
+
+    }
+
+    /**
+     * Returns a list of calendar objects.
+     *
+     * This method should work identical to getCalendarObject, but instead
+     * return all the calendar objects in the list as an array.
+     *
+     * If the backend supports this, it may allow for some speed-ups.
+     *
+     * @param mixed $calendarId
+     * @param array $uris
+     * @return array
+     */
+    function getMultipleCalendarObjects($calendarId, array $uris) {
+
+        return array_map(function($uri) use ($calendarId) {
+            return $this->getCalendarObject($calendarId, $uri);
+        }, $uris);
+
+    }
+
+    /**
+     * Performs a calendar-query on the contents of this calendar.
+     *
+     * The calendar-query is defined in RFC4791 : CalDAV. Using the
+     * calendar-query it is possible for a client to request a specific set of
+     * object, based on contents of iCalendar properties, date-ranges and
+     * iCalendar component types (VTODO, VEVENT).
+     *
+     * This method should just return a list of (relative) urls that match this
+     * query.
+     *
+     * The list of filters are specified as an array. The exact array is
+     * documented by \Sabre\CalDAV\CalendarQueryParser.
+     *
+     * Note that it is extremely likely that getCalendarObject for every path
+     * returned from this method will be called almost immediately after. You
+     * may want to anticipate this to speed up these requests.
+     *
+     * This method provides a default implementation, which parses *all* the
+     * iCalendar objects in the specified calendar.
+     *
+     * This default may well be good enough for personal use, and calendars
+     * that aren't very large. But if you anticipate high usage, big calendars
+     * or high loads, you are strongly adviced to optimize certain paths.
+     *
+     * The best way to do so is override this method and to optimize
+     * specifically for 'common filters'.
+     *
+     * Requests that are extremely common are:
+     *   * requests for just VEVENTS
+     *   * requests for just VTODO
+     *   * requests with a time-range-filter on either VEVENT or VTODO.
+     *
+     * ..and combinations of these requests. It may not be worth it to try to
+     * handle every possible situation and just rely on the (relatively
+     * easy to use) CalendarQueryValidator to handle the rest.
+     *
+     * Note that especially time-range-filters may be difficult to parse. A
+     * time-range filter specified on a VEVENT must for instance also handle
+     * recurrence rules correctly.
+     * A good example of how to interprete all these filters can also simply
+     * be found in \Sabre\CalDAV\CalendarQueryFilter. This class is as correct
+     * as possible, so it gives you a good idea on what type of stuff you need
+     * to think of.
+     *
+     * @param mixed $calendarId
+     * @param array $filters
+     * @return array
+     */
+    function calendarQuery($calendarId, array $filters) {
+
+        $result = [];
+        $objects = $this->getCalendarObjects($calendarId);
+
+        foreach ($objects as $object) {
+
+            if ($this->validateFilterForObject($object, $filters)) {
+                $result[] = $object['uri'];
+            }
+
+        }
+
+        return $result;
+
+    }
+
+    /**
+     * This method validates if a filter (as passed to calendarQuery) matches
+     * the given object.
+     *
+     * @param array $object
+     * @param array $filters
+     * @return bool
+     */
+    protected function validateFilterForObject(array $object, array $filters) {
+
+        // Unfortunately, setting the 'calendardata' here is optional. If
+        // it was excluded, we actually need another call to get this as
+        // well.
+        if (!isset($object['calendardata'])) {
+            $object = $this->getCalendarObject($object['calendarid'], $object['uri']);
+        }
+
+        $vObject = VObject\Reader::read($object['calendardata']);
+
+        $validator = new CalDAV\CalendarQueryValidator();
+        $result = $validator->validate($vObject, $filters);
+
+        // Destroy circular references so PHP will GC the object.
+        $vObject->destroy();
+
+        return $result;
+
+    }
+
+    /**
+     * Searches through all of a users calendars and calendar objects to find
+     * an object with a specific UID.
+     *
+     * This method should return the path to this object, relative to the
+     * calendar home, so this path usually only contains two parts:
+     *
+     * calendarpath/objectpath.ics
+     *
+     * If the uid is not found, return null.
+     *
+     * This method should only consider * objects that the principal owns, so
+     * any calendars owned by other principals that also appear in this
+     * collection should be ignored.
+     *
+     * @param string $principalUri
+     * @param string $uid
+     * @return string|null
+     */
+    function getCalendarObjectByUID($principalUri, $uid) {
+
+        // Note: this is a super slow naive implementation of this method. You
+        // are highly recommended to optimize it, if your backend allows it.
+        foreach ($this->getCalendarsForUser($principalUri) as $calendar) {
+
+            // We must ignore calendars owned by other principals.
+            if ($calendar['principaluri'] !== $principalUri) {
+                continue;
+            }
+
+            // Ignore calendars that are shared.
+            if (isset($calendar['{http://sabredav.org/ns}owner-principal']) && $calendar['{http://sabredav.org/ns}owner-principal'] !== $principalUri) {
+                continue;
+            }
+
+            $results = $this->calendarQuery(
+                $calendar['id'],
+                [
+                    'name'         => 'VCALENDAR',
+                    'prop-filters' => [],
+                    'comp-filters' => [
+                        [
+                            'name'           => 'VEVENT',
+                            'is-not-defined' => false,
+                            'time-range'     => null,
+                            'comp-filters'   => [],
+                            'prop-filters'   => [
+                                [
+                                    'name'           => 'UID',
+                                    'is-not-defined' => false,
+                                    'time-range'     => null,
+                                    'text-match'     => [
+                                        'value'            => $uid,
+                                        'negate-condition' => false,
+                                        'collation'        => 'i;octet',
+                                    ],
+                                    'param-filters' => [],
+                                ],
+                            ]
+                        ]
+                    ],
+                ]
+            );
+            if ($results) {
+                // We have a match
+                return $calendar['uri'] . '/' . $results[0];
+            }
+
+        }
+
+    }
+
+}

+ 268 - 0
lib/sabre/sabre/dav/lib/CalDAV/Backend/BackendInterface.php

@@ -0,0 +1,268 @@
+<?php
+
+namespace Sabre\CalDAV\Backend;
+
+/**
+ * Every CalDAV backend must at least implement this interface.
+ *
+ * @copyright Copyright (C) fruux GmbH (https://fruux.com/)
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+interface BackendInterface {
+
+    /**
+     * Returns a list of calendars for a principal.
+     *
+     * Every project is an array with the following keys:
+     *  * id, a unique id that will be used by other functions to modify the
+     *    calendar. This can be the same as the uri or a database key.
+     *  * uri, which is the basename of the uri with which the calendar is
+     *    accessed.
+     *  * principaluri. The owner of the calendar. Almost always the same as
+     *    principalUri passed to this method.
+     *
+     * Furthermore it can contain webdav properties in clark notation. A very
+     * common one is '{DAV:}displayname'.
+     *
+     * Many clients also require:
+     * {urn:ietf:params:xml:ns:caldav}supported-calendar-component-set
+     * For this property, you can just return an instance of
+     * Sabre\CalDAV\Property\SupportedCalendarComponentSet.
+     *
+     * If you return {http://sabredav.org/ns}read-only and set the value to 1,
+     * ACL will automatically be put in read-only mode.
+     *
+     * @param string $principalUri
+     * @return array
+     */
+    function getCalendarsForUser($principalUri);
+
+    /**
+     * Creates a new calendar for a principal.
+     *
+     * If the creation was a success, an id must be returned that can be used to
+     * reference this calendar in other methods, such as updateCalendar.
+     *
+     * @param string $principalUri
+     * @param string $calendarUri
+     * @param array $properties
+     * @return void
+     */
+    function createCalendar($principalUri, $calendarUri, array $properties);
+
+    /**
+     * Updates properties for a calendar.
+     *
+     * The list of mutations is stored in a Sabre\DAV\PropPatch object.
+     * To do the actual updates, you must tell this object which properties
+     * you're going to process with the handle() method.
+     *
+     * Calling the handle method is like telling the PropPatch object "I
+     * promise I can handle updating this property".
+     *
+     * Read the PropPatch documentation for more info and examples.
+     *
+     * @param string $path
+     * @param \Sabre\DAV\PropPatch $propPatch
+     * @return void
+     */
+    function updateCalendar($calendarId, \Sabre\DAV\PropPatch $propPatch);
+
+    /**
+     * Delete a calendar and all its objects
+     *
+     * @param mixed $calendarId
+     * @return void
+     */
+    function deleteCalendar($calendarId);
+
+    /**
+     * Returns all calendar objects within a calendar.
+     *
+     * Every item contains an array with the following keys:
+     *   * calendardata - The iCalendar-compatible calendar data
+     *   * uri - a unique key which will be used to construct the uri. This can
+     *     be any arbitrary string, but making sure it ends with '.ics' is a
+     *     good idea. This is only the basename, or filename, not the full
+     *     path.
+     *   * lastmodified - a timestamp of the last modification time
+     *   * etag - An arbitrary string, surrounded by double-quotes. (e.g.:
+     *   '"abcdef"')
+     *   * size - The size of the calendar objects, in bytes.
+     *   * component - optional, a string containing the type of object, such
+     *     as 'vevent' or 'vtodo'. If specified, this will be used to populate
+     *     the Content-Type header.
+     *
+     * Note that the etag is optional, but it's highly encouraged to return for
+     * speed reasons.
+     *
+     * The calendardata is also optional. If it's not returned
+     * 'getCalendarObject' will be called later, which *is* expected to return
+     * calendardata.
+     *
+     * If neither etag or size are specified, the calendardata will be
+     * used/fetched to determine these numbers. If both are specified the
+     * amount of times this is needed is reduced by a great degree.
+     *
+     * @param mixed $calendarId
+     * @return array
+     */
+    function getCalendarObjects($calendarId);
+
+    /**
+     * Returns information from a single calendar object, based on it's object
+     * uri.
+     *
+     * The object uri is only the basename, or filename and not a full path.
+     *
+     * The returned array must have the same keys as getCalendarObjects. The
+     * 'calendardata' object is required here though, while it's not required
+     * for getCalendarObjects.
+     *
+     * This method must return null if the object did not exist.
+     *
+     * @param mixed $calendarId
+     * @param string $objectUri
+     * @return array|null
+     */
+    function getCalendarObject($calendarId, $objectUri);
+
+    /**
+     * Returns a list of calendar objects.
+     *
+     * This method should work identical to getCalendarObject, but instead
+     * return all the calendar objects in the list as an array.
+     *
+     * If the backend supports this, it may allow for some speed-ups.
+     *
+     * @param mixed $calendarId
+     * @param array $uris
+     * @return array
+     */
+    function getMultipleCalendarObjects($calendarId, array $uris);
+
+    /**
+     * Creates a new calendar object.
+     *
+     * The object uri is only the basename, or filename and not a full path.
+     *
+     * It is possible to return an etag from this function, which will be used
+     * in the response to this PUT request. Note that the ETag must be
+     * surrounded by double-quotes.
+     *
+     * However, you should only really return this ETag if you don't mangle the
+     * calendar-data. If the result of a subsequent GET to this object is not
+     * the exact same as this request body, you should omit the ETag.
+     *
+     * @param mixed $calendarId
+     * @param string $objectUri
+     * @param string $calendarData
+     * @return string|null
+     */
+    function createCalendarObject($calendarId, $objectUri, $calendarData);
+
+    /**
+     * Updates an existing calendarobject, based on it's uri.
+     *
+     * The object uri is only the basename, or filename and not a full path.
+     *
+     * It is possible return an etag from this function, which will be used in
+     * the response to this PUT request. Note that the ETag must be surrounded
+     * by double-quotes.
+     *
+     * However, you should only really return this ETag if you don't mangle the
+     * calendar-data. If the result of a subsequent GET to this object is not
+     * the exact same as this request body, you should omit the ETag.
+     *
+     * @param mixed $calendarId
+     * @param string $objectUri
+     * @param string $calendarData
+     * @return string|null
+     */
+    function updateCalendarObject($calendarId, $objectUri, $calendarData);
+
+    /**
+     * Deletes an existing calendar object.
+     *
+     * The object uri is only the basename, or filename and not a full path.
+     *
+     * @param mixed $calendarId
+     * @param string $objectUri
+     * @return void
+     */
+    function deleteCalendarObject($calendarId, $objectUri);
+
+    /**
+     * Performs a calendar-query on the contents of this calendar.
+     *
+     * The calendar-query is defined in RFC4791 : CalDAV. Using the
+     * calendar-query it is possible for a client to request a specific set of
+     * object, based on contents of iCalendar properties, date-ranges and
+     * iCalendar component types (VTODO, VEVENT).
+     *
+     * This method should just return a list of (relative) urls that match this
+     * query.
+     *
+     * The list of filters are specified as an array. The exact array is
+     * documented by Sabre\CalDAV\CalendarQueryParser.
+     *
+     * Note that it is extremely likely that getCalendarObject for every path
+     * returned from this method will be called almost immediately after. You
+     * may want to anticipate this to speed up these requests.
+     *
+     * This method provides a default implementation, which parses *all* the
+     * iCalendar objects in the specified calendar.
+     *
+     * This default may well be good enough for personal use, and calendars
+     * that aren't very large. But if you anticipate high usage, big calendars
+     * or high loads, you are strongly adviced to optimize certain paths.
+     *
+     * The best way to do so is override this method and to optimize
+     * specifically for 'common filters'.
+     *
+     * Requests that are extremely common are:
+     *   * requests for just VEVENTS
+     *   * requests for just VTODO
+     *   * requests with a time-range-filter on either VEVENT or VTODO.
+     *
+     * ..and combinations of these requests. It may not be worth it to try to
+     * handle every possible situation and just rely on the (relatively
+     * easy to use) CalendarQueryValidator to handle the rest.
+     *
+     * Note that especially time-range-filters may be difficult to parse. A
+     * time-range filter specified on a VEVENT must for instance also handle
+     * recurrence rules correctly.
+     * A good example of how to interprete all these filters can also simply
+     * be found in Sabre\CalDAV\CalendarQueryFilter. This class is as correct
+     * as possible, so it gives you a good idea on what type of stuff you need
+     * to think of.
+     *
+     * @param mixed $calendarId
+     * @param array $filters
+     * @return array
+     */
+    function calendarQuery($calendarId, array $filters);
+
+    /**
+     * Searches through all of a users calendars and calendar objects to find
+     * an object with a specific UID.
+     *
+     * This method should return the path to this object, relative to the
+     * calendar home, so this path usually only contains two parts:
+     *
+     * calendarpath/objectpath.ics
+     *
+     * If the uid is not found, return null.
+     *
+     * This method should only consider * objects that the principal owns, so
+     * any calendars owned by other principals that also appear in this
+     * collection should be ignored.
+     *
+     * @param string $principalUri
+     * @param string $uid
+     * @return string|null
+     */
+    function getCalendarObjectByUID($principalUri, $uid);
+
+}

+ 46 - 0
lib/sabre/sabre/dav/lib/CalDAV/Backend/NotificationSupport.php

@@ -0,0 +1,46 @@
+<?php
+
+namespace Sabre\CalDAV\Backend;
+
+use Sabre\CalDAV\Xml\Notification\NotificationInterface;
+
+/**
+ * Adds caldav notification support to a backend.
+ *
+ * Note: This feature is experimental, and may change in between different
+ * SabreDAV versions.
+ *
+ * Notifications are defined at:
+ * http://svn.calendarserver.org/repository/calendarserver/CalendarServer/trunk/doc/Extensions/caldav-notifications.txt
+ *
+ * These notifications are basically a list of server-generated notifications
+ * displayed to the user. Users can dismiss notifications by deleting them.
+ *
+ * The primary usecase is to allow for calendar-sharing.
+ *
+ * @copyright Copyright (C) fruux GmbH (https://fruux.com/)
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+interface NotificationSupport extends BackendInterface {
+
+    /**
+     * Returns a list of notifications for a given principal url.
+     *
+     * @param string $principalUri
+     * @return NotificationInterface[]
+     */
+    function getNotificationsForPrincipal($principalUri);
+
+    /**
+     * This deletes a specific notifcation.
+     *
+     * This may be called by a client once it deems a notification handled.
+     *
+     * @param string $principalUri
+     * @param NotificationInterface $notification
+     * @return void
+     */
+    function deleteNotification($principalUri, NotificationInterface $notification);
+
+}

+ 1210 - 0
lib/sabre/sabre/dav/lib/CalDAV/Backend/PDO.php

@@ -0,0 +1,1210 @@
+<?php
+
+namespace Sabre\CalDAV\Backend;
+
+use Sabre\VObject;
+use Sabre\CalDAV;
+use Sabre\DAV;
+use Sabre\DAV\Exception\Forbidden;
+
+/**
+ * PDO CalDAV backend
+ *
+ * This backend is used to store calendar-data in a PDO database, such as
+ * sqlite or MySQL
+ *
+ * @copyright Copyright (C) fruux GmbH (https://fruux.com/)
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+class PDO extends AbstractBackend implements SyncSupport, SubscriptionSupport, SchedulingSupport {
+
+    /**
+     * We need to specify a max date, because we need to stop *somewhere*
+     *
+     * On 32 bit system the maximum for a signed integer is 2147483647, so
+     * MAX_DATE cannot be higher than date('Y-m-d', 2147483647) which results
+     * in 2038-01-19 to avoid problems when the date is converted
+     * to a unix timestamp.
+     */
+    const MAX_DATE = '2038-01-01';
+
+    /**
+     * pdo
+     *
+     * @var \PDO
+     */
+    protected $pdo;
+
+    /**
+     * The table name that will be used for calendars
+     *
+     * @var string
+     */
+    public $calendarTableName = 'calendars';
+
+    /**
+     * The table name that will be used for calendar objects
+     *
+     * @var string
+     */
+    public $calendarObjectTableName = 'calendarobjects';
+
+    /**
+     * The table name that will be used for tracking changes in calendars.
+     *
+     * @var string
+     */
+    public $calendarChangesTableName = 'calendarchanges';
+
+    /**
+     * The table name that will be used inbox items.
+     *
+     * @var string
+     */
+    public $schedulingObjectTableName = 'schedulingobjects';
+
+    /**
+     * The table name that will be used for calendar subscriptions.
+     *
+     * @var string
+     */
+    public $calendarSubscriptionsTableName = 'calendarsubscriptions';
+
+    /**
+     * List of CalDAV properties, and how they map to database fieldnames
+     * Add your own properties by simply adding on to this array.
+     *
+     * Note that only string-based properties are supported here.
+     *
+     * @var array
+     */
+    public $propertyMap = [
+        '{DAV:}displayname'                                   => 'displayname',
+        '{urn:ietf:params:xml:ns:caldav}calendar-description' => 'description',
+        '{urn:ietf:params:xml:ns:caldav}calendar-timezone'    => 'timezone',
+        '{http://apple.com/ns/ical/}calendar-order'           => 'calendarorder',
+        '{http://apple.com/ns/ical/}calendar-color'           => 'calendarcolor',
+    ];
+
+    /**
+     * List of subscription properties, and how they map to database fieldnames.
+     *
+     * @var array
+     */
+    public $subscriptionPropertyMap = [
+        '{DAV:}displayname'                                           => 'displayname',
+        '{http://apple.com/ns/ical/}refreshrate'                      => 'refreshrate',
+        '{http://apple.com/ns/ical/}calendar-order'                   => 'calendarorder',
+        '{http://apple.com/ns/ical/}calendar-color'                   => 'calendarcolor',
+        '{http://calendarserver.org/ns/}subscribed-strip-todos'       => 'striptodos',
+        '{http://calendarserver.org/ns/}subscribed-strip-alarms'      => 'stripalarms',
+        '{http://calendarserver.org/ns/}subscribed-strip-attachments' => 'stripattachments',
+    ];
+
+    /**
+     * Creates the backend
+     *
+     * @param \PDO $pdo
+     */
+    function __construct(\PDO $pdo) {
+
+        $this->pdo = $pdo;
+
+    }
+
+    /**
+     * Returns a list of calendars for a principal.
+     *
+     * Every project is an array with the following keys:
+     *  * id, a unique id that will be used by other functions to modify the
+     *    calendar. This can be the same as the uri or a database key.
+     *  * uri. This is just the 'base uri' or 'filename' of the calendar.
+     *  * principaluri. The owner of the calendar. Almost always the same as
+     *    principalUri passed to this method.
+     *
+     * Furthermore it can contain webdav properties in clark notation. A very
+     * common one is '{DAV:}displayname'.
+     *
+     * Many clients also require:
+     * {urn:ietf:params:xml:ns:caldav}supported-calendar-component-set
+     * For this property, you can just return an instance of
+     * Sabre\CalDAV\Xml\Property\SupportedCalendarComponentSet.
+     *
+     * If you return {http://sabredav.org/ns}read-only and set the value to 1,
+     * ACL will automatically be put in read-only mode.
+     *
+     * @param string $principalUri
+     * @return array
+     */
+    function getCalendarsForUser($principalUri) {
+
+        $fields = array_values($this->propertyMap);
+        $fields[] = 'id';
+        $fields[] = 'uri';
+        $fields[] = 'synctoken';
+        $fields[] = 'components';
+        $fields[] = 'principaluri';
+        $fields[] = 'transparent';
+
+        // Making fields a comma-delimited list
+        $fields = implode(', ', $fields);
+        $stmt = $this->pdo->prepare("SELECT " . $fields . " FROM " . $this->calendarTableName . " WHERE principaluri = ? ORDER BY calendarorder ASC");
+        $stmt->execute([$principalUri]);
+
+        $calendars = [];
+        while ($row = $stmt->fetch(\PDO::FETCH_ASSOC)) {
+
+            $components = [];
+            if ($row['components']) {
+                $components = explode(',', $row['components']);
+            }
+
+            $calendar = [
+                'id'                                                                 => $row['id'],
+                'uri'                                                                => $row['uri'],
+                'principaluri'                                                       => $row['principaluri'],
+                '{' . CalDAV\Plugin::NS_CALENDARSERVER . '}getctag'                  => 'http://sabre.io/ns/sync/' . ($row['synctoken'] ? $row['synctoken'] : '0'),
+                '{http://sabredav.org/ns}sync-token'                                 => $row['synctoken'] ? $row['synctoken'] : '0',
+                '{' . CalDAV\Plugin::NS_CALDAV . '}supported-calendar-component-set' => new CalDAV\Xml\Property\SupportedCalendarComponentSet($components),
+                '{' . CalDAV\Plugin::NS_CALDAV . '}schedule-calendar-transp'         => new CalDAV\Xml\Property\ScheduleCalendarTransp($row['transparent'] ? 'transparent' : 'opaque'),
+            ];
+
+
+            foreach ($this->propertyMap as $xmlName => $dbName) {
+                $calendar[$xmlName] = $row[$dbName];
+            }
+
+            $calendars[] = $calendar;
+
+        }
+
+        return $calendars;
+
+    }
+
+    /**
+     * Creates a new calendar for a principal.
+     *
+     * If the creation was a success, an id must be returned that can be used
+     * to reference this calendar in other methods, such as updateCalendar.
+     *
+     * @param string $principalUri
+     * @param string $calendarUri
+     * @param array $properties
+     * @return string
+     */
+    function createCalendar($principalUri, $calendarUri, array $properties) {
+
+        $fieldNames = [
+            'principaluri',
+            'uri',
+            'synctoken',
+            'transparent',
+        ];
+        $values = [
+            ':principaluri' => $principalUri,
+            ':uri'          => $calendarUri,
+            ':synctoken'    => 1,
+            ':transparent'  => 0,
+        ];
+
+        // Default value
+        $sccs = '{urn:ietf:params:xml:ns:caldav}supported-calendar-component-set';
+        $fieldNames[] = 'components';
+        if (!isset($properties[$sccs])) {
+            $values[':components'] = 'VEVENT,VTODO';
+        } else {
+            if (!($properties[$sccs] instanceof CalDAV\Xml\Property\SupportedCalendarComponentSet)) {
+                throw new DAV\Exception('The ' . $sccs . ' property must be of type: \Sabre\CalDAV\Xml\Property\SupportedCalendarComponentSet');
+            }
+            $values[':components'] = implode(',', $properties[$sccs]->getValue());
+        }
+        $transp = '{' . CalDAV\Plugin::NS_CALDAV . '}schedule-calendar-transp';
+        if (isset($properties[$transp])) {
+            $values[':transparent'] = $properties[$transp]->getValue() === 'transparent';
+        }
+
+        foreach ($this->propertyMap as $xmlName => $dbName) {
+            if (isset($properties[$xmlName])) {
+
+                $values[':' . $dbName] = $properties[$xmlName];
+                $fieldNames[] = $dbName;
+            }
+        }
+
+        $stmt = $this->pdo->prepare("INSERT INTO " . $this->calendarTableName . " (" . implode(', ', $fieldNames) . ") VALUES (" . implode(', ', array_keys($values)) . ")");
+        $stmt->execute($values);
+
+        return $this->pdo->lastInsertId();
+
+    }
+
+    /**
+     * Updates properties for a calendar.
+     *
+     * The list of mutations is stored in a Sabre\DAV\PropPatch object.
+     * To do the actual updates, you must tell this object which properties
+     * you're going to process with the handle() method.
+     *
+     * Calling the handle method is like telling the PropPatch object "I
+     * promise I can handle updating this property".
+     *
+     * Read the PropPatch documenation for more info and examples.
+     *
+     * @param string $calendarId
+     * @param \Sabre\DAV\PropPatch $propPatch
+     * @return void
+     */
+    function updateCalendar($calendarId, \Sabre\DAV\PropPatch $propPatch) {
+
+        $supportedProperties = array_keys($this->propertyMap);
+        $supportedProperties[] = '{' . CalDAV\Plugin::NS_CALDAV . '}schedule-calendar-transp';
+
+        $propPatch->handle($supportedProperties, function($mutations) use ($calendarId) {
+            $newValues = [];
+            foreach ($mutations as $propertyName => $propertyValue) {
+
+                switch ($propertyName) {
+                    case '{' . CalDAV\Plugin::NS_CALDAV . '}schedule-calendar-transp' :
+                        $fieldName = 'transparent';
+                        $newValues[$fieldName] = $propertyValue->getValue() === 'transparent';
+                        break;
+                    default :
+                        $fieldName = $this->propertyMap[$propertyName];
+                        $newValues[$fieldName] = $propertyValue;
+                        break;
+                }
+
+            }
+            $valuesSql = [];
+            foreach ($newValues as $fieldName => $value) {
+                $valuesSql[] = $fieldName . ' = ?';
+            }
+
+            $stmt = $this->pdo->prepare("UPDATE " . $this->calendarTableName . " SET " . implode(', ', $valuesSql) . " WHERE id = ?");
+            $newValues['id'] = $calendarId;
+            $stmt->execute(array_values($newValues));
+
+            $this->addChange($calendarId, "", 2);
+
+            return true;
+
+        });
+
+    }
+
+    /**
+     * Delete a calendar and all it's objects
+     *
+     * @param string $calendarId
+     * @return void
+     */
+    function deleteCalendar($calendarId) {
+
+        $stmt = $this->pdo->prepare('DELETE FROM ' . $this->calendarObjectTableName . ' WHERE calendarid = ?');
+        $stmt->execute([$calendarId]);
+
+        $stmt = $this->pdo->prepare('DELETE FROM ' . $this->calendarTableName . ' WHERE id = ?');
+        $stmt->execute([$calendarId]);
+
+        $stmt = $this->pdo->prepare('DELETE FROM ' . $this->calendarChangesTableName . ' WHERE calendarid = ?');
+        $stmt->execute([$calendarId]);
+
+    }
+
+    /**
+     * Returns all calendar objects within a calendar.
+     *
+     * Every item contains an array with the following keys:
+     *   * calendardata - The iCalendar-compatible calendar data
+     *   * uri - a unique key which will be used to construct the uri. This can
+     *     be any arbitrary string, but making sure it ends with '.ics' is a
+     *     good idea. This is only the basename, or filename, not the full
+     *     path.
+     *   * lastmodified - a timestamp of the last modification time
+     *   * etag - An arbitrary string, surrounded by double-quotes. (e.g.:
+     *   '  "abcdef"')
+     *   * size - The size of the calendar objects, in bytes.
+     *   * component - optional, a string containing the type of object, such
+     *     as 'vevent' or 'vtodo'. If specified, this will be used to populate
+     *     the Content-Type header.
+     *
+     * Note that the etag is optional, but it's highly encouraged to return for
+     * speed reasons.
+     *
+     * The calendardata is also optional. If it's not returned
+     * 'getCalendarObject' will be called later, which *is* expected to return
+     * calendardata.
+     *
+     * If neither etag or size are specified, the calendardata will be
+     * used/fetched to determine these numbers. If both are specified the
+     * amount of times this is needed is reduced by a great degree.
+     *
+     * @param string $calendarId
+     * @return array
+     */
+    function getCalendarObjects($calendarId) {
+
+        $stmt = $this->pdo->prepare('SELECT id, uri, lastmodified, etag, calendarid, size, componenttype FROM ' . $this->calendarObjectTableName . ' WHERE calendarid = ?');
+        $stmt->execute([$calendarId]);
+
+        $result = [];
+        foreach ($stmt->fetchAll(\PDO::FETCH_ASSOC) as $row) {
+            $result[] = [
+                'id'           => $row['id'],
+                'uri'          => $row['uri'],
+                'lastmodified' => $row['lastmodified'],
+                'etag'         => '"' . $row['etag'] . '"',
+                'calendarid'   => $row['calendarid'],
+                'size'         => (int)$row['size'],
+                'component'    => strtolower($row['componenttype']),
+            ];
+        }
+
+        return $result;
+
+    }
+
+    /**
+     * Returns information from a single calendar object, based on it's object
+     * uri.
+     *
+     * The object uri is only the basename, or filename and not a full path.
+     *
+     * The returned array must have the same keys as getCalendarObjects. The
+     * 'calendardata' object is required here though, while it's not required
+     * for getCalendarObjects.
+     *
+     * This method must return null if the object did not exist.
+     *
+     * @param string $calendarId
+     * @param string $objectUri
+     * @return array|null
+     */
+    function getCalendarObject($calendarId, $objectUri) {
+
+        $stmt = $this->pdo->prepare('SELECT id, uri, lastmodified, etag, calendarid, size, calendardata, componenttype FROM ' . $this->calendarObjectTableName . ' WHERE calendarid = ? AND uri = ?');
+        $stmt->execute([$calendarId, $objectUri]);
+        $row = $stmt->fetch(\PDO::FETCH_ASSOC);
+
+        if (!$row) return null;
+
+        return [
+            'id'            => $row['id'],
+            'uri'           => $row['uri'],
+            'lastmodified'  => $row['lastmodified'],
+            'etag'          => '"' . $row['etag'] . '"',
+            'calendarid'    => $row['calendarid'],
+            'size'          => (int)$row['size'],
+            'calendardata'  => $row['calendardata'],
+            'component'     => strtolower($row['componenttype']),
+         ];
+
+    }
+
+    /**
+     * Returns a list of calendar objects.
+     *
+     * This method should work identical to getCalendarObject, but instead
+     * return all the calendar objects in the list as an array.
+     *
+     * If the backend supports this, it may allow for some speed-ups.
+     *
+     * @param mixed $calendarId
+     * @param array $uris
+     * @return array
+     */
+    function getMultipleCalendarObjects($calendarId, array $uris) {
+
+        $query = 'SELECT id, uri, lastmodified, etag, calendarid, size, calendardata, componenttype FROM ' . $this->calendarObjectTableName . ' WHERE calendarid = ? AND uri IN (';
+        // Inserting a whole bunch of question marks
+        $query .= implode(',', array_fill(0, count($uris), '?'));
+        $query .= ')';
+
+        $stmt = $this->pdo->prepare($query);
+        $stmt->execute(array_merge([$calendarId], $uris));
+
+        $result = [];
+        while ($row = $stmt->fetch(\PDO::FETCH_ASSOC)) {
+
+            $result[] = [
+                'id'           => $row['id'],
+                'uri'          => $row['uri'],
+                'lastmodified' => $row['lastmodified'],
+                'etag'         => '"' . $row['etag'] . '"',
+                'calendarid'   => $row['calendarid'],
+                'size'         => (int)$row['size'],
+                'calendardata' => $row['calendardata'],
+                'component'    => strtolower($row['componenttype']),
+            ];
+
+        }
+        return $result;
+
+    }
+
+
+    /**
+     * Creates a new calendar object.
+     *
+     * The object uri is only the basename, or filename and not a full path.
+     *
+     * It is possible return an etag from this function, which will be used in
+     * the response to this PUT request. Note that the ETag must be surrounded
+     * by double-quotes.
+     *
+     * However, you should only really return this ETag if you don't mangle the
+     * calendar-data. If the result of a subsequent GET to this object is not
+     * the exact same as this request body, you should omit the ETag.
+     *
+     * @param mixed $calendarId
+     * @param string $objectUri
+     * @param string $calendarData
+     * @return string|null
+     */
+    function createCalendarObject($calendarId, $objectUri, $calendarData) {
+
+        $extraData = $this->getDenormalizedData($calendarData);
+
+        $stmt = $this->pdo->prepare('INSERT INTO ' . $this->calendarObjectTableName . ' (calendarid, uri, calendardata, lastmodified, etag, size, componenttype, firstoccurence, lastoccurence, uid) VALUES (?,?,?,?,?,?,?,?,?,?)');
+        $stmt->execute([
+            $calendarId,
+            $objectUri,
+            $calendarData,
+            time(),
+            $extraData['etag'],
+            $extraData['size'],
+            $extraData['componentType'],
+            $extraData['firstOccurence'],
+            $extraData['lastOccurence'],
+            $extraData['uid'],
+        ]);
+        $this->addChange($calendarId, $objectUri, 1);
+
+        return '"' . $extraData['etag'] . '"';
+
+    }
+
+    /**
+     * Updates an existing calendarobject, based on it's uri.
+     *
+     * The object uri is only the basename, or filename and not a full path.
+     *
+     * It is possible return an etag from this function, which will be used in
+     * the response to this PUT request. Note that the ETag must be surrounded
+     * by double-quotes.
+     *
+     * However, you should only really return this ETag if you don't mangle the
+     * calendar-data. If the result of a subsequent GET to this object is not
+     * the exact same as this request body, you should omit the ETag.
+     *
+     * @param mixed $calendarId
+     * @param string $objectUri
+     * @param string $calendarData
+     * @return string|null
+     */
+    function updateCalendarObject($calendarId, $objectUri, $calendarData) {
+
+        $extraData = $this->getDenormalizedData($calendarData);
+
+        $stmt = $this->pdo->prepare('UPDATE ' . $this->calendarObjectTableName . ' SET calendardata = ?, lastmodified = ?, etag = ?, size = ?, componenttype = ?, firstoccurence = ?, lastoccurence = ?, uid = ? WHERE calendarid = ? AND uri = ?');
+        $stmt->execute([$calendarData, time(), $extraData['etag'], $extraData['size'], $extraData['componentType'], $extraData['firstOccurence'], $extraData['lastOccurence'], $extraData['uid'], $calendarId, $objectUri]);
+
+        $this->addChange($calendarId, $objectUri, 2);
+
+        return '"' . $extraData['etag'] . '"';
+
+    }
+
+    /**
+     * Parses some information from calendar objects, used for optimized
+     * calendar-queries.
+     *
+     * Returns an array with the following keys:
+     *   * etag - An md5 checksum of the object without the quotes.
+     *   * size - Size of the object in bytes
+     *   * componentType - VEVENT, VTODO or VJOURNAL
+     *   * firstOccurence
+     *   * lastOccurence
+     *   * uid - value of the UID property
+     *
+     * @param string $calendarData
+     * @return array
+     */
+    protected function getDenormalizedData($calendarData) {
+
+        $vObject = VObject\Reader::read($calendarData);
+        $componentType = null;
+        $component = null;
+        $firstOccurence = null;
+        $lastOccurence = null;
+        $uid = null;
+        foreach ($vObject->getComponents() as $component) {
+            if ($component->name !== 'VTIMEZONE') {
+                $componentType = $component->name;
+                $uid = (string)$component->UID;
+                break;
+            }
+        }
+        if (!$componentType) {
+            throw new \Sabre\DAV\Exception\BadRequest('Calendar objects must have a VJOURNAL, VEVENT or VTODO component');
+        }
+        if ($componentType === 'VEVENT') {
+            $firstOccurence = $component->DTSTART->getDateTime()->getTimeStamp();
+            // Finding the last occurence is a bit harder
+            if (!isset($component->RRULE)) {
+                if (isset($component->DTEND)) {
+                    $lastOccurence = $component->DTEND->getDateTime()->getTimeStamp();
+                } elseif (isset($component->DURATION)) {
+                    $endDate = clone $component->DTSTART->getDateTime();
+                    $endDate = $endDate->add(VObject\DateTimeParser::parse($component->DURATION->getValue()));
+                    $lastOccurence = $endDate->getTimeStamp();
+                } elseif (!$component->DTSTART->hasTime()) {
+                    $endDate = clone $component->DTSTART->getDateTime();
+                    $endDate = $endDate->modify('+1 day');
+                    $lastOccurence = $endDate->getTimeStamp();
+                } else {
+                    $lastOccurence = $firstOccurence;
+                }
+            } else {
+                $it = new VObject\Recur\EventIterator($vObject, (string)$component->UID);
+                $maxDate = new \DateTime(self::MAX_DATE);
+                if ($it->isInfinite()) {
+                    $lastOccurence = $maxDate->getTimeStamp();
+                } else {
+                    $end = $it->getDtEnd();
+                    while ($it->valid() && $end < $maxDate) {
+                        $end = $it->getDtEnd();
+                        $it->next();
+
+                    }
+                    $lastOccurence = $end->getTimeStamp();
+                }
+
+            }
+        }
+
+        // Destroy circular references to PHP will GC the object.
+        $vObject->destroy();
+
+        return [
+            'etag'           => md5($calendarData),
+            'size'           => strlen($calendarData),
+            'componentType'  => $componentType,
+            'firstOccurence' => $firstOccurence,
+            'lastOccurence'  => $lastOccurence,
+            'uid'            => $uid,
+        ];
+
+    }
+
+    /**
+     * Deletes an existing calendar object.
+     *
+     * The object uri is only the basename, or filename and not a full path.
+     *
+     * @param string $calendarId
+     * @param string $objectUri
+     * @return void
+     */
+    function deleteCalendarObject($calendarId, $objectUri) {
+
+        $stmt = $this->pdo->prepare('DELETE FROM ' . $this->calendarObjectTableName . ' WHERE calendarid = ? AND uri = ?');
+        $stmt->execute([$calendarId, $objectUri]);
+
+        $this->addChange($calendarId, $objectUri, 3);
+
+    }
+
+    /**
+     * Performs a calendar-query on the contents of this calendar.
+     *
+     * The calendar-query is defined in RFC4791 : CalDAV. Using the
+     * calendar-query it is possible for a client to request a specific set of
+     * object, based on contents of iCalendar properties, date-ranges and
+     * iCalendar component types (VTODO, VEVENT).
+     *
+     * This method should just return a list of (relative) urls that match this
+     * query.
+     *
+     * The list of filters are specified as an array. The exact array is
+     * documented by \Sabre\CalDAV\CalendarQueryParser.
+     *
+     * Note that it is extremely likely that getCalendarObject for every path
+     * returned from this method will be called almost immediately after. You
+     * may want to anticipate this to speed up these requests.
+     *
+     * This method provides a default implementation, which parses *all* the
+     * iCalendar objects in the specified calendar.
+     *
+     * This default may well be good enough for personal use, and calendars
+     * that aren't very large. But if you anticipate high usage, big calendars
+     * or high loads, you are strongly adviced to optimize certain paths.
+     *
+     * The best way to do so is override this method and to optimize
+     * specifically for 'common filters'.
+     *
+     * Requests that are extremely common are:
+     *   * requests for just VEVENTS
+     *   * requests for just VTODO
+     *   * requests with a time-range-filter on a VEVENT.
+     *
+     * ..and combinations of these requests. It may not be worth it to try to
+     * handle every possible situation and just rely on the (relatively
+     * easy to use) CalendarQueryValidator to handle the rest.
+     *
+     * Note that especially time-range-filters may be difficult to parse. A
+     * time-range filter specified on a VEVENT must for instance also handle
+     * recurrence rules correctly.
+     * A good example of how to interprete all these filters can also simply
+     * be found in \Sabre\CalDAV\CalendarQueryFilter. This class is as correct
+     * as possible, so it gives you a good idea on what type of stuff you need
+     * to think of.
+     *
+     * This specific implementation (for the PDO) backend optimizes filters on
+     * specific components, and VEVENT time-ranges.
+     *
+     * @param string $calendarId
+     * @param array $filters
+     * @return array
+     */
+    function calendarQuery($calendarId, array $filters) {
+
+        $componentType = null;
+        $requirePostFilter = true;
+        $timeRange = null;
+
+        // if no filters were specified, we don't need to filter after a query
+        if (!$filters['prop-filters'] && !$filters['comp-filters']) {
+            $requirePostFilter = false;
+        }
+
+        // Figuring out if there's a component filter
+        if (count($filters['comp-filters']) > 0 && !$filters['comp-filters'][0]['is-not-defined']) {
+            $componentType = $filters['comp-filters'][0]['name'];
+
+            // Checking if we need post-filters
+            if (!$filters['prop-filters'] && !$filters['comp-filters'][0]['comp-filters'] && !$filters['comp-filters'][0]['time-range'] && !$filters['comp-filters'][0]['prop-filters']) {
+                $requirePostFilter = false;
+            }
+            // There was a time-range filter
+            if ($componentType == 'VEVENT' && isset($filters['comp-filters'][0]['time-range'])) {
+                $timeRange = $filters['comp-filters'][0]['time-range'];
+
+                // If start time OR the end time is not specified, we can do a
+                // 100% accurate mysql query.
+                if (!$filters['prop-filters'] && !$filters['comp-filters'][0]['comp-filters'] && !$filters['comp-filters'][0]['prop-filters'] && (!$timeRange['start'] || !$timeRange['end'])) {
+                    $requirePostFilter = false;
+                }
+            }
+
+        }
+
+        if ($requirePostFilter) {
+            $query = "SELECT uri, calendardata FROM " . $this->calendarObjectTableName . " WHERE calendarid = :calendarid";
+        } else {
+            $query = "SELECT uri FROM " . $this->calendarObjectTableName . " WHERE calendarid = :calendarid";
+        }
+
+        $values = [
+            'calendarid' => $calendarId,
+        ];
+
+        if ($componentType) {
+            $query .= " AND componenttype = :componenttype";
+            $values['componenttype'] = $componentType;
+        }
+
+        if ($timeRange && $timeRange['start']) {
+            $query .= " AND lastoccurence > :startdate";
+            $values['startdate'] = $timeRange['start']->getTimeStamp();
+        }
+        if ($timeRange && $timeRange['end']) {
+            $query .= " AND firstoccurence < :enddate";
+            $values['enddate'] = $timeRange['end']->getTimeStamp();
+        }
+
+        $stmt = $this->pdo->prepare($query);
+        $stmt->execute($values);
+
+        $result = [];
+        while ($row = $stmt->fetch(\PDO::FETCH_ASSOC)) {
+            if ($requirePostFilter) {
+                if (!$this->validateFilterForObject($row, $filters)) {
+                    continue;
+                }
+            }
+            $result[] = $row['uri'];
+
+        }
+
+        return $result;
+
+    }
+
+    /**
+     * Searches through all of a users calendars and calendar objects to find
+     * an object with a specific UID.
+     *
+     * This method should return the path to this object, relative to the
+     * calendar home, so this path usually only contains two parts:
+     *
+     * calendarpath/objectpath.ics
+     *
+     * If the uid is not found, return null.
+     *
+     * This method should only consider * objects that the principal owns, so
+     * any calendars owned by other principals that also appear in this
+     * collection should be ignored.
+     *
+     * @param string $principalUri
+     * @param string $uid
+     * @return string|null
+     */
+    function getCalendarObjectByUID($principalUri, $uid) {
+
+        $query = <<<SQL
+SELECT
+    calendars.uri AS calendaruri, calendarobjects.uri as objecturi
+FROM
+    $this->calendarObjectTableName AS calendarobjects
+LEFT JOIN
+    $this->calendarTableName AS calendars
+    ON calendarobjects.calendarid = calendars.id
+WHERE
+    calendars.principaluri = ?
+    AND
+    calendarobjects.uid = ?
+SQL;
+
+        $stmt = $this->pdo->prepare($query);
+        $stmt->execute([$principalUri, $uid]);
+
+        if ($row = $stmt->fetch(\PDO::FETCH_ASSOC)) {
+            return $row['calendaruri'] . '/' . $row['objecturi'];
+        }
+
+    }
+
+    /**
+     * The getChanges method returns all the changes that have happened, since
+     * the specified syncToken in the specified calendar.
+     *
+     * This function should return an array, such as the following:
+     *
+     * [
+     *   'syncToken' => 'The current synctoken',
+     *   'added'   => [
+     *      'new.txt',
+     *   ],
+     *   'modified'   => [
+     *      'modified.txt',
+     *   ],
+     *   'deleted' => [
+     *      'foo.php.bak',
+     *      'old.txt'
+     *   ]
+     * ];
+     *
+     * The returned syncToken property should reflect the *current* syncToken
+     * of the calendar, as reported in the {http://sabredav.org/ns}sync-token
+     * property this is needed here too, to ensure the operation is atomic.
+     *
+     * If the $syncToken argument is specified as null, this is an initial
+     * sync, and all members should be reported.
+     *
+     * The modified property is an array of nodenames that have changed since
+     * the last token.
+     *
+     * The deleted property is an array with nodenames, that have been deleted
+     * from collection.
+     *
+     * The $syncLevel argument is basically the 'depth' of the report. If it's
+     * 1, you only have to report changes that happened only directly in
+     * immediate descendants. If it's 2, it should also include changes from
+     * the nodes below the child collections. (grandchildren)
+     *
+     * The $limit argument allows a client to specify how many results should
+     * be returned at most. If the limit is not specified, it should be treated
+     * as infinite.
+     *
+     * If the limit (infinite or not) is higher than you're willing to return,
+     * you should throw a Sabre\DAV\Exception\TooMuchMatches() exception.
+     *
+     * If the syncToken is expired (due to data cleanup) or unknown, you must
+     * return null.
+     *
+     * The limit is 'suggestive'. You are free to ignore it.
+     *
+     * @param string $calendarId
+     * @param string $syncToken
+     * @param int $syncLevel
+     * @param int $limit
+     * @return array
+     */
+    function getChangesForCalendar($calendarId, $syncToken, $syncLevel, $limit = null) {
+
+        // Current synctoken
+        $stmt = $this->pdo->prepare('SELECT synctoken FROM ' . $this->calendarTableName . ' WHERE id = ?');
+        $stmt->execute([ $calendarId ]);
+        $currentToken = $stmt->fetchColumn(0);
+
+        if (is_null($currentToken)) return null;
+
+        $result = [
+            'syncToken' => $currentToken,
+            'added'     => [],
+            'modified'  => [],
+            'deleted'   => [],
+        ];
+
+        if ($syncToken) {
+
+            $query = "SELECT uri, operation FROM " . $this->calendarChangesTableName . " WHERE synctoken >= ? AND synctoken < ? AND calendarid = ? ORDER BY synctoken";
+            if ($limit > 0) $query .= " LIMIT " . (int)$limit;
+
+            // Fetching all changes
+            $stmt = $this->pdo->prepare($query);
+            $stmt->execute([$syncToken, $currentToken, $calendarId]);
+
+            $changes = [];
+
+            // This loop ensures that any duplicates are overwritten, only the
+            // last change on a node is relevant.
+            while ($row = $stmt->fetch(\PDO::FETCH_ASSOC)) {
+
+                $changes[$row['uri']] = $row['operation'];
+
+            }
+
+            foreach ($changes as $uri => $operation) {
+
+                switch ($operation) {
+                    case 1 :
+                        $result['added'][] = $uri;
+                        break;
+                    case 2 :
+                        $result['modified'][] = $uri;
+                        break;
+                    case 3 :
+                        $result['deleted'][] = $uri;
+                        break;
+                }
+
+            }
+        } else {
+            // No synctoken supplied, this is the initial sync.
+            $query = "SELECT uri FROM " . $this->calendarObjectTableName . " WHERE calendarid = ?";
+            $stmt = $this->pdo->prepare($query);
+            $stmt->execute([$calendarId]);
+
+            $result['added'] = $stmt->fetchAll(\PDO::FETCH_COLUMN);
+        }
+        return $result;
+
+    }
+
+    /**
+     * Adds a change record to the calendarchanges table.
+     *
+     * @param mixed $calendarId
+     * @param string $objectUri
+     * @param int $operation 1 = add, 2 = modify, 3 = delete.
+     * @return void
+     */
+    protected function addChange($calendarId, $objectUri, $operation) {
+
+        $stmt = $this->pdo->prepare('INSERT INTO ' . $this->calendarChangesTableName . ' (uri, synctoken, calendarid, operation) SELECT ?, synctoken, ?, ? FROM ' . $this->calendarTableName . ' WHERE id = ?');
+        $stmt->execute([
+            $objectUri,
+            $calendarId,
+            $operation,
+            $calendarId
+        ]);
+        $stmt = $this->pdo->prepare('UPDATE ' . $this->calendarTableName . ' SET synctoken = synctoken + 1 WHERE id = ?');
+        $stmt->execute([
+            $calendarId
+        ]);
+
+    }
+
+    /**
+     * Returns a list of subscriptions for a principal.
+     *
+     * Every subscription is an array with the following keys:
+     *  * id, a unique id that will be used by other functions to modify the
+     *    subscription. This can be the same as the uri or a database key.
+     *  * uri. This is just the 'base uri' or 'filename' of the subscription.
+     *  * principaluri. The owner of the subscription. Almost always the same as
+     *    principalUri passed to this method.
+     *  * source. Url to the actual feed
+     *
+     * Furthermore, all the subscription info must be returned too:
+     *
+     * 1. {DAV:}displayname
+     * 2. {http://apple.com/ns/ical/}refreshrate
+     * 3. {http://calendarserver.org/ns/}subscribed-strip-todos (omit if todos
+     *    should not be stripped).
+     * 4. {http://calendarserver.org/ns/}subscribed-strip-alarms (omit if alarms
+     *    should not be stripped).
+     * 5. {http://calendarserver.org/ns/}subscribed-strip-attachments (omit if
+     *    attachments should not be stripped).
+     * 7. {http://apple.com/ns/ical/}calendar-color
+     * 8. {http://apple.com/ns/ical/}calendar-order
+     * 9. {urn:ietf:params:xml:ns:caldav}supported-calendar-component-set
+     *    (should just be an instance of
+     *    Sabre\CalDAV\Property\SupportedCalendarComponentSet, with a bunch of
+     *    default components).
+     *
+     * @param string $principalUri
+     * @return array
+     */
+    function getSubscriptionsForUser($principalUri) {
+
+        $fields = array_values($this->subscriptionPropertyMap);
+        $fields[] = 'id';
+        $fields[] = 'uri';
+        $fields[] = 'source';
+        $fields[] = 'principaluri';
+        $fields[] = 'lastmodified';
+
+        // Making fields a comma-delimited list
+        $fields = implode(', ', $fields);
+        $stmt = $this->pdo->prepare("SELECT " . $fields . " FROM " . $this->calendarSubscriptionsTableName . " WHERE principaluri = ? ORDER BY calendarorder ASC");
+        $stmt->execute([$principalUri]);
+
+        $subscriptions = [];
+        while ($row = $stmt->fetch(\PDO::FETCH_ASSOC)) {
+
+            $subscription = [
+                'id'           => $row['id'],
+                'uri'          => $row['uri'],
+                'principaluri' => $row['principaluri'],
+                'source'       => $row['source'],
+                'lastmodified' => $row['lastmodified'],
+
+                '{' . CalDAV\Plugin::NS_CALDAV . '}supported-calendar-component-set' => new CalDAV\Xml\Property\SupportedCalendarComponentSet(['VTODO', 'VEVENT']),
+            ];
+
+            foreach ($this->subscriptionPropertyMap as $xmlName => $dbName) {
+                if (!is_null($row[$dbName])) {
+                    $subscription[$xmlName] = $row[$dbName];
+                }
+            }
+
+            $subscriptions[] = $subscription;
+
+        }
+
+        return $subscriptions;
+
+    }
+
+    /**
+     * Creates a new subscription for a principal.
+     *
+     * If the creation was a success, an id must be returned that can be used to reference
+     * this subscription in other methods, such as updateSubscription.
+     *
+     * @param string $principalUri
+     * @param string $uri
+     * @param array $properties
+     * @return mixed
+     */
+    function createSubscription($principalUri, $uri, array $properties) {
+
+        $fieldNames = [
+            'principaluri',
+            'uri',
+            'source',
+            'lastmodified',
+        ];
+
+        if (!isset($properties['{http://calendarserver.org/ns/}source'])) {
+            throw new Forbidden('The {http://calendarserver.org/ns/}source property is required when creating subscriptions');
+        }
+
+        $values = [
+            ':principaluri' => $principalUri,
+            ':uri'          => $uri,
+            ':source'       => $properties['{http://calendarserver.org/ns/}source']->getHref(),
+            ':lastmodified' => time(),
+        ];
+
+        foreach ($this->subscriptionPropertyMap as $xmlName => $dbName) {
+            if (isset($properties[$xmlName])) {
+
+                $values[':' . $dbName] = $properties[$xmlName];
+                $fieldNames[] = $dbName;
+            }
+        }
+
+        $stmt = $this->pdo->prepare("INSERT INTO " . $this->calendarSubscriptionsTableName . " (" . implode(', ', $fieldNames) . ") VALUES (" . implode(', ', array_keys($values)) . ")");
+        $stmt->execute($values);
+
+        return $this->pdo->lastInsertId();
+
+    }
+
+    /**
+     * Updates a subscription
+     *
+     * The list of mutations is stored in a Sabre\DAV\PropPatch object.
+     * To do the actual updates, you must tell this object which properties
+     * you're going to process with the handle() method.
+     *
+     * Calling the handle method is like telling the PropPatch object "I
+     * promise I can handle updating this property".
+     *
+     * Read the PropPatch documenation for more info and examples.
+     *
+     * @param mixed $subscriptionId
+     * @param \Sabre\DAV\PropPatch $propPatch
+     * @return void
+     */
+    function updateSubscription($subscriptionId, DAV\PropPatch $propPatch) {
+
+        $supportedProperties = array_keys($this->subscriptionPropertyMap);
+        $supportedProperties[] = '{http://calendarserver.org/ns/}source';
+
+        $propPatch->handle($supportedProperties, function($mutations) use ($subscriptionId) {
+
+            $newValues = [];
+
+            foreach ($mutations as $propertyName => $propertyValue) {
+
+                if ($propertyName === '{http://calendarserver.org/ns/}source') {
+                    $newValues['source'] = $propertyValue->getHref();
+                } else {
+                    $fieldName = $this->subscriptionPropertyMap[$propertyName];
+                    $newValues[$fieldName] = $propertyValue;
+                }
+
+            }
+
+            // Now we're generating the sql query.
+            $valuesSql = [];
+            foreach ($newValues as $fieldName => $value) {
+                $valuesSql[] = $fieldName . ' = ?';
+            }
+
+            $stmt = $this->pdo->prepare("UPDATE " . $this->calendarSubscriptionsTableName . " SET " . implode(', ', $valuesSql) . ", lastmodified = ? WHERE id = ?");
+            $newValues['lastmodified'] = time();
+            $newValues['id'] = $subscriptionId;
+            $stmt->execute(array_values($newValues));
+
+            return true;
+
+        });
+
+    }
+
+    /**
+     * Deletes a subscription
+     *
+     * @param mixed $subscriptionId
+     * @return void
+     */
+    function deleteSubscription($subscriptionId) {
+
+        $stmt = $this->pdo->prepare('DELETE FROM ' . $this->calendarSubscriptionsTableName . ' WHERE id = ?');
+        $stmt->execute([$subscriptionId]);
+
+    }
+
+    /**
+     * Returns a single scheduling object.
+     *
+     * The returned array should contain the following elements:
+     *   * uri - A unique basename for the object. This will be used to
+     *           construct a full uri.
+     *   * calendardata - The iCalendar object
+     *   * lastmodified - The last modification date. Can be an int for a unix
+     *                    timestamp, or a PHP DateTime object.
+     *   * etag - A unique token that must change if the object changed.
+     *   * size - The size of the object, in bytes.
+     *
+     * @param string $principalUri
+     * @param string $objectUri
+     * @return array
+     */
+    function getSchedulingObject($principalUri, $objectUri) {
+
+        $stmt = $this->pdo->prepare('SELECT uri, calendardata, lastmodified, etag, size FROM ' . $this->schedulingObjectTableName . ' WHERE principaluri = ? AND uri = ?');
+        $stmt->execute([$principalUri, $objectUri]);
+        $row = $stmt->fetch(\PDO::FETCH_ASSOC);
+
+        if (!$row) return null;
+
+        return [
+            'uri'          => $row['uri'],
+            'calendardata' => $row['calendardata'],
+            'lastmodified' => $row['lastmodified'],
+            'etag'         => '"' . $row['etag'] . '"',
+            'size'         => (int)$row['size'],
+         ];
+
+    }
+
+    /**
+     * Returns all scheduling objects for the inbox collection.
+     *
+     * These objects should be returned as an array. Every item in the array
+     * should follow the same structure as returned from getSchedulingObject.
+     *
+     * The main difference is that 'calendardata' is optional.
+     *
+     * @param string $principalUri
+     * @return array
+     */
+    function getSchedulingObjects($principalUri) {
+
+        $stmt = $this->pdo->prepare('SELECT id, calendardata, uri, lastmodified, etag, size FROM ' . $this->schedulingObjectTableName . ' WHERE principaluri = ?');
+        $stmt->execute([$principalUri]);
+
+        $result = [];
+        foreach ($stmt->fetchAll(\PDO::FETCH_ASSOC) as $row) {
+            $result[] = [
+                'calendardata' => $row['calendardata'],
+                'uri'          => $row['uri'],
+                'lastmodified' => $row['lastmodified'],
+                'etag'         => '"' . $row['etag'] . '"',
+                'size'         => (int)$row['size'],
+            ];
+        }
+
+        return $result;
+
+    }
+
+    /**
+     * Deletes a scheduling object
+     *
+     * @param string $principalUri
+     * @param string $objectUri
+     * @return void
+     */
+    function deleteSchedulingObject($principalUri, $objectUri) {
+
+        $stmt = $this->pdo->prepare('DELETE FROM ' . $this->schedulingObjectTableName . ' WHERE principaluri = ? AND uri = ?');
+        $stmt->execute([$principalUri, $objectUri]);
+
+    }
+
+    /**
+     * Creates a new scheduling object. This should land in a users' inbox.
+     *
+     * @param string $principalUri
+     * @param string $objectUri
+     * @param string $objectData
+     * @return void
+     */
+    function createSchedulingObject($principalUri, $objectUri, $objectData) {
+
+        $stmt = $this->pdo->prepare('INSERT INTO ' . $this->schedulingObjectTableName . ' (principaluri, calendardata, uri, lastmodified, etag, size) VALUES (?, ?, ?, ?, ?, ?)');
+        $stmt->execute([$principalUri, $objectData, $objectUri, time(), md5($objectData), strlen($objectData) ]);
+
+    }
+
+}

+ 65 - 0
lib/sabre/sabre/dav/lib/CalDAV/Backend/SchedulingSupport.php

@@ -0,0 +1,65 @@
+<?php
+
+namespace Sabre\CalDAV\Backend;
+
+/**
+ * Implementing this interface adds CalDAV Scheduling support to your caldav
+ * server, as defined in rfc6638.
+ *
+ * @copyright Copyright (C) fruux GmbH (https://fruux.com/)
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+interface SchedulingSupport extends BackendInterface {
+
+    /**
+     * Returns a single scheduling object for the inbox collection.
+     *
+     * The returned array should contain the following elements:
+     *   * uri - A unique basename for the object. This will be used to
+     *           construct a full uri.
+     *   * calendardata - The iCalendar object
+     *   * lastmodified - The last modification date. Can be an int for a unix
+     *                    timestamp, or a PHP DateTime object.
+     *   * etag - A unique token that must change if the object changed.
+     *   * size - The size of the object, in bytes.
+     *
+     * @param string $principalUri
+     * @param string $objectUri
+     * @return array
+     */
+    function getSchedulingObject($principalUri, $objectUri);
+
+    /**
+     * Returns all scheduling objects for the inbox collection.
+     *
+     * These objects should be returned as an array. Every item in the array
+     * should follow the same structure as returned from getSchedulingObject.
+     *
+     * The main difference is that 'calendardata' is optional.
+     *
+     * @param string $principalUri
+     * @return array
+     */
+    function getSchedulingObjects($principalUri);
+
+    /**
+     * Deletes a scheduling object from the inbox collection.
+     *
+     * @param string $principalUri
+     * @param string $objectUri
+     * @return void
+     */
+    function deleteSchedulingObject($principalUri, $objectUri);
+
+    /**
+     * Creates a new scheduling object. This should land in a users' inbox.
+     *
+     * @param string $principalUri
+     * @param string $objectUri
+     * @param string $objectData
+     * @return void
+     */
+    function createSchedulingObject($principalUri, $objectUri, $objectData);
+
+}

+ 243 - 0
lib/sabre/sabre/dav/lib/CalDAV/Backend/SharingSupport.php

@@ -0,0 +1,243 @@
+<?php
+
+namespace Sabre\CalDAV\Backend;
+
+/**
+ * Adds support for sharing features to a CalDAV server.
+ *
+ * Note: This feature is experimental, and may change in between different
+ * SabreDAV versions.
+ *
+ * Early warning: Currently SabreDAV provides no implementation for this. This
+ * is, because in it's current state there is no elegant way to do this.
+ * The problem lies in the fact that a real CalDAV server with sharing support
+ * would first need email support (with invite notifications), and really also
+ * a browser-frontend that allows people to accept or reject these shares.
+ *
+ * In addition, the CalDAV backends are currently kept as independent as
+ * possible, and should not be aware of principals, email addresses or
+ * accounts.
+ *
+ * Adding an implementation for Sharing to standard-sabredav would contradict
+ * these goals, so for this reason this is currently not implemented, although
+ * it may very well in the future; but probably not before SabreDAV 2.0.
+ *
+ * The interface works however, so if you implement all this, and do it
+ * correctly sharing _will_ work. It's not particularly easy, and I _urge you_
+ * to make yourself acquainted with the following document first:
+ *
+ * https://trac.calendarserver.org/browser/CalendarServer/trunk/doc/Extensions/caldav-sharing.txt
+ *
+ * An overview
+ * ===========
+ *
+ * Implementing this interface will allow a user to share his or her calendars
+ * to other users. Effectively, when a calendar is shared the calendar will
+ * show up in both the Sharer's and Sharee's calendar-home root.
+ * This interface adds a few methods that ensure that this happens, and there
+ * are also a number of new requirements in the base-class you must now follow.
+ *
+ *
+ * How it works
+ * ============
+ *
+ * When a user shares a calendar, the updateShares() method will be called with
+ * a list of sharees that are now added, and a list of sharees that have been
+ * removed.
+ * Removal is instant, but when a sharee is added the sharee first gets a
+ * chance to accept or reject the invitation for a share.
+ *
+ * After a share is accepted, the calendar will be returned from
+ * getUserCalendars for both the sharer, and the sharee.
+ *
+ * If the sharee deletes the calendar, only their share gets deleted. When the
+ * owner deletes a calendar, it will be removed for everybody.
+ *
+ *
+ * Notifications
+ * =============
+ *
+ * During all these sharing operations, a lot of notifications are sent back
+ * and forward.
+ *
+ * Whenever the list of sharees for a calendar has been changed (they have been
+ * added, removed or modified) all sharees should get a notification for this
+ * change.
+ * This notification is always represented by:
+ *
+ * Sabre\CalDAV\Notifications\Notification\Invite
+ *
+ * In the case of an invite, the sharee may reply with an 'accept' or
+ * 'decline'. These are always represented by:
+ *
+ * Sabre\CalDAV\Notifications\Notification\InviteReply
+ *
+ *
+ * Calendar access by sharees
+ * ==========================
+ *
+ * As mentioned earlier, shared calendars must now also be returned for
+ * getCalendarsForUser for sharees. A few things change though.
+ *
+ * The following properties must be specified:
+ *
+ * 1. {http://calendarserver.org/ns/}shared-url
+ *
+ * This property MUST contain the url to the original calendar, that is.. the
+ * path to the calendar from the owner.
+ *
+ * 2. {http://sabredav.org/ns}owner-principal
+ *
+ * This is a url to to the principal who is sharing the calendar.
+ *
+ * 3. {http://sabredav.org/ns}read-only
+ *
+ * This should be either 0 or 1, depending on if the user has read-only or
+ * read-write access to the calendar.
+ *
+ * Only when this is done, the calendar will correctly be marked as a calendar
+ * that's shared to him, thus allowing clients to display the correct interface
+ * and ACL enforcement.
+ *
+ * If a sharee deletes their calendar, only their instance of the calendar
+ * should be deleted, the original should still exists.
+ * Pretty much any 'dead' WebDAV properties on these shared calendars should be
+ * specific to a user. This means that if the displayname is changed by a
+ * sharee, the original is not affected. This is also true for:
+ *   * The description
+ *   * The color
+ *   * The order
+ *   * And any other dead properties.
+ *
+ * Properties like a ctag should not be different for multiple instances of the
+ * calendar.
+ *
+ * Lastly, objects *within* calendars should also have user-specific data. The
+ * two things that are user-specific are:
+ *   * VALARM objects
+ *   * The TRANSP property
+ *
+ * This _also_ implies that if a VALARM is deleted by a sharee for some event,
+ * this has no effect on the original VALARM.
+ *
+ * Understandably, the this last requirement is one of the hardest.
+ * Realisticly, I can see people ignoring this part of the spec, but that could
+ * cause a different set of issues.
+ *
+ *
+ * Publishing
+ * ==========
+ *
+ * When a user publishes a url, the server should generate a 'publish url'.
+ * This is a read-only url, anybody can use to consume the calendar feed.
+ *
+ * Calendars are in one of two states:
+ *   * published
+ *   * unpublished
+ *
+ * If a calendar is published, the following property should be returned
+ * for each calendar in getCalendarsForUser.
+ *
+ * {http://calendarserver.org/ns/}publish-url
+ *
+ * This element should contain a {DAV:}href element, which points to the
+ * public url that does not require authentication. Unlike every other href,
+ * this url must be absolute.
+ *
+ * Ideally, the following property is always returned
+ *
+ * {http://calendarserver.org/ns/}pre-publish-url
+ *
+ * This property should contain the url that the calendar _would_ have, if it
+ * were to be published. iCal uses this to display the url, before the user
+ * will actually publish it.
+ *
+ *
+ * Selectively disabling publish or share feature
+ * ==============================================
+ *
+ * If Sabre\CalDAV\Property\AllowedSharingModes is returned from
+ * getCalendarsForUser, this allows the server to specify whether either sharing,
+ * or publishing is supported.
+ *
+ * This allows a client to determine in advance which features are available,
+ * and update the interface appropriately. If this property is not returned by
+ * the backend, the SharingPlugin automatically injects it and assumes both
+ * features are available.
+ *
+ * @copyright Copyright (C) fruux GmbH (https://fruux.com/)
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+interface SharingSupport extends NotificationSupport {
+
+    /**
+     * Updates the list of shares.
+     *
+     * The first array is a list of people that are to be added to the
+     * calendar.
+     *
+     * Every element in the add array has the following properties:
+     *   * href - A url. Usually a mailto: address
+     *   * commonName - Usually a first and last name, or false
+     *   * summary - A description of the share, can also be false
+     *   * readOnly - A boolean value
+     *
+     * Every element in the remove array is just the address string.
+     *
+     * Note that if the calendar is currently marked as 'not shared' by and
+     * this method is called, the calendar should be 'upgraded' to a shared
+     * calendar.
+     *
+     * @param mixed $calendarId
+     * @param array $add
+     * @param array $remove
+     * @return void
+     */
+    function updateShares($calendarId, array $add, array $remove);
+
+    /**
+     * Returns the list of people whom this calendar is shared with.
+     *
+     * Every element in this array should have the following properties:
+     *   * href - Often a mailto: address
+     *   * commonName - Optional, for example a first + last name
+     *   * status - See the Sabre\CalDAV\SharingPlugin::STATUS_ constants.
+     *   * readOnly - boolean
+     *   * summary - Optional, a description for the share
+     *
+     * This method may be called by either the original instance of the
+     * calendar, as well as the shared instances. In the case of the shared
+     * instances, it is perfectly acceptable to return an empty array in case
+     * there are privacy concerns.
+     *
+     * @param mixed $calendarId
+     * @return array
+     */
+    function getShares($calendarId);
+
+    /**
+     * This method is called when a user replied to a request to share.
+     *
+     * If the user chose to accept the share, this method should return the
+     * newly created calendar url.
+     *
+     * @param string href The sharee who is replying (often a mailto: address)
+     * @param int status One of the SharingPlugin::STATUS_* constants
+     * @param string $calendarUri The url to the calendar thats being shared
+     * @param string $inReplyTo The unique id this message is a response to
+     * @param string $summary A description of the reply
+     * @return null|string
+     */
+    function shareReply($href, $status, $calendarUri, $inReplyTo, $summary = null);
+
+    /**
+     * Publishes a calendar
+     *
+     * @param mixed $calendarId
+     * @param bool $value
+     * @return void
+     */
+    function setPublishStatus($calendarId, $value);
+
+}

+ 89 - 0
lib/sabre/sabre/dav/lib/CalDAV/Backend/SubscriptionSupport.php

@@ -0,0 +1,89 @@
+<?php
+
+namespace Sabre\CalDAV\Backend;
+
+use Sabre\DAV;
+
+/**
+ * Every CalDAV backend must at least implement this interface.
+ *
+ * @copyright Copyright (C) fruux GmbH (https://fruux.com/)
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+interface SubscriptionSupport extends BackendInterface {
+
+    /**
+     * Returns a list of subscriptions for a principal.
+     *
+     * Every subscription is an array with the following keys:
+     *  * id, a unique id that will be used by other functions to modify the
+     *    subscription. This can be the same as the uri or a database key.
+     *  * uri. This is just the 'base uri' or 'filename' of the subscription.
+     *  * principaluri. The owner of the subscription. Almost always the same as
+     *    principalUri passed to this method.
+     *
+     * Furthermore, all the subscription info must be returned too:
+     *
+     * 1. {DAV:}displayname
+     * 2. {http://apple.com/ns/ical/}refreshrate
+     * 3. {http://calendarserver.org/ns/}subscribed-strip-todos (omit if todos
+     *    should not be stripped).
+     * 4. {http://calendarserver.org/ns/}subscribed-strip-alarms (omit if alarms
+     *    should not be stripped).
+     * 5. {http://calendarserver.org/ns/}subscribed-strip-attachments (omit if
+     *    attachments should not be stripped).
+     * 6. {http://calendarserver.org/ns/}source (Must be a
+     *     Sabre\DAV\Property\Href).
+     * 7. {http://apple.com/ns/ical/}calendar-color
+     * 8. {http://apple.com/ns/ical/}calendar-order
+     * 9. {urn:ietf:params:xml:ns:caldav}supported-calendar-component-set
+     *    (should just be an instance of
+     *    Sabre\CalDAV\Property\SupportedCalendarComponentSet, with a bunch of
+     *    default components).
+     *
+     * @param string $principalUri
+     * @return array
+     */
+    function getSubscriptionsForUser($principalUri);
+
+    /**
+     * Creates a new subscription for a principal.
+     *
+     * If the creation was a success, an id must be returned that can be used to reference
+     * this subscription in other methods, such as updateSubscription.
+     *
+     * @param string $principalUri
+     * @param string $uri
+     * @param array $properties
+     * @return mixed
+     */
+    function createSubscription($principalUri, $uri, array $properties);
+
+    /**
+     * Updates a subscription
+     *
+     * The list of mutations is stored in a Sabre\DAV\PropPatch object.
+     * To do the actual updates, you must tell this object which properties
+     * you're going to process with the handle() method.
+     *
+     * Calling the handle method is like telling the PropPatch object "I
+     * promise I can handle updating this property".
+     *
+     * Read the PropPatch documenation for more info and examples.
+     *
+     * @param mixed $subscriptionId
+     * @param \Sabre\DAV\PropPatch $propPatch
+     * @return void
+     */
+    function updateSubscription($subscriptionId, DAV\PropPatch $propPatch);
+
+    /**
+     * Deletes a subscription.
+     *
+     * @param mixed $subscriptionId
+     * @return void
+     */
+    function deleteSubscription($subscriptionId);
+
+}

+ 81 - 0
lib/sabre/sabre/dav/lib/CalDAV/Backend/SyncSupport.php

@@ -0,0 +1,81 @@
+<?php
+
+namespace Sabre\CalDAV\Backend;
+
+/**
+ * WebDAV-sync support for CalDAV backends.
+ *
+ * In order for backends to advertise support for WebDAV-sync, this interface
+ * must be implemented.
+ *
+ * Implementing this can result in a significant reduction of bandwidth and CPU
+ * time.
+ *
+ * For this to work, you _must_ return a {http://sabredav.org/ns}sync-token
+ * property from getCalendarsFromUser.
+ *
+ * @copyright Copyright (C) fruux GmbH (https://fruux.com/)
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+interface SyncSupport extends BackendInterface {
+
+    /**
+     * The getChanges method returns all the changes that have happened, since
+     * the specified syncToken in the specified calendar.
+     *
+     * This function should return an array, such as the following:
+     *
+     * [
+     *   'syncToken' => 'The current synctoken',
+     *   'added'   => [
+     *      'new.txt',
+     *   ],
+     *   'modified'   => [
+     *      'modified.txt',
+     *   ],
+     *   'deleted' => [
+     *      'foo.php.bak',
+     *      'old.txt'
+     *   ]
+     * );
+     *
+     * The returned syncToken property should reflect the *current* syncToken
+     * of the calendar, as reported in the {http://sabredav.org/ns}sync-token
+     * property This is * needed here too, to ensure the operation is atomic.
+     *
+     * If the $syncToken argument is specified as null, this is an initial
+     * sync, and all members should be reported.
+     *
+     * The modified property is an array of nodenames that have changed since
+     * the last token.
+     *
+     * The deleted property is an array with nodenames, that have been deleted
+     * from collection.
+     *
+     * The $syncLevel argument is basically the 'depth' of the report. If it's
+     * 1, you only have to report changes that happened only directly in
+     * immediate descendants. If it's 2, it should also include changes from
+     * the nodes below the child collections. (grandchildren)
+     *
+     * The $limit argument allows a client to specify how many results should
+     * be returned at most. If the limit is not specified, it should be treated
+     * as infinite.
+     *
+     * If the limit (infinite or not) is higher than you're willing to return,
+     * you should throw a Sabre\DAV\Exception\TooMuchMatches() exception.
+     *
+     * If the syncToken is expired (due to data cleanup) or unknown, you must
+     * return null.
+     *
+     * The limit is 'suggestive'. You are free to ignore it.
+     *
+     * @param string $calendarId
+     * @param string $syncToken
+     * @param int $syncLevel
+     * @param int $limit
+     * @return array
+     */
+    function getChangesForCalendar($calendarId, $syncToken, $syncLevel, $limit = null);
+
+}

+ 527 - 0
lib/sabre/sabre/dav/lib/CalDAV/Calendar.php

@@ -0,0 +1,527 @@
+<?php
+
+namespace Sabre\CalDAV;
+
+use Sabre\DAV;
+use Sabre\DAVACL;
+use Sabre\DAV\PropPatch;
+
+/**
+ * This object represents a CalDAV calendar.
+ *
+ * A calendar can contain multiple TODO and or Events. These are represented
+ * as \Sabre\CalDAV\CalendarObject objects.
+ *
+ * @copyright Copyright (C) fruux GmbH (https://fruux.com/)
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+class Calendar implements ICalendar, DAV\IProperties, DAV\Sync\ISyncCollection, DAV\IMultiGet {
+
+    /**
+     * This is an array with calendar information
+     *
+     * @var array
+     */
+    protected $calendarInfo;
+
+    /**
+     * CalDAV backend
+     *
+     * @var Backend\BackendInterface
+     */
+    protected $caldavBackend;
+
+    /**
+     * Constructor
+     *
+     * @param Backend\BackendInterface $caldavBackend
+     * @param array $calendarInfo
+     */
+    function __construct(Backend\BackendInterface $caldavBackend, $calendarInfo) {
+
+        $this->caldavBackend = $caldavBackend;
+        $this->calendarInfo = $calendarInfo;
+
+    }
+
+    /**
+     * Returns the name of the calendar
+     *
+     * @return string
+     */
+    function getName() {
+
+        return $this->calendarInfo['uri'];
+
+    }
+
+    /**
+     * Updates properties on this node.
+     *
+     * This method received a PropPatch object, which contains all the
+     * information about the update.
+     *
+     * To update specific properties, call the 'handle' method on this object.
+     * Read the PropPatch documentation for more information.
+     *
+     * @param PropPatch $propPatch
+     * @return void
+     */
+    function propPatch(PropPatch $propPatch) {
+
+        return $this->caldavBackend->updateCalendar($this->calendarInfo['id'], $propPatch);
+
+    }
+
+    /**
+     * Returns the list of properties
+     *
+     * @param array $requestedProperties
+     * @return array
+     */
+    function getProperties($requestedProperties) {
+
+        $response = [];
+
+        foreach ($this->calendarInfo as $propName => $propValue) {
+
+            if ($propName[0] === '{')
+                $response[$propName] = $this->calendarInfo[$propName];
+
+        }
+        return $response;
+
+    }
+
+    /**
+     * Returns a calendar object
+     *
+     * The contained calendar objects are for example Events or Todo's.
+     *
+     * @param string $name
+     * @return \Sabre\CalDAV\ICalendarObject
+     */
+    function getChild($name) {
+
+        $obj = $this->caldavBackend->getCalendarObject($this->calendarInfo['id'], $name);
+
+        if (!$obj) throw new DAV\Exception\NotFound('Calendar object not found');
+
+        $obj['acl'] = $this->getChildACL();
+
+        return new CalendarObject($this->caldavBackend, $this->calendarInfo, $obj);
+
+    }
+
+    /**
+     * Returns the full list of calendar objects
+     *
+     * @return array
+     */
+    function getChildren() {
+
+        $objs = $this->caldavBackend->getCalendarObjects($this->calendarInfo['id']);
+        $children = [];
+        foreach ($objs as $obj) {
+            $obj['acl'] = $this->getChildACL();
+            $children[] = new CalendarObject($this->caldavBackend, $this->calendarInfo, $obj);
+        }
+        return $children;
+
+    }
+
+    /**
+     * This method receives a list of paths in it's first argument.
+     * It must return an array with Node objects.
+     *
+     * If any children are not found, you do not have to return them.
+     *
+     * @param string[] $paths
+     * @return array
+     */
+    function getMultipleChildren(array $paths) {
+
+        $objs = $this->caldavBackend->getMultipleCalendarObjects($this->calendarInfo['id'], $paths);
+        $children = [];
+        foreach ($objs as $obj) {
+            $obj['acl'] = $this->getChildACL();
+            $children[] = new CalendarObject($this->caldavBackend, $this->calendarInfo, $obj);
+        }
+        return $children;
+
+    }
+
+    /**
+     * Checks if a child-node exists.
+     *
+     * @param string $name
+     * @return bool
+     */
+    function childExists($name) {
+
+        $obj = $this->caldavBackend->getCalendarObject($this->calendarInfo['id'], $name);
+        if (!$obj)
+            return false;
+        else
+            return true;
+
+    }
+
+    /**
+     * Creates a new directory
+     *
+     * We actually block this, as subdirectories are not allowed in calendars.
+     *
+     * @param string $name
+     * @return void
+     */
+    function createDirectory($name) {
+
+        throw new DAV\Exception\MethodNotAllowed('Creating collections in calendar objects is not allowed');
+
+    }
+
+    /**
+     * Creates a new file
+     *
+     * The contents of the new file must be a valid ICalendar string.
+     *
+     * @param string $name
+     * @param resource $calendarData
+     * @return string|null
+     */
+    function createFile($name, $calendarData = null) {
+
+        if (is_resource($calendarData)) {
+            $calendarData = stream_get_contents($calendarData);
+        }
+        return $this->caldavBackend->createCalendarObject($this->calendarInfo['id'], $name, $calendarData);
+
+    }
+
+    /**
+     * Deletes the calendar.
+     *
+     * @return void
+     */
+    function delete() {
+
+        $this->caldavBackend->deleteCalendar($this->calendarInfo['id']);
+
+    }
+
+    /**
+     * Renames the calendar. Note that most calendars use the
+     * {DAV:}displayname to display a name to display a name.
+     *
+     * @param string $newName
+     * @return void
+     */
+    function setName($newName) {
+
+        throw new DAV\Exception\MethodNotAllowed('Renaming calendars is not yet supported');
+
+    }
+
+    /**
+     * Returns the last modification date as a unix timestamp.
+     *
+     * @return void
+     */
+    function getLastModified() {
+
+        return null;
+
+    }
+
+    /**
+     * Returns the owner principal
+     *
+     * This must be a url to a principal, or null if there's no owner
+     *
+     * @return string|null
+     */
+    function getOwner() {
+
+        return $this->calendarInfo['principaluri'];
+
+    }
+
+    /**
+     * Returns a group principal
+     *
+     * This must be a url to a principal, or null if there's no owner
+     *
+     * @return string|null
+     */
+    function getGroup() {
+
+        return null;
+
+    }
+
+    /**
+     * Returns a list of ACE's for this node.
+     *
+     * Each ACE has the following properties:
+     *   * 'privilege', a string such as {DAV:}read or {DAV:}write. These are
+     *     currently the only supported privileges
+     *   * 'principal', a url to the principal who owns the node
+     *   * 'protected' (optional), indicating that this ACE is not allowed to
+     *      be updated.
+     *
+     * @return array
+     */
+    function getACL() {
+
+        $acl = [
+            [
+                'privilege' => '{DAV:}read',
+                'principal' => $this->getOwner(),
+                'protected' => true,
+            ],
+            [
+                'privilege' => '{DAV:}read',
+                'principal' => $this->getOwner() . '/calendar-proxy-write',
+                'protected' => true,
+            ],
+            [
+                'privilege' => '{DAV:}read',
+                'principal' => $this->getOwner() . '/calendar-proxy-read',
+                'protected' => true,
+            ],
+            [
+                'privilege' => '{' . Plugin::NS_CALDAV . '}read-free-busy',
+                'principal' => '{DAV:}authenticated',
+                'protected' => true,
+            ],
+
+        ];
+        if (empty($this->calendarInfo['{http://sabredav.org/ns}read-only'])) {
+            $acl[] = [
+                'privilege' => '{DAV:}write',
+                'principal' => $this->getOwner(),
+                'protected' => true,
+            ];
+            $acl[] = [
+                'privilege' => '{DAV:}write',
+                'principal' => $this->getOwner() . '/calendar-proxy-write',
+                'protected' => true,
+            ];
+        }
+
+        return $acl;
+
+    }
+
+    /**
+     * This method returns the ACL's for calendar objects in this calendar.
+     * The result of this method automatically gets passed to the
+     * calendar-object nodes in the calendar.
+     *
+     * @return array
+     */
+    function getChildACL() {
+
+        $acl = [
+            [
+                'privilege' => '{DAV:}read',
+                'principal' => $this->getOwner(),
+                'protected' => true,
+            ],
+
+            [
+                'privilege' => '{DAV:}read',
+                'principal' => $this->getOwner() . '/calendar-proxy-write',
+                'protected' => true,
+            ],
+            [
+                'privilege' => '{DAV:}read',
+                'principal' => $this->getOwner() . '/calendar-proxy-read',
+                'protected' => true,
+            ],
+
+        ];
+        if (empty($this->calendarInfo['{http://sabredav.org/ns}read-only'])) {
+            $acl[] = [
+                'privilege' => '{DAV:}write',
+                'principal' => $this->getOwner(),
+                'protected' => true,
+            ];
+            $acl[] = [
+                'privilege' => '{DAV:}write',
+                'principal' => $this->getOwner() . '/calendar-proxy-write',
+                'protected' => true,
+            ];
+
+        }
+        return $acl;
+
+    }
+
+    /**
+     * Updates the ACL
+     *
+     * This method will receive a list of new ACE's.
+     *
+     * @param array $acl
+     * @return void
+     */
+    function setACL(array $acl) {
+
+        throw new DAV\Exception\MethodNotAllowed('Changing ACL is not yet supported');
+
+    }
+
+    /**
+     * Returns the list of supported privileges for this node.
+     *
+     * The returned data structure is a list of nested privileges.
+     * See \Sabre\DAVACL\Plugin::getDefaultSupportedPrivilegeSet for a simple
+     * standard structure.
+     *
+     * If null is returned from this method, the default privilege set is used,
+     * which is fine for most common usecases.
+     *
+     * @return array|null
+     */
+    function getSupportedPrivilegeSet() {
+
+        $default = DAVACL\Plugin::getDefaultSupportedPrivilegeSet();
+
+        // We need to inject 'read-free-busy' in the tree, aggregated under
+        // {DAV:}read.
+        foreach ($default['aggregates'] as &$agg) {
+
+            if ($agg['privilege'] !== '{DAV:}read') continue;
+
+            $agg['aggregates'][] = [
+                'privilege' => '{' . Plugin::NS_CALDAV . '}read-free-busy',
+            ];
+
+        }
+        return $default;
+
+    }
+
+    /**
+     * Performs a calendar-query on the contents of this calendar.
+     *
+     * The calendar-query is defined in RFC4791 : CalDAV. Using the
+     * calendar-query it is possible for a client to request a specific set of
+     * object, based on contents of iCalendar properties, date-ranges and
+     * iCalendar component types (VTODO, VEVENT).
+     *
+     * This method should just return a list of (relative) urls that match this
+     * query.
+     *
+     * The list of filters are specified as an array. The exact array is
+     * documented by Sabre\CalDAV\CalendarQueryParser.
+     *
+     * @param array $filters
+     * @return array
+     */
+    function calendarQuery(array $filters) {
+
+        return $this->caldavBackend->calendarQuery($this->calendarInfo['id'], $filters);
+
+    }
+
+    /**
+     * This method returns the current sync-token for this collection.
+     * This can be any string.
+     *
+     * If null is returned from this function, the plugin assumes there's no
+     * sync information available.
+     *
+     * @return string|null
+     */
+    function getSyncToken() {
+
+        if (
+            $this->caldavBackend instanceof Backend\SyncSupport &&
+            isset($this->calendarInfo['{DAV:}sync-token'])
+        ) {
+            return $this->calendarInfo['{DAV:}sync-token'];
+        }
+        if (
+            $this->caldavBackend instanceof Backend\SyncSupport &&
+            isset($this->calendarInfo['{http://sabredav.org/ns}sync-token'])
+        ) {
+            return $this->calendarInfo['{http://sabredav.org/ns}sync-token'];
+        }
+
+    }
+
+    /**
+     * The getChanges method returns all the changes that have happened, since
+     * the specified syncToken and the current collection.
+     *
+     * This function should return an array, such as the following:
+     *
+     * [
+     *   'syncToken' => 'The current synctoken',
+     *   'added'   => [
+     *      'new.txt',
+     *   ],
+     *   'modified'   => [
+     *      'modified.txt',
+     *   ],
+     *   'deleted' => [
+     *      'foo.php.bak',
+     *      'old.txt'
+     *   ]
+     * ];
+     *
+     * The syncToken property should reflect the *current* syncToken of the
+     * collection, as reported getSyncToken(). This is needed here too, to
+     * ensure the operation is atomic.
+     *
+     * If the syncToken is specified as null, this is an initial sync, and all
+     * members should be reported.
+     *
+     * The modified property is an array of nodenames that have changed since
+     * the last token.
+     *
+     * The deleted property is an array with nodenames, that have been deleted
+     * from collection.
+     *
+     * The second argument is basically the 'depth' of the report. If it's 1,
+     * you only have to report changes that happened only directly in immediate
+     * descendants. If it's 2, it should also include changes from the nodes
+     * below the child collections. (grandchildren)
+     *
+     * The third (optional) argument allows a client to specify how many
+     * results should be returned at most. If the limit is not specified, it
+     * should be treated as infinite.
+     *
+     * If the limit (infinite or not) is higher than you're willing to return,
+     * you should throw a Sabre\DAV\Exception\TooMuchMatches() exception.
+     *
+     * If the syncToken is expired (due to data cleanup) or unknown, you must
+     * return null.
+     *
+     * The limit is 'suggestive'. You are free to ignore it.
+     *
+     * @param string $syncToken
+     * @param int $syncLevel
+     * @param int $limit
+     * @return array
+     */
+    function getChanges($syncToken, $syncLevel, $limit = null) {
+
+        if (!$this->caldavBackend instanceof Backend\SyncSupport) {
+            return null;
+        }
+
+        return $this->caldavBackend->getChangesForCalendar(
+            $this->calendarInfo['id'],
+            $syncToken,
+            $syncLevel,
+            $limit
+        );
+
+    }
+
+}

+ 430 - 0
lib/sabre/sabre/dav/lib/CalDAV/CalendarHome.php

@@ -0,0 +1,430 @@
+<?php
+
+namespace Sabre\CalDAV;
+
+use Sabre\DAV;
+use Sabre\DAV\Exception\NotFound;
+use Sabre\DAV\MkCol;
+use Sabre\DAVACL;
+use Sabre\HTTP\URLUtil;
+
+/**
+ * The CalendarHome represents a node that is usually in a users'
+ * calendar-homeset.
+ *
+ * It contains all the users' calendars, and can optionally contain a
+ * notifications collection, calendar subscriptions, a users' inbox, and a
+ * users' outbox.
+ *
+ * @copyright Copyright (C) fruux GmbH (https://fruux.com/)
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+class CalendarHome implements DAV\IExtendedCollection, DAVACL\IACL {
+
+    /**
+     * CalDAV backend
+     *
+     * @var Sabre\CalDAV\Backend\BackendInterface
+     */
+    protected $caldavBackend;
+
+    /**
+     * Principal information
+     *
+     * @var array
+     */
+    protected $principalInfo;
+
+    /**
+     * Constructor
+     *
+     * @param Backend\BackendInterface $caldavBackend
+     * @param mixed $userUri
+     */
+    function __construct(Backend\BackendInterface $caldavBackend, $principalInfo) {
+
+        $this->caldavBackend = $caldavBackend;
+        $this->principalInfo = $principalInfo;
+
+    }
+
+    /**
+     * Returns the name of this object
+     *
+     * @return string
+     */
+    function getName() {
+
+        list(, $name) = URLUtil::splitPath($this->principalInfo['uri']);
+        return $name;
+
+    }
+
+    /**
+     * Updates the name of this object
+     *
+     * @param string $name
+     * @return void
+     */
+    function setName($name) {
+
+        throw new DAV\Exception\Forbidden();
+
+    }
+
+    /**
+     * Deletes this object
+     *
+     * @return void
+     */
+    function delete() {
+
+        throw new DAV\Exception\Forbidden();
+
+    }
+
+    /**
+     * Returns the last modification date
+     *
+     * @return int
+     */
+    function getLastModified() {
+
+        return null;
+
+    }
+
+    /**
+     * Creates a new file under this object.
+     *
+     * This is currently not allowed
+     *
+     * @param string $filename
+     * @param resource $data
+     * @return void
+     */
+    function createFile($filename, $data = null) {
+
+        throw new DAV\Exception\MethodNotAllowed('Creating new files in this collection is not supported');
+
+    }
+
+    /**
+     * Creates a new directory under this object.
+     *
+     * This is currently not allowed.
+     *
+     * @param string $filename
+     * @return void
+     */
+    function createDirectory($filename) {
+
+        throw new DAV\Exception\MethodNotAllowed('Creating new collections in this collection is not supported');
+
+    }
+
+    /**
+     * Returns a single calendar, by name
+     *
+     * @param string $name
+     * @return Calendar
+     */
+    function getChild($name) {
+
+        // Special nodes
+        if ($name === 'inbox' && $this->caldavBackend instanceof Backend\SchedulingSupport) {
+            return new Schedule\Inbox($this->caldavBackend, $this->principalInfo['uri']);
+        }
+        if ($name === 'outbox' && $this->caldavBackend instanceof Backend\SchedulingSupport) {
+            return new Schedule\Outbox($this->principalInfo['uri']);
+        }
+        if ($name === 'notifications' && $this->caldavBackend instanceof Backend\NotificationSupport) {
+            return new Notifications\Collection($this->caldavBackend, $this->principalInfo['uri']);
+        }
+
+        // Calendars
+        foreach ($this->caldavBackend->getCalendarsForUser($this->principalInfo['uri']) as $calendar) {
+            if ($calendar['uri'] === $name) {
+                if ($this->caldavBackend instanceof Backend\SharingSupport) {
+                    if (isset($calendar['{http://calendarserver.org/ns/}shared-url'])) {
+                        return new SharedCalendar($this->caldavBackend, $calendar);
+                    } else {
+                        return new ShareableCalendar($this->caldavBackend, $calendar);
+                    }
+                } else {
+                    return new Calendar($this->caldavBackend, $calendar);
+                }
+            }
+        }
+
+        if ($this->caldavBackend instanceof Backend\SubscriptionSupport) {
+            foreach ($this->caldavBackend->getSubscriptionsForUser($this->principalInfo['uri']) as $subscription) {
+                if ($subscription['uri'] === $name) {
+                    return new Subscriptions\Subscription($this->caldavBackend, $subscription);
+                }
+            }
+
+        }
+
+        throw new NotFound('Node with name \'' . $name . '\' could not be found');
+
+    }
+
+    /**
+     * Checks if a calendar exists.
+     *
+     * @param string $name
+     * @return bool
+     */
+    function childExists($name) {
+
+        try {
+            return !!$this->getChild($name);
+        } catch (NotFound $e) {
+            return false;
+        }
+
+    }
+
+    /**
+     * Returns a list of calendars
+     *
+     * @return array
+     */
+    function getChildren() {
+
+        $calendars = $this->caldavBackend->getCalendarsForUser($this->principalInfo['uri']);
+        $objs = [];
+        foreach ($calendars as $calendar) {
+            if ($this->caldavBackend instanceof Backend\SharingSupport) {
+                if (isset($calendar['{http://calendarserver.org/ns/}shared-url'])) {
+                    $objs[] = new SharedCalendar($this->caldavBackend, $calendar);
+                } else {
+                    $objs[] = new ShareableCalendar($this->caldavBackend, $calendar);
+                }
+            } else {
+                $objs[] = new Calendar($this->caldavBackend, $calendar);
+            }
+        }
+
+        if ($this->caldavBackend instanceof Backend\SchedulingSupport) {
+            $objs[] = new Schedule\Inbox($this->caldavBackend, $this->principalInfo['uri']);
+            $objs[] = new Schedule\Outbox($this->principalInfo['uri']);
+        }
+
+        // We're adding a notifications node, if it's supported by the backend.
+        if ($this->caldavBackend instanceof Backend\NotificationSupport) {
+            $objs[] = new Notifications\Collection($this->caldavBackend, $this->principalInfo['uri']);
+        }
+
+        // If the backend supports subscriptions, we'll add those as well,
+        if ($this->caldavBackend instanceof Backend\SubscriptionSupport) {
+            foreach ($this->caldavBackend->getSubscriptionsForUser($this->principalInfo['uri']) as $subscription) {
+                $objs[] = new Subscriptions\Subscription($this->caldavBackend, $subscription);
+            }
+        }
+
+        return $objs;
+
+    }
+
+    /**
+     * Creates a new calendar or subscription.
+     *
+     * @param string $name
+     * @param MkCol $mkCol
+     * @throws DAV\Exception\InvalidResourceType
+     * @return void
+     */
+    function createExtendedCollection($name, MkCol $mkCol) {
+
+        $isCalendar = false;
+        $isSubscription = false;
+        foreach ($mkCol->getResourceType() as $rt) {
+            switch ($rt) {
+                case '{DAV:}collection' :
+                case '{http://calendarserver.org/ns/}shared-owner' :
+                    // ignore
+                    break;
+                case '{urn:ietf:params:xml:ns:caldav}calendar' :
+                    $isCalendar = true;
+                    break;
+                case '{http://calendarserver.org/ns/}subscribed' :
+                    $isSubscription = true;
+                    break;
+                default :
+                    throw new DAV\Exception\InvalidResourceType('Unknown resourceType: ' . $rt);
+            }
+        }
+
+        $properties = $mkCol->getRemainingValues();
+        $mkCol->setRemainingResultCode(201);
+
+        if ($isSubscription) {
+            if (!$this->caldavBackend instanceof Backend\SubscriptionSupport) {
+                throw new DAV\Exception\InvalidResourceType('This backend does not support subscriptions');
+            }
+            $this->caldavBackend->createSubscription($this->principalInfo['uri'], $name, $properties);
+
+        } elseif ($isCalendar) {
+            $this->caldavBackend->createCalendar($this->principalInfo['uri'], $name, $properties);
+
+        } else {
+            throw new DAV\Exception\InvalidResourceType('You can only create calendars and subscriptions in this collection');
+
+        }
+
+    }
+
+    /**
+     * Returns the owner principal
+     *
+     * This must be a url to a principal, or null if there's no owner
+     *
+     * @return string|null
+     */
+    function getOwner() {
+
+        return $this->principalInfo['uri'];
+
+    }
+
+    /**
+     * Returns a group principal
+     *
+     * This must be a url to a principal, or null if there's no owner
+     *
+     * @return string|null
+     */
+    function getGroup() {
+
+        return null;
+
+    }
+
+    /**
+     * Returns a list of ACE's for this node.
+     *
+     * Each ACE has the following properties:
+     *   * 'privilege', a string such as {DAV:}read or {DAV:}write. These are
+     *     currently the only supported privileges
+     *   * 'principal', a url to the principal who owns the node
+     *   * 'protected' (optional), indicating that this ACE is not allowed to
+     *      be updated.
+     *
+     * @return array
+     */
+    function getACL() {
+
+        return [
+            [
+                'privilege' => '{DAV:}read',
+                'principal' => $this->principalInfo['uri'],
+                'protected' => true,
+            ],
+            [
+                'privilege' => '{DAV:}write',
+                'principal' => $this->principalInfo['uri'],
+                'protected' => true,
+            ],
+            [
+                'privilege' => '{DAV:}read',
+                'principal' => $this->principalInfo['uri'] . '/calendar-proxy-write',
+                'protected' => true,
+            ],
+            [
+                'privilege' => '{DAV:}write',
+                'principal' => $this->principalInfo['uri'] . '/calendar-proxy-write',
+                'protected' => true,
+            ],
+            [
+                'privilege' => '{DAV:}read',
+                'principal' => $this->principalInfo['uri'] . '/calendar-proxy-read',
+                'protected' => true,
+            ],
+
+        ];
+
+    }
+
+    /**
+     * Updates the ACL
+     *
+     * This method will receive a list of new ACE's.
+     *
+     * @param array $acl
+     * @return void
+     */
+    function setACL(array $acl) {
+
+        throw new DAV\Exception\MethodNotAllowed('Changing ACL is not yet supported');
+
+    }
+
+    /**
+     * Returns the list of supported privileges for this node.
+     *
+     * The returned data structure is a list of nested privileges.
+     * See Sabre\DAVACL\Plugin::getDefaultSupportedPrivilegeSet for a simple
+     * standard structure.
+     *
+     * If null is returned from this method, the default privilege set is used,
+     * which is fine for most common usecases.
+     *
+     * @return array|null
+     */
+    function getSupportedPrivilegeSet() {
+
+        return null;
+
+    }
+
+    /**
+     * This method is called when a user replied to a request to share.
+     *
+     * This method should return the url of the newly created calendar if the
+     * share was accepted.
+     *
+     * @param string href The sharee who is replying (often a mailto: address)
+     * @param int status One of the SharingPlugin::STATUS_* constants
+     * @param string $calendarUri The url to the calendar thats being shared
+     * @param string $inReplyTo The unique id this message is a response to
+     * @param string $summary A description of the reply
+     * @return null|string
+     */
+    function shareReply($href, $status, $calendarUri, $inReplyTo, $summary = null) {
+
+        if (!$this->caldavBackend instanceof Backend\SharingSupport) {
+            throw new DAV\Exception\NotImplemented('Sharing support is not implemented by this backend.');
+        }
+
+        return $this->caldavBackend->shareReply($href, $status, $calendarUri, $inReplyTo, $summary);
+
+    }
+
+    /**
+     * Searches through all of a users calendars and calendar objects to find
+     * an object with a specific UID.
+     *
+     * This method should return the path to this object, relative to the
+     * calendar home, so this path usually only contains two parts:
+     *
+     * calendarpath/objectpath.ics
+     *
+     * If the uid is not found, return null.
+     *
+     * This method should only consider * objects that the principal owns, so
+     * any calendars owned by other principals that also appear in this
+     * collection should be ignored.
+     *
+     * @param string $uid
+     * @return string|null
+     */
+    function getCalendarObjectByUID($uid) {
+
+        return $this->caldavBackend->getCalendarObjectByUID($this->principalInfo['uri'], $uid);
+
+    }
+
+}

+ 290 - 0
lib/sabre/sabre/dav/lib/CalDAV/CalendarObject.php

@@ -0,0 +1,290 @@
+<?php
+
+namespace Sabre\CalDAV;
+
+/**
+ * The CalendarObject represents a single VEVENT or VTODO within a Calendar.
+ *
+ * @copyright Copyright (C) fruux GmbH (https://fruux.com/)
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+class CalendarObject extends \Sabre\DAV\File implements ICalendarObject, \Sabre\DAVACL\IACL {
+
+    /**
+     * Sabre\CalDAV\Backend\BackendInterface
+     *
+     * @var Sabre\CalDAV\Backend\AbstractBackend
+     */
+    protected $caldavBackend;
+
+    /**
+     * Array with information about this CalendarObject
+     *
+     * @var array
+     */
+    protected $objectData;
+
+    /**
+     * Array with information about the containing calendar
+     *
+     * @var array
+     */
+    protected $calendarInfo;
+
+    /**
+     * Constructor
+     *
+     * The following properties may be passed within $objectData:
+     *
+     *   * calendarid - This must refer to a calendarid from a caldavBackend
+     *   * uri - A unique uri. Only the 'basename' must be passed.
+     *   * calendardata (optional) - The iCalendar data
+     *   * etag - (optional) The etag for this object, MUST be encloded with
+     *            double-quotes.
+     *   * size - (optional) The size of the data in bytes.
+     *   * lastmodified - (optional) format as a unix timestamp.
+     *   * acl - (optional) Use this to override the default ACL for the node.
+     *
+     * @param Backend\BackendInterface $caldavBackend
+     * @param array $calendarInfo
+     * @param array $objectData
+     */
+    function __construct(Backend\BackendInterface $caldavBackend, array $calendarInfo, array $objectData) {
+
+        $this->caldavBackend = $caldavBackend;
+
+        if (!isset($objectData['uri'])) {
+            throw new \InvalidArgumentException('The objectData argument must contain an \'uri\' property');
+        }
+
+        $this->calendarInfo = $calendarInfo;
+        $this->objectData = $objectData;
+
+    }
+
+    /**
+     * Returns the uri for this object
+     *
+     * @return string
+     */
+    function getName() {
+
+        return $this->objectData['uri'];
+
+    }
+
+    /**
+     * Returns the ICalendar-formatted object
+     *
+     * @return string
+     */
+    function get() {
+
+        // Pre-populating the 'calendardata' is optional, if we don't have it
+        // already we fetch it from the backend.
+        if (!isset($this->objectData['calendardata'])) {
+            $this->objectData = $this->caldavBackend->getCalendarObject($this->calendarInfo['id'], $this->objectData['uri']);
+        }
+        return $this->objectData['calendardata'];
+
+    }
+
+    /**
+     * Updates the ICalendar-formatted object
+     *
+     * @param string|resource $calendarData
+     * @return string
+     */
+    function put($calendarData) {
+
+        if (is_resource($calendarData)) {
+            $calendarData = stream_get_contents($calendarData);
+        }
+        $etag = $this->caldavBackend->updateCalendarObject($this->calendarInfo['id'], $this->objectData['uri'], $calendarData);
+        $this->objectData['calendardata'] = $calendarData;
+        $this->objectData['etag'] = $etag;
+
+        return $etag;
+
+    }
+
+    /**
+     * Deletes the calendar object
+     *
+     * @return void
+     */
+    function delete() {
+
+        $this->caldavBackend->deleteCalendarObject($this->calendarInfo['id'], $this->objectData['uri']);
+
+    }
+
+    /**
+     * Returns the mime content-type
+     *
+     * @return string
+     */
+    function getContentType() {
+
+        $mime = 'text/calendar; charset=utf-8';
+        if (isset($this->objectData['component']) && $this->objectData['component']) {
+            $mime .= '; component=' . $this->objectData['component'];
+        }
+        return $mime;
+
+    }
+
+    /**
+     * Returns an ETag for this object.
+     *
+     * The ETag is an arbitrary string, but MUST be surrounded by double-quotes.
+     *
+     * @return string
+     */
+    function getETag() {
+
+        if (isset($this->objectData['etag'])) {
+            return $this->objectData['etag'];
+        } else {
+            return '"' . md5($this->get()) . '"';
+        }
+
+    }
+
+    /**
+     * Returns the last modification date as a unix timestamp
+     *
+     * @return int
+     */
+    function getLastModified() {
+
+        return $this->objectData['lastmodified'];
+
+    }
+
+    /**
+     * Returns the size of this object in bytes
+     *
+     * @return int
+     */
+    function getSize() {
+
+        if (array_key_exists('size', $this->objectData)) {
+            return $this->objectData['size'];
+        } else {
+            return strlen($this->get());
+        }
+
+    }
+
+    /**
+     * Returns the owner principal
+     *
+     * This must be a url to a principal, or null if there's no owner
+     *
+     * @return string|null
+     */
+    function getOwner() {
+
+        return $this->calendarInfo['principaluri'];
+
+    }
+
+    /**
+     * Returns a group principal
+     *
+     * This must be a url to a principal, or null if there's no owner
+     *
+     * @return string|null
+     */
+    function getGroup() {
+
+        return null;
+
+    }
+
+    /**
+     * Returns a list of ACE's for this node.
+     *
+     * Each ACE has the following properties:
+     *   * 'privilege', a string such as {DAV:}read or {DAV:}write. These are
+     *     currently the only supported privileges
+     *   * 'principal', a url to the principal who owns the node
+     *   * 'protected' (optional), indicating that this ACE is not allowed to
+     *      be updated.
+     *
+     * @return array
+     */
+    function getACL() {
+
+        // An alternative acl may be specified in the object data.
+        if (isset($this->objectData['acl'])) {
+            return $this->objectData['acl'];
+        }
+
+        // The default ACL
+        return [
+            [
+                'privilege' => '{DAV:}read',
+                'principal' => $this->calendarInfo['principaluri'],
+                'protected' => true,
+            ],
+            [
+                'privilege' => '{DAV:}write',
+                'principal' => $this->calendarInfo['principaluri'],
+                'protected' => true,
+            ],
+            [
+                'privilege' => '{DAV:}read',
+                'principal' => $this->calendarInfo['principaluri'] . '/calendar-proxy-write',
+                'protected' => true,
+            ],
+            [
+                'privilege' => '{DAV:}write',
+                'principal' => $this->calendarInfo['principaluri'] . '/calendar-proxy-write',
+                'protected' => true,
+            ],
+            [
+                'privilege' => '{DAV:}read',
+                'principal' => $this->calendarInfo['principaluri'] . '/calendar-proxy-read',
+                'protected' => true,
+            ],
+
+        ];
+
+    }
+
+    /**
+     * Updates the ACL
+     *
+     * This method will receive a list of new ACE's.
+     *
+     * @param array $acl
+     * @return void
+     */
+    function setACL(array $acl) {
+
+        throw new \Sabre\DAV\Exception\MethodNotAllowed('Changing ACL is not yet supported');
+
+    }
+
+    /**
+     * Returns the list of supported privileges for this node.
+     *
+     * The returned data structure is a list of nested privileges.
+     * See \Sabre\DAVACL\Plugin::getDefaultSupportedPrivilegeSet for a simple
+     * standard structure.
+     *
+     * If null is returned from this method, the default privilege set is used,
+     * which is fine for most common usecases.
+     *
+     * @return array|null
+     */
+    function getSupportedPrivilegeSet() {
+
+        return null;
+
+    }
+
+}

+ 375 - 0
lib/sabre/sabre/dav/lib/CalDAV/CalendarQueryValidator.php

@@ -0,0 +1,375 @@
+<?php
+
+namespace Sabre\CalDAV;
+
+use Sabre\VObject;
+use DateTime;
+
+/**
+ * CalendarQuery Validator
+ *
+ * This class is responsible for checking if an iCalendar object matches a set
+ * of filters. The main function to do this is 'validate'.
+ *
+ * This is used to determine which icalendar objects should be returned for a
+ * calendar-query REPORT request.
+ *
+ * @copyright Copyright (C) fruux GmbH (https://fruux.com/)
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+class CalendarQueryValidator {
+
+    /**
+     * Verify if a list of filters applies to the calendar data object
+     *
+     * The list of filters must be formatted as parsed by \Sabre\CalDAV\CalendarQueryParser
+     *
+     * @param VObject\Component $vObject
+     * @param array $filters
+     * @return bool
+     */
+    function validate(VObject\Component\VCalendar $vObject, array $filters) {
+
+        // The top level object is always a component filter.
+        // We'll parse it manually, as it's pretty simple.
+        if ($vObject->name !== $filters['name']) {
+            return false;
+        }
+
+        return
+            $this->validateCompFilters($vObject, $filters['comp-filters']) &&
+            $this->validatePropFilters($vObject, $filters['prop-filters']);
+
+
+    }
+
+    /**
+     * This method checks the validity of comp-filters.
+     *
+     * A list of comp-filters needs to be specified. Also the parent of the
+     * component we're checking should be specified, not the component to check
+     * itself.
+     *
+     * @param VObject\Component $parent
+     * @param array $filters
+     * @return bool
+     */
+    protected function validateCompFilters(VObject\Component $parent, array $filters) {
+
+        foreach ($filters as $filter) {
+
+            $isDefined = isset($parent->{$filter['name']});
+
+            if ($filter['is-not-defined']) {
+
+                if ($isDefined) {
+                    return false;
+                } else {
+                    continue;
+                }
+
+            }
+            if (!$isDefined) {
+                return false;
+            }
+
+            if ($filter['time-range']) {
+                foreach ($parent->{$filter['name']} as $subComponent) {
+                    if ($this->validateTimeRange($subComponent, $filter['time-range']['start'], $filter['time-range']['end'])) {
+                        continue 2;
+                    }
+                }
+                return false;
+            }
+
+            if (!$filter['comp-filters'] && !$filter['prop-filters']) {
+                continue;
+            }
+
+            // If there are sub-filters, we need to find at least one component
+            // for which the subfilters hold true.
+            foreach ($parent->{$filter['name']} as $subComponent) {
+
+                if (
+                    $this->validateCompFilters($subComponent, $filter['comp-filters']) &&
+                    $this->validatePropFilters($subComponent, $filter['prop-filters'])) {
+                        // We had a match, so this comp-filter succeeds
+                        continue 2;
+                }
+
+            }
+
+            // If we got here it means there were sub-comp-filters or
+            // sub-prop-filters and there was no match. This means this filter
+            // needs to return false.
+            return false;
+
+        }
+
+        // If we got here it means we got through all comp-filters alive so the
+        // filters were all true.
+        return true;
+
+    }
+
+    /**
+     * This method checks the validity of prop-filters.
+     *
+     * A list of prop-filters needs to be specified. Also the parent of the
+     * property we're checking should be specified, not the property to check
+     * itself.
+     *
+     * @param VObject\Component $parent
+     * @param array $filters
+     * @return bool
+     */
+    protected function validatePropFilters(VObject\Component $parent, array $filters) {
+
+        foreach ($filters as $filter) {
+
+            $isDefined = isset($parent->{$filter['name']});
+
+            if ($filter['is-not-defined']) {
+
+                if ($isDefined) {
+                    return false;
+                } else {
+                    continue;
+                }
+
+            }
+            if (!$isDefined) {
+                return false;
+            }
+
+            if ($filter['time-range']) {
+                foreach ($parent->{$filter['name']} as $subComponent) {
+                    if ($this->validateTimeRange($subComponent, $filter['time-range']['start'], $filter['time-range']['end'])) {
+                        continue 2;
+                    }
+                }
+                return false;
+            }
+
+            if (!$filter['param-filters'] && !$filter['text-match']) {
+                continue;
+            }
+
+            // If there are sub-filters, we need to find at least one property
+            // for which the subfilters hold true.
+            foreach ($parent->{$filter['name']} as $subComponent) {
+
+                if (
+                    $this->validateParamFilters($subComponent, $filter['param-filters']) &&
+                    (!$filter['text-match'] || $this->validateTextMatch($subComponent, $filter['text-match']))
+                ) {
+                    // We had a match, so this prop-filter succeeds
+                    continue 2;
+                }
+
+            }
+
+            // If we got here it means there were sub-param-filters or
+            // text-match filters and there was no match. This means the
+            // filter needs to return false.
+            return false;
+
+        }
+
+        // If we got here it means we got through all prop-filters alive so the
+        // filters were all true.
+        return true;
+
+    }
+
+    /**
+     * This method checks the validity of param-filters.
+     *
+     * A list of param-filters needs to be specified. Also the parent of the
+     * parameter we're checking should be specified, not the parameter to check
+     * itself.
+     *
+     * @param VObject\Property $parent
+     * @param array $filters
+     * @return bool
+     */
+    protected function validateParamFilters(VObject\Property $parent, array $filters) {
+
+        foreach ($filters as $filter) {
+
+            $isDefined = isset($parent[$filter['name']]);
+
+            if ($filter['is-not-defined']) {
+
+                if ($isDefined) {
+                    return false;
+                } else {
+                    continue;
+                }
+
+            }
+            if (!$isDefined) {
+                return false;
+            }
+
+            if (!$filter['text-match']) {
+                continue;
+            }
+
+            // If there are sub-filters, we need to find at least one parameter
+            // for which the subfilters hold true.
+            foreach ($parent[$filter['name']]->getParts() as $paramPart) {
+
+                if ($this->validateTextMatch($paramPart, $filter['text-match'])) {
+                    // We had a match, so this param-filter succeeds
+                    continue 2;
+                }
+
+            }
+
+            // If we got here it means there was a text-match filter and there
+            // were no matches. This means the filter needs to return false.
+            return false;
+
+        }
+
+        // If we got here it means we got through all param-filters alive so the
+        // filters were all true.
+        return true;
+
+    }
+
+    /**
+     * This method checks the validity of a text-match.
+     *
+     * A single text-match should be specified as well as the specific property
+     * or parameter we need to validate.
+     *
+     * @param VObject\Node|string $check Value to check against.
+     * @param array $textMatch
+     * @return bool
+     */
+    protected function validateTextMatch($check, array $textMatch) {
+
+        if ($check instanceof VObject\Node) {
+            $check = $check->getValue();
+        }
+
+        $isMatching = \Sabre\DAV\StringUtil::textMatch($check, $textMatch['value'], $textMatch['collation']);
+
+        return ($textMatch['negate-condition'] xor $isMatching);
+
+    }
+
+    /**
+     * Validates if a component matches the given time range.
+     *
+     * This is all based on the rules specified in rfc4791, which are quite
+     * complex.
+     *
+     * @param VObject\Node $component
+     * @param DateTime $start
+     * @param DateTime $end
+     * @return bool
+     */
+    protected function validateTimeRange(VObject\Node $component, $start, $end) {
+
+        if (is_null($start)) {
+            $start = new DateTime('1900-01-01');
+        }
+        if (is_null($end)) {
+            $end = new DateTime('3000-01-01');
+        }
+
+        switch ($component->name) {
+
+            case 'VEVENT' :
+            case 'VTODO' :
+            case 'VJOURNAL' :
+
+                return $component->isInTimeRange($start, $end);
+
+            case 'VALARM' :
+
+                // If the valarm is wrapped in a recurring event, we need to
+                // expand the recursions, and validate each.
+                //
+                // Our datamodel doesn't easily allow us to do this straight
+                // in the VALARM component code, so this is a hack, and an
+                // expensive one too.
+                if ($component->parent->name === 'VEVENT' && $component->parent->RRULE) {
+
+                    // Fire up the iterator!
+                    $it = new VObject\Recur\EventIterator($component->parent->parent, (string)$component->parent->UID);
+                    while ($it->valid()) {
+                        $expandedEvent = $it->getEventObject();
+
+                        // We need to check from these expanded alarms, which
+                        // one is the first to trigger. Based on this, we can
+                        // determine if we can 'give up' expanding events.
+                        $firstAlarm = null;
+                        if ($expandedEvent->VALARM !== null) {
+                            foreach ($expandedEvent->VALARM as $expandedAlarm) {
+
+                                $effectiveTrigger = $expandedAlarm->getEffectiveTriggerTime();
+                                if ($expandedAlarm->isInTimeRange($start, $end)) {
+                                    return true;
+                                }
+
+                                if ((string)$expandedAlarm->TRIGGER['VALUE'] === 'DATE-TIME') {
+                                    // This is an alarm with a non-relative trigger
+                                    // time, likely created by a buggy client. The
+                                    // implication is that every alarm in this
+                                    // recurring event trigger at the exact same
+                                    // time. It doesn't make sense to traverse
+                                    // further.
+                                } else {
+                                    // We store the first alarm as a means to
+                                    // figure out when we can stop traversing.
+                                    if (!$firstAlarm || $effectiveTrigger < $firstAlarm) {
+                                        $firstAlarm = $effectiveTrigger;
+                                    }
+                                }
+                            }
+                        }
+                        if (is_null($firstAlarm)) {
+                            // No alarm was found.
+                            //
+                            // Or technically: No alarm that will change for
+                            // every instance of the recurrence was found,
+                            // which means we can assume there was no match.
+                            return false;
+                        }
+                        if ($firstAlarm > $end) {
+                            return false;
+                        }
+                        $it->next();
+                    }
+                    return false;
+                } else {
+                    return $component->isInTimeRange($start, $end);
+                }
+
+            case 'VFREEBUSY' :
+                throw new \Sabre\DAV\Exception\NotImplemented('time-range filters are currently not supported on ' . $component->name . ' components');
+
+            case 'COMPLETED' :
+            case 'CREATED' :
+            case 'DTEND' :
+            case 'DTSTAMP' :
+            case 'DTSTART' :
+            case 'DUE' :
+            case 'LAST-MODIFIED' :
+                return ($start <= $component->getDateTime() && $end >= $component->getDateTime());
+
+
+
+            default :
+                throw new \Sabre\DAV\Exception\BadRequest('You cannot create a time-range filter on a ' . $component->name . ' component');
+
+        }
+
+    }
+
+}

+ 80 - 0
lib/sabre/sabre/dav/lib/CalDAV/CalendarRoot.php

@@ -0,0 +1,80 @@
+<?php
+
+namespace Sabre\CalDAV;
+
+use Sabre\DAVACL\PrincipalBackend;
+
+/**
+ * Calendars collection
+ *
+ * This object is responsible for generating a list of calendar-homes for each
+ * user.
+ *
+ * This is the top-most node for the calendars tree. In most servers this class
+ * represents the "/calendars" path.
+ *
+ * @copyright Copyright (C) fruux GmbH (https://fruux.com/)
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+class CalendarRoot extends \Sabre\DAVACL\AbstractPrincipalCollection {
+
+    /**
+     * CalDAV backend
+     *
+     * @var Sabre\CalDAV\Backend\BackendInterface
+     */
+    protected $caldavBackend;
+
+    /**
+     * Constructor
+     *
+     * This constructor needs both an authentication and a caldav backend.
+     *
+     * By default this class will show a list of calendar collections for
+     * principals in the 'principals' collection. If your main principals are
+     * actually located in a different path, use the $principalPrefix argument
+     * to override this.
+     *
+     * @param PrincipalBackend\BackendInterface $principalBackend
+     * @param Backend\BackendInterface $caldavBackend
+     * @param string $principalPrefix
+     */
+    function __construct(PrincipalBackend\BackendInterface $principalBackend, Backend\BackendInterface $caldavBackend, $principalPrefix = 'principals') {
+
+        parent::__construct($principalBackend, $principalPrefix);
+        $this->caldavBackend = $caldavBackend;
+
+    }
+
+    /**
+     * Returns the nodename
+     *
+     * We're overriding this, because the default will be the 'principalPrefix',
+     * and we want it to be Sabre\CalDAV\Plugin::CALENDAR_ROOT
+     *
+     * @return string
+     */
+    function getName() {
+
+        return Plugin::CALENDAR_ROOT;
+
+    }
+
+    /**
+     * This method returns a node for a principal.
+     *
+     * The passed array contains principal information, and is guaranteed to
+     * at least contain a uri item. Other properties may or may not be
+     * supplied by the authentication backend.
+     *
+     * @param array $principal
+     * @return \Sabre\DAV\INode
+     */
+    function getChildForPrincipal(array $principal) {
+
+        return new CalendarHome($this->caldavBackend, $principal);
+
+    }
+
+}

+ 35 - 0
lib/sabre/sabre/dav/lib/CalDAV/Exception/InvalidComponentType.php

@@ -0,0 +1,35 @@
+<?php
+
+namespace Sabre\CalDAV\Exception;
+
+use Sabre\DAV;
+use Sabre\CalDAV;
+
+/**
+ * InvalidComponentType
+ *
+ * @copyright Copyright (C) fruux GmbH (https://fruux.com/)
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+class InvalidComponentType extends DAV\Exception\Forbidden {
+
+    /**
+     * Adds in extra information in the xml response.
+     *
+     * This method adds the {CALDAV:}supported-calendar-component as defined in rfc4791
+     *
+     * @param DAV\Server $server
+     * @param \DOMElement $errorNode
+     * @return void
+     */
+    function serialize(DAV\Server $server, \DOMElement $errorNode) {
+
+        $doc = $errorNode->ownerDocument;
+
+        $np = $doc->createElementNS(CalDAV\Plugin::NS_CALDAV, 'cal:supported-calendar-component');
+        $errorNode->appendChild($np);
+
+    }
+
+}

+ 366 - 0
lib/sabre/sabre/dav/lib/CalDAV/ICSExportPlugin.php

@@ -0,0 +1,366 @@
+<?php
+
+namespace Sabre\CalDAV;
+
+use DateTimeZone;
+use Sabre\DAV;
+use Sabre\VObject;
+use Sabre\HTTP\RequestInterface;
+use Sabre\HTTP\ResponseInterface;
+use Sabre\DAV\Exception\BadRequest;
+use DateTime;
+
+/**
+ * ICS Exporter
+ *
+ * This plugin adds the ability to export entire calendars as .ics files.
+ * This is useful for clients that don't support CalDAV yet. They often do
+ * support ics files.
+ *
+ * To use this, point a http client to a caldav calendar, and add ?expand to
+ * the url.
+ *
+ * Further options that can be added to the url:
+ *   start=123456789 - Only return events after the given unix timestamp
+ *   end=123245679   - Only return events from before the given unix timestamp
+ *   expand=1        - Strip timezone information and expand recurring events.
+ *                     If you'd like to expand, you _must_ also specify start
+ *                     and end.
+ *
+ * By default this plugin returns data in the text/calendar format (iCalendar
+ * 2.0). If you'd like to receive jCal data instead, you can use an Accept
+ * header:
+ *
+ * Accept: application/calendar+json
+ *
+ * Alternatively, you can also specify this in the url using
+ * accept=application/calendar+json, or accept=jcal for short. If the url
+ * parameter and Accept header is specified, the url parameter wins.
+ *
+ * Note that specifying a start or end data implies that only events will be
+ * returned. VTODO and VJOURNAL will be stripped.
+ *
+ * @copyright Copyright (C) fruux GmbH (https://fruux.com/)
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+class ICSExportPlugin extends DAV\ServerPlugin {
+
+    /**
+     * Reference to Server class
+     *
+     * @var \Sabre\DAV\Server
+     */
+    protected $server;
+
+    /**
+     * Initializes the plugin and registers event handlers
+     *
+     * @param \Sabre\DAV\Server $server
+     * @return void
+     */
+    function initialize(DAV\Server $server) {
+
+        $this->server = $server;
+        $server->on('method:GET', [$this, 'httpGet'], 90);
+        $server->on('browserButtonActions', function($path, $node, &$actions) {
+            if ($node instanceof ICalendar) {
+                $actions .= '<a href="' . htmlspecialchars($path, ENT_QUOTES, 'UTF-8') . '?export"><span class="oi" data-glyph="calendar"></span></a>';
+            }
+        });
+
+    }
+
+    /**
+     * Intercepts GET requests on calendar urls ending with ?export.
+     *
+     * @param RequestInterface $request
+     * @param ResponseInterface $response
+     * @return bool
+     */
+    function httpGet(RequestInterface $request, ResponseInterface $response) {
+
+        $queryParams = $request->getQueryParameters();
+        if (!array_key_exists('export', $queryParams)) return;
+
+        $path = $request->getPath();
+
+        $node = $this->server->getProperties($path, [
+            '{DAV:}resourcetype',
+            '{DAV:}displayname',
+            '{http://sabredav.org/ns}sync-token',
+            '{DAV:}sync-token',
+            '{http://apple.com/ns/ical/}calendar-color',
+        ]);
+
+        if (!isset($node['{DAV:}resourcetype']) || !$node['{DAV:}resourcetype']->is('{' . Plugin::NS_CALDAV . '}calendar')) {
+            return;
+        }
+        // Marking the transactionType, for logging purposes.
+        $this->server->transactionType = 'get-calendar-export';
+
+        $properties = $node;
+
+        $start = null;
+        $end = null;
+        $expand = false;
+        $componentType = false;
+        if (isset($queryParams['start'])) {
+            if (!ctype_digit($queryParams['start'])) {
+                throw new BadRequest('The start= parameter must contain a unix timestamp');
+            }
+            $start = DateTime::createFromFormat('U', $queryParams['start']);
+        }
+        if (isset($queryParams['end'])) {
+            if (!ctype_digit($queryParams['end'])) {
+                throw new BadRequest('The end= parameter must contain a unix timestamp');
+            }
+            $end = DateTime::createFromFormat('U', $queryParams['end']);
+        }
+        if (isset($queryParams['expand']) && !!$queryParams['expand']) {
+            if (!$start || !$end) {
+                throw new BadRequest('If you\'d like to expand recurrences, you must specify both a start= and end= parameter.');
+            }
+            $expand = true;
+            $componentType = 'VEVENT';
+        }
+        if (isset($queryParams['componentType'])) {
+            if (!in_array($queryParams['componentType'], ['VEVENT', 'VTODO', 'VJOURNAL'])) {
+                throw new BadRequest('You are not allowed to search for components of type: ' . $queryParams['componentType'] . ' here');
+            }
+            $componentType = $queryParams['componentType'];
+        }
+
+        $format = \Sabre\HTTP\Util::Negotiate(
+            $request->getHeader('Accept'),
+            [
+                'text/calendar',
+                'application/calendar+json',
+            ]
+        );
+
+        if (isset($queryParams['accept'])) {
+            if ($queryParams['accept'] === 'application/calendar+json' || $queryParams['accept'] === 'jcal') {
+                $format = 'application/calendar+json';
+            }
+        }
+        if (!$format) {
+            $format = 'text/calendar';
+        }
+
+        $this->generateResponse($path, $start, $end, $expand, $componentType, $format, $properties, $response);
+
+        // Returning false to break the event chain
+        return false;
+
+    }
+
+    /**
+     * This method is responsible for generating the actual, full response.
+     *
+     * @param string $path
+     * @param DateTime|null $start
+     * @param DateTime|null $end
+     * @param bool $expand
+     * @param string $componentType
+     * @param string $format
+     * @param array $properties
+     * @param ResponseInterface $response
+     */
+    protected function generateResponse($path, $start, $end, $expand, $componentType, $format, $properties, ResponseInterface $response) {
+
+        $calDataProp = '{' . Plugin::NS_CALDAV . '}calendar-data';
+
+        $blobs = [];
+        if ($start || $end || $componentType) {
+
+            // If there was a start or end filter, we need to enlist
+            // calendarQuery for speed.
+            $calendarNode = $this->server->tree->getNodeForPath($path);
+            $queryResult = $calendarNode->calendarQuery([
+                'name'         => 'VCALENDAR',
+                'comp-filters' => [
+                    [
+                        'name'           => $componentType,
+                        'comp-filters'   => [],
+                        'prop-filters'   => [],
+                        'is-not-defined' => false,
+                        'time-range'     => [
+                            'start' => $start,
+                            'end'   => $end,
+                        ],
+                    ],
+                ],
+                'prop-filters'   => [],
+                'is-not-defined' => false,
+                'time-range'     => null,
+            ]);
+
+            // queryResult is just a list of base urls. We need to prefix the
+            // calendar path.
+            $queryResult = array_map(
+                function($item) use ($path) {
+                    return $path . '/' . $item;
+                },
+                $queryResult
+            );
+            $nodes = $this->server->getPropertiesForMultiplePaths($queryResult, [$calDataProp]);
+            unset($queryResult);
+
+        } else {
+            $nodes = $this->server->getPropertiesForPath($path, [$calDataProp], 1);
+        }
+
+        // Flattening the arrays
+        foreach ($nodes as $node) {
+            if (isset($node[200][$calDataProp])) {
+                $blobs[$node['href']] = $node[200][$calDataProp];
+            }
+        }
+        unset($nodes);
+
+        $mergedCalendar = $this->mergeObjects(
+            $properties,
+            $blobs
+        );
+
+        if ($expand) {
+            $calendarTimeZone = null;
+            // We're expanding, and for that we need to figure out the
+            // calendar's timezone.
+            $tzProp = '{' . Plugin::NS_CALDAV . '}calendar-timezone';
+            $tzResult = $this->server->getProperties($path, [$tzProp]);
+            if (isset($tzResult[$tzProp])) {
+                // This property contains a VCALENDAR with a single
+                // VTIMEZONE.
+                $vtimezoneObj = VObject\Reader::read($tzResult[$tzProp]);
+                $calendarTimeZone = $vtimezoneObj->VTIMEZONE->getTimeZone();
+                // Destroy circular references to PHP will GC the object.
+                $vtimezoneObj->destroy();
+                unset($vtimezoneObj);
+            } else {
+                // Defaulting to UTC.
+                $calendarTimeZone = new DateTimeZone('UTC');
+            }
+
+            $mergedCalendar = $mergedCalendar->expand($start, $end, $calendarTimeZone);
+        }
+
+        $response->setHeader('Content-Type', $format);
+
+        switch ($format) {
+            case 'text/calendar' :
+                $mergedCalendar = $mergedCalendar->serialize();
+                break;
+            case 'application/calendar+json' :
+                $mergedCalendar = json_encode($mergedCalendar->jsonSerialize());
+                break;
+        }
+
+        $response->setStatus(200);
+        $response->setBody($mergedCalendar);
+
+    }
+
+    /**
+     * Merges all calendar objects, and builds one big iCalendar blob.
+     *
+     * @param array $properties Some CalDAV properties
+     * @param array $inputObjects
+     * @return VObject\Component\VCalendar
+     */
+    function mergeObjects(array $properties, array $inputObjects) {
+
+        $calendar = new VObject\Component\VCalendar();
+        $calendar->version = '2.0';
+        if (DAV\Server::$exposeVersion) {
+            $calendar->prodid = '-//SabreDAV//SabreDAV ' . DAV\Version::VERSION . '//EN';
+        } else {
+            $calendar->prodid = '-//SabreDAV//SabreDAV//EN';
+        }
+        if (isset($properties['{DAV:}displayname'])) {
+            $calendar->{'X-WR-CALNAME'} = $properties['{DAV:}displayname'];
+        }
+        if (isset($properties['{http://apple.com/ns/ical/}calendar-color'])) {
+            $calendar->{'X-APPLE-CALENDAR-COLOR'} = $properties['{http://apple.com/ns/ical/}calendar-color'];
+        }
+
+        $collectedTimezones = [];
+
+        $timezones = [];
+        $objects = [];
+
+        foreach ($inputObjects as $href => $inputObject) {
+
+            $nodeComp = VObject\Reader::read($inputObject);
+
+            foreach ($nodeComp->children() as $child) {
+
+                switch ($child->name) {
+                    case 'VEVENT' :
+                    case 'VTODO' :
+                    case 'VJOURNAL' :
+                        $objects[] = clone $child;
+                        break;
+
+                    // VTIMEZONE is special, because we need to filter out the duplicates
+                    case 'VTIMEZONE' :
+                        // Naively just checking tzid.
+                        if (in_array((string)$child->TZID, $collectedTimezones)) continue;
+
+                        $timezones[] = clone $child;
+                        $collectedTimezones[] = $child->TZID;
+                        break;
+
+                }
+
+            }
+            // Destroy circular references to PHP will GC the object.
+            $nodeComp->destroy();
+            unset($nodeComp);
+
+        }
+
+        foreach ($timezones as $tz) $calendar->add($tz);
+        foreach ($objects as $obj) $calendar->add($obj);
+
+        return $calendar;
+
+    }
+
+    /**
+     * Returns a plugin name.
+     *
+     * Using this name other plugins will be able to access other plugins
+     * using \Sabre\DAV\Server::getPlugin
+     *
+     * @return string
+     */
+    function getPluginName() {
+
+        return 'ics-export';
+
+    }
+
+    /**
+     * Returns a bunch of meta-data about the plugin.
+     *
+     * Providing this information is optional, and is mainly displayed by the
+     * Browser plugin.
+     *
+     * The description key in the returned array may contain html and will not
+     * be sanitized.
+     *
+     * @return array
+     */
+    function getPluginInfo() {
+
+        return [
+            'name'        => $this->getPluginName(),
+            'description' => 'Adds the ability to export CalDAV calendars as a single iCalendar file.',
+            'link'        => 'http://sabre.io/dav/ics-export-plugin/',
+        ];
+
+    }
+
+}

+ 18 - 0
lib/sabre/sabre/dav/lib/CalDAV/ICalendar.php

@@ -0,0 +1,18 @@
+<?php
+
+namespace Sabre\CalDAV;
+
+use Sabre\DAVACL;
+
+/**
+ * Calendar interface
+ *
+ * Implement this interface to allow a node to be recognized as an calendar.
+ *
+ * @copyright Copyright (C) fruux GmbH (https://fruux.com/)
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+interface ICalendar extends ICalendarObjectContainer, DAVACL\IACL {
+
+}

+ 21 - 0
lib/sabre/sabre/dav/lib/CalDAV/ICalendarObject.php

@@ -0,0 +1,21 @@
+<?php
+
+namespace Sabre\CalDAV;
+
+use Sabre\DAV;
+
+/**
+ * CalendarObject interface
+ *
+ * Extend the ICalendarObject interface to allow your custom nodes to be picked up as
+ * CalendarObjects.
+ *
+ * Calendar objects are resources such as Events, Todo's or Journals.
+ *
+ * @copyright Copyright (C) fruux GmbH (https://fruux.com/)
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+interface ICalendarObject extends DAV\IFile {
+
+}

+ 39 - 0
lib/sabre/sabre/dav/lib/CalDAV/ICalendarObjectContainer.php

@@ -0,0 +1,39 @@
+<?php
+
+namespace Sabre\CalDAV;
+
+/**
+ * This interface represents a node that may contain calendar objects.
+ *
+ * This is the shared parent for both the Inbox collection and calendars
+ * resources.
+ *
+ * In most cases you will likely want to look at ICalendar instead of this
+ * interface.
+ *
+ * @copyright Copyright (C) fruux GmbH (https://fruux.com/)
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+interface ICalendarObjectContainer extends \Sabre\DAV\ICollection {
+
+    /**
+     * Performs a calendar-query on the contents of this calendar.
+     *
+     * The calendar-query is defined in RFC4791 : CalDAV. Using the
+     * calendar-query it is possible for a client to request a specific set of
+     * object, based on contents of iCalendar properties, date-ranges and
+     * iCalendar component types (VTODO, VEVENT).
+     *
+     * This method should just return a list of (relative) urls that match this
+     * query.
+     *
+     * The list of filters are specified as an array. The exact array is
+     * documented by \Sabre\CalDAV\CalendarQueryParser.
+     *
+     * @param array $filters
+     * @return array
+     */
+    function calendarQuery(array $filters);
+
+}

+ 48 - 0
lib/sabre/sabre/dav/lib/CalDAV/IShareableCalendar.php

@@ -0,0 +1,48 @@
+<?php
+
+namespace Sabre\CalDAV;
+
+/**
+ * This interface represents a Calendar that can be shared with other users.
+ *
+ * @copyright Copyright (C) fruux GmbH (https://fruux.com/)
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+interface IShareableCalendar extends ICalendar {
+
+    /**
+     * Updates the list of shares.
+     *
+     * The first array is a list of people that are to be added to the
+     * calendar.
+     *
+     * Every element in the add array has the following properties:
+     *   * href - A url. Usually a mailto: address
+     *   * commonName - Usually a first and last name, or false
+     *   * summary - A description of the share, can also be false
+     *   * readOnly - A boolean value
+     *
+     * Every element in the remove array is just the address string.
+     *
+     * @param array $add
+     * @param array $remove
+     * @return void
+     */
+    function updateShares(array $add, array $remove);
+
+    /**
+     * Returns the list of people whom this calendar is shared with.
+     *
+     * Every element in this array should have the following properties:
+     *   * href - Often a mailto: address
+     *   * commonName - Optional, for example a first + last name
+     *   * status - See the Sabre\CalDAV\SharingPlugin::STATUS_ constants.
+     *   * readOnly - boolean
+     *   * summary - Optional, a description for the share
+     *
+     * @return array
+     */
+    function getShares();
+
+}

+ 36 - 0
lib/sabre/sabre/dav/lib/CalDAV/ISharedCalendar.php

@@ -0,0 +1,36 @@
+<?php
+
+namespace Sabre\CalDAV;
+
+/**
+ * This interface represents a Calendar that is shared by a different user.
+ *
+ * @copyright Copyright (C) fruux GmbH (https://fruux.com/)
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+interface ISharedCalendar extends ICalendar {
+
+    /**
+     * This method should return the url of the owners' copy of the shared
+     * calendar.
+     *
+     * @return string
+     */
+    function getSharedUrl();
+
+    /**
+     * Returns the list of people whom this calendar is shared with.
+     *
+     * Every element in this array should have the following properties:
+     *   * href - Often a mailto: address
+     *   * commonName - Optional, for example a first + last name
+     *   * status - See the Sabre\CalDAV\SharingPlugin::STATUS_ constants.
+     *   * readOnly - boolean
+     *   * summary - Optional, a description for the share
+     *
+     * @return array
+     */
+    function getShares();
+
+}

+ 173 - 0
lib/sabre/sabre/dav/lib/CalDAV/Notifications/Collection.php

@@ -0,0 +1,173 @@
+<?php
+
+namespace Sabre\CalDAV\Notifications;
+
+use Sabre\DAV;
+use Sabre\CalDAV;
+use Sabre\DAVACL;
+
+/**
+ * This node represents a list of notifications.
+ *
+ * It provides no additional functionality, but you must implement this
+ * interface to allow the Notifications plugin to mark the collection
+ * as a notifications collection.
+ *
+ * This collection should only return Sabre\CalDAV\Notifications\INode nodes as
+ * its children.
+ *
+ * @copyright Copyright (C) fruux GmbH (https://fruux.com/)
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+class Collection extends DAV\Collection implements ICollection, DAVACL\IACL {
+
+    /**
+     * The notification backend
+     *
+     * @var Sabre\CalDAV\Backend\NotificationSupport
+     */
+    protected $caldavBackend;
+
+    /**
+     * Principal uri
+     *
+     * @var string
+     */
+    protected $principalUri;
+
+    /**
+     * Constructor
+     *
+     * @param CalDAV\Backend\NotificationSupport $caldavBackend
+     * @param string $principalUri
+     */
+    function __construct(CalDAV\Backend\NotificationSupport $caldavBackend, $principalUri) {
+
+        $this->caldavBackend = $caldavBackend;
+        $this->principalUri = $principalUri;
+
+    }
+
+    /**
+     * Returns all notifications for a principal
+     *
+     * @return array
+     */
+    function getChildren() {
+
+        $children = [];
+        $notifications = $this->caldavBackend->getNotificationsForPrincipal($this->principalUri);
+
+        foreach ($notifications as $notification) {
+
+            $children[] = new Node(
+                $this->caldavBackend,
+                $this->principalUri,
+                $notification
+            );
+        }
+
+        return $children;
+
+    }
+
+    /**
+     * Returns the name of this object
+     *
+     * @return string
+     */
+    function getName() {
+
+        return 'notifications';
+
+    }
+
+    /**
+     * Returns the owner principal
+     *
+     * This must be a url to a principal, or null if there's no owner
+     *
+     * @return string|null
+     */
+    function getOwner() {
+
+        return $this->principalUri;
+
+    }
+
+    /**
+     * Returns a group principal
+     *
+     * This must be a url to a principal, or null if there's no owner
+     *
+     * @return string|null
+     */
+    function getGroup() {
+
+        return null;
+
+    }
+
+    /**
+     * Returns a list of ACE's for this node.
+     *
+     * Each ACE has the following properties:
+     *   * 'privilege', a string such as {DAV:}read or {DAV:}write. These are
+     *     currently the only supported privileges
+     *   * 'principal', a url to the principal who owns the node
+     *   * 'protected' (optional), indicating that this ACE is not allowed to
+     *      be updated.
+     *
+     * @return array
+     */
+    function getACL() {
+
+        return [
+            [
+                'principal' => $this->getOwner(),
+                'privilege' => '{DAV:}read',
+                'protected' => true,
+            ],
+            [
+                'principal' => $this->getOwner(),
+                'privilege' => '{DAV:}write',
+                'protected' => true,
+            ]
+        ];
+
+    }
+
+    /**
+     * Updates the ACL
+     *
+     * This method will receive a list of new ACE's as an array argument.
+     *
+     * @param array $acl
+     * @return void
+     */
+    function setACL(array $acl) {
+
+        throw new DAV\Exception\NotImplemented('Updating ACLs is not implemented here');
+
+    }
+
+    /**
+     * Returns the list of supported privileges for this node.
+     *
+     * The returned data structure is a list of nested privileges.
+     * See Sabre\DAVACL\Plugin::getDefaultSupportedPrivilegeSet for a simple
+     * standard structure.
+     *
+     * If null is returned from this method, the default privilege set is used,
+     * which is fine for most common usecases.
+     *
+     * @return array|null
+     */
+    function getSupportedPrivilegeSet() {
+
+        return null;
+
+    }
+
+}

+ 23 - 0
lib/sabre/sabre/dav/lib/CalDAV/Notifications/ICollection.php

@@ -0,0 +1,23 @@
+<?php
+
+namespace Sabre\CalDAV\Notifications;
+
+use Sabre\DAV;
+
+/**
+ * This node represents a list of notifications.
+ *
+ * It provides no additional functionality, but you must implement this
+ * interface to allow the Notifications plugin to mark the collection
+ * as a notifications collection.
+ *
+ * This collection should only return Sabre\CalDAV\Notifications\INode nodes as
+ * its children.
+ *
+ * @copyright Copyright (C) fruux GmbH (https://fruux.com/)
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+interface ICollection extends DAV\ICollection {
+
+}

+ 38 - 0
lib/sabre/sabre/dav/lib/CalDAV/Notifications/INode.php

@@ -0,0 +1,38 @@
+<?php
+
+namespace Sabre\CalDAV\Notifications;
+
+/**
+ * This node represents a single notification.
+ *
+ * The signature is mostly identical to that of Sabre\DAV\IFile, but the get() method
+ * MUST return an xml document that matches the requirements of the
+ * 'caldav-notifications.txt' spec.
+ *
+ * For a complete example, check out the Notification class, which contains
+ * some helper functions.
+ *
+ * @copyright Copyright (C) fruux GmbH (https://fruux.com/)
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+interface INode {
+
+    /**
+     * This method must return an xml element, using the
+     * Sabre\CalDAV\Notifications\INotificationType classes.
+     *
+     * @return INotificationType
+     */
+    function getNotificationType();
+
+    /**
+     * Returns the etag for the notification.
+     *
+     * The etag must be surrounded by litteral double-quotes.
+     *
+     * @return string
+     */
+    function getETag();
+
+}

+ 193 - 0
lib/sabre/sabre/dav/lib/CalDAV/Notifications/Node.php

@@ -0,0 +1,193 @@
+<?php
+
+namespace Sabre\CalDAV\Notifications;
+
+use Sabre\DAV;
+use Sabre\CalDAV;
+use Sabre\CalDAV\Xml\Notification\NotificationInterface;
+use Sabre\DAVACL;
+
+/**
+ * This node represents a single notification.
+ *
+ * The signature is mostly identical to that of Sabre\DAV\IFile, but the get() method
+ * MUST return an xml document that matches the requirements of the
+ * 'caldav-notifications.txt' spec.
+ *
+ * @copyright Copyright (C) fruux GmbH (https://fruux.com/)
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+class Node extends DAV\File implements INode, DAVACL\IACL {
+
+    /**
+     * The notification backend
+     *
+     * @var Sabre\CalDAV\Backend\NotificationSupport
+     */
+    protected $caldavBackend;
+
+    /**
+     * The actual notification
+     *
+     * @var Sabre\CalDAV\Notifications\INotificationType
+     */
+    protected $notification;
+
+    /**
+     * Owner principal of the notification
+     *
+     * @var string
+     */
+    protected $principalUri;
+
+    /**
+     * Constructor
+     *
+     * @param CalDAV\Backend\NotificationSupport $caldavBackend
+     * @param string $principalUri
+     * @param NotificationInterface $notification
+     */
+    function __construct(CalDAV\Backend\NotificationSupport $caldavBackend, $principalUri, NotificationInterface $notification) {
+
+        $this->caldavBackend = $caldavBackend;
+        $this->principalUri = $principalUri;
+        $this->notification = $notification;
+
+    }
+
+    /**
+     * Returns the path name for this notification
+     *
+     * @return id
+     */
+    function getName() {
+
+        return $this->notification->getId() . '.xml';
+
+    }
+
+    /**
+     * Returns the etag for the notification.
+     *
+     * The etag must be surrounded by litteral double-quotes.
+     *
+     * @return string
+     */
+    function getETag() {
+
+        return $this->notification->getETag();
+
+    }
+
+    /**
+     * This method must return an xml element, using the
+     * Sabre\CalDAV\Notifications\INotificationType classes.
+     *
+     * @return INotificationType
+     */
+    function getNotificationType() {
+
+        return $this->notification;
+
+    }
+
+    /**
+     * Deletes this notification
+     *
+     * @return void
+     */
+    function delete() {
+
+        $this->caldavBackend->deleteNotification($this->getOwner(), $this->notification);
+
+    }
+
+    /**
+     * Returns the owner principal
+     *
+     * This must be a url to a principal, or null if there's no owner
+     *
+     * @return string|null
+     */
+    function getOwner() {
+
+        return $this->principalUri;
+
+    }
+
+    /**
+     * Returns a group principal
+     *
+     * This must be a url to a principal, or null if there's no owner
+     *
+     * @return string|null
+     */
+    function getGroup() {
+
+        return null;
+
+    }
+
+    /**
+     * Returns a list of ACE's for this node.
+     *
+     * Each ACE has the following properties:
+     *   * 'privilege', a string such as {DAV:}read or {DAV:}write. These are
+     *     currently the only supported privileges
+     *   * 'principal', a url to the principal who owns the node
+     *   * 'protected' (optional), indicating that this ACE is not allowed to
+     *      be updated.
+     *
+     * @return array
+     */
+    function getACL() {
+
+        return [
+            [
+                'principal' => $this->getOwner(),
+                'privilege' => '{DAV:}read',
+                'protected' => true,
+            ],
+            [
+                'principal' => $this->getOwner(),
+                'privilege' => '{DAV:}write',
+                'protected' => true,
+            ]
+        ];
+
+    }
+
+    /**
+     * Updates the ACL
+     *
+     * This method will receive a list of new ACE's as an array argument.
+     *
+     * @param array $acl
+     * @return void
+     */
+    function setACL(array $acl) {
+
+        throw new DAV\Exception\NotImplemented('Updating ACLs is not implemented here');
+
+    }
+
+    /**
+     * Returns the list of supported privileges for this node.
+     *
+     * The returned data structure is a list of nested privileges.
+     * See Sabre\DAVACL\Plugin::getDefaultSupportedPrivilegeSet for a simple
+     * standard structure.
+     *
+     * If null is returned from this method, the default privilege set is used,
+     * which is fine for most common usecases.
+     *
+     * @return array|null
+     */
+    function getSupportedPrivilegeSet() {
+
+        return null;
+
+    }
+
+}

+ 180 - 0
lib/sabre/sabre/dav/lib/CalDAV/Notifications/Plugin.php

@@ -0,0 +1,180 @@
+<?php
+
+namespace Sabre\CalDAV\Notifications;
+
+use Sabre\DAV;
+use Sabre\DAV\PropFind;
+use Sabre\DAV\INode as BaseINode;
+use Sabre\DAV\ServerPlugin;
+use Sabre\DAV\Server;
+use Sabre\DAVACL;
+use Sabre\HTTP\RequestInterface;
+use Sabre\HTTP\ResponseInterface;
+
+/**
+ * Notifications plugin
+ *
+ * This plugin implements several features required by the caldav-notification
+ * draft specification.
+ *
+ * Before version 2.1.0 this functionality was part of Sabre\CalDAV\Plugin but
+ * this has since been split up.
+ *
+ * @copyright Copyright (C) fruux GmbH (https://fruux.com/)
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+class Plugin extends ServerPlugin {
+
+    /**
+     * This is the namespace for the proprietary calendarserver extensions
+     */
+    const NS_CALENDARSERVER = 'http://calendarserver.org/ns/';
+
+    /**
+     * Reference to the main server object.
+     *
+     * @var Server
+     */
+    protected $server;
+
+    /**
+     * Returns a plugin name.
+     *
+     * Using this name other plugins will be able to access other plugins
+     * using \Sabre\DAV\Server::getPlugin
+     *
+     * @return string
+     */
+    function getPluginName() {
+
+        return 'notifications';
+
+    }
+
+    /**
+     * This initializes the plugin.
+     *
+     * This function is called by Sabre\DAV\Server, after
+     * addPlugin is called.
+     *
+     * This method should set up the required event subscriptions.
+     *
+     * @param Server $server
+     * @return void
+     */
+    function initialize(Server $server) {
+
+        $this->server = $server;
+        $server->on('method:GET', [$this, 'httpGet'], 90);
+        $server->on('propFind',   [$this, 'propFind']);
+
+        $server->xml->namespaceMap[self::NS_CALENDARSERVER] = 'cs';
+        $server->resourceTypeMapping['\\Sabre\\CalDAV\\Notifications\\ICollection'] = '{' . self::NS_CALENDARSERVER . '}notification';
+
+        array_push($server->protectedProperties,
+            '{' . self::NS_CALENDARSERVER . '}notification-URL',
+            '{' . self::NS_CALENDARSERVER . '}notificationtype'
+        );
+
+    }
+
+    /**
+     * PropFind
+     *
+     * @param PropFind $propFind
+     * @param BaseINode $node
+     * @return void
+     */
+    function propFind(PropFind $propFind, BaseINode $node) {
+
+        $caldavPlugin = $this->server->getPlugin('caldav');
+
+        if ($node instanceof DAVACL\IPrincipal) {
+
+            $principalUrl = $node->getPrincipalUrl();
+
+            // notification-URL property
+            $propFind->handle('{' . self::NS_CALENDARSERVER . '}notification-URL', function() use ($principalUrl, $caldavPlugin) {
+
+                $notificationPath = $caldavPlugin->getCalendarHomeForPrincipal($principalUrl) . '/notifications/';
+                return new DAV\Xml\Property\Href($notificationPath);
+
+            });
+
+        }
+
+        if ($node instanceof INode) {
+
+            $propFind->handle(
+                '{' . self::NS_CALENDARSERVER . '}notificationtype',
+                [$node, 'getNotificationType']
+            );
+
+        }
+
+    }
+
+    /**
+     * This event is triggered before the usual GET request handler.
+     *
+     * We use this to intercept GET calls to notification nodes, and return the
+     * proper response.
+     *
+     * @param RequestInterface $request
+     * @param ResponseInterface $response
+     * @return void
+     */
+    function httpGet(RequestInterface $request, ResponseInterface $response) {
+
+        $path = $request->getPath();
+
+        try {
+            $node = $this->server->tree->getNodeForPath($path);
+        } catch (DAV\Exception\NotFound $e) {
+            return;
+        }
+
+        if (!$node instanceof INode)
+            return;
+
+        $writer = $this->server->xml->getWriter();
+        $writer->contextUri = $this->server->getBaseUri();
+        $writer->openMemory();
+        $writer->startDocument('1.0', 'UTF-8');
+        $writer->startElement('{http://calendarserver.org/ns/}notification');
+        $node->getNotificationType()->xmlSerializeFull($writer);
+        $writer->endElement();
+
+        $response->setHeader('Content-Type', 'application/xml');
+        $response->setHeader('ETag', $node->getETag());
+        $response->setStatus(200);
+        $response->setBody($writer->outputMemory());
+
+        // Return false to break the event chain.
+        return false;
+
+    }
+
+    /**
+     * Returns a bunch of meta-data about the plugin.
+     *
+     * Providing this information is optional, and is mainly displayed by the
+     * Browser plugin.
+     *
+     * The description key in the returned array may contain html and will not
+     * be sanitized.
+     *
+     * @return array
+     */
+    function getPluginInfo() {
+
+        return [
+            'name'        => $this->getPluginName(),
+            'description' => 'Adds support for caldav-notifications, which is required to enable caldav-sharing.',
+            'link'        => 'http://sabre.io/dav/caldav-sharing/',
+        ];
+
+    }
+
+}

+ 1025 - 0
lib/sabre/sabre/dav/lib/CalDAV/Plugin.php

@@ -0,0 +1,1025 @@
+<?php
+
+namespace Sabre\CalDAV;
+
+use DateTimeZone;
+use Sabre\DAV;
+use Sabre\DAV\Exception\BadRequest;
+use Sabre\DAV\MkCol;
+use Sabre\DAV\Xml\Property\Href;
+use Sabre\DAVACL;
+use Sabre\VObject;
+use Sabre\HTTP;
+use Sabre\Uri;
+use Sabre\HTTP\RequestInterface;
+use Sabre\HTTP\ResponseInterface;
+
+/**
+ * CalDAV plugin
+ *
+ * This plugin provides functionality added by CalDAV (RFC 4791)
+ * It implements new reports, and the MKCALENDAR method.
+ *
+ * @copyright Copyright (C) fruux GmbH (https://fruux.com/)
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+class Plugin extends DAV\ServerPlugin {
+
+    /**
+     * This is the official CalDAV namespace
+     */
+    const NS_CALDAV = 'urn:ietf:params:xml:ns:caldav';
+
+    /**
+     * This is the namespace for the proprietary calendarserver extensions
+     */
+    const NS_CALENDARSERVER = 'http://calendarserver.org/ns/';
+
+    /**
+     * The hardcoded root for calendar objects. It is unfortunate
+     * that we're stuck with it, but it will have to do for now
+     */
+    const CALENDAR_ROOT = 'calendars';
+
+    /**
+     * Reference to server object
+     *
+     * @var DAV\Server
+     */
+    protected $server;
+
+    /**
+     * The default PDO storage uses a MySQL MEDIUMBLOB for iCalendar data,
+     * which can hold up to 2^24 = 16777216 bytes. This is plenty. We're
+     * capping it to 10M here.
+     */
+    protected $maxResourceSize = 10000000;
+
+    /**
+     * Use this method to tell the server this plugin defines additional
+     * HTTP methods.
+     *
+     * This method is passed a uri. It should only return HTTP methods that are
+     * available for the specified uri.
+     *
+     * @param string $uri
+     * @return array
+     */
+    function getHTTPMethods($uri) {
+
+        // The MKCALENDAR is only available on unmapped uri's, whose
+        // parents extend IExtendedCollection
+        list($parent, $name) = Uri\split($uri);
+
+        $node = $this->server->tree->getNodeForPath($parent);
+
+        if ($node instanceof DAV\IExtendedCollection) {
+            try {
+                $node->getChild($name);
+            } catch (DAV\Exception\NotFound $e) {
+                return ['MKCALENDAR'];
+            }
+        }
+        return [];
+
+    }
+
+    /**
+     * Returns the path to a principal's calendar home.
+     *
+     * The return url must not end with a slash.
+     * This function should return null in case a principal did not have
+     * a calendar home.
+     *
+     * @param string $principalUrl
+     * @return string
+     */
+    function getCalendarHomeForPrincipal($principalUrl) {
+
+        // The default behavior for most sabre/dav servers is that there is a
+        // principals root node, which contains users directly under it.
+        //
+        // This function assumes that there are two components in a principal
+        // path. If there's more, we don't return a calendar home. This
+        // excludes things like the calendar-proxy-read principal (which it
+        // should).
+        $parts = explode('/', trim($principalUrl, '/'));
+        if (count($parts) !== 2) return;
+        if ($parts[0] !== 'principals') return;
+
+        return self::CALENDAR_ROOT . '/' . $parts[1];
+
+    }
+
+    /**
+     * Returns a list of features for the DAV: HTTP header.
+     *
+     * @return array
+     */
+    function getFeatures() {
+
+        return ['calendar-access', 'calendar-proxy'];
+
+    }
+
+    /**
+     * Returns a plugin name.
+     *
+     * Using this name other plugins will be able to access other plugins
+     * using DAV\Server::getPlugin
+     *
+     * @return string
+     */
+    function getPluginName() {
+
+        return 'caldav';
+
+    }
+
+    /**
+     * Returns a list of reports this plugin supports.
+     *
+     * This will be used in the {DAV:}supported-report-set property.
+     * Note that you still need to subscribe to the 'report' event to actually
+     * implement them
+     *
+     * @param string $uri
+     * @return array
+     */
+    function getSupportedReportSet($uri) {
+
+        $node = $this->server->tree->getNodeForPath($uri);
+
+        $reports = [];
+        if ($node instanceof ICalendarObjectContainer || $node instanceof ICalendarObject) {
+            $reports[] = '{' . self::NS_CALDAV . '}calendar-multiget';
+            $reports[] = '{' . self::NS_CALDAV . '}calendar-query';
+        }
+        if ($node instanceof ICalendar) {
+            $reports[] = '{' . self::NS_CALDAV . '}free-busy-query';
+        }
+        // iCal has a bug where it assumes that sync support is enabled, only
+        // if we say we support it on the calendar-home, even though this is
+        // not actually the case.
+        if ($node instanceof CalendarHome && $this->server->getPlugin('sync')) {
+            $reports[] = '{DAV:}sync-collection';
+        }
+        return $reports;
+
+    }
+
+    /**
+     * Initializes the plugin
+     *
+     * @param DAV\Server $server
+     * @return void
+     */
+    function initialize(DAV\Server $server) {
+
+        $this->server = $server;
+
+        $server->on('method:MKCALENDAR',   [$this, 'httpMkCalendar']);
+        $server->on('report',              [$this, 'report']);
+        $server->on('propFind',            [$this, 'propFind']);
+        $server->on('onHTMLActionsPanel',  [$this, 'htmlActionsPanel']);
+        $server->on('beforeCreateFile',    [$this, 'beforeCreateFile']);
+        $server->on('beforeWriteContent',  [$this, 'beforeWriteContent']);
+        $server->on('afterMethod:GET',     [$this, 'httpAfterGET']);
+
+        $server->xml->namespaceMap[self::NS_CALDAV] = 'cal';
+        $server->xml->namespaceMap[self::NS_CALENDARSERVER] = 'cs';
+
+        $server->xml->elementMap['{' . self::NS_CALDAV . '}supported-calendar-component-set'] = 'Sabre\\CalDAV\\Xml\\Property\\SupportedCalendarComponentSet';
+        $server->xml->elementMap['{' . self::NS_CALDAV . '}calendar-query'] = 'Sabre\\CalDAV\\Xml\\Request\\CalendarQueryReport';
+        $server->xml->elementMap['{' . self::NS_CALDAV . '}calendar-multiget'] = 'Sabre\\CalDAV\\Xml\\Request\\CalendarMultiGetReport';
+        $server->xml->elementMap['{' . self::NS_CALDAV . '}free-busy-query'] = 'Sabre\\CalDAV\\Xml\\Request\\FreeBusyQueryReport';
+        $server->xml->elementMap['{' . self::NS_CALDAV . '}mkcalendar'] = 'Sabre\\CalDAV\\Xml\\Request\\MkCalendar';
+        $server->xml->elementMap['{' . self::NS_CALDAV . '}schedule-calendar-transp'] = 'Sabre\\CalDAV\\Xml\\Property\\ScheduleCalendarTransp';
+        $server->xml->elementMap['{' . self::NS_CALDAV . '}supported-calendar-component-set'] = 'Sabre\\CalDAV\\Xml\\Property\\SupportedCalendarComponentSet';
+
+        $server->resourceTypeMapping['\\Sabre\\CalDAV\\ICalendar'] = '{urn:ietf:params:xml:ns:caldav}calendar';
+
+        $server->resourceTypeMapping['\\Sabre\\CalDAV\\Principal\\IProxyRead'] = '{http://calendarserver.org/ns/}calendar-proxy-read';
+        $server->resourceTypeMapping['\\Sabre\\CalDAV\\Principal\\IProxyWrite'] = '{http://calendarserver.org/ns/}calendar-proxy-write';
+
+        array_push($server->protectedProperties,
+
+            '{' . self::NS_CALDAV . '}supported-calendar-component-set',
+            '{' . self::NS_CALDAV . '}supported-calendar-data',
+            '{' . self::NS_CALDAV . '}max-resource-size',
+            '{' . self::NS_CALDAV . '}min-date-time',
+            '{' . self::NS_CALDAV . '}max-date-time',
+            '{' . self::NS_CALDAV . '}max-instances',
+            '{' . self::NS_CALDAV . '}max-attendees-per-instance',
+            '{' . self::NS_CALDAV . '}calendar-home-set',
+            '{' . self::NS_CALDAV . '}supported-collation-set',
+            '{' . self::NS_CALDAV . '}calendar-data',
+
+            // CalendarServer extensions
+            '{' . self::NS_CALENDARSERVER . '}getctag',
+            '{' . self::NS_CALENDARSERVER . '}calendar-proxy-read-for',
+            '{' . self::NS_CALENDARSERVER . '}calendar-proxy-write-for'
+
+        );
+
+        if ($aclPlugin = $server->getPlugin('acl')) {
+            $aclPlugin->principalSearchPropertySet['{' . self::NS_CALDAV . '}calendar-user-address-set'] = 'Calendar address';
+        }
+    }
+
+    /**
+     * This functions handles REPORT requests specific to CalDAV
+     *
+     * @param string $reportName
+     * @param mixed $report
+     * @return bool
+     */
+    function report($reportName, $report) {
+
+        switch ($reportName) {
+            case '{' . self::NS_CALDAV . '}calendar-multiget' :
+                $this->server->transactionType = 'report-calendar-multiget';
+                $this->calendarMultiGetReport($report);
+                return false;
+            case '{' . self::NS_CALDAV . '}calendar-query' :
+                $this->server->transactionType = 'report-calendar-query';
+                $this->calendarQueryReport($report);
+                return false;
+            case '{' . self::NS_CALDAV . '}free-busy-query' :
+                $this->server->transactionType = 'report-free-busy-query';
+                $this->freeBusyQueryReport($report);
+                return false;
+
+        }
+
+
+    }
+
+    /**
+     * This function handles the MKCALENDAR HTTP method, which creates
+     * a new calendar.
+     *
+     * @param RequestInterface $request
+     * @param ResponseInterface $response
+     * @return bool
+     */
+    function httpMkCalendar(RequestInterface $request, ResponseInterface $response) {
+
+        $body = $request->getBodyAsString();
+        $path = $request->getPath();
+
+        $properties = [];
+
+        if ($body) {
+
+            try {
+                $mkcalendar = $this->server->xml->expect(
+                    '{urn:ietf:params:xml:ns:caldav}mkcalendar',
+                    $body
+                );
+            } catch (\Sabre\Xml\ParseException $e) {
+                throw new BadRequest($e->getMessage(), null, $e);
+            }
+            $properties = $mkcalendar->getProperties();
+
+        }
+
+        // iCal abuses MKCALENDAR since iCal 10.9.2 to create server-stored
+        // subscriptions. Before that it used MKCOL which was the correct way
+        // to do this.
+        //
+        // If the body had a {DAV:}resourcetype, it means we stumbled upon this
+        // request, and we simply use it instead of the pre-defined list.
+        if (isset($properties['{DAV:}resourcetype'])) {
+            $resourceType = $properties['{DAV:}resourcetype']->getValue();
+        } else {
+            $resourceType = ['{DAV:}collection','{urn:ietf:params:xml:ns:caldav}calendar'];
+        }
+
+        $this->server->createCollection($path, new MkCol($resourceType, $properties));
+
+        $this->server->httpResponse->setStatus(201);
+        $this->server->httpResponse->setHeader('Content-Length', 0);
+
+        // This breaks the method chain.
+        return false;
+    }
+
+    /**
+     * PropFind
+     *
+     * This method handler is invoked before any after properties for a
+     * resource are fetched. This allows us to add in any CalDAV specific
+     * properties.
+     *
+     * @param DAV\PropFind $propFind
+     * @param DAV\INode $node
+     * @return void
+     */
+    function propFind(DAV\PropFind $propFind, DAV\INode $node) {
+
+        $ns = '{' . self::NS_CALDAV . '}';
+
+        if ($node instanceof ICalendarObjectContainer) {
+
+            $propFind->handle($ns . 'max-resource-size', $this->maxResourceSize);
+            $propFind->handle($ns . 'supported-calendar-data', function() {
+                return new Xml\Property\SupportedCalendarData();
+            });
+            $propFind->handle($ns . 'supported-collation-set', function() {
+                return new Xml\Property\SupportedCollationSet();
+            });
+
+        }
+
+        if ($node instanceof DAVACL\IPrincipal) {
+
+            $principalUrl = $node->getPrincipalUrl();
+
+            $propFind->handle('{' . self::NS_CALDAV . '}calendar-home-set', function() use ($principalUrl) {
+
+                $calendarHomePath = $this->getCalendarHomeForPrincipal($principalUrl);
+                if (is_null($calendarHomePath)) return null;
+                return new Href($calendarHomePath . '/');
+
+            });
+            // The calendar-user-address-set property is basically mapped to
+            // the {DAV:}alternate-URI-set property.
+            $propFind->handle('{' . self::NS_CALDAV . '}calendar-user-address-set', function() use ($node) {
+                $addresses = $node->getAlternateUriSet();
+                $addresses[] = $this->server->getBaseUri() . $node->getPrincipalUrl() . '/';
+                return new Href($addresses, false);
+            });
+            // For some reason somebody thought it was a good idea to add
+            // another one of these properties. We're supporting it too.
+            $propFind->handle('{' . self::NS_CALENDARSERVER . '}email-address-set', function() use ($node) {
+                $addresses = $node->getAlternateUriSet();
+                $emails = [];
+                foreach ($addresses as $address) {
+                    if (substr($address, 0, 7) === 'mailto:') {
+                        $emails[] = substr($address, 7);
+                    }
+                }
+                return new Xml\Property\EmailAddressSet($emails);
+            });
+
+            // These two properties are shortcuts for ical to easily find
+            // other principals this principal has access to.
+            $propRead = '{' . self::NS_CALENDARSERVER . '}calendar-proxy-read-for';
+            $propWrite = '{' . self::NS_CALENDARSERVER . '}calendar-proxy-write-for';
+
+            if ($propFind->getStatus($propRead) === 404 || $propFind->getStatus($propWrite) === 404) {
+
+                $aclPlugin = $this->server->getPlugin('acl');
+                $membership = $aclPlugin->getPrincipalMembership($propFind->getPath());
+                $readList = [];
+                $writeList = [];
+
+                foreach ($membership as $group) {
+
+                    $groupNode = $this->server->tree->getNodeForPath($group);
+
+                    $listItem = Uri\split($group)[0] . '/';
+
+                    // If the node is either ap proxy-read or proxy-write
+                    // group, we grab the parent principal and add it to the
+                    // list.
+                    if ($groupNode instanceof Principal\IProxyRead) {
+                        $readList[] = $listItem;
+                    }
+                    if ($groupNode instanceof Principal\IProxyWrite) {
+                        $writeList[] = $listItem;
+                    }
+
+                }
+
+                $propFind->set($propRead, new Href($readList));
+                $propFind->set($propWrite, new Href($writeList));
+
+            }
+
+        } // instanceof IPrincipal
+
+        if ($node instanceof ICalendarObject) {
+
+            // The calendar-data property is not supposed to be a 'real'
+            // property, but in large chunks of the spec it does act as such.
+            // Therefore we simply expose it as a property.
+            $propFind->handle('{' . self::NS_CALDAV . '}calendar-data', function() use ($node) {
+                $val = $node->get();
+                if (is_resource($val))
+                    $val = stream_get_contents($val);
+
+                // Taking out \r to not screw up the xml output
+                return str_replace("\r", "", $val);
+
+            });
+
+        }
+
+    }
+
+    /**
+     * This function handles the calendar-multiget REPORT.
+     *
+     * This report is used by the client to fetch the content of a series
+     * of urls. Effectively avoiding a lot of redundant requests.
+     *
+     * @param CalendarMultiGetReport $report
+     * @return void
+     */
+    function calendarMultiGetReport($report) {
+
+        $needsJson = $report->contentType === 'application/calendar+json';
+
+        $timeZones = [];
+        $propertyList = [];
+
+        $paths = array_map(
+            [$this->server, 'calculateUri'],
+            $report->hrefs
+        );
+
+        foreach ($this->server->getPropertiesForMultiplePaths($paths, $report->properties) as $uri => $objProps) {
+
+            if (($needsJson || $report->expand) && isset($objProps[200]['{' . self::NS_CALDAV . '}calendar-data'])) {
+                $vObject = VObject\Reader::read($objProps[200]['{' . self::NS_CALDAV . '}calendar-data']);
+
+                if ($report->expand) {
+                    // We're expanding, and for that we need to figure out the
+                    // calendar's timezone.
+                    list($calendarPath) = Uri\split($uri);
+                    if (!isset($timeZones[$calendarPath])) {
+                        // Checking the calendar-timezone property.
+                        $tzProp = '{' . self::NS_CALDAV . '}calendar-timezone';
+                        $tzResult = $this->server->getProperties($calendarPath, [$tzProp]);
+                        if (isset($tzResult[$tzProp])) {
+                            // This property contains a VCALENDAR with a single
+                            // VTIMEZONE.
+                            $vtimezoneObj = VObject\Reader::read($tzResult[$tzProp]);
+                            $timeZone = $vtimezoneObj->VTIMEZONE->getTimeZone();
+                        } else {
+                            // Defaulting to UTC.
+                            $timeZone = new DateTimeZone('UTC');
+                        }
+                        $timeZones[$calendarPath] = $timeZone;
+                    }
+
+                    $vObject = $vObject->expand($report->expand['start'], $report->expand['end'], $timeZones[$calendarPath]);
+                }
+                if ($needsJson) {
+                    $objProps[200]['{' . self::NS_CALDAV . '}calendar-data'] = json_encode($vObject->jsonSerialize());
+                } else {
+                    $objProps[200]['{' . self::NS_CALDAV . '}calendar-data'] = $vObject->serialize();
+                }
+                // Destroy circular references so PHP will garbage collect the
+                // object.
+                $vObject->destroy();
+            }
+
+            $propertyList[] = $objProps;
+
+        }
+
+        $prefer = $this->server->getHTTPPrefer();
+
+        $this->server->httpResponse->setStatus(207);
+        $this->server->httpResponse->setHeader('Content-Type', 'application/xml; charset=utf-8');
+        $this->server->httpResponse->setHeader('Vary', 'Brief,Prefer');
+        $this->server->httpResponse->setBody($this->server->generateMultiStatus($propertyList, $prefer['return'] === 'minimal'));
+
+    }
+
+    /**
+     * This function handles the calendar-query REPORT
+     *
+     * This report is used by clients to request calendar objects based on
+     * complex conditions.
+     *
+     * @param Xml\Request\CalendarQueryReport $report
+     * @return void
+     */
+    function calendarQueryReport($report) {
+
+        $path = $this->server->getRequestUri();
+
+        $needsJson = $report->contentType === 'application/calendar+json';
+
+        $node = $this->server->tree->getNodeForPath($this->server->getRequestUri());
+        $depth = $this->server->getHTTPDepth(0);
+
+        // The default result is an empty array
+        $result = [];
+
+        $calendarTimeZone = null;
+        if ($report->expand) {
+            // We're expanding, and for that we need to figure out the
+            // calendar's timezone.
+            $tzProp = '{' . self::NS_CALDAV . '}calendar-timezone';
+            $tzResult = $this->server->getProperties($path, [$tzProp]);
+            if (isset($tzResult[$tzProp])) {
+                // This property contains a VCALENDAR with a single
+                // VTIMEZONE.
+                $vtimezoneObj = VObject\Reader::read($tzResult[$tzProp]);
+                $calendarTimeZone = $vtimezoneObj->VTIMEZONE->getTimeZone();
+
+                // Destroy circular references so PHP will garbage collect the
+                // object.
+                $vtimezoneObj->destroy();
+            } else {
+                // Defaulting to UTC.
+                $calendarTimeZone = new DateTimeZone('UTC');
+            }
+        }
+
+        // The calendarobject was requested directly. In this case we handle
+        // this locally.
+        if ($depth == 0 && $node instanceof ICalendarObject) {
+
+            $requestedCalendarData = true;
+            $requestedProperties = $report->properties;
+
+            if (!in_array('{urn:ietf:params:xml:ns:caldav}calendar-data', $requestedProperties)) {
+
+                // We always retrieve calendar-data, as we need it for filtering.
+                $requestedProperties[] = '{urn:ietf:params:xml:ns:caldav}calendar-data';
+
+                // If calendar-data wasn't explicitly requested, we need to remove
+                // it after processing.
+                $requestedCalendarData = false;
+            }
+
+            $properties = $this->server->getPropertiesForPath(
+                $path,
+                $requestedProperties,
+                0
+            );
+
+            // This array should have only 1 element, the first calendar
+            // object.
+            $properties = current($properties);
+
+            // If there wasn't any calendar-data returned somehow, we ignore
+            // this.
+            if (isset($properties[200]['{urn:ietf:params:xml:ns:caldav}calendar-data'])) {
+
+                $validator = new CalendarQueryValidator();
+
+                $vObject = VObject\Reader::read($properties[200]['{urn:ietf:params:xml:ns:caldav}calendar-data']);
+                if ($validator->validate($vObject, $report->filters)) {
+
+                    // If the client didn't require the calendar-data property,
+                    // we won't give it back.
+                    if (!$requestedCalendarData) {
+                        unset($properties[200]['{urn:ietf:params:xml:ns:caldav}calendar-data']);
+                    } else {
+
+
+                        if ($report->expand) {
+                            $vObject = $vObject->expand($report->expand['start'], $report->expand['end'], $calendarTimeZone);
+                        }
+                        if ($needsJson) {
+                            $properties[200]['{' . self::NS_CALDAV . '}calendar-data'] = json_encode($vObject->jsonSerialize());
+                        } elseif ($report->expand) {
+                            $properties[200]['{' . self::NS_CALDAV . '}calendar-data'] = $vObject->serialize();
+                        }
+                    }
+
+                    $result = [$properties];
+
+                }
+                // Destroy circular references so PHP will garbage collect the
+                // object.
+                $vObject->destroy();
+
+            }
+
+        }
+
+        if ($node instanceof ICalendarObjectContainer && $depth === 0) {
+
+            if (strpos($this->server->httpRequest->getHeader('User-Agent'), 'MSFT-') === 0) {
+                // Microsoft clients incorrectly supplied depth as 0, when it actually
+                // should have set depth to 1. We're implementing a workaround here
+                // to deal with this.
+                //
+                // This targets at least the following clients:
+                //   Windows 10
+                //   Windows Phone 8, 10
+                $depth = 1;
+            } else {
+                throw new BadRequest('A calendar-query REPORT on a calendar with a Depth: 0 is undefined. Set Depth to 1');
+            }
+
+        }
+
+        // If we're dealing with a calendar, the calendar itself is responsible
+        // for the calendar-query.
+        if ($node instanceof ICalendarObjectContainer && $depth == 1) {
+
+            $nodePaths = $node->calendarQuery($report->filters);
+
+            foreach ($nodePaths as $path) {
+
+                list($properties) =
+                    $this->server->getPropertiesForPath($this->server->getRequestUri() . '/' . $path, $report->properties);
+
+                if (($needsJson || $report->expand)) {
+                    $vObject = VObject\Reader::read($properties[200]['{' . self::NS_CALDAV . '}calendar-data']);
+
+                    if ($report->expand) {
+                        $vObject = $vObject->expand($report->expand['start'], $report->expand['end'], $calendarTimeZone);
+                    }
+
+                    if ($needsJson) {
+                        $properties[200]['{' . self::NS_CALDAV . '}calendar-data'] = json_encode($vObject->jsonSerialize());
+                    } else {
+                        $properties[200]['{' . self::NS_CALDAV . '}calendar-data'] = $vObject->serialize();
+                    }
+
+                    // Destroy circular references so PHP will garbage collect the
+                    // object.
+                    $vObject->destroy();
+                }
+                $result[] = $properties;
+
+            }
+
+        }
+
+        $prefer = $this->server->getHTTPPrefer();
+
+        $this->server->httpResponse->setStatus(207);
+        $this->server->httpResponse->setHeader('Content-Type', 'application/xml; charset=utf-8');
+        $this->server->httpResponse->setHeader('Vary', 'Brief,Prefer');
+        $this->server->httpResponse->setBody($this->server->generateMultiStatus($result, $prefer['return'] === 'minimal'));
+
+    }
+
+    /**
+     * This method is responsible for parsing the request and generating the
+     * response for the CALDAV:free-busy-query REPORT.
+     *
+     * @param Xml\Request\FreeBusyQueryReport $report
+     * @return void
+     */
+    protected function freeBusyQueryReport(Xml\Request\FreeBusyQueryReport $report) {
+
+        $uri = $this->server->getRequestUri();
+
+        $acl = $this->server->getPlugin('acl');
+        if ($acl) {
+            $acl->checkPrivileges($uri, '{' . self::NS_CALDAV . '}read-free-busy');
+        }
+
+        $calendar = $this->server->tree->getNodeForPath($uri);
+        if (!$calendar instanceof ICalendar) {
+            throw new DAV\Exception\NotImplemented('The free-busy-query REPORT is only implemented on calendars');
+        }
+
+        $tzProp = '{' . self::NS_CALDAV . '}calendar-timezone';
+
+        // Figuring out the default timezone for the calendar, for floating
+        // times.
+        $calendarProps = $this->server->getProperties($uri, [$tzProp]);
+
+        if (isset($calendarProps[$tzProp])) {
+            $vtimezoneObj = VObject\Reader::read($calendarProps[$tzProp]);
+            $calendarTimeZone = $vtimezoneObj->VTIMEZONE->getTimeZone();
+            // Destroy circular references so PHP will garbage collect the object.
+            $vtimezoneObj->destroy();
+        } else {
+            $calendarTimeZone = new DateTimeZone('UTC');
+        }
+
+        // Doing a calendar-query first, to make sure we get the most
+        // performance.
+        $urls = $calendar->calendarQuery([
+            'name'         => 'VCALENDAR',
+            'comp-filters' => [
+                [
+                    'name'           => 'VEVENT',
+                    'comp-filters'   => [],
+                    'prop-filters'   => [],
+                    'is-not-defined' => false,
+                    'time-range'     => [
+                        'start' => $report->start,
+                        'end'   => $report->end,
+                    ],
+                ],
+            ],
+            'prop-filters'   => [],
+            'is-not-defined' => false,
+            'time-range'     => null,
+        ]);
+
+        $objects = array_map(function($url) use ($calendar) {
+            $obj = $calendar->getChild($url)->get();
+            return $obj;
+        }, $urls);
+
+        $generator = new VObject\FreeBusyGenerator();
+        $generator->setObjects($objects);
+        $generator->setTimeRange($report->start, $report->end);
+        $generator->setTimeZone($calendarTimeZone);
+        $result = $generator->getResult();
+        $result = $result->serialize();
+
+        $this->server->httpResponse->setStatus(200);
+        $this->server->httpResponse->setHeader('Content-Type', 'text/calendar');
+        $this->server->httpResponse->setHeader('Content-Length', strlen($result));
+        $this->server->httpResponse->setBody($result);
+
+    }
+
+    /**
+     * This method is triggered before a file gets updated with new content.
+     *
+     * This plugin uses this method to ensure that CalDAV objects receive
+     * valid calendar data.
+     *
+     * @param string $path
+     * @param DAV\IFile $node
+     * @param resource $data
+     * @param bool $modified Should be set to true, if this event handler
+     *                       changed &$data.
+     * @return void
+     */
+    function beforeWriteContent($path, DAV\IFile $node, &$data, &$modified) {
+
+        if (!$node instanceof ICalendarObject)
+            return;
+
+        // We're onyl interested in ICalendarObject nodes that are inside of a
+        // real calendar. This is to avoid triggering validation and scheduling
+        // for non-calendars (such as an inbox).
+        list($parent) = Uri\split($path);
+        $parentNode = $this->server->tree->getNodeForPath($parent);
+
+        if (!$parentNode instanceof ICalendar)
+            return;
+
+        $this->validateICalendar(
+            $data,
+            $path,
+            $modified,
+            $this->server->httpRequest,
+            $this->server->httpResponse,
+            false
+        );
+
+    }
+
+    /**
+     * This method is triggered before a new file is created.
+     *
+     * This plugin uses this method to ensure that newly created calendar
+     * objects contain valid calendar data.
+     *
+     * @param string $path
+     * @param resource $data
+     * @param DAV\ICollection $parentNode
+     * @param bool $modified Should be set to true, if this event handler
+     *                       changed &$data.
+     * @return void
+     */
+    function beforeCreateFile($path, &$data, DAV\ICollection $parentNode, &$modified) {
+
+        if (!$parentNode instanceof ICalendar)
+            return;
+
+        $this->validateICalendar(
+            $data,
+            $path,
+            $modified,
+            $this->server->httpRequest,
+            $this->server->httpResponse,
+            true
+        );
+
+    }
+
+    /**
+     * Checks if the submitted iCalendar data is in fact, valid.
+     *
+     * An exception is thrown if it's not.
+     *
+     * @param resource|string $data
+     * @param string $path
+     * @param bool $modified Should be set to true, if this event handler
+     *                       changed &$data.
+     * @param RequestInterface $request The http request.
+     * @param ResponseInterface $response The http response.
+     * @param bool $isNew Is the item a new one, or an update.
+     * @return void
+     */
+    protected function validateICalendar(&$data, $path, &$modified, RequestInterface $request, ResponseInterface $response, $isNew) {
+
+        // If it's a stream, we convert it to a string first.
+        if (is_resource($data)) {
+            $data = stream_get_contents($data);
+        }
+
+        $before = md5($data);
+        // Converting the data to unicode, if needed.
+        $data = DAV\StringUtil::ensureUTF8($data);
+
+        if ($before !== md5($data)) $modified = true;
+
+        try {
+
+            // If the data starts with a [, we can reasonably assume we're dealing
+            // with a jCal object.
+            if (substr($data, 0, 1) === '[') {
+                $vobj = VObject\Reader::readJson($data);
+
+                // Converting $data back to iCalendar, as that's what we
+                // technically support everywhere.
+                $data = $vobj->serialize();
+                $modified = true;
+            } else {
+                $vobj = VObject\Reader::read($data);
+            }
+
+        } catch (VObject\ParseException $e) {
+
+            throw new DAV\Exception\UnsupportedMediaType('This resource only supports valid iCalendar 2.0 data. Parse error: ' . $e->getMessage());
+
+        }
+
+        if ($vobj->name !== 'VCALENDAR') {
+            throw new DAV\Exception\UnsupportedMediaType('This collection can only support iCalendar objects.');
+        }
+
+        $sCCS = '{urn:ietf:params:xml:ns:caldav}supported-calendar-component-set';
+
+        // Get the Supported Components for the target calendar
+        list($parentPath) = Uri\split($path);
+        $calendarProperties = $this->server->getProperties($parentPath, [$sCCS]);
+
+        if (isset($calendarProperties[$sCCS])) {
+            $supportedComponents = $calendarProperties[$sCCS]->getValue();
+        } else {
+            $supportedComponents = ['VJOURNAL', 'VTODO', 'VEVENT'];
+        }
+
+        $foundType = null;
+        $foundUID = null;
+        foreach ($vobj->getComponents() as $component) {
+            switch ($component->name) {
+                case 'VTIMEZONE' :
+                    continue 2;
+                case 'VEVENT' :
+                case 'VTODO' :
+                case 'VJOURNAL' :
+                    if (is_null($foundType)) {
+                        $foundType = $component->name;
+                        if (!in_array($foundType, $supportedComponents)) {
+                            throw new Exception\InvalidComponentType('This calendar only supports ' . implode(', ', $supportedComponents) . '. We found a ' . $foundType);
+                        }
+                        if (!isset($component->UID)) {
+                            throw new DAV\Exception\BadRequest('Every ' . $component->name . ' component must have an UID');
+                        }
+                        $foundUID = (string)$component->UID;
+                    } else {
+                        if ($foundType !== $component->name) {
+                            throw new DAV\Exception\BadRequest('A calendar object must only contain 1 component. We found a ' . $component->name . ' as well as a ' . $foundType);
+                        }
+                        if ($foundUID !== (string)$component->UID) {
+                            throw new DAV\Exception\BadRequest('Every ' . $component->name . ' in this object must have identical UIDs');
+                        }
+                    }
+                    break;
+                default :
+                    throw new DAV\Exception\BadRequest('You are not allowed to create components of type: ' . $component->name . ' here');
+
+            }
+        }
+        if (!$foundType)
+            throw new DAV\Exception\BadRequest('iCalendar object must contain at least 1 of VEVENT, VTODO or VJOURNAL');
+
+        // We use an extra variable to allow event handles to tell us wether
+        // the object was modified or not.
+        //
+        // This helps us determine if we need to re-serialize the object.
+        $subModified = false;
+
+        $this->server->emit(
+            'calendarObjectChange',
+            [
+                $request,
+                $response,
+                $vobj,
+                $parentPath,
+                &$subModified,
+                $isNew
+            ]
+        );
+
+        if ($subModified) {
+            // An event handler told us that it modified the object.
+            $data = $vobj->serialize();
+
+            // Using md5 to figure out if there was an *actual* change.
+            if (!$modified && $before !== md5($data)) {
+                $modified = true;
+            }
+
+        }
+
+        // Destroy circular references so PHP will garbage collect the object.
+        $vobj->destroy();
+
+    }
+
+
+    /**
+     * This method is used to generate HTML output for the
+     * DAV\Browser\Plugin. This allows us to generate an interface users
+     * can use to create new calendars.
+     *
+     * @param DAV\INode $node
+     * @param string $output
+     * @return bool
+     */
+    function htmlActionsPanel(DAV\INode $node, &$output) {
+
+        if (!$node instanceof CalendarHome)
+            return;
+
+        $output .= '<tr><td colspan="2"><form method="post" action="">
+            <h3>Create new calendar</h3>
+            <input type="hidden" name="sabreAction" value="mkcol" />
+            <input type="hidden" name="resourceType" value="{DAV:}collection,{' . self::NS_CALDAV . '}calendar" />
+            <label>Name (uri):</label> <input type="text" name="name" /><br />
+            <label>Display name:</label> <input type="text" name="{DAV:}displayname" /><br />
+            <input type="submit" value="create" />
+            </form>
+            </td></tr>';
+
+        return false;
+
+    }
+
+    /**
+     * This event is triggered after GET requests.
+     *
+     * This is used to transform data into jCal, if this was requested.
+     *
+     * @param RequestInterface $request
+     * @param ResponseInterface $response
+     * @return void
+     */
+    function httpAfterGet(RequestInterface $request, ResponseInterface $response) {
+
+        if (strpos($response->getHeader('Content-Type'), 'text/calendar') === false) {
+            return;
+        }
+
+        $result = HTTP\Util::negotiate(
+            $request->getHeader('Accept'),
+            ['text/calendar', 'application/calendar+json']
+        );
+
+        if ($result !== 'application/calendar+json') {
+            // Do nothing
+            return;
+        }
+
+        // Transforming.
+        $vobj = VObject\Reader::read($response->getBody());
+
+        $jsonBody = json_encode($vobj->jsonSerialize());
+        $response->setBody($jsonBody);
+
+        // Destroy circular references so PHP will garbage collect the object.
+        $vobj->destroy();
+
+        $response->setHeader('Content-Type', 'application/calendar+json');
+        $response->setHeader('Content-Length', strlen($jsonBody));
+
+    }
+
+    /**
+     * Returns a bunch of meta-data about the plugin.
+     *
+     * Providing this information is optional, and is mainly displayed by the
+     * Browser plugin.
+     *
+     * The description key in the returned array may contain html and will not
+     * be sanitized.
+     *
+     * @return array
+     */
+    function getPluginInfo() {
+
+        return [
+            'name'        => $this->getPluginName(),
+            'description' => 'Adds support for CalDAV (rfc4791)',
+            'link'        => 'http://sabre.io/dav/caldav/',
+        ];
+
+    }
+
+}

+ 33 - 0
lib/sabre/sabre/dav/lib/CalDAV/Principal/Collection.php

@@ -0,0 +1,33 @@
+<?php
+
+namespace Sabre\CalDAV\Principal;
+
+use Sabre\DAVACL;
+
+/**
+ * Principal collection
+ *
+ * This is an alternative collection to the standard ACL principal collection.
+ * This collection adds support for the calendar-proxy-read and
+ * calendar-proxy-write sub-principals, as defined by the caldav-proxy
+ * specification.
+ *
+ * @copyright Copyright (C) fruux GmbH (https://fruux.com/)
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+class Collection extends DAVACL\PrincipalCollection {
+
+    /**
+     * Returns a child object based on principal information
+     *
+     * @param array $principalInfo
+     * @return User
+     */
+    function getChildForPrincipal(array $principalInfo) {
+
+        return new User($this->principalBackend, $principalInfo);
+
+    }
+
+}

+ 19 - 0
lib/sabre/sabre/dav/lib/CalDAV/Principal/IProxyRead.php

@@ -0,0 +1,19 @@
+<?php
+
+namespace Sabre\CalDAV\Principal;
+
+use Sabre\DAVACL;
+
+/**
+ * ProxyRead principal interface
+ *
+ * Any principal node implementing this interface will be picked up as a 'proxy
+ * principal group'.
+ *
+ * @copyright Copyright (C) fruux GmbH (https://fruux.com/)
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+interface IProxyRead extends DAVACL\IPrincipal {
+
+}

+ 19 - 0
lib/sabre/sabre/dav/lib/CalDAV/Principal/IProxyWrite.php

@@ -0,0 +1,19 @@
+<?php
+
+namespace Sabre\CalDAV\Principal;
+
+use Sabre\DAVACL;
+
+/**
+ * ProxyWrite principal interface
+ *
+ * Any principal node implementing this interface will be picked up as a 'proxy
+ * principal group'.
+ *
+ * @copyright Copyright (C) fruux GmbH (https://fruux.com/)
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+interface IProxyWrite extends DAVACL\IPrincipal {
+
+}

+ 181 - 0
lib/sabre/sabre/dav/lib/CalDAV/Principal/ProxyRead.php

@@ -0,0 +1,181 @@
+<?php
+
+namespace Sabre\CalDAV\Principal;
+
+use Sabre\DAVACL;
+use Sabre\DAV;
+
+/**
+ * ProxyRead principal
+ *
+ * This class represents a principal group, hosted under the main principal.
+ * This is needed to implement 'Calendar delegation' support. This class is
+ * instantiated by User.
+ *
+ * @copyright Copyright (C) fruux GmbH (https://fruux.com/)
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+class ProxyRead implements IProxyRead {
+
+    /**
+     * Principal information from the parent principal.
+     *
+     * @var array
+     */
+    protected $principalInfo;
+
+    /**
+     * Principal backend
+     *
+     * @var DAVACL\PrincipalBackend\BackendInterface
+     */
+    protected $principalBackend;
+
+    /**
+     * Creates the object.
+     *
+     * Note that you MUST supply the parent principal information.
+     *
+     * @param DAVACL\PrincipalBackend\BackendInterface $principalBackend
+     * @param array $principalInfo
+     */
+    function __construct(DAVACL\PrincipalBackend\BackendInterface $principalBackend, array $principalInfo) {
+
+        $this->principalInfo = $principalInfo;
+        $this->principalBackend = $principalBackend;
+
+    }
+
+    /**
+     * Returns this principals name.
+     *
+     * @return string
+     */
+    function getName() {
+
+        return 'calendar-proxy-read';
+
+    }
+
+    /**
+     * Returns the last modification time
+     *
+     * @return null
+     */
+    function getLastModified() {
+
+        return null;
+
+    }
+
+    /**
+     * Deletes the current node
+     *
+     * @throws DAV\Exception\Forbidden
+     * @return void
+     */
+    function delete() {
+
+        throw new DAV\Exception\Forbidden('Permission denied to delete node');
+
+    }
+
+    /**
+     * Renames the node
+     *
+     * @throws DAV\Exception\Forbidden
+     * @param string $name The new name
+     * @return void
+     */
+    function setName($name) {
+
+        throw new DAV\Exception\Forbidden('Permission denied to rename file');
+
+    }
+
+
+    /**
+     * Returns a list of alternative urls for a principal
+     *
+     * This can for example be an email address, or ldap url.
+     *
+     * @return array
+     */
+    function getAlternateUriSet() {
+
+        return [];
+
+    }
+
+    /**
+     * Returns the full principal url
+     *
+     * @return string
+     */
+    function getPrincipalUrl() {
+
+        return $this->principalInfo['uri'] . '/' . $this->getName();
+
+    }
+
+    /**
+     * Returns the list of group members
+     *
+     * If this principal is a group, this function should return
+     * all member principal uri's for the group.
+     *
+     * @return array
+     */
+    function getGroupMemberSet() {
+
+        return $this->principalBackend->getGroupMemberSet($this->getPrincipalUrl());
+
+    }
+
+    /**
+     * Returns the list of groups this principal is member of
+     *
+     * If this principal is a member of a (list of) groups, this function
+     * should return a list of principal uri's for it's members.
+     *
+     * @return array
+     */
+    function getGroupMembership() {
+
+        return $this->principalBackend->getGroupMembership($this->getPrincipalUrl());
+
+    }
+
+    /**
+     * Sets a list of group members
+     *
+     * If this principal is a group, this method sets all the group members.
+     * The list of members is always overwritten, never appended to.
+     *
+     * This method should throw an exception if the members could not be set.
+     *
+     * @param array $principals
+     * @return void
+     */
+    function setGroupMemberSet(array $principals) {
+
+        $this->principalBackend->setGroupMemberSet($this->getPrincipalUrl(), $principals);
+
+    }
+
+    /**
+     * Returns the displayname
+     *
+     * This should be a human readable name for the principal.
+     * If none is available, return the nodename.
+     *
+     * @return string
+     */
+    function getDisplayName() {
+
+        return $this->getName();
+
+    }
+
+}

+ 181 - 0
lib/sabre/sabre/dav/lib/CalDAV/Principal/ProxyWrite.php

@@ -0,0 +1,181 @@
+<?php
+
+namespace Sabre\CalDAV\Principal;
+
+use Sabre\DAVACL;
+use Sabre\DAV;
+
+/**
+ * ProxyWrite principal
+ *
+ * This class represents a principal group, hosted under the main principal.
+ * This is needed to implement 'Calendar delegation' support. This class is
+ * instantiated by User.
+ *
+ * @copyright Copyright (C) fruux GmbH (https://fruux.com/)
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+class ProxyWrite implements IProxyWrite {
+
+    /**
+     * Parent principal information
+     *
+     * @var array
+     */
+    protected $principalInfo;
+
+    /**
+     * Principal Backend
+     *
+     * @var DAVACL\PrincipalBackend\BackendInterface
+     */
+    protected $principalBackend;
+
+    /**
+     * Creates the object
+     *
+     * Note that you MUST supply the parent principal information.
+     *
+     * @param DAVACL\PrincipalBackend\BackendInterface $principalBackend
+     * @param array $principalInfo
+     */
+    function __construct(DAVACL\PrincipalBackend\BackendInterface $principalBackend, array $principalInfo) {
+
+        $this->principalInfo = $principalInfo;
+        $this->principalBackend = $principalBackend;
+
+    }
+
+    /**
+     * Returns this principals name.
+     *
+     * @return string
+     */
+    function getName() {
+
+        return 'calendar-proxy-write';
+
+    }
+
+    /**
+     * Returns the last modification time
+     *
+     * @return null
+     */
+    function getLastModified() {
+
+        return null;
+
+    }
+
+    /**
+     * Deletes the current node
+     *
+     * @throws DAV\Exception\Forbidden
+     * @return void
+     */
+    function delete() {
+
+        throw new DAV\Exception\Forbidden('Permission denied to delete node');
+
+    }
+
+    /**
+     * Renames the node
+     *
+     * @throws DAV\Exception\Forbidden
+     * @param string $name The new name
+     * @return void
+     */
+    function setName($name) {
+
+        throw new DAV\Exception\Forbidden('Permission denied to rename file');
+
+    }
+
+
+    /**
+     * Returns a list of alternative urls for a principal
+     *
+     * This can for example be an email address, or ldap url.
+     *
+     * @return array
+     */
+    function getAlternateUriSet() {
+
+        return [];
+
+    }
+
+    /**
+     * Returns the full principal url
+     *
+     * @return string
+     */
+    function getPrincipalUrl() {
+
+        return $this->principalInfo['uri'] . '/' . $this->getName();
+
+    }
+
+    /**
+     * Returns the list of group members
+     *
+     * If this principal is a group, this function should return
+     * all member principal uri's for the group.
+     *
+     * @return array
+     */
+    function getGroupMemberSet() {
+
+        return $this->principalBackend->getGroupMemberSet($this->getPrincipalUrl());
+
+    }
+
+    /**
+     * Returns the list of groups this principal is member of
+     *
+     * If this principal is a member of a (list of) groups, this function
+     * should return a list of principal uri's for it's members.
+     *
+     * @return array
+     */
+    function getGroupMembership() {
+
+        return $this->principalBackend->getGroupMembership($this->getPrincipalUrl());
+
+    }
+
+    /**
+     * Sets a list of group members
+     *
+     * If this principal is a group, this method sets all the group members.
+     * The list of members is always overwritten, never appended to.
+     *
+     * This method should throw an exception if the members could not be set.
+     *
+     * @param array $principals
+     * @return void
+     */
+    function setGroupMemberSet(array $principals) {
+
+        $this->principalBackend->setGroupMemberSet($this->getPrincipalUrl(), $principals);
+
+    }
+
+    /**
+     * Returns the displayname
+     *
+     * This should be a human readable name for the principal.
+     * If none is available, return the nodename.
+     *
+     * @return string
+     */
+    function getDisplayName() {
+
+        return $this->getName();
+
+    }
+
+}

+ 135 - 0
lib/sabre/sabre/dav/lib/CalDAV/Principal/User.php

@@ -0,0 +1,135 @@
+<?php
+
+namespace Sabre\CalDAV\Principal;
+
+use Sabre\DAV;
+use Sabre\DAVACL;
+
+/**
+ * CalDAV principal
+ *
+ * This is a standard user-principal for CalDAV. This principal is also a
+ * collection and returns the caldav-proxy-read and caldav-proxy-write child
+ * principals.
+ *
+ * @copyright Copyright (C) fruux GmbH (https://fruux.com/)
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+class User extends DAVACL\Principal implements DAV\ICollection {
+
+    /**
+     * Creates a new file in the directory
+     *
+     * @param string $name Name of the file
+     * @param resource $data Initial payload, passed as a readable stream resource.
+     * @throws DAV\Exception\Forbidden
+     * @return void
+     */
+    function createFile($name, $data = null) {
+
+        throw new DAV\Exception\Forbidden('Permission denied to create file (filename ' . $name . ')');
+
+    }
+
+    /**
+     * Creates a new subdirectory
+     *
+     * @param string $name
+     * @throws DAV\Exception\Forbidden
+     * @return void
+     */
+    function createDirectory($name) {
+
+        throw new DAV\Exception\Forbidden('Permission denied to create directory');
+
+    }
+
+    /**
+     * Returns a specific child node, referenced by its name
+     *
+     * @param string $name
+     * @return DAV\INode
+     */
+    function getChild($name) {
+
+        $principal = $this->principalBackend->getPrincipalByPath($this->getPrincipalURL() . '/' . $name);
+        if (!$principal) {
+            throw new DAV\Exception\NotFound('Node with name ' . $name . ' was not found');
+        }
+        if ($name === 'calendar-proxy-read')
+            return new ProxyRead($this->principalBackend, $this->principalProperties);
+
+        if ($name === 'calendar-proxy-write')
+            return new ProxyWrite($this->principalBackend, $this->principalProperties);
+
+        throw new DAV\Exception\NotFound('Node with name ' . $name . ' was not found');
+
+    }
+
+    /**
+     * Returns an array with all the child nodes
+     *
+     * @return DAV\INode[]
+     */
+    function getChildren() {
+
+        $r = [];
+        if ($this->principalBackend->getPrincipalByPath($this->getPrincipalURL() . '/calendar-proxy-read')) {
+            $r[] = new ProxyRead($this->principalBackend, $this->principalProperties);
+        }
+        if ($this->principalBackend->getPrincipalByPath($this->getPrincipalURL() . '/calendar-proxy-write')) {
+            $r[] = new ProxyWrite($this->principalBackend, $this->principalProperties);
+        }
+
+        return $r;
+
+    }
+
+    /**
+     * Returns whether or not the child node exists
+     *
+     * @param string $name
+     * @return bool
+     */
+    function childExists($name) {
+
+        try {
+            $this->getChild($name);
+            return true;
+        } catch (DAV\Exception\NotFound $e) {
+            return false;
+        }
+
+    }
+
+    /**
+     * Returns a list of ACE's for this node.
+     *
+     * Each ACE has the following properties:
+     *   * 'privilege', a string such as {DAV:}read or {DAV:}write. These are
+     *     currently the only supported privileges
+     *   * 'principal', a url to the principal who owns the node
+     *   * 'protected' (optional), indicating that this ACE is not allowed to
+     *      be updated.
+     *
+     * @return array
+     */
+    function getACL() {
+
+        $acl = parent::getACL();
+        $acl[] = [
+            'privilege' => '{DAV:}read',
+            'principal' => $this->principalProperties['uri'] . '/calendar-proxy-read',
+            'protected' => true,
+        ];
+        $acl[] = [
+            'privilege' => '{DAV:}read',
+            'principal' => $this->principalProperties['uri'] . '/calendar-proxy-write',
+            'protected' => true,
+        ];
+        return $acl;
+
+    }
+
+}

+ 15 - 0
lib/sabre/sabre/dav/lib/CalDAV/Schedule/IInbox.php

@@ -0,0 +1,15 @@
+<?php
+
+namespace Sabre\CalDAV\Schedule;
+
+/**
+ * Implement this interface to have a node be recognized as a CalDAV scheduling
+ * inbox.
+ *
+ * @copyright Copyright (C) fruux GmbH (https://fruux.com/)
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+interface IInbox extends \Sabre\CalDAV\ICalendarObjectContainer, \Sabre\DAVACL\IACL {
+
+}

+ 190 - 0
lib/sabre/sabre/dav/lib/CalDAV/Schedule/IMipPlugin.php

@@ -0,0 +1,190 @@
+<?php
+
+namespace Sabre\CalDAV\Schedule;
+
+use Sabre\DAV;
+use Sabre\VObject\ITip;
+
+/**
+ * iMIP handler.
+ *
+ * This class is responsible for sending out iMIP messages. iMIP is the
+ * email-based transport for iTIP. iTIP deals with scheduling operations for
+ * iCalendar objects.
+ *
+ * If you want to customize the email that gets sent out, you can do so by
+ * extending this class and overriding the sendMessage method.
+ *
+ * @copyright Copyright (C) fruux GmbH (https://fruux.com/)
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+class IMipPlugin extends DAV\ServerPlugin {
+
+    /**
+     * Email address used in From: header.
+     *
+     * @var string
+     */
+    protected $senderEmail;
+
+    /**
+     * ITipMessage
+     *
+     * @var ITip\Message
+     */
+    protected $itipMessage;
+
+    /**
+     * Creates the email handler.
+     *
+     * @param string $senderEmail. The 'senderEmail' is the email that shows up
+     *                             in the 'From:' address. This should
+     *                             generally be some kind of no-reply email
+     *                             address you own.
+     */
+    function __construct($senderEmail) {
+
+        $this->senderEmail = $senderEmail;
+
+    }
+
+    /*
+     * This initializes the plugin.
+     *
+     * This function is called by Sabre\DAV\Server, after
+     * addPlugin is called.
+     *
+     * This method should set up the required event subscriptions.
+     *
+     * @param DAV\Server $server
+     * @return void
+     */
+    function initialize(DAV\Server $server) {
+
+        $server->on('schedule', [$this, 'schedule'], 120);
+
+    }
+
+    /**
+     * Returns a plugin name.
+     *
+     * Using this name other plugins will be able to access other plugins
+     * using \Sabre\DAV\Server::getPlugin
+     *
+     * @return string
+     */
+    function getPluginName() {
+
+        return 'imip';
+
+    }
+
+    /**
+     * Event handler for the 'schedule' event.
+     *
+     * @param ITip\Message $iTipMessage
+     * @return void
+     */
+    function schedule(ITip\Message $iTipMessage) {
+
+        // Not sending any emails if the system considers the update
+        // insignificant.
+        if (!$iTipMessage->significantChange) {
+            if (!$iTipMessage->scheduleStatus) {
+                $iTipMessage->scheduleStatus = '1.0;We got the message, but it\'s not significant enough to warrant an email';
+            }
+            return;
+        }
+
+        $summary = $iTipMessage->message->VEVENT->SUMMARY;
+
+        if (parse_url($iTipMessage->sender, PHP_URL_SCHEME) !== 'mailto')
+            return;
+
+        if (parse_url($iTipMessage->recipient, PHP_URL_SCHEME) !== 'mailto')
+            return;
+
+        $sender = substr($iTipMessage->sender, 7);
+        $recipient = substr($iTipMessage->recipient, 7);
+
+        if ($iTipMessage->senderName) {
+            $sender = $iTipMessage->senderName . ' <' . $sender . '>';
+        }
+        if ($iTipMessage->recipientName) {
+            $recipient = $iTipMessage->recipientName . ' <' . $recipient . '>';
+        }
+
+        $subject = 'SabreDAV iTIP message';
+        switch (strtoupper($iTipMessage->method)) {
+            case 'REPLY' :
+                $subject = 'Re: ' . $summary;
+                break;
+            case 'REQUEST' :
+                $subject = $summary;
+                break;
+            case 'CANCEL' :
+                $subject = 'Cancelled: ' . $summary;
+                break;
+        }
+
+        $headers = [
+            'Reply-To: ' . $sender,
+            'From: ' . $this->senderEmail,
+            'Content-Type: text/calendar; charset=UTF-8; method=' . $iTipMessage->method,
+        ];
+        if (DAV\Server::$exposeVersion) {
+            $headers[] = 'X-Sabre-Version: ' . DAV\Version::VERSION;
+        }
+        $this->mail(
+            $recipient,
+            $subject,
+            $iTipMessage->message->serialize(),
+            $headers
+        );
+        $iTipMessage->scheduleStatus = '1.1; Scheduling message is sent via iMip';
+
+    }
+
+    // @codeCoverageIgnoreStart
+    // This is deemed untestable in a reasonable manner
+
+    /**
+     * This function is responsible for sending the actual email.
+     *
+     * @param string $to Recipient email address
+     * @param string $subject Subject of the email
+     * @param string $body iCalendar body
+     * @param array $headers List of headers
+     * @return void
+     */
+    protected function mail($to, $subject, $body, array $headers) {
+
+        mail($to, $subject, $body, implode("\r\n", $headers));
+
+    }
+
+    // @codeCoverageIgnoreEnd
+
+    /**
+     * Returns a bunch of meta-data about the plugin.
+     *
+     * Providing this information is optional, and is mainly displayed by the
+     * Browser plugin.
+     *
+     * The description key in the returned array may contain html and will not
+     * be sanitized.
+     *
+     * @return array
+     */
+    function getPluginInfo() {
+
+        return [
+            'name'        => $this->getPluginName(),
+            'description' => 'Email delivery (rfc6037) for CalDAV scheduling',
+            'link'        => 'http://sabre.io/dav/scheduling/',
+        ];
+
+    }
+
+}

+ 15 - 0
lib/sabre/sabre/dav/lib/CalDAV/Schedule/IOutbox.php

@@ -0,0 +1,15 @@
+<?php
+
+namespace Sabre\CalDAV\Schedule;
+
+/**
+ * Implement this interface to have a node be recognized as a CalDAV scheduling
+ * outbox.
+ *
+ * @copyright Copyright (C) fruux GmbH (https://fruux.com/)
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+interface IOutbox extends \Sabre\DAV\ICollection, \Sabre\DAVACL\IACL {
+
+}

+ 13 - 0
lib/sabre/sabre/dav/lib/CalDAV/Schedule/ISchedulingObject.php

@@ -0,0 +1,13 @@
+<?php
+
+namespace Sabre\CalDAV\Schedule;
+
+/**
+ * The SchedulingObject represents a scheduling object in the Inbox collection
+ *
+ * @license http://sabre.io/license/ Modified BSD License
+ * @copyright Copyright (C) fruux GmbH (https://fruux.com/)
+ */
+interface ISchedulingObject extends \Sabre\CalDAV\ICalendarObject {
+
+}

+ 261 - 0
lib/sabre/sabre/dav/lib/CalDAV/Schedule/Inbox.php

@@ -0,0 +1,261 @@
+<?php
+
+namespace Sabre\CalDAV\Schedule;
+
+use Sabre\DAV;
+use Sabre\CalDAV;
+use Sabre\DAVACL;
+use Sabre\CalDAV\Backend;
+use Sabre\VObject;
+
+/**
+ * The CalDAV scheduling inbox
+ *
+ * @copyright Copyright (C) fruux GmbH (https://fruux.com/)
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+class Inbox extends DAV\Collection implements IInbox {
+
+    /**
+     * CalDAV backend
+     *
+     * @var Backend\BackendInterface
+     */
+    protected $caldavBackend;
+
+    /**
+     * The principal Uri
+     *
+     * @var string
+     */
+    protected $principalUri;
+
+    /**
+     * Constructor
+     *
+     * @param Backend\SchedulingSupport $caldavBackend
+     * @param string $principalUri
+     */
+    function __construct(Backend\SchedulingSupport $caldavBackend, $principalUri) {
+
+        $this->caldavBackend = $caldavBackend;
+        $this->principalUri = $principalUri;
+
+    }
+
+    /**
+     * Returns the name of the node.
+     *
+     * This is used to generate the url.
+     *
+     * @return string
+     */
+    function getName() {
+
+        return 'inbox';
+
+    }
+
+    /**
+     * Returns an array with all the child nodes
+     *
+     * @return \Sabre\DAV\INode[]
+     */
+    function getChildren() {
+
+        $objs = $this->caldavBackend->getSchedulingObjects($this->principalUri);
+        $children = [];
+        foreach ($objs as $obj) {
+            //$obj['acl'] = $this->getACL();
+            $obj['principaluri'] = $this->principalUri;
+            $children[] = new SchedulingObject($this->caldavBackend, $obj);
+        }
+        return $children;
+
+    }
+
+    /**
+     * Creates a new file in the directory
+     *
+     * Data will either be supplied as a stream resource, or in certain cases
+     * as a string. Keep in mind that you may have to support either.
+     *
+     * After succesful creation of the file, you may choose to return the ETag
+     * of the new file here.
+     *
+     * The returned ETag must be surrounded by double-quotes (The quotes should
+     * be part of the actual string).
+     *
+     * If you cannot accurately determine the ETag, you should not return it.
+     * If you don't store the file exactly as-is (you're transforming it
+     * somehow) you should also not return an ETag.
+     *
+     * This means that if a subsequent GET to this new file does not exactly
+     * return the same contents of what was submitted here, you are strongly
+     * recommended to omit the ETag.
+     *
+     * @param string $name Name of the file
+     * @param resource|string $data Initial payload
+     * @return null|string
+     */
+    function createFile($name, $data = null) {
+
+        $this->caldavBackend->createSchedulingObject($this->principalUri, $name, $data);
+
+    }
+
+    /**
+     * Returns the owner principal
+     *
+     * This must be a url to a principal, or null if there's no owner
+     *
+     * @return string|null
+     */
+    function getOwner() {
+
+        return $this->principalUri;
+
+    }
+
+    /**
+     * Returns a group principal
+     *
+     * This must be a url to a principal, or null if there's no owner
+     *
+     * @return string|null
+     */
+    function getGroup() {
+
+        return null;
+
+    }
+
+    /**
+     * Returns a list of ACE's for this node.
+     *
+     * Each ACE has the following properties:
+     *   * 'privilege', a string such as {DAV:}read or {DAV:}write. These are
+     *     currently the only supported privileges
+     *   * 'principal', a url to the principal who owns the node
+     *   * 'protected' (optional), indicating that this ACE is not allowed to
+     *      be updated.
+     *
+     * @return array
+     */
+    function getACL() {
+
+        return [
+            [
+                'privilege' => '{DAV:}read',
+                'principal' => '{DAV:}authenticated',
+                'protected' => true,
+            ],
+            [
+                'privilege' => '{DAV:}write-properties',
+                'principal' => $this->getOwner(),
+                'protected' => true,
+            ],
+            [
+                'privilege' => '{DAV:}unbind',
+                'principal' => $this->getOwner(),
+                'protected' => true,
+            ],
+            [
+                'privilege' => '{DAV:}unbind',
+                'principal' => $this->getOwner() . '/calendar-proxy-write',
+                'protected' => true,
+            ],
+            [
+                'privilege' => '{' . CalDAV\Plugin::NS_CALDAV . '}schedule-deliver-invite',
+                'principal' => '{DAV:}authenticated',
+                'protected' => true,
+            ],
+            [
+                'privilege' => '{' . CalDAV\Plugin::NS_CALDAV . '}schedule-deliver-reply',
+                'principal' => '{DAV:}authenticated',
+                'protected' => true,
+            ],
+        ];
+
+    }
+
+    /**
+     * Updates the ACL
+     *
+     * This method will receive a list of new ACE's.
+     *
+     * @param array $acl
+     * @return void
+     */
+    function setACL(array $acl) {
+
+        throw new DAV\Exception\MethodNotAllowed('You\'re not allowed to update the ACL');
+
+    }
+
+    /**
+     * Returns the list of supported privileges for this node.
+     *
+     * The returned data structure is a list of nested privileges.
+     * See Sabre\DAVACL\Plugin::getDefaultSupportedPrivilegeSet for a simple
+     * standard structure.
+     *
+     * If null is returned from this method, the default privilege set is used,
+     * which is fine for most common usecases.
+     *
+     * @return array|null
+     */
+    function getSupportedPrivilegeSet() {
+
+        $ns = '{' . CalDAV\Plugin::NS_CALDAV . '}';
+
+        $default = DAVACL\Plugin::getDefaultSupportedPrivilegeSet();
+        $default['aggregates'][] = [
+            'privilege'  => $ns . 'schedule-deliver',
+            'aggregates' => [
+               ['privilege' => $ns . 'schedule-deliver-invite'],
+               ['privilege' => $ns . 'schedule-deliver-reply'],
+            ],
+        ];
+        return $default;
+
+    }
+
+    /**
+     * Performs a calendar-query on the contents of this calendar.
+     *
+     * The calendar-query is defined in RFC4791 : CalDAV. Using the
+     * calendar-query it is possible for a client to request a specific set of
+     * object, based on contents of iCalendar properties, date-ranges and
+     * iCalendar component types (VTODO, VEVENT).
+     *
+     * This method should just return a list of (relative) urls that match this
+     * query.
+     *
+     * The list of filters are specified as an array. The exact array is
+     * documented by \Sabre\CalDAV\CalendarQueryParser.
+     *
+     * @param array $filters
+     * @return array
+     */
+    function calendarQuery(array $filters) {
+
+        $result = [];
+        $validator = new CalDAV\CalendarQueryValidator();
+
+        $objects = $this->caldavBackend->getSchedulingObjects($this->principalUri);
+        foreach ($objects as $object) {
+            $vObject = VObject\Reader::read($object['calendardata']);
+            if ($validator->validate($vObject, $filters)) {
+                $result[] = $object['uri'];
+            }
+
+            // Destroy circular references to PHP will GC the object.
+            $vObject->destroy();
+        }
+        return $result;
+
+    }
+
+}

+ 184 - 0
lib/sabre/sabre/dav/lib/CalDAV/Schedule/Outbox.php

@@ -0,0 +1,184 @@
+<?php
+
+namespace Sabre\CalDAV\Schedule;
+
+use Sabre\DAV;
+use Sabre\CalDAV;
+use Sabre\DAVACL;
+
+/**
+ * The CalDAV scheduling outbox
+ *
+ * The outbox is mainly used as an endpoint in the tree for a client to do
+ * free-busy requests. This functionality is completely handled by the
+ * Scheduling plugin, so this object is actually mostly static.
+ *
+ * @copyright Copyright (C) fruux GmbH (https://fruux.com/)
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+class Outbox extends DAV\Collection implements IOutbox {
+
+    /**
+     * The principal Uri
+     *
+     * @var string
+     */
+    protected $principalUri;
+
+    /**
+     * Constructor
+     *
+     * @param string $principalUri
+     */
+    function __construct($principalUri) {
+
+        $this->principalUri = $principalUri;
+
+    }
+
+    /**
+     * Returns the name of the node.
+     *
+     * This is used to generate the url.
+     *
+     * @return string
+     */
+    function getName() {
+
+        return 'outbox';
+
+    }
+
+    /**
+     * Returns an array with all the child nodes
+     *
+     * @return \Sabre\DAV\INode[]
+     */
+    function getChildren() {
+
+        return [];
+
+    }
+
+    /**
+     * Returns the owner principal
+     *
+     * This must be a url to a principal, or null if there's no owner
+     *
+     * @return string|null
+     */
+    function getOwner() {
+
+        return $this->principalUri;
+
+    }
+
+    /**
+     * Returns a group principal
+     *
+     * This must be a url to a principal, or null if there's no owner
+     *
+     * @return string|null
+     */
+    function getGroup() {
+
+        return null;
+
+    }
+
+    /**
+     * Returns a list of ACE's for this node.
+     *
+     * Each ACE has the following properties:
+     *   * 'privilege', a string such as {DAV:}read or {DAV:}write. These are
+     *     currently the only supported privileges
+     *   * 'principal', a url to the principal who owns the node
+     *   * 'protected' (optional), indicating that this ACE is not allowed to
+     *      be updated.
+     *
+     * @return array
+     */
+    function getACL() {
+
+        return [
+            [
+                'privilege' => '{' . CalDAV\Plugin::NS_CALDAV . '}schedule-query-freebusy',
+                'principal' => $this->getOwner(),
+                'protected' => true,
+            ],
+            [
+                'privilege' => '{' . CalDAV\Plugin::NS_CALDAV . '}schedule-post-vevent',
+                'principal' => $this->getOwner(),
+                'protected' => true,
+            ],
+            [
+                'privilege' => '{DAV:}read',
+                'principal' => $this->getOwner(),
+                'protected' => true,
+            ],
+            [
+                'privilege' => '{' . CalDAV\Plugin::NS_CALDAV . '}schedule-query-freebusy',
+                'principal' => $this->getOwner() . '/calendar-proxy-write',
+                'protected' => true,
+            ],
+            [
+                'privilege' => '{' . CalDAV\Plugin::NS_CALDAV . '}schedule-post-vevent',
+                'principal' => $this->getOwner() . '/calendar-proxy-write',
+                'protected' => true,
+            ],
+            [
+                'privilege' => '{DAV:}read',
+                'principal' => $this->getOwner() . '/calendar-proxy-read',
+                'protected' => true,
+            ],
+            [
+                'privilege' => '{DAV:}read',
+                'principal' => $this->getOwner() . '/calendar-proxy-write',
+                'protected' => true,
+            ],
+        ];
+
+    }
+
+    /**
+     * Updates the ACL
+     *
+     * This method will receive a list of new ACE's.
+     *
+     * @param array $acl
+     * @return void
+     */
+    function setACL(array $acl) {
+
+        throw new DAV\Exception\MethodNotAllowed('You\'re not allowed to update the ACL');
+
+    }
+
+    /**
+     * Returns the list of supported privileges for this node.
+     *
+     * The returned data structure is a list of nested privileges.
+     * See Sabre\DAVACL\Plugin::getDefaultSupportedPrivilegeSet for a simple
+     * standard structure.
+     *
+     * If null is returned from this method, the default privilege set is used,
+     * which is fine for most common usecases.
+     *
+     * @return array|null
+     */
+    function getSupportedPrivilegeSet() {
+
+        $default = DAVACL\Plugin::getDefaultSupportedPrivilegeSet();
+        $default['aggregates'][] = [
+            'privilege' => '{' . CalDAV\Plugin::NS_CALDAV . '}schedule-query-freebusy',
+        ];
+        $default['aggregates'][] = [
+            'privilege' => '{' . CalDAV\Plugin::NS_CALDAV . '}schedule-post-vevent',
+        ];
+
+        return $default;
+
+    }
+
+}

+ 994 - 0
lib/sabre/sabre/dav/lib/CalDAV/Schedule/Plugin.php

@@ -0,0 +1,994 @@
+<?php
+
+namespace Sabre\CalDAV\Schedule;
+
+use DateTimeZone;
+use Sabre\DAV\Server;
+use Sabre\DAV\ServerPlugin;
+use Sabre\DAV\PropFind;
+use Sabre\DAV\PropPatch;
+use Sabre\DAV\INode;
+use Sabre\DAV\Xml\Property\Href;
+use Sabre\HTTP\RequestInterface;
+use Sabre\HTTP\ResponseInterface;
+use Sabre\VObject;
+use Sabre\VObject\Reader;
+use Sabre\VObject\Component\VCalendar;
+use Sabre\VObject\ITip;
+use Sabre\VObject\ITip\Message;
+use Sabre\DAVACL;
+use Sabre\CalDAV\ICalendar;
+use Sabre\CalDAV\ICalendarObject;
+use Sabre\CalDAV\Xml\Property\ScheduleCalendarTransp;
+use Sabre\DAV\Exception\NotFound;
+use Sabre\DAV\Exception\Forbidden;
+use Sabre\DAV\Exception\BadRequest;
+use Sabre\DAV\Exception\NotImplemented;
+
+/**
+ * CalDAV scheduling plugin.
+ * =========================
+ *
+ * This plugin provides the functionality added by the "Scheduling Extensions
+ * to CalDAV" standard, as defined in RFC6638.
+ *
+ * calendar-auto-schedule largely works by intercepting a users request to
+ * update their local calendar. If a user creates a new event with attendees,
+ * this plugin is supposed to grab the information from that event, and notify
+ * the attendees of this.
+ *
+ * There's 3 possible transports for this:
+ * * local delivery
+ * * delivery through email (iMip)
+ * * server-to-server delivery (iSchedule)
+ *
+ * iMip is simply, because we just need to add the iTip message as an email
+ * attachment. Local delivery is harder, because we both need to add this same
+ * message to a local DAV inbox, as well as live-update the relevant events.
+ *
+ * iSchedule is something for later.
+ *
+ * @copyright Copyright (C) fruux GmbH (https://fruux.com/)
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+class Plugin extends ServerPlugin {
+
+    /**
+     * This is the official CalDAV namespace
+     */
+    const NS_CALDAV = 'urn:ietf:params:xml:ns:caldav';
+
+    /**
+     * Reference to main Server object.
+     *
+     * @var Server
+     */
+    protected $server;
+
+    /**
+     * Returns a list of features for the DAV: HTTP header.
+     *
+     * @return array
+     */
+    function getFeatures() {
+
+        return ['calendar-auto-schedule', 'calendar-availability'];
+
+    }
+
+    /**
+     * Returns the name of the plugin.
+     *
+     * Using this name other plugins will be able to access other plugins
+     * using Server::getPlugin
+     *
+     * @return string
+     */
+    function getPluginName() {
+
+        return 'caldav-schedule';
+
+    }
+
+    /**
+     * Initializes the plugin
+     *
+     * @param Server $server
+     * @return void
+     */
+    function initialize(Server $server) {
+
+        $this->server = $server;
+        $server->on('method:POST',          [$this, 'httpPost']);
+        $server->on('propFind',             [$this, 'propFind']);
+        $server->on('propPatch',            [$this, 'propPatch']);
+        $server->on('calendarObjectChange', [$this, 'calendarObjectChange']);
+        $server->on('beforeUnbind',         [$this, 'beforeUnbind']);
+        $server->on('schedule',             [$this, 'scheduleLocalDelivery']);
+
+        $ns = '{' . self::NS_CALDAV . '}';
+
+        /**
+         * This information ensures that the {DAV:}resourcetype property has
+         * the correct values.
+         */
+        $server->resourceTypeMapping['\\Sabre\\CalDAV\\Schedule\\IOutbox'] = $ns . 'schedule-outbox';
+        $server->resourceTypeMapping['\\Sabre\\CalDAV\\Schedule\\IInbox'] = $ns . 'schedule-inbox';
+
+        /**
+         * Properties we protect are made read-only by the server.
+         */
+        array_push($server->protectedProperties,
+            $ns . 'schedule-inbox-URL',
+            $ns . 'schedule-outbox-URL',
+            $ns . 'calendar-user-address-set',
+            $ns . 'calendar-user-type',
+            $ns . 'schedule-default-calendar-URL'
+        );
+
+    }
+
+    /**
+     * Use this method to tell the server this plugin defines additional
+     * HTTP methods.
+     *
+     * This method is passed a uri. It should only return HTTP methods that are
+     * available for the specified uri.
+     *
+     * @param string $uri
+     * @return array
+     */
+    function getHTTPMethods($uri) {
+
+        try {
+            $node = $this->server->tree->getNodeForPath($uri);
+        } catch (NotFound $e) {
+            return [];
+        }
+
+        if ($node instanceof IOutbox) {
+            return ['POST'];
+        }
+
+        return [];
+
+    }
+
+    /**
+     * This method handles POST request for the outbox.
+     *
+     * @param RequestInterface $request
+     * @param ResponseInterface $response
+     * @return bool
+     */
+    function httpPost(RequestInterface $request, ResponseInterface $response) {
+
+        // Checking if this is a text/calendar content type
+        $contentType = $request->getHeader('Content-Type');
+        if (strpos($contentType, 'text/calendar') !== 0) {
+            return;
+        }
+
+        $path = $request->getPath();
+
+        // Checking if we're talking to an outbox
+        try {
+            $node = $this->server->tree->getNodeForPath($path);
+        } catch (NotFound $e) {
+            return;
+        }
+        if (!$node instanceof IOutbox)
+            return;
+
+        $this->server->transactionType = 'post-caldav-outbox';
+        $this->outboxRequest($node, $request, $response);
+
+        // Returning false breaks the event chain and tells the server we've
+        // handled the request.
+        return false;
+
+    }
+
+    /**
+     * This method handler is invoked during fetching of properties.
+     *
+     * We use this event to add calendar-auto-schedule-specific properties.
+     *
+     * @param PropFind $propFind
+     * @param INode $node
+     * @return void
+     */
+    function propFind(PropFind $propFind, INode $node) {
+
+        if ($node instanceof DAVACL\IPrincipal) {
+
+            $caldavPlugin = $this->server->getPlugin('caldav');
+            $principalUrl = $node->getPrincipalUrl();
+
+            // schedule-outbox-URL property
+            $propFind->handle('{' . self::NS_CALDAV . '}schedule-outbox-URL', function() use ($principalUrl, $caldavPlugin) {
+
+                $calendarHomePath = $caldavPlugin->getCalendarHomeForPrincipal($principalUrl);
+                if (!$calendarHomePath) {
+                    return null;
+                }
+                $outboxPath = $calendarHomePath . '/outbox/';
+
+                return new Href($outboxPath);
+
+            });
+            // schedule-inbox-URL property
+            $propFind->handle('{' . self::NS_CALDAV . '}schedule-inbox-URL', function() use ($principalUrl, $caldavPlugin) {
+
+                $calendarHomePath = $caldavPlugin->getCalendarHomeForPrincipal($principalUrl);
+                if (!$calendarHomePath) {
+                    return null;
+                }
+                $inboxPath = $calendarHomePath . '/inbox/';
+
+                return new Href($inboxPath);
+
+            });
+
+            $propFind->handle('{' . self::NS_CALDAV . '}schedule-default-calendar-URL', function() use ($principalUrl, $caldavPlugin) {
+
+                // We don't support customizing this property yet, so in the
+                // meantime we just grab the first calendar in the home-set.
+                $calendarHomePath = $caldavPlugin->getCalendarHomeForPrincipal($principalUrl);
+
+                if (!$calendarHomePath) {
+                    return null;
+                }
+
+                $sccs = '{' . self::NS_CALDAV . '}supported-calendar-component-set';
+
+                $result = $this->server->getPropertiesForPath($calendarHomePath, [
+                    '{DAV:}resourcetype',
+                    $sccs,
+                ], 1);
+
+                foreach ($result as $child) {
+                    if (!isset($child[200]['{DAV:}resourcetype']) || !$child[200]['{DAV:}resourcetype']->is('{' . self::NS_CALDAV . '}calendar') || $child[200]['{DAV:}resourcetype']->is('{http://calendarserver.org/ns/}shared')) {
+                        // Node is either not a calendar or a shared instance.
+                        continue;
+                    }
+                    if (!isset($child[200][$sccs]) || in_array('VEVENT', $child[200][$sccs]->getValue())) {
+                        // Either there is no supported-calendar-component-set
+                        // (which is fine) or we found one that supports VEVENT.
+                        return new Href($child['href']);
+                    }
+                }
+
+            });
+
+            // The server currently reports every principal to be of type
+            // 'INDIVIDUAL'
+            $propFind->handle('{' . self::NS_CALDAV . '}calendar-user-type', function() {
+
+                return 'INDIVIDUAL';
+
+            });
+
+        }
+
+        // Mapping the old property to the new property.
+        $propFind->handle('{http://calendarserver.org/ns/}calendar-availability', function() use ($propFind, $node) {
+
+             // In case it wasn't clear, the only difference is that we map the
+            // old property to a different namespace.
+             $availProp = '{' . self::NS_CALDAV . '}calendar-availability';
+             $subPropFind = new PropFind(
+                 $propFind->getPath(),
+                 [$availProp]
+             );
+
+             $this->server->getPropertiesByNode(
+                 $subPropFind,
+                 $node
+             );
+
+             $propFind->set(
+                 '{http://calendarserver.org/ns/}calendar-availability',
+                 $subPropFind->get($availProp),
+                 $subPropFind->getStatus($availProp)
+             );
+
+        });
+
+    }
+
+    /**
+     * This method is called during property updates.
+     *
+     * @param string $path
+     * @param PropPatch $propPatch
+     * @return void
+     */
+    function propPatch($path, PropPatch $propPatch) {
+
+        // Mapping the old property to the new property.
+        $propPatch->handle('{http://calendarserver.org/ns/}calendar-availability', function($value) use ($path) {
+
+            $availProp = '{' . self::NS_CALDAV . '}calendar-availability';
+            $subPropPatch = new PropPatch([$availProp => $value]);
+            $this->server->emit('propPatch', [$path, $subPropPatch]);
+            $subPropPatch->commit();
+
+            return $subPropPatch->getResult()[$availProp];
+
+        });
+
+    }
+
+    /**
+     * This method is triggered whenever there was a calendar object gets
+     * created or updated.
+     *
+     * @param RequestInterface $request HTTP request
+     * @param ResponseInterface $response HTTP Response
+     * @param VCalendar $vCal Parsed iCalendar object
+     * @param mixed $calendarPath Path to calendar collection
+     * @param mixed $modified The iCalendar object has been touched.
+     * @param mixed $isNew Whether this was a new item or we're updating one
+     * @return void
+     */
+    function calendarObjectChange(RequestInterface $request, ResponseInterface $response, VCalendar $vCal, $calendarPath, &$modified, $isNew) {
+
+        if (!$this->scheduleReply($this->server->httpRequest)) {
+            return;
+        }
+
+        $calendarNode = $this->server->tree->getNodeForPath($calendarPath);
+
+        $addresses = $this->getAddressesForPrincipal(
+            $calendarNode->getOwner()
+        );
+
+        if (!$isNew) {
+            $node = $this->server->tree->getNodeForPath($request->getPath());
+            $oldObj = Reader::read($node->get());
+        } else {
+            $oldObj = null;
+        }
+
+        $this->processICalendarChange($oldObj, $vCal, $addresses, [], $modified);
+
+        if ($oldObj) {
+            // Destroy circular references so PHP will GC the object.
+            $oldObj->destroy();
+        }
+
+    }
+
+    /**
+     * This method is responsible for delivering the ITip message.
+     *
+     * @param ITip\Message $itipMessage
+     * @return void
+     */
+    function deliver(ITip\Message $iTipMessage) {
+
+        $this->server->emit('schedule', [$iTipMessage]);
+        if (!$iTipMessage->scheduleStatus) {
+            $iTipMessage->scheduleStatus = '5.2;There was no system capable of delivering the scheduling message';
+        }
+        // In case the change was considered 'insignificant', we are going to
+        // remove any error statuses, if any. See ticket #525.
+        list($baseCode) = explode('.', $iTipMessage->scheduleStatus);
+        if (!$iTipMessage->significantChange && in_array($baseCode, ['3', '5'])) {
+            $iTipMessage->scheduleStatus = null;
+        }
+
+    }
+
+    /**
+     * This method is triggered before a file gets deleted.
+     *
+     * We use this event to make sure that when this happens, attendees get
+     * cancellations, and organizers get 'DECLINED' statuses.
+     *
+     * @param string $path
+     * @return void
+     */
+    function beforeUnbind($path) {
+
+        // FIXME: We shouldn't trigger this functionality when we're issuing a
+        // MOVE. This is a hack.
+        if ($this->server->httpRequest->getMethod() === 'MOVE') return;
+
+        $node = $this->server->tree->getNodeForPath($path);
+
+        if (!$node instanceof ICalendarObject || $node instanceof ISchedulingObject) {
+            return;
+        }
+
+        if (!$this->scheduleReply($this->server->httpRequest)) {
+            return;
+        }
+
+        $addresses = $this->getAddressesForPrincipal(
+            $node->getOwner()
+        );
+
+        $broker = new ITip\Broker();
+        $messages = $broker->parseEvent(null, $addresses, $node->get());
+
+        foreach ($messages as $message) {
+            $this->deliver($message);
+        }
+
+    }
+
+    /**
+     * Event handler for the 'schedule' event.
+     *
+     * This handler attempts to look at local accounts to deliver the
+     * scheduling object.
+     *
+     * @param ITip\Message $iTipMessage
+     * @return void
+     */
+    function scheduleLocalDelivery(ITip\Message $iTipMessage) {
+
+        $aclPlugin = $this->server->getPlugin('acl');
+
+        // Local delivery is not available if the ACL plugin is not loaded.
+        if (!$aclPlugin) {
+            return;
+        }
+
+        $caldavNS = '{' . self::NS_CALDAV . '}';
+
+        $principalUri = $aclPlugin->getPrincipalByUri($iTipMessage->recipient);
+        if (!$principalUri) {
+            $iTipMessage->scheduleStatus = '3.7;Could not find principal.';
+            return;
+        }
+
+        // We found a principal URL, now we need to find its inbox.
+        // Unfortunately we may not have sufficient privileges to find this, so
+        // we are temporarily turning off ACL to let this come through.
+        //
+        // Once we support PHP 5.5, this should be wrapped in a try..finally
+        // block so we can ensure that this privilege gets added again after.
+        $this->server->removeListener('propFind', [$aclPlugin, 'propFind']);
+
+        $result = $this->server->getProperties(
+            $principalUri,
+            [
+                '{DAV:}principal-URL',
+                 $caldavNS . 'calendar-home-set',
+                 $caldavNS . 'schedule-inbox-URL',
+                 $caldavNS . 'schedule-default-calendar-URL',
+                '{http://sabredav.org/ns}email-address',
+            ]
+        );
+
+        // Re-registering the ACL event
+        $this->server->on('propFind', [$aclPlugin, 'propFind'], 20);
+
+        if (!isset($result[$caldavNS . 'schedule-inbox-URL'])) {
+            $iTipMessage->scheduleStatus = '5.2;Could not find local inbox';
+            return;
+        }
+        if (!isset($result[$caldavNS . 'calendar-home-set'])) {
+            $iTipMessage->scheduleStatus = '5.2;Could not locate a calendar-home-set';
+            return;
+        }
+        if (!isset($result[$caldavNS . 'schedule-default-calendar-URL'])) {
+            $iTipMessage->scheduleStatus = '5.2;Could not find a schedule-default-calendar-URL property';
+            return;
+        }
+
+        $calendarPath = $result[$caldavNS . 'schedule-default-calendar-URL']->getHref();
+        $homePath = $result[$caldavNS . 'calendar-home-set']->getHref();
+        $inboxPath = $result[$caldavNS . 'schedule-inbox-URL']->getHref();
+
+        if ($iTipMessage->method === 'REPLY') {
+            $privilege = 'schedule-deliver-reply';
+        } else {
+            $privilege = 'schedule-deliver-invite';
+        }
+
+        if (!$aclPlugin->checkPrivileges($inboxPath, $caldavNS . $privilege, DAVACL\Plugin::R_PARENT, false)) {
+            $iTipMessage->scheduleStatus = '3.8;organizer did not have the ' . $privilege . ' privilege on the attendees inbox';
+            return;
+        }
+
+        // Next, we're going to find out if the item already exits in one of
+        // the users' calendars.
+        $uid = $iTipMessage->uid;
+
+        $newFileName = 'sabredav-' . \Sabre\DAV\UUIDUtil::getUUID() . '.ics';
+
+        $home = $this->server->tree->getNodeForPath($homePath);
+        $inbox = $this->server->tree->getNodeForPath($inboxPath);
+
+        $currentObject = null;
+        $objectNode = null;
+        $isNewNode = false;
+
+        $result = $home->getCalendarObjectByUID($uid);
+        if ($result) {
+            // There was an existing object, we need to update probably.
+            $objectPath = $homePath . '/' . $result;
+            $objectNode = $this->server->tree->getNodeForPath($objectPath);
+            $oldICalendarData = $objectNode->get();
+            $currentObject = Reader::read($oldICalendarData);
+        } else {
+            $isNewNode = true;
+        }
+
+        $broker = new ITip\Broker();
+        $newObject = $broker->processMessage($iTipMessage, $currentObject);
+
+        $inbox->createFile($newFileName, $iTipMessage->message->serialize());
+
+        if (!$newObject) {
+            // We received an iTip message referring to a UID that we don't
+            // have in any calendars yet, and processMessage did not give us a
+            // calendarobject back.
+            //
+            // The implication is that processMessage did not understand the
+            // iTip message.
+            $iTipMessage->scheduleStatus = '5.0;iTip message was not processed by the server, likely because we didn\'t understand it.';
+            return;
+        }
+
+        // Note that we are bypassing ACL on purpose by calling this directly.
+        // We may need to look a bit deeper into this later. Supporting ACL
+        // here would be nice.
+        if ($isNewNode) {
+            $calendar = $this->server->tree->getNodeForPath($calendarPath);
+            $calendar->createFile($newFileName, $newObject->serialize());
+        } else {
+            // If the message was a reply, we may have to inform other
+            // attendees of this attendees status. Therefore we're shooting off
+            // another itipMessage.
+            if ($iTipMessage->method === 'REPLY') {
+                $this->processICalendarChange(
+                    $oldICalendarData,
+                    $newObject,
+                    [$iTipMessage->recipient],
+                    [$iTipMessage->sender]
+                );
+            }
+            $objectNode->put($newObject->serialize());
+        }
+        $iTipMessage->scheduleStatus = '1.2;Message delivered locally';
+
+    }
+
+    /**
+     * This method looks at an old iCalendar object, a new iCalendar object and
+     * starts sending scheduling messages based on the changes.
+     *
+     * A list of addresses needs to be specified, so the system knows who made
+     * the update, because the behavior may be different based on if it's an
+     * attendee or an organizer.
+     *
+     * This method may update $newObject to add any status changes.
+     *
+     * @param VCalendar|string $oldObject
+     * @param VCalendar $newObject
+     * @param array $addresses
+     * @param array $ignore Any addresses to not send messages to.
+     * @param bool $modified A marker to indicate that the original object
+     *   modified by this process.
+     * @return void
+     */
+    protected function processICalendarChange($oldObject = null, VCalendar $newObject, array $addresses, array $ignore = [], &$modified = false) {
+
+        $broker = new ITip\Broker();
+        $messages = $broker->parseEvent($newObject, $addresses, $oldObject);
+
+        if ($messages) $modified = true;
+
+        foreach ($messages as $message) {
+
+            if (in_array($message->recipient, $ignore)) {
+                continue;
+            }
+
+            $this->deliver($message);
+
+            if (isset($newObject->VEVENT->ORGANIZER) && ($newObject->VEVENT->ORGANIZER->getNormalizedValue() === $message->recipient)) {
+                if ($message->scheduleStatus) {
+                    $newObject->VEVENT->ORGANIZER['SCHEDULE-STATUS'] = $message->getScheduleStatus();
+                }
+                unset($newObject->VEVENT->ORGANIZER['SCHEDULE-FORCE-SEND']);
+
+            } else {
+
+                if (isset($newObject->VEVENT->ATTENDEE)) foreach ($newObject->VEVENT->ATTENDEE as $attendee) {
+
+                    if ($attendee->getNormalizedValue() === $message->recipient) {
+                        if ($message->scheduleStatus) {
+                            $attendee['SCHEDULE-STATUS'] = $message->getScheduleStatus();
+                        }
+                        unset($attendee['SCHEDULE-FORCE-SEND']);
+                        break;
+                    }
+
+                }
+
+            }
+
+        }
+
+    }
+
+    /**
+     * Returns a list of addresses that are associated with a principal.
+     *
+     * @param string $principal
+     * @return array
+     */
+    protected function getAddressesForPrincipal($principal) {
+
+        $CUAS = '{' . self::NS_CALDAV . '}calendar-user-address-set';
+
+        $properties = $this->server->getProperties(
+            $principal,
+            [$CUAS]
+        );
+
+        // If we can't find this information, we'll stop processing
+        if (!isset($properties[$CUAS])) {
+            return;
+        }
+
+        $addresses = $properties[$CUAS]->getHrefs();
+        return $addresses;
+
+    }
+
+    /**
+     * This method handles POST requests to the schedule-outbox.
+     *
+     * Currently, two types of requests are support:
+     *   * FREEBUSY requests from RFC 6638
+     *   * Simple iTIP messages from draft-desruisseaux-caldav-sched-04
+     *
+     * The latter is from an expired early draft of the CalDAV scheduling
+     * extensions, but iCal depends on a feature from that spec, so we
+     * implement it.
+     *
+     * @param IOutbox $outboxNode
+     * @param RequestInterface $request
+     * @param ResponseInterface $response
+     * @return void
+     */
+    function outboxRequest(IOutbox $outboxNode, RequestInterface $request, ResponseInterface $response) {
+
+        $outboxPath = $request->getPath();
+
+        // Parsing the request body
+        try {
+            $vObject = VObject\Reader::read($request->getBody());
+        } catch (VObject\ParseException $e) {
+            throw new BadRequest('The request body must be a valid iCalendar object. Parse error: ' . $e->getMessage());
+        }
+
+        // The incoming iCalendar object must have a METHOD property, and a
+        // component. The combination of both determines what type of request
+        // this is.
+        $componentType = null;
+        foreach ($vObject->getComponents() as $component) {
+            if ($component->name !== 'VTIMEZONE') {
+                $componentType = $component->name;
+                break;
+            }
+        }
+        if (is_null($componentType)) {
+            throw new BadRequest('We expected at least one VTODO, VJOURNAL, VFREEBUSY or VEVENT component');
+        }
+
+        // Validating the METHOD
+        $method = strtoupper((string)$vObject->METHOD);
+        if (!$method) {
+            throw new BadRequest('A METHOD property must be specified in iTIP messages');
+        }
+
+        // So we support one type of request:
+        //
+        // REQUEST with a VFREEBUSY component
+
+        $acl = $this->server->getPlugin('acl');
+
+        if ($componentType === 'VFREEBUSY' && $method === 'REQUEST') {
+
+            $acl && $acl->checkPrivileges($outboxPath, '{' . self::NS_CALDAV . '}schedule-query-freebusy');
+            $this->handleFreeBusyRequest($outboxNode, $vObject, $request, $response);
+
+            // Destroy circular references so PHP can GC the object.
+            $vObject->destroy();
+            unset($vObject);
+
+        } else {
+
+            throw new NotImplemented('We only support VFREEBUSY (REQUEST) on this endpoint');
+
+        }
+
+    }
+
+    /**
+     * This method is responsible for parsing a free-busy query request and
+     * returning it's result.
+     *
+     * @param IOutbox $outbox
+     * @param VObject\Component $vObject
+     * @param RequestInterface $request
+     * @param ResponseInterface $response
+     * @return string
+     */
+    protected function handleFreeBusyRequest(IOutbox $outbox, VObject\Component $vObject, RequestInterface $request, ResponseInterface $response) {
+
+        $vFreeBusy = $vObject->VFREEBUSY;
+        $organizer = $vFreeBusy->organizer;
+
+        $organizer = (string)$organizer;
+
+        // Validating if the organizer matches the owner of the inbox.
+        $owner = $outbox->getOwner();
+
+        $caldavNS = '{' . self::NS_CALDAV . '}';
+
+        $uas = $caldavNS . 'calendar-user-address-set';
+        $props = $this->server->getProperties($owner, [$uas]);
+
+        if (empty($props[$uas]) || !in_array($organizer, $props[$uas]->getHrefs())) {
+            throw new Forbidden('The organizer in the request did not match any of the addresses for the owner of this inbox');
+        }
+
+        if (!isset($vFreeBusy->ATTENDEE)) {
+            throw new BadRequest('You must at least specify 1 attendee');
+        }
+
+        $attendees = [];
+        foreach ($vFreeBusy->ATTENDEE as $attendee) {
+            $attendees[] = (string)$attendee;
+        }
+
+
+        if (!isset($vFreeBusy->DTSTART) || !isset($vFreeBusy->DTEND)) {
+            throw new BadRequest('DTSTART and DTEND must both be specified');
+        }
+
+        $startRange = $vFreeBusy->DTSTART->getDateTime();
+        $endRange = $vFreeBusy->DTEND->getDateTime();
+
+        $results = [];
+        foreach ($attendees as $attendee) {
+            $results[] = $this->getFreeBusyForEmail($attendee, $startRange, $endRange, $vObject);
+        }
+
+        $dom = new \DOMDocument('1.0', 'utf-8');
+        $dom->formatOutput = true;
+        $scheduleResponse = $dom->createElement('cal:schedule-response');
+        foreach ($this->server->xml->namespaceMap as $namespace => $prefix) {
+
+            $scheduleResponse->setAttribute('xmlns:' . $prefix, $namespace);
+
+        }
+        $dom->appendChild($scheduleResponse);
+
+        foreach ($results as $result) {
+            $xresponse = $dom->createElement('cal:response');
+
+            $recipient = $dom->createElement('cal:recipient');
+            $recipientHref = $dom->createElement('d:href');
+
+            $recipientHref->appendChild($dom->createTextNode($result['href']));
+            $recipient->appendChild($recipientHref);
+            $xresponse->appendChild($recipient);
+
+            $reqStatus = $dom->createElement('cal:request-status');
+            $reqStatus->appendChild($dom->createTextNode($result['request-status']));
+            $xresponse->appendChild($reqStatus);
+
+            if (isset($result['calendar-data'])) {
+
+                $calendardata = $dom->createElement('cal:calendar-data');
+                $calendardata->appendChild($dom->createTextNode(str_replace("\r\n", "\n", $result['calendar-data']->serialize())));
+                $xresponse->appendChild($calendardata);
+
+            }
+            $scheduleResponse->appendChild($xresponse);
+        }
+
+        $response->setStatus(200);
+        $response->setHeader('Content-Type', 'application/xml');
+        $response->setBody($dom->saveXML());
+
+    }
+
+    /**
+     * Returns free-busy information for a specific address. The returned
+     * data is an array containing the following properties:
+     *
+     * calendar-data : A VFREEBUSY VObject
+     * request-status : an iTip status code.
+     * href: The principal's email address, as requested
+     *
+     * The following request status codes may be returned:
+     *   * 2.0;description
+     *   * 3.7;description
+     *
+     * @param string $email address
+     * @param DateTimeInterface $start
+     * @param DateTimeInterface $end
+     * @param VObject\Component $request
+     * @return array
+     */
+    protected function getFreeBusyForEmail($email, \DateTimeInterface $start, \DateTimeInterface $end, VObject\Component $request) {
+
+        $caldavNS = '{' . self::NS_CALDAV . '}';
+
+        $aclPlugin = $this->server->getPlugin('acl');
+        if (substr($email, 0, 7) === 'mailto:') $email = substr($email, 7);
+
+        $result = $aclPlugin->principalSearch(
+            ['{http://sabredav.org/ns}email-address' => $email],
+            [
+                '{DAV:}principal-URL',
+                $caldavNS . 'calendar-home-set',
+                $caldavNS . 'schedule-inbox-URL',
+                '{http://sabredav.org/ns}email-address',
+
+            ]
+        );
+
+        if (!count($result)) {
+            return [
+                'request-status' => '3.7;Could not find principal',
+                'href'           => 'mailto:' . $email,
+            ];
+        }
+
+        if (!isset($result[0][200][$caldavNS . 'calendar-home-set'])) {
+            return [
+                'request-status' => '3.7;No calendar-home-set property found',
+                'href'           => 'mailto:' . $email,
+            ];
+        }
+        if (!isset($result[0][200][$caldavNS . 'schedule-inbox-URL'])) {
+            return [
+                'request-status' => '3.7;No schedule-inbox-URL property found',
+                'href'           => 'mailto:' . $email,
+            ];
+        }
+        $homeSet = $result[0][200][$caldavNS . 'calendar-home-set']->getHref();
+        $inboxUrl = $result[0][200][$caldavNS . 'schedule-inbox-URL']->getHref();
+
+        // Grabbing the calendar list
+        $objects = [];
+        $calendarTimeZone = new DateTimeZone('UTC');
+
+        foreach ($this->server->tree->getNodeForPath($homeSet)->getChildren() as $node) {
+            if (!$node instanceof ICalendar) {
+                continue;
+            }
+
+            $sct = $caldavNS . 'schedule-calendar-transp';
+            $ctz = $caldavNS . 'calendar-timezone';
+            $props = $node->getProperties([$sct, $ctz]);
+
+            if (isset($props[$sct]) && $props[$sct]->getValue() == ScheduleCalendarTransp::TRANSPARENT) {
+                // If a calendar is marked as 'transparent', it means we must
+                // ignore it for free-busy purposes.
+                continue;
+            }
+
+            $aclPlugin->checkPrivileges($homeSet . $node->getName(), $caldavNS . 'read-free-busy');
+
+            if (isset($props[$ctz])) {
+                $vtimezoneObj = VObject\Reader::read($props[$ctz]);
+                $calendarTimeZone = $vtimezoneObj->VTIMEZONE->getTimeZone();
+
+                // Destroy circular references so PHP can garbage collect the object.
+                $vtimezoneObj->destroy();
+
+            }
+
+            // Getting the list of object uris within the time-range
+            $urls = $node->calendarQuery([
+                'name'         => 'VCALENDAR',
+                'comp-filters' => [
+                    [
+                        'name'           => 'VEVENT',
+                        'comp-filters'   => [],
+                        'prop-filters'   => [],
+                        'is-not-defined' => false,
+                        'time-range'     => [
+                            'start' => $start,
+                            'end'   => $end,
+                        ],
+                    ],
+                ],
+                'prop-filters'   => [],
+                'is-not-defined' => false,
+                'time-range'     => null,
+            ]);
+
+            $calObjects = array_map(function($url) use ($node) {
+                $obj = $node->getChild($url)->get();
+                return $obj;
+            }, $urls);
+
+            $objects = array_merge($objects, $calObjects);
+
+        }
+
+        $inboxProps = $this->server->getProperties(
+            $inboxUrl,
+            $caldavNS . 'calendar-availability'
+        );
+
+        $vcalendar = new VObject\Component\VCalendar();
+        $vcalendar->METHOD = 'REPLY';
+
+        $generator = new VObject\FreeBusyGenerator();
+        $generator->setObjects($objects);
+        $generator->setTimeRange($start, $end);
+        $generator->setBaseObject($vcalendar);
+        $generator->setTimeZone($calendarTimeZone);
+
+        if ($inboxProps) {
+            $generator->setVAvailability(
+                VObject\Reader::read(
+                    $inboxProps[$caldavNS . 'calendar-availability']
+                )
+            );
+        }
+
+        $result = $generator->getResult();
+
+        $vcalendar->VFREEBUSY->ATTENDEE = 'mailto:' . $email;
+        $vcalendar->VFREEBUSY->UID = (string)$request->VFREEBUSY->UID;
+        $vcalendar->VFREEBUSY->ORGANIZER = clone $request->VFREEBUSY->ORGANIZER;
+
+        return [
+            'calendar-data'  => $result,
+            'request-status' => '2.0;Success',
+            'href'           => 'mailto:' . $email,
+        ];
+    }
+
+    /**
+     * This method checks the 'Schedule-Reply' header
+     * and returns false if it's 'F', otherwise true.
+     *
+     * @param RequestInterface $request
+     * @return bool
+     */
+    private function scheduleReply(RequestInterface $request) {
+
+        $scheduleReply = $request->getHeader('Schedule-Reply');
+        return $scheduleReply !== 'F';
+
+    }
+
+    /**
+     * Returns a bunch of meta-data about the plugin.
+     *
+     * Providing this information is optional, and is mainly displayed by the
+     * Browser plugin.
+     *
+     * The description key in the returned array may contain html and will not
+     * be sanitized.
+     *
+     * @return array
+     */
+    function getPluginInfo() {
+
+        return [
+            'name'        => $this->getPluginName(),
+            'description' => 'Adds calendar-auto-schedule, as defined in rf6868',
+            'link'        => 'http://sabre.io/dav/scheduling/',
+        ];
+
+    }
+}

+ 165 - 0
lib/sabre/sabre/dav/lib/CalDAV/Schedule/SchedulingObject.php

@@ -0,0 +1,165 @@
+<?php
+
+namespace Sabre\CalDAV\Schedule;
+
+use Sabre\CalDAV\Backend;
+use Sabre\DAV\Exception\MethodNotAllowed;
+
+/**
+ * The SchedulingObject represents a scheduling object in the Inbox collection
+ *
+ * @author Brett (https://github.com/bretten)
+ * @license http://sabre.io/license/ Modified BSD License
+ * @copyright Copyright (C) fruux GmbH (https://fruux.com/)
+ */
+class SchedulingObject extends \Sabre\CalDAV\CalendarObject implements ISchedulingObject {
+
+    /**
+     /* The CalDAV backend
+     *
+     * @var Backend\SchedulingSupport
+     */
+    protected $caldavBackend;
+
+    /**
+     * Array with information about this SchedulingObject
+     *
+     * @var array
+     */
+    protected $objectData;
+
+    /**
+     * Constructor
+     *
+     * The following properties may be passed within $objectData:
+     *
+     *   * uri - A unique uri. Only the 'basename' must be passed.
+     *   * principaluri - the principal that owns the object.
+     *   * calendardata (optional) - The iCalendar data
+     *   * etag - (optional) The etag for this object, MUST be encloded with
+     *            double-quotes.
+     *   * size - (optional) The size of the data in bytes.
+     *   * lastmodified - (optional) format as a unix timestamp.
+     *   * acl - (optional) Use this to override the default ACL for the node.
+     *
+     * @param Backend\BackendInterface $caldavBackend
+     * @param array $objectData
+     */
+    function __construct(Backend\SchedulingSupport $caldavBackend, array $objectData) {
+
+        $this->caldavBackend = $caldavBackend;
+
+        if (!isset($objectData['uri'])) {
+            throw new \InvalidArgumentException('The objectData argument must contain an \'uri\' property');
+        }
+
+        $this->objectData = $objectData;
+
+    }
+
+    /**
+     * Returns the ICalendar-formatted object
+     *
+     * @return string
+     */
+    function get() {
+
+        // Pre-populating the 'calendardata' is optional, if we don't have it
+        // already we fetch it from the backend.
+        if (!isset($this->objectData['calendardata'])) {
+            $this->objectData = $this->caldavBackend->getSchedulingObject($this->objectData['principaluri'], $this->objectData['uri']);
+        }
+        return $this->objectData['calendardata'];
+
+    }
+
+    /**
+     * Updates the ICalendar-formatted object
+     *
+     * @param string|resource $calendarData
+     * @return string
+     */
+    function put($calendarData) {
+
+        throw new MethodNotAllowed('Updating scheduling objects is not supported');
+
+    }
+
+    /**
+     * Deletes the scheduling message
+     *
+     * @return void
+     */
+    function delete() {
+
+        $this->caldavBackend->deleteSchedulingObject($this->objectData['principaluri'], $this->objectData['uri']);
+
+    }
+
+    /**
+     * Returns the owner principal
+     *
+     * This must be a url to a principal, or null if there's no owner
+     *
+     * @return string|null
+     */
+    function getOwner() {
+
+        return $this->objectData['principaluri'];
+
+    }
+
+
+    /**
+     * Returns a list of ACE's for this node.
+     *
+     * Each ACE has the following properties:
+     *   * 'privilege', a string such as {DAV:}read or {DAV:}write. These are
+     *     currently the only supported privileges
+     *   * 'principal', a url to the principal who owns the node
+     *   * 'protected' (optional), indicating that this ACE is not allowed to
+     *      be updated.
+     *
+     * @return array
+     */
+    function getACL() {
+
+        // An alternative acl may be specified in the object data.
+        //
+
+        if (isset($this->objectData['acl'])) {
+            return $this->objectData['acl'];
+        }
+
+        // The default ACL
+        return [
+            [
+                'privilege' => '{DAV:}read',
+                'principal' => $this->objectData['principaluri'],
+                'protected' => true,
+            ],
+            [
+                'privilege' => '{DAV:}write',
+                'principal' => $this->objectData['principaluri'],
+                'protected' => true,
+            ],
+            [
+                'privilege' => '{DAV:}read',
+                'principal' => $this->objectData['principaluri'] . '/calendar-proxy-write',
+                'protected' => true,
+            ],
+            [
+                'privilege' => '{DAV:}write',
+                'principal' => $this->objectData['principaluri'] . '/calendar-proxy-write',
+                'protected' => true,
+            ],
+            [
+                'privilege' => '{DAV:}read',
+                'principal' => $this->objectData['principaluri'] . '/calendar-proxy-read',
+                'protected' => true,
+            ],
+        ];
+
+    }
+
+}

+ 72 - 0
lib/sabre/sabre/dav/lib/CalDAV/ShareableCalendar.php

@@ -0,0 +1,72 @@
+<?php
+
+namespace Sabre\CalDAV;
+
+/**
+ * This object represents a CalDAV calendar that can be shared with other
+ * users.
+ *
+ * @copyright Copyright (C) fruux GmbH (https://fruux.com/)
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://sabre.io/license/ Modified BSD License
+ */
+class ShareableCalendar extends Calendar implements IShareableCalendar {
+
+    /**
+     * Updates the list of shares.
+     *
+     * The first array is a list of people that are to be added to the
+     * calendar.
+     *
+     * Every element in the add array has the following properties:
+     *   * href - A url. Usually a mailto: address
+     *   * commonName - Usually a first and last name, or false
+     *   * summary - A description of the share, can also be false
+     *   * readOnly - A boolean value
+     *
+     * Every element in the remove array is just the address string.
+     *
+     * @param array $add
+     * @param array $remove
+     * @return void
+     */
+    function updateShares(array $add, array $remove) {
+
+        $this->caldavBackend->updateShares($this->calendarInfo['id'], $add, $remove);
+
+    }
+
+    /**
+     * Returns the list of people whom this calendar is shared with.
+     *
+     * Every element in this array should have the following properties:
+     *   * href - Often a mailto: address
+     *   * commonName - Optional, for example a first + last name
+     *   * status - See the Sabre\CalDAV\SharingPlugin::STATUS_ constants.
+     *   * readOnly - boolean
+     *   * summary - Optional, a description for the share
+     *
+     * @return array
+     */
+    function getShares() {
+
+        return $this->caldavBackend->getShares($this->calendarInfo['id']);
+
+    }
+
+    /**
+     * Marks this calendar as published.
+     *
+     * Publishing a calendar should automatically create a read-only, public,
+     * subscribable calendar.
+     *
+     * @param bool $value
+     * @return void
+     */
+    function setPublishStatus($value) {
+
+        $this->caldavBackend->setPublishStatus($this->calendarInfo['id'], $value);
+
+    }
+
+}

Some files were not shown because too many files changed in this diff