#1<\/a><\/p>\n\n my user title<\/em>\n<\/p>"
+ }
+ ]
+}
+```
+
+### createMyPrivateProject
+
+- Purpose: **Create a private project for the logged user**
+- Parameters:
+ - **name** (string, required)
+ - **description** (string, optional)
+- Result on success: **project_id**
+- Result on failure: **false**
+
+Request example:
+
+```json
+{
+ "jsonrpc": "2.0",
+ "method": "createMyPrivateProject",
+ "id": 1271580569,
+ "params": [
+ "my project"
+ ]
+}
+```
+
+Response example:
+
+```json
+{
+ "jsonrpc": "2.0",
+ "id": 1271580569,
+ "result": 2
+}
+```
+
+### getMyProjectsList
+
+- Purpose: **Get projects of the connected user**
+- Parameters: None
+- Result on success: **dictionary of project_id => project_name**
+- Result on failure: **false**
+
+Request example:
+
+```json
+{
+ "jsonrpc": "2.0",
+ "method": "getMyProjectsList",
+ "id": 987834805
+}
+```
+
+Response example:
+
+```json
+{
+ "jsonrpc": "2.0",
+ "id": 987834805,
+ "result": {
+ "2": "my project"
+ }
+}
+```
diff --git a/sources/doc/application-configuration.markdown b/sources/doc/application-configuration.markdown
new file mode 100644
index 0000000..8792973
--- /dev/null
+++ b/sources/doc/application-configuration.markdown
@@ -0,0 +1,41 @@
+Application settings
+====================
+
+Some parameters for the application can be changed on the settings page.
+Only administrators can change those settings.
+
+Go to the menu **Settings**, then choose **Application settings** on the left.
+
+![Application settings](http://kanboard.net/screenshots/documentation/application-settings.png)
+
+### Application URL
+
+This parameter is used for email notifications.
+The email footer will contains a link to the Kanboard task.
+
+### Language
+
+The application language can be changed at anytime.
+The language will be set for all users.
+
+### Timezone
+
+By default, Kanboard use UTC as timezone, but you can define your own timezone.
+The list contains all supported timezones by your web server.
+
+### Date format
+
+Input format used for date fields, by example the due date for tasks.
+
+Kanboard offer 4 different formats:
+
+- DD/MM/YYYY
+- MM/DD/YYYY (default)
+- YYYY/MM/DD
+- MM.DD.YYYY
+
+The [ISO 8601](http://en.wikipedia.org/wiki/ISO_8601) format is always accepted (YYYY-MM-DD or YYYY_MM_DD).
+
+### Custom Stylesheet
+
+Write your own CSS to override or improve Kanboard default style.
diff --git a/sources/doc/assets.markdown b/sources/doc/assets.markdown
new file mode 100644
index 0000000..29bf515
--- /dev/null
+++ b/sources/doc/assets.markdown
@@ -0,0 +1,25 @@
+How to build assets (Javascript and CSS files)
+==============================================
+
+Stylesheet and Javascript files are merged together and minified.
+
+- Original CSS files are stored in the folder `assets/css/src/*.css`
+- Original Javascript code is stored in the folder `assets/js/src/*.js`
+
+Requirements
+------------
+
+- Unix operating system
+- make
+- yuicompressor in your path (`brew install yuicompressor`)
+
+Build assets
+------------
+
+- Build Stylesheet files: `make css`
+- Build Javascript files: `make js`
+- Build both: `make`
+
+This script generates the files `assets/css/app.css` and `assets/js/app.js`.
+
+This tool is only available in the repository (development version).
diff --git a/sources/doc/automatic-actions.markdown b/sources/doc/automatic-actions.markdown
new file mode 100644
index 0000000..71dfa90
--- /dev/null
+++ b/sources/doc/automatic-actions.markdown
@@ -0,0 +1,138 @@
+Automatic Actions
+=================
+
+To minimize the user interaction, Kanboard support automated actions.
+
+Each automatic action is defined like that:
+
+- An event to listen
+- An action linked to this event
+- Eventually there is some parameters to define
+
+Each project have a different set of automatic actions, the configuration panel is located on the project listing page, just click on the link **Automatic actions**.
+
+Add a new action
+----------------
+
+### Choose an action
+
+![Choose an action](http://kanboard.net/screenshots/documentation/project-automatic-action-step1.png)
+
+### Choose an event
+
+![Choose an event](http://kanboard.net/screenshots/documentation/project-automatic-action-step2.png)
+
+### Define action parameters
+
+![Define parameters](http://kanboard.net/screenshots/documentation/project-automatic-action-step3.png)
+
+List of available events
+------------------------
+
+- Move a task to another column
+- Move a task to another position in the same column
+- Task modification
+- Task creation
+- Reopen a task
+- Closing a task
+- Task creation or modification
+- Task assignee change
+- Task link created or updated
+- Github commit received
+- Github issue opened
+- Github issue closed
+- Github issue reopened
+- Github issue assignee change
+- Github issue label change
+- Github issue comment created
+- Gitlab issue opened
+- Gitlab issue closed
+- Gitlab commit received
+- Bitbucket commit received
+- Bitbucket issue opened
+- Bitbucket issue closed
+- Bitbucket issue reopened
+- Bitbucket issue assignee change
+- Bitbucket issue comment created
+
+List of available actions
+-------------------------
+
+- Close the task
+- Open a task
+- Assign the task to a specific user
+- Assign the task to the person who does the action
+- Duplicate the task to another project
+- Move the task to another project
+- Move the task to another column when assigned to a user
+- Move the task to another column when assignee is cleared
+- Assign a color when the task is moved to a specific column
+- Assign a color to a specific user
+- Assign automatically a color based on a category
+- Assign automatically a category based on a color
+- Create a comment from an external provider
+- Create a task from an external provider
+- Add a comment log when moving the task between columns
+- Change the assignee based on an external username
+- Change the category based on an external label
+- Automatically update the start date
+- Move the task to another column when the category is changed
+- Send a task by email to someone
+- Change task color when using a specific task link
+
+Examples
+--------
+
+Here are some examples used in the real life:
+
+### When I move a task to the column "Done", automatically close this task
+
+- Choose the action: **Close the task**
+- Choose the event: **Move a task to another column**
+- Define the action parameter: **Column = Done** (this is the destination column)
+
+### When I move a task to the column "To be validated", assign this task to a specific user
+
+- Choose the action: **Assign the task to a specific user**
+- Choose the event: **Move a task to another column**
+- Define the action parameters: **Column = To be validated** and **User = Bob** (Bob is our tester)
+
+### When I move a task to the column "Work in progress", assign this task to the current user
+
+- Choose the action: **Assign the task to the person who does the action**
+- Choose the event: **Move a task to another column**
+- Define the action parameter: **Column = Work in progress**
+
+### When a task is completed, duplicate this task to another project
+
+Let's say we have two projects "Customer orders" and "Production", once the order is validated, swap it to the "Production" project.
+
+- Choose the action: **Duplicate the task to another project**
+- Choose the event: **Closing a task**
+- Define the action parameters: **Column = Validated** and **Project = Production**
+
+### When a task is moved to the last column, move the exact same task to another project
+
+Let's say we have two projects "Ideas" and "Development", once the idea is validated, swap it to the "Development" project.
+
+- Choose the action: **Move the task to another project**
+- Choose the event: **Move a task to another column**
+- Define the action parameters: **Column = Validated** and **Project = Development**
+
+### I want to assign automatically a color to the user Bob
+
+- Choose the action: **Assign a color to a specific user**
+- Choose the event: **Task assignee change**
+- Define the action parameters: **Color = Green** and **Assignee = Bob**
+
+### I want to assign automatically a color to the defined category "Feature Request"
+
+- Choose the action: **Assign automatically a color based on a category**
+- Choose the event: **Task creation or modification**
+- Define the action parameters: **Color = Blue** and **Category = Feature Request**
+
+### I want to set the start date automatically when the task is moved to the column "Work in progress"
+
+- Choose the action: **Automatically update the start date**
+- Choose the event: **Move a task to another column**
+- Define the action parameters: **Column = Work in progress**
diff --git a/sources/doc/bitbucket-webhooks.markdown b/sources/doc/bitbucket-webhooks.markdown
new file mode 100644
index 0000000..7f59aa1
--- /dev/null
+++ b/sources/doc/bitbucket-webhooks.markdown
@@ -0,0 +1,55 @@
+Bitbucket webhooks
+==================
+
+Bitbucket events can be connected to Kanboard automatic actions.
+
+List of supported events
+------------------------
+
+- Bitbucket commit received
+- Bitbucket issue opened
+- Bitbucket issue closed
+- Bitbucket issue reopened
+- Bitbucket issue assignee change
+- Bitbucket issue comment created
+
+List of supported actions
+-------------------------
+
+- Create a task from an external provider
+- Change the assignee based on an external username
+- Create a comment from an external provider
+- Close a task
+- Open a task
+
+Configuration
+-------------
+
+![Bitbucket configuration](http://kanboard.net/screenshots/documentation/bitbucket-webhooks.png)
+
+1. On Kanboard, go to the project settings and choose the section **Integrations**
+2. Copy the Bitbucket webhook url
+3. On Bitbucket, go to the project settings and go to the section **Webhooks**
+4. Choose a title for your webhook and paste the Kanboard url
+
+Examples
+--------
+
+### Close a Kanboard task when a commit pushed to Bitbucket
+
+- Choose the event: **Bitbucket commit received**
+- Choose the action: **Close the task**
+
+When one or more commits are sent to Bitbucket, Kanboard will receive the information, each commit message with a task number included will be closed.
+
+Example:
+
+- Commit message: "Fix bug #1234"
+- That will close the Kanboard task #1234
+
+### Add comment when a commit received
+
+- Choose the event: **Bitbucket commit received**
+- Choose the action: **Create a comment from an external provider**
+
+The comment will contains the commit message and the url to the commit.
diff --git a/sources/doc/board-collapsed-expanded.markdown b/sources/doc/board-collapsed-expanded.markdown
new file mode 100644
index 0000000..e094e81
--- /dev/null
+++ b/sources/doc/board-collapsed-expanded.markdown
@@ -0,0 +1,19 @@
+Collapsed and Expanded mode
+===========================
+
+Tasks on the board can be displayed in collapsed or in expanded mode.
+Switching from one view to another can be done with the keyboard shortcut **"s"** or by using the dropdown menu on the left.
+
+Collapsed mode
+--------------
+
+![Tasks collapsed](http://kanboard.net/screenshots/documentation/board-collapsed-mode.png)
+
+- If the task is assigned to someone, the initials of the person are shown next to the task number
+- If the task title is too long, you can put your mouse over the task to show a tooltip with the full title.
+
+Expanded mode
+-------------
+
+![Tasks expanded](http://kanboard.net/screenshots/documentation/board-expanded-mode.png)
+
diff --git a/sources/doc/board-configuration.markdown b/sources/doc/board-configuration.markdown
new file mode 100644
index 0000000..1c5ff51
--- /dev/null
+++ b/sources/doc/board-configuration.markdown
@@ -0,0 +1,24 @@
+Board settings
+==============
+
+Go to the menu **Settings**, then choose **Board settings** on the left.
+
+![Board settings](http://kanboard.net/screenshots/documentation/board-settings.png)
+
+### Task highlighting
+
+This feature display a shadow around the task when a task is moved recently.
+
+Set the value 0 to disable this feature, 2 days by default (172800 seconds).
+
+Everything moved since 2 days will have shadow around the task.
+
+### Refresh interval for public board
+
+When you share a board, the page will refresh automatically every 60 seconds by default.
+
+### Refresh interval for private board
+
+When your web browser is open on a board, Kanboard check every 10 seconds if something have been changed by someone else.
+
+Technically this process is done by Ajax polling.
diff --git a/sources/doc/board-horizontal-scrolling-and-compact-view.markdown b/sources/doc/board-horizontal-scrolling-and-compact-view.markdown
new file mode 100644
index 0000000..5dc5dcc
--- /dev/null
+++ b/sources/doc/board-horizontal-scrolling-and-compact-view.markdown
@@ -0,0 +1,12 @@
+Horizontal scrolling and compact mode
+=====================================
+
+When the board cannot fit on your screen, an horizontal scroll bar will appear at the bottom.
+
+However, it's possible to switch to the compact the view to display all columns in your screen.
+
+![Board in compact mode](http://kanboard.net/screenshots/documentation/board-compact-mode.png)
+
+Switching between horizontal scrolling and compact view can be done with the keyboard shortcut **"c"** or by using the dropdown menu on the top left.
+
+Note: It's possible that text overlaps in compact mode, that will be improved over the next releases.
\ No newline at end of file
diff --git a/sources/doc/board-show-hide-columns.markdown b/sources/doc/board-show-hide-columns.markdown
new file mode 100644
index 0000000..e459f55
--- /dev/null
+++ b/sources/doc/board-show-hide-columns.markdown
@@ -0,0 +1,11 @@
+Show and hide columns on the board
+==================================
+
+You can hide or display columns very easily on the board:
+
+![Board with hidden columns](http://kanboard.net/screenshots/documentation/board-hide-show-column.png)
+
+- To hide a column, just click on the column title
+- To show a hidden column, click on the vertical title
+
+When a column is hidden the number of tasks is displayed at the top.
diff --git a/sources/doc/bruteforce-protection.markdown b/sources/doc/bruteforce-protection.markdown
new file mode 100644
index 0000000..633cfe8
--- /dev/null
+++ b/sources/doc/bruteforce-protection.markdown
@@ -0,0 +1,26 @@
+Bruteforce Protection
+=====================
+
+The brute force protection of Kanboard works at the user account level:
+
+- After 3 authentication failure for the same username, the login form show a captcha image to prevent automated bot tentatives.
+- After 6 authentication failure, the user account is locked down for a period of 15 minutes.
+
+This feature works only for authentication methods that use the login form.
+
+However, **after 3 authentication failure through the user API**, the account have to be unlocked by using the login form.
+
+Kanboard doesn't block any IP addresses since bots can use several anonymous proxies. However, you can use external tools like [fail2ban](http://www.fail2ban.org) to avoid massive scans.
+
+Default settings can be changed with these configuration variables:
+
+```php
+// Enable captcha after 3 authentication failure
+define('BRUTEFORCE_CAPTCHA', 3);
+
+// Lock the account after 6 authentication failure
+define('BRUTEFORCE_LOCKDOWN', 6);
+
+// Lock account duration in minute
+define('BRUTEFORCE_LOCKDOWN_DURATION', 15);
+```
diff --git a/sources/doc/budget.markdown b/sources/doc/budget.markdown
new file mode 100644
index 0000000..63408d4
--- /dev/null
+++ b/sources/doc/budget.markdown
@@ -0,0 +1,34 @@
+Budget management
+=================
+
+Budget management is based on the subtask time tracking, the user timetable and the user hourly rate.
+
+This section is available from project settings page: **Project > Budget**. There is also a shortcut from the dropdown menu on the board.
+
+Budget lines
+------------
+
+![Cost Lines](http://kanboard.net/screenshots/documentation/budget-lines.png)
+
+Budget lines are used to define a budget for the project.
+This budget can be adjusted by adding a new entry with an effective date.
+
+Cost breakdown
+--------------
+
+![Cost Breakdown](http://kanboard.net/screenshots/documentation/budget-cost-breakdown.png)
+
+Based on the subtask time tracking table and user information you can see the cost of each subtask.
+
+The time spent is rounded to nearest quarter.
+
+Budget graph
+------------
+
+![Budget Graph](http://kanboard.net/screenshots/documentation/budget-graph.png)
+
+Finally, by combining all information we can generate a graph:
+
+- Expenses represents user cost
+- Budget lines are the provisioned budget
+- Remaining is the budget left at the given time
diff --git a/sources/doc/calendar-configuration.markdown b/sources/doc/calendar-configuration.markdown
new file mode 100644
index 0000000..95e13e5
--- /dev/null
+++ b/sources/doc/calendar-configuration.markdown
@@ -0,0 +1,42 @@
+Calendar settings
+=================
+
+Go to the menu **Settings**, then choose **Calendar settings** on the left.
+
+![Calendar settings](http://kanboard.net/screenshots/documentation/calendar-settings.png)
+
+There are two different calendars in Kanboard:
+
+- Project calendar
+- User calendar (available from the dashboard)
+
+Project calendar
+----------------
+
+This calendar show tasks with defined due date and tasks based on the creation date or the start date.
+
+### Show tasks based on the creation date
+
+- The start date of the calendar event is the creation date of the task.
+- The end date of the event is the date of completion.
+
+### Show tasks based on the start date
+
+- The start date of the calendar event is the start date of the task.
+- This date can be defined manually.
+- The end date of the event is the date of completion.
+- If there is no start date the task will not appear on the calendar.
+
+User calendar
+-------------
+
+This calendar show only tasks assigned to the user and optionally subtasks information.
+
+### Show subtasks based on the time tracking
+
+- Display subtasks in the calendar from the information recorded in the time tracking table.
+- The intersection with the user timetable is also calculated.
+
+### Show subtask estimates (forecast of future work)
+
+- Display the estimate of future work for subtasks in status "todo" and with a defined "estimate" value.
diff --git a/sources/doc/calendar.markdown b/sources/doc/calendar.markdown
new file mode 100644
index 0000000..7b95baa
--- /dev/null
+++ b/sources/doc/calendar.markdown
@@ -0,0 +1,20 @@
+Calendar
+========
+
+There are two different views for the calendar:
+
+- The project view with filters (available from the board)
+- The user view (available from the dashboard and from the user section)
+
+At this time the calendar is able to display these information:
+
+- Tasks with a due date, displayed at the top. **The due date can be changed by moving the task to another day**.
+- Tasks based on the creation date or the start date. **These events cannot be modified with the calendar**.
+- Subtask time tracking, all recorded time slot will be shown in the calendar.
+- Subtask estimates, forecast of work left
+
+![Calendar](http://kanboard.net/screenshots/documentation/calendar.png)
+
+The calendar configuration can be changed in the settings page.
+
+Note: The due date doesn't contains time information.
diff --git a/sources/doc/centos-installation.markdown b/sources/doc/centos-installation.markdown
new file mode 100644
index 0000000..6cfc31f
--- /dev/null
+++ b/sources/doc/centos-installation.markdown
@@ -0,0 +1,77 @@
+Centos Installation
+===================
+
+Centos 7
+--------
+
+Install PHP and Apache:
+
+```bash
+yum install -y php php-mbstring php-pdo php-gd unzip wget
+```
+
+By default Centos 7 use PHP 5.4.16 and Apache 2.4.6.
+
+Restart Apache:
+
+```bash
+systemctl restart httpd.service
+```
+
+Install Kanboard:
+
+```bash
+cd /var/www/html
+wget http://kanboard.net/kanboard-latest.zip
+unzip kanboard-latest.zip
+chown -R apache:apache kanboard/data
+rm kanboard-latest.zip
+```
+
+If SELinux is enabled, be sure that the Apache user can write to the directory data:
+
+```bash
+chcon -R -t httpd_sys_content_rw_t /var/www/html/kanboard/data
+```
+
+Be sure to configure your server to allow Kanboard to send emails and make external network requests, by example with SELinux:
+
+```bash
+setsebool -P httpd_can_network_connect=1
+```
+
+Allowing external connections is necessary if you use LDAP, SMTP, Webhooks or any third-party integrations.
+
+Centos 6.x
+----------
+
+Install PHP and Apache:
+
+```bash
+yum install -y php php-mbstring php-pdo php-gd unzip wget
+```
+
+By default Centos 6.5 use PHP 5.3.3 and Apache 2.2.15.
+
+Enable short tags:
+
+- Edit the file `/etc/php.ini`
+- Change the line `short_open_tag = On`
+
+Restart Apache:
+
+```bash
+service httpd restart
+```
+
+Install Kanboard:
+
+```bash
+cd /var/www/html
+wget http://kanboard.net/kanboard-latest.zip
+unzip kanboard-latest.zip
+chown -R apache:apache kanboard/data
+rm kanboard-latest.zip
+```
+
+Go to `http://your_server/kanboard/`.
\ No newline at end of file
diff --git a/sources/doc/cli.markdown b/sources/doc/cli.markdown
new file mode 100644
index 0000000..38cba49
--- /dev/null
+++ b/sources/doc/cli.markdown
@@ -0,0 +1,144 @@
+Command Line Interface
+======================
+
+Kanboard provide a simple command line interface that can be used from any Unix terminal.
+This tool can be used only on the local machine.
+
+This feature is useful to run commands outside the web server process by example running a huge report.
+
+Usage
+-----
+
+- Open a terminal and go to your Kanboard directory (example: `cd /var/www/kanboard`)
+- Run the command `./kanboard`
+
+```bash
+Kanboard version master
+
+Usage:
+ command [options] [arguments]
+
+Options:
+ -h, --help Display this help message
+ -q, --quiet Do not output any message
+ -V, --version Display this application version
+ --ansi Force ANSI output
+ --no-ansi Disable ANSI output
+ -n, --no-interaction Do not ask any interactive question
+ -v|vv|vvv, --verbose Increase the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debug
+
+Available commands:
+ help Displays help for a command
+ list Lists commands
+ export
+ export:daily-project-column-stats Daily project column stats CSV export (number of tasks per column and per day)
+ export:subtasks Subtasks CSV export
+ export:tasks Tasks CSV export
+ export:transitions Task transitions CSV export
+ locale
+ locale:compare Compare application translations with the fr_FR locale
+ locale:sync Synchronize all translations based on the fr_FR locale
+ notification
+ notification:overdue-tasks Send notifications for overdue tasks
+ projects
+ projects:daily-stats Calculate daily statistics for all projects
+```
+
+Available commands
+------------------
+
+### Tasks CSV export
+
+Usage:
+
+```bash
+./kanboard export:tasks
+```
+
+Example:
+
+```bash
+./kanboard export:tasks 1 2014-10-01 2014-11-30 > /tmp/my_custom_export.csv
+```
+
+CSV data are sent to `stdout`.
+
+### Subtasks CSV export
+
+Usage:
+
+```bash
+./kanboard export:subtasks
+```
+
+Example:
+
+```bash
+./kanboard export:subtasks 1 2014-10-01 2014-11-30 > /tmp/my_custom_export.csv
+```
+
+### Task transitions CSV export
+
+Usage:
+
+```bash
+./kanboard export:transitions
+```
+
+Example:
+
+```bash
+./kanboard export:transitions 1 2014-10-01 2014-11-30 > /tmp/my_custom_export.csv
+```
+
+### Export daily summaries data in CSV
+
+The exported data will be printed on the standard output:
+
+```bash
+./kanboard export:daily-project-column-stats
+```
+
+Example:
+
+```bash
+./kanboard export:daily-project-column-stats 1 2014-10-01 2014-11-30 > /tmp/my_custom_export.csv
+```
+
+### Send notifications for overdue tasks
+
+Emails will be sent to all users with notifications enabled.
+
+```bash
+./kanboard notification:overdue-tasks
+```
+
+You can also display the overdue tasks with the flag `--show`:
+
+```bash
+$ ./kanboard notification:overdue-tasks --show
++-----+---------+------------+------------+--------------+----------+
+| Id | Title | Due date | Project Id | Project name | Assignee |
++-----+---------+------------+------------+--------------+----------+
+| 201 | Test | 2014-10-26 | 1 | Project #0 | admin |
+| 202 | My task | 2014-10-28 | 1 | Project #0 | |
++-----+---------+------------+------------+--------------+----------+
+```
+
+Cronjob example:
+
+```bash
+# Everyday at 8am we check for due tasks
+0 8 * * * cd /path/to/kanboard && ./kanboard notification:overdue-tasks >/dev/null 2>&1
+```
+
+### Run daily project stats calculation
+
+You can add a background task to calculate the project statistics everyday:
+
+```bash
+$ ./kanboard projects:daily-stats
+Run calculation for Project #0
+Run calculation for Project #1
+Run calculation for Project #10
+```
diff --git a/sources/doc/closing-tasks.markdown b/sources/doc/closing-tasks.markdown
new file mode 100644
index 0000000..235387a
--- /dev/null
+++ b/sources/doc/closing-tasks.markdown
@@ -0,0 +1,16 @@
+Closing tasks
+=============
+
+When a task is closed, they are hidden from the board.
+
+However, you can always access to the list of closed tasks by using the query **status:closed** in any search form or simply choose **Closed tasks** from the filter dropdown.
+
+There are two different way to close a task, from the task dropdown menu on the board:
+
+![Close a task from dropdown menu](http://kanboard.net/screenshots/documentation/menu-close-task.png)
+
+Or from the task sidebar menu in the task detail view:
+
+![Close a task](http://kanboard.net/screenshots/documentation/closing-tasks.png)
+
+Note: When you close a task, all subtasks not completed will be changed to the status "Done".
\ No newline at end of file
diff --git a/sources/doc/coding-standards.markdown b/sources/doc/coding-standards.markdown
new file mode 100644
index 0000000..e0e762d
--- /dev/null
+++ b/sources/doc/coding-standards.markdown
@@ -0,0 +1,24 @@
+Coding standards
+================
+
+PHP code
+--------
+
+- Indentation: 4 spaces
+- Line return: Unix => `\n`
+- Encoding: UTF-8
+- Use only the opening tags ` `\n`
+
+CSS code
+--------
+
+- Indentation: 4 spaces
+- Line return: Unix => `\n`
diff --git a/sources/doc/config.markdown b/sources/doc/config.markdown
new file mode 100644
index 0000000..b5c3ce0
--- /dev/null
+++ b/sources/doc/config.markdown
@@ -0,0 +1,242 @@
+Config file
+===========
+
+You can customize the default settings of Kanboard by adding a file `config.php` at the project root.
+You can also rename the `config.default.php` and change the desired values.
+
+Enable/Disable debug mode
+-------------------------
+
+```php
+define('DEBUG', false);
+```
+
+The debug mode logs all SQL queries and the time taken to generate pages.
+
+Debug file path
+---------------
+
+```php
+define('DEBUG_FILE', __DIR__.'/data/debug.log');
+```
+
+All debug information are saved in this file.
+If you prefer to send logs to `stdout` or `stderr` replace the value by `php://stdout` or `php://stderr`.
+
+Folder for uploaded files
+-------------------------
+
+```php
+define('FILES_DIR', 'data/files/');
+```
+
+Don't forget the trailing slash.
+
+Enable/disable url rewrite
+--------------------------
+
+```php
+define('ENABLE_URL_REWRITE', false);
+```
+
+Email configuration
+-------------------
+
+```php
+// E-mail address for the "From" header (notifications)
+define('MAIL_FROM', 'notifications@kanboard.local');
+
+// Mail transport to use: "smtp", "sendmail" or "mail" (PHP mail function)
+define('MAIL_TRANSPORT', 'mail');
+
+// SMTP configuration to use when the "smtp" transport is chosen
+define('MAIL_SMTP_HOSTNAME', '');
+define('MAIL_SMTP_PORT', 25);
+define('MAIL_SMTP_USERNAME', '');
+define('MAIL_SMTP_PASSWORD', '');
+define('MAIL_SMTP_ENCRYPTION', null); // Valid values are "null", "ssl" or "tls"
+
+// Sendmail command to use when the transport is "sendmail"
+define('MAIL_SENDMAIL_COMMAND', '/usr/sbin/sendmail -bs');
+```
+
+Database settings
+-----------------
+
+```php
+// Database driver: sqlite, mysql or postgres (sqlite by default)
+define('DB_DRIVER', 'sqlite');
+
+// Mysql/Postgres username
+define('DB_USERNAME', 'root');
+
+// Mysql/Postgres password
+define('DB_PASSWORD', '');
+
+// Mysql/Postgres hostname
+define('DB_HOSTNAME', 'localhost');
+
+// Mysql/Postgres database name
+define('DB_NAME', 'kanboard');
+
+// Mysql/Postgres custom port (null = default port)
+define('DB_PORT', null);
+```
+
+LDAP settings
+-------------
+
+```php
+// Enable LDAP authentication (false by default)
+define('LDAP_AUTH', false);
+
+// LDAP server hostname
+define('LDAP_SERVER', '');
+
+// LDAP server port (389 by default)
+define('LDAP_PORT', 389);
+
+// By default, require certificate to be verified for ldaps:// style URL. Set to false to skip the verification.
+define('LDAP_SSL_VERIFY', true);
+
+// Enable LDAP START_TLS
+define('LDAP_START_TLS', false);
+
+// LDAP bind type: "anonymous", "user" (use the given user/password from the form) and "proxy" (a specific user to browse the LDAP directory)
+define('LDAP_BIND_TYPE', 'anonymous');
+
+// LDAP username to connect with. null for anonymous bind (by default).
+// Or for user bind type, you can use a pattern: %s@kanboard.local
+define('LDAP_USERNAME', null);
+
+// LDAP password to connect with. null for anonymous bind (by default).
+define('LDAP_PASSWORD', null);
+
+// LDAP account base, i.e. root of all user account
+// Example: ou=People,dc=example,dc=com
+define('LDAP_ACCOUNT_BASE', '');
+
+// LDAP query pattern to use when searching for a user account
+// Example for ActiveDirectory: '(&(objectClass=user)(sAMAccountName=%s))'
+// Example for OpenLDAP: 'uid=%s'
+define('LDAP_USER_PATTERN', '');
+
+// Name of an attribute of the user account object which should be used as the full name of the user.
+define('LDAP_ACCOUNT_FULLNAME', 'displayname');
+
+// Name of an attribute of the user account object which should be used as the email of the user.
+define('LDAP_ACCOUNT_EMAIL', 'mail');
+
+// Name of an attribute of the user account object which should be used as the id of the user.
+// Example for ActiveDirectory: 'samaccountname'
+// Example for OpenLDAP: 'uid'
+define('LDAP_ACCOUNT_ID', 'samaccountname');
+
+// By default Kanboard lowercase the ldap username to avoid duplicate users (the database is case sensitive)
+// Set to true if you want to preserve the case
+define('LDAP_USERNAME_CASE_SENSITIVE', false);
+
+// Automatically create user account
+define('LDAP_ACCOUNT_CREATION', true);
+```
+
+Google Authentication settings
+------------------------------
+
+```php
+// Enable/disable Google authentication
+define('GOOGLE_AUTH', false);
+
+// Google client id (Get this value from the Google developer console)
+define('GOOGLE_CLIENT_ID', '');
+
+// Google client secret key (Get this value from the Google developer console)
+define('GOOGLE_CLIENT_SECRET', '');
+```
+
+Github Authentication settings
+------------------------------
+
+```php
+// Enable/disable GitHub authentication
+define('GITHUB_AUTH', false);
+
+// GitHub client id (Copy it from your settings -> Applications -> Developer applications)
+define('GITHUB_CLIENT_ID', '');
+
+// GitHub client secret key (Copy it from your settings -> Applications -> Developer applications)
+define('GITHUB_CLIENT_SECRET', '');
+```
+
+Reverse-Proxy Authentication settings
+-------------------------------------
+
+```php
+// Enable/disable the reverse proxy authentication
+define('REVERSE_PROXY_AUTH', false);
+
+// Header name to use for the username
+define('REVERSE_PROXY_USER_HEADER', 'REMOTE_USER');
+
+// Username of the admin, by default blank
+define('REVERSE_PROXY_DEFAULT_ADMIN', '');
+
+// Default domain to use for setting the email address
+define('REVERSE_PROXY_DEFAULT_DOMAIN', '');
+```
+
+RememberMe Authentication settings
+----------------------------------
+
+```php
+// Enable/disable remember me authentication
+define('REMEMBER_ME_AUTH', true);
+```
+
+Secure HTTP headers settings
+----------------------------
+
+```php
+// Enable or disable "Strict-Transport-Security" HTTP header
+define('ENABLE_HSTS', true);
+
+// Enable or disable "X-Frame-Options: DENY" HTTP header
+define('ENABLE_XFRAME', true);
+```
+
+Bruteforce protection
+---------------------
+
+```php
+// Enable captcha after 3 authentication failure
+define('BRUTEFORCE_CAPTCHA', 3);
+
+// Lock the account after 6 authentication failure
+define('BRUTEFORCE_LOCKDOWN', 6);
+
+// Lock account duration in minute
+define('BRUTEFORCE_LOCKDOWN_DURATION', 15);
+```
+
+Session
+-------
+
+```php
+// Session duration in second (0 = until the browser is closed)
+// See http://php.net/manual/en/session.configuration.php#ini.session.cookie-lifetime
+define('SESSION_DURATION', 0);
+```
+
+Various settings
+----------------
+
+```php
+// Escape html inside markdown text
+define('MARKDOWN_ESCAPE_HTML', true);
+
+// API alternative authentication header, the default is HTTP Basic Authentication defined in RFC2617
+define('API_AUTHENTICATION_HEADER', '');
+
+// Hide login form, useful if all your users use Google/Github/ReverseProxy authentication
+define('HIDE_LOGIN_FORM', false);
+```
diff --git a/sources/doc/contributing.markdown b/sources/doc/contributing.markdown
new file mode 100644
index 0000000..63c5678
--- /dev/null
+++ b/sources/doc/contributing.markdown
@@ -0,0 +1,69 @@
+Contributor Guidelines
+======================
+
+How can I help?
+---------------
+
+Kanboard is not perfect but there is many ways to help:
+
+- Give feedback
+- Report bugs
+- Add or update translations
+- Improve the documentation
+- Writing code
+- Tell your friends that Kanboard is awesome :)
+
+Before doing any large undertaking, open a new issue and explain your proposal.
+
+I want to give feedback
+-----------------------
+
+- You think something should be improved (user interface, feature request)
+- Check if your idea is not already proposed
+- Open a new issue
+- Describe your idea
+- You can also up vote with +1 on existing proposals
+
+I want to report a bug
+----------------------
+
+- Check if the issue is not already reported
+- Open a new ticket
+- Explain what is broken
+- Describe how to reproduce the bug
+- Describe your environment (Kanboard version, OS, web server, PHP version, database version, hosting type)
+
+I want to translate Kanboard
+----------------------------
+
+Kanboard is translated in many languages.
+However, translations are not complete, take look at the [translation guide to contribute](http://kanboard.net/documentation/translations).
+
+I want to improve the documentation
+-----------------------------------
+
+- You think something is not clear, there is grammatical errors, typo errors, anything.
+- The documentation is written in Markdown and stored in the folder `docs`.
+- Edit the file and send a pull-request.
+- The documentation on the official website is synchronized with the repository.
+
+I want to contribute to the code
+--------------------------------
+
+Pull-requests are always welcome, however to be accepted you have to follow those directives:
+
+- **Before doing any large change or design proposal, open a new ticket to start a discussion.**
+- If you want to add a new feature, respect the philosophy behind Kanboard. **We focus on simplicity**, we don't want to have a bloated software.
+- The same apply for the user interface, **simplicity and efficiency**.
+- Send only one pull-request per feature or bug fix, your patch will be merged into one single commit in the master branch.
+- Make sure the [unit tests pass](tests.markdown).
+- Respect the [coding standards](coding-standards.markdown).
+- Write maintainable code, avoid code duplication, use PHP good practices.
+
+In any case, if you are not sure about something open a new ticket.
+
+Tell your friends that Kanboard is awesome :)
+---------------------------------------------
+
+If you use Kanboard, spread the word around you.
+Tell them that free and open source software are cool :)
diff --git a/sources/doc/create-tasks-by-email.markdown b/sources/doc/create-tasks-by-email.markdown
new file mode 100644
index 0000000..46dae48
--- /dev/null
+++ b/sources/doc/create-tasks-by-email.markdown
@@ -0,0 +1,44 @@
+Create tasks by email
+=====================
+
+You can create tasks directly by sending an email.
+
+At the moment, Kanboard is integrated with 3 external services:
+
+- [Mailgun](http://kanboard.net/documentation/mailgun)
+- [Sendgrid](http://kanboard.net/documentation/sendgrid)
+- [Postmark](http://kanboard.net/documentation/postmark)
+
+These services handle incoming emails without having to configure any SMTP server.
+
+When an email is received, Kanboard receive the message on a specific end-point.
+All complicated works are already handled by those services.
+
+Incoming emails workflow
+------------------------
+
+1. You send an email to a specific address, by example **something+myproject@inbound.mydomain.tld**
+2. Your email is forwarded to the third-party SMTP servers
+3. The SMTP provider call the Kanboard webhook with the email in JSON or multipart/form-data formats
+4. Kanboard parse the received email and create the task to the right project
+
+Note: New tasks are automatically created in the first column.
+
+Email format
+------------
+
+- The local part of the email address must use the plus separator, by example **kanboard+project123**
+- The string defined after the plus sign must match a project identifier, by example **project123** is the identifier of the project **Project 123**
+- The email subject becomes the task title
+- The email body becomes the task description (Markdown format)
+
+Incoming emails can be written in text or HTML formats.
+**Kanboard is able to convert simple HTML emails to Markdown**.
+
+Security and requirements
+-------------------------
+
+- The Kanboard webhook is protected by a random token
+- The sender email address must match a Kanboard user
+- The Kanboard project must have a unique identifier, by example **MYPROJECT**
+- The Kanboard user must be member of the project
diff --git a/sources/doc/creating-projects.markdown b/sources/doc/creating-projects.markdown
new file mode 100644
index 0000000..6177d16
--- /dev/null
+++ b/sources/doc/creating-projects.markdown
@@ -0,0 +1,32 @@
+Creating projects
+=================
+
+Kanboard can handle multiple projects. There are two kinds of project:
+
+- Project with mutliple users (you work in team)
+- Private project for a single user
+
+Creating projects for multiple users
+-------------------------------------
+
+- Only administrators and project administrators can create those projects
+- User management is available
+
+From the dashboard, click on the link **New project**:
+
+![Project creation form](http://kanboard.net/screenshots/documentation/project-creation-form.png)
+
+It's very easy, you just have to find a name for your project!
+
+Creating a private project
+--------------------------
+
+- Everybody can create a private project
+- There is **NO** user management
+- Only the owner and administrators can access to the project
+
+From the dashboard, click on the link **New private project**.
+
+![New private project](http://kanboard.net/screenshots/documentation/new-private-project.png)
+
+Note: project names must be unique across the application.
diff --git a/sources/doc/creating-tasks.markdown b/sources/doc/creating-tasks.markdown
new file mode 100644
index 0000000..afcc5ec
--- /dev/null
+++ b/sources/doc/creating-tasks.markdown
@@ -0,0 +1,27 @@
+Creating tasks
+==============
+
+From the board, click on the plus sign next to the column name:
+
+![Task creation from the board](http://kanboard.net/screenshots/documentation/task-creation-board.png)
+
+Then the task creation form appears:
+
+![Task creation form](http://kanboard.net/screenshots/documentation/task-creation-form.png)
+
+The only mandatory field is the title.
+
+Field description:
+
+- **Title**: The title of your task, that will be displayed on the board.
+- **Description**: Allow you to add more information about the task, the content can be written in [Markdown](http://kanboard.net/documentation/syntax-guide).
+- **Create another task**: Check this box if you want to create a similar task (fields will be prefilled).
+- **Assignee**: The person that will work on the task.
+- **Category**: Only one category can be assign to a task.
+- **Column**: The column where the task will be created, your task will be positioned at the bottom.
+- **Color**: Choose the color of the card.
+- **Complexity**: Used in agile project management (Scrum), the complexity or story points is a number that tells the team how hard the story is. Often, people use the fibonacci series.
+- **Original Estimate**: Estimation in hours to complete the tasks.
+- **Due Date**: Overdue tasks will have a red due date and upcoming due dates will be black on the board. Several date format are accepted in addition to the date picker.
+
+With the preview link, you can see the task description converted from the Markdown syntax.
\ No newline at end of file
diff --git a/sources/doc/currency-rate.markdown b/sources/doc/currency-rate.markdown
new file mode 100644
index 0000000..b959e4d
--- /dev/null
+++ b/sources/doc/currency-rate.markdown
@@ -0,0 +1,11 @@
+Currency Rate
+==============
+
+Since each user can have a predefined hourly rate in different currency.
+If you have to handle multiple currencies, you define here the rate according to the reference currency.
+
+This feature is used for project budget calculation.
+
+![Currency Rate](http://kanboard.net/screenshots/documentation/currency-rate.png)
+
+Currency rate settings are located in **Settings > Currency rates**.
\ No newline at end of file
diff --git a/sources/doc/debian-installation.markdown b/sources/doc/debian-installation.markdown
new file mode 100644
index 0000000..147fe45
--- /dev/null
+++ b/sources/doc/debian-installation.markdown
@@ -0,0 +1,63 @@
+How to install Kanboard on Debian?
+==================================
+
+Debian 8 (Jessie)
+-----------------
+
+Install Apache and PHP:
+
+```bash
+apt-get update
+apt-get install -y php5 php5-sqlite php5-gd unzip
+service apache2 restart
+```
+
+Install Kanboard:
+
+```bash
+cd /var/www/html
+wget http://kanboard.net/kanboard-latest.zip
+unzip kanboard-latest.zip
+chown -R www-data:www-data kanboard/data
+rm kanboard-latest.zip
+```
+
+Debian 7 (Wheezy)
+-----------------
+
+Install Apache and PHP:
+
+```bash
+apt-get update
+apt-get install -y php5 php5-sqlite php5-gd unzip
+```
+
+Install Kanboard:
+
+```bash
+cd /var/www
+wget http://kanboard.net/kanboard-latest.zip
+unzip kanboard-latest.zip
+chown -R www-data:www-data kanboard/data
+rm kanboard-latest.zip
+```
+
+Debian 6 (Squeeze)
+------------------
+
+Install Apache and PHP:
+
+```bash
+apt-get update
+apt-get install -y libapache2-mod-php5 php5-sqlite php5-gd unzip
+```
+
+Install Kanboard:
+
+```bash
+cd /var/www
+wget http://kanboard.net/kanboard-latest.zip
+unzip kanboard-latest.zip
+chown -R www-data:www-data kanboard/data
+rm kanboard-latest.zip
+```
diff --git a/sources/doc/docker.markdown b/sources/doc/docker.markdown
new file mode 100644
index 0000000..44f3b97
--- /dev/null
+++ b/sources/doc/docker.markdown
@@ -0,0 +1,47 @@
+How to run Kanboard with Docker?
+================================
+
+Kanboard can run easily with [Docker](https://www.docker.com).
+There is a `Dockerfile` in the repository to build your own container.
+
+Use the automated build
+-----------------------
+
+Every new commit on the repository trigger a new build on [Docker Hub](https://registry.hub.docker.com/u/kanboard/kanboard/).
+
+```bash
+docker pull kanboard/kanboard
+docker run -d --name kanboard -p 80:80 -t kanboard/kanboard:latest
+```
+
+The tag **latest** is the **development version** of Kanboard, use at your own risk.
+
+Build your own Docker image
+---------------------------
+
+Clone the Kanboard repository and run the following command:
+
+```bash
+docker build -t youruser/kanboard:master .
+```
+
+To run your image in background on the port 80:
+
+```bash
+docker run -d --name kanboard -p 80:80 -t youruser/kanboard:master
+```
+
+Store your data on a volume
+---------------------------
+
+By default Kanboard will store attachments and the Sqlite database in the directory data. Run this command to use a custom volume path:
+
+```bash
+docker run -d --name kanboard -v /your/local/data/folder:/var/www/html/data -p 80:80 -t kanboard/kanboard:master
+```
+
+References
+----------
+
+- [Official Kanboard images](https://registry.hub.docker.com/u/kanboard/kanboard/)
+- [Docker documentation](https://docs.docker.com/)
diff --git a/sources/doc/duplicate-move-tasks.markdown b/sources/doc/duplicate-move-tasks.markdown
new file mode 100644
index 0000000..dcb01df
--- /dev/null
+++ b/sources/doc/duplicate-move-tasks.markdown
@@ -0,0 +1,58 @@
+Duplicate and move tasks
+========================
+
+Duplicate a task into the same project
+--------------------------------------
+
+Go to the task view and choose **Duplicate** on the left.
+
+![Task Duplication](http://kanboard.net/screenshots/documentation/task-duplication.png)
+
+A new task will be created with the same properties as the original.
+
+Duplicate a task to another project
+-----------------------------------
+
+Go to the task view and choose **Duplicate to another project**.
+
+![Task Duplication Another Project](http://kanboard.net/screenshots/documentation/task-duplication-another-project.png)
+
+Only projects where you are member will be shown in the dropdown.
+
+Before to copy the tasks, Kanboard will ask you the destination properties that are not common between the source and destination project.
+
+Basically, you need to define:
+
+- The destination swimlane
+- The column
+- The category
+- The assignee
+
+Move a task to another project
+------------------------------
+
+Go to the task view and choose **Move to another project**.
+
+Moving a task to another project work in the same way as the duplication, you have to choose the new properties of the task.
+
+List of fields duplicated
+-------------------------
+
+Here are the list of properties duplicated:
+
+- title
+- description
+- date_due
+- color_id
+- project_id
+- column_id
+- owner_id
+- score
+- category_id
+- time_estimated
+- swimlane_id
+- recurrence_status
+- recurrence_trigger
+- recurrence_factor
+- recurrence_timeframe
+- recurrence_basedate
diff --git a/sources/doc/editing-projects.markdown b/sources/doc/editing-projects.markdown
new file mode 100644
index 0000000..7e0fc8e
--- /dev/null
+++ b/sources/doc/editing-projects.markdown
@@ -0,0 +1,15 @@
+Editing projects
+================
+
+Projects can be renamed and disabled at any time.
+
+To rename a project, just click on the link "Edit project" on the left.
+
+![Project edition](http://kanboard.net/screenshots/documentation/project-edition.png)
+
+- The start date and end date are used to generate the project Gantt chart
+- The description is visible as tooltip on the board and on the projects listing page
+- Administrators and project administrators can convert a private project to a multiple users project by changing the checkbox "Private project".
+- You can also convert a multiple users project to a private project.
+
+Note: When you make a project private, all existing users will still have access to the project. Adjust the list of users according to your needs.
diff --git a/sources/doc/email-configuration.markdown b/sources/doc/email-configuration.markdown
new file mode 100644
index 0000000..c66996c
--- /dev/null
+++ b/sources/doc/email-configuration.markdown
@@ -0,0 +1,173 @@
+Email configuration
+===================
+
+User settings
+-------------
+
+To receive email notifications, users of Kanboard must have:
+
+- Activated notifications in their profile
+- Have a valid email address in their profile
+- Be member of the project that will trigger notifications
+
+Note: The logged user who performs the action doesn't receive any notifications, only other project members.
+
+Email transports
+----------------
+
+There are several email transports available:
+
+- SMTP
+- Sendmail
+- PHP native mail function
+- Mailgun
+- Postmark
+- Sendgrid
+
+Server settings
+---------------
+
+By default, Kanboard will use the bundled PHP mail function to send emails.
+Usually that require no configuration if your server can already send emails.
+
+However, it's possible to use other methods, the SMTP protocol and Sendmail.
+
+### SMTP configuration
+
+Rename the file `config.default.php` to `config.php` and change these values:
+
+```php
+// We choose "smtp" as mail transport
+define('MAIL_TRANSPORT', 'smtp');
+
+// We define our server settings
+define('MAIL_SMTP_HOSTNAME', 'mail.example.com');
+define('MAIL_SMTP_PORT', 25);
+
+// Credentials for authentication on the SMTP server (not mandatory)
+define('MAIL_SMTP_USERNAME', 'username');
+define('MAIL_SMTP_PASSWORD', 'super password');
+```
+
+It's also possible to use a secure connection, TLS or SSL:
+
+```php
+define('MAIL_SMTP_ENCRYPTION', 'ssl'); // Valid values are "null", "ssl" or "tls"
+```
+
+### Sendmail configuration
+
+By default the sendmail command will be `/usr/sbin/sendmail -bs` but you can customize that in your config file.
+
+Example:
+
+```php
+// We choose "sendmail" as mail transport
+define('MAIL_TRANSPORT', 'sendmail');
+
+// If you need to change the sendmail command, replace the value
+define('MAIL_SENDMAIL_COMMAND', '/usr/sbin/sendmail -bs');
+```
+
+### PHP native mail function
+
+This is the default configuration:
+
+```php
+define('MAIL_TRANSPORT', 'mail');
+```
+
+### Mailgun HTTP API
+
+You can use the HTTP API of Mailgun to send emails.
+
+Configuration:
+
+```php
+// We choose "mailgun" as mail transport
+define('MAIL_TRANSPORT', 'mailgun');
+
+// Mailgun API key
+define('MAILGUN_API_TOKEN', 'YOUR_API_KEY');
+
+// Mailgun domain name
+define('MAILGUN_DOMAIN', 'YOUR_DOMAIN_CONFIGURED_IN_MAILGUN');
+
+// Be sure to use the sender email address configured in Mailgun
+define('MAIL_FROM', 'sender-address-configured-in-mailgun@example.org');
+```
+
+### Postmark HTTP API
+
+Postmark is a third-party email service.
+If you already use the Postmark integration to receive emails in Kanboard you can use the same provider to send email too.
+
+This system use their HTTP API instead of the SMTP protocol.
+
+Here are the required settings for this configuration:
+
+```php
+// We choose "postmark" as mail transport
+define('MAIL_TRANSPORT', 'postmark');
+
+// Copy and paste your Postmark API token
+define('POSTMARK_API_TOKEN', 'COPY HERE YOUR POSTMARK API TOKEN');
+
+// Be sure to use the Postmark configured sender email address
+define('MAIL_FROM', 'sender-address-configured-in-postmark@example.org');
+```
+
+### Sendgrid HTTP API
+
+You can use the HTTP API of Sendgrid to send emails.
+
+Configuration:
+
+```php
+// We choose "sendgrid" as mail transport
+define('MAIL_TRANSPORT', 'sendgrid');
+
+// Sendgrid username
+define('SENDGRID_API_USER', 'YOUR_SENDGRID_USERNAME');
+
+// Sendgrid password
+define('SENDGRID_API_KEY', 'YOUR_SENDGRID_PASSWORD');
+```
+
+### The sender email address
+
+By default, emails will use the sender address `notifications@kanboard.local`.
+It's not possible to reply to this address.
+
+You can customize this address by changing the value of the constant `MAIL_FROM` in your config file.
+
+```php
+define('MAIL_FROM', 'kanboard@mydomain.tld');
+```
+
+That can be useful if your SMTP server configuration doesn't accept the default address.
+
+### How to display a link to the task in notifications?
+
+To do that, you have to specify the URL of your Kanboard installation in your [Application Settings](http://kanboard.net/documentation/application-configuration).
+By default, nothing is defined, so no links will be displayed.
+
+Examples:
+
+- http://demo.kanboard.net/
+- http://myserver/kanboard/
+- http://kanboard.mydomain.com/
+
+Don't forget the ending slash `/`.
+
+You need to define that manually because Kanboard cannot guess the URL from a command line script and some people have very specific configuration.
+
+Troubleshooting
+---------------
+
+If no emails are send and you are sure that everything is configured correctly:
+
+- Check your spam folder
+- Enable the debug mode and check the debug file `data/debug.log`, you should see the exact error
+- Be sure that your server or your hosting provider allow you to send emails
+- If you use SeLinux, allow PHP to send emails
diff --git a/sources/doc/faq.markdown b/sources/doc/faq.markdown
new file mode 100644
index 0000000..3d542a3
--- /dev/null
+++ b/sources/doc/faq.markdown
@@ -0,0 +1,113 @@
+Frequently Asked Questions
+==========================
+
+Can you recommend a web hosting provider for Kanboard?
+------------------------------------------------------
+
+Kanboard works well with any great VPS hosting provider such as [Digital Ocean](https://www.digitalocean.com/?refcode=4b541f47aae4),
+[Linode](https://www.linode.com/?r=4e381ac8a61116f40c60dc7438acc719610d8b11) or [Gandi](https://www.gandi.net/).
+
+To have the best performances, choose a provider with fast disk I/O because Kanboard use Sqlite by default.
+Avoid hosting providers that use a shared NFS mount point.
+
+I get a blank page after installing or upgrading Kanboard
+---------------------------------------------------------
+
+- Check if you have installed all requirements on your server
+- Check if the files have the correct permissions
+- If you use php-fpm and opcode caching, reload the process to be sure to clear the cache
+- Enable PHP error logging in your php.ini
+- Check the PHP and Apache error logs you should see the exact error
+
+
+Page not found and the url seems wrong (&)
+----------------------------------------------
+
+- The url looks like `/?controller=auth&action=login&redirect_query=` instead of `?controller=auth&action=login&redirect_query=`
+- Kanboard returns a "Page not found" error
+
+This issue come from your PHP configuration, the value of `arg_separator.output` is not the PHP's default, there is different ways to fix that:
+
+Change the value directly in your `php.ini` if you have the permission:
+
+```
+arg_separator.output = "&"
+```
+
+Override the value with a `.htaccess`:
+
+```
+php_value arg_separator.output "&"
+```
+
+Otherwise Kanboard will try to override the value directly in PHP.
+
+
+Known issues with eAccelerator
+------------------------------
+
+Kanboard doesn't work very well with [eAccelerator](http://eaccelerator.net).
+The issue caused can be a blank page or an Apache crash:
+
+```
+[Wed Mar 05 21:36:56 2014] [notice] child pid 22630 exit signal Segmentation fault (11)
+```
+
+The best way to avoid this issue is to disable eAccelerator or define manually which files you want to cache with the config parameter `eaccelerator.filter`.
+
+The project [eAccelerator seems dead and not updated since 2012](https://github.com/eaccelerator/eaccelerator/commits/master).
+We recommend to switch to the last version of PHP because it's bundled with [OPcache](http://php.net/manual/en/intro.opcache.php).
+
+
+Why the minimum requirement is PHP 5.3.3?
+-----------------------------------------
+
+Kanboard use the function `password_hash()` to crypt passwords but it's available only for PHP >= 5.5.
+
+However, there is a backport for [older versions of PHP](https://github.com/ircmaxell/password_compat#requirements).
+This library require at least PHP 5.3.7 to work correctly.
+
+Apparently, Centos and Debian backports security patches so PHP 5.3.3 should be ok.
+
+Kanboard v1.0.10 and v1.0.11 requires at least PHP 5.3.7 but this change has been reverted to be compatible with PHP 5.3.3 with Kanboard >= v1.0.12
+
+
+How to test Kanboard with the PHP built-in web server?
+------------------------------------------------------
+
+If you don't want to install a web server like Apache on localhost. You can test with the [embedded web server of PHP](http://www.php.net/manual/en/features.commandline.webserver.php):
+
+```bash
+unzip kanboard-VERSION.zip
+cd kanboard
+php -S localhost:8000
+open http://localhost:8000/
+```
+
+
+How to migrate my tasks from Wunderlist?
+----------------------------------------
+
+You can use an external tool to import automatically your tasks and lists from Wunderlist to Kanboard.
+
+This is a command line script made by a contributor of Kanboard.
+It's simple, quick and dirty but it works :)
+
+More information here:
+
+- [Wunderlist](http://www.wunderlist.com/)
+-
+
+
+How to install Kanboard on Yunohost?
+------------------------------------
+
+[YunoHost](https://yunohost.org/) is a server operating system aiming to make self-hosting accessible to everyone.
+
+There is a [package to install Kanboard on Yunohost easily](https://github.com/mbugeia/kanboard_ynh).
+
+
+Are there some tutorials about Kanboard in other languages?
+-----------------------------------------------------------
+
+- [German article series about Kanboard](http://demaya.de/wp/2014/07/kanboard-eine-jira-alternative-im-detail-installation/)
diff --git a/sources/doc/freebsd-installation.markdown b/sources/doc/freebsd-installation.markdown
new file mode 100644
index 0000000..84b35ad
--- /dev/null
+++ b/sources/doc/freebsd-installation.markdown
@@ -0,0 +1,127 @@
+FreeBSD 10 Installation
+=======================
+
+Install from packages
+---------------------
+
+```bash
+$ pkg update
+$ pkg upgrade
+$ pkg install apache24 mod_php56 kanboard
+```
+
+Enable Apache in your `/etc/rc.conf`:
+
+```bash
+$ echo apache24_enable="YES" >> /etc/rc.conf
+```
+
+Set up PHP for Apache:
+
+```bash
+$ echo "AddType application/x-httpd-php .php" >> /usr/local/etc/apache24/Includes/php.conf
+$ echo "DirectoryIndex index.php index.html" >> /usr/local/etc/apache24/Includes/php.conf
+```
+
+Then start Apache:
+
+```bash
+$ service apache24 start
+```
+
+Add symlink to Kanboard folder into your Apache docroot:
+
+```bash
+cd /usr/local/www/apache24/data
+ln -s /usr/local/www/kanboard
+```
+
+Go to http://your.server.domain.tld/kanboard and enjoy!
+
+*Notes*:
+- If you want to use additional features like LDAP integration etc.
+please install proper PHP module using pkg.
+- You may have to adjust the permissions of the folder data
+
+Installing from ports
+---------------------
+
+Generally 3 elements have to be installed:
+
+- Apache
+- mod_php for Apache
+- Kanboard
+
+Fetch and extract ports...
+
+```bash
+$ portsnap fetch
+$ portsnap extract
+```
+
+or update already existing:
+
+```bash
+$ portsnap fetch
+$ portsnap update
+```
+
+More details regarding portsnap can be found in the [FreeBSD Handbook](https://www.freebsd.org/doc/handbook/ports-using.html).
+
+Install Apache:
+
+```bash
+$ cd /usr/ports/www/apache24
+$ make install clean
+```
+Enable Apache in your `/etc/rc.conf`:
+
+```bash
+$ echo apache24_enable="YES" >> /etc/rc.conf
+```
+
+Install mod_php for Apache:
+
+```bash
+$ cd /usr/ports/www/mod_php5
+$ make install clean
+```
+
+Install Kanboard form ports:
+
+```bash
+$ cd /usr/ports/www/kanboard
+$ make install clean
+```
+
+Set up PHP for Apache:
+
+```bash
+$ echo "AddType application/x-httpd-php .php" >> /usr/local/etc/apache24/Includes/php.conf
+$ echo "DirectoryIndex index.php index.html" >> /usr/local/etc/apache24/Includes/php.conf
+```
+
+Then start Apache:
+
+```bash
+$ service apache24 start
+```
+
+Go to http://your.server.domain.tld/kanboard and enjoy!
+
+*Note*:
+If you want to use additional features like LDAP integration etc.
+please install proper PHP module from `lang/php5-extensions`.
+
+Manual installation
+-------------------
+
+As of version 1.0.16 Kanboard can be found in FreeBSD ports
+there is no need to install it manually.
+
+Please note
+-----------
+
+Port is being hosted on [bitbucket](https://bitbucket.org/if0/freebsd-kanboard/). Feel free to comment,
+fork and suggest updates!
+
\ No newline at end of file
diff --git a/sources/doc/gantt-chart-projects.markdown b/sources/doc/gantt-chart-projects.markdown
new file mode 100644
index 0000000..4dfe95f
--- /dev/null
+++ b/sources/doc/gantt-chart-projects.markdown
@@ -0,0 +1,17 @@
+Gantt chart for all projects
+============================
+
+The goal of this Gantt chart is to display an overview of all projects based on the start and end dates.
+
+- This Gantt chart is available in the project management section
+- Only project administrators and administrators can access to this section
+- Project administrators will see only projects where they are members
+- Private projects are not shown on this chart
+
+![Gantt Chart for all projects](http://kanboard.net/screenshots/documentation/gantt-chart-all-projects.png)
+
+- The **start date** and the **end date** of projects are used to draw the chart
+- Horizontal bars can be resized and moved horizontally with your mouse
+- There is no vertical drag and drop
+- Project bars are displayed in black when there is no start or end date defined
+- The information tooltip show the list of project managers and standard members
diff --git a/sources/doc/gantt-chart-tasks.markdown b/sources/doc/gantt-chart-tasks.markdown
new file mode 100644
index 0000000..ca6521a
--- /dev/null
+++ b/sources/doc/gantt-chart-tasks.markdown
@@ -0,0 +1,20 @@
+Gantt chart for tasks
+======================
+
+The goal of this Gantt chart is to display a time based overview of the tasks for a given project.
+
+- The Gantt chart is available from the "view switcher"
+- Only project managers can access to this section
+
+![Gantt Chart](http://kanboard.net/screenshots/documentation/gantt-chart-project.png)
+
+- The **start date** and the **due date** of tasks are used to draw the chart
+- Tasks can be resized and moved horizontally with your mouse
+- There is no vertical drag and drop
+- The bar is the same color as the task
+- Each bar display a progression status in percentage, this percentage is calculated by using the column position on the board
+- To fit with the Kanban model, tasks can be ordered by the board positions or by start date
+- New tasks created from this view will be displayed on the board at the position 1 in the first column
+- Tasks are displayed in black when there is no start or due date defined
+
+![Task not defined](http://kanboard.net/screenshots/documentation/gantt-chart-not-defined.png)
diff --git a/sources/doc/github-authentication.markdown b/sources/doc/github-authentication.markdown
new file mode 100644
index 0000000..ba0f371
--- /dev/null
+++ b/sources/doc/github-authentication.markdown
@@ -0,0 +1,80 @@
+Github Authentication
+=====================
+
+Requirements
+------------
+
+OAuth Github API credentials (available in your [Settings > Applications > Developer applications](https://github.com/settings/applications))
+
+How does this work?
+-------------------
+
+The Github authentication in Kanboard uses the [OAuth 2.0](http://oauth.net/2/) protocol, so any user of Kanboard can be linked to a Github account.
+
+That means you can use your Github account to login on Kanboard.
+
+How to link a Github account
+----------------------------
+
+1. Go to your user profile
+2. Click on **External accounts**
+3. Click on the link **Link my Github Account**
+4. You are redirected to the **Github Authorize application form**
+5. Authorize Kanboard by clicking on the button **Accept**
+6. Your account is now linked
+
+Now, on the login page you can be authenticated in one click with the link **Login with my Github Account**.
+
+Your name and email are automatically updated from your Github Account if defined.
+
+Installation instructions
+-------------------------
+
+### Setting up OAuth 2.0
+
+- On Github, go to the page [Register a new OAuth application](https://github.com/settings/applications/new)
+- Just follow the [official Github documentation](https://developer.github.com/guides/basics-of-authentication/#registering-your-app)
+- In Kanboard, you can get the **callback url** in **Settings > Integrations > Github Authentication**
+
+### Setting up Kanboard
+
+Either create a new `config.php` file or rename the `config.default.php` file and set the following values:
+
+```php
+// Enable/disable Github authentication
+define('GITHUB_AUTH', true);
+
+// Github client id (Copy it from your settings -> Applications -> Developer applications)
+define('GITHUB_CLIENT_ID', 'YOUR_GITHUB_CLIENT_ID');
+
+// Github client secret key (Copy it from your settings -> Applications -> Developer applications)
+define('GITHUB_CLIENT_SECRET', 'YOUR_GITHUB_CLIENT_SECRET');
+```
+
+### Github Entreprise
+
+To use this authentication method with Github Enterprise you have to change the default urls.
+
+Replace these values by your self-hosted instance of Github:
+
+```php
+// Github oauth2 authorize url
+define('GITHUB_OAUTH_AUTHORIZE_URL', 'https://github.com/login/oauth/authorize');
+
+// Github oauth2 token url
+define('GITHUB_OAUTH_TOKEN_URL', 'https://github.com/login/oauth/access_token');
+
+// Github API url (don't forget the slash at the end)
+define('GITHUB_API_URL', 'https://api.github.com/');
+```
+
+Notes
+-----
+
+Kanboard uses these information from your public Github profile:
+
+- Full name
+- Public email address
+- Github unique id
+
+The Github unique id is used to link the local user account and the Github account.
diff --git a/sources/doc/github-webhooks.markdown b/sources/doc/github-webhooks.markdown
new file mode 100644
index 0000000..a20b5a1
--- /dev/null
+++ b/sources/doc/github-webhooks.markdown
@@ -0,0 +1,99 @@
+Github webhooks integration
+===========================
+
+Kanboard can be synchronized with Github.
+Currently, it's only a one-way synchronization: Github to Kanboard.
+
+Github webhooks are plugged to Kanboard automatic actions.
+When an event occurs on Github, an action can be performed on Kanboard.
+
+List of available events
+------------------------
+
+- Github commit received
+- Github issue opened
+- Github issue closed
+- Github issue reopened
+- Github issue assignee change
+- Github issue label change
+- Github issue comment created
+
+List of available actions
+-------------------------
+
+- Create a task from an external provider
+- Change the assignee based on an external username
+- Change the category based on an external label
+- Create a comment from an external provider
+- Close a task
+- Open a task
+
+Configuration on Github
+-----------------------
+
+Go to your project settings page, on the left choose "Webhooks & Services", then click on the button "Add webhook".
+
+![Github configuration](http://kanboard.net/screenshots/documentation/github-webhooks.png)
+
+- **Payload url**: Copy and paste the link from the Kanboard project settings (section **Integrations > Github**).
+- Select **"Send me everything"**
+
+![Github webhook](http://kanboard.net/screenshots/documentation/kanboard-github-webhooks.png)
+
+Each time an event happens, Github will send an event to Kanboard now.
+The Kanboard webhook url is protected by a random token.
+
+Everything else is handled by automatic actions in your Kanboard project settings.
+
+Examples
+--------
+
+### Close a Kanboard task when a commit pushed to Github
+
+- Choose the event: **Github commit received**
+- Choose the action: **Close the task**
+
+When one or more commits are sent to Github, Kanboard will receive the information, each commit message with a task number included will be closed.
+
+Example:
+
+- Commit message: "Fix bug #1234"
+- That will close the Kanboard task #1234
+
+### Create a Kanboard task when a new issue is opened on Github
+
+- Choose the event: **Github issue opened**
+- Choose the action: **Create a task from an external provider**
+
+When a task is created from a Github issue, the link to the issue is added to the description and the task have a new field named "Reference" (this is the Github ticket number).
+
+### Close a Kanboard task when an issue is closed on Github
+
+- Choose the event: **Github issue closed**
+- Choose the action: **Close the task**
+
+### Reopen a Kanboard task when an issue is reopened on Github
+
+- Choose the event: **Github issue reopened**
+- Choose the action: **Open the task**
+
+### Assign a task to a Kanboard user when an issue is assigned on Github
+
+- Choose the event: **Github issue assignee change**
+- Choose the action: **Change the assignee based on an external username**
+
+Note: The username must be the same between Github and Kanboard and the user must be member of the project.
+
+### Assign a category when an issue is tagged on Github
+
+- Choose the event: **Github issue label change**
+- Choose the action: **Change the category based on an external label**
+- Define the label and the category
+
+### Create a comment on Kanboard when an issue is commented on Github
+
+- Choose the event: **Github issue comment created**
+- Choose the action: **Create a comment from an external provider**
+
+If the username is the same between Github and Kanboard the comment author will be assigned, otherwise there is no author.
+The user also have to be member of the project in Kanboard.
diff --git a/sources/doc/gitlab-authentication.markdown b/sources/doc/gitlab-authentication.markdown
new file mode 100644
index 0000000..3cf6d28
--- /dev/null
+++ b/sources/doc/gitlab-authentication.markdown
@@ -0,0 +1,78 @@
+Gitlab Authentication
+=====================
+
+Requirements
+------------
+
+- Account on [Gitlab.com](https://gitlab.com) or you own self-hosted Gitlab instance
+- Have Kanboard registered as application in Gitlab
+
+How does this work?
+-------------------
+
+The Gitlab authentication in Kanboard uses the [OAuth 2.0](http://oauth.net/2/) protocol, so any user of Kanboard can be linked to a Gitlab account.
+
+That means you can use your Gitlab account to login on Kanboard.
+
+How to link a Gitlab account
+----------------------------
+
+1. Go to your user profile
+2. Click on **External accounts**
+3. Click on the link **Link my Gitlab Account**
+4. You are redirected to the **Gitlab authorization form**
+5. Authorize Kanboard by clicking on the button **Accept**
+6. Your account is now linked
+
+Now, on the login page you can be authenticated in one click with the link **Login with my Gitlab Account**.
+
+Your name and email are automatically updated from your Gitlab Account if defined.
+
+Installation instructions
+-------------------------
+
+### Setting up OAuth 2.0
+
+- On Gitlab, register a new application by following the [official documentation](http://doc.gitlab.com/ce/integration/oauth_provider.html)
+- In Kanboard, you can get the **callback url** in **Settings > Integrations > Gitlab Authentication**, just copy and paste the url
+
+### Setting up Kanboard
+
+Either create a new `config.php` file or rename the `config.default.php` file and set the following values:
+
+```php
+// Enable/disable Gitlab authentication
+define('GITLAB_AUTH', true);
+
+// Gitlab application id
+define('GITLAB_CLIENT_ID', 'YOUR_APPLICATION_ID');
+
+// Gitlab application secret
+define('GITLAB_CLIENT_SECRET', 'YOUR_APPLICATION_SECRET');
+```
+
+### Custom endpoints for self-hosted Gitlab
+
+Change these default values if you use a self-hosted instance of Gitlab:
+
+```php
+// Gitlab oauth2 authorize url
+define('GITLAB_OAUTH_AUTHORIZE_URL', 'https://gitlab.com/oauth/authorize');
+
+// Gitlab oauth2 token url
+define('GITLAB_OAUTH_TOKEN_URL', 'https://gitlab.com/oauth/token');
+
+// Gitlab API url endpoint (don't forget the slash at the end)
+define('GITLAB_API_URL', 'https://gitlab.com/api/v3/');
+```
+
+Notes
+-----
+
+Kanboard uses these information from your Gitlab profile:
+
+- Full name
+- Email address
+- Gitlab unique id
+
+The Gitlab unique id is used to link the local user account and the Gitlab account.
diff --git a/sources/doc/gitlab-webhooks.markdown b/sources/doc/gitlab-webhooks.markdown
new file mode 100644
index 0000000..9d9ecaf
--- /dev/null
+++ b/sources/doc/gitlab-webhooks.markdown
@@ -0,0 +1,65 @@
+Gitlab webhooks
+===============
+
+Gitlab events can be connected to Kanboard automatic actions.
+
+List of supported events
+------------------------
+
+- Gitlab commit received
+- Gitlab issue opened
+- Gitlab issue closed
+- Gitlab issue comment created
+
+List of supported actions
+-------------------------
+
+- Create a task from an external provider
+- Close a task
+- Create a comment from an external provider
+
+Configuration
+-------------
+
+![Gitlab configuration](http://kanboard.net/screenshots/documentation/gitlab-webhooks.png)
+
+1. On Kanboard, go to the project settings and choose the section **Integrations**
+2. Copy the Gitlab webhook url
+3. On Gitlab, go to the project settings and go to the section **Webhooks**
+4. Check the boxes **Push Events**, **Comments** and **Issues Events**
+5. Paste the url and save
+
+Examples
+--------
+
+### Close a Kanboard task when a commit pushed to Gitlab
+
+- Choose the event: **Gitlab commit received**
+- Choose the action: **Close the task**
+
+When one or more commits are sent to Gitlab, Kanboard will receive the information, each commit message with a task number included will be closed.
+
+Example:
+
+- Commit message: "Fix bug #1234"
+- That will close the Kanboard task #1234
+
+### Create a Kanboard task when a new issue is opened on Gitlab
+
+- Choose the event: **Gitlab issue opened**
+- Choose the action: **Create a task from an external provider**
+
+When a task is created from a Gitlab issue, the link to the issue is added to the description and the task have a new field named "Reference" (this is the Gitlab ticket number).
+
+### Close a Kanboard task when an issue is closed on Gitlab
+
+- Choose the event: **Gitlab issue closed**
+- Choose the action: **Close the task**
+
+### Create a comment on Kanboard when an issue is commented on Gitlab
+
+- Choose the event: **Gitlab issue comment created**
+- Choose the action: **Create a comment from an external provider**
+
+If the username is the same between Gitlab and Kanboard the comment author will be assigned, otherwise there is no author.
+The user also have to be member of the project in Kanboard.
\ No newline at end of file
diff --git a/sources/doc/google-authentication.markdown b/sources/doc/google-authentication.markdown
new file mode 100644
index 0000000..0f4f3ec
--- /dev/null
+++ b/sources/doc/google-authentication.markdown
@@ -0,0 +1,64 @@
+Google Authentication
+=====================
+
+Requirements
+------------
+
+OAuth Google API credentials (available in the Google Developer Console)
+
+How does this work?
+-------------------
+
+- The Google authentication in Kanboard use the OAuth 2.0 protocol
+- Any user account in Kanboard can be linked to a Google Account
+- When a Kanboard user account is linked to Google, you can login with one click
+
+Procedure to link a Google Account
+----------------------------------
+
+1. Go to your user profile
+2. Click on **External accounts**
+3. Click on the link **Link my Google Account**
+4. You are redirected to the **Google Consent screen**
+5. Authorize Kanboard by clicking on the button **Accept**
+6. Your account is now linked
+
+Now, on the login page you can be authenticated in one click with the link **Login with my Google Account**.
+
+Your name and email are automatically updated from your Google Account.
+
+Installation instructions
+-------------------------
+
+### Setting up OAuth 2.0 in Google Developer Console
+
+- Follow the [official Google documentation](https://developers.google.com/accounts/docs/OAuth2Login#appsetup) to create a new application
+- In Kanboard, you can get the **redirect url** in **Settings > Integrations > Google Authentication**
+
+### Setting up Kanboad
+
+Create a custom `config.php` file or copy the `config.default.php` file:
+
+```php
+ Integrations > Hipchat**
+- To send notifications for only some projects, go to **Project settings > Integrations > Hipchat**
+
+Each project can send notifications to a separate room.
+
+Send notifications to a room
+-----------------------------
+
+Example of notifications:
+
+![Hipchat notification](http://kanboard.net/screenshots/documentation/hipchat-notification.png)
+
+This feature use the room notification token system of Hipchat.
+
+### Hipchat configuration
+
+![Hipchat room token](http://kanboard.net/screenshots/documentation/hipchat-room-token.png)
+
+1. Go to to **My account**
+2. Click on the tab **Rooms** and select the room you want to send the notifications
+3. On the left, choose **Tokens**
+4. Enter a label, by example "Kanboard" and save
+
+### Kanboard configuration
+
+![Hipchat settings](http://kanboard.net/screenshots/documentation/hipchat-settings.png)
+
+1. Go to **Settings > Integrations > Hipchat** or **Project settings > Integrations > Hipchat**
+2. Replace the API url if you use the self-hosted version of Hipchat
+3. Set the room name or the room API ID
+4. Copy and paste the token generated previously
+
+Now, Kanboard events will be sent to the Hipchat room.
diff --git a/sources/doc/hourly-rate.markdown b/sources/doc/hourly-rate.markdown
new file mode 100644
index 0000000..172f2f4
--- /dev/null
+++ b/sources/doc/hourly-rate.markdown
@@ -0,0 +1,11 @@
+Hourly Rate
+===========
+
+Each user can have a predefined hourly rate.
+This feature is used for budget calculation.
+
+To define a new price, go to **User profile > Hourly rates**.
+
+![Hourly Rate](http://kanboard.net/screenshots/documentation/hourly-rate.png)
+
+Each hourly rate can have an effective date and and different currency.
diff --git a/sources/doc/ical.markdown b/sources/doc/ical.markdown
new file mode 100644
index 0000000..5c52bf8
--- /dev/null
+++ b/sources/doc/ical.markdown
@@ -0,0 +1,78 @@
+Syncing your calendars
+======================
+
+Kanboard supports iCal feeds for projects and users.
+This feature allow you to import Kanboard tasks in almost any calendar program (by example Microsoft Outlook, Apple Calendar, Mozilla Thunderbird and Google Calendar).
+
+Calendar subscriptions are **read-only** access, you cannot create tasks from an external calendar software.
+The Calendar feed export follow the iCal standard.
+
+Note: Only tasks within the date range of -2 months to +6 months are exported to the iCalendar feed.
+
+Project calendars
+-----------------
+
+- Each project have its own calendar.
+- The subscription link is unique per project, the link is activated when you enable the public access of your project: **Project settings > Public access**.
+- This calendar show only tasks for the selected project.
+
+User calendars
+--------------
+
+- Each user have its own calendar.
+- The subscription link is unique per user, the link is activated when you enable the public access of your user: **User profile > Public access**.
+- This calendar show tasks assigned to the user for all projects.
+
+Adding your Kanboard calendar to Apple Calendar
+-----------------------------------------------
+
+- Open Calendar
+- Select **File > New Calendar Subscription**
+- Copy and paste the iCal feed url from Kanboard
+
+![Add iCal subscription](http://kanboard.net/screenshots/documentation/apple-calendar-add-subscription.png)
+
+- You can choose to synchronize the calendar with iCloud to be available across all your devices
+- Don't forget to select the refresh frequency
+
+![Edit iCal subscription](http://kanboard.net/screenshots/documentation/apple-calendar-edit-subscription.png)
+
+Adding your Kanboard calendar to Microsoft Outlook
+--------------------------------------------------
+
+![Outlook Add Internet Calendar](http://kanboard.net/screenshots/documentation/outlook-add-subscription.png)
+
+- Open Outlook
+- Select **Open Calendar > From Internet**
+- Copy and paste the iCal feed url from Kanboard
+
+![Outlook Edit Internet Calendar](http://kanboard.net/screenshots/documentation/outlook-edit-subscription.png)
+
+Adding your Kanboard calendar to Mozilla Thunderbird
+----------------------------------------------------
+
+- Install the Add-on **Lightning** to add the calendar support to Thunderbird
+- Click on **File > New Calendar**
+- In the dialog box, choose **On the Network**
+
+![Thunderbird Step 1](http://kanboard.net/screenshots/documentation/thunderbird-new-calendar-step1.png)
+
+- Choose the format iCalendar
+- Copy and paste the iCal feed url from Kanboard
+
+![Thunderbird Step 2](http://kanboard.net/screenshots/documentation/thunderbird-new-calendar-step2.png)
+
+- Choose the colors and other settings and finally save
+
+Adding your Kanboard calendar to Google Calendar
+------------------------------------------------
+
+- Click the down-arrow next to **Other calendars**.
+- Select **Add by URL** from the menu.
+- Copy and paste the iCal feed url from Kanboard
+
+![Google Calendar](http://kanboard.net/screenshots/documentation/google-calendar-add-subscription.png)
+
+Your Kanboard calendar can also be available from your Android device if you enable the synchronization.
+
+Note: According to the Google Support, external calendars are not refreshed very often, [read the documentation](https://support.google.com/calendar/answer/37100?hl=en&ref_topic=1672445).
diff --git a/sources/doc/index.markdown b/sources/doc/index.markdown
new file mode 100644
index 0000000..0c33bfa
--- /dev/null
+++ b/sources/doc/index.markdown
@@ -0,0 +1,138 @@
+Documentation
+=============
+
+Using Kanboard
+--------------
+
+### Introduction
+
+- [What is Kanban?](what-is-kanban.markdown)
+- [Kanban vs Todo Lists and Scrum](kanban-vs-todo-and-scrum.markdown)
+- [Usage examples](usage-examples.markdown)
+
+### Using the board
+
+- [Board, Calendar, List and Gantt views](project-views.markdown)
+- [Collapsed and expanded mode](board-collapsed-expanded.markdown)
+- [Horizontal scrolling and compact mode](board-horizontal-scrolling-and-compact-view.markdown)
+- [Show and hide columns](board-show-hide-columns.markdown)
+
+### Working with projects
+
+- [Creating projects](creating-projects.markdown)
+- [Editing projects](editing-projects.markdown)
+- [Sharing boards and tasks](sharing-projects.markdown)
+- [Automatic actions](automatic-actions.markdown)
+- [Project permissions](project-permissions.markdown)
+- [Swimlanes](swimlanes.markdown)
+- [Calendar](calendar.markdown)
+- [Budget](budget.markdown)
+- [Analytics](analytics.markdown)
+- [Gantt chart for tasks](gantt-chart-tasks.markdown)
+- [Gantt chart for projects](gantt-chart-projects.markdown)
+
+### Working with tasks
+
+- [Creating tasks](creating-tasks.markdown)
+- [Closing tasks](closing-tasks.markdown)
+- [Duplicate and move tasks](duplicate-move-tasks.markdown)
+- [Adding screenshots](screenshots.markdown)
+- [Task links](task-links.markdown)
+- [Transitions](transitions.markdown)
+- [Time tracking](time-tracking.markdown)
+- [Recurring tasks](recurring-tasks.markdown)
+- [Create tasks by email](create-tasks-by-email.markdown)
+- [Subtasks](subtasks.markdown)
+- [Analytics for tasks](analytics-tasks.markdown)
+
+### Working with users
+
+- [User management](user-management.markdown)
+- [Notifications](notifications.markdown)
+- [Hourly rate](hourly-rate.markdown)
+- [Timetable](timetable.markdown)
+- [Two factor authentication](2fa.markdown)
+
+### Settings
+
+- [Keyboard shortcuts](keyboard-shortcuts.markdown)
+- [Application settings](application-configuration.markdown)
+- [Project settings](project-configuration.markdown)
+- [Board settings](board-configuration.markdown)
+- [Calendar settings](calendar-configuration.markdown)
+- [Link settings](link-labels.markdown)
+- [Currency rate](currency-rate.markdown)
+
+### Integrations
+
+- [Bitbucket webhooks](bitbucket-webhooks.markdown)
+- [Github webhooks](github-webhooks.markdown)
+- [Gitlab webhooks](gitlab-webhooks.markdown)
+- [Hipchat](hipchat.markdown)
+- [Jabber](jabber.markdown)
+- [Mailgun](mailgun.markdown)
+- [Sendgrid](sendgrid.markdown)
+- [Slack](slack.markdown)
+- [Postmark](postmark.markdown)
+- [iCalendar subscriptions](ical.markdown)
+- [RSS/Atom subscriptions](rss.markdown)
+- [Json-RPC API](api-json-rpc.markdown)
+- [Webhooks](webhooks.markdown)
+
+### More
+
+- [Advanced Search Syntax](search.markdown)
+- [Command line interface](cli.markdown)
+- [Syntax guide](syntax-guide.markdown)
+- [Bruteforce protection](bruteforce-protection.markdown)
+- [Frequently asked questions](faq.markdown)
+
+Technical details
+-----------------
+
+### Installation
+
+- [Recommended configuration](recommended-configuration.markdown)
+- [Installation instructions](installation.markdown)
+- [Upgrade Kanboard to a new version](update.markdown)
+- [Installation on Ubuntu](ubuntu-installation.markdown)
+- [Installation on Debian](debian-installation.markdown)
+- [Installation on Centos](centos-installation.markdown)
+- [Installation on FreeBSD](freebsd-installation.markdown)
+- [Installation on Windows Server with IIS](windows-iis-installation.markdown)
+- [Installation on Windows Server with Apache](windows-apache-installation.markdown)
+- [Installation on Heroku](heroku.markdown)
+- [Example with Nginx + HTTPS + SPDY + PHP-FPM](nginx-ssl-php-fpm.markdown)
+- [Run Kanboard with Docker](docker.markdown)
+
+### Configuration
+
+- [Config file](config.markdown)
+- [Email configuration](email-configuration.markdown)
+- [URL rewriting](nice-urls.markdown)
+
+### Database
+
+- [Sqlite database management](sqlite-database.markdown)
+- [How to use Mysql](mysql-configuration.markdown)
+- [How to use Postgresql](postgresql-configuration.markdown)
+
+### Authentication
+
+- [LDAP authentication](ldap-authentication.markdown)
+- [Google authentication](google-authentication.markdown)
+- [Github authentication](github-authentication.markdown)
+- [Gitlab authentication](gitlab-authentication.markdown)
+- [Reverse proxy authentication](reverse-proxy-authentication.markdown)
+
+### Contributors
+
+- [Contributor guide](contributing.markdown)
+- [Translations](translations.markdown)
+- [Coding standards](coding-standards.markdown)
+- [Running tests](tests.markdown)
+- [Build assets](assets.markdown)
+- [Run Kanboard with Vagrant](vagrant.markdown)
+
+The documentation is written in [Markdown](http://en.wikipedia.org/wiki/Markdown).
+If you want to improve the documentation, just send a pull-request.
diff --git a/sources/doc/installation.markdown b/sources/doc/installation.markdown
new file mode 100644
index 0000000..53e7095
--- /dev/null
+++ b/sources/doc/installation.markdown
@@ -0,0 +1,40 @@
+Installation
+============
+
+Requirements
+------------
+
+- Apache or Nginx
+- PHP >= 5.3.3 (Kanboard is compatible with PHP 5.3, 5.4, 5.5, 5.6 and 7.0)
+- PHP extensions required: mbstring, gd and pdo_sqlite
+- A modern web browser
+
+From the archive (stable version)
+---------------------------------
+
+1. You must have a web server with PHP installed
+2. Download the source code and copy the directory `kanboard` where you want
+3. Check if the directory `data` is writeable
+4. With your browser go to
+5. The default login and password is **admin/admin**
+6. Start to use the software
+7. Don't forget to change your password!
+
+Note: The folder data is the location where Kanboard stores uploaded files as well as the Sqlite database.
+
+From the repository (development version)
+-----------------------------------------
+
+You must install [composer](https://getcomposer.org/) to use this method.
+
+1. `git clone https://github.com/fguillot/kanboard.git`
+2. `composer install`
+3. Go to the third step just above
+
+Note: This method will install the **current development version**, use at your own risk.
+
+Security
+--------
+
+- Don't forget to change the default user/password
+- Don't allow everybody to access to the directory `data` from the URL. There is already a `.htaccess` for Apache but nothing for Nginx.
diff --git a/sources/doc/jabber.markdown b/sources/doc/jabber.markdown
new file mode 100644
index 0000000..fe36516
--- /dev/null
+++ b/sources/doc/jabber.markdown
@@ -0,0 +1,34 @@
+Jabber/XMPP integration
+=======================
+
+You can send notifications to a Jabber room for all projects or only for specific projects.
+
+- To send notifications for all projects, go to **Settings > Integrations > Jabber**
+- To send notifications for only some projects, go to **Project settings > Integrations > Jabber**
+
+Each project can send notifications to a separate room.
+
+## Example of notification
+
+Here an example with the Jabber client Adium:
+
+![Jabber notification](http://kanboard.net/screenshots/documentation/jabber-notification.png)
+
+## Configuration
+
+![Jabber settings](http://kanboard.net/screenshots/documentation/jabber-settings.png)
+
+1. Go to **Settings > Integrations > Jabber** or **Project settings > Integrations > Jabber**
+2. **XMPP server address**: URL of the XMPP server, example: **tcp://172.28.128.3:5222**
+3. **Jabber domain**: The **"to"** attribute of the XMPP protocol, example: **example.com**
+4. **Username**: The Jabber username used by Kanboard, example: **kanboard**
+5. **Password**: The Jabber password
+6. **Jabber nickname**: The nickname used to connect to the room
+7. **Multi-user chat room**: The address of the room, example: **demo@conference.example.com**
+
+Now, Kanboard events will be sent to the Jabber conference room.
+
+## Troubleshooting
+
+- Enable the debug mode
+- All connection errors with the XMPP server are recorded in the log files `data/debug.log` or syslog
diff --git a/sources/doc/kanban-vs-todo-and-scrum.markdown b/sources/doc/kanban-vs-todo-and-scrum.markdown
new file mode 100644
index 0000000..3d53023
--- /dev/null
+++ b/sources/doc/kanban-vs-todo-and-scrum.markdown
@@ -0,0 +1,37 @@
+Kanban vs Todo lists and Scrum
+==============================
+
+Kanban vs Todo lists
+--------------------
+
+### Todo lists:
+
+- Single phase (just a list of items)
+- Multitasking possible (not efficient)
+
+### Kanban:
+
+- Multiple phases, each column represent a step
+- Bring focus and avoid multitasking because you can set a work in progress limit per column
+
+Kanban vs Scrum
+---------------
+
+### Scrum:
+
+- Sprints are time-boxed, usually 2 or 4 weeks
+- Do not allow changes during the iteration
+- Estimation is required
+- Uses velocity as default metric
+- Scrum board is cleared between each sprint
+- Scrum has predefined roles like scrum master, product owner and the team
+- A lot of meetings: planning, backlog grooming, daily stand-up, retrospective
+
+### Kanban:
+
+- Continuous flow
+- Changes can be made at anytime
+- Estimation is optional
+- Use lead and cycle time to measure performance
+- Kanban board is persistent
+- Kanban doesn't impose strict constraints or meetings, process is more flexible
diff --git a/sources/doc/keyboard-shortcuts.markdown b/sources/doc/keyboard-shortcuts.markdown
new file mode 100644
index 0000000..065d20c
--- /dev/null
+++ b/sources/doc/keyboard-shortcuts.markdown
@@ -0,0 +1,28 @@
+Keyboard shortcuts
+==================
+
+Keyboard shortcuts availability depends of the page you are presently.
+
+Project views (Board, Calendar, List, Gantt)
+--------------------------------------------
+
+- Switch to the board view = **v b** (press on **v** then **b**)
+- Switch to the calendar view = **v c**
+- Switch to the list view = **v l**
+- Switch to the Gantt view = **v g**
+
+Board view
+----------
+
+- New task = **n**
+- Expand/collapse tasks = **s**
+- Compact/wide view = **c**
+
+Application
+-----------
+
+- Open board switcher = **b**
+- Go to the search box = **f**
+- Reset the search box = **r**
+- Close dialog box = **ESC**
+- Submit a form = **CTRL+ENTER** or **⌘+ENTER**
\ No newline at end of file
diff --git a/sources/doc/ldap-authentication.markdown b/sources/doc/ldap-authentication.markdown
new file mode 100644
index 0000000..53b3d01
--- /dev/null
+++ b/sources/doc/ldap-authentication.markdown
@@ -0,0 +1,234 @@
+LDAP authentication
+===================
+
+Requirements
+------------
+
+- LDAP extension for PHP
+- LDAP server:
+ - OpenLDAP
+ - Microsoft Active Directory
+ - Novell eDirectory
+
+Workflow
+--------
+
+When the LDAP authentication is activated, the login process work like that:
+
+1. Try first to authenticate the user by using the database
+2. If the user is not found inside the database, a LDAP authentication is performed
+3. If the LDAP authentication is successful, by default a local user is created automatically with no password and marked as LDAP user.
+
+### Differences between a local user and a LDAP user are the following:
+
+- LDAP users have no local passwords
+- LDAP users can't modify their password with the user interface
+- By default, all LDAP users have no admin privileges
+- To become administrator, a LDAP user must be promoted by another administrator
+
+The full name and the email address are automatically fetched from the LDAP server.
+
+Configuration
+-------------
+
+You have to create a custom config file named `config.php` (you can also use the template `config.default.php`).
+This file must be stored in the root directory of Kanboard.
+
+### LDAP bind type
+
+There is 3 possible ways to browse the LDAP directory:
+
+#### Anonymous browsing
+
+```php
+define('LDAP_BIND_TYPE', 'anonymous');
+define('LDAP_USERNAME', null);
+define('LDAP_PASSWORD', null);
+```
+
+This is the default value but some LDAP servers don't allow that.
+
+#### Proxy user
+
+A specific user is used to browse the LDAP directory.
+By example, Novell eDirectory use that method.
+
+```php
+define('LDAP_BIND_TYPE', 'proxy');
+define('LDAP_USERNAME', 'my proxy user');
+define('LDAP_PASSWORD', 'my proxy password');
+```
+
+#### User credentials
+
+This method use the credentials provided by the end-user.
+By example, Microsoft Active Directory doesn't allow anonymous browsing by default and if you don't want to use a proxy user you can use this method.
+
+```php
+define('LDAP_BIND_TYPE', 'user');
+define('LDAP_USERNAME', '%s@mydomain.local');
+define('LDAP_PASSWORD', null);
+```
+
+Here, the `LDAP_USERNAME` is use to define a replacement pattern:
+
+```php
+define('LDAP_USERNAME', '%s@mydomain.local');
+
+// Another way to do the same:
+
+define('LDAP_USERNAME', 'MYDOMAIN\\%s');
+```
+
+### Example for Microsoft Active Directory
+
+Let's say we have a domain `KANBOARD` (kanboard.local) and the primary controller is `myserver.kanboard.local`.
+Microsoft Active Directory doesn't allow anonymous binding by default.
+
+First example with a proxy user:
+
+```php
+ Link settings**)
+
+![Link Labels](http://kanboard.net/screenshots/documentation/link-labels.png)
+
+Each label may have an opposite label defined.
+If there is no opposite, the label is considered bi-directionnal.
+
+![Link Label Creation](http://kanboard.net/screenshots/documentation/link-label-creation.png)
diff --git a/sources/doc/mailgun.markdown b/sources/doc/mailgun.markdown
new file mode 100644
index 0000000..6465903
--- /dev/null
+++ b/sources/doc/mailgun.markdown
@@ -0,0 +1,28 @@
+Mailgun
+=======
+
+You can use the service [Mailgun](http://www.mailgun.com/) to create tasks directly by email.
+
+This integration works with the inbound email service of Mailgun (routes).
+Kanboard use a webhook to handle incoming emails.
+
+The [incoming email workflow is described here](create-tasks-by-email.markdown).
+
+Mailgun configuration
+---------------------
+
+Create a new route in the web interface or via the API ([official documentation](https://documentation.mailgun.com/user_manual.html#routes)), here an example:
+
+```
+match_recipient("^kanboard\+(.*)@mydomain.tld$")
+forward("https://mykanboard/?controller=webhook&action=mailgun&token=mytoken")
+```
+
+The Kanboard webhook url is displayed in **Settings > Integrations > Mailgun**
+
+Kanboard configuration
+----------------------
+
+1. Be sure that your users have an email address in their profiles
+2. Assign a project identifier to the desired projects: **Project settings > Edit**
+3. Try to send an email to your project
diff --git a/sources/doc/mysql-configuration.markdown b/sources/doc/mysql-configuration.markdown
new file mode 100644
index 0000000..eef1e12
--- /dev/null
+++ b/sources/doc/mysql-configuration.markdown
@@ -0,0 +1,44 @@
+How to use Mysql or MariaDB instead of Sqlite
+=============================================
+
+By default Kanboard use Sqlite to stores its data.
+However it's possible to use Mysql or MariaDB instead of Sqlite.
+
+Requirements
+------------
+
+- Mysql server
+- The PHP extension `pdo_mysql` installed (Debian/Ubuntu: `apt-get install php5-mysql`)
+
+Note: Kanboard is tested with **Mysql >= 5.5 and MariaDB >= 10.0**
+
+Mysql configuration
+-------------------
+
+### Create a database
+
+The first step is to create a database on your Mysql server.
+By example, you can do that with the command line mysql client:
+
+```sql
+CREATE DATABASE kanboard;
+```
+
+### Create a config file
+
+The file `config.php` should contains those values:
+
+```php
+ kanboard.pem
+```
+
+Copy the certificates in a new directory:
+
+```bash
+mkdir /etc/nginx/ssl
+cp kanboard.pem /etc/nginx/ssl
+cp kanboard.key.nopass /etc/nginx/ssl
+chmod 400 /etc/nginx/ssl/*
+```
+
+Configure Nginx
+---------------
+
+Now, we can customize our installation, start to modify the main configuration file `/etc/nginx/nginx.conf`:
+
+```nginx
+user www-data;
+worker_processes auto;
+pid /run/nginx.pid;
+
+events {
+ worker_connections 1024;
+}
+
+http {
+ sendfile on;
+ tcp_nopush on;
+ tcp_nodelay on;
+ keepalive_timeout 65;
+ types_hash_max_size 2048;
+ server_tokens off;
+
+ # SSL shared cache between workers
+ ssl_session_cache shared:SSL:10m;
+ ssl_session_timeout 10m;
+
+ # We disable weak protocols and ciphers
+ ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
+ ssl_prefer_server_ciphers on;
+ ssl_ciphers HIGH:!SSLv2:!MEDIUM:!LOW:!EXP:!RC4:!DSS:!aNULL:@STRENGTH;
+
+ include /etc/nginx/mime.types;
+ default_type application/octet-stream;
+
+ access_log /var/log/nginx/access.log;
+ error_log /var/log/nginx/error.log;
+
+ # We enable the Gzip compression for some mime types
+ gzip on;
+ gzip_disable "msie6";
+ gzip_vary on;
+ gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript;
+
+ include /etc/nginx/conf.d/*.conf;
+ include /etc/nginx/sites-enabled/*;
+}
+```
+
+Create a new virtual host for Kanboard `/etc/nginx/sites-available/kanboard`
+
+
+```nginx
+server {
+ # We also enable the SPDY protocol
+ listen 443 ssl spdy;
+
+ # Our SSL certificate
+ ssl on;
+ ssl_certificate /etc/nginx/ssl/kanboard.pem;
+ ssl_certificate_key /etc/nginx/ssl/kanboard.key.nopass;
+
+ # You can change the default root directory here
+ root /usr/share/nginx/html;
+
+ index index.php;
+
+ # Your domain name
+ server_name localhost;
+
+ # The maximum body size, useful for file uploads
+ client_max_body_size 10M;
+
+ location / {
+ try_files $uri $uri/ =404;
+ }
+
+ error_page 404 /404.html;
+ error_page 500 502 503 504 /50x.html;
+ location = /50x.html {
+ root /usr/share/nginx/html;
+ }
+
+ # PHP-FPM configuration
+ location ~ \.php$ {
+ try_files $uri =404;
+ fastcgi_split_path_info ^(.+\.php)(/.+)$;
+ fastcgi_pass unix:/var/run/php5-fpm.sock;
+ fastcgi_index index.php;
+ include fastcgi.conf;
+ }
+
+ # Deny access to the directory data
+ location ~* /data {
+ deny all;
+ return 404;
+ }
+
+ # Deny access to .htaccess
+ location ~ /\.ht {
+ deny all;
+ return 404;
+ }
+}
+```
+
+Now it's time to test our setup
+
+```bash
+# Disable the default virtual host
+sudo unlink /etc/nginx/sites-enabled/default
+
+# Add our default virtual host
+sudo ln -s /etc/nginx/sites-available/kanboard /etc/nginx/sites-enabled/kanboard
+
+# Check the config file
+sudo nginx -t
+nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
+nginx: configuration file /etc/nginx/nginx.conf test is successful
+
+# Restart nginx
+sudo service nginx restart
+```
+
+Kanboard Installation
+---------------------
+
+You can install Kanboard in a subdirectory or not, it's up to you.
+
+```bash
+cd /usr/share/nginx/html
+sudo wget http://kanboard.net/kanboard-latest.zip
+sudo unzip kanboard-latest.zip
+sudo chown -R www-data:www-data kanboard/data
+sudo rm kanboard-latest.zip
+```
+
+Now, you should be able to use Kanboard with your web browser.
diff --git a/sources/doc/nice-urls.markdown b/sources/doc/nice-urls.markdown
new file mode 100644
index 0000000..38f7c41
--- /dev/null
+++ b/sources/doc/nice-urls.markdown
@@ -0,0 +1,36 @@
+URL rewriting
+=============
+
+Kanboard is able to work indifferently with url rewriting enabled or not.
+
+- Example of URL rewritten: `/board/123`
+- Otherwise: `?controller=board&action=show&project_id=123`
+
+If you use Kanboard with Apache and with the mode rewrite enabled, nice urls will be used automatically.
+
+URL Shortcuts
+-------------
+
+- Go to the task #123: **/t/123**
+- Go to the board of the project #2: **/b/2**
+- Go to the project calendar #5: **/c/5**
+- Go to the list view of the project #8: **/l/8**
+- Go to the project settings for the project id #42: **/p/42**
+
+Configuration
+-------------
+
+By default, Kanboard will check if the Apache mode rewrite is enabled.
+
+To avoid the automatic detection of url rewriting from the web server, you can enable this feature in your config file:
+
+```
+define('ENABLE_URL_REWRITE', true);
+```
+
+When this constant is at `true`:
+
+- URLs generated from command line tools will be also converted
+- If you use another web server than Apache, by example Nginx or Microsoft IIS, you have to configure yourself the url rewriting
+
+Note: Kanboard always fallback to old school urls when it's not configured, this configuration is optional.
diff --git a/sources/doc/notifications.markdown b/sources/doc/notifications.markdown
new file mode 100644
index 0000000..39512e7
--- /dev/null
+++ b/sources/doc/notifications.markdown
@@ -0,0 +1,30 @@
+Notifications
+=============
+
+Kanboard is able to send notifications through several channels:
+
+- Email
+- Jabber/XMPP
+- Hipchat
+- Slack
+
+Actually, Jabber/Hipchat/Slack notifications are sent to a room or group channel because they are configured at the project level.
+However, email notifications are sent to an individual person.
+
+User notifications
+------------------
+
+Each user must enable the notifications in their profile: **User Profile > Email notifications**. It's disabled by default.
+
+You need of course a valid email address in you profile and the application must be configured to send emails.
+
+![Notifications](http://kanboard.net/screenshots/documentation/notifications.png)
+
+For each project your are member, you can choose to receive notifications for:
+
+- All tasks
+- Only for tasks assigned to you
+- Only for tasks created by you
+- Only for tasks created by you and assigned to you
+
+You can also select only some projects, by default it's all projects where you are member.
diff --git a/sources/doc/postgresql-configuration.markdown b/sources/doc/postgresql-configuration.markdown
new file mode 100644
index 0000000..3c07ff1
--- /dev/null
+++ b/sources/doc/postgresql-configuration.markdown
@@ -0,0 +1,40 @@
+Postgresql configuration
+========================
+
+By default, Kanboard use Sqlite to store its data but it's also possible to use Postgresql.
+
+Requirements
+------------
+
+- A Postgresql server already installed and configured
+- The PHP extension `pdo_pgsql` installed (Debian/Ubuntu: `apt-get install php5-pgsql`)
+
+Note: Kanboard is tested with **Postgresql 9.3 and 9.4**
+
+Configuration
+-------------
+
+### Create an empty database with the command `pgsql`:
+
+```sql
+CREATE DATABASE kanboard;
+```
+
+### Create a config file
+
+The file `config.php` should contains those values:
+
+```php
+ Integrations > Postmark**
+
+Kanboard configuration
+----------------------
+
+1. Be sure that your users have an email address in their profiles
+2. Assign a project identifier to the desired projects: **Project settings > Edit**
+3. Try to send an email to your project
+
+Troubleshootings
+----------------
+
+- Test the webhook url from the Postmark console, you should have a status code `200 OK`
diff --git a/sources/doc/project-configuration.markdown b/sources/doc/project-configuration.markdown
new file mode 100644
index 0000000..252ace6
--- /dev/null
+++ b/sources/doc/project-configuration.markdown
@@ -0,0 +1,42 @@
+Project settings
+================
+
+Go to the menu **Settings**, then choose **Project settings** on the left.
+
+![Project settings](http://kanboard.net/screenshots/documentation/project-settings.png)
+
+### Default columns for new projects
+
+You can change the default column names here.
+It's useful if you always create projects with the same columns.
+
+Each column name must be separated by a comma.
+
+By default, Kanboard use those column names: Backlog, Ready, Work in progress and Done.
+
+### Default categories for new projects
+
+Categories are not global to the application but attached to a project.
+Each project can have different categories.
+
+However, if you always create the same categories for all your projects, you can define here the list of categories to create automatically.
+
+### Allow only one subtask in progress at the same time for a user
+
+When this option is enabled, a user can work with only one subtask at the time.
+
+If another subtask have the status "in progress", the user will see this dialog box:
+
+![Subtask user restriction](http://kanboard.net/screenshots/documentation/subtask-user-restriction.png)
+
+### Trigger automatically subtask time tracking
+
+- If enabled, when a subtask status is changed to "in progress", the timer will start automatically.
+- Disable this option if you don't use time tracking.
+
+### Include closed tasks in the cumulative flow diagram
+
+- If enabled, closed tasks will be included in the cumulative flow diagram.
+- If disabled, only open tasks will be included.
+- This option affect the column "total" of the table "project_daily_column_stats"
+
diff --git a/sources/doc/project-permissions.markdown b/sources/doc/project-permissions.markdown
new file mode 100644
index 0000000..762ab30
--- /dev/null
+++ b/sources/doc/project-permissions.markdown
@@ -0,0 +1,49 @@
+Project permissions
+===================
+
+A project can have two kinds of people: **project managers** and **project members**.
+
+- Project managers can manage the configuration of the project and access to the reports.
+- Project members can only do basic operations (create or move tasks).
+
+When you create a new project, you are automatically assigned as a project manager.
+
+Kanboard administrators can access to everything but they are not necessary project members or managers. **Those permissions are defined at the project level**.
+
+Permissions for each role
+-------------------------
+
+### Project members
+
+- Use the board (create, move and edit tasks)
+- Remove only tasks created by themselves
+
+### Project managers
+
+- Use the board
+- Configure the project
+ - Share, rename, duplicate and disable the project
+ - Manage swimlanes, categories, columns and users
+ - Edit automatic actions
+- CSV Exports
+- Remove tasks of any project members
+- Access to the analytics section
+
+They **cannot remove the project**.
+
+Manage users and permissions
+----------------------------
+
+To define project roles, go to the **project configuration page** then click on **User management**.
+
+### User management
+
+![Project permissions](http://kanboard.net/screenshots/documentation/project-permissions.png)
+
+From there, you can choose to add new members, change the role or revoke user access.
+
+### Allow everybody
+
+If you choose to allow everybody (all Kanboard users), the project is considered public.
+
+That means there is no role management anymore. Permissions per user cannot be applied.
diff --git a/sources/doc/project-views.markdown b/sources/doc/project-views.markdown
new file mode 100644
index 0000000..e1fb4c1
--- /dev/null
+++ b/sources/doc/project-views.markdown
@@ -0,0 +1,46 @@
+Board, Calendar and List views
+==============================
+
+For each project, tasks can be visualized with several views: **Board, Calendar, List and Gantt**. Each view show the result of the filter box at the top. The search engine use the [advanced syntax](search.markdown).
+
+Board view
+----------
+
+![Board view](http://kanboard.net/screenshots/documentation/board-view.png)
+
+- With this view you can drag and drop tasks between columns easily.
+- You can also use the keyboard shortcut **"v b"** to switch to the board view.
+- Tasks with a shadow are recently modified.
+
+![Board Task Limit](http://kanboard.net/screenshots/documentation/board-task-limit.png)
+
+When the task limit is reached for a column, the background becomes red. That means there are too many tasks in progress at the same time.
+
+[Learn more about board configuration](board-configuration.markdown)
+
+Calendar view
+--------------
+
+![Calendar view](http://kanboard.net/screenshots/documentation/calendar-view.png)
+
+- With this view you can visualize tasks with a due date.
+- Depending of the settings, you can also see tasks in progress.
+- You can also use the keyboard shortcut **"v c"** to switch to the calendar view.
+- [Learn more about calendar configuration](calendar-configuration.markdown)
+
+List view
+----------
+
+![List view](http://kanboard.net/screenshots/documentation/list-view.png)
+
+- With this view all results of your search are displayed in a table.
+- You can also use the keyboard shortcut **"v l"** to switch to the list view.
+
+Gantt view
+----------
+
+![Gantt view](http://kanboard.net/screenshots/documentation/gantt-view.png)
+
+- The Gantt view display tasks on a horizontal timeline
+- The start date and the due date are used to display the chart
+- For a quick access, use the keyboard shortcut: **v g**
diff --git a/sources/doc/recommended-configuration.markdown b/sources/doc/recommended-configuration.markdown
new file mode 100644
index 0000000..35ed652
--- /dev/null
+++ b/sources/doc/recommended-configuration.markdown
@@ -0,0 +1,36 @@
+Recommended Configuration
+=========================
+
+Server side
+-----------
+
+- Modern Linux/Unix operating system: **Ubuntu/Debian or FreeBSD**
+- Most recent version of PHP and Apache (Kanboard is compatible with PHP 5.3, 5.4, 5.5, 5.6 and 7.0)
+- Use the Sqlite database only when you have a disk with fast I/O (SSD disks) otherwise use Mysql or Postgresql
+
+Client side
+-----------
+
+- Use a modern browser: **Mozilla Firefox or Google Chrome or Safari**
+
+Tested configurations
+---------------------
+
+The following configurations are tested with Kanboard but that doesn't mean all features are available:
+
+### Server
+
+- Ubuntu 14.04 LTS
+- Debian 6, 7 and 8
+- Centos 6.x and 7.0
+- Windows 2012 Server
+- Windows 2008 Server
+
+### Desktops
+
+- Last version of Mozilla Firefox, Safari and Google Chrome
+- Microsoft Internet Explorer 11
+
+### Tablets
+
+- iPad mini 3
diff --git a/sources/doc/recurring-tasks.markdown b/sources/doc/recurring-tasks.markdown
new file mode 100644
index 0000000..b1de8d0
--- /dev/null
+++ b/sources/doc/recurring-tasks.markdown
@@ -0,0 +1,24 @@
+Recurring tasks
+===============
+
+To fit with the Kanban methodology, the recurring tasks are not based on a date but on board events.
+
+- Recurring tasks are duplicated to the first column of the board when the selected events occurs
+- The due date can be recalculated automatically
+- Each task records the task id of the parent task that created it and the child task created
+
+Configuration
+-------------
+
+Go to the task view page or use the dropdown menu on the board, then select **Edit recurrence**.
+
+![Recurring task](http://kanboard.net/screenshots/documentation/recurring-tasks.png)
+
+There are 3 triggers that currently create a new recurring task:
+
+- Moving a task from the first column
+- Moving a task to the last column
+- Closing the task
+
+Due dates, if set on the current task, can be recalculated by a given factor of days, months or years.
+The base date for the calculation of the new due date can be either the existing due date, or the action date.
diff --git a/sources/doc/reverse-proxy-authentication.markdown b/sources/doc/reverse-proxy-authentication.markdown
new file mode 100644
index 0000000..7c001f3
--- /dev/null
+++ b/sources/doc/reverse-proxy-authentication.markdown
@@ -0,0 +1,64 @@
+Reverse Proxy Authentication
+============================
+
+This authentication method is often used for [SSO](http://en.wikipedia.org/wiki/Single_sign-on) (Single Sign-On) especially for large organizations.
+
+The authentication is done by another system, Kanboard doesn't know your password and suppose you are already authenticated.
+
+Requirements
+------------
+
+- A well configured reverse proxy
+
+or
+
+- Apache auth on the same server
+
+
+How does this work?
+-------------------
+
+1. Your reverse proxy authenticates the user and send the username through a HTTP header.
+2. Kanboard retreive the username from the request
+ - The user is created automatically if necessary
+ - Open a new Kanboard session without any prompt assuming it's valid
+
+Installation instructions
+-------------------------
+
+### Setting up your reverse proxy
+
+This is not in the scope of this documentation.
+You should check the user login is sent by the reverse proxy using a HTTP header, and find which one.
+
+### Setting up Kanboard
+
+Create a custom `config.php` file or copy the `config.default.php` file:
+
+```php
+ Public access**.
+
+![Disable public access](http://kanboard.net/screenshots/documentation/project-disable-sharing.png)
+
+Enable/disable user RSS feeds
+--------------------------------
+
+Go to **User profile > Public access**.
+
+The RSS link is protected by a random token, only people who knows the url can access to the feed.
\ No newline at end of file
diff --git a/sources/doc/screenshots.markdown b/sources/doc/screenshots.markdown
new file mode 100644
index 0000000..419de41
--- /dev/null
+++ b/sources/doc/screenshots.markdown
@@ -0,0 +1,25 @@
+Adding screenshots
+==================
+
+You can copy and paste images directly in Kanboard to save time.
+These images are uploaded as attachments to the task.
+
+This is especially useful for taking screenshots to describe an issue by example.
+
+You can add screenshots directly from the board by clicking on the dropdown menu or in the task view page.
+
+![Dropdown screenshot menu](http://kanboard.net/screenshots/documentation/dropdown-screenshot.png)
+
+To add a new image, take your screenshot and paste with CTRL+V or Command+V:
+
+![Screenshot page](http://kanboard.net/screenshots/documentation/task-screenshot.png)
+
+On Mac OS X, you can use those shortcuts to take screenshots:
+
+- Command-Control-Shift-3: Take a screenshot of the screen, and save it to the clipboard
+- Command-Control-Shift-4, then select an area: Take a screenshot of an area and save it to the clipboard
+- Command-Control-Shift-4, then space, then click a window: Take a screenshot of a window and save it to the clipboard
+
+There are also several third-party applications that can be used to take screenshots with annotations and shapes.
+
+**Note: This feature doesn't works with all browsers.** It doesn't work with Safari due to this bug: https://bugs.webkit.org/show_bug.cgi?id=49141
diff --git a/sources/doc/search.markdown b/sources/doc/search.markdown
new file mode 100644
index 0000000..34a20bc
--- /dev/null
+++ b/sources/doc/search.markdown
@@ -0,0 +1,138 @@
+Advanced Search Syntax
+======================
+
+Kanboard use a simple query language for advanced search.
+
+Example of query
+----------------
+
+This example will returns all tasks assigned to me with a due date for tomorrow and a title that contains "my title":
+
+```
+assigne:me due:tomorrow my title
+```
+
+Search by task id or title
+--------------------------
+
+- Search by task id: `#123`
+- Search by task id and task title: `123`
+- Search by task title: anything that don't match any search attributes
+
+Search by status
+----------------
+
+Attribute: **status**
+
+- Query to find open tasks: `status:open`
+- Query to find closed tasks: `status:closed`
+
+Search by assignee
+------------------
+
+Attribute: **assignee**
+
+- Query with the full name: `assignee:"Frederic Guillot"`
+- Query with the username: `assignee:fguillot`
+- Multiple assignee lookup: `assignee:user1 assignee:"John Doe"`
+- Query for unassigned tasks: `assignee:nobody`
+- Query for my assigned tasks: `assignee:me`
+
+Note: Kanboard will also search in assigned subtasks with the status todo and in progress.
+
+Search by color
+---------------
+
+Attribute: **color**
+
+- Query to search by color id: `color:blue`
+- Query to search by color name: `color:"Deep Orange"`
+
+Search by due date
+------------------
+
+Attribute: **due**
+
+- Search tasks due today: `due:today`
+- Search tasks due tomorrow: `due:tomorrow`
+- Search tasks due yesterday: `due:yesterday`
+- Search tasks due with the exact date: `due:2015-06-29`
+
+The date must use the ISO8601 format: **YYYY-MM-DD**.
+
+All string formats supported by the `strtotime()` function are supported, by example `next Thursday`, `-2 days`, `+2 months`, `tomorrow`, etc...
+
+Operators supported with a date:
+
+- Greater than: **due:>2015-06-29**
+- Lower than: **due:<2015-06-29**
+- Greater than or equal: **due:>=2015-06-29**
+- Lower than or equal: **due:<=2015-06-29**
+
+Search by modification date
+---------------------------
+
+Attribute: **modified** or **updated**
+
+The date formats are the same as the due date.
+
+There is also a filter by recently modified tasks: `modified:recently`.
+
+This query will use the same value as the board highlight period configured in settings.
+
+Search by creation date
+-----------------------
+
+Attribute: **created**
+
+Works in the same way as the modification date queries.
+
+Search by description
+---------------------
+
+Attribute: **description**
+
+Example: `description:"text search"`
+
+Search by external reference
+----------------------------
+
+The task reference is an external id of your task, by example a ticket number from another software.
+
+- Find tasks with a reference: `ref:1234` or `reference:TICKET-1234`
+
+Search by category
+------------------
+
+Attribute: **category**
+
+- Find tasks with a specific category: `category:"Feature Request"`
+- Find all tasks that have those categories: `category:"Bug" category:"Improvements"`
+- Find tasks with no category assigned: `category:none`
+
+Search by project
+-----------------
+
+Attribute: **project**
+
+- Find tasks by project name: `project:"My project name"`
+- Find tasks by project id: `project:23`
+- Find tasks for several projects: `project:"My project A" project:"My project B"`
+
+Search by column
+----------------
+
+Attribute: **column**
+
+- Find tasks by column name: `column:"Work in progress"`
+- Find tasks for several columns: `column:"Backlog" column:ready`
+
+Search by swimlane
+------------------
+
+Attribute: **swimlane**
+
+- Find tasks by swimlane: `swimlane:"Version 42"`
+- Find tasks in the default swimlane: `swimlane:default`
+- Find tasks into several swimlanes: `swimlane:"Version 1.2" swimlane:"Version 1.3"`
+
diff --git a/sources/doc/sendgrid.markdown b/sources/doc/sendgrid.markdown
new file mode 100644
index 0000000..0f307da
--- /dev/null
+++ b/sources/doc/sendgrid.markdown
@@ -0,0 +1,24 @@
+Sendgrid
+========
+
+You can use the service [Sendgrid](https://sendgrid.com/) to create tasks directly by email.
+
+This integration works with the [Parse API of Sendgrid](https://sendgrid.com/docs/API_Reference/Webhooks/parse.html).
+Kanboard use a webhook to handle incoming emails.
+
+The [incoming email workflow is described here](create-tasks-by-email.markdown).
+
+Sendgrid configuration
+----------------------
+
+1. Create a new domain or subdomain (by example **inbound.mydomain.tld**) with a MX record that point to **mx.sendgrid.net**
+2. Add your domain and the Kanboard webhook url to [the configuration page in Sendgrid](https://sendgrid.com/developer/reply)
+
+The Kanboard webhook url is displayed in **Settings > Integrations > Sendgrid**
+
+Kanboard configuration
+----------------------
+
+1. Be sure that your users have an email address in their profiles
+2. Assign a project identifier to the desired projects: **Project settings > Edit**
+3. Try to send an email to your project
diff --git a/sources/doc/sharing-projects.markdown b/sources/doc/sharing-projects.markdown
new file mode 100644
index 0000000..17a552c
--- /dev/null
+++ b/sources/doc/sharing-projects.markdown
@@ -0,0 +1,35 @@
+Sharing boards and tasks
+========================
+
+By default, boards are private but it's possible to make a board public.
+
+A public board **cannot be modified, it's a read-only access**.
+This access is protected by a random token, only people who have the right url can see the board.
+
+Public boards are automatically refreshed every 60 seconds.
+Task details are also available in read-only.
+
+Usage examples:
+
+- Share your board with someone outside of your organization
+- Display the board on a large screen in your office
+
+Enable public access
+-------------------
+
+Select your project, then click on "Public access" and finally click on the button "Enable public access".
+
+![Enable public access](http://kanboard.net/screenshots/documentation/project-enable-sharing.png)
+
+When the public access is enabled, a couple of links are generated:
+
+- Public board view
+- RSS feed subscription link
+- iCalendar subscription link
+
+![Disable public access](http://kanboard.net/screenshots/documentation/project-disable-sharing.png)
+
+You can also disable the public access whenever you want.
+
+Each time, you enable or disable the public access a new random token is generated.
+**The previous links will not work anymore**.
diff --git a/sources/doc/slack.markdown b/sources/doc/slack.markdown
new file mode 100644
index 0000000..f90464e
--- /dev/null
+++ b/sources/doc/slack.markdown
@@ -0,0 +1,38 @@
+Slack integration
+=================
+
+You can send notifications to Slack for all projects or only for specific projects.
+
+- To send notifications for all projects, go to **Settings > Integrations > Slack**
+- To send notifications for only some projects, go to **Project settings > Integrations > Slack**
+
+Each project can send notifications to a separate channel.
+
+Send notifications to a channel
+-------------------------------
+
+Example of notifications:
+
+![Slack notification](http://kanboard.net/screenshots/documentation/slack-notification.png)
+
+This feature use the [Incoming webhook](https://api.slack.com/incoming-webhooks) system of Slack.
+
+### Slack configuration
+
+![Slack webhook creation](http://kanboard.net/screenshots/documentation/slack-add-incoming-webhook.png)
+
+1. Click on the Team dropdown and choose **Configure Integrations**
+2. On the list of services, scroll-down and choose **DIY Integrations & Customizations > Incoming WebHooks**
+3. Copy the webhook url to the Kanboard settings page: **Settings > Integrations > Slack** or **Project settings > Integrations > Slack**
+
+Now, Kanboard events will be sent to the Slack channel.
+
+### Overriding Channel (Optional)
+
+Optionally you can override the channel, private group or send direct messages by filling up **Channel/Group/User** text box. Leaving it empty will post to the channel configured during webhook configuration.
+
+Examples:
+
+- Send messages to another channel: **#mychannel1**
+- Send messages to a private group: **#myprivategroup1**
+- Send messages directly to someone: **@anotheruser1**
diff --git a/sources/doc/sqlite-database.markdown b/sources/doc/sqlite-database.markdown
new file mode 100644
index 0000000..0a6a0ab
--- /dev/null
+++ b/sources/doc/sqlite-database.markdown
@@ -0,0 +1,50 @@
+Sqlite database management
+==========================
+
+Kanboard uses Sqlite by default to store its data.
+All tasks, projects and users are stored inside this database.
+
+Technically, the database is just a single file located inside the directory `data` and named `db.sqlite`.
+
+Export/Backup
+-------------
+
+### Command line
+
+Doing a backup is very easy, just copy the file `data/db.sqlite` somewhere else when nobody use the software.
+
+### User interface
+
+You can also download at any time the database directly from the **settings** menu.
+
+The downloaded database is compressed with Gzip, the filename becomes `db.sqlite.gz`.
+
+Import/Restoration
+------------------
+
+There is actually no way to restore the database from the user interface.
+The restoration must be done manually when no body use the software.
+
+- To restore an old backup, just replace and overwrite the actual file `data/db.sqlite`.
+- To uncompress a gzipped database, execute this command from a terminal `gunzip db.sqlite.gz`.
+
+Optimization
+------------
+
+Occasionally, it's possible to optimize the database file by running the command `VACUUM`.
+This command rebuild the entire database and can be used for several reasons:
+
+- Reduce the file size, deleting data produce empty space but doesn't change the file size.
+- The database is fragmented due to frequent inserts or updates.
+
+### From the command line
+
+```
+sqlite3 data/db.sqlite 'VACUUM'
+```
+
+### From the user interface
+
+Go to the menu **settings** and click on the link **Optimize the database**.
+
+For more information, read the [Sqlite documentation](https://sqlite.org/lang_vacuum.html).
diff --git a/sources/doc/subtasks.markdown b/sources/doc/subtasks.markdown
new file mode 100644
index 0000000..3c9e835
--- /dev/null
+++ b/sources/doc/subtasks.markdown
@@ -0,0 +1,44 @@
+Subtasks
+========
+
+Subtasks are useful to split the work of a task.
+
+Each subtask:
+
+- Can be assigned to a project member
+- Have 3 different statuses: **Todo**, **In progress**, **Done**
+- Have time tracking information: **time spent** and **time estimated**
+- Be ordered by position
+
+Creating subtasks
+-----------------
+
+From the task view, on left sidebar click on **Add a subtask**:
+
+![Add a subtask](http://kanboard.net/screenshots/documentation/add-subtask.png)
+
+You can also add quickly a subtask by entering only the title:
+
+![Add a subtask from the task view](http://kanboard.net/screenshots/documentation/add-subtask-shortcut.png)
+
+Change subtask status
+---------------------
+
+When you click on the subtask title the status change:
+
+![Subtask in progress](http://kanboard.net/screenshots/documentation/subtask-status-inprogress.png)
+
+The icon before the title is updated according to the status.
+
+![Subtask done](http://kanboard.net/screenshots/documentation/subtask-status-done.png)
+
+Note: When the task is closed, all subtasks are changed to the status **Done**.
+
+Subtask timer
+-------------
+
+- Each time a subtask is in progress, the timer is also started. The timer can be started and stopped at any time.
+- The timer records the time spent on the subtask automatically. You can also change manually the value of the time spent field when you edit a subtask.
+- The time calculated is rounded to the nearest quarter. This information is recorded in a separate table.
+- The task time spent is updated automatically according to the sum of all subtasks time spent.
+
diff --git a/sources/doc/swimlanes.markdown b/sources/doc/swimlanes.markdown
new file mode 100644
index 0000000..25e8b6b
--- /dev/null
+++ b/sources/doc/swimlanes.markdown
@@ -0,0 +1,29 @@
+Swimlanes
+=========
+
+Swimlanes are horizontal separations in your board.
+By example, it's useful to separate software releases, divide your tasks in different products, teams or what ever you want.
+
+Board with swimlanes
+--------------------
+
+![Swimlanes Configuration](http://kanboard.net/screenshots/documentation/swimlanes.png)
+
+Managing swimlanes
+------------------
+
+- All projects have a default swimlane.
+- If there is more than one swimlane, the board will show all swimlanes.
+- You can drag and drop tasks between swimlanes.
+
+To configure swimlanes go to the **project configuration page** and choose the section **Swimlanes**.
+
+![Swimlanes Configuration](http://kanboard.net/screenshots/documentation/swimlanes-configuration.png)
+
+From there, you can add a new swimlane or rename the default one.
+You can also disable and change the position of the different swimlanes.
+
+- The default swimlane is always on the top but you can hide it.
+- Inactive swimlanes are not shown on the board.
+- **Removing a swimlane doesn't remove tasks assigned to it**, those tasks will be moved to the default swimlane.
+
diff --git a/sources/doc/syntax-guide.markdown b/sources/doc/syntax-guide.markdown
new file mode 100644
index 0000000..8b1324f
--- /dev/null
+++ b/sources/doc/syntax-guide.markdown
@@ -0,0 +1,139 @@
+Syntax Guide
+============
+
+Kanboard use the [Markdown syntax](http://en.wikipedia.org/wiki/Markdown) for comments or task descriptions.
+Here are some examples:
+
+Bold and italic
+----------------
+
+- Bold text: Use 2 asterisks or 2 underscores
+- Italic text: Use 1 asterisk or 1 underscore
+
+### Source
+```
+This **word** is very __important__.
+
+And here, an *italic* word with one _underscore_.
+```
+
+### Result
+
+This **word** is very __important__.
+
+And here, an *italic* word with one _underscore_.
+
+Unordered Lists
+---------------
+
+Unordered list can use asterisks, minuses or pluses.
+
+### Source
+
+```
+- Item 1
+- Item 2
+- Item 3
+
+or
+
+* Item 1
+* Item 2
+* Item 3
+```
+
+### Result
+
+- Item 1
+- Item 2
+- Item 3
+
+Ordered lists
+-------------
+
+Ordered lists are prefixed by a number like that:
+
+### Source
+
+```
+1. Do that first
+2. Do this
+3. And that
+```
+
+### Result
+
+1. Do that first
+2. Do this
+3. And that
+
+Links
+-----
+
+### Source
+
+```
+[My link title](http://kanboard.net/)
+
+
+
+```
+
+### Result
+
+[My link title](http://kanboard.net/)
+
+
+
+Source code
+-----------
+
+### Inline code
+
+Use a backtick.
+
+```
+Execute this command: `tail -f /var/log/messages`.
+```
+
+### Result
+
+Execute this command: `tail -f /var/log/messages`.
+
+### Code blocks
+
+Use 3 backticks with eventually the language name.
+
+
+```php
+<?php
+
+phpinfo();
+
+?>
+```
+
+
+
+### Result
+
+```
+
+```
+
+Titles
+------
+
+### Source
+
+```
+# Title level 1
+
+## Title level 2
+
+### Title level 3
+```
diff --git a/sources/doc/task-links.markdown b/sources/doc/task-links.markdown
new file mode 100644
index 0000000..1eab51f
--- /dev/null
+++ b/sources/doc/task-links.markdown
@@ -0,0 +1,22 @@
+Task Links
+==========
+
+Tasks can be linked together with predefined relationships:
+
+![Task Links](http://kanboard.net/screenshots/documentation/task-links.png)
+
+The default relationships are:
+
+- **relates to**
+- **blocks** | is blocked by
+- **is blocked by** | blocks
+- **duplicates** | is duplicated by
+- **is duplicated by** | duplicates
+- **is a child of** | is a parent of
+- **is a parent of** | is a child of
+- **targets milestone** | is a milestone of
+- **is a milestone of** | targets milestone
+- **fixes** | is fixed by
+- **is fixed by** | fixes
+
+Those labels can be changed in the application settings.
diff --git a/sources/doc/tests.markdown b/sources/doc/tests.markdown
new file mode 100644
index 0000000..31937f3
--- /dev/null
+++ b/sources/doc/tests.markdown
@@ -0,0 +1,176 @@
+How to run units and functionals tests?
+=======================================
+
+[PHPUnit](https://phpunit.de/) is used to run automatic tests on Kanboard.
+
+You can run tests across different databases (Sqlite, Mysql and Postgresql) to be sure that the result is the same everywhere.
+
+Requirements
+------------
+
+- Linux/Unix machine
+- PHP command line
+- PHPUnit installed
+- Mysql and Postgresql (optional)
+
+Install the latest version of PHPUnit
+-------------------------------------
+
+Simply download the PHPUnit PHAR et copy the file somewhere in your `$PATH`:
+
+```bash
+wget https://phar.phpunit.de/phpunit.phar
+chmod +x phpunit.phar
+sudo mv phpunit.phar /usr/local/bin/phpunit
+phpunit --version
+PHPUnit 4.2.6 by Sebastian Bergmann.
+```
+
+Running unit tests
+------------------
+
+### Testing with Sqlite
+
+Sqlite tests use a in-memory database, nothing is written on the filesystem.
+
+The config file is `tests/units.sqlite.xml`.
+From your Kanboard directory, run the command `phpunit -c tests/units.sqlite.xml`.
+
+Example:
+
+```bash
+phpunit -c tests/units.sqlite.xml
+
+PHPUnit 4.2.6 by Sebastian Bergmann.
+
+Configuration read from /Volumes/Devel/apps/kanboard/tests/units.sqlite.xml
+
+................................................................. 65 / 74 ( 87%)
+.........
+
+Time: 9.05 seconds, Memory: 17.75Mb
+
+OK (74 tests, 6145 assertions)
+```
+
+**NOTE:** PHPUnit is already included in the Vagrant environment
+
+### Testing with Mysql
+
+You must have Mysql or MariaDb installed on localhost.
+
+By default, those credentials are used:
+
+- Hostname: **localhost**
+- Username: **root**
+- Password: none
+- Database: **kanboard_unit_test**
+
+For each execution the database is dropped and created again.
+
+The config file is `tests/units.mysql.xml`.
+From your Kanboard directory, run the command `phpunit -c tests/units.mysql.xml`.
+
+Example:
+
+```bash
+phpunit -c tests/units.mysql.xml
+
+PHPUnit 4.2.6 by Sebastian Bergmann.
+
+Configuration read from /Volumes/Devel/apps/kanboard/tests/units.mysql.xml
+
+................................................................. 65 / 74 ( 87%)
+.........
+
+Time: 49.77 seconds, Memory: 17.50Mb
+
+OK (74 tests, 6145 assertions)
+```
+
+### Testing with Postgresql
+
+You must have Postgresql installed on localhost.
+
+By default, those credentials are used:
+
+- Hostname: **localhost**
+- Username: **postgres**
+- Password: none
+- Database: **kanboard_unit_test**
+
+Be sure to allow the user `postgres` to create and drop databases.
+For each execution the database is dropped and created again.
+
+The config file is `tests/units.postgres.xml`.
+From your Kanboard directory, run the command `phpunit -c tests/units.postgres.xml`.
+
+Example:
+
+```bash
+phpunit -c tests/units.postgres.xml
+
+PHPUnit 4.2.6 by Sebastian Bergmann.
+
+Configuration read from /Volumes/Devel/apps/kanboard/tests/units.postgres.xml
+
+................................................................. 65 / 74 ( 87%)
+.........
+
+Time: 52.66 seconds, Memory: 17.50Mb
+
+OK (74 tests, 6145 assertions)
+```
+
+Running functionals tests
+-------------------------
+
+Actually only the API calls are tested.
+
+Real HTTP calls are made with those tests.
+So a local instance of Kanboard is necessary and must listen on `http://localhost:8000`.
+
+Don't forget that all data will be removed/altered by the test suite.
+Moreover the script will reset and set a new API key.
+
+1. Start a local instance of Kanboard `php -S 127.0.0.1:8000`
+2. Run the test suite from another terminal
+
+The same method as above is used to run tests across different databases:
+
+- Sqlite: `phpunit -c tests/functionals.sqlite.xml`
+- Mysql: `phpunit -c tests/functionals.mysql.xml`
+- Postgresql: `phpunit -c tests/functionals.postgres.xml`
+
+Example:
+
+```bash
+phpunit -c tests/functionals.sqlite.xml
+
+PHPUnit 4.2.6 by Sebastian Bergmann.
+
+Configuration read from /Volumes/Devel/apps/kanboard/tests/functionals.sqlite.xml
+
+..........................................
+
+Time: 1.72 seconds, Memory: 4.25Mb
+
+OK (42 tests, 160 assertions)
+```
+
+Continuous Integration with Travis-ci
+-------------------------------------
+
+After each commit pushed on the main repository, unit tests are executed across 5 different versions of PHP:
+
+- PHP 7.0
+- PHP 5.6
+- PHP 5.5
+- PHP 5.4
+- PHP 5.3
+
+Each version of PHP is tested against the 3 supported database: Sqlite, Mysql and Postgresql.
+
+That mean we run 15 jobs each time the repository is updated. The execution time is around 25 minutes.
+
+The Travis config file `.travis.yml` is located on the root directory of Kanboard.
diff --git a/sources/doc/time-tracking.markdown b/sources/doc/time-tracking.markdown
new file mode 100644
index 0000000..8c65b16
--- /dev/null
+++ b/sources/doc/time-tracking.markdown
@@ -0,0 +1,43 @@
+Time Tracking
+=============
+
+Time tracking information can be defined at the task level or at the subtask level.
+
+Task time tracking
+------------------
+
+![Task time tracking](http://kanboard.net/screenshots/documentation/task-time-tracking.png)
+
+Tasks have two fields:
+
+- Time estimated
+- Time spent
+
+These values represents hours of work and have to be set manually.
+
+Subtask time tracking
+---------------------
+
+![Subtask time tracking](http://kanboard.net/screenshots/documentation/subtask-time-tracking.png)
+
+Subtasks also have the fields "time spent" and "time estimated".
+
+When you change the value of these fields, **the task time tracking values are updated automatically and becomes the sum of all subtask values**.
+
+Kanboard records the time between each subtask status change in a separate table.
+
+- Changing subtask status from **todo** to **in pogress** logs the start time
+- Changing subtask status from **in progress** to **done** logs the end time but also update the time spent of the subtask and the task
+
+The breakdown of all records is visible in the task view page:
+
+![Task timesheet](http://kanboard.net/screenshots/documentation/task-timesheet.png)
+
+For each subtask, the timer can be stopped/started at any time:
+
+![Subtask timer](http://kanboard.net/screenshots/documentation/subtask-timer.png)
+
+- The timer doesn't depends of the subtask status
+- Each time you start the timer a new record is created in the time tracking table
+- Each time you stop the clock the end date is recorded in the time tracking table
+- The calculated time spent is rounded to the nearest quarter
diff --git a/sources/doc/timetable.markdown b/sources/doc/timetable.markdown
new file mode 100644
index 0000000..5d2f4c8
--- /dev/null
+++ b/sources/doc/timetable.markdown
@@ -0,0 +1,46 @@
+User Timetable
+==============
+
+Each user can have a predefined timetable.
+This feature mainly is used for time tracking, project budget calculation and to display subtasks in the calendar.
+
+Each user have his own timetable. At the moment, that need to be specified manually for each person.
+You can also schedule time-off or overtime.
+
+The timetable section is available from the user profile: **User profile > Timetable**.
+
+Work timetable
+--------------
+
+This timetable is dynamically calculated according to the regular week timetable, time-off and overtime.
+
+![Timetable](http://kanboard.net/screenshots/documentation/timetable.png)
+
+Week timetable
+--------------
+
+![Week Timetable](http://kanboard.net/screenshots/documentation/week-timetable.png)
+
+The week timetable is used to define regular work hours for the selected user.
+
+To add a new time slot, just select the day of the week and the time range.
+
+Time off timetable
+------------------
+
+The time-off timetable is used to schedule not worked time slot.
+This time is deducted from the regular work hours.
+
+When you check the box "All day", the regular day timetable is used to define the regular work hours.
+
+Overtime timetable
+------------------
+
+![Overtime Timetable](http://kanboard.net/screenshots/documentation/overtime-timetable.png)
+
+The overtime timetable is used to define worked hours outside of regular hours.
+
+Day timetable
+-------------
+
+This timetable is used when the checkbox "All day" is checked for overtime and time-off entries.
diff --git a/sources/doc/transitions.markdown b/sources/doc/transitions.markdown
new file mode 100644
index 0000000..c32f696
--- /dev/null
+++ b/sources/doc/transitions.markdown
@@ -0,0 +1,20 @@
+Task transitions
+================
+
+Transitions record each movement of the tasks between columns.
+
+![Transitions](http://kanboard.net/screenshots/documentation/transitions.png)
+
+Available from the task view, you can see those information:
+
+- Date of the action
+- Source column
+- Destination column
+- Executer (user that move the task)
+- Time spent in the origin column
+
+Task transition data can also be exported from the project settings page.
+
+![Transitions Export](http://kanboard.net/screenshots/documentation/transitions-export.png)
+
+For the specified time range you will generate a CSV file that you can use with any spreadsheet software.
diff --git a/sources/doc/translations.markdown b/sources/doc/translations.markdown
new file mode 100644
index 0000000..7a4e325
--- /dev/null
+++ b/sources/doc/translations.markdown
@@ -0,0 +1,79 @@
+Translations
+============
+
+How to translate Kanboard to a new language?
+--------------------------------------------
+
+- Translations are stored inside the directory `app/Locale`
+- There is sub-directory for each language, by example for the French we have `fr_FR`, Italian `it_IT` etc...
+- A translation is a PHP file that returns an Array with a key-value pairs
+- The key is the original text in english and the value is the translation for the corresponding language
+- **French translations are always up to date**
+- Always use the last version (branch master)
+
+### Create a new translation:
+
+1. Make a new directory: `app/Locale/xx_XX` by example `app/Locale/fr_CA` for French Canadian
+2. Create a new file for the translation: `app/Locale/xx_XX/translations.php`
+3. Use the content of the French locales and replace the values
+4. Inside the file `app/Model/Config.php`, add a new entry for your translation inside the function `getLanguages()`
+5. Check with your local installation of Kanboard if everything is ok
+6. Send a [pull-request with Github](https://help.github.com/articles/using-pull-requests/)
+
+How to update an existing translation?
+--------------------------------------
+
+1. Open the translation file `app/Locale/xx_XX/translations.php`
+2. Missing translations are commented with `//` and the values are empty, just fill blank and remove the comment
+3. Check with your local installation of Kanboard and send a [pull-request](https://help.github.com/articles/using-pull-requests/)
+
+How to add new translated text in the application?
+--------------------------------------------------
+
+Translations are displayed with the following functions in the source code:
+
+- `t()`: dispaly text with HTML escaping
+- `e()`: display text without HTML escaping
+- `dt()`: display date and time using the `strftime()` function formats
+
+Always use the english version in the source code.
+
+### Date and time translation
+
+Date strings use the function `strftime()` to format the date.
+
+By example, the original English version can be defined like that `Created on %B %e, %Y at %k:%M %p` and that will output something like that `Created on January 11, 2015 at 15:19 PM`. The French version can be modified to display a different format, `Créé le %d/%m/%Y à %H:%M` and the result will be `Créé le 11/01/2015 à 15:19`.
+
+All formats are available in the [PHP documentation](http://php.net/strftime).
+
+### Placeholders
+
+Text strings use the function `sprintf()` to replace elements:
+
+- `%s` is used to replace a string
+- `%d` is used to replace an integer
+
+All formats are available in the [PHP documentation](http://php.net/sprintf).
+
+How to find missing translations in the applications?
+-----------------------------------------------------
+
+From a terminal, run the following command:
+
+```bash
+./kanboard locale:compare
+```
+
+All missing and unused translations are displayed on the screen.
+Put that in the French locale and sync other locales (see below).
+
+How to synchronize translation files?
+-------------------------------------
+
+From a Unix shell run this command:
+
+```bash
+./kanboard locale:sync
+```
+
+The French translation is used a reference for other locales.
diff --git a/sources/doc/ubuntu-installation.markdown b/sources/doc/ubuntu-installation.markdown
new file mode 100644
index 0000000..5f9ee65
--- /dev/null
+++ b/sources/doc/ubuntu-installation.markdown
@@ -0,0 +1,28 @@
+How to install Kanboard on Ubuntu?
+==================================
+
+Ubuntu 14.04 LTS
+----------------
+
+Install Apache and PHP:
+
+```bash
+sudo apt-get update
+sudo apt-get install -y php5 php5-sqlite unzip
+```
+
+In case your webserver was running restart to make sure the php modules are reloaded
+
+```bash
+service apache2 restart
+```
+
+Install Kanboard:
+
+```bash
+cd /var/www/html
+sudo wget http://kanboard.net/kanboard-latest.zip
+sudo unzip kanboard-latest.zip
+sudo chown -R www-data:www-data kanboard/data
+sudo rm kanboard-latest.zip
+```
diff --git a/sources/doc/update.markdown b/sources/doc/update.markdown
new file mode 100644
index 0000000..0c59a85
--- /dev/null
+++ b/sources/doc/update.markdown
@@ -0,0 +1,26 @@
+Update
+======
+
+**Always make a backup of your database before upgrading!**
+
+From the archive (stable version)
+---------------------------------
+
+1. Close all sessions (logout)
+2. Rename your actual Kanboard directory (to keep a backup)
+3. Uncompress the new archive and copy your `data` directory to the newly uncompressed directory.
+4. Copy your custom `config.php` (if you created one) to the root of the newly uncompressed directory.
+5. Make the directory `data` writeable by the web server user
+6. Login and check if everything is ok
+7. Remove the old Kanboard directory
+
+
+From the repository (development version)
+-----------------------------------------
+
+1. Close all sessions
+2. `git pull`
+3. `composer install`
+3. Login and check if everything is ok
+
+Note: This method will install the **current development version**, use at your own risk.
diff --git a/sources/doc/usage-examples.markdown b/sources/doc/usage-examples.markdown
new file mode 100644
index 0000000..f2d457a
--- /dev/null
+++ b/sources/doc/usage-examples.markdown
@@ -0,0 +1,67 @@
+Usage examples
+==============
+
+You can customize your boards according to your business activities:
+
+Software development
+--------------------
+
+- Backlog
+- Ready
+- Work in progress
+- To be validated
+- Validated
+- Deployed in production
+
+Bug tracking
+------------
+
+- Reported
+- Confirmed
+- Work in progress
+- Tested
+- Fixed
+
+Sales
+-----
+
+- Leads
+- Meeting
+- Proposal
+- Purchase
+
+Lean business management
+------------------------
+
+- Ideas
+- Development
+- Measure
+- Analysis
+- Done
+
+Recruiting process
+------------------
+
+- Job offers
+- Candidates
+- Phone screens
+- Interviews
+- Hires
+
+Online shops
+------------
+
+- Orders
+- Packaging
+- Ready to send
+- Shipped
+
+Manufactory
+-----------
+
+- Customer Orders
+- Assembly
+- Tests
+- Packaging
+- Ready to ship
+- Shipped
diff --git a/sources/doc/user-management.markdown b/sources/doc/user-management.markdown
new file mode 100644
index 0000000..25cc853
--- /dev/null
+++ b/sources/doc/user-management.markdown
@@ -0,0 +1,81 @@
+User management
+===============
+
+Roles at the application level
+------------------------------
+
+Kanboard use a basic permission system, there are 3 type of users:
+
+### Administrators
+
+- Access to everything
+
+### Project Administrators
+
+- Can create multi-users and private projects
+- Can convert multi-users and private projects
+- Can see only their own projects
+- Cannot change application settings
+- Cannot manage users
+
+### Standard Users
+
+- Can create only private projects
+- Can see only their own projects
+- Cannot remove projects
+
+Roles at the project level
+--------------------------
+
+These role are related to the project permission.
+
+### Project Managers
+
+- Can manage only their own projects
+- Can access to reports and budget section
+
+### Project Members
+
+- Can do any daily operations in their projects (create and move tasks...)
+- Cannot configure projects
+
+Note: Any "Standard User" can be promotted "Project Manager" for a given project, they don't necessary need to be "Project Administrator".
+
+Local and remote users
+----------------------
+
+- A local user is an account that use the database to store credentials. Local users use the login form for the authentication.
+- A remote user is an account that use an external system to store credentials. By example, it can be LDAP, Github or Google accounts. Authentication of these users can be done through the login form or not.
+
+Add a new user
+--------------
+
+To add a new user, you must be administrator.
+
+1. From the dashboard, go to the menu **User Management**
+2. On the top, you have a link **New local user** or **New remote user**
+3. Fill the form and save
+
+![New user](http://kanboard.net/screenshots/documentation/new-user.png)
+
+When you create a **local user**, you have to specify at least those values:
+
+- **username**: This is the unique identifier of your user (login)
+- **password**: The password of your user must have at least 6 characters
+
+For **remote users**, only the username is mandatory. You can also pre-link Github or Google accounts if you already know their unique id.
+
+Edit users
+----------
+
+When you go to the **users** menu, you have the list of users, to modify a user click on the **edit link**.
+
+- If you are a regular user, you can change only your own profile
+- You have to be administrator to be able to edit any users
+
+Remove users
+------------
+
+From the **users** menu, click on the link **remove**. This link is visible only if you are administrator.
+
+If you remove a specific user, **tasks assigned to this person will be unassigned** after the operation.
diff --git a/sources/doc/vagrant.markdown b/sources/doc/vagrant.markdown
new file mode 100644
index 0000000..beebb32
--- /dev/null
+++ b/sources/doc/vagrant.markdown
@@ -0,0 +1,64 @@
+Run Kanboard with Vagrant
+=========================
+
+Vagrant is used to test Kanboard in different environments.
+
+Several configurations are available:
+
+- Ubuntu 14.04 LTS with Sqlite
+- Ubuntu 14.04 LTS with Mysql
+- Ubuntu 14.04 LTS with Postgresql
+- Debian 8 with sqlite
+- Debian 7.6 with Sqlite
+- Debian 6 with Sqlite
+- Centos 7 with Sqlite
+- Centos 6.5 with Sqlite
+- Freebsd 10 with Sqlite
+
+The installation process is not fully automated for all VM, manual configuration can be required.
+
+To use those configurations, you have to install the **last version of Virtualbox and Vagrant**.
+
+Standard boxes can be downloaded from Vagrant:
+
+```bash
+vagrant box add ubuntu/trusty64
+vagrant box add debian/jessie64
+vagrant box add chef/debian-7.6
+vagrant box add chef/debian-6.0.10
+vagrant box add chef/centos-7.0
+vagrant box add chef/centos-6.5
+vagrant box add freebsd/FreeBSD-10.2-STABLE
+```
+
+### Example with Ubuntu and Sqlite
+
+If you want to test Kanboard on Ubuntu with Sqlite:
+
+```bash
+vagrant up sqlite
+```
+
+Run composer:
+
+```bash
+vagrant ssh sqlite
+cd /var/www/html # change the path according to the chosen distribution
+sudo composer install
+```
+
+After the initialization, go to **http://localhost:8001/**.
+
+If you want to use Postgresql or Mysql, you have to configure Kanboard manually (`config.php`) and configure the database inside the virtual machine.
+
+Available boxes are:
+
+- `vagrant up sqlite`
+- `vagrant up mysql`
+- `vagrant up postgres`
+- `vagrant up debian8`
+- `vagrant up debian7`
+- `vagrant up debian6`
+- `vagrant up centos7`
+- `vagrant up centos65`
+- `vagrant up freebsd10`
diff --git a/sources/doc/webhooks.markdown b/sources/doc/webhooks.markdown
new file mode 100644
index 0000000..f792535
--- /dev/null
+++ b/sources/doc/webhooks.markdown
@@ -0,0 +1,306 @@
+Webhooks
+========
+
+Webhooks are useful to perform actions with external applications.
+
+- Webhooks can be used to create a task by calling a simple URL (You can also do that with the API)
+- An external URL can be called automatically when an event occurs in Kanboard (task creation, comment updated, etc)
+
+How to write a webhook receiver?
+--------------------------------
+
+All internal events of Kanboard can be sent to an external URL.
+
+- The webhook url have to be defined in **Settings > Webhooks > Webhook URL**.
+- When an event is triggered Kanboard call automatically the predefined URL
+- The data are encoded in JSON format and sent with a POST HTTP request
+- The webhook token is also sent as a query string parameter, so you can check if the request really come from Kanboard.
+- **Your custom URL must answer in less than 1 second**, those requests are synchronous (PHP limitation) and that can slow down the user interface if your script is too slow!
+
+### List of supported events
+
+- comment.create
+- comment.update
+- file.create
+- task.move.project
+- task.move.column
+- task.move.position
+- task.move.swimlane
+- task.update
+- task.create
+- task.close
+- task.open
+- task.assignee_change
+- subtask.update
+- subtask.create
+
+### Example of HTTP request
+
+```
+POST https://your_webhook_url/?token=WEBHOOK_TOKEN_HERE
+User-Agent: Kanboard Webhook
+Content-Type: application/json
+Connection: close
+
+{
+ "event_name": "task.move.column",
+ "event_data": {
+ "task_id": "1",
+ "project_id": "1",
+ "position": 1,
+ "column_id": "1",
+ "swimlane_id": "0",
+ "src_column_id": "2",
+ "dst_column_id": "1",
+ "date_moved": "1431991532",
+ "recurrence_status": "0",
+ "recurrence_trigger": "0"
+ }
+}
+```
+
+All event payloads are in the following format:
+
+```json
+{
+ "event_name": "model.event_name",
+ "event_data": {
+ "key1": "value1",
+ "key2": "value2",
+ ...
+ }
+}
+```
+
+The `event_data` values are not necessary normalized across events.
+
+### Examples of event payloads
+
+Task creation:
+
+```json
+{
+ "event_name": "task.create",
+ "event_data": {
+ "title": "Demo",
+ "description": "",
+ "project_id": "1",
+ "owner_id": "1",
+ "category_id": 0,
+ "swimlane_id": 0,
+ "column_id": "2",
+ "color_id": "yellow",
+ "score": 0,
+ "time_estimated": 0,
+ "date_due": 0,
+ "creator_id": 1,
+ "date_creation": 1431991532,
+ "date_modification": 1431991532,
+ "date_moved": 1431991532,
+ "position": 1,
+ "task_id": 1
+ }
+}
+```
+
+Task modification:
+
+```json
+{
+ "event_name": "task.update",
+ "event_data": {
+ "id": "1",
+ "title": "Demo",
+ "description": "",
+ "date_creation": "1431991532",
+ "color_id": "yellow",
+ "project_id": "1",
+ "column_id": "1",
+ "owner_id": "1",
+ "position": "1",
+ "is_active": "1",
+ "date_completed": null,
+ "score": "0",
+ "date_due": "0",
+ "category_id": "2",
+ "creator_id": "1",
+ "date_modification": 1431991603,
+ "reference": "",
+ "date_started": 1431993600,
+ "time_spent": 0,
+ "time_estimated": 0,
+ "swimlane_id": "0",
+ "date_moved": "1431991572",
+ "recurrence_status": "0",
+ "recurrence_trigger": "0",
+ "recurrence_factor": "0",
+ "recurrence_timeframe": "0",
+ "recurrence_basedate": "0",
+ "recurrence_parent": null,
+ "recurrence_child": null,
+ "task_id": "1",
+ "changes": {
+ "category_id": "2"
+ }
+ }
+}
+```
+
+Task update events have a field called `changes` that contains updated values.
+
+Move a task to another column:
+
+```json
+{
+ "event_name": "task.move.column",
+ "event_data": {
+ "task_id": "1",
+ "project_id": "1",
+ "position": 1,
+ "column_id": "1",
+ "swimlane_id": "0",
+ "src_column_id": "2",
+ "dst_column_id": "1",
+ "date_moved": "1431991532",
+ "recurrence_status": "0",
+ "recurrence_trigger": "0"
+ }
+}
+```
+
+Move a task to another position:
+
+```json
+{
+ "event_name": "task.move.position",
+ "event_data": {
+ "task_id": "2",
+ "project_id": "1",
+ "position": 1,
+ "column_id": "1",
+ "swimlane_id": "0",
+ "src_column_id": "1",
+ "dst_column_id": "1",
+ "date_moved": "1431996905",
+ "recurrence_status": "0",
+ "recurrence_trigger": "0"
+ }
+}
+```
+
+Comment creation:
+
+```json
+{
+ "event_name": "comment.create",
+ "event_data": {
+ "id": 1,
+ "task_id": "1",
+ "user_id": "1",
+ "comment": "test",
+ "date_creation": 1431991615
+ }
+}
+```
+
+Comment modification:
+
+```
+{
+ "event_name": "comment.update",
+ "event_data": {
+ "id": "1",
+ "task_id": "1",
+ "user_id": "1",
+ "comment": "test edit"
+ }
+}
+```
+
+Subtask creation:
+
+```json
+{
+ "event_name": "subtask.create",
+ "event_data": {
+ "id": 3,
+ "task_id": "1",
+ "title": "Test",
+ "user_id": "1",
+ "time_estimated": "2",
+ "position": 3
+ }
+}
+```
+
+Subtask modification:
+
+```json
+{
+ "event_name": "subtask.update",
+ "event_data": {
+ "id": "1",
+ "status": 1,
+ "task_id": "1"
+ }
+}
+```
+
+File upload:
+
+```json
+{
+ "event_name": "file.create",
+ "event_data": {
+ "task_id": "1",
+ "name": "test.png"
+ }
+}
+```
+
+Screenshot created:
+
+```json
+{
+ "event_name": "file.create",
+ "event_data": {
+ "task_id": "2",
+ "name": "Screenshot taken May 19, 2015 at 10:56 AM"
+ }
+}
+```
+
+Note: Webhooks configuration and payload have changed since Kanboard >= 1.0.15
+
+How to create a task with a webhook?
+------------------------------------
+
+Firstly, you have to get the token from the settings page. After that, just call this url from anywhere:
+
+```bash
+# Create a task for the default project inside the first column
+curl "http://myserver/?controller=webhook&action=task&token=superSecretToken&title=mySuperTask"
+
+# Create a task to another project inside a specific column with the color red
+curl "http://myserver/?controller=webhook&action=task&token=superSecretToken&title=task123&project_id=3&column_id=7&color_id=red"
+```
+
+### Available responses
+
+- When a task is created successfully, Kanboard return the message "OK" in plain text.
+- However if the task creation fail, you will got a "FAILED" message.
+- If the token is wrong, you got a "Not Authorized" message and a HTTP status code 401.
+
+### Available parameters
+
+Base URL: `http://YOUR_SERVER_HOSTNAME/?controller=webhook&action=task`
+
+- `token`: Token displayed on the settings page (required)
+- `title`: Task title (required)
+- `description`: Task description
+- `color_id`: Supported colors are yellow, blue, green, purple, red, orange and grey
+- `project_id`: Project id (Get the id from the project page)
+- `owner_id`: Assignee (Get the user id from the users page)
+- `column_id`: Column on the board (Get the column id from the projects page, mouse over on the column name)
+
+Only the token and the title parameters are mandatory. The different id can also be found in the database.
diff --git a/sources/doc/what-is-kanban.markdown b/sources/doc/what-is-kanban.markdown
new file mode 100644
index 0000000..3a37bb7
--- /dev/null
+++ b/sources/doc/what-is-kanban.markdown
@@ -0,0 +1,32 @@
+What is Kanban?
+===============
+
+Kanban is a methodology originally developed by Toyota to be more efficient.
+
+There is only two constraints imposed by Kanban:
+
+- Visualize your workflow
+- Limit your work in progress
+
+Visualize your workflow
+-----------------------
+
+- Your work is displayed on a board, you have a clear overview of your project
+- Each column represent a step in your workflow
+
+Bring focus and avoid multitasking
+----------------------------------
+
+- Each phase can have a work in progress limit
+- Limits are great to identify bottlenecks
+- Limits avoid working on too many tasks in the same time
+
+Measure performance and improvement
+-----------------------------------
+
+Kanban uses lead and cycle times to measure performance:
+
+- **Lead time**: Time between the task is created and completed
+- **Cycle time**: Time between the task is started and completed
+
+By example, you may have a lead time of 100 days and only have to work 1 hour to complete the task.
diff --git a/sources/doc/windows-apache-installation.markdown b/sources/doc/windows-apache-installation.markdown
new file mode 100644
index 0000000..25d3937
--- /dev/null
+++ b/sources/doc/windows-apache-installation.markdown
@@ -0,0 +1,126 @@
+Installation on Windows Server and Apache
+=========================================
+
+This guide will help you to setup step by step Kanboard on a Windows Server with Apache and PHP.
+
+Note: If you have a 64 bits platform choose "x64" otherwise choose "x86" for 32 bits systems.
+
+Visual C++ Redistributable Installation
+---------------------------------------
+
+PHP and Apache are compiled with Visual Studio so you need to install this library if it's not already done.
+
+1. Download the library from the [official Microsoft website](http://www.microsoft.com/en-us/download/details.aspx?id=30679)
+2. Run the installer `vcredist_x64.exe` or `vcredist_x86.exe` according to your platform
+
+Apache installation
+-------------------
+
+1. Download Apache binary from [Apache Lounge](http://www.apachelounge.com/download/)
+2. Unzip the Apache24 folder to `C:\Apache24`
+
+### Define the server name
+
+Open the file `C:\Apache24\conf\httpd.conf` and add the directive:
+
+```
+ServerName localhost
+```
+
+### Install the Apache service
+
+Open a command prompt (`cmd.exe`) and go to the directory `C:\Apache24\bin`:
+
+```bash
+cd C:\Apache24\bin
+
+# Install the windows service
+httpd.exe -k install
+```
+
+### Install ApacheMonitor
+
+- Double click on `C:\Apache24\bin\ApacheMonitor.exe`, or put it in your startup folder.
+- Right click on the icon and start Apache
+
+### Check the Apache installation
+
+Go to http://localhost/ you should see a blank page with the text "It works!".
+
+PHP installation
+----------------
+
+1. Download the last stable version of PHP from the [official PHP website](http://windows.php.net/download/), choose the **Thread Safe** version and use the exact same build type as Apache: x86 or x64
+2. Unzip the files to `C:\php`
+3. Navigate to the PHP folder and rename the file `php.ini-production` to `php.ini`
+
+Edit the `php.ini`:
+
+Uncomment extension directory:
+
+```ini
+extension_dir = "C:/php/ext"
+```
+
+Uncomment these PHP modules:
+
+```ini
+extension=php_curl.dll
+extension=php_gd2.dll
+extension=php_ldap.dll
+extension=php_mbstring.dll
+extension=php_openssl.dll
+extension=php_pdo_sqlite.dll
+```
+
+Set the timezone:
+
+```ini
+date.timezone = America/Montreal
+```
+
+The list of supported timezones can be found in the [PHP documentation](http://php.net/manual/en/timezones.america.php).
+
+Load the PHP module for Apache:
+
+Add this configuration in the file `C:\Apache24\conf\httpd.conf`:
+
+```
+LoadModule php5_module "c:/php/php5apache2_4.dll"
+AddHandler application/x-httpd-php .php
+
+# configure the path to php.ini
+PHPIniDir "C:/php"
+
+# change this directive
+DirectoryIndex index.php index.html
+```
+
+Restart Apache.
+
+Test your PHP installation:
+
+Create a file named `phpinfo.php` in the folder `C:\Apache24\htdocs`:
+
+```php
+
+```
+
+Go to http://localhost/phpinfo.php and should see all information about your PHP installation.
+
+Kanboard installation
+---------------------
+
+- Download the zip file
+- Uncompress the archive in `C:\Apache24\htdocs\kanboard` by example
+- Open your web browser to use Kanboard http://localhost/kanboard/
+- The default credentials are **admin/admin**
+
+Tested configuration
+--------------------
+
+- Windows 2008 R2 / Apache 2.4.12 / PHP 5.6.8
diff --git a/sources/doc/windows-iis-installation.markdown b/sources/doc/windows-iis-installation.markdown
new file mode 100644
index 0000000..84a2e53
--- /dev/null
+++ b/sources/doc/windows-iis-installation.markdown
@@ -0,0 +1,68 @@
+Installation on Windows 2008/2012 with IIS
+==========================================
+
+This guide will help you to setup step by step Kanboard on a Windows Server with IIS and PHP.
+
+PHP installation
+----------------
+
+- Install IIS on your server (Add a new role and don't forget to enable CGI/FastCGI)
+- Install PHP by following the official documentation:
+ - [Microsoft IIS 5.1 and IIS 6.0](http://php.net/manual/en/install.windows.iis6.php)
+ - [Microsoft IIS 7.0 and later](http://php.net/manual/en/install.windows.iis7.php)
+ - [PHP for Windows is available here](http://windows.php.net/download/)
+
+Edit the `php.ini`, uncomment these PHP modules:
+
+```ini
+extension=php_curl.dll
+extension=php_gd2.dll
+extension=php_ldap.dll
+extension=php_mbstring.dll
+extension=php_openssl.dll
+extension=php_pdo_sqlite.dll
+```
+
+Set the timezone:
+
+```ini
+date.timezone = America/Montreal
+```
+
+The list of supported timezones can be found in the [PHP documentation](http://php.net/manual/en/timezones.america.php).
+
+Check if PHP runs correctly:
+
+Go the IIS document root `C:\inetpub\wwwroot` and create a file `phpinfo.php`:
+
+```php
+
+```
+
+Open a browser at `http://localhost/phpinfo.php` and you should see the current PHP settings.
+If you got an error 500, something is not correctly done in your installation.
+
+Notes:
+
+- If you use PHP < 5.4, you have to enable the short tags in your php.ini
+- Don't forget to enable the required php extensions mentioned above
+- If you got an error about "the library MSVCP110.dll is missing", you probably need to download the Visual C++ Redistributable for Visual Studio from the Microsoft website.
+
+Kanboard installation
+---------------------
+
+- Download the zip file
+- Uncompress the archive in `C:\inetpub\wwwroot\kanboard` by example
+- Make sure the directory `data` is writable by the IIS user
+- Open your web browser to use Kanboard http://localhost/kanboard/
+- The default credentials are **admin/admin**
+
+Tested configurations
+---------------------
+
+- Windows 2008 R2 Standard Edition / IIS 7.5 / PHP 5.5.16
+- Windows 2012 Standard Edition / IIS 8.5 / PHP 5.3.29
diff --git a/sources/vendor/eluceo/ical/.scrutinizer.yml b/sources/vendor/eluceo/ical/.scrutinizer.yml
new file mode 100644
index 0000000..d3b381a
--- /dev/null
+++ b/sources/vendor/eluceo/ical/.scrutinizer.yml
@@ -0,0 +1,20 @@
+filter:
+ excluded_paths:
+ - tests/*
+
+tools:
+ php_cs_fixer: true
+ php_code_sniffer:
+ config:
+ standard: PSR2
+ php_mess_detector: true
+ php_analyzer: true
+ sensiolabs_security_checker: true
+ external_code_coverage:
+ timeout: 300
+ runs: 1
+
+checks:
+ php:
+ code_rating: true
+ duplication: true
diff --git a/sources/vendor/eluceo/ical/LICENSE b/sources/vendor/eluceo/ical/LICENSE
new file mode 100644
index 0000000..92be8d0
--- /dev/null
+++ b/sources/vendor/eluceo/ical/LICENSE
@@ -0,0 +1,19 @@
+Copyright (c) 2012-2015 Markus Poerschke
+
+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.
diff --git a/sources/vendor/eluceo/ical/UPGRADE.md b/sources/vendor/eluceo/ical/UPGRADE.md
new file mode 100644
index 0000000..02aae19
--- /dev/null
+++ b/sources/vendor/eluceo/ical/UPGRADE.md
@@ -0,0 +1,4 @@
+# v0.7.0 -> v0.8.0
+
+- The signature of the ```Event::setOrganizer``` method was changed: Now there are
+two paramters name and email instead of an already formatted string.
diff --git a/sources/vendor/eluceo/ical/examples/example1.php b/sources/vendor/eluceo/ical/examples/example1.php
new file mode 100644
index 0000000..4d1d32c
--- /dev/null
+++ b/sources/vendor/eluceo/ical/examples/example1.php
@@ -0,0 +1,30 @@
+setDtStart(new \DateTime('2012-12-24'));
+$vEvent->setDtEnd(new \DateTime('2012-12-24'));
+$vEvent->setNoTime(true);
+$vEvent->setSummary('Christmas');
+
+// Adding Timezone (optional)
+$vEvent->setUseTimezone(true);
+
+// 3. Add event to calendar
+$vCalendar->addComponent($vEvent);
+
+// 4. Set headers
+header('Content-Type: text/calendar; charset=utf-8');
+header('Content-Disposition: attachment; filename="cal.ics"');
+
+// 5. Output
+echo $vCalendar->render();
diff --git a/sources/vendor/eluceo/ical/examples/example2.php b/sources/vendor/eluceo/ical/examples/example2.php
new file mode 100644
index 0000000..bafcf03
--- /dev/null
+++ b/sources/vendor/eluceo/ical/examples/example2.php
@@ -0,0 +1,31 @@
+setDtStart(new \DateTime('2012-12-24'));
+$vEvent->setDtEnd(new \DateTime('2012-12-24'));
+$vEvent->setNoTime(true);
+$vEvent->setSummary('Summary with some german "umlauten" and a backslash \\: Kinder mögen Äpfel pflücken.');
+$vEvent->setCategories(['holidays']);
+
+// Adding Timezone (optional)
+$vEvent->setUseTimezone(true);
+
+// 3. Add event to calendar
+$vCalendar->addComponent($vEvent);
+
+// 4. Set headers
+header('Content-Type: text/calendar; charset=utf-8');
+header('Content-Disposition: attachment; filename="cal.ics"');
+
+// 5. Output
+echo $vCalendar->render();
diff --git a/sources/vendor/eluceo/ical/examples/example3.php b/sources/vendor/eluceo/ical/examples/example3.php
new file mode 100644
index 0000000..290ff2d
--- /dev/null
+++ b/sources/vendor/eluceo/ical/examples/example3.php
@@ -0,0 +1,36 @@
+setDtStart(new \DateTime('2012-12-31'));
+$vEvent->setDtEnd(new \DateTime('2012-12-31'));
+$vEvent->setNoTime(true);
+$vEvent->setSummary('New Year’s Eve');
+
+// Set recurrence rule
+$recurrenceRule = new \Eluceo\iCal\Property\Event\RecurrenceRule();
+$recurrenceRule->setFreq(\Eluceo\iCal\Property\Event\RecurrenceRule::FREQ_YEARLY);
+$recurrenceRule->setInterval(1);
+$vEvent->setRecurrenceRule($recurrenceRule);
+
+// Adding Timezone (optional)
+$vEvent->setUseTimezone(true);
+
+// 3. Add event to calendar
+$vCalendar->addComponent($vEvent);
+
+// 4. Set headers
+header('Content-Type: text/calendar; charset=utf-8');
+header('Content-Disposition: attachment; filename="cal.ics"');
+
+// 5. Output
+echo $vCalendar->render();
diff --git a/sources/vendor/eluceo/ical/examples/example4.php b/sources/vendor/eluceo/ical/examples/example4.php
new file mode 100644
index 0000000..7b87144
--- /dev/null
+++ b/sources/vendor/eluceo/ical/examples/example4.php
@@ -0,0 +1,35 @@
+setDtStart(new \DateTime('2012-11-11 13:00:00'));
+$vEvent->setDtEnd(new \DateTime('2012-11-11 14:30:00'));
+$vEvent->setSummary('Weekly lunch with Markus');
+
+// Set recurrence rule
+$recurrenceRule = new \Eluceo\iCal\Property\Event\RecurrenceRule();
+$recurrenceRule->setFreq(\Eluceo\iCal\Property\Event\RecurrenceRule::FREQ_WEEKLY);
+$recurrenceRule->setInterval(1);
+$vEvent->setRecurrenceRule($recurrenceRule);
+
+// Adding Timezone (optional)
+$vEvent->setUseTimezone(true);
+
+// 3. Add event to calendar
+$vCalendar->addComponent($vEvent);
+
+// 4. Set headers
+header('Content-Type: text/calendar; charset=utf-8');
+header('Content-Disposition: attachment; filename="cal.ics"');
+
+// 5. Output
+echo $vCalendar->render();
diff --git a/sources/vendor/eluceo/ical/examples/example5.php b/sources/vendor/eluceo/ical/examples/example5.php
new file mode 100644
index 0000000..a7da6e2
--- /dev/null
+++ b/sources/vendor/eluceo/ical/examples/example5.php
@@ -0,0 +1,66 @@
+setTzName('CEST');
+$vTimezoneRuleDst->setDtStart(new \DateTime('1981-03-29 02:00:00', $dtz));
+$vTimezoneRuleDst->setTzOffsetFrom('+0100');
+$vTimezoneRuleDst->setTzOffsetTo('+0200');
+$dstRecurrenceRule = new \Eluceo\iCal\Property\Event\RecurrenceRule();
+$dstRecurrenceRule->setFreq(\Eluceo\iCal\Property\Event\RecurrenceRule::FREQ_YEARLY);
+$dstRecurrenceRule->setByMonth(3);
+$dstRecurrenceRule->setByDay('-1SU');
+$vTimezoneRuleDst->setRecurrenceRule($dstRecurrenceRule);
+
+// 3. Create timezone rule object for Standard Time
+$vTimezoneRuleStd = new \Eluceo\iCal\Component\TimezoneRule(\Eluceo\iCal\Component\TimezoneRule::TYPE_STANDARD);
+$vTimezoneRuleStd->setTzName('CET');
+$vTimezoneRuleStd->setDtStart(new \DateTime('1996-10-27 03:00:00', $dtz));
+$vTimezoneRuleStd->setTzOffsetFrom('+0200');
+$vTimezoneRuleStd->setTzOffsetTo('+0100');
+$stdRecurrenceRule = new \Eluceo\iCal\Property\Event\RecurrenceRule();
+$stdRecurrenceRule->setFreq(\Eluceo\iCal\Property\Event\RecurrenceRule::FREQ_YEARLY);
+$stdRecurrenceRule->setByMonth(10);
+$stdRecurrenceRule->setByDay('-1SU');
+$vTimezoneRuleStd->setRecurrenceRule($stdRecurrenceRule);
+
+// 4. Create timezone definition and add rules
+$vTimezone = new \Eluceo\iCal\Component\Timezone($tz);
+$vTimezone->addComponent($vTimezoneRuleDst);
+$vTimezone->addComponent($vTimezoneRuleStd);
+$vCalendar->setTimezone($vTimezone);
+
+// 5. Create an event
+$vEvent = new \Eluceo\iCal\Component\Event();
+$vEvent->setDtStart(new \DateTime('2012-12-24', $dtz));
+$vEvent->setDtEnd(new \DateTime('2012-12-24', $dtz));
+$vEvent->setSummary('Summary with some german "umlauten" and a backslash \\: Kinder mögen Äpfel pflücken.');
+
+// 6. Adding Timezone
+$vEvent->setUseTimezone(true);
+
+// 7. Add event to calendar
+$vCalendar->addComponent($vEvent);
+
+// 8. Set headers
+header('Content-Type: text/calendar; charset=utf-8');
+header('Content-Disposition: attachment; filename="cal.ics"');
+
+// 9. Output
+echo $vCalendar->render();
diff --git a/sources/vendor/eluceo/ical/examples/example6.php b/sources/vendor/eluceo/ical/examples/example6.php
new file mode 100644
index 0000000..1040099
--- /dev/null
+++ b/sources/vendor/eluceo/ical/examples/example6.php
@@ -0,0 +1,30 @@
+setDtStart(new \DateTime('2012-12-24'));
+$vEvent->setDtEnd(new \DateTime('2012-12-24'));
+$vEvent->setNoTime(true);
+$vEvent->setSummary('Christmas');
+
+// add some location information for apple devices
+$vEvent->setLocation("Infinite Loop\nCupertino CA 95014", 'Infinite Loop', '37.332095,-122.030743');
+
+// 3. Add event to calendar
+$vCalendar->addComponent($vEvent);
+
+// 4. Set headers
+header('Content-Type: text/calendar; charset=utf-8');
+header('Content-Disposition: attachment; filename="cal.ics"');
+
+// 5. Output
+echo $vCalendar->render();
diff --git a/sources/vendor/eluceo/ical/src/Eluceo/iCal/Component.php b/sources/vendor/eluceo/ical/src/Eluceo/iCal/Component.php
new file mode 100644
index 0000000..e13c7e0
--- /dev/null
+++ b/sources/vendor/eluceo/ical/src/Eluceo/iCal/Component.php
@@ -0,0 +1,118 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Eluceo\iCal;
+use Eluceo\iCal\Util\ComponentUtil;
+
+/**
+ * Abstract Calender Component.
+ */
+abstract class Component
+{
+ /**
+ * Array of Components.
+ *
+ * @var Component[]
+ */
+ protected $components = array();
+
+ /**
+ * The type of the concrete Component.
+ *
+ * @abstract
+ *
+ * @return string
+ */
+ abstract public function getType();
+
+ /**
+ * Adds a Component.
+ *
+ * If $key is given, the component at $key will be replaced else the component will be append.
+ *
+ * @param Component $component The Component that will be added
+ * @param null $key The key of the Component
+ */
+ public function addComponent(Component $component, $key = null)
+ {
+ if (null == $key) {
+ $this->components[] = $component;
+ } else {
+ $this->components[$key] = $component;
+ }
+ }
+
+ /**
+ * Renders an array containing the lines of the iCal file.
+ *
+ * @return array
+ */
+ public function build()
+ {
+ $lines = array();
+
+ $lines[] = sprintf('BEGIN:%s', $this->getType());
+
+ /** @var $property Property */
+ foreach ($this->buildPropertyBag() as $property) {
+ foreach ($property->toLines() as $l) {
+ $lines[] = $l;
+ }
+ }
+
+ /** @var $component Component */
+ foreach ($this->components as $component) {
+ foreach ($component->build() as $l) {
+ $lines[] = $l;
+ }
+ }
+
+ $lines[] = sprintf('END:%s', $this->getType());
+
+ $ret = array();
+
+ foreach ($lines as $line) {
+ foreach (ComponentUtil::fold($line) as $l) {
+ $ret[] = $l;
+ }
+ }
+
+ return $ret;
+ }
+
+ /**
+ * Renders the output.
+ *
+ * @return string
+ */
+ public function render()
+ {
+ return implode("\r\n", $this->build());
+ }
+
+ /**
+ * Renders the output when treating the class as a string.
+ *
+ * @return string
+ */
+ public function __toString()
+ {
+ return $this->render();
+ }
+
+ /**
+ * Building the PropertyBag.
+ *
+ * @abstract
+ * @return PropertyBag
+ */
+ abstract public function buildPropertyBag();
+}
diff --git a/sources/vendor/eluceo/ical/src/Eluceo/iCal/Component/Alarm.php b/sources/vendor/eluceo/ical/src/Eluceo/iCal/Component/Alarm.php
new file mode 100644
index 0000000..4926e97
--- /dev/null
+++ b/sources/vendor/eluceo/ical/src/Eluceo/iCal/Component/Alarm.php
@@ -0,0 +1,151 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Eluceo\iCal\Component;
+
+use Eluceo\iCal\Component;
+use Eluceo\iCal\PropertyBag;
+use Eluceo\iCal\Property;
+
+/**
+ * Implementation of the VALARM component.
+ */
+class Alarm extends Component
+{
+ /**
+ * Alarm ACTION property.
+ *
+ * According to RFC 5545: 3.8.6.1. Action
+ *
+ * @link http://tools.ietf.org/html/rfc5545#section-3.8.6.1
+ */
+ const ACTION_AUDIO = 'AUDIO';
+ const ACTION_DISPLAY = 'DISPLAY';
+ const ACTION_EMAIL = 'EMAIL';
+
+ protected $action;
+ protected $repeat;
+ protected $duration;
+ protected $description;
+ protected $attendee;
+ protected $trigger;
+
+ public function getType()
+ {
+ return "VALARM";
+ }
+
+ public function getAction()
+ {
+ return $this->action;
+ }
+
+ public function getRepeat()
+ {
+ return $this->repeat;
+ }
+
+ public function getDuration()
+ {
+ return $this->duration;
+ }
+
+ public function getDescription()
+ {
+ return $this->description;
+ }
+
+ public function getAttendee()
+ {
+ return $this->attendee;
+ }
+
+ public function getTrigger()
+ {
+ return $this->trigger;
+ }
+
+ public function setAction($action)
+ {
+ $this->action = $action;
+
+ return $this;
+ }
+
+ public function setRepeat($repeat)
+ {
+ $this->repeat = $repeat;
+
+ return $this;
+ }
+
+ public function setDuration($duration)
+ {
+ $this->duration = $duration;
+
+ return $this;
+ }
+
+ public function setDescription($description)
+ {
+ $this->description = $description;
+
+ return $this;
+ }
+
+ public function setAttendee($attendee)
+ {
+ $this->attendee = $attendee;
+
+ return $this;
+ }
+
+ public function setTrigger($trigger)
+ {
+ $this->trigger = $trigger;
+
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function buildPropertyBag()
+ {
+ $propertyBag = new PropertyBag();
+
+ if (null != $this->trigger) {
+ $propertyBag->set('TRIGGER', $this->trigger);
+ }
+
+ if (null != $this->action) {
+ $propertyBag->set('ACTION', $this->action);
+ }
+
+ if (null != $this->repeat) {
+ $propertyBag->set('REPEAT', $this->repeat);
+ }
+
+ if (null != $this->duration) {
+ $propertyBag->set('DURATION', $this->duration);
+ }
+
+ if (null != $this->description) {
+ $propertyBag->set('DESCRIPTION', $this->description);
+ }
+
+ if (null != $this->attendee) {
+ $propertyBag->set('ATTENDEE', $this->attendee);
+ }
+
+ return $propertyBag;
+ }
+}
diff --git a/sources/vendor/eluceo/ical/src/Eluceo/iCal/Component/Calendar.php b/sources/vendor/eluceo/ical/src/Eluceo/iCal/Component/Calendar.php
new file mode 100644
index 0000000..6b2da3b
--- /dev/null
+++ b/sources/vendor/eluceo/ical/src/Eluceo/iCal/Component/Calendar.php
@@ -0,0 +1,296 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Eluceo\iCal\Component;
+
+use Eluceo\iCal\Component;
+use Eluceo\iCal\PropertyBag;
+
+class Calendar extends Component
+{
+ /**
+ * Methods for calendar components.
+ *
+ * According to RFP 5545: 3.7.2. Method
+ *
+ * @link http://tools.ietf.org/html/rfc5545#section-3.7.2
+ *
+ * And then according to RFC 2446: 3 APPLICATION PROTOCOL ELEMENTS
+ * @link https://www.ietf.org/rfc/rfc2446.txt
+ */
+ const METHOD_PUBLISH = 'PUBLISH';
+ const METHOD_REQUEST = 'REQUEST';
+ const METHOD_REPLY = 'REPLY';
+ const METHOD_ADD = 'ADD';
+ const METHOD_CANCEL = 'CANCEL';
+ const METHOD_REFRESH = 'REFRESH';
+ const METHOD_COUNTER = 'COUNTER';
+ const METHOD_DECLINECOUNTER = 'DECLINECOUNTER';
+
+ /**
+ * This property defines the calendar scale used for the calendar information specified in the iCalendar object.
+ *
+ * According to RFC 5545: 3.7.1. Calendar Scale
+ *
+ * @link http://tools.ietf.org/html/rfc5545#section-3.7
+ */
+ const CALSCALE_GREGORIAN = 'GREGORIAN';
+
+ /**
+ * The Product Identifier.
+ *
+ * According to RFC 2445: 4.7.3 Product Identifier
+ *
+ * This property specifies the identifier for the product that created the Calendar object.
+ *
+ * @link http://www.ietf.org/rfc/rfc2445.txt
+ *
+ * @var string
+ */
+ protected $prodId = null;
+ protected $method = null;
+ protected $name = null;
+ protected $description = null;
+ protected $timezone = null;
+
+ /**
+ * This property defines the calendar scale used for the
+ * calendar information specified in the iCalendar object.
+ *
+ * Also identifies the calendar type of a non-Gregorian recurring appointment.
+ *
+ * @var string
+ *
+ * @see http://tools.ietf.org/html/rfc5545#section-3.7
+ * @see http://msdn.microsoft.com/en-us/library/ee237520(v=exchg.80).aspx
+ */
+ protected $calendarScale = null;
+
+ /**
+ * Specifies whether or not the iCalendar file only contains one appointment.
+ *
+ * @var boolean
+ *
+ * @see http://msdn.microsoft.com/en-us/library/ee203486(v=exchg.80).aspx
+ */
+ protected $forceInspectOrOpen = false;
+
+ /**
+ * Specifies a globally unique identifier for the calendar.
+ *
+ * @var string
+ *
+ * @see http://msdn.microsoft.com/en-us/library/ee179588(v=exchg.80).aspx
+ */
+ protected $calId = null;
+
+ /**
+ * Specifies a suggested iCalendar file download frequency for clients and
+ * servers with sync capabilities.
+ *
+ * @var string
+ *
+ * @see http://msdn.microsoft.com/en-us/library/ee178699(v=exchg.80).aspx
+ */
+ protected $publishedTTL = 'P1W';
+
+ public function __construct($prodId)
+ {
+ if (empty($prodId)) {
+ throw new \UnexpectedValueException('PRODID cannot be empty');
+ }
+
+ $this->prodId = $prodId;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getType()
+ {
+ return 'VCALENDAR';
+ }
+
+ /**
+ * @param $method
+ *
+ * @return $this
+ */
+ public function setMethod($method)
+ {
+ $this->method = $method;
+
+ return $this;
+ }
+
+ /**
+ * @param $name
+ *
+ * @return $this
+ */
+ public function setName($name)
+ {
+ $this->name = $name;
+
+ return $this;
+ }
+
+ /**
+ * @param $description
+ *
+ * @return $this
+ */
+ public function setDescription($description)
+ {
+ $this->description = $description;
+
+ return $this;
+ }
+
+ /**
+ * @param $timezone
+ *
+ * @return $this
+ */
+ public function setTimezone($timezone)
+ {
+ $this->timezone = $timezone;
+
+ return $this;
+ }
+
+ /**
+ * @param $calendarScale
+ *
+ * @return $this
+ */
+ public function setCalendarScale($calendarScale)
+ {
+ $this->calendarScale = $calendarScale;
+
+ return $this;
+ }
+
+ /**
+ * @param boolean $forceInspectOrOpen
+ *
+ * @return $this
+ */
+ public function setForceInspectOrOpen($forceInspectOrOpen)
+ {
+ $this->forceInspectOrOpen = $forceInspectOrOpen;
+
+ return $this;
+ }
+
+ /**
+ * @param string $calId
+ *
+ * @return $this
+ */
+ public function setCalId($calId)
+ {
+ $this->calId = $calId;
+
+ return $this;
+ }
+
+ /**
+ * @param string $ttl
+ *
+ * @return $this
+ */
+ public function setPublishedTTL($ttl)
+ {
+ $this->publishedTTL = $ttl;
+
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function buildPropertyBag()
+ {
+ $propertyBag = new PropertyBag();
+ $propertyBag->set('VERSION', '2.0');
+ $propertyBag->set('PRODID', $this->prodId);
+
+ if ($this->method) {
+ $propertyBag->set('METHOD', $this->method);
+ }
+
+ if ($this->calendarScale) {
+ $propertyBag->set('CALSCALE', $this->calendarScale);
+ $propertyBag->set('X-MICROSOFT-CALSCALE', $this->calendarScale);
+ }
+
+ if ($this->name) {
+ $propertyBag->set('X-WR-CALNAME', $this->name);
+ }
+
+ if ($this->description) {
+ $propertyBag->set('X-WR-CALDESC', $this->description);
+ }
+
+ if ($this->timezone) {
+ if ($this->timezone instanceof Timezone) {
+ $propertyBag->set('X-WR-TIMEZONE', $this->timezone->getZoneIdentifier());
+ $this->addComponent($this->timezone);
+ } else {
+ $propertyBag->set('X-WR-TIMEZONE', $this->timezone);
+ $this->addComponent(new Timezone($this->timezone));
+ }
+ }
+
+ if ($this->forceInspectOrOpen) {
+ $propertyBag->set('X-MS-OLK-FORCEINSPECTOROPEN', $this->forceInspectOrOpen);
+ }
+
+ if ($this->calId) {
+ $propertyBag->set('X-WR-RELCALID', $this->calId);
+ }
+
+ if ($this->publishedTTL) {
+ $propertyBag->set('X-PUBLISHED-TTL', $this->publishedTTL);
+ }
+
+ return $propertyBag;
+ }
+
+ /**
+ * Adds an Event to the Calendar.
+ *
+ * Wrapper for addComponent()
+ *
+ * @see Eluceo\iCal::addComponent
+ * @deprecated Please, use public method addComponent() from abstract Component class
+ *
+ * @param Event $event
+ */
+ public function addEvent(Event $event)
+ {
+ $this->addComponent($event);
+ }
+
+ /**
+ * @return null|string
+ */
+ public function getProdId()
+ {
+ return $this->prodId;
+ }
+
+ public function getMethod()
+ {
+ return $this->method;
+ }
+}
diff --git a/sources/vendor/eluceo/ical/src/Eluceo/iCal/Component/Event.php b/sources/vendor/eluceo/ical/src/Eluceo/iCal/Component/Event.php
new file mode 100644
index 0000000..3e70c64
--- /dev/null
+++ b/sources/vendor/eluceo/ical/src/Eluceo/iCal/Component/Event.php
@@ -0,0 +1,668 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Eluceo\iCal\Component;
+
+use Eluceo\iCal\Component;
+use Eluceo\iCal\Property;
+use Eluceo\iCal\Property\DateTimeProperty;
+use Eluceo\iCal\Property\Event\Attendees;
+use Eluceo\iCal\Property\Event\Organizer;
+use Eluceo\iCal\Property\Event\RecurrenceRule;
+use Eluceo\iCal\PropertyBag;
+
+/**
+ * Implementation of the EVENT component.
+ */
+class Event extends Component
+{
+ const TIME_TRANSPARENCY_OPAQUE = 'OPAQUE';
+ const TIME_TRANSPARENCY_TRANSPARENT = 'TRANSPARENT';
+
+ const STATUS_TENTATIVE = 'TENTATIVE';
+ const STATUS_CONFIRMED = 'CONFIRMED';
+ const STATUS_CANCELLED = 'CANCELLED';
+
+ /**
+ * @var string
+ */
+ protected $uniqueId;
+
+ /**
+ * The property indicates the date/time that the instance of
+ * the iCalendar object was created.
+ *
+ * The value MUST be specified in the UTC time format.
+ *
+ * @var \DateTime
+ */
+ protected $dtStamp;
+
+ /**
+ * @var \DateTime
+ */
+ protected $dtStart;
+
+ /**
+ * Preferentially chosen over the duration if both are set.
+ *
+ * @var \DateTime
+ */
+ protected $dtEnd;
+
+ /**
+ * @var \DateInterval
+ */
+ protected $duration;
+
+ /**
+ * @var boolean
+ */
+ protected $noTime = false;
+
+ /**
+ * @var string
+ */
+ protected $url;
+
+ /**
+ * @var string
+ */
+ protected $location;
+
+ /**
+ * @var string
+ */
+ protected $locationTitle;
+
+ /**
+ * @var string
+ */
+ protected $locationGeo;
+
+ /**
+ * @var string
+ */
+ protected $summary;
+
+ /**
+ * @var Organizer
+ */
+ protected $organizer;
+
+ /**
+ * @see http://www.ietf.org/rfc/rfc2445.txt 4.8.2.7 Time Transparency
+ *
+ * @var string
+ */
+ protected $transparency = self::TIME_TRANSPARENCY_OPAQUE;
+
+ /**
+ * If set to true the timezone will be added to the event.
+ *
+ * @var bool
+ */
+ protected $useTimezone = false;
+
+ /**
+ * @var int
+ */
+ protected $sequence = 0;
+
+ /**
+ * @var Attendees
+ */
+ protected $attendees;
+
+ /**
+ * @var string
+ */
+ protected $description;
+
+ /**
+ * @var string
+ */
+ protected $status;
+
+ /**
+ * @var RecurrenceRule
+ */
+ protected $recurrenceRule;
+
+ /**
+ * This property specifies the date and time that the calendar
+ * information was created.
+ *
+ * The value MUST be specified in the UTC time format.
+ *
+ * @var \DateTime
+ */
+ protected $created;
+
+ /**
+ * The property specifies the date and time that the information
+ * associated with the calendar component was last revised.
+ *
+ * The value MUST be specified in the UTC time format.
+ *
+ * @var \DateTime
+ */
+ protected $modified;
+
+ /**
+ * Indicates if the UTC time should be used or not.
+ *
+ * @var bool
+ */
+ protected $useUtc = true;
+
+ /**
+ * @var bool
+ */
+ protected $cancelled;
+
+ /**
+ * This property is used to specify categories or subtypes
+ * of the calendar component. The categories are useful in searching
+ * for a calendar component of a particular type and category.
+ *
+ * @see https://tools.ietf.org/html/rfc5545#section-3.8.1.2
+ *
+ * @var array
+ */
+ protected $categories;
+
+ /**
+ * https://tools.ietf.org/html/rfc5545#section-3.8.1.3
+ *
+ * @var bool
+ */
+ protected $isPrivate = false;
+
+ public function __construct($uniqueId = null)
+ {
+ if (null == $uniqueId) {
+ $uniqueId = uniqid();
+ }
+
+ $this->uniqueId = $uniqueId;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getType()
+ {
+ return 'VEVENT';
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function buildPropertyBag()
+ {
+ $propertyBag = new PropertyBag();
+
+ // mandatory information
+ $propertyBag->set('UID', $this->uniqueId);
+
+ $propertyBag->add(new DateTimeProperty('DTSTART', $this->dtStart, $this->noTime, $this->useTimezone, $this->useUtc));
+ $propertyBag->set('SEQUENCE', $this->sequence);
+ $propertyBag->set('TRANSP', $this->transparency);
+
+ if ($this->status) {
+ $propertyBag->set('STATUS', $this->status);
+ }
+
+ // An event can have a 'dtend' or 'duration', but not both.
+ if (null != $this->dtEnd) {
+ $propertyBag->add(new DateTimeProperty('DTEND', $this->dtEnd, $this->noTime, $this->useTimezone, $this->useUtc));
+ } elseif (null != $this->duration) {
+ $propertyBag->set('DURATION', $this->duration->format('P%dDT%hH%iM%sS'));
+ }
+
+ // optional information
+ if (null != $this->url) {
+ $propertyBag->set('URL', $this->url);
+ }
+
+ if (null != $this->location) {
+ $propertyBag->set('LOCATION', $this->location);
+
+ if (null != $this->locationGeo) {
+ $propertyBag->add(
+ new Property(
+ 'X-APPLE-STRUCTURED-LOCATION',
+ 'geo:' . $this->locationGeo,
+ array(
+ 'VALUE' => 'URI',
+ 'X-ADDRESS' => $this->location,
+ 'X-APPLE-RADIUS' => 49,
+ 'X-TITLE' => $this->locationTitle,
+ )
+ )
+ );
+ }
+ }
+
+ if (null != $this->summary) {
+ $propertyBag->set('SUMMARY', $this->summary);
+ }
+
+ if (null != $this->attendees) {
+ $propertyBag->add($this->attendees);
+ }
+
+ $propertyBag->set('CLASS', $this->isPrivate ? 'PRIVATE' : 'PUBLIC');
+
+ if (null != $this->description) {
+ $propertyBag->set('DESCRIPTION', $this->description);
+ }
+
+ if (null != $this->recurrenceRule) {
+ $propertyBag->set('RRULE', $this->recurrenceRule);
+ }
+
+ if ($this->cancelled) {
+ $propertyBag->set('STATUS', 'CANCELLED');
+ }
+
+ if (null != $this->organizer) {
+ $propertyBag->add($this->organizer);
+ }
+
+ if ($this->noTime) {
+ $propertyBag->set('X-MICROSOFT-CDO-ALLDAYEVENT', 'TRUE');
+ }
+
+ if (null != $this->categories) {
+ $propertyBag->set('CATEGORIES', $this->categories);
+ }
+
+ $propertyBag->add(
+ new DateTimeProperty('DTSTAMP', $this->dtStamp ?: new \DateTime(), false, false, true)
+ );
+
+ if ($this->created) {
+ $propertyBag->add(new DateTimeProperty('CREATED', $this->created, false, false, true));
+ }
+
+ if ($this->modified) {
+ $propertyBag->add(new DateTimeProperty('LAST-MODIFIED', $this->modified, false, false, true));
+ }
+
+ return $propertyBag;
+ }
+
+ /**
+ * @param $dtEnd
+ *
+ * @return $this
+ */
+ public function setDtEnd($dtEnd)
+ {
+ $this->dtEnd = $dtEnd;
+
+ return $this;
+ }
+
+ public function getDtEnd()
+ {
+ return $this->dtEnd;
+ }
+
+ public function setDtStart($dtStart)
+ {
+ $this->dtStart = $dtStart;
+
+ return $this;
+ }
+
+ /**
+ * @param $dtStamp
+ *
+ * @return $this
+ */
+ public function setDtStamp($dtStamp)
+ {
+ $this->dtStamp = $dtStamp;
+
+ return $this;
+ }
+
+ /**
+ * @param $duration
+ *
+ * @return $this
+ */
+ public function setDuration($duration)
+ {
+ $this->duration = $duration;
+
+ return $this;
+ }
+
+ /**
+ * @param $location
+ * @param string $title
+ * @param null $geo
+ *
+ * @return $this
+ */
+ public function setLocation($location, $title = '', $geo = null)
+ {
+ $this->location = $location;
+ $this->locationTitle = $title;
+ $this->locationGeo = $geo;
+
+ return $this;
+ }
+
+ /**
+ * @param $noTime
+ *
+ * @return $this
+ */
+ public function setNoTime($noTime)
+ {
+ $this->noTime = $noTime;
+
+ return $this;
+ }
+
+ /**
+ * @param int $sequence
+ *
+ * @return $this
+ */
+ public function setSequence($sequence)
+ {
+ $this->sequence = $sequence;
+
+ return $this;
+ }
+
+ /**
+ * @return int
+ */
+ public function getSequence()
+ {
+ return $this->sequence;
+ }
+
+ /**
+ * @param string $name
+ * @param string $email
+ * @return $this
+ */
+ public function setOrganizer($name, $email = '')
+ {
+ $this->organizer = new Organizer($name, $email);
+
+ return $this;
+ }
+
+ /**
+ * @param $summary
+ *
+ * @return $this
+ */
+ public function setSummary($summary)
+ {
+ $this->summary = $summary;
+
+ return $this;
+ }
+
+ /**
+ * @param $uniqueId
+ *
+ * @return $this
+ */
+ public function setUniqueId($uniqueId)
+ {
+ $this->uniqueId = $uniqueId;
+
+ return $this;
+ }
+
+ /**
+ * @return string
+ */
+ public function getUniqueId()
+ {
+ return $this->uniqueId;
+ }
+
+ /**
+ * @param $url
+ *
+ * @return $this
+ */
+ public function setUrl($url)
+ {
+ $this->url = $url;
+
+ return $this;
+ }
+
+ /**
+ * @param $useTimezone
+ *
+ * @return $this
+ */
+ public function setUseTimezone($useTimezone)
+ {
+ $this->useTimezone = $useTimezone;
+
+ return $this;
+ }
+
+ /**
+ * @return bool
+ */
+ public function getUseTimezone()
+ {
+ return $this->useTimezone;
+ }
+
+ /**
+ * @param Attendees $attendees
+ *
+ * @return $this
+ */
+ public function setAttendees(Attendees $attendees)
+ {
+ $this->attendees = $attendees;
+
+ return $this;
+ }
+
+ /**
+ * @param string $attendee
+ * @param array $params
+ *
+ * @return $this
+ */
+ public function addAttendee($attendee, $params = array())
+ {
+ if (!isset($this->attendees)) {
+ $this->attendees = new Attendees();
+ }
+ $this->attendees->add($attendee, $params);
+
+ return $this;
+ }
+
+ /**
+ * @return Attendees
+ */
+ public function getAttendees()
+ {
+ return $this->attendees;
+ }
+
+ /**
+ * @param $description
+ *
+ * @return $this
+ */
+ public function setDescription($description)
+ {
+ $this->description = $description;
+
+ return $this;
+ }
+
+ /**
+ * @param bool $useUtc
+ *
+ * @return $this
+ */
+ public function setUseUtc($useUtc = true)
+ {
+ $this->useUtc = $useUtc;
+
+ return $this;
+ }
+
+ /**
+ * @return string
+ */
+ public function getDescription()
+ {
+ return $this->description;
+ }
+
+ /**
+ * @param $status
+ *
+ * @return $this
+ */
+ public function setCancelled($status)
+ {
+ $this->cancelled = (bool) $status;
+
+ return $this;
+ }
+
+ /**
+ * @param $transparency
+ *
+ * @return $this
+ *
+ * @throws \InvalidArgumentException
+ */
+ public function setTimeTransparency($transparency)
+ {
+ $transparency = strtoupper($transparency);
+ if ($transparency === self::TIME_TRANSPARENCY_OPAQUE
+ || $transparency === self::TIME_TRANSPARENCY_TRANSPARENT
+ ) {
+ $this->transparency = $transparency;
+ } else {
+ throw new \InvalidArgumentException('Invalid value for transparancy');
+ }
+
+ return $this;
+ }
+
+ /**
+ * @param $status
+ *
+ * @return $this
+ *
+ * @throws \InvalidArgumentException
+ */
+ public function setStatus($status)
+ {
+ $status = strtoupper($status);
+ if ($status == self::STATUS_CANCELLED
+ || $status == self::STATUS_CONFIRMED
+ || $status == self::STATUS_TENTATIVE
+ ) {
+ $this->status = $status;
+ } else {
+ throw new \InvalidArgumentException('Invalid value for status');
+ }
+
+ return $this;
+ }
+
+ /**
+ * @param RecurrenceRule $recurrenceRule
+ *
+ * @return $this
+ */
+ public function setRecurrenceRule(RecurrenceRule $recurrenceRule)
+ {
+ $this->recurrenceRule = $recurrenceRule;
+
+ return $this;
+ }
+
+ /**
+ * @return RecurrenceRule
+ */
+ public function getRecurrenceRule()
+ {
+ return $this->recurrenceRule;
+ }
+
+ /**
+ * @param $dtStamp
+ *
+ * @return $this
+ */
+ public function setCreated($dtStamp)
+ {
+ $this->created = $dtStamp;
+
+ return $this;
+ }
+
+ /**
+ * @param $dtStamp
+ *
+ * @return $this
+ */
+ public function setModified($dtStamp)
+ {
+ $this->modified = $dtStamp;
+
+ return $this;
+ }
+
+ /**
+ * @param $categories
+ *
+ * @return $this
+ */
+ public function setCategories($categories)
+ {
+ $this->categories = $categories;
+
+ return $this;
+ }
+
+ /**
+ * Sets the event privacy
+ *
+ * @param bool $flag
+ * @return $this
+ */
+ public function setIsPrivate($flag)
+ {
+ $this->isPrivate = (bool) $flag;
+
+ return $this;
+ }
+}
diff --git a/sources/vendor/eluceo/ical/src/Eluceo/iCal/Component/Timezone.php b/sources/vendor/eluceo/ical/src/Eluceo/iCal/Component/Timezone.php
new file mode 100644
index 0000000..4d73dc2
--- /dev/null
+++ b/sources/vendor/eluceo/ical/src/Eluceo/iCal/Component/Timezone.php
@@ -0,0 +1,57 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Eluceo\iCal\Component;
+
+use Eluceo\iCal\Component;
+use Eluceo\iCal\PropertyBag;
+
+/**
+ * Implementation of the TIMEZONE component.
+ */
+class Timezone extends Component
+{
+ /**
+ * @var string
+ */
+ protected $timezone;
+
+ public function __construct($timezone)
+ {
+ $this->timezone = $timezone;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getType()
+ {
+ return 'VTIMEZONE';
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function buildPropertyBag()
+ {
+ $propertyBag = new PropertyBag();
+
+ $propertyBag->set('TZID', $this->timezone);
+ $propertyBag->set('X-LIC-LOCATION', $this->timezone);
+
+ return $propertyBag;
+ }
+
+ public function getZoneIdentifier()
+ {
+ return $this->timezone;
+ }
+}
diff --git a/sources/vendor/eluceo/ical/src/Eluceo/iCal/Component/TimezoneRule.php b/sources/vendor/eluceo/ical/src/Eluceo/iCal/Component/TimezoneRule.php
new file mode 100644
index 0000000..ebd3680
--- /dev/null
+++ b/sources/vendor/eluceo/ical/src/Eluceo/iCal/Component/TimezoneRule.php
@@ -0,0 +1,213 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Eluceo\iCal\Component;
+
+use Eluceo\iCal\Component;
+use Eluceo\iCal\PropertyBag;
+use Eluceo\iCal\Property\Event\RecurrenceRule;
+
+/**
+ * Implementation of Standard Time and Daylight Saving Time observances (or rules)
+ * which define the TIMEZONE component.
+ */
+class TimezoneRule extends Component
+{
+ const TYPE_DAYLIGHT = 'DAYLIGHT';
+ const TYPE_STANDARD = 'STANDARD';
+
+ /**
+ * @var string
+ */
+ protected $type;
+
+ /**
+ * @var string
+ */
+ protected $tzOffsetFrom;
+
+ /**
+ * @var string
+ */
+ protected $tzOffsetTo;
+
+ /**
+ * @var string
+ */
+ protected $tzName;
+
+ /**
+ * @var \DateTime
+ */
+ protected $dtStart;
+
+ /**
+ * @var RecurrenceRule
+ */
+ protected $recurrenceRule;
+
+ /**
+ * create new Timezone Rule object by giving a rule type identifier.
+ *
+ * @param string $ruleType one of DAYLIGHT or STANDARD
+ *
+ * @throws \InvalidArgumentException
+ */
+ public function __construct($ruleType)
+ {
+ $ruleType = strtoupper($ruleType);
+ if ($ruleType === self::TYPE_DAYLIGHT || $ruleType === self::TYPE_STANDARD) {
+ $this->type = $ruleType;
+ } else {
+ throw new \InvalidArgumentException('Invalid value for timezone rule type');
+ }
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function buildPropertyBag()
+ {
+ $propertyBag = new PropertyBag();
+
+ if (null != $this->getTzName()) {
+ $propertyBag->set('TZNAME', $this->getTzName());
+ }
+
+ if (null != $this->getTzOffsetFrom()) {
+ $propertyBag->set('TZOFFSETFROM', $this->getTzOffsetFrom());
+ }
+
+ if (null != $this->getTzOffsetTo()) {
+ $propertyBag->set('TZOFFSETTO', $this->getTzOffsetTo());
+ }
+
+ if (null != $this->getDtStart()) {
+ $propertyBag->set('DTSTART', $this->getDtStart());
+ }
+
+ if (null != $this->recurrenceRule) {
+ $propertyBag->set('RRULE', $this->recurrenceRule);
+ }
+
+ return $propertyBag;
+ }
+
+ /**
+ * @param $offset
+ *
+ * @return $this
+ */
+ public function setTzOffsetFrom($offset)
+ {
+ $this->tzOffsetFrom = $offset;
+
+ return $this;
+ }
+
+ /**
+ * @param $offset
+ *
+ * @return $this
+ */
+ public function setTzOffsetTo($offset)
+ {
+ $this->tzOffsetTo = $offset;
+
+ return $this;
+ }
+
+ /**
+ * @param $name
+ *
+ * @return $this
+ */
+ public function setTzName($name)
+ {
+ $this->tzName = $name;
+
+ return $this;
+ }
+
+ /**
+ * @param \DateTime $dtStart
+ *
+ * @return $this
+ */
+ public function setDtStart(\DateTime $dtStart)
+ {
+ $this->dtStart = $dtStart;
+
+ return $this;
+ }
+
+ /**
+ * @param RecurrenceRule $recurrenceRule
+ *
+ * @return $this
+ */
+ public function setRecurrenceRule(RecurrenceRule $recurrenceRule)
+ {
+ $this->recurrenceRule = $recurrenceRule;
+
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getType()
+ {
+ return $this->type;
+ }
+
+ /**
+ * @return string
+ */
+ public function getTzOffsetFrom()
+ {
+ return $this->tzOffsetFrom;
+ }
+
+ /**
+ * @return string
+ */
+ public function getTzOffsetTo()
+ {
+ return $this->tzOffsetTo;
+ }
+
+ /**
+ * @return string
+ */
+ public function getTzName()
+ {
+ return $this->tzName;
+ }
+
+ /**
+ * @return RecurrenceRule
+ */
+ public function getRecurrenceRule()
+ {
+ return $this->recurrenceRule;
+ }
+
+ /**
+ * @return mixed return string representation of start date or NULL if no date was given
+ */
+ public function getDtStart()
+ {
+ if ($this->dtStart) {
+ return $this->dtStart->format('Ymd\THis');
+ }
+ }
+}
diff --git a/sources/vendor/eluceo/ical/src/Eluceo/iCal/ParameterBag.php b/sources/vendor/eluceo/ical/src/Eluceo/iCal/ParameterBag.php
new file mode 100644
index 0000000..a7ef58b
--- /dev/null
+++ b/sources/vendor/eluceo/ical/src/Eluceo/iCal/ParameterBag.php
@@ -0,0 +1,99 @@
+params = $params;
+ }
+
+ /**
+ * @param string $name
+ * @param mixed $value
+ */
+ public function setParam($name, $value)
+ {
+ $this->params[$name] = $value;
+ }
+
+ /**
+ * @param $name
+ */
+ public function getParam($name)
+ {
+ if (array_key_exists($name, $this->params)) {
+ return $this->params[$name];
+ }
+ }
+
+ /**
+ * Checks if there are any params.
+ *
+ * @return bool
+ */
+ public function hasParams()
+ {
+ return count($this->params) > 0;
+ }
+
+ /**
+ * @return string
+ */
+ public function toString()
+ {
+ $line = '';
+ foreach ($this->params as $param => $paramValues) {
+ if (!is_array($paramValues)) {
+ $paramValues = array($paramValues);
+ }
+ foreach ($paramValues as $k => $v) {
+ $paramValues[$k] = $this->escapeParamValue($v);
+ }
+
+ if ('' != $line) {
+ $line .= ';';
+ }
+
+ $line .= $param . '=' . implode(',', $paramValues);
+ }
+
+ return $line;
+ }
+
+ /**
+ * Returns an escaped string for a param value.
+ *
+ * @param string $value
+ *
+ * @return string
+ */
+ public function escapeParamValue($value)
+ {
+ $count = 0;
+ $value = str_replace('\\', '\\\\', $value);
+ $value = str_replace('"', '\"', $value, $count);
+ $value = str_replace("\n", '\\n', $value);
+ if (false !== strpos($value, ';') || false !== strpos($value, ',') || false !== strpos($value, ':') || $count) {
+ $value = '"' . $value . '"';
+ }
+
+ return $value;
+ }
+
+ /**
+ * @return string
+ */
+ public function __toString()
+ {
+ return $this->toString();
+ }
+}
diff --git a/sources/vendor/eluceo/ical/src/Eluceo/iCal/Property.php b/sources/vendor/eluceo/ical/src/Eluceo/iCal/Property.php
new file mode 100644
index 0000000..2514568
--- /dev/null
+++ b/sources/vendor/eluceo/ical/src/Eluceo/iCal/Property.php
@@ -0,0 +1,149 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Eluceo\iCal;
+
+use Eluceo\iCal\Property\ArrayValue;
+use Eluceo\iCal\Property\StringValue;
+use Eluceo\iCal\Property\ValueInterface;
+
+/**
+ * The Property Class represents a property as defined in RFC 2445.
+ *
+ * The content of a line (unfolded) will be rendered in this class
+ */
+class Property
+{
+ /**
+ * The value of the Property.
+ *
+ * @var ValueInterface
+ */
+ protected $value;
+
+ /**
+ * The params of the Property.
+ *
+ * @var ParameterBag
+ */
+ protected $parameterBag;
+
+ /**
+ * @var string
+ */
+ protected $name;
+
+ /**
+ * @param $name
+ * @param $value
+ * @param array $params
+ *
+ * @return \Eluceo\iCal\Property
+ */
+ public function __construct($name, $value, $params = array())
+ {
+ $this->name = $name;
+ $this->setValue($value);
+ $this->parameterBag = new ParameterBag($params);
+ }
+
+ /**
+ * Renders an unfolded line.
+ *
+ * @return string
+ */
+ public function toLine()
+ {
+ // Property-name
+ $line = $this->getName();
+
+ // Adding params
+ if ($this->parameterBag->hasParams()) {
+ $line .= ';' . $this->parameterBag->toString();
+ }
+
+ // Property value
+ $line .= ':' . $this->value->getEscapedValue();
+
+ return $line;
+ }
+
+ /**
+ * Get all unfolded lines.
+ *
+ * @return array
+ */
+ public function toLines()
+ {
+ return array($this->toLine());
+ }
+
+ /**
+ * @param string $name
+ * @param mixed $value
+ *
+ * @return $this
+ */
+ public function setParam($name, $value)
+ {
+ $this->parameterBag->setParam($name, $value);
+
+ return $this;
+ }
+
+ /**
+ * @param $name
+ */
+ public function getParam($name)
+ {
+ return $this->parameterBag->getParam($name);
+ }
+
+ /**
+ * @param mixed $value
+ *
+ * @return $this
+ *
+ * @throws \Exception
+ */
+ public function setValue($value)
+ {
+ if (is_scalar($value)) {
+ $this->value = new StringValue($value);
+ } elseif (is_array($value)) {
+ $this->value = new ArrayValue($value);
+ } else {
+ if (!$value instanceof ValueInterface) {
+ throw new \Exception("The value must implement the ValueInterface.");
+ } else {
+ $this->value = $value;
+ }
+ }
+
+ return $this;
+ }
+
+ /**
+ * @return mixed
+ */
+ public function getValue()
+ {
+ return $this->value;
+ }
+
+ /**
+ * @return string
+ */
+ public function getName()
+ {
+ return $this->name;
+ }
+}
diff --git a/sources/vendor/eluceo/ical/src/Eluceo/iCal/Property/ArrayValue.php b/sources/vendor/eluceo/ical/src/Eluceo/iCal/Property/ArrayValue.php
new file mode 100644
index 0000000..2ca71e5
--- /dev/null
+++ b/sources/vendor/eluceo/ical/src/Eluceo/iCal/Property/ArrayValue.php
@@ -0,0 +1,34 @@
+values = $values;
+ }
+
+ public function setValues(array $values)
+ {
+ $this->values = $values;
+
+ return $this;
+ }
+
+ public function getEscapedValue()
+ {
+ return implode(',', array_map(function ($value) {
+ return PropertyValueUtil::escapeValue((string) $value);
+ }, $this->values));
+ }
+}
diff --git a/sources/vendor/eluceo/ical/src/Eluceo/iCal/Property/DateTimeProperty.php b/sources/vendor/eluceo/ical/src/Eluceo/iCal/Property/DateTimeProperty.php
new file mode 100644
index 0000000..33f1748
--- /dev/null
+++ b/sources/vendor/eluceo/ical/src/Eluceo/iCal/Property/DateTimeProperty.php
@@ -0,0 +1,76 @@
+getDateString($dateTime, $noTime, $useTimezone, $useUtc);
+ $params = array();
+
+ if ($useTimezone) {
+ $timeZone = $dateTime->getTimezone()->getName();
+ $params['TZID'] = $timeZone;
+ }
+
+ if ($noTime) {
+ $params['VALUE'] = 'DATE';
+ }
+
+ parent::__construct($name, $dateString, $params);
+ }
+
+ /**
+ * Returns a formatted date string.
+ *
+ * @param \DateTime|null $dateTime The DateTime object
+ * @param bool $noTime Indicates if the time will be added
+ * @param bool $useTimezone
+ * @param bool $useUtc
+ *
+ * @return mixed
+ */
+ private function getDateString(\DateTime $dateTime = null, $noTime = false, $useTimezone = false, $useUtc = false)
+ {
+ if (empty($dateTime)) {
+ $dateTime = new \DateTime();
+ }
+
+ return $dateTime->format($this->getDateFormat($noTime, $useTimezone, $useUtc));
+ }
+
+ /**
+ * Returns the date format that can be passed to DateTime::format().
+ *
+ * @param bool $noTime Indicates if the time will be added
+ * @param bool $useTimezone
+ * @param bool $useUtc
+ *
+ * @return string
+ */
+ private function getDateFormat($noTime = false, $useTimezone = false, $useUtc = false)
+ {
+ // Do not use UTC time (Z) if timezone support is enabled.
+ if ($useTimezone || !$useUtc) {
+ return $noTime ? 'Ymd' : 'Ymd\THis';
+ }
+
+ return $noTime ? 'Ymd' : 'Ymd\THis\Z';
+ }
+}
diff --git a/sources/vendor/eluceo/ical/src/Eluceo/iCal/Property/Event/Attendees.php b/sources/vendor/eluceo/ical/src/Eluceo/iCal/Property/Event/Attendees.php
new file mode 100644
index 0000000..3392ad3
--- /dev/null
+++ b/sources/vendor/eluceo/ical/src/Eluceo/iCal/Property/Event/Attendees.php
@@ -0,0 +1,93 @@
+attendees[] = new Property('ATTENDEE', $value, $params);
+
+ return $this;
+ }
+
+ /**
+ * @param Property[] $value
+ *
+ * @return $this
+ */
+ public function setValue($value)
+ {
+ $this->attendees = $value;
+
+ return $this;
+ }
+
+ /**
+ * @return Property[]
+ */
+ public function getValue()
+ {
+ return $this->attendees;
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function toLines()
+ {
+ $lines = array();
+ foreach ($this->attendees as $attendee) {
+ $lines[] = $attendee->toLine();
+ }
+
+ return $lines;
+ }
+
+ /**
+ * @param string $name
+ * @param mixed $value
+ *
+ * @throws \BadMethodCallException
+ */
+ public function setParam($name, $value)
+ {
+ throw new \BadMethodCallException('Cannot call setParam on Attendees Property');
+ }
+
+ /**
+ * @param $name
+ *
+ * @throws \BadMethodCallException
+ */
+ public function getParam($name)
+ {
+ throw new \BadMethodCallException('Cannot call getParam on Attendees Property');
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function getName()
+ {
+ return self::PROPERTY_NAME;
+ }
+}
diff --git a/sources/vendor/eluceo/ical/src/Eluceo/iCal/Property/Event/Organizer.php b/sources/vendor/eluceo/ical/src/Eluceo/iCal/Property/Event/Organizer.php
new file mode 100644
index 0000000..22a91dc
--- /dev/null
+++ b/sources/vendor/eluceo/ical/src/Eluceo/iCal/Property/Event/Organizer.php
@@ -0,0 +1,29 @@
+ $name) : array();
+ $email = !$email ?: sprintf('MAILTO:%s', $email);
+
+ return parent::__construct($this->getName(), $email, $name);
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function getName()
+ {
+ return self::PROPERTY_NAME;
+ }
+}
diff --git a/sources/vendor/eluceo/ical/src/Eluceo/iCal/Property/Event/RecurrenceRule.php b/sources/vendor/eluceo/ical/src/Eluceo/iCal/Property/Event/RecurrenceRule.php
new file mode 100644
index 0000000..e4467fd
--- /dev/null
+++ b/sources/vendor/eluceo/ical/src/Eluceo/iCal/Property/Event/RecurrenceRule.php
@@ -0,0 +1,436 @@
+buildParameterBag()->toString();
+ }
+
+ /**
+ * @return ParameterBag
+ */
+ protected function buildParameterBag()
+ {
+ $parameterBag = new ParameterBag();
+
+ $parameterBag->setParam('FREQ', $this->freq);
+
+ if (null !== $this->interval) {
+ $parameterBag->setParam('INTERVAL', $this->interval);
+ }
+
+ if (null !== $this->count) {
+ $parameterBag->setParam('COUNT', $this->count);
+ }
+
+ if (null != $this->until) {
+ $parameterBag->setParam('UNTIL', $this->until->format('Ymd\THis\Z'));
+ }
+
+ if (null !== $this->wkst) {
+ $parameterBag->setParam('WKST', $this->wkst);
+ }
+
+ if (null !== $this->byMonth) {
+ $parameterBag->setParam('BYMONTH', $this->byMonth);
+ }
+
+ if (null !== $this->byWeekNo) {
+ $parameterBag->setParam('BYWEEKNO', $this->byWeekNo);
+ }
+
+ if (null !== $this->byYearDay) {
+ $parameterBag->setParam('BYYEARDAY', $this->byYearDay);
+ }
+
+ if (null !== $this->byMonthDay) {
+ $parameterBag->setParam('BYMONTHDAY', $this->byMonthDay);
+ }
+
+ if (null !== $this->byDay) {
+ $parameterBag->setParam('BYDAY', $this->byDay);
+ }
+
+ if (null !== $this->byHour) {
+ $parameterBag->setParam('BYHOUR', $this->byHour);
+ }
+
+ if (null !== $this->byMinute) {
+ $parameterBag->setParam('BYMINUTE', $this->byMinute);
+ }
+
+ if (null !== $this->bySecond) {
+ $parameterBag->setParam('BYSECOND', $this->bySecond);
+ }
+
+ return $parameterBag;
+ }
+
+ /**
+ * @param int|null $count
+ *
+ * @return $this
+ */
+ public function setCount($count)
+ {
+ $this->count = $count;
+
+ return $this;
+ }
+
+ /**
+ * @return int|null
+ */
+ public function getCount()
+ {
+ return $this->count;
+ }
+
+ /**
+ * @param \DateTime|null $count
+ *
+ * @return $this
+ */
+ public function setUntil(\DateTime $until = null)
+ {
+ $this->until = $until;
+
+ return $this;
+ }
+
+ /**
+ * @return \DateTime|null
+ */
+ public function getUntil()
+ {
+ return $this->until;
+ }
+
+ /**
+ * The FREQ rule part identifies the type of recurrence rule. This
+ * rule part MUST be specified in the recurrence rule. Valid values
+ * include.
+ *
+ * SECONDLY, to specify repeating events based on an interval of a second or more;
+ * MINUTELY, to specify repeating events based on an interval of a minute or more;
+ * HOURLY, to specify repeating events based on an interval of an hour or more;
+ * DAILY, to specify repeating events based on an interval of a day or more;
+ * WEEKLY, to specify repeating events based on an interval of a week or more;
+ * MONTHLY, to specify repeating events based on an interval of a month or more;
+ * YEARLY, to specify repeating events based on an interval of a year or more.
+ *
+ * @param string $freq
+ *
+ * @return $this
+ *
+ * @throws \InvalidArgumentException
+ */
+ public function setFreq($freq)
+ {
+ if (self::FREQ_YEARLY === $freq || self::FREQ_MONTHLY === $freq
+ || self::FREQ_WEEKLY === $freq
+ || self::FREQ_DAILY === $freq
+ ) {
+ $this->freq = $freq;
+ } else {
+ throw new \InvalidArgumentException("The Frequency {$freq} is not supported.");
+ }
+
+ return $this;
+ }
+
+ /**
+ * @return string
+ */
+ public function getFreq()
+ {
+ return $this->freq;
+ }
+
+ /**
+ * The INTERVAL rule part contains a positive integer representing at
+ * which intervals the recurrence rule repeats.
+ *
+ * @param int|null $interval
+ *
+ * @return $this
+ */
+ public function setInterval($interval)
+ {
+ $this->interval = $interval;
+
+ return $this;
+ }
+
+ /**
+ * @return int|null
+ */
+ public function getInterval()
+ {
+ return $this->interval;
+ }
+
+ /**
+ * The WKST rule part specifies the day on which the workweek starts.
+ * Valid values are MO, TU, WE, TH, FR, SA, and SU.
+ *
+ * @param integer $value
+ *
+ * @return $this
+ */
+ public function setWkst($value)
+ {
+ $this->wkst = $value;
+
+ return $this;
+ }
+
+ /**
+ * The BYMONTH rule part specifies a COMMA-separated list of months of the year.
+ * Valid values are 1 to 12.
+ *
+ * @param integer $month
+ *
+ * @throws InvalidArgumentException
+ *
+ * @return $this
+ */
+ public function setByMonth($month)
+ {
+ if (!is_integer($month) || $month < 0 || $month > 12) {
+ throw new InvalidArgumentException('Invalid value for BYMONTH');
+ }
+
+ $this->byMonth = $month;
+
+ return $this;
+ }
+
+ /**
+ * The BYWEEKNO rule part specifies a COMMA-separated list of ordinals specifying weeks of the year.
+ * Valid values are 1 to 53 or -53 to -1.
+ *
+ * @param integer $value
+ *
+ * @return $this
+ */
+ public function setByWeekNo($value)
+ {
+ $this->byWeekNo = $value;
+
+ return $this;
+ }
+
+ /**
+ * The BYYEARDAY rule part specifies a COMMA-separated list of days of the year.
+ * Valid values are 1 to 366 or -366 to -1.
+ *
+ * @param integer $day
+ *
+ * @return $this
+ */
+ public function setByYearDay($day)
+ {
+ $this->byYearDay = $day;
+
+ return $this;
+ }
+
+ /**
+ * The BYMONTHDAY rule part specifies a COMMA-separated list of days of the month.
+ * Valid values are 1 to 31 or -31 to -1.
+ *
+ * @param integer $day
+ *
+ * @return $this
+ */
+ public function setByMonthDay($day)
+ {
+ $this->byMonthDay = $day;
+
+ return $this;
+ }
+
+ /**
+ * The BYDAY rule part specifies a COMMA-separated list of days of the week;.
+ *
+ * SU indicates Sunday; MO indicates Monday; TU indicates Tuesday;
+ * WE indicates Wednesday; TH indicates Thursday; FR indicates Friday; and SA indicates Saturday.
+ *
+ * Each BYDAY value can also be preceded by a positive (+n) or negative (-n) integer.
+ * If present, this indicates the nth occurrence of a specific day within the MONTHLY or YEARLY "RRULE".
+ *
+ * @param string $day
+ *
+ * @return $this
+ */
+ public function setByDay($day)
+ {
+ $this->byDay = $day;
+
+ return $this;
+ }
+
+ /**
+ * The BYHOUR rule part specifies a COMMA-separated list of hours of the day.
+ * Valid values are 0 to 23.
+ *
+ * @param integer $value
+ *
+ * @return $this
+ *
+ * @throws \InvalidArgumentException
+ */
+ public function setByHour($value)
+ {
+ if (!is_integer($value) || $value < 0 || $value > 23) {
+ throw new \InvalidArgumentException('Invalid value for BYHOUR');
+ }
+
+ $this->byHour = $value;
+
+ return $this;
+ }
+
+ /**
+ * The BYMINUTE rule part specifies a COMMA-separated list of minutes within an hour.
+ * Valid values are 0 to 59.
+ *
+ * @param integer $value
+ *
+ * @return $this
+ *
+ * @throws \InvalidArgumentException
+ */
+ public function setByMinute($value)
+ {
+ if (!is_integer($value) || $value < 0 || $value > 59) {
+ throw new \InvalidArgumentException('Invalid value for BYMINUTE');
+ }
+
+ $this->byMinute = $value;
+
+ return $this;
+ }
+
+ /**
+ * The BYSECOND rule part specifies a COMMA-separated list of seconds within a minute.
+ * Valid values are 0 to 60.
+ *
+ * @param integer $value
+ *
+ * @return $this
+ *
+ * @throws \InvalidArgumentException
+ */
+ public function setBySecond($value)
+ {
+ if (!is_integer($value) || $value < 0 || $value > 60) {
+ throw new \InvalidArgumentException('Invalid value for BYSECOND');
+ }
+
+ $this->bySecond = $value;
+
+ return $this;
+ }
+}
diff --git a/sources/vendor/eluceo/ical/src/Eluceo/iCal/Property/StringValue.php b/sources/vendor/eluceo/ical/src/Eluceo/iCal/Property/StringValue.php
new file mode 100644
index 0000000..4501237
--- /dev/null
+++ b/sources/vendor/eluceo/ical/src/Eluceo/iCal/Property/StringValue.php
@@ -0,0 +1,52 @@
+value = $value;
+ }
+
+ /**
+ * Return the value of the Property as an escaped string.
+ *
+ * Escape values as per RFC 2445. See http://www.kanzaki.com/docs/ical/text.html
+ *
+ * @return string
+ */
+ public function getEscapedValue()
+ {
+ return PropertyValueUtil::escapeValue((string) $this->value);
+ }
+
+ /**
+ * @param string $value
+ *
+ * @return $this
+ */
+ public function setValue($value)
+ {
+ $this->value = $value;
+
+ return $this;
+ }
+
+ /**
+ * @return string
+ */
+ public function getValue()
+ {
+ return $this->value;
+ }
+}
diff --git a/sources/vendor/eluceo/ical/src/Eluceo/iCal/Property/ValueInterface.php b/sources/vendor/eluceo/ical/src/Eluceo/iCal/Property/ValueInterface.php
new file mode 100644
index 0000000..f1563be
--- /dev/null
+++ b/sources/vendor/eluceo/ical/src/Eluceo/iCal/Property/ValueInterface.php
@@ -0,0 +1,15 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Eluceo\iCal;
+
+
+class PropertyBag implements \IteratorAggregate
+{
+ /**
+ * @var array
+ */
+ protected $elements = array();
+
+ /**
+ * Creates a new Property with $name, $value and $params.
+ *
+ * @param $name
+ * @param $value
+ * @param array $params
+ *
+ * @return $this
+ */
+ public function set($name, $value, $params = array())
+ {
+ $property = new Property($name, $value, $params);
+ $this->elements[] = $property;
+
+ return $this;
+ }
+
+ /**
+ * @param string $name
+ *
+ * @return null|Property
+ */
+ public function get($name)
+ {
+ // Searching Property in elements-array
+ /** @var $property Property */
+ foreach ($this->elements as $property) {
+ if ($property->getName() == $name) {
+ return $property;
+ }
+ }
+ }
+
+ /**
+ * Adds a Property. If Property already exists an Exception will be thrown.
+ *
+ * @param Property $property
+ *
+ * @return $this
+ *
+ * @throws \Exception
+ */
+ public function add(Property $property)
+ {
+ // Property already exists?
+ if (null !== $this->get($property->getName())) {
+ throw new \Exception("Property with name '{$property->getName()}' already exists");
+ }
+
+ $this->elements[] = $property;
+
+ return $this;
+ }
+
+ public function getIterator()
+ {
+ return new \ArrayObject($this->elements);
+ }
+}
diff --git a/sources/vendor/eluceo/ical/src/Eluceo/iCal/Util/ComponentUtil.php b/sources/vendor/eluceo/ical/src/Eluceo/iCal/Util/ComponentUtil.php
new file mode 100644
index 0000000..ecf9d75
--- /dev/null
+++ b/sources/vendor/eluceo/ical/src/Eluceo/iCal/Util/ComponentUtil.php
@@ -0,0 +1,40 @@
+ 75) {
+ $line = ' ' . $char;
+ ++$lineNo;
+ } else {
+ $line .= $char;
+ }
+ $lines[$lineNo] = $line;
+ }
+
+ return $lines;
+ }
+}
diff --git a/sources/vendor/eluceo/ical/src/Eluceo/iCal/Util/PropertyValueUtil.php b/sources/vendor/eluceo/ical/src/Eluceo/iCal/Util/PropertyValueUtil.php
new file mode 100644
index 0000000..b2c8766
--- /dev/null
+++ b/sources/vendor/eluceo/ical/src/Eluceo/iCal/Util/PropertyValueUtil.php
@@ -0,0 +1,25 @@
+
+ * @copyright 2014 Fabian Grutschus. All rights reserved.
+ * @license BSD
+ * @link http://github.com/fabiang/xmpp
+ */
+
+namespace Fabiang\Xmpp;
+
+use Fabiang\Xmpp\Options;
+use Fabiang\Xmpp\Connection\ConnectionInterface;
+use Fabiang\Xmpp\Connection\Socket;
+use Fabiang\Xmpp\Protocol\ProtocolImplementationInterface;
+use Fabiang\Xmpp\Event\EventManagerAwareInterface;
+use Fabiang\Xmpp\Event\EventManagerInterface;
+use Fabiang\Xmpp\Event\EventManager;
+use Fabiang\Xmpp\EventListener\Logger;
+
+/**
+ * Xmpp connection client.
+ *
+ * @package Xmpp
+ */
+class Client implements EventManagerAwareInterface
+{
+
+ /**
+ * Eventmanager.
+ *
+ * @var EventManagerInterface
+ */
+ protected $eventManager;
+
+ /**
+ * Options.
+ *
+ * @var Options
+ */
+ protected $options;
+
+ /**
+ * @var ConnectionInterface
+ */
+ protected $connection;
+
+ /**
+ * Constructor.
+ *
+ * @param Options $options Client options
+ * @param EventManagerInterface $eventManager Event manager
+ */
+ public function __construct(Options $options, EventManagerInterface $eventManager = null)
+ {
+ // create default connection
+ if (null !== $options->getConnection()) {
+ $connection = $options->getConnection();
+ } else {
+ $connection = Socket::factory($options);
+ $options->setConnection($connection);
+ }
+ $this->options = $options;
+ $this->connection = $connection;
+
+ if (null === $eventManager) {
+ $eventManager = new EventManager();
+ }
+ $this->eventManager = $eventManager;
+
+ $this->setupImplementation();
+ }
+
+ /**
+ * Setup implementation.
+ *
+ * @return void
+ */
+ protected function setupImplementation()
+ {
+ $this->connection->setEventManager($this->eventManager);
+ $this->connection->setOptions($this->options);
+
+ $implementation = $this->options->getImplementation();
+ $implementation->setEventManager($this->eventManager);
+ $implementation->setOptions($this->options);
+ $implementation->register();
+
+ $implementation->registerListener(new Logger());
+ }
+
+ /**
+ * Connect to server.
+ *
+ * @return void
+ */
+ public function connect()
+ {
+ $this->connection->connect();
+ }
+
+ /**
+ * Disconnect from server.
+ *
+ * @return void
+ */
+ public function disconnect()
+ {
+ $this->connection->disconnect();
+ }
+
+ /**
+ * Send data to server.
+ *
+ * @param ProtocolImplementationInterface $interface Interface
+ * @return void
+ */
+ public function send(ProtocolImplementationInterface $interface)
+ {
+ $data = $interface->toString();
+ $this->connection->send($data);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function getEventManager()
+ {
+ return $this->eventManager;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function setEventManager(EventManagerInterface $eventManager)
+ {
+ $this->eventManager = $eventManager;
+ return $this;
+ }
+
+ /**
+ * Get options.
+ *
+ * @return Options
+ */
+ public function getOptions()
+ {
+ return $this->options;
+ }
+
+ /**
+ * @return ConnectionInterface
+ */
+ public function getConnection()
+ {
+ return $this->connection;
+ }
+}
diff --git a/sources/vendor/fabiang/xmpp/src/Connection/AbstractConnection.php b/sources/vendor/fabiang/xmpp/src/Connection/AbstractConnection.php
new file mode 100644
index 0000000..30f79c4
--- /dev/null
+++ b/sources/vendor/fabiang/xmpp/src/Connection/AbstractConnection.php
@@ -0,0 +1,311 @@
+
+ * @copyright 2014 Fabian Grutschus. All rights reserved.
+ * @license BSD
+ * @link http://github.com/fabiang/xmpp
+ */
+
+namespace Fabiang\Xmpp\Connection;
+
+use Fabiang\Xmpp\Stream\XMLStream;
+use Fabiang\Xmpp\EventListener\EventListenerInterface;
+use Fabiang\Xmpp\Event\EventManager;
+use Fabiang\Xmpp\Event\EventManagerInterface;
+use Fabiang\Xmpp\EventListener\BlockingEventListenerInterface;
+use Fabiang\Xmpp\Options;
+use Fabiang\Xmpp\Exception\TimeoutException;
+use Psr\Log\LogLevel;
+
+/**
+ * Connection test double.
+ *
+ * @package Xmpp\Connection
+ */
+abstract class AbstractConnection implements ConnectionInterface
+{
+
+ /**
+ *
+ * @var XMLStream
+ */
+ protected $outputStream;
+
+ /**
+ *
+ * @var XMLStream
+ */
+ protected $inputStream;
+
+ /**
+ * Options.
+ *
+ * @var Options
+ */
+ protected $options;
+
+ /**
+ * Eventmanager.
+ *
+ * @var EventManagerInterface
+ */
+ protected $events;
+
+ /**
+ * Event listeners.
+ *
+ * @var EventListenerInterface[]
+ */
+ protected $listeners = array();
+
+ /**
+ * Connected.
+ *
+ * @var boolean
+ */
+ protected $connected = false;
+
+ /**
+ *
+ * @var boolean
+ */
+ protected $ready = false;
+
+ /**
+ * Timestamp of last response data received.
+ *
+ * @var integer
+ */
+ private $lastResponse;
+
+ /**
+ * Last blocking event listener.
+ *
+ * Cached to reduce debug output a bit.
+ *
+ * @var BlockingEventListenerInterface
+ */
+ private $lastBlockingListener;
+
+ /**
+ * {@inheritDoc}
+ */
+ public function getOutputStream()
+ {
+ if (null === $this->outputStream) {
+ $this->outputStream = new XMLStream();
+ }
+
+ return $this->outputStream;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function getInputStream()
+ {
+ if (null === $this->inputStream) {
+ $this->inputStream = new XMLStream();
+ }
+
+ return $this->inputStream;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function setOutputStream(XMLStream $outputStream)
+ {
+ $this->outputStream = $outputStream;
+ return $this;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function setInputStream(XMLStream $inputStream)
+ {
+ $this->inputStream = $inputStream;
+ return $this;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function addListener(EventListenerInterface $eventListener)
+ {
+ $this->listeners[] = $eventListener;
+ return $this;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function isConnected()
+ {
+ return $this->connected;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function isReady()
+ {
+ return $this->ready;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function setReady($flag)
+ {
+ $this->ready = (bool) $flag;
+ return $this;
+ }
+
+ /**
+ * Reset streams.
+ *
+ * @return void
+ */
+ public function resetStreams()
+ {
+ $this->getInputStream()->reset();
+ $this->getOutputStream()->reset();
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function getEventManager()
+ {
+ if (null === $this->events) {
+ $this->setEventManager(new EventManager());
+ }
+
+ return $this->events;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function setEventManager(EventManagerInterface $events)
+ {
+ $this->events = $events;
+ return $this;
+ }
+
+ /**
+ * Get listeners.
+ *
+ * @return EventListenerInterface
+ */
+ public function getListeners()
+ {
+ return $this->listeners;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function getOptions()
+ {
+ return $this->options;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function setOptions(Options $options)
+ {
+ $this->options = $options;
+ return $this;
+ }
+
+ /**
+ * Call logging event.
+ *
+ * @param string $message Log message
+ * @param integer $level Log level
+ * @return void
+ */
+ protected function log($message, $level = LogLevel::DEBUG)
+ {
+ $this->getEventManager()->trigger('logger', $this, array($message, $level));
+ }
+
+ /**
+ * Check blocking event listeners.
+ *
+ * @return boolean
+ */
+ protected function checkBlockingListeners()
+ {
+ $blocking = false;
+ foreach ($this->listeners as $listener) {
+ $instanceof = $listener instanceof BlockingEventListenerInterface;
+ if ($instanceof && true === $listener->isBlocking()) {
+ // cache the last blocking listener. Reducing output.
+ if ($this->lastBlockingListener !== $listener) {
+ $this->log('Listener "' . get_class($listener) . '" is currently blocking', LogLevel::DEBUG);
+ $this->lastBlockingListener = $listener;
+ }
+ $blocking = true;
+ }
+ }
+
+ return $blocking;
+ }
+
+ /**
+ * Check for timeout.
+ *
+ * @param string $buffer Function required current received buffer
+ * @throws TimeoutException
+ */
+ protected function checkTimeout($buffer)
+ {
+ if (!empty($buffer)) {
+ $this->lastResponse = time();
+ return;
+ }
+
+ if (null === $this->lastResponse) {
+ $this->lastResponse = time();
+ }
+
+ $timeout = $this->getOptions()->getTimeout();
+
+ if (time() >= $this->lastResponse + $timeout) {
+ throw new TimeoutException('Connection lost after ' . $timeout . ' seconds');
+ }
+ }
+}
diff --git a/sources/vendor/fabiang/xmpp/src/Connection/ConnectionInterface.php b/sources/vendor/fabiang/xmpp/src/Connection/ConnectionInterface.php
new file mode 100644
index 0000000..b9f6fc8
--- /dev/null
+++ b/sources/vendor/fabiang/xmpp/src/Connection/ConnectionInterface.php
@@ -0,0 +1,146 @@
+
+ * @copyright 2014 Fabian Grutschus. All rights reserved.
+ * @license BSD
+ * @link http://github.com/fabiang/xmpp
+ */
+
+namespace Fabiang\Xmpp\Connection;
+
+use Fabiang\Xmpp\Stream\XMLStream;
+use Fabiang\Xmpp\Event\EventManagerAwareInterface;
+use Fabiang\Xmpp\EventListener\EventListenerInterface;
+use Fabiang\Xmpp\OptionsAwareInterface;
+
+/**
+ * Connections must implement this interface.
+ *
+ * @package Xmpp\Connection
+ */
+interface ConnectionInterface extends EventManagerAwareInterface, OptionsAwareInterface
+{
+ /**
+ * Connect.
+ *
+ * @return void
+ */
+ public function connect();
+
+ /**
+ * Disconnect.
+ *
+ * @return void
+ */
+ public function disconnect();
+
+ /**
+ * Set stream is ready.
+ *
+ * @param boolean $flag Flag
+ * @return $this
+ */
+ public function setReady($flag);
+
+ /**
+ * Is stream ready.
+ *
+ * @return boolean
+ */
+ public function isReady();
+
+ /**
+ * Is connection established.
+ *
+ * @return boolean
+ */
+ public function isConnected();
+
+ /**
+ * Receive data.
+ *
+ * @return string
+ */
+ public function receive();
+
+ /**
+ * Send data.
+ *
+ * @param string $buffer Data to send.
+ * @return void
+ */
+ public function send($buffer);
+
+ /**
+ * Get output stream.
+ *
+ * @return XMLStream
+ */
+ public function getOutputStream();
+
+ /**
+ * Get input stream.
+ *
+ * @return XMLStream
+ */
+ public function getInputStream();
+
+ /**
+ * Set output stream.
+ *
+ * @param XMLStream $outputStream Output stream
+ * @return $this
+ */
+ public function setOutputStream(XMLStream $outputStream);
+
+ /**
+ * Set input stream.
+ *
+ * @param XMLStream $inputStream Input stream
+ * @return $this
+ */
+ public function setInputStream(XMLStream $inputStream);
+
+ /**
+ * Reset streams.
+ *
+ * @return void
+ */
+ public function resetStreams();
+
+ /**
+ * Add listener.
+ *
+ * @param EventListenerInterface $eventListener
+ * @return $this
+ */
+ public function addListener(EventListenerInterface $eventListener);
+}
diff --git a/sources/vendor/fabiang/xmpp/src/Connection/Socket.php b/sources/vendor/fabiang/xmpp/src/Connection/Socket.php
new file mode 100644
index 0000000..8ce49a7
--- /dev/null
+++ b/sources/vendor/fabiang/xmpp/src/Connection/Socket.php
@@ -0,0 +1,228 @@
+
+ * @copyright 2014 Fabian Grutschus. All rights reserved.
+ * @license BSD
+ * @link http://github.com/fabiang/xmpp
+ */
+
+namespace Fabiang\Xmpp\Connection;
+
+use Psr\Log\LogLevel;
+use Fabiang\Xmpp\Stream\SocketClient;
+use Fabiang\Xmpp\Util\XML;
+use Fabiang\Xmpp\Options;
+use Fabiang\Xmpp\Exception\TimeoutException;
+
+/**
+ * Connection to a socket stream.
+ *
+ * @package Xmpp\Connection
+ */
+class Socket extends AbstractConnection implements SocketConnectionInterface
+{
+
+ const DEFAULT_LENGTH = 4096;
+ const STREAM_START = <<<'XML'
+
+
+XML;
+ const STREAM_END = '';
+
+ /**
+ * Socket.
+ *
+ * @var SocketClient
+ */
+ protected $socket;
+
+ /**
+ * Did we received any data yet?
+ *
+ * @var bool
+ */
+ private $receivedAnyData = false;
+
+ /**
+ * Constructor set default socket instance if no socket was given.
+ *
+ * @param StreamSocket $socket Socket instance
+ */
+ public function __construct(SocketClient $socket)
+ {
+ $this->setSocket($socket);
+ }
+
+ /**
+ * Factory for connection class.
+ *
+ * @param Options $options Options object
+ * @return static
+ */
+ public static function factory(Options $options)
+ {
+ $socket = new SocketClient($options->getAddress());
+ $object = new static($socket);
+ $object->setOptions($options);
+ return $object;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function receive()
+ {
+ $buffer = $this->getSocket()->read(static::DEFAULT_LENGTH);
+
+ if ($buffer) {
+ $this->receivedAnyData = true;
+ $address = $this->getAddress();
+ $this->log("Received buffer '$buffer' from '{$address}'", LogLevel::DEBUG);
+ $this->getInputStream()->parse($buffer);
+ return $buffer;
+ }
+
+ try {
+ $this->checkTimeout($buffer);
+ } catch (TimeoutException $exception) {
+ $this->reconnectTls($exception);
+ }
+ }
+
+ /**
+ * Try to reconnect via TLS.
+ *
+ * @param TimeoutException $exception
+ * @return null
+ * @throws TimeoutException
+ */
+ private function reconnectTls(TimeoutException $exception)
+ {
+ // check if we didn't receive any data
+ // if not we re-try to connect via TLS
+ if (false === $this->receivedAnyData) {
+ $matches = array();
+ $previousAddress = $this->getOptions()->getAddress();
+ // only reconnect via tls if we've used tcp before.
+ if (preg_match('#tcp://(?.+)#', $previousAddress, $matches)) {
+ $this->log('Connecting via TCP failed, now trying to connect via TLS');
+
+ $address = 'tls://' . $matches['address'];
+ $this->connected = false;
+ $this->getOptions()->setAddress($address);
+ $this->getSocket()->reconnect($address);
+ $this->connect();
+ return;
+ }
+ }
+
+ throw $exception;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function send($buffer)
+ {
+ if (false === $this->isConnected()) {
+ $this->connect();
+ }
+
+ $address = $this->getAddress();
+ $this->log("Sending data '$buffer' to '{$address}'", LogLevel::DEBUG);
+ $this->getSocket()->write($buffer);
+ $this->getOutputStream()->parse($buffer);
+
+ while ($this->checkBlockingListeners()) {
+ $this->receive();
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function connect()
+ {
+ if (false === $this->connected) {
+ $address = $this->getAddress();
+ $this->getSocket()->connect($this->getOptions()->getTimeout());
+ $this->getSocket()->setBlocking(true);
+
+ $this->connected = true;
+ $this->log("Connected to '{$address}'", LogLevel::DEBUG);
+ }
+
+ $this->send(sprintf(static::STREAM_START, XML::quote($this->getOptions()->getTo())));
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function disconnect()
+ {
+ if (true === $this->connected) {
+ $address = $this->getAddress();
+ $this->send(static::STREAM_END);
+ $this->getSocket()->close();
+ $this->connected = false;
+ $this->log("Disconnected from '{$address}'", LogLevel::DEBUG);
+ }
+ }
+
+ /**
+ * Get address from options object.
+ *
+ * @return string
+ */
+ protected function getAddress()
+ {
+ return $this->getOptions()->getAddress();
+ }
+
+ /**
+ * Return socket instance.
+ *
+ * @return SocketClient
+ */
+ public function getSocket()
+ {
+ return $this->socket;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function setSocket(SocketClient $socket)
+ {
+ $this->socket = $socket;
+ return $this;
+ }
+}
diff --git a/sources/vendor/fabiang/xmpp/src/Connection/SocketConnectionInterface.php b/sources/vendor/fabiang/xmpp/src/Connection/SocketConnectionInterface.php
new file mode 100644
index 0000000..ea17ef8
--- /dev/null
+++ b/sources/vendor/fabiang/xmpp/src/Connection/SocketConnectionInterface.php
@@ -0,0 +1,56 @@
+
+ * @copyright 2014 Fabian Grutschus. All rights reserved.
+ * @license BSD
+ * @link http://github.com/fabiang/xmpp
+ */
+
+namespace Fabiang\Xmpp\Connection;
+
+use Fabiang\Xmpp\Stream\SocketClient;
+
+/**
+ * Interface for connection that connect to a socket.
+ *
+ * @package Xmpp\Connection
+ */
+interface SocketConnectionInterface
+{
+
+ /**
+ * Set socket instance.
+ *
+ * @param SocketClient $socket
+ * @return $this
+ */
+ public function setSocket(SocketClient $socket);
+}
diff --git a/sources/vendor/fabiang/xmpp/src/Event/Event.php b/sources/vendor/fabiang/xmpp/src/Event/Event.php
new file mode 100644
index 0000000..abce6f4
--- /dev/null
+++ b/sources/vendor/fabiang/xmpp/src/Event/Event.php
@@ -0,0 +1,165 @@
+
+ * @copyright 2014 Fabian Grutschus. All rights reserved.
+ * @license BSD
+ * @link http://github.com/fabiang/xmpp
+ */
+
+namespace Fabiang\Xmpp\Event;
+
+use Fabiang\Xmpp\Exception\OutOfRangeException;
+use Fabiang\Xmpp\Exception\InvalidArgumentException;
+
+/**
+ * Generic event.
+ *
+ * @package Xmpp\Event
+ */
+class Event implements EventInterface
+{
+
+ /**
+ * Event name.
+ *
+ * @var string
+ */
+ protected $name;
+
+ /**
+ * Target object.
+ *
+ * @var object
+ */
+ protected $target;
+
+ /**
+ * Event parameters.
+ *
+ * @var array
+ */
+ protected $parameters = array();
+
+ /**
+ * Event stack.
+ *
+ * @var array
+ */
+ protected $eventStack = array();
+
+ /**
+ * {@inheritDoc}
+ */
+ public function getName()
+ {
+ return $this->name;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function getTarget()
+ {
+ return $this->target;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function getParameters()
+ {
+ return $this->parameters;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function setName($name)
+ {
+ $this->name = (string) $name;
+ return $this;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function setTarget($target)
+ {
+ $this->target = $target;
+ return $this;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function setParameters(array $parameters)
+ {
+ $this->parameters = array_values($parameters);
+ return $this;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function getEventStack()
+ {
+ return $this->eventStack;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function setEventStack(array $eventStack)
+ {
+ $this->eventStack = $eventStack;
+ return $this;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function getParameter($index)
+ {
+ $parameters = $this->getParameters();
+
+ if (!is_int($index)) {
+ throw new InvalidArgumentException(
+ 'Argument #1 of "' . __CLASS__ . '::' . __METHOD__ . '" must be an integer'
+ );
+ }
+
+ if (!array_key_exists($index, $parameters)) {
+ throw new OutOfRangeException("The offset $index is out of range.");
+ }
+
+ return $parameters[$index];
+ }
+}
diff --git a/sources/vendor/fabiang/xmpp/src/Event/EventInterface.php b/sources/vendor/fabiang/xmpp/src/Event/EventInterface.php
new file mode 100644
index 0000000..85a711d
--- /dev/null
+++ b/sources/vendor/fabiang/xmpp/src/Event/EventInterface.php
@@ -0,0 +1,114 @@
+
+ * @copyright 2014 Fabian Grutschus. All rights reserved.
+ * @license BSD
+ * @link http://github.com/fabiang/xmpp
+ */
+
+namespace Fabiang\Xmpp\Event;
+
+/**
+ * Interface for events.
+ *
+ * @package Xmpp\Event
+ */
+interface EventInterface
+{
+
+ /**
+ * Get event name.
+ *
+ * @return string
+ */
+ public function getName();
+
+ /**
+ * Set event name.
+ *
+ * @param string $name Event name
+ * @return $this
+ */
+ public function setName($name);
+
+ /**
+ * Return calling object.
+ *
+ * @return object
+ */
+ public function getTarget();
+
+ /**
+ * Set calling object.
+ *
+ * @param object $target Calling object
+ * @return $this
+ */
+ public function setTarget($target);
+
+ /**
+ * Return parameters.
+ *
+ * @return array
+ */
+ public function getParameters();
+
+ /**
+ * Set parameters.
+ *
+ * @param array $parameters Parameters
+ * @return $this
+ */
+ public function setParameters(array $parameters);
+
+ /**
+ * Get a parameter by index.
+ *
+ * @param integer $index
+ * @retrun mixed
+ */
+ public function getParameter($index);
+
+ /**
+ * Get list of previous called callbacks.
+ *
+ * @return array
+ */
+ public function getEventStack();
+
+ /**
+ * Set event stack.
+ *
+ * @param array $stack Event stack
+ * @return $this
+ */
+ public function setEventStack(array $stack);
+}
diff --git a/sources/vendor/fabiang/xmpp/src/Event/EventManager.php b/sources/vendor/fabiang/xmpp/src/Event/EventManager.php
new file mode 100644
index 0000000..f8ea266
--- /dev/null
+++ b/sources/vendor/fabiang/xmpp/src/Event/EventManager.php
@@ -0,0 +1,160 @@
+
+ * @copyright 2014 Fabian Grutschus. All rights reserved.
+ * @license BSD
+ * @link http://github.com/fabiang/xmpp
+ */
+
+namespace Fabiang\Xmpp\Event;
+
+use Fabiang\Xmpp\Exception\InvalidArgumentException;
+
+/**
+ * Event manager.
+ *
+ * The EventManager holds and triggers events.
+ *
+ * @package Xmpp\Event
+ */
+class EventManager implements EventManagerInterface
+{
+
+ const WILDCARD = '*';
+
+ /**
+ * Attached events.
+ *
+ * @var array
+ */
+ protected $events = array(self::WILDCARD => array());
+
+ /**
+ * Event object.
+ *
+ * @var EventInterface
+ */
+ protected $eventObject;
+
+ /**
+ * Constructor sets default event object.
+ *
+ * @param EventInterface $eventObject Event object
+ */
+ public function __construct(EventInterface $eventObject = null)
+ {
+ if (null === $eventObject) {
+ $eventObject = new Event;
+ }
+
+ $this->eventObject = $eventObject;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function attach($event, $callback)
+ {
+ if (!is_callable($callback, true)) {
+ throw new InvalidArgumentException(
+ 'Second argument of "' . __CLASS__ . '"::attach must be a valid callback'
+ );
+ }
+
+ if (!isset($this->events[$event])) {
+ $this->events[$event] = array();
+ }
+
+ if (!in_array($callback, $this->events[$event], true)) {
+ $this->events[$event][] = $callback;
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function trigger($event, $caller, array $parameters)
+ {
+ if (empty($this->events[$event]) && empty($this->events[self::WILDCARD])) {
+ return;
+ }
+
+ $events = array();
+ if (!empty($this->events[$event])) {
+ $events = $this->events[$event];
+ }
+
+ $callbacks = array_merge($events, $this->events[self::WILDCARD]);
+ $previous = array();
+
+ $eventObject = clone $this->getEventObject();
+ $eventObject->setName($event);
+ $eventObject->setTarget($caller);
+ $eventObject->setParameters($parameters);
+
+ do {
+ $current = array_shift($callbacks);
+
+ call_user_func($current, $eventObject);
+
+ $previous[] = $current;
+ $eventObject = clone $eventObject;
+ $eventObject->setEventStack($previous);
+ } while (count($callbacks) > 0);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function getEventObject()
+ {
+ return $this->eventObject;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function setEventObject(EventInterface $eventObject)
+ {
+ $this->eventObject = $eventObject;
+ return $this;
+ }
+
+ /**
+ * Return list of events.
+ *
+ * @return array
+ */
+ public function getEventList()
+ {
+ return $this->events;
+ }
+}
diff --git a/sources/vendor/fabiang/xmpp/src/Event/EventManagerAwareInterface.php b/sources/vendor/fabiang/xmpp/src/Event/EventManagerAwareInterface.php
new file mode 100644
index 0000000..e352ffc
--- /dev/null
+++ b/sources/vendor/fabiang/xmpp/src/Event/EventManagerAwareInterface.php
@@ -0,0 +1,61 @@
+
+ * @copyright 2014 Fabian Grutschus. All rights reserved.
+ * @license BSD
+ * @link http://github.com/fabiang/xmpp
+ */
+
+namespace Fabiang\Xmpp\Event;
+
+/**
+ * Objects that use an event manager must implement this interface.
+ *
+ * @package Xmpp\Event
+ */
+interface EventManagerAwareInterface
+{
+
+ /**
+ * Set event manager.
+ *
+ * @param EventManagerInterface $events Instance of event manager
+ * @return $this
+ */
+ public function setEventManager(EventManagerInterface $events);
+
+ /**
+ * Get event manager instance.
+ *
+ * @return EventManagerInterface
+ */
+ public function getEventManager();
+}
diff --git a/sources/vendor/fabiang/xmpp/src/Event/EventManagerInterface.php b/sources/vendor/fabiang/xmpp/src/Event/EventManagerInterface.php
new file mode 100644
index 0000000..e54a602
--- /dev/null
+++ b/sources/vendor/fabiang/xmpp/src/Event/EventManagerInterface.php
@@ -0,0 +1,81 @@
+
+ * @copyright 2014 Fabian Grutschus. All rights reserved.
+ * @license BSD
+ * @link http://github.com/fabiang/xmpp
+ */
+
+namespace Fabiang\Xmpp\Event;
+
+use Fabiang\Xmpp\Event\EventInterface;
+
+/**
+ * Event manager interface.
+ *
+ * @package Xmpp\Event
+ */
+interface EventManagerInterface
+{
+ /**
+ * Trigger an event.
+ *
+ * @param string $event Name of the event
+ * @param object $caller Triggering object (caller)
+ * @param array $parameters Event parameters
+ * @return void
+ */
+ public function trigger($event, $caller, array $parameters);
+
+ /**
+ * Attach event.
+ *
+ * @param string $event Name of the event
+ * @param callback $callback Callback that handles the event
+ * @return void
+ */
+ public function attach($event, /*callback*/ $callback);
+
+ /**
+ * Return event object.
+ *
+ * @return EventInterface
+ */
+ public function getEventObject();
+
+ /**
+ * Set event object.
+ *
+ * @param EventInterface $eventObject
+ * @return $this
+ */
+ public function setEventObject(EventInterface $eventObject);
+}
diff --git a/sources/vendor/fabiang/xmpp/src/Event/XMLEvent.php b/sources/vendor/fabiang/xmpp/src/Event/XMLEvent.php
new file mode 100644
index 0000000..c4fb5c5
--- /dev/null
+++ b/sources/vendor/fabiang/xmpp/src/Event/XMLEvent.php
@@ -0,0 +1,78 @@
+
+ * @copyright 2014 Fabian Grutschus. All rights reserved.
+ * @license BSD
+ * @link http://github.com/fabiang/xmpp
+ */
+
+namespace Fabiang\Xmpp\Event;
+
+/**
+ * XML parsing events.
+ *
+ * @package Xmpp\Event
+ */
+class XMLEvent extends Event implements XMLEventInterface
+{
+
+ /**
+ * Is start tag event.
+ *
+ * @var boolean
+ */
+ protected $startTag = false;
+
+ /**
+ * {@inheritDoc}
+ */
+ public function isStartTag()
+ {
+ return $this->startTag;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function setStartTag($startTag)
+ {
+ $this->startTag = (bool) $startTag;
+ return $this;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function isEndTag()
+ {
+ return !$this->isStartTag();
+ }
+}
diff --git a/sources/vendor/fabiang/xmpp/src/Event/XMLEventInterface.php b/sources/vendor/fabiang/xmpp/src/Event/XMLEventInterface.php
new file mode 100644
index 0000000..bc4a8e6
--- /dev/null
+++ b/sources/vendor/fabiang/xmpp/src/Event/XMLEventInterface.php
@@ -0,0 +1,68 @@
+
+ * @copyright 2014 Fabian Grutschus. All rights reserved.
+ * @license BSD
+ * @link http://github.com/fabiang/xmpp
+ */
+
+namespace Fabiang\Xmpp\Event;
+
+/**
+ * INterface for xml events.
+ *
+ * @package Xmpp\Event
+ */
+interface XMLEventInterface extends EventInterface
+{
+
+ /**
+ * Is event triggered by a start tag.
+ *
+ * @return boolean
+ */
+ public function isStartTag();
+
+ /**
+ * Set if event triggered by a start tag.
+ *
+ * @param boolean $startTag Flag
+ * @return $this
+ */
+ public function setStartTag($startTag);
+
+ /**
+ * Was event triggered by end tag of an element?
+ *
+ * @return boolean
+ */
+ public function isEndTag();
+}
diff --git a/sources/vendor/fabiang/xmpp/src/EventListener/AbstractEventListener.php b/sources/vendor/fabiang/xmpp/src/EventListener/AbstractEventListener.php
new file mode 100644
index 0000000..d6d56d9
--- /dev/null
+++ b/sources/vendor/fabiang/xmpp/src/EventListener/AbstractEventListener.php
@@ -0,0 +1,132 @@
+
+ * @copyright 2014 Fabian Grutschus. All rights reserved.
+ * @license BSD
+ * @link http://github.com/fabiang/xmpp
+ */
+
+namespace Fabiang\Xmpp\EventListener;
+
+use Fabiang\Xmpp\Connection\ConnectionInterface;
+use Fabiang\Xmpp\Event\EventManagerInterface;
+use Fabiang\Xmpp\Event\EventManager;
+use Fabiang\Xmpp\Options;
+
+/**
+ * Abstract implementaion of event listener
+ *
+ * @package Xmpp\EventListener
+ */
+abstract class AbstractEventListener implements EventListenerInterface
+{
+ /**
+ * Options.
+ *
+ * @var Options
+ */
+ protected $options;
+
+ /**
+ * Eventmanager.
+ *
+ * @var EventManagerInterface
+ */
+ protected $eventManager;
+
+ /**
+ * Get connection.
+ *
+ * @return ConnectionInterface
+ */
+ protected function getConnection()
+ {
+ return $this->getOptions()->getConnection();
+ }
+
+ /**
+ * Get event manager for XML input.
+ *
+ * @return EventManager
+ */
+ protected function getInputEventManager()
+ {
+ return $this->getConnection()->getInputStream()->getEventManager();
+ }
+
+ /**
+ * Get event manager for XML output.
+ *
+ * @return EventManager
+ */
+ protected function getOutputEventManager()
+ {
+ return $this->getConnection()->getOutputStream()->getEventManager();
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function getEventManager()
+ {
+ if (null === $this->eventManager) {
+ $this->setEventManager(new EventManager());
+ }
+
+ return $this->eventManager;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function setEventManager(EventManagerInterface $eventManager)
+ {
+ $this->eventManager = $eventManager;
+ return $this;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function getOptions()
+ {
+ return $this->options;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function setOptions(Options $options)
+ {
+ $this->options = $options;
+ return $this;
+ }
+}
diff --git a/sources/vendor/fabiang/xmpp/src/EventListener/BlockingEventListenerInterface.php b/sources/vendor/fabiang/xmpp/src/EventListener/BlockingEventListenerInterface.php
new file mode 100644
index 0000000..397841f
--- /dev/null
+++ b/sources/vendor/fabiang/xmpp/src/EventListener/BlockingEventListenerInterface.php
@@ -0,0 +1,53 @@
+
+ * @copyright 2014 Fabian Grutschus. All rights reserved.
+ * @license BSD
+ * @link http://github.com/fabiang/xmpp
+ */
+
+namespace Fabiang\Xmpp\EventListener;
+
+/**
+ * Interface for event listeners.
+ *
+ * @package Xmpp\EventListener
+ */
+interface BlockingEventListenerInterface
+{
+
+ /**
+ * Event listener should return false as long he waits for events to finish.
+ *
+ * @return boolean
+ */
+ public function isBlocking();
+}
diff --git a/sources/vendor/fabiang/xmpp/src/EventListener/EventListenerInterface.php b/sources/vendor/fabiang/xmpp/src/EventListener/EventListenerInterface.php
new file mode 100644
index 0000000..384bee3
--- /dev/null
+++ b/sources/vendor/fabiang/xmpp/src/EventListener/EventListenerInterface.php
@@ -0,0 +1,56 @@
+
+ * @copyright 2014 Fabian Grutschus. All rights reserved.
+ * @license BSD
+ * @link http://github.com/fabiang/xmpp
+ */
+
+namespace Fabiang\Xmpp\EventListener;
+
+use Fabiang\Xmpp\Event\EventManagerAwareInterface;
+use Fabiang\Xmpp\OptionsAwareInterface;
+
+/**
+ * Interface for event listeners.
+ *
+ * @package Xmpp\EventListener
+ */
+interface EventListenerInterface extends EventManagerAwareInterface, OptionsAwareInterface
+{
+
+ /**
+ * Register events.
+ *
+ * @return void
+ */
+ public function attachEvents();
+}
diff --git a/sources/vendor/fabiang/xmpp/src/EventListener/Logger.php b/sources/vendor/fabiang/xmpp/src/EventListener/Logger.php
new file mode 100644
index 0000000..f914698
--- /dev/null
+++ b/sources/vendor/fabiang/xmpp/src/EventListener/Logger.php
@@ -0,0 +1,74 @@
+
+ * @copyright 2014 Fabian Grutschus. All rights reserved.
+ * @license BSD
+ * @link http://github.com/fabiang/xmpp
+ */
+
+namespace Fabiang\Xmpp\EventListener;
+
+use Fabiang\Xmpp\Event\EventInterface;
+
+/**
+ * Event listener for logging events.
+ *
+ * @package Xmpp\EventListener
+ */
+class Logger extends AbstractEventListener
+{
+
+ /**
+ * Log event.
+ *
+ * @param \Fabiang\Xmpp\Event\EventInterface $event
+ * @return $this
+ */
+ public function event(EventInterface $event)
+ {
+ $logger = $this->getOptions()->getLogger();
+
+ if (null !== $logger) {
+ list($message, $level) = $event->getParameters();
+ $logger->log($level, $message);
+ }
+ }
+
+ /**
+ * Attach events.
+ *
+ * @return void
+ */
+ public function attachEvents()
+ {
+ $this->getEventManager()->attach('logger', array($this, 'event'));
+ }
+}
diff --git a/sources/vendor/fabiang/xmpp/src/EventListener/Stream/AbstractSessionEvent.php b/sources/vendor/fabiang/xmpp/src/EventListener/Stream/AbstractSessionEvent.php
new file mode 100644
index 0000000..b9aa59d
--- /dev/null
+++ b/sources/vendor/fabiang/xmpp/src/EventListener/Stream/AbstractSessionEvent.php
@@ -0,0 +1,121 @@
+
+ * @copyright 2014 Fabian Grutschus. All rights reserved.
+ * @license BSD
+ * @link http://github.com/fabiang/xmpp
+ */
+
+namespace Fabiang\Xmpp\EventListener\Stream;
+
+use Fabiang\Xmpp\EventListener\AbstractEventListener;
+use Fabiang\Xmpp\Event\XMLEvent;
+use Fabiang\Xmpp\Util\XML;
+
+/**
+ * Listener
+ *
+ * @package Xmpp\EventListener
+ */
+abstract class AbstractSessionEvent extends AbstractEventListener
+{
+
+ /**
+ * Generated id.
+ *
+ * @var string
+ */
+ protected $id;
+
+ /**
+ * Listener is blocking.
+ *
+ * @var boolean
+ */
+ protected $blocking = false;
+
+ /**
+ * Handle session event.
+ *
+ * @param XMLEvent $event
+ * @return void
+ */
+ protected function respondeToFeatures(XMLEvent $event, $data)
+ {
+ if ($event->isEndTag()) {
+ /* @var $element \DOMElement */
+ $element = $event->getParameter(0);
+
+ // bind element occured in
+ if ('features' === $element->parentNode->localName) {
+ $this->blocking = true;
+ $this->getConnection()->send(sprintf(
+ $data,
+ $this->getId()
+ ));
+ }
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function isBlocking()
+ {
+ return $this->blocking;
+ }
+
+ /**
+ * Get generated id.
+ *
+ * @return string
+ */
+ public function getId()
+ {
+ if (null === $this->id) {
+ $this->id = XML::generateId();
+ }
+
+ return $this->id;
+ }
+
+ /**
+ * Set generated id.
+ *
+ * @param string $id
+ * @return $this
+ */
+ public function setId($id)
+ {
+ $this->id = (string) $id;
+ return $this;
+ }
+}
diff --git a/sources/vendor/fabiang/xmpp/src/EventListener/Stream/Authentication.php b/sources/vendor/fabiang/xmpp/src/EventListener/Stream/Authentication.php
new file mode 100644
index 0000000..2934abc
--- /dev/null
+++ b/sources/vendor/fabiang/xmpp/src/EventListener/Stream/Authentication.php
@@ -0,0 +1,211 @@
+
+ * @copyright 2014 Fabian Grutschus. All rights reserved.
+ * @license BSD
+ * @link http://github.com/fabiang/xmpp
+ */
+
+namespace Fabiang\Xmpp\EventListener\Stream;
+
+use Fabiang\Xmpp\Event\XMLEvent;
+use Fabiang\Xmpp\Exception\RuntimeException;
+use Fabiang\Xmpp\EventListener\Stream\Authentication\AuthenticationInterface;
+use Fabiang\Xmpp\Exception\Stream\AuthenticationErrorException;
+use Fabiang\Xmpp\EventListener\AbstractEventListener;
+use Fabiang\Xmpp\EventListener\BlockingEventListenerInterface;
+
+/**
+ * Listener
+ *
+ * @package Xmpp\EventListener
+ */
+class Authentication extends AbstractEventListener implements BlockingEventListenerInterface
+{
+
+ /**
+ * Listener is blocking.
+ *
+ * @var boolean
+ */
+ protected $blocking = false;
+
+ /**
+ * Collected mechanisms.
+ *
+ * @var array
+ */
+ protected $mechanisms = array();
+
+ /**
+ * {@inheritDoc}
+ */
+ public function attachEvents()
+ {
+ $input = $this->getConnection()->getInputStream()->getEventManager();
+ $input->attach('{urn:ietf:params:xml:ns:xmpp-sasl}mechanisms', array($this, 'authenticate'));
+ $input->attach('{urn:ietf:params:xml:ns:xmpp-sasl}mechanism', array($this, 'collectMechanisms'));
+ $input->attach('{urn:ietf:params:xml:ns:xmpp-sasl}failure', array($this, 'failure'));
+ $input->attach('{urn:ietf:params:xml:ns:xmpp-sasl}success', array($this, 'success'));
+ }
+
+ /**
+ * Collect authentication machanisms.
+ *
+ * @param XMLEvent $event
+ * @return void
+ */
+ public function collectMechanisms(XMLEvent $event)
+ {
+ if ($this->getConnection()->isReady() && false === $this->isAuthenticated()) {
+ /* @var $element \DOMElement */
+ list($element) = $event->getParameters();
+ $this->blocking = true;
+ if (false === $event->isStartTag()) {
+ $this->mechanisms[] = strtolower($element->nodeValue);
+ }
+ }
+ }
+
+ /**
+ * Authenticate after collecting machanisms.
+ *
+ * @param XMLEvent $event
+ * @return void
+ */
+ public function authenticate(XMLEvent $event)
+ {
+ if ($this->getConnection()->isReady() && false === $this->isAuthenticated() && false === $event->isStartTag()) {
+ $this->blocking = true;
+
+ $authentication = $this->determineMechanismClass();
+
+ $authentication->setEventManager($this->getEventManager())
+ ->setOptions($this->getOptions())
+ ->attachEvents();
+
+ $this->getConnection()->addListener($authentication);
+ $authentication->authenticate($this->getOptions()->getUsername(), $this->getOptions()->getPassword());
+ }
+ }
+
+ /**
+ * Determine mechanismclass from collected mechanisms.
+ *
+ * @return AuthenticationInterface
+ * @throws RuntimeException
+ */
+ protected function determineMechanismClass()
+ {
+ $authenticationClass = null;
+
+ $authenticationClasses = $this->getOptions()->getAuthenticationClasses();
+ foreach ($this->mechanisms as $mechanism) {
+ if (array_key_exists($mechanism, $authenticationClasses)) {
+ $authenticationClass = $authenticationClasses[$mechanism];
+ break;
+ }
+ }
+
+ if (null === $authenticationClass) {
+ throw new RuntimeException('No supportet authentication machanism found.');
+ }
+
+ $authentication = new $authenticationClass;
+
+ if (!($authentication instanceof AuthenticationInterface)) {
+ $message = 'Authentication class "' . get_class($authentication)
+ . '" is no instanceof AuthenticationInterface';
+ throw new RuntimeException($message);
+ }
+
+ return $authentication;
+ }
+
+ /**
+ * Authentication failed.
+ *
+ * @param XMLEvent $event
+ * @throws StreamErrorException
+ */
+ public function failure(XMLEvent $event)
+ {
+ if (false === $event->isStartTag()) {
+ $this->blocking = false;
+ throw AuthenticationErrorException::createFromEvent($event);
+ }
+ }
+
+ /**
+ * Authentication successful.
+ *
+ * @param XMLEvent $event
+ */
+ public function success(XMLEvent $event)
+ {
+ if (false === $event->isStartTag()) {
+ $this->blocking = false;
+
+ $connection = $this->getConnection();
+ $connection->resetStreams();
+ $connection->connect();
+
+ $this->getOptions()->setAuthenticated(true);
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function isBlocking()
+ {
+ return $this->blocking;
+ }
+
+ /**
+ * Get collected mechanisms.
+ *
+ * @return array
+ */
+ public function getMechanisms()
+ {
+ return $this->mechanisms;
+ }
+
+ /**
+ *
+ * @return boolean
+ */
+ protected function isAuthenticated()
+ {
+ return $this->getOptions()->isAuthenticated();
+ }
+}
diff --git a/sources/vendor/fabiang/xmpp/src/EventListener/Stream/Authentication/AuthenticationInterface.php b/sources/vendor/fabiang/xmpp/src/EventListener/Stream/Authentication/AuthenticationInterface.php
new file mode 100644
index 0000000..903c763
--- /dev/null
+++ b/sources/vendor/fabiang/xmpp/src/EventListener/Stream/Authentication/AuthenticationInterface.php
@@ -0,0 +1,57 @@
+
+ * @copyright 2014 Fabian Grutschus. All rights reserved.
+ * @license BSD
+ * @link http://github.com/fabiang/xmpp
+ */
+
+namespace Fabiang\Xmpp\EventListener\Stream\Authentication;
+
+use Fabiang\Xmpp\EventListener\EventListenerInterface;
+
+/**
+ * Interface for classes that handle authentication.
+ *
+ * @package Xmpp\EventListener\Authentication
+ */
+interface AuthenticationInterface extends EventListenerInterface
+{
+
+ /**
+ * Authenticate.
+ *
+ * @param string $username Username
+ * @param string $password Password
+ * @return void
+ */
+ public function authenticate($username, $password);
+}
diff --git a/sources/vendor/fabiang/xmpp/src/EventListener/Stream/Authentication/DigestMd5.php b/sources/vendor/fabiang/xmpp/src/EventListener/Stream/Authentication/DigestMd5.php
new file mode 100644
index 0000000..49b43fd
--- /dev/null
+++ b/sources/vendor/fabiang/xmpp/src/EventListener/Stream/Authentication/DigestMd5.php
@@ -0,0 +1,238 @@
+
+ * @copyright 2014 Fabian Grutschus. All rights reserved.
+ * @license BSD
+ * @link http://github.com/fabiang/xmpp
+ */
+
+namespace Fabiang\Xmpp\EventListener\Stream\Authentication;
+
+use Fabiang\Xmpp\EventListener\AbstractEventListener;
+use Fabiang\Xmpp\Event\XMLEvent;
+use Fabiang\Xmpp\Util\XML;
+use Fabiang\Xmpp\Exception\Stream\AuthenticationErrorException;
+
+/**
+ * Handler for "digest md5" authentication mechanism.
+ *
+ * @package Xmpp\EventListener\Authentication
+ */
+class DigestMd5 extends AbstractEventListener implements AuthenticationInterface
+{
+
+ /**
+ * Is event blocking stream.
+ *
+ * @var boolean
+ */
+ protected $blocking = false;
+
+ /**
+ *
+ * @var string
+ */
+ protected $username;
+
+ /**
+ *
+ * @var string
+ */
+ protected $password;
+
+ /**
+ * {@inheritDoc}
+ */
+ public function attachEvents()
+ {
+ $input = $this->getInputEventManager();
+ $input->attach('{urn:ietf:params:xml:ns:xmpp-sasl}challenge', array($this, 'challenge'));
+ $input->attach('{urn:ietf:params:xml:ns:xmpp-sasl}success', array($this, 'success'));
+
+ $output = $this->getOutputEventManager();
+ $output->attach('{urn:ietf:params:xml:ns:xmpp-sasl}auth', array($this, 'auth'));
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function authenticate($username, $password)
+ {
+ $this->setUsername($username)->setPassword($password);
+ $auth = '';
+ $this->getConnection()->send($auth);
+ }
+
+ /**
+ * Authentication starts -> blocking.
+ *
+ * @return void
+ */
+ public function auth()
+ {
+ $this->blocking = true;
+ }
+
+ /**
+ * Challenge string received.
+ *
+ * @param XMLEvent $event XML event
+ * @return void
+ */
+ public function challenge(XMLEvent $event)
+ {
+ if ($event->isEndTag()) {
+ list($element) = $event->getParameters();
+
+ $challenge = XML::base64Decode($element->nodeValue);
+ $values = $this->parseCallenge($challenge);
+
+ if (isset($values['nonce'])) {
+ $send = ''
+ . $this->response($values) . '';
+ } elseif (isset($values['rspauth'])) {
+ $send = '';
+ } else {
+ throw new AuthenticationErrorException("Error when receiving challenge: \"$challenge\"");
+ }
+
+ $this->getConnection()->send($send);
+ }
+ }
+
+ /**
+ * Generate response data.
+ *
+ * @param array $values
+ */
+ protected function response($values)
+ {
+ $values['cnonce'] = uniqid(mt_rand(), false);
+ $values['nc'] = '00000001';
+ $values['qop'] = 'auth';
+
+ if (!isset($values['realm'])) {
+ $values['realm'] = $this->getOptions()->getTo();
+ }
+
+ if (!isset($values['digest-uri'])) {
+ $values['digest-uri'] = 'xmpp/' . $this->getOptions()->getTo();
+ }
+
+ $a1 = sprintf('%s:%s:%s', $this->getUsername(), $values['realm'], $this->getPassword());
+
+ if ('md5-sess' === $values['algorithm']) {
+ $a1 = pack('H32', md5($a1)) . ':' . $values['nonce'] . ':' . $values['cnonce'];
+ }
+
+ $a2 = "AUTHENTICATE:" . $values['digest-uri'];
+
+ $password = md5($a1) . ':' . $values['nonce'] . ':' . $values['nc'] . ':'
+ . $values['cnonce'] . ':' . $values['qop'] . ':' . md5($a2);
+ $password = md5($password);
+
+ $response = sprintf(
+ 'username="%s",realm="%s",nonce="%s",cnonce="%s",nc=%s,qop=%s,digest-uri="%s",response=%s,charset=utf-8',
+ $this->getUsername(),
+ $values['realm'],
+ $values['nonce'],
+ $values['cnonce'],
+ $values['nc'],
+ $values['qop'],
+ $values['digest-uri'],
+ $password
+ );
+
+ return XML::base64Encode($response);
+ }
+
+ /**
+ * Parse challenge string and return its values as array.
+ *
+ * @param string $challenge
+ * @return array
+ */
+ protected function parseCallenge($challenge)
+ {
+ if (!$challenge) {
+ return array();
+ }
+
+ $matches = array();
+ preg_match_all('#(\w+)\=(?:"([^"]+)"|([^,]+))#', $challenge, $matches);
+ list(, $variables, $quoted, $unquoted) = $matches;
+ // filter empty strings; preserve keys
+ $quoted = array_filter($quoted);
+ $unquoted = array_filter($unquoted);
+ // replace "unquoted" values into "quoted" array and combine variables array with it
+ return array_combine($variables, array_replace($quoted, $unquoted));
+ }
+
+ /**
+ * Handle success event.
+ *
+ * @return void
+ */
+ public function success()
+ {
+ $this->blocking = false;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function isBlocking()
+ {
+ return $this->blocking;
+ }
+
+ public function getUsername()
+ {
+ return $this->username;
+ }
+
+ public function setUsername($username)
+ {
+ $this->username = $username;
+ return $this;
+ }
+
+ public function getPassword()
+ {
+ return $this->password;
+ }
+
+ public function setPassword($password)
+ {
+ $this->password = $password;
+ return $this;
+ }
+}
diff --git a/sources/vendor/fabiang/xmpp/src/EventListener/Stream/Authentication/Plain.php b/sources/vendor/fabiang/xmpp/src/EventListener/Stream/Authentication/Plain.php
new file mode 100644
index 0000000..9a08fc4
--- /dev/null
+++ b/sources/vendor/fabiang/xmpp/src/EventListener/Stream/Authentication/Plain.php
@@ -0,0 +1,68 @@
+
+ * @copyright 2014 Fabian Grutschus. All rights reserved.
+ * @license BSD
+ * @link http://github.com/fabiang/xmpp
+ */
+
+namespace Fabiang\Xmpp\EventListener\Stream\Authentication;
+
+use Fabiang\Xmpp\EventListener\AbstractEventListener;
+use Fabiang\Xmpp\Util\XML;
+
+/**
+ * Handler for "plain" authentication mechanism.
+ *
+ * @package Xmpp\EventListener\Authentication
+ */
+class Plain extends AbstractEventListener implements AuthenticationInterface
+{
+
+ /**
+ * {@inheritDoc}
+ */
+ public function attachEvents()
+ {
+
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function authenticate($username, $password)
+ {
+ $authString = XML::quote(base64_encode("\x00" . $username . "\x00" . $password));
+ $this->getConnection()->send(
+ '' . $authString . ''
+ );
+ }
+}
diff --git a/sources/vendor/fabiang/xmpp/src/EventListener/Stream/Bind.php b/sources/vendor/fabiang/xmpp/src/EventListener/Stream/Bind.php
new file mode 100644
index 0000000..d3d8937
--- /dev/null
+++ b/sources/vendor/fabiang/xmpp/src/EventListener/Stream/Bind.php
@@ -0,0 +1,89 @@
+
+ * @copyright 2014 Fabian Grutschus. All rights reserved.
+ * @license BSD
+ * @link http://github.com/fabiang/xmpp
+ */
+
+namespace Fabiang\Xmpp\EventListener\Stream;
+
+use Fabiang\Xmpp\EventListener\BlockingEventListenerInterface;
+use Fabiang\Xmpp\Event\XMLEvent;
+
+/**
+ * Listener
+ *
+ * @package Xmpp\EventListener
+ */
+class Bind extends AbstractSessionEvent implements BlockingEventListenerInterface
+{
+
+ /**
+ * {@inheritDoc}
+ */
+ public function attachEvents()
+ {
+ $input = $this->getInputEventManager();
+ $input->attach('{urn:ietf:params:xml:ns:xmpp-bind}bind', array($this, 'bindFeatures'));
+ $input->attach('{urn:ietf:params:xml:ns:xmpp-bind}jid', array($this, 'jid'));
+ }
+
+ /**
+ * Handle XML events for "bind".
+ *
+ * @param XMLEvent $event
+ * @return void
+ */
+ public function bindFeatures(XMLEvent $event)
+ {
+ $this->respondeToFeatures(
+ $event,
+ ''
+ );
+ }
+
+ /**
+ * Handle jid.
+ *
+ * @param XMLEvent $event
+ * @return void
+ */
+ public function jid(XMLEvent $event)
+ {
+ /* @var $element \DOMDocument */
+ $element = $event->getParameter(0);
+
+ $jid = $element->nodeValue;
+ $this->getOptions()->setJid($jid);
+ $this->blocking = false;
+ }
+}
diff --git a/sources/vendor/fabiang/xmpp/src/EventListener/Stream/Roster.php b/sources/vendor/fabiang/xmpp/src/EventListener/Stream/Roster.php
new file mode 100644
index 0000000..223070f
--- /dev/null
+++ b/sources/vendor/fabiang/xmpp/src/EventListener/Stream/Roster.php
@@ -0,0 +1,154 @@
+
+ * @copyright 2014 Fabian Grutschus. All rights reserved.
+ * @license BSD
+ * @link http://github.com/fabiang/xmpp
+ */
+
+namespace Fabiang\Xmpp\EventListener\Stream;
+
+use Fabiang\Xmpp\Event\XMLEvent;
+use Fabiang\Xmpp\EventListener\AbstractEventListener;
+use Fabiang\Xmpp\EventListener\BlockingEventListenerInterface;
+use Fabiang\Xmpp\Protocol\User\User;
+
+/**
+ * Listener
+ *
+ * @package Xmpp\EventListener
+ */
+class Roster extends AbstractEventListener implements BlockingEventListenerInterface
+{
+
+ /**
+ * Blocking.
+ *
+ * @var boolean
+ */
+ protected $blocking = false;
+
+ /**
+ * user object.
+ *
+ * @var User
+ */
+ protected $userObject;
+
+ /**
+ * {@inheritDoc}
+ */
+ public function attachEvents()
+ {
+ $this->getOutputEventManager()
+ ->attach('{jabber:iq:roster}query', array($this, 'query'));
+ $this->getInputEventManager()
+ ->attach('{jabber:iq:roster}query', array($this, 'result'));
+ }
+
+ /**
+ * Sending a query request for roster sets listener to blocking mode.
+ *
+ * @return void
+ */
+ public function query()
+ {
+ $this->blocking = true;
+ }
+
+ /**
+ * Result received.
+ *
+ * @param \Fabiang\Xmpp\Event\XMLEvent $event
+ * @return void
+ */
+ public function result(XMLEvent $event)
+ {
+ if ($event->isEndTag()) {
+ $users = array();
+
+ /* @var $element \DOMElement */
+ $element = $event->getParameter(0);
+ $items = $element->getElementsByTagName('item');
+ /* @var $item \DOMElement */
+ foreach ($items as $item) {
+ $user = clone $this->getUserObject();
+ $user->setName($item->getAttribute('name'))
+ ->setJid($item->getAttribute('jid'))
+ ->setSubscription($item->getAttribute('subscription'));
+
+ $groups = $item->getElementsByTagName('group');
+ foreach ($groups as $group) {
+ $user->addGroup($group->nodeValue);
+ }
+
+ $users[] = $user;
+ }
+
+ $this->getOptions()->setUsers($users);
+ $this->blocking = false;
+ }
+ }
+
+ /**
+ * Get user object.
+ *
+ * @return User
+ */
+ public function getUserObject()
+ {
+ if (null === $this->userObject) {
+ $this->setUserObject(new User);
+ }
+
+ return $this->userObject;
+ }
+
+ /**
+ * Set user object.
+ *
+ * @param User $userObject
+ * @return $this
+ */
+ public function setUserObject(User $userObject)
+ {
+ $this->userObject = $userObject;
+ return $this;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function isBlocking()
+ {
+ return $this->blocking;
+ }
+}
diff --git a/sources/vendor/fabiang/xmpp/src/EventListener/Stream/Session.php b/sources/vendor/fabiang/xmpp/src/EventListener/Stream/Session.php
new file mode 100644
index 0000000..10b8c60
--- /dev/null
+++ b/sources/vendor/fabiang/xmpp/src/EventListener/Stream/Session.php
@@ -0,0 +1,90 @@
+
+ * @copyright 2014 Fabian Grutschus. All rights reserved.
+ * @license BSD
+ * @link http://github.com/fabiang/xmpp
+ */
+
+namespace Fabiang\Xmpp\EventListener\Stream;
+
+use Fabiang\Xmpp\EventListener\BlockingEventListenerInterface;
+use Fabiang\Xmpp\Event\XMLEvent;
+
+/**
+ * Listener
+ *
+ * @package Xmpp\EventListener
+ */
+class Session extends AbstractSessionEvent implements BlockingEventListenerInterface
+{
+
+ /**
+ * {@inheritDoc}
+ */
+ public function attachEvents()
+ {
+ $input = $this->getInputEventManager();
+ $input->attach('{urn:ietf:params:xml:ns:xmpp-session}session', array($this, 'sessionStart'));
+ $input->attach('{jabber:client}iq', array($this, 'iq'));
+ }
+
+ /**
+ * Handle session event.
+ *
+ * @param XMLEvent $event
+ * @return void
+ */
+ public function sessionStart(XMLEvent $event)
+ {
+ $this->respondeToFeatures(
+ $event,
+ ''
+ );
+ }
+
+ /**
+ * Handle iq event.
+ *
+ * @param XMLEvent $event
+ * @retrun void
+ */
+ public function iq(XMLEvent $event)
+ {
+ if ($event->isEndTag()) {
+ /* @var $element \DOMElement */
+ $element = $event->getParameter(0);
+ if ($this->getId() === $element->getAttribute('id')) {
+ $this->blocking = false;
+ }
+ }
+ }
+}
diff --git a/sources/vendor/fabiang/xmpp/src/EventListener/Stream/StartTls.php b/sources/vendor/fabiang/xmpp/src/EventListener/Stream/StartTls.php
new file mode 100644
index 0000000..9e001ce
--- /dev/null
+++ b/sources/vendor/fabiang/xmpp/src/EventListener/Stream/StartTls.php
@@ -0,0 +1,112 @@
+
+ * @copyright 2014 Fabian Grutschus. All rights reserved.
+ * @license BSD
+ * @link http://github.com/fabiang/xmpp
+ */
+
+namespace Fabiang\Xmpp\EventListener\Stream;
+
+use Fabiang\Xmpp\Event\XMLEvent;
+use Fabiang\Xmpp\EventListener\AbstractEventListener;
+use Fabiang\Xmpp\EventListener\BlockingEventListenerInterface;
+use Fabiang\Xmpp\Connection\SocketConnectionInterface;
+
+/**
+ * Listener
+ *
+ * @package Xmpp\EventListener
+ */
+class StartTls extends AbstractEventListener implements BlockingEventListenerInterface
+{
+
+ /**
+ * Listener blocks stream.
+ *
+ * @var boolean
+ */
+ protected $blocking = false;
+
+ /**
+ * {@inheritDoc}
+ */
+ public function attachEvents()
+ {
+ $input = $this->getInputEventManager();
+ $input->attach('{urn:ietf:params:xml:ns:xmpp-tls}starttls', array($this, 'starttlsEvent'));
+ $input->attach('{urn:ietf:params:xml:ns:xmpp-tls}proceed', array($this, 'proceed'));
+ }
+
+ /**
+ * Send start tls command.
+ *
+ * @param XMLEvent $event XMLEvent object
+ */
+ public function starttlsEvent(XMLEvent $event)
+ {
+ if (false === $event->isStartTag()) {
+ $this->blocking = true;
+
+ $connection = $this->getConnection();
+ $connection->setReady(false);
+ $connection->send('');
+ }
+ }
+
+ /**
+ * Start TLS response.
+ *
+ * @param XMLEvent $event XMLEvent object
+ * @return void
+ */
+ public function proceed(XMLEvent $event)
+ {
+ if (false === $event->isStartTag()) {
+ $this->blocking = false;
+
+ $connection = $this->getConnection();
+ if ($connection instanceof SocketConnectionInterface) {
+ $connection->getSocket()->crypto(true, STREAM_CRYPTO_METHOD_SSLv23_CLIENT);
+ }
+ $connection->resetStreams();
+ $connection->connect();
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function isBlocking()
+ {
+ return $this->blocking;
+ }
+}
diff --git a/sources/vendor/fabiang/xmpp/src/EventListener/Stream/Stream.php b/sources/vendor/fabiang/xmpp/src/EventListener/Stream/Stream.php
new file mode 100644
index 0000000..df2a31f
--- /dev/null
+++ b/sources/vendor/fabiang/xmpp/src/EventListener/Stream/Stream.php
@@ -0,0 +1,119 @@
+
+ * @copyright 2014 Fabian Grutschus. All rights reserved.
+ * @license BSD
+ * @link http://github.com/fabiang/xmpp
+ */
+
+namespace Fabiang\Xmpp\EventListener\Stream;
+
+use Fabiang\Xmpp\Event\XMLEvent;
+use Fabiang\Xmpp\EventListener\AbstractEventListener;
+use Fabiang\Xmpp\EventListener\BlockingEventListenerInterface;
+
+/**
+ * Listener
+ *
+ * @package Xmpp\EventListener
+ */
+class Stream extends AbstractEventListener implements BlockingEventListenerInterface
+{
+
+ /**
+ * Listener blocks stream.
+ *
+ * @var boolean
+ */
+ protected $blocking = false;
+
+ /**
+ * {@inheritDoc}
+ */
+ public function attachEvents()
+ {
+ $this->getOutputEventManager()
+ ->attach('{http://etherx.jabber.org/streams}stream', array($this, 'streamStart'));
+
+ $input = $this->getInputEventManager();
+ $input->attach('{http://etherx.jabber.org/streams}stream', array($this, 'streamServer'));
+ $input->attach('{http://etherx.jabber.org/streams}features', array($this, 'features'));
+ }
+
+ /**
+ * Stream starts.
+ *
+ * @param XMLEvent $event XMLEvent
+ * @return void
+ */
+ public function streamStart(XMLEvent $event)
+ {
+ if (true === $event->isStartTag()) {
+ $this->blocking = true;
+ }
+ }
+
+ /**
+ * Stream server.
+ *
+ * @param XMLEvent $event XMLEvent
+ * @return void
+ */
+ public function streamServer(XMLEvent $event)
+ {
+ if (false === $event->isStartTag()) {
+ $this->blocking = false;
+
+ if ($this->getConnection()->isConnected()) {
+ $this->getConnection()->disconnect();
+ }
+ }
+ }
+
+ /**
+ * Server send stream start.
+ *
+ * @return void
+ */
+ public function features()
+ {
+ $this->blocking = false;
+ $this->getConnection()->setReady(true);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function isBlocking()
+ {
+ return $this->blocking;
+ }
+}
diff --git a/sources/vendor/fabiang/xmpp/src/EventListener/Stream/StreamError.php b/sources/vendor/fabiang/xmpp/src/EventListener/Stream/StreamError.php
new file mode 100644
index 0000000..1a75748
--- /dev/null
+++ b/sources/vendor/fabiang/xmpp/src/EventListener/Stream/StreamError.php
@@ -0,0 +1,74 @@
+
+ * @copyright 2014 Fabian Grutschus. All rights reserved.
+ * @license BSD
+ * @link http://github.com/fabiang/xmpp
+ */
+
+namespace Fabiang\Xmpp\EventListener\Stream;
+
+use Fabiang\Xmpp\Event\XMLEvent;
+use Fabiang\Xmpp\Exception\Stream\StreamErrorException;
+use Fabiang\Xmpp\EventListener\AbstractEventListener;
+
+/**
+ * Listener for stream errors.
+ *
+ * @package Xmpp\EventListener
+ */
+class StreamError extends AbstractEventListener
+{
+
+ /**
+ * {@inheritDoc}
+ */
+ public function attachEvents()
+ {
+ $this->getInputEventManager()->attach(
+ '{http://etherx.jabber.org/streams}error',
+ array($this, 'error')
+ );
+ }
+
+ /**
+ * Throws an exception when stream error comes from input stream.
+ *
+ * @param \Fabiang\Xmpp\Event\XMLEvent $event
+ * @throws StreamErrorException
+ */
+ public function error(XMLEvent $event)
+ {
+ if (false === $event->isStartTag()) {
+ throw StreamErrorException::createFromEvent($event);
+ }
+ }
+}
diff --git a/sources/vendor/fabiang/xmpp/src/Exception/ErrorException.php b/sources/vendor/fabiang/xmpp/src/Exception/ErrorException.php
new file mode 100644
index 0000000..0e352c0
--- /dev/null
+++ b/sources/vendor/fabiang/xmpp/src/Exception/ErrorException.php
@@ -0,0 +1,47 @@
+
+ * @copyright 2014 Fabian Grutschus. All rights reserved.
+ * @license BSD
+ * @link http://github.com/fabiang/xmpp
+ */
+
+namespace Fabiang\Xmpp\Exception;
+
+/**
+ * Exception interface.
+ *
+ * @package Xmpp\Exception
+ */
+class ErrorException extends \ErrorException implements ExceptionInterface
+{
+
+}
diff --git a/sources/vendor/fabiang/xmpp/src/Exception/ExceptionInterface.php b/sources/vendor/fabiang/xmpp/src/Exception/ExceptionInterface.php
new file mode 100644
index 0000000..5cb3007
--- /dev/null
+++ b/sources/vendor/fabiang/xmpp/src/Exception/ExceptionInterface.php
@@ -0,0 +1,47 @@
+
+ * @copyright 2014 Fabian Grutschus. All rights reserved.
+ * @license BSD
+ * @link http://github.com/fabiang/xmpp
+ */
+
+namespace Fabiang\Xmpp\Exception;
+
+/**
+ * Exception interface.
+ *
+ * @package Xmpp\Exception
+ */
+interface ExceptionInterface
+{
+
+}
diff --git a/sources/vendor/fabiang/xmpp/src/Exception/InvalidArgumentException.php b/sources/vendor/fabiang/xmpp/src/Exception/InvalidArgumentException.php
new file mode 100644
index 0000000..585ddb2
--- /dev/null
+++ b/sources/vendor/fabiang/xmpp/src/Exception/InvalidArgumentException.php
@@ -0,0 +1,49 @@
+
+ * @copyright 2014 Fabian Grutschus. All rights reserved.
+ * @license BSD
+ * @link http://github.com/fabiang/xmpp
+ */
+
+namespace Fabiang\Xmpp\Exception;
+
+/**
+ * Exception for invalid arguments.
+ *
+ * "Throw an InvalidArgumentException when your functions or methods receive arguments that are invalid."
+ *
+ * @package Xmpp\Exception
+ */
+class InvalidArgumentException extends \InvalidArgumentException implements ExceptionInterface
+{
+
+}
diff --git a/sources/vendor/fabiang/xmpp/src/Exception/OutOfRangeException.php b/sources/vendor/fabiang/xmpp/src/Exception/OutOfRangeException.php
new file mode 100644
index 0000000..15c7fb1
--- /dev/null
+++ b/sources/vendor/fabiang/xmpp/src/Exception/OutOfRangeException.php
@@ -0,0 +1,50 @@
+
+ * @copyright 2014 Fabian Grutschus. All rights reserved.
+ * @license BSD
+ * @link http://github.com/fabiang/xmpp
+ */
+
+namespace Fabiang\Xmpp\Exception;
+
+/**
+ * Exception for out-of-bounds.
+ *
+ * "This is the same as OutOfBoundsException, but this should be used
+ * for normal arrays which are indexed by number, not by key."
+ *
+ * @package Xmpp\Exception
+ */
+class OutOfRangeException extends \OutOfBoundsException implements ExceptionInterface
+{
+
+}
diff --git a/sources/vendor/fabiang/xmpp/src/Exception/RuntimeException.php b/sources/vendor/fabiang/xmpp/src/Exception/RuntimeException.php
new file mode 100644
index 0000000..23ac010
--- /dev/null
+++ b/sources/vendor/fabiang/xmpp/src/Exception/RuntimeException.php
@@ -0,0 +1,49 @@
+
+ * @copyright 2014 Fabian Grutschus. All rights reserved.
+ * @license BSD
+ * @link http://github.com/fabiang/xmpp
+ */
+
+namespace Fabiang\Xmpp\Exception;
+
+/**
+ * Runtime exception.
+ *
+ * "It should be throw in cases where the calling code does not necessarily have the capacity to handle it."
+ *
+ * @package Xmpp\Exception
+ */
+class RuntimeException extends \RuntimeException implements ExceptionInterface
+{
+
+}
diff --git a/sources/vendor/fabiang/xmpp/src/Exception/SocketException.php b/sources/vendor/fabiang/xmpp/src/Exception/SocketException.php
new file mode 100644
index 0000000..18f2e53
--- /dev/null
+++ b/sources/vendor/fabiang/xmpp/src/Exception/SocketException.php
@@ -0,0 +1,47 @@
+
+ * @copyright 2014 Fabian Grutschus. All rights reserved.
+ * @license BSD
+ * @link http://github.com/fabiang/xmpp
+ */
+
+namespace Fabiang\Xmpp\Exception;
+
+/**
+ * XML parser exception.
+ *
+ * @package Xmpp\Exception
+ */
+class SocketException extends RuntimeException
+{
+
+}
diff --git a/sources/vendor/fabiang/xmpp/src/Exception/Stream/AuthenticationErrorException.php b/sources/vendor/fabiang/xmpp/src/Exception/Stream/AuthenticationErrorException.php
new file mode 100644
index 0000000..be3a215
--- /dev/null
+++ b/sources/vendor/fabiang/xmpp/src/Exception/Stream/AuthenticationErrorException.php
@@ -0,0 +1,46 @@
+
+ * @copyright 2014 Fabian Grutschus. All rights reserved.
+ * @license BSD
+ * @link http://github.com/fabiang/xmpp
+ */
+
+namespace Fabiang\Xmpp\Exception\Stream;
+
+/**
+ * Exception class for error generated by stream,
+ *
+ * @package Xmpp\Exception\Stream
+ */
+class AuthenticationErrorException extends StreamErrorException
+{
+}
diff --git a/sources/vendor/fabiang/xmpp/src/Exception/Stream/StreamErrorException.php b/sources/vendor/fabiang/xmpp/src/Exception/Stream/StreamErrorException.php
new file mode 100644
index 0000000..18ba4f8
--- /dev/null
+++ b/sources/vendor/fabiang/xmpp/src/Exception/Stream/StreamErrorException.php
@@ -0,0 +1,103 @@
+
+ * @copyright 2014 Fabian Grutschus. All rights reserved.
+ * @license BSD
+ * @link http://github.com/fabiang/xmpp
+ */
+
+namespace Fabiang\Xmpp\Exception\Stream;
+
+use Fabiang\Xmpp\Exception\RuntimeException;
+use Fabiang\Xmpp\Event\XMLEvent;
+
+/**
+ * Exception class for error generated by stream,
+ *
+ * @package Xmpp\Exception\Stream
+ */
+class StreamErrorException extends RuntimeException
+{
+
+ /**
+ * XML content.
+ *
+ * @var string
+ */
+ protected $content;
+
+ /**
+ * Create exception from XMLEvent object.
+ *
+ * @param \Fabiang\Xmpp\Event\XMLEvent $event XMLEvent object
+ * @return static
+ */
+ public static function createFromEvent(XMLEvent $event)
+ {
+ /* @var $element \DOMElement */
+ list($element) = $event->getParameters();
+
+ /* @var $first \DOMElement */
+ $first = $element->firstChild;
+
+ if (null !== $first && XML_ELEMENT_NODE === $first->nodeType) {
+ $message = 'Stream Error: "' . $first->localName . '"';
+ } else {
+ $message = 'Generic stream error';
+ }
+
+ $exception = new static($message);
+ $exception->setContent($element->ownerDocument->saveXML($element));
+ return $exception;
+ }
+
+ /**
+ * Get xml content.
+ *
+ * @return string
+ */
+ public function getContent()
+ {
+ return $this->content;
+ }
+
+ /**
+ * Set XML contents.
+ *
+ * @param string $content
+ * @return $this
+ */
+ public function setContent($content)
+ {
+ $this->content = (string) $content;
+ return $this;
+ }
+}
diff --git a/sources/vendor/fabiang/xmpp/src/Exception/TimeoutException.php b/sources/vendor/fabiang/xmpp/src/Exception/TimeoutException.php
new file mode 100644
index 0000000..363a8b8
--- /dev/null
+++ b/sources/vendor/fabiang/xmpp/src/Exception/TimeoutException.php
@@ -0,0 +1,47 @@
+
+ * @copyright 2014 Fabian Grutschus. All rights reserved.
+ * @license BSD
+ * @link http://github.com/fabiang/xmpp
+ */
+
+namespace Fabiang\Xmpp\Exception;
+
+/**
+ * XML parser exception.
+ *
+ * @package Xmpp\Exception
+ */
+class TimeoutException extends RuntimeException
+{
+
+}
diff --git a/sources/vendor/fabiang/xmpp/src/Exception/XMLParserException.php b/sources/vendor/fabiang/xmpp/src/Exception/XMLParserException.php
new file mode 100644
index 0000000..4192151
--- /dev/null
+++ b/sources/vendor/fabiang/xmpp/src/Exception/XMLParserException.php
@@ -0,0 +1,73 @@
+
+ * @copyright 2014 Fabian Grutschus. All rights reserved.
+ * @license BSD
+ * @link http://github.com/fabiang/xmpp
+ */
+
+namespace Fabiang\Xmpp\Exception;
+
+use Fabiang\Xmpp\Exception\InvalidArgumentException;
+
+/**
+ * XML parser exception.
+ *
+ * @package Xmpp\Exception
+ */
+class XMLParserException extends RuntimeException
+{
+
+ /**
+ * Factory XML parsing exception.
+ *
+ * @param resource $parser
+ * @throws static
+ */
+ public static function create($parser)
+ {
+ if (!is_resource($parser) || 'xml' !== get_resource_type($parser)) {
+ $message = 'Argument #1 of "' . __CLASS__ . '::'
+ . __METHOD__ . '" must be a resource returned by "xml_parser_create"';
+ throw new InvalidArgumentException($message);
+ }
+
+ $code = xml_get_error_code($parser);
+ $error = xml_error_string($code);
+ $line = xml_get_current_line_number($parser);
+ $column = xml_get_current_column_number($parser);
+
+ return new static(
+ sprintf('XML parsing error: "%s" at Line %d at column %d', $error, $line, $column),
+ $code
+ );
+ }
+}
diff --git a/sources/vendor/fabiang/xmpp/src/Options.php b/sources/vendor/fabiang/xmpp/src/Options.php
new file mode 100644
index 0000000..dd2fd55
--- /dev/null
+++ b/sources/vendor/fabiang/xmpp/src/Options.php
@@ -0,0 +1,416 @@
+
+ * @copyright 2014 Fabian Grutschus. All rights reserved.
+ * @license BSD
+ * @link http://github.com/fabiang/xmpp
+ */
+
+namespace Fabiang\Xmpp;
+
+use Fabiang\Xmpp\Connection\ConnectionInterface;
+use Fabiang\Xmpp\Protocol\ImplementationInterface;
+use Fabiang\Xmpp\Protocol\DefaultImplementation;
+use Psr\Log\LoggerInterface;
+
+/**
+ * Xmpp connection options.
+ *
+ * @package Xmpp
+ */
+class Options
+{
+
+ /**
+ *
+ * @var ImplementationInterface
+ */
+ protected $implementation;
+
+ /**
+ *
+ * @var string
+ */
+ protected $address;
+
+ /**
+ * Connection object.
+ *
+ * @var ConnectionInterface
+ */
+ protected $connection;
+
+ /**
+ * PSR-3 Logger interface.
+ *
+ * @var LoggerInterface
+ */
+ protected $logger;
+
+ /**
+ *
+ * @var string
+ */
+ protected $to;
+
+ /**
+ *
+ * @var string
+ */
+ protected $username;
+
+ /**
+ *
+ * @var string
+ */
+ protected $password;
+
+ /**
+ *
+ * @var string
+ */
+ protected $jid;
+
+ /**
+ *
+ * @var boolean
+ */
+ protected $authenticated = false;
+
+ /**
+ *
+ * @var array
+ */
+ protected $users = array();
+
+ /**
+ * Timeout for connection.
+ *
+ * @var integer
+ */
+ protected $timeout = 30;
+
+ /**
+ * Authentication methods.
+ *
+ * @var array
+ */
+ protected $authenticationClasses = array(
+ 'digest-md5' => '\\Fabiang\\Xmpp\\EventListener\\Stream\\Authentication\\DigestMd5',
+ 'plain' => '\\Fabiang\\Xmpp\\EventListener\\Stream\\Authentication\\Plain'
+ );
+
+ /**
+ * Constructor.
+ *
+ * @param string $address Server address
+ */
+ public function __construct($address = null)
+ {
+ if (null !== $address) {
+ $this->setAddress($address);
+ }
+ }
+
+ /**
+ * Get protocol implementation.
+ *
+ * @return ImplementationInterface
+ */
+ public function getImplementation()
+ {
+ if (null === $this->implementation) {
+ $this->setImplementation(new DefaultImplementation());
+ }
+
+ return $this->implementation;
+ }
+
+ /**
+ * Set protocol implementation.
+ *
+ * @param ImplementationInterface $implementation
+ * @return $this
+ */
+ public function setImplementation(ImplementationInterface $implementation)
+ {
+ $this->implementation = $implementation;
+ return $this;
+ }
+
+ /**
+ * Get server address.
+ *
+ * @return string
+ */
+ public function getAddress()
+ {
+ return $this->address;
+ }
+
+ /**
+ * Set server address.
+ *
+ * When a address is passed this setter also calls setTo with the hostname part of the address.
+ *
+ * @param string $address Server address
+ * @return $this
+ */
+ public function setAddress($address)
+ {
+ $this->address = (string) $address;
+ if (false !== ($host = parse_url($address, PHP_URL_HOST))) {
+ $this->setTo($host);
+ }
+ return $this;
+ }
+
+ /**
+ * Get connection object.
+ *
+ * @return ConnectionInterface
+ */
+ public function getConnection()
+ {
+ return $this->connection;
+ }
+
+ /**
+ * Set connection object.
+ *
+ * @param ConnectionInterface $connection
+ * @return $this
+ */
+ public function setConnection(ConnectionInterface $connection)
+ {
+ $this->connection = $connection;
+ return $this;
+ }
+
+ /**
+ * Get logger instance.
+ *
+ * @return LoggerInterface
+ */
+ public function getLogger()
+ {
+ return $this->logger;
+ }
+
+ /**
+ * Set logger instance.
+ *
+ * @param \Psr\Log\LoggerInterface $logger PSR-3 Logger
+ * @return $this
+ */
+ public function setLogger(LoggerInterface $logger)
+ {
+ $this->logger = $logger;
+ return $this;
+ }
+
+ /**
+ * Get server name.
+ *
+ * @return string
+ */
+ public function getTo()
+ {
+ return $this->to;
+ }
+
+ /**
+ * Set server name.
+ *
+ * This value is send to the server in requests as to="" attribute.
+ *
+ * @param string $to
+ * @return $this
+ */
+ public function setTo($to)
+ {
+ $this->to = (string) $to;
+ return $this;
+ }
+
+ /**
+ * Get username.
+ *
+ * @return string
+ */
+ public function getUsername()
+ {
+ return $this->username;
+ }
+
+ /**
+ * Set username.
+ *
+ * @param string $username
+ * @return $this
+ */
+ public function setUsername($username)
+ {
+ $this->username = (string) $username;
+ return $this;
+ }
+
+ /**
+ * Get password.
+ *
+ * @return string
+ */
+ public function getPassword()
+ {
+ return $this->password;
+ }
+
+ /**
+ * Set password.
+ *
+ * @param string $password
+ * @return $this
+ */
+ public function setPassword($password)
+ {
+ $this->password = (string) $password;
+ return $this;
+ }
+
+ /**
+ * Get users jid.
+ *
+ * @return string
+ */
+ public function getJid()
+ {
+ return $this->jid;
+ }
+
+ /**
+ * Set users jid.
+ *
+ * @param string $jid
+ * @return $this
+ */
+ public function setJid($jid)
+ {
+ $this->jid = (string) $jid;
+ return $this;
+ }
+
+ /**
+ * Is user authenticated.
+ *
+ * @return boolean
+ */
+ public function isAuthenticated()
+ {
+ return $this->authenticated;
+ }
+
+ /**
+ * Set authenticated.
+ *
+ * @param boolean $authenticated Flag
+ * @return $this
+ */
+ public function setAuthenticated($authenticated)
+ {
+ $this->authenticated = (bool) $authenticated;
+ return $this;
+ }
+
+ /**
+ * Get users.
+ *
+ * @return Protocol\User\User[]
+ */
+ public function getUsers()
+ {
+ return $this->users;
+ }
+
+ /**
+ * Set users.
+ *
+ * @param array $users User list
+ * @return $this
+ */
+ public function setUsers(array $users)
+ {
+ $this->users = $users;
+ return $this;
+ }
+
+ /**
+ * Get authentication classes.
+ *
+ * @return array
+ */
+ public function getAuthenticationClasses()
+ {
+ return $this->authenticationClasses;
+ }
+
+ /**
+ *
+ * @param array $authenticationClasses Authentication classes
+ * @return $this
+ */
+ public function setAuthenticationClasses(array $authenticationClasses)
+ {
+ $this->authenticationClasses = $authenticationClasses;
+ return $this;
+ }
+
+ /**
+ * Get timeout for connection.
+ *
+ * @return integer
+ */
+ public function getTimeout()
+ {
+ return $this->timeout;
+ }
+
+ /**
+ * Set timeout for connection.
+ *
+ * @param integer $timeout Seconds
+ * @return \Fabiang\Xmpp\Options
+ */
+ public function setTimeout($timeout)
+ {
+ $this->timeout = (int) $timeout;
+ return $this;
+ }
+}
diff --git a/sources/vendor/fabiang/xmpp/src/OptionsAwareInterface.php b/sources/vendor/fabiang/xmpp/src/OptionsAwareInterface.php
new file mode 100644
index 0000000..7fa3415
--- /dev/null
+++ b/sources/vendor/fabiang/xmpp/src/OptionsAwareInterface.php
@@ -0,0 +1,61 @@
+
+ * @copyright 2014 Fabian Grutschus. All rights reserved.
+ * @license BSD
+ * @link http://github.com/fabiang/xmpp
+ */
+
+namespace Fabiang\Xmpp;
+
+/**
+ * Classes that take options should implent this interface.
+ *
+ * @package Xmpp
+ */
+interface OptionsAwareInterface
+{
+
+ /**
+ * Set options.
+ *
+ * @param Options $options
+ * @return $this
+ */
+ public function setOptions(Options $options);
+
+ /**
+ * Get options.
+ *
+ * @return Options
+ */
+ public function getOptions();
+}
diff --git a/sources/vendor/fabiang/xmpp/src/Protocol/DefaultImplementation.php b/sources/vendor/fabiang/xmpp/src/Protocol/DefaultImplementation.php
new file mode 100644
index 0000000..93a4b58
--- /dev/null
+++ b/sources/vendor/fabiang/xmpp/src/Protocol/DefaultImplementation.php
@@ -0,0 +1,138 @@
+
+ * @copyright 2014 Fabian Grutschus. All rights reserved.
+ * @license BSD
+ * @link http://github.com/fabiang/xmpp
+ */
+
+namespace Fabiang\Xmpp\Protocol;
+
+use Fabiang\Xmpp\Options;
+use Fabiang\Xmpp\EventListener\EventListenerInterface;
+use Fabiang\Xmpp\Event\EventManagerInterface;
+use Fabiang\Xmpp\Event\EventManager;
+use Fabiang\Xmpp\EventListener\Stream\Stream;
+use Fabiang\Xmpp\EventListener\Stream\StreamError;
+use Fabiang\Xmpp\EventListener\Stream\StartTls;
+use Fabiang\Xmpp\EventListener\Stream\Authentication;
+use Fabiang\Xmpp\EventListener\Stream\Bind;
+use Fabiang\Xmpp\EventListener\Stream\Session;
+use Fabiang\Xmpp\EventListener\Stream\Roster as RosterListener;
+
+/**
+ * Default Protocol implementation.
+ *
+ * @package Xmpp\Protocol
+ */
+class DefaultImplementation implements ImplementationInterface
+{
+
+ /**
+ * Options.
+ *
+ * @var Options
+ */
+ protected $options;
+
+ /**
+ * Eventmanager.
+ *
+ * @var EventManagerInterface
+ */
+ protected $events;
+
+ /**
+ * {@inheritDoc}
+ */
+ public function register()
+ {
+ $this->registerListener(new Stream);
+ $this->registerListener(new StreamError);
+ $this->registerListener(new StartTls);
+ $this->registerListener(new Authentication);
+ $this->registerListener(new Bind);
+ $this->registerListener(new Session);
+ $this->registerListener(new RosterListener);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function registerListener(EventListenerInterface $eventListener)
+ {
+ $connection = $this->getOptions()->getConnection();
+
+ $eventListener->setEventManager($this->getEventManager())
+ ->setOptions($this->getOptions())
+ ->attachEvents();
+
+ $connection->addListener($eventListener);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function getOptions()
+ {
+ return $this->options;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function setOptions(Options $options)
+ {
+ $this->options = $options;
+ return $this;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function getEventManager()
+ {
+ if (null === $this->events) {
+ $this->setEventManager(new EventManager());
+ }
+
+ return $this->events;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function setEventManager(EventManagerInterface $events)
+ {
+ $this->events = $events;
+ return $this;
+ }
+}
diff --git a/sources/vendor/fabiang/xmpp/src/Protocol/ImplementationInterface.php b/sources/vendor/fabiang/xmpp/src/Protocol/ImplementationInterface.php
new file mode 100644
index 0000000..a83332d
--- /dev/null
+++ b/sources/vendor/fabiang/xmpp/src/Protocol/ImplementationInterface.php
@@ -0,0 +1,65 @@
+
+ * @copyright 2014 Fabian Grutschus. All rights reserved.
+ * @license BSD
+ * @link http://github.com/fabiang/xmpp
+ */
+
+namespace Fabiang\Xmpp\Protocol;
+
+use Fabiang\Xmpp\OptionsAwareInterface;
+use Fabiang\Xmpp\EventListener\EventListenerInterface;
+use Fabiang\Xmpp\Event\EventManagerAwareInterface;
+
+/**
+ * Protocol implementation interface.
+ *
+ * @package Xmpp\Protocol
+ */
+interface ImplementationInterface extends OptionsAwareInterface, EventManagerAwareInterface
+{
+
+ /**
+ * Register listeners that implement xmpp protocol.
+ *
+ * @return void
+ */
+ public function register();
+
+ /**
+ * Register a listener.
+ *
+ * @param EventListenerInterface $eventListener Event listener
+ * @return $this
+ */
+ public function registerListener(EventListenerInterface $eventListener);
+}
diff --git a/sources/vendor/fabiang/xmpp/src/Protocol/Message.php b/sources/vendor/fabiang/xmpp/src/Protocol/Message.php
new file mode 100644
index 0000000..eda3887
--- /dev/null
+++ b/sources/vendor/fabiang/xmpp/src/Protocol/Message.php
@@ -0,0 +1,173 @@
+
+ * @copyright 2014 Fabian Grutschus. All rights reserved.
+ * @license BSD
+ * @link http://github.com/fabiang/xmpp
+ */
+
+namespace Fabiang\Xmpp\Protocol;
+
+use Fabiang\Xmpp\Util\XML;
+
+/**
+ * Protocol setting for Xmpp.
+ *
+ * @package Xmpp\Protocol
+ */
+class Message implements ProtocolImplementationInterface
+{
+ /**
+ * Chat between to users.
+ */
+
+ const TYPE_CHAT = 'chat';
+
+ /**
+ * Chat in a multi-user channel (MUC).
+ */
+ const TYPE_GROUPCHAT = 'groupchat';
+
+ /**
+ * Message type.
+ *
+ * @var string
+ */
+ protected $type = self::TYPE_CHAT;
+
+ /**
+ * Set message receiver.
+ *
+ * @var string
+ */
+ protected $to;
+
+ /**
+ * Message.
+ *
+ * @var string
+ */
+ protected $message = '';
+
+ /**
+ * Constructor.
+ *
+ * @param string $message
+ * @param string $to
+ * @param string $type
+ */
+ public function __construct($message = '', $to = '', $type = self::TYPE_CHAT)
+ {
+ $this->setMessage($message)->setTo($to)->setType($type);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function toString()
+ {
+ return XML::quoteMessage(
+ '%s',
+ $this->getType(),
+ XML::generateId(),
+ $this->getTo(),
+ $this->getMessage()
+ );
+ }
+
+ /**
+ * Get message type.
+ *
+ * @return string
+ */
+ public function getType()
+ {
+ return $this->type;
+ }
+
+ /**
+ * Set message type.
+ *
+ * See {@link self::TYPE_CHAT} and {@link self::TYPE_GROUPCHAT}
+ *
+ * @param string $type
+ * @return $this
+ */
+ public function setType($type)
+ {
+ $this->type = $type;
+ return $this;
+ }
+
+ /**
+ * Get message receiver.
+ *
+ * @return string
+ */
+ public function getTo()
+ {
+ return $this->to;
+ }
+
+ /**
+ * Set message receiver.
+ *
+ * @param string $to
+ * @return $this
+ */
+ public function setTo($to)
+ {
+ $this->to = (string) $to;
+ return $this;
+ }
+
+ /**
+ * Get message.
+ *
+ * @return string
+ */
+ public function getMessage()
+ {
+ return $this->message;
+ }
+
+ /**
+ * Set message.
+ *
+ * @param string $message
+ * @return $this
+ */
+ public function setMessage($message)
+ {
+ $this->message = (string) $message;
+ return $this;
+ }
+}
diff --git a/sources/vendor/fabiang/xmpp/src/Protocol/Presence.php b/sources/vendor/fabiang/xmpp/src/Protocol/Presence.php
new file mode 100644
index 0000000..93cf800
--- /dev/null
+++ b/sources/vendor/fabiang/xmpp/src/Protocol/Presence.php
@@ -0,0 +1,226 @@
+
+ * @copyright 2014 Fabian Grutschus. All rights reserved.
+ * @license BSD
+ * @link http://github.com/fabiang/xmpp
+ */
+
+namespace Fabiang\Xmpp\Protocol;
+
+use Fabiang\Xmpp\Util\XML;
+
+/**
+ * Protocol setting for Xmpp.
+ *
+ * @package Xmpp\Protocol
+ */
+class Presence implements ProtocolImplementationInterface
+{
+ /**
+ * Signals that the entity is available for communication.
+ */
+
+ const TYPE_AVAILABLE = 'available';
+
+ /**
+ * Signals that the entity is no longer available for communication.
+ */
+ const TYPE_UNAVAILABLE = 'unavailable';
+
+ /**
+ * The sender wishes to subscribe to the recipient's presence.
+ */
+ const TYPE_SUBSCRIBE = 'subscribe';
+
+ /**
+ * The sender has allowed the recipient to receive their presence.
+ */
+ const TYPE_SUBSCRIBED = 'subscribed';
+
+ /**
+ * The sender is unsubscribing from another entity's presence.
+ */
+ const TYPE_UNSUBSCRIBE = 'unsubscribe';
+
+ /**
+ * The subscription request has been denied or a previously-granted subscription has been cancelled.
+ */
+ const TYPE_UNSUBSCRIBED = 'unsubscribed';
+
+ /**
+ * A request for an entity's current presence; SHOULD be generated only by a server on behalf of a user.
+ */
+ const TYPE_PROBE = 'probe';
+
+ /**
+ * An error has occurred regarding processing or delivery of a previously-sent presence stanza.
+ */
+ const TYPE_ERROR = 'error';
+
+ /**
+ * The entity or resource is available.
+ */
+ const SHOW_AVAILABLE = 'available';
+
+ /**
+ * The entity or resource is temporarily away.
+ */
+ const SHOW_AWAY = 'away';
+
+ /**
+ * The entity or resource is actively interested in chatting.
+ */
+ const SHOW_CHAT = 'chat';
+
+ /**
+ * The entity or resource is busy (dnd = "Do Not Disturb").
+ */
+ const SHOW_DND = 'dnd';
+
+ /**
+ * The entity or resource is away for an extended period (xa = "eXtended Away").
+ */
+ const SHOW_XA = 'xa';
+
+ /**
+ * Presence to.
+ *
+ * @var string|null
+ */
+ protected $to;
+
+ /**
+ * Priority.
+ *
+ * @var integer
+ */
+ protected $priority = 1;
+
+ /**
+ * Nickname for presence.
+ *
+ * @var string
+ */
+ protected $nickname;
+
+ /**
+ * Constructor.
+ *
+ * @param integer $priority
+ * @param string $to
+ * @param string $nickname
+ */
+ public function __construct($priority = 1, $to = null, $nickname = null)
+ {
+ $this->setPriority($priority)->setTo($to)->setNickname($nickname);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function toString()
+ {
+ $presence = 'getTo()) {
+ $presence .= ' to="' . XML::quote($this->getTo()) . '/' . XML::quote($this->getNickname()) . '"';
+ }
+
+ return $presence . '>' . $this->getPriority() . '';
+ }
+
+ /**
+ * Get nickname.
+ *
+ * @return string
+ */
+ public function getNickname()
+ {
+ return $this->nickname;
+ }
+
+ /**
+ * Set nickname.
+ *
+ * @param string $nickname
+ * @return $this
+ */
+ public function setNickname($nickname)
+ {
+ $this->nickname = (string) $nickname;
+ return $this;
+ }
+
+ /**
+ * Get to.
+ *
+ * @return string¦null
+ */
+ public function getTo()
+ {
+ return $this->to;
+ }
+
+ /**
+ * Set to.
+ *
+ * @param string|null $to
+ * @return $this
+ */
+ public function setTo($to = null)
+ {
+ $this->to = $to;
+ return $this;
+ }
+
+ /**
+ * Get priority.
+ *
+ * @return integer
+ */
+ public function getPriority()
+ {
+ return $this->priority;
+ }
+
+ /**
+ * Set priority.
+ *
+ * @param integer $priority
+ * @return $this
+ */
+ public function setPriority($priority)
+ {
+ $this->priority = (int) $priority;
+ return $this;
+ }
+}
diff --git a/sources/vendor/fabiang/xmpp/src/Protocol/ProtocolImplementationInterface.php b/sources/vendor/fabiang/xmpp/src/Protocol/ProtocolImplementationInterface.php
new file mode 100644
index 0000000..c0f918d
--- /dev/null
+++ b/sources/vendor/fabiang/xmpp/src/Protocol/ProtocolImplementationInterface.php
@@ -0,0 +1,53 @@
+
+ * @copyright 2014 Fabian Grutschus. All rights reserved.
+ * @license BSD
+ * @link http://github.com/fabiang/xmpp
+ */
+
+namespace Fabiang\Xmpp\Protocol;
+
+/**
+ * Protocol setting for Xmpp.
+ *
+ * @package Xmpp\Protocol
+ */
+interface ProtocolImplementationInterface
+{
+
+ /**
+ * Protocol implementations should be turned into an string.
+ *
+ * @return string
+ */
+ public function toString();
+}
diff --git a/sources/vendor/fabiang/xmpp/src/Protocol/Roster.php b/sources/vendor/fabiang/xmpp/src/Protocol/Roster.php
new file mode 100644
index 0000000..874ef90
--- /dev/null
+++ b/sources/vendor/fabiang/xmpp/src/Protocol/Roster.php
@@ -0,0 +1,56 @@
+
+ * @copyright 2014 Fabian Grutschus. All rights reserved.
+ * @license BSD
+ * @link http://github.com/fabiang/xmpp
+ */
+
+namespace Fabiang\Xmpp\Protocol;
+
+use Fabiang\Xmpp\Util\XML;
+
+/**
+ * Protocol setting for Xmpp.
+ *
+ * @package Xmpp\Protocol
+ */
+class Roster implements ProtocolImplementationInterface
+{
+
+ /**
+ * {@inheritDoc}
+ */
+ public function toString()
+ {
+ return '';
+ }
+}
diff --git a/sources/vendor/fabiang/xmpp/src/Protocol/User/User.php b/sources/vendor/fabiang/xmpp/src/Protocol/User/User.php
new file mode 100644
index 0000000..ba78d8c
--- /dev/null
+++ b/sources/vendor/fabiang/xmpp/src/Protocol/User/User.php
@@ -0,0 +1,124 @@
+
+ * @copyright 2014 Fabian Grutschus. All rights reserved.
+ * @license BSD
+ * @link http://github.com/fabiang/xmpp
+ */
+
+namespace Fabiang\Xmpp\Protocol\User;
+
+/**
+ * User object.
+ *
+ * @package Xmpp\Protocol
+ */
+class User
+{
+
+ /**
+ *
+ * @var string
+ */
+ protected $name;
+
+ /**
+ *
+ * @var string
+ */
+ protected $jid;
+
+ /**
+ *
+ * @var string
+ */
+ protected $subscription;
+
+ /**
+ *
+ * @var array
+ */
+ protected $groups = array();
+
+ public function getName()
+ {
+ return $this->name;
+ }
+
+ public function setName($name = null)
+ {
+ if (null === $name || '' === $name) {
+ $this->name = null;
+ } else {
+ $this->name = $name;
+ }
+ return $this;
+ }
+
+ public function getJid()
+ {
+ return $this->jid;
+ }
+
+ public function setJid($jid)
+ {
+ $this->jid = (string) $jid;
+ return $this;
+ }
+
+ public function getSubscription()
+ {
+ return $this->subscription;
+ }
+
+ public function setSubscription($subscription)
+ {
+ $this->subscription = (string) $subscription;
+ return $this;
+ }
+
+ public function getGroups()
+ {
+ return $this->groups;
+ }
+
+ public function setGroups(array $groups)
+ {
+ $this->groups = $groups;
+ return $this;
+ }
+
+ public function addGroup($group)
+ {
+ $this->groups[] = (string) $group;
+ return $this;
+ }
+}
diff --git a/sources/vendor/fabiang/xmpp/src/Stream/SocketClient.php b/sources/vendor/fabiang/xmpp/src/Stream/SocketClient.php
new file mode 100644
index 0000000..afba977
--- /dev/null
+++ b/sources/vendor/fabiang/xmpp/src/Stream/SocketClient.php
@@ -0,0 +1,214 @@
+
+ * @copyright 2014 Fabian Grutschus. All rights reserved.
+ * @license BSD
+ * @link http://github.com/fabiang/xmpp
+ */
+
+namespace Fabiang\Xmpp\Stream;
+
+use Fabiang\Xmpp\Exception\InvalidArgumentException;
+use Fabiang\Xmpp\Util\ErrorHandler;
+
+/**
+ * Stream functions wrapper class.
+ *
+ * @package Xmpp\Stream
+ */
+class SocketClient
+{
+
+ const BUFFER_LENGTH = 4096;
+
+ /**
+ * Resource.
+ *
+ * @var resource
+ */
+ protected $resource;
+
+ /**
+ * Address.
+ *
+ * @var string
+ */
+ protected $address;
+
+ /**
+ * Constructor takes address as argument.
+ *
+ * @param string $address
+ */
+ public function __construct($address)
+ {
+ $this->address = $address;
+ }
+
+ /**
+ * Connect.
+ *
+ * @param integer $timeout Timeout for connection
+ * @param boolean $persistent Persitent connection
+ * @return void
+ */
+ public function connect($timeout = 30, $persistent = false)
+ {
+ if (true === $persistent) {
+ $flags = STREAM_CLIENT_CONNECT | STREAM_CLIENT_PERSISTENT;
+ } else {
+ $flags = STREAM_CLIENT_CONNECT;
+ }
+
+ // call stream_socket_client with custom error handler enabled
+ $handler = new ErrorHandler(
+ function ($address, $timeout, $flags) {
+ return stream_socket_client($address, $errno, $errstr, $timeout, $flags);
+ },
+ $this->address,
+ $timeout,
+ $flags
+ );
+ $resource = $handler->execute(__FILE__, __LINE__);
+
+ stream_set_timeout($resource, $timeout);
+ $this->resource = $resource;
+ }
+
+ /**
+ * Reconnect and optionally use different address.
+ *
+ * @param string $address
+ * @param integer $timeout
+ * @param bool $persistent
+ */
+ public function reconnect($address = null, $timeout = 30, $persistent = false)
+ {
+ $this->close();
+
+ if (null !== $this->address) {
+ $this->address = $address;
+ }
+
+ $this->connect($timeout, $persistent);
+ }
+
+ /**
+ * Close stream.
+ *
+ * @return void
+ */
+ public function close()
+ {
+ fclose($this->resource);
+ }
+
+ /**
+ * Set stream blocking mode.
+ *
+ * @param boolean $flag Flag
+ * @return $this
+ */
+ public function setBlocking($flag = true)
+ {
+ stream_set_blocking($this->resource, (int) $flag);
+ return $this;
+ }
+
+ /**
+ * Read from stream.
+ *
+ * @param integer $length Bytes to read
+ * @return string
+ */
+ public function read($length = self::BUFFER_LENGTH)
+ {
+ return fread($this->resource, $length);
+ }
+
+ /**
+ * Write to stream.
+ *
+ * @param string $string String
+ * @param integer $length Limit
+ * @return void
+ */
+ public function write($string, $length = null)
+ {
+ if (null !== $length) {
+ fwrite($this->resource, $string, $length);
+ } else {
+ fwrite($this->resource, $string);
+ }
+ }
+
+ /**
+ * Enable/disable cryptography on stream.
+ *
+ * @param boolean $enable Flag
+ * @param integer $cryptoType One of the STREAM_CRYPTO_METHOD_* constants.
+ * @return void
+ * @throws InvalidArgumentException
+ */
+ public function crypto($enable, $cryptoType = null)
+ {
+ if (false === $enable) {
+ $handler = new ErrorHandler('stream_socket_enable_crypto', $this->resource, false);
+ return $handler->execute(__FILE__, __LINE__);
+ }
+
+ if (null === $cryptoType) {
+ throw new InvalidArgumentException('Second argument is require when enabling crypto an stream');
+ }
+
+ return stream_socket_enable_crypto($this->resource, $enable, $cryptoType);
+ }
+
+ /**
+ * Get socket stream.
+ *
+ * @return resource
+ */
+ public function getResource()
+ {
+ return $this->resource;
+ }
+
+ /**
+ * Return address.
+ *
+ * @return string
+ */
+ public function getAddress()
+ {
+ return $this->address;
+ }
+}
diff --git a/sources/vendor/fabiang/xmpp/src/Stream/XMLStream.php b/sources/vendor/fabiang/xmpp/src/Stream/XMLStream.php
new file mode 100644
index 0000000..72e26e4
--- /dev/null
+++ b/sources/vendor/fabiang/xmpp/src/Stream/XMLStream.php
@@ -0,0 +1,443 @@
+
+ * @copyright 2014 Fabian Grutschus. All rights reserved.
+ * @license BSD
+ * @link http://github.com/fabiang/xmpp
+ */
+
+namespace Fabiang\Xmpp\Stream;
+
+use Fabiang\Xmpp\Event\EventManagerAwareInterface;
+use Fabiang\Xmpp\Event\EventManagerInterface;
+use Fabiang\Xmpp\Event\EventManager;
+use Fabiang\Xmpp\Event\XMLEvent;
+use Fabiang\Xmpp\Event\XMLEventInterface;
+use Fabiang\Xmpp\Exception\XMLParserException;
+
+/**
+ * Xml stream class.
+ *
+ * @package Xmpp\Stream
+ */
+class XMLStream implements EventManagerAwareInterface
+{
+
+ const NAMESPACE_SEPARATOR = ':';
+
+ /**
+ * Eventmanager.
+ *
+ * @var EventManagerInterface
+ */
+ protected $events;
+
+ /**
+ * Document encoding.
+ *
+ * @var string
+ */
+ protected $encoding;
+
+ /**
+ * Current parsing depth.
+ *
+ * @var integer
+ */
+ protected $depth = 0;
+
+ /**
+ *
+ * @var \DOMDocument
+ */
+ protected $document;
+
+ /**
+ * Collected namespaces.
+ *
+ * @var array
+ */
+ protected $namespaces = array();
+
+ /**
+ * Cache of namespace prefixes.
+ *
+ * @var array
+ */
+ protected $namespacePrefixes = array();
+
+ /**
+ * Element cache.
+ *
+ * @var array
+ */
+ protected $elements = array();
+
+ /**
+ * XML parser.
+ *
+ * @var resource
+ */
+ protected $parser;
+
+ /**
+ * Event object.
+ *
+ * @var XMLEventInterface
+ */
+ protected $eventObject;
+
+ /**
+ * Collected events while parsing.
+ *
+ * @var array
+ */
+ protected $eventCache = array();
+
+ /**
+ * Constructor.
+ */
+ public function __construct($encoding = 'UTF-8', XMLEventInterface $eventObject = null)
+ {
+ $this->encoding = $encoding;
+ $this->reset();
+
+ if (null === $eventObject) {
+ $eventObject = new XMLEvent();
+ }
+
+ $this->eventObject = $eventObject;
+ }
+
+ /**
+ * Free XML parser on desturct.
+ */
+ public function __destruct()
+ {
+ xml_parser_free($this->parser);
+ }
+
+ /**
+ * Parse XML data and trigger events.
+ *
+ * @param string $source XML source
+ * @return \DOMDocument
+ */
+ public function parse($source)
+ {
+ $this->clearDocument($source);
+
+ $this->eventCache = array();
+ if (0 === xml_parse($this->parser, $source, false)) {
+ throw XMLParserException::create($this->parser);
+ }
+ // trigger collected events.
+ $this->trigger();
+ $this->eventCache = array();
+
+ // was not there, so lets close the document
+ if ($this->depth > 0) {
+ $this->document->appendChild($this->elements[0]);
+ }
+
+ return $this->document;
+ }
+
+ /**
+ * Clear document.
+ *
+ * Method resets the parser instance if document->documentElement;
+
+ // collect xml declaration
+ if ('reset();
+
+ $matches = array();
+ if (preg_match('/^<\?xml.*encoding=(\'|")([\w-]+)\1.*?>/i', $source, $matches)) {
+ $this->encoding = $matches[2];
+ xml_parser_set_option($this->parser, XML_OPTION_TARGET_ENCODING, $this->encoding);
+ }
+ } elseif (null !== $documentElement) {
+ // clean the document
+ /* @var $childNode \DOMNode */
+ while ($documentElement->hasChildNodes()) {
+ $documentElement->removeChild($documentElement->firstChild);
+ }
+ }
+ }
+
+ /**
+ * Starting tag found.
+ *
+ * @param resource $parser XML parser
+ * @param string $name Element name
+ * @param attribs $attribs Element attributes
+ * @return void
+ */
+ protected function startXml()
+ {
+ list (, $name, $attribs) = func_get_args();
+
+ $elementData = explode(static::NAMESPACE_SEPARATOR, $name, 2);
+ $elementName = $elementData[0];
+ $prefix = null;
+ if (isset($elementData[1])) {
+ $elementName = $elementData[1];
+ $prefix = $elementData[0];
+ }
+
+ $attributesNodes = $this->createAttributeNodes($attribs);
+ $namespaceAttrib = false;
+
+ // current namespace
+ if (array_key_exists('xmlns', $attribs)) {
+ $namespaceURI = $attribs['xmlns'];
+ } else {
+ $namespaceURI = $this->namespaces[$this->depth - 1];
+ }
+
+ // namespace of the element
+ if (null !== $prefix) {
+ $namespaceElement = $this->namespacePrefixes[$prefix];
+ } else {
+ $namespaceAttrib = true;
+ $namespaceElement = $namespaceURI;
+ }
+
+ $this->namespaces[$this->depth] = $namespaceURI;
+
+ // workaround for multiple xmlns defined, since we did have parent element inserted into the dom tree yet
+ if (true === $namespaceAttrib) {
+ $element = $this->document->createElement($elementName);
+ } else {
+ $elementNameFull = $elementName;
+ if (null !== $prefix) {
+ $elementNameFull = $prefix . static::NAMESPACE_SEPARATOR . $elementName;
+ }
+
+ $element = $this->document->createElementNS($namespaceElement, $elementNameFull);
+ }
+
+ foreach ($attributesNodes as $attributeNode) {
+ $element->setAttributeNode($attributeNode);
+ }
+
+ $this->elements[$this->depth] = $element;
+ $this->depth++;
+
+ $event = '{' . $namespaceElement . '}' . $elementName;
+ $this->cacheEvent($event, true, array($element));
+ }
+
+ /**
+ * Turn attribes into attribute nodes.
+ *
+ * @param array $attribs Attributes
+ * @return array
+ */
+ protected function createAttributeNodes(array $attribs)
+ {
+ $attributesNodes = array();
+ foreach ($attribs as $name => $value) {
+ // collect namespace prefixes
+ if ('xmlns:' === substr($name, 0, 6)) {
+ $prefix = substr($name, 6);
+
+ $this->namespacePrefixes[$prefix] = $value;
+ } else {
+ $attribute = $this->document->createAttribute($name);
+ $attribute->value = $value;
+ $attributesNodes[] = $attribute;
+ }
+ }
+ return $attributesNodes;
+ }
+
+ /**
+ * End tag found.
+ *
+ * @return void
+ */
+ protected function endXml()
+ {
+ $this->depth--;
+
+ $element = $this->elements[$this->depth];
+
+ if ($this->depth > 0) {
+ $parent = $this->elements[$this->depth - 1];
+ } else {
+ $parent = $this->document;
+ }
+ $parent->appendChild($element);
+
+ $localName = $element->localName;
+
+ // Frist: try to get the namespace from element.
+ $namespaceURI = $element->namespaceURI;
+
+ // Second: loop over namespaces till namespace is not null
+ if (null === $namespaceURI) {
+ $namespaceURI = $this->namespaces[$this->depth];
+ }
+
+ $event = '{' . $namespaceURI . '}' . $localName;
+ $this->cacheEvent($event, false, array($element));
+ }
+
+ /**
+ * Data found.
+ *
+ * @param resource $parser XML parser
+ * @param string $data Element data
+ * @return void
+ */
+ protected function dataXml()
+ {
+ $data = func_get_arg(1);
+ if (isset($this->elements[$this->depth - 1])) {
+ $element = $this->elements[$this->depth - 1];
+ $element->appendChild($this->document->createTextNode($data));
+ }
+ }
+
+ /**
+ * Add event to cache.
+ *
+ * @param string $event
+ * @param boolean $startTag
+ * @param array $params
+ * @return void
+ */
+ protected function cacheEvent($event, $startTag, $params)
+ {
+ $this->eventCache[] = array($event, $startTag, $params);
+ }
+
+ /**
+ * Trigger cached events
+ *
+ * @return void
+ */
+ protected function trigger()
+ {
+ foreach ($this->eventCache as $event) {
+ list($event, $startTag, $param) = $event;
+ $this->eventObject->setStartTag($startTag);
+ $this->getEventManager()->setEventObject($this->eventObject);
+ $this->getEventManager()->trigger($event, $this, $param);
+ }
+ }
+
+ /**
+ * Reset class properties.
+ *
+ * @return void
+ */
+ public function reset()
+ {
+ $parser = xml_parser_create($this->encoding);
+ xml_set_object($parser, $this);
+
+ xml_parser_set_option($parser, XML_OPTION_CASE_FOLDING, 0);
+ xml_parser_set_option($parser, XML_OPTION_SKIP_WHITE, 1);
+
+ xml_set_element_handler($parser, 'startXml', 'endXml');
+ xml_set_character_data_handler($parser, 'dataXml');
+
+ $this->parser = $parser;
+ $this->depth = 0;
+ $this->document = new \DOMDocument('1.0', $this->encoding);
+ $this->namespaces = array();
+ $this->namespacePrefixes = array();
+ $this->elements = array();
+ }
+
+ /**
+ * Get XML parser resource.
+ *
+ * @return resource
+ */
+ public function getParser()
+ {
+ return $this->parser;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function getEventManager()
+ {
+ if (null === $this->events) {
+ $this->setEventManager(new EventManager());
+ }
+
+ return $this->events;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function setEventManager(EventManagerInterface $events)
+ {
+ $this->events = $events;
+ $events->setEventObject($this->getEventObject());
+ return $this;
+ }
+
+ /**
+ * Get event object.
+ *
+ * @return XMLEventInterface
+ */
+ public function getEventObject()
+ {
+ return $this->eventObject;
+ }
+
+ /**
+ * Set event object.
+ *
+ * @param XMLEventInterface $eventObject
+ * @return $this
+ */
+ public function setEventObject(XMLEventInterface $eventObject)
+ {
+ $this->eventObject = $eventObject;
+ return $this;
+ }
+}
diff --git a/sources/vendor/fabiang/xmpp/src/Util/ErrorHandler.php b/sources/vendor/fabiang/xmpp/src/Util/ErrorHandler.php
new file mode 100644
index 0000000..cf63cad
--- /dev/null
+++ b/sources/vendor/fabiang/xmpp/src/Util/ErrorHandler.php
@@ -0,0 +1,100 @@
+
+ * @copyright 2014 Fabian Grutschus. All rights reserved.
+ * @license BSD
+ * @link http://github.com/fabiang/xmpp
+ */
+
+namespace Fabiang\Xmpp\Util;
+
+use Fabiang\Xmpp\Exception\InvalidArgumentException;
+use Fabiang\Xmpp\Exception\ErrorException;
+
+/**
+ * XML utility methods.
+ *
+ * @package Xmpp\Util
+ */
+class ErrorHandler
+{
+
+ /**
+ * Method to be called.
+ *
+ * @var callable
+ */
+ protected $method;
+
+ /**
+ * Arguments for method.
+ *
+ * @var array
+ */
+ protected $arguments = array();
+
+ public function __construct($method)
+ {
+ if (!is_callable($method)) {
+ throw new InvalidArgumentException('Argument 1 of "' . __METHOD__ . '" must be a callable');
+ }
+
+ $arguments = func_get_args();
+ array_shift($arguments);
+
+ $this->method = $method;
+ $this->arguments = $arguments;
+ }
+
+ /**
+ * Execute a function and handle all types of errors.
+ *
+ * @param string $file
+ * @param int $line
+ * @return mixed
+ * @throws ErrorException
+ */
+ public function execute($file, $line)
+ {
+ set_error_handler(function ($errno, $errstr) use ($file, $line) {
+ throw new ErrorException($errstr, 0, $errno, $file, $line);
+ });
+
+ try {
+ $value = call_user_func_array($this->method, $this->arguments);
+ restore_error_handler();
+ return $value;
+ } catch (ErrorException $exception) {
+ restore_error_handler();
+ throw $exception;
+ }
+ }
+}
diff --git a/sources/vendor/fabiang/xmpp/src/Util/XML.php b/sources/vendor/fabiang/xmpp/src/Util/XML.php
new file mode 100644
index 0000000..3b6c377
--- /dev/null
+++ b/sources/vendor/fabiang/xmpp/src/Util/XML.php
@@ -0,0 +1,128 @@
+
+ * @copyright 2014 Fabian Grutschus. All rights reserved.
+ * @license BSD
+ * @link http://github.com/fabiang/xmpp
+ */
+
+namespace Fabiang\Xmpp\Util;
+
+/**
+ * XML utility methods.
+ *
+ * @package Xmpp\Util
+ */
+class XML
+{
+
+ /**
+ * Quote XML string.
+ *
+ * @param string $string String to be quoted
+ * @param string $encoding Encoding used for quotation
+ * @return string
+ */
+ public static function quote($string, $encoding = 'UTF-8')
+ {
+ $flags = ENT_QUOTES;
+
+ if (defined('ENT_XML1')) {
+ $flags |= ENT_XML1;
+ }
+
+ return htmlspecialchars($string, $flags, $encoding);
+ }
+
+ /**
+ * Replace variables in a string and quote them before.
+ *
+ * Hint: this function works like sprintf
+ *
+ * @param string $message
+ * @param mixed $args
+ * @param mixed $...
+ * @return string
+ */
+ public static function quoteMessage($message)
+ {
+ $variables = func_get_args();
+
+ // shift message variable
+ array_shift($variables);
+
+ // workaround for `static` call in a closure
+ $class = __CLASS__;
+
+ return vsprintf(
+ $message,
+ array_map(
+ function ($var) use ($class) {
+ return $class::quote($var);
+ },
+ $variables
+ )
+ );
+ }
+
+ /**
+ * Generate a unique id.
+ *
+ * @return string
+ */
+ public static function generateId()
+ {
+ return static::quote('fabiang_xmpp_' . uniqid());
+ }
+
+ /**
+ * Encode a string with Base64 and quote it.
+ *
+ * @param string $data
+ * @param string $encoding
+ * @return string
+ */
+ public static function base64Encode($data, $encoding = 'UTF-8')
+ {
+ return static::quote(base64_encode($data), $encoding);
+ }
+
+ /**
+ * Decode a Base64 encoded string.
+ *
+ * @param string $data
+ * @return string
+ */
+ public static function base64Decode($data)
+ {
+ return base64_decode($data);
+ }
+}
diff --git a/sources/vendor/fguillot/json-rpc/LICENSE b/sources/vendor/fguillot/json-rpc/LICENSE
new file mode 100644
index 0000000..6a362bc
--- /dev/null
+++ b/sources/vendor/fguillot/json-rpc/LICENSE
@@ -0,0 +1,21 @@
+The MIT License (MIT)
+
+Copyright (c) 2015 Frederic Guillot
+
+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.
diff --git a/sources/vendor/fguillot/json-rpc/README.markdown b/sources/vendor/fguillot/json-rpc/README.markdown
new file mode 100644
index 0000000..3915b7e
--- /dev/null
+++ b/sources/vendor/fguillot/json-rpc/README.markdown
@@ -0,0 +1,364 @@
+JsonRPC PHP Client and Server
+=============================
+
+A simple Json-RPC client/server that just works.
+
+[![Scrutinizer Code Quality](https://scrutinizer-ci.com/g/fguillot/JsonRPC/badges/quality-score.png?b=master)](https://scrutinizer-ci.com/g/fguillot/JsonRPC/?branch=master)
+
+[![Build Status](https://travis-ci.org/fguillot/JsonRPC.svg?branch=master)](https://travis-ci.org/fguillot/JsonRPC)
+
+Features
+--------
+
+- JSON-RPC 2.0 protocol only
+- The server support batch requests and notifications
+- Authentication and IP based client restrictions
+- Minimalist: there is only 2 files
+- Fully unit tested
+- Requirements: PHP >= 5.3.4
+- License: MIT
+
+Author
+------
+
+Frédéric Guillot
+
+Installation with Composer
+--------------------------
+
+```bash
+composer require fguillot/json-rpc @stable
+```
+
+Examples
+--------
+
+### Server
+
+Callback binding:
+
+```php
+register('addition', function ($a, $b) {
+ return $a + $b;
+});
+
+$server->register('random', function ($start, $end) {
+ return mt_rand($start, $end);
+});
+
+// Return the response to the client
+echo $server->execute();
+
+?>
+```
+
+Class/Method binding:
+
+```php
+bind('myProcedure', 'Api', 'doSomething');
+
+// Use a class instance instead of the class name
+$server->bind('mySecondProcedure', new Api, 'doSomething');
+
+// The procedure and the method are the same
+$server->bind('doSomething', 'Api');
+
+// Attach the class, client will be able to call directly Api::doSomething()
+$server->attach(new Api);
+
+echo $server->execute();
+
+?>
+```
+
+Before callback:
+
+Before each procedure execution, a custom method can be called.
+
+This method receive the following arguments: `$username, $password, $class, $method`.
+
+```php
+authentication(['myuser' => 'mypassword']);
+
+// Register the before callback
+$server->before('beforeProcedure');
+
+$server->attach(new Api);
+
+echo $server->execute();
+
+?>
+```
+
+You can use this method to implements a custom authentication system or anything else.
+If you would like to reject the authentication, you can throw the exception `JsonRPC\AuthenticationFailure`.
+
+### Client
+
+Example with positional parameters:
+
+```php
+execute('addition', [3, 5]);
+
+var_dump($result);
+```
+
+Example with named arguments:
+
+```php
+execute('random', ['end' => 10, 'start' => 1]);
+
+var_dump($result);
+```
+
+Arguments are called in the right order.
+
+Examples with shortcut methods:
+
+```php
+random(50, 100);
+
+var_dump($result);
+```
+
+The example above use positional arguments for the request and this one use named arguments:
+
+```php
+$result = $client->random(['end' => 10, 'start' => 1]);
+```
+
+### Client batch requests
+
+Call several procedures in a single HTTP request:
+
+```php
+batch()
+ ->foo(['arg1' => 'bar'])
+ ->random(1, 100)
+ ->add(4, 3)
+ ->execute('add', [2, 5])
+ ->send();
+
+print_r($results);
+```
+
+All results are stored at the same position of the call.
+
+### Client exceptions
+
+- `BadFunctionCallException`: Procedure not found on the server
+- `InvalidArgumentException`: Wrong procedure arguments
+- `JsonRPC\AccessDeniedException`: Access denied
+- `JsonRPC\ConnectionFailureException`: Connection failure
+- `JsonRPC\ServerErrorException`: Internal server error
+- `RuntimeException`: Protocol error
+
+### Enable client debugging
+
+You can enable the debug to see the JSON request and response:
+
+```php
+debug = true;
+```
+
+The debug output is sent to the PHP's system logger.
+You can configure the log destination in your `php.ini`.
+
+Output example:
+
+```json
+==> Request:
+{
+ "jsonrpc": "2.0",
+ "method": "removeCategory",
+ "id": 486782327,
+ "params": [
+ 1
+ ]
+}
+==> Response:
+{
+ "jsonrpc": "2.0",
+ "id": 486782327,
+ "result": true
+}
+```
+
+### IP based client restrictions
+
+The server can allow only some IP adresses:
+
+```php
+allowHosts(['192.168.0.1', '127.0.0.1']);
+
+// Procedures registration
+
+[...]
+
+// Return the response to the client
+echo $server->execute();
+```
+
+If the client is blocked, you got a 403 Forbidden HTTP response.
+
+### HTTP Basic Authentication
+
+If you use HTTPS, you can allow client by using a username/password.
+
+```php
+authentication(['user1' => 'password1', 'user2' => 'password2']);
+
+// Procedures registration
+
+[...]
+
+// Return the response to the client
+echo $server->execute();
+```
+
+On the client, set credentials like that:
+
+```php
+authentication('user1', 'password1');
+```
+
+If the authentication failed, the client throw a RuntimeException.
+
+Using an alternative authentication header:
+
+```php
+
+use JsonRPC\Server;
+
+$server = new Server;
+$server->setAuthenticationHeader('X-Authentication');
+$server->authentication(['myusername' => 'mypassword']);
+```
+
+The example above will use the HTTP header `X-Authentication` instead of the standard `Authorization: Basic [BASE64_CREDENTIALS]`.
+The username/password values need be encoded in base64: `base64_encode('username:password')`.
+
+### Exceptions
+
+If you want to send an error to the client you can throw an exception.
+You should configure which exceptions should be relayed to the client first:
+
+```php
+attachException('MyException');
+
+// Procedures registration
+
+[...]
+
+// Return the response to the client
+echo $server->execute();
+```
+
+Then you can throw that exception inside your procedure:
+
+```
+throw new MyException("An error occured", 123);
+```
+
+To relay all exceptions regardless of type, leave out the exception class name:
+
+```
+$server->attachException();
+```
diff --git a/sources/vendor/fguillot/json-rpc/src/JsonRPC/AccessDeniedException.php b/sources/vendor/fguillot/json-rpc/src/JsonRPC/AccessDeniedException.php
new file mode 100644
index 0000000..ad92746
--- /dev/null
+++ b/sources/vendor/fguillot/json-rpc/src/JsonRPC/AccessDeniedException.php
@@ -0,0 +1,7 @@
+db = $db;
+ }
+
+ /**
+ * Build the SQL condition
+ *
+ * @access public
+ * @return string
+ */
+ public function build()
+ {
+ return empty($this->conditions) ? '' : ' WHERE '.implode(' AND ', $this->conditions);
+ }
+
+ /**
+ * Get condition values
+ *
+ * @access public
+ * @return array
+ */
+ public function getValues()
+ {
+ return $this->values;
+ }
+
+ /**
+ * Returns true if there is some conditions
+ *
+ * @access public
+ * @return boolean
+ */
+ public function hasCondition()
+ {
+ return ! empty($this->conditions);
+ }
+
+ /**
+ * Add custom condition
+ *
+ * @access public
+ * @param string $sql
+ */
+ public function addCondition($sql)
+ {
+ if ($this->beginOr) {
+ $this->or[] = $sql;
+ }
+ else {
+ $this->conditions[] = $sql;
+ }
+ }
+
+ /**
+ * Start OR condition
+ *
+ * @access public
+ */
+ public function beginOr()
+ {
+ $this->beginOr = true;
+ $this->or = array();
+ }
+ /**
+ * Close OR condition
+ *
+ * @access public
+ */
+ public function closeOr()
+ {
+ $this->beginOr = false;
+
+ if (! empty($this->or)) {
+ $this->conditions[] = '('.implode(' OR ', $this->or).')';
+ }
+ }
+
+ /**
+ * Equal condition
+ *
+ * @access public
+ * @param string $column
+ * @param mixed $value
+ */
+ public function eq($column, $value)
+ {
+ $this->addCondition($this->db->escapeIdentifier($column).' = ?');
+ $this->values[] = $value;
+ }
+
+ /**
+ * Not equal condition
+ *
+ * @access public
+ * @param string $column
+ * @param mixed $value
+ */
+ public function neq($column, $value)
+ {
+ $this->addCondition($this->db->escapeIdentifier($column).' != ?');
+ $this->values[] = $value;
+ }
+
+ /**
+ * IN condition
+ *
+ * @access public
+ * @param string $column
+ * @param array $values
+ */
+ public function in($column, array $values)
+ {
+ if (! empty($values)) {
+ $this->addCondition($this->db->escapeIdentifier($column).' IN ('.implode(', ', array_fill(0, count($values), '?')).')');
+ $this->values = array_merge($this->values, $values);
+ }
+ }
+
+ /**
+ * NOT IN condition
+ *
+ * @access public
+ * @param string $column
+ * @param array $values
+ */
+ public function notin($column, array $values)
+ {
+ if (! empty($values)) {
+ $this->addCondition($this->db->escapeIdentifier($column).' NOT IN ('.implode(', ', array_fill(0, count($values), '?')).')');
+ $this->values = array_merge($this->values, $values);
+ }
+ }
+
+ /**
+ * LIKE condition
+ *
+ * @access public
+ * @param string $column
+ * @param mixed $value
+ */
+ public function like($column, $value)
+ {
+ $this->addCondition($this->db->escapeIdentifier($column).' '.$this->db->getDriver()->getOperator('LIKE').' ?');
+ $this->values[] = $value;
+ }
+
+ /**
+ * ILIKE condition
+ *
+ * @access public
+ * @param string $column
+ * @param mixed $value
+ */
+ public function ilike($column, $value)
+ {
+ $this->addCondition($this->db->escapeIdentifier($column).' '.$this->db->getDriver()->getOperator('ILIKE').' ?');
+ $this->values[] = $value;
+ }
+
+ /**
+ * Greater than condition
+ *
+ * @access public
+ * @param string $column
+ * @param mixed $value
+ */
+ public function gt($column, $value)
+ {
+ $this->addCondition($this->db->escapeIdentifier($column).' > ?');
+ $this->values[] = $value;
+ }
+
+ /**
+ * Lower than condition
+ *
+ * @access public
+ * @param string $column
+ * @param mixed $value
+ */
+ public function lt($column, $value)
+ {
+ $this->addCondition($this->db->escapeIdentifier($column).' < ?');
+ $this->values[] = $value;
+ }
+
+ /**
+ * Greater than or equals condition
+ *
+ * @access public
+ * @param string $column
+ * @param mixed $value
+ */
+ public function gte($column, $value)
+ {
+ $this->addCondition($this->db->escapeIdentifier($column).' >= ?');
+ $this->values[] = $value;
+ }
+
+ /**
+ * Lower than or equals condition
+ *
+ * @access public
+ * @param string $column
+ * @param mixed $value
+ */
+ public function lte($column, $value)
+ {
+ $this->addCondition($this->db->escapeIdentifier($column).' <= ?');
+ $this->values[] = $value;
+ }
+
+ /**
+ * IS NULL condition
+ *
+ * @access public
+ * @param string $column
+ */
+ public function isNull($column)
+ {
+ $this->addCondition($this->db->escapeIdentifier($column).' IS NULL');
+ }
+
+ /**
+ * IS NOT NULL condition
+ *
+ * @access public
+ * @param string $column
+ */
+ public function notNull($column)
+ {
+ $this->addCondition($this->db->escapeIdentifier($column).' IS NOT NULL');
+ }
+}
diff --git a/sources/vendor/fguillot/picodb/lib/PicoDb/Driver/Base.php b/sources/vendor/fguillot/picodb/lib/PicoDb/Driver/Base.php
new file mode 100644
index 0000000..fd6aa40
--- /dev/null
+++ b/sources/vendor/fguillot/picodb/lib/PicoDb/Driver/Base.php
@@ -0,0 +1,192 @@
+requiredAtttributes as $attribute) {
+ if (! isset($settings[$attribute])) {
+ throw new LogicException('This configuration parameter is missing: "'.$attribute.'"');
+ }
+ }
+
+ $this->createConnection($settings);
+ $this->pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
+ }
+
+ /**
+ * Get the PDO connection
+ *
+ * @access public
+ * @return PDO
+ */
+ public function getConnection()
+ {
+ return $this->pdo;
+ }
+
+ /**
+ * Release the PDO connection
+ *
+ * @access public
+ */
+ public function closeConnection()
+ {
+ $this->pdo = null;
+ }
+
+ /**
+ * Upsert for a key/value variable
+ *
+ * @access public
+ * @param string $table
+ * @param string $keyColumn
+ * @param string $valueColumn
+ * @param array $dictionary
+ * @return bool False on failure
+ */
+ public function upsert($table, $keyColumn, $valueColumn, array $dictionary)
+ {
+ try {
+ $this->pdo->beginTransaction();
+
+ foreach ($dictionary as $key => $value) {
+
+ $rq = $this->pdo->prepare('SELECT 1 FROM '.$this->escape($table).' WHERE '.$this->escape($keyColumn).'=?');
+ $rq->execute(array($key));
+
+ if ($rq->fetchColumn()) {
+ $rq = $this->pdo->prepare('UPDATE '.$this->escape($table).' SET '.$this->escape($valueColumn).'=? WHERE '.$this->escape($keyColumn).'=?');
+ $rq->execute(array($value, $key));
+ }
+ else {
+ $rq = $this->pdo->prepare('INSERT INTO '.$this->escape($table).' ('.$this->escape($keyColumn).', '.$this->escape($valueColumn).') VALUES (?, ?)');
+ $rq->execute(array($key, $value));
+ }
+ }
+
+ $this->pdo->commit();
+
+ return true;
+ }
+ catch (PDOException $e) {
+ $this->pdo->rollback();
+ return false;
+ }
+ }
+}
diff --git a/sources/vendor/fguillot/picodb/lib/PicoDb/SQLException.php b/sources/vendor/fguillot/picodb/lib/PicoDb/SQLException.php
new file mode 100644
index 0000000..4d5cd68
--- /dev/null
+++ b/sources/vendor/fguillot/picodb/lib/PicoDb/SQLException.php
@@ -0,0 +1,14 @@
+field = $field;
+ $this->error_message = $error_message;
+ }
+
+ public function getErrorMessage()
+ {
+ return $this->error_message;
+ }
+
+ public function getField()
+ {
+ if (is_array($this->field)) {
+ return $this->field[0];
+ }
+
+ return $this->field;
+ }
+
+ public function isFieldNotEmpty(array $data)
+ {
+ return isset($data[$this->field]) && $data[$this->field] !== '';
+ }
+}
diff --git a/sources/vendor/gregwar/captcha/CaptchaBuilder.php b/sources/vendor/gregwar/captcha/CaptchaBuilder.php
new file mode 100644
index 0000000..d03b53f
--- /dev/null
+++ b/sources/vendor/gregwar/captcha/CaptchaBuilder.php
@@ -0,0 +1,716 @@
+
+ * @author Jeremy Livingston
+ */
+class CaptchaBuilder implements CaptchaBuilderInterface
+{
+ /**
+ * @var array
+ */
+ protected $fingerprint = array();
+
+ /**
+ * @var bool
+ */
+ protected $useFingerprint = false;
+
+ /**
+ * @var array
+ */
+ protected $textColor = null;
+
+ /**
+ * @var array
+ */
+ protected $backgroundColor = null;
+
+ /**
+ * @var array
+ */
+ protected $backgroundImages = array();
+
+ /**
+ * @var resource
+ */
+ protected $contents = null;
+
+ /**
+ * @var string
+ */
+ protected $phrase = null;
+
+ /**
+ * @var PhraseBuilderInterface
+ */
+ protected $builder;
+
+ /**
+ * @var bool
+ */
+ protected $distortion = true;
+
+ /**
+ * The maximum number of lines to draw in front of
+ * the image. null - use default algorithm
+ */
+ protected $maxFrontLines = null;
+
+ /**
+ * The maximum number of lines to draw behind
+ * the image. null - use default algorithm
+ */
+ protected $maxBehindLines = null;
+
+ /**
+ * The maximum angle of char
+ */
+ protected $maxAngle = 8;
+
+ /**
+ * The maximum offset of char
+ */
+ protected $maxOffset = 5;
+
+ /**
+ * Is the interpolation enabled ?
+ *
+ * @var bool
+ */
+ protected $interpolation = true;
+
+ /**
+ * Ignore all effects
+ *
+ * @var bool
+ */
+ protected $ignoreAllEffects = false;
+
+ /**
+ * Allowed image types for the background images
+ *
+ * @var array
+ */
+ protected $allowedBackgroundImageTypes = array('image/png', 'image/jpeg', 'image/gif');
+
+ /**
+ * The image contents
+ */
+ public function getContents()
+ {
+ return $this->contents;
+ }
+
+ /**
+ * Enable/Disables the interpolation
+ *
+ * @param $interpolate bool True to enable, false to disable
+ *
+ * @return CaptchaBuilder
+ */
+ public function setInterpolation($interpolate = true)
+ {
+ $this->interpolation = $interpolate;
+
+ return $this;
+ }
+
+ /**
+ * Temporary dir, for OCR check
+ */
+ public $tempDir = 'temp/';
+
+ public function __construct($phrase = null, PhraseBuilderInterface $builder = null)
+ {
+ if ($builder === null) {
+ $this->builder = new PhraseBuilder;
+ } else {
+ $this->builder = $builder;
+ }
+
+ if ($phrase === null) {
+ $phrase = $this->builder->build();
+ }
+
+ $this->phrase = $phrase;
+ }
+
+ /**
+ * Setting the phrase
+ */
+ public function setPhrase($phrase)
+ {
+ $this->phrase = (string) $phrase;
+ }
+
+ /**
+ * Enables/disable distortion
+ */
+ public function setDistortion($distortion)
+ {
+ $this->distortion = (bool) $distortion;
+
+ return $this;
+ }
+
+ public function setMaxBehindLines($maxBehindLines)
+ {
+ $this->maxBehindLines = $maxBehindLines;
+
+ return $this;
+ }
+
+ public function setMaxFrontLines($maxFrontLines)
+ {
+ $this->maxFrontLines = $maxFrontLines;
+
+ return $this;
+ }
+
+ public function setMaxAngle($maxAngle)
+ {
+ $this->maxAngle = $maxAngle;
+
+ return $this;
+ }
+
+ public function setMaxOffset($maxOffset)
+ {
+ $this->maxOffset = $maxOffset;
+
+ return $this;
+ }
+
+ /**
+ * Gets the captcha phrase
+ */
+ public function getPhrase()
+ {
+ return $this->phrase;
+ }
+
+ /**
+ * Returns true if the given phrase is good
+ */
+ public function testPhrase($phrase)
+ {
+ return ($this->builder->niceize($phrase) == $this->builder->niceize($this->getPhrase()));
+ }
+
+ /**
+ * Instantiation
+ */
+ public static function create($phrase = null)
+ {
+ return new self($phrase);
+ }
+
+ /**
+ * Sets the text color to use
+ */
+ public function setTextColor($r, $g, $b)
+ {
+ $this->textColor = array($r, $g, $b);
+
+ return $this;
+ }
+
+ /**
+ * Sets the background color to use
+ */
+ public function setBackgroundColor($r, $g, $b)
+ {
+ $this->backgroundColor = array($r, $g, $b);
+
+ return $this;
+ }
+
+ /**
+ * Sets the ignoreAllEffects value
+ *
+ * @param bool $ignoreAllEffects
+ * @return CaptchaBuilder
+ */
+ public function setIgnoreAllEffects($ignoreAllEffects)
+ {
+ $this->ignoreAllEffects = $ignoreAllEffects;
+
+ return $this;
+ }
+
+ /**
+ * Sets the list of background images to use (one image is randomly selected)
+ */
+ public function setBackgroundImages(array $backgroundImages)
+ {
+ $this->backgroundImages = $backgroundImages;
+
+ return $this;
+ }
+
+ /**
+ * Draw lines over the image
+ */
+ protected function drawLine($image, $width, $height, $tcol = null)
+ {
+ if ($tcol === null) {
+ $tcol = imagecolorallocate($image, $this->rand(100, 255), $this->rand(100, 255), $this->rand(100, 255));
+ }
+
+ if ($this->rand(0, 1)) { // Horizontal
+ $Xa = $this->rand(0, $width/2);
+ $Ya = $this->rand(0, $height);
+ $Xb = $this->rand($width/2, $width);
+ $Yb = $this->rand(0, $height);
+ } else { // Vertical
+ $Xa = $this->rand(0, $width);
+ $Ya = $this->rand(0, $height/2);
+ $Xb = $this->rand(0, $width);
+ $Yb = $this->rand($height/2, $height);
+ }
+ imagesetthickness($image, $this->rand(1, 3));
+ imageline($image, $Xa, $Ya, $Xb, $Yb, $tcol);
+ }
+
+ /**
+ * Apply some post effects
+ */
+ protected function postEffect($image)
+ {
+ if (!function_exists('imagefilter')) {
+ return;
+ }
+
+ if ($this->backgroundColor != null || $this->textColor != null) {
+ return;
+ }
+
+ // Negate ?
+ if ($this->rand(0, 1) == 0) {
+ imagefilter($image, IMG_FILTER_NEGATE);
+ }
+
+ // Edge ?
+ if ($this->rand(0, 10) == 0) {
+ imagefilter($image, IMG_FILTER_EDGEDETECT);
+ }
+
+ // Contrast
+ imagefilter($image, IMG_FILTER_CONTRAST, $this->rand(-50, 10));
+
+ // Colorize
+ if ($this->rand(0, 5) == 0) {
+ imagefilter($image, IMG_FILTER_COLORIZE, $this->rand(-80, 50), $this->rand(-80, 50), $this->rand(-80, 50));
+ }
+ }
+
+ /**
+ * Writes the phrase on the image
+ */
+ protected function writePhrase($image, $phrase, $font, $width, $height)
+ {
+ // Gets the text size and start position
+ $size = $width / strlen($phrase) - $this->rand(0, 3) - 1;
+ $box = imagettfbbox($size, 0, $font, $phrase);
+ $textWidth = $box[2] - $box[0];
+ $textHeight = $box[1] - $box[7];
+ $x = ($width - $textWidth) / 2;
+ $y = ($height - $textHeight) / 2 + $size;
+
+ if (!count($this->textColor)) {
+ $textColor = array($this->rand(0, 150), $this->rand(0, 150), $this->rand(0, 150));
+ } else {
+ $textColor = $this->textColor;
+ }
+ $col = imagecolorallocate($image, $textColor[0], $textColor[1], $textColor[2]);
+
+ // Write the letters one by one, with random angle
+ $length = strlen($phrase);
+ for ($i=0; $i<$length; $i++) {
+ $box = imagettfbbox($size, 0, $font, $phrase[$i]);
+ $w = $box[2] - $box[0];
+ $angle = $this->rand(-$this->maxAngle, $this->maxAngle);
+ $offset = $this->rand(-$this->maxOffset, $this->maxOffset);
+ imagettftext($image, $size, $angle, $x, $y + $offset, $col, $font, $phrase[$i]);
+ $x += $w;
+ }
+
+ return $col;
+ }
+
+ /**
+ * Try to read the code against an OCR
+ */
+ public function isOCRReadable()
+ {
+ if (!is_dir($this->tempDir)) {
+ @mkdir($this->tempDir, 0755, true);
+ }
+
+ $tempj = $this->tempDir . uniqid('captcha', true) . '.jpg';
+ $tempp = $this->tempDir . uniqid('captcha', true) . '.pgm';
+
+ $this->save($tempj);
+ shell_exec("convert $tempj $tempp");
+ $value = trim(strtolower(shell_exec("ocrad $tempp")));
+
+ @unlink($tempj);
+ @unlink($tempp);
+
+ return $this->testPhrase($value);
+ }
+
+ /**
+ * Builds while the code is readable against an OCR
+ */
+ public function buildAgainstOCR($width = 150, $height = 40, $font = null, $fingerprint = null)
+ {
+ do {
+ $this->build($width, $height, $font, $fingerprint);
+ } while ($this->isOCRReadable());
+ }
+
+ /**
+ * Generate the image
+ */
+ public function build($width = 150, $height = 40, $font = null, $fingerprint = null)
+ {
+ if (null !== $fingerprint) {
+ $this->fingerprint = $fingerprint;
+ $this->useFingerprint = true;
+ } else {
+ $this->fingerprint = array();
+ $this->useFingerprint = false;
+ }
+
+ if ($font === null) {
+ $font = __DIR__ . '/Font/captcha'.$this->rand(0, 5).'.ttf';
+ }
+
+ if (empty($this->backgroundImages)) {
+ // if background images list is not set, use a color fill as a background
+ $image = imagecreatetruecolor($width, $height);
+ if ($this->backgroundColor == null) {
+ $bg = imagecolorallocate($image, $this->rand(200, 255), $this->rand(200, 255), $this->rand(200, 255));
+ } else {
+ $color = $this->backgroundColor;
+ $bg = imagecolorallocate($image, $color[0], $color[1], $color[2]);
+ }
+ $this->background = $bg;
+ imagefill($image, 0, 0, $bg);
+ } else {
+ // use a random background image
+ $randomBackgroundImage = $this->backgroundImages[rand(0, count($this->backgroundImages)-1)];
+
+ $imageType = $this->validateBackgroundImage($randomBackgroundImage);
+
+ $image = $this->createBackgroundImageFromType($randomBackgroundImage, $imageType);
+ }
+
+ // Apply effects
+ if (!$this->ignoreAllEffects) {
+ $square = $width * $height;
+ $effects = $this->rand($square/3000, $square/2000);
+
+ // set the maximum number of lines to draw in front of the text
+ if ($this->maxBehindLines != null && $this->maxBehindLines > 0) {
+ $effects = min($this->maxBehindLines, $effects);
+ }
+
+ if ($this->maxBehindLines !== 0) {
+ for ($e = 0; $e < $effects; $e++) {
+ $this->drawLine($image, $width, $height);
+ }
+ }
+ }
+
+ // Write CAPTCHA text
+ $color = $this->writePhrase($image, $this->phrase, $font, $width, $height);
+
+ // Apply effects
+ if (!$this->ignoreAllEffects) {
+ $square = $width * $height;
+ $effects = $this->rand($square/3000, $square/2000);
+
+ // set the maximum number of lines to draw in front of the text
+ if ($this->maxFrontLines != null && $this->maxFrontLines > 0) {
+ $effects = min($this->maxFrontLines, $effects);
+ }
+
+ if ($this->maxFrontLines !== 0) {
+ for ($e = 0; $e < $effects; $e++) {
+ $this->drawLine($image, $width, $height, $color);
+ }
+ }
+ }
+
+ // Distort the image
+ if ($this->distortion && !$this->ignoreAllEffects) {
+ $image = $this->distort($image, $width, $height, $bg);
+ }
+
+ // Post effects
+ if (!$this->ignoreAllEffects) {
+ $this->postEffect($image);
+ }
+
+ $this->contents = $image;
+
+ return $this;
+ }
+
+ /**
+ * Distorts the image
+ */
+ public function distort($image, $width, $height, $bg)
+ {
+ $contents = imagecreatetruecolor($width, $height);
+ $X = $this->rand(0, $width);
+ $Y = $this->rand(0, $height);
+ $phase = $this->rand(0, 10);
+ $scale = 1.1 + $this->rand(0, 10000) / 30000;
+ for ($x = 0; $x < $width; $x++) {
+ for ($y = 0; $y < $height; $y++) {
+ $Vx = $x - $X;
+ $Vy = $y - $Y;
+ $Vn = sqrt($Vx * $Vx + $Vy * $Vy);
+
+ if ($Vn != 0) {
+ $Vn2 = $Vn + 4 * sin($Vn / 30);
+ $nX = $X + ($Vx * $Vn2 / $Vn);
+ $nY = $Y + ($Vy * $Vn2 / $Vn);
+ } else {
+ $nX = $X;
+ $nY = $Y;
+ }
+ $nY = $nY + $scale * sin($phase + $nX * 0.2);
+
+ if ($this->interpolation) {
+ $p = $this->interpolate(
+ $nX - floor($nX),
+ $nY - floor($nY),
+ $this->getCol($image, floor($nX), floor($nY), $bg),
+ $this->getCol($image, ceil($nX), floor($nY), $bg),
+ $this->getCol($image, floor($nX), ceil($nY), $bg),
+ $this->getCol($image, ceil($nX), ceil($nY), $bg)
+ );
+ } else {
+ $p = $this->getCol($image, round($nX), round($nY), $bg);
+ }
+
+ if ($p == 0) {
+ $p = $bg;
+ }
+
+ imagesetpixel($contents, $x, $y, $p);
+ }
+ }
+
+ return $contents;
+ }
+
+ /**
+ * Saves the Captcha to a jpeg file
+ */
+ public function save($filename, $quality = 90)
+ {
+ imagejpeg($this->contents, $filename, $quality);
+ }
+
+ /**
+ * Gets the image GD
+ */
+ public function getGd()
+ {
+ return $this->contents;
+ }
+
+ /**
+ * Gets the image contents
+ */
+ public function get($quality = 90)
+ {
+ ob_start();
+ $this->output($quality);
+
+ return ob_get_clean();
+ }
+
+ /**
+ * Gets the HTML inline base64
+ */
+ public function inline($quality = 90)
+ {
+ return 'data:image/jpeg;base64,' . base64_encode($this->get($quality));
+ }
+
+ /**
+ * Outputs the image
+ */
+ public function output($quality = 90)
+ {
+ imagejpeg($this->contents, null, $quality);
+ }
+
+ /**
+ * @return array
+ */
+ public function getFingerprint()
+ {
+ return $this->fingerprint;
+ }
+
+ /**
+ * Returns a random number or the next number in the
+ * fingerprint
+ */
+ protected function rand($min, $max)
+ {
+ if (!is_array($this->fingerprint)) {
+ $this->fingerprint = array();
+ }
+
+ if ($this->useFingerprint) {
+ $value = current($this->fingerprint);
+ next($this->fingerprint);
+ } else {
+ $value = mt_rand($min, $max);
+ $this->fingerprint[] = $value;
+ }
+
+ return $value;
+ }
+
+ /**
+ * @param $x
+ * @param $y
+ * @param $nw
+ * @param $ne
+ * @param $sw
+ * @param $se
+ *
+ * @return int
+ */
+ protected function interpolate($x, $y, $nw, $ne, $sw, $se)
+ {
+ list($r0, $g0, $b0) = $this->getRGB($nw);
+ list($r1, $g1, $b1) = $this->getRGB($ne);
+ list($r2, $g2, $b2) = $this->getRGB($sw);
+ list($r3, $g3, $b3) = $this->getRGB($se);
+
+ $cx = 1.0 - $x;
+ $cy = 1.0 - $y;
+
+ $m0 = $cx * $r0 + $x * $r1;
+ $m1 = $cx * $r2 + $x * $r3;
+ $r = (int) ($cy * $m0 + $y * $m1);
+
+ $m0 = $cx * $g0 + $x * $g1;
+ $m1 = $cx * $g2 + $x * $g3;
+ $g = (int) ($cy * $m0 + $y * $m1);
+
+ $m0 = $cx * $b0 + $x * $b1;
+ $m1 = $cx * $b2 + $x * $b3;
+ $b = (int) ($cy * $m0 + $y * $m1);
+
+ return ($r << 16) | ($g << 8) | $b;
+ }
+
+ /**
+ * @param $image
+ * @param $x
+ * @param $y
+ *
+ * @return int
+ */
+ protected function getCol($image, $x, $y, $background)
+ {
+ $L = imagesx($image);
+ $H = imagesy($image);
+ if ($x < 0 || $x >= $L || $y < 0 || $y >= $H) {
+ return $background;
+ }
+
+ return imagecolorat($image, $x, $y);
+ }
+
+ /**
+ * @param $col
+ *
+ * @return array
+ */
+ protected function getRGB($col)
+ {
+ return array(
+ (int) ($col >> 16) & 0xff,
+ (int) ($col >> 8) & 0xff,
+ (int) ($col) & 0xff,
+ );
+ }
+
+ /**
+ * Validate the background image path. Return the image type if valid
+ *
+ * @param string $backgroundImage
+ * @return string
+ * @throws Exception
+ */
+ protected function validateBackgroundImage($backgroundImage)
+ {
+ // check if file exists
+ if (!file_exists($backgroundImage)) {
+ $backgroundImageExploded = explode('/', $backgroundImage);
+ $imageFileName = count($backgroundImageExploded) > 1? $backgroundImageExploded[count($backgroundImageExploded)-1] : $backgroundImage;
+
+ throw new Exception('Invalid background image: ' . $imageFileName);
+ }
+
+ // check image type
+ $finfo = finfo_open(FILEINFO_MIME_TYPE); // return mime type ala mimetype extension
+ $imageType = finfo_file($finfo, $backgroundImage);
+ finfo_close($finfo);
+
+ if (!in_array ($imageType, $this->allowedBackgroundImageTypes)) {
+ throw new Exception('Invalid background image type! Allowed types are: ' . join(', ', $this->allowedBackgroundImageTypes));
+ }
+
+ return $imageType;
+ }
+
+ /**
+ * Create background image from type
+ *
+ * @param string $backgroundImage
+ * @param string $imageType
+ * @return resource
+ * @throws Exception
+ */
+ protected function createBackgroundImageFromType($backgroundImage, $imageType)
+ {
+ switch ($imageType) {
+ case 'image/jpeg':
+ $image = imagecreatefromjpeg($backgroundImage);
+ break;
+ case 'image/png':
+ $image = imagecreatefrompng($backgroundImage);
+ break;
+ case 'image/gif':
+ $image = imagecreatefromgif($backgroundImage);
+ break;
+
+ default:
+ throw new Exception('Not supported file type for background image!');
+ break;
+ }
+
+ return $image;
+ }
+}
diff --git a/sources/vendor/gregwar/captcha/CaptchaBuilderInterface.php b/sources/vendor/gregwar/captcha/CaptchaBuilderInterface.php
new file mode 100644
index 0000000..bdebf38
--- /dev/null
+++ b/sources/vendor/gregwar/captcha/CaptchaBuilderInterface.php
@@ -0,0 +1,30 @@
+
+ * @author Jeremy Livingston
+ */
+class ImageFileHandler
+{
+ /**
+ * Name of folder for captcha images
+ * @var string
+ */
+ protected $imageFolder;
+
+ /**
+ * Absolute path to public web folder
+ * @var string
+ */
+ protected $webPath;
+
+ /**
+ * Frequency of garbage collection in fractions of 1
+ * @var int
+ */
+ protected $gcFreq;
+
+ /**
+ * Maximum age of images in minutes
+ * @var int
+ */
+ protected $expiration;
+
+ /**
+ * @param $imageFolder
+ * @param $webPath
+ * @param $gcFreq
+ * @param $expiration
+ */
+ public function __construct($imageFolder, $webPath, $gcFreq, $expiration)
+ {
+ $this->imageFolder = $imageFolder;
+ $this->webPath = $webPath;
+ $this->gcFreq = $gcFreq;
+ $this->expiration = $expiration;
+ }
+
+ /**
+ * Saves the provided image content as a file
+ *
+ * @param string $contents
+ *
+ * @return string
+ */
+ public function saveAsFile($contents)
+ {
+ $this->createFolderIfMissing();
+
+ $filename = md5(uniqid()) . '.jpg';
+ $filePath = $this->webPath . '/' . $this->imageFolder . '/' . $filename;
+ imagejpeg($contents, $filePath, 15);
+
+ return '/' . $this->imageFolder . '/' . $filename;
+ }
+
+ /**
+ * Randomly runs garbage collection on the image directory
+ *
+ * @return bool
+ */
+ public function collectGarbage()
+ {
+ if (!mt_rand(1, $this->gcFreq) == 1) {
+ return false;
+ }
+
+ $this->createFolderIfMissing();
+
+ $finder = new Finder();
+ $criteria = sprintf('<= now - %s minutes', $this->expiration);
+ $finder->in($this->webPath . '/' . $this->imageFolder)
+ ->date($criteria);
+
+ foreach($finder->files() as $file) {
+ unlink($file->getPathname());
+ }
+
+ return true;
+ }
+
+ /**
+ * Creates the folder if it doesn't exist
+ */
+ protected function createFolderIfMissing()
+ {
+ if (!file_exists($this->webPath . '/' . $this->imageFolder)) {
+ mkdir($this->webPath . '/' . $this->imageFolder, 0755);
+ }
+ }
+}
+
diff --git a/sources/vendor/gregwar/captcha/LICENSE b/sources/vendor/gregwar/captcha/LICENSE
new file mode 100644
index 0000000..7db6ad8
--- /dev/null
+++ b/sources/vendor/gregwar/captcha/LICENSE
@@ -0,0 +1,19 @@
+Copyright (c) <2012-2015> Grégoire Passault
+
+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.
diff --git a/sources/vendor/gregwar/captcha/PhraseBuilder.php b/sources/vendor/gregwar/captcha/PhraseBuilder.php
new file mode 100644
index 0000000..b94bd61
--- /dev/null
+++ b/sources/vendor/gregwar/captcha/PhraseBuilder.php
@@ -0,0 +1,34 @@
+
+ */
+class PhraseBuilder implements PhraseBuilderInterface
+{
+ /**
+ * Generates random phrase of given length with given charset
+ */
+ public function build($length = 5, $charset = 'abcdefghijklmnpqrstuvwxyz123456789')
+ {
+ $phrase = '';
+ $chars = str_split($charset);
+
+ for ($i = 0; $i < $length; $i++) {
+ $phrase .= $chars[array_rand($chars)];
+ }
+
+ return $phrase;
+ }
+
+ /**
+ * "Niceize" a code
+ */
+ public function niceize($str)
+ {
+ return strtr(strtolower($str), '01', 'ol');
+ }
+}
diff --git a/sources/vendor/gregwar/captcha/PhraseBuilderInterface.php b/sources/vendor/gregwar/captcha/PhraseBuilderInterface.php
new file mode 100644
index 0000000..0a4f536
--- /dev/null
+++ b/sources/vendor/gregwar/captcha/PhraseBuilderInterface.php
@@ -0,0 +1,21 @@
+
+ */
+interface PhraseBuilderInterface
+{
+ /**
+ * Generates random phrase of given length with given charset
+ */
+ public function build($length, $charset);
+
+ /**
+ * "Niceize" a code
+ */
+ public function niceize($str);
+}
diff --git a/sources/vendor/gregwar/captcha/autoload.php b/sources/vendor/gregwar/captcha/autoload.php
new file mode 100644
index 0000000..8b3fa39
--- /dev/null
+++ b/sources/vendor/gregwar/captcha/autoload.php
@@ -0,0 +1,16 @@
+build()
+ ->save('out.jpg')
+;
diff --git a/sources/vendor/gregwar/captcha/demo/fingerprint.php b/sources/vendor/gregwar/captcha/demo/fingerprint.php
new file mode 100644
index 0000000..ce30d99
--- /dev/null
+++ b/sources/vendor/gregwar/captcha/demo/fingerprint.php
@@ -0,0 +1,15 @@
+build()
+ ->getFingerprint()
+);
+
+echo "\n";
diff --git a/sources/vendor/gregwar/captcha/demo/index.php b/sources/vendor/gregwar/captcha/demo/index.php
new file mode 100644
index 0000000..e543883
--- /dev/null
+++ b/sources/vendor/gregwar/captcha/demo/index.php
@@ -0,0 +1,15 @@
+
+
+
+
+
+
+ Captchas gallery
+
+
+
+
+
+
+
+
diff --git a/sources/vendor/gregwar/captcha/demo/ocr.php b/sources/vendor/gregwar/captcha/demo/ocr.php
new file mode 100644
index 0000000..3d745f6
--- /dev/null
+++ b/sources/vendor/gregwar/captcha/demo/ocr.php
@@ -0,0 +1,42 @@
+setDistortion(false)
+ ->build()
+ ;
+
+ if ($captcha->isOCRReadable()) {
+ $passed++;
+ $captcha->save("passed$passed.jpg");
+ echo "passed at ocr... ";
+ } else {
+ echo "failed... ";
+ }
+
+ echo "pass rate: ".round(100*$passed/($i+1),2)."%\n";
+}
+
+echo "\n";
+echo "Over, $passed/$tests readed with OCR\n";
diff --git a/sources/vendor/gregwar/captcha/demo/output.php b/sources/vendor/gregwar/captcha/demo/output.php
new file mode 100644
index 0000000..2a4f330
--- /dev/null
+++ b/sources/vendor/gregwar/captcha/demo/output.php
@@ -0,0 +1,15 @@
+build()
+ ->output()
+;
diff --git a/sources/vendor/nickcernis/html-to-markdown/HTML_To_Markdown.php b/sources/vendor/nickcernis/html-to-markdown/HTML_To_Markdown.php
new file mode 100644
index 0000000..109780e
--- /dev/null
+++ b/sources/vendor/nickcernis/html-to-markdown/HTML_To_Markdown.php
@@ -0,0 +1,598 @@
+
+ * @link https://github.com/nickcernis/html2markdown/ Latest version on GitHub.
+ * @link http://twitter.com/nickcernis Nick on twitter.
+ * @license http://www.opensource.org/licenses/mit-license.php MIT
+ */
+class HTML_To_Markdown
+{
+ /**
+ * @var DOMDocument The root of the document tree that holds our HTML.
+ */
+ private $document;
+
+ /**
+ * @var string|boolean The Markdown version of the original HTML, or false if conversion failed
+ */
+ private $output;
+
+ /**
+ * @var array Class-wide options users can override.
+ */
+ private $options = array(
+ 'header_style' => 'setext', // Set to "atx" to output H1 and H2 headers as # Header1 and ## Header2
+ 'suppress_errors' => true, // Set to false to show warnings when loading malformed HTML
+ 'strip_tags' => false, // Set to true to strip tags that don't have markdown equivalents. N.B. Strips tags, not their content. Useful to clean MS Word HTML output.
+ 'bold_style' => '**', // Set to '__' if you prefer the underlined style
+ 'italic_style' => '*', // Set to '_' if you prefer the underlined style
+ 'remove_nodes' => '', // space-separated list of dom nodes that should be removed. example: "meta style script"
+ );
+
+
+ /**
+ * Constructor
+ *
+ * Set up a new DOMDocument from the supplied HTML, convert it to Markdown, and store it in $this->$output.
+ *
+ * @param string $html The HTML to convert to Markdown.
+ * @param array $overrides [optional] List of style and error display overrides.
+ */
+ public function __construct($html = null, $overrides = null)
+ {
+ if ($overrides)
+ $this->options = array_merge($this->options, $overrides);
+
+ if ($html)
+ $this->convert($html);
+ }
+
+
+ /**
+ * Setter for conversion options
+ *
+ * @param $name
+ * @param $value
+ */
+ public function set_option($name, $value)
+ {
+ $this->options[$name] = $value;
+ }
+
+
+ /**
+ * Convert
+ *
+ * Loads HTML and passes to get_markdown()
+ *
+ * @param $html
+ * @return string The Markdown version of the html
+ */
+ public function convert($html)
+ {
+ $html = preg_replace('~>\s+<~', '><', $html); // Strip white space between tags to prevent creation of empty #text nodes
+
+ $this->document = new DOMDocument();
+
+ if ($this->options['suppress_errors'])
+ libxml_use_internal_errors(true); // Suppress conversion errors (from http://bit.ly/pCCRSX )
+
+ $this->document->loadHTML('' . $html); // Hack to load utf-8 HTML (from http://bit.ly/pVDyCt )
+ $this->document->encoding = 'UTF-8';
+
+ if ($this->options['suppress_errors'])
+ libxml_clear_errors();
+
+ return $this->get_markdown($html);
+ }
+
+
+ /**
+ * Is Child Of?
+ *
+ * Is the node a child of the given parent tag?
+ *
+ * @param $parent_name string|array The name of the parent node(s) to search for e.g. 'code' or array('pre', 'code')
+ * @param $node
+ * @return bool
+ */
+ private static function is_child_of($parent_name, $node)
+ {
+ for ($p = $node->parentNode; $p != false; $p = $p->parentNode) {
+ if (is_null($p))
+ return false;
+
+ if ( is_array($parent_name) && in_array($p->nodeName, $parent_name) )
+ return true;
+
+ if ($p->nodeName == $parent_name)
+ return true;
+ }
+ return false;
+ }
+
+
+ /**
+ * Convert Children
+ *
+ * Recursive function to drill into the DOM and convert each node into Markdown from the inside out.
+ *
+ * Finds children of each node and convert those to #text nodes containing their Markdown equivalent,
+ * starting with the innermost element and working up to the outermost element.
+ *
+ * @param $node
+ */
+ private function convert_children($node)
+ {
+ // Don't convert HTML code inside and blocks to Markdown - that should stay as HTML
+ if (self::is_child_of(array('pre', 'code'), $node))
+ return;
+
+ // If the node has children, convert those to Markdown first
+ if ($node->hasChildNodes()) {
+ $length = $node->childNodes->length;
+
+ for ($i = 0; $i < $length; $i++) {
+ $child = $node->childNodes->item($i);
+ $this->convert_children($child);
+ }
+ }
+
+ // Now that child nodes have been converted, convert the original node
+ $markdown = $this->convert_to_markdown($node);
+
+ // Create a DOM text node containing the Markdown equivalent of the original node
+ $markdown_node = $this->document->createTextNode($markdown);
+
+ // Replace the old $node e.g. "Title
" with the new $markdown_node e.g. "### Title"
+ $node->parentNode->replaceChild($markdown_node, $node);
+ }
+
+
+ /**
+ * Get Markdown
+ *
+ * Sends the body node to convert_children() to change inner nodes to Markdown #text nodes, then saves and
+ * returns the resulting converted document as a string in Markdown format.
+ *
+ * @return string|boolean The converted HTML as Markdown, or false if conversion failed
+ */
+ private function get_markdown()
+ {
+ // Work on the entire DOM tree (including head and body)
+ $input = $this->document->getElementsByTagName("html")->item(0);
+
+ if (!$input)
+ return false;
+
+ // Convert all children of this root element. The DOMDocument stored in $this->doc will
+ // then consist of #text nodes, each containing a Markdown version of the original node
+ // that it replaced.
+ $this->convert_children($input);
+
+ // Sanitize and return the body contents as a string.
+ $markdown = $this->document->saveHTML(); // stores the DOMDocument as a string
+ $markdown = html_entity_decode($markdown, ENT_QUOTES, 'UTF-8');
+ $markdown = html_entity_decode($markdown, ENT_QUOTES, 'UTF-8'); // Double decode to cover cases like http://www.php.net/manual/en/function.htmlentities.php#99984
+ $markdown = preg_replace("/]+>/", "", $markdown); // Strip doctype declaration
+ $unwanted = array('', '', '', '', '', '', '', '
');
+ $markdown = str_replace($unwanted, '', $markdown); // Strip unwanted tags
+ $markdown = trim($markdown, "\n\r\0\x0B");
+
+ $this->output = $markdown;
+
+ return $markdown;
+ }
+
+
+ /**
+ * Convert to Markdown
+ *
+ * Converts an individual node into a #text node containing a string of its Markdown equivalent.
+ *
+ * Example: An node with text content of "Title" becomes a text node with content of "### Title"
+ *
+ * @param $node
+ * @return string The converted HTML as Markdown
+ */
+ private function convert_to_markdown($node)
+ {
+ $tag = $node->nodeName; // the type of element, e.g. h1
+ $value = $node->nodeValue; // the value of that element, e.g. The Title
+
+ // Strip nodes named in remove_nodes
+ $tags_to_remove = explode(' ', $this->options['remove_nodes']);
+ if ( in_array($tag, $tags_to_remove) )
+ return false;
+
+ switch ($tag) {
+ case "p":
+ $markdown = (trim($value)) ? rtrim($value) . PHP_EOL . PHP_EOL : '';
+ break;
+ case "pre":
+ $markdown = PHP_EOL . $this->convert_code($node) . PHP_EOL;
+ break;
+ case "h1":
+ case "h2":
+ $markdown = $this->convert_header($tag, $node);
+ break;
+ case "h3":
+ $markdown = "### " . $value . PHP_EOL . PHP_EOL;
+ break;
+ case "h4":
+ $markdown = "#### " . $value . PHP_EOL . PHP_EOL;
+ break;
+ case "h5":
+ $markdown = "##### " . $value . PHP_EOL . PHP_EOL;
+ break;
+ case "h6":
+ $markdown = "###### " . $value . PHP_EOL . PHP_EOL;
+ break;
+ case "em":
+ case "i":
+ case "strong":
+ case "b":
+ $markdown = $this->convert_emphasis($tag, $value);
+ break;
+ case "hr":
+ $markdown = "- - - - - -" . PHP_EOL . PHP_EOL;
+ break;
+ case "br":
+ $markdown = " " . PHP_EOL;
+ break;
+ case "blockquote":
+ $markdown = $this->convert_blockquote($node);
+ break;
+ case "code":
+ $markdown = $this->convert_code($node);
+ break;
+ case "ol":
+ case "ul":
+ $markdown = $value . PHP_EOL;
+ break;
+ case "li":
+ $markdown = $this->convert_list($node);
+ break;
+ case "img":
+ $markdown = $this->convert_image($node);
+ break;
+ case "a":
+ $markdown = $this->convert_anchor($node);
+ break;
+ case "#text":
+ $markdown = preg_replace('~\s+~', ' ', $value);
+ $markdown = preg_replace('~^#~', '\\\\#', $markdown);
+ break;
+ case "#comment":
+ $markdown = '';
+ break;
+ case "div":
+ $markdown = ($this->options['strip_tags']) ? $value . PHP_EOL . PHP_EOL : html_entity_decode($node->C14N());
+ break;
+ default:
+ // If strip_tags is false (the default), preserve tags that don't have Markdown equivalents,
+ // such as nodes on their own. C14N() canonicalizes the node to a string.
+ // See: http://www.php.net/manual/en/domnode.c14n.php
+ $markdown = ($this->options['strip_tags']) ? $value : html_entity_decode($node->C14N());
+ }
+
+ return $markdown;
+ }
+
+
+ /**
+ * Convert Header
+ *
+ * Converts h1 and h2 headers to Markdown-style headers in setext style,
+ * matching the number of underscores with the length of the title.
+ *
+ * e.g. Header 1 Header Two
+ * ======== ----------
+ *
+ * Returns atx headers instead if $this->options['header_style'] is "atx"
+ *
+ * e.g. # Header 1 ## Header Two
+ *
+ * @param string $level The header level, including the "h". e.g. h1
+ * @param string $node The node to convert.
+ * @return string The Markdown version of the header.
+ */
+ private function convert_header($level, $node)
+ {
+ $content = $node->nodeValue;
+
+ if (!$this->is_child_of('blockquote', $node) && $this->options['header_style'] == "setext") {
+ $length = (function_exists('mb_strlen')) ? mb_strlen($content, 'utf-8') : strlen($content);
+ $underline = ($level == "h1") ? "=" : "-";
+ $markdown = $content . PHP_EOL . str_repeat($underline, $length) . PHP_EOL . PHP_EOL; // setext style
+ } else {
+ $prefix = ($level == "h1") ? "# " : "## ";
+ $markdown = $prefix . $content . PHP_EOL . PHP_EOL; // atx style
+ }
+
+ return $markdown;
+ }
+
+
+ /**
+ * Converts inline styles
+ * This function is used to render strong and em tags
+ *
+ * eg bold text becomes **bold text** or __bold text__
+ *
+ * @param string $tag
+ * @param string $value
+ * @return string
+ */
+ private function convert_emphasis($tag, $value)
+ {
+ if ($tag == 'i' || $tag == 'em') {
+ $markdown = $this->options['italic_style'] . $value . $this->options['italic_style'];
+ } else {
+ $markdown = $this->options['bold_style'] . $value . $this->options['bold_style'];
+ }
+
+ return $markdown;
+ }
+
+
+ /**
+ * Convert Image
+ *
+ * Converts tags to Markdown.
+ *
+ * e.g.
+ * becomes ![alt text](/path/img.jpg "Title")
+ *
+ * @param $node
+ * @return string
+ */
+ private function convert_image($node)
+ {
+ $src = $node->getAttribute('src');
+ $alt = $node->getAttribute('alt');
+ $title = $node->getAttribute('title');
+
+ if ($title != "") {
+ $markdown = '![' . $alt . '](' . $src . ' "' . $title . '")'; // No newlines added. should be in a block-level element.
+ } else {
+ $markdown = '![' . $alt . '](' . $src . ')';
+ }
+
+ return $markdown;
+ }
+
+
+ /**
+ * Convert Anchor
+ *
+ * Converts tags to Markdown.
+ *
+ * e.g. Modern Nerd
+ * becomes [Modern Nerd](http://modernnerd.net "Title")
+ *
+ * @param $node
+ * @return string
+ */
+ private function convert_anchor($node)
+ {
+ $href = $node->getAttribute('href');
+ $title = $node->getAttribute('title');
+ $text = $node->nodeValue;
+
+ if ($title != "") {
+ $markdown = '[' . $text . '](' . $href . ' "' . $title . '")';
+ } else {
+ $markdown = '[' . $text . '](' . $href . ')';
+ }
+
+ if (! $href)
+ $markdown = html_entity_decode($node->C14N());
+
+ // Append a space if the node after this one is also an anchor
+ $next_node_name = $this->get_next_node_name($node);
+
+ if ($next_node_name == 'a')
+ $markdown = $markdown . ' ';
+
+ return $markdown;
+ }
+
+
+ /**
+ * Convert List
+ *
+ * Converts