jmontoya %!s(int64=9) %!d(string=hai) anos
pai
achega
2edaee9b28
Modificáronse 72 ficheiros con 9093 adicións e 154 borrados
  1. 1 0
      .scrutinizer.yml
  2. 11 3
      .travis.yml
  3. 4 1
      composer.json
  4. 22 1
      main/admin/statistics/index.php
  5. 8 0
      main/announcements/announcements.php
  6. 8 0
      main/calendar/agenda_list.php
  7. 2 1
      main/course_home/course_home.php
  8. 122 0
      main/exercice/fill_blanks.class.php
  9. 5 12
      main/exercice/stats.php
  10. 185 0
      main/forum/editthread.php
  11. 238 4
      main/forum/forumfunction.inc.php
  12. 12 10
      main/forum/newthread.php
  13. 8 1
      main/forum/viewthread_flat.inc.php
  14. 3 1
      main/gradebook/lib/GradebookUtils.php
  15. 1 1
      main/group/group_space.php
  16. 55 0
      main/inc/ajax/statistics.ajax.php
  17. 21 27
      main/inc/lib/agenda.lib.php
  18. 1 1
      main/inc/lib/api.lib.php
  19. 7 4
      main/inc/lib/database.lib.php
  20. 73 7
      main/inc/lib/exercise.lib.php
  21. 5 1
      main/inc/lib/extra_field.lib.php
  22. 3731 0
      main/inc/lib/javascript/chartjs/Chart.js
  23. 9 0
      main/inc/lib/javascript/chartjs/Chart.min.js
  24. 12 10
      main/inc/lib/nusoap/class.soap_parser.php
  25. 1 3
      main/inc/lib/nusoap/class.soap_server.php
  26. 12 12
      main/inc/lib/sessionmanager.lib.php
  27. 2 2
      main/inc/lib/social.lib.php
  28. 34 0
      main/inc/lib/statistics.lib.php
  29. 1 1
      main/inc/lib/system_announcements.lib.php
  30. 42 23
      main/inc/lib/tracking.lib.php
  31. 1 1
      main/inc/lib/usermanager.lib.php
  32. 3 1
      main/lang/english/trad4all.inc.php
  33. 4 2
      main/lang/french/trad4all.inc.php
  34. 3 1
      main/lang/spanish/trad4all.inc.php
  35. 9 0
      main/session/session_edit.php
  36. 13 6
      main/social/profile.php
  37. 3 0
      main/upload/form.scorm.php
  38. 10 1
      main/webservices/lp.php
  39. 35 11
      main/webservices/registration.soap.php
  40. 9 1
      main/webservices/webservice_user.php
  41. 3 0
      main/work/view.php
  42. 4 4
      plugin/buycourses/lang/french.php
  43. 674 0
      plugin/pens/COPYING
  44. 248 0
      plugin/pens/chamilo_pens.php
  45. 674 0
      plugin/pens/lib/COPYING
  46. 177 0
      plugin/pens/lib/PENSPlugin.php
  47. 79 0
      plugin/pens/lib/README.textile
  48. 7 0
      plugin/pens/lib/pens.php
  49. 113 0
      plugin/pens/lib/pens/lib/rfc2396regexes.php
  50. 113 0
      plugin/pens/lib/pens/pens_client.php
  51. 69 0
      plugin/pens/lib/pens/pens_config.php
  52. 62 0
      plugin/pens/lib/pens/pens_controller.php
  53. 98 0
      plugin/pens/lib/pens/pens_exception.php
  54. 5 0
      plugin/pens/lib/pens/pens_message.php
  55. 114 0
      plugin/pens/lib/pens/pens_package_handler.php
  56. 495 0
      plugin/pens/lib/pens/pens_request.php
  57. 48 0
      plugin/pens/lib/pens/pens_request_alert.php
  58. 48 0
      plugin/pens/lib/pens/pens_request_collect.php
  59. 68 0
      plugin/pens/lib/pens/pens_request_factory.php
  60. 48 0
      plugin/pens/lib/pens/pens_request_handler.php
  61. 48 0
      plugin/pens/lib/pens/pens_request_receipt.php
  62. 175 0
      plugin/pens/lib/pens/pens_response.php
  63. 313 0
      plugin/pens/lib/pens/pens_server.php
  64. 39 0
      plugin/pens/lib/pens_client_test.php
  65. 41 0
      plugin/pens/lib/pens_server_test.php
  66. 87 0
      plugin/pens/lib/tests/test_pens_package_handler.php
  67. 213 0
      plugin/pens/lib/tests/test_pens_request.php
  68. 68 0
      plugin/pens/lib/tests/test_pens_response.php
  69. 122 0
      plugin/pens/lib/tests/test_pens_server.php
  70. 54 0
      plugin/pens/pens.php
  71. 36 0
      plugin/pens/plugin.php
  72. 24 0
      plugin/pens/readme.txt

+ 1 - 0
.scrutinizer.yml

@@ -21,6 +21,7 @@ filter:
         - app/cache/*
         - app/logs/*
         - web/*
+        - vendor/*
 checks:
     php:
         code_rating: true

+ 11 - 3
.travis.yml

@@ -30,11 +30,19 @@ before_script:
   - php5 -m
   - composer self-update
   - composer global require "fxp/composer-asset-plugin:1.0.3"
-  - composer install -n
-  - composer -v update
+  # You can either use the composer install method and face the Github limit
+  #- composer install -n
+  #- composer -v update
+  # ...OR use the proxy repository to download the necessary dependencies
+  - git clone -b 1.10.x --depth=1 https://github.com/ywarnier/chamilo-vendors.git
+  - sudo mv chamilo-vendors/vendor .
+  - sudo mv chamilo-vendors/web .
+  - sudo mv chamilo-vendors/composer.lock .
+  - sudo rm -rf chamilo-vendors
+  # Continue...
   - phpenv config-add tests/travis-php-config.ini
   # enable php-fpm
-  - sudo service php-fpm stop
+  - sudo /etc/init.d/php-fpm stop
   - sudo cp ~/.phpenv/versions/$(phpenv version-name)/etc/php-fpm.conf.default ~/.phpenv/versions/$(phpenv version-name)/etc/php-fpm.conf
   - sudo a2enmod rewrite actions fastcgi alias
   - echo "cgi.fix_pathinfo = 1" >> ~/.phpenv/versions/$(phpenv version-name)/etc/php.ini

+ 4 - 1
composer.json

@@ -6,7 +6,10 @@
     "license": "GPL-3.0",
     "support": {
         "forum": "http://www.chamilo.org/forum",
-        "irc": "irc://irc.freenode.org/chamilo"
+        "irc": "irc://irc.freenode.org/chamilo",
+        "source": "https://github.com/chamilo/chamilo-lms",
+        "docs": "https://github.com/chamilo/docs",
+        "issues": "https://support.chamilo.org/projects/chamilo-18"
     },
     "autoload": {
         "psr-4": {

+ 22 - 1
main/admin/statistics/index.php

@@ -12,6 +12,25 @@ api_protect_admin_script();
 
 $interbreadcrumb[] = array('url' => '../index.php', 'name' => get_lang('PlatformAdmin'));
 
+$report = isset($_REQUEST['report']) ? $_REQUEST['report'] : '';
+
+if ($report) {
+    $htmlHeadXtra[] = api_get_js('chartjs/Chart.min.js');
+    $htmlHeadXtra[] = ''
+    . '<script type="text/javascript">'
+        . '$(document).ready(function() {'
+            . '$.ajax({'
+                . 'url: "'. api_get_path(WEB_CODE_PATH) .'inc/ajax/statistics.ajax.php?a=recentlogins",'
+                . 'type: "POST",'
+                . 'success: function(data) {'
+                    . 'Chart.defaults.global.responsive = true;'
+                    . 'var myLine = new Chart(document.getElementById("canvas").getContext("2d")).Line(data);'
+                . '}'
+            . '});'
+        . '});' 
+    . '</script>'; 
+}
+        
 $tool_name = get_lang('Statistics');
 Display::display_header($tool_name);
 echo Display::page_header($tool_name);
@@ -61,7 +80,7 @@ echo '</tr></table>';
 
 $course_categories = Statistics::getCourseCategories();
 echo '<br/><br/>';//@todo: spaces between elements should be handled in the css, br should be removed if only there for presentation
-$report = isset($_REQUEST['report']) ? $_REQUEST['report'] : '';
+
 switch ($report) {
     case 'courses':
         // total amount of courses
@@ -102,6 +121,8 @@ switch ($report) {
         Statistics::printStats(get_lang('Students'), $students);
         break;
     case 'recentlogins':
+        echo '<h2>'. sprintf(get_lang('LastXDays'), '15') . '</h2>';
+        echo '<canvas class="col-md-12" id="canvas" height="100px" style="margin-bottom: 20px"></canvas>';
         Statistics::printRecentLoginStats();
         Statistics::printRecentLoginStats(true);
         break;

+ 8 - 0
main/announcements/announcements.php

@@ -330,6 +330,14 @@ switch ($action) {
 
         $form->addElement('text', 'title', get_lang('EmailTitle'));
         $form->addElement('hidden', 'id');
+        $htmlTags = "<b>".get_lang('Tags')."</b></br></br>";
+        $tags = AnnouncementManager::get_tags();
+        
+        foreach ($tags as $tag) {
+            $htmlTags .= "<b>".$tag."</b></br>";
+        }
+        
+        $form->addHtml("<div class='form-group'><div class='col-sm-2'></div><div class='col-sm-8'><div class='alert alert-info'>".$htmlTags."</div></div></div>");
         $form->addHtmlEditor(
             'content',
             get_lang('Description'),

+ 8 - 0
main/calendar/agenda_list.php

@@ -33,6 +33,14 @@ if (!empty($currentCourseId) && $currentCourseId != -1) {
     $this_section = SECTION_COURSES;
 } else {
     // Agenda is out of the course tool (e.g personal agenda)
+    
+    // Little hack to sort the events by start date in personal agenda (Agenda events List view - See #8014)
+    usort($events, function($a, $b) {
+        $t1 = strtotime($a['start']);
+        $t2 = strtotime($b['start']);
+        return $t1 - $t2;
+    });
+    
     $url = false;
     if (!empty($events)) {
         foreach ($events as &$event) {

+ 2 - 1
main/course_home/course_home.php

@@ -68,7 +68,8 @@ $(document).ready(function() {
 		tool_id = $(this).attr("id");
 		tool_info = tool_id.split("_");
 		my_tool_id = tool_info[1];
-
+        $("#id_normal_message").attr("class", "normal-message alert alert-success");
+        
 		$.ajax({
 			contentType: "application/x-www-form-urlencoded",
 			beforeSend: function(objeto) {

+ 122 - 0
main/exercice/fill_blanks.class.php

@@ -726,6 +726,128 @@ class FillBlanks extends Question
 
         return $listAnswerResults;
     }
+    
+    /**
+    * Return an array of student state answers for fill the blank questions
+    * for each students that answered the question
+    * -2  : didn't answer
+    * -1  : student answer is wrong
+    *  0  : student answer is correct
+    * >0  : for fill the blank question with choice menu, is the index of the student answer (right answer indice is 0)
+    *
+    * @param $testId
+    * @param $questionId
+    * @param $studentsIdList
+    * @param $startDate
+    * @param $endDate
+    * @param bool $useLastAnswerredAttempt
+    * @return array
+    * (
+    *     [student_id] => Array
+    *         (
+    *             [first fill the blank for question] => -1
+    *             [second fill the blank for question] => 2
+    *             [third fill the blank for question] => -1
+    *         )
+    * )
+    */
+    public static function getFillTheBlankTabResult($testId, $questionId, $studentsIdList, $startDate, $endDate, $useLastAnswerredAttempt = true) {
+
+       $tblTrackEAttempt = Database::get_main_table(TABLE_STATISTIC_TRACK_E_ATTEMPT);
+       $tblTrackEExercise = Database::get_main_table(TABLE_STATISTIC_TRACK_E_EXERCISES);
+       $courseId = api_get_course_int_id();
+
+       require_once api_get_path(SYS_PATH).'main/exercice/fill_blanks.class.php';
+
+       // request to have all the answers of student for this question
+       // student may have doing it several time
+       // student may have not answered the bracket id, in this case, is result of the answer is empty
+
+       // we got the less recent attempt first
+       $sql = '
+           SELECT * FROM '.$tblTrackEAttempt.' tea
+
+           LEFT JOIN '.$tblTrackEExercise.' tee
+           ON tee.exe_id = tea.exe_id
+           AND tea.c_id = '.$courseId.'
+           AND exe_exo_id = '.$testId.'
+
+           WHERE tee.c_id = '.$courseId.'
+           AND question_id = '.$questionId.'
+           AND tea.user_id IN ('.implode(',', $studentsIdList).')
+           AND tea.tms >= "'.$startDate.'"
+           AND tea.tms <= "'.$endDate.'"
+           ORDER BY user_id, tea.exe_id;
+       ';
+
+       $res = Database::query($sql);
+       $tabUserResult = array();
+       $bracketNumber = 0;
+       // foreach attempts for all students starting with his older attempt
+       while ($data = Database::fetch_array($res)) {
+           $tabAnswer = FillBlanks::getAnswerInfo($data['answer'], true);
+
+           // for each bracket to find in this question
+           foreach ($tabAnswer['studentanswer'] as $bracketNumber => $studentAnswer) {
+
+               if ($tabAnswer['studentanswer'][$bracketNumber] != '') {
+                   // student has answered this bracket, cool
+                   switch (FillBlanks::getFillTheBlankAnswerType($tabAnswer['tabwords'][$bracketNumber])) {
+                       case self::FILL_THE_BLANK_MENU :
+                           // get the indice of the choosen answer in the menu
+                           // we know that the right answer is the first entry of the menu, ie 0
+                           // (remember, menu entries are shuffled when taking the test)
+                           $tabUserResult[$data['user_id']][$bracketNumber] = FillBlanks::getFillTheBlankMenuAnswerNum($tabAnswer['tabwords'][$bracketNumber], $tabAnswer['studentanswer'][$bracketNumber]);
+                           break;
+                       default :
+                           if (FillBlanks::isGoodStudentAnswer($tabAnswer['studentanswer'][$bracketNumber], $tabAnswer['tabwords'][$bracketNumber])) {
+                               $tabUserResult[$data['user_id']][$bracketNumber] = 0;   //  right answer
+                           } else {
+                               $tabUserResult[$data['user_id']][$bracketNumber] = -1;  // wrong answer
+                           }
+                   }
+               } else {
+                   // student didn't answer this bracket
+                   if ($useLastAnswerredAttempt) {
+                       // if we take into account the last answered attempt
+                       if (!isset($tabUserResult[$data['user_id']][$bracketNumber])) {
+                           $tabUserResult[$data['user_id']][$bracketNumber] = -2;      // not answered
+                       }
+                   } else {
+                       // we take the last attempt, even if the student answer the question before
+                       $tabUserResult[$data['user_id']][$bracketNumber] = -2;      // not answered
+                   }
+               }
+           }
+
+
+       }
+       return $tabUserResult;
+    }
+
+
+
+    /**
+     * Return the number of student that give at leat an answer in the fill the blank test
+     * @param $resultList
+     * @return int
+     */
+    public static function getNbResultFillBlankAll($resultList)
+    {
+        $outRes = 0;
+        // for each student in group
+        foreach($resultList as $userId => $tabValue) {
+            $trouve = false;
+            // for each bracket, if student has at leat one answer ( choice > -2) then he pass the question
+            foreach($tabValue as $i => $choice) {
+                if ($choice > -2 && !$trouve) {
+                    $outRes++;
+                    $trouve = true;
+                }
+            }
+        }
+        return $outRes;
+    }
 
     /**
      * Replace the occurrence of blank word with [correct answer][student answer][answer is correct]

+ 5 - 12
main/exercice/stats.php

@@ -144,26 +144,19 @@ if (!empty($question_list)) {
 
                         $answer_item = api_substr($answer_item, 1);
                         $answer_item = api_substr($answer_item, 0, api_strlen($answer_item) -1);
+                        
+                        $data[$id]['answer'] 	= $answer_item;
 
                         $data[$id]['correct'] 	= '-';
 
-                        $count = ExerciseLib::get_number_students_answer_count(
-                            $real_answer_id,
-                            $question_id,
-                            $exercise_id,
-                            $courseCode,
-                            $sessionId,
-                            FILL_IN_BLANKS,
-                            $answer_info_db,
-                            $answer_item
-                        );
-
+                        $count = ExerciseLib::getNumberStudentsFillBlanksAnwserCount($question_id, $exercise_id);
+                        $count = $count[$counter];
+                        
                         $percentange = 0;
                         if (!empty($count_students)) {
                             $percentange = $count/$count_students*100;
                         }
                         $data[$id]['attempts'] = Display::bar_progress($percentange, false, $count .' / '.$count_students);
-
                         $id++;
                         $counter++;
                     }

+ 185 - 0
main/forum/editthread.php

@@ -0,0 +1,185 @@
+<?php
+/* For licensing terms, see /license.txt */
+
+/**
+ * Edit a Forum Thread
+ * @Author José Loguercio <jose.loguercio@beeznest.com>
+ *
+ * @package chamilo.forum
+ */
+
+use ChamiloSession as Session;
+
+// Including the global initialization file.
+require_once '../inc/global.inc.php';
+
+// The section (tabs).
+$this_section = SECTION_COURSES;
+// Notification for unauthorized people.
+api_protect_course_script(true);
+
+$cidreq = api_get_cidreq();
+
+$nameTools = get_lang('ToolForum');
+
+/* Including necessary files */
+
+require_once 'forumconfig.inc.php';
+require_once 'forumfunction.inc.php';
+
+// Are we in a lp ?
+$origin = '';
+if (isset($_GET['origin'])) {
+    $origin = Security::remove_XSS($_GET['origin']);
+}
+
+/* MAIN DISPLAY SECTION */
+$currentForum = get_forum_information($_GET['forum']);
+$currentForumCategory = get_forumcategory_information($currentForum['forum_category']);
+
+// the variable $forum_settings is declared in forumconfig.inc.php
+$forumSettings = $forum_setting;
+
+/* Breadcrumbs */
+
+if (isset($_SESSION['gradebook'])) {
+    $gradebook = Security::remove_XSS($_SESSION['gradebook']);
+}
+
+if (!empty($gradebook) && $gradebook == 'view') {
+    $interbreadcrumb[] = array (
+        'url' => '../gradebook/'.Security::remove_XSS($_SESSION['gradebook_dest']),
+        'name' => get_lang('ToolGradebook')
+    );
+}
+
+if (!empty($_GET['gidReq'])) {
+    $toolgroup = intval($_GET['gidReq']);
+    Session::write('toolgroup',$toolgroup);
+}
+
+$threadId = isset($_GET['thread']) ? intval($_GET['thread']) : 0;
+$courseInfo = isset($_GET['cidReq']) ? api_get_course_info($_GET['cidReq']) : 0;
+$cId = isset($courseInfo['real_id']) ? intval($courseInfo['real_id']) : 0; 
+
+/* Is the user allowed here? */
+
+// The user is not allowed here if:
+
+// 1. the forumcategory or forum is invisible (visibility==0) and the user is not a course manager
+if (!api_is_allowed_to_edit(false, true) &&
+    (($currentForumCategory['visibility'] && $currentForumCategory['visibility'] == 0) || $currentForum['visibility'] == 0)
+) {
+    api_not_allowed();
+}
+
+// 2. the forumcategory or forum is locked (locked <>0) and the user is not a course manager
+if (!api_is_allowed_to_edit(false, true) &&
+    (($currentForumCategory['visibility'] && $currentForumCategory['locked'] <> 0) OR $currentForum['locked'] <> 0)
+) {
+    api_not_allowed();
+}
+
+// 3. new threads are not allowed and the user is not a course manager
+if (!api_is_allowed_to_edit(false, true) &&
+    $currentForum['allow_new_threads'] <> 1
+) {
+    api_not_allowed();
+}
+// 4. anonymous posts are not allowed and the user is not logged in
+if (!$_user['user_id'] AND $currentForum['allow_anonymous'] <> 1) {
+    api_not_allowed();
+}
+
+// 5. Check user access
+if ($currentForum['forum_of_group'] != 0) {
+    $show_forum = GroupManager::user_has_access(
+        api_get_user_id(),
+        $currentForum['forum_of_group'],
+        GroupManager::GROUP_TOOL_FORUM
+    );
+    if (!$show_forum) {
+        api_not_allowed();
+    }
+}
+
+// 6. Invited users can't create new threads
+if (api_is_invitee()) {
+    api_not_allowed(true);
+}
+
+$groupId = api_get_group_id();
+if (!empty($groupId)) {
+    $groupProperties = GroupManager :: get_group_properties($groupId);
+    $interbreadcrumb[] = array('url' => '../group/group.php?'.$cidreq, 'name' => get_lang('Groups'));
+    $interbreadcrumb[] = array('url' => '../group/group_space.php?'.$cidreq, 'name' => get_lang('GroupSpace').' '.$groupProperties['name']);
+    $interbreadcrumb[] = array('url' => 'viewforum.php?'.$cidreq.'&forum='.Security::remove_XSS($_GET['forum']), 'name' => $currentForum['forum_title']);
+    $interbreadcrumb[] = array('url' => 'newthread.php?'.$cidreq.'&forum='.Security::remove_XSS($_GET['forum']),'name' => get_lang('EditThread'));
+} else {
+    $interbreadcrumb[] = array('url' => 'index.php?'.$cidreq, 'name' => $nameTools);
+    $interbreadcrumb[] = array('url' => 'viewforumcategory.php?'.$cidreq.'&forumcategory='.$currentForumCategory['cat_id'], 'name' => $currentForumCategory['cat_title']);
+    $interbreadcrumb[] = array('url' => 'viewforum.php?'.$cidreq.'&forum='.Security::remove_XSS($_GET['forum']), 'name' => $currentForum['forum_title']);
+    $interbreadcrumb[] = array('url' => '#', 'name' => get_lang('EditThread'));
+}
+
+$tableLink = Database :: get_main_table(TABLE_MAIN_GRADEBOOK_LINK);
+
+/* Header */
+
+$htmlHeadXtra[] = <<<JS
+    <script>
+    $(document).on('ready', function() {
+        
+        if ($('#thread_qualify_gradebook').is(':checked') == true) {
+            document.getElementById('options_field').style.display = 'block';
+        } else {
+            document.getElementById('options_field').style.display = 'none';
+        }
+        
+        $('#thread_qualify_gradebook').click(function() {
+            if ($('#thread_qualify_gradebook').is(':checked') == true) {
+                document.getElementById('options_field').style.display = 'block';
+            } else {
+                document.getElementById('options_field').style.display = 'none';
+                $("[name='numeric_calification']").val(0);
+                $("[name='calification_notebook_title']").val('');
+                $("[name='weight_calification']").val(0);
+                $("[name='thread_peer_qualify'][value='0']").prop('checked', true);
+            }
+        });
+    });
+    </script>
+JS;
+
+if ($origin == 'learnpath') {
+    Display::display_reduced_header();
+} else {
+    Display :: display_header(null);
+}
+
+handle_forum_and_forumcategories();
+
+// Action links
+echo '<div class="actions">';
+echo '<span style="float:right;">'.search_link().'</span>';
+echo '<a href="viewforum.php?forum='.intval($_GET['forum']).'&'.$cidreq.'">'.
+    Display::return_icon('back.png',get_lang('BackToForum'),'',ICON_SIZE_MEDIUM).'</a>';
+echo '</div>';
+
+$threadData = getThreadInfo($threadId, $cId);
+
+$values = showUpdateThreadForm(
+    $currentForum,
+    $forumSettings,
+    $threadData
+);
+
+if (!empty($values) && isset($values['SubmitPost'])) {
+    
+    // update thread in table forum_thread.
+    updateThread($values);
+}
+
+if (isset($origin) && $origin != 'learnpath') {
+    Display :: display_footer();
+}

+ 238 - 4
main/forum/forumfunction.inc.php

@@ -180,8 +180,8 @@ function handle_forum_and_forumcategories($lp_id = null)
                 intval($list_threads[$i]['thread_id']),
                 api_get_session_id()
             );
-            if ($link_info) {
-                GradebookUtils::remove_resource_from_course_gradebook($link_info->getId());
+            if ($link_info !== false) {
+                GradebookUtils::remove_resource_from_course_gradebook($link_info['id']);
             }
         }
         $return_message = deleteForumCategoryThread($get_content, $get_id);
@@ -1844,6 +1844,37 @@ function get_threads($forum_id, $course_code = null)
     return $thread_list;
 }
 
+/**
+ * Get a thread by Id and course id
+ *
+ * @param int $threadId the thread Id
+ * @param int $cId the course id
+ * @return array containing all the information about the thread
+ */
+function getThreadInfo($threadId, $cId)
+{
+    $em = Database::getManager();
+    $forumThread = $em->getRepository('ChamiloCourseBundle:CForumThread')->findOneBy(['threadId' => $threadId, 'cId' => $cId]);
+    
+    $thread = [];
+    
+    if ($forumThread) {
+        $thread['threadId'] = $forumThread->getThreadId();
+        $thread['threadTitle'] = $forumThread->getThreadTitle();
+        $thread['forumId'] = $forumThread->getForumId();
+        $thread['sessionId'] = $forumThread->getSessionId();
+        $thread['threadSticky'] = $forumThread->getThreadSticky();
+        $thread['locked'] = $forumThread->getLocked();
+        $thread['threadTitleQualify'] = $forumThread->getThreadTitleQualify();
+        $thread['threadQualifyMax'] = $forumThread->getThreadQualifyMax();
+        $thread['threadCloseDate'] = $forumThread->getThreadCloseDate();
+        $thread['threadWeight'] = $forumThread->getThreadWeight();
+        $thread['threadPeerQualify'] = $forumThread->isThreadPeerQualify();
+    }
+
+    return $thread;
+}
+
 /**
  * Retrieve all posts of a given thread
  * @param int $threadId The thread ID
@@ -2268,6 +2299,75 @@ function count_number_of_forums_in_category($cat_id)
     return $row['number_of_forums'];
 }
 
+/**
+ * This function update a thread
+ *
+ * @param array $values - The form Values
+ * @return void HTML
+ *
+ */
+function updateThread($values)
+{
+    $threadTable = Database :: get_course_table(TABLE_FORUM_THREAD);
+    $courseId = api_get_course_int_id();
+        
+    $params = [
+            'thread_title' => $values['thread_title'],
+            'thread_sticky' => isset($values['thread_sticky']) ? $values['thread_sticky'] : null,
+            'thread_title_qualify' => $values['calification_notebook_title'],
+            'thread_qualify_max' => $values['numeric_calification'],
+            'thread_weight' => $values['weight_calification'],
+            'thread_peer_qualify' => $values['thread_peer_qualify'],
+        ];
+        $where = ['c_id = ? AND thread_id = ?' => [$courseId, $values['thread_id']]];
+        Database::update($threadTable, $params, $where);
+        
+        if (api_is_course_admin() == true) {
+            $option_chek = isset($values['thread_qualify_gradebook']) ? $values['thread_qualify_gradebook'] : false; // values 1 or 0
+            if ($option_chek) {
+                $id = $values['thread_id'];
+                $titleGradebook = Security::remove_XSS(stripslashes($values['calification_notebook_title']));
+                $valueCalification = isset($values['numeric_calification']) ? intval($values['numeric_calification']) : 0;
+                $weightCalification = isset($values['weight_calification']) ? floatval($values['weight_calification']) : 0;
+                $description = '';
+                $sessionId = api_get_session_id();
+                $courseId = api_get_course_id();
+
+                $linkInfo = GradebookUtils::is_resource_in_course_gradebook(
+                    $courseId,
+                    LINK_FORUM_THREAD,
+                    $id,
+                    $sessionId
+                );
+                $linkId = $linkInfo['id'];
+
+                if (!$linkInfo) {
+                    GradebookUtils::add_resource_to_course_gradebook(
+                        $values['category_id'],
+                        $courseId,
+                        LINK_FORUM_THREAD,
+                        $id,
+                        $titleGradebook,
+                        $weightCalification,
+                        $valueCalification,
+                        $description,
+                        1,
+                        $sessionId
+                    );
+                } else {
+                    $em = Database::getManager();
+                    $gradebookLink = $em->getRepository('ChamiloCoreBundle:GradebookLink')->find($linkId);
+                    $gradebookLink->setWeight($weightCalification);
+                    $em->persist($gradebookLink);
+                    $em->flush();
+                }
+            }
+        }
+        
+        $message = get_lang('EditPostStored').'<br />';
+        Display :: display_confirmation_message($message, false);
+}
+
 /**
  * This function stores a new thread. This is done through an entry in the forum_thread table AND
  * in the forum_post table because. The threads are also stored in the item_property table. (forum posts are not (yet))
@@ -2506,6 +2606,140 @@ function store_thread($current_forum, $values, $courseInfo = array(), $showMessa
     }
 }
 
+/**
+ * This function displays the form that is used to UPDATE a Thread.
+ * @param array $currentForum
+ * @param array $forumSetting
+ * @param array $formValues
+ * @return void HMTL
+ * @author José Loguercio <jose.loguercio@beeznest.com>
+ * @version february 2016, chamilo 1.10.4
+ */
+function showUpdateThreadForm($currentForum, $forumSetting, $formValues = '')
+{
+    $userInfo = api_get_user_info();
+    
+    $myThread = isset($_GET['thread']) ? intval($_GET['thread']) : '';
+    $myForum = isset($_GET['forum']) ? intval($_GET['forum']) : '';
+    $myGradebook = isset($_GET['gradebook']) ? Security::remove_XSS($_GET['gradebook']) : '';
+    $form = new FormValidator(
+        'thread',
+        'post',
+        api_get_self() . '?' . http_build_query([
+            'forum' => $myForum,
+            'gradebook' => $myGradebook,
+            'thread' => $myThread,
+        ]) . '&' . api_get_cidreq()
+    );
+    
+    $form->addElement('header', get_lang('EditThread'));
+    $form->setConstants(array('forum' => '5'));
+    $form->addElement('hidden', 'forum_id', $myForum);
+    $form->addElement('hidden', 'thread_id', $myThread);
+    $form->addElement('hidden', 'gradebook', $myGradebook);
+    $form->addElement('text', 'thread_title', get_lang('Title'));
+    $form->addElement('advanced_settings', 'advanced_params', get_lang('AdvancedParameters'));
+    $form->addElement('html', '<div id="advanced_params_options" style="display:none">');
+    if ((api_is_course_admin() || api_is_course_coach() || api_is_course_tutor()) && ($myThread)) {
+        // Thread qualify
+        if (Gradebook::is_active()) {
+            //Loading gradebook select
+            GradebookUtils::load_gradebook_select_in_tool($form);
+            $form->addElement(
+                'checkbox',
+                'thread_qualify_gradebook',
+                '',
+                get_lang('QualifyThreadGradebook'),
+                [
+                    'id' => 'thread_qualify_gradebook'
+                ]
+            );
+        } else {
+            $form->addElement('hidden', 'thread_qualify_gradebook', false);
+        }
+        
+        $form->addElement('html', '<div id="options_field" style="display:none">');
+        $form->addElement('text', 'numeric_calification', get_lang('QualificationNumeric'));
+        $form->applyFilter('numeric_calification', 'html_filter');
+        $form->addElement('text', 'calification_notebook_title', get_lang('TitleColumnGradebook'));
+        $form->applyFilter('calification_notebook_title', 'html_filter');
+        $form->addElement(
+            'text',
+            'weight_calification',
+            get_lang('QualifyWeight'),
+            array('value' => '0.00', 'onfocus' => "javascript: this.select();")
+        );
+        $form->applyFilter('weight_calification', 'html_filter');
+        $group = array();
+        $group[] = $form->createElement('radio', 'thread_peer_qualify', null, get_lang('Yes'), 1);
+        $group[] = $form->createElement('radio', 'thread_peer_qualify', null, get_lang('No'), 0);
+        $form->addGroup(
+            $group,
+            '',
+            [
+                get_lang('ForumThreadPeerScoring'),
+                get_lang('ForumThreadPeerScoringComment'),
+            ],
+            ' '
+        );
+        $form->addElement('html', '</div>');
+    }
+    if ($forumSetting['allow_post_notification'] && isset($userInfo['user_id'])) {
+        $form->addElement('checkbox', 'post_notification', '', get_lang('NotifyByEmail').' ('.$userInfo['mail'].')');
+    }
+    if ($forumSetting['allow_sticky'] && api_is_allowed_to_edit(null, true)) {
+        $form->addElement('checkbox', 'thread_sticky', '', get_lang('StickyPost'));
+    }
+    
+    $form->addElement('html', '</div>');
+    
+    if (!empty($formValues)) {
+        $defaults['thread_qualify_gradebook'] = ($formValues['threadQualifyMax'] > 0 && empty($_POST)) ? 1 : 0 ;
+        $defaults['thread_title'] = prepare4display($formValues['threadTitle']);
+        $defaults['thread_sticky'] = strval(intval($formValues['threadSticky']));
+        $defaults['thread_peer_qualify'] = intval($formValues['threadPeerQualify']);
+        $defaults['numeric_calification'] = $formValues['threadQualifyMax'];
+        $defaults['calification_notebook_title'] = $formValues['threadTitleQualify'];
+        $defaults['weight_calification'] = $formValues['threadWeight'];
+    } else {
+        $defaults['thread_qualify_gradebook'] = 0;
+        $defaults['numeric_calification'] = 0;
+        $defaults['calification_notebook_title'] = '';
+        $defaults['weight_calification'] = 0;
+        $defaults['thread_peer_qualify'] = 0;
+    }
+    $form->setDefaults(isset($defaults) ? $defaults : null);
+    
+    $form->addButtonUpdate(get_lang('ModifyThread'), 'SubmitPost');
+    
+    if ($form->validate()) {
+        $check = Security::check_token('post');
+        if ($check) {
+            $values = $form->exportValues();
+            if (isset($values['thread_qualify_gradebook']) &&
+                $values['thread_qualify_gradebook'] == '1' &&
+                empty($values['weight_calification'])
+            ) {
+                Display::display_error_message(
+                    get_lang('YouMustAssignWeightOfQualification').'&nbsp;<a href="javascript:window.history.go(-1);">'.
+                    get_lang('Back').'</a>',
+                    false
+                );
+                return false;
+            }
+            Security::clear_token();
+            return $values;
+        }
+    } else {
+        $token = Security::get_token();
+        $form->addElement('hidden', 'sec_token');
+        $form->setConstants(array('sec_token' => $token));
+        
+        
+        $form->display();
+    }
+}
+
 /**
  * This function displays the form that is used to add a post. This can be a new thread or a reply.
  * @param array $current_forum
@@ -3084,7 +3318,7 @@ function store_reply($current_forum, $values)
             }
 
             // Update the thread.
-            update_thread($values['thread_id'], $new_post_id, $post_date);
+            updateThreadInfo($values['thread_id'], $new_post_id, $post_date);
 
             // Update the forum.
             api_item_property_update(
@@ -3531,7 +3765,7 @@ function increase_thread_view($thread_id)
  * @author Patrick Cool <patrick.cool@UGent.be>, Ghent University
  * @version february 2006, dokeos 1.8
  */
-function update_thread($thread_id, $last_post_id, $post_date)
+function updateThreadInfo($thread_id, $last_post_id, $post_date)
 {
     $table_threads = Database :: get_course_table(TABLE_FORUM_THREAD);
     $course_id = api_get_course_int_id();

+ 12 - 10
main/forum/newthread.php

@@ -33,6 +33,8 @@ $this_section = SECTION_COURSES;
 // Notification for unauthorized people.
 api_protect_course_script(true);
 
+$cidreq = api_get_cidreq();
+
 $nameTools = get_lang('ToolForum');
 
 /* Including necessary files */
@@ -43,7 +45,7 @@ require_once 'forumfunction.inc.php';
 // Are we in a lp ?
 $origin = '';
 if (isset($_GET['origin'])) {
-    $origin =  Security::remove_XSS($_GET['origin']);
+    $origin = Security::remove_XSS($_GET['origin']);
 }
 
 /* MAIN DISPLAY SECTION */
@@ -112,15 +114,15 @@ if (api_is_invitee()) {
 
 $groupId = api_get_group_id();
 if (!empty($groupId)) {
-    $group_properties = GroupManager :: get_group_properties($groupId);
-    $interbreadcrumb[] = array('url' => '../group/group.php?'.api_get_cidreq(), 'name' => get_lang('Groups'));
-    $interbreadcrumb[] = array('url' => '../group/group_space.php?'.api_get_cidreq(), 'name' => get_lang('GroupSpace').' '.$group_properties['name']);
-    $interbreadcrumb[] = array('url' => 'viewforum.php?'.api_get_cidreq().'&forum='.Security::remove_XSS($_GET['forum']), 'name' => $current_forum['forum_title']);
-    $interbreadcrumb[] = array('url' => 'newthread.php?'.api_get_cidreq().'&forum='.Security::remove_XSS($_GET['forum']),'name' => get_lang('NewTopic'));
+    $groupProperties = GroupManager :: get_group_properties($groupId);
+    $interbreadcrumb[] = array('url' => '../group/group.php?'.$cidreq, 'name' => get_lang('Groups'));
+    $interbreadcrumb[] = array('url' => '../group/group_space.php?'.$cidreq, 'name' => get_lang('GroupSpace').' '.$groupProperties['name']);
+    $interbreadcrumb[] = array('url' => 'viewforum.php?'.$cidreq.'&forum='.Security::remove_XSS($_GET['forum']), 'name' => $current_forum['forum_title']);
+    $interbreadcrumb[] = array('url' => 'newthread.php?'.$cidreq.'&forum='.Security::remove_XSS($_GET['forum']),'name' => get_lang('NewTopic'));
 } else {
-    $interbreadcrumb[] = array('url' => 'index.php?'.api_get_cidreq(), 'name' => $nameTools);
-    $interbreadcrumb[] = array('url' => 'viewforumcategory.php?'.api_get_cidreq().'&forumcategory='.$current_forum_category['cat_id'], 'name' => $current_forum_category['cat_title']);
-    $interbreadcrumb[] = array('url' => 'viewforum.php?'.api_get_cidreq().'&forum='.Security::remove_XSS($_GET['forum']), 'name' => $current_forum['forum_title']);
+    $interbreadcrumb[] = array('url' => 'index.php?'.$cidreq, 'name' => $nameTools);
+    $interbreadcrumb[] = array('url' => 'viewforumcategory.php?'.$cidreq.'&forumcategory='.$current_forum_category['cat_id'], 'name' => $current_forum_category['cat_title']);
+    $interbreadcrumb[] = array('url' => 'viewforum.php?'.$cidreq.'&forum='.Security::remove_XSS($_GET['forum']), 'name' => $current_forum['forum_title']);
     $interbreadcrumb[] = array('url' => '#', 'name' => get_lang('NewTopic'));
 }
 
@@ -163,7 +165,7 @@ handle_forum_and_forumcategories();
 // Action links
 echo '<div class="actions">';
 echo '<span style="float:right;">'.search_link().'</span>';
-echo '<a href="viewforum.php?forum='.Security::remove_XSS($_GET['forum']).'&'.api_get_cidreq().'">'.
+echo '<a href="viewforum.php?forum='.Security::remove_XSS($_GET['forum']).'&'.$cidreq.'">'.
     Display::return_icon('back.png',get_lang('BackToForum'),'',ICON_SIZE_MEDIUM).'</a>';
 echo '</div>';
 

+ 8 - 1
main/forum/viewthread_flat.inc.php

@@ -15,6 +15,13 @@ if ((isset($_GET['action']) &&
     delete_attachment(0, $_GET['id_attach']);
 }
 
+
+// Are we in a lp ?
+$origin = '';
+if (isset($_GET['origin'])) {
+    $origin = Security::remove_XSS($_GET['origin']);
+}
+
 $sessionId = api_get_session_id();
 $_user = api_get_user_info();
 $userId = api_get_user_id();
@@ -311,7 +318,7 @@ if (isset($current_thread['thread_id'])) {
                         (api_is_allowed_to_edit(false, true) && !(api_is_course_coach() && $current_forum['session_id'] != $sessionId))
                     ) {
                         $html .= '&nbsp;&nbsp;<a href="' . api_get_self() . '?' . api_get_cidreq() . '&origin='
-                            . Security::remove_XSS($_GET['origin']) . '&action=delete_attach&id_attach='
+                            . Security::remove_XSS($origin) . '&action=delete_attach&id_attach='
                             . $attachment['iid'] . '&forum=' . $clean_forum_id . '&thread=' . $clean_thread_id
                             . '" onclick="javascript:if(!confirm(\''
                             . addslashes(api_htmlentities(get_lang('ConfirmYourChoice'), ENT_QUOTES))

+ 3 - 1
main/gradebook/lib/GradebookUtils.php

@@ -884,9 +884,11 @@ class GradebookUtils
                 $row = current($res);
                 $category_id = $row->getId();
             }
+            
+            return $category_id;
         }
 
-        return $category_id;
+        return false;
     }
 
     /**

+ 1 - 1
main/group/group_space.php

@@ -372,7 +372,7 @@ function get_number_of_group_users()
     $table_group_user = Database :: get_course_table(TABLE_GROUP_USER);
 
     // Query
-    $sql = "SELECT count(id) AS number_of_users
+    $sql = "SELECT count(iid) AS number_of_users
             FROM ".$table_group_user."
             WHERE c_id = $course_id AND group_id='".Database::escape_string($current_group['id'])."'";
     $result = Database::query($sql);

+ 55 - 0
main/inc/ajax/statistics.ajax.php

@@ -0,0 +1,55 @@
+<?php
+
+/* For licensing terms, see /license.txt */
+/**
+ * Responses to AJAX calls
+ */
+
+require_once '../global.inc.php';
+
+api_protect_admin_script();
+
+$action = isset($_REQUEST['a']) ? $_REQUEST['a'] : null;
+
+switch ($action) {
+    case 'recentlogins':
+        header('Content-type: application/json');
+        
+        $list = [];
+        
+        $all = Statistics::getRecentLoginStats();
+        $distinct = Statistics::getRecentLoginStats(true);
+        
+        foreach ($all as $tick => $tock) {
+            $list['labels'][] = $tick;
+        }
+        
+        $list['datasets'][0]['label'] = get_lang('Logins');
+        $list['datasets'][0]['fillColor'] = "rgba(151,187,205,0.2)";
+        $list['datasets'][0]['strokeColor'] = "rgba(151,187,205,1)";
+        $list['datasets'][0]['pointColor'] = "rgba(151,187,205,1)";
+        $list['datasets'][0]['pointStrokeColor'] = "#fff";
+        $list['datasets'][0]['pointHighlightFill'] = "#fff";
+        $list['datasets'][0]['pointHighlightStroke'] = "rgba(151,187,205,1)";
+        
+        foreach ($all as $tick => $tock) {
+            $list['datasets'][0]['data'][] = $tock;
+        }
+        
+        $list['datasets'][1]['label'] = get_lang('DistinctUsersLogins');
+        $list['datasets'][1]['fillColor'] = "rgba(0,204,0,0.2)";
+        $list['datasets'][1]['strokeColor'] = "rgba(0,204,0,1)";
+        $list['datasets'][1]['pointColor'] = "rgba(0,204,0,1)";
+        $list['datasets'][1]['pointStrokeColor'] = "#fff";
+        $list['datasets'][1]['pointHighlightFill'] = "#fff";
+        $list['datasets'][1]['pointHighlightStroke'] = "rgba(0,204,0,1)";
+        
+        foreach ($distinct as $tick => $tock) {
+            $list['datasets'][1]['data'][] = $tock;
+        }
+        
+        echo json_encode($list);
+        break;
+}
+
+

+ 21 - 27
main/inc/lib/agenda.lib.php

@@ -1253,7 +1253,7 @@ class Agenda
             case 'admin':
             case 'platform':
                 $sql = "SELECT * FROM ".$this->tbl_global_agenda."
-                        WHERE id = ".$id;
+                        WHERE id = $id";
                 $result = Database::query($sql);
                 if (Database::num_rows($result)) {
                     $event = Database::fetch_array($result, 'ASSOC');
@@ -1370,12 +1370,15 @@ class Agenda
         $sql = "SELECT DISTINCT to_user_id, to_group_id
                 FROM $tbl_property ip
                 INNER JOIN $tlb_course_agenda agenda
-                ON (ip.ref = agenda.id AND ip.c_id = agenda.c_id)
+                ON (
+                  ip.ref = agenda.id AND
+                  ip.c_id = agenda.c_id AND
+                  ip.tool = '".TOOL_CALENDAR_EVENT."'
+                )
                 WHERE
-                    ip.tool = '".TOOL_CALENDAR_EVENT."' AND
                     ref = $eventId AND
                     ip.visibility = '1' AND
-                    ip.c_id         = $courseId AND
+                    ip.c_id = $courseId AND
                     $sessionCondition
                 ";
 
@@ -1463,7 +1466,6 @@ class Agenda
         if (empty($courseInfo)) {
             return array();
         }
-
         $course_id = $courseInfo['real_id'];
 
         if (empty($course_id)) {
@@ -1471,7 +1473,6 @@ class Agenda
         }
 
         $session_id = intval($session_id);
-
         $user_id = intval($user_id);
         $groupList = GroupManager::get_group_list(null, $courseInfo['code']);
 
@@ -1482,8 +1483,6 @@ class Agenda
             }
         }
 
-        $group_memberships = [];
-
         if (!empty($groupId)) {
             if (!api_is_allowed_to_edit()) {
                 $user_id = api_get_user_id();
@@ -1539,12 +1538,10 @@ class Agenda
                 $where_condition = "( ip.to_user_id = $user_id OR ip.to_user_id IS NULL OR (ip.to_group_id IN (0, ".implode(", ", $group_memberships).")) ) ";
             }
 
-            $sessionCondition = '';
             if (empty($session_id)) {
                 $sessionCondition =  "
                 (
-                    agenda.session_id = 0 AND
-                    ip.session_id IS NULL
+                    agenda.session_id = 0 AND (ip.session_id IS NULL OR ip.session_id = 0)
                 ) ";
             } else {
                 $sessionCondition =  "
@@ -1563,9 +1560,8 @@ class Agenda
                         to_user_id
                     FROM $tlb_course_agenda agenda
                     INNER JOIN $tbl_property ip
-                    ON (agenda.id = ip.ref AND agenda.c_id = ip.c_id)
+                    ON (agenda.id = ip.ref AND agenda.c_id = ip.c_id AND ip.tool = '".TOOL_CALENDAR_EVENT."')
                     WHERE
-                        ip.tool = '".TOOL_CALENDAR_EVENT."' AND
                         $where_condition AND
                         ip.visibility = '1' AND
                         agenda.c_id = $course_id AND
@@ -1574,7 +1570,6 @@ class Agenda
                     ";
         } else {
             $visibilityCondition = " ip.visibility='1' AND ";
-            $sessionCondition = '';
 
             if (api_is_allowed_to_edit()) {
                 if ($user_id == 0) {
@@ -1591,7 +1586,7 @@ class Agenda
                 $sessionCondition =  "
                 (
                     agenda.session_id = 0 AND
-                    ip.session_id IS NULL
+                    (ip.session_id IS NULL OR ip.session_id = 0)
                 ) ";
             } else {
                 $sessionCondition =  "
@@ -1610,9 +1605,8 @@ class Agenda
                         to_user_id
                     FROM $tlb_course_agenda agenda
                     INNER JOIN $tbl_property ip
-                    ON (agenda.id = ip.ref AND agenda.c_id = ip.c_id)
+                    ON (agenda.id = ip.ref AND agenda.c_id = ip.c_id AND ip.tool='".TOOL_CALENDAR_EVENT."' )
                     WHERE
-                        ip.tool='".TOOL_CALENDAR_EVENT."' AND
                         $where_condition
                         $visibilityCondition
                         agenda.c_id = $course_id AND
@@ -1645,24 +1639,24 @@ class Agenda
         if (Database::num_rows($result)) {
             $eventsAdded = array_column($this->events, 'unique_id');
             while ($row = Database::fetch_array($result, 'ASSOC')) {
-                $eventId = $row['ref'];
-                $items = $this->getUsersAndGroupSubscribedToEvent(
-                    $eventId,
-                    $course_id,
-                    $this->sessionId
-                );
-                $group_to_array = $items['groups'];
-                $user_to_array = $items['users'];
                 $event = array();
                 $event['id'] = 'course_'.$row['id'];
                 $event['unique_id']  = $row['iid'];
-
                 // To avoid doubles
                 if (in_array($event['unique_id'], $eventsAdded)) {
                     continue;
                 }
 
                 $eventsAdded[] = $event['unique_id'];
+
+                $eventId = $row['ref'];
+                $items = $this->getUsersAndGroupSubscribedToEvent(
+                    $eventId,
+                    $course_id,
+                    $this->sessionId
+                );
+                $group_to_array = $items['groups'];
+                $user_to_array = $items['users'];
                 $attachment = $this->getAttachment($row['id'], $courseInfo);
 
                 if (!empty($attachment)) {
@@ -1741,7 +1735,7 @@ class Agenda
                     $event['type'] = 'group';
                 }
 
-                //Event sent to a user?
+                // Event sent to a user?
                 if (isset($row['to_user_id'])) {
                     $sent_to = array();
                     if (!empty($user_to_array)) {

+ 1 - 1
main/inc/lib/api.lib.php

@@ -6886,7 +6886,7 @@ function api_get_real_ip(){
     $ip = trim($_SERVER['REMOTE_ADDR']);
     if (!empty($_SERVER['HTTP_X_FORWARDED_FOR'])) {
         if (preg_match('/,/', $_SERVER['HTTP_X_FORWARDED_FOR'])) {
-            list($ip1, $ip2) = explode(',', $_SERVER['HTTP_X_FORWARDED_FOR']);
+            @list($ip1, $ip2) = @explode(',', $_SERVER['HTTP_X_FORWARDED_FOR']);
         } else {
             $ip1 = $_SERVER['HTTP_X_FORWARDED_FOR'];
         }

+ 7 - 4
main/inc/lib/database.lib.php

@@ -131,6 +131,7 @@ class Database
     public function connect($params = array(), $sysPath = '', $entityRootPath = '')
     {
         $config = self::getDoctrineConfig($entityRootPath);
+        $config->setAutoGenerateProxyClasses(true);
 
         $config->setEntityNamespaces(
             array(
@@ -329,8 +330,9 @@ class Database
             try {
                 $result = $connection->executeQuery($query);
             } catch (Exception $e) {
-                 error_log($e->getMessage());
+                error_log($e->getMessage());
                 api_not_allowed(false, get_lang('GeneralError'));
+
                 exit;
             }
         }
@@ -639,9 +641,8 @@ class Database
      */
     public static function getDoctrineConfig($path)
     {
-        $isDevMode = true;
-        $isSimpleMode = false;
-        $proxyDir = null;
+        $isDevMode = false;
+        $isSimpleMode = false; // related to annotations @Entity
         $cache = null;
         $path = !empty($path) ? $path : api_get_path(SYS_PATH);
 
@@ -651,6 +652,8 @@ class Database
             $path.'src/Chamilo/CourseBundle/Entity'
         );
 
+        $proxyDir = $path.'app/cache/';
+
         return \Doctrine\ORM\Tools\Setup::createAnnotationMetadataConfiguration(
             $paths,
             $isDevMode,

+ 73 - 7
main/inc/lib/exercise.lib.php

@@ -2872,6 +2872,53 @@ HOTSPOT;
 
         return $return;
     }
+    
+    /**
+     * Get the correct answer count for a fill blanks question
+     * 
+     * @param int $question_id
+     * @param int $exercise_id
+     * @return int
+     */
+    public static function getNumberStudentsFillBlanksAnwserCount(
+        $question_id,
+        $exercise_id
+    ) {
+        $listStudentsId = [];
+            $listAllStudentInfo = CourseManager::get_student_list_from_course_code(
+                api_get_course_id(),
+                true
+            );
+            foreach ($listAllStudentInfo as $i => $listStudentInfo) {
+                $listStudentsId[] = $listStudentInfo['user_id'];
+            }
+
+            $listFillTheBlankResult = FillBlanks::getFillTheBlankTabResult(
+                $exercise_id,
+                $question_id,
+                $listStudentsId,
+                '1970-01-01',
+                '3000-01-01'
+            );
+
+            $arrayCount = [];
+
+            foreach ($listFillTheBlankResult as $resultCount) {
+                foreach ($resultCount as $index => $count) {
+                    //this is only for declare the array index per answer
+                    $arrayCount[$index] = 0;
+                }
+            }
+
+            foreach ($listFillTheBlankResult as $resultCount) {
+                foreach ($resultCount as $index => $count) {
+                    $count = ($count === 0) ? 1 : 0;
+                    $arrayCount[$index] += $count;
+                }
+            }
+            
+            return $arrayCount;
+    }
 
     /**
      * @param int $question_id
@@ -2915,7 +2962,7 @@ HOTSPOT;
                 $listStudentsId[] = $listStudentInfo['user_id'];
             }
 
-            $listFillTheBlankResult = getFillTheBlankTabResult(
+            $listFillTheBlankResult = FillBlanks::getFillTheBlankTabResult(
                 $exercise_id,
                 $question_id,
                 $listStudentsId,
@@ -2923,7 +2970,7 @@ HOTSPOT;
                 '3000-01-01'
             );
 
-            return getNbResultFillBlankAll($listFillTheBlankResult);
+            return FillBlanks::getNbResultFillBlankAll($listFillTheBlankResult);
         }
 
         if (empty($session_id)) {
@@ -3135,7 +3182,8 @@ HOTSPOT;
                     while ($row = Database::fetch_array($result, 'ASSOC')) {
                         $fill_blank = self::check_fill_in_blanks(
                             $correct_answer,
-                            $row['answer']
+                            $row['answer'],
+                            $current_answer
                         );
                         if (isset($fill_blank[$current_answer]) && $fill_blank[$current_answer] == 1) {
                             $good_answers++;
@@ -3160,7 +3208,7 @@ HOTSPOT;
      * @param string $user_answer
      * @return array
      */
-    public static function check_fill_in_blanks($answer, $user_answer)
+    public static function check_fill_in_blanks($answer, $user_answer, $current_answer)
     {
         // the question is encoded like this
         // [A] B [C] D [E] F::10,10,10@1
@@ -3217,10 +3265,28 @@ HOTSPOT;
 
             preg_match_all('#\[([^[]*)\]#', $str, $arr);
             $str = str_replace('\r\n', '', $str);
-            $choice = $arr[1];
-
+            $choices = $arr[1];
+            $choice = [];
+            $check = false;
+            $i = 0;
+            foreach ($choices as $item) {
+                if ($current_answer === $item) {
+                    $check = true;
+                }
+                if ($check) {
+                    $choice[] = $item;
+                    $i++;
+                }
+                if ($i == 3) {
+                    break;
+                }
+            }
             $tmp = api_strrpos($choice[$j], ' / ');
-            $choice[$j] = api_substr($choice[$j], 0, $tmp);
+            
+            if ($tmp !== false) {
+                $choice[$j] = api_substr($choice[$j], 0, $tmp);
+            }
+            
             $choice[$j] = trim($choice[$j]);
 
             //Needed to let characters ' and " to work as part of an answer

+ 5 - 1
main/inc/lib/extra_field.lib.php

@@ -80,6 +80,7 @@ class ExtraField extends Model
         $this->table = Database::get_main_table(TABLE_EXTRA_FIELD);
         $this->table_field_options = Database::get_main_table(TABLE_EXTRA_FIELD_OPTIONS);
         $this->table_field_values = Database::get_main_table(TABLE_EXTRA_FIELD_VALUES);
+        $this->handler_id = 'item_id';
 
         switch ($this->type) {
             case 'calendar_event':
@@ -87,12 +88,15 @@ class ExtraField extends Model
                 break;
             case 'course':
                 $this->extraFieldType = EntityExtraField::COURSE_FIELD_TYPE;
+                $this->primaryKey = 'id';
                 break;
             case 'user':
                 $this->extraFieldType = EntityExtraField::USER_FIELD_TYPE;
+                $this->primaryKey = 'id';
                 break;
             case 'session':
                 $this->extraFieldType = EntityExtraField::SESSION_FIELD_TYPE;
+                $this->primaryKey = 'id';
                 break;
             case 'question':
                 $this->extraFieldType = EntityExtraField::QUESTION_FIELD_TYPE;
@@ -2161,7 +2165,7 @@ EOF;
                 if (strpos($rule->field, '_second') === false) {
                     //No _second
                     $original_field = str_replace($stringToSearch, '', $rule->field);
-                    $field_option = $this->get_handler_field_info_by_variable($original_field);
+                    $field_option = $this->get_handler_field_info_by_field_variable($original_field);
 
                     if ($field_option['field_type'] == ExtraField::FIELD_TYPE_DOUBLE_SELECT) {
 

+ 3731 - 0
main/inc/lib/javascript/chartjs/Chart.js

@@ -0,0 +1,3731 @@
+/*!
+ * Chart.js
+ * http://chartjs.org/
+ * Version: 1.0.2
+ *
+ * Copyright 2015 Nick Downie
+ * Released under the MIT license
+ * https://github.com/nnnick/Chart.js/blob/master/LICENSE.md
+ */
+
+
+(function(){
+
+	"use strict";
+
+	//Declare root variable - window in the browser, global on the server
+	var root = this,
+		previous = root.Chart;
+
+	//Occupy the global variable of Chart, and create a simple base class
+	var Chart = function(context){
+		var chart = this;
+		this.canvas = context.canvas;
+
+		this.ctx = context;
+
+		//Variables global to the chart
+		var computeDimension = function(element,dimension)
+		{
+			if (element['offset'+dimension])
+			{
+				return element['offset'+dimension];
+			}
+			else
+			{
+				return document.defaultView.getComputedStyle(element).getPropertyValue(dimension);
+			}
+		};
+
+		var width = this.width = computeDimension(context.canvas,'Width') || context.canvas.width;
+		var height = this.height = computeDimension(context.canvas,'Height') || context.canvas.height;
+
+		width = this.width = context.canvas.width;
+		height = this.height = context.canvas.height;
+		this.aspectRatio = this.width / this.height;
+		//High pixel density displays - multiply the size of the canvas height/width by the device pixel ratio, then scale.
+		helpers.retinaScale(this);
+
+		return this;
+	};
+	//Globally expose the defaults to allow for user updating/changing
+	Chart.defaults = {
+		global: {
+			// Boolean - Whether to animate the chart
+			animation: true,
+
+			// Number - Number of animation steps
+			animationSteps: 60,
+
+			// String - Animation easing effect
+			animationEasing: "easeOutQuart",
+
+			// Boolean - If we should show the scale at all
+			showScale: true,
+
+			// Boolean - If we want to override with a hard coded scale
+			scaleOverride: false,
+
+			// ** Required if scaleOverride is true **
+			// Number - The number of steps in a hard coded scale
+			scaleSteps: null,
+			// Number - The value jump in the hard coded scale
+			scaleStepWidth: null,
+			// Number - The scale starting value
+			scaleStartValue: null,
+
+			// String - Colour of the scale line
+			scaleLineColor: "rgba(0,0,0,.1)",
+
+			// Number - Pixel width of the scale line
+			scaleLineWidth: 1,
+
+			// Boolean - Whether to show labels on the scale
+			scaleShowLabels: true,
+
+			// Interpolated JS string - can access value
+			scaleLabel: "<%=value%>",
+
+			// Boolean - Whether the scale should stick to integers, and not show any floats even if drawing space is there
+			scaleIntegersOnly: true,
+
+			// Boolean - Whether the scale should start at zero, or an order of magnitude down from the lowest value
+			scaleBeginAtZero: false,
+
+			// String - Scale label font declaration for the scale label
+			scaleFontFamily: "'Helvetica Neue', 'Helvetica', 'Arial', sans-serif",
+
+			// Number - Scale label font size in pixels
+			scaleFontSize: 12,
+
+			// String - Scale label font weight style
+			scaleFontStyle: "normal",
+
+			// String - Scale label font colour
+			scaleFontColor: "#666",
+
+			// Boolean - whether or not the chart should be responsive and resize when the browser does.
+			responsive: false,
+
+			// Boolean - whether to maintain the starting aspect ratio or not when responsive, if set to false, will take up entire container
+			maintainAspectRatio: true,
+
+			// Boolean - Determines whether to draw tooltips on the canvas or not - attaches events to touchmove & mousemove
+			showTooltips: true,
+
+			// Boolean - Determines whether to draw built-in tooltip or call custom tooltip function
+			customTooltips: false,
+
+			// Array - Array of string names to attach tooltip events
+			tooltipEvents: ["mousemove", "touchstart", "touchmove", "mouseout"],
+
+			// String - Tooltip background colour
+			tooltipFillColor: "rgba(0,0,0,0.8)",
+
+			// String - Tooltip label font declaration for the scale label
+			tooltipFontFamily: "'Helvetica Neue', 'Helvetica', 'Arial', sans-serif",
+
+			// Number - Tooltip label font size in pixels
+			tooltipFontSize: 14,
+
+			// String - Tooltip font weight style
+			tooltipFontStyle: "normal",
+
+			// String - Tooltip label font colour
+			tooltipFontColor: "#fff",
+
+			// String - Tooltip title font declaration for the scale label
+			tooltipTitleFontFamily: "'Helvetica Neue', 'Helvetica', 'Arial', sans-serif",
+
+			// Number - Tooltip title font size in pixels
+			tooltipTitleFontSize: 14,
+
+			// String - Tooltip title font weight style
+			tooltipTitleFontStyle: "bold",
+
+			// String - Tooltip title font colour
+			tooltipTitleFontColor: "#fff",
+
+			// String - Tooltip title template
+			tooltipTitleTemplate: "<%= label%>",
+
+			// Number - pixel width of padding around tooltip text
+			tooltipYPadding: 6,
+
+			// Number - pixel width of padding around tooltip text
+			tooltipXPadding: 6,
+
+			// Number - Size of the caret on the tooltip
+			tooltipCaretSize: 8,
+
+			// Number - Pixel radius of the tooltip border
+			tooltipCornerRadius: 6,
+
+			// Number - Pixel offset from point x to tooltip edge
+			tooltipXOffset: 10,
+
+			// String - Template string for single tooltips
+			tooltipTemplate: "<%if (label){%><%=label%>: <%}%><%= value %>",
+
+			// String - Template string for single tooltips
+			multiTooltipTemplate: "<%= datasetLabel %>: <%= value %>",
+
+			// String - Colour behind the legend colour block
+			multiTooltipKeyBackground: '#fff',
+
+			// Array - A list of colors to use as the defaults
+			segmentColorDefault: ["#A6CEE3", "#1F78B4", "#B2DF8A", "#33A02C", "#FB9A99", "#E31A1C", "#FDBF6F", "#FF7F00", "#CAB2D6", "#6A3D9A", "#B4B482", "#B15928" ],
+
+			// Array - A list of highlight colors to use as the defaults
+			segmentHighlightColorDefaults: [ "#CEF6FF", "#47A0DC", "#DAFFB2", "#5BC854", "#FFC2C1", "#FF4244", "#FFE797", "#FFA728", "#F2DAFE", "#9265C2", "#DCDCAA", "#D98150" ],
+
+			// Function - Will fire on animation progression.
+			onAnimationProgress: function(){},
+
+			// Function - Will fire on animation completion.
+			onAnimationComplete: function(){}
+
+		}
+	};
+
+	//Create a dictionary of chart types, to allow for extension of existing types
+	Chart.types = {};
+
+	//Global Chart helpers object for utility methods and classes
+	var helpers = Chart.helpers = {};
+
+		//-- Basic js utility methods
+	var each = helpers.each = function(loopable,callback,self){
+			var additionalArgs = Array.prototype.slice.call(arguments, 3);
+			// Check to see if null or undefined firstly.
+			if (loopable){
+				if (loopable.length === +loopable.length){
+					var i;
+					for (i=0; i<loopable.length; i++){
+						callback.apply(self,[loopable[i], i].concat(additionalArgs));
+					}
+				}
+				else{
+					for (var item in loopable){
+						callback.apply(self,[loopable[item],item].concat(additionalArgs));
+					}
+				}
+			}
+		},
+		clone = helpers.clone = function(obj){
+			var objClone = {};
+			each(obj,function(value,key){
+				if (obj.hasOwnProperty(key)){
+					objClone[key] = value;
+				}
+			});
+			return objClone;
+		},
+		extend = helpers.extend = function(base){
+			each(Array.prototype.slice.call(arguments,1), function(extensionObject) {
+				each(extensionObject,function(value,key){
+					if (extensionObject.hasOwnProperty(key)){
+						base[key] = value;
+					}
+				});
+			});
+			return base;
+		},
+		merge = helpers.merge = function(base,master){
+			//Merge properties in left object over to a shallow clone of object right.
+			var args = Array.prototype.slice.call(arguments,0);
+			args.unshift({});
+			return extend.apply(null, args);
+		},
+		indexOf = helpers.indexOf = function(arrayToSearch, item){
+			if (Array.prototype.indexOf) {
+				return arrayToSearch.indexOf(item);
+			}
+			else{
+				for (var i = 0; i < arrayToSearch.length; i++) {
+					if (arrayToSearch[i] === item) return i;
+				}
+				return -1;
+			}
+		},
+		where = helpers.where = function(collection, filterCallback){
+			var filtered = [];
+
+			helpers.each(collection, function(item){
+				if (filterCallback(item)){
+					filtered.push(item);
+				}
+			});
+
+			return filtered;
+		},
+		findNextWhere = helpers.findNextWhere = function(arrayToSearch, filterCallback, startIndex){
+			// Default to start of the array
+			if (!startIndex){
+				startIndex = -1;
+			}
+			for (var i = startIndex + 1; i < arrayToSearch.length; i++) {
+				var currentItem = arrayToSearch[i];
+				if (filterCallback(currentItem)){
+					return currentItem;
+				}
+			}
+		},
+		findPreviousWhere = helpers.findPreviousWhere = function(arrayToSearch, filterCallback, startIndex){
+			// Default to end of the array
+			if (!startIndex){
+				startIndex = arrayToSearch.length;
+			}
+			for (var i = startIndex - 1; i >= 0; i--) {
+				var currentItem = arrayToSearch[i];
+				if (filterCallback(currentItem)){
+					return currentItem;
+				}
+			}
+		},
+		inherits = helpers.inherits = function(extensions){
+			//Basic javascript inheritance based on the model created in Backbone.js
+			var parent = this;
+			var ChartElement = (extensions && extensions.hasOwnProperty("constructor")) ? extensions.constructor : function(){ return parent.apply(this, arguments); };
+
+			var Surrogate = function(){ this.constructor = ChartElement;};
+			Surrogate.prototype = parent.prototype;
+			ChartElement.prototype = new Surrogate();
+
+			ChartElement.extend = inherits;
+
+			if (extensions) extend(ChartElement.prototype, extensions);
+
+			ChartElement.__super__ = parent.prototype;
+
+			return ChartElement;
+		},
+		noop = helpers.noop = function(){},
+		uid = helpers.uid = (function(){
+			var id=0;
+			return function(){
+				return "chart-" + id++;
+			};
+		})(),
+		warn = helpers.warn = function(str){
+			//Method for warning of errors
+			if (window.console && typeof window.console.warn === "function") console.warn(str);
+		},
+		amd = helpers.amd = (typeof define === 'function' && define.amd),
+		//-- Math methods
+		isNumber = helpers.isNumber = function(n){
+			return !isNaN(parseFloat(n)) && isFinite(n);
+		},
+		max = helpers.max = function(array){
+			return Math.max.apply( Math, array );
+		},
+		min = helpers.min = function(array){
+			return Math.min.apply( Math, array );
+		},
+		cap = helpers.cap = function(valueToCap,maxValue,minValue){
+			if(isNumber(maxValue)) {
+				if( valueToCap > maxValue ) {
+					return maxValue;
+				}
+			}
+			else if(isNumber(minValue)){
+				if ( valueToCap < minValue ){
+					return minValue;
+				}
+			}
+			return valueToCap;
+		},
+		getDecimalPlaces = helpers.getDecimalPlaces = function(num){
+			if (num%1!==0 && isNumber(num)){
+				var s = num.toString();
+				if(s.indexOf("e-") < 0){
+					// no exponent, e.g. 0.01
+					return s.split(".")[1].length;
+				}
+				else if(s.indexOf(".") < 0) {
+					// no decimal point, e.g. 1e-9
+					return parseInt(s.split("e-")[1]);
+				}
+				else {
+					// exponent and decimal point, e.g. 1.23e-9
+					var parts = s.split(".")[1].split("e-");
+					return parts[0].length + parseInt(parts[1]);
+				}
+			}
+			else {
+				return 0;
+			}
+		},
+		toRadians = helpers.radians = function(degrees){
+			return degrees * (Math.PI/180);
+		},
+		// Gets the angle from vertical upright to the point about a centre.
+		getAngleFromPoint = helpers.getAngleFromPoint = function(centrePoint, anglePoint){
+			var distanceFromXCenter = anglePoint.x - centrePoint.x,
+				distanceFromYCenter = anglePoint.y - centrePoint.y,
+				radialDistanceFromCenter = Math.sqrt( distanceFromXCenter * distanceFromXCenter + distanceFromYCenter * distanceFromYCenter);
+
+
+			var angle = Math.PI * 2 + Math.atan2(distanceFromYCenter, distanceFromXCenter);
+
+			//If the segment is in the top left quadrant, we need to add another rotation to the angle
+			if (distanceFromXCenter < 0 && distanceFromYCenter < 0){
+				angle += Math.PI*2;
+			}
+
+			return {
+				angle: angle,
+				distance: radialDistanceFromCenter
+			};
+		},
+		aliasPixel = helpers.aliasPixel = function(pixelWidth){
+			return (pixelWidth % 2 === 0) ? 0 : 0.5;
+		},
+		splineCurve = helpers.splineCurve = function(FirstPoint,MiddlePoint,AfterPoint,t){
+			//Props to Rob Spencer at scaled innovation for his post on splining between points
+			//http://scaledinnovation.com/analytics/splines/aboutSplines.html
+			var d01=Math.sqrt(Math.pow(MiddlePoint.x-FirstPoint.x,2)+Math.pow(MiddlePoint.y-FirstPoint.y,2)),
+				d12=Math.sqrt(Math.pow(AfterPoint.x-MiddlePoint.x,2)+Math.pow(AfterPoint.y-MiddlePoint.y,2)),
+				fa=t*d01/(d01+d12),// scaling factor for triangle Ta
+				fb=t*d12/(d01+d12);
+			return {
+				inner : {
+					x : MiddlePoint.x-fa*(AfterPoint.x-FirstPoint.x),
+					y : MiddlePoint.y-fa*(AfterPoint.y-FirstPoint.y)
+				},
+				outer : {
+					x: MiddlePoint.x+fb*(AfterPoint.x-FirstPoint.x),
+					y : MiddlePoint.y+fb*(AfterPoint.y-FirstPoint.y)
+				}
+			};
+		},
+		calculateOrderOfMagnitude = helpers.calculateOrderOfMagnitude = function(val){
+			return Math.floor(Math.log(val) / Math.LN10);
+		},
+		calculateScaleRange = helpers.calculateScaleRange = function(valuesArray, drawingSize, textSize, startFromZero, integersOnly){
+
+			//Set a minimum step of two - a point at the top of the graph, and a point at the base
+			var minSteps = 2,
+				maxSteps = Math.floor(drawingSize/(textSize * 1.5)),
+				skipFitting = (minSteps >= maxSteps);
+
+			// Filter out null values since these would min() to zero
+			var values = [];
+			each(valuesArray, function( v ){
+				v == null || values.push( v );
+			});
+			var minValue = min(values),
+			    maxValue = max(values);
+
+			// We need some degree of separation here to calculate the scales if all the values are the same
+			// Adding/minusing 0.5 will give us a range of 1.
+			if (maxValue === minValue){
+				maxValue += 0.5;
+				// So we don't end up with a graph with a negative start value if we've said always start from zero
+				if (minValue >= 0.5 && !startFromZero){
+					minValue -= 0.5;
+				}
+				else{
+					// Make up a whole number above the values
+					maxValue += 0.5;
+				}
+			}
+
+			var	valueRange = Math.abs(maxValue - minValue),
+				rangeOrderOfMagnitude = calculateOrderOfMagnitude(valueRange),
+				graphMax = Math.ceil(maxValue / (1 * Math.pow(10, rangeOrderOfMagnitude))) * Math.pow(10, rangeOrderOfMagnitude),
+				graphMin = (startFromZero) ? 0 : Math.floor(minValue / (1 * Math.pow(10, rangeOrderOfMagnitude))) * Math.pow(10, rangeOrderOfMagnitude),
+				graphRange = graphMax - graphMin,
+				stepValue = Math.pow(10, rangeOrderOfMagnitude),
+				numberOfSteps = Math.round(graphRange / stepValue);
+
+			//If we have more space on the graph we'll use it to give more definition to the data
+			while((numberOfSteps > maxSteps || (numberOfSteps * 2) < maxSteps) && !skipFitting) {
+				if(numberOfSteps > maxSteps){
+					stepValue *=2;
+					numberOfSteps = Math.round(graphRange/stepValue);
+					// Don't ever deal with a decimal number of steps - cancel fitting and just use the minimum number of steps.
+					if (numberOfSteps % 1 !== 0){
+						skipFitting = true;
+					}
+				}
+				//We can fit in double the amount of scale points on the scale
+				else{
+					//If user has declared ints only, and the step value isn't a decimal
+					if (integersOnly && rangeOrderOfMagnitude >= 0){
+						//If the user has said integers only, we need to check that making the scale more granular wouldn't make it a float
+						if(stepValue/2 % 1 === 0){
+							stepValue /=2;
+							numberOfSteps = Math.round(graphRange/stepValue);
+						}
+						//If it would make it a float break out of the loop
+						else{
+							break;
+						}
+					}
+					//If the scale doesn't have to be an int, make the scale more granular anyway.
+					else{
+						stepValue /=2;
+						numberOfSteps = Math.round(graphRange/stepValue);
+					}
+
+				}
+			}
+
+			if (skipFitting){
+				numberOfSteps = minSteps;
+				stepValue = graphRange / numberOfSteps;
+			}
+
+			return {
+				steps : numberOfSteps,
+				stepValue : stepValue,
+				min : graphMin,
+				max	: graphMin + (numberOfSteps * stepValue)
+			};
+
+		},
+		/* jshint ignore:start */
+		// Blows up jshint errors based on the new Function constructor
+		//Templating methods
+		//Javascript micro templating by John Resig - source at http://ejohn.org/blog/javascript-micro-templating/
+		template = helpers.template = function(templateString, valuesObject){
+
+			// If templateString is function rather than string-template - call the function for valuesObject
+
+			if(templateString instanceof Function){
+			 	return templateString(valuesObject);
+		 	}
+
+			var cache = {};
+			function tmpl(str, data){
+				// Figure out if we're getting a template, or if we need to
+				// load the template - and be sure to cache the result.
+				var fn = !/\W/.test(str) ?
+				cache[str] = cache[str] :
+
+				// Generate a reusable function that will serve as a template
+				// generator (and which will be cached).
+				new Function("obj",
+					"var p=[],print=function(){p.push.apply(p,arguments);};" +
+
+					// Introduce the data as local variables using with(){}
+					"with(obj){p.push('" +
+
+					// Convert the template into pure JavaScript
+					str
+						.replace(/[\r\t\n]/g, " ")
+						.split("<%").join("\t")
+						.replace(/((^|%>)[^\t]*)'/g, "$1\r")
+						.replace(/\t=(.*?)%>/g, "',$1,'")
+						.split("\t").join("');")
+						.split("%>").join("p.push('")
+						.split("\r").join("\\'") +
+					"');}return p.join('');"
+				);
+
+				// Provide some basic currying to the user
+				return data ? fn( data ) : fn;
+			}
+			return tmpl(templateString,valuesObject);
+		},
+		/* jshint ignore:end */
+		generateLabels = helpers.generateLabels = function(templateString,numberOfSteps,graphMin,stepValue){
+			var labelsArray = new Array(numberOfSteps);
+			if (templateString){
+				each(labelsArray,function(val,index){
+					labelsArray[index] = template(templateString,{value: (graphMin + (stepValue*(index+1)))});
+				});
+			}
+			return labelsArray;
+		},
+		//--Animation methods
+		//Easing functions adapted from Robert Penner's easing equations
+		//http://www.robertpenner.com/easing/
+		easingEffects = helpers.easingEffects = {
+			linear: function (t) {
+				return t;
+			},
+			easeInQuad: function (t) {
+				return t * t;
+			},
+			easeOutQuad: function (t) {
+				return -1 * t * (t - 2);
+			},
+			easeInOutQuad: function (t) {
+				if ((t /= 1 / 2) < 1){
+					return 1 / 2 * t * t;
+				}
+				return -1 / 2 * ((--t) * (t - 2) - 1);
+			},
+			easeInCubic: function (t) {
+				return t * t * t;
+			},
+			easeOutCubic: function (t) {
+				return 1 * ((t = t / 1 - 1) * t * t + 1);
+			},
+			easeInOutCubic: function (t) {
+				if ((t /= 1 / 2) < 1){
+					return 1 / 2 * t * t * t;
+				}
+				return 1 / 2 * ((t -= 2) * t * t + 2);
+			},
+			easeInQuart: function (t) {
+				return t * t * t * t;
+			},
+			easeOutQuart: function (t) {
+				return -1 * ((t = t / 1 - 1) * t * t * t - 1);
+			},
+			easeInOutQuart: function (t) {
+				if ((t /= 1 / 2) < 1){
+					return 1 / 2 * t * t * t * t;
+				}
+				return -1 / 2 * ((t -= 2) * t * t * t - 2);
+			},
+			easeInQuint: function (t) {
+				return 1 * (t /= 1) * t * t * t * t;
+			},
+			easeOutQuint: function (t) {
+				return 1 * ((t = t / 1 - 1) * t * t * t * t + 1);
+			},
+			easeInOutQuint: function (t) {
+				if ((t /= 1 / 2) < 1){
+					return 1 / 2 * t * t * t * t * t;
+				}
+				return 1 / 2 * ((t -= 2) * t * t * t * t + 2);
+			},
+			easeInSine: function (t) {
+				return -1 * Math.cos(t / 1 * (Math.PI / 2)) + 1;
+			},
+			easeOutSine: function (t) {
+				return 1 * Math.sin(t / 1 * (Math.PI / 2));
+			},
+			easeInOutSine: function (t) {
+				return -1 / 2 * (Math.cos(Math.PI * t / 1) - 1);
+			},
+			easeInExpo: function (t) {
+				return (t === 0) ? 1 : 1 * Math.pow(2, 10 * (t / 1 - 1));
+			},
+			easeOutExpo: function (t) {
+				return (t === 1) ? 1 : 1 * (-Math.pow(2, -10 * t / 1) + 1);
+			},
+			easeInOutExpo: function (t) {
+				if (t === 0){
+					return 0;
+				}
+				if (t === 1){
+					return 1;
+				}
+				if ((t /= 1 / 2) < 1){
+					return 1 / 2 * Math.pow(2, 10 * (t - 1));
+				}
+				return 1 / 2 * (-Math.pow(2, -10 * --t) + 2);
+			},
+			easeInCirc: function (t) {
+				if (t >= 1){
+					return t;
+				}
+				return -1 * (Math.sqrt(1 - (t /= 1) * t) - 1);
+			},
+			easeOutCirc: function (t) {
+				return 1 * Math.sqrt(1 - (t = t / 1 - 1) * t);
+			},
+			easeInOutCirc: function (t) {
+				if ((t /= 1 / 2) < 1){
+					return -1 / 2 * (Math.sqrt(1 - t * t) - 1);
+				}
+				return 1 / 2 * (Math.sqrt(1 - (t -= 2) * t) + 1);
+			},
+			easeInElastic: function (t) {
+				var s = 1.70158;
+				var p = 0;
+				var a = 1;
+				if (t === 0){
+					return 0;
+				}
+				if ((t /= 1) == 1){
+					return 1;
+				}
+				if (!p){
+					p = 1 * 0.3;
+				}
+				if (a < Math.abs(1)) {
+					a = 1;
+					s = p / 4;
+				} else{
+					s = p / (2 * Math.PI) * Math.asin(1 / a);
+				}
+				return -(a * Math.pow(2, 10 * (t -= 1)) * Math.sin((t * 1 - s) * (2 * Math.PI) / p));
+			},
+			easeOutElastic: function (t) {
+				var s = 1.70158;
+				var p = 0;
+				var a = 1;
+				if (t === 0){
+					return 0;
+				}
+				if ((t /= 1) == 1){
+					return 1;
+				}
+				if (!p){
+					p = 1 * 0.3;
+				}
+				if (a < Math.abs(1)) {
+					a = 1;
+					s = p / 4;
+				} else{
+					s = p / (2 * Math.PI) * Math.asin(1 / a);
+				}
+				return a * Math.pow(2, -10 * t) * Math.sin((t * 1 - s) * (2 * Math.PI) / p) + 1;
+			},
+			easeInOutElastic: function (t) {
+				var s = 1.70158;
+				var p = 0;
+				var a = 1;
+				if (t === 0){
+					return 0;
+				}
+				if ((t /= 1 / 2) == 2){
+					return 1;
+				}
+				if (!p){
+					p = 1 * (0.3 * 1.5);
+				}
+				if (a < Math.abs(1)) {
+					a = 1;
+					s = p / 4;
+				} else {
+					s = p / (2 * Math.PI) * Math.asin(1 / a);
+				}
+				if (t < 1){
+					return -0.5 * (a * Math.pow(2, 10 * (t -= 1)) * Math.sin((t * 1 - s) * (2 * Math.PI) / p));}
+				return a * Math.pow(2, -10 * (t -= 1)) * Math.sin((t * 1 - s) * (2 * Math.PI) / p) * 0.5 + 1;
+			},
+			easeInBack: function (t) {
+				var s = 1.70158;
+				return 1 * (t /= 1) * t * ((s + 1) * t - s);
+			},
+			easeOutBack: function (t) {
+				var s = 1.70158;
+				return 1 * ((t = t / 1 - 1) * t * ((s + 1) * t + s) + 1);
+			},
+			easeInOutBack: function (t) {
+				var s = 1.70158;
+				if ((t /= 1 / 2) < 1){
+					return 1 / 2 * (t * t * (((s *= (1.525)) + 1) * t - s));
+				}
+				return 1 / 2 * ((t -= 2) * t * (((s *= (1.525)) + 1) * t + s) + 2);
+			},
+			easeInBounce: function (t) {
+				return 1 - easingEffects.easeOutBounce(1 - t);
+			},
+			easeOutBounce: function (t) {
+				if ((t /= 1) < (1 / 2.75)) {
+					return 1 * (7.5625 * t * t);
+				} else if (t < (2 / 2.75)) {
+					return 1 * (7.5625 * (t -= (1.5 / 2.75)) * t + 0.75);
+				} else if (t < (2.5 / 2.75)) {
+					return 1 * (7.5625 * (t -= (2.25 / 2.75)) * t + 0.9375);
+				} else {
+					return 1 * (7.5625 * (t -= (2.625 / 2.75)) * t + 0.984375);
+				}
+			},
+			easeInOutBounce: function (t) {
+				if (t < 1 / 2){
+					return easingEffects.easeInBounce(t * 2) * 0.5;
+				}
+				return easingEffects.easeOutBounce(t * 2 - 1) * 0.5 + 1 * 0.5;
+			}
+		},
+		//Request animation polyfill - http://www.paulirish.com/2011/requestanimationframe-for-smart-animating/
+		requestAnimFrame = helpers.requestAnimFrame = (function(){
+			return window.requestAnimationFrame ||
+				window.webkitRequestAnimationFrame ||
+				window.mozRequestAnimationFrame ||
+				window.oRequestAnimationFrame ||
+				window.msRequestAnimationFrame ||
+				function(callback) {
+					return window.setTimeout(callback, 1000 / 60);
+				};
+		})(),
+		cancelAnimFrame = helpers.cancelAnimFrame = (function(){
+			return window.cancelAnimationFrame ||
+				window.webkitCancelAnimationFrame ||
+				window.mozCancelAnimationFrame ||
+				window.oCancelAnimationFrame ||
+				window.msCancelAnimationFrame ||
+				function(callback) {
+					return window.clearTimeout(callback, 1000 / 60);
+				};
+		})(),
+		animationLoop = helpers.animationLoop = function(callback,totalSteps,easingString,onProgress,onComplete,chartInstance){
+
+			var currentStep = 0,
+				easingFunction = easingEffects[easingString] || easingEffects.linear;
+
+			var animationFrame = function(){
+				currentStep++;
+				var stepDecimal = currentStep/totalSteps;
+				var easeDecimal = easingFunction(stepDecimal);
+
+				callback.call(chartInstance,easeDecimal,stepDecimal, currentStep);
+				onProgress.call(chartInstance,easeDecimal,stepDecimal);
+				if (currentStep < totalSteps){
+					chartInstance.animationFrame = requestAnimFrame(animationFrame);
+				} else{
+					onComplete.apply(chartInstance);
+				}
+			};
+			requestAnimFrame(animationFrame);
+		},
+		//-- DOM methods
+		getRelativePosition = helpers.getRelativePosition = function(evt){
+			var mouseX, mouseY;
+			var e = evt.originalEvent || evt,
+				canvas = evt.currentTarget || evt.srcElement,
+				boundingRect = canvas.getBoundingClientRect();
+
+			if (e.touches){
+				mouseX = e.touches[0].clientX - boundingRect.left;
+				mouseY = e.touches[0].clientY - boundingRect.top;
+
+			}
+			else{
+				mouseX = e.clientX - boundingRect.left;
+				mouseY = e.clientY - boundingRect.top;
+			}
+
+			return {
+				x : mouseX,
+				y : mouseY
+			};
+
+		},
+		addEvent = helpers.addEvent = function(node,eventType,method){
+			if (node.addEventListener){
+				node.addEventListener(eventType,method);
+			} else if (node.attachEvent){
+				node.attachEvent("on"+eventType, method);
+			} else {
+				node["on"+eventType] = method;
+			}
+		},
+		removeEvent = helpers.removeEvent = function(node, eventType, handler){
+			if (node.removeEventListener){
+				node.removeEventListener(eventType, handler, false);
+			} else if (node.detachEvent){
+				node.detachEvent("on"+eventType,handler);
+			} else{
+				node["on" + eventType] = noop;
+			}
+		},
+		bindEvents = helpers.bindEvents = function(chartInstance, arrayOfEvents, handler){
+			// Create the events object if it's not already present
+			if (!chartInstance.events) chartInstance.events = {};
+
+			each(arrayOfEvents,function(eventName){
+				chartInstance.events[eventName] = function(){
+					handler.apply(chartInstance, arguments);
+				};
+				addEvent(chartInstance.chart.canvas,eventName,chartInstance.events[eventName]);
+			});
+		},
+		unbindEvents = helpers.unbindEvents = function (chartInstance, arrayOfEvents) {
+			each(arrayOfEvents, function(handler,eventName){
+				removeEvent(chartInstance.chart.canvas, eventName, handler);
+			});
+		},
+		getMaximumWidth = helpers.getMaximumWidth = function(domNode){
+			var container = domNode.parentNode,
+			    padding = parseInt(getStyle(container, 'padding-left')) + parseInt(getStyle(container, 'padding-right'));
+			// TODO = check cross browser stuff with this.
+			return container ? container.clientWidth - padding : 0;
+		},
+		getMaximumHeight = helpers.getMaximumHeight = function(domNode){
+			var container = domNode.parentNode,
+			    padding = parseInt(getStyle(container, 'padding-bottom')) + parseInt(getStyle(container, 'padding-top'));
+			// TODO = check cross browser stuff with this.
+			return container ? container.clientHeight - padding : 0;
+		},
+		getStyle = helpers.getStyle = function (el, property) {
+			return el.currentStyle ?
+				el.currentStyle[property] :
+				document.defaultView.getComputedStyle(el, null).getPropertyValue(property);
+		},
+		getMaximumSize = helpers.getMaximumSize = helpers.getMaximumWidth, // legacy support
+		retinaScale = helpers.retinaScale = function(chart){
+			var ctx = chart.ctx,
+				width = chart.canvas.width,
+				height = chart.canvas.height;
+
+			if (window.devicePixelRatio) {
+				ctx.canvas.style.width = width + "px";
+				ctx.canvas.style.height = height + "px";
+				ctx.canvas.height = height * window.devicePixelRatio;
+				ctx.canvas.width = width * window.devicePixelRatio;
+				ctx.scale(window.devicePixelRatio, window.devicePixelRatio);
+			}
+		},
+		//-- Canvas methods
+		clear = helpers.clear = function(chart){
+			chart.ctx.clearRect(0,0,chart.width,chart.height);
+		},
+		fontString = helpers.fontString = function(pixelSize,fontStyle,fontFamily){
+			return fontStyle + " " + pixelSize+"px " + fontFamily;
+		},
+		longestText = helpers.longestText = function(ctx,font,arrayOfStrings){
+			ctx.font = font;
+			var longest = 0;
+			each(arrayOfStrings,function(string){
+				var textWidth = ctx.measureText(string).width;
+				longest = (textWidth > longest) ? textWidth : longest;
+			});
+			return longest;
+		},
+		drawRoundedRectangle = helpers.drawRoundedRectangle = function(ctx,x,y,width,height,radius){
+			ctx.beginPath();
+			ctx.moveTo(x + radius, y);
+			ctx.lineTo(x + width - radius, y);
+			ctx.quadraticCurveTo(x + width, y, x + width, y + radius);
+			ctx.lineTo(x + width, y + height - radius);
+			ctx.quadraticCurveTo(x + width, y + height, x + width - radius, y + height);
+			ctx.lineTo(x + radius, y + height);
+			ctx.quadraticCurveTo(x, y + height, x, y + height - radius);
+			ctx.lineTo(x, y + radius);
+			ctx.quadraticCurveTo(x, y, x + radius, y);
+			ctx.closePath();
+		};
+
+
+	//Store a reference to each instance - allowing us to globally resize chart instances on window resize.
+	//Destroy method on the chart will remove the instance of the chart from this reference.
+	Chart.instances = {};
+
+	Chart.Type = function(data,options,chart){
+		this.options = options;
+		this.chart = chart;
+		this.id = uid();
+		//Add the chart instance to the global namespace
+		Chart.instances[this.id] = this;
+
+		// Initialize is always called when a chart type is created
+		// By default it is a no op, but it should be extended
+		if (options.responsive){
+			this.resize();
+		}
+		this.initialize.call(this,data);
+	};
+
+	//Core methods that'll be a part of every chart type
+	extend(Chart.Type.prototype,{
+		initialize : function(){return this;},
+		clear : function(){
+			clear(this.chart);
+			return this;
+		},
+		stop : function(){
+			// Stops any current animation loop occuring
+			Chart.animationService.cancelAnimation(this);
+			return this;
+		},
+		resize : function(callback){
+			this.stop();
+			var canvas = this.chart.canvas,
+				newWidth = getMaximumWidth(this.chart.canvas),
+				newHeight = this.options.maintainAspectRatio ? newWidth / this.chart.aspectRatio : getMaximumHeight(this.chart.canvas);
+
+			canvas.width = this.chart.width = newWidth;
+			canvas.height = this.chart.height = newHeight;
+
+			retinaScale(this.chart);
+
+			if (typeof callback === "function"){
+				callback.apply(this, Array.prototype.slice.call(arguments, 1));
+			}
+			return this;
+		},
+		reflow : noop,
+		render : function(reflow){
+			if (reflow){
+				this.reflow();
+			}
+			
+			if (this.options.animation && !reflow){
+				var animation = new Chart.Animation();
+				animation.numSteps = this.options.animationSteps;
+				animation.easing = this.options.animationEasing;
+				
+				// render function
+				animation.render = function(chartInstance, animationObject) {
+					var easingFunction = helpers.easingEffects[animationObject.easing];
+					var stepDecimal = animationObject.currentStep / animationObject.numSteps;
+					var easeDecimal = easingFunction(stepDecimal);
+					
+					chartInstance.draw(easeDecimal, stepDecimal, animationObject.currentStep);
+				};
+				
+				// user events
+				animation.onAnimationProgress = this.options.onAnimationProgress;
+				animation.onAnimationComplete = this.options.onAnimationComplete;
+				
+				Chart.animationService.addAnimation(this, animation);
+			}
+			else{
+				this.draw();
+				this.options.onAnimationComplete.call(this);
+			}
+			return this;
+		},
+		generateLegend : function(){
+			return template(this.options.legendTemplate,this);
+		},
+		destroy : function(){
+			this.clear();
+			unbindEvents(this, this.events);
+			var canvas = this.chart.canvas;
+
+			// Reset canvas height/width attributes starts a fresh with the canvas context
+			canvas.width = this.chart.width;
+			canvas.height = this.chart.height;
+
+			// < IE9 doesn't support removeProperty
+			if (canvas.style.removeProperty) {
+				canvas.style.removeProperty('width');
+				canvas.style.removeProperty('height');
+			} else {
+				canvas.style.removeAttribute('width');
+				canvas.style.removeAttribute('height');
+			}
+
+			delete Chart.instances[this.id];
+		},
+		showTooltip : function(ChartElements, forceRedraw){
+			// Only redraw the chart if we've actually changed what we're hovering on.
+			if (typeof this.activeElements === 'undefined') this.activeElements = [];
+
+			var isChanged = (function(Elements){
+				var changed = false;
+
+				if (Elements.length !== this.activeElements.length){
+					changed = true;
+					return changed;
+				}
+
+				each(Elements, function(element, index){
+					if (element !== this.activeElements[index]){
+						changed = true;
+					}
+				}, this);
+				return changed;
+			}).call(this, ChartElements);
+
+			if (!isChanged && !forceRedraw){
+				return;
+			}
+			else{
+				this.activeElements = ChartElements;
+			}
+			this.draw();
+			if(this.options.customTooltips){
+				this.options.customTooltips(false);
+			}
+			if (ChartElements.length > 0){
+				// If we have multiple datasets, show a MultiTooltip for all of the data points at that index
+				if (this.datasets && this.datasets.length > 1) {
+					var dataArray,
+						dataIndex;
+
+					for (var i = this.datasets.length - 1; i >= 0; i--) {
+						dataArray = this.datasets[i].points || this.datasets[i].bars || this.datasets[i].segments;
+						dataIndex = indexOf(dataArray, ChartElements[0]);
+						if (dataIndex !== -1){
+							break;
+						}
+					}
+					var tooltipLabels = [],
+						tooltipColors = [],
+						medianPosition = (function(index) {
+
+							// Get all the points at that particular index
+							var Elements = [],
+								dataCollection,
+								xPositions = [],
+								yPositions = [],
+								xMax,
+								yMax,
+								xMin,
+								yMin;
+							helpers.each(this.datasets, function(dataset){
+								dataCollection = dataset.points || dataset.bars || dataset.segments;
+								if (dataCollection[dataIndex] && dataCollection[dataIndex].hasValue()){
+									Elements.push(dataCollection[dataIndex]);
+								}
+							});
+
+							helpers.each(Elements, function(element) {
+								xPositions.push(element.x);
+								yPositions.push(element.y);
+
+
+								//Include any colour information about the element
+								tooltipLabels.push(helpers.template(this.options.multiTooltipTemplate, element));
+								tooltipColors.push({
+									fill: element._saved.fillColor || element.fillColor,
+									stroke: element._saved.strokeColor || element.strokeColor
+								});
+
+							}, this);
+
+							yMin = min(yPositions);
+							yMax = max(yPositions);
+
+							xMin = min(xPositions);
+							xMax = max(xPositions);
+
+							return {
+								x: (xMin > this.chart.width/2) ? xMin : xMax,
+								y: (yMin + yMax)/2
+							};
+						}).call(this, dataIndex);
+
+					new Chart.MultiTooltip({
+						x: medianPosition.x,
+						y: medianPosition.y,
+						xPadding: this.options.tooltipXPadding,
+						yPadding: this.options.tooltipYPadding,
+						xOffset: this.options.tooltipXOffset,
+						fillColor: this.options.tooltipFillColor,
+						textColor: this.options.tooltipFontColor,
+						fontFamily: this.options.tooltipFontFamily,
+						fontStyle: this.options.tooltipFontStyle,
+						fontSize: this.options.tooltipFontSize,
+						titleTextColor: this.options.tooltipTitleFontColor,
+						titleFontFamily: this.options.tooltipTitleFontFamily,
+						titleFontStyle: this.options.tooltipTitleFontStyle,
+						titleFontSize: this.options.tooltipTitleFontSize,
+						cornerRadius: this.options.tooltipCornerRadius,
+						labels: tooltipLabels,
+						legendColors: tooltipColors,
+						legendColorBackground : this.options.multiTooltipKeyBackground,
+						title: template(this.options.tooltipTitleTemplate,ChartElements[0]),
+						chart: this.chart,
+						ctx: this.chart.ctx,
+						custom: this.options.customTooltips
+					}).draw();
+
+				} else {
+					each(ChartElements, function(Element) {
+						var tooltipPosition = Element.tooltipPosition();
+						new Chart.Tooltip({
+							x: Math.round(tooltipPosition.x),
+							y: Math.round(tooltipPosition.y),
+							xPadding: this.options.tooltipXPadding,
+							yPadding: this.options.tooltipYPadding,
+							fillColor: this.options.tooltipFillColor,
+							textColor: this.options.tooltipFontColor,
+							fontFamily: this.options.tooltipFontFamily,
+							fontStyle: this.options.tooltipFontStyle,
+							fontSize: this.options.tooltipFontSize,
+							caretHeight: this.options.tooltipCaretSize,
+							cornerRadius: this.options.tooltipCornerRadius,
+							text: template(this.options.tooltipTemplate, Element),
+							chart: this.chart,
+							custom: this.options.customTooltips
+						}).draw();
+					}, this);
+				}
+			}
+			return this;
+		},
+		toBase64Image : function(){
+			return this.chart.canvas.toDataURL.apply(this.chart.canvas, arguments);
+		}
+	});
+
+	Chart.Type.extend = function(extensions){
+
+		var parent = this;
+
+		var ChartType = function(){
+			return parent.apply(this,arguments);
+		};
+
+		//Copy the prototype object of the this class
+		ChartType.prototype = clone(parent.prototype);
+		//Now overwrite some of the properties in the base class with the new extensions
+		extend(ChartType.prototype, extensions);
+
+		ChartType.extend = Chart.Type.extend;
+
+		if (extensions.name || parent.prototype.name){
+
+			var chartName = extensions.name || parent.prototype.name;
+			//Assign any potential default values of the new chart type
+
+			//If none are defined, we'll use a clone of the chart type this is being extended from.
+			//I.e. if we extend a line chart, we'll use the defaults from the line chart if our new chart
+			//doesn't define some defaults of their own.
+
+			var baseDefaults = (Chart.defaults[parent.prototype.name]) ? clone(Chart.defaults[parent.prototype.name]) : {};
+
+			Chart.defaults[chartName] = extend(baseDefaults,extensions.defaults);
+
+			Chart.types[chartName] = ChartType;
+
+			//Register this new chart type in the Chart prototype
+			Chart.prototype[chartName] = function(data,options){
+				var config = merge(Chart.defaults.global, Chart.defaults[chartName], options || {});
+				return new ChartType(data,config,this);
+			};
+		} else{
+			warn("Name not provided for this chart, so it hasn't been registered");
+		}
+		return parent;
+	};
+
+	Chart.Element = function(configuration){
+		extend(this,configuration);
+		this.initialize.apply(this,arguments);
+		this.save();
+	};
+	extend(Chart.Element.prototype,{
+		initialize : function(){},
+		restore : function(props){
+			if (!props){
+				extend(this,this._saved);
+			} else {
+				each(props,function(key){
+					this[key] = this._saved[key];
+				},this);
+			}
+			return this;
+		},
+		save : function(){
+			this._saved = clone(this);
+			delete this._saved._saved;
+			return this;
+		},
+		update : function(newProps){
+			each(newProps,function(value,key){
+				this._saved[key] = this[key];
+				this[key] = value;
+			},this);
+			return this;
+		},
+		transition : function(props,ease){
+			each(props,function(value,key){
+				this[key] = ((value - this._saved[key]) * ease) + this._saved[key];
+			},this);
+			return this;
+		},
+		tooltipPosition : function(){
+			return {
+				x : this.x,
+				y : this.y
+			};
+		},
+		hasValue: function(){
+			return isNumber(this.value);
+		}
+	});
+
+	Chart.Element.extend = inherits;
+
+
+	Chart.Point = Chart.Element.extend({
+		display: true,
+		inRange: function(chartX,chartY){
+			var hitDetectionRange = this.hitDetectionRadius + this.radius;
+			return ((Math.pow(chartX-this.x, 2)+Math.pow(chartY-this.y, 2)) < Math.pow(hitDetectionRange,2));
+		},
+		draw : function(){
+			if (this.display){
+				var ctx = this.ctx;
+				ctx.beginPath();
+
+				ctx.arc(this.x, this.y, this.radius, 0, Math.PI*2);
+				ctx.closePath();
+
+				ctx.strokeStyle = this.strokeColor;
+				ctx.lineWidth = this.strokeWidth;
+
+				ctx.fillStyle = this.fillColor;
+
+				ctx.fill();
+				ctx.stroke();
+			}
+
+
+			//Quick debug for bezier curve splining
+			//Highlights control points and the line between them.
+			//Handy for dev - stripped in the min version.
+
+			// ctx.save();
+			// ctx.fillStyle = "black";
+			// ctx.strokeStyle = "black"
+			// ctx.beginPath();
+			// ctx.arc(this.controlPoints.inner.x,this.controlPoints.inner.y, 2, 0, Math.PI*2);
+			// ctx.fill();
+
+			// ctx.beginPath();
+			// ctx.arc(this.controlPoints.outer.x,this.controlPoints.outer.y, 2, 0, Math.PI*2);
+			// ctx.fill();
+
+			// ctx.moveTo(this.controlPoints.inner.x,this.controlPoints.inner.y);
+			// ctx.lineTo(this.x, this.y);
+			// ctx.lineTo(this.controlPoints.outer.x,this.controlPoints.outer.y);
+			// ctx.stroke();
+
+			// ctx.restore();
+
+
+
+		}
+	});
+
+	Chart.Arc = Chart.Element.extend({
+		inRange : function(chartX,chartY){
+
+			var pointRelativePosition = helpers.getAngleFromPoint(this, {
+				x: chartX,
+				y: chartY
+			});
+
+			// Normalize all angles to 0 - 2*PI (0 - 360°)
+			var pointRelativeAngle = pointRelativePosition.angle % (Math.PI * 2),
+			    startAngle = (Math.PI * 2 + this.startAngle) % (Math.PI * 2),
+			    endAngle = (Math.PI * 2 + this.endAngle) % (Math.PI * 2) || 360;
+
+			// Calculate wether the pointRelativeAngle is between the start and the end angle
+			var betweenAngles = (endAngle < startAngle) ?
+				pointRelativeAngle <= endAngle || pointRelativeAngle >= startAngle:
+				pointRelativeAngle >= startAngle && pointRelativeAngle <= endAngle;
+
+			//Check if within the range of the open/close angle
+			var withinRadius = (pointRelativePosition.distance >= this.innerRadius && pointRelativePosition.distance <= this.outerRadius);
+
+			return (betweenAngles && withinRadius);
+			//Ensure within the outside of the arc centre, but inside arc outer
+		},
+		tooltipPosition : function(){
+			var centreAngle = this.startAngle + ((this.endAngle - this.startAngle) / 2),
+				rangeFromCentre = (this.outerRadius - this.innerRadius) / 2 + this.innerRadius;
+			return {
+				x : this.x + (Math.cos(centreAngle) * rangeFromCentre),
+				y : this.y + (Math.sin(centreAngle) * rangeFromCentre)
+			};
+		},
+		draw : function(animationPercent){
+
+			var easingDecimal = animationPercent || 1;
+
+			var ctx = this.ctx;
+
+			ctx.beginPath();
+
+			ctx.arc(this.x, this.y, this.outerRadius < 0 ? 0 : this.outerRadius, this.startAngle, this.endAngle);
+
+            ctx.arc(this.x, this.y, this.innerRadius < 0 ? 0 : this.innerRadius, this.endAngle, this.startAngle, true);
+
+			ctx.closePath();
+			ctx.strokeStyle = this.strokeColor;
+			ctx.lineWidth = this.strokeWidth;
+
+			ctx.fillStyle = this.fillColor;
+
+			ctx.fill();
+			ctx.lineJoin = 'bevel';
+
+			if (this.showStroke){
+				ctx.stroke();
+			}
+		}
+	});
+
+	Chart.Rectangle = Chart.Element.extend({
+		draw : function(){
+			var ctx = this.ctx,
+				halfWidth = this.width/2,
+				leftX = this.x - halfWidth,
+				rightX = this.x + halfWidth,
+				top = this.base - (this.base - this.y),
+				halfStroke = this.strokeWidth / 2;
+
+			// Canvas doesn't allow us to stroke inside the width so we can
+			// adjust the sizes to fit if we're setting a stroke on the line
+			if (this.showStroke){
+				leftX += halfStroke;
+				rightX -= halfStroke;
+				top += halfStroke;
+			}
+
+			ctx.beginPath();
+
+			ctx.fillStyle = this.fillColor;
+			ctx.strokeStyle = this.strokeColor;
+			ctx.lineWidth = this.strokeWidth;
+
+			// It'd be nice to keep this class totally generic to any rectangle
+			// and simply specify which border to miss out.
+			ctx.moveTo(leftX, this.base);
+			ctx.lineTo(leftX, top);
+			ctx.lineTo(rightX, top);
+			ctx.lineTo(rightX, this.base);
+			ctx.fill();
+			if (this.showStroke){
+				ctx.stroke();
+			}
+		},
+		height : function(){
+			return this.base - this.y;
+		},
+		inRange : function(chartX,chartY){
+			return (chartX >= this.x - this.width/2 && chartX <= this.x + this.width/2) && (chartY >= this.y && chartY <= this.base);
+		}
+	});
+
+	Chart.Animation = Chart.Element.extend({
+		currentStep: null, // the current animation step
+		numSteps: 60, // default number of steps
+		easing: "", // the easing to use for this animation
+		render: null, // render function used by the animation service
+		
+		onAnimationProgress: null, // user specified callback to fire on each step of the animation 
+		onAnimationComplete: null, // user specified callback to fire when the animation finishes
+	});
+	
+	Chart.Tooltip = Chart.Element.extend({
+		draw : function(){
+
+			var ctx = this.chart.ctx;
+
+			ctx.font = fontString(this.fontSize,this.fontStyle,this.fontFamily);
+
+			this.xAlign = "center";
+			this.yAlign = "above";
+
+			//Distance between the actual element.y position and the start of the tooltip caret
+			var caretPadding = this.caretPadding = 2;
+
+			var tooltipWidth = ctx.measureText(this.text).width + 2*this.xPadding,
+				tooltipRectHeight = this.fontSize + 2*this.yPadding,
+				tooltipHeight = tooltipRectHeight + this.caretHeight + caretPadding;
+
+			if (this.x + tooltipWidth/2 >this.chart.width){
+				this.xAlign = "left";
+			} else if (this.x - tooltipWidth/2 < 0){
+				this.xAlign = "right";
+			}
+
+			if (this.y - tooltipHeight < 0){
+				this.yAlign = "below";
+			}
+
+
+			var tooltipX = this.x - tooltipWidth/2,
+				tooltipY = this.y - tooltipHeight;
+
+			ctx.fillStyle = this.fillColor;
+
+			// Custom Tooltips
+			if(this.custom){
+				this.custom(this);
+			}
+			else{
+				switch(this.yAlign)
+				{
+				case "above":
+					//Draw a caret above the x/y
+					ctx.beginPath();
+					ctx.moveTo(this.x,this.y - caretPadding);
+					ctx.lineTo(this.x + this.caretHeight, this.y - (caretPadding + this.caretHeight));
+					ctx.lineTo(this.x - this.caretHeight, this.y - (caretPadding + this.caretHeight));
+					ctx.closePath();
+					ctx.fill();
+					break;
+				case "below":
+					tooltipY = this.y + caretPadding + this.caretHeight;
+					//Draw a caret below the x/y
+					ctx.beginPath();
+					ctx.moveTo(this.x, this.y + caretPadding);
+					ctx.lineTo(this.x + this.caretHeight, this.y + caretPadding + this.caretHeight);
+					ctx.lineTo(this.x - this.caretHeight, this.y + caretPadding + this.caretHeight);
+					ctx.closePath();
+					ctx.fill();
+					break;
+				}
+
+				switch(this.xAlign)
+				{
+				case "left":
+					tooltipX = this.x - tooltipWidth + (this.cornerRadius + this.caretHeight);
+					break;
+				case "right":
+					tooltipX = this.x - (this.cornerRadius + this.caretHeight);
+					break;
+				}
+
+				drawRoundedRectangle(ctx,tooltipX,tooltipY,tooltipWidth,tooltipRectHeight,this.cornerRadius);
+
+				ctx.fill();
+
+				ctx.fillStyle = this.textColor;
+				ctx.textAlign = "center";
+				ctx.textBaseline = "middle";
+				ctx.fillText(this.text, tooltipX + tooltipWidth/2, tooltipY + tooltipRectHeight/2);
+			}
+		}
+	});
+
+	Chart.MultiTooltip = Chart.Element.extend({
+		initialize : function(){
+			this.font = fontString(this.fontSize,this.fontStyle,this.fontFamily);
+
+			this.titleFont = fontString(this.titleFontSize,this.titleFontStyle,this.titleFontFamily);
+
+			this.titleHeight = this.title ? this.titleFontSize * 1.5 : 0;
+			this.height = (this.labels.length * this.fontSize) + ((this.labels.length-1) * (this.fontSize/2)) + (this.yPadding*2) + this.titleHeight;
+
+			this.ctx.font = this.titleFont;
+
+			var titleWidth = this.ctx.measureText(this.title).width,
+				//Label has a legend square as well so account for this.
+				labelWidth = longestText(this.ctx,this.font,this.labels) + this.fontSize + 3,
+				longestTextWidth = max([labelWidth,titleWidth]);
+
+			this.width = longestTextWidth + (this.xPadding*2);
+
+
+			var halfHeight = this.height/2;
+
+			//Check to ensure the height will fit on the canvas
+			if (this.y - halfHeight < 0 ){
+				this.y = halfHeight;
+			} else if (this.y + halfHeight > this.chart.height){
+				this.y = this.chart.height - halfHeight;
+			}
+
+			//Decide whether to align left or right based on position on canvas
+			if (this.x > this.chart.width/2){
+				this.x -= this.xOffset + this.width;
+			} else {
+				this.x += this.xOffset;
+			}
+
+
+		},
+		getLineHeight : function(index){
+			var baseLineHeight = this.y - (this.height/2) + this.yPadding,
+				afterTitleIndex = index-1;
+
+			//If the index is zero, we're getting the title
+			if (index === 0){
+				return baseLineHeight + this.titleHeight / 3;
+			} else{
+				return baseLineHeight + ((this.fontSize * 1.5 * afterTitleIndex) + this.fontSize / 2) + this.titleHeight;
+			}
+
+		},
+		draw : function(){
+			// Custom Tooltips
+			if(this.custom){
+				this.custom(this);
+			}
+			else{
+				drawRoundedRectangle(this.ctx,this.x,this.y - this.height/2,this.width,this.height,this.cornerRadius);
+				var ctx = this.ctx;
+				ctx.fillStyle = this.fillColor;
+				ctx.fill();
+				ctx.closePath();
+
+				ctx.textAlign = "left";
+				ctx.textBaseline = "middle";
+				ctx.fillStyle = this.titleTextColor;
+				ctx.font = this.titleFont;
+
+				ctx.fillText(this.title,this.x + this.xPadding, this.getLineHeight(0));
+
+				ctx.font = this.font;
+				helpers.each(this.labels,function(label,index){
+					ctx.fillStyle = this.textColor;
+					ctx.fillText(label,this.x + this.xPadding + this.fontSize + 3, this.getLineHeight(index + 1));
+
+					//A bit gnarly, but clearing this rectangle breaks when using explorercanvas (clears whole canvas)
+					//ctx.clearRect(this.x + this.xPadding, this.getLineHeight(index + 1) - this.fontSize/2, this.fontSize, this.fontSize);
+					//Instead we'll make a white filled block to put the legendColour palette over.
+
+					ctx.fillStyle = this.legendColorBackground;
+					ctx.fillRect(this.x + this.xPadding, this.getLineHeight(index + 1) - this.fontSize/2, this.fontSize, this.fontSize);
+
+					ctx.fillStyle = this.legendColors[index].fill;
+					ctx.fillRect(this.x + this.xPadding, this.getLineHeight(index + 1) - this.fontSize/2, this.fontSize, this.fontSize);
+
+
+				},this);
+			}
+		}
+	});
+
+	Chart.Scale = Chart.Element.extend({
+		initialize : function(){
+			this.fit();
+		},
+		buildYLabels : function(){
+			this.yLabels = [];
+
+			var stepDecimalPlaces = getDecimalPlaces(this.stepValue);
+
+			for (var i=0; i<=this.steps; i++){
+				this.yLabels.push(template(this.templateString,{value:(this.min + (i * this.stepValue)).toFixed(stepDecimalPlaces)}));
+			}
+			this.yLabelWidth = (this.display && this.showLabels) ? longestText(this.ctx,this.font,this.yLabels) + 10 : 0;
+		},
+		addXLabel : function(label){
+			this.xLabels.push(label);
+			this.valuesCount++;
+			this.fit();
+		},
+		removeXLabel : function(){
+			this.xLabels.shift();
+			this.valuesCount--;
+			this.fit();
+		},
+		// Fitting loop to rotate x Labels and figure out what fits there, and also calculate how many Y steps to use
+		fit: function(){
+			// First we need the width of the yLabels, assuming the xLabels aren't rotated
+
+			// To do that we need the base line at the top and base of the chart, assuming there is no x label rotation
+			this.startPoint = (this.display) ? this.fontSize : 0;
+			this.endPoint = (this.display) ? this.height - (this.fontSize * 1.5) - 5 : this.height; // -5 to pad labels
+
+			// Apply padding settings to the start and end point.
+			this.startPoint += this.padding;
+			this.endPoint -= this.padding;
+
+			// Cache the starting endpoint, excluding the space for x labels
+			var cachedEndPoint = this.endPoint;
+
+			// Cache the starting height, so can determine if we need to recalculate the scale yAxis
+			var cachedHeight = this.endPoint - this.startPoint,
+				cachedYLabelWidth;
+
+			// Build the current yLabels so we have an idea of what size they'll be to start
+			/*
+			 *	This sets what is returned from calculateScaleRange as static properties of this class:
+			 *
+				this.steps;
+				this.stepValue;
+				this.min;
+				this.max;
+			 *
+			 */
+			this.calculateYRange(cachedHeight);
+
+			// With these properties set we can now build the array of yLabels
+			// and also the width of the largest yLabel
+			this.buildYLabels();
+
+			this.calculateXLabelRotation();
+
+			while((cachedHeight > this.endPoint - this.startPoint)){
+				cachedHeight = this.endPoint - this.startPoint;
+				cachedYLabelWidth = this.yLabelWidth;
+
+				this.calculateYRange(cachedHeight);
+				this.buildYLabels();
+
+				// Only go through the xLabel loop again if the yLabel width has changed
+				if (cachedYLabelWidth < this.yLabelWidth){
+					this.endPoint = cachedEndPoint;
+					this.calculateXLabelRotation();
+				}
+			}
+
+		},
+		calculateXLabelRotation : function(){
+			//Get the width of each grid by calculating the difference
+			//between x offsets between 0 and 1.
+
+			this.ctx.font = this.font;
+
+			var firstWidth = this.ctx.measureText(this.xLabels[0]).width,
+				lastWidth = this.ctx.measureText(this.xLabels[this.xLabels.length - 1]).width,
+				firstRotated,
+				lastRotated;
+
+
+			this.xScalePaddingRight = lastWidth/2 + 3;
+			this.xScalePaddingLeft = (firstWidth/2 > this.yLabelWidth) ? firstWidth/2 : this.yLabelWidth;
+
+			this.xLabelRotation = 0;
+			if (this.display){
+				var originalLabelWidth = longestText(this.ctx,this.font,this.xLabels),
+					cosRotation,
+					firstRotatedWidth;
+				this.xLabelWidth = originalLabelWidth;
+				//Allow 3 pixels x2 padding either side for label readability
+				var xGridWidth = Math.floor(this.calculateX(1) - this.calculateX(0)) - 6;
+
+				//Max label rotate should be 90 - also act as a loop counter
+				while ((this.xLabelWidth > xGridWidth && this.xLabelRotation === 0) || (this.xLabelWidth > xGridWidth && this.xLabelRotation <= 90 && this.xLabelRotation > 0)){
+					cosRotation = Math.cos(toRadians(this.xLabelRotation));
+
+					firstRotated = cosRotation * firstWidth;
+					lastRotated = cosRotation * lastWidth;
+
+					// We're right aligning the text now.
+					if (firstRotated + this.fontSize / 2 > this.yLabelWidth){
+						this.xScalePaddingLeft = firstRotated + this.fontSize / 2;
+					}
+					this.xScalePaddingRight = this.fontSize/2;
+
+
+					this.xLabelRotation++;
+					this.xLabelWidth = cosRotation * originalLabelWidth;
+
+				}
+				if (this.xLabelRotation > 0){
+					this.endPoint -= Math.sin(toRadians(this.xLabelRotation))*originalLabelWidth + 3;
+				}
+			}
+			else{
+				this.xLabelWidth = 0;
+				this.xScalePaddingRight = this.padding;
+				this.xScalePaddingLeft = this.padding;
+			}
+
+		},
+		// Needs to be overidden in each Chart type
+		// Otherwise we need to pass all the data into the scale class
+		calculateYRange: noop,
+		drawingArea: function(){
+			return this.startPoint - this.endPoint;
+		},
+		calculateY : function(value){
+			var scalingFactor = this.drawingArea() / (this.min - this.max);
+			return this.endPoint - (scalingFactor * (value - this.min));
+		},
+		calculateX : function(index){
+			var isRotated = (this.xLabelRotation > 0),
+				// innerWidth = (this.offsetGridLines) ? this.width - offsetLeft - this.padding : this.width - (offsetLeft + halfLabelWidth * 2) - this.padding,
+				innerWidth = this.width - (this.xScalePaddingLeft + this.xScalePaddingRight),
+				valueWidth = innerWidth/Math.max((this.valuesCount - ((this.offsetGridLines) ? 0 : 1)), 1),
+				valueOffset = (valueWidth * index) + this.xScalePaddingLeft;
+
+			if (this.offsetGridLines){
+				valueOffset += (valueWidth/2);
+			}
+
+			return Math.round(valueOffset);
+		},
+		update : function(newProps){
+			helpers.extend(this, newProps);
+			this.fit();
+		},
+		draw : function(){
+			var ctx = this.ctx,
+				yLabelGap = (this.endPoint - this.startPoint) / this.steps,
+				xStart = Math.round(this.xScalePaddingLeft);
+			if (this.display){
+				ctx.fillStyle = this.textColor;
+				ctx.font = this.font;
+				each(this.yLabels,function(labelString,index){
+					var yLabelCenter = this.endPoint - (yLabelGap * index),
+						linePositionY = Math.round(yLabelCenter),
+						drawHorizontalLine = this.showHorizontalLines;
+
+					ctx.textAlign = "right";
+					ctx.textBaseline = "middle";
+					if (this.showLabels){
+						ctx.fillText(labelString,xStart - 10,yLabelCenter);
+					}
+
+					// This is X axis, so draw it
+					if (index === 0 && !drawHorizontalLine){
+						drawHorizontalLine = true;
+					}
+
+					if (drawHorizontalLine){
+						ctx.beginPath();
+					}
+
+					if (index > 0){
+						// This is a grid line in the centre, so drop that
+						ctx.lineWidth = this.gridLineWidth;
+						ctx.strokeStyle = this.gridLineColor;
+					} else {
+						// This is the first line on the scale
+						ctx.lineWidth = this.lineWidth;
+						ctx.strokeStyle = this.lineColor;
+					}
+
+					linePositionY += helpers.aliasPixel(ctx.lineWidth);
+
+					if(drawHorizontalLine){
+						ctx.moveTo(xStart, linePositionY);
+						ctx.lineTo(this.width, linePositionY);
+						ctx.stroke();
+						ctx.closePath();
+					}
+
+					ctx.lineWidth = this.lineWidth;
+					ctx.strokeStyle = this.lineColor;
+					ctx.beginPath();
+					ctx.moveTo(xStart - 5, linePositionY);
+					ctx.lineTo(xStart, linePositionY);
+					ctx.stroke();
+					ctx.closePath();
+
+				},this);
+
+				each(this.xLabels,function(label,index){
+					var xPos = this.calculateX(index) + aliasPixel(this.lineWidth),
+						// Check to see if line/bar here and decide where to place the line
+						linePos = this.calculateX(index - (this.offsetGridLines ? 0.5 : 0)) + aliasPixel(this.lineWidth),
+						isRotated = (this.xLabelRotation > 0),
+						drawVerticalLine = this.showVerticalLines;
+
+					// This is Y axis, so draw it
+					if (index === 0 && !drawVerticalLine){
+						drawVerticalLine = true;
+					}
+
+					if (drawVerticalLine){
+						ctx.beginPath();
+					}
+
+					if (index > 0){
+						// This is a grid line in the centre, so drop that
+						ctx.lineWidth = this.gridLineWidth;
+						ctx.strokeStyle = this.gridLineColor;
+					} else {
+						// This is the first line on the scale
+						ctx.lineWidth = this.lineWidth;
+						ctx.strokeStyle = this.lineColor;
+					}
+
+					if (drawVerticalLine){
+						ctx.moveTo(linePos,this.endPoint);
+						ctx.lineTo(linePos,this.startPoint - 3);
+						ctx.stroke();
+						ctx.closePath();
+					}
+
+
+					ctx.lineWidth = this.lineWidth;
+					ctx.strokeStyle = this.lineColor;
+
+
+					// Small lines at the bottom of the base grid line
+					ctx.beginPath();
+					ctx.moveTo(linePos,this.endPoint);
+					ctx.lineTo(linePos,this.endPoint + 5);
+					ctx.stroke();
+					ctx.closePath();
+
+					ctx.save();
+					ctx.translate(xPos,(isRotated) ? this.endPoint + 12 : this.endPoint + 8);
+					ctx.rotate(toRadians(this.xLabelRotation)*-1);
+					ctx.font = this.font;
+					ctx.textAlign = (isRotated) ? "right" : "center";
+					ctx.textBaseline = (isRotated) ? "middle" : "top";
+					ctx.fillText(label, 0, 0);
+					ctx.restore();
+				},this);
+
+			}
+		}
+
+	});
+
+	Chart.RadialScale = Chart.Element.extend({
+		initialize: function(){
+			this.size = min([this.height, this.width]);
+			this.drawingArea = (this.display) ? (this.size/2) - (this.fontSize/2 + this.backdropPaddingY) : (this.size/2);
+		},
+		calculateCenterOffset: function(value){
+			// Take into account half font size + the yPadding of the top value
+			var scalingFactor = this.drawingArea / (this.max - this.min);
+
+			return (value - this.min) * scalingFactor;
+		},
+		update : function(){
+			if (!this.lineArc){
+				this.setScaleSize();
+			} else {
+				this.drawingArea = (this.display) ? (this.size/2) - (this.fontSize/2 + this.backdropPaddingY) : (this.size/2);
+			}
+			this.buildYLabels();
+		},
+		buildYLabels: function(){
+			this.yLabels = [];
+
+			var stepDecimalPlaces = getDecimalPlaces(this.stepValue);
+
+			for (var i=0; i<=this.steps; i++){
+				this.yLabels.push(template(this.templateString,{value:(this.min + (i * this.stepValue)).toFixed(stepDecimalPlaces)}));
+			}
+		},
+		getCircumference : function(){
+			return ((Math.PI*2) / this.valuesCount);
+		},
+		setScaleSize: function(){
+			/*
+			 * Right, this is really confusing and there is a lot of maths going on here
+			 * The gist of the problem is here: https://gist.github.com/nnnick/696cc9c55f4b0beb8fe9
+			 *
+			 * Reaction: https://dl.dropboxusercontent.com/u/34601363/toomuchscience.gif
+			 *
+			 * Solution:
+			 *
+			 * We assume the radius of the polygon is half the size of the canvas at first
+			 * at each index we check if the text overlaps.
+			 *
+			 * Where it does, we store that angle and that index.
+			 *
+			 * After finding the largest index and angle we calculate how much we need to remove
+			 * from the shape radius to move the point inwards by that x.
+			 *
+			 * We average the left and right distances to get the maximum shape radius that can fit in the box
+			 * along with labels.
+			 *
+			 * Once we have that, we can find the centre point for the chart, by taking the x text protrusion
+			 * on each side, removing that from the size, halving it and adding the left x protrusion width.
+			 *
+			 * This will mean we have a shape fitted to the canvas, as large as it can be with the labels
+			 * and position it in the most space efficient manner
+			 *
+			 * https://dl.dropboxusercontent.com/u/34601363/yeahscience.gif
+			 */
+
+
+			// Get maximum radius of the polygon. Either half the height (minus the text width) or half the width.
+			// Use this to calculate the offset + change. - Make sure L/R protrusion is at least 0 to stop issues with centre points
+			var largestPossibleRadius = min([(this.height/2 - this.pointLabelFontSize - 5), this.width/2]),
+				pointPosition,
+				i,
+				textWidth,
+				halfTextWidth,
+				furthestRight = this.width,
+				furthestRightIndex,
+				furthestRightAngle,
+				furthestLeft = 0,
+				furthestLeftIndex,
+				furthestLeftAngle,
+				xProtrusionLeft,
+				xProtrusionRight,
+				radiusReductionRight,
+				radiusReductionLeft,
+				maxWidthRadius;
+			this.ctx.font = fontString(this.pointLabelFontSize,this.pointLabelFontStyle,this.pointLabelFontFamily);
+			for (i=0;i<this.valuesCount;i++){
+				// 5px to space the text slightly out - similar to what we do in the draw function.
+				pointPosition = this.getPointPosition(i, largestPossibleRadius);
+				textWidth = this.ctx.measureText(template(this.templateString, { value: this.labels[i] })).width + 5;
+				if (i === 0 || i === this.valuesCount/2){
+					// If we're at index zero, or exactly the middle, we're at exactly the top/bottom
+					// of the radar chart, so text will be aligned centrally, so we'll half it and compare
+					// w/left and right text sizes
+					halfTextWidth = textWidth/2;
+					if (pointPosition.x + halfTextWidth > furthestRight) {
+						furthestRight = pointPosition.x + halfTextWidth;
+						furthestRightIndex = i;
+					}
+					if (pointPosition.x - halfTextWidth < furthestLeft) {
+						furthestLeft = pointPosition.x - halfTextWidth;
+						furthestLeftIndex = i;
+					}
+				}
+				else if (i < this.valuesCount/2) {
+					// Less than half the values means we'll left align the text
+					if (pointPosition.x + textWidth > furthestRight) {
+						furthestRight = pointPosition.x + textWidth;
+						furthestRightIndex = i;
+					}
+				}
+				else if (i > this.valuesCount/2){
+					// More than half the values means we'll right align the text
+					if (pointPosition.x - textWidth < furthestLeft) {
+						furthestLeft = pointPosition.x - textWidth;
+						furthestLeftIndex = i;
+					}
+				}
+			}
+
+			xProtrusionLeft = furthestLeft;
+
+			xProtrusionRight = Math.ceil(furthestRight - this.width);
+
+			furthestRightAngle = this.getIndexAngle(furthestRightIndex);
+
+			furthestLeftAngle = this.getIndexAngle(furthestLeftIndex);
+
+			radiusReductionRight = xProtrusionRight / Math.sin(furthestRightAngle + Math.PI/2);
+
+			radiusReductionLeft = xProtrusionLeft / Math.sin(furthestLeftAngle + Math.PI/2);
+
+			// Ensure we actually need to reduce the size of the chart
+			radiusReductionRight = (isNumber(radiusReductionRight)) ? radiusReductionRight : 0;
+			radiusReductionLeft = (isNumber(radiusReductionLeft)) ? radiusReductionLeft : 0;
+
+			this.drawingArea = largestPossibleRadius - (radiusReductionLeft + radiusReductionRight)/2;
+
+			//this.drawingArea = min([maxWidthRadius, (this.height - (2 * (this.pointLabelFontSize + 5)))/2])
+			this.setCenterPoint(radiusReductionLeft, radiusReductionRight);
+
+		},
+		setCenterPoint: function(leftMovement, rightMovement){
+
+			var maxRight = this.width - rightMovement - this.drawingArea,
+				maxLeft = leftMovement + this.drawingArea;
+
+			this.xCenter = (maxLeft + maxRight)/2;
+			// Always vertically in the centre as the text height doesn't change
+			this.yCenter = (this.height/2);
+		},
+
+		getIndexAngle : function(index){
+			var angleMultiplier = (Math.PI * 2) / this.valuesCount;
+			// Start from the top instead of right, so remove a quarter of the circle
+
+			return index * angleMultiplier - (Math.PI/2);
+		},
+		getPointPosition : function(index, distanceFromCenter){
+			var thisAngle = this.getIndexAngle(index);
+			return {
+				x : (Math.cos(thisAngle) * distanceFromCenter) + this.xCenter,
+				y : (Math.sin(thisAngle) * distanceFromCenter) + this.yCenter
+			};
+		},
+		draw: function(){
+			if (this.display){
+				var ctx = this.ctx;
+				each(this.yLabels, function(label, index){
+					// Don't draw a centre value
+					if (index > 0){
+						var yCenterOffset = index * (this.drawingArea/this.steps),
+							yHeight = this.yCenter - yCenterOffset,
+							pointPosition;
+
+						// Draw circular lines around the scale
+						if (this.lineWidth > 0){
+							ctx.strokeStyle = this.lineColor;
+							ctx.lineWidth = this.lineWidth;
+
+							if(this.lineArc){
+								ctx.beginPath();
+								ctx.arc(this.xCenter, this.yCenter, yCenterOffset, 0, Math.PI*2);
+								ctx.closePath();
+								ctx.stroke();
+							} else{
+								ctx.beginPath();
+								for (var i=0;i<this.valuesCount;i++)
+								{
+									pointPosition = this.getPointPosition(i, this.calculateCenterOffset(this.min + (index * this.stepValue)));
+									if (i === 0){
+										ctx.moveTo(pointPosition.x, pointPosition.y);
+									} else {
+										ctx.lineTo(pointPosition.x, pointPosition.y);
+									}
+								}
+								ctx.closePath();
+								ctx.stroke();
+							}
+						}
+						if(this.showLabels){
+							ctx.font = fontString(this.fontSize,this.fontStyle,this.fontFamily);
+							if (this.showLabelBackdrop){
+								var labelWidth = ctx.measureText(label).width;
+								ctx.fillStyle = this.backdropColor;
+								ctx.fillRect(
+									this.xCenter - labelWidth/2 - this.backdropPaddingX,
+									yHeight - this.fontSize/2 - this.backdropPaddingY,
+									labelWidth + this.backdropPaddingX*2,
+									this.fontSize + this.backdropPaddingY*2
+								);
+							}
+							ctx.textAlign = 'center';
+							ctx.textBaseline = "middle";
+							ctx.fillStyle = this.fontColor;
+							ctx.fillText(label, this.xCenter, yHeight);
+						}
+					}
+				}, this);
+
+				if (!this.lineArc){
+					ctx.lineWidth = this.angleLineWidth;
+					ctx.strokeStyle = this.angleLineColor;
+					for (var i = this.valuesCount - 1; i >= 0; i--) {
+						var centerOffset = null, outerPosition = null;
+
+						if (this.angleLineWidth > 0){
+							centerOffset = this.calculateCenterOffset(this.max);
+							outerPosition = this.getPointPosition(i, centerOffset);
+							ctx.beginPath();
+							ctx.moveTo(this.xCenter, this.yCenter);
+							ctx.lineTo(outerPosition.x, outerPosition.y);
+							ctx.stroke();
+							ctx.closePath();
+						}
+
+						if (this.backgroundColors && this.backgroundColors.length == this.valuesCount) {
+							if (centerOffset == null)
+								centerOffset = this.calculateCenterOffset(this.max);
+
+							if (outerPosition == null)
+								outerPosition = this.getPointPosition(i, centerOffset);
+
+							var previousOuterPosition = this.getPointPosition(i === 0 ? this.valuesCount - 1 : i - 1, centerOffset);
+							var nextOuterPosition = this.getPointPosition(i === this.valuesCount - 1 ? 0 : i + 1, centerOffset);
+
+							var previousOuterHalfway = { x: (previousOuterPosition.x + outerPosition.x) / 2, y: (previousOuterPosition.y + outerPosition.y) / 2 };
+							var nextOuterHalfway = { x: (outerPosition.x + nextOuterPosition.x) / 2, y: (outerPosition.y + nextOuterPosition.y) / 2 };
+
+							ctx.beginPath();
+							ctx.moveTo(this.xCenter, this.yCenter);
+							ctx.lineTo(previousOuterHalfway.x, previousOuterHalfway.y);
+							ctx.lineTo(outerPosition.x, outerPosition.y);
+							ctx.lineTo(nextOuterHalfway.x, nextOuterHalfway.y);
+							ctx.fillStyle = this.backgroundColors[i];
+							ctx.fill();
+							ctx.closePath();
+						}
+						// Extra 3px out for some label spacing
+						var pointLabelPosition = this.getPointPosition(i, this.calculateCenterOffset(this.max) + 5);
+						ctx.font = fontString(this.pointLabelFontSize,this.pointLabelFontStyle,this.pointLabelFontFamily);
+						ctx.fillStyle = this.pointLabelFontColor;
+
+						var labelsCount = this.labels.length,
+							halfLabelsCount = this.labels.length/2,
+							quarterLabelsCount = halfLabelsCount/2,
+							upperHalf = (i < quarterLabelsCount || i > labelsCount - quarterLabelsCount),
+							exactQuarter = (i === quarterLabelsCount || i === labelsCount - quarterLabelsCount);
+						if (i === 0){
+							ctx.textAlign = 'center';
+						} else if(i === halfLabelsCount){
+							ctx.textAlign = 'center';
+						} else if (i < halfLabelsCount){
+							ctx.textAlign = 'left';
+						} else {
+							ctx.textAlign = 'right';
+						}
+
+						// Set the correct text baseline based on outer positioning
+						if (exactQuarter){
+							ctx.textBaseline = 'middle';
+						} else if (upperHalf){
+							ctx.textBaseline = 'bottom';
+						} else {
+							ctx.textBaseline = 'top';
+						}
+
+						ctx.fillText(this.labels[i], pointLabelPosition.x, pointLabelPosition.y);
+					}
+				}
+			}
+		}
+	});
+
+	Chart.animationService = {
+		frameDuration: 17,
+		animations: [],
+		dropFrames: 0,
+		addAnimation: function(chartInstance, animationObject) {
+			for (var index = 0; index < this.animations.length; ++ index){
+				if (this.animations[index].chartInstance === chartInstance){
+					// replacing an in progress animation
+					this.animations[index].animationObject = animationObject;
+					return;
+				}
+			}
+			
+			this.animations.push({
+				chartInstance: chartInstance,
+				animationObject: animationObject
+			});
+
+			// If there are no animations queued, manually kickstart a digest, for lack of a better word
+			if (this.animations.length == 1) {
+				helpers.requestAnimFrame.call(window, this.digestWrapper);
+			}
+		},
+		// Cancel the animation for a given chart instance
+		cancelAnimation: function(chartInstance) {
+			var index = helpers.findNextWhere(this.animations, function(animationWrapper) {
+				return animationWrapper.chartInstance === chartInstance;
+			});
+			
+			if (index)
+			{
+				this.animations.splice(index, 1);
+			}
+		},
+		// calls startDigest with the proper context
+		digestWrapper: function() {
+			Chart.animationService.startDigest.call(Chart.animationService);
+		},
+		startDigest: function() {
+
+			var startTime = Date.now();
+			var framesToDrop = 0;
+
+			if(this.dropFrames > 1){
+				framesToDrop = Math.floor(this.dropFrames);
+				this.dropFrames -= framesToDrop;
+			}
+
+			for (var i = 0; i < this.animations.length; i++) {
+
+				if (this.animations[i].animationObject.currentStep === null){
+					this.animations[i].animationObject.currentStep = 0;
+				}
+
+				this.animations[i].animationObject.currentStep += 1 + framesToDrop;
+				if(this.animations[i].animationObject.currentStep > this.animations[i].animationObject.numSteps){
+					this.animations[i].animationObject.currentStep = this.animations[i].animationObject.numSteps;
+				}
+				
+				this.animations[i].animationObject.render(this.animations[i].chartInstance, this.animations[i].animationObject);
+				
+				// Check if executed the last frame.
+				if (this.animations[i].animationObject.currentStep == this.animations[i].animationObject.numSteps){
+					// Call onAnimationComplete
+					this.animations[i].animationObject.onAnimationComplete.call(this.animations[i].chartInstance);
+					// Remove the animation.
+					this.animations.splice(i, 1);
+					// Keep the index in place to offset the splice
+					i--;
+				}
+			}
+
+			var endTime = Date.now();
+			var delay = endTime - startTime - this.frameDuration;
+			var frameDelay = delay / this.frameDuration;
+
+			if(frameDelay > 1){
+				this.dropFrames += frameDelay;
+			}
+
+			// Do we have more stuff to animate?
+			if (this.animations.length > 0){
+				helpers.requestAnimFrame.call(window, this.digestWrapper);
+			}
+		}
+	};
+
+	// Attach global event to resize each chart instance when the browser resizes
+	helpers.addEvent(window, "resize", (function(){
+		// Basic debounce of resize function so it doesn't hurt performance when resizing browser.
+		var timeout;
+		return function(){
+			clearTimeout(timeout);
+			timeout = setTimeout(function(){
+				each(Chart.instances,function(instance){
+					// If the responsive flag is set in the chart instance config
+					// Cascade the resize event down to the chart.
+					if (instance.options.responsive){
+						instance.resize(instance.render, true);
+					}
+				});
+			}, 50);
+		};
+	})());
+
+
+	if (amd) {
+		define('Chart', [], function(){
+			return Chart;
+		});
+	} else if (typeof module === 'object' && module.exports) {
+		module.exports = Chart;
+	}
+
+	root.Chart = Chart;
+
+	Chart.noConflict = function(){
+		root.Chart = previous;
+		return Chart;
+	};
+
+}).call(this);
+
+(function(){
+	"use strict";
+
+	var root = this,
+		Chart = root.Chart,
+		helpers = Chart.helpers;
+
+
+	var defaultConfig = {
+		//Boolean - Whether the scale should start at zero, or an order of magnitude down from the lowest value
+		scaleBeginAtZero : true,
+
+		//Boolean - Whether grid lines are shown across the chart
+		scaleShowGridLines : true,
+
+		//String - Colour of the grid lines
+		scaleGridLineColor : "rgba(0,0,0,.05)",
+
+		//Number - Width of the grid lines
+		scaleGridLineWidth : 1,
+
+		//Boolean - Whether to show horizontal lines (except X axis)
+		scaleShowHorizontalLines: true,
+
+		//Boolean - Whether to show vertical lines (except Y axis)
+		scaleShowVerticalLines: true,
+
+		//Boolean - If there is a stroke on each bar
+		barShowStroke : true,
+
+		//Number - Pixel width of the bar stroke
+		barStrokeWidth : 2,
+
+		//Number - Spacing between each of the X value sets
+		barValueSpacing : 5,
+
+		//Number - Spacing between data sets within X values
+		barDatasetSpacing : 1,
+
+		//String - A legend template
+		legendTemplate : "<ul class=\"<%=name.toLowerCase()%>-legend\"><% for (var i=0; i<datasets.length; i++){%><li><span style=\"background-color:<%=datasets[i].fillColor%>\"><%if(datasets[i].label){%><%=datasets[i].label%><%}%></span></li><%}%></ul>"
+
+	};
+
+
+	Chart.Type.extend({
+		name: "Bar",
+		defaults : defaultConfig,
+		initialize:  function(data){
+
+			//Expose options as a scope variable here so we can access it in the ScaleClass
+			var options = this.options;
+
+			this.ScaleClass = Chart.Scale.extend({
+				offsetGridLines : true,
+				calculateBarX : function(datasetCount, datasetIndex, barIndex){
+					//Reusable method for calculating the xPosition of a given bar based on datasetIndex & width of the bar
+					var xWidth = this.calculateBaseWidth(),
+						xAbsolute = this.calculateX(barIndex) - (xWidth/2),
+						barWidth = this.calculateBarWidth(datasetCount);
+
+					return xAbsolute + (barWidth * datasetIndex) + (datasetIndex * options.barDatasetSpacing) + barWidth/2;
+				},
+				calculateBaseWidth : function(){
+					return (this.calculateX(1) - this.calculateX(0)) - (2*options.barValueSpacing);
+				},
+				calculateBarWidth : function(datasetCount){
+					//The padding between datasets is to the right of each bar, providing that there are more than 1 dataset
+					var baseWidth = this.calculateBaseWidth() - ((datasetCount - 1) * options.barDatasetSpacing);
+
+					return (baseWidth / datasetCount);
+				}
+			});
+
+			this.datasets = [];
+
+			//Set up tooltip events on the chart
+			if (this.options.showTooltips){
+				helpers.bindEvents(this, this.options.tooltipEvents, function(evt){
+					var activeBars = (evt.type !== 'mouseout') ? this.getBarsAtEvent(evt) : [];
+
+					this.eachBars(function(bar){
+						bar.restore(['fillColor', 'strokeColor']);
+					});
+					helpers.each(activeBars, function(activeBar){
+						activeBar.fillColor = activeBar.highlightFill;
+						activeBar.strokeColor = activeBar.highlightStroke;
+					});
+					this.showTooltip(activeBars);
+				});
+			}
+
+			//Declare the extension of the default point, to cater for the options passed in to the constructor
+			this.BarClass = Chart.Rectangle.extend({
+				strokeWidth : this.options.barStrokeWidth,
+				showStroke : this.options.barShowStroke,
+				ctx : this.chart.ctx
+			});
+
+			//Iterate through each of the datasets, and build this into a property of the chart
+			helpers.each(data.datasets,function(dataset,datasetIndex){
+
+				var datasetObject = {
+					label : dataset.label || null,
+					fillColor : dataset.fillColor,
+					strokeColor : dataset.strokeColor,
+					bars : []
+				};
+
+				this.datasets.push(datasetObject);
+
+				helpers.each(dataset.data,function(dataPoint,index){
+					//Add a new point for each piece of data, passing any required data to draw.
+					datasetObject.bars.push(new this.BarClass({
+						value : dataPoint,
+						label : data.labels[index],
+						datasetLabel: dataset.label,
+						strokeColor : dataset.strokeColor,
+						fillColor : dataset.fillColor,
+						highlightFill : dataset.highlightFill || dataset.fillColor,
+						highlightStroke : dataset.highlightStroke || dataset.strokeColor
+					}));
+				},this);
+
+			},this);
+
+			this.buildScale(data.labels);
+
+			this.BarClass.prototype.base = this.scale.endPoint;
+
+			this.eachBars(function(bar, index, datasetIndex){
+				helpers.extend(bar, {
+					width : this.scale.calculateBarWidth(this.datasets.length),
+					x: this.scale.calculateBarX(this.datasets.length, datasetIndex, index),
+					y: this.scale.endPoint
+				});
+				bar.save();
+			}, this);
+
+			this.render();
+		},
+		update : function(){
+			this.scale.update();
+			// Reset any highlight colours before updating.
+			helpers.each(this.activeElements, function(activeElement){
+				activeElement.restore(['fillColor', 'strokeColor']);
+			});
+
+			this.eachBars(function(bar){
+				bar.save();
+			});
+			this.render();
+		},
+		eachBars : function(callback){
+			helpers.each(this.datasets,function(dataset, datasetIndex){
+				helpers.each(dataset.bars, callback, this, datasetIndex);
+			},this);
+		},
+		getBarsAtEvent : function(e){
+			var barsArray = [],
+				eventPosition = helpers.getRelativePosition(e),
+				datasetIterator = function(dataset){
+					barsArray.push(dataset.bars[barIndex]);
+				},
+				barIndex;
+
+			for (var datasetIndex = 0; datasetIndex < this.datasets.length; datasetIndex++) {
+				for (barIndex = 0; barIndex < this.datasets[datasetIndex].bars.length; barIndex++) {
+					if (this.datasets[datasetIndex].bars[barIndex].inRange(eventPosition.x,eventPosition.y)){
+						helpers.each(this.datasets, datasetIterator);
+						return barsArray;
+					}
+				}
+			}
+
+			return barsArray;
+		},
+		buildScale : function(labels){
+			var self = this;
+
+			var dataTotal = function(){
+				var values = [];
+				self.eachBars(function(bar){
+					values.push(bar.value);
+				});
+				return values;
+			};
+
+			var scaleOptions = {
+				templateString : this.options.scaleLabel,
+				height : this.chart.height,
+				width : this.chart.width,
+				ctx : this.chart.ctx,
+				textColor : this.options.scaleFontColor,
+				fontSize : this.options.scaleFontSize,
+				fontStyle : this.options.scaleFontStyle,
+				fontFamily : this.options.scaleFontFamily,
+				valuesCount : labels.length,
+				beginAtZero : this.options.scaleBeginAtZero,
+				integersOnly : this.options.scaleIntegersOnly,
+				calculateYRange: function(currentHeight){
+					var updatedRanges = helpers.calculateScaleRange(
+						dataTotal(),
+						currentHeight,
+						this.fontSize,
+						this.beginAtZero,
+						this.integersOnly
+					);
+					helpers.extend(this, updatedRanges);
+				},
+				xLabels : labels,
+				font : helpers.fontString(this.options.scaleFontSize, this.options.scaleFontStyle, this.options.scaleFontFamily),
+				lineWidth : this.options.scaleLineWidth,
+				lineColor : this.options.scaleLineColor,
+				showHorizontalLines : this.options.scaleShowHorizontalLines,
+				showVerticalLines : this.options.scaleShowVerticalLines,
+				gridLineWidth : (this.options.scaleShowGridLines) ? this.options.scaleGridLineWidth : 0,
+				gridLineColor : (this.options.scaleShowGridLines) ? this.options.scaleGridLineColor : "rgba(0,0,0,0)",
+				padding : (this.options.showScale) ? 0 : (this.options.barShowStroke) ? this.options.barStrokeWidth : 0,
+				showLabels : this.options.scaleShowLabels,
+				display : this.options.showScale
+			};
+
+			if (this.options.scaleOverride){
+				helpers.extend(scaleOptions, {
+					calculateYRange: helpers.noop,
+					steps: this.options.scaleSteps,
+					stepValue: this.options.scaleStepWidth,
+					min: this.options.scaleStartValue,
+					max: this.options.scaleStartValue + (this.options.scaleSteps * this.options.scaleStepWidth)
+				});
+			}
+
+			this.scale = new this.ScaleClass(scaleOptions);
+		},
+		addData : function(valuesArray,label){
+			//Map the values array for each of the datasets
+			helpers.each(valuesArray,function(value,datasetIndex){
+				//Add a new point for each piece of data, passing any required data to draw.
+				this.datasets[datasetIndex].bars.push(new this.BarClass({
+					value : value,
+					label : label,
+					datasetLabel: this.datasets[datasetIndex].label,
+					x: this.scale.calculateBarX(this.datasets.length, datasetIndex, this.scale.valuesCount+1),
+					y: this.scale.endPoint,
+					width : this.scale.calculateBarWidth(this.datasets.length),
+					base : this.scale.endPoint,
+					strokeColor : this.datasets[datasetIndex].strokeColor,
+					fillColor : this.datasets[datasetIndex].fillColor
+				}));
+			},this);
+
+			this.scale.addXLabel(label);
+			//Then re-render the chart.
+			this.update();
+		},
+		removeData : function(){
+			this.scale.removeXLabel();
+			//Then re-render the chart.
+			helpers.each(this.datasets,function(dataset){
+				dataset.bars.shift();
+			},this);
+			this.update();
+		},
+		reflow : function(){
+			helpers.extend(this.BarClass.prototype,{
+				y: this.scale.endPoint,
+				base : this.scale.endPoint
+			});
+			var newScaleProps = helpers.extend({
+				height : this.chart.height,
+				width : this.chart.width
+			});
+			this.scale.update(newScaleProps);
+		},
+		draw : function(ease){
+			var easingDecimal = ease || 1;
+			this.clear();
+
+			var ctx = this.chart.ctx;
+
+			this.scale.draw(easingDecimal);
+
+			//Draw all the bars for each dataset
+			helpers.each(this.datasets,function(dataset,datasetIndex){
+				helpers.each(dataset.bars,function(bar,index){
+					if (bar.hasValue()){
+						bar.base = this.scale.endPoint;
+						//Transition then draw
+						bar.transition({
+							x : this.scale.calculateBarX(this.datasets.length, datasetIndex, index),
+							y : this.scale.calculateY(bar.value),
+							width : this.scale.calculateBarWidth(this.datasets.length)
+						}, easingDecimal).draw();
+					}
+				},this);
+
+			},this);
+		}
+	});
+
+
+}).call(this);
+
+(function(){
+	"use strict";
+
+	var root = this,
+		Chart = root.Chart,
+		//Cache a local reference to Chart.helpers
+		helpers = Chart.helpers;
+
+	var defaultConfig = {
+		//Boolean - Whether we should show a stroke on each segment
+		segmentShowStroke : true,
+
+		//String - The colour of each segment stroke
+		segmentStrokeColor : "#fff",
+
+		//Number - The width of each segment stroke
+		segmentStrokeWidth : 2,
+
+		//The percentage of the chart that we cut out of the middle.
+		percentageInnerCutout : 50,
+
+		//Number - Amount of animation steps
+		animationSteps : 100,
+
+		//String - Animation easing effect
+		animationEasing : "easeOutBounce",
+
+		//Boolean - Whether we animate the rotation of the Doughnut
+		animateRotate : true,
+
+		//Boolean - Whether we animate scaling the Doughnut from the centre
+		animateScale : false,
+
+		//String - A legend template
+		legendTemplate : "<ul class=\"<%=name.toLowerCase()%>-legend\"><% for (var i=0; i<segments.length; i++){%><li><span style=\"background-color:<%=segments[i].fillColor%>\"><%if(segments[i].label){%><%=segments[i].label%><%}%></span></li><%}%></ul>"
+
+	};
+
+	Chart.Type.extend({
+		//Passing in a name registers this chart in the Chart namespace
+		name: "Doughnut",
+		//Providing a defaults will also register the deafults in the chart namespace
+		defaults : defaultConfig,
+		//Initialize is fired when the chart is initialized - Data is passed in as a parameter
+		//Config is automatically merged by the core of Chart.js, and is available at this.options
+		initialize:  function(data){
+
+			//Declare segments as a static property to prevent inheriting across the Chart type prototype
+			this.segments = [];
+			this.outerRadius = (helpers.min([this.chart.width,this.chart.height]) -	this.options.segmentStrokeWidth/2)/2;
+
+			this.SegmentArc = Chart.Arc.extend({
+				ctx : this.chart.ctx,
+				x : this.chart.width/2,
+				y : this.chart.height/2
+			});
+
+			//Set up tooltip events on the chart
+			if (this.options.showTooltips){
+				helpers.bindEvents(this, this.options.tooltipEvents, function(evt){
+					var activeSegments = (evt.type !== 'mouseout') ? this.getSegmentsAtEvent(evt) : [];
+
+					helpers.each(this.segments,function(segment){
+						segment.restore(["fillColor"]);
+					});
+					helpers.each(activeSegments,function(activeSegment){
+						activeSegment.fillColor = activeSegment.highlightColor;
+					});
+					this.showTooltip(activeSegments);
+				});
+			}
+			this.calculateTotal(data);
+
+			helpers.each(data,function(datapoint, index){
+				if (!datapoint.color) {
+					datapoint.color = 'hsl(' + (360 * index / data.length) + ', 100%, 50%)';
+				}
+				this.addData(datapoint, index, true);
+			},this);
+
+			this.render();
+		},
+		getSegmentsAtEvent : function(e){
+			var segmentsArray = [];
+
+			var location = helpers.getRelativePosition(e);
+
+			helpers.each(this.segments,function(segment){
+				if (segment.inRange(location.x,location.y)) segmentsArray.push(segment);
+			},this);
+			return segmentsArray;
+		},
+		addData : function(segment, atIndex, silent){
+			var index = atIndex !== undefined ? atIndex : this.segments.length;
+			if ( typeof(segment.color) === "undefined" ) {
+				segment.color = Chart.defaults.global.segmentColorDefault[index % Chart.defaults.global.segmentColorDefault.length];
+				segment.highlight = Chart.defaults.global.segmentHighlightColorDefaults[index % Chart.defaults.global.segmentHighlightColorDefaults.length];				
+			}
+			this.segments.splice(index, 0, new this.SegmentArc({
+				value : segment.value,
+				outerRadius : (this.options.animateScale) ? 0 : this.outerRadius,
+				innerRadius : (this.options.animateScale) ? 0 : (this.outerRadius/100) * this.options.percentageInnerCutout,
+				fillColor : segment.color,
+				highlightColor : segment.highlight || segment.color,
+				showStroke : this.options.segmentShowStroke,
+				strokeWidth : this.options.segmentStrokeWidth,
+				strokeColor : this.options.segmentStrokeColor,
+				startAngle : Math.PI * 1.5,
+				circumference : (this.options.animateRotate) ? 0 : this.calculateCircumference(segment.value),
+				label : segment.label
+			}));
+			if (!silent){
+				this.reflow();
+				this.update();
+			}
+		},
+		calculateCircumference : function(value) {
+			if ( this.total > 0 ) {
+				return (Math.PI*2)*(value / this.total);
+			} else {
+				return 0;
+			}
+		},
+		calculateTotal : function(data){
+			this.total = 0;
+			helpers.each(data,function(segment){
+				this.total += Math.abs(segment.value);
+			},this);
+		},
+		update : function(){
+			this.calculateTotal(this.segments);
+
+			// Reset any highlight colours before updating.
+			helpers.each(this.activeElements, function(activeElement){
+				activeElement.restore(['fillColor']);
+			});
+
+			helpers.each(this.segments,function(segment){
+				segment.save();
+			});
+			this.render();
+		},
+
+		removeData: function(atIndex){
+			var indexToDelete = (helpers.isNumber(atIndex)) ? atIndex : this.segments.length-1;
+			this.segments.splice(indexToDelete, 1);
+			this.reflow();
+			this.update();
+		},
+
+		reflow : function(){
+			helpers.extend(this.SegmentArc.prototype,{
+				x : this.chart.width/2,
+				y : this.chart.height/2
+			});
+			this.outerRadius = (helpers.min([this.chart.width,this.chart.height]) -	this.options.segmentStrokeWidth/2)/2;
+			helpers.each(this.segments, function(segment){
+				segment.update({
+					outerRadius : this.outerRadius,
+					innerRadius : (this.outerRadius/100) * this.options.percentageInnerCutout
+				});
+			}, this);
+		},
+		draw : function(easeDecimal){
+			var animDecimal = (easeDecimal) ? easeDecimal : 1;
+			this.clear();
+			helpers.each(this.segments,function(segment,index){
+				segment.transition({
+					circumference : this.calculateCircumference(segment.value),
+					outerRadius : this.outerRadius,
+					innerRadius : (this.outerRadius/100) * this.options.percentageInnerCutout
+				},animDecimal);
+
+				segment.endAngle = segment.startAngle + segment.circumference;
+
+				segment.draw();
+				if (index === 0){
+					segment.startAngle = Math.PI * 1.5;
+				}
+				//Check to see if it's the last segment, if not get the next and update the start angle
+				if (index < this.segments.length-1){
+					this.segments[index+1].startAngle = segment.endAngle;
+				}
+			},this);
+
+		}
+	});
+
+	Chart.types.Doughnut.extend({
+		name : "Pie",
+		defaults : helpers.merge(defaultConfig,{percentageInnerCutout : 0})
+	});
+
+}).call(this);
+
+(function(){
+	"use strict";
+
+	var root = this,
+		Chart = root.Chart,
+		helpers = Chart.helpers;
+
+	var defaultConfig = {
+
+		///Boolean - Whether grid lines are shown across the chart
+		scaleShowGridLines : true,
+
+		//String - Colour of the grid lines
+		scaleGridLineColor : "rgba(0,0,0,.05)",
+
+		//Number - Width of the grid lines
+		scaleGridLineWidth : 1,
+
+		//Boolean - Whether to show horizontal lines (except X axis)
+		scaleShowHorizontalLines: true,
+
+		//Boolean - Whether to show vertical lines (except Y axis)
+		scaleShowVerticalLines: true,
+
+		//Boolean - Whether the line is curved between points
+		bezierCurve : true,
+
+		//Number - Tension of the bezier curve between points
+		bezierCurveTension : 0.4,
+
+		//Boolean - Whether to show a dot for each point
+		pointDot : true,
+
+		//Number - Radius of each point dot in pixels
+		pointDotRadius : 4,
+
+		//Number - Pixel width of point dot stroke
+		pointDotStrokeWidth : 1,
+
+		//Number - amount extra to add to the radius to cater for hit detection outside the drawn point
+		pointHitDetectionRadius : 20,
+
+		//Boolean - Whether to show a stroke for datasets
+		datasetStroke : true,
+
+		//Number - Pixel width of dataset stroke
+		datasetStrokeWidth : 2,
+
+		//Boolean - Whether to fill the dataset with a colour
+		datasetFill : true,
+
+		//String - A legend template
+		legendTemplate : "<ul class=\"<%=name.toLowerCase()%>-legend\"><% for (var i=0; i<datasets.length; i++){%><li><span style=\"background-color:<%=datasets[i].strokeColor%>\"><%if(datasets[i].label){%><%=datasets[i].label%><%}%></span></li><%}%></ul>",
+
+		//Boolean - Whether to horizontally center the label and point dot inside the grid
+		offsetGridLines : false
+
+	};
+
+
+	Chart.Type.extend({
+		name: "Line",
+		defaults : defaultConfig,
+		initialize:  function(data){
+			//Declare the extension of the default point, to cater for the options passed in to the constructor
+			this.PointClass = Chart.Point.extend({
+				offsetGridLines : this.options.offsetGridLines,
+				strokeWidth : this.options.pointDotStrokeWidth,
+				radius : this.options.pointDotRadius,
+				display: this.options.pointDot,
+				hitDetectionRadius : this.options.pointHitDetectionRadius,
+				ctx : this.chart.ctx,
+				inRange : function(mouseX){
+					return (Math.pow(mouseX-this.x, 2) < Math.pow(this.radius + this.hitDetectionRadius,2));
+				}
+			});
+
+			this.datasets = [];
+
+			//Set up tooltip events on the chart
+			if (this.options.showTooltips){
+				helpers.bindEvents(this, this.options.tooltipEvents, function(evt){
+					var activePoints = (evt.type !== 'mouseout') ? this.getPointsAtEvent(evt) : [];
+					this.eachPoints(function(point){
+						point.restore(['fillColor', 'strokeColor']);
+					});
+					helpers.each(activePoints, function(activePoint){
+						activePoint.fillColor = activePoint.highlightFill;
+						activePoint.strokeColor = activePoint.highlightStroke;
+					});
+					this.showTooltip(activePoints);
+				});
+			}
+
+			//Iterate through each of the datasets, and build this into a property of the chart
+			helpers.each(data.datasets,function(dataset){
+
+				var datasetObject = {
+					label : dataset.label || null,
+					fillColor : dataset.fillColor,
+					strokeColor : dataset.strokeColor,
+					pointColor : dataset.pointColor,
+					pointStrokeColor : dataset.pointStrokeColor,
+					points : []
+				};
+
+				this.datasets.push(datasetObject);
+
+
+				helpers.each(dataset.data,function(dataPoint,index){
+					//Add a new point for each piece of data, passing any required data to draw.
+					datasetObject.points.push(new this.PointClass({
+						value : dataPoint,
+						label : data.labels[index],
+						datasetLabel: dataset.label,
+						strokeColor : dataset.pointStrokeColor,
+						fillColor : dataset.pointColor,
+						highlightFill : dataset.pointHighlightFill || dataset.pointColor,
+						highlightStroke : dataset.pointHighlightStroke || dataset.pointStrokeColor
+					}));
+				},this);
+
+				this.buildScale(data.labels);
+
+
+				this.eachPoints(function(point, index){
+					helpers.extend(point, {
+						x: this.scale.calculateX(index),
+						y: this.scale.endPoint
+					});
+					point.save();
+				}, this);
+
+			},this);
+
+
+			this.render();
+		},
+		update : function(){
+			this.scale.update();
+			// Reset any highlight colours before updating.
+			helpers.each(this.activeElements, function(activeElement){
+				activeElement.restore(['fillColor', 'strokeColor']);
+			});
+			this.eachPoints(function(point){
+				point.save();
+			});
+			this.render();
+		},
+		eachPoints : function(callback){
+			helpers.each(this.datasets,function(dataset){
+				helpers.each(dataset.points,callback,this);
+			},this);
+		},
+		getPointsAtEvent : function(e){
+			var pointsArray = [],
+				eventPosition = helpers.getRelativePosition(e);
+			helpers.each(this.datasets,function(dataset){
+				helpers.each(dataset.points,function(point){
+					if (point.inRange(eventPosition.x,eventPosition.y)) pointsArray.push(point);
+				});
+			},this);
+			return pointsArray;
+		},
+		buildScale : function(labels){
+			var self = this;
+
+			var dataTotal = function(){
+				var values = [];
+				self.eachPoints(function(point){
+					values.push(point.value);
+				});
+
+				return values;
+			};
+
+			var scaleOptions = {
+				templateString : this.options.scaleLabel,
+				height : this.chart.height,
+				width : this.chart.width,
+				ctx : this.chart.ctx,
+				textColor : this.options.scaleFontColor,
+				offsetGridLines : this.options.offsetGridLines,
+				fontSize : this.options.scaleFontSize,
+				fontStyle : this.options.scaleFontStyle,
+				fontFamily : this.options.scaleFontFamily,
+				valuesCount : labels.length,
+				beginAtZero : this.options.scaleBeginAtZero,
+				integersOnly : this.options.scaleIntegersOnly,
+				calculateYRange : function(currentHeight){
+					var updatedRanges = helpers.calculateScaleRange(
+						dataTotal(),
+						currentHeight,
+						this.fontSize,
+						this.beginAtZero,
+						this.integersOnly
+					);
+					helpers.extend(this, updatedRanges);
+				},
+				xLabels : labels,
+				font : helpers.fontString(this.options.scaleFontSize, this.options.scaleFontStyle, this.options.scaleFontFamily),
+				lineWidth : this.options.scaleLineWidth,
+				lineColor : this.options.scaleLineColor,
+				showHorizontalLines : this.options.scaleShowHorizontalLines,
+				showVerticalLines : this.options.scaleShowVerticalLines,
+				gridLineWidth : (this.options.scaleShowGridLines) ? this.options.scaleGridLineWidth : 0,
+				gridLineColor : (this.options.scaleShowGridLines) ? this.options.scaleGridLineColor : "rgba(0,0,0,0)",
+				padding: (this.options.showScale) ? 0 : this.options.pointDotRadius + this.options.pointDotStrokeWidth,
+				showLabels : this.options.scaleShowLabels,
+				display : this.options.showScale
+			};
+
+			if (this.options.scaleOverride){
+				helpers.extend(scaleOptions, {
+					calculateYRange: helpers.noop,
+					steps: this.options.scaleSteps,
+					stepValue: this.options.scaleStepWidth,
+					min: this.options.scaleStartValue,
+					max: this.options.scaleStartValue + (this.options.scaleSteps * this.options.scaleStepWidth)
+				});
+			}
+
+
+			this.scale = new Chart.Scale(scaleOptions);
+		},
+		addData : function(valuesArray,label){
+			//Map the values array for each of the datasets
+
+			helpers.each(valuesArray,function(value,datasetIndex){
+				//Add a new point for each piece of data, passing any required data to draw.
+				this.datasets[datasetIndex].points.push(new this.PointClass({
+					value : value,
+					label : label,
+					datasetLabel: this.datasets[datasetIndex].label,
+					x: this.scale.calculateX(this.scale.valuesCount+1),
+					y: this.scale.endPoint,
+					strokeColor : this.datasets[datasetIndex].pointStrokeColor,
+					fillColor : this.datasets[datasetIndex].pointColor
+				}));
+			},this);
+
+			this.scale.addXLabel(label);
+			//Then re-render the chart.
+			this.update();
+		},
+		removeData : function(){
+			this.scale.removeXLabel();
+			//Then re-render the chart.
+			helpers.each(this.datasets,function(dataset){
+				dataset.points.shift();
+			},this);
+			this.update();
+		},
+		reflow : function(){
+			var newScaleProps = helpers.extend({
+				height : this.chart.height,
+				width : this.chart.width
+			});
+			this.scale.update(newScaleProps);
+		},
+		draw : function(ease){
+			var easingDecimal = ease || 1;
+			this.clear();
+
+			var ctx = this.chart.ctx;
+
+			// Some helper methods for getting the next/prev points
+			var hasValue = function(item){
+				return item.value !== null;
+			},
+			nextPoint = function(point, collection, index){
+				return helpers.findNextWhere(collection, hasValue, index) || point;
+			},
+			previousPoint = function(point, collection, index){
+				return helpers.findPreviousWhere(collection, hasValue, index) || point;
+			};
+
+			if (!this.scale) return;
+			this.scale.draw(easingDecimal);
+
+
+			helpers.each(this.datasets,function(dataset){
+				var pointsWithValues = helpers.where(dataset.points, hasValue);
+
+				//Transition each point first so that the line and point drawing isn't out of sync
+				//We can use this extra loop to calculate the control points of this dataset also in this loop
+
+				helpers.each(dataset.points, function(point, index){
+					if (point.hasValue()){
+						point.transition({
+							y : this.scale.calculateY(point.value),
+							x : this.scale.calculateX(index)
+						}, easingDecimal);
+					}
+				},this);
+
+
+				// Control points need to be calculated in a separate loop, because we need to know the current x/y of the point
+				// This would cause issues when there is no animation, because the y of the next point would be 0, so beziers would be skewed
+				if (this.options.bezierCurve){
+					helpers.each(pointsWithValues, function(point, index){
+						var tension = (index > 0 && index < pointsWithValues.length - 1) ? this.options.bezierCurveTension : 0;
+						point.controlPoints = helpers.splineCurve(
+							previousPoint(point, pointsWithValues, index),
+							point,
+							nextPoint(point, pointsWithValues, index),
+							tension
+						);
+
+						// Prevent the bezier going outside of the bounds of the graph
+
+						// Cap puter bezier handles to the upper/lower scale bounds
+						if (point.controlPoints.outer.y > this.scale.endPoint){
+							point.controlPoints.outer.y = this.scale.endPoint;
+						}
+						else if (point.controlPoints.outer.y < this.scale.startPoint){
+							point.controlPoints.outer.y = this.scale.startPoint;
+						}
+
+						// Cap inner bezier handles to the upper/lower scale bounds
+						if (point.controlPoints.inner.y > this.scale.endPoint){
+							point.controlPoints.inner.y = this.scale.endPoint;
+						}
+						else if (point.controlPoints.inner.y < this.scale.startPoint){
+							point.controlPoints.inner.y = this.scale.startPoint;
+						}
+					},this);
+				}
+
+
+				//Draw the line between all the points
+				ctx.lineWidth = this.options.datasetStrokeWidth;
+				ctx.strokeStyle = dataset.strokeColor;
+				ctx.beginPath();
+
+				helpers.each(pointsWithValues, function(point, index){
+					if (index === 0){
+						ctx.moveTo(point.x, point.y);
+					}
+					else{
+						if(this.options.bezierCurve){
+							var previous = previousPoint(point, pointsWithValues, index);
+
+							ctx.bezierCurveTo(
+								previous.controlPoints.outer.x,
+								previous.controlPoints.outer.y,
+								point.controlPoints.inner.x,
+								point.controlPoints.inner.y,
+								point.x,
+								point.y
+							);
+						}
+						else{
+							ctx.lineTo(point.x,point.y);
+						}
+					}
+				}, this);
+
+				if (this.options.datasetStroke) {
+					ctx.stroke();
+				}
+
+				if (this.options.datasetFill && pointsWithValues.length > 0){
+					//Round off the line by going to the base of the chart, back to the start, then fill.
+					ctx.lineTo(pointsWithValues[pointsWithValues.length - 1].x, this.scale.endPoint);
+					ctx.lineTo(pointsWithValues[0].x, this.scale.endPoint);
+					ctx.fillStyle = dataset.fillColor;
+					ctx.closePath();
+					ctx.fill();
+				}
+
+				//Now draw the points over the line
+				//A little inefficient double looping, but better than the line
+				//lagging behind the point positions
+				helpers.each(pointsWithValues,function(point){
+					point.draw();
+				});
+			},this);
+		}
+	});
+
+
+}).call(this);
+
+(function(){
+	"use strict";
+
+	var root = this,
+		Chart = root.Chart,
+		//Cache a local reference to Chart.helpers
+		helpers = Chart.helpers;
+
+	var defaultConfig = {
+		//Boolean - Show a backdrop to the scale label
+		scaleShowLabelBackdrop : true,
+
+		//String - The colour of the label backdrop
+		scaleBackdropColor : "rgba(255,255,255,0.75)",
+
+		// Boolean - Whether the scale should begin at zero
+		scaleBeginAtZero : true,
+
+		//Number - The backdrop padding above & below the label in pixels
+		scaleBackdropPaddingY : 2,
+
+		//Number - The backdrop padding to the side of the label in pixels
+		scaleBackdropPaddingX : 2,
+
+		//Boolean - Show line for each value in the scale
+		scaleShowLine : true,
+
+		//Boolean - Stroke a line around each segment in the chart
+		segmentShowStroke : true,
+
+		//String - The colour of the stroke on each segment.
+		segmentStrokeColor : "#fff",
+
+		//Number - The width of the stroke value in pixels
+		segmentStrokeWidth : 2,
+
+		//Number - Amount of animation steps
+		animationSteps : 100,
+
+		//String - Animation easing effect.
+		animationEasing : "easeOutBounce",
+
+		//Boolean - Whether to animate the rotation of the chart
+		animateRotate : true,
+
+		//Boolean - Whether to animate scaling the chart from the centre
+		animateScale : false,
+
+		//String - A legend template
+		legendTemplate : "<ul class=\"<%=name.toLowerCase()%>-legend\"><% for (var i=0; i<segments.length; i++){%><li><span style=\"background-color:<%=segments[i].fillColor%>\"><%if(segments[i].label){%><%=segments[i].label%><%}%></span></li><%}%></ul>"
+	};
+
+
+	Chart.Type.extend({
+		//Passing in a name registers this chart in the Chart namespace
+		name: "PolarArea",
+		//Providing a defaults will also register the deafults in the chart namespace
+		defaults : defaultConfig,
+		//Initialize is fired when the chart is initialized - Data is passed in as a parameter
+		//Config is automatically merged by the core of Chart.js, and is available at this.options
+		initialize:  function(data){
+			this.segments = [];
+			//Declare segment class as a chart instance specific class, so it can share props for this instance
+			this.SegmentArc = Chart.Arc.extend({
+				showStroke : this.options.segmentShowStroke,
+				strokeWidth : this.options.segmentStrokeWidth,
+				strokeColor : this.options.segmentStrokeColor,
+				ctx : this.chart.ctx,
+				innerRadius : 0,
+				x : this.chart.width/2,
+				y : this.chart.height/2
+			});
+			this.scale = new Chart.RadialScale({
+				display: this.options.showScale,
+				fontStyle: this.options.scaleFontStyle,
+				fontSize: this.options.scaleFontSize,
+				fontFamily: this.options.scaleFontFamily,
+				fontColor: this.options.scaleFontColor,
+				showLabels: this.options.scaleShowLabels,
+				showLabelBackdrop: this.options.scaleShowLabelBackdrop,
+				backdropColor: this.options.scaleBackdropColor,
+				backdropPaddingY : this.options.scaleBackdropPaddingY,
+				backdropPaddingX: this.options.scaleBackdropPaddingX,
+				lineWidth: (this.options.scaleShowLine) ? this.options.scaleLineWidth : 0,
+				lineColor: this.options.scaleLineColor,
+				lineArc: true,
+				width: this.chart.width,
+				height: this.chart.height,
+				xCenter: this.chart.width/2,
+				yCenter: this.chart.height/2,
+				ctx : this.chart.ctx,
+				templateString: this.options.scaleLabel,
+				valuesCount: data.length
+			});
+
+			this.updateScaleRange(data);
+
+			this.scale.update();
+
+			helpers.each(data,function(segment,index){
+				this.addData(segment,index,true);
+			},this);
+
+			//Set up tooltip events on the chart
+			if (this.options.showTooltips){
+				helpers.bindEvents(this, this.options.tooltipEvents, function(evt){
+					var activeSegments = (evt.type !== 'mouseout') ? this.getSegmentsAtEvent(evt) : [];
+					helpers.each(this.segments,function(segment){
+						segment.restore(["fillColor"]);
+					});
+					helpers.each(activeSegments,function(activeSegment){
+						activeSegment.fillColor = activeSegment.highlightColor;
+					});
+					this.showTooltip(activeSegments);
+				});
+			}
+
+			this.render();
+		},
+		getSegmentsAtEvent : function(e){
+			var segmentsArray = [];
+
+			var location = helpers.getRelativePosition(e);
+
+			helpers.each(this.segments,function(segment){
+				if (segment.inRange(location.x,location.y)) segmentsArray.push(segment);
+			},this);
+			return segmentsArray;
+		},
+		addData : function(segment, atIndex, silent){
+			var index = atIndex || this.segments.length;
+
+			this.segments.splice(index, 0, new this.SegmentArc({
+				fillColor: segment.color,
+				highlightColor: segment.highlight || segment.color,
+				label: segment.label,
+				value: segment.value,
+				outerRadius: (this.options.animateScale) ? 0 : this.scale.calculateCenterOffset(segment.value),
+				circumference: (this.options.animateRotate) ? 0 : this.scale.getCircumference(),
+				startAngle: Math.PI * 1.5
+			}));
+			if (!silent){
+				this.reflow();
+				this.update();
+			}
+		},
+		removeData: function(atIndex){
+			var indexToDelete = (helpers.isNumber(atIndex)) ? atIndex : this.segments.length-1;
+			this.segments.splice(indexToDelete, 1);
+			this.reflow();
+			this.update();
+		},
+		calculateTotal: function(data){
+			this.total = 0;
+			helpers.each(data,function(segment){
+				this.total += segment.value;
+			},this);
+			this.scale.valuesCount = this.segments.length;
+		},
+		updateScaleRange: function(datapoints){
+			var valuesArray = [];
+			helpers.each(datapoints,function(segment){
+				valuesArray.push(segment.value);
+			});
+
+			var scaleSizes = (this.options.scaleOverride) ?
+				{
+					steps: this.options.scaleSteps,
+					stepValue: this.options.scaleStepWidth,
+					min: this.options.scaleStartValue,
+					max: this.options.scaleStartValue + (this.options.scaleSteps * this.options.scaleStepWidth)
+				} :
+				helpers.calculateScaleRange(
+					valuesArray,
+					helpers.min([this.chart.width, this.chart.height])/2,
+					this.options.scaleFontSize,
+					this.options.scaleBeginAtZero,
+					this.options.scaleIntegersOnly
+				);
+
+			helpers.extend(
+				this.scale,
+				scaleSizes,
+				{
+					size: helpers.min([this.chart.width, this.chart.height]),
+					xCenter: this.chart.width/2,
+					yCenter: this.chart.height/2
+				}
+			);
+
+		},
+		update : function(){
+			this.calculateTotal(this.segments);
+
+			helpers.each(this.segments,function(segment){
+				segment.save();
+			});
+			
+			this.reflow();
+			this.render();
+		},
+		reflow : function(){
+			helpers.extend(this.SegmentArc.prototype,{
+				x : this.chart.width/2,
+				y : this.chart.height/2
+			});
+			this.updateScaleRange(this.segments);
+			this.scale.update();
+
+			helpers.extend(this.scale,{
+				xCenter: this.chart.width/2,
+				yCenter: this.chart.height/2
+			});
+
+			helpers.each(this.segments, function(segment){
+				segment.update({
+					outerRadius : this.scale.calculateCenterOffset(segment.value)
+				});
+			}, this);
+
+		},
+		draw : function(ease){
+			var easingDecimal = ease || 1;
+			//Clear & draw the canvas
+			this.clear();
+			helpers.each(this.segments,function(segment, index){
+				segment.transition({
+					circumference : this.scale.getCircumference(),
+					outerRadius : this.scale.calculateCenterOffset(segment.value)
+				},easingDecimal);
+
+				segment.endAngle = segment.startAngle + segment.circumference;
+
+				// If we've removed the first segment we need to set the first one to
+				// start at the top.
+				if (index === 0){
+					segment.startAngle = Math.PI * 1.5;
+				}
+
+				//Check to see if it's the last segment, if not get the next and update the start angle
+				if (index < this.segments.length - 1){
+					this.segments[index+1].startAngle = segment.endAngle;
+				}
+				segment.draw();
+			}, this);
+			this.scale.draw();
+		}
+	});
+
+}).call(this);
+
+(function(){
+	"use strict";
+
+	var root = this,
+		Chart = root.Chart,
+		helpers = Chart.helpers;
+
+
+
+	Chart.Type.extend({
+		name: "Radar",
+		defaults:{
+			//Boolean - Whether to show lines for each scale point
+			scaleShowLine : true,
+
+			//Boolean - Whether we show the angle lines out of the radar
+			angleShowLineOut : true,
+
+			//Boolean - Whether to show labels on the scale
+			scaleShowLabels : false,
+
+			// Boolean - Whether the scale should begin at zero
+			scaleBeginAtZero : true,
+
+			//String - Colour of the angle line
+			angleLineColor : "rgba(0,0,0,.1)",
+
+			//Number - Pixel width of the angle line
+			angleLineWidth : 1,
+
+			//String - Point label font declaration
+			pointLabelFontFamily : "'Arial'",
+
+			//String - Point label font weight
+			pointLabelFontStyle : "normal",
+
+			//Number - Point label font size in pixels
+			pointLabelFontSize : 10,
+
+			//String - Point label font colour
+			pointLabelFontColor : "#666",
+
+			//Boolean - Whether to show a dot for each point
+			pointDot : true,
+
+			//Number - Radius of each point dot in pixels
+			pointDotRadius : 3,
+
+			//Number - Pixel width of point dot stroke
+			pointDotStrokeWidth : 1,
+
+			//Number - amount extra to add to the radius to cater for hit detection outside the drawn point
+			pointHitDetectionRadius : 20,
+
+			//Boolean - Whether to show a stroke for datasets
+			datasetStroke : true,
+
+			//Number - Pixel width of dataset stroke
+			datasetStrokeWidth : 2,
+
+			//Boolean - Whether to fill the dataset with a colour
+			datasetFill : true,
+
+			//String - A legend template
+			legendTemplate : "<ul class=\"<%=name.toLowerCase()%>-legend\"><% for (var i=0; i<datasets.length; i++){%><li><span style=\"background-color:<%=datasets[i].strokeColor%>\"><%if(datasets[i].label){%><%=datasets[i].label%><%}%></span></li><%}%></ul>"
+
+		},
+
+		initialize: function(data){
+			this.PointClass = Chart.Point.extend({
+				strokeWidth : this.options.pointDotStrokeWidth,
+				radius : this.options.pointDotRadius,
+				display: this.options.pointDot,
+				hitDetectionRadius : this.options.pointHitDetectionRadius,
+				ctx : this.chart.ctx
+			});
+
+			this.datasets = [];
+
+			this.buildScale(data);
+
+			//Set up tooltip events on the chart
+			if (this.options.showTooltips){
+				helpers.bindEvents(this, this.options.tooltipEvents, function(evt){
+					var activePointsCollection = (evt.type !== 'mouseout') ? this.getPointsAtEvent(evt) : [];
+
+					this.eachPoints(function(point){
+						point.restore(['fillColor', 'strokeColor']);
+					});
+					helpers.each(activePointsCollection, function(activePoint){
+						activePoint.fillColor = activePoint.highlightFill;
+						activePoint.strokeColor = activePoint.highlightStroke;
+					});
+
+					this.showTooltip(activePointsCollection);
+				});
+			}
+
+			//Iterate through each of the datasets, and build this into a property of the chart
+			helpers.each(data.datasets,function(dataset){
+
+				var datasetObject = {
+					label: dataset.label || null,
+					fillColor : dataset.fillColor,
+					strokeColor : dataset.strokeColor,
+					pointColor : dataset.pointColor,
+					pointStrokeColor : dataset.pointStrokeColor,
+					points : []
+				};
+
+				this.datasets.push(datasetObject);
+
+				helpers.each(dataset.data,function(dataPoint,index){
+					//Add a new point for each piece of data, passing any required data to draw.
+					var pointPosition;
+					if (!this.scale.animation){
+						pointPosition = this.scale.getPointPosition(index, this.scale.calculateCenterOffset(dataPoint));
+					}
+					datasetObject.points.push(new this.PointClass({
+						value : dataPoint,
+						label : data.labels[index],
+						datasetLabel: dataset.label,
+						x: (this.options.animation) ? this.scale.xCenter : pointPosition.x,
+						y: (this.options.animation) ? this.scale.yCenter : pointPosition.y,
+						strokeColor : dataset.pointStrokeColor,
+						fillColor : dataset.pointColor,
+						highlightFill : dataset.pointHighlightFill || dataset.pointColor,
+						highlightStroke : dataset.pointHighlightStroke || dataset.pointStrokeColor
+					}));
+				},this);
+
+			},this);
+
+			this.render();
+		},
+		eachPoints : function(callback){
+			helpers.each(this.datasets,function(dataset){
+				helpers.each(dataset.points,callback,this);
+			},this);
+		},
+
+		getPointsAtEvent : function(evt){
+			var mousePosition = helpers.getRelativePosition(evt),
+				fromCenter = helpers.getAngleFromPoint({
+					x: this.scale.xCenter,
+					y: this.scale.yCenter
+				}, mousePosition);
+
+			var anglePerIndex = (Math.PI * 2) /this.scale.valuesCount,
+				pointIndex = Math.round((fromCenter.angle - Math.PI * 1.5) / anglePerIndex),
+				activePointsCollection = [];
+
+			// If we're at the top, make the pointIndex 0 to get the first of the array.
+			if (pointIndex >= this.scale.valuesCount || pointIndex < 0){
+				pointIndex = 0;
+			}
+
+			if (fromCenter.distance <= this.scale.drawingArea){
+				helpers.each(this.datasets, function(dataset){
+					activePointsCollection.push(dataset.points[pointIndex]);
+				});
+			}
+
+			return activePointsCollection;
+		},
+
+		buildScale : function(data){
+			this.scale = new Chart.RadialScale({
+				display: this.options.showScale,
+				fontStyle: this.options.scaleFontStyle,
+				fontSize: this.options.scaleFontSize,
+				fontFamily: this.options.scaleFontFamily,
+				fontColor: this.options.scaleFontColor,
+				showLabels: this.options.scaleShowLabels,
+				showLabelBackdrop: this.options.scaleShowLabelBackdrop,
+				backdropColor: this.options.scaleBackdropColor,
+				backgroundColors: this.options.scaleBackgroundColors,
+				backdropPaddingY : this.options.scaleBackdropPaddingY,
+				backdropPaddingX: this.options.scaleBackdropPaddingX,
+				lineWidth: (this.options.scaleShowLine) ? this.options.scaleLineWidth : 0,
+				lineColor: this.options.scaleLineColor,
+				angleLineColor : this.options.angleLineColor,
+				angleLineWidth : (this.options.angleShowLineOut) ? this.options.angleLineWidth : 0,
+				// Point labels at the edge of each line
+				pointLabelFontColor : this.options.pointLabelFontColor,
+				pointLabelFontSize : this.options.pointLabelFontSize,
+				pointLabelFontFamily : this.options.pointLabelFontFamily,
+				pointLabelFontStyle : this.options.pointLabelFontStyle,
+				height : this.chart.height,
+				width: this.chart.width,
+				xCenter: this.chart.width/2,
+				yCenter: this.chart.height/2,
+				ctx : this.chart.ctx,
+				templateString: this.options.scaleLabel,
+				labels: data.labels,
+				valuesCount: data.datasets[0].data.length
+			});
+
+			this.scale.setScaleSize();
+			this.updateScaleRange(data.datasets);
+			this.scale.buildYLabels();
+		},
+		updateScaleRange: function(datasets){
+			var valuesArray = (function(){
+				var totalDataArray = [];
+				helpers.each(datasets,function(dataset){
+					if (dataset.data){
+						totalDataArray = totalDataArray.concat(dataset.data);
+					}
+					else {
+						helpers.each(dataset.points, function(point){
+							totalDataArray.push(point.value);
+						});
+					}
+				});
+				return totalDataArray;
+			})();
+
+
+			var scaleSizes = (this.options.scaleOverride) ?
+				{
+					steps: this.options.scaleSteps,
+					stepValue: this.options.scaleStepWidth,
+					min: this.options.scaleStartValue,
+					max: this.options.scaleStartValue + (this.options.scaleSteps * this.options.scaleStepWidth)
+				} :
+				helpers.calculateScaleRange(
+					valuesArray,
+					helpers.min([this.chart.width, this.chart.height])/2,
+					this.options.scaleFontSize,
+					this.options.scaleBeginAtZero,
+					this.options.scaleIntegersOnly
+				);
+
+			helpers.extend(
+				this.scale,
+				scaleSizes
+			);
+
+		},
+		addData : function(valuesArray,label){
+			//Map the values array for each of the datasets
+			this.scale.valuesCount++;
+			helpers.each(valuesArray,function(value,datasetIndex){
+				var pointPosition = this.scale.getPointPosition(this.scale.valuesCount, this.scale.calculateCenterOffset(value));
+				this.datasets[datasetIndex].points.push(new this.PointClass({
+					value : value,
+					label : label,
+					datasetLabel: this.datasets[datasetIndex].label,
+					x: pointPosition.x,
+					y: pointPosition.y,
+					strokeColor : this.datasets[datasetIndex].pointStrokeColor,
+					fillColor : this.datasets[datasetIndex].pointColor
+				}));
+			},this);
+
+			this.scale.labels.push(label);
+
+			this.reflow();
+
+			this.update();
+		},
+		removeData : function(){
+			this.scale.valuesCount--;
+			this.scale.labels.shift();
+			helpers.each(this.datasets,function(dataset){
+				dataset.points.shift();
+			},this);
+			this.reflow();
+			this.update();
+		},
+		update : function(){
+			this.eachPoints(function(point){
+				point.save();
+			});
+			this.reflow();
+			this.render();
+		},
+		reflow: function(){
+			helpers.extend(this.scale, {
+				width : this.chart.width,
+				height: this.chart.height,
+				size : helpers.min([this.chart.width, this.chart.height]),
+				xCenter: this.chart.width/2,
+				yCenter: this.chart.height/2
+			});
+			this.updateScaleRange(this.datasets);
+			this.scale.setScaleSize();
+			this.scale.buildYLabels();
+		},
+		draw : function(ease){
+			var easeDecimal = ease || 1,
+				ctx = this.chart.ctx;
+			this.clear();
+			this.scale.draw();
+
+			helpers.each(this.datasets,function(dataset){
+
+				//Transition each point first so that the line and point drawing isn't out of sync
+				helpers.each(dataset.points,function(point,index){
+					if (point.hasValue()){
+						point.transition(this.scale.getPointPosition(index, this.scale.calculateCenterOffset(point.value)), easeDecimal);
+					}
+				},this);
+
+
+
+				//Draw the line between all the points
+				ctx.lineWidth = this.options.datasetStrokeWidth;
+				ctx.strokeStyle = dataset.strokeColor;
+				ctx.beginPath();
+				helpers.each(dataset.points,function(point,index){
+					if (index === 0){
+						ctx.moveTo(point.x,point.y);
+					}
+					else{
+						ctx.lineTo(point.x,point.y);
+					}
+				},this);
+				ctx.closePath();
+				ctx.stroke();
+
+				ctx.fillStyle = dataset.fillColor;
+				if(this.options.datasetFill){
+					ctx.fill();
+				}
+				//Now draw the points over the line
+				//A little inefficient double looping, but better than the line
+				//lagging behind the point positions
+				helpers.each(dataset.points,function(point){
+					if (point.hasValue()){
+						point.draw();
+					}
+				});
+
+			},this);
+
+		}
+
+	});
+
+
+
+
+
+}).call(this);

A diferenza do arquivo foi suprimida porque é demasiado grande
+ 9 - 0
main/inc/lib/javascript/chartjs/Chart.min.js


+ 12 - 10
main/inc/lib/nusoap/class.soap_parser.php

@@ -1,8 +1,5 @@
 <?php
 
-
-
-
 /**
 *
 * nusoap_parser class parses SOAP XML messages into native PHP values
@@ -12,8 +9,8 @@
 * @version  $Id: class.soap_parser.php,v 1.42 2010/04/26 20:15:08 snichol Exp $
 * @access   public
 */
-class nusoap_parser extends nusoap_base {
-
+class nusoap_parser extends nusoap_base
+{
 	var $xml = '';
 	var $xml_encoding = '';
 	var $method = '';
@@ -65,7 +62,7 @@ class nusoap_parser extends nusoap_base {
 		$this->decode_utf8 = $decode_utf8;
 
 		// Check whether content has been read.
-		if(!empty($xml)){
+		if(!empty($this->xml)){
 			// Check XML encoding
 			$pos_xml = strpos($xml, '<?xml');
 			if ($pos_xml !== FALSE) {
@@ -102,8 +99,11 @@ class nusoap_parser extends nusoap_base {
 			xml_set_element_handler($this->parser, 'start_element','end_element');
 			xml_set_character_data_handler($this->parser,'character_data');
 
+			xml_parse($this->parser, $this->xml);
+
 			// Parse the XML file.
-			if(!xml_parse($this->parser,$xml,true)){
+			//if (!xml_parse($this->parser,$xml,true)){
+			if (false) {
 			    // Display an error message.
 			    $err = sprintf('XML error parsing SOAP payload on line %d: %s',
 			    xml_get_current_line_number($this->parser),
@@ -152,6 +152,7 @@ class nusoap_parser extends nusoap_base {
 	* @access   private
 	*/
 	function start_element($parser, $name, $attrs) {
+
 		// position in a total number of elements, starting from 0
 		// update class level pos
 		$pos = $this->position++;
@@ -412,9 +413,10 @@ class nusoap_parser extends nusoap_base {
 	* @param    string $data element content
 	* @access   private
 	*/
-	function character_data($parser, $data){
+	function character_data($parser, $data)
+    {
 		$pos = $this->depth_array[$this->depth];
-		if ($this->xml_encoding=='UTF-8'){
+		if ($this->xml_encoding == 'UTF-8'){
 			// TODO: add an option to disable this for folks who want
 			// raw UTF-8 that, e.g., might not map to iso-8859-1
 			// TODO: this can also be handled with xml_parser_set_option($this->parser, XML_OPTION_TARGET_ENCODING, "ISO-8859-1");
@@ -610,7 +612,7 @@ class nusoap_parser extends nusoap_base {
 			}
 			$ret = is_array($params) ? $params : array();
 			$this->debug('in buildVal, return:');
-			$this->appendDebug($this->varDump($ret));
+			//$this->appendDebug($this->varDump($ret));
 			return $ret;
 		} else {
         	$this->debug('in buildVal, no children, building scalar');

+ 1 - 3
main/inc/lib/nusoap/class.soap_server.php

@@ -661,6 +661,7 @@ class nusoap_server extends nusoap_base {
 			$this->debug('got a(n) '.gettype($this->methodreturn).' from method');
 			$this->debug('serializing return value');
 			if($this->wsdl){
+				$opParams = "";
 				if (sizeof($this->opData['output']['parts']) > 1) {
 					$this->debug('more than one output part, so use the method return unchanged');
 			    	$opParams = $this->methodreturn;
@@ -1123,6 +1124,3 @@ class nusoap_server extends nusoap_base {
  */
 class soap_server extends nusoap_server {
 }
-
-
-?>

+ 12 - 12
main/inc/lib/sessionmanager.lib.php

@@ -172,26 +172,26 @@ class SessionManager
                 );
 
                 if (!empty($startDate)) {
-                    $values['access_start_date'] = $startDate;
+                    $values['access_start_date'] = api_get_utc_datetime($startDate);
                 }
 
                 if (!empty($endDate)) {
-                    $values['access_end_date'] = $endDate;
+                    $values['access_end_date'] = api_get_utc_datetime($endDate);
                 }
 
                 if (!empty($displayStartDate)) {
-                    $values['display_start_date'] = $displayStartDate;
+                    $values['display_start_date'] = api_get_utc_datetime($displayStartDate);
                 }
 
                 if (!empty($displayEndDate)) {
-                    $values['display_end_date'] = $displayEndDate;
+                    $values['display_end_date'] = api_get_utc_datetime($displayEndDate);
                 }
 
                 if (!empty($coachStartDate)) {
-                    $values['coach_access_start_date'] = $coachStartDate;
+                    $values['coach_access_start_date'] = api_get_utc_datetime($coachStartDate);
                 }
                 if (!empty($coachEndDate)) {
-                    $values['coach_access_end_date'] = $coachEndDate;
+                    $values['coach_access_end_date'] = api_get_utc_datetime($coachEndDate);
                 }
 
                 if (!empty($sessionCategoryId)) {
@@ -1427,26 +1427,26 @@ class SessionManager
                 }
 
                 if (!empty($startDate)) {
-                    $values['access_start_date'] = $startDate;
+                    $values['access_start_date'] = api_get_utc_datetime($startDate);
                 }
 
                 if (!empty($endDate)) {
-                    $values['access_end_date'] = $endDate;
+                    $values['access_end_date'] = api_get_utc_datetime($endDate);
                 }
 
                 if (!empty($displayStartDate)) {
-                    $values['display_start_date'] = $displayStartDate;
+                    $values['display_start_date'] = api_get_utc_datetime($displayStartDate);
                 }
 
                 if (!empty($displayEndDate)) {
-                    $values['display_end_date'] = $displayEndDate;
+                    $values['display_end_date'] = api_get_utc_datetime($displayEndDate);
                 }
 
                 if (!empty($coachStartDate)) {
-                    $values['coach_access_start_date'] = $coachStartDate;
+                    $values['coach_access_start_date'] = api_get_utc_datetime($coachStartDate);
                 }
                 if (!empty($coachEndDate)) {
-                    $values['coach_access_end_date'] = $coachEndDate;
+                    $values['coach_access_end_date'] = api_get_utc_datetime($coachEndDate);
                 }
 
                 if (!empty($sessionCategoryId)) {

+ 2 - 2
main/inc/lib/social.lib.php

@@ -1227,7 +1227,7 @@ class SocialManager extends UserManager
      * @return boolean
      * @author Yannick Warnier
      */
-    public static function sendWallMessage($userId, $friendId, $messageContent, $messageId = 0 ,$messageStatus)
+    public static function sendWallMessage($userId, $friendId, $messageContent, $messageId = 0, $messageStatus = '')
     {
         $tblMessage = Database::get_main_table(TABLE_MESSAGE);
         $userId = intval($userId);
@@ -1453,7 +1453,7 @@ class SocialManager extends UserManager
             $start = '0000-00-00';
         }
         $isOwnWall = (api_get_user_id() == $userId  && $userId == $friendId);
-        $messages = self::getWallMessages($userId, MESSAGE_STATUS_WALL_POST , null, $start, $limit, $offset);
+        $messages = self::getWallMessages($userId, MESSAGE_STATUS_WALL_POST, null, $start, $limit, $offset);
         $users = array();
         $data = array();
         foreach ($messages as $key => $message) {

+ 34 - 0
main/inc/lib/statistics.lib.php

@@ -530,6 +530,40 @@ class Statistics
             Statistics::printStats(get_lang('Logins'), $totalLogin, false);
         }
     }
+    
+    /**
+     * get the number of recent logins
+     * @param bool    $distinct   Whether to only give distinct users stats, or *all* logins
+     * @return array
+     */
+    public static function getRecentLoginStats($distinct = false)
+    {
+        $totalLogin = [];
+        $table = Database::get_main_table(TABLE_STATISTIC_TRACK_E_LOGIN);
+        $access_url_rel_user_table= Database :: get_main_table(TABLE_MAIN_ACCESS_URL_REL_USER);
+        $current_url_id = api_get_current_access_url_id();
+        if (api_is_multiple_url_enabled()) {
+            $table_url = ", $access_url_rel_user_table";
+            $where_url = " AND login_user_id=user_id AND access_url_id='".$current_url_id."'";
+        } else {
+            $table_url = '';
+            $where_url='';
+        }
+        $now = api_get_utc_datetime();
+        $field = 'login_user_id';
+        if ($distinct) {
+            $field = 'DISTINCT(login_user_id)';
+        }
+        $sql = "SELECT count($field) AS number, date(login_date) as login_date FROM $table $table_url WHERE DATE_ADD(login_date, INTERVAL 15 DAY) >= '$now' $where_url GROUP BY date(login_date)";
+        
+        $res = Database::query($sql);
+        while($row = Database::fetch_array($res,'ASSOC')){
+            $totalLogin[$row['login_date']] = $row['number'];
+        }
+        
+        return $totalLogin;
+    }
+    
     /**
      * Show some stats about the accesses to the different course tools
      */

+ 1 - 1
main/inc/lib/system_announcements.lib.php

@@ -732,7 +732,7 @@ class SystemAnnouncementManager
 
         $sql = "SELECT * FROM " . $table . "
 				WHERE
-				    (lang = '$user_selected_language' OR lang IS NULL) AND
+				    (lang = '$user_selected_language' OR lang = '') AND
 				    ('$now' >= date_start AND '$now' <= date_end) ";
 
         switch ($visible) {

+ 42 - 23
main/inc/lib/tracking.lib.php

@@ -3697,8 +3697,7 @@ class Tracking
         $tbl_track_login = Database :: get_main_table(TABLE_STATISTIC_TRACK_E_COURSE_ACCESS);
         $tbl_session_course_user = Database :: get_main_table(TABLE_MAIN_SESSION_COURSE_USER);
         $table_course_rel_user = Database :: get_main_table(TABLE_MAIN_COURSE_USER);
-        $tableCourse   = Database :: get_main_table(TABLE_MAIN_COURSE);
-        $inner = '';
+        $tableCourse = Database :: get_main_table(TABLE_MAIN_COURSE);
         $now = api_get_utc_datetime();
         $courseId = intval($courseId);
 
@@ -3706,37 +3705,57 @@ class Tracking
             return false;
         }
 
-        if ($session_id != 0) {
-            $inner = ' INNER JOIN '.$tbl_session_course_user.' session_course_user
-                    ON c.id = session_course_user.c_id
-                    AND session_course_user.session_id = '.intval($session_id).'
-                    AND session_course_user.user_id = stats_login.user_id ';
+        if (empty($session_id)) {
+            $inner = '
+                INNER JOIN '.$table_course_rel_user.' course_user
+                ON course_user.user_id = stats_login.user_id AND course_user.c_id = c.id
+            ';
+        } else {
+            $inner = '
+                    INNER JOIN '.$tbl_session_course_user.' session_course_user
+                    ON
+                        c.id = session_course_user.c_id AND
+                        session_course_user.session_id = '.intval($session_id).' AND
+                        session_course_user.user_id = stats_login.user_id ';
         }
 
         $sql = 'SELECT stats_login.user_id, MAX(login_course_date) max_date
-                FROM '.$tbl_track_login.' stats_login '.$inner.'
+                FROM '.$tbl_track_login.' stats_login
                 INNER JOIN '.$tableCourse.' c
                 ON (c.id = stats_login.c_id)
-                INNER JOIN '.$table_course_rel_user.' course_user
-                ON course_user.user_id = stats_login.user_id AND course_user.c_id = c.id
+                '.$inner.'
                 WHERE c.id = '.$courseId.'
-                GROUP BY user_id
+                GROUP BY stats_login.user_id
                 HAVING DATE_SUB( "' . $now . '", INTERVAL '.$since.' DAY) > max_date ';
 
         if ($since == 'never') {
-            $sql = 'SELECT course_user.user_id
-                    FROM '.$table_course_rel_user.' course_user
-                    LEFT JOIN '. $tbl_track_login.' stats_login
-                    ON course_user.user_id = stats_login.user_id AND
-                    relation_type<>'.COURSE_RELATION_TYPE_RRHH.'
-                    INNER JOIN '.$tableCourse.' c
-                    ON (c.id = stats_login.c_id)
-                    '.$inner.'
-                    WHERE
-                        course_user.c_id = '.$courseId.' AND
-                        stats_login.login_course_date IS NULL
-                    GROUP BY course_user.user_id';
+            if (empty($session_id)) {
+                $sql = 'SELECT course_user.user_id
+                        FROM ' . $table_course_rel_user . ' course_user
+                        LEFT JOIN ' . $tbl_track_login . ' stats_login
+                        ON course_user.user_id = stats_login.user_id AND
+                        relation_type<>' . COURSE_RELATION_TYPE_RRHH . '
+                        INNER JOIN ' . $tableCourse . ' c
+                        ON (c.id = course_user.c_id)
+                        WHERE
+                            course_user.c_id = ' . $courseId . ' AND
+                            stats_login.login_course_date IS NULL
+                        GROUP BY course_user.user_id';
+            } else {
+                $sql = 'SELECT session_course_user.user_id
+                        FROM '.$tbl_session_course_user.' session_course_user
+                        LEFT JOIN ' . $tbl_track_login . ' stats_login
+                        ON session_course_user.user_id = stats_login.user_id
+                        INNER JOIN ' . $tableCourse . ' c
+                        ON (c.id = session_course_user.c_id)
+                        WHERE
+                            session_course_user.c_id = ' . $courseId . ' AND
+                            stats_login.login_course_date IS NULL
+                        GROUP BY session_course_user.user_id';
+
+            }
         }
+
         $rs = Database::query($sql);
         $inactive_users = array();
         while($user = Database::fetch_array($rs)) {

+ 1 - 1
main/inc/lib/usermanager.lib.php

@@ -1824,7 +1824,7 @@ class UserManager
             'variable' => $variable,
             'value' => $value
         ];
-        $extraFieldValue->save($params);
+        return $extraFieldValue->save($params);
     }
 
     /**

+ 3 - 1
main/lang/english/trad4all.inc.php

@@ -3169,7 +3169,7 @@ $ModifyQuestion = "Save the question";
 $Example = "Example";
 $CheckAll = "Check all";
 $NbAnnoucement = "Announcement";
-$OtherCourses = "other courses";
+$OtherCourses = "Other courses";
 $Doc = "Document";
 $PlataformAdmin = "Portal Admin";
 $Groups = "Groups";
@@ -7609,4 +7609,6 @@ $DissociateForumToLPItem = "Dissociate the forum of this learning path item";
 $AssociateForumToLPItem = "Associate a forum to this learning path item";
 $ForumDissociated = "Forum dissociated";
 $ClickOrDropOneFileHere = "Click or drop one file here";
+$ModuloPercentage = "Modulo:\t\t\t%";
+$LastXDays = "Last % days";
 ?>

+ 4 - 2
main/lang/french/trad4all.inc.php

@@ -3133,7 +3133,7 @@ $Statistics = "Statistiques";
 $GroupList = "Liste des groupes";
 $Previous = "précédent";
 $DestDirectoryDoesntExist = "Le répertoire de destination n'existe pas";
-$Courses = "cours";
+$Courses = "Cours";
 $In = "dans";
 $ShowAll = "Afficher tout";
 $Page = "Page";
@@ -3160,7 +3160,7 @@ $ModifyQuestion = "Modifier la question";
 $Example = "Exemple";
 $CheckAll = "Sélectionner tout";
 $NbAnnoucement = "Annonces";
-$OtherCourses = "autres cours";
+$OtherCourses = "Autres cours";
 $Doc = "Parcours";
 $PlataformAdmin = "Administrateur de la plateforme";
 $Groups = "Groupes";
@@ -7596,4 +7596,6 @@ $DissociateForumToLPItem = "Dissocier le fil de conversation de cette élément
 $AssociateForumToLPItem = "Associer un fil de conversation du forum à cet élément du parcours";
 $ForumDissociated = "Forum dissocié";
 $ClickOrDropOneFileHere = "Déplacez un fichier ici ou cliquez";
+$ModuloPercentage = "Modulo:\t\t\t%";
+$LastXDays = "Derniers %s jours";
 ?>

+ 3 - 1
main/lang/spanish/trad4all.inc.php

@@ -3169,7 +3169,7 @@ $ModifyQuestion = "Modificar pregunta";
 $Example = "Ejemplo";
 $CheckAll = "comprobar";
 $NbAnnoucement = "Avisos";
-$OtherCourses = "otros cursos";
+$OtherCourses = "Otros cursos";
 $Doc = "documento";
 $PlataformAdmin = "Administración de la plataforma";
 $Groups = "Grupos";
@@ -7634,4 +7634,6 @@ $DissociateForumToLPItem = "Disociar el hilo de la conversación de este paso de
 $AssociateForumToLPItem = "Asociar un hilo de conversación de foro a este paso de la lección";
 $ForumDissociated = "Foro disociado";
 $ClickOrDropOneFileHere = "Suelte un archivo aquí o haga clic";
+$ModuloPercentage = "Módulo:\t\t\t%";
+$LastXDays = "Últimos %s días";
 ?>

+ 9 - 0
main/session/session_edit.php

@@ -24,6 +24,15 @@ SessionManager::protectSession($id);
 
 $sessionInfo = SessionManager::fetch($id);
 
+// Sets to local time to show it correctly when you edit a session
+
+$sessionInfo['display_start_date'] = api_get_local_time($sessionInfo['display_start_date']);
+$sessionInfo['display_end_date'] = api_get_local_time($sessionInfo['display_end_date']);
+$sessionInfo['access_start_date'] = api_get_local_time($sessionInfo['access_start_date']);
+$sessionInfo['access_end_date'] = api_get_local_time($sessionInfo['access_end_date']);
+$sessionInfo['coach_access_start_date'] = api_get_local_time($sessionInfo['coach_access_start_date']);
+$sessionInfo['coach_access_end_date'] = api_get_local_time($sessionInfo['coach_access_end_date']);
+
 $id_coach = $sessionInfo['id_coach'];
 $tool_name = get_lang('EditSession');
 

+ 13 - 6
main/social/profile.php

@@ -84,12 +84,19 @@ if (!empty($_POST['social_wall_new_msg_main']) || !empty($_FILES['picture']['tmp
     exit;
 
 } else if (isset($_GET['messageId'])) {
-    $messageId = Security::remove_XSS($_GET['messageId']);
-    $status = SocialManager::deleteMessage($messageId);
-    Display::addFlash(Display::return_message(get_lang('MessageDeleted')));
-    header('Location: ' . api_get_path(WEB_CODE_PATH) . 'social/profile.php');
-    exit;
-
+    $messageId = intval($_GET['messageId']);
+    $messageInfo = MessageManager::get_message_by_id($messageId);
+    if (!empty($messageInfo)) {
+        // I can only delete messages of my own wall
+        if ($messageInfo['user_receiver_id'] == $user_id) {
+            $status = SocialManager::deleteMessage($messageId);
+
+            Display::addFlash(Display::return_message(get_lang('MessageDeleted')));
+            header('Location: ' . api_get_path(WEB_CODE_PATH) . 'social/profile.php');
+            exit;
+        }
+    }
+    api_not_allowed(true);
 } else if (isset($_GET['u'])) { //I'm your friend? I can see your profile?
     $user_id = intval($_GET['u']);
     if (api_is_anonymous($user_id, true)) {

+ 3 - 0
main/upload/form.scorm.php

@@ -106,6 +106,8 @@ if (count($list)>0) {
 
 $form->addButtonUpload(get_lang('Upload'));
 
+/*
+TODO: check the pens plugin is enabled before using it
 if (is_dir(api_get_path(PLUGIN_PATH)."/pens")) {
     require_once api_get_path(PLUGIN_PATH)."/pens/chamilo_pens.php";
     $list = ChamiloPens::findAll();
@@ -116,6 +118,7 @@ if (is_dir(api_get_path(PLUGIN_PATH)."/pens")) {
         }
     }
 }
+*/
 
 // the default values for the form
 $defaults = array('index_document' => 'checked="checked"', 'use_max_score' => 1);

+ 10 - 1
main/webservices/lp.php

@@ -7,6 +7,15 @@
 require_once '../inc/global.inc.php';
 $libpath = api_get_path(LIBRARY_PATH);
 
+ini_set('memory_limit', -1);
+/*
+ini_set('upload_max_filesize', '4000M');
+ini_set('post_max_size', '4000M');
+ini_set('max_execution_time', '80000');
+ini_set('max_input_time', '80000');
+*/
+
+
 $debug = true;
 
 define('WS_ERROR_SECRET_KEY', 1);
@@ -39,7 +48,7 @@ function WSHelperVerifyKey($params)
     // if we are behind a reverse proxy, assume it will send the
     // HTTP_X_FORWARDED_FOR header and use this IP instead
     if (!empty($_SERVER['HTTP_X_FORWARDED_FOR'])) {
-        list($ip1, $ip2) = split(',', $_SERVER['HTTP_X_FORWARDED_FOR']);
+        list($ip1, $ip2) = explode(',', $_SERVER['HTTP_X_FORWARDED_FOR']);
         $ip = trim($ip1);
     }
     if ($debug)

+ 35 - 11
main/webservices/registration.soap.php

@@ -17,6 +17,10 @@ define('WS_ERROR_NOT_FOUND_RESULT', 2);
 define('WS_ERROR_INVALID_INPUT', 3);
 define('WS_ERROR_SETTING', 4);
 
+/**
+ * @param string $code
+ * @return null|soap_fault
+ */
 function returnError($code)
 {
     $fault = null;
@@ -2426,26 +2430,33 @@ function WSEditUserPasswordCrypted($params)
         return returnError(WS_ERROR_SECRET_KEY);
     }
 
+    if ($debug) {
+        error_log('WSEditUserPasswordCrypted');
+    }
+
     $table_user = Database::get_main_table(TABLE_MAIN_USER);
 
     $original_user_id_value = $params['original_user_id_value'];
     $original_user_id_name = $params['original_user_id_name'];
-    $firstname = $params['firstname'];
-    $lastname = $params['lastname'];
-    $username = $params['username'];
+
+    $firstname = isset($params['firstname']) ? $params['firstname'] : '';
+    $lastname = isset($params['lastname']) ? $params['lastname'] : '';
+    $username = isset($params['username']) ? $params['username'] : '';
     $password = null;
     $auth_source = null;
-    $email = $params['email'];
-    $status = $params['status'];
+    $email = isset($params['email']) ? $params['email'] : '';
+    $status = isset($params['status']) ? $params['status'] : '';
     $official_code = '';
-    $phone = $params['phone'];
+    $phone = isset($params['phone']) ? $params['phone'] : '';
     $picture_uri = '';
-    $expiration_date = $params['expiration_date'];
+    $expiration_date = isset($params['expiration_date']) ? $params['expiration_date'] : '';
     $active = 1;
     $creator_id = null;
     $hr_dept_id = 0;
     $extra = null;
-    $extra_list = $params['extra'];
+    $extra_list = isset($params['extra']) ? $params['extra'] : '';
+    $params['password'] = isset($params['password']) ? $params['password'] : '';
+    $params['encrypt_method'] = isset($params['encrypt_method']) ? $params['encrypt_method'] : '';
 
     if (!empty($params['password']) && !empty($params['encrypt_method'])) {
         $password = $params['password'];
@@ -2464,9 +2475,11 @@ function WSEditUserPasswordCrypted($params)
         }
     } elseif (!empty($params['password']) && empty($params['encrypt_method'])) {
         $msg = "If password is not empty the encrypt_method param is required ";
+
         return $msg;
     } elseif (empty($params['password']) && !empty($params['encrypt_method'])) {
         $msg = "If encrypt_method is not empty the password param is required ";
+
         return $msg;
     }
 
@@ -2475,6 +2488,10 @@ function WSEditUserPasswordCrypted($params)
         $original_user_id_name
     );
 
+    if ($debug) {
+        error_log("user: $user_id");
+    }
+
     if ($user_id == 0) {
         return 0;
     } else {
@@ -2505,10 +2522,12 @@ function WSEditUserPasswordCrypted($params)
         $sql .= " firstname='".Database::escape_string($firstname)."', ";
     }
     $sql .= " username='".Database::escape_string($username)."',";
-    if (!is_null($password)) {
+
+    if (!empty($password)) {
         $sql .= " password='".Database::escape_string($password)."',";
     }
-    if (!is_null($auth_source)) {
+
+    if (!empty($auth_source)) {
         $sql .= " auth_source='".Database::escape_string($auth_source)."',";
     }
 
@@ -2539,15 +2558,20 @@ function WSEditUserPasswordCrypted($params)
     if (!is_null($creator_id)) {
         $sql .= ", creator_id='".Database::escape_string($creator_id)."'";
     }
+
     $sql .= " WHERE user_id='$user_id'";
     $return = @Database::query($sql);
 
+    if ($debug) {
+        error_log("SQL: $sql");
+    }
+
     if (is_array($extra_list) && count($extra_list) > 0) {
         foreach ($extra_list as $extra) {
             $extra_field_name = $extra['field_name'];
             $extra_field_value = $extra['field_value'];
             // save the external system's id into user_field_value table'
-            $res = UserManager::update_extra_field_value(
+            UserManager::update_extra_field_value(
                 $user_id,
                 $extra_field_name,
                 $extra_field_value

+ 9 - 1
main/webservices/webservice_user.php

@@ -418,7 +418,15 @@ class WSUser extends WS {
 		if($verifKey instanceof WSError) {
 			$this->handleError($verifKey);
 		} else {
-			$result = $this->editUserHelper($user_id_field_name, $user_id_value, $firstname, $lastname, $status, $loginname, $password, $encrypt_method, $email, $language, $phone, $expiration_date, $extras);
+
+			$extras_associative = array();
+			if (!empty($extras)) {
+				foreach($extras as $extra) {
+					$extras_associative[$extra['field_name']] = $extra['field_value'];
+				}
+			}
+
+			$result = $this->editUserHelper($user_id_field_name, $user_id_value, $firstname, $lastname, $status, $loginname, $password, $encrypt_method, $email, $language, $phone, $expiration_date, $extras_associative);
 			if($result instanceof WSError) {
 				$this->handleError($result);
 			}

+ 3 - 0
main/work/view.php

@@ -19,6 +19,9 @@ if ($work['active'] != 1) {
     api_not_allowed(true);
 }
 
+$work['title'] = isset($work['title']) ? Security::remove_XSS($work['title']) : '';
+$work['description'] = isset($work['description']) ? Security::remove_XSS($work['description']) : '';
+
 $interbreadcrumb[] = array ('url' => 'work.php?'.api_get_cidreq(), 'name' => get_lang('StudentPublications'));
 
 $my_folder_data = get_work_data_by_id($work['parent_id']);

+ 4 - 4
plugin/buycourses/lang/french.php

@@ -62,7 +62,7 @@ $strings['WaitingToReceiveThePayment'] = "En attente de réception du paiement";
 $strings['TheUserIsAlreadyRegisteredInTheSession'] = "Vous êtes déjà inscrit à cette session";
 $strings['ItemNotSaved'] = "Article non sauvé";
 $strings['TitlePlugin'] = "Tout le nécessaire pour enseigner et vendre des cours en ligne";
-$strings['PluginPresentation'] = "Le plugin BuyCourses vous donne les moyens nécessaires pour vendre vos cours ou vos sessions existants et d\'enseigner en ligne, en quelques étapes très simples. Qu\'attendez-vous? Commencez à vendre des cours grâce à Chamilo dès maintenant!";
+$strings['PluginPresentation'] = "Le plugin BuyCourses vous donne les moyens nécessaires pour vendre vos cours ou vos sessions existants et d'enseigner en ligne, en quelques étapes très simples. Qu'attendez-vous? Commencez à vendre des cours grâce à Chamilo dès maintenant!";
 $strings['Instructions'] = "Instructions d'utilisation";
 $strings['InstructionsStepOne'] = "Créer un cours ou une session de formation sur la plateforme.";
 $strings['InstructionsStepTwo'] = "Dans la section <strong>configuration de paiements</strong>, configurer le type de devise dans lequel vous souhaitez vendre vos cours et sessions";
@@ -119,9 +119,9 @@ $strings['ApiUsername'] = "Nom d'utilisateur de l'API";
 $strings['ApiPassword'] = "Mot de passe de l'API";
 $strings['ApiSignature'] = "Signature de l'API";
 $strings['InfoApiCredentials'] = "Pour générer vos données API pour intégrer votre compte PayPal à Chamilo, il vous faudra suivre les étapes suivantes";
-$strings['InfoApiStepOne'] = "Aller dans l\'option <strong>profil</strong> de PayPal, et ensuite dans <strong>Outils de vente</strong>";
-$strings['InfoApiStepTwo'] = "Dans la section <strong>API d\'accès</strong>, cloquer sur l\'option <strong>Mettre à jour</strong>";
-$strings['InfoApiStepThree'] = "Dans l\'option 2 de Configuration des données et permissions API, cliquer sur <strong>Voir signature API</strong>. Copier ces donées dans le formulaire de configuration de ce plugin";
+$strings['InfoApiStepOne'] = "Aller dans l'option <strong>profil</strong> de PayPal, et ensuite dans <strong>Outils de vente</strong>";
+$strings['InfoApiStepTwo'] = "Dans la section <strong>API d'accès</strong>, cloquer sur l'option <strong>Mettre à jour</strong>";
+$strings['InfoApiStepThree'] = "Dans l'option 2 de Configuration des données et permissions API, cliquer sur <strong>Voir signature API</strong>. Copier ces donées dans le formulaire de configuration de ce plugin";
 $strings['ErrorOccurred'] = "<strong>Une erreur est survenue</strong>. Code: %s. Message: %s. Veuillez contacter l'administrateur de la plateforme";
 $strings['VisibleInCatalog'] = "Visible dans le catalogue";
 $strings['Beneficiaries'] = "Bénéficiaires";

+ 674 - 0
plugin/pens/COPYING

@@ -0,0 +1,674 @@
+                    GNU GENERAL PUBLIC LICENSE
+                       Version 3, 29 June 2007
+
+ Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+                            Preamble
+
+  The GNU General Public License is a free, copyleft license for
+software and other kinds of works.
+
+  The licenses for most software and other practical works are designed
+to take away your freedom to share and change the works.  By contrast,
+the GNU General Public License is intended to guarantee your freedom to
+share and change all versions of a program--to make sure it remains free
+software for all its users.  We, the Free Software Foundation, use the
+GNU General Public License for most of our software; it applies also to
+any other work released this way by its authors.  You can apply it to
+your programs, too.
+
+  When we speak of free software, we are referring to freedom, not
+price.  Our General Public Licenses are designed to make sure that you
+have the freedom to distribute copies of free software (and charge for
+them if you wish), that you receive source code or can get it if you
+want it, that you can change the software or use pieces of it in new
+free programs, and that you know you can do these things.
+
+  To protect your rights, we need to prevent others from denying you
+these rights or asking you to surrender the rights.  Therefore, you have
+certain responsibilities if you distribute copies of the software, or if
+you modify it: responsibilities to respect the freedom of others.
+
+  For example, if you distribute copies of such a program, whether
+gratis or for a fee, you must pass on to the recipients the same
+freedoms that you received.  You must make sure that they, too, receive
+or can get the source code.  And you must show them these terms so they
+know their rights.
+
+  Developers that use the GNU GPL protect your rights with two steps:
+(1) assert copyright on the software, and (2) offer you this License
+giving you legal permission to copy, distribute and/or modify it.
+
+  For the developers' and authors' protection, the GPL clearly explains
+that there is no warranty for this free software.  For both users' and
+authors' sake, the GPL requires that modified versions be marked as
+changed, so that their problems will not be attributed erroneously to
+authors of previous versions.
+
+  Some devices are designed to deny users access to install or run
+modified versions of the software inside them, although the manufacturer
+can do so.  This is fundamentally incompatible with the aim of
+protecting users' freedom to change the software.  The systematic
+pattern of such abuse occurs in the area of products for individuals to
+use, which is precisely where it is most unacceptable.  Therefore, we
+have designed this version of the GPL to prohibit the practice for those
+products.  If such problems arise substantially in other domains, we
+stand ready to extend this provision to those domains in future versions
+of the GPL, as needed to protect the freedom of users.
+
+  Finally, every program is threatened constantly by software patents.
+States should not allow patents to restrict development and use of
+software on general-purpose computers, but in those that do, we wish to
+avoid the special danger that patents applied to a free program could
+make it effectively proprietary.  To prevent this, the GPL assures that
+patents cannot be used to render the program non-free.
+
+  The precise terms and conditions for copying, distribution and
+modification follow.
+
+                       TERMS AND CONDITIONS
+
+  0. Definitions.
+
+  "This License" refers to version 3 of the GNU General Public License.
+
+  "Copyright" also means copyright-like laws that apply to other kinds of
+works, such as semiconductor masks.
+
+  "The Program" refers to any copyrightable work licensed under this
+License.  Each licensee is addressed as "you".  "Licensees" and
+"recipients" may be individuals or organizations.
+
+  To "modify" a work means to copy from or adapt all or part of the work
+in a fashion requiring copyright permission, other than the making of an
+exact copy.  The resulting work is called a "modified version" of the
+earlier work or a work "based on" the earlier work.
+
+  A "covered work" means either the unmodified Program or a work based
+on the Program.
+
+  To "propagate" a work means to do anything with it that, without
+permission, would make you directly or secondarily liable for
+infringement under applicable copyright law, except executing it on a
+computer or modifying a private copy.  Propagation includes copying,
+distribution (with or without modification), making available to the
+public, and in some countries other activities as well.
+
+  To "convey" a work means any kind of propagation that enables other
+parties to make or receive copies.  Mere interaction with a user through
+a computer network, with no transfer of a copy, is not conveying.
+
+  An interactive user interface displays "Appropriate Legal Notices"
+to the extent that it includes a convenient and prominently visible
+feature that (1) displays an appropriate copyright notice, and (2)
+tells the user that there is no warranty for the work (except to the
+extent that warranties are provided), that licensees may convey the
+work under this License, and how to view a copy of this License.  If
+the interface presents a list of user commands or options, such as a
+menu, a prominent item in the list meets this criterion.
+
+  1. Source Code.
+
+  The "source code" for a work means the preferred form of the work
+for making modifications to it.  "Object code" means any non-source
+form of a work.
+
+  A "Standard Interface" means an interface that either is an official
+standard defined by a recognized standards body, or, in the case of
+interfaces specified for a particular programming language, one that
+is widely used among developers working in that language.
+
+  The "System Libraries" of an executable work include anything, other
+than the work as a whole, that (a) is included in the normal form of
+packaging a Major Component, but which is not part of that Major
+Component, and (b) serves only to enable use of the work with that
+Major Component, or to implement a Standard Interface for which an
+implementation is available to the public in source code form.  A
+"Major Component", in this context, means a major essential component
+(kernel, window system, and so on) of the specific operating system
+(if any) on which the executable work runs, or a compiler used to
+produce the work, or an object code interpreter used to run it.
+
+  The "Corresponding Source" for a work in object code form means all
+the source code needed to generate, install, and (for an executable
+work) run the object code and to modify the work, including scripts to
+control those activities.  However, it does not include the work's
+System Libraries, or general-purpose tools or generally available free
+programs which are used unmodified in performing those activities but
+which are not part of the work.  For example, Corresponding Source
+includes interface definition files associated with source files for
+the work, and the source code for shared libraries and dynamically
+linked subprograms that the work is specifically designed to require,
+such as by intimate data communication or control flow between those
+subprograms and other parts of the work.
+
+  The Corresponding Source need not include anything that users
+can regenerate automatically from other parts of the Corresponding
+Source.
+
+  The Corresponding Source for a work in source code form is that
+same work.
+
+  2. Basic Permissions.
+
+  All rights granted under this License are granted for the term of
+copyright on the Program, and are irrevocable provided the stated
+conditions are met.  This License explicitly affirms your unlimited
+permission to run the unmodified Program.  The output from running a
+covered work is covered by this License only if the output, given its
+content, constitutes a covered work.  This License acknowledges your
+rights of fair use or other equivalent, as provided by copyright law.
+
+  You may make, run and propagate covered works that you do not
+convey, without conditions so long as your license otherwise remains
+in force.  You may convey covered works to others for the sole purpose
+of having them make modifications exclusively for you, or provide you
+with facilities for running those works, provided that you comply with
+the terms of this License in conveying all material for which you do
+not control copyright.  Those thus making or running the covered works
+for you must do so exclusively on your behalf, under your direction
+and control, on terms that prohibit them from making any copies of
+your copyrighted material outside their relationship with you.
+
+  Conveying under any other circumstances is permitted solely under
+the conditions stated below.  Sublicensing is not allowed; section 10
+makes it unnecessary.
+
+  3. Protecting Users' Legal Rights From Anti-Circumvention Law.
+
+  No covered work shall be deemed part of an effective technological
+measure under any applicable law fulfilling obligations under article
+11 of the WIPO copyright treaty adopted on 20 December 1996, or
+similar laws prohibiting or restricting circumvention of such
+measures.
+
+  When you convey a covered work, you waive any legal power to forbid
+circumvention of technological measures to the extent such circumvention
+is effected by exercising rights under this License with respect to
+the covered work, and you disclaim any intention to limit operation or
+modification of the work as a means of enforcing, against the work's
+users, your or third parties' legal rights to forbid circumvention of
+technological measures.
+
+  4. Conveying Verbatim Copies.
+
+  You may convey verbatim copies of the Program's source code as you
+receive it, in any medium, provided that you conspicuously and
+appropriately publish on each copy an appropriate copyright notice;
+keep intact all notices stating that this License and any
+non-permissive terms added in accord with section 7 apply to the code;
+keep intact all notices of the absence of any warranty; and give all
+recipients a copy of this License along with the Program.
+
+  You may charge any price or no price for each copy that you convey,
+and you may offer support or warranty protection for a fee.
+
+  5. Conveying Modified Source Versions.
+
+  You may convey a work based on the Program, or the modifications to
+produce it from the Program, in the form of source code under the
+terms of section 4, provided that you also meet all of these conditions:
+
+    a) The work must carry prominent notices stating that you modified
+    it, and giving a relevant date.
+
+    b) The work must carry prominent notices stating that it is
+    released under this License and any conditions added under section
+    7.  This requirement modifies the requirement in section 4 to
+    "keep intact all notices".
+
+    c) You must license the entire work, as a whole, under this
+    License to anyone who comes into possession of a copy.  This
+    License will therefore apply, along with any applicable section 7
+    additional terms, to the whole of the work, and all its parts,
+    regardless of how they are packaged.  This License gives no
+    permission to license the work in any other way, but it does not
+    invalidate such permission if you have separately received it.
+
+    d) If the work has interactive user interfaces, each must display
+    Appropriate Legal Notices; however, if the Program has interactive
+    interfaces that do not display Appropriate Legal Notices, your
+    work need not make them do so.
+
+  A compilation of a covered work with other separate and independent
+works, which are not by their nature extensions of the covered work,
+and which are not combined with it such as to form a larger program,
+in or on a volume of a storage or distribution medium, is called an
+"aggregate" if the compilation and its resulting copyright are not
+used to limit the access or legal rights of the compilation's users
+beyond what the individual works permit.  Inclusion of a covered work
+in an aggregate does not cause this License to apply to the other
+parts of the aggregate.
+
+  6. Conveying Non-Source Forms.
+
+  You may convey a covered work in object code form under the terms
+of sections 4 and 5, provided that you also convey the
+machine-readable Corresponding Source under the terms of this License,
+in one of these ways:
+
+    a) Convey the object code in, or embodied in, a physical product
+    (including a physical distribution medium), accompanied by the
+    Corresponding Source fixed on a durable physical medium
+    customarily used for software interchange.
+
+    b) Convey the object code in, or embodied in, a physical product
+    (including a physical distribution medium), accompanied by a
+    written offer, valid for at least three years and valid for as
+    long as you offer spare parts or customer support for that product
+    model, to give anyone who possesses the object code either (1) a
+    copy of the Corresponding Source for all the software in the
+    product that is covered by this License, on a durable physical
+    medium customarily used for software interchange, for a price no
+    more than your reasonable cost of physically performing this
+    conveying of source, or (2) access to copy the
+    Corresponding Source from a network server at no charge.
+
+    c) Convey individual copies of the object code with a copy of the
+    written offer to provide the Corresponding Source.  This
+    alternative is allowed only occasionally and noncommercially, and
+    only if you received the object code with such an offer, in accord
+    with subsection 6b.
+
+    d) Convey the object code by offering access from a designated
+    place (gratis or for a charge), and offer equivalent access to the
+    Corresponding Source in the same way through the same place at no
+    further charge.  You need not require recipients to copy the
+    Corresponding Source along with the object code.  If the place to
+    copy the object code is a network server, the Corresponding Source
+    may be on a different server (operated by you or a third party)
+    that supports equivalent copying facilities, provided you maintain
+    clear directions next to the object code saying where to find the
+    Corresponding Source.  Regardless of what server hosts the
+    Corresponding Source, you remain obligated to ensure that it is
+    available for as long as needed to satisfy these requirements.
+
+    e) Convey the object code using peer-to-peer transmission, provided
+    you inform other peers where the object code and Corresponding
+    Source of the work are being offered to the general public at no
+    charge under subsection 6d.
+
+  A separable portion of the object code, whose source code is excluded
+from the Corresponding Source as a System Library, need not be
+included in conveying the object code work.
+
+  A "User Product" is either (1) a "consumer product", which means any
+tangible personal property which is normally used for personal, family,
+or household purposes, or (2) anything designed or sold for incorporation
+into a dwelling.  In determining whether a product is a consumer product,
+doubtful cases shall be resolved in favor of coverage.  For a particular
+product received by a particular user, "normally used" refers to a
+typical or common use of that class of product, regardless of the status
+of the particular user or of the way in which the particular user
+actually uses, or expects or is expected to use, the product.  A product
+is a consumer product regardless of whether the product has substantial
+commercial, industrial or non-consumer uses, unless such uses represent
+the only significant mode of use of the product.
+
+  "Installation Information" for a User Product means any methods,
+procedures, authorization keys, or other information required to install
+and execute modified versions of a covered work in that User Product from
+a modified version of its Corresponding Source.  The information must
+suffice to ensure that the continued functioning of the modified object
+code is in no case prevented or interfered with solely because
+modification has been made.
+
+  If you convey an object code work under this section in, or with, or
+specifically for use in, a User Product, and the conveying occurs as
+part of a transaction in which the right of possession and use of the
+User Product is transferred to the recipient in perpetuity or for a
+fixed term (regardless of how the transaction is characterized), the
+Corresponding Source conveyed under this section must be accompanied
+by the Installation Information.  But this requirement does not apply
+if neither you nor any third party retains the ability to install
+modified object code on the User Product (for example, the work has
+been installed in ROM).
+
+  The requirement to provide Installation Information does not include a
+requirement to continue to provide support service, warranty, or updates
+for a work that has been modified or installed by the recipient, or for
+the User Product in which it has been modified or installed.  Access to a
+network may be denied when the modification itself materially and
+adversely affects the operation of the network or violates the rules and
+protocols for communication across the network.
+
+  Corresponding Source conveyed, and Installation Information provided,
+in accord with this section must be in a format that is publicly
+documented (and with an implementation available to the public in
+source code form), and must require no special password or key for
+unpacking, reading or copying.
+
+  7. Additional Terms.
+
+  "Additional permissions" are terms that supplement the terms of this
+License by making exceptions from one or more of its conditions.
+Additional permissions that are applicable to the entire Program shall
+be treated as though they were included in this License, to the extent
+that they are valid under applicable law.  If additional permissions
+apply only to part of the Program, that part may be used separately
+under those permissions, but the entire Program remains governed by
+this License without regard to the additional permissions.
+
+  When you convey a copy of a covered work, you may at your option
+remove any additional permissions from that copy, or from any part of
+it.  (Additional permissions may be written to require their own
+removal in certain cases when you modify the work.)  You may place
+additional permissions on material, added by you to a covered work,
+for which you have or can give appropriate copyright permission.
+
+  Notwithstanding any other provision of this License, for material you
+add to a covered work, you may (if authorized by the copyright holders of
+that material) supplement the terms of this License with terms:
+
+    a) Disclaiming warranty or limiting liability differently from the
+    terms of sections 15 and 16 of this License; or
+
+    b) Requiring preservation of specified reasonable legal notices or
+    author attributions in that material or in the Appropriate Legal
+    Notices displayed by works containing it; or
+
+    c) Prohibiting misrepresentation of the origin of that material, or
+    requiring that modified versions of such material be marked in
+    reasonable ways as different from the original version; or
+
+    d) Limiting the use for publicity purposes of names of licensors or
+    authors of the material; or
+
+    e) Declining to grant rights under trademark law for use of some
+    trade names, trademarks, or service marks; or
+
+    f) Requiring indemnification of licensors and authors of that
+    material by anyone who conveys the material (or modified versions of
+    it) with contractual assumptions of liability to the recipient, for
+    any liability that these contractual assumptions directly impose on
+    those licensors and authors.
+
+  All other non-permissive additional terms are considered "further
+restrictions" within the meaning of section 10.  If the Program as you
+received it, or any part of it, contains a notice stating that it is
+governed by this License along with a term that is a further
+restriction, you may remove that term.  If a license document contains
+a further restriction but permits relicensing or conveying under this
+License, you may add to a covered work material governed by the terms
+of that license document, provided that the further restriction does
+not survive such relicensing or conveying.
+
+  If you add terms to a covered work in accord with this section, you
+must place, in the relevant source files, a statement of the
+additional terms that apply to those files, or a notice indicating
+where to find the applicable terms.
+
+  Additional terms, permissive or non-permissive, may be stated in the
+form of a separately written license, or stated as exceptions;
+the above requirements apply either way.
+
+  8. Termination.
+
+  You may not propagate or modify a covered work except as expressly
+provided under this License.  Any attempt otherwise to propagate or
+modify it is void, and will automatically terminate your rights under
+this License (including any patent licenses granted under the third
+paragraph of section 11).
+
+  However, if you cease all violation of this License, then your
+license from a particular copyright holder is reinstated (a)
+provisionally, unless and until the copyright holder explicitly and
+finally terminates your license, and (b) permanently, if the copyright
+holder fails to notify you of the violation by some reasonable means
+prior to 60 days after the cessation.
+
+  Moreover, your license from a particular copyright holder is
+reinstated permanently if the copyright holder notifies you of the
+violation by some reasonable means, this is the first time you have
+received notice of violation of this License (for any work) from that
+copyright holder, and you cure the violation prior to 30 days after
+your receipt of the notice.
+
+  Termination of your rights under this section does not terminate the
+licenses of parties who have received copies or rights from you under
+this License.  If your rights have been terminated and not permanently
+reinstated, you do not qualify to receive new licenses for the same
+material under section 10.
+
+  9. Acceptance Not Required for Having Copies.
+
+  You are not required to accept this License in order to receive or
+run a copy of the Program.  Ancillary propagation of a covered work
+occurring solely as a consequence of using peer-to-peer transmission
+to receive a copy likewise does not require acceptance.  However,
+nothing other than this License grants you permission to propagate or
+modify any covered work.  These actions infringe copyright if you do
+not accept this License.  Therefore, by modifying or propagating a
+covered work, you indicate your acceptance of this License to do so.
+
+  10. Automatic Licensing of Downstream Recipients.
+
+  Each time you convey a covered work, the recipient automatically
+receives a license from the original licensors, to run, modify and
+propagate that work, subject to this License.  You are not responsible
+for enforcing compliance by third parties with this License.
+
+  An "entity transaction" is a transaction transferring control of an
+organization, or substantially all assets of one, or subdividing an
+organization, or merging organizations.  If propagation of a covered
+work results from an entity transaction, each party to that
+transaction who receives a copy of the work also receives whatever
+licenses to the work the party's predecessor in interest had or could
+give under the previous paragraph, plus a right to possession of the
+Corresponding Source of the work from the predecessor in interest, if
+the predecessor has it or can get it with reasonable efforts.
+
+  You may not impose any further restrictions on the exercise of the
+rights granted or affirmed under this License.  For example, you may
+not impose a license fee, royalty, or other charge for exercise of
+rights granted under this License, and you may not initiate litigation
+(including a cross-claim or counterclaim in a lawsuit) alleging that
+any patent claim is infringed by making, using, selling, offering for
+sale, or importing the Program or any portion of it.
+
+  11. Patents.
+
+  A "contributor" is a copyright holder who authorizes use under this
+License of the Program or a work on which the Program is based.  The
+work thus licensed is called the contributor's "contributor version".
+
+  A contributor's "essential patent claims" are all patent claims
+owned or controlled by the contributor, whether already acquired or
+hereafter acquired, that would be infringed by some manner, permitted
+by this License, of making, using, or selling its contributor version,
+but do not include claims that would be infringed only as a
+consequence of further modification of the contributor version.  For
+purposes of this definition, "control" includes the right to grant
+patent sublicenses in a manner consistent with the requirements of
+this License.
+
+  Each contributor grants you a non-exclusive, worldwide, royalty-free
+patent license under the contributor's essential patent claims, to
+make, use, sell, offer for sale, import and otherwise run, modify and
+propagate the contents of its contributor version.
+
+  In the following three paragraphs, a "patent license" is any express
+agreement or commitment, however denominated, not to enforce a patent
+(such as an express permission to practice a patent or covenant not to
+sue for patent infringement).  To "grant" such a patent license to a
+party means to make such an agreement or commitment not to enforce a
+patent against the party.
+
+  If you convey a covered work, knowingly relying on a patent license,
+and the Corresponding Source of the work is not available for anyone
+to copy, free of charge and under the terms of this License, through a
+publicly available network server or other readily accessible means,
+then you must either (1) cause the Corresponding Source to be so
+available, or (2) arrange to deprive yourself of the benefit of the
+patent license for this particular work, or (3) arrange, in a manner
+consistent with the requirements of this License, to extend the patent
+license to downstream recipients.  "Knowingly relying" means you have
+actual knowledge that, but for the patent license, your conveying the
+covered work in a country, or your recipient's use of the covered work
+in a country, would infringe one or more identifiable patents in that
+country that you have reason to believe are valid.
+
+  If, pursuant to or in connection with a single transaction or
+arrangement, you convey, or propagate by procuring conveyance of, a
+covered work, and grant a patent license to some of the parties
+receiving the covered work authorizing them to use, propagate, modify
+or convey a specific copy of the covered work, then the patent license
+you grant is automatically extended to all recipients of the covered
+work and works based on it.
+
+  A patent license is "discriminatory" if it does not include within
+the scope of its coverage, prohibits the exercise of, or is
+conditioned on the non-exercise of one or more of the rights that are
+specifically granted under this License.  You may not convey a covered
+work if you are a party to an arrangement with a third party that is
+in the business of distributing software, under which you make payment
+to the third party based on the extent of your activity of conveying
+the work, and under which the third party grants, to any of the
+parties who would receive the covered work from you, a discriminatory
+patent license (a) in connection with copies of the covered work
+conveyed by you (or copies made from those copies), or (b) primarily
+for and in connection with specific products or compilations that
+contain the covered work, unless you entered into that arrangement,
+or that patent license was granted, prior to 28 March 2007.
+
+  Nothing in this License shall be construed as excluding or limiting
+any implied license or other defenses to infringement that may
+otherwise be available to you under applicable patent law.
+
+  12. No Surrender of Others' Freedom.
+
+  If conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License.  If you cannot convey a
+covered work so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you may
+not convey it at all.  For example, if you agree to terms that obligate you
+to collect a royalty for further conveying from those to whom you convey
+the Program, the only way you could satisfy both those terms and this
+License would be to refrain entirely from conveying the Program.
+
+  13. Use with the GNU Affero General Public License.
+
+  Notwithstanding any other provision of this License, you have
+permission to link or combine any covered work with a work licensed
+under version 3 of the GNU Affero General Public License into a single
+combined work, and to convey the resulting work.  The terms of this
+License will continue to apply to the part which is the covered work,
+but the special requirements of the GNU Affero General Public License,
+section 13, concerning interaction through a network will apply to the
+combination as such.
+
+  14. Revised Versions of this License.
+
+  The Free Software Foundation may publish revised and/or new versions of
+the GNU General Public License from time to time.  Such new versions will
+be similar in spirit to the present version, but may differ in detail to
+address new problems or concerns.
+
+  Each version is given a distinguishing version number.  If the
+Program specifies that a certain numbered version of the GNU General
+Public License "or any later version" applies to it, you have the
+option of following the terms and conditions either of that numbered
+version or of any later version published by the Free Software
+Foundation.  If the Program does not specify a version number of the
+GNU General Public License, you may choose any version ever published
+by the Free Software Foundation.
+
+  If the Program specifies that a proxy can decide which future
+versions of the GNU General Public License can be used, that proxy's
+public statement of acceptance of a version permanently authorizes you
+to choose that version for the Program.
+
+  Later license versions may give you additional or different
+permissions.  However, no additional obligations are imposed on any
+author or copyright holder as a result of your choosing to follow a
+later version.
+
+  15. Disclaimer of Warranty.
+
+  THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
+APPLICABLE LAW.  EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
+HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
+OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
+THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+PURPOSE.  THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
+IS WITH YOU.  SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
+ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
+
+  16. Limitation of Liability.
+
+  IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
+THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
+GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
+USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
+DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
+PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
+EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
+SUCH DAMAGES.
+
+  17. Interpretation of Sections 15 and 16.
+
+  If the disclaimer of warranty and limitation of liability provided
+above cannot be given local legal effect according to their terms,
+reviewing courts shall apply local law that most closely approximates
+an absolute waiver of all civil liability in connection with the
+Program, unless a warranty or assumption of liability accompanies a
+copy of the Program in return for a fee.
+
+                     END OF TERMS AND CONDITIONS
+
+            How to Apply These Terms to Your New Programs
+
+  If you develop a new program, and you want it to be of the greatest
+possible use to the public, the best way to achieve this is to make it
+free software which everyone can redistribute and change under these terms.
+
+  To do so, attach the following notices to the program.  It is safest
+to attach them to the start of each source file to most effectively
+state the exclusion of warranty; and each file should have at least
+the "copyright" line and a pointer to where the full notice is found.
+
+    <one line to give the program's name and a brief idea of what it does.>
+    Copyright (C) <year>  <name of author>
+
+    This program is free software: you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation, either version 3 of the License, or
+    (at your option) any later version.
+
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU General Public License for more details.
+
+    You should have received a copy of the GNU General Public License
+    along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+Also add information on how to contact you by electronic and paper mail.
+
+  If the program does terminal interaction, make it output a short
+notice like this when it starts in an interactive mode:
+
+    <program>  Copyright (C) <year>  <name of author>
+    This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
+    This is free software, and you are welcome to redistribute it
+    under certain conditions; type `show c' for details.
+
+The hypothetical commands `show w' and `show c' should show the appropriate
+parts of the General Public License.  Of course, your program's commands
+might be different; for a GUI interface, you would use an "about box".
+
+  You should also get your employer (if you work as a programmer) or school,
+if any, to sign a "copyright disclaimer" for the program, if necessary.
+For more information on this, and how to apply and follow the GNU GPL, see
+<http://www.gnu.org/licenses/>.
+
+  The GNU General Public License does not permit incorporating your program
+into proprietary programs.  If your program is a subroutine library, you
+may consider it more useful to permit linking proprietary applications with
+the library.  If this is what you want to do, use the GNU Lesser General
+Public License instead of this License.  But first, please read
+<http://www.gnu.org/philosophy/why-not-lgpl.html>.

+ 248 - 0
plugin/pens/chamilo_pens.php

@@ -0,0 +1,248 @@
+<?php
+/**
+ * This file is part of chamilo-pens.
+ * 
+ * chamilo-pens is free software: you can redistribute it and/or modify 
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ * 
+ * chamilo-pens is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ * 
+ * You should have received a copy of the GNU General Public License
+ * along with chamilo-pens.  If not, see <http://www.gnu.org/licenses/>.
+ */
+ 
+ /**
+ * ChamiloPens
+ * 
+ * This file provides the ChamiloPens class
+ * 
+ * @author Guillaume Viguier-Just <guillaume@viguierjust.com>
+ * @licence http://www.gnu.org/licenses/gpl.txt
+ */
+
+require_once __DIR__ . '/../../main/inc/global.inc.php';
+require_once __DIR__ . '/lib/pens.php';
+
+/**
+ * ChamiloPens
+ * 
+ * Model class that stores a PENS request made to a Chamilo server into the database
+ * 
+ * @author Guillaume Viguier-Just <guillaume@viguierjust.com>
+ * @licence http://www.gnu.org/licenses/gpl.txt
+ */
+class ChamiloPens extends Plugin {
+	/**
+	 * Database table to be used
+	 */
+	const TABLE_NAME = "plugin_pens";
+	
+	/**
+	 * Id of the object
+	 * @var int
+	 */
+	protected $_id = null;
+	
+	/**
+	 * PENS Version to be used. Currently, the only valid value is 1.0.0. Required.
+	 * @var string
+	 */
+	protected $_pens_version = null;
+	
+	/**
+	 * Package type being used. The only valid values are aicc-pkg, scorm-pif, ims-qti. Required
+	 * @var string
+	 */
+	protected $_package_type = null;
+	
+	/**
+	 * Package type version. Required
+	 * @var string
+	 */
+	protected $_package_type_version = null;
+	
+	/**
+	 * Package format. The only valid values are zip, url, jar, war and xml. Required
+	 * @var string
+	 */
+	protected $_package_format = null;
+	
+	/**
+	 * Package id. Requires a valid URI according to RFC 2396. Required
+	 * @var string
+	 */
+	protected $_package_id = null;
+	
+	/**
+	 * Name or ID for client submitting the content package to the target system. Required.
+	 * @var string
+	 */
+	protected $_client = null;
+	
+	/**
+	 * Unstructured character string that may be used to transfer vendor-specific data such as processing hints or deployment information. Optional.
+	 * @var string
+	 */
+	protected $_vendor_data = null;
+	
+	/**
+	 * Package name
+	 * @var string
+	 */
+	protected $_package_name = null;
+	
+	/**
+	 * Date of creation
+	 * @var DateTime
+	 */
+	protected $_created_at = null;
+	
+	/**
+	 * Update date
+	 * @var DateTime
+	 */
+	protected $_updated_at = null;
+	
+	/**
+	 * Constructor. Takes a PENSRequest as an argument
+	 * 
+	 * @param PENSRequest Request
+	 */
+	public function __construct($request) {
+		if($request instanceof PENSRequest) {
+			$this->_id = 0;
+			$this->_pens_version = $request->getPensVersion();
+			$this->_package_type = $request->getPackageType();
+			$this->_package_type_version = $request->getPackageTypeVersion();
+			$this->_package_format = $request->getPackageFormat();
+			$this->_package_id = $request->getPackageId();
+			$this->_client = $request->getClient();
+			$this->_vendor_data = $request->getVendorData();
+			$this->_package_name = $request->getFilename();
+		} else if(is_array($request)) {
+			$this->_id = $request['id'];
+			$this->_pens_version = $request['pens_version'];
+			$this->_package_type = $request['package_type'];
+			$this->_package_type_version = $request['package_type_version'];
+			$this->_package_format = $request['package_format'];
+			$this->_package_id = $request['package_id'];
+			$this->_client = $request['client'];
+			$this->_vendor_data = $request['vendor_data'];
+			$this->_package_name = $request['package_name'];
+			$this->_created_at = new DateTime($request['created_at'], new DateTimeZone('UTC'));
+			if(!empty($request['updated_at'])) {
+				$this->_updated_at = new DateTime($request['updated_at'], new DateTimeZone('UTC'));
+			}
+		}
+	}
+	
+	/**
+	 * Saves the object in the DB
+	 */
+	public function save() {
+		$clean_package_type_version = Database::escape_string($this->_package_type_version);
+		$clean_package_id = Database::escape_string($this->_package_id);
+		$clean_client = Database::escape_string($this->_client);
+		$clean_vendor_data = Database::escape_string($this->_vendor_data);
+		$created_at = api_get_utc_datetime();
+		$table = Database::get_main_table(ChamiloPens::TABLE_NAME);
+		$sql_query = "INSERT INTO $table (pens_version, package_type, package_type_version, package_format, package_id, client, vendor_data, package_name, created_at) VALUES (" .
+			"'". $this->_pens_version. "', " .
+			"'".$this->_package_type. "', " .
+			"'".$clean_package_type_version. "', " .
+			"'".$this->_package_format. "', " .
+			"'".$clean_package_id. "', " .
+			"'".$clean_client."', " .
+			"'".$clean_vendor_data."', " .
+			"'".$this->_package_name."', " .
+			"'".$created_at."') ON DUPLICATE KEY UPDATE " .
+			"pens_version = VALUES(pens_version), ".
+			"package_type = VALUES(package_type), ".
+			"package_type_version = VALUES(package_type_version), ".
+			"package_format = VALUES(package_format), ".
+			"client = VALUES(client), ".
+			"vendor_data = VALUES(vendor_data), ".
+			"package_name = VALUES(package_name), ".
+			"updated_at = '".$created_at."';";
+		Database::query($sql_query);
+	}
+	
+	/**
+	 * Returns a ChamiloPens object, based on package id
+	 * 
+	 * @param string Package id
+	 */
+	public static function findByPackageId($package_id) {
+		$table = Database::get_main_table(ChamiloPens::TABLE_NAME);
+		$sql_query = "SELECT * FROM $table WHERE package_id = '".$package_id."';";
+		$results = Database::query($sql_query);
+		$number = Database::num_rows($results);
+		if($number == 1) {
+			$obj = Database::fetch_assoc($results);
+			return new ChamiloPens($obj);
+		} else {
+			return null;
+		}
+	}
+	
+	/**
+	 * Returns an array of all the objects of the DB
+	 * 
+	 * @return array Array of ChamiloPens objects
+	 */
+	public static function findAll() {
+		$table = Database::get_main_table(ChamiloPens::TABLE_NAME);
+		$sql_query = "SELECT * FROM $table ORDER BY created_at;";
+		$results = Database::query($sql_query);
+		$return = array();
+		while($assoc = Database::fetch_assoc($results)) {
+			$return[] = new ChamiloPens($assoc);
+		}
+		return $return;
+	}
+	
+	public function getId() {
+		return $this->_id;
+	}
+	
+	public function getPensVersion() {
+		return $this->_pens_version;
+	}
+	
+	public function getPackageType() {
+		return $this->_package_type;
+	}
+	
+	public function getPackageTypeVersion() {
+		return $this->_package_type_version;
+	}
+	
+	public function getPackageFormat() {
+		return $this->_package_format;
+	}
+	
+	public function getPackageId() {
+		return $this->_package_id;
+	}
+	
+	public function getClient() {
+		return $this->_client;
+	}
+	
+	public function getVendorData() {
+		return $this->_vendor_data;
+	}
+	
+	public function getPackageName() {
+		return $this->_package_name;
+	}
+	
+	public function getCreatedAt() {
+		return $this->_created_at;
+	}
+}

+ 674 - 0
plugin/pens/lib/COPYING

@@ -0,0 +1,674 @@
+                    GNU GENERAL PUBLIC LICENSE
+                       Version 3, 29 June 2007
+
+ Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+                            Preamble
+
+  The GNU General Public License is a free, copyleft license for
+software and other kinds of works.
+
+  The licenses for most software and other practical works are designed
+to take away your freedom to share and change the works.  By contrast,
+the GNU General Public License is intended to guarantee your freedom to
+share and change all versions of a program--to make sure it remains free
+software for all its users.  We, the Free Software Foundation, use the
+GNU General Public License for most of our software; it applies also to
+any other work released this way by its authors.  You can apply it to
+your programs, too.
+
+  When we speak of free software, we are referring to freedom, not
+price.  Our General Public Licenses are designed to make sure that you
+have the freedom to distribute copies of free software (and charge for
+them if you wish), that you receive source code or can get it if you
+want it, that you can change the software or use pieces of it in new
+free programs, and that you know you can do these things.
+
+  To protect your rights, we need to prevent others from denying you
+these rights or asking you to surrender the rights.  Therefore, you have
+certain responsibilities if you distribute copies of the software, or if
+you modify it: responsibilities to respect the freedom of others.
+
+  For example, if you distribute copies of such a program, whether
+gratis or for a fee, you must pass on to the recipients the same
+freedoms that you received.  You must make sure that they, too, receive
+or can get the source code.  And you must show them these terms so they
+know their rights.
+
+  Developers that use the GNU GPL protect your rights with two steps:
+(1) assert copyright on the software, and (2) offer you this License
+giving you legal permission to copy, distribute and/or modify it.
+
+  For the developers' and authors' protection, the GPL clearly explains
+that there is no warranty for this free software.  For both users' and
+authors' sake, the GPL requires that modified versions be marked as
+changed, so that their problems will not be attributed erroneously to
+authors of previous versions.
+
+  Some devices are designed to deny users access to install or run
+modified versions of the software inside them, although the manufacturer
+can do so.  This is fundamentally incompatible with the aim of
+protecting users' freedom to change the software.  The systematic
+pattern of such abuse occurs in the area of products for individuals to
+use, which is precisely where it is most unacceptable.  Therefore, we
+have designed this version of the GPL to prohibit the practice for those
+products.  If such problems arise substantially in other domains, we
+stand ready to extend this provision to those domains in future versions
+of the GPL, as needed to protect the freedom of users.
+
+  Finally, every program is threatened constantly by software patents.
+States should not allow patents to restrict development and use of
+software on general-purpose computers, but in those that do, we wish to
+avoid the special danger that patents applied to a free program could
+make it effectively proprietary.  To prevent this, the GPL assures that
+patents cannot be used to render the program non-free.
+
+  The precise terms and conditions for copying, distribution and
+modification follow.
+
+                       TERMS AND CONDITIONS
+
+  0. Definitions.
+
+  "This License" refers to version 3 of the GNU General Public License.
+
+  "Copyright" also means copyright-like laws that apply to other kinds of
+works, such as semiconductor masks.
+
+  "The Program" refers to any copyrightable work licensed under this
+License.  Each licensee is addressed as "you".  "Licensees" and
+"recipients" may be individuals or organizations.
+
+  To "modify" a work means to copy from or adapt all or part of the work
+in a fashion requiring copyright permission, other than the making of an
+exact copy.  The resulting work is called a "modified version" of the
+earlier work or a work "based on" the earlier work.
+
+  A "covered work" means either the unmodified Program or a work based
+on the Program.
+
+  To "propagate" a work means to do anything with it that, without
+permission, would make you directly or secondarily liable for
+infringement under applicable copyright law, except executing it on a
+computer or modifying a private copy.  Propagation includes copying,
+distribution (with or without modification), making available to the
+public, and in some countries other activities as well.
+
+  To "convey" a work means any kind of propagation that enables other
+parties to make or receive copies.  Mere interaction with a user through
+a computer network, with no transfer of a copy, is not conveying.
+
+  An interactive user interface displays "Appropriate Legal Notices"
+to the extent that it includes a convenient and prominently visible
+feature that (1) displays an appropriate copyright notice, and (2)
+tells the user that there is no warranty for the work (except to the
+extent that warranties are provided), that licensees may convey the
+work under this License, and how to view a copy of this License.  If
+the interface presents a list of user commands or options, such as a
+menu, a prominent item in the list meets this criterion.
+
+  1. Source Code.
+
+  The "source code" for a work means the preferred form of the work
+for making modifications to it.  "Object code" means any non-source
+form of a work.
+
+  A "Standard Interface" means an interface that either is an official
+standard defined by a recognized standards body, or, in the case of
+interfaces specified for a particular programming language, one that
+is widely used among developers working in that language.
+
+  The "System Libraries" of an executable work include anything, other
+than the work as a whole, that (a) is included in the normal form of
+packaging a Major Component, but which is not part of that Major
+Component, and (b) serves only to enable use of the work with that
+Major Component, or to implement a Standard Interface for which an
+implementation is available to the public in source code form.  A
+"Major Component", in this context, means a major essential component
+(kernel, window system, and so on) of the specific operating system
+(if any) on which the executable work runs, or a compiler used to
+produce the work, or an object code interpreter used to run it.
+
+  The "Corresponding Source" for a work in object code form means all
+the source code needed to generate, install, and (for an executable
+work) run the object code and to modify the work, including scripts to
+control those activities.  However, it does not include the work's
+System Libraries, or general-purpose tools or generally available free
+programs which are used unmodified in performing those activities but
+which are not part of the work.  For example, Corresponding Source
+includes interface definition files associated with source files for
+the work, and the source code for shared libraries and dynamically
+linked subprograms that the work is specifically designed to require,
+such as by intimate data communication or control flow between those
+subprograms and other parts of the work.
+
+  The Corresponding Source need not include anything that users
+can regenerate automatically from other parts of the Corresponding
+Source.
+
+  The Corresponding Source for a work in source code form is that
+same work.
+
+  2. Basic Permissions.
+
+  All rights granted under this License are granted for the term of
+copyright on the Program, and are irrevocable provided the stated
+conditions are met.  This License explicitly affirms your unlimited
+permission to run the unmodified Program.  The output from running a
+covered work is covered by this License only if the output, given its
+content, constitutes a covered work.  This License acknowledges your
+rights of fair use or other equivalent, as provided by copyright law.
+
+  You may make, run and propagate covered works that you do not
+convey, without conditions so long as your license otherwise remains
+in force.  You may convey covered works to others for the sole purpose
+of having them make modifications exclusively for you, or provide you
+with facilities for running those works, provided that you comply with
+the terms of this License in conveying all material for which you do
+not control copyright.  Those thus making or running the covered works
+for you must do so exclusively on your behalf, under your direction
+and control, on terms that prohibit them from making any copies of
+your copyrighted material outside their relationship with you.
+
+  Conveying under any other circumstances is permitted solely under
+the conditions stated below.  Sublicensing is not allowed; section 10
+makes it unnecessary.
+
+  3. Protecting Users' Legal Rights From Anti-Circumvention Law.
+
+  No covered work shall be deemed part of an effective technological
+measure under any applicable law fulfilling obligations under article
+11 of the WIPO copyright treaty adopted on 20 December 1996, or
+similar laws prohibiting or restricting circumvention of such
+measures.
+
+  When you convey a covered work, you waive any legal power to forbid
+circumvention of technological measures to the extent such circumvention
+is effected by exercising rights under this License with respect to
+the covered work, and you disclaim any intention to limit operation or
+modification of the work as a means of enforcing, against the work's
+users, your or third parties' legal rights to forbid circumvention of
+technological measures.
+
+  4. Conveying Verbatim Copies.
+
+  You may convey verbatim copies of the Program's source code as you
+receive it, in any medium, provided that you conspicuously and
+appropriately publish on each copy an appropriate copyright notice;
+keep intact all notices stating that this License and any
+non-permissive terms added in accord with section 7 apply to the code;
+keep intact all notices of the absence of any warranty; and give all
+recipients a copy of this License along with the Program.
+
+  You may charge any price or no price for each copy that you convey,
+and you may offer support or warranty protection for a fee.
+
+  5. Conveying Modified Source Versions.
+
+  You may convey a work based on the Program, or the modifications to
+produce it from the Program, in the form of source code under the
+terms of section 4, provided that you also meet all of these conditions:
+
+    a) The work must carry prominent notices stating that you modified
+    it, and giving a relevant date.
+
+    b) The work must carry prominent notices stating that it is
+    released under this License and any conditions added under section
+    7.  This requirement modifies the requirement in section 4 to
+    "keep intact all notices".
+
+    c) You must license the entire work, as a whole, under this
+    License to anyone who comes into possession of a copy.  This
+    License will therefore apply, along with any applicable section 7
+    additional terms, to the whole of the work, and all its parts,
+    regardless of how they are packaged.  This License gives no
+    permission to license the work in any other way, but it does not
+    invalidate such permission if you have separately received it.
+
+    d) If the work has interactive user interfaces, each must display
+    Appropriate Legal Notices; however, if the Program has interactive
+    interfaces that do not display Appropriate Legal Notices, your
+    work need not make them do so.
+
+  A compilation of a covered work with other separate and independent
+works, which are not by their nature extensions of the covered work,
+and which are not combined with it such as to form a larger program,
+in or on a volume of a storage or distribution medium, is called an
+"aggregate" if the compilation and its resulting copyright are not
+used to limit the access or legal rights of the compilation's users
+beyond what the individual works permit.  Inclusion of a covered work
+in an aggregate does not cause this License to apply to the other
+parts of the aggregate.
+
+  6. Conveying Non-Source Forms.
+
+  You may convey a covered work in object code form under the terms
+of sections 4 and 5, provided that you also convey the
+machine-readable Corresponding Source under the terms of this License,
+in one of these ways:
+
+    a) Convey the object code in, or embodied in, a physical product
+    (including a physical distribution medium), accompanied by the
+    Corresponding Source fixed on a durable physical medium
+    customarily used for software interchange.
+
+    b) Convey the object code in, or embodied in, a physical product
+    (including a physical distribution medium), accompanied by a
+    written offer, valid for at least three years and valid for as
+    long as you offer spare parts or customer support for that product
+    model, to give anyone who possesses the object code either (1) a
+    copy of the Corresponding Source for all the software in the
+    product that is covered by this License, on a durable physical
+    medium customarily used for software interchange, for a price no
+    more than your reasonable cost of physically performing this
+    conveying of source, or (2) access to copy the
+    Corresponding Source from a network server at no charge.
+
+    c) Convey individual copies of the object code with a copy of the
+    written offer to provide the Corresponding Source.  This
+    alternative is allowed only occasionally and noncommercially, and
+    only if you received the object code with such an offer, in accord
+    with subsection 6b.
+
+    d) Convey the object code by offering access from a designated
+    place (gratis or for a charge), and offer equivalent access to the
+    Corresponding Source in the same way through the same place at no
+    further charge.  You need not require recipients to copy the
+    Corresponding Source along with the object code.  If the place to
+    copy the object code is a network server, the Corresponding Source
+    may be on a different server (operated by you or a third party)
+    that supports equivalent copying facilities, provided you maintain
+    clear directions next to the object code saying where to find the
+    Corresponding Source.  Regardless of what server hosts the
+    Corresponding Source, you remain obligated to ensure that it is
+    available for as long as needed to satisfy these requirements.
+
+    e) Convey the object code using peer-to-peer transmission, provided
+    you inform other peers where the object code and Corresponding
+    Source of the work are being offered to the general public at no
+    charge under subsection 6d.
+
+  A separable portion of the object code, whose source code is excluded
+from the Corresponding Source as a System Library, need not be
+included in conveying the object code work.
+
+  A "User Product" is either (1) a "consumer product", which means any
+tangible personal property which is normally used for personal, family,
+or household purposes, or (2) anything designed or sold for incorporation
+into a dwelling.  In determining whether a product is a consumer product,
+doubtful cases shall be resolved in favor of coverage.  For a particular
+product received by a particular user, "normally used" refers to a
+typical or common use of that class of product, regardless of the status
+of the particular user or of the way in which the particular user
+actually uses, or expects or is expected to use, the product.  A product
+is a consumer product regardless of whether the product has substantial
+commercial, industrial or non-consumer uses, unless such uses represent
+the only significant mode of use of the product.
+
+  "Installation Information" for a User Product means any methods,
+procedures, authorization keys, or other information required to install
+and execute modified versions of a covered work in that User Product from
+a modified version of its Corresponding Source.  The information must
+suffice to ensure that the continued functioning of the modified object
+code is in no case prevented or interfered with solely because
+modification has been made.
+
+  If you convey an object code work under this section in, or with, or
+specifically for use in, a User Product, and the conveying occurs as
+part of a transaction in which the right of possession and use of the
+User Product is transferred to the recipient in perpetuity or for a
+fixed term (regardless of how the transaction is characterized), the
+Corresponding Source conveyed under this section must be accompanied
+by the Installation Information.  But this requirement does not apply
+if neither you nor any third party retains the ability to install
+modified object code on the User Product (for example, the work has
+been installed in ROM).
+
+  The requirement to provide Installation Information does not include a
+requirement to continue to provide support service, warranty, or updates
+for a work that has been modified or installed by the recipient, or for
+the User Product in which it has been modified or installed.  Access to a
+network may be denied when the modification itself materially and
+adversely affects the operation of the network or violates the rules and
+protocols for communication across the network.
+
+  Corresponding Source conveyed, and Installation Information provided,
+in accord with this section must be in a format that is publicly
+documented (and with an implementation available to the public in
+source code form), and must require no special password or key for
+unpacking, reading or copying.
+
+  7. Additional Terms.
+
+  "Additional permissions" are terms that supplement the terms of this
+License by making exceptions from one or more of its conditions.
+Additional permissions that are applicable to the entire Program shall
+be treated as though they were included in this License, to the extent
+that they are valid under applicable law.  If additional permissions
+apply only to part of the Program, that part may be used separately
+under those permissions, but the entire Program remains governed by
+this License without regard to the additional permissions.
+
+  When you convey a copy of a covered work, you may at your option
+remove any additional permissions from that copy, or from any part of
+it.  (Additional permissions may be written to require their own
+removal in certain cases when you modify the work.)  You may place
+additional permissions on material, added by you to a covered work,
+for which you have or can give appropriate copyright permission.
+
+  Notwithstanding any other provision of this License, for material you
+add to a covered work, you may (if authorized by the copyright holders of
+that material) supplement the terms of this License with terms:
+
+    a) Disclaiming warranty or limiting liability differently from the
+    terms of sections 15 and 16 of this License; or
+
+    b) Requiring preservation of specified reasonable legal notices or
+    author attributions in that material or in the Appropriate Legal
+    Notices displayed by works containing it; or
+
+    c) Prohibiting misrepresentation of the origin of that material, or
+    requiring that modified versions of such material be marked in
+    reasonable ways as different from the original version; or
+
+    d) Limiting the use for publicity purposes of names of licensors or
+    authors of the material; or
+
+    e) Declining to grant rights under trademark law for use of some
+    trade names, trademarks, or service marks; or
+
+    f) Requiring indemnification of licensors and authors of that
+    material by anyone who conveys the material (or modified versions of
+    it) with contractual assumptions of liability to the recipient, for
+    any liability that these contractual assumptions directly impose on
+    those licensors and authors.
+
+  All other non-permissive additional terms are considered "further
+restrictions" within the meaning of section 10.  If the Program as you
+received it, or any part of it, contains a notice stating that it is
+governed by this License along with a term that is a further
+restriction, you may remove that term.  If a license document contains
+a further restriction but permits relicensing or conveying under this
+License, you may add to a covered work material governed by the terms
+of that license document, provided that the further restriction does
+not survive such relicensing or conveying.
+
+  If you add terms to a covered work in accord with this section, you
+must place, in the relevant source files, a statement of the
+additional terms that apply to those files, or a notice indicating
+where to find the applicable terms.
+
+  Additional terms, permissive or non-permissive, may be stated in the
+form of a separately written license, or stated as exceptions;
+the above requirements apply either way.
+
+  8. Termination.
+
+  You may not propagate or modify a covered work except as expressly
+provided under this License.  Any attempt otherwise to propagate or
+modify it is void, and will automatically terminate your rights under
+this License (including any patent licenses granted under the third
+paragraph of section 11).
+
+  However, if you cease all violation of this License, then your
+license from a particular copyright holder is reinstated (a)
+provisionally, unless and until the copyright holder explicitly and
+finally terminates your license, and (b) permanently, if the copyright
+holder fails to notify you of the violation by some reasonable means
+prior to 60 days after the cessation.
+
+  Moreover, your license from a particular copyright holder is
+reinstated permanently if the copyright holder notifies you of the
+violation by some reasonable means, this is the first time you have
+received notice of violation of this License (for any work) from that
+copyright holder, and you cure the violation prior to 30 days after
+your receipt of the notice.
+
+  Termination of your rights under this section does not terminate the
+licenses of parties who have received copies or rights from you under
+this License.  If your rights have been terminated and not permanently
+reinstated, you do not qualify to receive new licenses for the same
+material under section 10.
+
+  9. Acceptance Not Required for Having Copies.
+
+  You are not required to accept this License in order to receive or
+run a copy of the Program.  Ancillary propagation of a covered work
+occurring solely as a consequence of using peer-to-peer transmission
+to receive a copy likewise does not require acceptance.  However,
+nothing other than this License grants you permission to propagate or
+modify any covered work.  These actions infringe copyright if you do
+not accept this License.  Therefore, by modifying or propagating a
+covered work, you indicate your acceptance of this License to do so.
+
+  10. Automatic Licensing of Downstream Recipients.
+
+  Each time you convey a covered work, the recipient automatically
+receives a license from the original licensors, to run, modify and
+propagate that work, subject to this License.  You are not responsible
+for enforcing compliance by third parties with this License.
+
+  An "entity transaction" is a transaction transferring control of an
+organization, or substantially all assets of one, or subdividing an
+organization, or merging organizations.  If propagation of a covered
+work results from an entity transaction, each party to that
+transaction who receives a copy of the work also receives whatever
+licenses to the work the party's predecessor in interest had or could
+give under the previous paragraph, plus a right to possession of the
+Corresponding Source of the work from the predecessor in interest, if
+the predecessor has it or can get it with reasonable efforts.
+
+  You may not impose any further restrictions on the exercise of the
+rights granted or affirmed under this License.  For example, you may
+not impose a license fee, royalty, or other charge for exercise of
+rights granted under this License, and you may not initiate litigation
+(including a cross-claim or counterclaim in a lawsuit) alleging that
+any patent claim is infringed by making, using, selling, offering for
+sale, or importing the Program or any portion of it.
+
+  11. Patents.
+
+  A "contributor" is a copyright holder who authorizes use under this
+License of the Program or a work on which the Program is based.  The
+work thus licensed is called the contributor's "contributor version".
+
+  A contributor's "essential patent claims" are all patent claims
+owned or controlled by the contributor, whether already acquired or
+hereafter acquired, that would be infringed by some manner, permitted
+by this License, of making, using, or selling its contributor version,
+but do not include claims that would be infringed only as a
+consequence of further modification of the contributor version.  For
+purposes of this definition, "control" includes the right to grant
+patent sublicenses in a manner consistent with the requirements of
+this License.
+
+  Each contributor grants you a non-exclusive, worldwide, royalty-free
+patent license under the contributor's essential patent claims, to
+make, use, sell, offer for sale, import and otherwise run, modify and
+propagate the contents of its contributor version.
+
+  In the following three paragraphs, a "patent license" is any express
+agreement or commitment, however denominated, not to enforce a patent
+(such as an express permission to practice a patent or covenant not to
+sue for patent infringement).  To "grant" such a patent license to a
+party means to make such an agreement or commitment not to enforce a
+patent against the party.
+
+  If you convey a covered work, knowingly relying on a patent license,
+and the Corresponding Source of the work is not available for anyone
+to copy, free of charge and under the terms of this License, through a
+publicly available network server or other readily accessible means,
+then you must either (1) cause the Corresponding Source to be so
+available, or (2) arrange to deprive yourself of the benefit of the
+patent license for this particular work, or (3) arrange, in a manner
+consistent with the requirements of this License, to extend the patent
+license to downstream recipients.  "Knowingly relying" means you have
+actual knowledge that, but for the patent license, your conveying the
+covered work in a country, or your recipient's use of the covered work
+in a country, would infringe one or more identifiable patents in that
+country that you have reason to believe are valid.
+
+  If, pursuant to or in connection with a single transaction or
+arrangement, you convey, or propagate by procuring conveyance of, a
+covered work, and grant a patent license to some of the parties
+receiving the covered work authorizing them to use, propagate, modify
+or convey a specific copy of the covered work, then the patent license
+you grant is automatically extended to all recipients of the covered
+work and works based on it.
+
+  A patent license is "discriminatory" if it does not include within
+the scope of its coverage, prohibits the exercise of, or is
+conditioned on the non-exercise of one or more of the rights that are
+specifically granted under this License.  You may not convey a covered
+work if you are a party to an arrangement with a third party that is
+in the business of distributing software, under which you make payment
+to the third party based on the extent of your activity of conveying
+the work, and under which the third party grants, to any of the
+parties who would receive the covered work from you, a discriminatory
+patent license (a) in connection with copies of the covered work
+conveyed by you (or copies made from those copies), or (b) primarily
+for and in connection with specific products or compilations that
+contain the covered work, unless you entered into that arrangement,
+or that patent license was granted, prior to 28 March 2007.
+
+  Nothing in this License shall be construed as excluding or limiting
+any implied license or other defenses to infringement that may
+otherwise be available to you under applicable patent law.
+
+  12. No Surrender of Others' Freedom.
+
+  If conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License.  If you cannot convey a
+covered work so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you may
+not convey it at all.  For example, if you agree to terms that obligate you
+to collect a royalty for further conveying from those to whom you convey
+the Program, the only way you could satisfy both those terms and this
+License would be to refrain entirely from conveying the Program.
+
+  13. Use with the GNU Affero General Public License.
+
+  Notwithstanding any other provision of this License, you have
+permission to link or combine any covered work with a work licensed
+under version 3 of the GNU Affero General Public License into a single
+combined work, and to convey the resulting work.  The terms of this
+License will continue to apply to the part which is the covered work,
+but the special requirements of the GNU Affero General Public License,
+section 13, concerning interaction through a network will apply to the
+combination as such.
+
+  14. Revised Versions of this License.
+
+  The Free Software Foundation may publish revised and/or new versions of
+the GNU General Public License from time to time.  Such new versions will
+be similar in spirit to the present version, but may differ in detail to
+address new problems or concerns.
+
+  Each version is given a distinguishing version number.  If the
+Program specifies that a certain numbered version of the GNU General
+Public License "or any later version" applies to it, you have the
+option of following the terms and conditions either of that numbered
+version or of any later version published by the Free Software
+Foundation.  If the Program does not specify a version number of the
+GNU General Public License, you may choose any version ever published
+by the Free Software Foundation.
+
+  If the Program specifies that a proxy can decide which future
+versions of the GNU General Public License can be used, that proxy's
+public statement of acceptance of a version permanently authorizes you
+to choose that version for the Program.
+
+  Later license versions may give you additional or different
+permissions.  However, no additional obligations are imposed on any
+author or copyright holder as a result of your choosing to follow a
+later version.
+
+  15. Disclaimer of Warranty.
+
+  THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
+APPLICABLE LAW.  EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
+HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
+OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
+THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+PURPOSE.  THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
+IS WITH YOU.  SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
+ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
+
+  16. Limitation of Liability.
+
+  IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
+THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
+GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
+USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
+DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
+PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
+EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
+SUCH DAMAGES.
+
+  17. Interpretation of Sections 15 and 16.
+
+  If the disclaimer of warranty and limitation of liability provided
+above cannot be given local legal effect according to their terms,
+reviewing courts shall apply local law that most closely approximates
+an absolute waiver of all civil liability in connection with the
+Program, unless a warranty or assumption of liability accompanies a
+copy of the Program in return for a fee.
+
+                     END OF TERMS AND CONDITIONS
+
+            How to Apply These Terms to Your New Programs
+
+  If you develop a new program, and you want it to be of the greatest
+possible use to the public, the best way to achieve this is to make it
+free software which everyone can redistribute and change under these terms.
+
+  To do so, attach the following notices to the program.  It is safest
+to attach them to the start of each source file to most effectively
+state the exclusion of warranty; and each file should have at least
+the "copyright" line and a pointer to where the full notice is found.
+
+    <one line to give the program's name and a brief idea of what it does.>
+    Copyright (C) <year>  <name of author>
+
+    This program is free software: you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation, either version 3 of the License, or
+    (at your option) any later version.
+
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU General Public License for more details.
+
+    You should have received a copy of the GNU General Public License
+    along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+Also add information on how to contact you by electronic and paper mail.
+
+  If the program does terminal interaction, make it output a short
+notice like this when it starts in an interactive mode:
+
+    <program>  Copyright (C) <year>  <name of author>
+    This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
+    This is free software, and you are welcome to redistribute it
+    under certain conditions; type `show c' for details.
+
+The hypothetical commands `show w' and `show c' should show the appropriate
+parts of the General Public License.  Of course, your program's commands
+might be different; for a GUI interface, you would use an "about box".
+
+  You should also get your employer (if you work as a programmer) or school,
+if any, to sign a "copyright disclaimer" for the program, if necessary.
+For more information on this, and how to apply and follow the GNU GPL, see
+<http://www.gnu.org/licenses/>.
+
+  The GNU General Public License does not permit incorporating your program
+into proprietary programs.  If your program is a subroutine library, you
+may consider it more useful to permit linking proprietary applications with
+the library.  If this is what you want to do, use the GNU Lesser General
+Public License instead of this License.  But first, please read
+<http://www.gnu.org/philosophy/why-not-lgpl.html>.

+ 177 - 0
plugin/pens/lib/PENSPlugin.php

@@ -0,0 +1,177 @@
+<?php
+/* For licensing terms, see /license.txt */
+
+/**
+ * Class PENSPlugin
+ * This class is used to add an advanced subscription allowing the admin to
+ * create user queues requesting a subscribe to a session
+ * @package chamilo.plugin.pens
+ */
+class PENSPlugin extends Plugin implements HookPluginInterface
+{
+    protected $strings;
+    private $errorMessages;
+    const TABLE_PENS = 'plugin_pens';
+
+
+    /**
+     * Constructor
+     */
+    public function __construct()
+    {
+        $parameters = array(
+        );
+
+        parent::__construct($this->get_version(), $this->get_author(), $parameters);
+
+        $this->errorMessages = array();
+    }
+
+    /**
+     * Instance the plugin
+     * @staticvar null $result
+     * @return AdvancedSubscriptionPlugin
+     */
+    public static function create()
+    {
+        static $result = null;
+
+        return $result ? $result : $result = new self();
+    }
+
+    /**
+     * Install the plugin
+     * @return void
+     */
+    public function install()
+    {
+        $this->installDatabase();
+        $this->installHook();
+    }
+
+    /**
+     * Uninstall the plugin
+     * @return void
+     */
+    public function uninstall()
+    {
+        $setting = api_get_setting('plugin_pens');
+        if (!empty($setting)) {
+            $this->uninstallHook();
+            // Note: Keeping area field data is intended so it will not be removed
+            $this->uninstallDatabase();
+        }
+    }
+
+    /**
+     * Create the database tables for the plugin
+     * @return void
+     */
+    private function installDatabase()
+    {
+        $pensTable = Database::get_main_table(PENSPlugin::TABLE_PENS);
+
+        $sql = "CREATE TABLE $pensTable (
+	        id int unsigned NOT NULL auto_increment,
+	        pens_version varchar(255) NOT NULL,
+	        package_type varchar(255) NOT NULL,
+	        package_type_version varchar(255) NOT NULL,
+        	package_format varchar(255) NOT NULL,
+	        package_id varchar (255) NOT NULL,
+	        client varchar(255) NOT NULL,
+	        vendor_data text,
+	        package_name varchar(255) NOT NULL,
+	        created_at datetime NOT NULL,
+	        updated_at datetime NULL,
+	        PRIMARY KEY (id),
+	        UNIQUE KEY package_id (package_id)
+	        ";
+        Database::query($sql);
+    }
+
+    /**
+     * Drop the database tables for the plugin
+     * @return void
+     */
+    private function uninstallDatabase()
+    {
+        /* Drop plugin tables */
+        $pensTable = Database::get_main_table(PENSPlugin::TABLE_PENS);
+
+        $sql = "DROP TABLE IF EXISTS $pensTable; ";
+        Database::query($sql);
+
+        /* Delete settings */
+        $settingsTable = Database::get_main_table(TABLE_MAIN_SETTINGS_CURRENT);
+        Database::query("DELETE FROM $settingsTable WHERE subkey = 'plugin_pens'");
+    }
+
+    /**
+     * Get the error messages list
+     * @return array The message list
+     */
+    public function getErrorMessages()
+    {
+        return $this->errorMessages;
+    }
+
+    /**
+     * This method will call the Hook management insertHook to add Hook observer from this plugin
+     * @return int
+     */
+    public function installHook()
+    {
+    }
+
+    /**
+     * This method will call the Hook management deleteHook to disable Hook observer from this plugin
+     * @return int
+     */
+    public function uninstallHook()
+    {
+    }
+
+    /**
+     * Copied and fixed from plugin.class.php
+     * Returns the "system" name of the plugin in lowercase letters
+     * @return string
+     */
+    public function get_name()
+    {
+        return 'PENS';
+    }
+
+    /**
+     * Get author(s)
+     * @return string
+     */
+    public function get_author()
+    {
+        return 'Guillaume Viguier-Just, Yannick Warnier';
+    }
+
+    /**
+     * Returns the plugin version
+     * @return string
+     */
+    public function get_version()
+    {
+        return '1.1';
+    }
+    /**
+     * Get generic plugin info
+     * @return array
+     */
+    public function get_info()
+    {
+        $result = array();
+        $result['title'] = $this->get_name();
+        $result['comment'] = 'Provides support for the PENS course exchange standard. Read the readme.txt file in the plugin/pens/ folder for a complete installation.';
+        $result['version'] = $this->get_version();
+        $result['author'] = $this->get_author();
+        $result['plugin_class'] = get_class($this);
+        $result['is_course_plugin'] = false;
+        $result['is_mail_plugin'] = false;
+        return $result;
+    }
+}

+ 79 - 0
plugin/pens/lib/README.textile

@@ -0,0 +1,79 @@
+h1. Introduction
+
+This library is an implementation of the PENS specification (see http://pens.lmstesting.com/pages/whatispens.htm) in PHP. It provides a PENSServer class and, in the future, will provide a PENSClient class.
+
+h1. Requirements
+
+You need at least PHP 5.2 with the curl extension installed to use this library.
+
+h1. State
+
+Currently, some features are missing (such as the possibility to send alerts or the possibility to send receipts to a mailto url), and the library is not considered as stable.
+
+h1. How do I create a server ?
+
+Assuming the URL of your server is http://www.myserver.com, create a file called pens.php at the root so that it is accessible through http://www.myserver.com/pens.php.
+
+In this file, write the following code:
+
+pre.. // Assuming the library is accessible through pens/pens.php
+
+require_once("pens/pens.php");
+
+// Create a concrete class that extends PENSPackageHandler and 
+// write your implementation of the processPackage method in it
+class MyPackageHandler extends PENSPackageHandler {
+	// See the documentation of the processPackage method for more information
+	public function processPackage($request, $path_to_package) {
+		// Write your implementation of the package handling here
+	}
+}
+
+// Create an instance of your handler and configure it
+// See the file pens_package_handler.php for more information
+$handler = new MyPackageHandler();
+$handler->setSupportedPackageTypes(array("scorm-pif"));
+$handler->setSupportedPackageFormats(array("zip"));
+
+// Initialize the server
+$server = PENSServer::singleton();
+$server->setPackageHandler($handler);
+
+// The server should only receive collect commands, so call the receiveCollect method
+$server->receiveCollect();
+
+h1. How do I create a client ?
+
+The role of the PENSClient is simply to receive receipts and alerts from the server and process them. Here is how to create a client.
+
+pre.. require_once("pens/pens.php");
+
+// Create a request handler if you need one. This allows you to do some processing on the requests (store them in a DB, send them to an email address etc...)
+// You do not need to create a request handler if you do not want to process the requests
+class MyRequestHandler extends PENSRequestHandler {
+	public function processRequest($request, $response) {
+		// Do the processing of the alert or the receipt here
+	}
+}
+
+// Instantiate the handler
+$handler = new MyRequestHandler();
+
+// Create the client and set the request handler
+$client = PENSClient::singleton();
+$client->setRequestHandler($handler);
+
+// Parse and process the request
+$client->receiveRequest();
+
+h1. Licence
+
+The php-pens library is published under the GNU GPL 3 licence (see COPYING)
+
+h1. Credits
+
+The author of the library is Guillaume Viguier-Just <guillaume@viguierjust.com> and this library has been realized mainly for use in the Chamilo project (http://www.chamilo.org).
+
+This library was realized as part of my work in Beeznest Latino (http://www.beeznest.com), for the Chamilo project.
+
+Also note the use of a library called rfc2396regexes written by Rudy Desjardins and distributed as GPL v2

+ 7 - 0
plugin/pens/lib/pens.php

@@ -0,0 +1,7 @@
+<?php
+
+require_once __DIR__ . '/pens/pens_request_factory.php';
+require_once __DIR__ . '/pens/pens_response.php';
+require_once __DIR__ . '/pens/pens_package_handler.php';
+require_once __DIR__ . '/pens/pens_server.php';
+require_once __DIR__ . '/pens/pens_client.php';

+ 113 - 0
plugin/pens/lib/pens/lib/rfc2396regexes.php

@@ -0,0 +1,113 @@
+<?php
+
+
+/// Copyright 2006 Rudy Desjardins
+
+/// This program is free software; you can redistribute it and/or modify
+/// it under the terms of the GNU General Public License as published by
+/// the Free Software Foundation; either version 2 of the License, or
+/// (at your option) any later version.
+///
+/// This program is distributed in the hope that it will be useful,
+/// but WITHOUT ANY WARRANTY; without even the implied warranty of
+/// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+/// GNU General Public License for more details.
+///
+/// You should have received a copy of the GNU General Public License
+/// along with this program; if not, write to the Free Software
+/// Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
+
+
+///
+/// This file defines constants for use as regular expressions for validating
+/// RFC 2396 compliant URI strings
+///
+/// See Appendix A of RFC 2396 for the origin of these expressions
+///
+
+define('ALPHA_2396', '[a-zA-Z]');
+define('DIGIT_2396', '[0-9]');
+
+define('LOWALPHA_2396', '[a-z]');
+define('UPALPHA_2396', '[A-Z]');
+
+define('ALPHANUM_2396', '(?:'.ALPHA_2396.'|'.DIGIT_2396.')');
+define('ALPHANUM_OPT_2396', '[a-zA-Z0-9]');
+
+define('HEX_2396', '(?:'.DIGIT_2396.'|[a-fA-F])');
+define('HEX_OPT_2396', '[0-9a-fA-F]');
+
+define('ESCAPED_2396', '(?:%'.HEX_OPT_2396.HEX_OPT_2396.')');
+
+define('MARK_2396', '[\\x2D\\x2E\\x5F\\x21\\x7E\\x2A\\x27\\x28\\x29]');
+define('RESERVED_2396', '[\\x3B\\x3F\\x2F3A\\x40\\x26\\x2B\\x3D\\x24\\x2C]');
+define('UNRESERVED_2396', '(?:'.ALPHANUM_OPT_2396.'|'.MARK_2396.')');
+
+define('URIC_2396', '(?:'.RESERVED_2396.'|'.UNRESERVED_2396.'|'.ESCAPED_2396.')');
+
+define('QUERY_2396', '(?:'.URIC_2396.'*)');
+define('FRAGMENT_2396', QUERY_2396);
+
+define('PCHAR_2396', '(?:'.UNRESERVED_2396.'|'.ESCAPED_2396.'|[\\x3A\\x40\\x26\\x3D\\x2B\\x24\\x2C])');
+
+define('PARAM_2396', '(?:'.PCHAR_2396.'*)');
+
+define('SEGMENT_2396', '(?:'.PCHAR_2396.'*(?:;'.PARAM_2396.')*)');
+
+define('PATH_SEGMENTS_2396', '(?:'.SEGMENT_2396.'(?:\\/'.SEGMENT_2396.')*)');
+
+define('ABS_PATH_2396', '(?:\\/'.PATH_SEGMENTS_2396.')');
+
+define('URIC_NO_SLASH_2396', '(?:'.UNRESERVED_2396.'|'.ESCAPED_2396.'|'.'[\\x3A\\x3B\\x3F\\x40\\x26\\x3D\\x24\\x2B\\x2C])');
+
+define('OPAQUE_PART_2396', '(?:'.URIC_NO_SLASH_2396.URIC_2396.'*)');
+
+define('PATH_2396', '(?:'.ABS_PATH_2396.'|'.OPAQUE_PART_2396.')');
+
+/// This is probably wrong... should end in '+', not '*'...
+///
+define('PORT_2396', '(?:'.DIGIT_2396.'*)');
+
+/// Same here... pieces should be '[012]*'.DIGIT_2396.'{1,2}', although that's not perfect either...
+///
+define('IPV4ADDRESS_2396', '(?:'.DIGIT_2396.'+\\.'.DIGIT_2396.'+\\.'.DIGIT_2396.'+\\.'.DIGIT_2396.'+)');
+
+define('TOPLABEL_2396', '(?:'.ALPHA_2396.'|'.ALPHA_2396.'(?:'.ALPHANUM_OPT_2396.'|\\x2D)*'.ALPHANUM_OPT_2396.')');
+define('DOMAINLABEL_2396', '(?:'.ALPHANUM_OPT_2396.'|'.ALPHANUM_OPT_2396.'(?:'.ALPHANUM_OPT_2396.'|\\x2D)*'.ALPHANUM_OPT_2396.')');
+
+define('HOSTNAME_2396', '(?:(?:'.DOMAINLABEL_2396.'\\.)*'.TOPLABEL_2396.'\\.?)');
+
+define('HOST_2396', '(?:'.HOSTNAME_2396.'|'.IPV4ADDRESS_2396.')');
+
+define('HOSTPORT_2396', '(?:'.HOST_2396.PORT_2396.'?)');
+
+define('USERINFO_2396', '(?:(?:'.UNRESERVED_2396.'|'.ESCAPED_2396.'|[\\x26\\x3A\\x3B\\x3D\\x2B\\x24\\x2C])*)');
+
+/// This should probably be '(?:'.USERINFO_2396.'\\x40)?'.HOSTPORT_2396'...
+///
+define('SERVER_2396', '(?:(?:'.USERINFO_2396.'\\x40)?'.HOSTPORT_2396.')');
+
+define('REGNAME_2396', '(?:(?:'.UNRESERVED_2396.'|'.ESCAPED_2396.'|[\\x24\\x2C\\x3A\\x3B\\x40\\x26\\x3D\\x2B])+)');
+
+define('AUTHORITY_2396', '(?:'.SERVER_2396.'|'.REGNAME_2396.')');
+
+define('SCHEME_2396', '(?:'.ALPHA_2396.'(?:'.ALPHA_2396.'|'.DIGIT_2396.'|[\\x2B\\x2D\\x2E])*)');
+
+define('REL_SEGMENT_2396', '(?:(?:'.UNRESERVED_2396.'|'.ESCAPED_2396.'|[\\x40\\x26\\x3B\\x3D\\x2B\\x24\\x2C])+)');
+
+define('REL_PATH_2396', '(?:'.REL_SEGMENT_2396.ABS_PATH_2396.'?)');
+define('NET_PATH_2396', '(?:\\/\\/'.AUTHORITY_2396.ABS_PATH_2396.'?)');
+
+define('HIER_PART_2396', '(?:(?:'.NET_PATH_2396.'|'.ABS_PATH_2396.')(?:\\?'.QUERY_2396.')?)');
+
+define('RELATIVEURI_2396', '(?:('.NET_PATH_2396.'|'.ABS_PATH_2396.'|'.REL_PATH_2396.')(?:\\?'.QUERY_2396.')?)');
+define('ABSOLUTEURI_2396', '(?:'.SCHEME_2396.'\\x3A(?:'.HIER_PART_2396.'|'.OPAQUE_PART_2396.'))');
+
+/// This was the only expression changed to use parens instead of brackets around the URI_2396 options, because
+/// otherwise a blank string would match the whole expression incorrectly...
+///
+define('URI_REFERENCE_2396', '(?:(?:'.ABSOLUTEURI_2396.'|'.RELATIVEURI_2396.')(?:\\x23'.FRAGMENT_2396.')?)');
+
+define('URI_2396', URI_REFERENCE_2396);
+
+?>

+ 113 - 0
plugin/pens/lib/pens/pens_client.php

@@ -0,0 +1,113 @@
+<?php
+/**
+ * This file is part of php-pens.
+ * 
+ * php-pens is free software: you can redistribute it and/or modify 
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ * 
+ * php-pens is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ * 
+ * You should have received a copy of the GNU General Public License
+ * along with php-pens.  If not, see <http://www.gnu.org/licenses/>.
+ */
+ 
+ /**
+ * PENSClient
+ * 
+ * Provides the PENSClient class
+ * 
+ * @package PENS
+ * @author Guillaume Viguier-Just <guillaume@viguierjust.com>
+ * @licence http://www.gnu.org/licenses/gpl.txt
+ */
+ 
+require_once(dirname(__FILE__)."/pens_controller.php");
+require_once(dirname(__FILE__)."/pens_request_handler.php");
+
+/**
+ * PENSClient
+ * 
+ * Class that implements the PENS Client
+ * 
+ * @package PENS
+ * @author Guillaume Viguier-Just <guillaume@viguierjust.com>
+ * @licence http://www.gnu.org/licenses/gpl.txt
+ */
+class PENSClient extends PENSController {
+	/**
+	 * Instance of the PENSClient
+	 * @var PENSServer
+	 */
+	private static $_instance;
+	
+	/**
+	 * Request handler
+	 * @var PENSRequestHandler
+	 */
+	protected $_request_handler = null;
+	
+	/**
+	 * Private constructor
+	 */
+	private function __construct() {
+	}
+	
+	/**
+	 * Singleton method
+	 */
+	public static function singleton() {
+		if(!isset(self::$_instance)) {
+			$c = __CLASS__;
+			self::$_instance = new $c;
+		}
+		return self::$_instance;
+	}
+	
+	/**
+	 * Prevent users to clone the instance
+	 */
+	public function __clone() {
+		trigger_error('Clone is not allowed', E_USER_ERROR);
+	}
+	
+	public function getRequestHandler() {
+		return $this->_request_handler;
+	}
+	
+	/**
+	 * Sets the request handler. Does nothing if the argument is not an instance of PENSRequestHandler
+	 * 
+	 * @param PENSRequestHandler Request handler
+	 */
+	public function setRequestHandler($request_handler) {
+		if($request_handler instanceof PENSRequestHandler) {
+			$this->_request_handler = $request_handler;
+		}
+	}
+	
+	/**
+	 * Receives a request, calls the handler and displays the response
+	 */
+	public function receiveRequest() {
+		$request = null;
+		try {
+			$request = $this->parseRequest();
+			$command = $request->getCommand();
+			if($command == "alert" || $command == "receipt") {
+				if(!is_null($this->_request_handler)) {
+					// Call the handler
+					$this->_request_handler->processRequest($request, new PENSResponse($_REQUEST));
+				}
+				$this->sendResponse(new PENSResponse(0, $command." received and understood"));
+			}
+		} catch(PENSException $e) {
+			// If we could not parse the request, send the error
+			$this->sendResponse(new PENSResponse($e));
+		}
+	}
+}

+ 69 - 0
plugin/pens/lib/pens/pens_config.php

@@ -0,0 +1,69 @@
+<?php
+/**
+ * This file is part of php-pens.
+ * 
+ * php-pens is free software: you can redistribute it and/or modify 
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ * 
+ * php-pens is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ * 
+ * You should have received a copy of the GNU General Public License
+ * along with php-pens.  If not, see <http://www.gnu.org/licenses/>.
+ */
+ 
+ /**
+ * PENSConfig
+ * 
+ * Provides the PENSConfig class
+ * 
+ * @package PENS
+ * @author Guillaume Viguier-Just <guillaume@viguierjust.com>
+ * @licence http://www.gnu.org/licenses/gpl.txt
+ */
+ 
+ /**
+ * PENSConfig
+ * 
+ * This class provides different static variables to configure php-pens.
+ * 
+ * @package PENS
+ * @author Guillaume Viguier-Just <guillaume@viguierjust.com>
+ * @licence http://www.gnu.org/licenses/gpl.txt
+ */
+class PENSConfig {
+
+	/**
+	 * PENS version
+	 * @var string
+	 */
+	public static $version = "1.0.0";
+	
+	/**
+	 * Allowed commands
+	 * @var array
+	 */
+	public static $allowed_commands = array("collect", "receipt", "alert");
+	
+	/**
+	 * Allowed package types
+	 * @var array
+	 */
+	public static $allowed_package_types = array("aicc-pkg", "scorm-pif", "ims-qti");
+	
+	/**
+	 * Allowed package formats
+	 * @var array
+	 */
+	public static $allowed_package_formats = array("zip", "url", "jar", "war", "xml");
+	
+	/**
+	 * End of line as specified by the PENS specification
+	 * @var string
+	 */
+	public static $eol = "\r\n";
+}

+ 62 - 0
plugin/pens/lib/pens/pens_controller.php

@@ -0,0 +1,62 @@
+<?php
+/**
+ * This file is part of php-pens.
+ * 
+ * php-pens is free software: you can redistribute it and/or modify 
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ * 
+ * php-pens is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ * 
+ * You should have received a copy of the GNU General Public License
+ * along with php-pens.  If not, see <http://www.gnu.org/licenses/>.
+ */
+ 
+ /**
+ * PENSController
+ * 
+ * Provides the PENSController class
+ * 
+ * @package PENS
+ * @author Guillaume Viguier-Just <guillaume@viguierjust.com>
+ * @licence http://www.gnu.org/licenses/gpl.txt
+ */
+
+require_once(dirname(__FILE__)."/pens_request_factory.php");
+
+/**
+ * PENSController
+ * 
+ * Base class for the PENSServer and PENSClient
+ * 
+ * @package PENS
+ * @author Guillaume Viguier-Just <guillaume@viguierjust.com>
+ * @licence http://www.gnu.org/licenses/gpl.txt
+ */
+abstract class PENSController {
+	
+	/**
+	 * Parses a request based on the values present in $_REQUEST
+	 * 
+	 * @return PENSRequest Request generated
+	 * @throws PENSException if the request could not be parsed
+	 */
+	protected function parseRequest() {
+		$request = PENSRequestFactory::createPENSRequest($_REQUEST);
+		return $request;
+	}
+	
+	/**
+	 * Sends the HTTP Response to the client
+	 * 
+	 * @param PENSResponse Response to be sent
+	 */
+	protected function sendResponse($response) {
+		$response->send();
+	}
+	
+}

+ 98 - 0
plugin/pens/lib/pens/pens_exception.php

@@ -0,0 +1,98 @@
+<?php
+/**
+ * This file is part of php-pens.
+ * 
+ * php-pens is free software: you can redistribute it and/or modify 
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ * 
+ * php-pens is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ * 
+ * You should have received a copy of the GNU General Public License
+ * along with php-pens.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+/**
+ * PENSException
+ * 
+ * Provides the PENSException class
+ * 
+ * @package PENS
+ * @author Guillaume Viguier-Just <guillaume@viguierjust.com>
+ * @licence http://www.gnu.org/licenses/gpl.txt
+ */
+
+/**
+ * PENSException
+ * 
+ * Class extending the PHP Exception class
+ * 
+ * @package PENS
+ * @author Guillaume Viguier-Just <guillaume@viguierjust.com>
+ * @licence http://www.gnu.org/licenses/gpl.txt
+ */
+class PENSException extends Exception {
+
+	/**
+	 * Array that provides an association between exception codes and messages
+	 * @var array
+	 */
+	protected static $_code_to_messages = array(
+		1101 => "Unable to parse PENS command",
+		1201 => "Attempt to pass an invalid argument",
+		1301 => "Unable to retrieve package",
+		1302 => "Unable to retrieve package via HTTPS",
+		1304 => "Unable to retrieve package via FTP",
+		1306 => "Unable to retrieve package via FTPS",
+		1310 => "Unable to retrieve package at specified URL due to error in URL or lack of response from URL",
+		1312 => "Unable to retrieve package at specified URL due to error with access credential for package URL",
+		1320 => "Expiration date is non-null and in an improper format",
+		1322 => "Current time indicates expiry date has passed",
+		1420 => "PENS version not supported",
+		1421 => "Command not supported",
+		1430 => "Package type not supported",
+		// The following error code is not specified in the PENS specification and was added in this implementation
+		1431 => "Package format not supported",
+		1432 => "Internal package error",
+		1440 => "Insufficient host space/storage available",
+		1500 => "Unable to communicate with provided acknowledgement URL",
+		1510 => "Unsupported acknowledgement protocol",
+		1520 => "Unsupported alert protocol",
+		2001 => "PENS version invalid or not specified",
+		2002 => "PENS command invalid or not specified",
+		2003 => "package-type invalid or not specified",
+		2004 => "package-type-version invalid or not specified",
+		2005 => "package-format invalid or not specified",
+		2007 => "package-id invalid or not specified",
+		2008 => "package-url invalid or not specified",
+		2009 => "package-url-expiry date invalid or not specified",
+		2010 => "client submitting package invalid or not specified",
+		2011 => "receipt url invalid or not specified"
+	);
+	
+	/**
+	 * Redefines the constructor so that code is the first argument
+	 * 
+	 * @param int Exception code
+	 * @param string Message to display
+	 * @return PENSException Exception created
+	 */
+	public function __construct($code, $message = null) {
+		parent::__construct($message, $code);
+		$this->setMessageBasedOnCode();
+	}
+	
+	/**
+	 * Sets the message based on the code
+	 */
+	protected function setMessageBasedOnCode() {
+		if(empty($this->message) && !empty(self::$_code_to_messages[$this->code])) {
+			$this->message = self::$_code_to_messages[$this->code];
+		}
+	}
+	
+}

+ 5 - 0
plugin/pens/lib/pens/pens_message.php

@@ -0,0 +1,5 @@
+<?php
+
+abstract class PENSMessage {
+
+}

+ 114 - 0
plugin/pens/lib/pens/pens_package_handler.php

@@ -0,0 +1,114 @@
+<?php
+/**
+ * This file is part of php-pens.
+ * 
+ * php-pens is free software: you can redistribute it and/or modify 
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ * 
+ * php-pens is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ * 
+ * You should have received a copy of the GNU General Public License
+ * along with php-pens.  If not, see <http://www.gnu.org/licenses/>.
+ */
+ 
+ /**
+ * PENSPackageHandler
+ * 
+ * Provides the PENSPackageHandler abstract class
+ * 
+ * @package PENS
+ * @author Guillaume Viguier-Just <guillaume@viguierjust.com>
+ * @licence http://www.gnu.org/licenses/gpl.txt
+ */
+
+require_once(dirname(__FILE__)."/pens_config.php");
+
+/**
+ * PENSPackageHandler
+ * 
+ * This class is an abstract class used to handle the processing of the package on the server side. Create a class that inherits from
+ * this class and implements the processPackage method, then set the package handler of the PENSServer class using the instance newly created
+ * 
+ * @package PENS
+ * @author Guillaume Viguier-Just <guillaume@viguierjust.com>
+ * @licence http://www.gnu.org/licenses/gpl.txt
+ */
+abstract class PENSPackageHandler {
+	/**
+	 * Array of supported package types for this handler. Set by default to PENSConfig::$allowed_package_types
+	 * @var array
+	 */
+	protected $_supported_package_types = null;
+	
+	/**
+	 * Array of supported package formats for this handler. Set by default to PENSConfig::$allowed_package_formats
+	 * @var array
+	 */
+	protected $_supported_package_formats = null;
+	
+	/**
+	 * Constructor. Sets the default values of supported_package_types and supported_package_formats
+	 */
+	public function __construct() {
+		$this->_supported_package_types = PENSConfig::$allowed_package_types;
+		$this->_supported_package_formats = PENSConfig::$allowed_package_formats;
+	}
+	
+	public function getSupportedPackageTypes() {
+		return $this->_supported_package_types;
+	}
+	
+	/**
+	 * Sets the supported package types. All package types in the array provided as an argument
+	 * not present in PENSConfig::$allowed_package_types will be ignored. The supported_package_types
+	 * will be set to null if the argument given is not an array
+	 * 
+	 * @param array Supported package types
+	 */
+	public function setSupportedPackageTypes($supported_package_types) {
+		$this->_supported_package_types = null;
+		if(is_array($supported_package_types)) {
+			foreach($supported_package_types as $package_type) {
+				if(in_array($package_type, PENSConfig::$allowed_package_types)) {
+					$this->_supported_package_types[] = $package_type;
+				}
+			}
+		}
+	}
+	
+	public function getSupportedPackageFormats() {
+		return $this->_supported_package_formats;
+	}
+	
+	/**
+	 * Sets the supported package formats. All package types in the array provided as an argument
+	 * not present in PENSConfig::$allowed_package_formats will be ignored. The supported_package_formats
+	 * will be set to null if the argument given is not an array.
+	 * 
+	 * @param array Supported package formats
+	 */
+	public function setSupportedPackageFormats($supported_package_formats) {
+		$this->_supported_package_formats = null;
+		if(is_array($supported_package_formats)) {
+			foreach($supported_package_formats as $package_format) {
+				if(in_array($package_format, PENSConfig::$allowed_package_formats)) {
+					$this->_supported_package_formats[] = $package_format;
+				}
+			}
+		}
+	}
+	
+	/**
+	 * Processes the package. Must be implemented by subclasses
+	 * 
+	 * @param PENSRequestCollect Collect request sent by the client
+	 * @param string Path to the collected package on the hard drive
+	 */
+	abstract public function processPackage($request, $path_to_package);
+	
+}

+ 495 - 0
plugin/pens/lib/pens/pens_request.php

@@ -0,0 +1,495 @@
+<?php
+/**
+ * This file is part of php-pens.
+ * 
+ * php-pens is free software: you can redistribute it and/or modify 
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ * 
+ * php-pens is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ * 
+ * You should have received a copy of the GNU General Public License
+ * along with php-pens.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+/**
+ * PENSRequest
+ * 
+ * Provides the PENSRequest abstract class
+ * 
+ * @package PENS
+ * @author Guillaume Viguier-Just <guillaume@viguierjust.com>
+ * @licence http://www.gnu.org/licenses/gpl.txt
+ */
+
+require_once(dirname(__FILE__)."/pens_config.php");
+require_once(dirname(__FILE__)."/pens_message.php");
+require_once(dirname(__FILE__)."/pens_exception.php");
+require_once(dirname(__FILE__)."/lib/rfc2396regexes.php");
+
+/**
+ * PENSRequest
+ * 
+ * Base class for all the request types (collect, receipt, alert)
+ * 
+ * @package PENS
+ * @author Guillaume Viguier-Just <guillaume@viguierjust.com>
+ * @licence http://www.gnu.org/licenses/gpl.txt
+ */
+
+abstract class PENSRequest extends PENSMessage {
+
+	/**
+	 * PENS Version to be used. Currently, the only valid value is 1.0.0. Required.
+	 * @var string
+	 */
+	protected $_pens_version = null;
+	
+	/**
+	 * Command being used. The only valid values are collect, alert and receipt.Required
+	 * @var string
+	 */
+	protected $_command = null;
+	
+	/**
+	 * Package type being used. The only valid values are aicc-pkg, scorm-pif, ims-qti. Required
+	 * @var string
+	 */
+	protected $_package_type = null;
+	
+	/**
+	 * Package type version. Required
+	 * @var string
+	 */
+	protected $_package_type_version = null;
+	
+	/**
+	 * Package format. The only valid values are zip, url, jar, war and xml. Required
+	 * @var string
+	 */
+	protected $_package_format = null;
+	
+	/**
+	 * Package id. Requires a valid URI according to RFC 2396. Required
+	 * @var string
+	 */
+	protected $_package_id = null;
+	
+	/**
+	 * Package url. Requires a valid, fully qualified URL including transport protocol and filename extension. Required
+	 * @var string
+	 */
+	protected $_package_url = null;
+	
+	/**
+	 * User id required for system to retrieve package from URL. Optional.
+	 * @var string
+	 */
+	protected $_package_url_user_id = null;
+	
+	/**
+	 * Account required for system to retrieve package from URL. Optional.
+	 * @var string
+	 */
+	protected $_package_url_account = null;
+	
+	/**
+	 * Password required for system to retrieve package from URL. Optional.
+	 * @var string
+	 */
+	protected $_package_url_password = null;
+	
+	/**
+	 * Expiry date for package URL. ISO 8601 format expressed as UTC. Will be transformed into a PHP DateTime object during construction. Required
+	 * @var DateTime
+	 */
+	protected $_package_url_expiry = null;
+	
+	/**
+	 * Name or ID for client submitting the content package to the target system. Required.
+	 * @var string
+	 */
+	protected $_client = null;
+	
+	/**
+	 * User-id or sign-on for target system. Optional
+	 * @var string
+	 */
+	protected $_system_user_id = null;
+	
+	/**
+	 * Either a URL-encoded password token or the null string. If the 
+	 * target system requires a password and the null string value is 
+	 * passed, then the target system is responsible for prompting for a 
+	 * password for target system. Optional
+	 * @var string
+	 */
+	protected $_system_password = null;
+	
+	/**
+	 * URL to send acknowledgement receipt after collecting a package. Any URL, including mailto (as per RFC 2368 and RFC 2822). Required.
+	 * @var string
+	 */
+	protected $_receipt = null;
+	
+	/**
+	 * URL to send alerts to while processing the package. Any URL, including mailto (as per RFC 2368 and RFC 2822). Optional.
+	 * @var string
+	 */
+	protected $_alerts = null;
+	
+	/**
+	 * Unstructured character string that may be used to transfer vendor-specific data such as processing hints or deployment information. Optional.
+	 * @var string
+	 */
+	protected $_vendor_data = null;
+	
+	/**
+	 * Constructor
+	 * 
+	 * Constructs a PENSRequest based class using the arguments given
+	 * 
+	 * @param array Arguments
+	 */
+	public function __construct($arguments) {
+		$this->setPensVersion($arguments["pens-version"]);
+		$this->setPackageType($arguments["package-type"]);
+		$this->setPackageTypeVersion($arguments["package-type-version"]);
+		$this->setPackageFormat($arguments["package-format"]);
+		$this->setPackageId($arguments["package-id"]);
+		$this->setPackageUrl($arguments["package-url"]);
+		$this->setPackageUrlUserId($arguments["package-url-user-id"]);
+		$this->setPackageUrlAccount($arguments["package-url-account"]);
+		$this->setPackageUrlPassword($arguments["package-url-password"]);
+		$this->setPackageUrlExpiry($arguments["package-url-expiry"]);
+		$this->setClient($arguments["client"]);
+		$this->setSystemUserId($arguments["system-user-id"]);
+		$this->setSystemPassword($arguments["system-password"]);
+		$this->setReceipt($arguments["receipt"]);
+		$this->setAlerts($arguments["alerts"]);
+		$this->setVendorData($arguments["vendor-data"]);
+	}
+	
+	public function getPensVersion() {
+		return $this->_pens_version;
+	}
+	
+	/**
+	 * Sets the PENS version
+	 * 
+	 * @param string PENS version
+	 * 
+	 * @throws PENSException with code 2001 if invalid
+	 */
+	public function setPensVersion($pens_version) {
+		if($pens_version == PENSConfig::$version) {
+			$this->_pens_version = $pens_version;
+		} else {
+			throw new PENSException(2001);
+		}
+	}
+	
+	public function getCommand() {
+		return $this->_command;
+	}
+	
+	/**
+	 * Sets the command
+	 * 
+	 * @param string command
+	 * 
+	 * @throws PENSException with code 2002 if invalid
+	 */
+	protected function setCommand($command) {
+		if(in_array($command, PENSConfig::$allowed_commands)) {
+			$this->_command = $command;
+		} else {
+			throw new PENSException(2002);
+		}
+	}
+	
+	public function getPackageType() {
+		return $this->_package_type;
+	}
+	
+	/**
+	 * Sets the package type
+	 * 
+	 * @param string package type
+	 * 
+	 * @throws PENSException with code 2003 if invalid
+	 */
+	public function setPackageType($package_type) {
+		if(in_array($package_type, PENSConfig::$allowed_package_types)) {
+			$this->_package_type = $package_type;
+		} else {
+			throw new PENSException(2003);
+		}
+	}
+	
+	public function getPackageTypeVersion() {
+		return $this->_package_type_version;
+	}
+	
+	/**
+	 * Sets the package type version
+	 * 
+	 * @param string package type version
+	 * 
+	 * @throws PENSException with code 2004 if invalid
+	 */
+	public function setPackageTypeVersion($package_type_version) {
+		if(empty($package_type_version)) {
+			throw new PENSException(2004);
+		} else {
+			$this->_package_type_version = $package_type_version;
+		}
+	}
+	
+	public function getPackageFormat() {
+		return $this->_package_format;
+	}
+	
+	/**
+	 * Sets the package format
+	 * 
+	 * @param string package format
+	 * 
+	 * @throws PENSException with code 2005 if invalid
+	 */
+	public function setPackageFormat($package_format) {
+		if(in_array($package_format, PENSConfig::$allowed_package_formats)) {
+			$this->_package_format = $package_format;
+		} else {
+			throw new PENSException(2005);
+		}
+	}
+	
+	public function getPackageId() {
+		return $this->_package_id;
+	}
+	
+	/**
+	 * Sets the package Id
+	 * 
+	 * @param string package Id
+	 * 
+	 * @throws PENSException with code 2007 if invalid
+	 */
+	public function setPackageId($package_id) {
+		if (preg_match('/'.ABSOLUTEURI_2396.'/', $package_id)) {
+			$this->_package_id = $package_id;
+		} else {
+			throw new PENSException(2007);
+		}
+	}
+	
+	public function getPackageUrl() {
+		return $this->_package_url;
+	}
+	
+	/**
+	 * Sets the package url
+	 * 
+	 * @param string package url
+	 * 
+	 * @throws PENSException with code 2008 if invalid
+	 */
+	public function setPackageUrl($package_url) {
+		if (preg_match('/'.ABSOLUTEURI_2396.'/', $package_url) && substr($package_url, -4) == ".".$this->_package_format) {
+			$this->_package_url = $package_url;
+		} else {
+			throw new PENSException(2008);
+		}
+	}
+	
+	public function getFilename() {
+		return substr(strrchr($this->_package_url, "/"), 1);
+	}
+	
+	public function getPackageUrlUserId() {
+		return $this->_package_url_user_id;
+	}
+	
+	public function setPackageUrlUserId($package_url_user_id) {
+		if(!empty($package_url_user_id)) {
+			$this->_package_url_user_id = $package_url_user_id;
+		}
+	}
+	
+	public function getPackageUrlAccount() {
+		return $this->_package_url_account;
+	}
+	
+	public function setPackageUrlAccount($package_url_account) {
+		if(!empty($package_url_account)) {
+			$this->_package_url_account = $package_url_account;
+		}
+	}
+	
+	public function getPackageUrlPassword() {
+		return $this->_package_url_password;
+	}
+	
+	public function setPackageUrlPassword($package_url_password) {
+		if(!empty($package_url_password)) {
+			$this->_package_url_password = $package_url_password;
+		}
+	}
+	
+	public function getPackageUrlExpiry() {
+		return $this->_package_url_expiry;
+	}
+	
+	/**
+	 * Sets the package url expiry and transforms it into a php DateTime object
+	 * 
+	 * @param string package url expiry
+	 * 
+	 * @throws PENSException with code 2009 if invalid
+	 * @todo Perform a better validation of the date
+	 */
+	public function setPackageUrlExpiry($package_url_expiry) {
+		if(empty($package_url_expiry)) {
+			throw new PENSException(2009);
+		} else {
+			try {
+				$expiry = new DateTime($package_url_expiry, new DateTimeZone('UTC'));
+				$this->_package_url_expiry = $expiry;
+			} catch(Exception $e) {
+				throw new PENSException(2009);
+			}
+		}
+	}
+	
+	public function getClient() {
+		return $this->_client;
+	}
+	
+	/**
+	 * Sets the client
+	 * 
+	 * @param string client
+	 * 
+	 * @throws PENSException with code 2010 if invalid
+	 */
+	public function setClient($client) {
+		if(!empty($client)) {
+			$this->_client = $client;
+		} else {
+			throw new PENSException(2010);
+		}
+	}
+	
+	public function getSystemUserId() {
+		return $this->_system_user_id;
+	}
+	
+	public function setSystemUserId($system_user_id) {
+		if(!empty($system_user_id)) {
+			$this->_system_user_id = $system_user_id;
+		}
+	}
+	
+	public function getSystemPassword() {
+		return $this->_system_password;
+	}
+	
+	public function setSystemPassword($system_password) {
+		if(!empty($system_password)) {
+			$this->_system_password = $system_password;
+		}
+	}
+	
+	public function getReceipt() {
+		return $this->_receipt;
+	}
+	
+	/**
+	 * Sets the receipt url
+	 * 
+	 * @param string receipt url
+	 * 
+	 * @throws PENSException with code 2011 if invalid
+	 */
+	public function setReceipt($receipt) {
+		if($this instanceof PENSRequestCollect) {
+			if (preg_match('/'.ABSOLUTEURI_2396.'/', $receipt)) {
+				$this->_receipt = $receipt;
+			} else {
+				throw new PENSException(2011);
+			}
+		}
+	}
+	
+	public function getAlerts() {
+		return $this->_alerts;
+	}
+	
+	public function setAlerts($alerts) {
+		if(!empty($alerts)) {
+			if(preg_match('/'.ABSOLUTEURI_2396.'/', $alerts)) {
+				$this->_alerts = $alerts;
+			} else {
+				throw new PENSException(1201);
+			}
+		}
+	}
+	
+	public function getVendorData() {
+		return $this->_vendor_data;
+	}
+	
+	public function setVendorData($vendor_data) {
+		if(!empty($vendor_data)) {
+			$this->_vendor_data = $vendor_data;
+		}
+	}
+	
+	/**
+	 * Returns an associative that contains all the fields needed to send a
+	 * receipt or an alert to the client
+	 * 
+	 * @return array Associative array
+	 */
+	protected function getSendReceiptAlertArray() {
+		return array("pens-version" => $this->getPensVersion(),
+			"package-type" => $this->getPackageType(),
+			"package-type-version" => $this->getPackageTypeVersion(),
+			"package-format" => $this->getPackageFormat(),
+			"package-id" => $this->getPackageId(),
+			"package-url" => $this->getPackageUrl(),
+			"package-url-expiry" => $this->getPackageUrlExpiry()->format(DateTime::ISO8601),
+			"client" => $this->getClient());
+	}
+	
+	/**
+	 * Returns an associative that contains all the fields needed to send a
+	 * receipt to the client
+	 * 
+	 * @return array Associative array
+	 */
+	public function getSendReceiptArray() {
+		$receipt = $this->getSendReceiptAlertArray();
+		$receipt["command"] = "receipt";
+		return $receipt;
+	}
+	
+	/**
+	 * Returns an associative that contains all the fields needed to send an
+	 * alert to the client
+	 * 
+	 * @return array Associative array
+	 */
+	public function getSendAlertArray() {
+		$alert = $this->getSendReceiptAlertArray();
+		$alert["command"] = "alert";
+		return $alert;
+	}
+	
+	
+}

+ 48 - 0
plugin/pens/lib/pens/pens_request_alert.php

@@ -0,0 +1,48 @@
+<?php
+/**
+ * This file is part of php-pens.
+ * 
+ * php-pens is free software: you can redistribute it and/or modify 
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ * 
+ * php-pens is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ * 
+ * You should have received a copy of the GNU General Public License
+ * along with php-pens.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+/**
+ * PENSRequestAlert
+ * 
+ * Provides the PENSRequestAlert class
+ * 
+ * @package PENS
+ * @author Guillaume Viguier-Just <guillaume@viguierjust.com>
+ * @licence http://www.gnu.org/licenses/gpl.txt
+ */
+
+require_once __DIR__ . '/pens_request.php';
+
+/**
+ * PENSRequestAlert
+ * 
+ * PENS Request of type alert
+ * 
+ * @package PENS
+ * @author Guillaume Viguier-Just <guillaume@viguierjust.com>
+ * @licence http://www.gnu.org/licenses/gpl.txt
+ */
+
+class PENSRequestAlert extends PENSRequest {
+	
+	public function __construct($arguments) {
+		parent::__construct($arguments);
+		$this->setCommand("alert");
+	}
+
+}

+ 48 - 0
plugin/pens/lib/pens/pens_request_collect.php

@@ -0,0 +1,48 @@
+<?php
+/**
+ * This file is part of php-pens.
+ * 
+ * php-pens is free software: you can redistribute it and/or modify 
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ * 
+ * php-pens is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ * 
+ * You should have received a copy of the GNU General Public License
+ * along with php-pens.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+/**
+ * PENSRequestCollect
+ * 
+ * Provides the PENSRequestCollect class
+ * 
+ * @package PENS
+ * @author Guillaume Viguier-Just <guillaume@viguierjust.com>
+ * @licence http://www.gnu.org/licenses/gpl.txt
+ */
+
+require_once __DIR__ . '/pens_request.php';
+
+/**
+ * PENSRequestCollect
+ * 
+ * PENS Request of type collect
+ * 
+ * @package PENS
+ * @author Guillaume Viguier-Just <guillaume@viguierjust.com>
+ * @licence http://www.gnu.org/licenses/gpl.txt
+ */
+
+class PENSRequestCollect extends PENSRequest {
+	
+	public function __construct($arguments) {
+		parent::__construct($arguments);
+		$this->setCommand("collect");
+	}
+
+}

+ 68 - 0
plugin/pens/lib/pens/pens_request_factory.php

@@ -0,0 +1,68 @@
+<?php
+
+/**
+ * This file is part of php-pens.
+ * 
+ * php-pens is free software: you can redistribute it and/or modify 
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ * 
+ * php-pens is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ * 
+ * You should have received a copy of the GNU General Public License
+ * along with php-pens.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+/**
+ * PENSRequestFactory
+ * 
+ * Provides the PENSRequestFactory class
+ * 
+ * @package PENS
+ * @author Guillaume Viguier-Just <guillaume@viguierjust.com>
+ * @licence http://www.gnu.org/licenses/gpl.txt
+ */
+
+require_once __DIR__ . '/pens_exception.php';
+require_once __DIR__ . '/pens_request_receipt.php';
+require_once __DIR__ . '/pens_request_collect.php';
+require_once __DIR__ . '/pens_request_alert.php';
+
+/**
+ * PENSRequestFactory
+ * 
+ * Class used to create a PENSRequestCollect, PENSRequestReceipt or PENSRequestAlert object, depending on the "command" argument
+ * 
+ * @package PENS
+ * @author Guillaume Viguier-Just <guillaume@viguierjust.com>
+ * @licence http://www.gnu.org/licenses/gpl.txt
+ */
+class PENSRequestFactory {
+
+	/**
+	 * Factory method
+	 * 
+	 * Creates the right PENSRequest object, based on the command inside the arguments
+	 * 
+	 * @param array Associative array of arguments
+	 * @return mixed PENSRequestAlert or PENSRequestCollect or PENSRequestReceipt
+	 * 
+	 * @throws PENSException with code 2002 if command is invalid
+	 */
+	public static function createPENSRequest($arguments) {
+		$command = $arguments["command"];
+		if($command == "alert") {
+			return new PENSRequestAlert($arguments);
+		} else if($command == "collect") {
+			return new PENSRequestCollect($arguments);
+		} else if($command == "receipt") {
+			return new PENSRequestReceipt($arguments);
+		} else {
+			throw new PENSException(2002);
+		}
+	}
+}

+ 48 - 0
plugin/pens/lib/pens/pens_request_handler.php

@@ -0,0 +1,48 @@
+<?php
+/**
+ * This file is part of php-pens.
+ * 
+ * php-pens is free software: you can redistribute it and/or modify 
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ * 
+ * php-pens is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ * 
+ * You should have received a copy of the GNU General Public License
+ * along with php-pens.  If not, see <http://www.gnu.org/licenses/>.
+ */
+ 
+ /**
+ * PENSRequestHandler
+ * 
+ * Provides the PENSRequestHandler abstract class
+ * 
+ * @package PENS
+ * @author Guillaume Viguier-Just <guillaume@viguierjust.com>
+ * @licence http://www.gnu.org/licenses/gpl.txt
+ */
+
+/**
+ * PENSRequestHandler
+ * 
+ * This class is an abstract class used to handle the processing of the requests on the client side. Create a class that inherits from
+ * this class and implements the processRequest method, then set the request handler of the PENSClient object.
+ * 
+ * @package PENS
+ * @author Guillaume Viguier-Just <guillaume@viguierjust.com>
+ * @licence http://www.gnu.org/licenses/gpl.txt
+ */
+abstract class PENSRequestHandler {
+	/**
+	 * Processes the request. Must be implemented by subclasses
+	 * 
+	 * @param PENSRequest Alert or Receipt request sent by the server
+	 * @param PENSResponse Response sent by the server (error, error-text etc...)
+	 */
+	abstract public function processRequest($request, $response);
+}
+

+ 48 - 0
plugin/pens/lib/pens/pens_request_receipt.php

@@ -0,0 +1,48 @@
+<?php
+/**
+ * This file is part of php-pens.
+ * 
+ * php-pens is free software: you can redistribute it and/or modify 
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ * 
+ * php-pens is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ * 
+ * You should have received a copy of the GNU General Public License
+ * along with php-pens.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+/**
+ * PENSRequestReceipt
+ * 
+ * Provides the PENSRequestReceipt class
+ * 
+ * @package PENS
+ * @author Guillaume Viguier-Just <guillaume@viguierjust.com>
+ * @licence http://www.gnu.org/licenses/gpl.txt
+ */
+
+require_once __DIR__ . '/pens_request.php';
+
+/**
+ * PENSRequestReceipt
+ * 
+ * PENS Request of type receipt
+ * 
+ * @package PENS
+ * @author Guillaume Viguier-Just <guillaume@viguierjust.com>
+ * @licence http://www.gnu.org/licenses/gpl.txt
+ */
+
+class PENSRequestReceipt extends PENSRequest {
+	
+	public function __construct($arguments) {
+		parent::__construct($arguments);
+		$this->setCommand("receipt");
+	}
+
+}

+ 175 - 0
plugin/pens/lib/pens/pens_response.php

@@ -0,0 +1,175 @@
+<?php
+/**
+ * This file is part of php-pens.
+ * 
+ * php-pens is free software: you can redistribute it and/or modify 
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ * 
+ * php-pens is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ * 
+ * You should have received a copy of the GNU General Public License
+ * along with php-pens.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+/**
+ * PENSResponse
+ * 
+ * Provides the PENSResponse class
+ * 
+ * @package PENS
+ * @author Guillaume Viguier-Just <guillaume@viguierjust.com>
+ * @licence http://www.gnu.org/licenses/gpl.txt
+ */
+
+require_once __DIR__ . '/pens_config.php';
+require_once __DIR__ . '/pens_message.php';
+require_once __DIR__ . '/pens_exception.php';
+
+/**
+ * PENSResponse
+ * 
+ * PENSResponse class
+ * 
+ * @package PENS
+ * @author Guillaume Viguier-Just <guillaume@viguierjust.com>
+ * @licence http://www.gnu.org/licenses/gpl.txt
+ */
+ class PENSResponse extends PENSMessage {
+	 /**
+	  * Error code
+	  * @var int
+	  */
+	 protected $_error = null;
+	 
+	 /**
+	  * Descriptive text of the error
+	  * @var string
+	  */
+	 protected $_error_text = null;
+	 
+	 /**
+	  * Data linked to the error
+	  * @var string
+	  */
+	 protected $_pens_data = null;
+	 
+	 /**
+	  * Version
+	  * @var string
+	  */
+	 protected $_pens_version = null;
+	 
+	 /**
+	  * Constructor
+	  * 
+	  * @param mixed Can be a PENSException or an error code or a string representing an HTTP response
+	  * @param string error text
+	  * @param string PENS data
+	  */
+	public function __construct($error, $error_text = null, $pens_data = null) {
+		$this->_pens_version = PENSConfig::$version;
+		if($error instanceof PENSException) {
+			$this->_error = $error->getCode();
+			$this->_error_text = $error->getMessage();
+		} else if(is_string($error)){
+			// Parse the string
+			$this->parseResponse($error);
+		} else if(is_array($error)) {
+			// Try to build from array
+			$this->_error = $error["error"];
+			$this->_error_text = $error["error-text"];
+			$this->_pens_data = $error["pens-data"];
+		} else {
+			$this->_error = $error;
+			$this->_error_text = $error_text;
+			$this->_pens_data = $pens_data;
+		}
+		
+	}
+	
+	/**
+	 * Parses an HTTP response and assigns the attributes of the object
+	 * 
+	 * @param string HTTP response
+	 */
+	protected function parseResponse($response) {
+		$lines = explode(PENSConfig::$eol, $response);
+		$i = 1;
+		foreach($lines as $line) {
+			if($i < 5) {
+				$pair = explode("=", $line);
+				if($pair[0] == "error") {
+					$this->_error = intval($pair[1]);
+				} else if($pair[0] == "error-text") {
+					$this->_error_text = $pair[1];
+				} else if($pair[0] == "version") {
+					$this->_pens_version = $pair[1];
+				} else if($pair[0] == "pens-data") {
+					if(!empty($pair[1])) {
+						$this->_pens_data = $pair[1].PENSConfig::$eol;
+					}
+				}
+			} else {
+				if(!empty($line)) {
+					$this->_pens_data .= $line.PENSConfig::$eol;
+				}
+			}
+			$i++;
+		}
+	}
+	
+	public function getError() {
+		return $this->_error;
+	}
+	
+	public function getErrorText() {
+		return $this->_error_text;
+	}
+	
+	public function getPensData() {
+		return $this->_pens_data;
+	}
+	
+	public function getVersion() {
+		return $this->_pens_version;
+	}
+	
+	/**
+	 * Sends the response content through HTTP headers
+	 */
+	public function send() {
+		header("Content-Type: text/plain");
+		print $this;
+	}
+	
+	/**
+	 * Returns an associative array of the response
+	 * 
+	 * @return array Associative array
+	 */
+	public function getArray() {
+		return array("error" => $this->_error,
+			"error-text" => $this->_error_text,
+			"version" => PENSConfig::$version,
+			"pens-data" => $this->_pens_data);
+	}
+	
+	/**
+	 * Transforms the object into a string
+	 */
+	public function __toString() {
+		$eol = PENSConfig::$eol;
+		$return = "error=".$this->_error.$eol;
+		$return .= "error-text=".$this->_error_text.$eol;
+		$return .= "version=".PENSConfig::$version.$eol;
+		$return .= "pens-data=".$this->_pens_data.$eol;
+		return $return;
+	}
+ 
+ }
+ 

+ 313 - 0
plugin/pens/lib/pens/pens_server.php

@@ -0,0 +1,313 @@
+<?php
+/**
+ * This file is part of php-pens.
+ * 
+ * php-pens is free software: you can redistribute it and/or modify 
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ * 
+ * php-pens is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ * 
+ * You should have received a copy of the GNU General Public License
+ * along with php-pens.  If not, see <http://www.gnu.org/licenses/>.
+ */
+ 
+ /**
+ * PENSServer
+ * 
+ * Provides the PENSServer class
+ * 
+ * @package PENS
+ * @author Guillaume Viguier-Just <guillaume@viguierjust.com>
+ * @licence http://www.gnu.org/licenses/gpl.txt
+ */
+
+require_once __DIR__ . '/pens_controller.php';
+require_once __DIR__ . '/pens_package_handler.php';
+require_once __DIR__ . '/pens_exception.php';
+require_once __DIR__ . '/pens_response.php';
+
+/**
+ * PENSServer
+ * 
+ * Class that implements the PENS Server
+ * 
+ * @package PENS
+ * @author Guillaume Viguier-Just <guillaume@viguierjust.com>
+ * @licence http://www.gnu.org/licenses/gpl.txt
+ */
+class PENSServer extends PENSController {
+
+	/**
+	 * Instance of the PENSServer
+	 * @var PENSServer
+	 */
+	private static $_instance;
+	
+	/**
+	 * Package handler
+	 * @var PENSPackageHandler
+	 */
+	protected $_package_handler = null;
+	
+	/**
+	 * Private constructor
+	 */
+	private function __construct() {
+	}
+	
+	/**
+	 * Singleton method
+	 */
+	public static function singleton() {
+		if(!isset(self::$_instance)) {
+			$c = __CLASS__;
+			self::$_instance = new $c;
+		}
+		return self::$_instance;
+	}
+	
+	/**
+	 * Prevent users to clone the instance
+	 */
+	public function __clone() {
+		trigger_error('Clone is not allowed', E_USER_ERROR);
+	}
+	
+	public function getPackageHandler() {
+		return $this->_package_handler;
+	}
+	
+	/**
+	 * Sets the package handler. Does nothing if the argument is not an instance of PENSPackageHandler
+	 * 
+	 * @param PENSPackageHandler Package handler
+	 */
+	public function setPackageHandler($package_handler) {
+		if($package_handler instanceof PENSPackageHandler) {
+			$this->_package_handler = $package_handler;
+		}
+	}
+	
+	/**
+	 * Receives a collect request and treats it
+	 */
+	public function receiveCollect() {
+		$request = null;
+		try {
+			// First, try to parse the request
+			$request = $this->parseRequest();
+			if($request->getCommand() == "collect") {
+				if(isset($_REQUEST['process'])) {
+					// Collect the package and process it
+					$receipt = null;
+					$path_to_package = null;
+					try {
+						// Collect the package
+						$path_to_package = $this->collectPackage($request);
+						$receipt = new PENSResponse(0, "package successfully collected");
+					} catch(PENSException $e) {
+						$receipt = new PENSResponse($e);
+					}
+					// Send receipt
+					$response = $this->sendReceipt($request, $receipt);
+					if(!is_null($response) && !is_null($path_to_package)) {
+						if($response->getError() === 0) {
+							// Process package
+							$this->processPackage($request, $path_to_package);
+						}
+						unlink($path_to_package);
+					}
+				} else {
+					// Then, send a success response to the client
+					$this->sendResponse(new PENSResponse(0, "collect command received and understood"));
+					// Send a request to process the package: fake multithreading
+					$params = $_REQUEST;
+					$params['process'] = 1;
+					$scheme = "http";
+					if(!empty($_SERVER['HTTPS'])) {
+						$scheme = "https";
+					}
+					$ch = curl_init();
+					curl_setopt($ch, CURLOPT_URL, $scheme."://".$_SERVER['HTTP_HOST'].$_SERVER['REQUEST_URI']);
+					curl_setopt($ch, CURLOPT_POSTFIELDS, $params);
+					curl_setopt($ch, CURLOPT_TIMEOUT, 1);
+					curl_exec($ch);
+					curl_close($ch);
+				}
+			}
+				
+		} catch(PENSException $e) {
+			// If we could not parse the request, send the error back to the client
+			$this->sendResponse(new PENSResponse($e));
+		}
+	}
+	
+	/**
+	 * Collects the package onto the local server
+	 * 
+	 * @param PENSRequest request
+	 * @return string Path to the package on the hard drive
+	 * @throws PENSException if an exception occured
+	 */
+	protected function collectPackage($request) {
+		$supported_package_types = $this->_package_handler->getSupportedPackageTypes();
+		if(!in_array($request->getPackageType(), $supported_package_types)) {
+			throw new PENSException(1430);
+		}
+		$supported_package_formats = $this->_package_handler->getSupportedPackageFormats();
+		if(!in_array($request->getPackageFormat(), $supported_package_formats)) {
+			throw new PENSException(1431);
+		}
+		if(!$this->isExpiryDateValid($request->getPackageUrlExpiry())) {
+			throw new PENSException(1322);
+		}
+		
+		// Try to download the package in the temporary directory
+		$tmp = null;
+		if(function_exists("sys_get_temp_dir")) {
+			$tmp = sys_get_temp_dir();
+		} else {
+			$tmp = "/tmp";
+		}
+		$path_to_file = $tmp."/".$request->getFilename();
+		$fp = fopen($path_to_file, 'w');
+		$ch = curl_init();
+		curl_setopt($ch, CURLOPT_URL, $request->getPackageUrl());
+		curl_setopt($ch, CURLOPT_HEADER, false);	
+		curl_setopt($ch, CURLOPT_FILE, $fp);
+		if(!is_null($request->getPackageUrlUserId())) {
+			curl_setopt($ch, CURLOPT_USERPWD, $request->getPackageUrlUserId().":".$request->getPackageUrlPassword());
+		}
+		if(curl_exec($ch) === false) {
+			$errno = curl_errno($ch);
+			curl_close($ch);
+			// Error occured. Throw an exception
+			switch($errno) {
+				case CURLE_UNSUPPORTED_PROTOCOL:
+					throw new PENSException(1301);
+					break;
+				case CURLE_URL_MALFORMAT:
+				case CURLE_COULDNT_RESOLVE_PROXY:
+				case CURLE_COULDNT_RESOLVE_HOST:
+				case CURLE_COULDNT_CONNECT:
+				case CURLE_OPERATION_TIMEOUT:
+				case CURLE_REMOTE_FILE_NOT_FOUND:
+					throw new PENSException(1310);
+					break;
+				case CURLE_REMOTE_ACCESS_DENIED:
+					throw new PENSException(1312);
+					break;
+				default:
+					throw new PENSException(1301);
+					break;
+			}
+			return null;
+				
+		} else {
+			curl_close($ch);
+			return $path_to_file;
+		}
+	}
+	
+	/**
+	 * Verifies that the package url is not expired
+	 * 
+	 * @param DateTime DateTime object to verify against current time
+	 */
+	protected function isExpiryDateValid($expiry) {
+		date_default_timezone_set('UTC');
+		$current_time = time();
+		$expiry_time = strtotime($expiry->format(DateTime::ISO8601));
+		if($current_time > $expiry_time) {
+			return false;
+		} else {
+			return true;
+		}
+	}
+	
+	/**
+	 * Sends an alert or a receipt. Called by sendReceipt and sendAlert
+	 * 
+	 * @param PENSRequest Original collect request
+	 * @param PENSResponse Reponse to send in the receipt or the alert
+	 * @param string Mode (alert | receipt)
+	 * @return PENSResponse Response
+	 */
+	protected function sendAlertOrReceipt($request, $response, $mode) {
+		if($mode == "alert") {
+			$url = $request->getAlerts();
+		} else {
+			$url = $request->getReceipt();
+		}
+		if(!empty($url)) {
+			$url_components = parse_url($url);
+			$scheme = $url_components["scheme"];
+			if($scheme == "mailto") {
+				$to = $url_components["path"];
+				if($mode == "alert") {
+					$subject = "PENS Alert for ".$request->getPackageId();
+				} else {
+					$subject = "PENS Receipt for ".$request->getPackageId();
+				}
+				$message = $response->__toString();
+				mail($to, $subject, $message);
+				return new PENSResponse(0, "");
+			} else if($scheme == "http" || $scheme == "https") {
+				if($mode == "alert") {
+					$params = array_merge($request->getSendAlertArray(), $response->getArray());
+				} else {
+					$params = array_merge($request->getSendReceiptArray(), $response->getArray());
+				}
+				$ch = curl_init($url);
+				curl_setopt($ch, CURLOPT_POST, true);
+				curl_setopt($ch, CURLOPT_POSTFIELDS, $params);
+				curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
+				$data = curl_exec($ch);
+				curl_close($ch);
+				if($data === false) {
+					return null;
+				} else {
+					return new PENSResponse($data);
+				}
+			}
+		}
+	}
+	
+	/**
+	 * Sends a receipt. Returns a PENSResponse in case of success, null if a problem occured
+	 * 
+	 * @param PENSRequest Original collect request
+	 * @param PENSResponse Response to send in the receipt
+	 * @return PENSResponse Response
+	 */
+	protected function sendReceipt($request, $receipt) {
+		return $this->sendAlertOrReceipt($request, $receipt, "receipt");
+	}
+	
+	/**
+	 * Processes the package using the handler provided
+	 * 
+	 * @param PENSRequest Original collect request
+	 * @param string Path to the package on the hard drive
+	 */
+	protected function processPackage($request, $path_to_package) {
+		return $this->_package_handler->processPackage($request, $path_to_package);
+	}
+	
+	/**
+	 * Sends an alert to the client. Returns a PENSResponse in case of success, null if a problem occured
+	 * 
+	 * @param PENSRequest Original collect request
+	 * @param PENSResponse Response to send in the alert
+	 * @return PENSResponse Response
+	 */
+	public function sendAlert($request, $alert) {
+		return $this->sendAlertOrReceipt($request, $alert, "alert");
+	}
+	
+}

+ 39 - 0
plugin/pens/lib/pens_client_test.php

@@ -0,0 +1,39 @@
+<?php
+/**
+ * This file is part of php-pens.
+ * 
+ * php-pens is free software: you can redistribute it and/or modify 
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ * 
+ * php-pens is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ * 
+ * You should have received a copy of the GNU General Public License
+ * along with php-pens.  If not, see <http://www.gnu.org/licenses/>.
+ */
+ 
+/**
+ * Provides a test implementation of a PENS Client.
+ * 
+ * @author Guillaume Viguier-Just <guillaume@viguierjust.com>
+ * @licence http://www.gnu.org/licenses/gpl.txt
+ */
+
+require_once __DIR__ . '/pens.php';
+
+class MyRequestHandler extends PENSRequestHandler {
+	public function processRequest($request, $response) {
+		// Do the processing of the alert or the receipt here
+	}
+}
+
+$handler = new MyRequestHandler();
+
+$client = PENSClient::singleton();
+$client->setRequestHandler($handler);
+
+$client->receiveRequest();

+ 41 - 0
plugin/pens/lib/pens_server_test.php

@@ -0,0 +1,41 @@
+<?php
+/**
+ * This file is part of php-pens.
+ * 
+ * php-pens is free software: you can redistribute it and/or modify 
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ * 
+ * php-pens is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ * 
+ * You should have received a copy of the GNU General Public License
+ * along with php-pens.  If not, see <http://www.gnu.org/licenses/>.
+ */
+ 
+/**
+ * Provides a test implementation of a PENS Server.
+ * 
+ * @author Guillaume Viguier-Just <guillaume@viguierjust.com>
+ * @licence http://www.gnu.org/licenses/gpl.txt
+ */
+
+require_once __DIR__ . '/pens.php';
+
+class MyPackageHandler extends PENSPackageHandler {
+	public function processPackage($request, $path_to_package) {
+		// Do the package processing here
+	}
+}
+
+$handler = new MyPackageHandler();
+$handler->setSupportedPackageTypes(array("scorm-pif", "ims-qti", "aicc-pkg"));
+$handler->setSupportedPackageFormats(array("zip"));
+
+$server = PENSServer::singleton();
+$server->setPackageHandler($handler);
+
+$server->receiveCollect();

+ 87 - 0
plugin/pens/lib/tests/test_pens_package_handler.php

@@ -0,0 +1,87 @@
+<?php
+
+/**
+ * This file is part of php-pens.
+ * 
+ * php-pens is free software: you can redistribute it and/or modify 
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ * 
+ * php-pens is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ * 
+ * You should have received a copy of the GNU General Public License
+ * along with php-pens.  If not, see <http://www.gnu.org/licenses/>.
+ */ 
+
+
+/**
+ * Unit tests for the PENSPackageHandler class
+ * 
+ * This class provides unit tests for the PENSPackageHandler class
+ * 
+ * @package PENS
+ * @subpackage Tests
+ * @author Guillaume Viguier-Just <guillaume@viguierjust.com>
+ * @licence http://www.gnu.org/licenses/gpl.txt
+ */
+
+require_once("simpletest/autorun.php");
+require_once(dirname(__FILE__)."/../pens.php");
+
+class MyPackageHandler extends PENSPackageHandler {
+	public function processPackage($request, $path_to_package) {
+	}
+}
+
+/**
+ * Unit tests for the PENSPackageHandler class
+ * 
+ * This class provides unit tests for the PENSPackageHandler class
+ * 
+ * @package PENS
+ * @subpackage Tests
+ * @author Guillaume Viguier-Just <guillaume@viguierjust.com>
+ * @licence http://www.gnu.org/licenses/gpl.txt
+ */
+class TestPENSPackageHandler extends UnitTestCase {
+	
+	public function testSupportedPackageTypesInvalid() {
+		$object = new MyPackageHandler();
+		$object->setSupportedPackageTypes("testing");
+		$this->assertEqual($object->getSupportedPackageTypes(), null);
+	}
+	
+	public function testSupportedPackageTypesInvalid2() {
+		$object = new MyPackageHandler();
+		$object->setSupportedPackageTypes(array("aicc-pkg", "testing"));
+		$this->assertEqual($object->getSupportedPackageTypes(), array("aicc-pkg"));
+	}
+	
+	public function testSupportedPackageTypesValid() {
+		$object = new MyPackageHandler();
+		$object->setSupportedPackageTypes(array("aicc-pkg", "lms-qti"));
+		$this->assertEqual($object->getSupportedPackageTypes(), array("aicc-pkg", "lms-qti"));
+	}
+	
+	public function testSupportedPackageFormatsInvalid() {
+		$object = new MyPackageHandler();
+		$object->setSupportedPackageFormats("testing");
+		$this->assertEqual($object->getSupportedPackageFormats(), null);
+	}
+	
+	public function testSupportedPackageFormatsInvalid2() {
+		$object = new MyPackageHandler();
+		$object->setSupportedPackageFormats(array("zip", "testing"));
+		$this->assertEqual($object->getSupportedPackageFormats(), array("zip"));
+	}
+	
+	public function testSupportedPackageFormatsValid() {
+		$object = new MyPackageHandler();
+		$object->setSupportedPackageFormats(array("zip", "jar"));
+		$this->assertEqual($object->getSupportedPackageFormats(), array("zip", "jar"));
+	}
+}

+ 213 - 0
plugin/pens/lib/tests/test_pens_request.php

@@ -0,0 +1,213 @@
+<?php
+
+/**
+ * This file is part of php-pens.
+ * 
+ * php-pens is free software: you can redistribute it and/or modify 
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ * 
+ * php-pens is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ * 
+ * You should have received a copy of the GNU General Public License
+ * along with php-pens.  If not, see <http://www.gnu.org/licenses/>.
+ */ 
+
+
+/**
+ * Unit tests for the PENSRequest class
+ * 
+ * This class provides unit tests for the PENSRequest class
+ * 
+ * @package PENS
+ * @subpackage Tests
+ * @author Guillaume Viguier-Just <guillaume@viguierjust.com>
+ * @licence http://www.gnu.org/licenses/gpl.txt
+ */
+
+require_once("simpletest/autorun.php");
+require_once(dirname(__FILE__)."/../pens.php");
+
+/**
+ * Unit tests for the PENSRequest class
+ * 
+ * This class provides unit tests for the PENSRequest class
+ * 
+ * @package PENS
+ * @subpackage Tests
+ * @author Guillaume Viguier-Just <guillaume@viguierjust.com>
+ * @licence http://www.gnu.org/licenses/gpl.txt
+ */
+class TestPENSRequest extends UnitTestCase {
+	
+	/**
+	 * Valid arguments to be used for tests
+	 * 
+	 * @var array
+	 */
+	private $args = null;
+	
+	public function setUp() {
+		$this->args = array(
+			"pens-version" => "1.0.0",
+			"command" => "collect",
+			"package-type" => "aicc-pkg",
+			"package-type-version" => "1.0",
+			"package-format" => "zip",
+			"package-id" => "http://myurl.com/12345",
+			"package-url" => "http://myurl.com/mypackage.zip",
+			"package-url-expiry" => "2006-04-01T06:51:29Z",
+			"client" => "Authorware7",
+			"receipt" => "mailto:name@domain.com",
+			"package-url-user-id" => "guillaumev",
+			"package-url-account" => "toto",
+			"package-url-password" => "12345",
+			"system-user-id" => "guillaumev",
+			"system-password" => "12345",
+			"alerts" => "http://myurl.com/alerts",
+			"vendor-data" => "here are my data");
+	}
+	
+	public function createObject($myargs) {
+		return PENSRequestFactory::createPENSRequest($myargs);
+	}
+	
+	public function exceptionTestForValue($key, $value, $code) {
+		try {
+			$myargs = $this->args;
+			if($value === null) {
+				unset($myargs[$key]);
+			} else {
+				$myargs[$key] = $value;
+			}
+			$object = $this->createObject($myargs);
+			$this->fail();
+		} catch(PENSException $e) {
+			$this->assertEqual($e->getCode(), $code);
+		}
+	}
+	
+	public function testPensVersionInvalid() {
+		$this->exceptionTestForValue("pens-version", "0.8.0", 2001);
+	}
+	
+	public function testPensVersionNull() {
+		$this->exceptionTestForValue("pens-version", null, 2001);
+	}
+	
+	
+	public function testCommandInvalid() {
+		$this->exceptionTestForValue("command", "testing", 2002);
+	}
+	
+	public function testCommandNull() {
+		$this->exceptionTestForValue("command", null, 2002);
+	}
+	
+	public function testPackageTypeInvalid() {
+		$this->exceptionTestForValue("package-type", "testing", 2003);
+	}
+	
+	public function testPackageTypeNull() {
+		$this->exceptionTestForValue("package-type", null, 2003);
+	}
+	
+	public function testPackageTypeVersionNull() {
+		$this->exceptionTestForValue("package-type-version", null, 2004);
+	}
+	
+	public function testPackageFormatInvalid() {
+		$this->exceptionTestForValue("package-format", "testing", 2005);
+	}
+	
+	public function testPackageFormatNull() {
+		$this->exceptionTestForValue("package-format", null, 2005);
+	}
+	
+	public function testPackageIdInvalid() {
+		$this->exceptionTestForValue("package-id", "testing", 2007);
+	}
+	
+	public function testPackageIdNull() {
+		$this->exceptionTestForValue("package-id", null, 2007);
+	}
+	
+	public function testPackageUrlInvalid() {
+		$this->exceptionTestForValue("package-url", "testing", 2008);
+	}
+	
+	public function testPackageUrlInvalid2() {
+		$this->exceptionTestForValue("package-url", "http://myurl.com/mypackage", 2008);
+	}
+	
+	public function testPackageUrlNull() {
+		$this->exceptionTestForValue("package-url", null, 2008);
+	}
+	
+	public function testPackageUrlExpiryNull() {
+		$this->exceptionTestForValue("package-url-expiry", null, 2009);
+	}
+	
+	public function testPackageUrlExpiryInvalid() {
+		$this->exceptionTestForValue("package-url-expiry", "testing", 2009);
+	}
+	
+	public function testClientNull() {
+		$this->exceptionTestForValue("client", null, 2010);
+	}
+	
+	public function testReceiptNull() {
+		$this->exceptionTestForValue("receipt", null, 2011);
+	}
+	
+	public function testReceiptInvalid() {
+		$this->exceptionTestForValue("receipt", "testing", 2011);
+	}
+	
+	public function testAlertsInvalid() {
+		$this->exceptionTestForValue("alerts", "testing", 1201);
+	}
+	
+	public function testReceiptValid() {
+		try {
+			$myargs = $this->args;
+			$myargs["receipt"] = "mailto:name@domain.com,name2@domain.com";
+			$object = $this->createObject($myargs);
+			$this->pass();
+		} catch(PENSException $e) {
+			$this->fail();
+		}
+	}
+	
+	public function testValid() {
+		try {
+			$object = $this->createObject($this->args);
+			$this->assertIsA($object, "PENSRequestCollect");
+			$this->assertEqual($object->getPensVersion(), "1.0.0");
+			$this->assertEqual($object->getPackageType(), "aicc-pkg");
+			$this->assertEqual($object->getPackageTypeVersion(), "1.0");
+			$this->assertEqual($object->getPackageFormat(), "zip");
+			$this->assertEqual($object->getPackageId(), "http://myurl.com/12345");
+			$this->assertEqual($object->getPackageUrl(), "http://myurl.com/mypackage.zip");
+			$this->assertIsA($object->getPackageUrlExpiry(), "DateTime");
+			$this->assertEqual($object->getClient(), "Authorware7");
+			$this->assertEqual($object->getReceipt(), "mailto:name@domain.com");
+			$this->assertEqual($object->getPackageUrlUserId(), "guillaumev");
+			$this->assertEqual($object->getPackageUrlAccount(), "toto");
+			$this->assertEqual($object->getPackageUrlPassword(), "12345");
+			$this->assertEqual($object->getSystemUserId(), "guillaumev");
+			$this->assertEqual($object->getSystemPassword(), "12345");
+			$this->assertEqual($object->getAlerts(), "http://myurl.com/alerts");
+			$this->assertEqual($object->getVendorData(), "here are my data");
+			
+		} catch(PENSException $e) {
+			$this->fail();
+		}
+	}
+	
+	
+}

+ 68 - 0
plugin/pens/lib/tests/test_pens_response.php

@@ -0,0 +1,68 @@
+<?php
+
+/**
+ * This file is part of php-pens.
+ * 
+ * php-pens is free software: you can redistribute it and/or modify 
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ * 
+ * php-pens is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ * 
+ * You should have received a copy of the GNU General Public License
+ * along with php-pens.  If not, see <http://www.gnu.org/licenses/>.
+ */ 
+
+
+/**
+ * Unit tests for the PENSResponse class
+ * 
+ * This class provides unit tests for the PENSResponse class
+ * 
+ * @package PENS
+ * @subpackage Tests
+ * @author Guillaume Viguier-Just <guillaume@viguierjust.com>
+ * @licence http://www.gnu.org/licenses/gpl.txt
+ */
+
+require_once("simpletest/autorun.php");
+require_once(dirname(__FILE__)."/../pens.php");
+
+/**
+ * Unit tests for the PENSResponse class
+ * 
+ * This class provides unit tests for the PENSResponse class
+ * 
+ * @package PENS
+ * @subpackage Tests
+ * @author Guillaume Viguier-Just <guillaume@viguierjust.com>
+ * @licence http://www.gnu.org/licenses/gpl.txt
+ */
+class TestPENSResponse extends UnitTestCase {
+	
+	public function testCreationFromPENSException() {
+		$object = new PENSResponse(new PENSException(1101));
+		$this->assertEqual($object->getError(), 1101);
+		$this->assertNotNull($object->getErrorText());
+	}
+	
+	public function testCreationFromArguments() {
+		$object = new PENSResponse(0, "collect command received and understood");
+		$this->assertIdentical($object->getError(), 0);
+		$this->assertEqual($object->getErrorText(), "collect command received and understood");
+	}
+	
+	public function testCreationFromHTTPResponse() {
+		$eol = PENSConfig::$eol;
+		$response = "error=1101".$eol."error-text=unable to parse PENS command".$eol."version=1.0.0".$eol."pens-data=".$eol;
+		$object = new PENSResponse($response);
+		$this->assertIdentical($object->getError(), 1101);
+		$this->assertIdentical($object->getErrorText(), "unable to parse PENS command");
+		$this->assertIdentical($object->getVersion(), "1.0.0");
+		$this->assertNull($object->getPensData());
+	}
+}

+ 122 - 0
plugin/pens/lib/tests/test_pens_server.php

@@ -0,0 +1,122 @@
+<?php
+
+/**
+ * This file is part of php-pens.
+ * 
+ * php-pens is free software: you can redistribute it and/or modify 
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ * 
+ * php-pens is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ * 
+ * You should have received a copy of the GNU General Public License
+ * along with php-pens.  If not, see <http://www.gnu.org/licenses/>.
+ */ 
+
+
+/**
+ * Functional tests for the PENS Server
+ * 
+ * This class provides functional testing of the PENS Server
+ * 
+ * @package PENS
+ * @subpackage Tests
+ * @author Guillaume Viguier-Just <guillaume@viguierjust.com>
+ * @licence http://www.gnu.org/licenses/gpl.txt
+ */
+require_once("simpletest/autorun.php");
+require_once("simpletest/web_tester.php");
+
+define(PENS_TEST_SERVER_URL, "http://localhost/pens/pens_server_test.php");
+define(PENS_TEST_RECEIPT_MAILTO, "mailto:test@test.com");
+
+/**
+ * Functional tests for the PENS Server
+ * 
+ * This class provides functional tests for the PENS Server
+ * 
+ * @package PENS
+ * @subpackage Tests
+ * @author Guillaume Viguier-Just <guillaume@viguierjust.com>
+ * @licence http://www.gnu.org/licenses/gpl.txt
+ */
+class TestPENSServer extends WebTestCase {
+	
+	private $_valid_params = array(
+		"command" => "collect",
+		"pens-version" => "1.0.0",
+		"package-type" => "aicc-pkg",
+		"package-type-version" => "1.0",
+		"package-format" => "zip",
+		"package-id" => "http://myurl.com/12345",
+		"package-url" => "http://www.aicc.org/pens/sample/captivatequiz.zip",
+		"package-url-expiry" => "2099-04-09T14:17:43Z",
+		"client" => "test-pens-server",
+		"receipt" => PENS_TEST_RECEIPT_MAILTO);
+		
+	public function testEmptyQuery() {
+		$this->get(PENS_TEST_SERVER_URL);
+		$this->assertText("error=2002");
+	}
+	
+	public function testValidQuery() {
+		$this->get(PENS_TEST_SERVER_URL, $this->_valid_params);
+		$this->assertText("error=0");
+	}
+	
+	private function invalidParameterTest($name, $value, $error) {
+		$params = $this->_valid_params;
+		$params[$name] = $value;
+		$this->get(PENS_TEST_SERVER_URL, $params);
+		$this->assertText("error=".$error);
+	}
+	
+	private function validParameterTest($name, $value) {
+		$params = $this->_valid_params;
+		$params[$name] = $value;
+		$this->get(PENS_TEST_SERVER_URL, $params);
+		$this->assertText("error=0");
+	}
+	
+	public function testInvalidCommand() {
+		$this->invalidParameterTest("command", "invalid", 2002);
+	}
+	
+	public function testInvalidVersion() {
+		$this->invalidParameterTest("pens-version", "0.8.0", 2001);
+	}
+	
+	public function testInvalidPackageType() {
+		$this->invalidParameterTest("package-type", "invalid", 2003);
+	}
+	
+	public function testEmptyPackageTypeVersion() {
+		$this->invalidParameterTest("package-type-version", null, 2004);
+	}
+	
+	public function testInvalidPackageFormat() {
+		$this->invalidParameterTest("package-format", "invalid", 2005);
+	}
+	
+	public function testInvalidPackageId() {
+		$this->invalidParameterTest("package-id", "invalid", 2007);
+	}
+	
+	public function testInvalidPackageUrl() {
+		$this->invalidParameterTest("package-url", "invalid", 2008);
+	}
+	
+	public function testInvalidPackageUrlExpiry() {
+		$this->invalidParameterTest("package-url-expiry", "invalid", 2009);
+	}
+	
+	public function testValidPackageUrlExpiry() {
+		$this->validParameterTest("package-url-expiry", "2099-01-01");
+	}
+	
+}
+

+ 54 - 0
plugin/pens/pens.php

@@ -0,0 +1,54 @@
+<?php
+/**
+ * This file is part of chamilo-pens.
+ * 
+ * chamilo-pens is free software: you can redistribute it and/or modify 
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ * 
+ * chamilo-pens is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ * 
+ * You should have received a copy of the GNU General Public License
+ * along with chamilo-pens.  If not, see <http://www.gnu.org/licenses/>.
+ */
+ 
+/**
+ * Provides an implementation of the PENS server, using the php-pens library.
+ * This file must be required by a file pens.php accessible at the Chamilo root 
+ * 
+ * @author Guillaume Viguier-Just <guillaume@viguierjust.com>
+ * @licence http://www.gnu.org/licenses/gpl.txt
+ */
+
+require_once __DIR__ . '/../../main/inc/global.inc.php';
+require_once __DIR__ . '/lib/pens.php';
+require_once __DIR__ . '/chamilo_pens.php';
+
+class ChamiloPackageHandler extends PENSPackageHandler {
+	public function processPackage($request, $path_to_package) {
+		$server = PENSServer::singleton();
+		// Moves the package to archive/pens
+		$path_to_archives = api_get_path(SYS_ARCHIVE_PATH) . 'pens';
+		if(!is_dir($path_to_archives)) {
+			mkdir($path_to_archives, 0777, true);
+		}
+		rename($path_to_package, $path_to_archives . '/' . $request->getFilename());
+		// Insert the request in the database
+		$chamilo_pens = new ChamiloPens($request);
+		$chamilo_pens->save();
+		$server->sendAlert($request, new PENSResponse(0, 'Package successfully processed'));
+	}
+}
+
+$handler = new ChamiloPackageHandler();
+$handler->setSupportedPackageTypes(array('scorm-pif'));
+$handler->setSupportedPackageFormats(array('zip'));
+
+$server = PENSServer::singleton();
+$server->setPackageHandler($handler);
+
+$server->receiveCollect();

+ 36 - 0
plugin/pens/plugin.php

@@ -0,0 +1,36 @@
+<?php
+/**
+ * This file is part of chamilo-pens.
+ * 
+ * chamilo-pens is free software: you can redistribute it and/or modify 
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ * 
+ * chamilo-pens is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ * 
+ * You should have received a copy of the GNU General Public License
+ * along with chamilo-pens.  If not, see <http://www.gnu.org/licenses/>.
+ */
+ 
+/**
+ * Plugin definition file
+ * 
+ * @author Guillaume Viguier-Just <guillaume@viguierjust.com>
+ * @licence http://www.gnu.org/licenses/gpl.txt
+ */
+
+//the plugin title
+$plugin_info['title']='PENS';
+//the comments that go with the plugin
+$plugin_info['comment']="PENS implementation for Chamilo";
+//the locations where this plugin can be shown
+$plugin_info['location']=array();
+//the plugin version
+$plugin_info['version']='1.1';
+//the plugin author
+$plugin_info['author']='Guillaume Viguier-Just, Yannick Warnier';
+$plugin_info = PENSPlugin::create()->get_info();

+ 24 - 0
plugin/pens/readme.txt

@@ -0,0 +1,24 @@
+<h1>Introduction</h1>
+
+This plugin implements PENS in Chamilo, using the library php-pens available here: http://github.com/guillaumev/php-pens
+
+<h1>Requirements</h1>
+
+You need the curl extension installed to use this library.
+
+<h1>How to install ?</h1>
+
+Install the plugin and create a file at the root of your chamilo server called pens.php. Put the following code in the pens.php file you created:
+<pre>
+require_once __DIR__ . '/plugin/pens/pens.php';
+</pre>
+
+<h1>Licence</h1>
+
+The chamilo-pens plugin is published under the GNU/GPLv3+ licence (see COPYING)
+
+<h1>Credits</h1>
+
+The original author of the plugin is Guillaume Viguier-Just <guillaume@viguierjust.com>.
+
+This plugin was realized as part of his work in BeezNest (https://beeznest.com) and is maintained by BeezNest.

Algúns arquivos non se mostraron porque demasiados arquivos cambiaron neste cambio