From 29f52612bb1a5da087545f67385a246808345247 Mon Sep 17 00:00:00 2001 From: "Matthias P. Walther" Date: Mon, 23 Jul 2018 10:52:22 +0200 Subject: [PATCH 001/352] Inlcuded useage of libsodium23 1.0.16 in the devel branch and updated dependencies and configuration on latest experiences. --- INSTALLATION.md | 44 +++++++++++++++++++++++++++++++++++++++----- 1 file changed, 39 insertions(+), 5 deletions(-) diff --git a/INSTALLATION.md b/INSTALLATION.md index d5af154ab..9bcf7bfd3 100755 --- a/INSTALLATION.md +++ b/INSTALLATION.md @@ -61,18 +61,22 @@ The following requirements should be installed on the system you intend to insta * [Git](http://git-scm.com/) (for installation) * PHP >= 5.6 + * composer * php-curl - * php7.0-fpm + * php-fpm (often: php7.x-fpm e. g. php7.2-fpm) + * php-mbstring (often: php7.x-mbstring e. g. php7.2-mbstring) + * php-mysql * php-zip * php-xml * php-gd * php-intl - * pandoc + * pandoc (not needed in devel branch for libsodium23) * Apache >= 2.4 * MySQL / MariaDB >= 5.6 * [Composer](https://getcomposer.org/) (for installing dependencies) * [The Sodium crypto library (Libsodium)](https://paragonie.com/book/pecl-libsodium/read/00-intro.md#installing-libsodium) * The repository version of libsodium is currently incompatible to formR. Use [these instructions](https://github.com/paragonie/halite/issues/48) to set it up. + * The Branch devel supports libsodium23 v1.0.16 which is the default version on most current distributions. * [Gearman](http://gearman.org/) (Server + Client) *OPTIONAL* (for running background jobs) * [Supervisor](http://supervisord.org/) *OPTIONAL* * [smysqlin](https://bitbucket.org/cyriltata/smysqlin) *OPTIONAL* (for managing database patches) @@ -80,16 +84,46 @@ The following requirements should be installed on the system you intend to insta Paket list for copying: ``` -sudo apt-get install git php apache2 mysql-server composer php-curl php7.0-fpm php7.0-mbstring php-mysql php-zip php-xml php-gd php-intl pandoc +sudo apt-get install git php apache2 mysql-server composer php-curl php-fpm php-mbstring php-mysql php-zip php-xml php-gd php-intl pandoc ``` Install libsodium now. See instructions above. +Apache needs the rewrite mod enabled: + +```sh + sudo a2enmod rewrite +``` + +And overrides need to be allowed for the virtual host. On a standard Ubuntu or Debian installation, insert the following block at the end of your default virtual host in `/etc/apache2/sites-enabled/000-default.conf`. + +``` + + Options Indexes FollowSymlinks MultiViews + AllowOverride All + Order allow,deny + allow from all + + +``` + +Make sure apache2 and php7.x-fpm run. + ### 1. Clone the Git repository and checkout the *desired* release (version) +The suggested file structure is as follows: Place formr.org's folder, e. g. `/var/www`, accessible for apache's user e. g. `www-data` and to create a symlink to the webroot. + You'll need [Git](http://git-scm.com/) (a version management software). After installing it, navigate to the folder where you want to place formr and run ```sh - git clone https://github.com/rubenarslan/formr.org.git + cd /var/www/ + git clone https://github.com/rubenarslan/formr.org.git #depending on the system you might need sudo for this +``` + +Create the symlink and fix access rights: + +```sh + ln -s /var/www/formr.org/webroot /var/www/html/formr + chown -R www-data:www-data /var/www/formr.org ``` You can also [download the repository as a zip file](https://github.com/rubenarslan/formr/archive/master.zip), but trust me, use Git for this. @@ -106,7 +140,7 @@ At this point you should have your formr files present in the installation direc composer install ``` - +  ### 2. Create an empty MySQL database Login to mysql server with a user that has appropriate privileges and execute these commands to create the database From d99484a01752cf9c7cde72e528033bda441a00d2 Mon Sep 17 00:00:00 2001 From: "Matthias P. Walther" Date: Tue, 24 Jul 2018 08:09:26 +0200 Subject: [PATCH 002/352] Officially drop PHP 5.6 support --- INSTALLATION.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/INSTALLATION.md b/INSTALLATION.md index 9bcf7bfd3..0c3eac38f 100755 --- a/INSTALLATION.md +++ b/INSTALLATION.md @@ -60,7 +60,7 @@ at [formr.org](https://formr.org). The following requirements should be installed on the system you intend to install formr on: * [Git](http://git-scm.com/) (for installation) -* PHP >= 5.6 +* PHP ≥ 7.0 * composer * php-curl * php-fpm (often: php7.x-fpm e. g. php7.2-fpm) From 8347b2462df5d85a291b740add353c9fa11735e3 Mon Sep 17 00:00:00 2001 From: "Matthias P. Walther" Date: Wed, 25 Jul 2018 10:44:32 +0200 Subject: [PATCH 003/352] Ported tests to PHPUnit 6+, removed unneeded code, fixed dependencies, added phpunit.xml --- tests/ClassLoaderTest.php | 26 ++++----- tests/ConfigTest.php | 78 ++++++++++++------------- tests/FirstTest.php | 38 ++++-------- tests/OpenCPUTest.php | 44 ++++++-------- tests/block_ordering.php | 118 -------------------------------------- tests/phpunit.xml | 7 +++ 6 files changed, 87 insertions(+), 224 deletions(-) delete mode 100644 tests/block_ordering.php create mode 100644 tests/phpunit.xml diff --git a/tests/ClassLoaderTest.php b/tests/ClassLoaderTest.php index d1a90122b..dbeb9faff 100644 --- a/tests/ClassLoaderTest.php +++ b/tests/ClassLoaderTest.php @@ -1,23 +1,19 @@ assertTrue(class_exists('DB') && class_exists('Email') && class_exists('RunUnit'), "Some class in test not autoloaded"); - $this->assertEquals(0, $zero); - } - - public function testNonExistent() { - $this->assertFalse(class_exists('User', false)); - } + public function testClasses() { + $email = new Email(null); + $this->assertTrue(class_exists('DB') && class_exists('Email') && class_exists('RunUnit'), "Some class in test not autoloaded"); + } + public function testNonExistent() { + $this->assertFalse(class_exists('User', false)); + } } - diff --git a/tests/ConfigTest.php b/tests/ConfigTest.php index e42052ee7..c6650863d 100644 --- a/tests/ConfigTest.php +++ b/tests/ConfigTest.php @@ -1,45 +1,43 @@ 'cyril', - 'names' => array( - 'first_name' => 'Cyril', - 'last_name' => 'Tata', - ), - 'deep' => array('deeper' => array('deepest' => 'inner_value')) - ); - - return array(array($settings)); - } - - /** - * - * @dataProvider configProvider - */ - public function testGet($settings) { - Config::initialize($settings); - $this->assertEquals('cyril', Config::get('username')); - $this->assertEquals('Tata', Config::get('names.last_name')); - $this->assertEquals('inner_value', Config::get('deep.deeper.deepest')); - } - - /** - * - * @dataProvider configProvider - */ - public function testDefault($settings) { - Config::initialize($settings); - $this->assertEquals('none', Config::get('middlename', 'none')); - $this->assertNull(Config::get('non_existent.whatever')); - } - +class ConfigTest extends PHPUnit\Framework\TestCase { + + public function configProvider() { + $settings = array( + 'username' => 'cyril', + 'names' => array( + 'first_name' => 'Cyril', + 'last_name' => 'Tata', + ), + 'deep' => array('deeper' => array('deepest' => 'inner_value')) + ); + + return array(array($settings)); + } + + /** + * + * @dataProvider configProvider + */ + public function testGet($settings) { + Config::initialize($settings); + $this->assertEquals('cyril', Config::get('username')); + $this->assertEquals('Tata', Config::get('names.last_name')); + $this->assertEquals('inner_value', Config::get('deep.deeper.deepest')); + } + + /** + * + * @dataProvider configProvider + */ + public function testDefault($settings) { + Config::initialize($settings); + $this->assertEquals('none', Config::get('middlename', 'none')); + $this->assertNull(Config::get('non_existent.whatever')); + } } - diff --git a/tests/FirstTest.php b/tests/FirstTest.php index 92325b08a..8c6a38f80 100644 --- a/tests/FirstTest.php +++ b/tests/FirstTest.php @@ -1,41 +1,27 @@ assertEquals(FALSE, strpos($bla, "Fatal error") OR strpos($bla, "Parse error") OR strpos($bla, "Warning:") OR strpos($bla, "Notice:") ); } - public function testAllPages() - { + + public function testAllPages() { $this->checkPageForPHPErrors(WEBROOT); $this->checkPageForPHPErrors(WEBROOT."/public/login"); $this->checkPageForPHPErrors(WEBROOT."/public/documentation"); $this->checkPageForPHPErrors(WEBROOT."/public/studies"); - $this->checkPageForPHPErrors(WEBROOT."/public/team"); + $this->checkPageForPHPErrors(WEBROOT."/public/about"); $this->checkPageForPHPErrors(WEBROOT."/public/register"); $this->checkPageForPHPErrors(WEBROOT."/public/logout"); - + $this->checkPageForPHPErrors(WEBROOT."/admin/mail/"); $this->checkPageForPHPErrors(WEBROOT."/admin/mail/edit"); - + $this->checkPageForPHPErrors(WEBROOT."/admin/run/"); - + $this->checkPageForPHPErrors(WEBROOT."/admin/survey/"); - -// $this->checkPageForPHPErrors(WEBROOT."/admin/cron_log/"); } -# so basically because I rely on a global singleton-ish thing testing is difficult -# public function testUploadOfAllWidgetsTable() -# { -# -# $study = new Survey($fdb, null, array( -# 'name' => 'all_widgets', -# 'user_id' => 1 -# )); -# $this->assertEquals(TRUE, $study->createIndependently() ); -# $works = $study->uploadItemTable(APPLICATION_ROOT."/webroot/assets/example_surveys/all_widgets.xlsx"); -# $this->assertEquals(TRUE, $works); -# } -} \ No newline at end of file +} diff --git a/tests/OpenCPUTest.php b/tests/OpenCPUTest.php index 7b7b54e53..9453628f2 100644 --- a/tests/OpenCPUTest.php +++ b/tests/OpenCPUTest.php @@ -1,31 +1,27 @@ 'http://opencpu.psych.bio.uni-goettingen.de', - ); - return array(array($settings)); - } + public function configProvider() { + $settings = array( + 'opencpu_instance' => 'http://opencpu.psych.bio.uni-goettingen.de', + ); + return array(array($settings)); + } - /** - * - * @dataProvider configProvider - */ - public function testConnection($settings) { - Config::initialize($settings); + /** + * + * @dataProvider configProvider + */ + public function testConnection($settings) { + Config::initialize($settings); $ocpu = OpenCPU::getInstance('opencpu_instance'); $session = $ocpu->snippet('rnorm(5)'); $this->assertInstanceOf('OpenCPU_Session', $session); @@ -38,12 +34,10 @@ public function testConnection($settings) { $this->write($session->getStdout()); $this->write("Object"); $this->write($session->getObject()); - } + } private function write($object) { echo "\n"; print_r($object); } - } - diff --git a/tests/block_ordering.php b/tests/block_ordering.php deleted file mode 100644 index 1a0c324d7..000000000 --- a/tests/block_ordering.php +++ /dev/null @@ -1,118 +0,0 @@ - "", "item_order" => 1, "id" => "xstart1"), - array("block_order" => "", "item_order" => 1, "id" => "xstart2"), - array("block_order" => "B", "item_order" => 1, "id" => "xB1"), - array("block_order" => "B", "item_order" => 1, "id" => "xB2"), - array("block_order" => "B", "item_order" => 2, "id" => "xB3"), - array("block_order" => "C", "item_order" => 1, "id" => "xC1"), - array("block_order" => "C", "item_order" => 1, "id" => "xC2"), - array("block_order" => "", "item_order" => 5, "id" => "xend"), - -) ); - -echo " ---------- -"; -blocksort( array( - array("block_order" => "A", "item_order" => 1, "id" => "xA1"), - array("block_order" => "A", "item_order" => 2, "id" => "xA2"), - array("block_order" => "B", "item_order" => 1, "id" => "xB1"), - array("block_order" => "B", "item_order" => 1, "id" => "xB2"), - array("block_order" => "B", "item_order" => 2, "id" => "xB3"), - array("block_order" => "C", "item_order" => 1, "id" => "xC1"), - array("block_order" => "C", "item_order" => 1, "id" => "xC2"), - array("block_order" => "C", "item_order" => 1, "id" => "xC3"), - -)); -echo " ---------- -"; - -blocksort( array( - array("block_order" => "A", "item_order" => 1, "id" => "A"), - array("block_order" => "B", "item_order" => 1, "id" => "B"), - array("block_order" => "C", "item_order" => 1, "id" => "C"), - array("block_order" => "D", "item_order" => 1, "id" => "D"), - array("block_order" => "E", "item_order" => 1, "id" => "E"), - array("block_order" => "F", "item_order" => 1, "id" => "F"), - array("block_order" => "G", "item_order" => 1, "id" => "G"), - array("block_order" => "H", "item_order" => 1, "id" => "H"), -)); -echo " ---------- -"; -blocksort( array( - array("block_order" => "", "item_order" => 1, "id" => "x1"), - array("block_order" => "", "item_order" => 2, "id" => "x2"), - array("block_order" => "", "item_order" => 3, "id" => "x3"), - array("block_order" => "", "item_order" => 4, "id" => "x4"), - array("block_order" => "", "item_order" => 5, "id" => "x5"), - array("block_order" => "", "item_order" => 6, "id" => "x6"), - array("block_order" => "", "item_order" => 7, "id" => "x7"), - array("block_order" => "", "item_order" => 8, "id" => "x8"), -)); -echo " ---------- -"; -blocksort( array( - array("block_order" => "", "item_order" => 1, "id" => "x1"), - array("block_order" => "", "item_order" => 1, "id" => "x2"), - array("block_order" => "", "item_order" => 1, "id" => "x3"), - array("block_order" => "", "item_order" => 1, "id" => "x4"), - array("block_order" => "", "item_order" => 1, "id" => "x5"), - array("block_order" => "", "item_order" => 1, "id" => "x6"), - array("block_order" => "", "item_order" => 1, "id" => "x7"), - array("block_order" => "", "item_order" => 1, "id" => "x8"), -)); -echo " ---------- -"; - -blocksort( array( - array("block_order" => "A", "item_order" => 1, "id" => "xA1"), - array("block_order" => "A", "item_order" => 1, "id" => "xA2"), - array("block_order" => "B", "item_order" => 1, "id" => "xB1"), - array("block_order" => "B", "item_order" => 1, "id" => "xB2"), - array("block_order" => "", "item_order" => 1, "id" => "xnoblock"), - array("block_order" => "*", "item_order" => 1, "id" => "x*"), - array("block_order" => "*", "item_order" => 1, "id" => "x*"), - array("block_order" => "&", "item_order" => 1, "id" => "x&"), - array("block_order" => "&", "item_order" => 1, "id" => "x&"), -)); diff --git a/tests/phpunit.xml b/tests/phpunit.xml new file mode 100644 index 000000000..593a8742c --- /dev/null +++ b/tests/phpunit.xml @@ -0,0 +1,7 @@ + + + + . + + + From 649ddf41595f54b759ac5fdc176e6fe893b5670c Mon Sep 17 00:00:00 2001 From: "Matthias P. Walther" Date: Mon, 30 Jul 2018 14:06:04 +0200 Subject: [PATCH 004/352] Restored block_ordering.php --- tests/block_ordering.php | 118 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 118 insertions(+) create mode 100644 tests/block_ordering.php diff --git a/tests/block_ordering.php b/tests/block_ordering.php new file mode 100644 index 000000000..1a0c324d7 --- /dev/null +++ b/tests/block_ordering.php @@ -0,0 +1,118 @@ + "", "item_order" => 1, "id" => "xstart1"), + array("block_order" => "", "item_order" => 1, "id" => "xstart2"), + array("block_order" => "B", "item_order" => 1, "id" => "xB1"), + array("block_order" => "B", "item_order" => 1, "id" => "xB2"), + array("block_order" => "B", "item_order" => 2, "id" => "xB3"), + array("block_order" => "C", "item_order" => 1, "id" => "xC1"), + array("block_order" => "C", "item_order" => 1, "id" => "xC2"), + array("block_order" => "", "item_order" => 5, "id" => "xend"), + +) ); + +echo " +--------- +"; +blocksort( array( + array("block_order" => "A", "item_order" => 1, "id" => "xA1"), + array("block_order" => "A", "item_order" => 2, "id" => "xA2"), + array("block_order" => "B", "item_order" => 1, "id" => "xB1"), + array("block_order" => "B", "item_order" => 1, "id" => "xB2"), + array("block_order" => "B", "item_order" => 2, "id" => "xB3"), + array("block_order" => "C", "item_order" => 1, "id" => "xC1"), + array("block_order" => "C", "item_order" => 1, "id" => "xC2"), + array("block_order" => "C", "item_order" => 1, "id" => "xC3"), + +)); +echo " +--------- +"; + +blocksort( array( + array("block_order" => "A", "item_order" => 1, "id" => "A"), + array("block_order" => "B", "item_order" => 1, "id" => "B"), + array("block_order" => "C", "item_order" => 1, "id" => "C"), + array("block_order" => "D", "item_order" => 1, "id" => "D"), + array("block_order" => "E", "item_order" => 1, "id" => "E"), + array("block_order" => "F", "item_order" => 1, "id" => "F"), + array("block_order" => "G", "item_order" => 1, "id" => "G"), + array("block_order" => "H", "item_order" => 1, "id" => "H"), +)); +echo " +--------- +"; +blocksort( array( + array("block_order" => "", "item_order" => 1, "id" => "x1"), + array("block_order" => "", "item_order" => 2, "id" => "x2"), + array("block_order" => "", "item_order" => 3, "id" => "x3"), + array("block_order" => "", "item_order" => 4, "id" => "x4"), + array("block_order" => "", "item_order" => 5, "id" => "x5"), + array("block_order" => "", "item_order" => 6, "id" => "x6"), + array("block_order" => "", "item_order" => 7, "id" => "x7"), + array("block_order" => "", "item_order" => 8, "id" => "x8"), +)); +echo " +--------- +"; +blocksort( array( + array("block_order" => "", "item_order" => 1, "id" => "x1"), + array("block_order" => "", "item_order" => 1, "id" => "x2"), + array("block_order" => "", "item_order" => 1, "id" => "x3"), + array("block_order" => "", "item_order" => 1, "id" => "x4"), + array("block_order" => "", "item_order" => 1, "id" => "x5"), + array("block_order" => "", "item_order" => 1, "id" => "x6"), + array("block_order" => "", "item_order" => 1, "id" => "x7"), + array("block_order" => "", "item_order" => 1, "id" => "x8"), +)); +echo " +--------- +"; + +blocksort( array( + array("block_order" => "A", "item_order" => 1, "id" => "xA1"), + array("block_order" => "A", "item_order" => 1, "id" => "xA2"), + array("block_order" => "B", "item_order" => 1, "id" => "xB1"), + array("block_order" => "B", "item_order" => 1, "id" => "xB2"), + array("block_order" => "", "item_order" => 1, "id" => "xnoblock"), + array("block_order" => "*", "item_order" => 1, "id" => "x*"), + array("block_order" => "*", "item_order" => 1, "id" => "x*"), + array("block_order" => "&", "item_order" => 1, "id" => "x&"), + array("block_order" => "&", "item_order" => 1, "id" => "x&"), +)); From 2a87878918421f7d74c5222134cf78c7548b40d5 Mon Sep 17 00:00:00 2001 From: "Matthias P. Walther" Date: Thu, 2 Aug 2018 10:50:11 +0200 Subject: [PATCH 005/352] =?UTF-8?q?Extend=20=E2=80=9CLog=20of=20user=20act?= =?UTF-8?q?ivity=E2=80=9C=20by=20column=20=E2=80=9CModule=20Description?= =?UTF-8?q?=E2=80=9C.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixes https://github.com/rubenarslan/formr.org/issues/319 --- application/Controller/AdminRunController.php | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/application/Controller/AdminRunController.php b/application/Controller/AdminRunController.php index 9d98a45ea..30a63c47e 100644 --- a/application/Controller/AdminRunController.php +++ b/application/Controller/AdminRunController.php @@ -221,6 +221,7 @@ private function userDetailAction() { `survey_unit_sessions`.id AS session_id, `survey_runs`.name AS run_name, `survey_run_units`.position, + `survey_run_units`.description, `survey_units`.type AS unit_type, `survey_unit_sessions`.created, `survey_unit_sessions`.ended, @@ -238,6 +239,7 @@ private function userDetailAction() { $users = array(); foreach ($g_users as $userx) { $userx['Unit in Run'] = $userx['unit_type']. " ({$userx['position']})"; + $userx['Module Description'] = "" . $userx['description'] . ""; $userx['Session'] = "".mb_substr($userx['session'],0,10)."…"; $userx['Entered'] = "{$userx['created']}"; $staid = ($userx['ended'] ? strtotime($userx['ended']) : time() ) - strtotime($userx['created']); @@ -251,7 +253,7 @@ private function userDetailAction() { else $userx['Delete'] = ""; - unset($userx['session'], $userx['session_id'], $userx['run_name'], $userx['unit_type'], $userx['position'], $userx['left']); + unset($userx['session'], $userx['session_id'], $userx['run_name'], $userx['unit_type'], $userx['position'], $userx['description'], $userx['left']); $users[] = $userx; } From 2a90f85562a62a18e539537ef0f624735f8ebec5 Mon Sep 17 00:00:00 2001 From: "Matthias P. Walther" Date: Fri, 3 Aug 2018 13:46:12 +0200 Subject: [PATCH 006/352] Fix Google Spreadsheet import on PHP7.2. Details see: https://stackoverflow.com/questions/51668723/rename-gives-undocumented-return-value-null/51671831 --- application/Library/CURL.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/application/Library/CURL.php b/application/Library/CURL.php index 4474e4e0f..56cbc0f58 100644 --- a/application/Library/CURL.php +++ b/application/Library/CURL.php @@ -315,7 +315,7 @@ public static function DownloadUrl($url, $output_file, $params = array(), $metho touch($tmpfile, $last_modified); } - $res = rename($tmpfile, $output_file); + $res = \rename($tmpfile, $output_file); if ($res !== true) { throw new Exception("Unable to rename temporary file"); } From ddcdddc665e5f59e9bca045b1d8f7263d805ca5a Mon Sep 17 00:00:00 2001 From: "Matthias P. Walther" Date: Tue, 7 Aug 2018 13:28:13 +0200 Subject: [PATCH 007/352] Order columsn in result table by order. This results in them being sorted as they were in the import sheet. --- application/Model/Survey.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/application/Model/Survey.php b/application/Model/Survey.php index d2cfcbf21..c1f195820 100644 --- a/application/Model/Survey.php +++ b/application/Model/Survey.php @@ -1688,7 +1688,7 @@ public function getItems($columns = null, $whereIn = null) { if ($whereIn) { $select->whereIn($whereIn['field'], $whereIn['values']); } - $select->order("`survey_items`.item_order"); + $select->order("order"); return $select->fetchAll(); } From c23e475f7d79ab66c924e11ac55ccdc934e7597f Mon Sep 17 00:00:00 2001 From: Cyril Tata Date: Wed, 29 Aug 2018 14:21:34 +0200 Subject: [PATCH 008/352] move checkbox selection js to admin/main.js --- application/View/admin/run/user_overview.php | 4 ++-- webroot/assets/admin/js/main.js | 20 ++++++++++++++++++++ 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/application/View/admin/run/user_overview.php b/application/View/admin/run/user_overview.php index e4c0023ac..e4bd17653 100755 --- a/application/View/admin/run/user_overview.php +++ b/application/View/admin/run/user_overview.php @@ -73,7 +73,7 @@ - + @@ -186,4 +186,4 @@ $reminders)); Template::load('admin/footer'); -?> \ No newline at end of file +?> diff --git a/webroot/assets/admin/js/main.js b/webroot/assets/admin/js/main.js index 41bb4a079..f804bb3dc 100644 --- a/webroot/assets/admin/js/main.js +++ b/webroot/assets/admin/js/main.js @@ -763,3 +763,23 @@ function _init() { }); }; }(jQuery)); + +/* + * Admin > Run + * ----------------------- + * + */ +jQuery(document).ready(function() { + // Select all checkbox in 'Users overview' + $('#user-overview-select-all').click(function() { + var $select = $(this); + var $checkboxes = $select.parents('table').find('.ba-select-session'); + + if ($select.is(':checked')) { + $checkboxes.prop('checked', true); + } else { + $checkboxes.prop('checked', false); + } + }); + +}); From 2693f0555cbe8722351d5957795f21e74a995381 Mon Sep 17 00:00:00 2001 From: "Matthias P. Walther" Date: Thu, 30 Aug 2018 09:34:42 +0200 Subject: [PATCH 009/352] Move JS code to new admin.js Closes #379 --- webroot/assets/admin/js/admin.js | 19 +++++++++++++++++++ webroot/assets/admin/js/main.js | 20 -------------------- webroot/assets/assets.json | 7 +++++-- webroot/assets/build/js/formr-admin.min.js | 10 +--------- 4 files changed, 25 insertions(+), 31 deletions(-) create mode 100644 webroot/assets/admin/js/admin.js diff --git a/webroot/assets/admin/js/admin.js b/webroot/assets/admin/js/admin.js new file mode 100644 index 000000000..a5353e65b --- /dev/null +++ b/webroot/assets/admin/js/admin.js @@ -0,0 +1,19 @@ +/* + * Admin > Run + * ----------------------- + * + */ +jQuery(document).ready(function() { + // Select all checkbox in 'Users overview' + $('#user-overview-select-all').click(function() { + var $select = $(this); + var $checkboxes = $select.parents('table').find('.ba-select-session'); + + if ($select.is(':checked')) { + $checkboxes.prop('checked', true); + } else { + $checkboxes.prop('checked', false); + } + }); + +}); diff --git a/webroot/assets/admin/js/main.js b/webroot/assets/admin/js/main.js index f804bb3dc..41bb4a079 100644 --- a/webroot/assets/admin/js/main.js +++ b/webroot/assets/admin/js/main.js @@ -763,23 +763,3 @@ function _init() { }); }; }(jQuery)); - -/* - * Admin > Run - * ----------------------- - * - */ -jQuery(document).ready(function() { - // Select all checkbox in 'Users overview' - $('#user-overview-select-all').click(function() { - var $select = $(this); - var $checkboxes = $select.parents('table').find('.ba-select-session'); - - if ($select.is(':checked')) { - $checkboxes.prop('checked', true); - } else { - $checkboxes.prop('checked', false); - } - }); - -}); diff --git a/webroot/assets/assets.json b/webroot/assets/assets.json index 40468e509..6ec3eefea 100644 --- a/webroot/assets/assets.json +++ b/webroot/assets/assets.json @@ -75,7 +75,10 @@ }, "admin": { - "js": "admin/js/main.js", + "js": [ + "admin/js/main.js", + "admin/js/admin.js" + ], "css": [ "admin/css/AdminLTE.css", "admin/css/style.css" @@ -101,4 +104,4 @@ "main:js": { "js": "common/js/main.js" } -} \ No newline at end of file +} diff --git a/webroot/assets/build/js/formr-admin.min.js b/webroot/assets/build/js/formr-admin.min.js index eb30330f8..24a9bae87 100644 --- a/webroot/assets/build/js/formr-admin.min.js +++ b/webroot/assets/build/js/formr-admin.min.js @@ -1,9 +1 @@ -function mysql_datetime(){return(new Date).toISOString().slice(0,19).replace("T"," ")}function flatStringifyGeo(a){var b={};b.timestamp=a.timestamp;var c={};return c.accuracy=a.coords.accuracy,c.altitude=a.coords.altitude,c.altitudeAccuracy=a.coords.altitudeAccuracy,c.heading=a.coords.heading,c.latitude=a.coords.latitude,c.longitude=a.coords.longitude,c.speed=a.coords.speed,b.coords=c,JSON.stringify(b)}function general_alert(a,b){$(b).append(a)}function bootstrap_alert(a,b,c,d){d=d||"alert-danger",c=c||".alerts-container";var e=$('
'+(b?b:"Problem")+" "+a+"
");e.prependTo($(c)),e[0].scrollIntoView(!1)}function bootstrap_modal(a,b,c){c=c||"tpl-feedback-modal";var d=$($.parseHTML(getHTMLTemplate(c,{body:b,header:a})));return d.modal("show").on("hidden.bs.modal",function(){d.remove()}),d}function bootstrap_spinner(){return' '}function ajaxErrorHandling(a,b,c,d){var e,f={400:"Server understood the request but request content was invalid.",401:"You don't have access.",403:"You were logged out while coding, please open a new tab and login again. This way no data will be lost.",404:"Page not found.",500:"Internal Server Error.",503:"Server can't be reached."};if(a.status?(e=f[a.status],e||(e="undefined"!=typeof a.statusText&&"error"!==a.statusText?a.statusText:"Unknown error. Check your internet connection.")):e="parsererror"===a.statusText?"Parsing JSON Request failed.":"timeout"===a.statusText?"The attempt to save timed out. Are you connected to the internet?":"abort"===a.statusText?"The request was aborted by the server.":"undefined"!=typeof a.statusText&&"error"!==a.statusText?a.statusText:"Unknown error. Check your internet connection.",a.responseText){var g=$(a.responseText);g=g.find(".alert").addBack().filter(".alert").html(),e=e+"
"+g}bootstrap_alert(e,"Error.",".alerts-container")}function stringTemplate(a,b){for(var c in b){var d="%?{"+c+"}";a=a.replace(new RegExp(d,"g"),b[c])}return a}function getHTMLTemplate(a,b){var c=jQuery("#"+a);if(c.length)return stringTemplate($.trim(c.html()),b)}function toggleElement(a){$("#"+a).toggleClass("hidden")}function download(a,b){var c=document.createElement("a");c.setAttribute("href","data:text/plain;charset=utf-8,"+encodeURIComponent(b)),c.setAttribute("download",a),c.style.display="none",document.body.appendChild(c),c.click(),document.body.removeChild(c)}function download_next_textarea(a){var b=$(a);return download(b.data("filename"),b.parent().find("textarea").val()),!1}function cookies_enabled(){try{document.cookie="cookietest=1";var a=-1!=document.cookie.indexOf("cookietest=");return document.cookie="cookietest=1; expires=Thu, 01-Jan-1970 00:00:01 GMT",a}catch(b){return!1}}function _init(){"use strict";$.AdminLTE.layout={activate:function(){var a=this;a.fix(),a.fixSidebar(),$("body, html, .wrapper").css("height","auto"),$(window,".wrapper").resize(function(){a.fix(),a.fixSidebar()})},fix:function(){$(".layout-boxed > .wrapper").css("overflow","hidden");var a=$(".main-footer").outerHeight()||0,b=$(".main-header").outerHeight()+a,c=$(window).height(),d=$(".sidebar").height()||0;if($("body").hasClass("fixed"))$(".content-wrapper, .right-side").css("min-height",c-a);else{var e;c>=d?($(".content-wrapper, .right-side").css("min-height",c-b),e=c-b):($(".content-wrapper, .right-side").css("min-height",d),e=d);var f=$($.AdminLTE.options.controlSidebarOptions.selector);"undefined"!=typeof f&&f.height()>e&&$(".content-wrapper, .right-side").css("min-height",f.height())}},fixSidebar:function(){return $("body").hasClass("fixed")?("undefined"==typeof $.fn.slimScroll&&window.console&&window.console.error("Error: the fixed layout requires the slimscroll plugin!"),void($.AdminLTE.options.sidebarSlimScroll&&"undefined"!=typeof $.fn.slimScroll&&($(".sidebar").slimScroll({destroy:!0}).height("auto"),$(".sidebar").slimScroll({height:$(window).height()-$(".main-header").height()+"px",color:"rgba(0,0,0,0.2)",size:"3px"})))):void("undefined"!=typeof $.fn.slimScroll&&$(".sidebar").slimScroll({destroy:!0}).height("auto"))}},$.AdminLTE.pushMenu={activate:function(a){var b=$.AdminLTE.options.screenSizes;$(document).on("click",a,function(a){a.preventDefault(),$(window).width()>b.sm-1?$("body").hasClass("sidebar-collapse")?$("body").removeClass("sidebar-collapse").trigger("expanded.pushMenu"):$("body").addClass("sidebar-collapse").trigger("collapsed.pushMenu"):$("body").hasClass("sidebar-open")?$("body").removeClass("sidebar-open").removeClass("sidebar-collapse").trigger("collapsed.pushMenu"):$("body").addClass("sidebar-open").trigger("expanded.pushMenu")}),$(".content-wrapper").click(function(){$(window).width()<=b.sm-1&&$("body").hasClass("sidebar-open")&&$("body").removeClass("sidebar-open")}),($.AdminLTE.options.sidebarExpandOnHover||$("body").hasClass("fixed")&&$("body").hasClass("sidebar-mini"))&&this.expandOnHover()},expandOnHover:function(){var a=this,b=$.AdminLTE.options.screenSizes.sm-1;$(".main-sidebar").hover(function(){$("body").hasClass("sidebar-mini")&&$("body").hasClass("sidebar-collapse")&&$(window).width()>b&&a.expand()},function(){$("body").hasClass("sidebar-mini")&&$("body").hasClass("sidebar-expanded-on-hover")&&$(window).width()>b&&a.collapse()})},expand:function(){$("body").removeClass("sidebar-collapse").addClass("sidebar-expanded-on-hover")},collapse:function(){$("body").hasClass("sidebar-expanded-on-hover")&&$("body").removeClass("sidebar-expanded-on-hover").addClass("sidebar-collapse")}},$.AdminLTE.tree=function(a){var b=this,c=$.AdminLTE.options.animationSpeed;$(document).off("click",a+" li a").on("click",a+" li a",function(a){var d=$(this),e=d.next();if(e.is(".treeview-menu")&&e.is(":visible")&&!$("body").hasClass("sidebar-collapse"))e.slideUp(c,function(){e.removeClass("menu-open")}),e.parent("li").removeClass("active");else if(e.is(".treeview-menu")&&!e.is(":visible")){var f=d.parents("ul").first(),g=f.find("ul:visible").slideUp(c);g.removeClass("menu-open");var h=d.parent("li");e.slideDown(c,function(){e.addClass("menu-open"),f.find("li.active").removeClass("active"),h.addClass("active"),b.layout.fix()})}e.is(".treeview-menu")&&a.preventDefault()})},$.AdminLTE.controlSidebar={activate:function(){var a=this,b=$.AdminLTE.options.controlSidebarOptions,c=$(b.selector),d=$(b.toggleBtnSelector);d.on("click",function(d){d.preventDefault(),c.hasClass("control-sidebar-open")||$("body").hasClass("control-sidebar-open")?a.close(c,b.slide):a.open(c,b.slide)});var e=$(".control-sidebar-bg");a._fix(e),$("body").hasClass("fixed")?a._fixForFixed(c):$(".content-wrapper, .right-side").height() .box-body, > .box-footer, > form >.box-body, > form > .box-footer");c.hasClass("collapsed-box")?(a.children(":first").removeClass(b.icons.open).addClass(b.icons.collapse),d.slideDown(b.animationSpeed,function(){c.removeClass("collapsed-box")})):(a.children(":first").removeClass(b.icons.collapse).addClass(b.icons.open),d.slideUp(b.animationSpeed,function(){c.addClass("collapsed-box")}))},remove:function(a){var b=a.parents(".box").first();b.slideUp(this.animationSpeed)}}}if(function(a,b){"object"==typeof module&&"object"==typeof module.exports?module.exports=a.document?b(a,!0):function(a){if(!a.document)throw new Error("jQuery requires a window with a document");return b(a)}:b(a)}("undefined"!=typeof window?window:this,function(a,b){function c(a){var b=!!a&&"length"in a&&a.length,c=fa.type(a);return"function"===c||fa.isWindow(a)?!1:"array"===c||0===b||"number"==typeof b&&b>0&&b-1 in a}function d(a,b,c){if(fa.isFunction(b))return fa.grep(a,function(a,d){return!!b.call(a,d,a)!==c});if(b.nodeType)return fa.grep(a,function(a){return a===b!==c});if("string"==typeof b){if(pa.test(b))return fa.filter(b,a,c);b=fa.filter(b,a)}return fa.grep(a,function(a){return _.call(b,a)>-1!==c})}function e(a,b){for(;(a=a[b])&&1!==a.nodeType;);return a}function f(a){var b={};return fa.each(a.match(va)||[],function(a,c){b[c]=!0}),b}function g(){X.removeEventListener("DOMContentLoaded",g),a.removeEventListener("load",g),fa.ready()}function h(){this.expando=fa.expando+h.uid++}function i(a,b,c){var d;if(void 0===c&&1===a.nodeType)if(d="data-"+b.replace(Ca,"-$&").toLowerCase(),c=a.getAttribute(d),"string"==typeof c){try{c="true"===c?!0:"false"===c?!1:"null"===c?null:+c+""===c?+c:Ba.test(c)?fa.parseJSON(c):c}catch(e){}Aa.set(a,b,c)}else c=void 0;return c}function j(a,b,c,d){var e,f=1,g=20,h=d?function(){return d.cur()}:function(){return fa.css(a,b,"")},i=h(),j=c&&c[3]||(fa.cssNumber[b]?"":"px"),k=(fa.cssNumber[b]||"px"!==j&&+i)&&Ea.exec(fa.css(a,b));if(k&&k[3]!==j){j=j||k[3],c=c||[],k=+i||1;do f=f||".5",k/=f,fa.style(a,b,k+j);while(f!==(f=h()/i)&&1!==f&&--g)}return c&&(k=+k||+i||0,e=c[1]?k+(c[1]+1)*c[2]:+c[2],d&&(d.unit=j,d.start=k,d.end=e)),e}function k(a,b){var c="undefined"!=typeof a.getElementsByTagName?a.getElementsByTagName(b||"*"):"undefined"!=typeof a.querySelectorAll?a.querySelectorAll(b||"*"):[];return void 0===b||b&&fa.nodeName(a,b)?fa.merge([a],c):c}function l(a,b){for(var c=0,d=a.length;d>c;c++)za.set(a[c],"globalEval",!b||za.get(b[c],"globalEval"))}function m(a,b,c,d,e){for(var f,g,h,i,j,m,n=b.createDocumentFragment(),o=[],p=0,q=a.length;q>p;p++)if(f=a[p],f||0===f)if("object"===fa.type(f))fa.merge(o,f.nodeType?[f]:f);else if(La.test(f)){for(g=g||n.appendChild(b.createElement("div")),h=(Ia.exec(f)||["",""])[1].toLowerCase(),i=Ka[h]||Ka._default,g.innerHTML=i[1]+fa.htmlPrefilter(f)+i[2],m=i[0];m--;)g=g.lastChild;fa.merge(o,g.childNodes),g=n.firstChild,g.textContent=""}else o.push(b.createTextNode(f));for(n.textContent="",p=0;f=o[p++];)if(d&&fa.inArray(f,d)>-1)e&&e.push(f);else if(j=fa.contains(f.ownerDocument,f),g=k(n.appendChild(f),"script"),j&&l(g),c)for(m=0;f=g[m++];)Ja.test(f.type||"")&&c.push(f);return n}function n(){return!0}function o(){return!1}function p(){try{return X.activeElement}catch(a){}}function q(a,b,c,d,e,f){var g,h;if("object"==typeof b){"string"!=typeof c&&(d=d||c,c=void 0);for(h in b)q(a,h,c,d,b[h],f);return a}if(null==d&&null==e?(e=c,d=c=void 0):null==e&&("string"==typeof c?(e=d,d=void 0):(e=d,d=c,c=void 0)),e===!1)e=o;else if(!e)return a;return 1===f&&(g=e,e=function(a){return fa().off(a),g.apply(this,arguments)},e.guid=g.guid||(g.guid=fa.guid++)),a.each(function(){fa.event.add(this,b,e,d,c)})}function r(a,b){return fa.nodeName(a,"table")&&fa.nodeName(11!==b.nodeType?b:b.firstChild,"tr")?a.getElementsByTagName("tbody")[0]||a.appendChild(a.ownerDocument.createElement("tbody")):a}function s(a){return a.type=(null!==a.getAttribute("type"))+"/"+a.type,a}function t(a){var b=Sa.exec(a.type);return b?a.type=b[1]:a.removeAttribute("type"),a}function u(a,b){var c,d,e,f,g,h,i,j;if(1===b.nodeType){if(za.hasData(a)&&(f=za.access(a),g=za.set(b,f),j=f.events)){delete g.handle,g.events={};for(e in j)for(c=0,d=j[e].length;d>c;c++)fa.event.add(b,e,j[e][c])}Aa.hasData(a)&&(h=Aa.access(a),i=fa.extend({},h),Aa.set(b,i))}}function v(a,b){var c=b.nodeName.toLowerCase();"input"===c&&Ha.test(a.type)?b.checked=a.checked:"input"!==c&&"textarea"!==c||(b.defaultValue=a.defaultValue)}function w(a,b,c,d){b=Z.apply([],b);var e,f,g,h,i,j,l=0,n=a.length,o=n-1,p=b[0],q=fa.isFunction(p);if(q||n>1&&"string"==typeof p&&!da.checkClone&&Ra.test(p))return a.each(function(e){var f=a.eq(e);q&&(b[0]=p.call(this,e,f.html())),w(f,b,c,d)});if(n&&(e=m(b,a[0].ownerDocument,!1,a,d),f=e.firstChild,1===e.childNodes.length&&(e=f),f||d)){for(g=fa.map(k(e,"script"),s),h=g.length;n>l;l++)i=e,l!==o&&(i=fa.clone(i,!0,!0),h&&fa.merge(g,k(i,"script"))),c.call(a[l],i,l);if(h)for(j=g[g.length-1].ownerDocument,fa.map(g,t),l=0;h>l;l++)i=g[l],Ja.test(i.type||"")&&!za.access(i,"globalEval")&&fa.contains(j,i)&&(i.src?fa._evalUrl&&fa._evalUrl(i.src):fa.globalEval(i.textContent.replace(Ta,"")))}return a}function x(a,b,c){for(var d,e=b?fa.filter(b,a):a,f=0;null!=(d=e[f]);f++)c||1!==d.nodeType||fa.cleanData(k(d)),d.parentNode&&(c&&fa.contains(d.ownerDocument,d)&&l(k(d,"script")),d.parentNode.removeChild(d));return a}function y(a,b){var c=fa(b.createElement(a)).appendTo(b.body),d=fa.css(c[0],"display");return c.detach(),d}function z(a){var b=X,c=Va[a];return c||(c=y(a,b),"none"!==c&&c||(Ua=(Ua||fa(" '; } - if($this->session_id) { + if($this->session_id && $cache_session) { $set_report = $this->dbh->prepare( "INSERT INTO `survey_reports` (`session_id`, `unit_id`, `opencpu_url`, `created`, `last_viewed`) VALUES (:session_id, :unit_id, :opencpu_url, NOW(), NOW() ) From 0831653027ca5c22043cc83b25d8890cbe0c7a99 Mon Sep 17 00:00:00 2001 From: Cyril Tata Date: Fri, 22 Mar 2019 10:51:44 +0100 Subject: [PATCH 035/352] remove debugging --- application/Model/RunUnit.php | 1 - 1 file changed, 1 deletion(-) diff --git a/application/Model/RunUnit.php b/application/Model/RunUnit.php index 8a32be0a3..28d239aff 100644 --- a/application/Model/RunUnit.php +++ b/application/Model/RunUnit.php @@ -770,7 +770,6 @@ public function getParsedBody($source, $email_embed = false, $admin = false, $ha 'body' => $session->getObject(), 'images' => $images, ); - error_log(print_r($report, 1)); } else { $this->run->renderedDescAndFooterAlready = true; $iframesrc = $files['knit.html']; From 0923b0802b3bc9f8e15b5810546cc20fdfe77dd3 Mon Sep 17 00:00:00 2001 From: Cyril Tata Date: Fri, 22 Mar 2019 11:55:35 +0100 Subject: [PATCH 036/352] all queue to be populated even if not processed --- application/Model/RunSession.php | 2 +- application/Model/RunUnit.php | 7 ++++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/application/Model/RunSession.php b/application/Model/RunSession.php index f668ea9bf..4fed9712e 100644 --- a/application/Model/RunSession.php +++ b/application/Model/RunSession.php @@ -168,7 +168,7 @@ public function getUnit() { $output = $unit->exec(); //@TODO check whether output is set or NOT - $queue = $this->unit_session->id && !$unit->ended && !$unit->expired && Config::get('unit_session.use_queue'); + $queue = $this->unit_session->id && !$unit->ended && !$unit->expired; if ($queue) { $queued = UnitSessionQueue::addItem($this->unit_session, $unit, $output); } diff --git a/application/Model/RunUnit.php b/application/Model/RunUnit.php index 28d239aff..9966c8fd4 100644 --- a/application/Model/RunUnit.php +++ b/application/Model/RunUnit.php @@ -704,15 +704,16 @@ public function getParsedBodyAdmin($source, $email_embed = false, $pick_session } public function getParsedBody($source, $email_embed = false, $admin = false, $has_session_data = true) { - /* @var $session OpenCPU_Session */ - if (!$this->knittingNeeded($source)) { // knit if need be + + if (!$this->knittingNeeded($source)) { if($email_embed) { return array('body' => $this->body_parsed, 'images' => array()); } else { return $this->body_parsed; } } - + + /* @var $session OpenCPU_Session */ $session = null; $cache_session = false; From 7442c1ccc624eb03db18f145596a30eb1ef62b00 Mon Sep 17 00:00:00 2001 From: Cyril Tata Date: Fri, 22 Mar 2019 18:20:53 +0100 Subject: [PATCH 037/352] move run unit admin html to templates for ease of readability --- application/Library/Template.php | 4 +- application/Model/Branch.php | 33 +++------- application/Model/Email.php | 49 +++++---------- application/Model/External.php | 20 +++--- application/Model/Page.php | 16 ++--- application/Model/Pause.php | 44 +++---------- application/Model/RunUnit.php | 24 +++----- application/Model/Shuffle.php | 17 ++---- application/Model/SkipBackward.php | 23 ++----- application/Model/Survey.php | 61 ++++--------------- application/View/admin/run/units/email.php | 49 +++++++++++++++ application/View/admin/run/units/endpage.php | 12 ++++ application/View/admin/run/units/external.php | 25 ++++++++ application/View/admin/run/units/pause.php | 39 ++++++++++++ application/View/admin/run/units/shuffle.php | 18 ++++++ .../View/admin/run/units/skipbackward.php | 18 ++++++ .../View/admin/run/units/skipforward.php | 30 +++++++++ application/View/admin/run/units/survey.php | 45 ++++++++++++++ application/View/admin/run/units/unit.php | 18 ++++++ webroot/assets/admin/css/style.css | 2 + 20 files changed, 332 insertions(+), 215 deletions(-) create mode 100644 application/View/admin/run/units/email.php create mode 100644 application/View/admin/run/units/endpage.php create mode 100644 application/View/admin/run/units/external.php create mode 100644 application/View/admin/run/units/pause.php create mode 100644 application/View/admin/run/units/shuffle.php create mode 100644 application/View/admin/run/units/skipbackward.php create mode 100644 application/View/admin/run/units/skipforward.php create mode 100644 application/View/admin/run/units/survey.php create mode 100644 application/View/admin/run/units/unit.php diff --git a/application/Library/Template.php b/application/Library/Template.php index 638501186..fb0683836 100644 --- a/application/Library/Template.php +++ b/application/Library/Template.php @@ -32,8 +32,8 @@ public static function get($template, $vars = array()) { return ob_get_clean(); } - public static function get_replace($template, $params = array()) { - $text = self::get($template); + public static function get_replace($template, $params = array(), $vars = array()) { + $text = self::get($template, $vars); return self::replace($text, $params); } diff --git a/application/Model/Branch.php b/application/Model/Branch.php index c4254e8ce..4c690ca84 100644 --- a/application/Model/Branch.php +++ b/application/Model/Branch.php @@ -69,31 +69,14 @@ public function create($options) { } public function displayForRun($prepend = '') { - $dialog = '
-
- -
- else - - go on -
'; - $dialog .= ' -

- Save - Test -

'; - - $dialog = $prepend . $dialog; + $dialog = Template::get($this->getUnitTemplatePath(), array( + 'prepend' => $prepend, + 'condition' => $this->condition, + 'position' => $this->position, + 'ifTrue' => $this->if_true, + 'jump' => $this->automatically_jump, + 'goOn' => $this->automatically_go_on, + )); return parent::runDialog($dialog); } diff --git a/application/Model/Email.php b/application/Model/Email.php index b0f96bf3c..eb362910a 100644 --- a/application/Model/Email.php +++ b/application/Model/Email.php @@ -164,40 +164,19 @@ protected function getPotentialRecipientFields() { } public function displayForRun($prepend = '') { - $email_accounts = Site::getCurrentUser()->getEmailAccounts(); - - if (!empty($email_accounts)): - $dialog = '

- "; - $dialog .= '

'; - else: - $dialog = "
No email accounts. Add some here.
"; - endif; - $dialog .= '

- -

-

- -

-

-

- {{login_link}} will be replaced by a personalised link to this run, {{login_code}} will be replaced with this user\'s session code.

'; -//

'; - - $dialog .= '

'; - $dialog .= '

Save - Test

'; + $dialog = Template::get($this->getUnitTemplatePath(), array( + 'email' => $this, + 'prepend' => $prepend, + 'email_accounts' => Site::getCurrentUser()->getEmailAccounts(), + 'body' => $this->body, + 'subject' => $this->subject, + 'account_id' => $this->account_id, + 'cron_only' => $this->cron_only, + 'recipient_field' => $this->recipient_field, + 'potentialRecipientFields' => $this->getPotentialRecipientFields(), + )); - $dialog = $prepend . $dialog; - return parent::runDialog($dialog, 'fa-envelope'); + return parent::runDialog($dialog); } public function getRecipientField($return_format = 'json', $return_session = false) { @@ -395,8 +374,8 @@ public function test() { if (!$this->grabRandomSession()) { return false; } - global $user; - + + $user = Site::getCurrentUser(); $receiver = $user->getEmail(); echo "

Recipient

"; diff --git a/application/Model/External.php b/application/Model/External.php index 29808190c..e09a31962 100644 --- a/application/Model/External.php +++ b/application/Model/External.php @@ -59,20 +59,14 @@ public function create($options) { } public function displayForRun($prepend = '') { - $dialog = '

-

-

-

Enter a URL like http://example.org?code={{login_code}} and the user will be sent to that URL, replacing {{login_code}} with that user\'s code. Enter R-code to e.g. send more data along: paste0(\'http:example.org?code={{login_link}}&
age=\', demographics$age)
.

- '; - - $dialog .= '

Save - Test

'; - - - $dialog = $prepend . $dialog; + $dialog = Template::get($this->getUnitTemplatePath(), array( + 'prepend' => $prepend, + 'address' => $this->address, + 'expire_after' => $this->expire_after, + 'api_end' => $this->api_end, + )); - return parent::runDialog($dialog, 'fa-external-link-square'); + return parent::runDialog($dialog); } public function removeFromRun($special = null) { diff --git a/application/Model/Page.php b/application/Model/Page.php index a5c0fdf7e..888c7cb4c 100644 --- a/application/Model/Page.php +++ b/application/Model/Page.php @@ -74,18 +74,12 @@ public function create($options) { } public function displayForRun($prepend = '') { - $dialog = - // '

'. - '

'; -# '

'; - $dialog .= '

Save - Test

'; - - - $dialog = $prepend . $dialog; + $dialog = Template::get($this->getUnitTemplatePath(), array( + 'prepend' => $prepend, + 'body' => $this->body, + )); - return parent::runDialog($dialog, 'fa-stop fa-1-5x'); + return parent::runDialog($dialog); } public function removeFromRun($special = null) { diff --git a/application/Model/Pause.php b/application/Model/Pause.php index 5c6d23f54..8af2b65d9 100644 --- a/application/Model/Pause.php +++ b/application/Model/Pause.php @@ -91,42 +91,16 @@ public function create($options) { } public function displayForRun($prepend = '') { - $dialog = '

- - and - -

-

- and - -

-
- - - - - - -
- -
-

- '; - $dialog .= '

Save - Test

'; - - - $dialog = $prepend . $dialog; + $dialog = Template::get($this->getUnitTemplatePath(), array( + 'prepend' => $prepend, + 'wait_until_time' => $this->wait_until_time, + 'wait_until_date' => $this->wait_until_date, + 'wait_minutes' => $this->wait_minutes, + 'relative_to' => $this->relative_to, + 'body' => $this->body, + )); - return parent::runDialog($dialog, 'fa-pause'); + return parent::runDialog($dialog); } public function removeFromRun($special = null) { diff --git a/application/Model/RunUnit.php b/application/Model/RunUnit.php index 9966c8fd4..59575375c 100644 --- a/application/Model/RunUnit.php +++ b/application/Model/RunUnit.php @@ -365,22 +365,8 @@ public function howManyReachedIt() { } public function runDialog($dialog) { - return ' -
-

-
-

- ' . $this->howManyReachedIt() . '
-
-
-
- - - ' . $dialog . ' -
-
-
- '; + $tpl = $this->getUnitTemplatePath('unit'); + return Template::get($tpl, array('dialog' => $dialog, 'unit' => $this)); } public function hadMajorChanges() { @@ -391,7 +377,7 @@ protected function majorChange() { } public function displayForRun($prepend = '') { - return $this->runDialog($prepend, ''); // FIXME: This class has no parent + return $this->runDialog($prepend); // FIXME: This class has no parent } protected $survey_results; @@ -837,4 +823,8 @@ public static function getDefaults($type) { return array_val($defaults, $type, array()); } + protected function getUnitTemplatePath($tpl = null) { + return 'admin/run/units/' . ($tpl ? $tpl : strtolower($this->type)); + } + } diff --git a/application/Model/Shuffle.php b/application/Model/Shuffle.php index f563e9f07..71705c786 100644 --- a/application/Model/Shuffle.php +++ b/application/Model/Shuffle.php @@ -52,18 +52,13 @@ public function create($options) { } public function displayForRun($prepend = '') { - $dialog = '
Randomly assign to one of groups counting from one.
-

You can later read the assigned group using shuffle$group.
- You can then for example use a SkipForward to send one group to a different arm/path in the run or use a showif in a survey to show certain items/stimuli to one group only.

- '; -# '

'; - $dialog .= '

Save - Test

'; - - - $dialog = $prepend . $dialog; + + $dialog = Template::get($this->getUnitTemplatePath(), array( + 'prepend' => $prepend, + 'groups' => $this->groups + )); - return parent::runDialog($dialog, 'fa-random fa-1-5x'); + return parent::runDialog($dialog); } public function removeFromRun($special = null) { diff --git a/application/Model/SkipBackward.php b/application/Model/SkipBackward.php index a1e9fea5f..78106de77 100644 --- a/application/Model/SkipBackward.php +++ b/application/Model/SkipBackward.php @@ -11,23 +11,12 @@ class SkipBackward extends Branch { public $export_attribs = array('type', 'description', 'position', 'special', 'condition', 'if_true'); public function displayForRun($prepend = '') { - $dialog = '

- -

-
- - -
'; - $dialog .= '

- Save. - Test

'; - - - $dialog = $prepend . $dialog; + $dialog = Template::get($this->getUnitTemplatePath(), array( + 'prepend' => $prepend, + 'condition' => $this->condition, + 'position' => $this->position, + 'ifTrue' => $this->if_true, + )); return parent::runDialog($dialog); } diff --git a/application/Model/Survey.php b/application/Model/Survey.php index c4fde4a5b..206c31ea5 100644 --- a/application/Model/Survey.php +++ b/application/Model/Survey.php @@ -1063,6 +1063,10 @@ public function getTimeWhenLastViewedItem() { * @return boolean */ private function hasExpired() { + if (empty($this->run_session->unit_session)) { + return false; + } + $expire_invitation = (int) $this->settings['expire_invitation_after']; $grace_period = (int) $this->settings['expire_invitation_grace']; $expire_inactivity = (int) $this->settings['expire_after']; @@ -2143,56 +2147,15 @@ public function delete($special = null) { } public function displayForRun($prepend = '') { + $dialog = Template::get($this->getUnitTemplatePath(), array( + 'survey' => $this, + 'studies' => $this->dbh->select('id, name')->from('survey_studies')->where(array('user_id' => Site::getCurrentUser()->id))->fetchAll(), + 'prepend' => $prepend, + 'resultCount' => $this->howManyReachedItNumbers(), + 'time' => $this->getAverageTimeItTakes(), + )); - global $user; - $studies = $this->dbh->select('id, name')->from('survey_studies')->where(array('user_id' => $user->id))->fetchAll(); - - if ($studies): - $dialog = '
'; - $dialog .= '"; - $dialog .= '
'; - else: - $dialog = "
No studies. Add some first
"; - endif; - - if ($this->id) { - $resultCount = $this->howManyReachedItNumbers(); - - $time = $this->getAverageTimeItTakes(); - - $dialog .= " -

" . (int) $resultCount['finished'] . " complete results, " . (int) $resultCount['begun'] . " begun (in ~{$time}m) -

-

- View items - Upload items -

"; - $dialog .= '

- Save - Test -

'; -// elseif($studies): - } else { - $dialog .= '

-

- Save -
-

'; - } - - $dialog = $prepend . $dialog; - return parent::runDialog($dialog, 'fa-pencil-square'); + return parent::runDialog($dialog); } /** diff --git a/application/View/admin/run/units/email.php b/application/View/admin/run/units/email.php new file mode 100644 index 000000000..5222b8d35 --- /dev/null +++ b/application/View/admin/run/units/email.php @@ -0,0 +1,49 @@ + + + +
+ + +
+

+ +
+ + +
+
+ +
+ + +
+ +
+ + +
+

{{login_link}} will be replaced by a personalised link to this run, {{login_code}} will be replaced with this user's session code.

+
+
+ +
+ +
+ +
+ Save + Test +
+ + + +
No email accounts. Add some here.
+ + \ No newline at end of file diff --git a/application/View/admin/run/units/endpage.php b/application/View/admin/run/units/endpage.php new file mode 100644 index 000000000..551acb526 --- /dev/null +++ b/application/View/admin/run/units/endpage.php @@ -0,0 +1,12 @@ + + +

+ +

+ +

+ Save + Test +

\ No newline at end of file diff --git a/application/View/admin/run/units/external.php b/application/View/admin/run/units/external.php new file mode 100644 index 000000000..0b3f8ccc2 --- /dev/null +++ b/application/View/admin/run/units/external.php @@ -0,0 +1,25 @@ + + +

+ +

+ +

+ + +

+

+ +

+

+ Enter a URL like http://example.org?code={{login_code}} and the user will be sent to that URL, + replacing {{login_code}} with that user's code.
+ Enter R-code to e.g. send more data along:
paste0('http:example.org?code={{login_link}}&age=', demographics$age). +

+

+ Save + Test +

diff --git a/application/View/admin/run/units/pause.php b/application/View/admin/run/units/pause.php new file mode 100644 index 000000000..664b7f7b0 --- /dev/null +++ b/application/View/admin/run/units/pause.php @@ -0,0 +1,39 @@ + + +

+ + and + +

+

+ + and + +

+
+ + + + + + +
+ +
+ +

+ +

+ +

+ Save + Test +

\ No newline at end of file diff --git a/application/View/admin/run/units/shuffle.php b/application/View/admin/run/units/shuffle.php new file mode 100644 index 000000000..d1e346797 --- /dev/null +++ b/application/View/admin/run/units/shuffle.php @@ -0,0 +1,18 @@ + + +
+ Randomly assign to one of + + groups counting from one. +
+ +
+ You can later read the assigned group using shuffle$group.
+ You can then for example use a SkipForward to send one group to a different arm/path in the run or use a show-if in a survey to show certain items/stimuli to one group only. +
+

+ +

+ Save + Test +

\ No newline at end of file diff --git a/application/View/admin/run/units/skipbackward.php b/application/View/admin/run/units/skipbackward.php new file mode 100644 index 000000000..1db42e1e4 --- /dev/null +++ b/application/View/admin/run/units/skipbackward.php @@ -0,0 +1,18 @@ + + +

+ +

+
+ + +
+ +

+ Save + Test +

\ No newline at end of file diff --git a/application/View/admin/run/units/skipforward.php b/application/View/admin/run/units/skipforward.php new file mode 100644 index 000000000..59079d375 --- /dev/null +++ b/application/View/admin/run/units/skipforward.php @@ -0,0 +1,30 @@ + + +
+
+ + + +
+ + else + + + go on +
+
+
+
+ Save + Test +
\ No newline at end of file diff --git a/application/View/admin/run/units/survey.php b/application/View/admin/run/units/survey.php new file mode 100644 index 000000000..863ce2c61 --- /dev/null +++ b/application/View/admin/run/units/survey.php @@ -0,0 +1,45 @@ + + + + +
+ + + id): ?> +

+ complete results, + begun (in ~ m) +

+

+ View items + Upload items +

+
+

+ Save + Test +

+ +

+

+ Save +
+

+ + +
+ + + +
No studies. Add some first
+ + + diff --git a/application/View/admin/run/units/unit.php b/application/View/admin/run/units/unit.php new file mode 100644 index 000000000..a7660f09e --- /dev/null +++ b/application/View/admin/run/units/unit.php @@ -0,0 +1,18 @@ +
+
+

+
+
+

+ howManyReachedIt() ?>
+
+
+
+ + + + + +
+
+
\ No newline at end of file diff --git a/webroot/assets/admin/css/style.css b/webroot/assets/admin/css/style.css index b08e4bb0c..6cded9058 100644 --- a/webroot/assets/admin/css/style.css +++ b/webroot/assets/admin/css/style.css @@ -118,6 +118,8 @@ input:focus {outline:none;} } .run_unit_inner .run_unit_dialog .form-group { margin: 0px; + display: block; + margin-bottom: 4px; } .badge-info { background-color: #3a87ad; From 5cc3523326303a8c5ce192c1f7ebee6874e80069 Mon Sep 17 00:00:00 2001 From: Cyril Tata Date: Fri, 22 Mar 2019 18:28:45 +0100 Subject: [PATCH 038/352] grunt css --- webroot/assets/build/css/formr-admin.min.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/webroot/assets/build/css/formr-admin.min.css b/webroot/assets/build/css/formr-admin.min.css index 8484356d3..cb1902c0e 100644 --- a/webroot/assets/build/css/formr-admin.min.css +++ b/webroot/assets/build/css/formr-admin.min.css @@ -11,4 +11,4 @@ * Website: Almsaeed Studio * License: Open source - MIT * Please visit http://opensource.org/licenses/MIT for more information -!*/body{font-family:'Source Sans Pro','Helvetica Neue',Helvetica,Arial,sans-serif;font-weight:400}.wrapper{position:relative}.wrapper:after,.wrapper:before{content:" ";display:table}.layout-boxed .wrapper{max-width:1250px;margin:0 auto;min-height:100%;-webkit-box-shadow:0 0 8px rgba(0,0,0,.5);box-shadow:0 0 8px rgba(0,0,0,.5);position:relative}.layout-boxed{background:url(../img/boxed-bg.jpg) fixed}.content-wrapper,.main-footer,.right-side{-webkit-transition:-webkit-transform .3s ease-in-out,margin .3s ease-in-out;-o-transition:-o-transform .3s ease-in-out,margin .3s ease-in-out;transition:transform .3s ease-in-out,margin .3s ease-in-out;z-index:820}.layout-top-nav .content-wrapper,.layout-top-nav .main-footer,.layout-top-nav .right-side{margin-left:0}@media (min-width:768px){.sidebar-collapse .content-wrapper,.sidebar-collapse .main-footer,.sidebar-collapse .right-side{margin-left:0}}.content-wrapper,.right-side{min-height:100%;background-color:#ecf0f5;z-index:800}.main-footer{background:#fff;padding:15px;color:#444;border-top:1px solid #d2d6de}.fixed .left-side,.fixed .main-header,.fixed .main-sidebar{position:fixed}.fixed .main-header{top:0;right:0;left:0}.fixed .content-wrapper,.fixed .right-side{padding-top:50px}@media (max-width:767px){.content-wrapper,.main-footer,.right-side{margin-left:0}.sidebar-open .content-wrapper,.sidebar-open .main-footer,.sidebar-open .right-side{-webkit-transform:translate(230px,0);-ms-transform:translate(230px,0);-o-transform:translate(230px,0);transform:translate(230px,0)}.fixed .content-wrapper,.fixed .right-side{padding-top:100px}}.fixed.layout-boxed .wrapper{max-width:100%}body.hold-transition .content-wrapper,body.hold-transition .left-side,body.hold-transition .main-footer,body.hold-transition .main-header .logo,body.hold-transition .main-header .navbar,body.hold-transition .main-sidebar,body.hold-transition .right-side{-webkit-transition:none;-o-transition:none;transition:none}.content{min-height:250px;padding:15px;margin-right:auto;margin-left:auto;padding-left:15px;padding-right:15px}.h1,.h2,.h3,.h4,.h5,.h6,h1,h2,h3,h4,h5,h6{font-family:'Source Sans Pro',sans-serif}a{color:#3c8dbc}a:active,a:focus,a:hover{outline:0;text-decoration:none;color:#72afd2}.page-header{margin:10px 0 20px;font-size:22px}.page-header>small{color:#666;display:block;margin-top:5px}.main-header{position:relative;max-height:100px;z-index:1030}.main-header .navbar{-webkit-transition:margin-left .3s ease-in-out;-o-transition:margin-left .3s ease-in-out;transition:margin-left .3s ease-in-out;margin-bottom:0;border:none;min-height:50px;border-radius:0}.layout-top-nav .main-header .navbar{margin-left:0}.main-header #navbar-search-input.form-control{background:rgba(255,255,255,.2);border-color:transparent}.main-header #navbar-search-input.form-control:active,.main-header #navbar-search-input.form-control:focus{border-color:rgba(0,0,0,.1);background:rgba(255,255,255,.9)}.main-header #navbar-search-input.form-control::-moz-placeholder{color:#ccc;opacity:1}.main-header #navbar-search-input.form-control:-ms-input-placeholder{color:#ccc}.main-header #navbar-search-input.form-control::-webkit-input-placeholder{color:#ccc}@media (max-width:991px){.main-header .navbar-custom-menu a,.main-header .navbar-right a{color:inherit;background:0 0}}@media (max-width:767px){.main-header .navbar-right{float:none}.navbar-collapse .main-header .navbar-right{margin:7.5px -15px}.main-header .navbar-right>li{color:inherit;border:0}}.main-header .navbar-brand,.main-header .sidebar-toggle:hover{color:#fff}.main-header .sidebar-toggle{float:left;background-color:transparent;background-image:none;padding:15px;font-family:fontAwesome}.main-header .sidebar-toggle:before{content:"\f0c9"}.main-header .sidebar-toggle:active,.main-header .sidebar-toggle:focus{background:0 0}.main-header .sidebar-toggle .icon-bar{display:none}.main-header .navbar .nav>li.user>a>.fa,.main-header .navbar .nav>li.user>a>.glyphicon,.main-header .navbar .nav>li.user>a>.ion{margin-right:5px}.main-header .navbar .nav>li>a>.label{position:absolute;top:9px;right:7px;text-align:center;font-size:9px;padding:2px 3px;line-height:.9}.main-header .logo{-webkit-transition:width .3s ease-in-out;-o-transition:width .3s ease-in-out;transition:width .3s ease-in-out;display:block;float:left;height:50px;font-size:20px;line-height:50px;text-align:center;width:230px;font-family:"Helvetica Neue",Helvetica,Arial,sans-serif;padding:0 15px;font-weight:300;overflow:hidden}.main-header .logo .logo-lg{display:block}.main-header .logo .logo-mini{display:none}.content-header{position:relative;padding:15px 15px 0}.content-header>h1{margin:0;font-size:24px}.content-header>h1>small{font-size:15px;display:inline-block;padding-left:4px;font-weight:300}.content-header>.breadcrumb{float:right;background:0 0;margin-top:0;margin-bottom:0;font-size:12px;padding:7px 5px;position:absolute;top:15px;right:10px;border-radius:2px}.content-header>.breadcrumb>li>a{color:#444;text-decoration:none;display:inline-block}.content-header>.breadcrumb>li>a>.fa,.content-header>.breadcrumb>li>a>.glyphicon,.content-header>.breadcrumb>li>a>.ion{margin-right:5px}.content-header>.breadcrumb>li+li:before{content:'>\00a0'}@media (max-width:991px){.content-header>.breadcrumb{position:relative;margin-top:5px;top:0;right:0;float:none;background:#d2d6de;padding-left:10px}.content-header>.breadcrumb li:before{color:#97a0b3}.navbar-custom-menu .navbar-nav>li{float:left}.navbar-custom-menu .navbar-nav{margin:0;float:left}.navbar-custom-menu .navbar-nav>li>a{padding-top:15px;padding-bottom:15px;line-height:20px}}.navbar-toggle{color:#fff;border:0;margin:0;padding:15px}@media (max-width:767px){.main-header{position:relative}.main-header .logo,.main-header .navbar{width:100%;float:none}.main-header .navbar{margin:0}.main-header .navbar-custom-menu{float:right}}@media (max-width:991px){.navbar-collapse.pull-left{float:none!important}.navbar-collapse.pull-left+.navbar-custom-menu{display:block;position:absolute;top:0;right:40px}}.left-side,.main-sidebar{position:absolute;top:0;left:0;padding-top:50px;min-height:100%;width:230px;z-index:810;-webkit-transition:-webkit-transform .3s ease-in-out,width .3s ease-in-out;-o-transition:-o-transform .3s ease-in-out,width .3s ease-in-out;transition:transform .3s ease-in-out,width .3s ease-in-out}@media (max-width:767px){.left-side,.main-sidebar{padding-top:100px;-webkit-transform:translate(-230px,0);-ms-transform:translate(-230px,0);-o-transform:translate(-230px,0);transform:translate(-230px,0)}}@media (min-width:768px){.sidebar-collapse .left-side,.sidebar-collapse .main-sidebar{-webkit-transform:translate(-230px,0);-ms-transform:translate(-230px,0);-o-transform:translate(-230px,0);transform:translate(-230px,0)}}@media (max-width:767px){.sidebar-open .left-side,.sidebar-open .main-sidebar{-webkit-transform:translate(0,0);-ms-transform:translate(0,0);-o-transform:translate(0,0);transform:translate(0,0)}}.sidebar{padding-bottom:10px}.sidebar-form input:focus{border-color:transparent}.user-panel{position:relative;width:100%;padding:10px;overflow:hidden}.user-panel:after,.user-panel:before{content:" ";display:table}.user-panel>.image>img{width:100%;max-width:45px;height:auto}.user-panel>.info{padding:5px 5px 5px 15px;line-height:1;position:absolute;left:55px}.user-panel>.info>p{font-weight:600;margin-bottom:9px}.user-panel>.info>a{text-decoration:none;padding-right:5px;margin-top:3px;font-size:11px}.user-panel>.info>a>.fa,.user-panel>.info>a>.glyphicon,.user-panel>.info>a>.ion{margin-right:3px}.sidebar-menu{list-style:none;margin:0;padding:0}.sidebar-menu>li{position:relative;margin:0;padding:0}.sidebar-menu>li>a{padding:12px 5px 12px 15px;display:block}.sidebar-menu>li>a>.fa,.sidebar-menu>li>a>.glyphicon,.sidebar-menu>li>a>.ion{width:20px}.sidebar-menu>li .badge,.sidebar-menu>li .label{margin-right:5px}.sidebar-menu>li .badge{margin-top:3px}.sidebar-menu li.header{padding:10px 25px 10px 15px;font-size:12px}.sidebar-menu li>a>.fa-angle-left,.sidebar-menu li>a>.pull-right-container>.fa-angle-left{width:auto;height:auto;padding:0;margin-right:10px}.sidebar-menu li>a>.fa-angle-left{position:absolute;top:50%;right:10px;margin-top:-8px}.sidebar-menu li.active>a>.fa-angle-left,.sidebar-menu li.active>a>.pull-right-container>.fa-angle-left{-webkit-transform:rotate(-90deg);-ms-transform:rotate(-90deg);-o-transform:rotate(-90deg);transform:rotate(-90deg)}.sidebar-menu li.active>.treeview-menu{display:block}.sidebar-menu .treeview-menu{display:none;list-style:none;padding:0;margin:0;padding-left:5px}.sidebar-menu .treeview-menu .treeview-menu{padding-left:20px}.sidebar-menu .treeview-menu>li{margin:0}.sidebar-menu .treeview-menu>li>a{padding:5px 5px 5px 15px;display:block;font-size:14px}.sidebar-menu .treeview-menu>li>a>.fa,.sidebar-menu .treeview-menu>li>a>.glyphicon,.sidebar-menu .treeview-menu>li>a>.ion{width:20px}.sidebar-menu .treeview-menu>li>a>.fa-angle-down,.sidebar-menu .treeview-menu>li>a>.fa-angle-left,.sidebar-menu .treeview-menu>li>a>.pull-right-container>.fa-angle-down,.sidebar-menu .treeview-menu>li>a>.pull-right-container>.fa-angle-left{width:auto}@media (min-width:768px){.sidebar-mini.sidebar-collapse .content-wrapper,.sidebar-mini.sidebar-collapse .main-footer,.sidebar-mini.sidebar-collapse .right-side{margin-left:50px!important;z-index:840}.sidebar-mini.sidebar-collapse .main-sidebar{-webkit-transform:translate(0,0);-ms-transform:translate(0,0);-o-transform:translate(0,0);transform:translate(0,0);width:50px!important;z-index:850}.sidebar-mini.sidebar-collapse .sidebar-menu>li{position:relative}.sidebar-mini.sidebar-collapse .sidebar-menu>li>a{margin-right:0}.sidebar-mini.sidebar-collapse .sidebar-menu>li>a>span{border-top-right-radius:4px}.sidebar-mini.sidebar-collapse .sidebar-menu>li:not(.treeview)>a>span{border-bottom-right-radius:4px}.sidebar-mini.sidebar-collapse .sidebar-menu>li>.treeview-menu{padding-top:5px;padding-bottom:5px;border-bottom-right-radius:4px}.sidebar-mini.sidebar-collapse .sidebar-menu>li:hover>.treeview-menu,.sidebar-mini.sidebar-collapse .sidebar-menu>li:hover>a>span:not(.pull-right){display:block!important;position:absolute;width:180px;left:50px}.sidebar-mini.sidebar-collapse .sidebar-menu>li:hover>a>span{top:0;margin-left:-3px;padding:12px 5px 12px 20px;background-color:inherit}.sidebar-mini.sidebar-collapse .sidebar-menu>li:hover>a>.pull-right-container{position:relative!important;float:right;width:auto!important;left:180px!important;top:-22px!important;z-index:900}.sidebar-mini.sidebar-collapse .sidebar-menu>li:hover>a>.pull-right-container>.label:not(:first-of-type){display:none}.sidebar-mini.sidebar-collapse .sidebar-menu>li:hover>.treeview-menu{top:44px;margin-left:0}.sidebar-mini.sidebar-collapse .main-sidebar .user-panel>.info,.sidebar-mini.sidebar-collapse .sidebar-form,.sidebar-mini.sidebar-collapse .sidebar-menu li.header,.sidebar-mini.sidebar-collapse .sidebar-menu>li>.treeview-menu,.sidebar-mini.sidebar-collapse .sidebar-menu>li>a>.pull-right,.sidebar-mini.sidebar-collapse .sidebar-menu>li>a>span{display:none!important;-webkit-transform:translateZ(0)}.sidebar-mini.sidebar-collapse .main-header .logo{width:50px}.sidebar-mini.sidebar-collapse .main-header .logo>.logo-mini{display:block;margin-left:-15px;margin-right:-15px;font-size:18px}.sidebar-mini.sidebar-collapse .main-header .logo>.logo-lg{display:none}.sidebar-mini.sidebar-collapse .main-header .navbar{margin-left:50px}.control-sidebar-open .content-wrapper,.control-sidebar-open .main-footer,.control-sidebar-open .right-side{margin-right:230px}}.main-sidebar .user-panel,.sidebar-menu,.sidebar-menu>li.header{white-space:nowrap;overflow:hidden}.sidebar-menu:hover{overflow:visible}.sidebar-form,.sidebar-menu>li.header{overflow:hidden;text-overflow:clip}.sidebar-menu li>a{position:relative}.sidebar-menu li>a>.pull-right-container{position:absolute;right:10px;top:50%;margin-top:-7px}.control-sidebar-bg{position:fixed;z-index:1000;bottom:0}.control-sidebar,.control-sidebar-bg{top:0;right:-230px;width:230px;-webkit-transition:right .3s ease-in-out;-o-transition:right .3s ease-in-out;transition:right .3s ease-in-out}.control-sidebar{position:absolute;padding-top:50px;z-index:1010}@media (max-width:768px){.control-sidebar{padding-top:100px}.nav-tabs.control-sidebar-tabs{display:table}.nav-tabs.control-sidebar-tabs>li{display:table-cell}}.control-sidebar>.tab-content{padding:10px 15px}.control-sidebar-open .control-sidebar,.control-sidebar-open .control-sidebar-bg,.control-sidebar.control-sidebar-open,.control-sidebar.control-sidebar-open+.control-sidebar-bg{right:0}.nav-tabs.control-sidebar-tabs>li:first-of-type>a,.nav-tabs.control-sidebar-tabs>li:first-of-type>a:focus,.nav-tabs.control-sidebar-tabs>li:first-of-type>a:hover{border-left-width:0}.nav-tabs.control-sidebar-tabs>li>a{border-radius:0}.nav-tabs.control-sidebar-tabs>li>a,.nav-tabs.control-sidebar-tabs>li>a:hover{border-top:none;border-right:none;border-left:1px solid transparent;border-bottom:1px solid transparent}.nav-tabs.control-sidebar-tabs>li>a .icon{font-size:16px}.nav-tabs.control-sidebar-tabs>li.active>a,.nav-tabs.control-sidebar-tabs>li.active>a:active,.nav-tabs.control-sidebar-tabs>li.active>a:focus,.nav-tabs.control-sidebar-tabs>li.active>a:hover{border-top:none;border-right:none;border-bottom:none}.control-sidebar-heading{font-weight:400;font-size:16px;padding:10px 0;margin-bottom:10px}.control-sidebar-subheading{display:block;font-weight:400;font-size:14px}.control-sidebar-menu{list-style:none;padding:0;margin:0 -15px}.control-sidebar-menu>li>a{display:block;padding:10px 15px}.control-sidebar-menu>li>a:after,.control-sidebar-menu>li>a:before{content:" ";display:table}.control-sidebar-menu>li>a>.control-sidebar-subheading{margin-top:0}.control-sidebar-menu .menu-icon{float:left;width:35px;height:35px;border-radius:50%;text-align:center;line-height:35px}.control-sidebar-menu .menu-info{margin-left:45px;margin-top:3px}.control-sidebar-menu .menu-info>.control-sidebar-subheading,.control-sidebar-menu .progress{margin:0}.control-sidebar-menu .menu-info>p{margin:0;font-size:11px}.control-sidebar-dark{color:#b8c7ce}.control-sidebar-dark,.control-sidebar-dark+.control-sidebar-bg{background:#222d32}.control-sidebar-dark .nav-tabs.control-sidebar-tabs{border-bottom:#1c2529}.control-sidebar-dark .nav-tabs.control-sidebar-tabs>li>a{background:#181f23;color:#b8c7ce}.control-sidebar-dark .control-sidebar-heading,.control-sidebar-dark .control-sidebar-subheading,.control-sidebar-dark .nav-tabs.control-sidebar-tabs>li>a:hover{color:#fff}.control-sidebar-dark .nav-tabs.control-sidebar-tabs>li>a,.control-sidebar-dark .nav-tabs.control-sidebar-tabs>li>a:focus,.control-sidebar-dark .nav-tabs.control-sidebar-tabs>li>a:hover{border-left-color:#141a1d;border-bottom-color:#141a1d}.control-sidebar-dark .nav-tabs.control-sidebar-tabs>li>a:active,.control-sidebar-dark .nav-tabs.control-sidebar-tabs>li>a:focus,.control-sidebar-dark .nav-tabs.control-sidebar-tabs>li>a:hover{background:#1c2529}.control-sidebar-dark .nav-tabs.control-sidebar-tabs>li.active>a,.control-sidebar-dark .nav-tabs.control-sidebar-tabs>li.active>a:active,.control-sidebar-dark .nav-tabs.control-sidebar-tabs>li.active>a:focus,.control-sidebar-dark .nav-tabs.control-sidebar-tabs>li.active>a:hover{background:#222d32;color:#fff}.control-sidebar-dark .control-sidebar-menu>li>a:hover{background:#1e282c}.control-sidebar-dark .control-sidebar-menu>li>a .menu-info>p{color:#b8c7ce}.control-sidebar-light{color:#5e5e5e}.control-sidebar-light,.control-sidebar-light+.control-sidebar-bg{background:#f9fafc;border-left:1px solid #d2d6de}.control-sidebar-light .nav-tabs.control-sidebar-tabs{border-bottom:#d2d6de}.control-sidebar-light .nav-tabs.control-sidebar-tabs>li>a{background:#e8ecf4;color:#444}.control-sidebar-light .nav-tabs.control-sidebar-tabs>li>a,.control-sidebar-light .nav-tabs.control-sidebar-tabs>li>a:focus,.control-sidebar-light .nav-tabs.control-sidebar-tabs>li>a:hover{border-left-color:#d2d6de;border-bottom-color:#d2d6de}.control-sidebar-light .nav-tabs.control-sidebar-tabs>li>a:active,.control-sidebar-light .nav-tabs.control-sidebar-tabs>li>a:focus,.control-sidebar-light .nav-tabs.control-sidebar-tabs>li>a:hover{background:#eff1f7}.control-sidebar-light .nav-tabs.control-sidebar-tabs>li.active>a,.control-sidebar-light .nav-tabs.control-sidebar-tabs>li.active>a:active,.control-sidebar-light .nav-tabs.control-sidebar-tabs>li.active>a:focus,.control-sidebar-light .nav-tabs.control-sidebar-tabs>li.active>a:hover{background:#f9fafc;color:#111}.control-sidebar-light .control-sidebar-heading,.control-sidebar-light .control-sidebar-subheading{color:#111}.control-sidebar-light .control-sidebar-menu{margin-left:-14px}.control-sidebar-light .control-sidebar-menu>li>a:hover{background:#f4f4f5}.control-sidebar-light .control-sidebar-menu>li>a .menu-info>p{color:#5e5e5e}.dropdown-menu{-webkit-box-shadow:none;box-shadow:none;border-color:#eee}.dropdown-menu>li>a{color:#777}.dropdown-menu>li>a>.fa,.dropdown-menu>li>a>.glyphicon,.dropdown-menu>li>a>.ion{margin-right:10px}.dropdown-menu>li>a:hover{background-color:#e1e3e9;color:#333}.dropdown-menu>.divider{background-color:#eee}.navbar-nav>.messages-menu>.dropdown-menu,.navbar-nav>.notifications-menu>.dropdown-menu,.navbar-nav>.tasks-menu>.dropdown-menu{width:280px;padding:0;margin:0;top:100%}.navbar-nav>.messages-menu>.dropdown-menu>li,.navbar-nav>.notifications-menu>.dropdown-menu>li,.navbar-nav>.tasks-menu>.dropdown-menu>li{position:relative}.navbar-nav>.messages-menu>.dropdown-menu>li.header,.navbar-nav>.notifications-menu>.dropdown-menu>li.header,.navbar-nav>.tasks-menu>.dropdown-menu>li.header{border-top-left-radius:4px;border-top-right-radius:4px;border-bottom-right-radius:0;border-bottom-left-radius:0;background-color:#fff;padding:7px 10px;border-bottom:1px solid #f4f4f4;color:#444;font-size:14px}.navbar-nav>.messages-menu>.dropdown-menu>li.footer>a,.navbar-nav>.notifications-menu>.dropdown-menu>li.footer>a,.navbar-nav>.tasks-menu>.dropdown-menu>li.footer>a{border-top-left-radius:0;border-top-right-radius:0;border-bottom-right-radius:4px;border-bottom-left-radius:4px;font-size:12px;background-color:#fff;padding:7px 10px;border-bottom:1px solid #eee;color:#444!important;text-align:center}@media (max-width:991px){.navbar-nav>.messages-menu>.dropdown-menu>li.footer>a,.navbar-nav>.notifications-menu>.dropdown-menu>li.footer>a,.navbar-nav>.tasks-menu>.dropdown-menu>li.footer>a{background:#fff!important;color:#444!important}}.navbar-nav>.messages-menu>.dropdown-menu>li.footer>a:hover,.navbar-nav>.notifications-menu>.dropdown-menu>li.footer>a:hover,.navbar-nav>.tasks-menu>.dropdown-menu>li.footer>a:hover{text-decoration:none;font-weight:400}.navbar-nav>.messages-menu>.dropdown-menu>li .menu,.navbar-nav>.notifications-menu>.dropdown-menu>li .menu,.navbar-nav>.tasks-menu>.dropdown-menu>li .menu{max-height:200px;margin:0;padding:0;list-style:none;overflow-x:hidden}.navbar-nav>.messages-menu>.dropdown-menu>li .menu>li>a,.navbar-nav>.notifications-menu>.dropdown-menu>li .menu>li>a,.navbar-nav>.tasks-menu>.dropdown-menu>li .menu>li>a{display:block;white-space:nowrap;border-bottom:1px solid #f4f4f4}.navbar-nav>.messages-menu>.dropdown-menu>li .menu>li>a:hover,.navbar-nav>.notifications-menu>.dropdown-menu>li .menu>li>a:hover,.navbar-nav>.tasks-menu>.dropdown-menu>li .menu>li>a:hover{background:#f4f4f4;text-decoration:none}.navbar-nav>.notifications-menu>.dropdown-menu>li .menu>li>a{color:#444;overflow:hidden;text-overflow:ellipsis;padding:10px}.navbar-nav>.notifications-menu>.dropdown-menu>li .menu>li>a>.fa,.navbar-nav>.notifications-menu>.dropdown-menu>li .menu>li>a>.glyphicon,.navbar-nav>.notifications-menu>.dropdown-menu>li .menu>li>a>.ion{width:20px}.navbar-nav>.messages-menu>.dropdown-menu>li .menu>li>a{margin:0;padding:10px}.navbar-nav>.messages-menu>.dropdown-menu>li .menu>li>a>div>img{margin:auto 10px auto auto;width:40px;height:40px}.navbar-nav>.messages-menu>.dropdown-menu>li .menu>li>a>h4{padding:0;margin:0 0 0 45px;color:#444;font-size:15px;position:relative}.navbar-nav>.messages-menu>.dropdown-menu>li .menu>li>a>h4>small{color:#999;font-size:10px;position:absolute;top:0;right:0}.navbar-nav>.messages-menu>.dropdown-menu>li .menu>li>a>p{margin:0 0 0 45px;font-size:12px;color:#888}.navbar-nav>.messages-menu>.dropdown-menu>li .menu>li>a:after,.navbar-nav>.messages-menu>.dropdown-menu>li .menu>li>a:before{content:" ";display:table}.navbar-nav>.tasks-menu>.dropdown-menu>li .menu>li>a{padding:10px}.navbar-nav>.tasks-menu>.dropdown-menu>li .menu>li>a>h3{font-size:14px;padding:0;margin:0 0 10px;color:#666}.navbar-nav>.tasks-menu>.dropdown-menu>li .menu>li>a>.progress{padding:0;margin:0}.navbar-nav>.user-menu>.dropdown-menu{border-top-right-radius:0;border-top-left-radius:0;padding:1px 0 0;border-top-width:0;width:280px}.navbar-nav>.user-menu>.dropdown-menu,.navbar-nav>.user-menu>.dropdown-menu>.user-body{border-bottom-right-radius:4px;border-bottom-left-radius:4px}.navbar-nav>.user-menu>.dropdown-menu>li.user-header{height:175px;padding:10px;text-align:center}.navbar-nav>.user-menu>.dropdown-menu>li.user-header>img{z-index:5;height:90px;width:90px;border:3px solid;border-color:transparent;border-color:rgba(255,255,255,.2)}.navbar-nav>.user-menu>.dropdown-menu>li.user-header>p{z-index:5;color:#fff;color:rgba(255,255,255,.8);font-size:17px;margin-top:10px}.navbar-nav>.user-menu>.dropdown-menu>li.user-header>p>small{display:block;font-size:12px}.navbar-nav>.user-menu>.dropdown-menu>.user-body:after,.navbar-nav>.user-menu>.dropdown-menu>.user-body:before,.navbar-nav>.user-menu>.dropdown-menu>.user-footer:after,.navbar-nav>.user-menu>.dropdown-menu>.user-footer:before{display:table;content:" "}.navbar-nav>.user-menu>.dropdown-menu>.user-body{padding:15px;border-bottom:1px solid #f4f4f4;border-top:1px solid #ddd}.navbar-nav>.user-menu>.dropdown-menu>.user-body a{color:#444!important}@media (max-width:991px){.navbar-nav>.user-menu>.dropdown-menu>.user-body a{background:#fff!important;color:#444!important}.navbar-nav>.user-menu>.dropdown-menu>.user-footer .btn-default:hover{background-color:#f9f9f9}}.navbar-nav>.user-menu>.dropdown-menu>.user-footer{background-color:#f9f9f9;padding:10px}.navbar-nav>.user-menu>.dropdown-menu>.user-footer .btn-default{color:#666}.navbar-nav>.user-menu .user-image{float:left;width:25px;height:25px;border-radius:50%;margin-right:10px;margin-top:-2px}@media (max-width:767px){.navbar-nav>.user-menu .user-image{float:none;margin-right:0;margin-top:-8px;line-height:10px}}.open:not(.dropup)>.animated-dropdown-menu{-webkit-backface-visibility:visible!important;backface-visibility:visible!important;-webkit-animation:flipInX .7s both;-o-animation:flipInX .7s both;animation:flipInX .7s both}@-o-keyframes flipInX{0%{transform:perspective(400px) rotate3d(1,0,0,90deg);-o-transition-timing-function:ease-in;transition-timing-function:ease-in;opacity:0}40%{transform:perspective(400px) rotate3d(1,0,0,-20deg);-o-transition-timing-function:ease-in;transition-timing-function:ease-in}60%{transform:perspective(400px) rotate3d(1,0,0,10deg);opacity:1}80%{transform:perspective(400px) rotate3d(1,0,0,-5deg)}100%{transform:perspective(400px)}}@keyframes flipInX{0%{-webkit-transform:perspective(400px) rotate3d(1,0,0,90deg);transform:perspective(400px) rotate3d(1,0,0,90deg);-webkit-transition-timing-function:ease-in;-o-transition-timing-function:ease-in;transition-timing-function:ease-in;opacity:0}40%{-webkit-transform:perspective(400px) rotate3d(1,0,0,-20deg);transform:perspective(400px) rotate3d(1,0,0,-20deg);-webkit-transition-timing-function:ease-in;-o-transition-timing-function:ease-in;transition-timing-function:ease-in}60%{-webkit-transform:perspective(400px) rotate3d(1,0,0,10deg);transform:perspective(400px) rotate3d(1,0,0,10deg);opacity:1}80%{-webkit-transform:perspective(400px) rotate3d(1,0,0,-5deg);transform:perspective(400px) rotate3d(1,0,0,-5deg)}100%{-webkit-transform:perspective(400px);transform:perspective(400px)}}@-webkit-keyframes flipInX{0%{-webkit-transform:perspective(400px) rotate3d(1,0,0,90deg);-webkit-transition-timing-function:ease-in;opacity:0}40%{-webkit-transform:perspective(400px) rotate3d(1,0,0,-20deg);-webkit-transition-timing-function:ease-in}60%{-webkit-transform:perspective(400px) rotate3d(1,0,0,10deg);opacity:1}80%{-webkit-transform:perspective(400px) rotate3d(1,0,0,-5deg)}100%{-webkit-transform:perspective(400px)}}.direct-chat-messages,.direct-chat.chat-pane-open .direct-chat-contacts{-webkit-transform:translate(0,0);-ms-transform:translate(0,0);-o-transform:translate(0,0)}.navbar-custom-menu>.navbar-nav>li{position:relative}.navbar-custom-menu>.navbar-nav>li>.dropdown-menu{position:absolute;right:0;left:auto}@media (max-width:991px){.navbar-custom-menu>.navbar-nav{float:right}.navbar-custom-menu>.navbar-nav>li{position:static}.navbar-custom-menu>.navbar-nav>li>.dropdown-menu{position:absolute;right:5%;left:auto;border:1px solid #ddd;background:#fff}}.btn-group-vertical .btn.btn-flat:first-of-type,.btn-group-vertical .btn.btn-flat:last-of-type,.form-control{border-radius:0}.progress-striped .progress-bar-green,.progress-striped .progress-bar-light-blue,.progress-striped .progress-bar-primary,.progress-striped .progress-bar-success{background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:-o-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent)}.form-control{-webkit-box-shadow:none;box-shadow:none;border-color:#d2d6de}.form-control:focus{border-color:#3c8dbc;-webkit-box-shadow:none;box-shadow:none}.form-control:-ms-input-placeholder,.form-control::-moz-placeholder,.form-control::-webkit-input-placeholder{color:#bbb;opacity:1}.form-group.has-success .help-block,.form-group.has-success label{color:#00a65a}.form-control:not(select){-webkit-appearance:none;-moz-appearance:none;appearance:none}.form-group.has-success .form-control,.form-group.has-success .input-group-addon{border-color:#00a65a;-webkit-box-shadow:none;box-shadow:none}.form-group.has-warning .help-block,.form-group.has-warning label{color:#f39c12}.form-group.has-warning .form-control,.form-group.has-warning .input-group-addon{border-color:#f39c12;-webkit-box-shadow:none;box-shadow:none}.form-group.has-error .help-block,.form-group.has-error label{color:#dd4b39}.form-group.has-error .form-control,.form-group.has-error .input-group-addon{border-color:#dd4b39;-webkit-box-shadow:none;box-shadow:none}.input-group .input-group-addon{border-radius:0;border-color:#d2d6de;background-color:#fff}.progress,.progress .progress-bar,.progress-sm,.progress-sm .progress-bar,.progress-xs,.progress-xs .progress-bar,.progress-xxs,.progress-xxs .progress-bar,.progress.sm,.progress.sm .progress-bar,.progress.xs,.progress.xs .progress-bar,.progress.xxs,.progress.xxs .progress-bar,.progress>.progress-bar,.progress>.progress-bar .progress-bar{border-radius:1px}.icheck>label{padding-left:0}.form-control-feedback.fa{line-height:34px}.form-group-lg .form-control+.form-control-feedback.fa,.input-group-lg+.form-control-feedback.fa,.input-lg+.form-control-feedback.fa{line-height:46px}.form-group-sm .form-control+.form-control-feedback.fa,.input-group-sm+.form-control-feedback.fa,.input-sm+.form-control-feedback.fa{line-height:30px}.progress,.progress>.progress-bar{-webkit-box-shadow:none;box-shadow:none}.progress-sm,.progress.sm{height:10px}.progress-xs,.progress.xs{height:7px}.progress-xxs,.progress.xxs{height:3px}.progress.vertical{position:relative;width:30px;height:200px;display:inline-block;margin-right:10px}.progress.vertical>.progress-bar{width:100%;position:absolute;bottom:0}.progress.vertical.progress-sm,.progress.vertical.sm{width:20px}.progress.vertical.progress-xs,.progress.vertical.xs{width:10px}.progress.vertical.progress-xxs,.progress.vertical.xxs{width:3px}.progress-group .progress-text{font-weight:600}.progress-group .progress-number{float:right}.table tr>td .progress{margin:0}.progress-bar-light-blue,.progress-bar-primary{background-color:#3c8dbc}.progress-striped .progress-bar-light-blue,.progress-striped .progress-bar-primary{background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent)}.progress-bar-green,.progress-bar-success{background-color:#00a65a}.progress-striped .progress-bar-green,.progress-striped .progress-bar-success{background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent)}.progress-striped .progress-bar-aqua,.progress-striped .progress-bar-info,.progress-striped .progress-bar-warning,.progress-striped .progress-bar-yellow{background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:-o-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent)}.progress-bar-aqua,.progress-bar-info{background-color:#00c0ef}.progress-striped .progress-bar-aqua,.progress-striped .progress-bar-info{background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent)}.progress-bar-warning,.progress-bar-yellow{background-color:#f39c12}.progress-striped .progress-bar-warning,.progress-striped .progress-bar-yellow{background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent)}.progress-bar-danger,.progress-bar-red{background-color:#dd4b39}.progress-striped .progress-bar-danger,.progress-striped .progress-bar-red{background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:-o-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent)}.small-box{border-radius:2px;position:relative;display:block;margin-bottom:20px;-webkit-box-shadow:0 1px 1px rgba(0,0,0,.1);box-shadow:0 1px 1px rgba(0,0,0,.1)}.box,.info-box{-webkit-box-shadow:0 1px 1px rgba(0,0,0,.1)}.small-box>.inner{padding:10px}.small-box>.small-box-footer{position:relative;text-align:center;padding:3px 0;color:#fff;color:rgba(255,255,255,.8);display:block;z-index:10;background:rgba(0,0,0,.1);text-decoration:none}.small-box>.small-box-footer:hover{color:#fff;background:rgba(0,0,0,.15)}.small-box h3{font-size:38px;font-weight:700;margin:0 0 10px;white-space:nowrap;padding:0}.small-box p{font-size:15px}.small-box p>small{display:block;color:#f9f9f9;font-size:13px;margin-top:5px}.small-box h3,.small-box p{z-index:5}.small-box .icon{-webkit-transition:all .3s linear;-o-transition:all .3s linear;transition:all .3s linear;position:absolute;top:-10px;right:10px;z-index:0;font-size:90px;color:rgba(0,0,0,.15)}.small-box:hover{text-decoration:none;color:#f9f9f9}.small-box:hover .icon{font-size:95px}@media (max-width:767px){.small-box{text-align:center}.small-box .icon{display:none}.small-box p{font-size:12px}}.box{position:relative;border-radius:3px;background:#fff;border-top:3px solid #d2d6de;margin-bottom:20px;width:100%;box-shadow:0 1px 1px rgba(0,0,0,.1)}.box.box-primary{border-top-color:#3c8dbc}.box.box-info{border-top-color:#00c0ef}.box.box-danger{border-top-color:#dd4b39}.box.box-warning{border-top-color:#f39c12}.box.box-success{border-top-color:#00a65a}.box.box-default{border-top-color:#d2d6de}.box.collapsed-box .box-body,.box.collapsed-box .box-footer{display:none}.box .nav-stacked>li{border-bottom:1px solid #f4f4f4;margin:0}.box .nav-stacked>li:last-of-type{border-bottom:none}.box.height-control .box-body{max-height:300px;overflow:auto}.box .border-right{border-right:1px solid #f4f4f4}.box .border-left{border-left:1px solid #f4f4f4}.box.box-solid{border-top:0}.box.box-solid>.box-header .btn.btn-default{background:0 0}.box.box-solid>.box-header .btn:hover,.box.box-solid>.box-header a:hover{background:rgba(0,0,0,.1)}.box.box-solid.box-default{border:1px solid #d2d6de}.box.box-solid.box-default>.box-header{color:#444;background:#d2d6de;background-color:#d2d6de}.box.box-solid.box-default>.box-header .btn,.box.box-solid.box-default>.box-header a{color:#444}.box.box-solid.box-primary{border:1px solid #3c8dbc}.box.box-solid.box-primary>.box-header{color:#fff;background:#3c8dbc;background-color:#3c8dbc}.box.box-solid.box-primary>.box-header .btn,.box.box-solid.box-primary>.box-header a{color:#fff}.box.box-solid.box-info{border:1px solid #00c0ef}.box.box-solid.box-info>.box-header{color:#fff;background:#00c0ef;background-color:#00c0ef}.box.box-solid.box-info>.box-header .btn,.box.box-solid.box-info>.box-header a{color:#fff}.box.box-solid.box-danger{border:1px solid #dd4b39}.box.box-solid.box-danger>.box-header{color:#fff;background:#dd4b39;background-color:#dd4b39}.box.box-solid.box-danger>.box-header .btn,.box.box-solid.box-danger>.box-header a{color:#fff}.box.box-solid.box-warning{border:1px solid #f39c12}.box.box-solid.box-warning>.box-header{color:#fff;background:#f39c12;background-color:#f39c12}.box.box-solid.box-warning>.box-header .btn,.box.box-solid.box-warning>.box-header a{color:#fff}.box.box-solid.box-success{border:1px solid #00a65a}.box.box-solid.box-success>.box-header{color:#fff;background:#00a65a;background-color:#00a65a}.box.box-solid.box-success>.box-header .btn,.box.box-solid.box-success>.box-header a{color:#fff}.box.box-solid>.box-header>.box-tools .btn{border:0;-webkit-box-shadow:none;box-shadow:none}.box.box-solid[class*=bg]>.box-header{color:#fff}.box .box-group>.box{margin-bottom:5px}.box .knob-label{text-align:center;color:#333;font-weight:100;font-size:12px;margin-bottom:.3em}.box>.loading-img,.box>.overlay,.overlay-wrapper>.loading-img,.overlay-wrapper>.overlay{position:absolute;top:0;left:0;width:100%;height:100%}.box .overlay,.overlay-wrapper .overlay{z-index:50;background:rgba(255,255,255,.7);border-radius:3px}.box-body,.box-body .box-pane{border-top-left-radius:0;border-top-right-radius:0;border-bottom-left-radius:3px}.box .overlay>.fa,.overlay-wrapper .overlay>.fa{position:absolute;top:50%;left:50%;margin-left:-15px;margin-top:-15px;color:#000;font-size:30px}.box .overlay.dark,.overlay-wrapper .overlay.dark{background:rgba(0,0,0,.5)}.box-body:after,.box-body:before,.box-footer:after,.box-footer:before,.box-header:after,.box-header:before{content:" ";display:table}.box-body:after,.box-footer:after,.box-header:after{clear:both}.box-header{color:#444;display:block;padding:10px;position:relative}.box-header.with-border{border-bottom:1px solid #f4f4f4}.collapsed-box .box-header.with-border{border-bottom:none}.box-header .box-title,.box-header>.fa,.box-header>.glyphicon,.box-header>.ion{display:inline-block;font-size:18px;margin:0;line-height:1}.box-header>.fa,.box-header>.glyphicon,.box-header>.ion{margin-right:5px}.box-header>.box-tools{position:absolute;right:10px;top:5px}.box-header>.box-tools [data-toggle=tooltip],.timeline{position:relative}.box-header>.box-tools.pull-right .dropdown-menu{right:0;left:auto}.box-header>.box-tools .dropdown-menu>li>a{color:#444!important}.btn-box-tool{padding:5px;font-size:12px;background:0 0;color:#97a0b3}.btn-box-tool:hover,.open .btn-box-tool{color:#606c84}.btn-box-tool.btn:active{-webkit-box-shadow:none;box-shadow:none}.box-body{border-bottom-right-radius:3px;padding:10px}.no-header .box-body{border-top-right-radius:3px;border-top-left-radius:3px}.box-body>.table{margin-bottom:0}.box-body .fc{margin-top:5px}.box-body .full-width-chart{margin:-19px}.box-body.no-padding .full-width-chart{margin:-9px}.box-body .box-pane{border-bottom-right-radius:0}.box-body .box-pane-right,.box-footer{border-top-left-radius:0;border-top-right-radius:0;border-bottom-right-radius:3px}.box-body .box-pane-right{border-bottom-left-radius:0}.box-footer{border-bottom-left-radius:3px;border-top:1px solid #f4f4f4;padding:10px;background-color:#fff}@media (max-width:991px){.chart-legend>li{float:left;margin-right:10px}}.box-comments{background:#f7f7f7}.box-comments .box-comment{padding:8px 0;border-bottom:1px solid #eee}.box-comments .box-comment:after,.box-comments .box-comment:before{content:" ";display:table}.box-comments .box-comment:last-of-type{border-bottom:0}.box-comments .box-comment:first-of-type{padding-top:0}.box-comments .box-comment img{float:left}.box-comments .comment-text{margin-left:40px;color:#555}.box-comments .username{color:#444;display:block;font-weight:600}.box-comments .text-muted{font-weight:400;font-size:12px}.todo-list{margin:0;padding:0;list-style:none;overflow:auto}.todo-list>li{border-radius:2px;padding:10px;background:#f4f4f4;margin-bottom:2px;border-left:2px solid #e6e7e8;color:#444}.todo-list>li:last-of-type{margin-bottom:0}.todo-list>li>input[type=checkbox]{margin:0 10px 0 5px}.todo-list>li .text{display:inline-block;margin-left:5px;font-weight:600}.todo-list>li .label{margin-left:10px;font-size:9px}.todo-list>li .tools{display:none;float:right;color:#dd4b39}.todo-list .handle,.todo-list>li:hover .tools{display:inline-block}.todo-list>li .tools>.fa,.todo-list>li .tools>.glyphicon,.todo-list>li .tools>.ion{margin-right:5px;cursor:pointer}.todo-list>li.done{color:#999}.todo-list>li.done .text{text-decoration:line-through;font-weight:500}.todo-list>li.done .label{background:#d2d6de!important}.todo-list .danger{border-left-color:#dd4b39}.todo-list .warning{border-left-color:#f39c12}.todo-list .info{border-left-color:#00c0ef}.todo-list .success{border-left-color:#00a65a}.todo-list .primary{border-left-color:#3c8dbc}.todo-list .handle{cursor:move;margin:0 5px}.chat{padding:5px 20px 5px 10px}.chat .item{margin-bottom:10px}.chat .item:after,.chat .item:before{content:" ";display:table}.chat .item>img{width:40px;height:40px;border:2px solid transparent;border-radius:50%}.chat .item>.online{border:2px solid #00a65a}.chat .item>.offline{border:2px solid #dd4b39}.chat .item>.message{margin-left:55px;margin-top:-40px}.chat .item>.message>.name{display:block;font-weight:600}.chat .item>.attachment{border-radius:3px;background:#f4f4f4;margin-left:65px;margin-right:15px;padding:10px}.chat .item>.attachment>h4{margin:0 0 5px;font-weight:600;font-size:14px}.chat .item>.attachment>.filename,.chat .item>.attachment>p{font-weight:600;font-size:13px;font-style:italic;margin:0}.chat .item>.attachment:after,.chat .item>.attachment:before{content:" ";display:table}.info-box,.info-box-icon,.info-box-more,.info-box-number{display:block}.box-input{max-width:200px}.modal .panel-body{color:#444}.info-box{min-height:90px;background:#fff;width:100%;box-shadow:0 1px 1px rgba(0,0,0,.1);border-radius:2px;margin-bottom:15px}.info-box small{font-size:14px}.info-box .progress{background:rgba(0,0,0,.2);margin:5px -10px;height:2px}.info-box .progress,.info-box .progress .progress-bar{border-radius:0}.info-box .progress .progress-bar{background:#fff}.info-box-icon{border-top-left-radius:2px;border-top-right-radius:0;border-bottom-right-radius:0;border-bottom-left-radius:2px;float:left;height:90px;width:90px;text-align:center;font-size:45px;line-height:90px;background:rgba(0,0,0,.2)}.info-box-icon>img{max-width:100%}.info-box-content{padding:5px 10px;margin-left:90px}.info-box-number{font-weight:700;font-size:18px}.info-box-text,.progress-description{display:block;font-size:14px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.progress-description{margin:0}.timeline{margin:0 0 30px;padding:0;list-style:none}.timeline:before{content:'';position:absolute;top:0;bottom:0;width:4px;background:#ddd;left:31px;margin:0;border-radius:2px}.timeline>li{position:relative;margin-right:10px;margin-bottom:15px}.timeline>li:after,.timeline>li:before{content:" ";display:table}.timeline>li>.timeline-item{-webkit-box-shadow:0 1px 1px rgba(0,0,0,.1);box-shadow:0 1px 1px rgba(0,0,0,.1);border-radius:3px;margin-top:0;background:#fff;color:#444;margin-left:60px;margin-right:15px;padding:0;position:relative}.timeline>li>.timeline-item>.time{color:#999;float:right;padding:10px;font-size:12px}.timeline>li>.timeline-item>.timeline-header{margin:0;color:#555;border-bottom:1px solid #f4f4f4;padding:10px;font-size:16px;line-height:1.1}.timeline>li>.timeline-item>.timeline-header>a{font-weight:600}.timeline>li>.timeline-item>.timeline-body,.timeline>li>.timeline-item>.timeline-footer{padding:10px}.timeline>li>.fa,.timeline>li>.glyphicon,.timeline>li>.ion{width:30px;height:30px;font-size:15px;line-height:30px;position:absolute;color:#666;background:#d2d6de;border-radius:50%;text-align:center;left:18px;top:0}.timeline>.time-label>span{font-weight:600;padding:5px;display:inline-block;background-color:#fff;border-radius:4px}.timeline-inverse>li>.timeline-item{background:#f0f0f0;border:1px solid #ddd;-webkit-box-shadow:none;box-shadow:none}.btn,.btn-app{border-radius:3px}.timeline-inverse>li>.timeline-item>.timeline-header{border-bottom-color:#ddd}.btn{-webkit-box-shadow:none;box-shadow:none;border:1px solid transparent}.btn.btn-flat{border-radius:0;-webkit-box-shadow:none;box-shadow:none;border-width:1px}.btn:active{-webkit-box-shadow:inset 0 3px 5px rgba(0,0,0,.125);box-shadow:inset 0 3px 5px rgba(0,0,0,.125)}.btn:focus{outline:0}.btn.btn-file{position:relative;overflow:hidden}.btn.btn-file>input[type=file]{position:absolute;top:0;right:0;min-width:100%;min-height:100%;font-size:100px;text-align:right;opacity:0;filter:alpha(opacity=0);outline:0;background:#fff;cursor:inherit;display:block}.btn-app,table.text-center,table.text-center td,table.text-center th{text-align:center}.btn-default{background-color:#f9f9f9;color:#444;border-color:#888}.btn-default.hover,.btn-default:active,.btn-default:hover{background-color:#e7e7e7}.btn-primary{background-color:#3c8dbc;border-color:#367fa9;color:#fff}.btn-primary.hover,.btn-primary:active,.btn-primary:hover{background-color:#367fa9}.btn-success{background-color:#00a65a;border-color:#008d4c}.btn-success.hover,.btn-success:active,.btn-success:hover{background-color:#008d4c}.btn-info{background-color:#00c0ef;border-color:#00acd6;color:#fff}.btn-info.hover,.btn-info:active,.btn-info:hover{background-color:#00acd6}.btn-danger{background-color:#dd4b39;border-color:#d73925}.btn-danger.hover,.btn-danger:active,.btn-danger:hover{background-color:#d73925}.btn-warning{background-color:#f39c12;border-color:#e08e0b}.btn-warning.hover,.btn-warning:active,.btn-warning:hover{background-color:#e08e0b}.btn-outline{border:1px solid #fff;background:0 0;color:#fff}.btn-outline:active,.btn-outline:focus,.btn-outline:hover{color:rgba(255,255,255,.7);border-color:rgba(255,255,255,.7)}.btn-link{-webkit-box-shadow:none;box-shadow:none}.btn[class*=bg-]:hover{-webkit-box-shadow:inset 0 0 100px rgba(0,0,0,.2);box-shadow:inset 0 0 100px rgba(0,0,0,.2)}.btn-app{position:relative;padding:15px 5px;margin:0 0 10px 10px;min-width:80px;height:60px;color:#666;border:1px solid #ddd;background-color:#f4f4f4;font-size:12px}.alert,.callout{border-radius:3px}.btn-app>.fa,.btn-app>.glyphicon,.btn-app>.ion{font-size:20px;display:block}.btn-app:hover{background:#f4f4f4;color:#444;border-color:#aaa}.btn-app:active,.btn-app:focus{-webkit-box-shadow:inset 0 3px 5px rgba(0,0,0,.125);box-shadow:inset 0 3px 5px rgba(0,0,0,.125)}.btn-app>.badge{position:absolute;top:-3px;right:-10px;font-size:10px;font-weight:400}.alert h4,.callout h4,.contacts-list-name,.direct-chat-name,.nav-pills>li.active>a,.products-list .product-title{font-weight:600}.callout{margin:0 0 20px;padding:15px 30px 15px 15px;border-left:5px solid #eee}.callout a{color:#fff}.callout a:hover{color:#eee}.callout h4{margin-top:0}.callout p:last-child{margin-bottom:0}.callout .highlight,.callout code{background-color:#fff}.callout.callout-danger{border-color:#c23321}.callout.callout-warning{border-color:#c87f0a}.callout.callout-info{border-color:#0097bc}.callout.callout-success{border-color:#00733e}.alert .icon{margin-right:10px}.alert .close{color:#000;opacity:.2;filter:alpha(opacity=20)}.alert .close:hover{opacity:.5;filter:alpha(opacity=50)}.alert a{color:#fff}.dashboard-link,.skin-black .sidebar a:hover{text-decoration:none}.alert-success{border-color:#008d4c}.alert-danger,.alert-error{border-color:#d73925}.alert-warning{border-color:#e08e0b}.alert-info{border-color:#00acd6}.nav>li>a:active,.nav>li>a:focus,.nav>li>a:hover{color:#444;background:#f7f7f7}.nav-pills>li>a{border-radius:0;border-top:3px solid transparent;color:#444}.nav-pills>li>a>.fa,.nav-pills>li>a>.glyphicon,.nav-pills>li>a>.ion{margin-right:5px}.nav-pills>li.active>a,.nav-pills>li.active>a:focus,.nav-pills>li.active>a:hover{border-top-color:#3c8dbc}.nav-stacked>li>a{border-radius:0;border-top:0;border-left:3px solid transparent;color:#444}.nav-stacked>li.active>a,.nav-stacked>li.active>a:hover{background:0 0;color:#444;border-top:0;border-left-color:#3c8dbc}.nav-stacked>li.header{border-bottom:1px solid #ddd;color:#777;margin-bottom:10px;padding:5px 10px}.nav-tabs-custom{margin-bottom:20px;background:#fff;-webkit-box-shadow:0 1px 1px rgba(0,0,0,.1);box-shadow:0 1px 1px rgba(0,0,0,.1);border-radius:3px}.nav-tabs-custom>.nav-tabs{margin:0;border-bottom-color:#f4f4f4;border-top-right-radius:3px;border-top-left-radius:3px}.nav-tabs-custom>.nav-tabs>li{border-top:3px solid transparent;margin-bottom:-2px;margin-right:5px}.nav-tabs-custom>.nav-tabs>li>a{color:#444;border-radius:0}.nav-tabs-custom>.nav-tabs>li>a.text-muted,.nav-tabs-custom>.nav-tabs>li>a:hover{color:#999}.nav-tabs-custom>.nav-tabs>li>a,.nav-tabs-custom>.nav-tabs>li>a:hover{background:0 0;margin:0}.nav-tabs-custom>.nav-tabs>li:not(.active)>a:active,.nav-tabs-custom>.nav-tabs>li:not(.active)>a:focus,.nav-tabs-custom>.nav-tabs>li:not(.active)>a:hover{border-color:transparent}.nav-tabs-custom>.nav-tabs>li.active{border-top-color:#3c8dbc}.nav-tabs-custom>.nav-tabs>li.active:hover>a,.nav-tabs-custom>.nav-tabs>li.active>a{background-color:#fff;color:#444}.nav-tabs-custom>.nav-tabs>li.active>a{border-top-color:transparent;border-left-color:#f4f4f4;border-right-color:#f4f4f4}.nav-tabs-custom>.nav-tabs>li:first-of-type{margin-left:0}.nav-tabs-custom>.nav-tabs>li:first-of-type.active>a{border-left-color:transparent}.nav-tabs-custom>.nav-tabs.pull-right{float:none!important}.nav-tabs-custom>.nav-tabs.pull-right>li{float:right}.nav-tabs-custom>.nav-tabs.pull-right>li:first-of-type{margin-right:0}.nav-tabs-custom>.nav-tabs.pull-right>li:first-of-type>a{border-left-width:1px}.nav-tabs-custom>.nav-tabs.pull-right>li:first-of-type.active>a{border-left-color:#f4f4f4;border-right-color:transparent}.nav-tabs-custom>.nav-tabs>li.header{line-height:35px;padding:0 10px;font-size:20px;color:#444}.nav-tabs-custom>.nav-tabs>li.header>.fa,.nav-tabs-custom>.nav-tabs>li.header>.glyphicon,.nav-tabs-custom>.nav-tabs>li.header>.ion{margin-right:5px}.nav-tabs-custom>.tab-content{background:#fff;padding:10px;border-bottom-right-radius:3px;border-bottom-left-radius:3px}.nav-tabs-custom .dropdown.open>a:active,.nav-tabs-custom .dropdown.open>a:focus{background:0 0;color:#999}.nav-tabs-custom.tab-primary>.nav-tabs>li.active{border-top-color:#3c8dbc}.nav-tabs-custom.tab-info>.nav-tabs>li.active{border-top-color:#00c0ef}.nav-tabs-custom.tab-danger>.nav-tabs>li.active{border-top-color:#dd4b39}.nav-tabs-custom.tab-warning>.nav-tabs>li.active{border-top-color:#f39c12}.nav-tabs-custom.tab-success>.nav-tabs>li.active{border-top-color:#00a65a}.nav-tabs-custom.tab-default>.nav-tabs>li.active{border-top-color:#d2d6de}.pagination>li>a{background:#fafafa;color:#666}.pagination.pagination-flat>li>a{border-radius:0!important}.products-list{list-style:none;margin:0;padding:0}.products-list>.item{border-radius:3px;-webkit-box-shadow:0 1px 1px rgba(0,0,0,.1);box-shadow:0 1px 1px rgba(0,0,0,.1);padding:10px 0;background:#fff}.products-list>.item:after,.products-list>.item:before{content:" ";display:table}.products-list .product-img{float:left}.products-list .product-img img{width:50px;height:50px}.products-list .product-info{margin-left:60px}.products-list .product-description{display:block;color:#999;overflow:hidden;white-space:nowrap;text-overflow:ellipsis}.product-list-in-box>.item{-webkit-box-shadow:none;box-shadow:none;border-radius:0;border-bottom:1px solid #f4f4f4}.product-list-in-box>.item:last-of-type{border-bottom-width:0}.table>tbody>tr>td,.table>tbody>tr>th,.table>tfoot>tr>td,.table>tfoot>tr>th,.table>thead>tr>td,.table>thead>tr>th{border-top:1px solid #f4f4f4}.table>thead>tr>th{border-bottom:2px solid #f4f4f4}.table tr td .progress{margin-top:5px}.table-bordered,.table-bordered>tbody>tr>td,.table-bordered>tbody>tr>th,.table-bordered>tfoot>tr>td,.table-bordered>tfoot>tr>th,.table-bordered>thead>tr>td,.table-bordered>thead>tr>th{border:1px solid #f4f4f4}.table-bordered>thead>tr>td,.table-bordered>thead>tr>th{border-bottom-width:2px}.table.no-border,.table.no-border td,.table.no-border th{border:0}.table.align th{text-align:left}.table.align td{text-align:right}.label-default{background-color:#d2d6de;color:#444}.direct-chat .box-body{border-bottom-right-radius:0;border-bottom-left-radius:0;position:relative;overflow-x:hidden;padding:0}.direct-chat.chat-pane-open .direct-chat-contacts{transform:translate(0,0)}.direct-chat-messages{transform:translate(0,0);padding:10px;height:250px;overflow:auto}.direct-chat-msg,.direct-chat-text{display:block}.direct-chat-msg{margin-bottom:10px}.direct-chat-msg:after,.direct-chat-msg:before{content:" ";display:table}.direct-chat-contacts,.direct-chat-messages{-webkit-transition:-webkit-transform .5s ease-in-out;-o-transition:-o-transform .5s ease-in-out;transition:transform .5s ease-in-out}.direct-chat-text{border-radius:5px;position:relative;padding:5px 10px;background:#d2d6de;border:1px solid #d2d6de;margin:5px 0 0 50px;color:#444}.direct-chat-text:after,.direct-chat-text:before{position:absolute;right:100%;top:15px;border:solid transparent;border-right-color:#d2d6de;content:' ';height:0;width:0;pointer-events:none}.direct-chat-text:after{border-width:5px;margin-top:-5px}.direct-chat-text:before{border-width:6px;margin-top:-6px}.right .direct-chat-text{margin-right:50px;margin-left:0}.right .direct-chat-text:after,.right .direct-chat-text:before{right:auto;left:100%;border-right-color:transparent;border-left-color:#d2d6de}.direct-chat-img{border-radius:50%;float:left;width:40px;height:40px}.right .direct-chat-img{float:right}.direct-chat-info{display:block;margin-bottom:2px;font-size:12px}.direct-chat-timestamp{color:#999}.direct-chat-contacts-open .direct-chat-contacts{-webkit-transform:translate(0,0);-ms-transform:translate(0,0);-o-transform:translate(0,0);transform:translate(0,0)}.direct-chat-contacts{-webkit-transform:translate(101%,0);-ms-transform:translate(101%,0);-o-transform:translate(101%,0);transform:translate(101%,0);position:absolute;top:0;bottom:0;height:250px;width:100%;background:#222d32;color:#fff;overflow:auto}.contacts-list>li{border-bottom:1px solid rgba(0,0,0,.2);padding:10px;margin:0}.contacts-list>li:after,.contacts-list>li:before{content:" ";display:table}.contacts-list-name,.contacts-list-status,.users-list-date,.users-list-name{display:block}.contacts-list>li:last-of-type{border-bottom:none}.contacts-list-img{border-radius:50%;width:40px;float:left}.contacts-list-info{margin-left:45px;color:#fff}.contacts-list-status{font-size:12px}.contacts-list-date{color:#aaa;font-weight:400}.contacts-list-msg{color:#999}.direct-chat-danger .right>.direct-chat-text{background:#dd4b39;border-color:#dd4b39;color:#fff}.direct-chat-danger .right>.direct-chat-text:after,.direct-chat-danger .right>.direct-chat-text:before{border-left-color:#dd4b39}.direct-chat-primary .right>.direct-chat-text{background:#3c8dbc;border-color:#3c8dbc;color:#fff}.direct-chat-primary .right>.direct-chat-text:after,.direct-chat-primary .right>.direct-chat-text:before{border-left-color:#3c8dbc}.direct-chat-warning .right>.direct-chat-text{background:#f39c12;border-color:#f39c12;color:#fff}.direct-chat-warning .right>.direct-chat-text:after,.direct-chat-warning .right>.direct-chat-text:before{border-left-color:#f39c12}.direct-chat-info .right>.direct-chat-text{background:#00c0ef;border-color:#00c0ef;color:#fff}.direct-chat-info .right>.direct-chat-text:after,.direct-chat-info .right>.direct-chat-text:before{border-left-color:#00c0ef}.direct-chat-success .right>.direct-chat-text{background:#00a65a;border-color:#00a65a;color:#fff}.direct-chat-success .right>.direct-chat-text:after,.direct-chat-success .right>.direct-chat-text:before{border-left-color:#00a65a}.users-list>li{width:25%;float:left;padding:10px;text-align:center}.users-list>li img{border-radius:50%;max-width:100%;height:auto}.users-list>li>a:hover,.users-list>li>a:hover .users-list-name{color:#999}.users-list-name{font-weight:600;color:#444;overflow:hidden;white-space:nowrap;text-overflow:ellipsis}.users-list-date{color:#999;font-size:12px}.carousel-control.left,.carousel-control.right{background-image:none}.carousel-control>.fa{font-size:40px;position:absolute;top:50%;z-index:5;display:inline-block;margin-top:-20px}.modal{background:rgba(0,0,0,.3)}.modal-content{border-radius:0;-webkit-box-shadow:0 2px 3px rgba(0,0,0,.125);box-shadow:0 2px 3px rgba(0,0,0,.125);border:0}@media (min-width:768px){.modal-content{-webkit-box-shadow:0 2px 3px rgba(0,0,0,.125);box-shadow:0 2px 3px rgba(0,0,0,.125)}}.modal-header{border-bottom-color:#f4f4f4}.modal-footer{border-top-color:#f4f4f4}.modal-primary .modal-footer,.modal-primary .modal-header{border-color:#307095}.modal-warning .modal-footer,.modal-warning .modal-header{border-color:#c87f0a}.modal-info .modal-footer,.modal-info .modal-header{border-color:#0097bc}.modal-success .modal-footer,.modal-success .modal-header{border-color:#00733e}.modal-danger .modal-footer,.modal-danger .modal-header{border-color:#c23321}.box-widget{border:none;position:relative}.widget-user .widget-user-header{padding:20px;height:120px;border-top-right-radius:3px;border-top-left-radius:3px}.widget-user .widget-user-username{margin-top:0;margin-bottom:5px;font-size:25px;font-weight:300;text-shadow:0 1px 1px rgba(0,0,0,.2)}.widget-user .widget-user-desc{margin-top:0}.widget-user .widget-user-image{position:absolute;top:65px;left:50%;margin-left:-45px}.widget-user .widget-user-image>img{width:90px;height:auto;border:3px solid #fff}.mailbox-controls.with-border,.mailbox-read-info{border-bottom:1px solid #f4f4f4}.widget-user .box-footer{padding-top:30px}.widget-user-2 .widget-user-header{padding:20px;border-top-right-radius:3px;border-top-left-radius:3px}.widget-user-2 .widget-user-username{margin-top:5px;margin-bottom:5px;font-size:25px;font-weight:300}.widget-user-2 .widget-user-desc{margin-top:0}.widget-user-2 .widget-user-desc,.widget-user-2 .widget-user-username{margin-left:75px}.widget-user-2 .widget-user-image>img{width:65px;height:auto;float:left}.mailbox-messages>.table{margin:0}.mailbox-controls{padding:5px}.mailbox-read-info{padding:10px}.mailbox-read-info h3{font-size:20px;margin:0}.mailbox-read-info h5{margin:0;padding:5px 0 0}.mailbox-read-time{color:#999;font-size:13px}.mailbox-read-message{padding:10px}.mailbox-attachments li{float:left;width:200px;border:1px solid #eee;margin-bottom:10px;margin-right:10px}.mailbox-attachment-name{font-weight:700;color:#666}.mailbox-attachment-icon,.mailbox-attachment-info,.mailbox-attachment-size{display:block}.mailbox-attachment-info{padding:10px;background:#f4f4f4}.mailbox-attachment-size{color:#999;font-size:12px}.mailbox-attachment-icon{text-align:center;font-size:65px;color:#666;padding:20px 10px}.lockscreen-logo a,.login-logo a,.register-logo a{color:#444}.mailbox-attachment-icon.has-img{padding:0}.mailbox-attachment-icon.has-img>img{max-width:100%;height:auto}.lockscreen{background:#d2d6de}.lockscreen-logo{font-size:35px;text-align:center;margin-bottom:25px;font-weight:300}.lockscreen-wrapper{max-width:400px;margin:0 auto;margin-top:10%}.lockscreen .lockscreen-name{text-align:center;font-weight:600}.lockscreen-item{border-radius:4px;padding:0;background:#fff;position:relative;margin:10px auto 30px;width:290px}.lockscreen-image{border-radius:50%;position:absolute;left:-10px;top:-25px;background:#fff;padding:5px;z-index:10}.lockscreen-image>img{border-radius:50%;width:70px;height:70px}.lockscreen-credentials{margin-left:70px}.lockscreen-credentials .form-control{border:0}.lockscreen-credentials .btn{background-color:#fff;border:0;padding:0 10px}.lockscreen-footer{margin-top:10px}.login-logo,.register-logo{font-size:35px;text-align:center;margin-bottom:25px;font-weight:300}.login-page,.register-page{background:#d2d6de}.login-box,.register-box{width:360px;margin:7% auto}@media (max-width:768px){.login-box,.register-box{width:90%;margin-top:20px}}.login-box-body,.register-box-body{background:#fff;padding:20px;border-top:0;color:#666}.login-box-body .form-control-feedback,.register-box-body .form-control-feedback{color:#777}.login-box-msg,.register-box-msg{margin:0;text-align:center;padding:0 20px 20px}.social-auth-links{margin:10px 0}.error-page{width:600px;margin:20px auto 0}.error-page>.headline{float:left;font-size:100px;font-weight:300}.error-page>.error-content{margin-left:190px;display:block}.error-page>.error-content>h3{font-weight:300;font-size:25px}@media (max-width:991px){.error-page{width:100%}.error-page>.headline{float:none;text-align:center}.error-page>.error-content{margin-left:0}.error-page>.error-content>h3{text-align:center}}.invoice{position:relative;background:#fff;border:1px solid #f4f4f4;padding:20px;margin:10px 25px}.btn-adn.active,.btn-adn:active,.btn-bitbucket.active,.btn-bitbucket:active,.btn-dropbox.active,.btn-dropbox:active,.btn-facebook.active,.btn-facebook:active,.btn-flickr.active,.btn-flickr:active,.btn-foursquare.active,.btn-foursquare:active,.btn-github.active,.btn-github:active,.btn-google.active,.btn-google:active,.btn-instagram.active,.btn-instagram:active,.btn-microsoft.active,.btn-microsoft:active,.btn-openid.active,.btn-openid:active,.btn-pinterest.active,.btn-pinterest:active,.btn-reddit.active,.btn-reddit:active,.btn-soundcloud.active,.btn-soundcloud:active,.btn-tumblr.active,.btn-tumblr:active,.btn-twitter.active,.btn-twitter:active,.btn-vimeo.active,.btn-vimeo:active,.btn-vk.active,.btn-vk:active,.btn-yahoo.active,.btn-yahoo:active,.open>.dropdown-toggle.btn-adn,.open>.dropdown-toggle.btn-bitbucket,.open>.dropdown-toggle.btn-dropbox,.open>.dropdown-toggle.btn-facebook,.open>.dropdown-toggle.btn-flickr,.open>.dropdown-toggle.btn-foursquare,.open>.dropdown-toggle.btn-github,.open>.dropdown-toggle.btn-google,.open>.dropdown-toggle.btn-instagram,.open>.dropdown-toggle.btn-microsoft,.open>.dropdown-toggle.btn-openid,.open>.dropdown-toggle.btn-pinterest,.open>.dropdown-toggle.btn-reddit,.open>.dropdown-toggle.btn-soundcloud,.open>.dropdown-toggle.btn-tumblr,.open>.dropdown-toggle.btn-twitter,.open>.dropdown-toggle.btn-vimeo,.open>.dropdown-toggle.btn-vk,.open>.dropdown-toggle.btn-yahoo{background-image:none}.invoice-title{margin-top:0}.profile-user-img{margin:0 auto;width:100px;padding:3px;border:3px solid #d2d6de}.profile-username{font-size:21px;margin-top:5px}.post{border-bottom:1px solid #d2d6de;margin-bottom:15px;padding-bottom:15px;color:#666}.post:last-of-type{border-bottom:0;margin-bottom:0;padding-bottom:0}.post .user-block{margin-bottom:15px}.btn-social{position:relative;padding-left:44px;text-align:left;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.btn-social>:first-child{position:absolute;left:0;top:0;bottom:0;width:32px;line-height:34px;font-size:1.6em;text-align:center;border-right:1px solid rgba(0,0,0,.2)}.btn-social.btn-lg{padding-left:61px}.btn-social.btn-lg>:first-child{line-height:45px;width:45px;font-size:1.8em}.btn-social.btn-sm{padding-left:38px}.btn-social.btn-sm>:first-child{line-height:28px;width:28px;font-size:1.4em}.btn-social.btn-xs{padding-left:30px}.btn-social.btn-xs>:first-child{line-height:20px;width:20px;font-size:1.2em}.btn-social-icon{position:relative;padding-left:44px;text-align:left;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;height:34px;width:34px;padding:0}.btn-social-icon>:first-child{position:absolute;left:0;top:0;bottom:0;line-height:34px;font-size:1.6em}.btn-social-icon.btn-lg>:first-child{line-height:45px;width:45px;font-size:1.8em}.btn-social-icon.btn-sm>:first-child{line-height:28px;width:28px;font-size:1.4em}.btn-social-icon.btn-xs>:first-child{line-height:20px;width:20px;font-size:1.2em}.btn-social-icon>:first-child{border:none;text-align:center;width:100%}.btn-social-icon.btn-lg{height:45px;width:45px;padding-left:0;padding-right:0}.btn-social-icon.btn-sm{height:30px;width:30px;padding-left:0;padding-right:0}.btn-social-icon.btn-xs{height:22px;width:22px;padding-left:0;padding-right:0}.fc-day-number,.fc-header-right{padding-right:10px}.btn-adn{color:#fff;background-color:#d87a68;border-color:rgba(0,0,0,.2)}.btn-adn.active,.btn-adn.focus,.btn-adn:active,.btn-adn:focus,.btn-adn:hover,.open>.dropdown-toggle.btn-adn{color:#fff;background-color:#ce563f;border-color:rgba(0,0,0,.2)}.btn-adn .badge{color:#d87a68;background-color:#fff}.btn-bitbucket{color:#fff;background-color:#205081;border-color:rgba(0,0,0,.2)}.btn-bitbucket.active,.btn-bitbucket.focus,.btn-bitbucket:active,.btn-bitbucket:focus,.btn-bitbucket:hover,.open>.dropdown-toggle.btn-bitbucket{color:#fff;background-color:#163758;border-color:rgba(0,0,0,.2)}.btn-bitbucket .badge{color:#205081;background-color:#fff}.btn-dropbox{color:#fff;background-color:#1087dd;border-color:rgba(0,0,0,.2)}.btn-dropbox.active,.btn-dropbox.focus,.btn-dropbox:active,.btn-dropbox:focus,.btn-dropbox:hover,.open>.dropdown-toggle.btn-dropbox{color:#fff;background-color:#0d6aad;border-color:rgba(0,0,0,.2)}.btn-dropbox .badge{color:#1087dd;background-color:#fff}.btn-facebook{color:#fff;background-color:#3b5998;border-color:rgba(0,0,0,.2)}.btn-facebook.active,.btn-facebook.focus,.btn-facebook:active,.btn-facebook:focus,.btn-facebook:hover,.open>.dropdown-toggle.btn-facebook{color:#fff;background-color:#2d4373;border-color:rgba(0,0,0,.2)}.btn-facebook .badge{color:#3b5998;background-color:#fff}.btn-flickr{color:#fff;background-color:#ff0084;border-color:rgba(0,0,0,.2)}.btn-flickr.active,.btn-flickr.focus,.btn-flickr:active,.btn-flickr:focus,.btn-flickr:hover,.open>.dropdown-toggle.btn-flickr{color:#fff;background-color:#cc006a;border-color:rgba(0,0,0,.2)}.btn-flickr .badge{color:#ff0084;background-color:#fff}.btn-foursquare{color:#fff;background-color:#f94877;border-color:rgba(0,0,0,.2)}.btn-foursquare.active,.btn-foursquare.focus,.btn-foursquare:active,.btn-foursquare:focus,.btn-foursquare:hover,.open>.dropdown-toggle.btn-foursquare{color:#fff;background-color:#f71752;border-color:rgba(0,0,0,.2)}.btn-foursquare .badge{color:#f94877;background-color:#fff}.btn-github{color:#fff;background-color:#444;border-color:rgba(0,0,0,.2)}.btn-github.active,.btn-github.focus,.btn-github:active,.btn-github:focus,.btn-github:hover,.open>.dropdown-toggle.btn-github{color:#fff;background-color:#2b2b2b;border-color:rgba(0,0,0,.2)}.btn-github .badge{color:#444;background-color:#fff}.btn-google{color:#fff;background-color:#dd4b39;border-color:rgba(0,0,0,.2)}.btn-google.active,.btn-google.focus,.btn-google:active,.btn-google:focus,.btn-google:hover,.open>.dropdown-toggle.btn-google{color:#fff;background-color:#c23321;border-color:rgba(0,0,0,.2)}.btn-google .badge{color:#dd4b39;background-color:#fff}.btn-instagram{color:#fff;background-color:#3f729b;border-color:rgba(0,0,0,.2)}.btn-instagram.active,.btn-instagram.focus,.btn-instagram:active,.btn-instagram:focus,.btn-instagram:hover,.open>.dropdown-toggle.btn-instagram{color:#fff;background-color:#305777;border-color:rgba(0,0,0,.2)}.btn-instagram .badge{color:#3f729b;background-color:#fff}.btn-linkedin{color:#fff;background-color:#007bb6;border-color:rgba(0,0,0,.2)}.btn-linkedin.active,.btn-linkedin.focus,.btn-linkedin:active,.btn-linkedin:focus,.btn-linkedin:hover,.open>.dropdown-toggle.btn-linkedin{color:#fff;background-color:#005983;border-color:rgba(0,0,0,.2)}.btn-linkedin.active,.btn-linkedin:active,.open>.dropdown-toggle.btn-linkedin{background-image:none}.btn-linkedin .badge{color:#007bb6;background-color:#fff}.btn-microsoft{color:#fff;background-color:#2672ec;border-color:rgba(0,0,0,.2)}.btn-microsoft.active,.btn-microsoft.focus,.btn-microsoft:active,.btn-microsoft:focus,.btn-microsoft:hover,.open>.dropdown-toggle.btn-microsoft{color:#fff;background-color:#125acd;border-color:rgba(0,0,0,.2)}.btn-microsoft .badge{color:#2672ec;background-color:#fff}.btn-openid{color:#fff;background-color:#f7931e;border-color:rgba(0,0,0,.2)}.btn-openid.active,.btn-openid.focus,.btn-openid:active,.btn-openid:focus,.btn-openid:hover,.open>.dropdown-toggle.btn-openid{color:#fff;background-color:#da7908;border-color:rgba(0,0,0,.2)}.btn-openid .badge{color:#f7931e;background-color:#fff}.btn-pinterest{color:#fff;background-color:#cb2027;border-color:rgba(0,0,0,.2)}.btn-pinterest.active,.btn-pinterest.focus,.btn-pinterest:active,.btn-pinterest:focus,.btn-pinterest:hover,.open>.dropdown-toggle.btn-pinterest{color:#fff;background-color:#9f191f;border-color:rgba(0,0,0,.2)}.btn-pinterest .badge{color:#cb2027;background-color:#fff}.btn-reddit{color:#000;background-color:#eff7ff;border-color:rgba(0,0,0,.2)}.btn-reddit.active,.btn-reddit.focus,.btn-reddit:active,.btn-reddit:focus,.btn-reddit:hover,.open>.dropdown-toggle.btn-reddit{color:#000;background-color:#bcddff;border-color:rgba(0,0,0,.2)}.btn-reddit .badge{color:#eff7ff;background-color:#000}.btn-soundcloud{color:#fff;background-color:#f50;border-color:rgba(0,0,0,.2)}.btn-soundcloud.active,.btn-soundcloud.focus,.btn-soundcloud:active,.btn-soundcloud:focus,.btn-soundcloud:hover,.open>.dropdown-toggle.btn-soundcloud{color:#fff;background-color:#c40;border-color:rgba(0,0,0,.2)}.btn-soundcloud .badge{color:#f50;background-color:#fff}.btn-tumblr{color:#fff;background-color:#2c4762;border-color:rgba(0,0,0,.2)}.btn-tumblr.active,.btn-tumblr.focus,.btn-tumblr:active,.btn-tumblr:focus,.btn-tumblr:hover,.open>.dropdown-toggle.btn-tumblr{color:#fff;background-color:#1c2d3f;border-color:rgba(0,0,0,.2)}.btn-tumblr .badge{color:#2c4762;background-color:#fff}.btn-twitter{color:#fff;background-color:#55acee;border-color:rgba(0,0,0,.2)}.btn-twitter.active,.btn-twitter.focus,.btn-twitter:active,.btn-twitter:focus,.btn-twitter:hover,.open>.dropdown-toggle.btn-twitter{color:#fff;background-color:#2795e9;border-color:rgba(0,0,0,.2)}.btn-twitter .badge{color:#55acee;background-color:#fff}.btn-vimeo{color:#fff;background-color:#1ab7ea;border-color:rgba(0,0,0,.2)}.btn-vimeo.active,.btn-vimeo.focus,.btn-vimeo:active,.btn-vimeo:focus,.btn-vimeo:hover,.open>.dropdown-toggle.btn-vimeo{color:#fff;background-color:#1295bf;border-color:rgba(0,0,0,.2)}.btn-vimeo .badge{color:#1ab7ea;background-color:#fff}.btn-vk{color:#fff;background-color:#587ea3;border-color:rgba(0,0,0,.2)}.btn-vk.active,.btn-vk.focus,.btn-vk:active,.btn-vk:focus,.btn-vk:hover,.open>.dropdown-toggle.btn-vk{color:#fff;background-color:#466482;border-color:rgba(0,0,0,.2)}.btn-vk .badge{color:#587ea3;background-color:#fff}.btn-yahoo{color:#fff;background-color:#720e9e;border-color:rgba(0,0,0,.2)}.btn-yahoo.active,.btn-yahoo.focus,.btn-yahoo:active,.btn-yahoo:focus,.btn-yahoo:hover,.open>.dropdown-toggle.btn-yahoo{color:#fff;background-color:#500a6f;border-color:rgba(0,0,0,.2)}.btn-yahoo .badge{color:#720e9e;background-color:#fff}.fc-button{background:#f4f4f4;background-image:none;color:#444;border-color:#ddd;border-bottom-color:#ddd}.fc-button.hover,.fc-button:active,.fc-button:hover{background-color:#e9e9e9}.fc-header-title h2{font-size:15px;line-height:1.6em;color:#666;margin-left:10px}.fc-header-left{padding-left:10px}.fc-widget-header{background:#fafafa}.fc-grid{width:100%;border:0}.fc-widget-content:first-of-type,.fc-widget-header:first-of-type{border-left:0;border-right:0}.fc-widget-content:last-of-type,.fc-widget-header:last-of-type{border-right:0}.fc-toolbar{padding:10px;margin:0}.fc-day-number{font-size:20px;font-weight:300}.fc-color-picker{list-style:none;margin:0;padding:0}.fc-color-picker>li{float:left;font-size:30px;margin-right:5px;line-height:30px}.fc-color-picker>li .fa{-webkit-transition:-webkit-transform linear .3s;-o-transition:-o-transform linear .3s;transition:transform linear .3s}.fc-color-picker>li .fa:hover{-webkit-transform:rotate(30deg);-ms-transform:rotate(30deg);-o-transform:rotate(30deg);transform:rotate(30deg)}#add-new-event{-webkit-transition:all linear .3s;-o-transition:all linear .3s;transition:all linear .3s}.external-event{padding:5px 10px;font-weight:700;margin-bottom:4px;-webkit-box-shadow:0 1px 1px rgba(0,0,0,.1);box-shadow:0 1px 1px rgba(0,0,0,.1);text-shadow:0 1px 1px rgba(0,0,0,.1);border-radius:3px;cursor:move}.external-event:hover{-webkit-box-shadow:inset 0 0 90px rgba(0,0,0,.2);box-shadow:inset 0 0 90px rgba(0,0,0,.2)}.select2-container--default.select2-container--focus,.select2-container--default:active,.select2-container--default:focus,.select2-selection.select2-container--focus,.select2-selection:active,.select2-selection:focus{outline:0}.select2-container--default .select2-selection--single,.select2-selection .select2-selection--single{border:1px solid #d2d6de;border-radius:0;padding:6px 12px;height:34px}.select2-container--default.select2-container--open{border-color:#3c8dbc}.select2-dropdown{border:1px solid #d2d6de;border-radius:0}.select2-container--default .select2-results__option--highlighted[aria-selected]{background-color:#3c8dbc;color:#fff}.select2-results__option{padding:6px 12px;-moz-user-select:none;-ms-user-select:none;user-select:none;-webkit-user-select:none}.select2-container .select2-selection--single .select2-selection__rendered{padding-left:0;height:auto;margin-top:-4px}.select2-container[dir=rtl] .select2-selection--single .select2-selection__rendered{padding-right:6px;padding-left:20px}.select2-container--default .select2-selection--single .select2-selection__arrow{height:28px;right:3px}.select2-container--default .select2-selection--single .select2-selection__arrow b{margin-top:0}.select2-dropdown .select2-search__field,.select2-search--inline .select2-search__field{border:1px solid #d2d6de}.select2-dropdown .select2-search__field:focus,.select2-search--inline .select2-search__field:focus{outline:0;border:1px solid #3c8dbc}.select2-container--default .select2-results__option[aria-disabled=true]{color:#999}.select2-container--default .select2-results__option[aria-selected=true]{background-color:#ddd}.select2-container--default .select2-results__option[aria-selected=true],.select2-container--default .select2-results__option[aria-selected=true]:hover{color:#444}.select2-container--default .select2-selection--multiple{border:1px solid #d2d6de;border-radius:0}.select2-container--default .select2-selection--multiple:focus{border-color:#3c8dbc}.select2-container--default.select2-container--focus .select2-selection--multiple{border-color:#d2d6de}.select2-container--default .select2-selection--multiple .select2-selection__choice{background-color:#3c8dbc;border-color:#367fa9;padding:1px 10px;color:#fff}.select2-container--default .select2-selection--multiple .select2-selection__choice__remove{margin-right:5px;color:rgba(255,255,255,.7)}.select2-container--default .select2-selection--multiple .select2-selection__choice__remove:hover{color:#fff}.select2-container .select2-selection--single .select2-selection__rendered{padding-right:10px}.pad{padding:10px}.margin{margin:10px}.margin-bottom{margin-bottom:20px}.margin-bottom-none{margin-bottom:0}.margin-r-5{margin-right:5px}.inline{display:inline}.description-block{display:block;margin:10px 0;text-align:center}.description-block.margin-bottom{margin-bottom:25px}.description-block>.description-header{margin:0;padding:0;font-weight:600;font-size:16px}.list-header,.text-bold,.text-bold.table td,.text-bold.table th{font-weight:700}.alert-danger,.alert-error,.alert-info,.alert-success,.alert-warning,.bg-aqua,.bg-aqua-active,.bg-black,.bg-black-active,.bg-blue,.bg-blue-active,.bg-fuchsia,.bg-fuchsia-active,.bg-green,.bg-green-active,.bg-light-blue,.bg-light-blue-active,.bg-lime,.bg-lime-active,.bg-maroon,.bg-maroon-active,.bg-navy,.bg-navy-active,.bg-olive,.bg-olive-active,.bg-orange,.bg-orange-active,.bg-purple,.bg-purple-active,.bg-red,.bg-red-active,.bg-teal,.bg-teal-active,.bg-yellow,.bg-yellow-active,.callout.callout-danger,.callout.callout-info,.callout.callout-success,.callout.callout-warning,.label-danger,.label-info,.label-primary,.label-success,.label-warning,.modal-danger .modal-body,.modal-danger .modal-footer,.modal-danger .modal-header,.modal-info .modal-body,.modal-info .modal-footer,.modal-info .modal-header,.modal-primary .modal-body,.modal-primary .modal-footer,.modal-primary .modal-header,.modal-success .modal-body,.modal-success .modal-footer,.modal-success .modal-header,.modal-warning .modal-body,.modal-warning .modal-footer,.modal-warning .modal-header{color:#fff!important}.bg-gray{color:#000;background-color:#d2d6de!important}.bg-gray-light{background-color:#f7f7f7}.bg-black{background-color:#111!important}.alert-danger,.alert-error,.bg-red,.callout.callout-danger,.label-danger,.modal-danger .modal-body{background-color:#dd4b39!important}.alert-warning,.bg-yellow,.callout.callout-warning,.label-warning,.modal-warning .modal-body{background-color:#f39c12!important}.alert-info,.bg-aqua,.callout.callout-info,.label-info,.modal-info .modal-body{background-color:#00c0ef!important}.bg-blue{background-color:#0073b7!important}.bg-light-blue,.label-primary,.modal-primary .modal-body{background-color:#3c8dbc!important}.alert-success,.bg-green,.callout.callout-success,.label-success,.modal-success .modal-body{background-color:#00a65a!important}.bg-navy{background-color:#001f3f!important}.bg-teal{background-color:#39cccc!important}.bg-olive{background-color:#3d9970!important}.bg-lime{background-color:#01ff70!important}.bg-orange{background-color:#ff851b!important}.bg-fuchsia{background-color:#f012be!important}.bg-purple{background-color:#605ca8!important}.bg-maroon{background-color:#d81b60!important}.bg-gray-active{color:#000;background-color:#b5bbc8!important}.bg-black-active{background-color:#000!important}.bg-red-active,.modal-danger .modal-footer,.modal-danger .modal-header{background-color:#d33724!important}.bg-yellow-active,.modal-warning .modal-footer,.modal-warning .modal-header{background-color:#db8b0b!important}.bg-aqua-active,.modal-info .modal-footer,.modal-info .modal-header{background-color:#00a7d0!important}.bg-blue-active{background-color:#005384!important}.bg-light-blue-active,.modal-primary .modal-footer,.modal-primary .modal-header{background-color:#357ca5!important}.bg-green-active,.modal-success .modal-footer,.modal-success .modal-header{background-color:#008d4c!important}.bg-navy-active{background-color:#001a35!important}.bg-teal-active{background-color:#30bbbb!important}.bg-olive-active{background-color:#368763!important}.bg-lime-active{background-color:#00e765!important}.bg-orange-active{background-color:#ff7701!important}.bg-fuchsia-active{background-color:#db0ead!important}.bg-purple-active{background-color:#555299!important}.bg-maroon-active{background-color:#ca195a!important}[class^=bg-].disabled{opacity:.65;filter:alpha(opacity=65)}.text-red{color:#dd4b39!important}.text-yellow{color:#f39c12!important}.text-aqua{color:#00c0ef!important}.text-blue{color:#0073b7!important}.text-black{color:#111!important}.text-light-blue{color:#3c8dbc!important}.text-green{color:#00a65a!important}.text-gray{color:#d2d6de!important}.text-navy{color:#001f3f!important}.text-teal{color:#39cccc!important}.text-olive{color:#3d9970!important}.text-lime{color:#01ff70!important}.text-orange{color:#ff851b!important}.text-fuchsia{color:#f012be!important}.text-purple{color:#605ca8!important}.text-maroon{color:#d81b60!important}.link-muted{color:#7a869d}.link-muted:focus,.link-muted:hover{color:#606c84}.link-black{color:#666}.link-black:focus,.link-black:hover{color:#999}.hide{display:none!important}.no-border{border:0!important}.no-padding{padding:0!important}.no-margin{margin:0!important}.no-shadow{-webkit-box-shadow:none!important;box-shadow:none!important}.chart-legend,.contacts-list,.list-unstyled,.mailbox-attachments,.users-list{list-style:none;margin:0;padding:0}.list-group-unbordered>.list-group-item{border-left:0;border-right:0;border-radius:0;padding-left:0;padding-right:0}.flat{border-radius:0!important}.text-sm{font-size:12px}.jqstooltip{padding:5px!important;width:auto!important;height:auto!important}.bg-teal-gradient{background:#39cccc!important;background:-webkit-gradient(linear,left bottom,left top,color-stop(0,#39cccc),color-stop(1,#7adddd))!important;background:-o-linear-gradient(#7adddd,#39cccc)!important;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#7adddd', endColorstr='#39cccc', GradientType=0)!important;color:#fff}.bg-light-blue-gradient{background:#3c8dbc!important;background:-webkit-gradient(linear,left bottom,left top,color-stop(0,#3c8dbc),color-stop(1,#67a8ce))!important;background:-o-linear-gradient(#67a8ce,#3c8dbc)!important;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#67a8ce', endColorstr='#3c8dbc', GradientType=0)!important;color:#fff}.bg-blue-gradient{background:#0073b7!important;background:-webkit-gradient(linear,left bottom,left top,color-stop(0,#0073b7),color-stop(1,#0089db))!important;background:-o-linear-gradient(#0089db,#0073b7)!important;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#0089db', endColorstr='#0073b7', GradientType=0)!important;color:#fff}.bg-aqua-gradient{background:#00c0ef!important;background:-webkit-gradient(linear,left bottom,left top,color-stop(0,#00c0ef),color-stop(1,#14d1ff))!important;background:-o-linear-gradient(#14d1ff,#00c0ef)!important;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#14d1ff', endColorstr='#00c0ef', GradientType=0)!important;color:#fff}.bg-yellow-gradient{background:#f39c12!important;background:-webkit-gradient(linear,left bottom,left top,color-stop(0,#f39c12),color-stop(1,#f7bc60))!important;background:-o-linear-gradient(#f7bc60,#f39c12)!important;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#f7bc60', endColorstr='#f39c12', GradientType=0)!important;color:#fff}.bg-purple-gradient{background:#605ca8!important;background:-webkit-gradient(linear,left bottom,left top,color-stop(0,#605ca8),color-stop(1,#9491c4))!important;background:-o-linear-gradient(#9491c4,#605ca8)!important;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#9491c4', endColorstr='#605ca8', GradientType=0)!important;color:#fff}.bg-green-gradient{background:#00a65a!important;background:-webkit-gradient(linear,left bottom,left top,color-stop(0,#00a65a),color-stop(1,#00ca6d))!important;background:-o-linear-gradient(#00ca6d,#00a65a)!important;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#00ca6d', endColorstr='#00a65a', GradientType=0)!important;color:#fff}.bg-red-gradient{background:#dd4b39!important;background:-webkit-gradient(linear,left bottom,left top,color-stop(0,#dd4b39),color-stop(1,#e47365))!important;background:-o-linear-gradient(#e47365,#dd4b39)!important;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#e47365', endColorstr='#dd4b39', GradientType=0)!important;color:#fff}.bg-black-gradient{background:#111!important;background:-webkit-gradient(linear,left bottom,left top,color-stop(0,#111),color-stop(1,#2b2b2b))!important;background:-o-linear-gradient(#2b2b2b,#111)!important;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#2b2b2b', endColorstr='#111111', GradientType=0)!important;color:#fff}.bg-maroon-gradient{background:#d81b60!important;background:-webkit-gradient(linear,left bottom,left top,color-stop(0,#d81b60),color-stop(1,#e73f7c))!important;background:-o-linear-gradient(#e73f7c,#d81b60)!important;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#e73f7c', endColorstr='#d81b60', GradientType=0)!important;color:#fff}.description-block .description-icon{font-size:16px}.no-pad-top{padding-top:0}.position-static{position:static!important}.list-header{font-size:15px;padding:10px 4px;color:#666}.list-seperator{height:1px;background:#f4f4f4;margin:15px 0 9px}.list-link>a{padding:4px;color:#777}.list-link>a:hover{color:#222}.font-light{font-weight:300}.user-block:after,.user-block:before{content:" ";display:table}.user-block img{width:40px;height:40px;float:left}.user-block .comment,.user-block .description,.user-block .username{display:block;margin-left:50px}.img-sm+.img-push,.user-block.user-block-sm .comment,.user-block.user-block-sm .description,.user-block.user-block-sm .username{margin-left:40px}.user-block .username{font-size:16px;font-weight:600}.user-block .description{color:#999;font-size:13px}.user-block.user-block-sm .username{font-size:14px}.box-comments .box-comment img,.img-lg,.img-md,.img-sm,.user-block.user-block-sm img{float:left}.box-comments .box-comment img,.img-sm,.user-block.user-block-sm img{width:30px!important;height:30px!important}.img-md{width:60px;height:60px}.img-md+.img-push{margin-left:70px}.attachment-block .attachment-pushed,.img-lg+.img-push{margin-left:110px}.img-lg{width:100px;height:100px}.img-bordered{border:3px solid #d2d6de;padding:3px}.img-bordered-sm{border:2px solid #d2d6de;padding:2px}.attachment-block{border:1px solid #f4f4f4;padding:5px;margin-bottom:10px;background:#f7f7f7}.attachment-block .attachment-img{max-width:100px;max-height:100px;height:auto;float:left}.panel-default>.panel-heading+.panel-collapse>.panel-body img,.select2-container,.select2-drop-active{max-width:100%}.attachment-block .attachment-heading{margin:0}.attachment-block .attachment-text{color:#555}.skin-black .main-header .navbar .nav>li>a,.skin-black .main-header .navbar-toggle{color:#333}.connectedSortable{min-height:100px}.ui-helper-hidden-accessible{border:0;clip:rect(0 0 0 0);height:1px;margin:-1px;overflow:hidden;padding:0;position:absolute;width:1px}.sort-highlight{background:#f4f4f4;border:1px dashed #ddd;margin-bottom:10px}.full-opacity-hover{opacity:.65;filter:alpha(opacity=65)}.full-opacity-hover:hover{opacity:1;filter:alpha(opacity=100)}.chart{position:relative;overflow:hidden;width:100%}.chart canvas,.chart svg{width:100%!important}@media print{.content-header,.left-side,.main-header,.main-sidebar,.no-print{display:none!important}.content-wrapper,.main-footer,.right-side{margin-left:0!important;min-height:0!important;-webkit-transform:translate(0,0)!important;-ms-transform:translate(0,0)!important;-o-transform:translate(0,0)!important;transform:translate(0,0)!important}.fixed .content-wrapper,.fixed .right-side{padding-top:0!important}.invoice{width:100%;border:0;margin:0;padding:0}.invoice-col{float:left;width:33.3333333%}.table-responsive{overflow:auto}.table-responsive>.table tr td,.table-responsive>.table tr th{white-space:normal!important}}.skin-black .main-header{-webkit-box-shadow:0 1px 1px rgba(0,0,0,.05);box-shadow:0 1px 1px rgba(0,0,0,.05)}.skin-black .main-header .navbar-brand{color:#333;border-right:1px solid #eee}.skin-black .main-header .navbar{background-color:#fff}.skin-black .main-header .navbar .nav .open>a,.skin-black .main-header .navbar .nav .open>a:focus,.skin-black .main-header .navbar .nav .open>a:hover,.skin-black .main-header .navbar .nav>.active>a,.skin-black .main-header .navbar .nav>li>a:active,.skin-black .main-header .navbar .nav>li>a:focus,.skin-black .main-header .navbar .nav>li>a:hover{background:#fff;color:#999}.skin-black .main-header .navbar .sidebar-toggle{color:#333}.skin-black .main-header .navbar .sidebar-toggle:hover{color:#999;background:#fff}.skin-black .main-header .navbar>.sidebar-toggle{color:#333;border-right:1px solid #eee}.skin-black .main-header .navbar .navbar-nav>li>a{border-right:1px solid #eee}.skin-black .main-header .navbar .navbar-custom-menu .navbar-nav>li>a,.skin-black .main-header .navbar .navbar-right>li>a{border-left:1px solid #eee;border-right-width:0}.skin-black .main-header>.logo{background-color:#fff;color:#333;border-bottom:0 solid transparent;border-right:1px solid #eee}.skin-black .main-header>.logo:hover{background-color:#fcfcfc}@media (max-width:767px){.skin-black .main-header>.logo{background-color:#222;color:#fff;border-bottom:0 solid transparent;border-right:none}.skin-black .main-header>.logo:hover{background-color:#1f1f1f}}.skin-black .main-header li.user-header{background-color:#222}.skin-black .content-header{background:0 0;-webkit-box-shadow:none;box-shadow:none}.skin-black .left-side,.skin-black .main-sidebar,.skin-black .wrapper{background-color:#222d32}.skin-black .user-panel>.info,.skin-black .user-panel>.info>a{color:#fff}.skin-black .sidebar-menu>li.header{color:#4b646f;background:#1a2226}.skin-black .sidebar-menu>li>a{border-left:3px solid transparent}.skin-black .sidebar-menu>li.active>a,.skin-black .sidebar-menu>li:hover>a{color:#fff;background:#1e282c;border-left-color:#fff}.skin-black .sidebar-menu>li>.treeview-menu{margin:0 1px;background:#2c3b41}.skin-black .sidebar a{color:#b8c7ce}.skin-black .treeview-menu>li>a{color:#8aa4af}.skin-black .treeview-menu>li.active>a,.skin-black .treeview-menu>li>a:hover{color:#fff}.skin-black .sidebar-form{border-radius:3px;border:1px solid #374850;margin:10px}.skin-black .sidebar-form .btn,.skin-black .sidebar-form input[type=text]{-webkit-box-shadow:none;box-shadow:none;background-color:#374850;border:1px solid transparent;height:35px}.skin-black .sidebar-form input[type=text]{color:#666;border-top-left-radius:2px;border-top-right-radius:0;border-bottom-right-radius:0;border-bottom-left-radius:2px}.skin-black .sidebar-form input[type=text]:focus,.skin-black .sidebar-form input[type=text]:focus+.input-group-btn .btn{background-color:#fff;color:#666}.skin-black .sidebar-form input[type=text]:focus+.input-group-btn .btn{border-left-color:#fff}.skin-black .sidebar-form .btn{color:#999;border-top-left-radius:0;border-top-right-radius:2px;border-bottom-right-radius:2px;border-bottom-left-radius:0}.skin-black .pace .pace-progress{background:#222}.skin-black .pace .pace-activity{border-top-color:#222;border-left-color:#222}input:focus{outline:0}#login .content-wrapper,#login .main-footer,#login .right-side{margin-left:0}#login .content-wrapper{background-color:#222d32}#login .content-wrapper .content{width:500px;max-width:100%;margin:0 auto;padding-top:5%}.treeview-header a{border-bottom:1px solid #efefef}.dashboard-link{display:block;background:#fff;overflow:hidden;color:#555;text-align:center;padding:25px;width:99%;margin:0 auto;margin-bottom:15px}.dashboard-link span{display:block}.dashboard-link span.icon i{font-size:6em}.dashboard-link span.text{font-size:22px;font-weight:700;margin-top:20px}.dashboard-small-links i{font-size:16px}.dashboard-small-link{background:#fff;padding:6px 8px;border:1px solid #efefef;margin-right:5px;margin-bottom:5px;border-radius:2px;display:inline-block;font-size:14px}.dashboard-small-link:hover{color:#555;background:#e1e1e1}#show-items .modal-dialog{width:99%;margin-top:2px;margin-left:2px}.thick-border-top{border-top:2px solid gray}.pagination{padding-left:15px}.btn-danger{background:0 0;color:#d73925}.edit-run-header h5{display:inline-block}#run-unit-choices{display:block;margin-top:15px}.run_unit_inner{-webkit-box-shadow:0 4px 4px -2px #efefef;box-shadow:0 4px 4px -2px #efefef}.run_unit_inner.email label{display:block}.run_unit_inner.email .form-control{width:80%}.run_unit_position{text-align:center;padding-bottom:10px}.run_unit_inner .run_unit_description,.run_unit_position input{border:0;-webkit-box-shadow:none;box-shadow:none;font-weight:700;font-size:45px;height:65px;width:100px;background-color:transparent;text-align:center;border-bottom:1px solid #efefef}.run_unit_inner .run_unit_description{font-size:20px;width:100%;height:25px;background-color:#fff;border:0;text-align:left}.run_unit_inner .run_unit_dialog .form-group{margin:0}.badge-info{background-color:#3a87ad}.badge-success{background-color:#468847}.muted{color:#DDD}.skin-black .wrapper{background:#fff}tr.thick_border_top{border-top:2px solid #efefef}.log-entry .panel-content{padding:15px}.bulk-actions-ba form{border:1px solid #efefef;padding:5px;margin-top:15px}.has-actions .input-group .position_monkey{width:70px;padding:0}.readonly-textarea{border:1px solid #efefef;background:0 0;color:#555;width:70%;padding:8px}.dropzone{border:2px solid #efefef;padding:20px 15px}.lock-toggle.btn-checked{background-color:#e7e7e7}.well-sm{width:85%}.select2-container .select2-choice,.select2-dropdown-open .select2-choice{border-radius:0;background-image:none;background-color:#fff;height:30px}.select2-container.full_width{width:80%}.select2-container .select2-choice .select2-arrow{background-image:none;background:0 0;width:18px}.select2-results{padding:0 4px}.select2-results .select2-highlighted{background-color:#ddd;color:#000;font-weight:400}.select2-container-active .select2-choice,.select2-container-active .select2-choices,.select2-drop-active{border:1px solid #ddd}.modal-header .close{margin-top:5px}a.btn.btn-checked,button.btn.btn-checked,label.btn.btn-checked{background-color:#c3c3c3;z-index:2;border:1px solid #000;outline:1px;-webkit-box-shadow:inset 0 2px 4px rgba(0,0,0,.15),0 1px 2px rgba(0,0,0,.05);box-shadow:inset 0 2px 4px rgba(0,0,0,.15),0 1px 2px rgba(0,0,0,.05)}.panel-body .rmarkdown_iframe{position:static;width:100%;height:500px}.panel-body .rmarkdown_iframe iframe{border:0;width:100%;height:100%;height:100vh}.alert .panel-group.opencpu_accordion a{color:green;text-decoration:none} \ No newline at end of file +!*/body{font-family:'Source Sans Pro','Helvetica Neue',Helvetica,Arial,sans-serif;font-weight:400}.wrapper{position:relative}.wrapper:after,.wrapper:before{content:" ";display:table}.layout-boxed .wrapper{max-width:1250px;margin:0 auto;min-height:100%;-webkit-box-shadow:0 0 8px rgba(0,0,0,.5);box-shadow:0 0 8px rgba(0,0,0,.5);position:relative}.layout-boxed{background:url(../img/boxed-bg.jpg) fixed}.content-wrapper,.main-footer,.right-side{-webkit-transition:-webkit-transform .3s ease-in-out,margin .3s ease-in-out;-o-transition:-o-transform .3s ease-in-out,margin .3s ease-in-out;transition:transform .3s ease-in-out,margin .3s ease-in-out;z-index:820}.layout-top-nav .content-wrapper,.layout-top-nav .main-footer,.layout-top-nav .right-side{margin-left:0}@media (min-width:768px){.sidebar-collapse .content-wrapper,.sidebar-collapse .main-footer,.sidebar-collapse .right-side{margin-left:0}}.content-wrapper,.right-side{min-height:100%;background-color:#ecf0f5;z-index:800}.main-footer{background:#fff;padding:15px;color:#444;border-top:1px solid #d2d6de}.fixed .left-side,.fixed .main-header,.fixed .main-sidebar{position:fixed}.fixed .main-header{top:0;right:0;left:0}.fixed .content-wrapper,.fixed .right-side{padding-top:50px}@media (max-width:767px){.content-wrapper,.main-footer,.right-side{margin-left:0}.sidebar-open .content-wrapper,.sidebar-open .main-footer,.sidebar-open .right-side{-webkit-transform:translate(230px,0);-ms-transform:translate(230px,0);-o-transform:translate(230px,0);transform:translate(230px,0)}.fixed .content-wrapper,.fixed .right-side{padding-top:100px}}.fixed.layout-boxed .wrapper{max-width:100%}body.hold-transition .content-wrapper,body.hold-transition .left-side,body.hold-transition .main-footer,body.hold-transition .main-header .logo,body.hold-transition .main-header .navbar,body.hold-transition .main-sidebar,body.hold-transition .right-side{-webkit-transition:none;-o-transition:none;transition:none}.content{min-height:250px;padding:15px;margin-right:auto;margin-left:auto;padding-left:15px;padding-right:15px}.h1,.h2,.h3,.h4,.h5,.h6,h1,h2,h3,h4,h5,h6{font-family:'Source Sans Pro',sans-serif}a{color:#3c8dbc}a:active,a:focus,a:hover{outline:0;text-decoration:none;color:#72afd2}.page-header{margin:10px 0 20px;font-size:22px}.page-header>small{color:#666;display:block;margin-top:5px}.main-header{position:relative;max-height:100px;z-index:1030}.main-header .navbar{-webkit-transition:margin-left .3s ease-in-out;-o-transition:margin-left .3s ease-in-out;transition:margin-left .3s ease-in-out;margin-bottom:0;border:none;min-height:50px;border-radius:0}.layout-top-nav .main-header .navbar{margin-left:0}.main-header #navbar-search-input.form-control{background:rgba(255,255,255,.2);border-color:transparent}.main-header #navbar-search-input.form-control:active,.main-header #navbar-search-input.form-control:focus{border-color:rgba(0,0,0,.1);background:rgba(255,255,255,.9)}.main-header #navbar-search-input.form-control::-moz-placeholder{color:#ccc;opacity:1}.main-header #navbar-search-input.form-control:-ms-input-placeholder{color:#ccc}.main-header #navbar-search-input.form-control::-webkit-input-placeholder{color:#ccc}@media (max-width:991px){.main-header .navbar-custom-menu a,.main-header .navbar-right a{color:inherit;background:0 0}}@media (max-width:767px){.main-header .navbar-right{float:none}.navbar-collapse .main-header .navbar-right{margin:7.5px -15px}.main-header .navbar-right>li{color:inherit;border:0}}.main-header .navbar-brand,.main-header .sidebar-toggle:hover{color:#fff}.main-header .sidebar-toggle{float:left;background-color:transparent;background-image:none;padding:15px;font-family:fontAwesome}.main-header .sidebar-toggle:before{content:"\f0c9"}.main-header .sidebar-toggle:active,.main-header .sidebar-toggle:focus{background:0 0}.main-header .sidebar-toggle .icon-bar{display:none}.main-header .navbar .nav>li.user>a>.fa,.main-header .navbar .nav>li.user>a>.glyphicon,.main-header .navbar .nav>li.user>a>.ion{margin-right:5px}.main-header .navbar .nav>li>a>.label{position:absolute;top:9px;right:7px;text-align:center;font-size:9px;padding:2px 3px;line-height:.9}.main-header .logo{-webkit-transition:width .3s ease-in-out;-o-transition:width .3s ease-in-out;transition:width .3s ease-in-out;display:block;float:left;height:50px;font-size:20px;line-height:50px;text-align:center;width:230px;font-family:"Helvetica Neue",Helvetica,Arial,sans-serif;padding:0 15px;font-weight:300;overflow:hidden}.main-header .logo .logo-lg{display:block}.main-header .logo .logo-mini{display:none}.content-header{position:relative;padding:15px 15px 0}.content-header>h1{margin:0;font-size:24px}.content-header>h1>small{font-size:15px;display:inline-block;padding-left:4px;font-weight:300}.content-header>.breadcrumb{float:right;background:0 0;margin-top:0;margin-bottom:0;font-size:12px;padding:7px 5px;position:absolute;top:15px;right:10px;border-radius:2px}.content-header>.breadcrumb>li>a{color:#444;text-decoration:none;display:inline-block}.content-header>.breadcrumb>li>a>.fa,.content-header>.breadcrumb>li>a>.glyphicon,.content-header>.breadcrumb>li>a>.ion{margin-right:5px}.content-header>.breadcrumb>li+li:before{content:'>\00a0'}@media (max-width:991px){.content-header>.breadcrumb{position:relative;margin-top:5px;top:0;right:0;float:none;background:#d2d6de;padding-left:10px}.content-header>.breadcrumb li:before{color:#97a0b3}.navbar-custom-menu .navbar-nav>li{float:left}.navbar-custom-menu .navbar-nav{margin:0;float:left}.navbar-custom-menu .navbar-nav>li>a{padding-top:15px;padding-bottom:15px;line-height:20px}}.navbar-toggle{color:#fff;border:0;margin:0;padding:15px}@media (max-width:767px){.main-header{position:relative}.main-header .logo,.main-header .navbar{width:100%;float:none}.main-header .navbar{margin:0}.main-header .navbar-custom-menu{float:right}}@media (max-width:991px){.navbar-collapse.pull-left{float:none!important}.navbar-collapse.pull-left+.navbar-custom-menu{display:block;position:absolute;top:0;right:40px}}.left-side,.main-sidebar{position:absolute;top:0;left:0;padding-top:50px;min-height:100%;width:230px;z-index:810;-webkit-transition:-webkit-transform .3s ease-in-out,width .3s ease-in-out;-o-transition:-o-transform .3s ease-in-out,width .3s ease-in-out;transition:transform .3s ease-in-out,width .3s ease-in-out}@media (max-width:767px){.left-side,.main-sidebar{padding-top:100px;-webkit-transform:translate(-230px,0);-ms-transform:translate(-230px,0);-o-transform:translate(-230px,0);transform:translate(-230px,0)}}@media (min-width:768px){.sidebar-collapse .left-side,.sidebar-collapse .main-sidebar{-webkit-transform:translate(-230px,0);-ms-transform:translate(-230px,0);-o-transform:translate(-230px,0);transform:translate(-230px,0)}}@media (max-width:767px){.sidebar-open .left-side,.sidebar-open .main-sidebar{-webkit-transform:translate(0,0);-ms-transform:translate(0,0);-o-transform:translate(0,0);transform:translate(0,0)}}.sidebar{padding-bottom:10px}.sidebar-form input:focus{border-color:transparent}.user-panel{position:relative;width:100%;padding:10px;overflow:hidden}.user-panel:after,.user-panel:before{content:" ";display:table}.user-panel>.image>img{width:100%;max-width:45px;height:auto}.user-panel>.info{padding:5px 5px 5px 15px;line-height:1;position:absolute;left:55px}.user-panel>.info>p{font-weight:600;margin-bottom:9px}.user-panel>.info>a{text-decoration:none;padding-right:5px;margin-top:3px;font-size:11px}.user-panel>.info>a>.fa,.user-panel>.info>a>.glyphicon,.user-panel>.info>a>.ion{margin-right:3px}.sidebar-menu{list-style:none;margin:0;padding:0}.sidebar-menu>li{position:relative;margin:0;padding:0}.sidebar-menu>li>a{padding:12px 5px 12px 15px;display:block}.sidebar-menu>li>a>.fa,.sidebar-menu>li>a>.glyphicon,.sidebar-menu>li>a>.ion{width:20px}.sidebar-menu>li .badge,.sidebar-menu>li .label{margin-right:5px}.sidebar-menu>li .badge{margin-top:3px}.sidebar-menu li.header{padding:10px 25px 10px 15px;font-size:12px}.sidebar-menu li>a>.fa-angle-left,.sidebar-menu li>a>.pull-right-container>.fa-angle-left{width:auto;height:auto;padding:0;margin-right:10px}.sidebar-menu li>a>.fa-angle-left{position:absolute;top:50%;right:10px;margin-top:-8px}.sidebar-menu li.active>a>.fa-angle-left,.sidebar-menu li.active>a>.pull-right-container>.fa-angle-left{-webkit-transform:rotate(-90deg);-ms-transform:rotate(-90deg);-o-transform:rotate(-90deg);transform:rotate(-90deg)}.sidebar-menu li.active>.treeview-menu{display:block}.sidebar-menu .treeview-menu{display:none;list-style:none;padding:0;margin:0;padding-left:5px}.sidebar-menu .treeview-menu .treeview-menu{padding-left:20px}.sidebar-menu .treeview-menu>li{margin:0}.sidebar-menu .treeview-menu>li>a{padding:5px 5px 5px 15px;display:block;font-size:14px}.sidebar-menu .treeview-menu>li>a>.fa,.sidebar-menu .treeview-menu>li>a>.glyphicon,.sidebar-menu .treeview-menu>li>a>.ion{width:20px}.sidebar-menu .treeview-menu>li>a>.fa-angle-down,.sidebar-menu .treeview-menu>li>a>.fa-angle-left,.sidebar-menu .treeview-menu>li>a>.pull-right-container>.fa-angle-down,.sidebar-menu .treeview-menu>li>a>.pull-right-container>.fa-angle-left{width:auto}@media (min-width:768px){.sidebar-mini.sidebar-collapse .content-wrapper,.sidebar-mini.sidebar-collapse .main-footer,.sidebar-mini.sidebar-collapse .right-side{margin-left:50px!important;z-index:840}.sidebar-mini.sidebar-collapse .main-sidebar{-webkit-transform:translate(0,0);-ms-transform:translate(0,0);-o-transform:translate(0,0);transform:translate(0,0);width:50px!important;z-index:850}.sidebar-mini.sidebar-collapse .sidebar-menu>li{position:relative}.sidebar-mini.sidebar-collapse .sidebar-menu>li>a{margin-right:0}.sidebar-mini.sidebar-collapse .sidebar-menu>li>a>span{border-top-right-radius:4px}.sidebar-mini.sidebar-collapse .sidebar-menu>li:not(.treeview)>a>span{border-bottom-right-radius:4px}.sidebar-mini.sidebar-collapse .sidebar-menu>li>.treeview-menu{padding-top:5px;padding-bottom:5px;border-bottom-right-radius:4px}.sidebar-mini.sidebar-collapse .sidebar-menu>li:hover>.treeview-menu,.sidebar-mini.sidebar-collapse .sidebar-menu>li:hover>a>span:not(.pull-right){display:block!important;position:absolute;width:180px;left:50px}.sidebar-mini.sidebar-collapse .sidebar-menu>li:hover>a>span{top:0;margin-left:-3px;padding:12px 5px 12px 20px;background-color:inherit}.sidebar-mini.sidebar-collapse .sidebar-menu>li:hover>a>.pull-right-container{position:relative!important;float:right;width:auto!important;left:180px!important;top:-22px!important;z-index:900}.sidebar-mini.sidebar-collapse .sidebar-menu>li:hover>a>.pull-right-container>.label:not(:first-of-type){display:none}.sidebar-mini.sidebar-collapse .sidebar-menu>li:hover>.treeview-menu{top:44px;margin-left:0}.sidebar-mini.sidebar-collapse .main-sidebar .user-panel>.info,.sidebar-mini.sidebar-collapse .sidebar-form,.sidebar-mini.sidebar-collapse .sidebar-menu li.header,.sidebar-mini.sidebar-collapse .sidebar-menu>li>.treeview-menu,.sidebar-mini.sidebar-collapse .sidebar-menu>li>a>.pull-right,.sidebar-mini.sidebar-collapse .sidebar-menu>li>a>span{display:none!important;-webkit-transform:translateZ(0)}.sidebar-mini.sidebar-collapse .main-header .logo{width:50px}.sidebar-mini.sidebar-collapse .main-header .logo>.logo-mini{display:block;margin-left:-15px;margin-right:-15px;font-size:18px}.sidebar-mini.sidebar-collapse .main-header .logo>.logo-lg{display:none}.sidebar-mini.sidebar-collapse .main-header .navbar{margin-left:50px}.control-sidebar-open .content-wrapper,.control-sidebar-open .main-footer,.control-sidebar-open .right-side{margin-right:230px}}.main-sidebar .user-panel,.sidebar-menu,.sidebar-menu>li.header{white-space:nowrap;overflow:hidden}.sidebar-menu:hover{overflow:visible}.sidebar-form,.sidebar-menu>li.header{overflow:hidden;text-overflow:clip}.sidebar-menu li>a{position:relative}.sidebar-menu li>a>.pull-right-container{position:absolute;right:10px;top:50%;margin-top:-7px}.control-sidebar-bg{position:fixed;z-index:1000;bottom:0}.control-sidebar,.control-sidebar-bg{top:0;right:-230px;width:230px;-webkit-transition:right .3s ease-in-out;-o-transition:right .3s ease-in-out;transition:right .3s ease-in-out}.control-sidebar{position:absolute;padding-top:50px;z-index:1010}@media (max-width:768px){.control-sidebar{padding-top:100px}.nav-tabs.control-sidebar-tabs{display:table}.nav-tabs.control-sidebar-tabs>li{display:table-cell}}.control-sidebar>.tab-content{padding:10px 15px}.control-sidebar-open .control-sidebar,.control-sidebar-open .control-sidebar-bg,.control-sidebar.control-sidebar-open,.control-sidebar.control-sidebar-open+.control-sidebar-bg{right:0}.nav-tabs.control-sidebar-tabs>li:first-of-type>a,.nav-tabs.control-sidebar-tabs>li:first-of-type>a:focus,.nav-tabs.control-sidebar-tabs>li:first-of-type>a:hover{border-left-width:0}.nav-tabs.control-sidebar-tabs>li>a{border-radius:0}.nav-tabs.control-sidebar-tabs>li>a,.nav-tabs.control-sidebar-tabs>li>a:hover{border-top:none;border-right:none;border-left:1px solid transparent;border-bottom:1px solid transparent}.nav-tabs.control-sidebar-tabs>li>a .icon{font-size:16px}.nav-tabs.control-sidebar-tabs>li.active>a,.nav-tabs.control-sidebar-tabs>li.active>a:active,.nav-tabs.control-sidebar-tabs>li.active>a:focus,.nav-tabs.control-sidebar-tabs>li.active>a:hover{border-top:none;border-right:none;border-bottom:none}.control-sidebar-heading{font-weight:400;font-size:16px;padding:10px 0;margin-bottom:10px}.control-sidebar-subheading{display:block;font-weight:400;font-size:14px}.control-sidebar-menu{list-style:none;padding:0;margin:0 -15px}.control-sidebar-menu>li>a{display:block;padding:10px 15px}.control-sidebar-menu>li>a:after,.control-sidebar-menu>li>a:before{content:" ";display:table}.control-sidebar-menu>li>a>.control-sidebar-subheading{margin-top:0}.control-sidebar-menu .menu-icon{float:left;width:35px;height:35px;border-radius:50%;text-align:center;line-height:35px}.control-sidebar-menu .menu-info{margin-left:45px;margin-top:3px}.control-sidebar-menu .menu-info>.control-sidebar-subheading,.control-sidebar-menu .progress{margin:0}.control-sidebar-menu .menu-info>p{margin:0;font-size:11px}.control-sidebar-dark{color:#b8c7ce}.control-sidebar-dark,.control-sidebar-dark+.control-sidebar-bg{background:#222d32}.control-sidebar-dark .nav-tabs.control-sidebar-tabs{border-bottom:#1c2529}.control-sidebar-dark .nav-tabs.control-sidebar-tabs>li>a{background:#181f23;color:#b8c7ce}.control-sidebar-dark .control-sidebar-heading,.control-sidebar-dark .control-sidebar-subheading,.control-sidebar-dark .nav-tabs.control-sidebar-tabs>li>a:hover{color:#fff}.control-sidebar-dark .nav-tabs.control-sidebar-tabs>li>a,.control-sidebar-dark .nav-tabs.control-sidebar-tabs>li>a:focus,.control-sidebar-dark .nav-tabs.control-sidebar-tabs>li>a:hover{border-left-color:#141a1d;border-bottom-color:#141a1d}.control-sidebar-dark .nav-tabs.control-sidebar-tabs>li>a:active,.control-sidebar-dark .nav-tabs.control-sidebar-tabs>li>a:focus,.control-sidebar-dark .nav-tabs.control-sidebar-tabs>li>a:hover{background:#1c2529}.control-sidebar-dark .nav-tabs.control-sidebar-tabs>li.active>a,.control-sidebar-dark .nav-tabs.control-sidebar-tabs>li.active>a:active,.control-sidebar-dark .nav-tabs.control-sidebar-tabs>li.active>a:focus,.control-sidebar-dark .nav-tabs.control-sidebar-tabs>li.active>a:hover{background:#222d32;color:#fff}.control-sidebar-dark .control-sidebar-menu>li>a:hover{background:#1e282c}.control-sidebar-dark .control-sidebar-menu>li>a .menu-info>p{color:#b8c7ce}.control-sidebar-light{color:#5e5e5e}.control-sidebar-light,.control-sidebar-light+.control-sidebar-bg{background:#f9fafc;border-left:1px solid #d2d6de}.control-sidebar-light .nav-tabs.control-sidebar-tabs{border-bottom:#d2d6de}.control-sidebar-light .nav-tabs.control-sidebar-tabs>li>a{background:#e8ecf4;color:#444}.control-sidebar-light .nav-tabs.control-sidebar-tabs>li>a,.control-sidebar-light .nav-tabs.control-sidebar-tabs>li>a:focus,.control-sidebar-light .nav-tabs.control-sidebar-tabs>li>a:hover{border-left-color:#d2d6de;border-bottom-color:#d2d6de}.control-sidebar-light .nav-tabs.control-sidebar-tabs>li>a:active,.control-sidebar-light .nav-tabs.control-sidebar-tabs>li>a:focus,.control-sidebar-light .nav-tabs.control-sidebar-tabs>li>a:hover{background:#eff1f7}.control-sidebar-light .nav-tabs.control-sidebar-tabs>li.active>a,.control-sidebar-light .nav-tabs.control-sidebar-tabs>li.active>a:active,.control-sidebar-light .nav-tabs.control-sidebar-tabs>li.active>a:focus,.control-sidebar-light .nav-tabs.control-sidebar-tabs>li.active>a:hover{background:#f9fafc;color:#111}.control-sidebar-light .control-sidebar-heading,.control-sidebar-light .control-sidebar-subheading{color:#111}.control-sidebar-light .control-sidebar-menu{margin-left:-14px}.control-sidebar-light .control-sidebar-menu>li>a:hover{background:#f4f4f5}.control-sidebar-light .control-sidebar-menu>li>a .menu-info>p{color:#5e5e5e}.dropdown-menu{-webkit-box-shadow:none;box-shadow:none;border-color:#eee}.dropdown-menu>li>a{color:#777}.dropdown-menu>li>a>.fa,.dropdown-menu>li>a>.glyphicon,.dropdown-menu>li>a>.ion{margin-right:10px}.dropdown-menu>li>a:hover{background-color:#e1e3e9;color:#333}.dropdown-menu>.divider{background-color:#eee}.navbar-nav>.messages-menu>.dropdown-menu,.navbar-nav>.notifications-menu>.dropdown-menu,.navbar-nav>.tasks-menu>.dropdown-menu{width:280px;padding:0;margin:0;top:100%}.navbar-nav>.messages-menu>.dropdown-menu>li,.navbar-nav>.notifications-menu>.dropdown-menu>li,.navbar-nav>.tasks-menu>.dropdown-menu>li{position:relative}.navbar-nav>.messages-menu>.dropdown-menu>li.header,.navbar-nav>.notifications-menu>.dropdown-menu>li.header,.navbar-nav>.tasks-menu>.dropdown-menu>li.header{border-top-left-radius:4px;border-top-right-radius:4px;border-bottom-right-radius:0;border-bottom-left-radius:0;background-color:#fff;padding:7px 10px;border-bottom:1px solid #f4f4f4;color:#444;font-size:14px}.navbar-nav>.messages-menu>.dropdown-menu>li.footer>a,.navbar-nav>.notifications-menu>.dropdown-menu>li.footer>a,.navbar-nav>.tasks-menu>.dropdown-menu>li.footer>a{border-top-left-radius:0;border-top-right-radius:0;border-bottom-right-radius:4px;border-bottom-left-radius:4px;font-size:12px;background-color:#fff;padding:7px 10px;border-bottom:1px solid #eee;color:#444!important;text-align:center}@media (max-width:991px){.navbar-nav>.messages-menu>.dropdown-menu>li.footer>a,.navbar-nav>.notifications-menu>.dropdown-menu>li.footer>a,.navbar-nav>.tasks-menu>.dropdown-menu>li.footer>a{background:#fff!important;color:#444!important}}.navbar-nav>.messages-menu>.dropdown-menu>li.footer>a:hover,.navbar-nav>.notifications-menu>.dropdown-menu>li.footer>a:hover,.navbar-nav>.tasks-menu>.dropdown-menu>li.footer>a:hover{text-decoration:none;font-weight:400}.navbar-nav>.messages-menu>.dropdown-menu>li .menu,.navbar-nav>.notifications-menu>.dropdown-menu>li .menu,.navbar-nav>.tasks-menu>.dropdown-menu>li .menu{max-height:200px;margin:0;padding:0;list-style:none;overflow-x:hidden}.navbar-nav>.messages-menu>.dropdown-menu>li .menu>li>a,.navbar-nav>.notifications-menu>.dropdown-menu>li .menu>li>a,.navbar-nav>.tasks-menu>.dropdown-menu>li .menu>li>a{display:block;white-space:nowrap;border-bottom:1px solid #f4f4f4}.navbar-nav>.messages-menu>.dropdown-menu>li .menu>li>a:hover,.navbar-nav>.notifications-menu>.dropdown-menu>li .menu>li>a:hover,.navbar-nav>.tasks-menu>.dropdown-menu>li .menu>li>a:hover{background:#f4f4f4;text-decoration:none}.navbar-nav>.notifications-menu>.dropdown-menu>li .menu>li>a{color:#444;overflow:hidden;text-overflow:ellipsis;padding:10px}.navbar-nav>.notifications-menu>.dropdown-menu>li .menu>li>a>.fa,.navbar-nav>.notifications-menu>.dropdown-menu>li .menu>li>a>.glyphicon,.navbar-nav>.notifications-menu>.dropdown-menu>li .menu>li>a>.ion{width:20px}.navbar-nav>.messages-menu>.dropdown-menu>li .menu>li>a{margin:0;padding:10px}.navbar-nav>.messages-menu>.dropdown-menu>li .menu>li>a>div>img{margin:auto 10px auto auto;width:40px;height:40px}.navbar-nav>.messages-menu>.dropdown-menu>li .menu>li>a>h4{padding:0;margin:0 0 0 45px;color:#444;font-size:15px;position:relative}.navbar-nav>.messages-menu>.dropdown-menu>li .menu>li>a>h4>small{color:#999;font-size:10px;position:absolute;top:0;right:0}.navbar-nav>.messages-menu>.dropdown-menu>li .menu>li>a>p{margin:0 0 0 45px;font-size:12px;color:#888}.navbar-nav>.messages-menu>.dropdown-menu>li .menu>li>a:after,.navbar-nav>.messages-menu>.dropdown-menu>li .menu>li>a:before{content:" ";display:table}.navbar-nav>.tasks-menu>.dropdown-menu>li .menu>li>a{padding:10px}.navbar-nav>.tasks-menu>.dropdown-menu>li .menu>li>a>h3{font-size:14px;padding:0;margin:0 0 10px;color:#666}.navbar-nav>.tasks-menu>.dropdown-menu>li .menu>li>a>.progress{padding:0;margin:0}.navbar-nav>.user-menu>.dropdown-menu{border-top-right-radius:0;border-top-left-radius:0;padding:1px 0 0;border-top-width:0;width:280px}.navbar-nav>.user-menu>.dropdown-menu,.navbar-nav>.user-menu>.dropdown-menu>.user-body{border-bottom-right-radius:4px;border-bottom-left-radius:4px}.navbar-nav>.user-menu>.dropdown-menu>li.user-header{height:175px;padding:10px;text-align:center}.navbar-nav>.user-menu>.dropdown-menu>li.user-header>img{z-index:5;height:90px;width:90px;border:3px solid;border-color:transparent;border-color:rgba(255,255,255,.2)}.navbar-nav>.user-menu>.dropdown-menu>li.user-header>p{z-index:5;color:#fff;color:rgba(255,255,255,.8);font-size:17px;margin-top:10px}.navbar-nav>.user-menu>.dropdown-menu>li.user-header>p>small{display:block;font-size:12px}.navbar-nav>.user-menu>.dropdown-menu>.user-body:after,.navbar-nav>.user-menu>.dropdown-menu>.user-body:before,.navbar-nav>.user-menu>.dropdown-menu>.user-footer:after,.navbar-nav>.user-menu>.dropdown-menu>.user-footer:before{display:table;content:" "}.navbar-nav>.user-menu>.dropdown-menu>.user-body{padding:15px;border-bottom:1px solid #f4f4f4;border-top:1px solid #ddd}.navbar-nav>.user-menu>.dropdown-menu>.user-body a{color:#444!important}@media (max-width:991px){.navbar-nav>.user-menu>.dropdown-menu>.user-body a{background:#fff!important;color:#444!important}.navbar-nav>.user-menu>.dropdown-menu>.user-footer .btn-default:hover{background-color:#f9f9f9}}.navbar-nav>.user-menu>.dropdown-menu>.user-footer{background-color:#f9f9f9;padding:10px}.navbar-nav>.user-menu>.dropdown-menu>.user-footer .btn-default{color:#666}.navbar-nav>.user-menu .user-image{float:left;width:25px;height:25px;border-radius:50%;margin-right:10px;margin-top:-2px}@media (max-width:767px){.navbar-nav>.user-menu .user-image{float:none;margin-right:0;margin-top:-8px;line-height:10px}}.open:not(.dropup)>.animated-dropdown-menu{-webkit-backface-visibility:visible!important;backface-visibility:visible!important;-webkit-animation:flipInX .7s both;-o-animation:flipInX .7s both;animation:flipInX .7s both}@-o-keyframes flipInX{0%{transform:perspective(400px) rotate3d(1,0,0,90deg);-o-transition-timing-function:ease-in;transition-timing-function:ease-in;opacity:0}40%{transform:perspective(400px) rotate3d(1,0,0,-20deg);-o-transition-timing-function:ease-in;transition-timing-function:ease-in}60%{transform:perspective(400px) rotate3d(1,0,0,10deg);opacity:1}80%{transform:perspective(400px) rotate3d(1,0,0,-5deg)}100%{transform:perspective(400px)}}@keyframes flipInX{0%{-webkit-transform:perspective(400px) rotate3d(1,0,0,90deg);transform:perspective(400px) rotate3d(1,0,0,90deg);-webkit-transition-timing-function:ease-in;-o-transition-timing-function:ease-in;transition-timing-function:ease-in;opacity:0}40%{-webkit-transform:perspective(400px) rotate3d(1,0,0,-20deg);transform:perspective(400px) rotate3d(1,0,0,-20deg);-webkit-transition-timing-function:ease-in;-o-transition-timing-function:ease-in;transition-timing-function:ease-in}60%{-webkit-transform:perspective(400px) rotate3d(1,0,0,10deg);transform:perspective(400px) rotate3d(1,0,0,10deg);opacity:1}80%{-webkit-transform:perspective(400px) rotate3d(1,0,0,-5deg);transform:perspective(400px) rotate3d(1,0,0,-5deg)}100%{-webkit-transform:perspective(400px);transform:perspective(400px)}}@-webkit-keyframes flipInX{0%{-webkit-transform:perspective(400px) rotate3d(1,0,0,90deg);-webkit-transition-timing-function:ease-in;opacity:0}40%{-webkit-transform:perspective(400px) rotate3d(1,0,0,-20deg);-webkit-transition-timing-function:ease-in}60%{-webkit-transform:perspective(400px) rotate3d(1,0,0,10deg);opacity:1}80%{-webkit-transform:perspective(400px) rotate3d(1,0,0,-5deg)}100%{-webkit-transform:perspective(400px)}}.direct-chat-messages,.direct-chat.chat-pane-open .direct-chat-contacts{-webkit-transform:translate(0,0);-ms-transform:translate(0,0);-o-transform:translate(0,0)}.navbar-custom-menu>.navbar-nav>li{position:relative}.navbar-custom-menu>.navbar-nav>li>.dropdown-menu{position:absolute;right:0;left:auto}@media (max-width:991px){.navbar-custom-menu>.navbar-nav{float:right}.navbar-custom-menu>.navbar-nav>li{position:static}.navbar-custom-menu>.navbar-nav>li>.dropdown-menu{position:absolute;right:5%;left:auto;border:1px solid #ddd;background:#fff}}.btn-group-vertical .btn.btn-flat:first-of-type,.btn-group-vertical .btn.btn-flat:last-of-type,.form-control{border-radius:0}.progress-striped .progress-bar-green,.progress-striped .progress-bar-light-blue,.progress-striped .progress-bar-primary,.progress-striped .progress-bar-success{background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:-o-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent)}.form-control{-webkit-box-shadow:none;box-shadow:none;border-color:#d2d6de}.form-control:focus{border-color:#3c8dbc;-webkit-box-shadow:none;box-shadow:none}.form-control:-ms-input-placeholder,.form-control::-moz-placeholder,.form-control::-webkit-input-placeholder{color:#bbb;opacity:1}.form-group.has-success .help-block,.form-group.has-success label{color:#00a65a}.form-control:not(select){-webkit-appearance:none;-moz-appearance:none;appearance:none}.form-group.has-success .form-control,.form-group.has-success .input-group-addon{border-color:#00a65a;-webkit-box-shadow:none;box-shadow:none}.form-group.has-warning .help-block,.form-group.has-warning label{color:#f39c12}.form-group.has-warning .form-control,.form-group.has-warning .input-group-addon{border-color:#f39c12;-webkit-box-shadow:none;box-shadow:none}.form-group.has-error .help-block,.form-group.has-error label{color:#dd4b39}.form-group.has-error .form-control,.form-group.has-error .input-group-addon{border-color:#dd4b39;-webkit-box-shadow:none;box-shadow:none}.input-group .input-group-addon{border-radius:0;border-color:#d2d6de;background-color:#fff}.progress,.progress .progress-bar,.progress-sm,.progress-sm .progress-bar,.progress-xs,.progress-xs .progress-bar,.progress-xxs,.progress-xxs .progress-bar,.progress.sm,.progress.sm .progress-bar,.progress.xs,.progress.xs .progress-bar,.progress.xxs,.progress.xxs .progress-bar,.progress>.progress-bar,.progress>.progress-bar .progress-bar{border-radius:1px}.icheck>label{padding-left:0}.form-control-feedback.fa{line-height:34px}.form-group-lg .form-control+.form-control-feedback.fa,.input-group-lg+.form-control-feedback.fa,.input-lg+.form-control-feedback.fa{line-height:46px}.form-group-sm .form-control+.form-control-feedback.fa,.input-group-sm+.form-control-feedback.fa,.input-sm+.form-control-feedback.fa{line-height:30px}.progress,.progress>.progress-bar{-webkit-box-shadow:none;box-shadow:none}.progress-sm,.progress.sm{height:10px}.progress-xs,.progress.xs{height:7px}.progress-xxs,.progress.xxs{height:3px}.progress.vertical{position:relative;width:30px;height:200px;display:inline-block;margin-right:10px}.progress.vertical>.progress-bar{width:100%;position:absolute;bottom:0}.progress.vertical.progress-sm,.progress.vertical.sm{width:20px}.progress.vertical.progress-xs,.progress.vertical.xs{width:10px}.progress.vertical.progress-xxs,.progress.vertical.xxs{width:3px}.progress-group .progress-text{font-weight:600}.progress-group .progress-number{float:right}.table tr>td .progress{margin:0}.progress-bar-light-blue,.progress-bar-primary{background-color:#3c8dbc}.progress-striped .progress-bar-light-blue,.progress-striped .progress-bar-primary{background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent)}.progress-bar-green,.progress-bar-success{background-color:#00a65a}.progress-striped .progress-bar-green,.progress-striped .progress-bar-success{background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent)}.progress-striped .progress-bar-aqua,.progress-striped .progress-bar-info,.progress-striped .progress-bar-warning,.progress-striped .progress-bar-yellow{background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:-o-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent)}.progress-bar-aqua,.progress-bar-info{background-color:#00c0ef}.progress-striped .progress-bar-aqua,.progress-striped .progress-bar-info{background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent)}.progress-bar-warning,.progress-bar-yellow{background-color:#f39c12}.progress-striped .progress-bar-warning,.progress-striped .progress-bar-yellow{background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent)}.progress-bar-danger,.progress-bar-red{background-color:#dd4b39}.progress-striped .progress-bar-danger,.progress-striped .progress-bar-red{background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:-o-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent)}.small-box{border-radius:2px;position:relative;display:block;margin-bottom:20px;-webkit-box-shadow:0 1px 1px rgba(0,0,0,.1);box-shadow:0 1px 1px rgba(0,0,0,.1)}.box,.info-box{-webkit-box-shadow:0 1px 1px rgba(0,0,0,.1)}.small-box>.inner{padding:10px}.small-box>.small-box-footer{position:relative;text-align:center;padding:3px 0;color:#fff;color:rgba(255,255,255,.8);display:block;z-index:10;background:rgba(0,0,0,.1);text-decoration:none}.small-box>.small-box-footer:hover{color:#fff;background:rgba(0,0,0,.15)}.small-box h3{font-size:38px;font-weight:700;margin:0 0 10px;white-space:nowrap;padding:0}.small-box p{font-size:15px}.small-box p>small{display:block;color:#f9f9f9;font-size:13px;margin-top:5px}.small-box h3,.small-box p{z-index:5}.small-box .icon{-webkit-transition:all .3s linear;-o-transition:all .3s linear;transition:all .3s linear;position:absolute;top:-10px;right:10px;z-index:0;font-size:90px;color:rgba(0,0,0,.15)}.small-box:hover{text-decoration:none;color:#f9f9f9}.small-box:hover .icon{font-size:95px}@media (max-width:767px){.small-box{text-align:center}.small-box .icon{display:none}.small-box p{font-size:12px}}.box{position:relative;border-radius:3px;background:#fff;border-top:3px solid #d2d6de;margin-bottom:20px;width:100%;box-shadow:0 1px 1px rgba(0,0,0,.1)}.box.box-primary{border-top-color:#3c8dbc}.box.box-info{border-top-color:#00c0ef}.box.box-danger{border-top-color:#dd4b39}.box.box-warning{border-top-color:#f39c12}.box.box-success{border-top-color:#00a65a}.box.box-default{border-top-color:#d2d6de}.box.collapsed-box .box-body,.box.collapsed-box .box-footer{display:none}.box .nav-stacked>li{border-bottom:1px solid #f4f4f4;margin:0}.box .nav-stacked>li:last-of-type{border-bottom:none}.box.height-control .box-body{max-height:300px;overflow:auto}.box .border-right{border-right:1px solid #f4f4f4}.box .border-left{border-left:1px solid #f4f4f4}.box.box-solid{border-top:0}.box.box-solid>.box-header .btn.btn-default{background:0 0}.box.box-solid>.box-header .btn:hover,.box.box-solid>.box-header a:hover{background:rgba(0,0,0,.1)}.box.box-solid.box-default{border:1px solid #d2d6de}.box.box-solid.box-default>.box-header{color:#444;background:#d2d6de;background-color:#d2d6de}.box.box-solid.box-default>.box-header .btn,.box.box-solid.box-default>.box-header a{color:#444}.box.box-solid.box-primary{border:1px solid #3c8dbc}.box.box-solid.box-primary>.box-header{color:#fff;background:#3c8dbc;background-color:#3c8dbc}.box.box-solid.box-primary>.box-header .btn,.box.box-solid.box-primary>.box-header a{color:#fff}.box.box-solid.box-info{border:1px solid #00c0ef}.box.box-solid.box-info>.box-header{color:#fff;background:#00c0ef;background-color:#00c0ef}.box.box-solid.box-info>.box-header .btn,.box.box-solid.box-info>.box-header a{color:#fff}.box.box-solid.box-danger{border:1px solid #dd4b39}.box.box-solid.box-danger>.box-header{color:#fff;background:#dd4b39;background-color:#dd4b39}.box.box-solid.box-danger>.box-header .btn,.box.box-solid.box-danger>.box-header a{color:#fff}.box.box-solid.box-warning{border:1px solid #f39c12}.box.box-solid.box-warning>.box-header{color:#fff;background:#f39c12;background-color:#f39c12}.box.box-solid.box-warning>.box-header .btn,.box.box-solid.box-warning>.box-header a{color:#fff}.box.box-solid.box-success{border:1px solid #00a65a}.box.box-solid.box-success>.box-header{color:#fff;background:#00a65a;background-color:#00a65a}.box.box-solid.box-success>.box-header .btn,.box.box-solid.box-success>.box-header a{color:#fff}.box.box-solid>.box-header>.box-tools .btn{border:0;-webkit-box-shadow:none;box-shadow:none}.box.box-solid[class*=bg]>.box-header{color:#fff}.box .box-group>.box{margin-bottom:5px}.box .knob-label{text-align:center;color:#333;font-weight:100;font-size:12px;margin-bottom:.3em}.box>.loading-img,.box>.overlay,.overlay-wrapper>.loading-img,.overlay-wrapper>.overlay{position:absolute;top:0;left:0;width:100%;height:100%}.box .overlay,.overlay-wrapper .overlay{z-index:50;background:rgba(255,255,255,.7);border-radius:3px}.box-body,.box-body .box-pane{border-top-left-radius:0;border-top-right-radius:0;border-bottom-left-radius:3px}.box .overlay>.fa,.overlay-wrapper .overlay>.fa{position:absolute;top:50%;left:50%;margin-left:-15px;margin-top:-15px;color:#000;font-size:30px}.box .overlay.dark,.overlay-wrapper .overlay.dark{background:rgba(0,0,0,.5)}.box-body:after,.box-body:before,.box-footer:after,.box-footer:before,.box-header:after,.box-header:before{content:" ";display:table}.box-body:after,.box-footer:after,.box-header:after{clear:both}.box-header{color:#444;display:block;padding:10px;position:relative}.box-header.with-border{border-bottom:1px solid #f4f4f4}.collapsed-box .box-header.with-border{border-bottom:none}.box-header .box-title,.box-header>.fa,.box-header>.glyphicon,.box-header>.ion{display:inline-block;font-size:18px;margin:0;line-height:1}.box-header>.fa,.box-header>.glyphicon,.box-header>.ion{margin-right:5px}.box-header>.box-tools{position:absolute;right:10px;top:5px}.box-header>.box-tools [data-toggle=tooltip],.timeline{position:relative}.box-header>.box-tools.pull-right .dropdown-menu{right:0;left:auto}.box-header>.box-tools .dropdown-menu>li>a{color:#444!important}.btn-box-tool{padding:5px;font-size:12px;background:0 0;color:#97a0b3}.btn-box-tool:hover,.open .btn-box-tool{color:#606c84}.btn-box-tool.btn:active{-webkit-box-shadow:none;box-shadow:none}.box-body{border-bottom-right-radius:3px;padding:10px}.no-header .box-body{border-top-right-radius:3px;border-top-left-radius:3px}.box-body>.table{margin-bottom:0}.box-body .fc{margin-top:5px}.box-body .full-width-chart{margin:-19px}.box-body.no-padding .full-width-chart{margin:-9px}.box-body .box-pane{border-bottom-right-radius:0}.box-body .box-pane-right,.box-footer{border-top-left-radius:0;border-top-right-radius:0;border-bottom-right-radius:3px}.box-body .box-pane-right{border-bottom-left-radius:0}.box-footer{border-bottom-left-radius:3px;border-top:1px solid #f4f4f4;padding:10px;background-color:#fff}@media (max-width:991px){.chart-legend>li{float:left;margin-right:10px}}.box-comments{background:#f7f7f7}.box-comments .box-comment{padding:8px 0;border-bottom:1px solid #eee}.box-comments .box-comment:after,.box-comments .box-comment:before{content:" ";display:table}.box-comments .box-comment:last-of-type{border-bottom:0}.box-comments .box-comment:first-of-type{padding-top:0}.box-comments .box-comment img{float:left}.box-comments .comment-text{margin-left:40px;color:#555}.box-comments .username{color:#444;display:block;font-weight:600}.box-comments .text-muted{font-weight:400;font-size:12px}.todo-list{margin:0;padding:0;list-style:none;overflow:auto}.todo-list>li{border-radius:2px;padding:10px;background:#f4f4f4;margin-bottom:2px;border-left:2px solid #e6e7e8;color:#444}.todo-list>li:last-of-type{margin-bottom:0}.todo-list>li>input[type=checkbox]{margin:0 10px 0 5px}.todo-list>li .text{display:inline-block;margin-left:5px;font-weight:600}.todo-list>li .label{margin-left:10px;font-size:9px}.todo-list>li .tools{display:none;float:right;color:#dd4b39}.todo-list .handle,.todo-list>li:hover .tools{display:inline-block}.todo-list>li .tools>.fa,.todo-list>li .tools>.glyphicon,.todo-list>li .tools>.ion{margin-right:5px;cursor:pointer}.todo-list>li.done{color:#999}.todo-list>li.done .text{text-decoration:line-through;font-weight:500}.todo-list>li.done .label{background:#d2d6de!important}.todo-list .danger{border-left-color:#dd4b39}.todo-list .warning{border-left-color:#f39c12}.todo-list .info{border-left-color:#00c0ef}.todo-list .success{border-left-color:#00a65a}.todo-list .primary{border-left-color:#3c8dbc}.todo-list .handle{cursor:move;margin:0 5px}.chat{padding:5px 20px 5px 10px}.chat .item{margin-bottom:10px}.chat .item:after,.chat .item:before{content:" ";display:table}.chat .item>img{width:40px;height:40px;border:2px solid transparent;border-radius:50%}.chat .item>.online{border:2px solid #00a65a}.chat .item>.offline{border:2px solid #dd4b39}.chat .item>.message{margin-left:55px;margin-top:-40px}.chat .item>.message>.name{display:block;font-weight:600}.chat .item>.attachment{border-radius:3px;background:#f4f4f4;margin-left:65px;margin-right:15px;padding:10px}.chat .item>.attachment>h4{margin:0 0 5px;font-weight:600;font-size:14px}.chat .item>.attachment>.filename,.chat .item>.attachment>p{font-weight:600;font-size:13px;font-style:italic;margin:0}.chat .item>.attachment:after,.chat .item>.attachment:before{content:" ";display:table}.info-box,.info-box-icon,.info-box-more,.info-box-number{display:block}.box-input{max-width:200px}.modal .panel-body{color:#444}.info-box{min-height:90px;background:#fff;width:100%;box-shadow:0 1px 1px rgba(0,0,0,.1);border-radius:2px;margin-bottom:15px}.info-box small{font-size:14px}.info-box .progress{background:rgba(0,0,0,.2);margin:5px -10px;height:2px}.info-box .progress,.info-box .progress .progress-bar{border-radius:0}.info-box .progress .progress-bar{background:#fff}.info-box-icon{border-top-left-radius:2px;border-top-right-radius:0;border-bottom-right-radius:0;border-bottom-left-radius:2px;float:left;height:90px;width:90px;text-align:center;font-size:45px;line-height:90px;background:rgba(0,0,0,.2)}.info-box-icon>img{max-width:100%}.info-box-content{padding:5px 10px;margin-left:90px}.info-box-number{font-weight:700;font-size:18px}.info-box-text,.progress-description{display:block;font-size:14px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.progress-description{margin:0}.timeline{margin:0 0 30px;padding:0;list-style:none}.timeline:before{content:'';position:absolute;top:0;bottom:0;width:4px;background:#ddd;left:31px;margin:0;border-radius:2px}.timeline>li{position:relative;margin-right:10px;margin-bottom:15px}.timeline>li:after,.timeline>li:before{content:" ";display:table}.timeline>li>.timeline-item{-webkit-box-shadow:0 1px 1px rgba(0,0,0,.1);box-shadow:0 1px 1px rgba(0,0,0,.1);border-radius:3px;margin-top:0;background:#fff;color:#444;margin-left:60px;margin-right:15px;padding:0;position:relative}.timeline>li>.timeline-item>.time{color:#999;float:right;padding:10px;font-size:12px}.timeline>li>.timeline-item>.timeline-header{margin:0;color:#555;border-bottom:1px solid #f4f4f4;padding:10px;font-size:16px;line-height:1.1}.timeline>li>.timeline-item>.timeline-header>a{font-weight:600}.timeline>li>.timeline-item>.timeline-body,.timeline>li>.timeline-item>.timeline-footer{padding:10px}.timeline>li>.fa,.timeline>li>.glyphicon,.timeline>li>.ion{width:30px;height:30px;font-size:15px;line-height:30px;position:absolute;color:#666;background:#d2d6de;border-radius:50%;text-align:center;left:18px;top:0}.timeline>.time-label>span{font-weight:600;padding:5px;display:inline-block;background-color:#fff;border-radius:4px}.timeline-inverse>li>.timeline-item{background:#f0f0f0;border:1px solid #ddd;-webkit-box-shadow:none;box-shadow:none}.btn,.btn-app{border-radius:3px}.timeline-inverse>li>.timeline-item>.timeline-header{border-bottom-color:#ddd}.btn{-webkit-box-shadow:none;box-shadow:none;border:1px solid transparent}.btn.btn-flat{border-radius:0;-webkit-box-shadow:none;box-shadow:none;border-width:1px}.btn:active{-webkit-box-shadow:inset 0 3px 5px rgba(0,0,0,.125);box-shadow:inset 0 3px 5px rgba(0,0,0,.125)}.btn:focus{outline:0}.btn.btn-file{position:relative;overflow:hidden}.btn.btn-file>input[type=file]{position:absolute;top:0;right:0;min-width:100%;min-height:100%;font-size:100px;text-align:right;opacity:0;filter:alpha(opacity=0);outline:0;background:#fff;cursor:inherit;display:block}.btn-app,table.text-center,table.text-center td,table.text-center th{text-align:center}.btn-default{background-color:#f9f9f9;color:#444;border-color:#888}.btn-default.hover,.btn-default:active,.btn-default:hover{background-color:#e7e7e7}.btn-primary{background-color:#3c8dbc;border-color:#367fa9;color:#fff}.btn-primary.hover,.btn-primary:active,.btn-primary:hover{background-color:#367fa9}.btn-success{background-color:#00a65a;border-color:#008d4c}.btn-success.hover,.btn-success:active,.btn-success:hover{background-color:#008d4c}.btn-info{background-color:#00c0ef;border-color:#00acd6;color:#fff}.btn-info.hover,.btn-info:active,.btn-info:hover{background-color:#00acd6}.btn-danger{background-color:#dd4b39;border-color:#d73925}.btn-danger.hover,.btn-danger:active,.btn-danger:hover{background-color:#d73925}.btn-warning{background-color:#f39c12;border-color:#e08e0b}.btn-warning.hover,.btn-warning:active,.btn-warning:hover{background-color:#e08e0b}.btn-outline{border:1px solid #fff;background:0 0;color:#fff}.btn-outline:active,.btn-outline:focus,.btn-outline:hover{color:rgba(255,255,255,.7);border-color:rgba(255,255,255,.7)}.btn-link{-webkit-box-shadow:none;box-shadow:none}.btn[class*=bg-]:hover{-webkit-box-shadow:inset 0 0 100px rgba(0,0,0,.2);box-shadow:inset 0 0 100px rgba(0,0,0,.2)}.btn-app{position:relative;padding:15px 5px;margin:0 0 10px 10px;min-width:80px;height:60px;color:#666;border:1px solid #ddd;background-color:#f4f4f4;font-size:12px}.alert,.callout{border-radius:3px}.btn-app>.fa,.btn-app>.glyphicon,.btn-app>.ion{font-size:20px;display:block}.btn-app:hover{background:#f4f4f4;color:#444;border-color:#aaa}.btn-app:active,.btn-app:focus{-webkit-box-shadow:inset 0 3px 5px rgba(0,0,0,.125);box-shadow:inset 0 3px 5px rgba(0,0,0,.125)}.btn-app>.badge{position:absolute;top:-3px;right:-10px;font-size:10px;font-weight:400}.alert h4,.callout h4,.contacts-list-name,.direct-chat-name,.nav-pills>li.active>a,.products-list .product-title{font-weight:600}.callout{margin:0 0 20px;padding:15px 30px 15px 15px;border-left:5px solid #eee}.callout a{color:#fff}.callout a:hover{color:#eee}.callout h4{margin-top:0}.callout p:last-child{margin-bottom:0}.callout .highlight,.callout code{background-color:#fff}.callout.callout-danger{border-color:#c23321}.callout.callout-warning{border-color:#c87f0a}.callout.callout-info{border-color:#0097bc}.callout.callout-success{border-color:#00733e}.alert .icon{margin-right:10px}.alert .close{color:#000;opacity:.2;filter:alpha(opacity=20)}.alert .close:hover{opacity:.5;filter:alpha(opacity=50)}.alert a{color:#fff}.dashboard-link,.skin-black .sidebar a:hover{text-decoration:none}.alert-success{border-color:#008d4c}.alert-danger,.alert-error{border-color:#d73925}.alert-warning{border-color:#e08e0b}.alert-info{border-color:#00acd6}.nav>li>a:active,.nav>li>a:focus,.nav>li>a:hover{color:#444;background:#f7f7f7}.nav-pills>li>a{border-radius:0;border-top:3px solid transparent;color:#444}.nav-pills>li>a>.fa,.nav-pills>li>a>.glyphicon,.nav-pills>li>a>.ion{margin-right:5px}.nav-pills>li.active>a,.nav-pills>li.active>a:focus,.nav-pills>li.active>a:hover{border-top-color:#3c8dbc}.nav-stacked>li>a{border-radius:0;border-top:0;border-left:3px solid transparent;color:#444}.nav-stacked>li.active>a,.nav-stacked>li.active>a:hover{background:0 0;color:#444;border-top:0;border-left-color:#3c8dbc}.nav-stacked>li.header{border-bottom:1px solid #ddd;color:#777;margin-bottom:10px;padding:5px 10px}.nav-tabs-custom{margin-bottom:20px;background:#fff;-webkit-box-shadow:0 1px 1px rgba(0,0,0,.1);box-shadow:0 1px 1px rgba(0,0,0,.1);border-radius:3px}.nav-tabs-custom>.nav-tabs{margin:0;border-bottom-color:#f4f4f4;border-top-right-radius:3px;border-top-left-radius:3px}.nav-tabs-custom>.nav-tabs>li{border-top:3px solid transparent;margin-bottom:-2px;margin-right:5px}.nav-tabs-custom>.nav-tabs>li>a{color:#444;border-radius:0}.nav-tabs-custom>.nav-tabs>li>a.text-muted,.nav-tabs-custom>.nav-tabs>li>a:hover{color:#999}.nav-tabs-custom>.nav-tabs>li>a,.nav-tabs-custom>.nav-tabs>li>a:hover{background:0 0;margin:0}.nav-tabs-custom>.nav-tabs>li:not(.active)>a:active,.nav-tabs-custom>.nav-tabs>li:not(.active)>a:focus,.nav-tabs-custom>.nav-tabs>li:not(.active)>a:hover{border-color:transparent}.nav-tabs-custom>.nav-tabs>li.active{border-top-color:#3c8dbc}.nav-tabs-custom>.nav-tabs>li.active:hover>a,.nav-tabs-custom>.nav-tabs>li.active>a{background-color:#fff;color:#444}.nav-tabs-custom>.nav-tabs>li.active>a{border-top-color:transparent;border-left-color:#f4f4f4;border-right-color:#f4f4f4}.nav-tabs-custom>.nav-tabs>li:first-of-type{margin-left:0}.nav-tabs-custom>.nav-tabs>li:first-of-type.active>a{border-left-color:transparent}.nav-tabs-custom>.nav-tabs.pull-right{float:none!important}.nav-tabs-custom>.nav-tabs.pull-right>li{float:right}.nav-tabs-custom>.nav-tabs.pull-right>li:first-of-type{margin-right:0}.nav-tabs-custom>.nav-tabs.pull-right>li:first-of-type>a{border-left-width:1px}.nav-tabs-custom>.nav-tabs.pull-right>li:first-of-type.active>a{border-left-color:#f4f4f4;border-right-color:transparent}.nav-tabs-custom>.nav-tabs>li.header{line-height:35px;padding:0 10px;font-size:20px;color:#444}.nav-tabs-custom>.nav-tabs>li.header>.fa,.nav-tabs-custom>.nav-tabs>li.header>.glyphicon,.nav-tabs-custom>.nav-tabs>li.header>.ion{margin-right:5px}.nav-tabs-custom>.tab-content{background:#fff;padding:10px;border-bottom-right-radius:3px;border-bottom-left-radius:3px}.nav-tabs-custom .dropdown.open>a:active,.nav-tabs-custom .dropdown.open>a:focus{background:0 0;color:#999}.nav-tabs-custom.tab-primary>.nav-tabs>li.active{border-top-color:#3c8dbc}.nav-tabs-custom.tab-info>.nav-tabs>li.active{border-top-color:#00c0ef}.nav-tabs-custom.tab-danger>.nav-tabs>li.active{border-top-color:#dd4b39}.nav-tabs-custom.tab-warning>.nav-tabs>li.active{border-top-color:#f39c12}.nav-tabs-custom.tab-success>.nav-tabs>li.active{border-top-color:#00a65a}.nav-tabs-custom.tab-default>.nav-tabs>li.active{border-top-color:#d2d6de}.pagination>li>a{background:#fafafa;color:#666}.pagination.pagination-flat>li>a{border-radius:0!important}.products-list{list-style:none;margin:0;padding:0}.products-list>.item{border-radius:3px;-webkit-box-shadow:0 1px 1px rgba(0,0,0,.1);box-shadow:0 1px 1px rgba(0,0,0,.1);padding:10px 0;background:#fff}.products-list>.item:after,.products-list>.item:before{content:" ";display:table}.products-list .product-img{float:left}.products-list .product-img img{width:50px;height:50px}.products-list .product-info{margin-left:60px}.products-list .product-description{display:block;color:#999;overflow:hidden;white-space:nowrap;text-overflow:ellipsis}.product-list-in-box>.item{-webkit-box-shadow:none;box-shadow:none;border-radius:0;border-bottom:1px solid #f4f4f4}.product-list-in-box>.item:last-of-type{border-bottom-width:0}.table>tbody>tr>td,.table>tbody>tr>th,.table>tfoot>tr>td,.table>tfoot>tr>th,.table>thead>tr>td,.table>thead>tr>th{border-top:1px solid #f4f4f4}.table>thead>tr>th{border-bottom:2px solid #f4f4f4}.table tr td .progress{margin-top:5px}.table-bordered,.table-bordered>tbody>tr>td,.table-bordered>tbody>tr>th,.table-bordered>tfoot>tr>td,.table-bordered>tfoot>tr>th,.table-bordered>thead>tr>td,.table-bordered>thead>tr>th{border:1px solid #f4f4f4}.table-bordered>thead>tr>td,.table-bordered>thead>tr>th{border-bottom-width:2px}.table.no-border,.table.no-border td,.table.no-border th{border:0}.table.align th{text-align:left}.table.align td{text-align:right}.label-default{background-color:#d2d6de;color:#444}.direct-chat .box-body{border-bottom-right-radius:0;border-bottom-left-radius:0;position:relative;overflow-x:hidden;padding:0}.direct-chat.chat-pane-open .direct-chat-contacts{transform:translate(0,0)}.direct-chat-messages{transform:translate(0,0);padding:10px;height:250px;overflow:auto}.direct-chat-msg,.direct-chat-text{display:block}.direct-chat-msg{margin-bottom:10px}.direct-chat-msg:after,.direct-chat-msg:before{content:" ";display:table}.direct-chat-contacts,.direct-chat-messages{-webkit-transition:-webkit-transform .5s ease-in-out;-o-transition:-o-transform .5s ease-in-out;transition:transform .5s ease-in-out}.direct-chat-text{border-radius:5px;position:relative;padding:5px 10px;background:#d2d6de;border:1px solid #d2d6de;margin:5px 0 0 50px;color:#444}.direct-chat-text:after,.direct-chat-text:before{position:absolute;right:100%;top:15px;border:solid transparent;border-right-color:#d2d6de;content:' ';height:0;width:0;pointer-events:none}.direct-chat-text:after{border-width:5px;margin-top:-5px}.direct-chat-text:before{border-width:6px;margin-top:-6px}.right .direct-chat-text{margin-right:50px;margin-left:0}.right .direct-chat-text:after,.right .direct-chat-text:before{right:auto;left:100%;border-right-color:transparent;border-left-color:#d2d6de}.direct-chat-img{border-radius:50%;float:left;width:40px;height:40px}.right .direct-chat-img{float:right}.direct-chat-info{display:block;margin-bottom:2px;font-size:12px}.direct-chat-timestamp{color:#999}.direct-chat-contacts-open .direct-chat-contacts{-webkit-transform:translate(0,0);-ms-transform:translate(0,0);-o-transform:translate(0,0);transform:translate(0,0)}.direct-chat-contacts{-webkit-transform:translate(101%,0);-ms-transform:translate(101%,0);-o-transform:translate(101%,0);transform:translate(101%,0);position:absolute;top:0;bottom:0;height:250px;width:100%;background:#222d32;color:#fff;overflow:auto}.contacts-list>li{border-bottom:1px solid rgba(0,0,0,.2);padding:10px;margin:0}.contacts-list>li:after,.contacts-list>li:before{content:" ";display:table}.contacts-list-name,.contacts-list-status,.users-list-date,.users-list-name{display:block}.contacts-list>li:last-of-type{border-bottom:none}.contacts-list-img{border-radius:50%;width:40px;float:left}.contacts-list-info{margin-left:45px;color:#fff}.contacts-list-status{font-size:12px}.contacts-list-date{color:#aaa;font-weight:400}.contacts-list-msg{color:#999}.direct-chat-danger .right>.direct-chat-text{background:#dd4b39;border-color:#dd4b39;color:#fff}.direct-chat-danger .right>.direct-chat-text:after,.direct-chat-danger .right>.direct-chat-text:before{border-left-color:#dd4b39}.direct-chat-primary .right>.direct-chat-text{background:#3c8dbc;border-color:#3c8dbc;color:#fff}.direct-chat-primary .right>.direct-chat-text:after,.direct-chat-primary .right>.direct-chat-text:before{border-left-color:#3c8dbc}.direct-chat-warning .right>.direct-chat-text{background:#f39c12;border-color:#f39c12;color:#fff}.direct-chat-warning .right>.direct-chat-text:after,.direct-chat-warning .right>.direct-chat-text:before{border-left-color:#f39c12}.direct-chat-info .right>.direct-chat-text{background:#00c0ef;border-color:#00c0ef;color:#fff}.direct-chat-info .right>.direct-chat-text:after,.direct-chat-info .right>.direct-chat-text:before{border-left-color:#00c0ef}.direct-chat-success .right>.direct-chat-text{background:#00a65a;border-color:#00a65a;color:#fff}.direct-chat-success .right>.direct-chat-text:after,.direct-chat-success .right>.direct-chat-text:before{border-left-color:#00a65a}.users-list>li{width:25%;float:left;padding:10px;text-align:center}.users-list>li img{border-radius:50%;max-width:100%;height:auto}.users-list>li>a:hover,.users-list>li>a:hover .users-list-name{color:#999}.users-list-name{font-weight:600;color:#444;overflow:hidden;white-space:nowrap;text-overflow:ellipsis}.users-list-date{color:#999;font-size:12px}.carousel-control.left,.carousel-control.right{background-image:none}.carousel-control>.fa{font-size:40px;position:absolute;top:50%;z-index:5;display:inline-block;margin-top:-20px}.modal{background:rgba(0,0,0,.3)}.modal-content{border-radius:0;-webkit-box-shadow:0 2px 3px rgba(0,0,0,.125);box-shadow:0 2px 3px rgba(0,0,0,.125);border:0}@media (min-width:768px){.modal-content{-webkit-box-shadow:0 2px 3px rgba(0,0,0,.125);box-shadow:0 2px 3px rgba(0,0,0,.125)}}.modal-header{border-bottom-color:#f4f4f4}.modal-footer{border-top-color:#f4f4f4}.modal-primary .modal-footer,.modal-primary .modal-header{border-color:#307095}.modal-warning .modal-footer,.modal-warning .modal-header{border-color:#c87f0a}.modal-info .modal-footer,.modal-info .modal-header{border-color:#0097bc}.modal-success .modal-footer,.modal-success .modal-header{border-color:#00733e}.modal-danger .modal-footer,.modal-danger .modal-header{border-color:#c23321}.box-widget{border:none;position:relative}.widget-user .widget-user-header{padding:20px;height:120px;border-top-right-radius:3px;border-top-left-radius:3px}.widget-user .widget-user-username{margin-top:0;margin-bottom:5px;font-size:25px;font-weight:300;text-shadow:0 1px 1px rgba(0,0,0,.2)}.widget-user .widget-user-desc{margin-top:0}.widget-user .widget-user-image{position:absolute;top:65px;left:50%;margin-left:-45px}.widget-user .widget-user-image>img{width:90px;height:auto;border:3px solid #fff}.mailbox-controls.with-border,.mailbox-read-info{border-bottom:1px solid #f4f4f4}.widget-user .box-footer{padding-top:30px}.widget-user-2 .widget-user-header{padding:20px;border-top-right-radius:3px;border-top-left-radius:3px}.widget-user-2 .widget-user-username{margin-top:5px;margin-bottom:5px;font-size:25px;font-weight:300}.widget-user-2 .widget-user-desc{margin-top:0}.widget-user-2 .widget-user-desc,.widget-user-2 .widget-user-username{margin-left:75px}.widget-user-2 .widget-user-image>img{width:65px;height:auto;float:left}.mailbox-messages>.table{margin:0}.mailbox-controls{padding:5px}.mailbox-read-info{padding:10px}.mailbox-read-info h3{font-size:20px;margin:0}.mailbox-read-info h5{margin:0;padding:5px 0 0}.mailbox-read-time{color:#999;font-size:13px}.mailbox-read-message{padding:10px}.mailbox-attachments li{float:left;width:200px;border:1px solid #eee;margin-bottom:10px;margin-right:10px}.mailbox-attachment-name{font-weight:700;color:#666}.mailbox-attachment-icon,.mailbox-attachment-info,.mailbox-attachment-size{display:block}.mailbox-attachment-info{padding:10px;background:#f4f4f4}.mailbox-attachment-size{color:#999;font-size:12px}.mailbox-attachment-icon{text-align:center;font-size:65px;color:#666;padding:20px 10px}.lockscreen-logo a,.login-logo a,.register-logo a{color:#444}.mailbox-attachment-icon.has-img{padding:0}.mailbox-attachment-icon.has-img>img{max-width:100%;height:auto}.lockscreen{background:#d2d6de}.lockscreen-logo{font-size:35px;text-align:center;margin-bottom:25px;font-weight:300}.lockscreen-wrapper{max-width:400px;margin:0 auto;margin-top:10%}.lockscreen .lockscreen-name{text-align:center;font-weight:600}.lockscreen-item{border-radius:4px;padding:0;background:#fff;position:relative;margin:10px auto 30px;width:290px}.lockscreen-image{border-radius:50%;position:absolute;left:-10px;top:-25px;background:#fff;padding:5px;z-index:10}.lockscreen-image>img{border-radius:50%;width:70px;height:70px}.lockscreen-credentials{margin-left:70px}.lockscreen-credentials .form-control{border:0}.lockscreen-credentials .btn{background-color:#fff;border:0;padding:0 10px}.lockscreen-footer{margin-top:10px}.login-logo,.register-logo{font-size:35px;text-align:center;margin-bottom:25px;font-weight:300}.login-page,.register-page{background:#d2d6de}.login-box,.register-box{width:360px;margin:7% auto}@media (max-width:768px){.login-box,.register-box{width:90%;margin-top:20px}}.login-box-body,.register-box-body{background:#fff;padding:20px;border-top:0;color:#666}.login-box-body .form-control-feedback,.register-box-body .form-control-feedback{color:#777}.login-box-msg,.register-box-msg{margin:0;text-align:center;padding:0 20px 20px}.social-auth-links{margin:10px 0}.error-page{width:600px;margin:20px auto 0}.error-page>.headline{float:left;font-size:100px;font-weight:300}.error-page>.error-content{margin-left:190px;display:block}.error-page>.error-content>h3{font-weight:300;font-size:25px}@media (max-width:991px){.error-page{width:100%}.error-page>.headline{float:none;text-align:center}.error-page>.error-content{margin-left:0}.error-page>.error-content>h3{text-align:center}}.invoice{position:relative;background:#fff;border:1px solid #f4f4f4;padding:20px;margin:10px 25px}.btn-adn.active,.btn-adn:active,.btn-bitbucket.active,.btn-bitbucket:active,.btn-dropbox.active,.btn-dropbox:active,.btn-facebook.active,.btn-facebook:active,.btn-flickr.active,.btn-flickr:active,.btn-foursquare.active,.btn-foursquare:active,.btn-github.active,.btn-github:active,.btn-google.active,.btn-google:active,.btn-instagram.active,.btn-instagram:active,.btn-microsoft.active,.btn-microsoft:active,.btn-openid.active,.btn-openid:active,.btn-pinterest.active,.btn-pinterest:active,.btn-reddit.active,.btn-reddit:active,.btn-soundcloud.active,.btn-soundcloud:active,.btn-tumblr.active,.btn-tumblr:active,.btn-twitter.active,.btn-twitter:active,.btn-vimeo.active,.btn-vimeo:active,.btn-vk.active,.btn-vk:active,.btn-yahoo.active,.btn-yahoo:active,.open>.dropdown-toggle.btn-adn,.open>.dropdown-toggle.btn-bitbucket,.open>.dropdown-toggle.btn-dropbox,.open>.dropdown-toggle.btn-facebook,.open>.dropdown-toggle.btn-flickr,.open>.dropdown-toggle.btn-foursquare,.open>.dropdown-toggle.btn-github,.open>.dropdown-toggle.btn-google,.open>.dropdown-toggle.btn-instagram,.open>.dropdown-toggle.btn-microsoft,.open>.dropdown-toggle.btn-openid,.open>.dropdown-toggle.btn-pinterest,.open>.dropdown-toggle.btn-reddit,.open>.dropdown-toggle.btn-soundcloud,.open>.dropdown-toggle.btn-tumblr,.open>.dropdown-toggle.btn-twitter,.open>.dropdown-toggle.btn-vimeo,.open>.dropdown-toggle.btn-vk,.open>.dropdown-toggle.btn-yahoo{background-image:none}.invoice-title{margin-top:0}.profile-user-img{margin:0 auto;width:100px;padding:3px;border:3px solid #d2d6de}.profile-username{font-size:21px;margin-top:5px}.post{border-bottom:1px solid #d2d6de;margin-bottom:15px;padding-bottom:15px;color:#666}.post:last-of-type{border-bottom:0;margin-bottom:0;padding-bottom:0}.post .user-block{margin-bottom:15px}.btn-social{position:relative;padding-left:44px;text-align:left;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.btn-social>:first-child{position:absolute;left:0;top:0;bottom:0;width:32px;line-height:34px;font-size:1.6em;text-align:center;border-right:1px solid rgba(0,0,0,.2)}.btn-social.btn-lg{padding-left:61px}.btn-social.btn-lg>:first-child{line-height:45px;width:45px;font-size:1.8em}.btn-social.btn-sm{padding-left:38px}.btn-social.btn-sm>:first-child{line-height:28px;width:28px;font-size:1.4em}.btn-social.btn-xs{padding-left:30px}.btn-social.btn-xs>:first-child{line-height:20px;width:20px;font-size:1.2em}.btn-social-icon{position:relative;padding-left:44px;text-align:left;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;height:34px;width:34px;padding:0}.btn-social-icon>:first-child{position:absolute;left:0;top:0;bottom:0;line-height:34px;font-size:1.6em}.btn-social-icon.btn-lg>:first-child{line-height:45px;width:45px;font-size:1.8em}.btn-social-icon.btn-sm>:first-child{line-height:28px;width:28px;font-size:1.4em}.btn-social-icon.btn-xs>:first-child{line-height:20px;width:20px;font-size:1.2em}.btn-social-icon>:first-child{border:none;text-align:center;width:100%}.btn-social-icon.btn-lg{height:45px;width:45px;padding-left:0;padding-right:0}.btn-social-icon.btn-sm{height:30px;width:30px;padding-left:0;padding-right:0}.btn-social-icon.btn-xs{height:22px;width:22px;padding-left:0;padding-right:0}.fc-day-number,.fc-header-right{padding-right:10px}.btn-adn{color:#fff;background-color:#d87a68;border-color:rgba(0,0,0,.2)}.btn-adn.active,.btn-adn.focus,.btn-adn:active,.btn-adn:focus,.btn-adn:hover,.open>.dropdown-toggle.btn-adn{color:#fff;background-color:#ce563f;border-color:rgba(0,0,0,.2)}.btn-adn .badge{color:#d87a68;background-color:#fff}.btn-bitbucket{color:#fff;background-color:#205081;border-color:rgba(0,0,0,.2)}.btn-bitbucket.active,.btn-bitbucket.focus,.btn-bitbucket:active,.btn-bitbucket:focus,.btn-bitbucket:hover,.open>.dropdown-toggle.btn-bitbucket{color:#fff;background-color:#163758;border-color:rgba(0,0,0,.2)}.btn-bitbucket .badge{color:#205081;background-color:#fff}.btn-dropbox{color:#fff;background-color:#1087dd;border-color:rgba(0,0,0,.2)}.btn-dropbox.active,.btn-dropbox.focus,.btn-dropbox:active,.btn-dropbox:focus,.btn-dropbox:hover,.open>.dropdown-toggle.btn-dropbox{color:#fff;background-color:#0d6aad;border-color:rgba(0,0,0,.2)}.btn-dropbox .badge{color:#1087dd;background-color:#fff}.btn-facebook{color:#fff;background-color:#3b5998;border-color:rgba(0,0,0,.2)}.btn-facebook.active,.btn-facebook.focus,.btn-facebook:active,.btn-facebook:focus,.btn-facebook:hover,.open>.dropdown-toggle.btn-facebook{color:#fff;background-color:#2d4373;border-color:rgba(0,0,0,.2)}.btn-facebook .badge{color:#3b5998;background-color:#fff}.btn-flickr{color:#fff;background-color:#ff0084;border-color:rgba(0,0,0,.2)}.btn-flickr.active,.btn-flickr.focus,.btn-flickr:active,.btn-flickr:focus,.btn-flickr:hover,.open>.dropdown-toggle.btn-flickr{color:#fff;background-color:#cc006a;border-color:rgba(0,0,0,.2)}.btn-flickr .badge{color:#ff0084;background-color:#fff}.btn-foursquare{color:#fff;background-color:#f94877;border-color:rgba(0,0,0,.2)}.btn-foursquare.active,.btn-foursquare.focus,.btn-foursquare:active,.btn-foursquare:focus,.btn-foursquare:hover,.open>.dropdown-toggle.btn-foursquare{color:#fff;background-color:#f71752;border-color:rgba(0,0,0,.2)}.btn-foursquare .badge{color:#f94877;background-color:#fff}.btn-github{color:#fff;background-color:#444;border-color:rgba(0,0,0,.2)}.btn-github.active,.btn-github.focus,.btn-github:active,.btn-github:focus,.btn-github:hover,.open>.dropdown-toggle.btn-github{color:#fff;background-color:#2b2b2b;border-color:rgba(0,0,0,.2)}.btn-github .badge{color:#444;background-color:#fff}.btn-google{color:#fff;background-color:#dd4b39;border-color:rgba(0,0,0,.2)}.btn-google.active,.btn-google.focus,.btn-google:active,.btn-google:focus,.btn-google:hover,.open>.dropdown-toggle.btn-google{color:#fff;background-color:#c23321;border-color:rgba(0,0,0,.2)}.btn-google .badge{color:#dd4b39;background-color:#fff}.btn-instagram{color:#fff;background-color:#3f729b;border-color:rgba(0,0,0,.2)}.btn-instagram.active,.btn-instagram.focus,.btn-instagram:active,.btn-instagram:focus,.btn-instagram:hover,.open>.dropdown-toggle.btn-instagram{color:#fff;background-color:#305777;border-color:rgba(0,0,0,.2)}.btn-instagram .badge{color:#3f729b;background-color:#fff}.btn-linkedin{color:#fff;background-color:#007bb6;border-color:rgba(0,0,0,.2)}.btn-linkedin.active,.btn-linkedin.focus,.btn-linkedin:active,.btn-linkedin:focus,.btn-linkedin:hover,.open>.dropdown-toggle.btn-linkedin{color:#fff;background-color:#005983;border-color:rgba(0,0,0,.2)}.btn-linkedin.active,.btn-linkedin:active,.open>.dropdown-toggle.btn-linkedin{background-image:none}.btn-linkedin .badge{color:#007bb6;background-color:#fff}.btn-microsoft{color:#fff;background-color:#2672ec;border-color:rgba(0,0,0,.2)}.btn-microsoft.active,.btn-microsoft.focus,.btn-microsoft:active,.btn-microsoft:focus,.btn-microsoft:hover,.open>.dropdown-toggle.btn-microsoft{color:#fff;background-color:#125acd;border-color:rgba(0,0,0,.2)}.btn-microsoft .badge{color:#2672ec;background-color:#fff}.btn-openid{color:#fff;background-color:#f7931e;border-color:rgba(0,0,0,.2)}.btn-openid.active,.btn-openid.focus,.btn-openid:active,.btn-openid:focus,.btn-openid:hover,.open>.dropdown-toggle.btn-openid{color:#fff;background-color:#da7908;border-color:rgba(0,0,0,.2)}.btn-openid .badge{color:#f7931e;background-color:#fff}.btn-pinterest{color:#fff;background-color:#cb2027;border-color:rgba(0,0,0,.2)}.btn-pinterest.active,.btn-pinterest.focus,.btn-pinterest:active,.btn-pinterest:focus,.btn-pinterest:hover,.open>.dropdown-toggle.btn-pinterest{color:#fff;background-color:#9f191f;border-color:rgba(0,0,0,.2)}.btn-pinterest .badge{color:#cb2027;background-color:#fff}.btn-reddit{color:#000;background-color:#eff7ff;border-color:rgba(0,0,0,.2)}.btn-reddit.active,.btn-reddit.focus,.btn-reddit:active,.btn-reddit:focus,.btn-reddit:hover,.open>.dropdown-toggle.btn-reddit{color:#000;background-color:#bcddff;border-color:rgba(0,0,0,.2)}.btn-reddit .badge{color:#eff7ff;background-color:#000}.btn-soundcloud{color:#fff;background-color:#f50;border-color:rgba(0,0,0,.2)}.btn-soundcloud.active,.btn-soundcloud.focus,.btn-soundcloud:active,.btn-soundcloud:focus,.btn-soundcloud:hover,.open>.dropdown-toggle.btn-soundcloud{color:#fff;background-color:#c40;border-color:rgba(0,0,0,.2)}.btn-soundcloud .badge{color:#f50;background-color:#fff}.btn-tumblr{color:#fff;background-color:#2c4762;border-color:rgba(0,0,0,.2)}.btn-tumblr.active,.btn-tumblr.focus,.btn-tumblr:active,.btn-tumblr:focus,.btn-tumblr:hover,.open>.dropdown-toggle.btn-tumblr{color:#fff;background-color:#1c2d3f;border-color:rgba(0,0,0,.2)}.btn-tumblr .badge{color:#2c4762;background-color:#fff}.btn-twitter{color:#fff;background-color:#55acee;border-color:rgba(0,0,0,.2)}.btn-twitter.active,.btn-twitter.focus,.btn-twitter:active,.btn-twitter:focus,.btn-twitter:hover,.open>.dropdown-toggle.btn-twitter{color:#fff;background-color:#2795e9;border-color:rgba(0,0,0,.2)}.btn-twitter .badge{color:#55acee;background-color:#fff}.btn-vimeo{color:#fff;background-color:#1ab7ea;border-color:rgba(0,0,0,.2)}.btn-vimeo.active,.btn-vimeo.focus,.btn-vimeo:active,.btn-vimeo:focus,.btn-vimeo:hover,.open>.dropdown-toggle.btn-vimeo{color:#fff;background-color:#1295bf;border-color:rgba(0,0,0,.2)}.btn-vimeo .badge{color:#1ab7ea;background-color:#fff}.btn-vk{color:#fff;background-color:#587ea3;border-color:rgba(0,0,0,.2)}.btn-vk.active,.btn-vk.focus,.btn-vk:active,.btn-vk:focus,.btn-vk:hover,.open>.dropdown-toggle.btn-vk{color:#fff;background-color:#466482;border-color:rgba(0,0,0,.2)}.btn-vk .badge{color:#587ea3;background-color:#fff}.btn-yahoo{color:#fff;background-color:#720e9e;border-color:rgba(0,0,0,.2)}.btn-yahoo.active,.btn-yahoo.focus,.btn-yahoo:active,.btn-yahoo:focus,.btn-yahoo:hover,.open>.dropdown-toggle.btn-yahoo{color:#fff;background-color:#500a6f;border-color:rgba(0,0,0,.2)}.btn-yahoo .badge{color:#720e9e;background-color:#fff}.fc-button{background:#f4f4f4;background-image:none;color:#444;border-color:#ddd;border-bottom-color:#ddd}.fc-button.hover,.fc-button:active,.fc-button:hover{background-color:#e9e9e9}.fc-header-title h2{font-size:15px;line-height:1.6em;color:#666;margin-left:10px}.fc-header-left{padding-left:10px}.fc-widget-header{background:#fafafa}.fc-grid{width:100%;border:0}.fc-widget-content:first-of-type,.fc-widget-header:first-of-type{border-left:0;border-right:0}.fc-widget-content:last-of-type,.fc-widget-header:last-of-type{border-right:0}.fc-toolbar{padding:10px;margin:0}.fc-day-number{font-size:20px;font-weight:300}.fc-color-picker{list-style:none;margin:0;padding:0}.fc-color-picker>li{float:left;font-size:30px;margin-right:5px;line-height:30px}.fc-color-picker>li .fa{-webkit-transition:-webkit-transform linear .3s;-o-transition:-o-transform linear .3s;transition:transform linear .3s}.fc-color-picker>li .fa:hover{-webkit-transform:rotate(30deg);-ms-transform:rotate(30deg);-o-transform:rotate(30deg);transform:rotate(30deg)}#add-new-event{-webkit-transition:all linear .3s;-o-transition:all linear .3s;transition:all linear .3s}.external-event{padding:5px 10px;font-weight:700;margin-bottom:4px;-webkit-box-shadow:0 1px 1px rgba(0,0,0,.1);box-shadow:0 1px 1px rgba(0,0,0,.1);text-shadow:0 1px 1px rgba(0,0,0,.1);border-radius:3px;cursor:move}.external-event:hover{-webkit-box-shadow:inset 0 0 90px rgba(0,0,0,.2);box-shadow:inset 0 0 90px rgba(0,0,0,.2)}.select2-container--default.select2-container--focus,.select2-container--default:active,.select2-container--default:focus,.select2-selection.select2-container--focus,.select2-selection:active,.select2-selection:focus{outline:0}.select2-container--default .select2-selection--single,.select2-selection .select2-selection--single{border:1px solid #d2d6de;border-radius:0;padding:6px 12px;height:34px}.select2-container--default.select2-container--open{border-color:#3c8dbc}.select2-dropdown{border:1px solid #d2d6de;border-radius:0}.select2-container--default .select2-results__option--highlighted[aria-selected]{background-color:#3c8dbc;color:#fff}.select2-results__option{padding:6px 12px;-moz-user-select:none;-ms-user-select:none;user-select:none;-webkit-user-select:none}.select2-container .select2-selection--single .select2-selection__rendered{padding-left:0;height:auto;margin-top:-4px}.select2-container[dir=rtl] .select2-selection--single .select2-selection__rendered{padding-right:6px;padding-left:20px}.select2-container--default .select2-selection--single .select2-selection__arrow{height:28px;right:3px}.select2-container--default .select2-selection--single .select2-selection__arrow b{margin-top:0}.select2-dropdown .select2-search__field,.select2-search--inline .select2-search__field{border:1px solid #d2d6de}.select2-dropdown .select2-search__field:focus,.select2-search--inline .select2-search__field:focus{outline:0;border:1px solid #3c8dbc}.select2-container--default .select2-results__option[aria-disabled=true]{color:#999}.select2-container--default .select2-results__option[aria-selected=true]{background-color:#ddd}.select2-container--default .select2-results__option[aria-selected=true],.select2-container--default .select2-results__option[aria-selected=true]:hover{color:#444}.select2-container--default .select2-selection--multiple{border:1px solid #d2d6de;border-radius:0}.select2-container--default .select2-selection--multiple:focus{border-color:#3c8dbc}.select2-container--default.select2-container--focus .select2-selection--multiple{border-color:#d2d6de}.select2-container--default .select2-selection--multiple .select2-selection__choice{background-color:#3c8dbc;border-color:#367fa9;padding:1px 10px;color:#fff}.select2-container--default .select2-selection--multiple .select2-selection__choice__remove{margin-right:5px;color:rgba(255,255,255,.7)}.select2-container--default .select2-selection--multiple .select2-selection__choice__remove:hover{color:#fff}.select2-container .select2-selection--single .select2-selection__rendered{padding-right:10px}.pad{padding:10px}.margin{margin:10px}.margin-bottom{margin-bottom:20px}.margin-bottom-none{margin-bottom:0}.margin-r-5{margin-right:5px}.inline{display:inline}.description-block{display:block;margin:10px 0;text-align:center}.description-block.margin-bottom{margin-bottom:25px}.description-block>.description-header{margin:0;padding:0;font-weight:600;font-size:16px}.list-header,.text-bold,.text-bold.table td,.text-bold.table th{font-weight:700}.alert-danger,.alert-error,.alert-info,.alert-success,.alert-warning,.bg-aqua,.bg-aqua-active,.bg-black,.bg-black-active,.bg-blue,.bg-blue-active,.bg-fuchsia,.bg-fuchsia-active,.bg-green,.bg-green-active,.bg-light-blue,.bg-light-blue-active,.bg-lime,.bg-lime-active,.bg-maroon,.bg-maroon-active,.bg-navy,.bg-navy-active,.bg-olive,.bg-olive-active,.bg-orange,.bg-orange-active,.bg-purple,.bg-purple-active,.bg-red,.bg-red-active,.bg-teal,.bg-teal-active,.bg-yellow,.bg-yellow-active,.callout.callout-danger,.callout.callout-info,.callout.callout-success,.callout.callout-warning,.label-danger,.label-info,.label-primary,.label-success,.label-warning,.modal-danger .modal-body,.modal-danger .modal-footer,.modal-danger .modal-header,.modal-info .modal-body,.modal-info .modal-footer,.modal-info .modal-header,.modal-primary .modal-body,.modal-primary .modal-footer,.modal-primary .modal-header,.modal-success .modal-body,.modal-success .modal-footer,.modal-success .modal-header,.modal-warning .modal-body,.modal-warning .modal-footer,.modal-warning .modal-header{color:#fff!important}.bg-gray{color:#000;background-color:#d2d6de!important}.bg-gray-light{background-color:#f7f7f7}.bg-black{background-color:#111!important}.alert-danger,.alert-error,.bg-red,.callout.callout-danger,.label-danger,.modal-danger .modal-body{background-color:#dd4b39!important}.alert-warning,.bg-yellow,.callout.callout-warning,.label-warning,.modal-warning .modal-body{background-color:#f39c12!important}.alert-info,.bg-aqua,.callout.callout-info,.label-info,.modal-info .modal-body{background-color:#00c0ef!important}.bg-blue{background-color:#0073b7!important}.bg-light-blue,.label-primary,.modal-primary .modal-body{background-color:#3c8dbc!important}.alert-success,.bg-green,.callout.callout-success,.label-success,.modal-success .modal-body{background-color:#00a65a!important}.bg-navy{background-color:#001f3f!important}.bg-teal{background-color:#39cccc!important}.bg-olive{background-color:#3d9970!important}.bg-lime{background-color:#01ff70!important}.bg-orange{background-color:#ff851b!important}.bg-fuchsia{background-color:#f012be!important}.bg-purple{background-color:#605ca8!important}.bg-maroon{background-color:#d81b60!important}.bg-gray-active{color:#000;background-color:#b5bbc8!important}.bg-black-active{background-color:#000!important}.bg-red-active,.modal-danger .modal-footer,.modal-danger .modal-header{background-color:#d33724!important}.bg-yellow-active,.modal-warning .modal-footer,.modal-warning .modal-header{background-color:#db8b0b!important}.bg-aqua-active,.modal-info .modal-footer,.modal-info .modal-header{background-color:#00a7d0!important}.bg-blue-active{background-color:#005384!important}.bg-light-blue-active,.modal-primary .modal-footer,.modal-primary .modal-header{background-color:#357ca5!important}.bg-green-active,.modal-success .modal-footer,.modal-success .modal-header{background-color:#008d4c!important}.bg-navy-active{background-color:#001a35!important}.bg-teal-active{background-color:#30bbbb!important}.bg-olive-active{background-color:#368763!important}.bg-lime-active{background-color:#00e765!important}.bg-orange-active{background-color:#ff7701!important}.bg-fuchsia-active{background-color:#db0ead!important}.bg-purple-active{background-color:#555299!important}.bg-maroon-active{background-color:#ca195a!important}[class^=bg-].disabled{opacity:.65;filter:alpha(opacity=65)}.text-red{color:#dd4b39!important}.text-yellow{color:#f39c12!important}.text-aqua{color:#00c0ef!important}.text-blue{color:#0073b7!important}.text-black{color:#111!important}.text-light-blue{color:#3c8dbc!important}.text-green{color:#00a65a!important}.text-gray{color:#d2d6de!important}.text-navy{color:#001f3f!important}.text-teal{color:#39cccc!important}.text-olive{color:#3d9970!important}.text-lime{color:#01ff70!important}.text-orange{color:#ff851b!important}.text-fuchsia{color:#f012be!important}.text-purple{color:#605ca8!important}.text-maroon{color:#d81b60!important}.link-muted{color:#7a869d}.link-muted:focus,.link-muted:hover{color:#606c84}.link-black{color:#666}.link-black:focus,.link-black:hover{color:#999}.hide{display:none!important}.no-border{border:0!important}.no-padding{padding:0!important}.no-margin{margin:0!important}.no-shadow{-webkit-box-shadow:none!important;box-shadow:none!important}.chart-legend,.contacts-list,.list-unstyled,.mailbox-attachments,.users-list{list-style:none;margin:0;padding:0}.list-group-unbordered>.list-group-item{border-left:0;border-right:0;border-radius:0;padding-left:0;padding-right:0}.flat{border-radius:0!important}.text-sm{font-size:12px}.jqstooltip{padding:5px!important;width:auto!important;height:auto!important}.bg-teal-gradient{background:#39cccc!important;background:-webkit-gradient(linear,left bottom,left top,color-stop(0,#39cccc),color-stop(1,#7adddd))!important;background:-o-linear-gradient(#7adddd,#39cccc)!important;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#7adddd', endColorstr='#39cccc', GradientType=0)!important;color:#fff}.bg-light-blue-gradient{background:#3c8dbc!important;background:-webkit-gradient(linear,left bottom,left top,color-stop(0,#3c8dbc),color-stop(1,#67a8ce))!important;background:-o-linear-gradient(#67a8ce,#3c8dbc)!important;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#67a8ce', endColorstr='#3c8dbc', GradientType=0)!important;color:#fff}.bg-blue-gradient{background:#0073b7!important;background:-webkit-gradient(linear,left bottom,left top,color-stop(0,#0073b7),color-stop(1,#0089db))!important;background:-o-linear-gradient(#0089db,#0073b7)!important;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#0089db', endColorstr='#0073b7', GradientType=0)!important;color:#fff}.bg-aqua-gradient{background:#00c0ef!important;background:-webkit-gradient(linear,left bottom,left top,color-stop(0,#00c0ef),color-stop(1,#14d1ff))!important;background:-o-linear-gradient(#14d1ff,#00c0ef)!important;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#14d1ff', endColorstr='#00c0ef', GradientType=0)!important;color:#fff}.bg-yellow-gradient{background:#f39c12!important;background:-webkit-gradient(linear,left bottom,left top,color-stop(0,#f39c12),color-stop(1,#f7bc60))!important;background:-o-linear-gradient(#f7bc60,#f39c12)!important;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#f7bc60', endColorstr='#f39c12', GradientType=0)!important;color:#fff}.bg-purple-gradient{background:#605ca8!important;background:-webkit-gradient(linear,left bottom,left top,color-stop(0,#605ca8),color-stop(1,#9491c4))!important;background:-o-linear-gradient(#9491c4,#605ca8)!important;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#9491c4', endColorstr='#605ca8', GradientType=0)!important;color:#fff}.bg-green-gradient{background:#00a65a!important;background:-webkit-gradient(linear,left bottom,left top,color-stop(0,#00a65a),color-stop(1,#00ca6d))!important;background:-o-linear-gradient(#00ca6d,#00a65a)!important;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#00ca6d', endColorstr='#00a65a', GradientType=0)!important;color:#fff}.bg-red-gradient{background:#dd4b39!important;background:-webkit-gradient(linear,left bottom,left top,color-stop(0,#dd4b39),color-stop(1,#e47365))!important;background:-o-linear-gradient(#e47365,#dd4b39)!important;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#e47365', endColorstr='#dd4b39', GradientType=0)!important;color:#fff}.bg-black-gradient{background:#111!important;background:-webkit-gradient(linear,left bottom,left top,color-stop(0,#111),color-stop(1,#2b2b2b))!important;background:-o-linear-gradient(#2b2b2b,#111)!important;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#2b2b2b', endColorstr='#111111', GradientType=0)!important;color:#fff}.bg-maroon-gradient{background:#d81b60!important;background:-webkit-gradient(linear,left bottom,left top,color-stop(0,#d81b60),color-stop(1,#e73f7c))!important;background:-o-linear-gradient(#e73f7c,#d81b60)!important;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#e73f7c', endColorstr='#d81b60', GradientType=0)!important;color:#fff}.description-block .description-icon{font-size:16px}.no-pad-top{padding-top:0}.position-static{position:static!important}.list-header{font-size:15px;padding:10px 4px;color:#666}.list-seperator{height:1px;background:#f4f4f4;margin:15px 0 9px}.list-link>a{padding:4px;color:#777}.list-link>a:hover{color:#222}.font-light{font-weight:300}.user-block:after,.user-block:before{content:" ";display:table}.user-block img{width:40px;height:40px;float:left}.user-block .comment,.user-block .description,.user-block .username{display:block;margin-left:50px}.img-sm+.img-push,.user-block.user-block-sm .comment,.user-block.user-block-sm .description,.user-block.user-block-sm .username{margin-left:40px}.user-block .username{font-size:16px;font-weight:600}.user-block .description{color:#999;font-size:13px}.user-block.user-block-sm .username{font-size:14px}.box-comments .box-comment img,.img-lg,.img-md,.img-sm,.user-block.user-block-sm img{float:left}.box-comments .box-comment img,.img-sm,.user-block.user-block-sm img{width:30px!important;height:30px!important}.img-md{width:60px;height:60px}.img-md+.img-push{margin-left:70px}.attachment-block .attachment-pushed,.img-lg+.img-push{margin-left:110px}.img-lg{width:100px;height:100px}.img-bordered{border:3px solid #d2d6de;padding:3px}.img-bordered-sm{border:2px solid #d2d6de;padding:2px}.attachment-block{border:1px solid #f4f4f4;padding:5px;margin-bottom:10px;background:#f7f7f7}.attachment-block .attachment-img{max-width:100px;max-height:100px;height:auto;float:left}.panel-default>.panel-heading+.panel-collapse>.panel-body img,.select2-container,.select2-drop-active{max-width:100%}.attachment-block .attachment-heading{margin:0}.attachment-block .attachment-text{color:#555}.skin-black .main-header .navbar .nav>li>a,.skin-black .main-header .navbar-toggle{color:#333}.connectedSortable{min-height:100px}.ui-helper-hidden-accessible{border:0;clip:rect(0 0 0 0);height:1px;margin:-1px;overflow:hidden;padding:0;position:absolute;width:1px}.sort-highlight{background:#f4f4f4;border:1px dashed #ddd;margin-bottom:10px}.full-opacity-hover{opacity:.65;filter:alpha(opacity=65)}.full-opacity-hover:hover{opacity:1;filter:alpha(opacity=100)}.chart{position:relative;overflow:hidden;width:100%}.chart canvas,.chart svg{width:100%!important}@media print{.content-header,.left-side,.main-header,.main-sidebar,.no-print{display:none!important}.content-wrapper,.main-footer,.right-side{margin-left:0!important;min-height:0!important;-webkit-transform:translate(0,0)!important;-ms-transform:translate(0,0)!important;-o-transform:translate(0,0)!important;transform:translate(0,0)!important}.fixed .content-wrapper,.fixed .right-side{padding-top:0!important}.invoice{width:100%;border:0;margin:0;padding:0}.invoice-col{float:left;width:33.3333333%}.table-responsive{overflow:auto}.table-responsive>.table tr td,.table-responsive>.table tr th{white-space:normal!important}}.skin-black .main-header{-webkit-box-shadow:0 1px 1px rgba(0,0,0,.05);box-shadow:0 1px 1px rgba(0,0,0,.05)}.skin-black .main-header .navbar-brand{color:#333;border-right:1px solid #eee}.skin-black .main-header .navbar{background-color:#fff}.skin-black .main-header .navbar .nav .open>a,.skin-black .main-header .navbar .nav .open>a:focus,.skin-black .main-header .navbar .nav .open>a:hover,.skin-black .main-header .navbar .nav>.active>a,.skin-black .main-header .navbar .nav>li>a:active,.skin-black .main-header .navbar .nav>li>a:focus,.skin-black .main-header .navbar .nav>li>a:hover{background:#fff;color:#999}.skin-black .main-header .navbar .sidebar-toggle{color:#333}.skin-black .main-header .navbar .sidebar-toggle:hover{color:#999;background:#fff}.skin-black .main-header .navbar>.sidebar-toggle{color:#333;border-right:1px solid #eee}.skin-black .main-header .navbar .navbar-nav>li>a{border-right:1px solid #eee}.skin-black .main-header .navbar .navbar-custom-menu .navbar-nav>li>a,.skin-black .main-header .navbar .navbar-right>li>a{border-left:1px solid #eee;border-right-width:0}.skin-black .main-header>.logo{background-color:#fff;color:#333;border-bottom:0 solid transparent;border-right:1px solid #eee}.skin-black .main-header>.logo:hover{background-color:#fcfcfc}@media (max-width:767px){.skin-black .main-header>.logo{background-color:#222;color:#fff;border-bottom:0 solid transparent;border-right:none}.skin-black .main-header>.logo:hover{background-color:#1f1f1f}}.skin-black .main-header li.user-header{background-color:#222}.skin-black .content-header{background:0 0;-webkit-box-shadow:none;box-shadow:none}.skin-black .left-side,.skin-black .main-sidebar,.skin-black .wrapper{background-color:#222d32}.skin-black .user-panel>.info,.skin-black .user-panel>.info>a{color:#fff}.skin-black .sidebar-menu>li.header{color:#4b646f;background:#1a2226}.skin-black .sidebar-menu>li>a{border-left:3px solid transparent}.skin-black .sidebar-menu>li.active>a,.skin-black .sidebar-menu>li:hover>a{color:#fff;background:#1e282c;border-left-color:#fff}.skin-black .sidebar-menu>li>.treeview-menu{margin:0 1px;background:#2c3b41}.skin-black .sidebar a{color:#b8c7ce}.skin-black .treeview-menu>li>a{color:#8aa4af}.skin-black .treeview-menu>li.active>a,.skin-black .treeview-menu>li>a:hover{color:#fff}.skin-black .sidebar-form{border-radius:3px;border:1px solid #374850;margin:10px}.skin-black .sidebar-form .btn,.skin-black .sidebar-form input[type=text]{-webkit-box-shadow:none;box-shadow:none;background-color:#374850;border:1px solid transparent;height:35px}.skin-black .sidebar-form input[type=text]{color:#666;border-top-left-radius:2px;border-top-right-radius:0;border-bottom-right-radius:0;border-bottom-left-radius:2px}.skin-black .sidebar-form input[type=text]:focus,.skin-black .sidebar-form input[type=text]:focus+.input-group-btn .btn{background-color:#fff;color:#666}.skin-black .sidebar-form input[type=text]:focus+.input-group-btn .btn{border-left-color:#fff}.skin-black .sidebar-form .btn{color:#999;border-top-left-radius:0;border-top-right-radius:2px;border-bottom-right-radius:2px;border-bottom-left-radius:0}.skin-black .pace .pace-progress{background:#222}.skin-black .pace .pace-activity{border-top-color:#222;border-left-color:#222}input:focus{outline:0}#login .content-wrapper,#login .main-footer,#login .right-side{margin-left:0}#login .content-wrapper{background-color:#222d32}#login .content-wrapper .content{width:500px;max-width:100%;margin:0 auto;padding-top:5%}.treeview-header a{border-bottom:1px solid #efefef}.dashboard-link{display:block;background:#fff;overflow:hidden;color:#555;text-align:center;padding:25px;width:99%;margin:0 auto;margin-bottom:15px}.dashboard-link span{display:block}.dashboard-link span.icon i{font-size:6em}.dashboard-link span.text{font-size:22px;font-weight:700;margin-top:20px}.dashboard-small-links i{font-size:16px}.dashboard-small-link{background:#fff;padding:6px 8px;border:1px solid #efefef;margin-right:5px;margin-bottom:5px;border-radius:2px;display:inline-block;font-size:14px}.dashboard-small-link:hover{color:#555;background:#e1e1e1}#show-items .modal-dialog{width:99%;margin-top:2px;margin-left:2px}.thick-border-top{border-top:2px solid gray}.pagination{padding-left:15px}.btn-danger{background:0 0;color:#d73925}.edit-run-header h5{display:inline-block}#run-unit-choices{display:block;margin-top:15px}.run_unit_inner{-webkit-box-shadow:0 4px 4px -2px #efefef;box-shadow:0 4px 4px -2px #efefef}.run_unit_inner.email label{display:block}.run_unit_inner.email .form-control{width:80%}.run_unit_position{text-align:center;padding-bottom:10px}.run_unit_inner .run_unit_description,.run_unit_position input{border:0;-webkit-box-shadow:none;box-shadow:none;font-weight:700;font-size:45px;height:65px;width:100px;background-color:transparent;text-align:center;border-bottom:1px solid #efefef}.run_unit_inner .run_unit_description{font-size:20px;width:100%;height:25px;background-color:#fff;border:0;text-align:left}.run_unit_inner .run_unit_dialog .form-group{margin:0;display:block;margin-bottom:4px}.badge-info{background-color:#3a87ad}.badge-success{background-color:#468847}.muted{color:#DDD}.skin-black .wrapper{background:#fff}tr.thick_border_top{border-top:2px solid #efefef}.log-entry .panel-content{padding:15px}.bulk-actions-ba form{border:1px solid #efefef;padding:5px;margin-top:15px}.has-actions .input-group .position_monkey{width:70px;padding:0}.readonly-textarea{border:1px solid #efefef;background:0 0;color:#555;width:70%;padding:8px}.dropzone{border:2px solid #efefef;padding:20px 15px}.lock-toggle.btn-checked{background-color:#e7e7e7}.well-sm{width:85%}.select2-container .select2-choice,.select2-dropdown-open .select2-choice{border-radius:0;background-image:none;background-color:#fff;height:30px}.select2-container.full_width{width:80%}.select2-container .select2-choice .select2-arrow{background-image:none;background:0 0;width:18px}.select2-results{padding:0 4px}.select2-results .select2-highlighted{background-color:#ddd;color:#000;font-weight:400}.select2-container-active .select2-choice,.select2-container-active .select2-choices,.select2-drop-active{border:1px solid #ddd}.modal-header .close{margin-top:5px}a.btn.btn-checked,button.btn.btn-checked,label.btn.btn-checked{background-color:#c3c3c3;z-index:2;border:1px solid #000;outline:1px;-webkit-box-shadow:inset 0 2px 4px rgba(0,0,0,.15),0 1px 2px rgba(0,0,0,.05);box-shadow:inset 0 2px 4px rgba(0,0,0,.15),0 1px 2px rgba(0,0,0,.05)}.panel-body .rmarkdown_iframe{position:static;width:100%;height:500px}.panel-body .rmarkdown_iframe iframe{border:0;width:100%;height:100%;height:100vh}.alert .panel-group.opencpu_accordion a{color:green;text-decoration:none} \ No newline at end of file From c0ae33374b256f55acf0c686044f976d3c4df6c1 Mon Sep 17 00:00:00 2001 From: Cyril Tata Date: Sun, 24 Mar 2019 09:27:53 +0100 Subject: [PATCH 039/352] bug fix - missing variable --- application/Model/Survey.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/application/Model/Survey.php b/application/Model/Survey.php index 206c31ea5..49903a6c4 100644 --- a/application/Model/Survey.php +++ b/application/Model/Survey.php @@ -2151,8 +2151,8 @@ public function displayForRun($prepend = '') { 'survey' => $this, 'studies' => $this->dbh->select('id, name')->from('survey_studies')->where(array('user_id' => Site::getCurrentUser()->id))->fetchAll(), 'prepend' => $prepend, - 'resultCount' => $this->howManyReachedItNumbers(), - 'time' => $this->getAverageTimeItTakes(), + 'resultCount' => $this->id ? $this->howManyReachedItNumbers() : null, + 'time' => $this->id ? $this->getAverageTimeItTakes() : null, )); return parent::runDialog($dialog); From 42d5ab427389ac502e0cd7ff8eee976e615b9073 Mon Sep 17 00:00:00 2001 From: Cyril Tata Date: Sun, 24 Mar 2019 11:32:50 +0100 Subject: [PATCH 040/352] expire Pause a day after expiration hour passed for current day --- application/Model/Pause.php | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/application/Model/Pause.php b/application/Model/Pause.php index 8af2b65d9..e0d6e19d1 100644 --- a/application/Model/Pause.php +++ b/application/Model/Pause.php @@ -191,7 +191,13 @@ protected function checkWhetherPauseIsOver() { if (!empty($wait_date) && !empty($wait_time)) { $wait_datetime = $wait_date . ' ' . $wait_time; $this->execData['expire_timestamp'] = strtotime($wait_datetime); - + + // If the expiration hour already passed before the user entered the pause, set expiration to the next day (in 24 hours) + if (date('G') > date('G', $this->execData['expire_timestamp'])) { + $this->execData['expire_timestamp'] += 24 * 60 * 60; //strtotime('+1 day', $this->execData['expire_timestamp']); + return false; + } +/* // Check if this unit already expired today for current run_session_id $q = ' SELECT 1 AS finished FROM `survey_unit_sessions` @@ -206,6 +212,7 @@ protected function checkWhetherPauseIsOver() { $this->execData['expire_timestamp'] = strtotime('+1 day', $this->execData['expire_timestamp']); return false; } +*/ $conditions['datetime'] = ':wait_datetime <= NOW()'; } From e2737f3ba37e26b5ae226c4a00b55af65ce5deb7 Mon Sep 17 00:00:00 2001 From: Cyril Tata Date: Sun, 24 Mar 2019 12:45:39 +0100 Subject: [PATCH 041/352] use hour and minute for pause expiry check --- application/Model/Pause.php | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/application/Model/Pause.php b/application/Model/Pause.php index e0d6e19d1..9db15e796 100644 --- a/application/Model/Pause.php +++ b/application/Model/Pause.php @@ -193,7 +193,9 @@ protected function checkWhetherPauseIsOver() { $this->execData['expire_timestamp'] = strtotime($wait_datetime); // If the expiration hour already passed before the user entered the pause, set expiration to the next day (in 24 hours) - if (date('G') > date('G', $this->execData['expire_timestamp'])) { + $exp_ts = $this->execData['expire_timestamp']; + $exp_hour_min = mktime(date('G', $exp_ts), date('i', $exp_ts), 0); + if (time() > $exp_hour_min) { $this->execData['expire_timestamp'] += 24 * 60 * 60; //strtotime('+1 day', $this->execData['expire_timestamp']); return false; } From 7625bc8e895e91257f703f51c3fafa483348f5b2 Mon Sep 17 00:00:00 2001 From: Cyril Tata Date: Sun, 24 Mar 2019 17:35:56 +0100 Subject: [PATCH 042/352] compute expiration timestamps for External and Pause units --- application/Helper/RunUnitHelper.php | 8 +++++-- application/Model/Branch.php | 6 ++++++ application/Model/External.php | 18 ++++++++++------ application/Model/Pause.php | 32 +++++++++++++++++++++++++--- 4 files changed, 53 insertions(+), 11 deletions(-) diff --git a/application/Helper/RunUnitHelper.php b/application/Helper/RunUnitHelper.php index 3fff3231e..0ad3d0e00 100644 --- a/application/Helper/RunUnitHelper.php +++ b/application/Helper/RunUnitHelper.php @@ -57,7 +57,9 @@ public function getUnitSessionExpiration(UnitSession $unitSession, RunUnit $runU * @return int */ public function getExternalExpiration(UnitSession $unitSession, External $runUnit, $execResults) { - if ($execResults === true) { + if (!empty($runUnit->execData['expire_timestamp'])) { + return $runUnit->execData['expire_timestamp']; + } elseif ($execResults === true) { // set expiration to x minutes for unit session to be executed again return strtotime($this->expiration_extension); } @@ -176,7 +178,9 @@ public function getPauseExpiration(UnitSession $unitSession, Pause $runUnit, $ex * @return int */ public function getBranchExpiration(UnitSession $unitSession, Branch $runUnit, $execResults) { - if ($execResults === true) { + if (!empty($runUnit->execData['expire_timestamp'])) { + return (int)$runUnit->execData['expire_timestamp']; + } elseif ($execResults === true) { // set expiration to x minutes for unit session to be executed again return strtotime($this->expiration_extension); } diff --git a/application/Model/Branch.php b/application/Model/Branch.php index 4c690ca84..1128ebe4f 100644 --- a/application/Model/Branch.php +++ b/application/Model/Branch.php @@ -128,6 +128,12 @@ public function exec() { return true; // don't go anywhere, wait for the error to be fixed! } + // If execution returned a timestamp greater that now() queue it + if (($time = strtotime($eval)) && $time > time()) { + $this->execData['expire_timestamp'] = $time; + return true; + } + $result = (bool)$eval; // if condition is true and we're set to jump automatically, or if the user reacted if ($result && ($this->automatically_jump || !$this->called_by_cron)): diff --git a/application/Model/External.php b/application/Model/External.php index e09a31962..3b27b44bd 100644 --- a/application/Model/External.php +++ b/application/Model/External.php @@ -115,24 +115,30 @@ public function test() { } private function hasExpired() { - $expire = $this->expire_after; + $expire = (int)$this->expire_after; if ($expire === 0) { return false; } else { $last = $this->run_session->unit_session->created; - if (!$last) { + if (!$last || !strtotime($last)) { + return false; + } + + $expire_ts = strtotime($last) + ($expire * 60); + if (($expired = $expire_ts < time())) { + return true; + } else { + $this->execData['expire_timestamp'] = $expire_ts; return false; } - $query = 'SELECT :last <= DATE_SUB(NOW(), INTERVAL :expire_after MINUTE) AS no_longer_active'; - $params = array('last' => $last, 'expire_after' => $expire); - return (bool)$this->dbh->execute($query, $params, true); } } public function exec() { // never redirect, if we're just in the cronjob. just text for expiry + $expired = $this->hasExpired(); if ($this->called_by_cron) { - if ($this->hasExpired()) { + if ($expired) { $this->expire(); return false; } diff --git a/application/Model/Pause.php b/application/Model/Pause.php index 9db15e796..765d33b23 100644 --- a/application/Model/Pause.php +++ b/application/Model/Pause.php @@ -155,6 +155,11 @@ protected function checkWhetherPauseIsOver() { $conditions['relative_to'] = ':relative_to <= NOW()'; $bind_relative_to = true; $this->execData['expire_timestamp'] = strtotime($relative_to); + // If there was a wait_time, set the timestamp to have this time + if ($time = $this->parseWaitTime(true)) { + $ts = $this->execData['expire_timestamp']; + $this->execData['expire_timestamp'] = mktime($time[0], $time[1], 0, date('m', $ts), date('d', $ts), date('Y', $ts)); + } } else { alert("Pause {$this->position}: Relative to yields neither true nor false, nor a date, nor a time. " . print_r($relative_to, true), 'alert-warning'); $this->execData['check_failed'] = true; @@ -180,6 +185,9 @@ protected function checkWhetherPauseIsOver() { $wait_time = $this->wait_until_time; } + $wait_date = $this->parseWaitDate(); + $wait_time = $this->parseWaitTime(); + if (!empty($wait_date) && empty($wait_time)) { $wait_time = '00:00:01'; } @@ -188,7 +196,7 @@ protected function checkWhetherPauseIsOver() { $wait_date = date('Y-m-d'); } - if (!empty($wait_date) && !empty($wait_time)) { + if (!empty($wait_date) && !empty($wait_time) && empty($this->execData['expire_timestamp'])) { $wait_datetime = $wait_date . ' ' . $wait_time; $this->execData['expire_timestamp'] = strtotime($wait_datetime); @@ -196,7 +204,7 @@ protected function checkWhetherPauseIsOver() { $exp_ts = $this->execData['expire_timestamp']; $exp_hour_min = mktime(date('G', $exp_ts), date('i', $exp_ts), 0); if (time() > $exp_hour_min) { - $this->execData['expire_timestamp'] += 24 * 60 * 60; //strtotime('+1 day', $this->execData['expire_timestamp']); + $this->execData['expire_timestamp'] += 24 * 60 * 60; return false; } /* @@ -214,11 +222,13 @@ protected function checkWhetherPauseIsOver() { $this->execData['expire_timestamp'] = strtotime('+1 day', $this->execData['expire_timestamp']); return false; } -*/ +*/ $conditions['datetime'] = ':wait_datetime <= NOW()'; } + $result = !empty($this->execData['expire_timestamp']) && $this->execData['expire_timestamp'] <= time(); + if ($conditions) { $condition = implode(' AND ', $conditions); $stmt = $this->dbh->prepare("SELECT {$condition} AS test LIMIT 1"); @@ -244,6 +254,22 @@ protected function checkWhetherPauseIsOver() { return $result; } + protected function parseWaitTime($parts = false) { + if ($this->wait_until_time && $this->wait_until_time != '00:00:00') { + return $parts ? explode(':', $this->wait_until_time) : $this->wait_until_time; + } + + return null; + } + + protected function parseWaitDate($parts = false) { + if ($this->wait_until_date && $this->wait_until_date != '0000-00-00') { + return $parts ? explode('-', $this->wait_until_date) : $this->wait_until_date; + } + + return null; + } + public function test() { if (!$this->knittingNeeded($this->body)) { echo "

Pause message

"; From 76259e10a35460e256da9a7a8f5932e56d32164d Mon Sep 17 00:00:00 2001 From: Cyril Tata Date: Mon, 25 Mar 2019 11:03:27 +0100 Subject: [PATCH 043/352] use created timestamp of unit session to determine if to expire the next day --- application/Model/Pause.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/application/Model/Pause.php b/application/Model/Pause.php index 765d33b23..fa416a803 100644 --- a/application/Model/Pause.php +++ b/application/Model/Pause.php @@ -202,8 +202,9 @@ protected function checkWhetherPauseIsOver() { // If the expiration hour already passed before the user entered the pause, set expiration to the next day (in 24 hours) $exp_ts = $this->execData['expire_timestamp']; + $created_ts = strtotime($this->run_session->unit_session->created); $exp_hour_min = mktime(date('G', $exp_ts), date('i', $exp_ts), 0); - if (time() > $exp_hour_min) { + if ($created_ts > $exp_hour_min) { $this->execData['expire_timestamp'] += 24 * 60 * 60; return false; } From 85d77ac1c985557100931af703ce41660f45c7ed Mon Sep 17 00:00:00 2001 From: Cyril Tata Date: Mon, 25 Mar 2019 11:59:16 +0100 Subject: [PATCH 044/352] templatize run unit tests output --- application/Model/Branch.php | 40 +++++++++++------ application/Model/Email.php | 51 ++++++++++++---------- application/Model/External.php | 2 +- application/Model/Page.php | 6 +-- application/Model/Pause.php | 79 +++++++++++++++++++--------------- application/Model/Shuffle.php | 24 +++++++---- 6 files changed, 118 insertions(+), 84 deletions(-) diff --git a/application/Model/Branch.php b/application/Model/Branch.php index 1128ebe4f..f6a9efc4a 100644 --- a/application/Model/Branch.php +++ b/application/Model/Branch.php @@ -87,37 +87,49 @@ public function removeFromRun($special = null) { public function test() { $results = $this->getSampleSessions(); - if (!$results) { return false; } + $test_tpl = ' +
Run position Session Created
+ + + + + + %{rows} + +
Code (Position)Test
+ '; + + $row_tpl = ' + + %{session} (%{position}) + %{result} + + '; + $this->run_session_id = current($results)['id']; $opencpu_vars = $this->getUserDataInRun($this->condition); $ocpu_session = opencpu_evaluate($this->condition, $opencpu_vars, 'text', null, true); echo opencpu_debug($ocpu_session, null, 'text'); - echo ' - - - - - "'; - // Maybe there is a way that we prevent 'calling opencpu' in a loop by gathering what is needed to be evaluated // at opencpu in some 'box' and sending one request (also create new func in formr R package to open this box, evaluate what is inside and return the box) + $rows = ''; foreach ($results as $row) { $this->run_session_id = $row['id']; $opencpu_vars = $this->getUserDataInRun($this->condition); $eval = opencpu_evaluate($this->condition, $opencpu_vars); - - echo " - - - "; + $rows .= Template::replace($row_tpl, array( + 'session' => $row['session'], + 'position' => $row['position'], + 'result' => stringBool($eval), + )); } - echo '
Code (Position)Test
" . $row['session'] . " ({$row['position']})" . stringBool($eval) . "
'; + echo Template::replace($test_tpl, array('rows' => $rows)); $this->run_session_id = null; } diff --git a/application/Model/Email.php b/application/Model/Email.php index eb362910a..29c539eff 100644 --- a/application/Model/Email.php +++ b/application/Model/Email.php @@ -385,56 +385,63 @@ public function test() { } else { echo $this->mostrecent . ": " . $recipient_field; } + echo "

Subject

"; - if($this->knittingNeeded($this->subject)): + if($this->knittingNeeded($this->subject)) { echo $this->getParsedTextAdmin($this->subject); - else: + } else { echo $this->getSubject(); - endif; - echo "

Body

"; + } + echo "

Body

"; echo $this->getParsedBodyAdmin($this->body); echo "

Attempt to send email

"; - - if($this->sendMail($receiver)): + if($this->sendMail($receiver)) { echo "

An email was sent to your own email address (". h($receiver). ").

"; - else: + } else { echo "

No email sent.

"; - endif; + } $results = $this->getSampleSessions(); if ($results) { - if ($this->recipient_field === null OR trim($this->recipient_field) == '') { $this->recipient_field = 'survey_users$email'; } - $output = ' + $test_tpl = ' + %{rows} - %s -
Code (Position) Test
'; + + '; + + $row_tpl = ' + + %{session} (%{position}) + %{result} + + '; $rows = ''; - foreach ($results AS $row): + foreach ($results as $row) { $this->run_session_id = $row['id']; - $email = stringBool($this->getRecipientField()); - $good = filter_var($email, FILTER_VALIDATE_EMAIL) ? '' : 'text-warning'; - $rows .= " - - " . $row['session'] . " ({$row['position']}) - " . $email . " - "; - endforeach; + $class = filter_var($email, FILTER_VALIDATE_EMAIL) ? '' : 'text-warning'; + $rows .= Template::replace($row_tpl, array( + 'session' => $row['session'], + 'position' => $row['position'], + 'result' => stringBool($email), + 'class' => $class, + )); + } - echo sprintf($output, $rows); + echo Template::replace($test_tpl, array('rows' => $rows)); } $this->run_session_id = null; } diff --git a/application/Model/External.php b/application/Model/External.php index 3b27b44bd..12577a288 100644 --- a/application/Model/External.php +++ b/application/Model/External.php @@ -107,7 +107,7 @@ public function test() { $output = ''; } } else { - $output = ''.$this->address.""; + $output = Template::replace('%{address}', array('address' => $this->address)); } $this->session = "TESTCODE"; diff --git a/application/Model/Page.php b/application/Model/Page.php index 888c7cb4c..6f2a6e681 100644 --- a/application/Model/Page.php +++ b/application/Model/Page.php @@ -91,10 +91,10 @@ public function test() { } public function exec() { - if ($this->called_by_cron): + if ($this->called_by_cron) { $this->getParsedBody($this->body); // make report before showing it to the user, so they don't have to wait return true; // never show to the cronjob - endif; + } $run_name = $sess_code = null; if ($this->run_session) { @@ -109,8 +109,8 @@ public function exec() { } $body = do_run_shortcodes($this->body_parsed, $run_name, $sess_code); + return array( -// 'title' => $this->title, 'body' => $body, ); } diff --git a/application/Model/Pause.php b/application/Model/Pause.php index fa416a803..c6261fef2 100644 --- a/application/Model/Pause.php +++ b/application/Model/Pause.php @@ -272,56 +272,65 @@ protected function parseWaitDate($parts = false) { } public function test() { - if (!$this->knittingNeeded($this->body)) { - echo "

Pause message

"; - echo $this->getParsedBodyAdmin($this->body); - } - $results = $this->getSampleSessions(); if (!$results) { return false; } - if ($this->knittingNeeded($this->body)) { - echo "

Pause message

"; - echo $this->getParsedBodyAdmin($this->body); - } + + // take the first sample session + $sess = current($results); + $this->run_session_id = $sess['id']; + + + echo "

Pause message

"; + echo $this->getParsedBodyAdmin($this->body); + if ($this->checkRelativeTo()) { - // take the first sample session - $this->run_session_id = current($results)['id']; echo "

Pause relative to

"; - $opencpu_vars = $this->getUserDataInRun($this->relative_to); $session = opencpu_evaluate($this->relative_to, $opencpu_vars, 'json', null, true); - echo opencpu_debug($session); } if (!empty($results) && (empty($session) || !$session->hasError())) { + + $test_tpl = ' + + + + + + + + %{rows} + +
CodeRelative toOver
+ '; + + $row_tpl = ' + + %{session} (%{position}) + %{relative_to} + %{pause_over} + + '; - echo ' - - '; - if ($this->has_relative_to) { - echo ''; - } - echo ' - - '; - - foreach ($results AS $row): + $rows = ''; + foreach ($results as $row) { $this->run_session_id = $row['id']; + $runSession = new RunSession($this->dbh, $this->run->id, Site::getCurrentUser()->id, $row['session'], $this->run); + $runSession->unit_session = new UnitSession($this->dbh, $this->run_session_id, $this->id); + $this->run_session = $runSession; + + $rows .= Template::replace($row_tpl, array( + 'session' => $row['session'], + 'position' => $row['position'], + 'relative_to' => stringBool($this->relative_to_result), + 'pause_over' => stringBool($this->checkWhetherPauseIsOver()), + )); + } - $result = $this->checkWhetherPauseIsOver(); - echo " - "; - if ($this->has_relative_to) { - echo ""; - } - echo " - "; - - endforeach; - echo '
CodeRelative toWait?
" . $row['session'] . " ({$row['position']})" . stringBool($this->relative_to_result) . "" . stringBool($result) . "
'; + echo Template::replace($test_tpl, array('rows' => $rows)); } } diff --git a/application/Model/Shuffle.php b/application/Model/Shuffle.php index 71705c786..a165e052c 100644 --- a/application/Model/Shuffle.php +++ b/application/Model/Shuffle.php @@ -70,16 +70,22 @@ public function randomise_into_group() { } public function test() { + $test_tpl = ' +

Randomisation

+

We just generated fifty random group assignments:

+
%{groups}
+

Remember that we start counting at one (1), so if you have two groups you will check shuffle$group == 1 and shuffle$group == 2. + You can read a person\'s group using shuffle$group. + If you generate more than one random group in a run, you might have to use the last one tail(shuffle$group,1), + but usually you shouldn\'t do this.

+ '; + + $groups = ''; + for ($i = 0; $i < 50; $i++) { + $groups .= $this->randomise_into_group() . '  '; + } - echo '

Randomisation

-

We just generated fifty random group assignments:

'; - for ($i = 0; $i < 50; $i++): - echo $this->randomise_into_group() . '  '; - endfor; - echo '

Remember that we start counting at one (1), so if you have two groups you will check shuffle$group == 1 and shuffle$group == 2. You can read a person\'s - group using shuffle$group. If you generate more than one - random group in a run, you might have to use the last one tail(shuffle$group,1), but - usually you shouldn\'t do this.

'; + echo Template::replace($test_tpl, array('groups' => $groups)); } public function exec() { From 4257ab1e3aa3081b27504a32df1063874561bd5f Mon Sep 17 00:00:00 2001 From: Cyril Tata Date: Mon, 25 Mar 2019 12:24:16 +0100 Subject: [PATCH 045/352] Remove item from queue if a session could not be found --- application/Library/UnitSessionQueue.php | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/application/Library/UnitSessionQueue.php b/application/Library/UnitSessionQueue.php index eeb82496e..d7cbf26da 100644 --- a/application/Library/UnitSessionQueue.php +++ b/application/Library/UnitSessionQueue.php @@ -95,9 +95,11 @@ protected function processQueue() { while ($session = $sessionsStmt->fetch(PDO::FETCH_ASSOC)) { if (!$session['session']) { + $this->dbg('A session could not be found for item in queue: ' . print_r($session, 1)); + self::removeItem($session['unit_session_id'], $session['unit_id']); continue; } - + $run = $this->getRun($session['run']); $runSession = new RunSession($this->db, $run->id, 'cron', $session['session'], $run); From 1007df6104175e06b3dff382c7465014fd86b95d Mon Sep 17 00:00:00 2001 From: Cyril Tata Date: Mon, 25 Mar 2019 16:26:17 +0100 Subject: [PATCH 046/352] php autoformatting --- .../Controller/AdminAjaxController.php | 1018 ++-- application/Controller/AdminController.php | 280 +- .../Controller/AdminMailController.php | 124 +- application/Controller/AdminRunController.php | 1495 +++--- .../Controller/AdminSurveyController.php | 910 ++-- application/Controller/ApiController.php | 416 +- application/Controller/Controller.php | 357 +- application/Controller/PublicController.php | 438 +- application/Controller/RunController.php | 505 +- .../Controller/SuperadminController.php | 539 +-- application/Helper/ApiHelper.php | 656 +-- application/Helper/GearmanWorkerHelper.php | 469 +- application/Helper/OAuthHelper.php | 270 +- application/Helper/RunHelper.php | 252 +- application/Helper/RunUnitHelper.php | 396 +- application/Helper/SurveyHelper.php | 1178 +++-- application/Library/AnimalName.php | 851 ++-- application/Library/Autoloader.php | 173 +- application/Library/CURL.php | 764 +-- application/Library/Cache.php | 139 +- application/Library/Config.php | 5 +- application/Library/Cookie.php | 333 +- application/Library/Crypto.php | 149 +- application/Library/DB.php | 1700 +++---- application/Library/Deamon.php | 440 +- application/Library/EmailQueue.php | 471 +- application/Library/Functions.php | 2264 +++++---- application/Library/LogParser.php | 169 +- application/Library/OSF.php | 742 ++- application/Library/OpenCPU.php | 1012 ++-- application/Library/Queue.php | 244 +- application/Library/Request.php | 434 +- application/Library/Response.php | 544 +-- application/Library/Router.php | 311 +- application/Library/Session.php | 183 +- application/Library/Template.php | 100 +- application/Library/UnitSessionQueue.php | 342 +- application/Model/Branch.php | 282 +- application/Model/Email.php | 877 ++-- application/Model/EmailAccount.php | 250 +- application/Model/External.php | 350 +- application/Model/Item.php | 1348 +++--- application/Model/Item/Blank.php | 27 +- application/Model/Item/Block.php | 12 +- application/Model/Item/Browser.php | 4 +- application/Model/Item/Calculate.php | 16 +- application/Model/Item/Cc.php | 15 +- application/Model/Item/Check.php | 82 +- application/Model/Item/CheckButton.php | 36 +- application/Model/Item/ChooseTwoWeekdays.php | 10 +- application/Model/Item/Color.php | 34 +- application/Model/Item/Date.php | 10 +- application/Model/Item/Datetime.php | 94 +- application/Model/Item/DatetimeLocal.php | 4 +- application/Model/Item/Email.php | 31 +- application/Model/Item/File.php | 100 +- application/Model/Item/Geopoint.php | 46 +- application/Model/Item/Get.php | 76 +- application/Model/Item/Hidden.php | 22 +- application/Model/Item/Image.php | 12 +- application/Model/Item/Ip.php | 28 +- application/Model/Item/Letters.php | 16 +- application/Model/Item/Mc.php | 195 +- application/Model/Item/McButton.php | 30 +- application/Model/Item/McHeading.php | 91 +- application/Model/Item/McMultiple.php | 120 +- application/Model/Item/McMultipleButton.php | 32 +- application/Model/Item/Month.php | 8 +- application/Model/Item/Note.php | 38 +- application/Model/Item/NoteIframe.php | 28 +- application/Model/Item/Number.php | 227 +- application/Model/Item/Random.php | 42 +- application/Model/Item/Range.php | 67 +- application/Model/Item/RangeTicks.php | 94 +- application/Model/Item/RatingButton.php | 179 +- application/Model/Item/Referrer.php | 34 +- application/Model/Item/SelectMultiple.php | 40 +- application/Model/Item/SelectOne.php | 89 +- .../Model/Item/SelectOrAddMultiple.php | 32 +- application/Model/Item/SelectOrAddOne.php | 104 +- application/Model/Item/Server.php | 90 +- application/Model/Item/Sex.php | 16 +- application/Model/Item/Submit.php | 42 +- application/Model/Item/Tel.php | 17 +- application/Model/Item/Text.php | 40 +- application/Model/Item/Textarea.php | 26 +- application/Model/Item/Time.php | 11 +- application/Model/Item/Timezone.php | 84 +- application/Model/Item/Url.php | 37 +- application/Model/Item/Week.php | 13 +- application/Model/Item/Year.php | 11 +- application/Model/Item/Yearmonth.php | 28 +- application/Model/Page.php | 218 +- application/Model/Pagination.php | 194 +- application/Model/Pause.php | 655 ++- application/Model/Run.php | 1842 ++++---- application/Model/RunSession.php | 743 +-- application/Model/RunUnit.php | 1562 ++++--- application/Model/Shuffle.php | 176 +- application/Model/Site.php | 664 +-- application/Model/SkipBackward.php | 33 +- application/Model/SkipForward.php | 16 +- application/Model/SpreadsheetReader.php | 1649 ++++--- application/Model/Survey.php | 4141 ++++++++--------- application/Model/UnitSession.php | 108 +- application/Model/User.php | 875 ++-- application/View/admin/header.php | 82 +- application/View/admin/home.php | 316 +- application/View/admin/mail/edit.php | 132 +- application/View/admin/mail/index.php | 230 +- application/View/admin/misc/info.php | 2 +- application/View/admin/misc/osf.php | 144 +- application/View/admin/misc/test_opencpu.php | 53 +- .../View/admin/misc/test_opencpu_speed.php | 73 +- application/View/admin/run/add_run.php | 84 +- .../admin/run/create_new_named_session.php | 84 +- application/View/admin/run/cron_log.php | 107 +- .../View/admin/run/cron_log_parsed.php | 86 +- application/View/admin/run/delete_run.php | 84 +- application/View/admin/run/email_log.php | 106 +- application/View/admin/run/empty_run.php | 96 +- application/View/admin/run/index.php | 214 +- application/View/admin/run/list.php | 134 +- application/View/admin/run/menu.php | 140 +- application/View/admin/run/monkey_bar.php | 164 +- application/View/admin/run/overview.php | 78 +- application/View/admin/run/random_groups.php | 196 +- application/View/admin/run/rename_run.php | 92 +- .../View/admin/run/run_import_dialog.php | 42 +- application/View/admin/run/settings.php | 580 +-- application/View/admin/run/units/email.php | 83 +- application/View/admin/run/units/endpage.php | 10 +- application/View/admin/run/units/external.php | 24 +- application/View/admin/run/units/pause.php | 46 +- application/View/admin/run/units/shuffle.php | 14 +- .../View/admin/run/units/skipbackward.php | 16 +- .../View/admin/run/units/skipforward.php | 44 +- application/View/admin/run/units/survey.php | 71 +- application/View/admin/run/units/unit.php | 30 +- application/View/admin/run/upload_files.php | 138 +- application/View/admin/run/user_detail.php | 224 +- application/View/admin/run/user_overview.php | 394 +- application/View/admin/survey/add_survey.php | 156 +- .../View/admin/survey/delete_results.php | 100 +- .../View/admin/survey/delete_study.php | 90 +- .../View/admin/survey/google_sheet_import.php | 60 +- application/View/admin/survey/index.php | 376 +- application/View/admin/survey/list.php | 118 +- application/View/admin/survey/menu.php | 138 +- .../View/admin/survey/rename_study.php | 90 +- .../View/admin/survey/show_item_table.php | 232 +- .../admin/survey/show_item_table_table.php | 88 +- .../View/admin/survey/show_itemdisplay.php | 314 +- .../View/admin/survey/show_results.php | 286 +- .../View/admin/survey/upload_items.php | 216 +- application/View/public/about.php | 312 +- application/View/public/account.php | 158 +- application/View/public/alerts.php | 11 +- application/View/public/disclaimer.php | 32 +- application/View/public/documentation.php | 134 +- application/View/public/documentation/api.php | 42 +- .../View/public/documentation/features.php | 242 +- .../View/public/documentation/get_help.php | 52 +- .../public/documentation/getting_started.php | 46 +- .../View/public/documentation/item_types.php | 470 +- .../public/documentation/knitr_markdown.php | 78 +- .../View/public/documentation/r_helpers.php | 18 +- .../documentation/run_module_explanations.php | 804 ++-- .../documentation/sample_choices_sheet.php | 142 +- .../documentation/sample_survey_sheet.php | 540 +-- application/View/public/error.php | 56 +- application/View/public/forgot_password.php | 54 +- application/View/public/head.php | 8 +- application/View/public/header.php | 20 +- application/View/public/home.php | 300 +- application/View/public/login.php | 82 +- application/View/public/navigation.php | 62 +- application/View/public/publications.php | 92 +- application/View/public/register.php | 98 +- application/View/public/reset_password.php | 70 +- application/View/public/run/index.php | 50 +- application/View/public/run/settings.php | 106 +- application/View/public/social_share.php | 12 +- application/View/public/studies.php | 68 +- application/View/superadmin/active_users.php | 100 +- application/View/superadmin/cron_log.php | 99 +- .../View/superadmin/cron_log_parsed.php | 116 +- .../View/superadmin/runs_management.php | 132 +- .../View/superadmin/user_management.php | 158 +- bin/cron.php | 353 +- bin/deamon.php | 69 +- bin/import-results.php | 426 +- bin/queue.php | 40 +- bin/tasks.php | 145 +- 194 files changed, 25793 insertions(+), 25877 deletions(-) diff --git a/application/Controller/AdminAjaxController.php b/application/Controller/AdminAjaxController.php index 84ed59127..acb36cea7 100644 --- a/application/Controller/AdminAjaxController.php +++ b/application/Controller/AdminAjaxController.php @@ -8,519 +8,519 @@ */ class AdminAjaxController { - /** - * - * @var AdminController - */ - protected $controller; - - /** - * - * @var Site - */ - protected $site; - - /** - * @var Request - */ - protected $request; - - /** - * - * @var DB - */ - protected $dbh; - - public function __construct(AdminController $controller) { - $this->controller = $controller; - $this->site = $controller->getSite(); - $this->dbh = $controller->getDB(); - $this->request = new Request(); - } - - public static function call($method, AdminController $controller) { - $self = new self($controller); - $action = $self->getPrivateAction($method); - return $self->$action(); - } - - private function ajaxCreateRunUnit() { - if (!is_ajax_request()) { - formr_error(406, 'Not Acceptable'); - } - - $unit = $this->controller->createRunUnit(); - if ($unit->valid) { - $unit->addToRun($this->controller->run->id, $unit->position); - alert('Success. ' . ucfirst($unit->type) . ' unit was created.', 'alert-success'); - $response = $unit->displayForRun($this->site->renderAlerts()); - } else { - bad_request_header(); - $msg = 'Sorry. Run unit could not be created.'; - if (!empty($unit)) { - $msg .= implode("\n", $unit->errors); - } - alert($msg, 'alert-danger'); - $response = $this->site->renderAlerts(); - } - - echo $response; - exit; - } - - private function ajaxGetUnit() { - if (!is_ajax_request()) { - formr_error(406, 'Not Acceptable'); - } - - $run = $this->controller->run; - $dbh = $this->dbh; - - if ($run_unit_id = $this->request->getParam('run_unit_id')) { - $special = $this->request->getParam('special'); - - $unit_info = $run->getUnitAdmin($run_unit_id, $special); - $unit_factory = new RunUnitFactory(); - $unit = $unit_factory->make($dbh, null, $unit_info, null, $run); - - $response = $unit->displayForRun(); - } else { - bad_request_header(); - $msg = 'Sorry. Missing run unit.'; - if (!empty($unit)) { - $msg .= implode("\n", $unit->errors); - } - alert($msg, 'alert-danger'); - $response = $this->site->renderAlerts(); - } - - echo $response; - exit; - } - - private function ajaxRemind() { - if ($this->request->bool('get_count') === true) { - $sessions = $this->getSessionRemindersSent($this->request->int('run_session_id')); - $count = array(); - foreach ($sessions as $sess) { - if (!isset($count[$sess['unit_id']])) { - $count[$sess['unit_id']] = 0; - } + /** + * + * @var AdminController + */ + protected $controller; + + /** + * + * @var Site + */ + protected $site; + + /** + * @var Request + */ + protected $request; + + /** + * + * @var DB + */ + protected $dbh; + + public function __construct(AdminController $controller) { + $this->controller = $controller; + $this->site = $controller->getSite(); + $this->dbh = $controller->getDB(); + $this->request = new Request(); + } + + public static function call($method, AdminController $controller) { + $self = new self($controller); + $action = $self->getPrivateAction($method); + return $self->$action(); + } + + private function ajaxCreateRunUnit() { + if (!is_ajax_request()) { + formr_error(406, 'Not Acceptable'); + } + + $unit = $this->controller->createRunUnit(); + if ($unit->valid) { + $unit->addToRun($this->controller->run->id, $unit->position); + alert('Success. ' . ucfirst($unit->type) . ' unit was created.', 'alert-success'); + $response = $unit->displayForRun($this->site->renderAlerts()); + } else { + bad_request_header(); + $msg = 'Sorry. Run unit could not be created.'; + if (!empty($unit)) { + $msg .= implode("\n", $unit->errors); + } + alert($msg, 'alert-danger'); + $response = $this->site->renderAlerts(); + } + + echo $response; + exit; + } + + private function ajaxGetUnit() { + if (!is_ajax_request()) { + formr_error(406, 'Not Acceptable'); + } + + $run = $this->controller->run; + $dbh = $this->dbh; + + if ($run_unit_id = $this->request->getParam('run_unit_id')) { + $special = $this->request->getParam('special'); + + $unit_info = $run->getUnitAdmin($run_unit_id, $special); + $unit_factory = new RunUnitFactory(); + $unit = $unit_factory->make($dbh, null, $unit_info, null, $run); + + $response = $unit->displayForRun(); + } else { + bad_request_header(); + $msg = 'Sorry. Missing run unit.'; + if (!empty($unit)) { + $msg .= implode("\n", $unit->errors); + } + alert($msg, 'alert-danger'); + $response = $this->site->renderAlerts(); + } + + echo $response; + exit; + } + + private function ajaxRemind() { + if ($this->request->bool('get_count') === true) { + $sessions = $this->getSessionRemindersSent($this->request->int('run_session_id')); + $count = array(); + foreach ($sessions as $sess) { + if (!isset($count[$sess['unit_id']])) { + $count[$sess['unit_id']] = 0; + } $count[$sess['unit_id']]++; - } - return $this->outjson($count); - } - - $run = $this->controller->run; - // find the last email unit - $email = $run->getReminder($this->request->getParam('reminder'), $this->request->getParam('session'), $this->request->getParam('run_session_id')); - $email->run_session = new RunSession($this->dbh, $run->id, null, $this->request->getParam('session'), $run); - if ($email->exec() !== false) { - alert('Something went wrong with the reminder. Run: ' . $run->name, 'alert-danger'); - } else { - alert('Reminder sent!', 'alert-success'); - } - $email->end(); - - if (is_ajax_request()) { - echo $this->site->renderAlerts(); - exit; - } else { - redirect_to(admin_run_url($run->name, 'user_overview')); - } - } - - private function ajaxToggleTesting() { - $run = $this->controller->run; - $dbh = $this->dbh; - - $run_session = new RunSession($dbh, $run->id, null, $this->request->getParam('session'), $run); - - $status = $this->request->getParam('toggle_on') ? 1: 0; - $run_session->setTestingStatus($status); - - if (is_ajax_request()) { - echo $this->site->renderAlerts(); - exit; - } else { - redirect_to(admin_run_url($run->name, 'user_overview')); - } - } - - private function ajaxSendToPosition() { - $run = $this->controller->run; - $dbh = $this->dbh; - - $run_session = new RunSession($dbh, $run->id, null, $this->request->str('session'), $run); - $new_position = $this->request->int('new_position'); - - if (!$run_session->forceTo($new_position)) { - alert('Something went wrong with the position change. Run: ' . $run->name, 'alert-danger'); - bad_request_header(); - } - - if (is_ajax_request()) { - echo $this->site->renderAlerts(); - exit; - } else { - redirect_to(admin_run_url($run->name, 'user_overview')); - } - } - - private function ajaxNextInRun() { - $run = $this->controller->run; - $dbh = $this->dbh; - - $run_session = new RunSession($dbh, $run->id, null, $_GET['session'], $run); - - if (!$run_session->endUnitSession()) { - alert('Something went wrong with the unpause. in run ' . $run->name, 'alert-danger'); - bad_request_header(); - } - - if (is_ajax_request()) { - echo $this->site->renderAlerts(); - exit; - } else { - redirect_to(admin_run_url($run->name, 'user_overview')); - } - } - - private function ajaxSnipUnitSession() { - $run = $this->controller->run; - $dbh = $this->dbh; - $run_session = new RunSession($dbh, $run->id, null, $this->request->getParam('session'), $run); - - $unit_session = $run_session->getUnitSession(); - if($unit_session) { - $deleted = $dbh->delete('survey_unit_sessions', array('id' => $unit_session->id)); - if($deleted) { - alert('Success. You deleted the data at the current position.', 'alert-success'); - } else { - alert('Couldn\'t delete.', 'alert-danger'); - bad_request_header(); - } - } else { - alert("No unit session found", 'alert-danger'); - } - - if (is_ajax_request()) { - echo $this->site->renderAlerts(); - exit; - } else { - redirect_to(admin_run_url($run->name, 'user_overview')); - } - } - - private function ajaxDeleteUser() { - $run = $this->controller->run; - $deleted = $this->dbh->delete('survey_run_sessions', array('id' => $this->request->getParam('run_session_id'))); - if ($deleted) { - alert('User with session ' . h($_GET['session']) . ' was deleted.', 'alert-info'); - } else { - alert('User with session ' . h($_GET['session']) . ' could not be deleted.', 'alert-warning'); - bad_request_header(); - } - - if (is_ajax_request()) { - echo $this->site->renderAlerts(); - exit; - } else { - redirect_to(admin_run_url($run->name, 'user_overview')); - } - } - - private function ajaxDeleteUnitSession() { - $run = $this->controller->run; - $del = $this->dbh->prepare('DELETE FROM `survey_unit_sessions` WHERE id = :id'); - $del->bindValue(':id', $this->request->str('session_id')); - - if($del->execute()) { - alert('Success. You deleted this unit session.','alert-success'); - } else { - alert('Couldn\'t delete. Sorry.
'. print_r($del->errorInfo(), true).'
','alert-danger'); - bad_request_header(); - } - - if (is_ajax_request()) { - echo $this->site->renderAlerts(); - exit; - } else { - redirect_to(admin_run_url($run->name, 'user_detail')); - } - } - - private function ajaxRemoveRunUnitFromRun() { - if (!is_ajax_request()) { - formr_error(406, 'Not Acceptable'); - } - - $run = $this->controller->run; - $dbh = $this->dbh; - - if (($run_unit_id = $this->request->getParam('run_unit_id'))) { - $special = $this->request->getParam('special'); - - $unit_info = $run->getUnitAdmin($run_unit_id, $special); - $unit_factory = new RunUnitFactory(); - /* @var $unit RunUnit */ - $unit = $unit_factory->make($dbh, null, $unit_info, null, $run); - if (!$unit) { - formr_error(404, 'Not Found', 'Requested Run Unit was not found'); - } - $sess_key = __METHOD__ . $unit->id; - $results = $unit->howManyReachedItNumbers(); - $has_sessions = $results && (array_val($results, 'begun') || array_val($results, 'finished') || array_val($results, 'expired')); - - if ($has_sessions && !Session::get($sess_key)) { - Session::set($sess_key, $unit->id); - echo 'warn'; - exit; - } elseif (!$has_sessions || (Session::get($sess_key) === $unit->id && $this->request->getParam('confirm') === 'yes')) { - if ($unit->removeFromRun($special)) { - alert('Success. Unit with ID ' . $this->request->run_unit_id . ' was deleted.', 'alert-success'); - } else { - bad_request_header(); - $alert_msg = 'Sorry, could not remove unit. '; - $alert_msg .= implode($unit->errors); - alert($alert_msg, 'alert-danger'); - } - } - } - - Session::delete($sess_key); - echo $this->site->renderAlerts(); - exit; - } - - private function ajaxReorder() { - if (!is_ajax_request()) { - formr_error(406, 'Not Acceptable'); - } - - $run = $this->controller->run; - $positions = $this->request->arr('position'); - if ($positions) { - $unit = $run->reorder($positions); - $response = ''; - } else { - bad_request_header(); - $msg = 'Sorry. Re-ordering run units failed.'; - if (!empty($unit)) { - $msg .= implode("\n", $unit->errors); - } - alert($msg, 'alert-danger'); - $response = $this->site->renderAlerts(); - } - - echo $response; - exit; - } - - private function ajaxRunImport() { - $run = $this->controller->run; - $site = $this->site; - - if (!is_ajax_request()) { - formr_error(406, 'Not Acceptable'); - } - - // If only showing dialog then show it and exit - $dialog_only = $site->request->bool('dialog'); - if ($dialog_only) { - // Read on exported runs from configured directory - $dir = Config::get('run_exports_dir'); - if (!($exports = (array) get_run_dir_contents($dir))) { - $exports = array(); - } - - Template::load('admin/run/run_import_dialog', array('exports' => $exports, 'run' => $this->controller->run)); - exit; - } - } - - private function ajaxRunLockedToggle() { - if (!is_ajax_request()) { - formr_error(406, 'Not Acceptable'); - } - $run = $this->controller->run; - $lock = $this->request->int('on'); - if (in_array($lock, array(0, 1))) { - $run->toggleLocked($lock); - } - } - - private function ajaxRunPublicToggle() { - if (!is_ajax_request()) { - formr_error(406, 'Not Acceptable'); - } - $run = $this->controller->run; - $pub = $this->request->int('public'); - if (!$run->togglePublic($pub)) { - bad_request_header(); - } - } - - private function ajaxSaveRunUnit() { - if (!is_ajax_request()) { - formr_error(406, 'Not Acceptable'); - } - - $run = $this->controller->run; - $dbh = $this->dbh; - $response = ''; - - $unit_factory = new RunUnitFactory(); - if ($run_unit_id = $this->request->getParam('run_unit_id')) { - $special = $this->request->getParam('special'); - $unit_info = $run->getUnitAdmin($run_unit_id, $special); - - $unit = $unit_factory->make($dbh, null, $unit_info, null, $run); - $unit->create($_POST); - if ($unit->valid && ($unit->hadMajorChanges() || !empty($this->site->alerts))) { - $response = $unit->displayForRun($this->site->renderAlerts()); - } - } else { - bad_request_header(); - $alert_msg = "Sorry. Something went wrong while saving. Please contact formr devs, if this problem persists."; - if (!empty($unit)) { - $alert_msg .= implode("\n", $unit->errors); - } - alert($alert_msg, 'alert-danger'); - $response = $this->site->renderAlerts(); - } - - echo $response; - exit; - } - - private function ajaxSaveSettings() { - if (!is_ajax_request()) { - formr_error(406, 'Not Acceptable'); - } - - $run = $this->controller->run; - $post = new Request($_POST); - if ($run->saveSettings($post->getParams())) { - alert('Settings saved', 'alert-success'); - } else { - bad_request_header(); - alert('Error. ' . implode(".\n", $run->errors), 'alert-danger'); - } - - echo $this->site->renderAlerts(); - } - - private function ajaxTestUnit() { - if (!is_ajax_request()) { - formr_error(406, 'Not Acceptable'); - } - - $run = new Run($this->dbh, $this->controller->run->name); - - if ($run_unit_id = $this->request->getParam('run_unit_id')) { - $special = $this->request->getParam('special'); - $unit = $run->getUnitAdmin($run_unit_id, $special); - $unit_factory = new RunUnitFactory(); - $unit = $unit_factory->make($this->dbh, null, $unit, null, $run); - $unit->test(); - } else { - bad_request_header(); - $alert_msg = "Sorry. An error occured during the test."; - if (isset($unit)) { - $alert_msg .= implode("\n", $unit->errors); - } - alert($alert_msg, 'alert-danger'); - } - - echo $this->site->renderAlerts(); - exit; - } - - private function ajaxUserBulkActions() { - if (!is_ajax_request()) { - formr_error(406, 'Not Acceptable'); - } - - $action = $this->request->str('action'); - $sessions = $this->request->arr('sessions'); - $qs = $res = array(); - if (!$action || !$sessions) { - formr_error(500); - exit; - } - foreach ($sessions as $session) { - $qs[] = $this->dbh->quote($session); - } - $count = count($sessions); - if ($action === 'toggleTest') { - $query = 'UPDATE survey_run_sessions SET testing = 1 - testing WHERE session IN ('. implode(',', $qs) . ')'; - $this->dbh->query($query); - alert("{$count} selected session(s) were successfully modified", 'alert-success'); - $res['success'] = true; - } elseif ($action === 'sendReminder') { - $run = $this->controller->run; - $count = 0; - foreach ($sessions as $sess) { - $runSession = new RunSession($this->dbh, $run->id, null, $sess, $run); - $email = $run->getReminder($this->request->int('reminder'), $sess, $runSession->id); - $email->run_session = $runSession; - if ($email->exec() === false) { - $count++; - } - $email->end(); - } - - if ($count) { - alert("{$count} session(s) have been sent the reminder '{$email->getSubject()}'", 'alert-success'); - $res['success'] = true; - } else { - $res['error'] = $this->site->renderAlerts(); - } - } elseif ($action === 'deleteSessions') { - $query = 'DELETE FROM survey_run_sessions WHERE session IN ('. implode(',', $qs) . ')'; - $this->dbh->query($query); - alert("{$count} selected session(s) were successfully deleted", 'alert-success'); - $res['success'] = true; - } elseif ($action === 'positionSessions') { - $query = 'UPDATE survey_run_sessions SET position = ' . $this->request->int('pos') . ' WHERE session IN ('. implode(',', $qs) . ')'; - $this->dbh->query($query); - alert("{$count} selected session(s) were successfully moved", 'alert-success'); - $res['success'] = true; - } - - $this->outjson($res); - } - - protected function getPrivateAction($name) { - $parts = array_filter(explode('_', $name)); - $action = array_shift($parts); - $class = __CLASS__; - foreach ($parts as $part) { - $action .= ucwords(strtolower($part)); - } - if (!method_exists($this, $action)) { - throw new Exception("Action '$name' is not found in $class."); - } - return $action; - } - - protected function getSessionRemindersSent($run_session_id) { - $stmt = $this->dbh->prepare( - 'SELECT survey_unit_sessions.id as unit_session_id, survey_run_special_units.id as unit_id FROM survey_unit_sessions + } + return $this->outjson($count); + } + + $run = $this->controller->run; + // find the last email unit + $email = $run->getReminder($this->request->getParam('reminder'), $this->request->getParam('session'), $this->request->getParam('run_session_id')); + $email->run_session = new RunSession($this->dbh, $run->id, null, $this->request->getParam('session'), $run); + if ($email->exec() !== false) { + alert('Something went wrong with the reminder. Run: ' . $run->name, 'alert-danger'); + } else { + alert('Reminder sent!', 'alert-success'); + } + $email->end(); + + if (is_ajax_request()) { + echo $this->site->renderAlerts(); + exit; + } else { + redirect_to(admin_run_url($run->name, 'user_overview')); + } + } + + private function ajaxToggleTesting() { + $run = $this->controller->run; + $dbh = $this->dbh; + + $run_session = new RunSession($dbh, $run->id, null, $this->request->getParam('session'), $run); + + $status = $this->request->getParam('toggle_on') ? 1 : 0; + $run_session->setTestingStatus($status); + + if (is_ajax_request()) { + echo $this->site->renderAlerts(); + exit; + } else { + redirect_to(admin_run_url($run->name, 'user_overview')); + } + } + + private function ajaxSendToPosition() { + $run = $this->controller->run; + $dbh = $this->dbh; + + $run_session = new RunSession($dbh, $run->id, null, $this->request->str('session'), $run); + $new_position = $this->request->int('new_position'); + + if (!$run_session->forceTo($new_position)) { + alert('Something went wrong with the position change. Run: ' . $run->name, 'alert-danger'); + bad_request_header(); + } + + if (is_ajax_request()) { + echo $this->site->renderAlerts(); + exit; + } else { + redirect_to(admin_run_url($run->name, 'user_overview')); + } + } + + private function ajaxNextInRun() { + $run = $this->controller->run; + $dbh = $this->dbh; + + $run_session = new RunSession($dbh, $run->id, null, $_GET['session'], $run); + + if (!$run_session->endUnitSession()) { + alert('Something went wrong with the unpause. in run ' . $run->name, 'alert-danger'); + bad_request_header(); + } + + if (is_ajax_request()) { + echo $this->site->renderAlerts(); + exit; + } else { + redirect_to(admin_run_url($run->name, 'user_overview')); + } + } + + private function ajaxSnipUnitSession() { + $run = $this->controller->run; + $dbh = $this->dbh; + $run_session = new RunSession($dbh, $run->id, null, $this->request->getParam('session'), $run); + + $unit_session = $run_session->getUnitSession(); + if ($unit_session) { + $deleted = $dbh->delete('survey_unit_sessions', array('id' => $unit_session->id)); + if ($deleted) { + alert('Success. You deleted the data at the current position.', 'alert-success'); + } else { + alert('Couldn\'t delete.', 'alert-danger'); + bad_request_header(); + } + } else { + alert("No unit session found", 'alert-danger'); + } + + if (is_ajax_request()) { + echo $this->site->renderAlerts(); + exit; + } else { + redirect_to(admin_run_url($run->name, 'user_overview')); + } + } + + private function ajaxDeleteUser() { + $run = $this->controller->run; + $deleted = $this->dbh->delete('survey_run_sessions', array('id' => $this->request->getParam('run_session_id'))); + if ($deleted) { + alert('User with session ' . h($_GET['session']) . ' was deleted.', 'alert-info'); + } else { + alert('User with session ' . h($_GET['session']) . ' could not be deleted.', 'alert-warning'); + bad_request_header(); + } + + if (is_ajax_request()) { + echo $this->site->renderAlerts(); + exit; + } else { + redirect_to(admin_run_url($run->name, 'user_overview')); + } + } + + private function ajaxDeleteUnitSession() { + $run = $this->controller->run; + $del = $this->dbh->prepare('DELETE FROM `survey_unit_sessions` WHERE id = :id'); + $del->bindValue(':id', $this->request->str('session_id')); + + if ($del->execute()) { + alert('Success. You deleted this unit session.', 'alert-success'); + } else { + alert('Couldn\'t delete. Sorry.
' . print_r($del->errorInfo(), true) . '
', 'alert-danger'); + bad_request_header(); + } + + if (is_ajax_request()) { + echo $this->site->renderAlerts(); + exit; + } else { + redirect_to(admin_run_url($run->name, 'user_detail')); + } + } + + private function ajaxRemoveRunUnitFromRun() { + if (!is_ajax_request()) { + formr_error(406, 'Not Acceptable'); + } + + $run = $this->controller->run; + $dbh = $this->dbh; + + if (($run_unit_id = $this->request->getParam('run_unit_id'))) { + $special = $this->request->getParam('special'); + + $unit_info = $run->getUnitAdmin($run_unit_id, $special); + $unit_factory = new RunUnitFactory(); + /* @var $unit RunUnit */ + $unit = $unit_factory->make($dbh, null, $unit_info, null, $run); + if (!$unit) { + formr_error(404, 'Not Found', 'Requested Run Unit was not found'); + } + $sess_key = __METHOD__ . $unit->id; + $results = $unit->howManyReachedItNumbers(); + $has_sessions = $results && (array_val($results, 'begun') || array_val($results, 'finished') || array_val($results, 'expired')); + + if ($has_sessions && !Session::get($sess_key)) { + Session::set($sess_key, $unit->id); + echo 'warn'; + exit; + } elseif (!$has_sessions || (Session::get($sess_key) === $unit->id && $this->request->getParam('confirm') === 'yes')) { + if ($unit->removeFromRun($special)) { + alert('Success. Unit with ID ' . $this->request->run_unit_id . ' was deleted.', 'alert-success'); + } else { + bad_request_header(); + $alert_msg = 'Sorry, could not remove unit. '; + $alert_msg .= implode($unit->errors); + alert($alert_msg, 'alert-danger'); + } + } + } + + Session::delete($sess_key); + echo $this->site->renderAlerts(); + exit; + } + + private function ajaxReorder() { + if (!is_ajax_request()) { + formr_error(406, 'Not Acceptable'); + } + + $run = $this->controller->run; + $positions = $this->request->arr('position'); + if ($positions) { + $unit = $run->reorder($positions); + $response = ''; + } else { + bad_request_header(); + $msg = 'Sorry. Re-ordering run units failed.'; + if (!empty($unit)) { + $msg .= implode("\n", $unit->errors); + } + alert($msg, 'alert-danger'); + $response = $this->site->renderAlerts(); + } + + echo $response; + exit; + } + + private function ajaxRunImport() { + $run = $this->controller->run; + $site = $this->site; + + if (!is_ajax_request()) { + formr_error(406, 'Not Acceptable'); + } + + // If only showing dialog then show it and exit + $dialog_only = $site->request->bool('dialog'); + if ($dialog_only) { + // Read on exported runs from configured directory + $dir = Config::get('run_exports_dir'); + if (!($exports = (array) get_run_dir_contents($dir))) { + $exports = array(); + } + + Template::load('admin/run/run_import_dialog', array('exports' => $exports, 'run' => $this->controller->run)); + exit; + } + } + + private function ajaxRunLockedToggle() { + if (!is_ajax_request()) { + formr_error(406, 'Not Acceptable'); + } + $run = $this->controller->run; + $lock = $this->request->int('on'); + if (in_array($lock, array(0, 1))) { + $run->toggleLocked($lock); + } + } + + private function ajaxRunPublicToggle() { + if (!is_ajax_request()) { + formr_error(406, 'Not Acceptable'); + } + $run = $this->controller->run; + $pub = $this->request->int('public'); + if (!$run->togglePublic($pub)) { + bad_request_header(); + } + } + + private function ajaxSaveRunUnit() { + if (!is_ajax_request()) { + formr_error(406, 'Not Acceptable'); + } + + $run = $this->controller->run; + $dbh = $this->dbh; + $response = ''; + + $unit_factory = new RunUnitFactory(); + if ($run_unit_id = $this->request->getParam('run_unit_id')) { + $special = $this->request->getParam('special'); + $unit_info = $run->getUnitAdmin($run_unit_id, $special); + + $unit = $unit_factory->make($dbh, null, $unit_info, null, $run); + $unit->create($_POST); + if ($unit->valid && ($unit->hadMajorChanges() || !empty($this->site->alerts))) { + $response = $unit->displayForRun($this->site->renderAlerts()); + } + } else { + bad_request_header(); + $alert_msg = "Sorry. Something went wrong while saving. Please contact formr devs, if this problem persists."; + if (!empty($unit)) { + $alert_msg .= implode("\n", $unit->errors); + } + alert($alert_msg, 'alert-danger'); + $response = $this->site->renderAlerts(); + } + + echo $response; + exit; + } + + private function ajaxSaveSettings() { + if (!is_ajax_request()) { + formr_error(406, 'Not Acceptable'); + } + + $run = $this->controller->run; + $post = new Request($_POST); + if ($run->saveSettings($post->getParams())) { + alert('Settings saved', 'alert-success'); + } else { + bad_request_header(); + alert('Error. ' . implode(".\n", $run->errors), 'alert-danger'); + } + + echo $this->site->renderAlerts(); + } + + private function ajaxTestUnit() { + if (!is_ajax_request()) { + formr_error(406, 'Not Acceptable'); + } + + $run = new Run($this->dbh, $this->controller->run->name); + + if ($run_unit_id = $this->request->getParam('run_unit_id')) { + $special = $this->request->getParam('special'); + $unit = $run->getUnitAdmin($run_unit_id, $special); + $unit_factory = new RunUnitFactory(); + $unit = $unit_factory->make($this->dbh, null, $unit, null, $run); + $unit->test(); + } else { + bad_request_header(); + $alert_msg = "Sorry. An error occured during the test."; + if (isset($unit)) { + $alert_msg .= implode("\n", $unit->errors); + } + alert($alert_msg, 'alert-danger'); + } + + echo $this->site->renderAlerts(); + exit; + } + + private function ajaxUserBulkActions() { + if (!is_ajax_request()) { + formr_error(406, 'Not Acceptable'); + } + + $action = $this->request->str('action'); + $sessions = $this->request->arr('sessions'); + $qs = $res = array(); + if (!$action || !$sessions) { + formr_error(500); + exit; + } + foreach ($sessions as $session) { + $qs[] = $this->dbh->quote($session); + } + $count = count($sessions); + if ($action === 'toggleTest') { + $query = 'UPDATE survey_run_sessions SET testing = 1 - testing WHERE session IN (' . implode(',', $qs) . ')'; + $this->dbh->query($query); + alert("{$count} selected session(s) were successfully modified", 'alert-success'); + $res['success'] = true; + } elseif ($action === 'sendReminder') { + $run = $this->controller->run; + $count = 0; + foreach ($sessions as $sess) { + $runSession = new RunSession($this->dbh, $run->id, null, $sess, $run); + $email = $run->getReminder($this->request->int('reminder'), $sess, $runSession->id); + $email->run_session = $runSession; + if ($email->exec() === false) { + $count++; + } + $email->end(); + } + + if ($count) { + alert("{$count} session(s) have been sent the reminder '{$email->getSubject()}'", 'alert-success'); + $res['success'] = true; + } else { + $res['error'] = $this->site->renderAlerts(); + } + } elseif ($action === 'deleteSessions') { + $query = 'DELETE FROM survey_run_sessions WHERE session IN (' . implode(',', $qs) . ')'; + $this->dbh->query($query); + alert("{$count} selected session(s) were successfully deleted", 'alert-success'); + $res['success'] = true; + } elseif ($action === 'positionSessions') { + $query = 'UPDATE survey_run_sessions SET position = ' . $this->request->int('pos') . ' WHERE session IN (' . implode(',', $qs) . ')'; + $this->dbh->query($query); + alert("{$count} selected session(s) were successfully moved", 'alert-success'); + $res['success'] = true; + } + + $this->outjson($res); + } + + protected function getPrivateAction($name) { + $parts = array_filter(explode('_', $name)); + $action = array_shift($parts); + $class = __CLASS__; + foreach ($parts as $part) { + $action .= ucwords(strtolower($part)); + } + if (!method_exists($this, $action)) { + throw new Exception("Action '$name' is not found in $class."); + } + return $action; + } + + protected function getSessionRemindersSent($run_session_id) { + $stmt = $this->dbh->prepare( + 'SELECT survey_unit_sessions.id as unit_session_id, survey_run_special_units.id as unit_id FROM survey_unit_sessions LEFT JOIN survey_units ON survey_unit_sessions.unit_id = survey_units.id LEFT JOIN survey_run_special_units ON survey_run_special_units.id = survey_units.id WHERE survey_unit_sessions.run_session_id = :run_session_id AND survey_run_special_units.type = "ReminderEmail" '); - $stmt->bindValue('run_session_id', $run_session_id, PDO::PARAM_INT); - $stmt->execute(); - return $stmt->fetchAll(PDO::FETCH_ASSOC); - } - - protected function outjson($res) { - header('Content-Type: application/json'); - echo json_encode($res); - exit(0); - } + $stmt->bindValue('run_session_id', $run_session_id, PDO::PARAM_INT); + $stmt->execute(); + return $stmt->fetchAll(PDO::FETCH_ASSOC); + } + + protected function outjson($res) { + header('Content-Type: application/json'); + echo json_encode($res); + exit(0); + } } diff --git a/application/Controller/AdminController.php b/application/Controller/AdminController.php index b04d4214c..c55243717 100644 --- a/application/Controller/AdminController.php +++ b/application/Controller/AdminController.php @@ -2,144 +2,146 @@ class AdminController extends Controller { - public function __construct(Site &$site) { - parent::__construct($site); - $this->header(); - if (!Request::isAjaxRequest()) { - $default_assets = get_default_assets('admin'); - $this->registerAssets($default_assets); - $this->registerAssets('ace'); - } - } - - public function indexAction() { - $this->renderView('home', array( - 'runs' => $this->user->getRuns('id DESC', 5), - 'studies' => $this->user->getStudies('id DESC', 5), - )); - } - - public function infoAction() { - $this->renderView('misc/info'); - } -/* - public function cronAction() { - $this->renderView('misc/cron', array("fdb"=> $this->fdb)); - } - - public function cronForkedAction() { - $this->renderView('misc/cron_forked'); - } -*/ - public function testOpencpuAction() { - $this->renderView('misc/test_opencpu'); - } - - public function testOpencpuSpeedAction() { - $this->renderView('misc/test_opencpu_speed'); - } - - public function osfAction() { - if (!($token = OSF::getUserAccessToken($this->user))) { - redirect_to('api/osf/login'); - } - - $osf = new OSF(Config::get('osf')); - $osf->setAccessToken($token); - - if (Request::isHTTPPostRequest() && $this->request->getParam('osf_action') === 'export-run') { - $run = new Run($this->fdb, $this->request->getParam('formr_project')); - $osf_project = $this->request->getParam('osf_project'); - if (!$run->valid || !$osf_project) { - throw new Exception('Invalid Request'); - } - - $unitIds = $run->getAllUnitTypes(); - $units = array(); - $factory = new RunUnitFactory(); - - /* @var RunUnit $u */ - foreach ($unitIds as $u) { - $unit = $factory->make($this->fdb, null, $u, null, $run); - $ex_unit = $unit->getExportUnit(); - $ex_unit['unit_id'] = $unit->id; - $units[] = (object) $ex_unit; - } - - $export = $run->export($run->name, $units, true); - $export_file = Config::get('survey_upload_dir') . '/run-' . time() . '-' . $run->name . '.json'; - $create = file_put_contents($export_file, json_encode($export, JSON_PRETTY_PRINT + JSON_UNESCAPED_UNICODE + JSON_NUMERIC_CHECK)); - $response = $osf->upload($osf_project, $export_file, $run->name . '-' . date('YmdHis') . '.json'); - @unlink($export_file); - - if (!$response->hasError()) { - $run->saveSettings(array('osf_project_id' => $osf_project)); - alert('Run exported to OSF', 'alert-success'); - } else { - alert($response->getError(), 'alert-danger'); - } - - if ($redirect = $this->request->getParam('redirect')) { - redirect_to($redirect); - } - } - - // @todo implement get projects recursively - $response = $osf->getProjects(); - $osf_projects = array(); - if ($response->hasError()) { - alert($response->getError(), 'alert-danger'); - } else { - foreach ($response->getJSON()->data as $project) { - $osf_projects[] = array('id' => $project->id, 'name' => $project->attributes->title); - } - } - - $this->renderView('misc/osf', array( - 'token' => $token, - 'runs' => $this->user->getRuns(), - 'run_selected'=> $this->request->getParam('run'), - 'osf_projects' => $osf_projects, - )); - } - - protected function renderView($template, $vars = array()) { - $template = 'admin/' . $template; - parent::renderView($template, $vars); - } - - protected function header() { - if (!$this->user->loggedIn()) { - alert('You need to login to access the admin section', 'alert-warning'); - redirect_to('login'); - } - - if (!$this->user->isAdmin()) { - alert('You need to request for an admin account in order to access this section. See Documentation.', 'alert-warning'); - redirect_to('account'); - } - - if ($this->site->inSuperAdminArea() && !$this->user->isSuperAdmin()) { - formr_error(403, 'Forbidden', 'Sorry! Only super admins have access to this section.'); - } - } - - public function createRunUnit($id = null) { - $dbh = $this->fdb; - $run = $this->run; - $unit_factory = new RunUnitFactory(); - $unit_data = array( - 'type' => $this->request->type, - 'position' => (int)$this->request->position, - 'special' => $this->request->special, - ); - $unit_data = array_merge($this->request->getParams(), $unit_data, RunUnit::getDefaults($this->request->type), RunUnit::getDefaults($this->request->special)); - if ($id) { - $unit_data['unit_id'] = $id; - } - $unit = $unit_factory->make($dbh, null, $unit_data, null, $run); - $unit->create($unit_data); - return $unit; - } + public function __construct(Site &$site) { + parent::__construct($site); + $this->header(); + if (!Request::isAjaxRequest()) { + $default_assets = get_default_assets('admin'); + $this->registerAssets($default_assets); + $this->registerAssets('ace'); + } + } + + public function indexAction() { + $this->renderView('home', array( + 'runs' => $this->user->getRuns('id DESC', 5), + 'studies' => $this->user->getStudies('id DESC', 5), + )); + } + + public function infoAction() { + $this->renderView('misc/info'); + } + + /* + public function cronAction() { + $this->renderView('misc/cron', array("fdb"=> $this->fdb)); + } + + public function cronForkedAction() { + $this->renderView('misc/cron_forked'); + } + */ + + public function testOpencpuAction() { + $this->renderView('misc/test_opencpu'); + } + + public function testOpencpuSpeedAction() { + $this->renderView('misc/test_opencpu_speed'); + } + + public function osfAction() { + if (!($token = OSF::getUserAccessToken($this->user))) { + redirect_to('api/osf/login'); + } + + $osf = new OSF(Config::get('osf')); + $osf->setAccessToken($token); + + if (Request::isHTTPPostRequest() && $this->request->getParam('osf_action') === 'export-run') { + $run = new Run($this->fdb, $this->request->getParam('formr_project')); + $osf_project = $this->request->getParam('osf_project'); + if (!$run->valid || !$osf_project) { + throw new Exception('Invalid Request'); + } + + $unitIds = $run->getAllUnitTypes(); + $units = array(); + $factory = new RunUnitFactory(); + + /* @var RunUnit $u */ + foreach ($unitIds as $u) { + $unit = $factory->make($this->fdb, null, $u, null, $run); + $ex_unit = $unit->getExportUnit(); + $ex_unit['unit_id'] = $unit->id; + $units[] = (object) $ex_unit; + } + + $export = $run->export($run->name, $units, true); + $export_file = Config::get('survey_upload_dir') . '/run-' . time() . '-' . $run->name . '.json'; + $create = file_put_contents($export_file, json_encode($export, JSON_PRETTY_PRINT + JSON_UNESCAPED_UNICODE + JSON_NUMERIC_CHECK)); + $response = $osf->upload($osf_project, $export_file, $run->name . '-' . date('YmdHis') . '.json'); + @unlink($export_file); + + if (!$response->hasError()) { + $run->saveSettings(array('osf_project_id' => $osf_project)); + alert('Run exported to OSF', 'alert-success'); + } else { + alert($response->getError(), 'alert-danger'); + } + + if ($redirect = $this->request->getParam('redirect')) { + redirect_to($redirect); + } + } + + // @todo implement get projects recursively + $response = $osf->getProjects(); + $osf_projects = array(); + if ($response->hasError()) { + alert($response->getError(), 'alert-danger'); + } else { + foreach ($response->getJSON()->data as $project) { + $osf_projects[] = array('id' => $project->id, 'name' => $project->attributes->title); + } + } + + $this->renderView('misc/osf', array( + 'token' => $token, + 'runs' => $this->user->getRuns(), + 'run_selected' => $this->request->getParam('run'), + 'osf_projects' => $osf_projects, + )); + } + + protected function renderView($template, $vars = array()) { + $template = 'admin/' . $template; + parent::renderView($template, $vars); + } + + protected function header() { + if (!$this->user->loggedIn()) { + alert('You need to login to access the admin section', 'alert-warning'); + redirect_to('login'); + } + + if (!$this->user->isAdmin()) { + alert('You need to request for an admin account in order to access this section. See Documentation.', 'alert-warning'); + redirect_to('account'); + } + + if ($this->site->inSuperAdminArea() && !$this->user->isSuperAdmin()) { + formr_error(403, 'Forbidden', 'Sorry! Only super admins have access to this section.'); + } + } + + public function createRunUnit($id = null) { + $dbh = $this->fdb; + $run = $this->run; + $unit_factory = new RunUnitFactory(); + $unit_data = array( + 'type' => $this->request->type, + 'position' => (int) $this->request->position, + 'special' => $this->request->special, + ); + $unit_data = array_merge($this->request->getParams(), $unit_data, RunUnit::getDefaults($this->request->type), RunUnit::getDefaults($this->request->special)); + if ($id) { + $unit_data['unit_id'] = $id; + } + $unit = $unit_factory->make($dbh, null, $unit_data, null, $run); + $unit->create($unit_data); + return $unit; + } } diff --git a/application/Controller/AdminMailController.php b/application/Controller/AdminMailController.php index 1d38059ef..4cb09017a 100644 --- a/application/Controller/AdminMailController.php +++ b/application/Controller/AdminMailController.php @@ -2,77 +2,77 @@ class AdminMailController extends AdminController { - public function __construct(Site &$site) { - parent::__construct($site); - } + public function __construct(Site &$site) { + parent::__construct($site); + } - public function indexAction() { - $vars = array( - 'accs' => $this->user->getEmailAccounts(), - 'form_title' => 'Add Mail Account', - ); - $acc = new EmailAccount($this->fdb, null, $this->user->id); + public function indexAction() { + $vars = array( + 'accs' => $this->user->getEmailAccounts(), + 'form_title' => 'Add Mail Account', + ); + $acc = new EmailAccount($this->fdb, null, $this->user->id); - if ($this->request->account_id) { - $acc = new EmailAccount($this->fdb, $this->request->account_id, $this->user->id); - if (!$acc->valid || !$this->user->created($acc)) { - formr_error(401, 'Unauthorized', 'You do not have access to modify this email account'); - } - $vars['form_title'] = "Edit Mail Account ({$acc->account['username']})"; - } + if ($this->request->account_id) { + $acc = new EmailAccount($this->fdb, $this->request->account_id, $this->user->id); + if (!$acc->valid || !$this->user->created($acc)) { + formr_error(401, 'Unauthorized', 'You do not have access to modify this email account'); + } + $vars['form_title'] = "Edit Mail Account ({$acc->account['username']})"; + } - if (Request::isHTTPPostRequest()) { - if ($acc->id && $this->request->account_id == $acc->id) { - // we are editing - $this->edit($acc); - } else { - //we are creating - $this->create($acc); - } - } + if (Request::isHTTPPostRequest()) { + if ($acc->id && $this->request->account_id == $acc->id) { + // we are editing + $this->edit($acc); + } else { + //we are creating + $this->create($acc); + } + } - $vars['acc'] = $acc; - $this->renderView('mail/index', $vars); - } + $vars['acc'] = $acc; + $this->renderView('mail/index', $vars); + } - // @todo - public function deleteAction() { - if ($this->request->account_id) { - $acc = new EmailAccount($this->fdb, $this->request->account_id, $this->user->id); - if ($acc->valid && $this->user->created($acc)) { - $email = $acc->account['from']; - $acc->delete(); - alert("Success: Account with email '{$email}' was deleted", 'alert-success'); - } - }; - $this->redirect(); - } + // @todo + public function deleteAction() { + if ($this->request->account_id) { + $acc = new EmailAccount($this->fdb, $this->request->account_id, $this->user->id); + if ($acc->valid && $this->user->created($acc)) { + $email = $acc->account['from']; + $acc->delete(); + alert("Success: Account with email '{$email}' was deleted", 'alert-success'); + } + }; + $this->redirect(); + } - protected function create(EmailAccount $acc) { - if ($acc->create()) { - $this->edit($acc); - } else { - alert(implode($acc->errors), 'alert-danger'); - } - } + protected function create(EmailAccount $acc) { + if ($acc->create()) { + $this->edit($acc); + } else { + alert(implode($acc->errors), 'alert-danger'); + } + } - protected function edit(EmailAccount $acc) { - $acc->changeSettings($this->request->getParams()); - alert('Success! Your email account settings were saved!', 'alert-success'); + protected function edit(EmailAccount $acc) { + $acc->changeSettings($this->request->getParams()); + alert('Success! Your email account settings were saved!', 'alert-success'); - if ($this->request->test_account) { - $acc->test(); - } + if ($this->request->test_account) { + $acc->test(); + } - $this->redirect($acc); - } + $this->redirect($acc); + } - protected function redirect(EmailAccount $acc = null) { - if ($acc === null) { - redirect_to('admin/mail'); - } else { - redirect_to('admin/mail', array('account_id' => $acc->id)); - } - } + protected function redirect(EmailAccount $acc = null) { + if ($acc === null) { + redirect_to('admin/mail'); + } else { + redirect_to('admin/mail', array('account_id' => $acc->id)); + } + } } diff --git a/application/Controller/AdminRunController.php b/application/Controller/AdminRunController.php index ffbbb8450..8ca7738bd 100644 --- a/application/Controller/AdminRunController.php +++ b/application/Controller/AdminRunController.php @@ -2,118 +2,118 @@ class AdminRunController extends AdminController { - public function __construct(Site &$site) { - parent::__construct($site); - } - - public function indexAction($run_name = '', $private_action = '') { - $this->setRun($run_name); - if ($private_action) { - if (empty($this->run) || !$this->run->valid) { - throw new Exception("You cannot access this page with no valid run"); - } - - if (strpos($private_action, 'ajax') !== false) { - return AdminAjaxController::call($private_action, $this); - } - - $privateAction = $this->getPrivateAction($private_action); - return $this->$privateAction(); - } - - if (empty($this->run)) { - redirect_to('admin/run/add_run'); - } - - $vars = array( - 'show_panic' => $this->showPanicButton(), - ); - $this->renderView('run/index', $vars); - } - - public function listAction() { - $vars = array( - 'runs' => $this->user->getRuns('id DESC', null), - ); - $this->renderView('run/list', $vars); - } - - public function addRunAction() { - if ($this->request->isHTTPPostRequest()) { - $run_name = $this->request->str('run_name'); - if (!$run_name) { - $error = 'You have to specify a run name'; - } elseif (!preg_match("/^[a-zA-Z][a-zA-Z0-9-]{2,255}$/", $run_name)) { - $error = 'The run name can contain a to Z, 0 to 9 and the hyphen(-) (at least 2 characters, at most 255). It needs to start with a letter.'; - } elseif ($run_name == Run::TEST_RUN || Router::isWebRootDir($run_name) || in_array($run_name, Config::get('reserved_run_names', array())) || Run::nameExists($run_name)) { - $error = __('The run name "%s" is already taken. Please choose another name', $run_name); - } else { - $run = new Run($this->fdb, null); - $run->create(array('run_name' => $run_name, 'user_id' => $this->user->id)); - if ($run->valid) { - alert("Success. Run '{$run->name}' was created.", 'alert-success'); - redirect_to(admin_run_url($run->name)); - } else { - $error = 'An error creating your run please try again'; - } - } - - if (!empty($error)) { - alert("Error: {$error}", 'alert-danger'); - } - } - - $this->renderView('run/add_run'); - } - - private function userOverviewAction() { - $run = $this->run; - $fdb = $this->fdb; - - $search = ''; - $querystring = array(); - $position_cmp = '='; - $query_params = array(':run_id' => $run->id); - - if ($this->request->position_lt && in_array($this->request->position_lt, array('=', '>', '<'))) { - $position_cmp = $this->request->position_lt; - $querystring['position_lt'] = $position_cmp; - } - - if ($this->request->session) { - $session = str_replace("…", "", $this->request->session); - $search .= 'AND `survey_run_sessions`.session LIKE :session '; - $query_params[':session'] = "%" . $session . "%"; - $querystring['session'] = $session; - } - - if ($this->request->position) { - $position = $this->request->position; - $search .= "AND `survey_run_sessions`.position {$position_cmp} :position "; - $query_params[':position'] = $position; - $querystring['position'] = $position; - } - - if ($this->request->sessions) { - $sessions = array(); - foreach (explode("\n", $this->request->sessions) as $session) { - $session = $this->fdb->quote($session); - if($session) { - $sessions[] = $session; - } - } - $search .= " AND session IN (" . implode($sessions, ",") . ")"; - $querystring['sessions'] = $this->request->sessions; - } - - $user_count_query = "SELECT COUNT(`survey_run_sessions`.id) AS count FROM `survey_run_sessions` WHERE `survey_run_sessions`.run_id = :run_id $search;"; - $user_count = $fdb->execute($user_count_query, $query_params, true); - $pagination = new Pagination($user_count, 200, true); - $limits = $pagination->getLimits(); - - $query_params[':admin_code'] = $this->user->user_code; - - $users_query = "SELECT + public function __construct(Site &$site) { + parent::__construct($site); + } + + public function indexAction($run_name = '', $private_action = '') { + $this->setRun($run_name); + if ($private_action) { + if (empty($this->run) || !$this->run->valid) { + throw new Exception("You cannot access this page with no valid run"); + } + + if (strpos($private_action, 'ajax') !== false) { + return AdminAjaxController::call($private_action, $this); + } + + $privateAction = $this->getPrivateAction($private_action); + return $this->$privateAction(); + } + + if (empty($this->run)) { + redirect_to('admin/run/add_run'); + } + + $vars = array( + 'show_panic' => $this->showPanicButton(), + ); + $this->renderView('run/index', $vars); + } + + public function listAction() { + $vars = array( + 'runs' => $this->user->getRuns('id DESC', null), + ); + $this->renderView('run/list', $vars); + } + + public function addRunAction() { + if ($this->request->isHTTPPostRequest()) { + $run_name = $this->request->str('run_name'); + if (!$run_name) { + $error = 'You have to specify a run name'; + } elseif (!preg_match("/^[a-zA-Z][a-zA-Z0-9-]{2,255}$/", $run_name)) { + $error = 'The run name can contain a to Z, 0 to 9 and the hyphen(-) (at least 2 characters, at most 255). It needs to start with a letter.'; + } elseif ($run_name == Run::TEST_RUN || Router::isWebRootDir($run_name) || in_array($run_name, Config::get('reserved_run_names', array())) || Run::nameExists($run_name)) { + $error = __('The run name "%s" is already taken. Please choose another name', $run_name); + } else { + $run = new Run($this->fdb, null); + $run->create(array('run_name' => $run_name, 'user_id' => $this->user->id)); + if ($run->valid) { + alert("Success. Run '{$run->name}' was created.", 'alert-success'); + redirect_to(admin_run_url($run->name)); + } else { + $error = 'An error creating your run please try again'; + } + } + + if (!empty($error)) { + alert("Error: {$error}", 'alert-danger'); + } + } + + $this->renderView('run/add_run'); + } + + private function userOverviewAction() { + $run = $this->run; + $fdb = $this->fdb; + + $search = ''; + $querystring = array(); + $position_cmp = '='; + $query_params = array(':run_id' => $run->id); + + if ($this->request->position_lt && in_array($this->request->position_lt, array('=', '>', '<'))) { + $position_cmp = $this->request->position_lt; + $querystring['position_lt'] = $position_cmp; + } + + if ($this->request->session) { + $session = str_replace("…", "", $this->request->session); + $search .= 'AND `survey_run_sessions`.session LIKE :session '; + $query_params[':session'] = "%" . $session . "%"; + $querystring['session'] = $session; + } + + if ($this->request->position) { + $position = $this->request->position; + $search .= "AND `survey_run_sessions`.position {$position_cmp} :position "; + $query_params[':position'] = $position; + $querystring['position'] = $position; + } + + if ($this->request->sessions) { + $sessions = array(); + foreach (explode("\n", $this->request->sessions) as $session) { + $session = $this->fdb->quote($session); + if ($session) { + $sessions[] = $session; + } + } + $search .= " AND session IN (" . implode($sessions, ",") . ")"; + $querystring['sessions'] = $this->request->sessions; + } + + $user_count_query = "SELECT COUNT(`survey_run_sessions`.id) AS count FROM `survey_run_sessions` WHERE `survey_run_sessions`.run_id = :run_id $search;"; + $user_count = $fdb->execute($user_count_query, $query_params, true); + $pagination = new Pagination($user_count, 200, true); + $limits = $pagination->getLimits(); + + $query_params[':admin_code'] = $this->user->user_code; + + $users_query = "SELECT `survey_run_sessions`.id AS run_session_id, `survey_run_sessions`.session, `survey_run_sessions`.position, @@ -133,17 +133,17 @@ private function userOverviewAction() { ORDER BY `survey_run_sessions`.session != :admin_code, hang DESC, `survey_run_sessions`.last_access DESC LIMIT $limits;"; - $vars = get_defined_vars(); - $vars['users'] = $fdb->execute($users_query, $query_params); - $vars['position_lt'] = $position_cmp; - $vars['currentUser'] = $this->user; - $vars['unit_types'] = $run->getAllUnitTypes(); - $vars['reminders'] = $this->run->getSpecialUnits(false, 'ReminderEmail'); - $this->renderView('run/user_overview', $vars); - } - - private function exportUserOverviewAction() { - $users_query = "SELECT + $vars = get_defined_vars(); + $vars['users'] = $fdb->execute($users_query, $query_params); + $vars['position_lt'] = $position_cmp; + $vars['currentUser'] = $this->user; + $vars['unit_types'] = $run->getAllUnitTypes(); + $vars['reminders'] = $this->run->getSpecialUnits(false, 'ReminderEmail'); + $this->renderView('run/user_overview', $vars); + } + + private function exportUserOverviewAction() { + $users_query = "SELECT `survey_run_sessions`.position, `survey_units`.type AS unit_type, `survey_run_units`.description, @@ -157,20 +157,20 @@ private function exportUserOverviewAction() { LEFT JOIN `survey_units` ON `survey_run_units`.unit_id = `survey_units`.id WHERE `survey_run_sessions`.run_id = :run_id ORDER BY `survey_run_sessions`.session != :admin_code, hang DESC, `survey_run_sessions`.last_access DESC"; - $query_params = array(':run_id' => $this->run->id, ':admin_code' => $this->user->user_code); - $query_obj = $this->fdb->prepare($users_query); - $query_obj->execute($query_params); + $query_params = array(':run_id' => $this->run->id, ':admin_code' => $this->user->user_code); + $query_obj = $this->fdb->prepare($users_query); + $query_obj->execute($query_params); - $SPR = new SpreadsheetReader(); - $download_successfull = $SPR->exportInRequestedFormat($query_obj , $this->run->name . '_user_overview', $this->request->str('format')); - if (!$download_successfull) { - alert('An error occured during user overview download.', 'alert-danger'); - redirect_to(admin_run_url($this->run->name, 'user_overview')); - } - } + $SPR = new SpreadsheetReader(); + $download_successfull = $SPR->exportInRequestedFormat($query_obj, $this->run->name . '_user_overview', $this->request->str('format')); + if (!$download_successfull) { + alert('An error occured during user overview download.', 'alert-danger'); + redirect_to(admin_run_url($this->run->name, 'user_overview')); + } + } - private function exportUserDetailAction() { - $users_query = "SELECT + private function exportUserDetailAction() { + $users_query = "SELECT `survey_run_units`.position, `survey_units`.type AS unit_type, `survey_run_units`.description, @@ -189,94 +189,93 @@ private function exportUserDetailAction() { AND `survey_run_sessions`.run_id = :run_id2 ORDER BY `survey_run_sessions`.id DESC,`survey_unit_sessions`.id ASC;"; - $query_params = array(':run_id' => $this->run->id, ':run_id2' => $this->run->id); - $query_obj = $this->fdb->prepare($users_query); - $query_obj->execute($query_params); - - $SPR = new SpreadsheetReader(); - $download_successfull = $SPR->exportInRequestedFormat($query_obj , $this->run->name . '_user_detail', $this->request->str('format')); - if (!$download_successfull) { - alert('An error occured during user detail download.', 'alert-danger'); - redirect_to(admin_run_url($this->run->name, 'user_detail')); - } - } - - private function createNewTestCodeAction() { - $run_session = $this->run->makeTestRunSession(); - $sess = $run_session->session; - $animal = substr($sess, 0,strpos($sess, "XXX")); - $sess_url = run_url($this->run->name, null, array('code' => $sess)); - - //alert("You've created a new guinea pig, ".h($animal).". Use this guinea pig to move through the run like a normal user with special powers (accessibly via the monkey bar at the bottom right). As a guinea pig, you can see more detailed error messages than real users, so it is easier to e.g. debug R problems. If you want someone else to be the guinea pig, just forward them this link:
", "alert-info"); - redirect_to($sess_url); - } - - private function createNewNamedSessionAction() { - - if(Request::isHTTPPostRequest()) { - $code_name = $this->request->getParam('code_name'); - $run_session = $this->run->addNamedRunSession($code_name); - - if($run_session) { - $sess = $run_session->session; - $sess_url = run_url($this->run->name, null, array('code' => $sess)); - - //alert("You've added a user with the code name '{$code_name}'.
Send them this link to participate
", "alert-info"); - redirect_to(admin_run_url($this->run->name, 'user_overview', array('session' => $sess))); - } - } - - $this->renderView('run/create_new_named_session'); - - } - - private function userDetailAction() { - $run = $this->run; - $fdb = $this->fdb; - - $search = ''; - $querystring = array(); - $position_lt = '='; - if ($this->request->session) { - $session = str_replace('…', '', $this->request->session); - $search .= 'AND `survey_run_sessions`.session LIKE :session '; - $search_session = "%" . $session . "%"; - $querystring['session'] = $session; - } - - if ($this->request->position) { - $position = $this->request->position; - if (in_array($this->request->position_lt, array('>', '=', '<'))) { - $position_lt = $this->request->position_lt; - } - $search .= 'AND `survey_run_units`.position '.$position_lt.' :position '; - $search_position = $position; - $querystring['position_lt'] = $position_lt; - $querystring['position'] = $position; - } - - $user_count_query = " + $query_params = array(':run_id' => $this->run->id, ':run_id2' => $this->run->id); + $query_obj = $this->fdb->prepare($users_query); + $query_obj->execute($query_params); + + $SPR = new SpreadsheetReader(); + $download_successfull = $SPR->exportInRequestedFormat($query_obj, $this->run->name . '_user_detail', $this->request->str('format')); + if (!$download_successfull) { + alert('An error occured during user detail download.', 'alert-danger'); + redirect_to(admin_run_url($this->run->name, 'user_detail')); + } + } + + private function createNewTestCodeAction() { + $run_session = $this->run->makeTestRunSession(); + $sess = $run_session->session; + $animal = substr($sess, 0, strpos($sess, "XXX")); + $sess_url = run_url($this->run->name, null, array('code' => $sess)); + + //alert("You've created a new guinea pig, ".h($animal).". Use this guinea pig to move through the run like a normal user with special powers (accessibly via the monkey bar at the bottom right). As a guinea pig, you can see more detailed error messages than real users, so it is easier to e.g. debug R problems. If you want someone else to be the guinea pig, just forward them this link:
", "alert-info"); + redirect_to($sess_url); + } + + private function createNewNamedSessionAction() { + + if (Request::isHTTPPostRequest()) { + $code_name = $this->request->getParam('code_name'); + $run_session = $this->run->addNamedRunSession($code_name); + + if ($run_session) { + $sess = $run_session->session; + $sess_url = run_url($this->run->name, null, array('code' => $sess)); + + //alert("You've added a user with the code name '{$code_name}'.
Send them this link to participate
", "alert-info"); + redirect_to(admin_run_url($this->run->name, 'user_overview', array('session' => $sess))); + } + } + + $this->renderView('run/create_new_named_session'); + } + + private function userDetailAction() { + $run = $this->run; + $fdb = $this->fdb; + + $search = ''; + $querystring = array(); + $position_lt = '='; + if ($this->request->session) { + $session = str_replace('…', '', $this->request->session); + $search .= 'AND `survey_run_sessions`.session LIKE :session '; + $search_session = "%" . $session . "%"; + $querystring['session'] = $session; + } + + if ($this->request->position) { + $position = $this->request->position; + if (in_array($this->request->position_lt, array('>', '=', '<'))) { + $position_lt = $this->request->position_lt; + } + $search .= 'AND `survey_run_units`.position ' . $position_lt . ' :position '; + $search_position = $position; + $querystring['position_lt'] = $position_lt; + $querystring['position'] = $position; + } + + $user_count_query = " SELECT COUNT(`survey_unit_sessions`.id) AS count FROM `survey_unit_sessions` LEFT JOIN `survey_run_sessions` ON `survey_run_sessions`.id = `survey_unit_sessions`.run_session_id LEFT JOIN `survey_run_units` ON `survey_unit_sessions`.`unit_id` = `survey_run_units`.`unit_id` WHERE `survey_run_sessions`.run_id = :run_id $search"; - $params = array(':run_id' => $run->id); - if (isset($search_session)) { - $params[':session'] = $search_session; - } - if (isset($search_position)) { - $params[':position'] = $search_position; - } + $params = array(':run_id' => $run->id); + if (isset($search_session)) { + $params[':session'] = $search_session; + } + if (isset($search_position)) { + $params[':position'] = $search_position; + } - $user_count = $fdb->execute($user_count_query, $params, true); - $pagination = new Pagination($user_count, 400, true); - $limits = $pagination->getLimits(); + $user_count = $fdb->execute($user_count_query, $params, true); + $pagination = new Pagination($user_count, 400, true); + $limits = $pagination->getLimits(); - $params[':run_id2'] = $params[':run_id']; + $params[':run_id2'] = $params[':run_id']; - $users_query = "SELECT + $users_query = "SELECT `survey_run_sessions`.session, `survey_unit_sessions`.id AS session_id, `survey_runs`.name AS run_name, @@ -294,334 +293,333 @@ private function userDetailAction() { WHERE `survey_runs`.id = :run_id2 AND `survey_run_sessions`.run_id = :run_id $search ORDER BY `survey_run_sessions`.id DESC,`survey_unit_sessions`.id ASC LIMIT $limits"; - $users = $fdb->execute($users_query, $params); - - foreach ($users as $i => $userx) { - if ($userx['expired']) { - $stay_seconds = strtotime($userx['expired']) - strtotime($userx['created']); - } else { - $stay_seconds = ($userx['ended'] ? strtotime($userx['ended']) : time() ) - strtotime($userx['created']); - } - $userx['stay_seconds'] = $stay_seconds; - if($userx['expired']) { - $userx['ended'] = $userx['expired'] . ' (expired)'; - } - - if($userx['unit_type'] != 'Survey') { - $userx['delete_msg'] = "Are you sure you want to delete this unit session?"; - $userx['delete_title'] = "Delete this waypoint"; - } else { - $userx['delete_msg'] = "You SHOULDN'T delete survey sessions, you might delete data!
Are you REALLY sure you want to continue?"; - $userx['delete_title'] = "Survey unit sessions should not be deleted"; - } - - $users[$i] = $userx; - } - - $vars = get_defined_vars(); - $this->renderView('run/user_detail', $vars); - } - - private function uploadFilesAction() { - $run = $this->run; - - if (!empty($_FILES['uploaded_files'])) { - if($run->uploadFiles($_FILES['uploaded_files'])) { - alert('Success. The files were uploaded.','alert-success'); - if(!empty($run->messages)) { - alert(implode($run->messages, ' '), 'alert-info'); - } - redirect_to(admin_run_url($run->name, 'upload_files')); - } else { - alert('Sorry, files could not be uploaded.
' . nl2br(implode($run->errors, "\n")),'alert-danger'); - } - } elseif ($this->request->isHTTPPostRequest()) { - alert('The size of your request exceeds the allowed limit. Please report this to administrators indicating the size of your files.', 'alert-danger'); - } - - $this->renderView('run/upload_files', array('files' => $run->getUploadedFiles())); - } - - private function deleteFileAction() { - $id = $this->request->int('id'); - $filename = $this->request->str('file'); - $deleted = $this->run->deleteFile($id, $filename); - if ($deleted) { - alert('File Deleted', 'alert-success'); - } else { - alert('Unable to delete selected file', 'alert-danger'); - } - redirect_to(admin_run_url($this->run->name, 'upload_files')); - } - - private function settingsAction() { - $osf_projects = array(); - - if (($token = OSF::getUserAccessToken($this->user))) { - $osf = new OSF(Config::get('osf')); - $osf->setAccessToken($token); - $response = $osf->getProjects(); - - if ($response->hasError()) { - alert($response->getError(), 'alert-danger'); - $token = null; - } else { - foreach ($response->getJSON()->data as $project) { - $osf_projects[] = array('id' => $project->id, 'name' => $project->attributes->title); - } - } - } - - $this->renderView('run/settings', array( - 'osf_token' => $token, - 'run_selected'=> $this->request->getParam('run'), - 'osf_projects' => $osf_projects, - 'osf_project' => $this->run->osf_project_id, - 'run_id' => $this->run->id, - 'reminders' => $this->run->getSpecialUnits(true, 'ReminderEmail'), - 'service_messages' => $this->run->getSpecialUnits(true, 'ServiceMessagePage'), - 'overview_scripts' => $this->run->getSpecialUnits(true, 'OverviewScriptPage'), - )); - } - - private function renameRunAction() { - $run = $this->run; - if($this->request->isHTTPPostRequest()) { - $run_name = $this->request->str('new_name'); - if (!$run_name) { - $error = 'You have to specify a new run name'; - } elseif (!preg_match("/^[a-zA-Z][a-zA-Z0-9-]{2,255}$/", $run_name)) { - $error = 'The run name can contain a to Z, 0 to 9 and the hyphen(-) (at least 2 characters, at most 255). It needs to start with a letter.'; - } elseif ($run_name == Run::TEST_RUN || Router::isWebRootDir($run_name) || in_array($run_name, Config::get('reserved_run_names', array())) || Run::nameExists($run_name)) { - $error = __('The run name "%s" is already taken. Please choose another name', $run_name); - } else { - if ($run->rename($run_name)) { - alert("Success. Run was renamed to '{$run_name}'.", 'alert-success'); - redirect_to(admin_run_url($run_name)); - } else { - $error = 'An error renaming your run please try again'; - } - } - - if (!empty($error)) { - alert("Error: {$error}", 'alert-danger'); - } - } - - $this->renderView('run/rename_run'); - } - - private function exportDataAction() { - $run = $this->run; - $format = $this->request->str('format'); - $SPR = new SpreadsheetReader(); - SpreadsheetReader::verifyExportFormat($format); - - /* @var $resultsStmt PDOStatement */ - $resultsStmt = $run->getData(true); - if (!$resultsStmt->columnCount()) { - alert('No linked data yet', 'alert-info'); - redirect_to(admin_run_url($run->name)); - } - - $filename = $run->name . '_data'; - switch ($format) { - case 'xlsx': - $downloaded = $SPR->exportXLSX($resultsStmt, $filename); - break; - case 'xls': - $downloaded = $SPR->exportXLS($resultsStmt, $filename); - break; - case 'csv_german': - $downloaded = $SPR->exportCSV_german($resultsStmt, $filename); - break; - case 'tsv': - $downloaded = $SPR->exportTSV($resultsStmt, $filename); - break; - case 'json': - $downloaded = $SPR->exportJSON($resultsStmt, $filename); - break; - default: - $downloaded = $SPR->exportCSV($resultsStmt, $filename); - break; - } - - if (!$downloaded) { - alert('An error occured during results download.', 'alert-danger'); - redirect_to(admin_run_url($run->name)); - } - } - - private function exportSurveyResultsAction() { - $studies = $this->run->getAllSurveys(); - $dir = APPLICATION_ROOT . 'tmp/backups/results'; - if (!$dir) { - alert('Unable to create run backup directory', 'alert-danger'); - redirect_to(admin_run_url($this->run->name)); - } - - // create study result files - $SPR = new SpreadsheetReader(); - $errors = $files = $metadata = array(); - $metadata['run'] = array( - 'ID' => $this->run->id, - 'NAME' => $this->run->name, - ); - - foreach ($studies as $study) { - $survey = Survey::loadById($study['id']); - $backupFile = $dir . '/' . $this->run->name . '-' . $survey->name . '.tab'; - $backup = $SPR->exportTSV($survey->getResults(null, null, null, $this->run->id, true), $survey->name, $backupFile); - if (!$backup) { - $errors[] = "Unable to backup {$survey->name}"; - } else { - $files[] = $backupFile; - } - $metadata['survey:'.$survey->id] = array( - 'ID' => $survey->id, - 'NAME' => $survey->name, - 'RUN_ID' => $this->run->id - ); - } - - $metafile = $dir . '/' . $this->run->name . '.metadata'; - if (create_ini_file($metadata, $metafile)) { - $files[] = $metafile; - } - - // zip files and send to - if ($files) { - $zipfile = $dir . '/' . $this->run->name . '-' . date('d-m-Y') . '.zip'; - - //create the archive - if (!create_zip_archive($files, $zipfile)) { - alert('Unable to create zip archive: ' . basename($zipfile), 'alert-danger'); - redirect_to(admin_run_url($this->run->name)); - } - - $filename = basename($zipfile); - header("Content-Type: application/zip"); - header("Content-Disposition: attachment; filename=$filename"); - header("Content-Length: " . filesize($zipfile)); - readfile($zipfile); - // attempt to cleanup files after download - $files[] = $zipfile; - deletefiles($files); - exit; - } else { - alert('No files to zip and download', 'alert-danger'); - } - redirect_to(admin_run_url($this->run->name)); - } - - private function randomGroupsExportAction() { - $run = $this->run; - $format = $this->request->str('format'); - $SPR = new SpreadsheetReader(); - SpreadsheetReader::verifyExportFormat($format); - - /* @var $resultsStmt PDOStatement */ - $resultsStmt = $run->getRandomGroups(); //@TODO unset run_name, unit_type, ended, position - if (!$resultsStmt->columnCount()) { - alert('No linked data yet', 'alert-info'); - redirect_to(admin_run_url($run->name)); - } - - $filename = "Shuffle_Run_" . $run->name; - switch ($format) { - case 'xlsx': - $downloaded = $SPR->exportXLSX($resultsStmt, $filename); - break; - case 'xls': - $downloaded = $SPR->exportXLS($resultsStmt, $filename); - break; - case 'csv_german': - $downloaded = $SPR->exportCSV_german($resultsStmt, $filename); - break; - case 'tsv': - $downloaded = $SPR->exportTSV($resultsStmt, $filename); - break; - case 'json': - $downloaded = $SPR->exportJSON($resultsStmt, $filename); - break; - default: - $downloaded = $SPR->exportCSV($resultsStmt, $filename); - break; - } - - if (!$downloaded) { - alert('An error occured during results download.', 'alert-danger'); - redirect_to(admin_run_url($run->name)); - } - } - - private function randomGroupsAction() { - $run = $this->run; - $g_users = $run->getRandomGroups(); - - $users = array(); - while($userx = $g_users->fetch(PDO::FETCH_ASSOC)) { - $userx['Unit in Run'] = $userx['unit_type']. " ({$userx['position']})"; - # $userx['Email'] = "{$userx['email']}"; - $userx['Group'] = "{$userx['group']}"; - $userx['Created'] = "{$userx['created']}"; - - unset($userx['run_name']); - unset($userx['unit_type']); - unset($userx['created']); - unset($userx['ended']); - unset($userx['position']); - unset($userx['email']); - unset($userx['group']); - # $user['body'] = "". substr($user['body'],0,50). "…"; - - $users[] = $userx; - } - $this->renderView('run/random_groups', array('users' => $users)); - } - - private function overviewAction() { - $run = $this->run; - - $this->renderView('run/overview', array( - 'users' => $run->getNumberOfSessionsInRun(), - 'overview_script' => $run->getOverviewScript(), - 'user_overview' => $run->getUserCounts(), - )); - } - - private function emptyRunAction() { - $run = $this->run; - if ($this->request->isHTTPPostRequest()) { - if ($this->request->getParam('empty_confirm') === $run->name) { - $run->emptySelf(); - redirect_to(admin_run_url($run->name, "empty_run")); - } else { - alert("Error: You must type the run's name '{$run->name}' to empty it.", 'alert-danger'); - } - } - - $this->renderView('run/empty_run', array( - 'users' => $run->getNumberOfSessionsInRun(), - )); - - } - - private function emailLogAction() { - $run = $this->run; - $fdb = $this->fdb; - - $email_count_query = "SELECT COUNT(`survey_email_log`.id) AS count + $users = $fdb->execute($users_query, $params); + + foreach ($users as $i => $userx) { + if ($userx['expired']) { + $stay_seconds = strtotime($userx['expired']) - strtotime($userx['created']); + } else { + $stay_seconds = ($userx['ended'] ? strtotime($userx['ended']) : time() ) - strtotime($userx['created']); + } + $userx['stay_seconds'] = $stay_seconds; + if ($userx['expired']) { + $userx['ended'] = $userx['expired'] . ' (expired)'; + } + + if ($userx['unit_type'] != 'Survey') { + $userx['delete_msg'] = "Are you sure you want to delete this unit session?"; + $userx['delete_title'] = "Delete this waypoint"; + } else { + $userx['delete_msg'] = "You SHOULDN'T delete survey sessions, you might delete data!
Are you REALLY sure you want to continue?"; + $userx['delete_title'] = "Survey unit sessions should not be deleted"; + } + + $users[$i] = $userx; + } + + $vars = get_defined_vars(); + $this->renderView('run/user_detail', $vars); + } + + private function uploadFilesAction() { + $run = $this->run; + + if (!empty($_FILES['uploaded_files'])) { + if ($run->uploadFiles($_FILES['uploaded_files'])) { + alert('Success. The files were uploaded.', 'alert-success'); + if (!empty($run->messages)) { + alert(implode($run->messages, ' '), 'alert-info'); + } + redirect_to(admin_run_url($run->name, 'upload_files')); + } else { + alert('Sorry, files could not be uploaded.
' . nl2br(implode($run->errors, "\n")), 'alert-danger'); + } + } elseif ($this->request->isHTTPPostRequest()) { + alert('The size of your request exceeds the allowed limit. Please report this to administrators indicating the size of your files.', 'alert-danger'); + } + + $this->renderView('run/upload_files', array('files' => $run->getUploadedFiles())); + } + + private function deleteFileAction() { + $id = $this->request->int('id'); + $filename = $this->request->str('file'); + $deleted = $this->run->deleteFile($id, $filename); + if ($deleted) { + alert('File Deleted', 'alert-success'); + } else { + alert('Unable to delete selected file', 'alert-danger'); + } + redirect_to(admin_run_url($this->run->name, 'upload_files')); + } + + private function settingsAction() { + $osf_projects = array(); + + if (($token = OSF::getUserAccessToken($this->user))) { + $osf = new OSF(Config::get('osf')); + $osf->setAccessToken($token); + $response = $osf->getProjects(); + + if ($response->hasError()) { + alert($response->getError(), 'alert-danger'); + $token = null; + } else { + foreach ($response->getJSON()->data as $project) { + $osf_projects[] = array('id' => $project->id, 'name' => $project->attributes->title); + } + } + } + + $this->renderView('run/settings', array( + 'osf_token' => $token, + 'run_selected' => $this->request->getParam('run'), + 'osf_projects' => $osf_projects, + 'osf_project' => $this->run->osf_project_id, + 'run_id' => $this->run->id, + 'reminders' => $this->run->getSpecialUnits(true, 'ReminderEmail'), + 'service_messages' => $this->run->getSpecialUnits(true, 'ServiceMessagePage'), + 'overview_scripts' => $this->run->getSpecialUnits(true, 'OverviewScriptPage'), + )); + } + + private function renameRunAction() { + $run = $this->run; + if ($this->request->isHTTPPostRequest()) { + $run_name = $this->request->str('new_name'); + if (!$run_name) { + $error = 'You have to specify a new run name'; + } elseif (!preg_match("/^[a-zA-Z][a-zA-Z0-9-]{2,255}$/", $run_name)) { + $error = 'The run name can contain a to Z, 0 to 9 and the hyphen(-) (at least 2 characters, at most 255). It needs to start with a letter.'; + } elseif ($run_name == Run::TEST_RUN || Router::isWebRootDir($run_name) || in_array($run_name, Config::get('reserved_run_names', array())) || Run::nameExists($run_name)) { + $error = __('The run name "%s" is already taken. Please choose another name', $run_name); + } else { + if ($run->rename($run_name)) { + alert("Success. Run was renamed to '{$run_name}'.", 'alert-success'); + redirect_to(admin_run_url($run_name)); + } else { + $error = 'An error renaming your run please try again'; + } + } + + if (!empty($error)) { + alert("Error: {$error}", 'alert-danger'); + } + } + + $this->renderView('run/rename_run'); + } + + private function exportDataAction() { + $run = $this->run; + $format = $this->request->str('format'); + $SPR = new SpreadsheetReader(); + SpreadsheetReader::verifyExportFormat($format); + + /* @var $resultsStmt PDOStatement */ + $resultsStmt = $run->getData(true); + if (!$resultsStmt->columnCount()) { + alert('No linked data yet', 'alert-info'); + redirect_to(admin_run_url($run->name)); + } + + $filename = $run->name . '_data'; + switch ($format) { + case 'xlsx': + $downloaded = $SPR->exportXLSX($resultsStmt, $filename); + break; + case 'xls': + $downloaded = $SPR->exportXLS($resultsStmt, $filename); + break; + case 'csv_german': + $downloaded = $SPR->exportCSV_german($resultsStmt, $filename); + break; + case 'tsv': + $downloaded = $SPR->exportTSV($resultsStmt, $filename); + break; + case 'json': + $downloaded = $SPR->exportJSON($resultsStmt, $filename); + break; + default: + $downloaded = $SPR->exportCSV($resultsStmt, $filename); + break; + } + + if (!$downloaded) { + alert('An error occured during results download.', 'alert-danger'); + redirect_to(admin_run_url($run->name)); + } + } + + private function exportSurveyResultsAction() { + $studies = $this->run->getAllSurveys(); + $dir = APPLICATION_ROOT . 'tmp/backups/results'; + if (!$dir) { + alert('Unable to create run backup directory', 'alert-danger'); + redirect_to(admin_run_url($this->run->name)); + } + + // create study result files + $SPR = new SpreadsheetReader(); + $errors = $files = $metadata = array(); + $metadata['run'] = array( + 'ID' => $this->run->id, + 'NAME' => $this->run->name, + ); + + foreach ($studies as $study) { + $survey = Survey::loadById($study['id']); + $backupFile = $dir . '/' . $this->run->name . '-' . $survey->name . '.tab'; + $backup = $SPR->exportTSV($survey->getResults(null, null, null, $this->run->id, true), $survey->name, $backupFile); + if (!$backup) { + $errors[] = "Unable to backup {$survey->name}"; + } else { + $files[] = $backupFile; + } + $metadata['survey:' . $survey->id] = array( + 'ID' => $survey->id, + 'NAME' => $survey->name, + 'RUN_ID' => $this->run->id + ); + } + + $metafile = $dir . '/' . $this->run->name . '.metadata'; + if (create_ini_file($metadata, $metafile)) { + $files[] = $metafile; + } + + // zip files and send to + if ($files) { + $zipfile = $dir . '/' . $this->run->name . '-' . date('d-m-Y') . '.zip'; + + //create the archive + if (!create_zip_archive($files, $zipfile)) { + alert('Unable to create zip archive: ' . basename($zipfile), 'alert-danger'); + redirect_to(admin_run_url($this->run->name)); + } + + $filename = basename($zipfile); + header("Content-Type: application/zip"); + header("Content-Disposition: attachment; filename=$filename"); + header("Content-Length: " . filesize($zipfile)); + readfile($zipfile); + // attempt to cleanup files after download + $files[] = $zipfile; + deletefiles($files); + exit; + } else { + alert('No files to zip and download', 'alert-danger'); + } + redirect_to(admin_run_url($this->run->name)); + } + + private function randomGroupsExportAction() { + $run = $this->run; + $format = $this->request->str('format'); + $SPR = new SpreadsheetReader(); + SpreadsheetReader::verifyExportFormat($format); + + /* @var $resultsStmt PDOStatement */ + $resultsStmt = $run->getRandomGroups(); //@TODO unset run_name, unit_type, ended, position + if (!$resultsStmt->columnCount()) { + alert('No linked data yet', 'alert-info'); + redirect_to(admin_run_url($run->name)); + } + + $filename = "Shuffle_Run_" . $run->name; + switch ($format) { + case 'xlsx': + $downloaded = $SPR->exportXLSX($resultsStmt, $filename); + break; + case 'xls': + $downloaded = $SPR->exportXLS($resultsStmt, $filename); + break; + case 'csv_german': + $downloaded = $SPR->exportCSV_german($resultsStmt, $filename); + break; + case 'tsv': + $downloaded = $SPR->exportTSV($resultsStmt, $filename); + break; + case 'json': + $downloaded = $SPR->exportJSON($resultsStmt, $filename); + break; + default: + $downloaded = $SPR->exportCSV($resultsStmt, $filename); + break; + } + + if (!$downloaded) { + alert('An error occured during results download.', 'alert-danger'); + redirect_to(admin_run_url($run->name)); + } + } + + private function randomGroupsAction() { + $run = $this->run; + $g_users = $run->getRandomGroups(); + + $users = array(); + while ($userx = $g_users->fetch(PDO::FETCH_ASSOC)) { + $userx['Unit in Run'] = $userx['unit_type'] . " ({$userx['position']})"; + # $userx['Email'] = "{$userx['email']}"; + $userx['Group'] = "{$userx['group']}"; + $userx['Created'] = "{$userx['created']}"; + + unset($userx['run_name']); + unset($userx['unit_type']); + unset($userx['created']); + unset($userx['ended']); + unset($userx['position']); + unset($userx['email']); + unset($userx['group']); + # $user['body'] = "". substr($user['body'],0,50). "…"; + + $users[] = $userx; + } + $this->renderView('run/random_groups', array('users' => $users)); + } + + private function overviewAction() { + $run = $this->run; + + $this->renderView('run/overview', array( + 'users' => $run->getNumberOfSessionsInRun(), + 'overview_script' => $run->getOverviewScript(), + 'user_overview' => $run->getUserCounts(), + )); + } + + private function emptyRunAction() { + $run = $this->run; + if ($this->request->isHTTPPostRequest()) { + if ($this->request->getParam('empty_confirm') === $run->name) { + $run->emptySelf(); + redirect_to(admin_run_url($run->name, "empty_run")); + } else { + alert("Error: You must type the run's name '{$run->name}' to empty it.", 'alert-danger'); + } + } + + $this->renderView('run/empty_run', array( + 'users' => $run->getNumberOfSessionsInRun(), + )); + } + + private function emailLogAction() { + $run = $this->run; + $fdb = $this->fdb; + + $email_count_query = "SELECT COUNT(`survey_email_log`.id) AS count FROM `survey_email_log` LEFT JOIN `survey_unit_sessions` ON `survey_unit_sessions`.id = `survey_email_log`.session_id LEFT JOIN `survey_run_sessions` ON `survey_unit_sessions`.run_session_id = `survey_run_sessions`.id WHERE `survey_run_sessions`.run_id = :run_id"; - $email_count = $fdb->execute($email_count_query, array(':run_id' => $run->id), true); - $pagination = new Pagination($email_count, 50, true); - $limits = $pagination->getLimits(); + $email_count = $fdb->execute($email_count_query, array(':run_id' => $run->id), true); + $pagination = new Pagination($email_count, 50, true); + $limits = $pagination->getLimits(); - $emails_query = "SELECT + $emails_query = "SELECT `survey_email_accounts`.from_name, `survey_email_accounts`.`from`, `survey_email_log`.recipient AS `to`, @@ -639,56 +637,56 @@ private function emailLogAction() { WHERE `survey_run_sessions`.run_id = :run_id ORDER BY `survey_email_log`.id DESC LIMIT $limits ;"; - $g_emails = $fdb->execute($emails_query, array(':run_id' => $run->id)); - $emails = array(); - foreach ($g_emails as $email) { - $email['from'] = "{$email['from_name']}
{$email['from']}"; - unset($email['from_name']); - $email['to'] = $email['to']."
at run position ".$email['position_in_run'].""; - $email['mail'] = $email['subject']."
". h(substr($email['body'], 0, 100)). "…"; - $email['datetime'] = ''.timetostr(strtotime($email['created'])).' '; - $email['datetime'] .= $email['sent'] ? '' : ''; - unset($email['position_in_run'], $email['subject'], $email['body'], $email['created'], $email['sent']); - $emails[] = $email; - } - - $vars = get_defined_vars(); - $this->renderView('run/email_log', $vars); - } - - private function deleteRunAction() { - $run = $this->run; - if(isset($_POST['delete']) AND trim($_POST['delete_confirm']) === $run->name) { - $run->delete(); - } elseif(isset($_POST['delete'])) { - alert("Error: You must type the run's name '{$run->name}' to delete it.",'alert-danger'); - } - - $this->renderView('run/delete_run', array( - 'users' => $run->getNumberOfSessionsInRun(), - )); - } - - private function cronLogParsed() { - $parser = new LogParser(); - $parse = $this->run->name . '.log'; - $vars = get_defined_vars(); - $this->renderView('run/cron_log_parsed', $vars); - } - - private function cronLogAction() { - return $this->cronLogParsed(); - // @todo: deprecate code - $run = $this->run; - $fdb = $this->fdb; - - $fdb->count('survey_cron_log', array('run_id' => $run->id)); - $cron_entries_count = $fdb->count('survey_cron_log', array('run_id' => $run->id)); - - $pagination = new Pagination($cron_entries_count); - $limits = $pagination->getLimits(); - - $cron_query = "SELECT + $g_emails = $fdb->execute($emails_query, array(':run_id' => $run->id)); + $emails = array(); + foreach ($g_emails as $email) { + $email['from'] = "{$email['from_name']}
{$email['from']}"; + unset($email['from_name']); + $email['to'] = $email['to'] . "
at run position " . $email['position_in_run'] . ""; + $email['mail'] = $email['subject'] . "
" . h(substr($email['body'], 0, 100)) . "…"; + $email['datetime'] = '' . timetostr(strtotime($email['created'])) . ' '; + $email['datetime'] .= $email['sent'] ? '' : ''; + unset($email['position_in_run'], $email['subject'], $email['body'], $email['created'], $email['sent']); + $emails[] = $email; + } + + $vars = get_defined_vars(); + $this->renderView('run/email_log', $vars); + } + + private function deleteRunAction() { + $run = $this->run; + if (isset($_POST['delete']) AND trim($_POST['delete_confirm']) === $run->name) { + $run->delete(); + } elseif (isset($_POST['delete'])) { + alert("Error: You must type the run's name '{$run->name}' to delete it.", 'alert-danger'); + } + + $this->renderView('run/delete_run', array( + 'users' => $run->getNumberOfSessionsInRun(), + )); + } + + private function cronLogParsed() { + $parser = new LogParser(); + $parse = $this->run->name . '.log'; + $vars = get_defined_vars(); + $this->renderView('run/cron_log_parsed', $vars); + } + + private function cronLogAction() { + return $this->cronLogParsed(); + // @todo: deprecate code + $run = $this->run; + $fdb = $this->fdb; + + $fdb->count('survey_cron_log', array('run_id' => $run->id)); + $cron_entries_count = $fdb->count('survey_cron_log', array('run_id' => $run->id)); + + $pagination = new Pagination($cron_entries_count); + $limits = $pagination->getLimits(); + + $cron_query = "SELECT `survey_cron_log`.id, `survey_cron_log`.run_id, `survey_cron_log`.created, @@ -707,166 +705,167 @@ private function cronLogAction() { WHERE `survey_cron_log`.run_id = :run_id ORDER BY `survey_cron_log`.id DESC LIMIT $limits;"; - $g_cron = $fdb->execute($cron_query, array(':run_id' => $run->id)); - - $cronlogs = array(); - foreach ($g_cron as $cronlog) { - $cronlog = array_reverse($cronlog, true); - $cronlog['Modules'] = ''; - - if($cronlog['pauses']>0) - $cronlog['Modules'] .= $cronlog['pauses'].' '; - if($cronlog['skipbackwards']>0) - $cronlog['Modules'] .= $cronlog['skipbackwards'].' '; - if($cronlog['skipforwards']>0) - $cronlog['Modules'] .= $cronlog['skipforwards'].' '; - if($cronlog['emails']>0) - $cronlog['Modules'] .= $cronlog['emails'].' '; - if($cronlog['shuffles']>0) - $cronlog['Modules'] .= $cronlog['shuffles'].' '; - $cronlog['Modules'] .= ''; - $cronlog['took'] = ''.round($cronlog['time_in_seconds']/60, 2). 'm'; - $cronlog['time'] = ''.timetostr(strtotime($cronlog['created'])). ''; - $cronlog = array_reverse($cronlog, true); - unset($cronlog['created']); - unset($cronlog['time_in_seconds']); - unset($cronlog['skipforwards']); - unset($cronlog['skipbackwards']); - unset($cronlog['pauses']); - unset($cronlog['emails']); - unset($cronlog['shuffles']); - unset($cronlog['run_id']); - unset($cronlog['id']); - - $cronlogs[] = $cronlog; - } - - $vars = get_defined_vars(); - $this->renderView('run/cron_log', $vars); - } - - private function setRun($name) { - if (!$name) { - return; - } - - $run = new Run($this->fdb, $name); - if (!$run->valid) { - formr_error(404, 'Not Found', 'Requested Run does not exist or has been moved'); - } elseif (!$this->user->created($run)) { - formr_error(401, 'Unauthorized', 'You do not have access to modify this run'); - } - $this->run = $run; - } - - private function exportAction() { - $formats = array('json'); - $run = $this->run; - $site = $this->site; - - if (($units = (array)json_decode($site->request->str('units'))) && ($name = $site->request->str('export_name')) && preg_match('/^[a-z0-9-\s]+$/i', $name)) { - $format = $this->request->getParam('format'); - $inc_survey = $this->request->getParam('include_survey_details') === 'true'; - if (!in_array($format, $formats)) { - alert('Invalid Export format selected', 'alert-danger'); - redirect_to(admin_run_url($run->name)); - } - - if (!($export = $run->export($name, $units, $inc_survey))) { - bad_request_header(); - echo $site->renderAlerts(); - } else { - $SPR = new SpreadsheetReader(); - $SPR->exportJSON($export, $name); - } - } else { - alert('Run Export: Missing run units or invalid run name enterd.', 'alert-danger'); - redirect_to(admin_run_url($run->name)); - } - } - - private function importAction() { - if ($run_file = $this->request->getParam('run_file_name')) { - $file = Config::get('run_exports_dir') . '/' . $run_file; - } elseif (!empty($_FILES['run_file'])) { - $file = $_FILES['run_file']['tmp_name']; - } - - if (empty($file)) { - alert('Please select a run file or upload one', 'alert-danger'); - return redirect_to(admin_run_url($this->run->name)); - } - - if (!file_exists($file)) { - alert('The corresponding import file could not be found or is not readable', 'alert-danger'); - return redirect_to(admin_run_url($this->run->name)); - } - - $json_string = file_get_contents($file); - if (!$json_string) { - alert('Unable to extract JSON object from file', 'alert-danger'); - return redirect_to(admin_run_url($this->run->name)); - } - - $start_position = 10; - if ($this->run->importUnits($json_string, $start_position)) { - alert('Run modules imported successfully!', 'alert-success'); - } - - redirect_to(admin_run_url($this->run->name)); - } - - private function createRunUnitAction() { - $redirect = $this->request->redirect ? admin_run_url($this->run->name, $this->request->redirect) : admin_run_url($this->run->name); - $unit = $this->createRunUnit(); - if ($unit->valid) { - $unit->addToRun($this->run->id, $unit->position); - alert('Run unit created', 'alert-success'); - } else { - alert('An unexpected error occured. Unit could not be created', 'alert-danger'); - } - redirect_to(str_replace(':::', '#', $redirect)); - } - - private function deleteRunUnitAction() { - $id = (int)$this->request->unit_id; - if (!$id) { - throw new Exception('Missing Parameter'); - } - $redirect = $this->request->redirect ? admin_run_url($this->run->name, $this->request->redirect) : admin_run_url($this->run->name); - $unit = $this->createRunUnit($id); - if ($unit->valid) { - $unit->run_unit_id = $id; - $unit->removeFromRun($this->request->special); - alert('Run unit deleted', 'alert-success'); - } else { - alert('An unexpected error occured. Unit could not be deleted', 'alert-danger'); - } - redirect_to(str_replace(':::', '#', $redirect)); - } - - private function panicAction() { - $settings = array( - 'locked' => 1, - 'cron_active' => 0, - 'public' => 0, - //@todo maybe do more - ); - $updated = $this->fdb->update('survey_runs', $settings, array('id' => $this->run->id)); - if ($updated) { - $msg = array("Panic mode activated for '{$this->run->name}'"); - $msg[] = " - Only you can access this run"; - $msg[] = " - The cron job for this run has been deactivated"; - $msg[] = " - The run has been 'locked' for editing"; - alert(implode("\n", $msg), 'alert-success'); - } - redirect_to("admin/run/{$this->run->name}"); - } - - private function showPanicButton() { - $on = $this->run->locked === 1 && - $this->run->cron_active === 0 && - $this->run->public === 0; - return !$on; - } + $g_cron = $fdb->execute($cron_query, array(':run_id' => $run->id)); + + $cronlogs = array(); + foreach ($g_cron as $cronlog) { + $cronlog = array_reverse($cronlog, true); + $cronlog['Modules'] = ''; + + if ($cronlog['pauses'] > 0) + $cronlog['Modules'] .= $cronlog['pauses'] . ' '; + if ($cronlog['skipbackwards'] > 0) + $cronlog['Modules'] .= $cronlog['skipbackwards'] . ' '; + if ($cronlog['skipforwards'] > 0) + $cronlog['Modules'] .= $cronlog['skipforwards'] . ' '; + if ($cronlog['emails'] > 0) + $cronlog['Modules'] .= $cronlog['emails'] . ' '; + if ($cronlog['shuffles'] > 0) + $cronlog['Modules'] .= $cronlog['shuffles'] . ' '; + $cronlog['Modules'] .= ''; + $cronlog['took'] = '' . round($cronlog['time_in_seconds'] / 60, 2) . 'm'; + $cronlog['time'] = '' . timetostr(strtotime($cronlog['created'])) . ''; + $cronlog = array_reverse($cronlog, true); + unset($cronlog['created']); + unset($cronlog['time_in_seconds']); + unset($cronlog['skipforwards']); + unset($cronlog['skipbackwards']); + unset($cronlog['pauses']); + unset($cronlog['emails']); + unset($cronlog['shuffles']); + unset($cronlog['run_id']); + unset($cronlog['id']); + + $cronlogs[] = $cronlog; + } + + $vars = get_defined_vars(); + $this->renderView('run/cron_log', $vars); + } + + private function setRun($name) { + if (!$name) { + return; + } + + $run = new Run($this->fdb, $name); + if (!$run->valid) { + formr_error(404, 'Not Found', 'Requested Run does not exist or has been moved'); + } elseif (!$this->user->created($run)) { + formr_error(401, 'Unauthorized', 'You do not have access to modify this run'); + } + $this->run = $run; + } + + private function exportAction() { + $formats = array('json'); + $run = $this->run; + $site = $this->site; + + if (($units = (array) json_decode($site->request->str('units'))) && ($name = $site->request->str('export_name')) && preg_match('/^[a-z0-9-\s]+$/i', $name)) { + $format = $this->request->getParam('format'); + $inc_survey = $this->request->getParam('include_survey_details') === 'true'; + if (!in_array($format, $formats)) { + alert('Invalid Export format selected', 'alert-danger'); + redirect_to(admin_run_url($run->name)); + } + + if (!($export = $run->export($name, $units, $inc_survey))) { + bad_request_header(); + echo $site->renderAlerts(); + } else { + $SPR = new SpreadsheetReader(); + $SPR->exportJSON($export, $name); + } + } else { + alert('Run Export: Missing run units or invalid run name enterd.', 'alert-danger'); + redirect_to(admin_run_url($run->name)); + } + } + + private function importAction() { + if ($run_file = $this->request->getParam('run_file_name')) { + $file = Config::get('run_exports_dir') . '/' . $run_file; + } elseif (!empty($_FILES['run_file'])) { + $file = $_FILES['run_file']['tmp_name']; + } + + if (empty($file)) { + alert('Please select a run file or upload one', 'alert-danger'); + return redirect_to(admin_run_url($this->run->name)); + } + + if (!file_exists($file)) { + alert('The corresponding import file could not be found or is not readable', 'alert-danger'); + return redirect_to(admin_run_url($this->run->name)); + } + + $json_string = file_get_contents($file); + if (!$json_string) { + alert('Unable to extract JSON object from file', 'alert-danger'); + return redirect_to(admin_run_url($this->run->name)); + } + + $start_position = 10; + if ($this->run->importUnits($json_string, $start_position)) { + alert('Run modules imported successfully!', 'alert-success'); + } + + redirect_to(admin_run_url($this->run->name)); + } + + private function createRunUnitAction() { + $redirect = $this->request->redirect ? admin_run_url($this->run->name, $this->request->redirect) : admin_run_url($this->run->name); + $unit = $this->createRunUnit(); + if ($unit->valid) { + $unit->addToRun($this->run->id, $unit->position); + alert('Run unit created', 'alert-success'); + } else { + alert('An unexpected error occured. Unit could not be created', 'alert-danger'); + } + redirect_to(str_replace(':::', '#', $redirect)); + } + + private function deleteRunUnitAction() { + $id = (int) $this->request->unit_id; + if (!$id) { + throw new Exception('Missing Parameter'); + } + $redirect = $this->request->redirect ? admin_run_url($this->run->name, $this->request->redirect) : admin_run_url($this->run->name); + $unit = $this->createRunUnit($id); + if ($unit->valid) { + $unit->run_unit_id = $id; + $unit->removeFromRun($this->request->special); + alert('Run unit deleted', 'alert-success'); + } else { + alert('An unexpected error occured. Unit could not be deleted', 'alert-danger'); + } + redirect_to(str_replace(':::', '#', $redirect)); + } + + private function panicAction() { + $settings = array( + 'locked' => 1, + 'cron_active' => 0, + 'public' => 0, + //@todo maybe do more + ); + $updated = $this->fdb->update('survey_runs', $settings, array('id' => $this->run->id)); + if ($updated) { + $msg = array("Panic mode activated for '{$this->run->name}'"); + $msg[] = " - Only you can access this run"; + $msg[] = " - The cron job for this run has been deactivated"; + $msg[] = " - The run has been 'locked' for editing"; + alert(implode("\n", $msg), 'alert-success'); + } + redirect_to("admin/run/{$this->run->name}"); + } + + private function showPanicButton() { + $on = $this->run->locked === 1 && + $this->run->cron_active === 0 && + $this->run->public === 0; + return !$on; + } + } diff --git a/application/Controller/AdminSurveyController.php b/application/Controller/AdminSurveyController.php index 6db1ed2d4..e6eb957c9 100644 --- a/application/Controller/AdminSurveyController.php +++ b/application/Controller/AdminSurveyController.php @@ -2,460 +2,462 @@ class AdminSurveyController extends AdminController { - public function __construct(Site &$site) { - parent::__construct($site); - } - - public function indexAction($survey_name = '', $private_action = '') { - $this->setStudy($survey_name); - - if ($private_action) { - if (empty($this->study) || !$this->study->valid) { - throw new Exception("You cannot access this page with no valid study"); - } - $privateAction = $this->getPrivateAction($private_action); - return $this->$privateAction(); - } - - if ($this->request->isHTTPPostRequest()) { - $request = new Request($_POST); - $this->study->changeSettings($request->getParams()); - redirect_to(admin_study_url($this->study->name)); - } - - if (empty($this->study)) { - redirect_to(admin_url('survey/add_survey')); - } - - $this->renderView('survey/index', array( - 'survey_id' => $this->study->id, - )); - } - - public function listAction() { - $vars = array( - 'studies' => $this->user->getStudies('id DESC', null), - ); - $this->renderView('survey/list', $vars); - } - - public function addSurveyAction() { - $settings = $updates = array(); - if (Request::isHTTPPostRequest() && $this->request->google_sheet && $this->request->survey_name) { - $file = google_download_survey_sheet($this->request->survey_name, $this->request->google_sheet); - if (!$file) { - alert("Unable to download the file at '{$this->request->google_sheet}'", 'alert-danger'); - } else { - $updates['google_file_id'] = $file['google_id']; - } - } elseif (Request::isHTTPPostRequest() && !isset($_FILES['uploaded'])) { - alert('Error: You have to select an item table file here.', 'alert-danger'); - } elseif (isset($_FILES['uploaded'])) { - $file = $_FILES['uploaded']; - } - - if (!empty($file)) { - unset($_SESSION['study_id']); - unset($_GET['study_name']); - - $allowed_size = Config::get('admin_maximum_size_of_uploaded_files'); - if ($allowed_size && $file['size'] > $allowed_size * 1024 * 1024): - alert("File exceeds allowed size of {$allowed_size} MB", 'alert-danger'); - else: - $filename = basename($file['name']); - $survey_name = preg_filter("/^([a-zA-Z][a-zA-Z0-9_]{2,64})(-[a-z0-9A-Z]+)?\.[a-z]{3,4}$/", "$1", $filename); // take only the first part, before the dash if present or the dot - - $study = new Survey($this->fdb, null, array( - 'name' => $survey_name, - 'user_id' => $this->user->id - ), null, null); - - if ($study->createIndependently($settings, $updates)) { - $confirmed_deletion = true; - $created_new = true; - if ($study->uploadItemTable($file, $confirmed_deletion, $updates, $created_new)) { - alert('Success! New survey created!', 'alert-success'); - delete_tmp_file($file); - redirect_to(admin_study_url($study->name, 'show_item_table')); - } else { - alert('Bugger! A new survey was created, but there were problems with your item table. Please fix them and try again.', 'alert-danger'); - delete_tmp_file($file); - redirect_to(admin_study_url($study->name, 'upload_items')); - } - } - endif; - delete_tmp_file($file); - } - - $vars = array('google' => array()); - $this->renderView('survey/add_survey', $vars); - } - - - private function uploadItemsAction() { - $updates = array(); - $study = $this->study; - $google_id = $study->getGoogleFileId(); - $vars = array( - 'study_name' => $study->name, - 'google' => array('id' => $google_id, 'link' => google_get_sheet_link($google_id), 'name' => $study->name), - ); - - if(Request::isHTTPPostRequest()): - $confirmed_deletion = false; - if(isset($this->request->delete_confirm)): - $confirmed_deletion = $this->request->delete_confirm; - endif; - if (trim($confirmed_deletion) == ''): - $confirmed_deletion = false; - elseif ($confirmed_deletion === $study->name): - $confirmed_deletion = true; - else: - alert("Error: You confirmed the deletion of the study's results but your input did not match the study's name. Update aborted.", 'alert-danger'); - $confirmed_deletion = false; - redirect_to(admin_study_url($study->name, 'upload_items')); - return false; - endif; - endif; - - $file = null; - if (isset($_FILES['uploaded']) AND $_FILES['uploaded']['name'] !== "") { - $filename = basename($_FILES['uploaded']['name']); - $survey_name = preg_filter("/^([a-zA-Z][a-zA-Z0-9_]{2,64})(-[a-z0-9A-Z]+)?\.[a-z]{3,4}$/", "$1", $filename); // take only the first part, before the dash if present or the dot if present - - if ($study->name !== $survey_name) { - alert('Error: The uploaded file name ' . htmlspecialchars($survey_name) . ' did not match the study name ' . $study->name . '.', 'alert-danger'); - } else { - $file = $_FILES['uploaded']; - } - - } elseif (Request::isHTTPPostRequest() && $this->request->google_sheet) { - $file = google_download_survey_sheet($study->name, $this->request->google_sheet); - if (!$file) { - alert("Unable to download the file at '{$this->request->google_id}'", 'alert-danger'); - } else { - $updates['google_file_id'] = $file['google_id']; - } - } elseif (Request::isHTTPPostRequest()) { - alert('Error: You have to select an item table file or enter a Google link here.', 'alert-danger'); - } - - if (!empty($file)) { - $allowed_size = Config::get('admin_maximum_size_of_uploaded_files'); - if ($allowed_size && $file['size'] > $allowed_size * 1024 * 1024): - alert("File exceeds allowed size of {$allowed_size} MB", 'alert-danger'); - redirect_to(admin_study_url($study->name, 'upload_items')); - return false; - endif; - - if (($filename = $study->getOriginalFileName()) && - files_are_equal($file['tmp_name'], Config::get('survey_upload_dir') . '/' . $filename )) { - alert("Uploaded item table was identical to last uploaded item table.
- No changes carried out.", 'alert-info'); - $success = false; - } else { - $success = $study->uploadItemTable($file, $confirmed_deletion, $updates, false); - } - delete_tmp_file($file); - if($success) { - redirect_to(admin_study_url($study->name, 'show_item_table')); - } - } - - $this->renderView('survey/upload_items', $vars); - } - - private function accessAction() { - $study = $this->study; - if ($this->user->created($study)): - $session = new UnitSession($this->fdb, null, $study->id); - $session->create(); - - Session::set('dummy_survey_session', array( - "session_id" => $session->id, - "unit_id" => $study->id, - "run_session_id" => $session->run_session_id, - "run_name" => Run::TEST_RUN, - "survey_name" => $study->name - )); - - alert("Go ahead. You can test the study " . $study->name . " now.", 'alert-info'); - redirect_to(run_url(Run::TEST_RUN)); - else: - alert("Sorry. You don't have access to this study", 'alert-danger'); - redirect_to("index"); - endif; - } - - private function showItemTableAction() { - $vars = array( - 'google_id' => $this->study->getGoogleFileId(), - 'original_file' => $this->study->getOriginalFileName(), - 'results' => $this->study->getItemsWithChoices(), - 'shortcut' => $this->request->str('to', null) - ); - if(empty($vars['results'])) { - alert("No valid item table uploaded so far.", 'alert-warning'); - redirect_to(admin_study_url($this->study->name, 'upload_items')); - } else { - $this->renderView('survey/show_item_table', $vars); - } - } - - private function showItemdisplayAction() { - if ($this->study->settings['hide_results']) { - return $this->hideResults(); - } - - $filter = array( - 'session' => $this->request->str('session'), - 'results' => $this->request->str('rfilter'), - ); - - // paginate based on number of items on this sheet so that each - // run session will have all items for each pagination - $items = $this->study->getItems('id'); $ids = array(); - foreach ($items as $item) { - $ids[] = $item['id']; - } - - //$session = $this->request->str('session', null); - // show $no_sessions sessions per_page (so limit = $no_sessions * number of items in a survey) - //$limit = $this->request->int('per_page', $no_sessions * $itemsCount); - - $no_sessions = $this->request->int('sess_per_page', 10); - $count = $this->study->getResultCount(null, $filter); - $totalCount = $count['real_users'] + $count['testers']; - $limit = $no_sessions; - $page = ($this->request->int('page', 1) - 1); - $paginate = array( - 'limit' => $limit, - 'page' => $page, - 'offset' => $limit * $page, - 'count' => $totalCount, - ); - - if ($paginate['page'] < 0 || $paginate['limit'] < 0) { - throw new Exception('Invalid Page number'); - } - - $pagination = new Pagination($paginate['count'], $paginate['limit']); - $pagination->setPage($paginate['page']); - - $this->renderView('survey/show_itemdisplay', array( - 'resultCount' => $this->study->getResultCount(), - 'results' => $totalCount ? $this->study->getResultsByItemsPerSession(null, $filter, $paginate) : array(), - 'pagination' => $pagination, - 'study_name' => $this->study->name, - //'session' => $session, - 'session' => $this->request->str('session'), - 'rfilter' => $this->request->str('rfilter'), - 'results_filter' => $this->study->getResultsFilter(), - )); - } - - private function showResultsAction() { - if ($this->study->settings['hide_results']) { - return $this->hideResults(); - } - - $filter = array( - 'session' => $this->request->str('session'), - 'results' => $this->request->str('rfilter'), - ); - - $count = $this->study->getResultCount(null, $filter); - $totalCount = $count['real_users'] + $count['testers']; - $limit = $this->request->int('per_page', 100); - $page = ($this->request->int('page', 1) - 1); - $paginate = array( - 'limit' => $limit, - 'page' => $page, - 'offset' => $limit * $page, - 'order' => 'desc', - 'order_by' => 'session_id', - 'count' => $totalCount, - ); - - if ($paginate['page'] < 0 || $paginate['limit'] < 0) { - throw new Exception('Invalid Page number'); - } - - $pagination = new Pagination($paginate['count'], $paginate['limit']); - $pagination->setPage($paginate['page']); - - $this->renderView('survey/show_results', array( - 'resultCount' => $count, - 'results' => $totalCount <= 0 ? array() : $this->study->getResults(null, $filter, $paginate), - 'pagination' => $pagination, - 'study_name' => $this->study->name, - 'session' => $this->request->str('session'), - 'rfilter' => $this->request->str('rfilter'), - 'results_filter' => $this->study->getResultsFilter(), - )); - } - - private function hideResults() { - $this->renderView('survey/show_results', array( - 'resultCount' => $this->study->getResultCount(), - 'results' => array(), - 'pagination' => new Pagination(1), - 'study_name' => $this->study->name, - )); - } - - private function deleteResultsAction() { - $study = $this->study; - - if (isset($_POST['delete']) AND trim($_POST['delete_confirm']) === $study->name) { - if ($study->deleteResults()): - alert(implode($study->messages), 'alert-info'); - alert("Success. All results in '{$study->name}' were deleted.", 'alert-success'); - else: - alert(implode($study->errors), 'alert-danger'); - endif; - redirect_to(admin_study_url($study->name, 'delete_results')); - } elseif (isset($_POST['delete'])) { - alert("Error: Survey's name must match '{$study->name}' to delete results.", 'alert-danger'); - } - - $this->renderView('survey/delete_results', array( - 'resultCount' => $study->getResultCount(), - )); - } - - private function deleteStudyAction() { - $study = $this->study; - - if (isset($_POST['delete']) AND trim($_POST['delete_confirm']) === $study->name) { - $study->delete(); - alert("Success. Successfully deleted study '{$study->name}'.", 'alert-success'); - redirect_to(admin_url()); - } elseif (isset($_POST['delete'])) { - alert("Error: You must type the study's name '{$study->name}' to delete it.", 'alert-danger'); - } - - $this->renderView('survey/delete_study', array( - 'resultCount' => $study->getResultCount(), - )); - } - - private function renameStudyAction() { - $study = $this->study; - $new_name = $this->request->str('new_name'); - - if ($new_name && $new_name !== $study->name) { - $old_name = $study->name; - if($study->rename($new_name)) { - alert("Success. Successfully renamed study from '{$old_name}' to {$study->name}.", 'alert-success'); - redirect_to(admin_study_url($study->name, 'rename_study')); - } - } - - $this->renderView('survey/rename_study', array('study_name' => $study->name)); - } - - private function exportItemTableAction() { - $study = $this->study; - - $format = $this->request->getParam('format'); - SpreadsheetReader::verifyExportFormat($format); - - $SPR = new SpreadsheetReader(); - - if ($format == 'original') { - $filename = $study->getOriginalFileName(); - $file = Config::get('survey_upload_dir') . '/' . $filename; - if (!is_file($file)) { - alert('The original file could not be found. Try another format', 'alert-danger'); - redirect_to(admin_study_url($study->name)); - } - - $type = 'application/vnd.ms-excel'; - //@todo get right type - - header('Content-Disposition: attachment;filename="' . $filename . '"'); - header('Cache-Control: max-age=0'); - header('Content-Type: ' . $type); - readfile($file); - exit; - } elseif ($format == 'xlsx') { - $SPR->exportItemTableXLSX($study); - } elseif ($format == 'xls') { - $SPR->exportItemTableXLS($study); - } else { - $SPR->exportItemTableJSON($study); - } - } - private function verifyThereIsExportableData($resultsStmt) { - if ($resultsStmt->rowCount() < 1) { - alert('No data to export!', 'alert-danger'); - redirect_to(admin_study_url($this->study->name, 'show_itemdisplay')); - } - } - - private function exportItemdisplayAction() { - if ($this->study->settings['hide_results']) { - return $this->hideResults(); - } - - $study = $this->study; - $format = $this->request->str('format'); - SpreadsheetReader::verifyExportFormat($format); - - /* @var $resultsStmt PDOStatement */ - $resultsStmt = $study->getItemDisplayResults(null, null, null, true); - $this->verifyThereIsExportableData($resultsStmt); - - $SPR = new SpreadsheetReader(); - $download_successfull = $SPR->exportInRequestedFormat($resultsStmt, $study->name, $format); - if (!$download_successfull) { - alert('An error occured during results download.', 'alert-danger'); - redirect_to(admin_study_url($filename, 'show_results')); + public function __construct(Site &$site) { + parent::__construct($site); + } + + public function indexAction($survey_name = '', $private_action = '') { + $this->setStudy($survey_name); + + if ($private_action) { + if (empty($this->study) || !$this->study->valid) { + throw new Exception("You cannot access this page with no valid study"); + } + $privateAction = $this->getPrivateAction($private_action); + return $this->$privateAction(); + } + + if ($this->request->isHTTPPostRequest()) { + $request = new Request($_POST); + $this->study->changeSettings($request->getParams()); + redirect_to(admin_study_url($this->study->name)); + } + + if (empty($this->study)) { + redirect_to(admin_url('survey/add_survey')); + } + + $this->renderView('survey/index', array( + 'survey_id' => $this->study->id, + )); + } + + public function listAction() { + $vars = array( + 'studies' => $this->user->getStudies('id DESC', null), + ); + $this->renderView('survey/list', $vars); + } + + public function addSurveyAction() { + $settings = $updates = array(); + if (Request::isHTTPPostRequest() && $this->request->google_sheet && $this->request->survey_name) { + $file = google_download_survey_sheet($this->request->survey_name, $this->request->google_sheet); + if (!$file) { + alert("Unable to download the file at '{$this->request->google_sheet}'", 'alert-danger'); + } else { + $updates['google_file_id'] = $file['google_id']; + } + } elseif (Request::isHTTPPostRequest() && !isset($_FILES['uploaded'])) { + alert('Error: You have to select an item table file here.', 'alert-danger'); + } elseif (isset($_FILES['uploaded'])) { + $file = $_FILES['uploaded']; + } + + if (!empty($file)) { + unset($_SESSION['study_id']); + unset($_GET['study_name']); + + $allowed_size = Config::get('admin_maximum_size_of_uploaded_files'); + if ($allowed_size && $file['size'] > $allowed_size * 1024 * 1024): + alert("File exceeds allowed size of {$allowed_size} MB", 'alert-danger'); + else: + $filename = basename($file['name']); + $survey_name = preg_filter("/^([a-zA-Z][a-zA-Z0-9_]{2,64})(-[a-z0-9A-Z]+)?\.[a-z]{3,4}$/", "$1", $filename); // take only the first part, before the dash if present or the dot + + $study = new Survey($this->fdb, null, array( + 'name' => $survey_name, + 'user_id' => $this->user->id + ), null, null); + + if ($study->createIndependently($settings, $updates)) { + $confirmed_deletion = true; + $created_new = true; + if ($study->uploadItemTable($file, $confirmed_deletion, $updates, $created_new)) { + alert('Success! New survey created!', 'alert-success'); + delete_tmp_file($file); + redirect_to(admin_study_url($study->name, 'show_item_table')); + } else { + alert('Bugger! A new survey was created, but there were problems with your item table. Please fix them and try again.', 'alert-danger'); + delete_tmp_file($file); + redirect_to(admin_study_url($study->name, 'upload_items')); + } } - } - - private function exportResultsAction() { - if ($this->study->settings['hide_results']) { - return $this->hideResults(); - } - - $study = $this->study; - $format = $this->request->str('format'); - - - /* @var $resultsStmt PDOStatement */ - $resultsStmt = $study->getResults(null, null, null, null, true); - $this->verifyThereIsExportableData($resultsStmt); - - $SPR = new SpreadsheetReader(); - $download_successfull = $SPR->exportInRequestedFormat($resultsStmt, $study->name, $format); - - if (!$download_successfull) { - alert('An error occured during results download.', 'alert-danger'); - redirect_to(admin_study_url($filename, 'show_results')); - } - } - - private function setStudy($name) { - if (!$name) { - return; - } - - $study = new Survey($this->fdb, null, array('name' => $name, 'user_id' => $this->user->id), null, null); - if (!$study->valid) { - formr_error(404, 'Not Found', 'Requested Survey does not exist or has been moved'); - } elseif (!$this->user->created($study)) { - formr_error(401, 'Unauthorized', 'You do not have access to modify this survey'); - } - - $google_id = $study->getGoogleFileId(); - $this->vars['google'] = array( - 'id' => $google_id, - 'link' => google_get_sheet_link($google_id), - 'name' => $study->name - ); - $this->study = $study; - } + endif; + delete_tmp_file($file); + } + + $vars = array('google' => array()); + $this->renderView('survey/add_survey', $vars); + } + + private function uploadItemsAction() { + $updates = array(); + $study = $this->study; + $google_id = $study->getGoogleFileId(); + $vars = array( + 'study_name' => $study->name, + 'google' => array('id' => $google_id, 'link' => google_get_sheet_link($google_id), 'name' => $study->name), + ); + + if (Request::isHTTPPostRequest()) { + $confirmed_deletion = false; + if (isset($this->request->delete_confirm)) { + $confirmed_deletion = $this->request->delete_confirm; + } + + if (trim($confirmed_deletion) == '') { + $confirmed_deletion = false; + } elseif ($confirmed_deletion === $study->name) { + $confirmed_deletion = true; + } else { + alert("Error: You confirmed the deletion of the study's results but your input did not match the study's name. Update aborted.", 'alert-danger'); + $confirmed_deletion = false; + redirect_to(admin_study_url($study->name, 'upload_items')); + return false; + } + } + + $file = null; + if (isset($_FILES['uploaded']) AND $_FILES['uploaded']['name'] !== "") { + $filename = basename($_FILES['uploaded']['name']); + $survey_name = preg_filter("/^([a-zA-Z][a-zA-Z0-9_]{2,64})(-[a-z0-9A-Z]+)?\.[a-z]{3,4}$/", "$1", $filename); // take only the first part, before the dash if present or the dot if present + + if ($study->name !== $survey_name) { + alert('Error: The uploaded file name ' . htmlspecialchars($survey_name) . ' did not match the study name ' . $study->name . '.', 'alert-danger'); + } else { + $file = $_FILES['uploaded']; + } + } elseif (Request::isHTTPPostRequest() && $this->request->google_sheet) { + $file = google_download_survey_sheet($study->name, $this->request->google_sheet); + if (!$file) { + alert("Unable to download the file at '{$this->request->google_id}'", 'alert-danger'); + } else { + $updates['google_file_id'] = $file['google_id']; + } + } elseif (Request::isHTTPPostRequest()) { + alert('Error: You have to select an item table file or enter a Google link here.', 'alert-danger'); + } + + if (!empty($file)) { + $allowed_size = Config::get('admin_maximum_size_of_uploaded_files'); + if ($allowed_size && $file['size'] > $allowed_size * 1024 * 1024): + alert("File exceeds allowed size of {$allowed_size} MB", 'alert-danger'); + redirect_to(admin_study_url($study->name, 'upload_items')); + return false; + endif; + + if (($filename = $study->getOriginalFileName()) && files_are_equal($file['tmp_name'], Config::get('survey_upload_dir') . '/' . $filename)) { + alert("Uploaded item table was identical to last uploaded item table.
+ No changes carried out.", 'alert-info'); + $success = false; + } else { + $success = $study->uploadItemTable($file, $confirmed_deletion, $updates, false); + } + + delete_tmp_file($file); + if ($success) { + redirect_to(admin_study_url($study->name, 'show_item_table')); + } + } + + $this->renderView('survey/upload_items', $vars); + } + + private function accessAction() { + $study = $this->study; + if ($this->user->created($study)): + $session = new UnitSession($this->fdb, null, $study->id); + $session->create(); + + Session::set('dummy_survey_session', array( + "session_id" => $session->id, + "unit_id" => $study->id, + "run_session_id" => $session->run_session_id, + "run_name" => Run::TEST_RUN, + "survey_name" => $study->name + )); + + alert("Go ahead. You can test the study " . $study->name . " now.", 'alert-info'); + redirect_to(run_url(Run::TEST_RUN)); + else: + alert("Sorry. You don't have access to this study", 'alert-danger'); + redirect_to("index"); + endif; + } + + private function showItemTableAction() { + $vars = array( + 'google_id' => $this->study->getGoogleFileId(), + 'original_file' => $this->study->getOriginalFileName(), + 'results' => $this->study->getItemsWithChoices(), + 'shortcut' => $this->request->str('to', null) + ); + + if (empty($vars['results'])) { + alert("No valid item table uploaded so far.", 'alert-warning'); + redirect_to(admin_study_url($this->study->name, 'upload_items')); + } else { + $this->renderView('survey/show_item_table', $vars); + } + } + + private function showItemdisplayAction() { + if ($this->study->settings['hide_results']) { + return $this->hideResults(); + } + + $filter = array( + 'session' => $this->request->str('session'), + 'results' => $this->request->str('rfilter'), + ); + + // paginate based on number of items on this sheet so that each + // run session will have all items for each pagination + $items = $this->study->getItems('id'); + $ids = array(); + foreach ($items as $item) { + $ids[] = $item['id']; + } + + //$session = $this->request->str('session', null); + // show $no_sessions sessions per_page (so limit = $no_sessions * number of items in a survey) + //$limit = $this->request->int('per_page', $no_sessions * $itemsCount); + + $no_sessions = $this->request->int('sess_per_page', 10); + $count = $this->study->getResultCount(null, $filter); + $totalCount = $count['real_users'] + $count['testers']; + $limit = $no_sessions; + $page = ($this->request->int('page', 1) - 1); + $paginate = array( + 'limit' => $limit, + 'page' => $page, + 'offset' => $limit * $page, + 'count' => $totalCount, + ); + + if ($paginate['page'] < 0 || $paginate['limit'] < 0) { + throw new Exception('Invalid Page number'); + } + + $pagination = new Pagination($paginate['count'], $paginate['limit']); + $pagination->setPage($paginate['page']); + + $this->renderView('survey/show_itemdisplay', array( + 'resultCount' => $this->study->getResultCount(), + 'results' => $totalCount ? $this->study->getResultsByItemsPerSession(null, $filter, $paginate) : array(), + 'pagination' => $pagination, + 'study_name' => $this->study->name, + //'session' => $session, + 'session' => $this->request->str('session'), + 'rfilter' => $this->request->str('rfilter'), + 'results_filter' => $this->study->getResultsFilter(), + )); + } + + private function showResultsAction() { + if ($this->study->settings['hide_results']) { + return $this->hideResults(); + } + + $filter = array( + 'session' => $this->request->str('session'), + 'results' => $this->request->str('rfilter'), + ); + + $count = $this->study->getResultCount(null, $filter); + $totalCount = $count['real_users'] + $count['testers']; + $limit = $this->request->int('per_page', 100); + $page = ($this->request->int('page', 1) - 1); + $paginate = array( + 'limit' => $limit, + 'page' => $page, + 'offset' => $limit * $page, + 'order' => 'desc', + 'order_by' => 'session_id', + 'count' => $totalCount, + ); + + if ($paginate['page'] < 0 || $paginate['limit'] < 0) { + throw new Exception('Invalid Page number'); + } + + $pagination = new Pagination($paginate['count'], $paginate['limit']); + $pagination->setPage($paginate['page']); + + $this->renderView('survey/show_results', array( + 'resultCount' => $count, + 'results' => $totalCount <= 0 ? array() : $this->study->getResults(null, $filter, $paginate), + 'pagination' => $pagination, + 'study_name' => $this->study->name, + 'session' => $this->request->str('session'), + 'rfilter' => $this->request->str('rfilter'), + 'results_filter' => $this->study->getResultsFilter(), + )); + } + + private function hideResults() { + $this->renderView('survey/show_results', array( + 'resultCount' => $this->study->getResultCount(), + 'results' => array(), + 'pagination' => new Pagination(1), + 'study_name' => $this->study->name, + )); + } + + private function deleteResultsAction() { + $study = $this->study; + + if (isset($_POST['delete']) AND trim($_POST['delete_confirm']) === $study->name) { + if ($study->deleteResults()) { + alert(implode($study->messages), 'alert-info'); + alert("Success. All results in '{$study->name}' were deleted.", 'alert-success'); + } else { + alert(implode($study->errors), 'alert-danger'); + } + redirect_to(admin_study_url($study->name, 'delete_results')); + } elseif (isset($_POST['delete'])) { + alert("Error: Survey's name must match '{$study->name}' to delete results.", 'alert-danger'); + } + + $this->renderView('survey/delete_results', array( + 'resultCount' => $study->getResultCount(), + )); + } + + private function deleteStudyAction() { + $study = $this->study; + + if (isset($_POST['delete']) AND trim($_POST['delete_confirm']) === $study->name) { + $study->delete(); + alert("Success. Successfully deleted study '{$study->name}'.", 'alert-success'); + redirect_to(admin_url()); + } elseif (isset($_POST['delete'])) { + alert("Error: You must type the study's name '{$study->name}' to delete it.", 'alert-danger'); + } + + $this->renderView('survey/delete_study', array( + 'resultCount' => $study->getResultCount(), + )); + } + + private function renameStudyAction() { + $study = $this->study; + $new_name = $this->request->str('new_name'); + + if ($new_name && $new_name !== $study->name) { + $old_name = $study->name; + if ($study->rename($new_name)) { + alert("Success. Successfully renamed study from '{$old_name}' to {$study->name}.", 'alert-success'); + redirect_to(admin_study_url($study->name, 'rename_study')); + } + } + + $this->renderView('survey/rename_study', array('study_name' => $study->name)); + } + + private function exportItemTableAction() { + $study = $this->study; + + $format = $this->request->getParam('format'); + SpreadsheetReader::verifyExportFormat($format); + + $SPR = new SpreadsheetReader(); + + if ($format == 'original') { + $filename = $study->getOriginalFileName(); + $file = Config::get('survey_upload_dir') . '/' . $filename; + if (!is_file($file)) { + alert('The original file could not be found. Try another format', 'alert-danger'); + redirect_to(admin_study_url($study->name)); + } + + $type = 'application/vnd.ms-excel'; + //@todo get right type + + header('Content-Disposition: attachment;filename="' . $filename . '"'); + header('Cache-Control: max-age=0'); + header('Content-Type: ' . $type); + readfile($file); + exit; + } elseif ($format == 'xlsx') { + $SPR->exportItemTableXLSX($study); + } elseif ($format == 'xls') { + $SPR->exportItemTableXLS($study); + } else { + $SPR->exportItemTableJSON($study); + } + } + + private function verifyThereIsExportableData($resultsStmt) { + if ($resultsStmt->rowCount() < 1) { + alert('No data to export!', 'alert-danger'); + redirect_to(admin_study_url($this->study->name, 'show_itemdisplay')); + } + } + + private function exportItemdisplayAction() { + if ($this->study->settings['hide_results']) { + return $this->hideResults(); + } + + $study = $this->study; + $format = $this->request->str('format'); + SpreadsheetReader::verifyExportFormat($format); + + /* @var $resultsStmt PDOStatement */ + $resultsStmt = $study->getItemDisplayResults(null, null, null, true); + $this->verifyThereIsExportableData($resultsStmt); + + $SPR = new SpreadsheetReader(); + $download_successfull = $SPR->exportInRequestedFormat($resultsStmt, $study->name, $format); + if (!$download_successfull) { + alert('An error occured during results download.', 'alert-danger'); + redirect_to(admin_study_url($filename, 'show_results')); + } + } + + private function exportResultsAction() { + if ($this->study->settings['hide_results']) { + return $this->hideResults(); + } + + $study = $this->study; + $format = $this->request->str('format'); + + + /* @var $resultsStmt PDOStatement */ + $resultsStmt = $study->getResults(null, null, null, null, true); + $this->verifyThereIsExportableData($resultsStmt); + + $SPR = new SpreadsheetReader(); + $download_successfull = $SPR->exportInRequestedFormat($resultsStmt, $study->name, $format); + + if (!$download_successfull) { + alert('An error occured during results download.', 'alert-danger'); + redirect_to(admin_study_url($filename, 'show_results')); + } + } + + private function setStudy($name) { + if (!$name) { + return; + } + + $study = new Survey($this->fdb, null, array('name' => $name, 'user_id' => $this->user->id), null, null); + if (!$study->valid) { + formr_error(404, 'Not Found', 'Requested Survey does not exist or has been moved'); + } elseif (!$this->user->created($study)) { + formr_error(401, 'Unauthorized', 'You do not have access to modify this survey'); + } + + $google_id = $study->getGoogleFileId(); + $this->vars['google'] = array( + 'id' => $google_id, + 'link' => google_get_sheet_link($google_id), + 'name' => $study->name + ); + $this->study = $study; + } } diff --git a/application/Controller/ApiController.php b/application/Controller/ApiController.php index cc36341ec..2da19d031 100644 --- a/application/Controller/ApiController.php +++ b/application/Controller/ApiController.php @@ -1,217 +1,217 @@ initialize(); - } - - public function indexAction() { - $this->sendResponse(Response::STATUS_FORBIDDEN, 'Invalid', array('error' => 'No Entry Point')); - } - - public function oauthAction($action = null) { - if (!$this->isValidAction('oauth', $action)) { - $this->response->badRequest('Invalid Auth Request'); - } - - $this->oauthServer = Site::getOauthServer(); - if ($action === 'authorize') { - $this->authorize(); - } elseif ($action === 'access_token') { - $this->access_token(); - } - } - - public function postAction($action = null) { - if (!Request::isHTTPPostRequest()) { - $this->response->badMethod('Invalid Request Method'); - } - - if (!$this->isValidAction('post', $action)) { - $this->response->badRequest('Invalid Post Request'); - } - - $this->doAction($this->post, $action); - } - - public function getAction($action = null) { - if (!Request::isHTTPGetRequest()) { - $this->response->badMethod('Invalid Request Method'); - } - - if (!$this->isValidAction('get', $action)) { - $this->response->badRequest('Invalid Get Request'); - } - - $this->doAction($this->get, $action); - } - - public function osfAction($do = '') { - $user = Site::getCurrentUser(); - if (!$user->loggedIn()) { - alert('You need to login to access this section', 'alert-warning'); - redirect_to('login'); - } - - $osfconfg = Config::get('osf'); - $osfconfg['state'] = $user->user_code; - - $osf = new OSF($osfconfg); - - // Case 1: User wants to login to give formr authorization - // If user has a valid access token then just use it (i.e redirect to where access token is needed - if ($do === 'login') { - $redirect = $this->request->getParam('redirect', 'admin/osf') . '#osf'; - if ($token = OSF::getUserAccessToken($user)) { - // redirect user to where he came from and get access token from there for current user - alert('You have authorized FORMR to act on your behalf on the OSF', 'alert-success'); - } else { - Session::set('formr_redirect', $redirect); - // redirect user to login link - $redirect = $osf->getLoginUrl(); - } - redirect_to($redirect); - } - - // Case 2: User is oauth2-ing. Handle authorization code exchange - if ($code = $this->request->getParam('code')) { - if ($this->request->getParam('state') != $user->user_code) { - throw new Exception("Invalid OSF-OAUTH 2.0 request"); - } - - $params = $this->request->getParams(); - try { - $logged = $osf->login($params); - } catch (Exception $e) { - formr_log_exception($e, 'OSF'); - $logged = array('error' => $e->getMessage()); - } - - if (!empty($logged['access_token'])) { - // save this access token for this user - // redirect user to where osf actions need to happen (@todo pass this in a 'redirect session parameter' - OSF::setUserAccessToken($user, $logged); - alert('You have authorized FORMR to act on your behalf on the OSF', 'alert-success'); - if ($redirect = Session::get('formr_redirect')) { - Session::delete('formr_redirect'); - } else { - $redirect = 'admin/osf'; - } - redirect_to($redirect); - } else { - $error = !empty($logged['error']) ? $logged['error'] : 'Access token could not be obtained'; - alert('OSF API Error: ' . $error, 'alert-danger'); - redirect_to('admin'); - } - } - - // Case 3: User is oauth2-ing. Handle case when user cancels authorization - if ($error = $this->request->getParam('error')) { - alert('Access was denied at OSF-Formr with error code: ' . $error, 'alert-danger'); - redirect_to('admin'); - } - - redirect_to('index'); - } - - - protected function doAction(Request $request, $action) { - try { - $this->authenticate($action); // only proceed if authenticated, if not exit via response - $method = $this->getPrivateAction($action, '-', true); - $helper = new ApiHelper($request, $this->fdb); - $data = $helper->{$method}()->getData(); - } catch (Exception $e) { - formr_log_exception($e, 'API'); - $data = array( - 'statusCode' => Response::STATUS_INTERNAL_SERVER_ERROR, - 'statusText' => 'Internal Server Error', - 'response' => array('error' => 'An unexpected error occured', 'error_code' => Response::STATUS_INTERNAL_SERVER_ERROR), - ); - } - - $this->sendResponse($data['statusCode'], $data['statusText'], $data['response']); - } - - protected function isValidAction($type, $action) { - $actions = array( - 'oauth' => array('authorize', 'access_token'), - 'post' => array('create-session', 'end-last-external'), - 'get' => array('results'), - ); - - return isset($actions[$type]) && in_array($action, $actions[$type]); - } - - protected function authorize() { - /* - * @todo - * Implement authorization under oauth - */ - $this->response->badRequest('Not Implemented'); - } - - protected function access_token() { - // Ex: curl -u testclient:testpass http://formr.org/api/oauth/token -d 'grant_type=client_credentials' - $this->oauthServer->handleTokenRequest(OAuth2\Request::createFromGlobals())->send(); - } - - protected function authenticate($action) { - $publicActions = array("end-last-external"); - if(!in_array($action, $publicActions) ) { - $this->oauthServer = Site::getOauthServer(); - // Handle a request to a resource and authenticate the access token - // Ex: curl http://formr.org/api/post/action-name -d 'access_token=YOUR_TOKEN' - if (!$this->oauthServer->verifyResourceRequest(OAuth2\Request::createFromGlobals())) { - $this->sendResponse(Response::STATUS_UNAUTHORIZED, 'Unauthorized Access', array( - 'error' => 'Invalid/Unauthorized access token', - 'error_code' => Response::STATUS_UNAUTHORIZED, - 'error_description' => 'Access token for this resouce request is invalid or unauthorized', - )); - } - } - } - - protected function sendResponse($statusCode = Response::STATUS_OK, $statusText = 'OK', $response = null) { - $this->response->setStatusCode($statusCode, $statusText); - $this->response->setContentType('application/json'); - $this->response->setJsonContent($response); - $this->response->send(); - } - - protected function initialize() { - $this->post = new Request($_POST); - $this->get = new Request($_GET); - $this->response = new Response(); - } + /** + * POST Request variables + * + * @var Request + */ + protected $post; + + /** + * GET Request variables + * + * @var Request + */ + protected $get; + + /** + * @var Response + */ + protected $response; + + /** + * @var OAuth2\Server + */ + protected $oauthServer; + + public function __construct(Site &$site) { + parent::__construct($site); + $this->initialize(); + } + + public function indexAction() { + $this->sendResponse(Response::STATUS_FORBIDDEN, 'Invalid', array('error' => 'No Entry Point')); + } + + public function oauthAction($action = null) { + if (!$this->isValidAction('oauth', $action)) { + $this->response->badRequest('Invalid Auth Request'); + } + + $this->oauthServer = Site::getOauthServer(); + if ($action === 'authorize') { + $this->authorize(); + } elseif ($action === 'access_token') { + $this->access_token(); + } + } + + public function postAction($action = null) { + if (!Request::isHTTPPostRequest()) { + $this->response->badMethod('Invalid Request Method'); + } + + if (!$this->isValidAction('post', $action)) { + $this->response->badRequest('Invalid Post Request'); + } + + $this->doAction($this->post, $action); + } + + public function getAction($action = null) { + if (!Request::isHTTPGetRequest()) { + $this->response->badMethod('Invalid Request Method'); + } + + if (!$this->isValidAction('get', $action)) { + $this->response->badRequest('Invalid Get Request'); + } + + $this->doAction($this->get, $action); + } + + public function osfAction($do = '') { + $user = Site::getCurrentUser(); + if (!$user->loggedIn()) { + alert('You need to login to access this section', 'alert-warning'); + redirect_to('login'); + } + + $osfconfg = Config::get('osf'); + $osfconfg['state'] = $user->user_code; + + $osf = new OSF($osfconfg); + + // Case 1: User wants to login to give formr authorization + // If user has a valid access token then just use it (i.e redirect to where access token is needed + if ($do === 'login') { + $redirect = $this->request->getParam('redirect', 'admin/osf') . '#osf'; + if ($token = OSF::getUserAccessToken($user)) { + // redirect user to where he came from and get access token from there for current user + alert('You have authorized FORMR to act on your behalf on the OSF', 'alert-success'); + } else { + Session::set('formr_redirect', $redirect); + // redirect user to login link + $redirect = $osf->getLoginUrl(); + } + redirect_to($redirect); + } + + // Case 2: User is oauth2-ing. Handle authorization code exchange + if ($code = $this->request->getParam('code')) { + if ($this->request->getParam('state') != $user->user_code) { + throw new Exception("Invalid OSF-OAUTH 2.0 request"); + } + + $params = $this->request->getParams(); + try { + $logged = $osf->login($params); + } catch (Exception $e) { + formr_log_exception($e, 'OSF'); + $logged = array('error' => $e->getMessage()); + } + + if (!empty($logged['access_token'])) { + // save this access token for this user + // redirect user to where osf actions need to happen (@todo pass this in a 'redirect session parameter' + OSF::setUserAccessToken($user, $logged); + alert('You have authorized FORMR to act on your behalf on the OSF', 'alert-success'); + if ($redirect = Session::get('formr_redirect')) { + Session::delete('formr_redirect'); + } else { + $redirect = 'admin/osf'; + } + redirect_to($redirect); + } else { + $error = !empty($logged['error']) ? $logged['error'] : 'Access token could not be obtained'; + alert('OSF API Error: ' . $error, 'alert-danger'); + redirect_to('admin'); + } + } + + // Case 3: User is oauth2-ing. Handle case when user cancels authorization + if ($error = $this->request->getParam('error')) { + alert('Access was denied at OSF-Formr with error code: ' . $error, 'alert-danger'); + redirect_to('admin'); + } + + redirect_to('index'); + } + + protected function doAction(Request $request, $action) { + try { + $this->authenticate($action); // only proceed if authenticated, if not exit via response + $method = $this->getPrivateAction($action, '-', true); + $helper = new ApiHelper($request, $this->fdb); + $data = $helper->{$method}()->getData(); + } catch (Exception $e) { + formr_log_exception($e, 'API'); + $data = array( + 'statusCode' => Response::STATUS_INTERNAL_SERVER_ERROR, + 'statusText' => 'Internal Server Error', + 'response' => array('error' => 'An unexpected error occured', 'error_code' => Response::STATUS_INTERNAL_SERVER_ERROR), + ); + } + + $this->sendResponse($data['statusCode'], $data['statusText'], $data['response']); + } + + protected function isValidAction($type, $action) { + $actions = array( + 'oauth' => array('authorize', 'access_token'), + 'post' => array('create-session', 'end-last-external'), + 'get' => array('results'), + ); + + return isset($actions[$type]) && in_array($action, $actions[$type]); + } + + protected function authorize() { + /* + * @todo + * Implement authorization under oauth + */ + $this->response->badRequest('Not Implemented'); + } + + protected function access_token() { + // Ex: curl -u testclient:testpass http://formr.org/api/oauth/token -d 'grant_type=client_credentials' + $this->oauthServer->handleTokenRequest(OAuth2\Request::createFromGlobals())->send(); + } + + protected function authenticate($action) { + $publicActions = array("end-last-external"); + if (!in_array($action, $publicActions)) { + $this->oauthServer = Site::getOauthServer(); + // Handle a request to a resource and authenticate the access token + // Ex: curl http://formr.org/api/post/action-name -d 'access_token=YOUR_TOKEN' + if (!$this->oauthServer->verifyResourceRequest(OAuth2\Request::createFromGlobals())) { + $this->sendResponse(Response::STATUS_UNAUTHORIZED, 'Unauthorized Access', array( + 'error' => 'Invalid/Unauthorized access token', + 'error_code' => Response::STATUS_UNAUTHORIZED, + 'error_description' => 'Access token for this resouce request is invalid or unauthorized', + )); + } + } + } + + protected function sendResponse($statusCode = Response::STATUS_OK, $statusText = 'OK', $response = null) { + $this->response->setStatusCode($statusCode, $statusText); + $this->response->setContentType('application/json'); + $this->response->setJsonContent($response); + $this->response->send(); + } + + protected function initialize() { + $this->post = new Request($_POST); + $this->get = new Request($_GET); + $this->response = new Response(); + } } diff --git a/application/Controller/Controller.php b/application/Controller/Controller.php index e61f53147..9e5554005 100644 --- a/application/Controller/Controller.php +++ b/application/Controller/Controller.php @@ -2,185 +2,182 @@ abstract class Controller { - /** - * - * @var Site - */ - protected $site; - - /** - * - * @var User - */ - protected $user; - - /** - * - * @var Run - */ - public $run; - - /** - * - * @var DB - */ - protected $fdb; - - /** - * - * @var Survey - */ - public $study; - - /** - * - * @var Request - */ - protected $request; - - protected $css = array(); - - protected $js = array(); - - protected $vars = array(); - - public function __construct(Site &$site) { - /** @todo do these with dependency injection */ - global $user, $run, $study, $css, $js; - $this->site = $site; - $this->user = &$user; - $this->study = $study; - $this->run = $run; - if (is_array($css)) { - $this->css = $css; - } - if (is_array($js)) { - $this->js = $js; - } - - $this->fdb = DB::getInstance(); - $this->request = $site->request; - } - - protected function renderView($template, $vars = array()) { - $variables = array_merge(array( - 'site' => $this->site, - 'user' => $this->user, - 'fdb' => $this->fdb, - 'js' => $this->js, - 'css' => $this->css, - 'run' => $this->run, - 'study' => $this->study, - 'meta' => $this->generateMetaInfo(), - ), $this->vars, $vars); - Request::setGlobals('variables', $variables); - Template::load($template); - } - - protected function getPrivateAction($name, $separator = '_', $protected = false) { - $parts = array_filter(explode($separator, $name)); - $action = array_shift($parts); - foreach ($parts as $part) { - $action .= ucwords(strtolower($part)); - } - - if ($protected) { - return $action; - } - - $method = $action . 'Action'; - if (!method_exists($this, $method)) { - throw new Exception("Action '$name' is not found."); - } - return $method; - } - - /** - * @return Site - */ - public function getSite() { - return $this->site; - } - - public function getDB() { - return $this->fdb; - } - - protected function registerCSS($files, $name) { - $this->addFiles($files, $name, 'css'); - } - - protected function registerJS($files, $name) { - $this->addFiles($files, $name, 'js'); - } - - private function addFiles($files, $name, $type) { - if (!$files || !in_array($type, array('js', 'css'))) { - return; - } - if (!is_array($files)) { - $files = array($files); - } - foreach (array_filter($files) as $file) { - if (!isset($this->{$type}[$name])) { - $this->{$type}[$name] = array(); - } - $this->{$type}[$name][] = $file; - } - } - - protected function registerAssets($which) { - $assets = get_assets(); - if (!is_array($which)) { - $which = array($which); - } - foreach ($which as $asset) { - $this->registerCSS(array_val($assets[$asset], 'css'), $asset); - $this->registerJS(array_val($assets[$asset], 'js'), $asset); - } - } - - protected function unregisterAssets($which) { - if (is_array($which)) { - foreach ($which as $a) { - $this->unregisterAssets($a); - } - } - if (isset($this->css[$which])) { - unset($this->css[$which]); - } - if (isset($this->js[$which])) { - unset($this->js[$which]); - } - } - - protected function replaceAssets($old, $new, $module = 'site') { - $assets = get_default_assets($module); - foreach ($assets as $i => $asset) { - if ($asset === $old) { - $assets[$i] = $new; - } - } - $this->css = $this->js = array(); - $this->registerAssets($assets); - } - - protected function generateMetaInfo() { - $meta = array( - 'head_title' => $this->site->makeTitle(), - 'title' => 'formr - an online survey framework with live feedback', - 'description' => 'formr survey framework. chain simple surveys into long runs, use the power of R to generate pretty feedback and complex designs', - 'keywords' => 'formr, online survey, R software, opencpu, live feedback', - 'author' => 'formr.org', - 'url' => site_url(), - 'image' => asset_url('build/img/formr-og.png'), - ); - - return $meta; - } - - public function errorAction($code = null, $text = null) { - formr_error($code, $text); - } + /** + * + * @var Site + */ + protected $site; + + /** + * + * @var User + */ + protected $user; + + /** + * + * @var Run + */ + public $run; + + /** + * + * @var DB + */ + protected $fdb; + + /** + * + * @var Survey + */ + public $study; + + /** + * + * @var Request + */ + protected $request; + protected $css = array(); + protected $js = array(); + protected $vars = array(); + + public function __construct(Site &$site) { + /** @todo do these with dependency injection */ + global $user, $run, $study, $css, $js; + $this->site = $site; + $this->user = &$user; + $this->study = $study; + $this->run = $run; + if (is_array($css)) { + $this->css = $css; + } + if (is_array($js)) { + $this->js = $js; + } + + $this->fdb = DB::getInstance(); + $this->request = $site->request; + } + + protected function renderView($template, $vars = array()) { + $variables = array_merge(array( + 'site' => $this->site, + 'user' => $this->user, + 'fdb' => $this->fdb, + 'js' => $this->js, + 'css' => $this->css, + 'run' => $this->run, + 'study' => $this->study, + 'meta' => $this->generateMetaInfo(), + ), $this->vars, $vars); + Request::setGlobals('variables', $variables); + Template::load($template); + } + + protected function getPrivateAction($name, $separator = '_', $protected = false) { + $parts = array_filter(explode($separator, $name)); + $action = array_shift($parts); + foreach ($parts as $part) { + $action .= ucwords(strtolower($part)); + } + + if ($protected) { + return $action; + } + + $method = $action . 'Action'; + if (!method_exists($this, $method)) { + throw new Exception("Action '$name' is not found."); + } + return $method; + } + + /** + * @return Site + */ + public function getSite() { + return $this->site; + } + + public function getDB() { + return $this->fdb; + } + + protected function registerCSS($files, $name) { + $this->addFiles($files, $name, 'css'); + } + + protected function registerJS($files, $name) { + $this->addFiles($files, $name, 'js'); + } + + private function addFiles($files, $name, $type) { + if (!$files || !in_array($type, array('js', 'css'))) { + return; + } + if (!is_array($files)) { + $files = array($files); + } + foreach (array_filter($files) as $file) { + if (!isset($this->{$type}[$name])) { + $this->{$type}[$name] = array(); + } + $this->{$type}[$name][] = $file; + } + } + + protected function registerAssets($which) { + $assets = get_assets(); + if (!is_array($which)) { + $which = array($which); + } + foreach ($which as $asset) { + $this->registerCSS(array_val($assets[$asset], 'css'), $asset); + $this->registerJS(array_val($assets[$asset], 'js'), $asset); + } + } + + protected function unregisterAssets($which) { + if (is_array($which)) { + foreach ($which as $a) { + $this->unregisterAssets($a); + } + } + if (isset($this->css[$which])) { + unset($this->css[$which]); + } + if (isset($this->js[$which])) { + unset($this->js[$which]); + } + } + + protected function replaceAssets($old, $new, $module = 'site') { + $assets = get_default_assets($module); + foreach ($assets as $i => $asset) { + if ($asset === $old) { + $assets[$i] = $new; + } + } + $this->css = $this->js = array(); + $this->registerAssets($assets); + } + + protected function generateMetaInfo() { + $meta = array( + 'head_title' => $this->site->makeTitle(), + 'title' => 'formr - an online survey framework with live feedback', + 'description' => 'formr survey framework. chain simple surveys into long runs, use the power of R to generate pretty feedback and complex designs', + 'keywords' => 'formr, online survey, R software, opencpu, live feedback', + 'author' => 'formr.org', + 'url' => site_url(), + 'image' => asset_url('build/img/formr-og.png'), + ); + + return $meta; + } + + public function errorAction($code = null, $text = null) { + formr_error($code, $text); + } } diff --git a/application/Controller/PublicController.php b/application/Controller/PublicController.php index 93f5365c9..bdd7bfa2c 100644 --- a/application/Controller/PublicController.php +++ b/application/Controller/PublicController.php @@ -1,224 +1,224 @@ registerAssets($default_assets); - } - } - - public function indexAction() { - $this->renderView('public/home'); - } - - public function documentationAction() { - $this->renderView('public/documentation'); - } - - public function studiesAction() { - $this->renderView('public/studies', array('runs' => $this->user->getAvailableRuns())); - } - - public function aboutAction() { - $this->renderView('public/about', array('bodyClass' => 'fmr-about')); - } - - public function publicationsAction() { - $this->renderView('public/publications'); - } - - public function accountAction() { - /** - * @todo: - * - allow changing email address - * - email address verification - * - my access code has been compromised, reset? possible problems with external data, maybe they should get their own tokens... - */ - - if(!$this->user->loggedIn()) { - alert('You need to be logged in to go here.', 'alert-info'); - redirect_to('login'); - } - - $vars = array('showform' => false); - if($this->request->isHTTPPostRequest()) { - $redirect = false; - $oldEmail = $this->user->email; - - // Change basic info + email - $change = $this->user->changeData($this->request->str('password'), $this->request->getParams()); - if (!$change) { - alert(nl2br(implode($this->user->errors, "\n")), 'alert-danger'); - $vars['showform'] = 'show-form'; - } elseif ($oldEmail != $this->request->str('new_email')) { - $redirect = 'logout'; - } - - // Change password - if($this->request->str('new_password')) { - if ($this->request->str('new_password') !== $this->request->str('new_password_c')) { - alert('The new passwords do not match', 'alert-danger'); - $vars['showform'] = 'show-form'; - } elseif($this->user->changePassword($this->request->str('password'), $this->request->str('new_password'))) { - alert('Success! Your password was changed! Please sign-in with your new password.', 'alert-success'); - $redirect = 'logout'; - } else { - alert(implode($this->user->errors), 'alert-danger'); - $vars['showform'] = 'show-form'; - } - } - - if($redirect) { - redirect_to($redirect); - } - } - - $vars['user'] = $this->user; - $vars['joined'] = date('jS F Y', strtotime($this->user->created)); - $vars['studies'] = $this->fdb->count('survey_runs', array('user_id' => $this->user->id)); - $vars['names'] = sprintf('%s %s', $this->user->first_name, $this->user->last_name); - if ('' === trim($vars['names'])) { - $vars['names'] = $this->user->email; - } - $vars['affiliation'] = $this->user->affiliation ? $this->user->affiliation : '(no affiliation specified)'; - - $this->registerAssets('bootstrap-material-design'); - $this->renderView('public/account', $vars); - } - - public function loginAction() { - if($this->user->loggedIn()) { - redirect_to('account'); - } - - if($this->request->str('email') && $this->request->str('password')) { - $info = array( - 'email' => $this->request->str('email'), - 'password' => $this->request->str('password'), - ); - if($this->user->login($info)) { - alert('Success! You were logged in!', 'alert-success'); - Session::set('user', serialize($this->user)); - $redirect = $this->user->isAdmin() ? redirect_to('admin') : redirect_to('account'); - } else { - alert(implode($this->user->errors), 'alert-danger'); - } - } - - $this->registerAssets('bootstrap-material-design'); - $this->renderView('public/login', array('alerts' => $this->site->renderAlerts())); - } - - public function logoutAction() { - $user = $this->user; - if($user->loggedIn()) { - alert('You have been logged out!', 'alert-info'); - $alerts = $this->site->renderAlerts(); - $user->logout(); - $this->registerAssets('bootstrap-material-design'); - $this->renderView('public/login', array('alerts' => $alerts)); - } else { - Session::destroy(); - $redirect_to = $this->request->getParam('_rdir'); - redirect_to($redirect_to); - } - } - - public function registerAction() { - $user = $this->user; - $site = $this->site; - - //fixme: cookie problems lead to fatal error with missing user code - if($user->loggedIn()) { - alert('You were already logged in. Please logout before you can register.', 'alert-info'); - redirect_to("index"); - } - - if($this->request->isHTTPPostRequest() && $site->request->str('email')) { - $info = array( - 'email' => $site->request->str('email'), - 'password' => $site->request->str('password'), - 'referrer_code' => $site->request->str('referrer_code'), - ); - if($user->register($info)) { - //alert('Success! You were registered and logged in!','alert-success'); - redirect_to('index'); - } else { - alert(implode($user->errors),'alert-danger'); - } - } - - $this->registerAssets('bootstrap-material-design'); - $this->renderView('public/register'); - } - - public function verifyEmailAction() { - $user = $this->user; - $verification_token = $this->request->str('verification_token'); - $email = $this->request->str('email'); - - if ($this->request->isHTTPGetRequest() && $this->request->str('token')) { - $user->resendVerificationEmail($this->request->str('token')); - redirect_to('login'); - } elseif (!$verification_token || !$email) { - alert("You need to follow the link you received in your verification mail."); - redirect_to('login'); - } else { - $user->verify_email($email, $verification_token); - redirect_to('login'); - }; - } - - public function forgotPasswordAction() { - if($this->user->loggedIn()) { - redirect_to("index"); - } - - if($this->request->str('email')) { - $this->user->forgot_password($this->request->str('email')); - } - - $this->registerAssets('bootstrap-material-design'); - $this->renderView('public/forgot_password'); - } - - public function resetPasswordAction() { - $user = $this->user; - if($user->loggedIn()) { - redirect_to('index'); - } - - if ($this->request->isHTTPGetRequest() && (!$this->request->str('email') || !$this->request->str('reset_token')) && !$this->request->str('ok')) { - alert('You need to follow the link you received in your password reset mail'); - redirect_to('forgot_password'); - } elseif ($this->request->isHTTPPostRequest()) { - $postRequest = new Request($_POST); - $info = array( - 'email' => $postRequest->str('email'), - 'reset_token' => $postRequest->str('reset_token'), - 'new_password' => $postRequest->str('new_password'), - 'new_password_confirm' => $postRequest->str('new_password_c'), - ); - if (($done = $user->reset_password($info))) { - redirect_to('forgot_password'); - } - } - - $this->registerAssets('bootstrap-material-design'); - $this->renderView('public/reset_password', array( - 'reset_data_email' => $this->request->str('email'), - 'reset_data_token' => $this->request->str('reset_token'), - )); - } - - public function fileDownloadAction($run_id = 0, $original_filename = '') { - $path = $this->fdb->findValue('survey_uploaded_files', array('run_id' => (int)$run_id, 'original_file_name' => $original_filename), array('new_file_path')); - if ($path) { - return redirect_to(asset_url($path)); - } - formr_error(404, 'Not Found', 'The requested file does not exist'); - } + + public function __construct(Site &$site) { + parent::__construct($site); + if (!Request::isAjaxRequest()) { + $default_assets = get_default_assets('site'); + $this->registerAssets($default_assets); + } + } + + public function indexAction() { + $this->renderView('public/home'); + } + + public function documentationAction() { + $this->renderView('public/documentation'); + } + + public function studiesAction() { + $this->renderView('public/studies', array('runs' => $this->user->getAvailableRuns())); + } + + public function aboutAction() { + $this->renderView('public/about', array('bodyClass' => 'fmr-about')); + } + + public function publicationsAction() { + $this->renderView('public/publications'); + } + + public function accountAction() { + /** + * @todo: + * - allow changing email address + * - email address verification + * - my access code has been compromised, reset? possible problems with external data, maybe they should get their own tokens... + */ + if (!$this->user->loggedIn()) { + alert('You need to be logged in to go here.', 'alert-info'); + redirect_to('login'); + } + + $vars = array('showform' => false); + if ($this->request->isHTTPPostRequest()) { + $redirect = false; + $oldEmail = $this->user->email; + + // Change basic info + email + $change = $this->user->changeData($this->request->str('password'), $this->request->getParams()); + if (!$change) { + alert(nl2br(implode($this->user->errors, "\n")), 'alert-danger'); + $vars['showform'] = 'show-form'; + } elseif ($oldEmail != $this->request->str('new_email')) { + $redirect = 'logout'; + } + + // Change password + if ($this->request->str('new_password')) { + if ($this->request->str('new_password') !== $this->request->str('new_password_c')) { + alert('The new passwords do not match', 'alert-danger'); + $vars['showform'] = 'show-form'; + } elseif ($this->user->changePassword($this->request->str('password'), $this->request->str('new_password'))) { + alert('Success! Your password was changed! Please sign-in with your new password.', 'alert-success'); + $redirect = 'logout'; + } else { + alert(implode($this->user->errors), 'alert-danger'); + $vars['showform'] = 'show-form'; + } + } + + if ($redirect) { + redirect_to($redirect); + } + } + + $vars['user'] = $this->user; + $vars['joined'] = date('jS F Y', strtotime($this->user->created)); + $vars['studies'] = $this->fdb->count('survey_runs', array('user_id' => $this->user->id)); + $vars['names'] = sprintf('%s %s', $this->user->first_name, $this->user->last_name); + if ('' === trim($vars['names'])) { + $vars['names'] = $this->user->email; + } + $vars['affiliation'] = $this->user->affiliation ? $this->user->affiliation : '(no affiliation specified)'; + + $this->registerAssets('bootstrap-material-design'); + $this->renderView('public/account', $vars); + } + + public function loginAction() { + if ($this->user->loggedIn()) { + redirect_to('account'); + } + + if ($this->request->str('email') && $this->request->str('password')) { + $info = array( + 'email' => $this->request->str('email'), + 'password' => $this->request->str('password'), + ); + if ($this->user->login($info)) { + alert('Success! You were logged in!', 'alert-success'); + Session::set('user', serialize($this->user)); + $redirect = $this->user->isAdmin() ? redirect_to('admin') : redirect_to('account'); + } else { + alert(implode($this->user->errors), 'alert-danger'); + } + } + + $this->registerAssets('bootstrap-material-design'); + $this->renderView('public/login', array('alerts' => $this->site->renderAlerts())); + } + + public function logoutAction() { + $user = $this->user; + if ($user->loggedIn()) { + alert('You have been logged out!', 'alert-info'); + $alerts = $this->site->renderAlerts(); + $user->logout(); + $this->registerAssets('bootstrap-material-design'); + $this->renderView('public/login', array('alerts' => $alerts)); + } else { + Session::destroy(); + $redirect_to = $this->request->getParam('_rdir'); + redirect_to($redirect_to); + } + } + + public function registerAction() { + $user = $this->user; + $site = $this->site; + + //fixme: cookie problems lead to fatal error with missing user code + if ($user->loggedIn()) { + alert('You were already logged in. Please logout before you can register.', 'alert-info'); + redirect_to("index"); + } + + if ($this->request->isHTTPPostRequest() && $site->request->str('email')) { + $info = array( + 'email' => $site->request->str('email'), + 'password' => $site->request->str('password'), + 'referrer_code' => $site->request->str('referrer_code'), + ); + if ($user->register($info)) { + //alert('Success! You were registered and logged in!','alert-success'); + redirect_to('index'); + } else { + alert(implode($user->errors), 'alert-danger'); + } + } + + $this->registerAssets('bootstrap-material-design'); + $this->renderView('public/register'); + } + + public function verifyEmailAction() { + $user = $this->user; + $verification_token = $this->request->str('verification_token'); + $email = $this->request->str('email'); + + if ($this->request->isHTTPGetRequest() && $this->request->str('token')) { + $user->resendVerificationEmail($this->request->str('token')); + redirect_to('login'); + } elseif (!$verification_token || !$email) { + alert("You need to follow the link you received in your verification mail."); + redirect_to('login'); + } else { + $user->verify_email($email, $verification_token); + redirect_to('login'); + }; + } + + public function forgotPasswordAction() { + if ($this->user->loggedIn()) { + redirect_to("index"); + } + + if ($this->request->str('email')) { + $this->user->forgot_password($this->request->str('email')); + } + + $this->registerAssets('bootstrap-material-design'); + $this->renderView('public/forgot_password'); + } + + public function resetPasswordAction() { + $user = $this->user; + if ($user->loggedIn()) { + redirect_to('index'); + } + + if ($this->request->isHTTPGetRequest() && (!$this->request->str('email') || !$this->request->str('reset_token')) && !$this->request->str('ok')) { + alert('You need to follow the link you received in your password reset mail'); + redirect_to('forgot_password'); + } elseif ($this->request->isHTTPPostRequest()) { + $postRequest = new Request($_POST); + $info = array( + 'email' => $postRequest->str('email'), + 'reset_token' => $postRequest->str('reset_token'), + 'new_password' => $postRequest->str('new_password'), + 'new_password_confirm' => $postRequest->str('new_password_c'), + ); + if (($done = $user->reset_password($info))) { + redirect_to('forgot_password'); + } + } + + $this->registerAssets('bootstrap-material-design'); + $this->renderView('public/reset_password', array( + 'reset_data_email' => $this->request->str('email'), + 'reset_data_token' => $this->request->str('reset_token'), + )); + } + + public function fileDownloadAction($run_id = 0, $original_filename = '') { + $path = $this->fdb->findValue('survey_uploaded_files', array('run_id' => (int) $run_id, 'original_file_name' => $original_filename), array('new_file_path')); + if ($path) { + return redirect_to(asset_url($path)); + } + formr_error(404, 'Not Found', 'The requested file does not exist'); + } } diff --git a/application/Controller/RunController.php b/application/Controller/RunController.php index 9f09b9f64..8958ca1f2 100644 --- a/application/Controller/RunController.php +++ b/application/Controller/RunController.php @@ -2,258 +2,257 @@ class RunController extends Controller { - public function __construct(Site &$site) { - parent::__construct($site); - if (!Request::isAjaxRequest()) { - $default_assets = get_default_assets('site'); - $this->registerAssets($default_assets); - } - } - - public function indexAction($runName = '', $privateAction = null) { - // hack for run name - $_GET['run_name'] = $runName; - $this->site->request->run_name = $runName; - $pageNo = null; - - if ($method = $this->getPrivateActionMethod($privateAction)) { - $args = array_slice(func_get_args(), 2); - return call_user_func_array(array($this, $method), $args); - } elseif (is_numeric($privateAction)) { - $pageNo = (int) $privateAction; - Request::setGlobals('pageNo', $pageNo); - } elseif ($privateAction !== null) { - formr_error(404, 'Not Found', 'The requested URL was not found'); - } - - $this->run = $this->getRun(); - // @todo check the POSTed code ($_POST[formr_code]) and save data before redirecting - // OR if cookie is expired then logout - $this->user = $this->loginUser(); - - //Request::setGlobals('COOKIE', $this->setRunCookie()); - - $run_vars = $this->run->exec($this->user); - $run_vars['bodyClass'] = 'fmr-run'; - - $assset_vars = $this->filterAssets($run_vars); - unset($run_vars['css'], $run_vars['js']); - $this->renderView('public/run/index', array_merge($run_vars, $assset_vars)); - } - - protected function settingsAction() { - $run = $this->getRun(); - $run_name = $this->site->request->run_name; - - if (!$run->valid) { - formr_error(404, 'Not Found', 'Requested Run does not exist or has been moved'); - } - - // Login if user entered with code and redirect without login code - if (Request::isHTTPGetRequest() && ($code = $this->request->getParam('code'))) { - $_GET['run_name'] = $run_name; - $this->user = $this->loginUser(); - //Request::setGlobals('COOKIE', $this->setRunCookie()); - - if ($this->user->user_code != $code) { - alert('Unable to login with the provided code', 'alert-warning'); - } - redirect_to(run_url($run_name, 'settings')); - } - - // People who have no session in the run need not set anything - $session = new RunSession($this->fdb, $run->id, 'cron', $this->user->user_code, $run); - if (!$session->id) { - formr_error(401, 'Unauthorized', 'You cannot create settings in a study you have not participated in.'); - } - - $settings = array('no_email' => 1); - if (Request::isHTTPPostRequest() && $this->user->user_code == $this->request->getParam('_sess')) { - $update = array(); - $settings = array( - 'no_email' => $this->request->getParam('no_email'), - 'delete_cookie' => (int)$this->request->getParam('delete_cookie'), - ); - - if ($settings['no_email'] === '1') { - $update['no_email'] = null; - } elseif ($settings['no_email'] == 0) { - $update['no_email'] = 0; - } elseif ($ts = strtotime($settings['no_email'])) { - $update['no_email'] = $ts; - } - - $session->saveSettings($settings, $update); - - alert('Settings saved successfully for survey "'.$run->name.'"', 'alert-success'); - if ($settings['delete_cookie']) { - Session::destroy(); - redirect_to('index'); - } - redirect_to(run_url($run_name, 'settings')); - } - - $this->run = $run; - $this->renderView('public/run/settings', array( - 'settings' => $session->getSettings(), - 'email_subscriptions' => Config::get('email_subscriptions'), - )); - } - - protected function logoutAction() { - $this->run = $this->getRun(); - $this->user = $this->loginUser(); - $cookie = $this->getRunCookie(); - $cookie->destroy(); - Session::destroy(); - $hint = 'Session Ended'; - $text = 'Your session was successfully closed! You can restart a new session by clicking the link below.'; - formr_error(200, 'OK', $text, $hint, run_url($this->run->name), 'Start New Session'); - } - - protected function monkeyBarAction($action = '') { - $action = str_replace('ajax_', '', $action); - $allowed_actions = array('send_to_position', 'remind', 'next_in_run', 'delete_user', 'snip_unit_session'); - $run = $this->getRun(); - - if (!in_array($action, $allowed_actions) || !$run->valid) { - throw new Exception("Invalid Request parameters"); - } - - $parts = explode('_', $action); - $method = array_shift($parts) . str_replace(' ', '', ucwords(implode(' ', $parts))); - $runHelper = new RunHelper($this->request, $this->fdb, $run->name); - - // check if run session usedby the monkey bar is a test if not this action is not valid - if (!($runSession = $runHelper->getRunSession()) || !$runSession->isTesting()) { - throw new Exception ("Unauthorized access to run session"); - } - - if (!method_exists($runHelper, $method)) { - throw new Exception("Invalid method {$action}"); - } - - $runHelper->{$method}(); - if (($errors = $runHelper->getErrors())) { - $errors = implode("\n", $errors); - alert($errors, 'alert-danger'); - } - - if (($message = $runHelper->getMessage())) { - alert($message, 'alert-info'); - } - - if (is_ajax_request()) { - echo $this->site->renderAlerts(); - exit; - } - redirect_to(''); - } - - private function getRun() { - $name = $this->request->str('run_name'); - $run = new Run($this->fdb, $name); - if ($name !== Run::TEST_RUN && Config::get('use_study_subdomains') && !FMRSD_CONTEXT) { - //throw new Exception('Invalid Study Context'); - // Redirect existing users to run's sub-domain URL and QSA - $params = $this->request->getParams(); - unset($params['route'], $params['run_name']); - $name = str_replace('_', '-', $name); - $url = run_url($name, null, $params); - redirect_to($url); - } elseif (!$run->valid) { - $msg = __('If you\'re trying to create an online study, read the full documentation to learn how to create one.', site_url('documentation')); - formr_error(404, 'Not Found', $msg, 'There isn\'t an online study here.'); - } - - return $run; - } - - private function getPrivateActionMethod($action) { - $actionName = $this->getPrivateAction($action, '-', true) . 'Action'; - if (!method_exists($this, $actionName)) { - return false; - } - return $actionName; - } - - private function filterAssets($assets) { - $vars = array(); - if ($this->run->use_material_design === true || $this->request->str('tmd') === 'true') { - if (DEBUG) { - $this->unregisterAssets('site:custom'); - $this->registerAssets('bootstrap-material-design'); - $this->registerAssets('site:custom'); - } else { - $this->replaceAssets('site', 'site:material'); - } - $vars['bodyClass'] = 'bs-material fmr-run'; - } - $this->registerCSS($assets['css'], $this->run->name); - $this->registerJS($assets['js'], $this->run->name); - return $vars; - } - - protected function generateMetaInfo() { - $meta = parent::generateMetaInfo(); - $meta['title'] = $this->run->title ? $this->run->title : $this->run->name; - $meta['url'] = run_url($this->run->name); - if ($this->run->description) { - $meta['description'] = $this->run->description; - } - if ($this->run->header_image_path) { - $meta['image'] = $this->run->header_image_path; - } - - return $meta; - } - - protected function setRunCookie($refresh = false) { - $cookie = $this->getRunCookie(); - $expires = $this->run->expire_cookie ? time() + $this->run->expire_cookie : 0; - - if (!$cookie->exists() || $refresh === true) { - $data = array( - 'code' => $this->user->user_code, - 'created' => time(), - 'modified' => time(), - 'expires' => $expires, - ); - $cookie->create($data, $expires, '/', null, SSL, true); - } elseif ($cookie->exists()) { - $cookie->setExpiration($expires); - } - - return $cookie; - } - - /** - * - * @return \Cookie - */ - protected function getRunCookie() { - $cookie = new \Cookie($this->run->getCookieName()); - return $cookie; - } - - protected function loginUser() { - $id = null; - - // came here with a login link - if (isset($_GET['run_name']) && isset($_GET['code']) && strlen($_GET['code']) == 64) { - // user came in with login code - $loginCode = $_GET['code']; - //} elseif (($cookie = $this->getRunCookie()) && $cookie->exists() && $cookie->getData('code')) { - } elseif ($user = Site::getInstance()->getSessionUser()) { - // try to get user from cookie - $loginCode = $user->user_code; - $id = $user->id; - } else { - // new user just entering the run; - $loginCode = null; - } - return new User($this->fdb, $id, $loginCode); - } - + public function __construct(Site &$site) { + parent::__construct($site); + if (!Request::isAjaxRequest()) { + $default_assets = get_default_assets('site'); + $this->registerAssets($default_assets); + } + } + + public function indexAction($runName = '', $privateAction = null) { + // hack for run name + $_GET['run_name'] = $runName; + $this->site->request->run_name = $runName; + $pageNo = null; + + if ($method = $this->getPrivateActionMethod($privateAction)) { + $args = array_slice(func_get_args(), 2); + return call_user_func_array(array($this, $method), $args); + } elseif (is_numeric($privateAction)) { + $pageNo = (int) $privateAction; + Request::setGlobals('pageNo', $pageNo); + } elseif ($privateAction !== null) { + formr_error(404, 'Not Found', 'The requested URL was not found'); + } + + $this->run = $this->getRun(); + // @todo check the POSTed code ($_POST[formr_code]) and save data before redirecting + // OR if cookie is expired then logout + $this->user = $this->loginUser(); + + //Request::setGlobals('COOKIE', $this->setRunCookie()); + + $run_vars = $this->run->exec($this->user); + $run_vars['bodyClass'] = 'fmr-run'; + + $assset_vars = $this->filterAssets($run_vars); + unset($run_vars['css'], $run_vars['js']); + $this->renderView('public/run/index', array_merge($run_vars, $assset_vars)); + } + + protected function settingsAction() { + $run = $this->getRun(); + $run_name = $this->site->request->run_name; + + if (!$run->valid) { + formr_error(404, 'Not Found', 'Requested Run does not exist or has been moved'); + } + + // Login if user entered with code and redirect without login code + if (Request::isHTTPGetRequest() && ($code = $this->request->getParam('code'))) { + $_GET['run_name'] = $run_name; + $this->user = $this->loginUser(); + //Request::setGlobals('COOKIE', $this->setRunCookie()); + + if ($this->user->user_code != $code) { + alert('Unable to login with the provided code', 'alert-warning'); + } + redirect_to(run_url($run_name, 'settings')); + } + + // People who have no session in the run need not set anything + $session = new RunSession($this->fdb, $run->id, 'cron', $this->user->user_code, $run); + if (!$session->id) { + formr_error(401, 'Unauthorized', 'You cannot create settings in a study you have not participated in.'); + } + + $settings = array('no_email' => 1); + if (Request::isHTTPPostRequest() && $this->user->user_code == $this->request->getParam('_sess')) { + $update = array(); + $settings = array( + 'no_email' => $this->request->getParam('no_email'), + 'delete_cookie' => (int) $this->request->getParam('delete_cookie'), + ); + + if ($settings['no_email'] === '1') { + $update['no_email'] = null; + } elseif ($settings['no_email'] == 0) { + $update['no_email'] = 0; + } elseif ($ts = strtotime($settings['no_email'])) { + $update['no_email'] = $ts; + } + + $session->saveSettings($settings, $update); + + alert('Settings saved successfully for survey "' . $run->name . '"', 'alert-success'); + if ($settings['delete_cookie']) { + Session::destroy(); + redirect_to('index'); + } + redirect_to(run_url($run_name, 'settings')); + } + + $this->run = $run; + $this->renderView('public/run/settings', array( + 'settings' => $session->getSettings(), + 'email_subscriptions' => Config::get('email_subscriptions'), + )); + } + + protected function logoutAction() { + $this->run = $this->getRun(); + $this->user = $this->loginUser(); + $cookie = $this->getRunCookie(); + $cookie->destroy(); + Session::destroy(); + $hint = 'Session Ended'; + $text = 'Your session was successfully closed! You can restart a new session by clicking the link below.'; + formr_error(200, 'OK', $text, $hint, run_url($this->run->name), 'Start New Session'); + } + + protected function monkeyBarAction($action = '') { + $action = str_replace('ajax_', '', $action); + $allowed_actions = array('send_to_position', 'remind', 'next_in_run', 'delete_user', 'snip_unit_session'); + $run = $this->getRun(); + + if (!in_array($action, $allowed_actions) || !$run->valid) { + throw new Exception("Invalid Request parameters"); + } + + $parts = explode('_', $action); + $method = array_shift($parts) . str_replace(' ', '', ucwords(implode(' ', $parts))); + $runHelper = new RunHelper($this->request, $this->fdb, $run->name); + + // check if run session usedby the monkey bar is a test if not this action is not valid + if (!($runSession = $runHelper->getRunSession()) || !$runSession->isTesting()) { + throw new Exception("Unauthorized access to run session"); + } + + if (!method_exists($runHelper, $method)) { + throw new Exception("Invalid method {$action}"); + } + + $runHelper->{$method}(); + if (($errors = $runHelper->getErrors())) { + $errors = implode("\n", $errors); + alert($errors, 'alert-danger'); + } + + if (($message = $runHelper->getMessage())) { + alert($message, 'alert-info'); + } + + if (is_ajax_request()) { + echo $this->site->renderAlerts(); + exit; + } + redirect_to(''); + } + + private function getRun() { + $name = $this->request->str('run_name'); + $run = new Run($this->fdb, $name); + if ($name !== Run::TEST_RUN && Config::get('use_study_subdomains') && !FMRSD_CONTEXT) { + //throw new Exception('Invalid Study Context'); + // Redirect existing users to run's sub-domain URL and QSA + $params = $this->request->getParams(); + unset($params['route'], $params['run_name']); + $name = str_replace('_', '-', $name); + $url = run_url($name, null, $params); + redirect_to($url); + } elseif (!$run->valid) { + $msg = __('If you\'re trying to create an online study, read the full documentation to learn how to create one.', site_url('documentation')); + formr_error(404, 'Not Found', $msg, 'There isn\'t an online study here.'); + } + + return $run; + } + + private function getPrivateActionMethod($action) { + $actionName = $this->getPrivateAction($action, '-', true) . 'Action'; + if (!method_exists($this, $actionName)) { + return false; + } + return $actionName; + } + + private function filterAssets($assets) { + $vars = array(); + if ($this->run->use_material_design === true || $this->request->str('tmd') === 'true') { + if (DEBUG) { + $this->unregisterAssets('site:custom'); + $this->registerAssets('bootstrap-material-design'); + $this->registerAssets('site:custom'); + } else { + $this->replaceAssets('site', 'site:material'); + } + $vars['bodyClass'] = 'bs-material fmr-run'; + } + $this->registerCSS($assets['css'], $this->run->name); + $this->registerJS($assets['js'], $this->run->name); + return $vars; + } + + protected function generateMetaInfo() { + $meta = parent::generateMetaInfo(); + $meta['title'] = $this->run->title ? $this->run->title : $this->run->name; + $meta['url'] = run_url($this->run->name); + if ($this->run->description) { + $meta['description'] = $this->run->description; + } + if ($this->run->header_image_path) { + $meta['image'] = $this->run->header_image_path; + } + + return $meta; + } + + protected function setRunCookie($refresh = false) { + $cookie = $this->getRunCookie(); + $expires = $this->run->expire_cookie ? time() + $this->run->expire_cookie : 0; + + if (!$cookie->exists() || $refresh === true) { + $data = array( + 'code' => $this->user->user_code, + 'created' => time(), + 'modified' => time(), + 'expires' => $expires, + ); + $cookie->create($data, $expires, '/', null, SSL, true); + } elseif ($cookie->exists()) { + $cookie->setExpiration($expires); + } + + return $cookie; + } + + /** + * + * @return \Cookie + */ + protected function getRunCookie() { + $cookie = new \Cookie($this->run->getCookieName()); + return $cookie; + } + + protected function loginUser() { + $id = null; + + // came here with a login link + if (isset($_GET['run_name']) && isset($_GET['code']) && strlen($_GET['code']) == 64) { + // user came in with login code + $loginCode = $_GET['code']; + //} elseif (($cookie = $this->getRunCookie()) && $cookie->exists() && $cookie->getData('code')) { + } elseif ($user = Site::getInstance()->getSessionUser()) { + // try to get user from cookie + $loginCode = $user->user_code; + $id = $user->id; + } else { + // new user just entering the run; + $loginCode = null; + } + return new User($this->fdb, $id, $loginCode); + } } diff --git a/application/Controller/SuperadminController.php b/application/Controller/SuperadminController.php index eb4c643fb..25373375d 100644 --- a/application/Controller/SuperadminController.php +++ b/application/Controller/SuperadminController.php @@ -2,126 +2,126 @@ class SuperadminController extends Controller { - public function __construct(Site &$site) { - parent::__construct($site); - if (!$this->user->isSuperAdmin()) { - alert('You do not have sufficient rights to access the requested location', 'alert-danger'); - redirect_to('admin'); - } - if (!Request::isAjaxRequest()) { - $default_assets = get_default_assets('admin'); - $this->registerAssets($default_assets); - } - } - - public function indexAction() { - redirect_to('/'); - } - - public function ajaxAdminAction() { - if (!is_ajax_request()) { - return redirect_to('/'); - } - - $request = new Request($_POST); - if ($request->user_id && is_numeric($request->admin_level)) { - $this->setAdminLevel($request->user_id, $request->admin_level); - } - - if ($request->user_api) { - $this->apiUserManagement($request->user_id, $request->user_email, $request->api_action); - } - } - - private function setAdminLevel($user_id, $level) { - $level = (int) $level; - $allowed_levels = array(0, 1, 100); - $user = new User($this->fdb, $user_id, null); - - if (!in_array($level, $allowed_levels) || !$user->email) { - alert('Level not supported or could not be assigned to user', 'alert-danger'); - } elseif ($level == $user->getAdminLevel()) { - alert('User already has requested admin rights', 'alert-warning'); - } else { - if (!$user->setAdminLevelTo($level)) : - alert('Something went wrong with the admin level change.', 'alert-danger'); - bad_request_header(); - else: - alert('Level assigned to user.', 'alert-success'); - endif; - } - - echo $this->site->renderAlerts(); - exit; - } - - private function apiUserManagement($user_id, $user_email, $action) { - $user = new User($this->fdb, $user_id, null); - $response = new Response(); - $response->setContentType('application/json'); - if ($user->email !== $user_email) { - $response->setJsonContent(array('success' => false, 'message' => 'Invalid User')); - } elseif ($action === 'create') { - $client = OAuthHelper::getInstance()->createClient($user); - if (!$client) { - $response->setJsonContent(array('success' => false, 'message' => 'Unable to create client')); - } else { - $client['user'] = $user->email; - $response->setJsonContent(array('success' => true, 'data' => $client)); - } - } elseif ($action === 'get') { - $client = OAuthHelper::getInstance()->getClient($user); - if (!$client) { - $response->setJsonContent(array('success' => true, 'data' => array('client_id' => '', 'client_secret' => '', 'user' => $user->email))); - } else { - $client['user'] = $user->email; - $response->setJsonContent(array('success' => true, 'data' => $client)); - } - } elseif ($action === 'delete') { - if (OAuthHelper::getInstance()->deleteClient($user)) { - $response->setJsonContent(array('success' => true, 'message' => 'Credentials revoked for user ' . $user->email)); - } else { - $response->setJsonContent(array('success' => false, 'message' => 'An error occured')); - } - } elseif ($action === 'change') { - $client = OAuthHelper::getInstance()->refreshToken($user); - if (!$client) { - $response->setJsonContent(array('success' => false, 'message' => 'An error occured refereshing API secret.')); - } else { - $client['user'] = $user->email; - $response->setJsonContent(array('success' => true, 'data' => $client)); - } - } - - $response->send(); - } - - public function cronLogParsed() { - $parser = new LogParser(); - $files = $parser->getCronLogFiles(); - $file = $this->request->getParam('f'); - $expand = $this->request->getParam('e'); - $parse = null; - if ($file && isset($files[$file])) { - $parse = $file; - } - - $this->renderView('superadmin/cron_log_parsed', array( - 'files' => $files, - 'parse' => $parse, - 'parser' => $parser, - 'expand_logs' => $expand, - )); - } - - public function cronLogAction() { - return $this->cronLogParsed(); - // @todo: deprecate code - $cron_entries_count = $this->fdb->count('survey_cron_log'); - $pagination = new Pagination($cron_entries_count); - $limits = $pagination->getLimits(); - - $cron_entries_query = "SELECT + public function __construct(Site &$site) { + parent::__construct($site); + if (!$this->user->isSuperAdmin()) { + alert('You do not have sufficient rights to access the requested location', 'alert-danger'); + redirect_to('admin'); + } + if (!Request::isAjaxRequest()) { + $default_assets = get_default_assets('admin'); + $this->registerAssets($default_assets); + } + } + + public function indexAction() { + redirect_to('/'); + } + + public function ajaxAdminAction() { + if (!is_ajax_request()) { + return redirect_to('/'); + } + + $request = new Request($_POST); + if ($request->user_id && is_numeric($request->admin_level)) { + $this->setAdminLevel($request->user_id, $request->admin_level); + } + + if ($request->user_api) { + $this->apiUserManagement($request->user_id, $request->user_email, $request->api_action); + } + } + + private function setAdminLevel($user_id, $level) { + $level = (int) $level; + $allowed_levels = array(0, 1, 100); + $user = new User($this->fdb, $user_id, null); + + if (!in_array($level, $allowed_levels) || !$user->email) { + alert('Level not supported or could not be assigned to user', 'alert-danger'); + } elseif ($level == $user->getAdminLevel()) { + alert('User already has requested admin rights', 'alert-warning'); + } else { + if (!$user->setAdminLevelTo($level)) : + alert('Something went wrong with the admin level change.', 'alert-danger'); + bad_request_header(); + else: + alert('Level assigned to user.', 'alert-success'); + endif; + } + + echo $this->site->renderAlerts(); + exit; + } + + private function apiUserManagement($user_id, $user_email, $action) { + $user = new User($this->fdb, $user_id, null); + $response = new Response(); + $response->setContentType('application/json'); + if ($user->email !== $user_email) { + $response->setJsonContent(array('success' => false, 'message' => 'Invalid User')); + } elseif ($action === 'create') { + $client = OAuthHelper::getInstance()->createClient($user); + if (!$client) { + $response->setJsonContent(array('success' => false, 'message' => 'Unable to create client')); + } else { + $client['user'] = $user->email; + $response->setJsonContent(array('success' => true, 'data' => $client)); + } + } elseif ($action === 'get') { + $client = OAuthHelper::getInstance()->getClient($user); + if (!$client) { + $response->setJsonContent(array('success' => true, 'data' => array('client_id' => '', 'client_secret' => '', 'user' => $user->email))); + } else { + $client['user'] = $user->email; + $response->setJsonContent(array('success' => true, 'data' => $client)); + } + } elseif ($action === 'delete') { + if (OAuthHelper::getInstance()->deleteClient($user)) { + $response->setJsonContent(array('success' => true, 'message' => 'Credentials revoked for user ' . $user->email)); + } else { + $response->setJsonContent(array('success' => false, 'message' => 'An error occured')); + } + } elseif ($action === 'change') { + $client = OAuthHelper::getInstance()->refreshToken($user); + if (!$client) { + $response->setJsonContent(array('success' => false, 'message' => 'An error occured refereshing API secret.')); + } else { + $client['user'] = $user->email; + $response->setJsonContent(array('success' => true, 'data' => $client)); + } + } + + $response->send(); + } + + public function cronLogParsed() { + $parser = new LogParser(); + $files = $parser->getCronLogFiles(); + $file = $this->request->getParam('f'); + $expand = $this->request->getParam('e'); + $parse = null; + if ($file && isset($files[$file])) { + $parse = $file; + } + + $this->renderView('superadmin/cron_log_parsed', array( + 'files' => $files, + 'parse' => $parse, + 'parser' => $parser, + 'expand_logs' => $expand, + )); + } + + public function cronLogAction() { + return $this->cronLogParsed(); + // @todo: deprecate code + $cron_entries_count = $this->fdb->count('survey_cron_log'); + $pagination = new Pagination($cron_entries_count); + $limits = $pagination->getLimits(); + + $cron_entries_query = "SELECT `survey_cron_log`.id, `survey_users`.email, `survey_cron_log`.run_id, @@ -142,56 +142,56 @@ public function cronLogAction() { LEFT JOIN `survey_users` ON `survey_users`.id = `survey_runs`.user_id ORDER BY `survey_cron_log`.id DESC LIMIT $limits;"; - $g_cron = $this->fdb->execute($cron_entries_query, array('user_id', $this->user->id)); - $cronlogs = array(); - foreach ($g_cron as $cronlog) { - $cronlog = array_reverse($cronlog, true); - $cronlog['Modules'] = ''; - - if ($cronlog['pauses'] > 0) - $cronlog['Modules'] .= $cronlog['pauses'] . ' '; - if ($cronlog['skipbackwards'] > 0) - $cronlog['Modules'] .= $cronlog['skipbackwards'] . ' '; - if ($cronlog['skipforwards'] > 0) - $cronlog['Modules'] .= $cronlog['skipforwards'] . ' '; - if ($cronlog['emails'] > 0) - $cronlog['Modules'] .= $cronlog['emails'] . ' '; - if ($cronlog['shuffles'] > 0) - $cronlog['Modules'] .= $cronlog['shuffles'] . ' '; - $cronlog['Modules'] .= ''; - $cronlog['took'] = '' . round($cronlog['time_in_seconds'] / 60, 2) . 'm'; - $cronlog['time'] = '' . timetostr(strtotime($cronlog['created'])) . ''; - $cronlog['Run name'] = $cronlog['run_name']; - $cronlog['Owner'] = $cronlog['email']; - - $cronlog = array_reverse($cronlog, true); - unset($cronlog['run_name']); - unset($cronlog['created']); - unset($cronlog['time_in_seconds']); - unset($cronlog['skipforwards']); - unset($cronlog['skipbackwards']); - unset($cronlog['pauses']); - unset($cronlog['emails']); - unset($cronlog['shuffles']); - unset($cronlog['run_id']); - unset($cronlog['id']); - unset($cronlog['email']); - - $cronlogs[] = $cronlog; - } - - $this->renderView('superadmin/cron_log', array( - 'cronlogs' => $cronlogs, - 'pagination' => $pagination, - )); - } - - public function userManagementAction() { - $user_count = $this->fdb->count('survey_users'); - $pagination = new Pagination($user_count, 200, true); - $limits = $pagination->getLimits(); - - $users_query = "SELECT + $g_cron = $this->fdb->execute($cron_entries_query, array('user_id', $this->user->id)); + $cronlogs = array(); + foreach ($g_cron as $cronlog) { + $cronlog = array_reverse($cronlog, true); + $cronlog['Modules'] = ''; + + if ($cronlog['pauses'] > 0) + $cronlog['Modules'] .= $cronlog['pauses'] . ' '; + if ($cronlog['skipbackwards'] > 0) + $cronlog['Modules'] .= $cronlog['skipbackwards'] . ' '; + if ($cronlog['skipforwards'] > 0) + $cronlog['Modules'] .= $cronlog['skipforwards'] . ' '; + if ($cronlog['emails'] > 0) + $cronlog['Modules'] .= $cronlog['emails'] . ' '; + if ($cronlog['shuffles'] > 0) + $cronlog['Modules'] .= $cronlog['shuffles'] . ' '; + $cronlog['Modules'] .= ''; + $cronlog['took'] = '' . round($cronlog['time_in_seconds'] / 60, 2) . 'm'; + $cronlog['time'] = '' . timetostr(strtotime($cronlog['created'])) . ''; + $cronlog['Run name'] = $cronlog['run_name']; + $cronlog['Owner'] = $cronlog['email']; + + $cronlog = array_reverse($cronlog, true); + unset($cronlog['run_name']); + unset($cronlog['created']); + unset($cronlog['time_in_seconds']); + unset($cronlog['skipforwards']); + unset($cronlog['skipbackwards']); + unset($cronlog['pauses']); + unset($cronlog['emails']); + unset($cronlog['shuffles']); + unset($cronlog['run_id']); + unset($cronlog['id']); + unset($cronlog['email']); + + $cronlogs[] = $cronlog; + } + + $this->renderView('superadmin/cron_log', array( + 'cronlogs' => $cronlogs, + 'pagination' => $pagination, + )); + } + + public function userManagementAction() { + $user_count = $this->fdb->count('survey_users'); + $pagination = new Pagination($user_count, 200, true); + $limits = $pagination->getLimits(); + + $users_query = "SELECT `survey_users`.id, `survey_users`.created, `survey_users`.modified, @@ -201,43 +201,43 @@ public function userManagementAction() { FROM `survey_users` ORDER BY `survey_users`.id ASC LIMIT $limits;"; - $g_users = $this->fdb->prepare($users_query); - $g_users->execute(); + $g_users = $this->fdb->prepare($users_query); + $g_users->execute(); - $users = array(); - while($userx = $g_users->fetch(PDO::FETCH_ASSOC)) { - $userx['Email'] = ''.h($userx['email']).'' . ($userx['email_verified'] ? " ":" "); - $userx['Created'] = "".timetostr(strtotime($userx['created'])).""; - $userx['Modified'] = "".timetostr(strtotime($userx['modified'])).""; - $userx['Admin'] = " + $users = array(); + while ($userx = $g_users->fetch(PDO::FETCH_ASSOC)) { + $userx['Email'] = '' . h($userx['email']) . '' . ($userx['email_verified'] ? " " : " "); + $userx['Created'] = "" . timetostr(strtotime($userx['created'])) . ""; + $userx['Modified'] = "" . timetostr(strtotime($userx['modified'])) . ""; + $userx['Admin'] = "
- +
"; - $userx['API Access'] = ''; + $userx['API Access'] = ''; - unset($userx['email'], $userx['created'], $userx['modified'], $userx['admin'], $userx['id'], $userx['email_verified']); + unset($userx['email'], $userx['created'], $userx['modified'], $userx['admin'], $userx['id'], $userx['email_verified']); - $users[] = $userx; - } + $users[] = $userx; + } - $this->renderView('superadmin/user_management', array( - 'users' => $users, - 'pagination' => $pagination, - )); - } - - public function activeUsersAction() { - $user_count = $this->fdb->count('survey_users'); - $pagination = new Pagination($user_count, 200, true); - $limits = $pagination->getLimits(); + $this->renderView('superadmin/user_management', array( + 'users' => $users, + 'pagination' => $pagination, + )); + } - $users_query = "SELECT + public function activeUsersAction() { + $user_count = $this->fdb->count('survey_users'); + $pagination = new Pagination($user_count, 200, true); + $limits = $pagination->getLimits(); + + $users_query = "SELECT `survey_users`.id, `survey_users`.created, `survey_users`.modified, @@ -256,84 +256,85 @@ public function activeUsersAction() { GROUP BY `survey_runs`.id ORDER BY `survey_users`.id ASC, last_edit DESC LIMIT $limits;"; - $g_users = $this->fdb->prepare($users_query); - $g_users->execute(); - - $users = array(); - $last_user = ""; - while($userx = $g_users->fetch(PDO::FETCH_ASSOC)) { - $public_status = (int)$userx['public']; - $public_logo = ''; - if($public_status===0): - $public_logo = "fa-eject"; - elseif($public_status === 1): - $public_logo = "fa-volume-off"; - elseif($public_status === 2): - $public_logo = "fa-volume-down"; - elseif($public_status === 3): - $public_logo = "fa-volume-up"; - endif; - if($last_user !== $userx['id']): - $userx['Email'] = ''.h($userx['email']).'' . ($userx['email_verified'] ? " ":" "); - $last_user = $userx['id']; - else: - $userx['Email'] = ''; - endif; - $userx['Created'] = "".timetostr(strtotime($userx['created'])).""; - $userx['Modified'] = "".timetostr(strtotime($userx['modified'])).""; - $userx['Run'] = h($userx['run_name'])." ". - ($userx['cron_active']? "":"").' ' . - ""; - $userx['Users'] = $userx['number_of_users_in_run']; - $userx['Last active'] = "".timetostr(strtotime($userx['last_edit'])).""; - - unset($userx['email']); - unset($userx['created']); - unset($userx['modified']); - unset($userx['admin']); - unset($userx['number_of_users_in_run']); - unset($userx['public']); - unset($userx['cron_active']); - unset($userx['run_name']); - unset($userx['id']); - unset($userx['last_edit']); - unset($userx['email_verified']); - # $user['body'] = "". substr($user['body'],0,50). "…"; - - $users[] = $userx; - } - - $this->renderView('superadmin/active_users', array( - 'users' => $users, - 'pagination' => $pagination, - )); - } - - public function runsManagementAction() { - if (Request::isHTTPPostRequest()) { - // process post request and redirect - foreach ($this->request->arr('runs') as $id => $data) { - $update = array( - 'cron_active' => (int)isset($data['cron_active']), - 'cron_fork' => (int)isset($data['cron_fork']), - 'locked' => (int)isset($data['locked']), - ); - $this->fdb->update('survey_runs', $update, array('id' => (int)$id)); - } - alert('Changes saved', 'alert-success'); - redirect_to('superadmin/runs_management'); - } - - $q = 'SELECT survey_runs.id AS run_id, name, survey_runs.user_id, cron_active, cron_fork, locked, count(survey_run_sessions.session) AS sessions, survey_users.email + $g_users = $this->fdb->prepare($users_query); + $g_users->execute(); + + $users = array(); + $last_user = ""; + while ($userx = $g_users->fetch(PDO::FETCH_ASSOC)) { + $public_status = (int) $userx['public']; + $public_logo = ''; + if ($public_status === 0): + $public_logo = "fa-eject"; + elseif ($public_status === 1): + $public_logo = "fa-volume-off"; + elseif ($public_status === 2): + $public_logo = "fa-volume-down"; + elseif ($public_status === 3): + $public_logo = "fa-volume-up"; + endif; + if ($last_user !== $userx['id']): + $userx['Email'] = '' . h($userx['email']) . '' . ($userx['email_verified'] ? " " : " "); + $last_user = $userx['id']; + else: + $userx['Email'] = ''; + endif; + $userx['Created'] = "" . timetostr(strtotime($userx['created'])) . ""; + $userx['Modified'] = "" . timetostr(strtotime($userx['modified'])) . ""; + $userx['Run'] = h($userx['run_name']) . " " . + ($userx['cron_active'] ? "" : "") . ' ' . + ""; + $userx['Users'] = $userx['number_of_users_in_run']; + $userx['Last active'] = "" . timetostr(strtotime($userx['last_edit'])) . ""; + + unset($userx['email']); + unset($userx['created']); + unset($userx['modified']); + unset($userx['admin']); + unset($userx['number_of_users_in_run']); + unset($userx['public']); + unset($userx['cron_active']); + unset($userx['run_name']); + unset($userx['id']); + unset($userx['last_edit']); + unset($userx['email_verified']); + # $user['body'] = "". substr($user['body'],0,50). "…"; + + $users[] = $userx; + } + + $this->renderView('superadmin/active_users', array( + 'users' => $users, + 'pagination' => $pagination, + )); + } + + public function runsManagementAction() { + if (Request::isHTTPPostRequest()) { + // process post request and redirect + foreach ($this->request->arr('runs') as $id => $data) { + $update = array( + 'cron_active' => (int) isset($data['cron_active']), + 'cron_fork' => (int) isset($data['cron_fork']), + 'locked' => (int) isset($data['locked']), + ); + $this->fdb->update('survey_runs', $update, array('id' => (int) $id)); + } + alert('Changes saved', 'alert-success'); + redirect_to('superadmin/runs_management'); + } + + $q = 'SELECT survey_runs.id AS run_id, name, survey_runs.user_id, cron_active, cron_fork, locked, count(survey_run_sessions.session) AS sessions, survey_users.email FROM survey_runs LEFT JOIN survey_users ON survey_users.id = survey_runs.user_id LEFT JOIN survey_run_sessions ON survey_run_sessions.run_id = survey_runs.id GROUP BY survey_run_sessions.run_id ORDER BY survey_runs.name ASC '; - $this->renderView('superadmin/runs_management', array( - 'runs' => $this->fdb->query($q), - 'i' => 1, - )); - } -} \ No newline at end of file + $this->renderView('superadmin/runs_management', array( + 'runs' => $this->fdb->query($q), + 'i' => 1, + )); + } + +} diff --git a/application/Helper/ApiHelper.php b/application/Helper/ApiHelper.php index 2e29f79e5..bda1197df 100644 --- a/application/Helper/ApiHelper.php +++ b/application/Helper/ApiHelper.php @@ -2,333 +2,333 @@ class ApiHelper { - /** - * @var DB - */ - protected $fdb; - - /** - * @var Request - */ - protected $request; - - /** - * A conainer to hold processed request's outcome - * - * @var array - */ - protected $data = array( - 'statusCode' => Response::STATUS_OK, - 'statusText' => 'OK', - 'response' => array(), - ); - - /** - * Formr user object current accessing data - * @var User - */ - protected $user = null; - - /** - * Error information - * - * @var array - */ - protected $error = array(); - - public function __construct(Request $request, DB $db) { - $this->fdb = $db; - $this->request = $request; - $this->user = OAuthHelper::getInstance()->getUserByAccessToken($this->request->getParam('access_token')); - } - - public function results() { - /** - * $request in this method is expected to be an object of the following format - * $request = '{ - * run: { - * name: 'some_run_name', - * api_secret: 'run_api_secret_as_show_in_run_settings' - * surveys: [{ - * name: 'survey_name', - * items: 'name, email, pov_1' - * }], - * sessions: ['xxxx', 'xxxx'] - * } - * }' - */ - - // Get run object from request - $request_run = $this->request->arr('run'); - $request_surveys = $this->request->arr('surveys'); - $request = array('run' => array( - 'name' => array_val($request_run, 'name', null), - 'session' => array_val($request_run, 'session', null), - 'sessions' => array_filter(explode(',', array_val($request_run, 'sessions', false))), - 'surveys' => array() - )); - foreach ($request_surveys as $survey_name => $survey_fields) { - $request['run']['surveys'][] = (object)array( - 'name' => $survey_name, - 'items' => $survey_fields, - ); - } - - $request = json_decode(json_encode($request)); - if (!($run = $this->getRunFromRequest($request))) { - return $this; - } - $requested_run = $request->run; - - // Determine which surveys in the run for which to collect data - if (!empty($requested_run->survey)) { - $surveys = array($requested_run->survey); - } elseif (!empty($requested_run->surveys)) { - $surveys = $requested_run->surveys; - } else { - $surveys = array(); - $run_surveys = $run->getAllSurveys(); - foreach ($run_surveys as $survey) { - /** @var Survey $svy */ - $svy = Survey::loadById($survey['id']); - $items = $svy->getItems('id, type, name'); - $items_names = array(); - foreach ($items as $item) { - $items_names[] = $item['name']; - } - $surveys[] = (object) array( - 'name' => $svy->name, - 'items' => implode(',', $items_names), - 'object' => $svy, - ); - } - } - - // Determine which run sessions in the run will be returned. - // (For now let's prevent returning data of all sessions so this should be required) - if (!empty($requested_run->session)) { - $requested_run->sessions = array($requested_run->session); - } - // If no specific sessions are requested then return all sessions - if (empty($requested_run->sessions)) { - $sessions = $this->fdb->find('survey_run_sessions', array('run_id' => $run->id), array('cols' => 'session')); - foreach ($sessions as $sess) { - $requested_run->sessions[] = $sess['session']; - } - } - - // If sessions are still not available then run is empty - if (empty($requested_run->sessions)) { - $this->setData(Response::STATUS_NOT_FOUND, 'Empty Run', null, 'No sessions were found in this run.'); - return $this; - } - - - // Get result for each survey for each session - $results = array(); - foreach ($surveys as $s) { - if (empty($s->name)) { - continue; - } - if (empty($s->object)) { - $s->object = Survey::loadByUserAndName($this->user, $s->name); - } - - /** @var Survey $svy */ - $svy = $s->object; - if (empty($svy->valid)) { - $results[$s->name] = null; - continue; - } - - if (empty($s->items)) { - $items = array(); - } elseif (is_array($s->items)) { - $items = array_map('trim', $s->items); - } elseif (is_string($s->items)) { - $items = array_map('trim', explode(',', $s->items)); - } else { - throw new Exception("Invalid type for survey items. Type: " . gettype($s->itmes)); - } - - //Get data for all requested sessions in this survey - $results[$s->name] = array(); - foreach ($requested_run->sessions as $session) { - $results[$s->name] = array_merge($results[$s->name], $this->getSurveyResults($svy, $session, $items)); - } - } - - $this->setData(Response::STATUS_OK, 'OK', $results); - return $this; - } - - public function createSession() { - if (!($request = $this->parseJsonRequest()) || !($run = $this->getRunFromRequest($request))) { - return $this; - } - - $i = 0; - $run_session = new RunSession($this->fdb, $run->id, null, null, null); - $code = null; - if (!empty($request->run->code)) { - $code = $request->run->code; - } - - if (!is_array($code)) { - $code = array($code); - } - - $sessions = array(); - foreach ($code as $session) { - if (($created = $run_session->create($session))) { - $sessions[] = $run_session->session; - $i++; - } - } - - if ($i) { - $this->setData(Response::STATUS_OK, 'OK', array('created_sessions' => $i, 'sessions' => $sessions)); - } else { - $this->setData(Response::STATUS_INTERNAL_SERVER_ERROR, 'Error Request', null, 'Error occured when creating session'); - } - - return $this; - } - - public function endLastExternal() { - if (!($request = $this->parseJsonRequest())) { - return $this; - } - - $run = new Run($this->fdb, $request->run->name); - if (!$run->valid) { - $this->setData(Response::STATUS_NOT_FOUND, 'Not Found', null, 'Invalid Run'); - return $this; - } - - if (!empty($request->run->session)) { - $session_code = $request->run->session; - $run_session = new RunSession($this->fdb, $run->id, null, $session_code, null); - - if ($run_session->session !== NULL) { - $run_session->endLastExternal(); - $this->setData(Response::STATUS_OK, 'OK', array('success' => 'external unit ended')); - } else { - $this->setData(Response::STATUS_NOT_FOUND, 'Not Found', null, 'Invalid user session'); - } - } else { - $this->setData(Response::STATUS_NOT_FOUND, 'Not Found', null, 'Session code not found'); - } - - return $this; - } - - public function getData() { - return $this->data; - } - - /** - * Get Run object from an API request - * $request object must have a root element called 'run' which must have child elements called 'name' and 'api_secret' - * Example: - * $request = '{ - * run: { - * name: 'some_run_name', - * api_secret: 'run_api_secret_as_show_in_run_settings' - * } - * }' - * - * @param object $request A JSON object of the sent request - * @return boolean|Run Returns a run object if a valid run is found or FALSE otherwise - */ - protected function getRunFromRequest($request) { - if (empty($request->run->name)) { - $this->setData(Response::STATUS_NOT_FOUND, 'Not Found', null, 'Required "run : { name: }" parameter not found.'); - return false; - } - - $run = new Run($this->fdb, $request->run->name); - if(!$run->valid || !$this->user) { - $this->setData(Response::STATUS_NOT_FOUND, 'Not Found', null, 'Invalid Run or run/user not found'); - return false; - } elseif (!$this->user->created($run)) { - $this->setData(Response::STATUS_UNAUTHORIZED, 'Unauthorized Access', null, 'Unauthorized access to run'); - return false; - } - - return $run; - } - - private function setData($statusCode = null, $statusText = null, $response = null, $error = null) { - if ($error !== null) { - $this->setError($statusCode, $statusText, $error); - return $this->setData($statusCode, $statusText, $this->error); - } - - if ($statusCode !== null) { - $this->data['statusCode'] = $statusCode; - } - if ($statusText !== null) { - $this->data['statusText'] = $statusText; - } - if ($response !== null) { - $this->data['response'] = $response; - } - } - - private function setError($code = null, $error = null, $desc = null) { - if ($code !== null) { - $this->error['error_code'] = $code; - } - if ($error !== null) { - $this->error['error'] = $error; - } - if ($desc !== null) { - $this->error['error_description'] = $desc; - } - } - - private function parseJsonRequest() { - $request = $this->request->str('request', null) ? $this->request->str('request') : file_get_contents('php://input'); - if (!$request) { - $this->setData(Response::STATUS_BAD_REQUEST, 'Invalid Request', null, "Request payload not found"); - return false; - } - $object = json_decode($request); - if (!$object) { - $this->setData(Response::STATUS_BAD_REQUEST, 'Invalid Request', null, "Unable to parse JSON request"); - return false; - } - return $object; - } - - private function getSurveyResults(Survey $survey, $session = null, $requested_items = array()) { - $data = $survey->getItemDisplayResults($requested_items, array('session' => $session)); - // Get requested item names to match by id - $select = $this->fdb->select('id, name') - ->from('survey_items') - ->where(array('study_id' => $survey->id)); - if ($requested_items) { - $select->whereIn('name', $requested_items); - } - $stmt = $select->statement(); - $items = array(); - while (($row = $stmt->fetch(PDO::FETCH_ASSOC))) { - $items[$row['id']] = $row['name']; - } - - $results = array(); - foreach ($data as $row) { - if (!isset($items[$row['item_id']])) { - continue; - } - $session_id = $row['unit_session_id']; - if (!isset($results[$session_id])) { - $results[$session_id] = array('session' => $session); - } - $results[$session_id][$items[$row['item_id']]] = $row['answer']; - } - return array_values($results); - } + /** + * @var DB + */ + protected $fdb; + + /** + * @var Request + */ + protected $request; + + /** + * A conainer to hold processed request's outcome + * + * @var array + */ + protected $data = array( + 'statusCode' => Response::STATUS_OK, + 'statusText' => 'OK', + 'response' => array(), + ); + + /** + * Formr user object current accessing data + * @var User + */ + protected $user = null; + + /** + * Error information + * + * @var array + */ + protected $error = array(); + + public function __construct(Request $request, DB $db) { + $this->fdb = $db; + $this->request = $request; + $this->user = OAuthHelper::getInstance()->getUserByAccessToken($this->request->getParam('access_token')); + } + + public function results() { + /** + * $request in this method is expected to be an object of the following format + * $request = '{ + * run: { + * name: 'some_run_name', + * api_secret: 'run_api_secret_as_show_in_run_settings' + * surveys: [{ + * name: 'survey_name', + * items: 'name, email, pov_1' + * }], + * sessions: ['xxxx', 'xxxx'] + * } + * }' + */ + // Get run object from request + $request_run = $this->request->arr('run'); + $request_surveys = $this->request->arr('surveys'); + $request = array('run' => array( + 'name' => array_val($request_run, 'name', null), + 'session' => array_val($request_run, 'session', null), + 'sessions' => array_filter(explode(',', array_val($request_run, 'sessions', false))), + 'surveys' => array() + )); + + foreach ($request_surveys as $survey_name => $survey_fields) { + $request['run']['surveys'][] = (object) array( + 'name' => $survey_name, + 'items' => $survey_fields, + ); + } + + $request = json_decode(json_encode($request)); + if (!($run = $this->getRunFromRequest($request))) { + return $this; + } + $requested_run = $request->run; + + // Determine which surveys in the run for which to collect data + if (!empty($requested_run->survey)) { + $surveys = array($requested_run->survey); + } elseif (!empty($requested_run->surveys)) { + $surveys = $requested_run->surveys; + } else { + $surveys = array(); + $run_surveys = $run->getAllSurveys(); + foreach ($run_surveys as $survey) { + /** @var Survey $svy */ + $svy = Survey::loadById($survey['id']); + $items = $svy->getItems('id, type, name'); + $items_names = array(); + foreach ($items as $item) { + $items_names[] = $item['name']; + } + $surveys[] = (object) array( + 'name' => $svy->name, + 'items' => implode(',', $items_names), + 'object' => $svy, + ); + } + } + + // Determine which run sessions in the run will be returned. + // (For now let's prevent returning data of all sessions so this should be required) + if (!empty($requested_run->session)) { + $requested_run->sessions = array($requested_run->session); + } + // If no specific sessions are requested then return all sessions + if (empty($requested_run->sessions)) { + $sessions = $this->fdb->find('survey_run_sessions', array('run_id' => $run->id), array('cols' => 'session')); + foreach ($sessions as $sess) { + $requested_run->sessions[] = $sess['session']; + } + } + + // If sessions are still not available then run is empty + if (empty($requested_run->sessions)) { + $this->setData(Response::STATUS_NOT_FOUND, 'Empty Run', null, 'No sessions were found in this run.'); + return $this; + } + + + // Get result for each survey for each session + $results = array(); + foreach ($surveys as $s) { + if (empty($s->name)) { + continue; + } + if (empty($s->object)) { + $s->object = Survey::loadByUserAndName($this->user, $s->name); + } + + /** @var Survey $svy */ + $svy = $s->object; + if (empty($svy->valid)) { + $results[$s->name] = null; + continue; + } + + if (empty($s->items)) { + $items = array(); + } elseif (is_array($s->items)) { + $items = array_map('trim', $s->items); + } elseif (is_string($s->items)) { + $items = array_map('trim', explode(',', $s->items)); + } else { + throw new Exception("Invalid type for survey items. Type: " . gettype($s->itmes)); + } + + //Get data for all requested sessions in this survey + $results[$s->name] = array(); + foreach ($requested_run->sessions as $session) { + $results[$s->name] = array_merge($results[$s->name], $this->getSurveyResults($svy, $session, $items)); + } + } + + $this->setData(Response::STATUS_OK, 'OK', $results); + return $this; + } + + public function createSession() { + if (!($request = $this->parseJsonRequest()) || !($run = $this->getRunFromRequest($request))) { + return $this; + } + + $i = 0; + $run_session = new RunSession($this->fdb, $run->id, null, null, null); + $code = null; + if (!empty($request->run->code)) { + $code = $request->run->code; + } + + if (!is_array($code)) { + $code = array($code); + } + + $sessions = array(); + foreach ($code as $session) { + if (($created = $run_session->create($session))) { + $sessions[] = $run_session->session; + $i++; + } + } + + if ($i) { + $this->setData(Response::STATUS_OK, 'OK', array('created_sessions' => $i, 'sessions' => $sessions)); + } else { + $this->setData(Response::STATUS_INTERNAL_SERVER_ERROR, 'Error Request', null, 'Error occured when creating session'); + } + + return $this; + } + + public function endLastExternal() { + if (!($request = $this->parseJsonRequest())) { + return $this; + } + + $run = new Run($this->fdb, $request->run->name); + if (!$run->valid) { + $this->setData(Response::STATUS_NOT_FOUND, 'Not Found', null, 'Invalid Run'); + return $this; + } + + if (!empty($request->run->session)) { + $session_code = $request->run->session; + $run_session = new RunSession($this->fdb, $run->id, null, $session_code, null); + + if ($run_session->session !== NULL) { + $run_session->endLastExternal(); + $this->setData(Response::STATUS_OK, 'OK', array('success' => 'external unit ended')); + } else { + $this->setData(Response::STATUS_NOT_FOUND, 'Not Found', null, 'Invalid user session'); + } + } else { + $this->setData(Response::STATUS_NOT_FOUND, 'Not Found', null, 'Session code not found'); + } + + return $this; + } + + public function getData() { + return $this->data; + } + + /** + * Get Run object from an API request + * $request object must have a root element called 'run' which must have child elements called 'name' and 'api_secret' + * Example: + * $request = '{ + * run: { + * name: 'some_run_name', + * api_secret: 'run_api_secret_as_show_in_run_settings' + * } + * }' + * + * @param object $request A JSON object of the sent request + * @return boolean|Run Returns a run object if a valid run is found or FALSE otherwise + */ + protected function getRunFromRequest($request) { + if (empty($request->run->name)) { + $this->setData(Response::STATUS_NOT_FOUND, 'Not Found', null, 'Required "run : { name: }" parameter not found.'); + return false; + } + + $run = new Run($this->fdb, $request->run->name); + if (!$run->valid || !$this->user) { + $this->setData(Response::STATUS_NOT_FOUND, 'Not Found', null, 'Invalid Run or run/user not found'); + return false; + } elseif (!$this->user->created($run)) { + $this->setData(Response::STATUS_UNAUTHORIZED, 'Unauthorized Access', null, 'Unauthorized access to run'); + return false; + } + + return $run; + } + + private function setData($statusCode = null, $statusText = null, $response = null, $error = null) { + if ($error !== null) { + $this->setError($statusCode, $statusText, $error); + return $this->setData($statusCode, $statusText, $this->error); + } + + if ($statusCode !== null) { + $this->data['statusCode'] = $statusCode; + } + if ($statusText !== null) { + $this->data['statusText'] = $statusText; + } + if ($response !== null) { + $this->data['response'] = $response; + } + } + + private function setError($code = null, $error = null, $desc = null) { + if ($code !== null) { + $this->error['error_code'] = $code; + } + if ($error !== null) { + $this->error['error'] = $error; + } + if ($desc !== null) { + $this->error['error_description'] = $desc; + } + } + + private function parseJsonRequest() { + $request = $this->request->str('request', null) ? $this->request->str('request') : file_get_contents('php://input'); + if (!$request) { + $this->setData(Response::STATUS_BAD_REQUEST, 'Invalid Request', null, "Request payload not found"); + return false; + } + $object = json_decode($request); + if (!$object) { + $this->setData(Response::STATUS_BAD_REQUEST, 'Invalid Request', null, "Unable to parse JSON request"); + return false; + } + return $object; + } + + private function getSurveyResults(Survey $survey, $session = null, $requested_items = array()) { + $data = $survey->getItemDisplayResults($requested_items, array('session' => $session)); + // Get requested item names to match by id + $select = $this->fdb->select('id, name') + ->from('survey_items') + ->where(array('study_id' => $survey->id)); + if ($requested_items) { + $select->whereIn('name', $requested_items); + } + $stmt = $select->statement(); + $items = array(); + while (($row = $stmt->fetch(PDO::FETCH_ASSOC))) { + $items[$row['id']] = $row['name']; + } + + $results = array(); + foreach ($data as $row) { + if (!isset($items[$row['item_id']])) { + continue; + } + $session_id = $row['unit_session_id']; + if (!isset($results[$session_id])) { + $results[$session_id] = array('session' => $session); + } + $results[$session_id][$items[$row['item_id']]] = $row['answer']; + } + return array_values($results); + } } diff --git a/application/Helper/GearmanWorkerHelper.php b/application/Helper/GearmanWorkerHelper.php index 9cc403b9d..c6c0c425c 100644 --- a/application/Helper/GearmanWorkerHelper.php +++ b/application/Helper/GearmanWorkerHelper.php @@ -2,247 +2,248 @@ class GearmanWorkerHelper extends GearmanWorker { - /** - * - * @var GearmanClient - */ - protected $gearmanClient; - - /** - * - * @var string - */ - protected $logFile; - - public function __construct() { - parent::__construct(); - $servers = Config::get('deamon.gearman_servers'); - foreach ($servers as $server) { - list($host, $port) = explode(':', $server, 2); - $this->addServer($host, $port); - } - $this->logFile = get_log_file('deamon.log'); - } - - /** - * Will run jobs until $amount of jobs is processed. - * $timeout is used for to terminate if waiting for a new job takes too long - * Pass $timeout = 0 for no timeout - * - * @param int $amount Amount of jobs to process , Default: 1 - * @param int $timeout Time (in seconds) to spend waiting for jobs. , Default: 0 - no timeout. will wait infinitely - */ - public function doJobs($amount = 1, $timeout = 0) { - $amount = intval($amount); - $timeout = intval($timeout); - if ($amount == 0) { - $this->dbg('Running 0 jobs, exiting.'); - exit(0); - } - $originalAmount = $amount; - $this->setTimeout($timeout * 1000 + 100); - $keepWorking = true; - while ($keepWorking) { - if ($this->returnCode() === GEARMAN_TIMEOUT) { - $startTime = isset($startTime) ? $startTime : time(); - } - $this->dbg('Attempting to get job'); - $this->work(); - - if ($this->returnCode() === GEARMAN_TIMEOUT) { - $startTime = isset($startTime) ? $startTime : time(); - if (time() - $startTime >= $timeout) { - $this->dbg("Waited for a new job for $timeout seconds, giving up."); - // @todo notify admin that worker is going offline and use exit code 90 (expected) - exit(0); - } - } else if (in_array($this->returnCode(), array(GEARMAN_WORK_EXCEPTION, GEARMAN_SUCCESS, GEARMAN_WORK_FAIL))) { - - if(in_array($this->returnCode(), array(GEARMAN_WORK_EXCEPTION, GEARMAN_WORK_FAIL))) { - $this->dbg('GearmanError: ' . $this->error()); - } - - $this->dbg('Job complete'); - $amount -= 1; - if ($amount == 0) { - $this->dbg("Completed $originalAmount jobs"); - $keepWorking = false; - // sleep abit so that process can spawn again (notify admin) - sleep(10); - } - - } - } - $this->dbg('finished'); - } - - /** - * - * @param boolean $refresh - * @return GearmanClient - */ - protected function getGearmanClient($refresh = false) { - if ($this->gearmanClient === null || $refresh === true) { - $client = new GearmanClient(); - $servers = Config::get('deamon.gearman_servers'); - foreach ($servers as $server) { - list($host, $port) = explode(':', $server, 2); - $client->addServer($host, $port); - } - $this->gearmanClient = $client; - } - return $this->gearmanClient; - } - - /** - * Debug output - * - * @param string $str - * @param array $args - * @param Run $run - */ - protected function dbg($str, $args = array(), Run $run = null) { - if ($run !== null && is_object($run)) { - $logfile = get_log_file("cron/cron-run-{$run->name}.log"); - } else { - $logfile = $this->logFile; - } - - if (count($args) > 0) { - $str = vsprintf($str, $args); - } - - $str = join(" ", array( - date('Y-m-d H:i:s'), - get_class($this), - getmypid(), - $str, - PHP_EOL - )); - return error_log($str, 3, $logfile); - } - - protected function cleanup(Run $run = null) { - global $site; - if ($site->alerts) { - $this->dbg("\n\n%s\n", array($site->renderAlerts()), $run); - } - } - - /** - * Sets job return status and writes statistics. - * - * @param GearmanJob $job - * @param int $GEARMAN_JOB_STATUS - * @param Run $run - */ - protected function setJobReturn(GearmanJob $job, $GEARMAN_JOB_STATUS, Run $run = null) { - $job->setReturn($GEARMAN_JOB_STATUS); - $this->cleanup($run); - } - - /** - * Send a GEARMAN_WORK_EXCEPTION status for a job and exit with error code 225 - * (the sleeping is added so that processes like supervisor should not attempt to restart quickly) - * - * @param GearmanJob $job - * @param Exception $ex - * @param Run $run - */ - protected function sendJobException(GearmanJob $job, Exception $ex, Run $run = null) { - $job->sendException($ex->getMessage() . PHP_EOL . $ex->getTraceAsString()); - $job->sendFail(); - $this->cleanup($run); - // sleep abit so that process can spawn again (notify admin) - sleep(4); - exit(225); - } + /** + * + * @var GearmanClient + */ + protected $gearmanClient; + + /** + * + * @var string + */ + protected $logFile; + + public function __construct() { + parent::__construct(); + $servers = Config::get('deamon.gearman_servers'); + foreach ($servers as $server) { + list($host, $port) = explode(':', $server, 2); + $this->addServer($host, $port); + } + $this->logFile = get_log_file('deamon.log'); + } + + /** + * Will run jobs until $amount of jobs is processed. + * $timeout is used for to terminate if waiting for a new job takes too long + * Pass $timeout = 0 for no timeout + * + * @param int $amount Amount of jobs to process , Default: 1 + * @param int $timeout Time (in seconds) to spend waiting for jobs. , Default: 0 - no timeout. will wait infinitely + */ + public function doJobs($amount = 1, $timeout = 0) { + $amount = intval($amount); + $timeout = intval($timeout); + if ($amount == 0) { + $this->dbg('Running 0 jobs, exiting.'); + exit(0); + } + $originalAmount = $amount; + $this->setTimeout($timeout * 1000 + 100); + $keepWorking = true; + while ($keepWorking) { + if ($this->returnCode() === GEARMAN_TIMEOUT) { + $startTime = isset($startTime) ? $startTime : time(); + } + $this->dbg('Attempting to get job'); + $this->work(); + + if ($this->returnCode() === GEARMAN_TIMEOUT) { + $startTime = isset($startTime) ? $startTime : time(); + if (time() - $startTime >= $timeout) { + $this->dbg("Waited for a new job for $timeout seconds, giving up."); + // @todo notify admin that worker is going offline and use exit code 90 (expected) + exit(0); + } + } else if (in_array($this->returnCode(), array(GEARMAN_WORK_EXCEPTION, GEARMAN_SUCCESS, GEARMAN_WORK_FAIL))) { + + if (in_array($this->returnCode(), array(GEARMAN_WORK_EXCEPTION, GEARMAN_WORK_FAIL))) { + $this->dbg('GearmanError: ' . $this->error()); + } + + $this->dbg('Job complete'); + $amount -= 1; + if ($amount == 0) { + $this->dbg("Completed $originalAmount jobs"); + $keepWorking = false; + // sleep abit so that process can spawn again (notify admin) + sleep(10); + } + } + } + $this->dbg('finished'); + } + + /** + * + * @param boolean $refresh + * @return GearmanClient + */ + protected function getGearmanClient($refresh = false) { + if ($this->gearmanClient === null || $refresh === true) { + $client = new GearmanClient(); + $servers = Config::get('deamon.gearman_servers'); + foreach ($servers as $server) { + list($host, $port) = explode(':', $server, 2); + $client->addServer($host, $port); + } + $this->gearmanClient = $client; + } + return $this->gearmanClient; + } + + /** + * Debug output + * + * @param string $str + * @param array $args + * @param Run $run + */ + protected function dbg($str, $args = array(), Run $run = null) { + if ($run !== null && is_object($run)) { + $logfile = get_log_file("cron/cron-run-{$run->name}.log"); + } else { + $logfile = $this->logFile; + } + + if (count($args) > 0) { + $str = vsprintf($str, $args); + } + + $str = join(" ", array( + date('Y-m-d H:i:s'), + get_class($this), + getmypid(), + $str, + PHP_EOL + )); + return error_log($str, 3, $logfile); + } + + protected function cleanup(Run $run = null) { + global $site; + if ($site->alerts) { + $this->dbg("\n\n%s\n", array($site->renderAlerts()), $run); + } + } + + /** + * Sets job return status and writes statistics. + * + * @param GearmanJob $job + * @param int $GEARMAN_JOB_STATUS + * @param Run $run + */ + protected function setJobReturn(GearmanJob $job, $GEARMAN_JOB_STATUS, Run $run = null) { + $job->setReturn($GEARMAN_JOB_STATUS); + $this->cleanup($run); + } + + /** + * Send a GEARMAN_WORK_EXCEPTION status for a job and exit with error code 225 + * (the sleeping is added so that processes like supervisor should not attempt to restart quickly) + * + * @param GearmanJob $job + * @param Exception $ex + * @param Run $run + */ + protected function sendJobException(GearmanJob $job, Exception $ex, Run $run = null) { + $job->sendException($ex->getMessage() . PHP_EOL . $ex->getTraceAsString()); + $job->sendFail(); + $this->cleanup($run); + // sleep abit so that process can spawn again (notify admin) + sleep(4); + exit(225); + } } class RunWorkerHelper extends GearmanWorkerHelper { - public function __construct() { - parent::__construct(); - $this->addFunction('process_run', array($this, 'processRun')); - } - - public function processRun(GearmanJob $job) { - $r = null; - try { - $run = json_decode($job->workload(), true); - if (empty($run['name'])) { - $ex = new Exception("Missing parameters for job: " . $job->workload()); - return $this->sendJobException($job, $ex); - } - - $r = new Run(DB::getInstance(), $run['name']); - if (!$r->valid) { - $ex = new Exception("Invalid Run {$run['name']}"); - return $this->sendJobException($job, $ex); - } - - $this->dbg("Processing run >>> %s", array($run['name']), $r); - $dues = $r->getCronDues(); - $i = 0; - foreach ($dues as $session) { - $data = array( - 'session' => $session, - 'run_name' => $run['name'], - 'run_id' => $run['id'], - ); - $workload = json_encode($data); - $this->getGearmanClient()->doBackground('process_run_session', $workload, md5($workload)); - $i++; - } - - $this->dbg("%s sessions in the run '%s' were queued for processing", array($i, $run['name']), $r); - } catch (Exception $e) { - $this->dbg("Error: " . $e->getMessage() . PHP_EOL . $e->getTraceAsString()); - return $this->sendJobException($job, $e, $r); - } - - $this->setJobReturn($job, GEARMAN_SUCCESS, $r); - } + public function __construct() { + parent::__construct(); + $this->addFunction('process_run', array($this, 'processRun')); + } + + public function processRun(GearmanJob $job) { + $r = null; + try { + $run = json_decode($job->workload(), true); + if (empty($run['name'])) { + $ex = new Exception("Missing parameters for job: " . $job->workload()); + return $this->sendJobException($job, $ex); + } + + $r = new Run(DB::getInstance(), $run['name']); + if (!$r->valid) { + $ex = new Exception("Invalid Run {$run['name']}"); + return $this->sendJobException($job, $ex); + } + + $this->dbg("Processing run >>> %s", array($run['name']), $r); + $dues = $r->getCronDues(); + $i = 0; + foreach ($dues as $session) { + $data = array( + 'session' => $session, + 'run_name' => $run['name'], + 'run_id' => $run['id'], + ); + $workload = json_encode($data); + $this->getGearmanClient()->doBackground('process_run_session', $workload, md5($workload)); + $i++; + } + + $this->dbg("%s sessions in the run '%s' were queued for processing", array($i, $run['name']), $r); + } catch (Exception $e) { + $this->dbg("Error: " . $e->getMessage() . PHP_EOL . $e->getTraceAsString()); + return $this->sendJobException($job, $e, $r); + } + + $this->setJobReturn($job, GEARMAN_SUCCESS, $r); + } + } class RunSessionWorkerHelper extends GearmanWorkerHelper { - public function __construct() { - parent::__construct(); - $this->addFunction('process_run_session', array($this, 'processRunSession')); - } - - public function processRunSession(GearmanJob $job) { - $r = null; - try { - $session = json_decode($job->workload(), true); - - if (empty($session['session'])) { - $ex = new Exception("Missing parameters for job: " . $job->workload()); - return $this->sendJobException($job, $ex); - } - - $r = new Run(DB::getInstance(), $session['run_name']); - if (!$r->valid) { - $ex = new Exception("Invalid Run {$session['run_name']}"); - return $this->sendJobException($job, $ex); - } - $owner = $r->getOwner(); - //$this->dbg("Processing run session >>> %s > %s", array($session['run_name'], $session['session']), $r); - - $run_session = new RunSession(DB::getInstance(), $r->id, 'cron', $session['session'], $r); - $types = $run_session->getUnit(); // start looping thru their units. - if ($types === false) { - $error = "This session '{$session['session']}' caused problems"; - alert($error, 'alert-danger'); - //throw new Exception($error); - } - } catch (Exception $e) { - $this->dbg("Error: " . $e->getMessage() . PHP_EOL . $e->getTraceAsString()); - return $this->sendJobException($job, $e, $r); - } - // @todo. Echo types - $this->setJobReturn($job, GEARMAN_SUCCESS, $r); - } + public function __construct() { + parent::__construct(); + $this->addFunction('process_run_session', array($this, 'processRunSession')); + } + + public function processRunSession(GearmanJob $job) { + $r = null; + try { + $session = json_decode($job->workload(), true); + + if (empty($session['session'])) { + $ex = new Exception("Missing parameters for job: " . $job->workload()); + return $this->sendJobException($job, $ex); + } + + $r = new Run(DB::getInstance(), $session['run_name']); + if (!$r->valid) { + $ex = new Exception("Invalid Run {$session['run_name']}"); + return $this->sendJobException($job, $ex); + } + $owner = $r->getOwner(); + //$this->dbg("Processing run session >>> %s > %s", array($session['run_name'], $session['session']), $r); + + $run_session = new RunSession(DB::getInstance(), $r->id, 'cron', $session['session'], $r); + $types = $run_session->getUnit(); // start looping thru their units. + if ($types === false) { + $error = "This session '{$session['session']}' caused problems"; + alert($error, 'alert-danger'); + //throw new Exception($error); + } + } catch (Exception $e) { + $this->dbg("Error: " . $e->getMessage() . PHP_EOL . $e->getTraceAsString()); + return $this->sendJobException($job, $e, $r); + } + // @todo. Echo types + $this->setJobReturn($job, GEARMAN_SUCCESS, $r); + } + } diff --git a/application/Helper/OAuthHelper.php b/application/Helper/OAuthHelper.php index 162dfa528..a5eece9a0 100644 --- a/application/Helper/OAuthHelper.php +++ b/application/Helper/OAuthHelper.php @@ -6,140 +6,140 @@ */ class OAuthHelper { - /** - * @var \OAuth2\Server - */ - protected $server; - - /** - * - * @var \OAuth2\Storage\Pdo - */ - protected $storage; - - /** - * @var array - */ - protected $config; - - /** - * @var OAuthHelper - */ - public static $instance; - - const DEFAULT_REDIRECT_URL = 'http://formr.org'; - - public function __construct($config = array()) { - $this->server = Site::getOauthServer(); - $this->storage = $this->server->getStorage('client'); - $this->config = array_merge(array( - 'client_table' => 'oauth_clients', - 'access_token_table' => 'oauth_access_tokens', - 'refresh_token_table' => 'oauth_refresh_tokens', - 'code_table' => 'oauth_authorization_codes', - 'user_table' => 'oauth_users', - 'jwt_table' => 'oauth_jwt', - 'jti_table' => 'oauth_jti', - 'scope_table' => 'oauth_scopes', - 'public_key_table' => 'oauth_public_keys', - ), $config); - } - - public static function getInstance() { - if (self::$instance === null) { - self::$instance = new self(); - } - return self::$instance; - } - - public function createClient(User $formrUser) { - /* @var $storage \OAuth2\Storage\Pdo */ - if (($client = $this->getClient($formrUser))) { - return $client; - } - - $details = $this->generateClientDetails($formrUser); - $this->storage->setClientDetails($details['client_id'], $details['client_secret'], self::DEFAULT_REDIRECT_URL, null, null, $formrUser->email); - return $this->getClient($formrUser); - } - - public function getClient(User $formrUser) { - $db = Site::getDb(); - $client_id = $db->findValue($this->config['client_table'], array('user_id' => $formrUser->email), 'client_id'); - if (!$client_id) { - return false; - } - if (($client = $this->storage->getClientDetails($client_id))) { - return $client; - } - return false; - } - - public function deleteClient(User $formrUser) { - $client = $this->getClient($formrUser); - if (!$client) { - return false; - } - - $client_id = $client['client_id']; - $db = Site::getDb(); - $db->delete($this->config['client_table'], array('client_id' => $client_id)); - $db->delete($this->config['access_token_table'], array('client_id' => $client_id)); - $db->delete($this->config['refresh_token_table'], array('client_id' => $client_id)); - $db->delete($this->config['code_table'], array('client_id' => $client_id)); - $db->delete($this->config['jwt_table'], array('client_id' => $client_id)); - return true; - } - - public function refreshToken(User $formrUser) { - $client = $this->getClient($formrUser); - if (!$client) { - return false; - } - $details = $this->generateClientDetails($formrUser, true); - $client_id = $client['client_id']; - $client_secret = $details['client_secret']; - $this->storage->setClientDetails($client_id, $client_secret, self::DEFAULT_REDIRECT_URL, null, null, $formrUser->email); - return compact('client_id', 'client_secret'); - } - - /** - * Get formr user object from API access token - * - * @param string $access_token - * @return User|boolean If no corresponding user is found, FALSE is returned - */ - public function getUserByAccessToken($access_token) { - if (!$access_token) { - return false; - } - - $db = Site::getDb(); - $user_email = $db->findValue($this->config['access_token_table'], array('access_token' => $access_token), 'user_id'); - $user_id = $db->findValue('survey_users', array('email' => $user_email), 'id'); - if (!$user_id) { - return false; - } - return new User($db, $user_id); - } - - /** - * Generate client ID and client Secret from User object - * - * @todo Re-do the algorithm to create credentials - * @param User $formrUser - * @param bool $refresh - * @return array - */ - protected function generateClientDetails(User $formrUser, $refresh = false) { - $jwt = new OAuth2\Encryption\Jwt(); - $append = $refresh ? microtime(true) : ''; - $client_id = md5($formrUser->id . $formrUser->email . $append); - if ($refresh) { - $client_id = $append . $formrUser->email . $formrUser->id; - } - $client_secret = substr(str_replace('.', '', $jwt->encode($client_id, $client_id)), 0, 60); - return compact('client_id', 'client_secret'); - } + /** + * @var \OAuth2\Server + */ + protected $server; + + /** + * + * @var \OAuth2\Storage\Pdo + */ + protected $storage; + + /** + * @var array + */ + protected $config; + + /** + * @var OAuthHelper + */ + public static $instance; + + const DEFAULT_REDIRECT_URL = 'http://formr.org'; + + public function __construct($config = array()) { + $this->server = Site::getOauthServer(); + $this->storage = $this->server->getStorage('client'); + $this->config = array_merge(array( + 'client_table' => 'oauth_clients', + 'access_token_table' => 'oauth_access_tokens', + 'refresh_token_table' => 'oauth_refresh_tokens', + 'code_table' => 'oauth_authorization_codes', + 'user_table' => 'oauth_users', + 'jwt_table' => 'oauth_jwt', + 'jti_table' => 'oauth_jti', + 'scope_table' => 'oauth_scopes', + 'public_key_table' => 'oauth_public_keys', + ), $config); + } + + public static function getInstance() { + if (self::$instance === null) { + self::$instance = new self(); + } + return self::$instance; + } + + public function createClient(User $formrUser) { + /* @var $storage \OAuth2\Storage\Pdo */ + if (($client = $this->getClient($formrUser))) { + return $client; + } + + $details = $this->generateClientDetails($formrUser); + $this->storage->setClientDetails($details['client_id'], $details['client_secret'], self::DEFAULT_REDIRECT_URL, null, null, $formrUser->email); + return $this->getClient($formrUser); + } + + public function getClient(User $formrUser) { + $db = Site::getDb(); + $client_id = $db->findValue($this->config['client_table'], array('user_id' => $formrUser->email), 'client_id'); + if (!$client_id) { + return false; + } + if (($client = $this->storage->getClientDetails($client_id))) { + return $client; + } + return false; + } + + public function deleteClient(User $formrUser) { + $client = $this->getClient($formrUser); + if (!$client) { + return false; + } + + $client_id = $client['client_id']; + $db = Site::getDb(); + $db->delete($this->config['client_table'], array('client_id' => $client_id)); + $db->delete($this->config['access_token_table'], array('client_id' => $client_id)); + $db->delete($this->config['refresh_token_table'], array('client_id' => $client_id)); + $db->delete($this->config['code_table'], array('client_id' => $client_id)); + $db->delete($this->config['jwt_table'], array('client_id' => $client_id)); + return true; + } + + public function refreshToken(User $formrUser) { + $client = $this->getClient($formrUser); + if (!$client) { + return false; + } + $details = $this->generateClientDetails($formrUser, true); + $client_id = $client['client_id']; + $client_secret = $details['client_secret']; + $this->storage->setClientDetails($client_id, $client_secret, self::DEFAULT_REDIRECT_URL, null, null, $formrUser->email); + return compact('client_id', 'client_secret'); + } + + /** + * Get formr user object from API access token + * + * @param string $access_token + * @return User|boolean If no corresponding user is found, FALSE is returned + */ + public function getUserByAccessToken($access_token) { + if (!$access_token) { + return false; + } + + $db = Site::getDb(); + $user_email = $db->findValue($this->config['access_token_table'], array('access_token' => $access_token), 'user_id'); + $user_id = $db->findValue('survey_users', array('email' => $user_email), 'id'); + if (!$user_id) { + return false; + } + return new User($db, $user_id); + } + + /** + * Generate client ID and client Secret from User object + * + * @todo Re-do the algorithm to create credentials + * @param User $formrUser + * @param bool $refresh + * @return array + */ + protected function generateClientDetails(User $formrUser, $refresh = false) { + $jwt = new OAuth2\Encryption\Jwt(); + $append = $refresh ? microtime(true) : ''; + $client_id = md5($formrUser->id . $formrUser->email . $append); + if ($refresh) { + $client_id = $append . $formrUser->email . $formrUser->id; + } + $client_secret = substr(str_replace('.', '', $jwt->encode($client_id, $client_id)), 0, 60); + return compact('client_id', 'client_secret'); + } } diff --git a/application/Helper/RunHelper.php b/application/Helper/RunHelper.php index c09dd23a9..1e4067727 100644 --- a/application/Helper/RunHelper.php +++ b/application/Helper/RunHelper.php @@ -2,130 +2,130 @@ class RunHelper { - /** - * - * @var Request - */ - protected $request; - - /** - * - * @var string - */ - protected $run_name; - - /** - * @var Run - */ - protected $run; - - /** - * - * @var RunSession - */ - protected $runSession; - - /** - * - * @var DB - */ - protected $db; - - protected $errors = array(); - protected $message = null; - - public function __construct(Request $r, DB $db, $run) { - $this->request = $r; - $this->run_name = $run; - $this->db = $db; - $this->run = new Run($this->db, $run); - - if (!$this->run->valid) { - throw new Exception("Run with name {$run} not found"); - } - - if ($this->request->session) { - $this->runSession = new RunSession($this->db, $this->run->id, null, $this->request->session, $this->run); - } - } - - public function sendToPosition() { - if ($this->request->session === null || $this->request->new_position === null) { - $this->errors[] = 'Missing session or position parameter'; - return false; - } - - $run_session = $this->runSession; - if (!$run_session->forceTo($this->request->new_position)) { - $this->errors[] = 'Something went wrong with the position change in run ' . $this->run->name; - return false; - } - - $this->message = 'Run-session successfully set to position ' . $this->request->new_position; - return true; - } - - public function remind() { - $email = $this->run->getReminder($this->request->reminder_id, $this->request->session, $this->request->run_session_id); - if ($email->exec() !== false) { - $this->errors[] = 'Something went wrong with the reminder. in run ' . $this->run->name; - $email->end(); - return false; - } - $email->end(); - $this->message = 'Reminder sent'; - return true; - } - - public function nextInRun() { - if (!$this->runSession->endUnitSession()) { - $this->errors[] = 'Unable to move to next unit in run ' . $this->run->name; - return false; - } - $this->message = 'Move done'; - return true; - } - - public function deleteUser() { - $session = $this->request->session; - if (($deleted = $this->db->delete('survey_run_sessions', array('id' => $this->request->run_session_id)))) { - $this->message = "User with session '{$session}' was deleted"; - } else { - $this->errors[] = "User with session '{$session}' could not be deleted"; - } - } - - public function snipUnitSession() { - $run = $this->run; - $session = $this->request->session; - $run_session = new RunSession($this->db, $run->id, null, $session, $run); - - $unit_session = $run_session->getUnitSession(); - if($unit_session): - $deleted = $this->db->delete('survey_unit_sessions', array('id' => $unit_session->id)); - if($deleted): - $this->message = 'Success. You deleted the data at the current position.'; - if (!$run_session->forceTo($run_session->position)): - $this->errors[] = 'Data was deleted, but could not stay at position. ' . $run->name; - return false; - endif; - else: - $this->errors[] = 'Couldn\'t delete.'; - endif; - else: - $this->errors[] = "No unit session found"; - endif; - } - - public function getRunSession() { - return $this->runSession; - } - - public function getErrors() { - return $this->errors; - } - - public function getMessage() { - return $this->message; - } + /** + * + * @var Request + */ + protected $request; + + /** + * + * @var string + */ + protected $run_name; + + /** + * @var Run + */ + protected $run; + + /** + * + * @var RunSession + */ + protected $runSession; + + /** + * + * @var DB + */ + protected $db; + protected $errors = array(); + protected $message = null; + + public function __construct(Request $r, DB $db, $run) { + $this->request = $r; + $this->run_name = $run; + $this->db = $db; + $this->run = new Run($this->db, $run); + + if (!$this->run->valid) { + throw new Exception("Run with name {$run} not found"); + } + + if ($this->request->session) { + $this->runSession = new RunSession($this->db, $this->run->id, null, $this->request->session, $this->run); + } + } + + public function sendToPosition() { + if ($this->request->session === null || $this->request->new_position === null) { + $this->errors[] = 'Missing session or position parameter'; + return false; + } + + $run_session = $this->runSession; + if (!$run_session->forceTo($this->request->new_position)) { + $this->errors[] = 'Something went wrong with the position change in run ' . $this->run->name; + return false; + } + + $this->message = 'Run-session successfully set to position ' . $this->request->new_position; + return true; + } + + public function remind() { + $email = $this->run->getReminder($this->request->reminder_id, $this->request->session, $this->request->run_session_id); + if ($email->exec() !== false) { + $this->errors[] = 'Something went wrong with the reminder. in run ' . $this->run->name; + $email->end(); + return false; + } + $email->end(); + $this->message = 'Reminder sent'; + return true; + } + + public function nextInRun() { + if (!$this->runSession->endUnitSession()) { + $this->errors[] = 'Unable to move to next unit in run ' . $this->run->name; + return false; + } + $this->message = 'Move done'; + return true; + } + + public function deleteUser() { + $session = $this->request->session; + if (($deleted = $this->db->delete('survey_run_sessions', array('id' => $this->request->run_session_id)))) { + $this->message = "User with session '{$session}' was deleted"; + } else { + $this->errors[] = "User with session '{$session}' could not be deleted"; + } + } + + public function snipUnitSession() { + $run = $this->run; + $session = $this->request->session; + $run_session = new RunSession($this->db, $run->id, null, $session, $run); + + $unit_session = $run_session->getUnitSession(); + if ($unit_session): + $deleted = $this->db->delete('survey_unit_sessions', array('id' => $unit_session->id)); + if ($deleted): + $this->message = 'Success. You deleted the data at the current position.'; + if (!$run_session->forceTo($run_session->position)): + $this->errors[] = 'Data was deleted, but could not stay at position. ' . $run->name; + return false; + endif; + else: + $this->errors[] = 'Couldn\'t delete.'; + endif; + else: + $this->errors[] = "No unit session found"; + endif; + } + + public function getRunSession() { + return $this->runSession; + } + + public function getErrors() { + return $this->errors; + } + + public function getMessage() { + return $this->message; + } + } diff --git a/application/Helper/RunUnitHelper.php b/application/Helper/RunUnitHelper.php index 0ad3d0e00..dabe48e9a 100644 --- a/application/Helper/RunUnitHelper.php +++ b/application/Helper/RunUnitHelper.php @@ -1,204 +1,202 @@ db = $db; - $this->expiration_extension = Config::get('unit_session.queue_expiration_extension', '+10 minutes'); - } - - /** - * @return RunUnitHelper - */ - public static function getInstance() { - if (self::$instance === null) { - self::$instance = new self(DB::getInstance()); - } - - return self::$instance; - } - - /** - * Wrapper function - * - * @param UnitSession $unitSession - * @param RunUnit $runUnit - * @param mixed $execResults - * @return int - */ - public function getUnitSessionExpiration(UnitSession $unitSession, RunUnit $runUnit, $execResults) { - $method = sprintf('get%sExpiration', $runUnit->type); - return call_user_func(array($this, $method), $unitSession, $runUnit, $execResults); - } - - /** - * Get expiration timestamp for External Run Unit - * If the results from executing the unit is TRUE then we queue the unit to be executed again after x minutes. - * Other wise 0 should be returned because unit ended after execution - * - * @param UnitSession $unitSession - * @param External $runUnit - * @param mixed $execResults - * @return int - */ - public function getExternalExpiration(UnitSession $unitSession, External $runUnit, $execResults) { - if (!empty($runUnit->execData['expire_timestamp'])) { - return $runUnit->execData['expire_timestamp']; - } elseif ($execResults === true) { - // set expiration to x minutes for unit session to be executed again - return strtotime($this->expiration_extension); - } - return 0; - } - - /** - * Get expiration timestamp for Email Run Unit - * This should always return 0 because email-sending is managed in separate queue - * - * @param UnitSession $unitSession - * @param Email $runUnit - * @param mixed $execResults - * @return int - */ - public function getEmailExpiration(UnitSession $unitSession, Email $runUnit, $execResults) { - return 0; - } - - /** - * Get expiration timestamp for Shuffle Run Unit - * This should always return 0 because a shuffle unit ends immediately after execution - * - * @param UnitSession $unitSession - * @param Shuffle $runUnit - * @param mixed $execResults - * @return int - */ - public function getShuffleExpiration(UnitSession $unitSession, Shuffle $runUnit, $execResults) { - return 0; - } - - /** - * Get expiration timestamp for Page Run Unit - * Does not need to be executed in intervals so no need to queue. - * - * @param UnitSession $unitSession - * @param Page $runUnit - * @param mixed $execResults - * @return int - */ - public function getPageExpiration(UnitSession $unitSession, Page $runUnit, $execResults) { - return 0; - } - - /** - * - * @see self::getPageExpiration() - */ - public function getStopExpiration(UnitSession $unitSession, $runUnit, $execResults) { - return $this->getPageExpiration($unitSession, $runUnit, $execResults); - } - - /** - * - * @see self::getPageExpiration() - */ - public function getEndpageExpiration(UnitSession $unitSession, $runUnit, $execResults) { - return $this->getPageExpiration($unitSession, $runUnit, $execResults); - } - - /** - * Get expiration timestamp for Survey Run Unit - * - * @param UnitSession $unitSession - * @param Survey $runUnit - * @param mixed $execResults - * @return int - */ - public function getSurveyExpiration(UnitSession $unitSession, Survey $runUnit, $execResults) { - if ($execResults === false) { - // Survey expired or ended so no need to queue - return 0; - } - - if (isset($runUnit->execData['expire_timestamp'])) { - return $runUnit->execData['expire_timestamp']; - } else { - return 0; - } - } - - /** - * Get expiration timestamp for Pause Run Unit - * - * @param UnitSession $unitSession - * @param Pause $runUnit - * @param mixed $execResults - * @return int - */ - public function getPauseExpiration(UnitSession $unitSession, Pause $runUnit, $execResults) { - $execData = $runUnit->execData; - if (!empty($execData['pause_over'])) { - // pause is over no need to queue - return 0; - } - - if ($execData['check_failed'] === true || $execData['expire_relatively'] === false) { - // check again in x minutes something went wrong with ocpu evaluation - return strtotime($this->expiration_extension); - } - - if (isset($execData['expire_timestamp'])) { - return $execData['expire_timestamp']; - } - - return 0; - } - - /** - * Get expiration timestamp for Branch (SkipForward | SkipBackward) Run Unit - * - * @param UnitSession $unitSession - * @param Branch $runUnit - * @param mixed $execResults - * @return int - */ - public function getBranchExpiration(UnitSession $unitSession, Branch $runUnit, $execResults) { - if (!empty($runUnit->execData['expire_timestamp'])) { - return (int)$runUnit->execData['expire_timestamp']; - } elseif ($execResults === true) { - // set expiration to x minutes for unit session to be executed again - return strtotime($this->expiration_extension); - } - return 0; - } - - /** - * @see getBranchExpiration - */ - public function getSkipBackwardExpiration(UnitSession $unitSession, Branch $runUnit, $execResults) { - return $this->getBranchExpiration($unitSession, $runUnit, $execResults); - } - - /** - * @see getBranchExpiration - */ - public function getSkipForwardExpiration(UnitSession $unitSession, Branch $runUnit, $execResults) { - return $this->getBranchExpiration($unitSession, $runUnit, $execResults); - } - + /** + * + * @var DB + */ + protected $db; + + /** + * + * @var RunUnitHelper + */ + protected static $instance = null; + protected $expiration_extension; + + protected function __construct(DB $db) { + $this->db = $db; + $this->expiration_extension = Config::get('unit_session.queue_expiration_extension', '+10 minutes'); + } + + /** + * @return RunUnitHelper + */ + public static function getInstance() { + if (self::$instance === null) { + self::$instance = new self(DB::getInstance()); + } + + return self::$instance; + } + + /** + * Wrapper function + * + * @param UnitSession $unitSession + * @param RunUnit $runUnit + * @param mixed $execResults + * @return int + */ + public function getUnitSessionExpiration(UnitSession $unitSession, RunUnit $runUnit, $execResults) { + $method = sprintf('get%sExpiration', $runUnit->type); + return call_user_func(array($this, $method), $unitSession, $runUnit, $execResults); + } + + /** + * Get expiration timestamp for External Run Unit + * If the results from executing the unit is TRUE then we queue the unit to be executed again after x minutes. + * Other wise 0 should be returned because unit ended after execution + * + * @param UnitSession $unitSession + * @param External $runUnit + * @param mixed $execResults + * @return int + */ + public function getExternalExpiration(UnitSession $unitSession, External $runUnit, $execResults) { + if (!empty($runUnit->execData['expire_timestamp'])) { + return $runUnit->execData['expire_timestamp']; + } elseif ($execResults === true) { + // set expiration to x minutes for unit session to be executed again + return strtotime($this->expiration_extension); + } + return 0; + } + + /** + * Get expiration timestamp for Email Run Unit + * This should always return 0 because email-sending is managed in separate queue + * + * @param UnitSession $unitSession + * @param Email $runUnit + * @param mixed $execResults + * @return int + */ + public function getEmailExpiration(UnitSession $unitSession, Email $runUnit, $execResults) { + return 0; + } + + /** + * Get expiration timestamp for Shuffle Run Unit + * This should always return 0 because a shuffle unit ends immediately after execution + * + * @param UnitSession $unitSession + * @param Shuffle $runUnit + * @param mixed $execResults + * @return int + */ + public function getShuffleExpiration(UnitSession $unitSession, Shuffle $runUnit, $execResults) { + return 0; + } + + /** + * Get expiration timestamp for Page Run Unit + * Does not need to be executed in intervals so no need to queue. + * + * @param UnitSession $unitSession + * @param Page $runUnit + * @param mixed $execResults + * @return int + */ + public function getPageExpiration(UnitSession $unitSession, Page $runUnit, $execResults) { + return 0; + } + + /** + * + * @see self::getPageExpiration() + */ + public function getStopExpiration(UnitSession $unitSession, $runUnit, $execResults) { + return $this->getPageExpiration($unitSession, $runUnit, $execResults); + } + + /** + * + * @see self::getPageExpiration() + */ + public function getEndpageExpiration(UnitSession $unitSession, $runUnit, $execResults) { + return $this->getPageExpiration($unitSession, $runUnit, $execResults); + } + + /** + * Get expiration timestamp for Survey Run Unit + * + * @param UnitSession $unitSession + * @param Survey $runUnit + * @param mixed $execResults + * @return int + */ + public function getSurveyExpiration(UnitSession $unitSession, Survey $runUnit, $execResults) { + if ($execResults === false) { + // Survey expired or ended so no need to queue + return 0; + } + + if (isset($runUnit->execData['expire_timestamp'])) { + return $runUnit->execData['expire_timestamp']; + } else { + return 0; + } + } + + /** + * Get expiration timestamp for Pause Run Unit + * + * @param UnitSession $unitSession + * @param Pause $runUnit + * @param mixed $execResults + * @return int + */ + public function getPauseExpiration(UnitSession $unitSession, Pause $runUnit, $execResults) { + $execData = $runUnit->execData; + if (!empty($execData['pause_over'])) { + // pause is over no need to queue + return 0; + } + + if ($execData['check_failed'] === true || $execData['expire_relatively'] === false) { + // check again in x minutes something went wrong with ocpu evaluation + return strtotime($this->expiration_extension); + } + + if (isset($execData['expire_timestamp'])) { + return $execData['expire_timestamp']; + } + + return 0; + } + + /** + * Get expiration timestamp for Branch (SkipForward | SkipBackward) Run Unit + * + * @param UnitSession $unitSession + * @param Branch $runUnit + * @param mixed $execResults + * @return int + */ + public function getBranchExpiration(UnitSession $unitSession, Branch $runUnit, $execResults) { + if (!empty($runUnit->execData['expire_timestamp'])) { + return (int) $runUnit->execData['expire_timestamp']; + } elseif ($execResults === true) { + // set expiration to x minutes for unit session to be executed again + return strtotime($this->expiration_extension); + } + return 0; + } + + /** + * @see getBranchExpiration + */ + public function getSkipBackwardExpiration(UnitSession $unitSession, Branch $runUnit, $execResults) { + return $this->getBranchExpiration($unitSession, $runUnit, $execResults); + } + + /** + * @see getBranchExpiration + */ + public function getSkipForwardExpiration(UnitSession $unitSession, Branch $runUnit, $execResults) { + return $this->getBranchExpiration($unitSession, $runUnit, $execResults); + } + } diff --git a/application/Helper/SurveyHelper.php b/application/Helper/SurveyHelper.php index c0477e565..64ddde8f7 100644 --- a/application/Helper/SurveyHelper.php +++ b/application/Helper/SurveyHelper.php @@ -11,324 +11,320 @@ */ class SurveyHelper { - /** - * - * @var Request - */ - protected $request; - - /** - * @var Run - */ - protected $run; - - /** - * @var Survey - */ - protected $survey; - - /** - * - * @var DB - */ - protected $db; - - /** - * - * @var UnitSession - */ - protected $unitSession; - - protected $errors = array(); - protected $message = null; - - protected $maxPage = null; - - protected $postedValues = array(); - - protected $answeredItems = array(); - - const FMR_PAGE_ELEMENT = 'fmr_unit_page_element'; - - public function __construct(Request $rq, Survey $s, Run $r) { - $this->request = $rq; - $this->survey = $s; - $this->run = $r; - $this->db = $s->dbh; - } - - /** - * Returns HTML page to be rendered for Survey or FALSE if survey ended - * - * @return string|boolean - */ - public function renderSurvey($unitSessionId) { - $unitSession = $this->getUnitSession($unitSessionId); - if (!Request::getGlobals('pageNo')) { - $pageNo = $this->getCurrentPage(); - $this->redirectToPage($pageNo); - } - - $pageNo = $this->getCurrentPage(); - if ($pageNo < 1) { - throw new Exception('Invalid Survey Page'); - } - - // Check if user is allowed to enter this page - if ($prev = $this->emptyPreviousPageExists($pageNo)) { - //alert('There are missing responses in your survey. Please proceed from here', 'alert-warning'); - $this->redirectToPage($prev); - } - - if ($pageNo > $this->getMaxPage()) { - $this->survey->end(); - return false; - } - - $formAction = ''; //run_url($this->run->name, $pageNo); - - $this->survey->rendered_items = $this->getPageItems($pageNo); - $pageElement = $this->getPageElement($pageNo); - Session::delete('is-survey-post'); - return $this->survey->render($formAction, $pageElement); - } - - /** - * Save posted page item for specified Unit Session - * - * @param int $unitSessionId - */ - public function savePageItems($unitSessionId) { - $unitSession = $this->getUnitSession($unitSessionId); - if (!Request::isHTTPPostRequest()) { - // Accept only POST requests - return; - } - - if ($this->request->getParam(self::FMR_PAGE_ELEMENT) != $this->getCurrentPage()) { - throw new Exception('Invalid Survey Page'); - } - - $currPage = $this->request->getParam(self::FMR_PAGE_ELEMENT); - - $pageItems = $this->getPageItems($currPage, false); - $this->postedValues = $this->request->getParams(); - - // Mock the "posting" other items that are suppose to be on this page because user is leaving the page anyway - // and hidden items must have been skipped for this session - foreach ($pageItems as $name => $item) { - if (isset($this->postedValues[$name])) { - $oldValue = $item->value_validated; - $item->value_validated = $this->postedValues[$name]; - if (!$item->requiresUserInput()) { - $item->skip_validation = true; - $item->value_validated = $oldValue; - } - } else { - $item->skip_validation = true; - // If item required user input but was not submitted then it was disabled on the page by show-if - // so set it's value to NULL to revert any previously saved values - if ($item->requiresUserInput()) { - $item->value_validated = null; - } - } - - //$item->value_validated = null; - $this->postedValues[$name] = $item; - } - - unset($this->postedValues['fmr_unit_page_element']); - $save = $this->saveSuryeyItems($this->postedValues); - if ($save) { - Session::set('is-survey-post', true); - $currPage++; - $this->redirectToPage($currPage); - } - } - - /** - * Get Items to be displayed on indicated page No - * - * @param int $pageNo - * @param boolean $process - * @return Item[] - */ - protected function getPageItems($pageNo, $process = true) { - $select = $this->db->select(' + /** + * + * @var Request + */ + protected $request; + + /** + * @var Run + */ + protected $run; + + /** + * @var Survey + */ + protected $survey; + + /** + * + * @var DB + */ + protected $db; + + /** + * + * @var UnitSession + */ + protected $unitSession; + protected $errors = array(); + protected $message = null; + protected $maxPage = null; + protected $postedValues = array(); + protected $answeredItems = array(); + + const FMR_PAGE_ELEMENT = 'fmr_unit_page_element'; + + public function __construct(Request $rq, Survey $s, Run $r) { + $this->request = $rq; + $this->survey = $s; + $this->run = $r; + $this->db = $s->dbh; + } + + /** + * Returns HTML page to be rendered for Survey or FALSE if survey ended + * + * @return string|boolean + */ + public function renderSurvey($unitSessionId) { + $unitSession = $this->getUnitSession($unitSessionId); + if (!Request::getGlobals('pageNo')) { + $pageNo = $this->getCurrentPage(); + $this->redirectToPage($pageNo); + } + + $pageNo = $this->getCurrentPage(); + if ($pageNo < 1) { + throw new Exception('Invalid Survey Page'); + } + + // Check if user is allowed to enter this page + if ($prev = $this->emptyPreviousPageExists($pageNo)) { + //alert('There are missing responses in your survey. Please proceed from here', 'alert-warning'); + $this->redirectToPage($prev); + } + + if ($pageNo > $this->getMaxPage()) { + $this->survey->end(); + return false; + } + + $formAction = ''; //run_url($this->run->name, $pageNo); + + $this->survey->rendered_items = $this->getPageItems($pageNo); + $pageElement = $this->getPageElement($pageNo); + Session::delete('is-survey-post'); + return $this->survey->render($formAction, $pageElement); + } + + /** + * Save posted page item for specified Unit Session + * + * @param int $unitSessionId + */ + public function savePageItems($unitSessionId) { + $unitSession = $this->getUnitSession($unitSessionId); + if (!Request::isHTTPPostRequest()) { + // Accept only POST requests + return; + } + + if ($this->request->getParam(self::FMR_PAGE_ELEMENT) != $this->getCurrentPage()) { + throw new Exception('Invalid Survey Page'); + } + + $currPage = $this->request->getParam(self::FMR_PAGE_ELEMENT); + + $pageItems = $this->getPageItems($currPage, false); + $this->postedValues = $this->request->getParams(); + + // Mock the "posting" other items that are suppose to be on this page because user is leaving the page anyway + // and hidden items must have been skipped for this session + foreach ($pageItems as $name => $item) { + if (isset($this->postedValues[$name])) { + $oldValue = $item->value_validated; + $item->value_validated = $this->postedValues[$name]; + if (!$item->requiresUserInput()) { + $item->skip_validation = true; + $item->value_validated = $oldValue; + } + } else { + $item->skip_validation = true; + // If item required user input but was not submitted then it was disabled on the page by show-if + // so set it's value to NULL to revert any previously saved values + if ($item->requiresUserInput()) { + $item->value_validated = null; + } + } + + //$item->value_validated = null; + $this->postedValues[$name] = $item; + } + + unset($this->postedValues['fmr_unit_page_element']); + $save = $this->saveSuryeyItems($this->postedValues); + if ($save) { + Session::set('is-survey-post', true); + $currPage++; + $this->redirectToPage($currPage); + } + } + + /** + * Get Items to be displayed on indicated page No + * + * @param int $pageNo + * @param boolean $process + * @return Item[] + */ + protected function getPageItems($pageNo, $process = true) { + $select = $this->db->select(' survey_items.id, survey_items.study_id, survey_items.type, survey_items.choice_list, survey_items.type_options, survey_items.name, survey_items.label, survey_items.label_parsed, survey_items.optional, survey_items.class, survey_items.showif, survey_items.value, survey_items_display.displaycount, survey_items_display.session_id, survey_items_display.display_order, survey_items_display.hidden, survey_items_display.answer as value_validated, survey_items_display.answered, survey_items_display.page'); - $select->from('survey_items'); - $select->leftJoin('survey_items_display', 'survey_items_display.session_id = :session_id', 'survey_items_display.item_id = survey_items.id'); - $select->where('survey_items.study_id = :study_id AND survey_items_display.page = :page'); - $select->order('survey_items_display.display_order', 'asc'); - $select->order('survey_items.order', 'asc'); // only needed for transfer - $select->order('survey_items.id', 'asc'); - - $select->bindParams(array( - 'session_id' => $this->unitSession->id, - 'study_id' => $this->survey->id, - 'page' => $pageNo, - )); - $stmt = $select->statement(); - - // We initialise item factory with no choice list because we don't know which choices will be used yet. - // This assumes choices are not required for show-ifs and dynamic values (hope so) - $itemFactory = new ItemFactory(array()); - /* @var $pageItems Item[] */ - $pageItems = array(); - $processShowIfs = true; - - while ($item = $stmt->fetch(PDO::FETCH_ASSOC)) { - $hidden = $item['hidden']; - if ($hidden !== null) { - // show-ifs have been processed for this page - $processShowIfs = false; - } - /* @var $oItem Item */ - $oItem = $itemFactory->make($item); - $oItem->hidden = null; - $visibility = $hidden === null ? true : (bool)!$hidden; - $v = $oItem->type !== 'submit' ? $oItem->setVisibility(array($visibility)) : null; - if ($hidden === null || Session::get('is-survey-post')) { - $oItem->hidden = null; - } else { - $oItem->hidden = (int)$hidden; - } - - $this->markItemAsShown($oItem); - $pItem = array_val($this->postedValues, $oItem->name, $oItem->value_validated); - $oItem->value_validated = $pItem instanceof Item ? $pItem->value_validated : $pItem; - $pageItems[$oItem->name] = $oItem; - - if ($item['answered']) { - $this->answeredItems[$oItem->name] = $oItem->value_validated; - } - - if ($oItem->type === 'submit') { - break; - } - } - - if (!$pageItems) { - return array(); - } - - if ($process === false) { - // Processing is skipped when user has submitted data and we just need to check if what submitted is what was requested - return $pageItems; - } - - $pageItems = $this->processAutomaticItems($pageItems); - // Porcess show-ifs only when necessary i.e when user is not going to a previous page OR page is not being POSTed - if ($processShowIfs || Session::get('is-survey-post') || $this->request->getParam('_rsi_')) { - $pageItems = $this->processDynamicValuesAndShowIfs($pageItems); - } - $pageItems = $this->processDynamicLabelsAndChoices($pageItems); - - // add a submit button if none exists - $lastItem = end($pageItems); - if (($lastItem && $lastItem->type !== 'submit') || ($lastItem && $lastItem->hidden)) { - $pageItems[] = $this->getSubmitButton(); - } - - //Check if there is any rendered item and if not, dummy post these and move to next page - if (!$this->displayedItemExists($pageItems)) { - $this->saveSuryeyItems($pageItems, false); - Session::set('is-survey-post', true); - $pageNo++; - $this->redirectToPage($pageNo); - } - return $pageItems; - } - - protected function getCurrentPage() { - // Check if page exists in Request::globals(); - if ($page = Request::getGlobals('pageNo')) { - return $page; - } - - // If page is not in request then get from DB - $query = ' + $select->from('survey_items'); + $select->leftJoin('survey_items_display', 'survey_items_display.session_id = :session_id', 'survey_items_display.item_id = survey_items.id'); + $select->where('survey_items.study_id = :study_id AND survey_items_display.page = :page'); + $select->order('survey_items_display.display_order', 'asc'); + $select->order('survey_items.order', 'asc'); // only needed for transfer + $select->order('survey_items.id', 'asc'); + + $select->bindParams(array( + 'session_id' => $this->unitSession->id, + 'study_id' => $this->survey->id, + 'page' => $pageNo, + )); + $stmt = $select->statement(); + + // We initialise item factory with no choice list because we don't know which choices will be used yet. + // This assumes choices are not required for show-ifs and dynamic values (hope so) + $itemFactory = new ItemFactory(array()); + /* @var $pageItems Item[] */ + $pageItems = array(); + $processShowIfs = true; + + while ($item = $stmt->fetch(PDO::FETCH_ASSOC)) { + $hidden = $item['hidden']; + if ($hidden !== null) { + // show-ifs have been processed for this page + $processShowIfs = false; + } + /* @var $oItem Item */ + $oItem = $itemFactory->make($item); + $oItem->hidden = null; + $visibility = $hidden === null ? true : (bool) !$hidden; + $v = $oItem->type !== 'submit' ? $oItem->setVisibility(array($visibility)) : null; + if ($hidden === null || Session::get('is-survey-post')) { + $oItem->hidden = null; + } else { + $oItem->hidden = (int) $hidden; + } + + $this->markItemAsShown($oItem); + $pItem = array_val($this->postedValues, $oItem->name, $oItem->value_validated); + $oItem->value_validated = $pItem instanceof Item ? $pItem->value_validated : $pItem; + $pageItems[$oItem->name] = $oItem; + + if ($item['answered']) { + $this->answeredItems[$oItem->name] = $oItem->value_validated; + } + + if ($oItem->type === 'submit') { + break; + } + } + + if (!$pageItems) { + return array(); + } + + if ($process === false) { + // Processing is skipped when user has submitted data and we just need to check if what submitted is what was requested + return $pageItems; + } + + $pageItems = $this->processAutomaticItems($pageItems); + // Porcess show-ifs only when necessary i.e when user is not going to a previous page OR page is not being POSTed + if ($processShowIfs || Session::get('is-survey-post') || $this->request->getParam('_rsi_')) { + $pageItems = $this->processDynamicValuesAndShowIfs($pageItems); + } + $pageItems = $this->processDynamicLabelsAndChoices($pageItems); + + // add a submit button if none exists + $lastItem = end($pageItems); + if (($lastItem && $lastItem->type !== 'submit') || ($lastItem && $lastItem->hidden)) { + $pageItems[] = $this->getSubmitButton(); + } + + //Check if there is any rendered item and if not, dummy post these and move to next page + if (!$this->displayedItemExists($pageItems)) { + $this->saveSuryeyItems($pageItems, false); + Session::set('is-survey-post', true); + $pageNo++; + $this->redirectToPage($pageNo); + } + return $pageItems; + } + + protected function getCurrentPage() { + // Check if page exists in Request::globals(); + if ($page = Request::getGlobals('pageNo')) { + return $page; + } + + // If page is not in request then get from DB + $query = ' SELECT itms_display.page FROM survey_items_display AS itms_display WHERE itms_display.session_id = :unit_session_id AND itms_display.answered IS NULL ORDER BY itms_display.display_order ASC LIMIT 1; '; - $stmt = $this->db->prepare($query); - $stmt->bindValue('unit_session_id', $this->unitSession->id, PDO::PARAM_INT); - $stmt->execute(); - $row = $stmt->fetch(PDO::FETCH_ASSOC); - - if ($row && !empty($row['page'])) { - return $row['page']; - } - - // If all the above fail then we are on first page - return 1; - } - - protected function getMaxPage() { - if ($this->maxPage === null) { - $this->maxPage = $this->db->findValue('survey_items_display', array('session_id' => $this->unitSession->id), 'MAX(page) as maxPage'); - } - return $this->maxPage; - } - - protected function getSurveyProgress($currentPage) { - /* @TODO Fix progress counts */ - $maxPage = $this->getMaxPage(); - if (!$maxPage) { - return; - } - $progress = $currentPage / $maxPage; - $data = array( - 'progress' => $progress, - 'prevProgress' => ($currentPage - 1) / $maxPage, - 'pageProgress' => 1 / $maxPage, - 'page' => $currentPage, - 'maxPage' => $maxPage, - 'pageItems' => count($this->survey->rendered_items), - 'answeredItems' => count($this->answeredItems), - ); - return $data; - } - - /** - * Get current unit session accessing the Survey - * - * @param int $unitSessionId - * @return UnitSession - */ - protected function getUnitSession($unitSessionId) { - if (!$this->unitSession) { - $this->unitSession = new UnitSession($this->db, null, null, $unitSessionId); - } - return $this->unitSession; - } - - /** - * - * @return Submit_Item - */ - protected function getSubmitButton () { - $opts = array( - 'label_parsed' => 'Continue ', - 'classes_input' => array('btn-info default_formr_button'), - ); - $submitButton = new Submit_Item($opts); - $submitButton->input_attributes['value'] = 1; - return $submitButton; - } - - protected function getPageElement($pageNo) { - $progress = $this->getSurveyProgress($pageNo); - $progressAttribs = array(); - foreach ($progress as $attr => $value) { - $progressAttribs[] = sprintf('%s="%s"', 'data-'.$attr, (string)$value); - } - - $tpl = '
+ $stmt = $this->db->prepare($query); + $stmt->bindValue('unit_session_id', $this->unitSession->id, PDO::PARAM_INT); + $stmt->execute(); + $row = $stmt->fetch(PDO::FETCH_ASSOC); + + if ($row && !empty($row['page'])) { + return $row['page']; + } + + // If all the above fail then we are on first page + return 1; + } + + protected function getMaxPage() { + if ($this->maxPage === null) { + $this->maxPage = $this->db->findValue('survey_items_display', array('session_id' => $this->unitSession->id), 'MAX(page) as maxPage'); + } + return $this->maxPage; + } + + protected function getSurveyProgress($currentPage) { + /* @TODO Fix progress counts */ + $maxPage = $this->getMaxPage(); + if (!$maxPage) { + return; + } + $progress = $currentPage / $maxPage; + $data = array( + 'progress' => $progress, + 'prevProgress' => ($currentPage - 1) / $maxPage, + 'pageProgress' => 1 / $maxPage, + 'page' => $currentPage, + 'maxPage' => $maxPage, + 'pageItems' => count($this->survey->rendered_items), + 'answeredItems' => count($this->answeredItems), + ); + return $data; + } + + /** + * Get current unit session accessing the Survey + * + * @param int $unitSessionId + * @return UnitSession + */ + protected function getUnitSession($unitSessionId) { + if (!$this->unitSession) { + $this->unitSession = new UnitSession($this->db, null, null, $unitSessionId); + } + return $this->unitSession; + } + + /** + * + * @return Submit_Item + */ + protected function getSubmitButton() { + $opts = array( + 'label_parsed' => 'Continue ', + 'classes_input' => array('btn-info default_formr_button'), + ); + $submitButton = new Submit_Item($opts); + $submitButton->input_attributes['value'] = 1; + return $submitButton; + } + + protected function getPageElement($pageNo) { + $progress = $this->getSurveyProgress($pageNo); + $progressAttribs = array(); + foreach ($progress as $attr => $value) { + $progressAttribs[] = sprintf('%s="%s"', 'data-' . $attr, (string) $value); + } + + $tpl = '
Page %{page}/%{max_page}
@@ -337,286 +333,286 @@ protected function getPageElement($pageNo) {
'; - $buttons = ''; - for ($i = 1; $i < $pageNo; $i++) { - $buttons .= Template::replace('%{page_no}', array( - 'run_url' => $this->getPageUrl($i), - 'page_no' => $i - )); - } - - return Template::replace($tpl, array( - 'page' => $pageNo, - 'max_page' => $this->getMaxPage(), - 'name' => self::FMR_PAGE_ELEMENT, - 'value' => $pageNo, - 'buttons' => $buttons ? ' Back to Page ' . $buttons : null, - 'progress_attributes' => implode(' ', $progressAttribs), - )); - } - - protected function emptyPreviousPageExists($pageNo) { - $prev = $pageNo - 1; - if ($prev < 1) { - return false; - } - - $query = array( - 'survey_items_display.page <=' => $prev, - 'survey_items_display.session_id' => $this->unitSession->id - ); - - $select = $this->db->select('survey_items_display.item_id, survey_items_display.page'); - $select->from('survey_items_display'); - $select->where($query); - $select->where('survey_items_display.answered IS NULL'); - $select->order('survey_items_display.page', 'ASC'); - $select->limit(1); - $row = $select->statement()->fetch(PDO::FETCH_ASSOC); - if ($row) { - return $row['page']; - } - return false; - } - - /** - * All items that don't require connecting to openCPU and don't require user input are posted immediately. - * Examples: get parameters, browser, ip. - * - * @param Item[] $items - * @return array Returns items that may have to be sent to openCPU or be rendered for user input - */ - protected function processAutomaticItems($items) { - $hiddenItems = array(); - foreach ($items as $name => $item) { - if (!$item->needsDynamicValue() && !$item->requiresUserInput()) { - $hiddenItems[$name] = $item->getComputedValue(); - //unset($items[$name]); - continue; - } - } - - // save these values - if ($hiddenItems) { - $this->saveSuryeyItems($hiddenItems, false); - } - - // return possibly shortened item array - return $items; - } - - /** - * Process show-ifs and dynamic values for a given set of items in survey - * @note: All dynamic values are processed (even for those we don't know if they will be shown) - * - * @param Item[] $items - * @return array - */ - protected function processDynamicValuesAndShowIfs(&$items) { - // In this loop we gather all show-ifs and dynamic-values that need processing and all values. - $code = array(); - $save = array(); - - /* @var $item Item */ - foreach ($items as $name => &$item) { - // 1. Check item's show-if - $showif = $item->getShowIf(); - if ($showif) { - $siname = "si.{$name}"; - $showif = str_replace("\n", "\n\t", $showif); - $code[$siname] = "{$siname} = (function(){ \n {$showif} \n})()"; - } - - // 2. Check item's value - if ($item->needsDynamicValue()) { - $val = str_replace("\n", "\n\t", $item->getValue($this->survey)); - $code[$name] = "{$name} = (function(){ \n {$val} \n })()"; - if ($showif) { - $code[$name] = "if({$siname}) { \n {$code[$name]} \n }"; - } - // If item is to be shown (rendered), return evaluated dynamic value, else keep dynamic value as string - } - } - - if (!$code) { - return $items; - } - - $ocpu_session = opencpu_multiparse_showif($this->survey, $code, true); - if (!$ocpu_session || $ocpu_session->hasError()) { - notify_user_error(opencpu_debug($ocpu_session), "There was a problem evaluating showifs using openCPU."); - foreach ($items as $name => &$item) { - $item->alwaysInvalid(); - } - } else { - print_hidden_opencpu_debug_message($ocpu_session, "OpenCPU debugger for dynamic values and showifs."); - $results = $ocpu_session->getJSONObject(); - $updateVisibility = $this->db->prepare("UPDATE `survey_items_display` SET hidden = :hidden WHERE item_id = :item_id AND session_id = :session_id"); - $updateVisibility->bindValue(":session_id", $this->unitSession->id); - - $definitelyShownItems = 0; - foreach ($items as $item_name => &$item) { - // set show-if visibility for items - $siname = "si.{$item->name}"; - $isVisible = $item->setVisibility(array_val($results, $siname)); - - // three possible states: 1 = hidden, 0 = shown, null = depends on JS on the page, render anyway - if ($isVisible === null) { - // we only render it, if there are some items before it on which its display could depend - // otherwise it's hidden for good - $hidden = $definitelyShownItems > 0 ? null : 1; - } else { - $hidden = (int) !$isVisible; - } - $item->hidden = $hidden; - $updateVisibility->bindValue(':item_id', $item->id); - $updateVisibility->bindValue(':hidden', $hidden); - $updateVisibility->execute(); - - if ($hidden === 1) { // gone for good - //unset($items[$item_name]); // we remove items that are definitely hidden from consideration - unset($item->parent_attributes['data-show']); - $item->hidden = null; - $item->hide(); - continue; // don't increment counter - } else { - // set dynamic values for items - $val = array_val($results, $item->name, null); - $item->setDynamicValue($val); - // save dynamic value - // if a. we have a value b. this item does not require user input (e.g. calculate) - if (array_key_exists($item->name, $results) && !$item->requiresUserInput()) { - $save[$item->name] = $item->getComputedValue(); - //unset($items[$item_name]); // we remove items that are immediately written from consideration - continue; // don't increment counter - } - $this->markItemAsShown($item); - } - $definitelyShownItems++; // track whether there are any items certain to be shown - } - $this->saveSuryeyItems($save, false); - } - - return $items; - } - - protected function processDynamicLabelsAndChoices(&$items) { - // Gather choice lists - $lists_to_fetch = $strings_to_parse = array(); - $session_labels = array(); - foreach ($items as $name => &$item) { - if ($item->choice_list) { - $lists_to_fetch[] = $item->choice_list; - } - - if ($item->needsDynamicLabel($this->survey)) { - $items[$name]->label_parsed = opencpu_string_key(count($strings_to_parse)); - $strings_to_parse[] = $item->label; - } - } - - // gather and format choice_lists and save all choice labels that need parsing - $choices = $this->survey->getChoices($lists_to_fetch, null); - $choice_lists = array(); - foreach ($choices as $i => $choice) { - if ($choice['label_parsed'] === null) { - $choices[$i]['label_parsed'] = opencpu_string_key(count($strings_to_parse)); - $strings_to_parse[] = $choice['label']; - } - - if (!isset($choice_lists[$choice['list_name']])) { - $choice_lists[$choice['list_name']] = array(); - } - $choice_lists[$choice['list_name']][$choice['name']] = $choices[$i]['label_parsed']; - } - - // Now that we have the items and the choices, If there was anything left to parse, we do so here! - if ($strings_to_parse) { - $parsed_strings = opencpu_multistring_parse($this->survey, $strings_to_parse); - // Replace parsed strings in $choice_list array - opencpu_substitute_parsed_strings($choice_lists, $parsed_strings); - // Replace parsed strings in unanswered items array - opencpu_substitute_parsed_strings($items, $parsed_strings); - } - - // Merge parsed choice lists into items - foreach ($items as $name => &$item) { - $choice_list = $item->choice_list; - if (isset($choice_lists[$choice_list])) { - $list = $choice_lists[$choice_list]; - $list = array_filter($list, 'is_formr_truthy'); - $items[$name]->setChoices($list); - } - $session_labels[$name] = $item->label_parsed; - //$items[$name]->refresh($item, array('label_parsed')); - } - - Session::set('labels', $session_labels); - return $items; - } - - /** - * Mark as item as "to be shown" - * - * @param Item $item - * @return Item - */ - protected function markItemAsShown(&$item) { - if ($item->hidden === 0) { - $item->parent_attributes['data-show'] = true; - } - $item->data_showif = $item->js_showif ? true : false; - return $item; - } - - /** - * Save Survey Items - * - * @param Item[] $items - * @param boolean $validate - */ - protected function saveSuryeyItems($items, $validate = true) { - if (!$items) { - return false; - } - - if (!$validate) { - foreach ($items as &$item) { - if ($item instanceof Item) { - $item->skip_validation = true; - } - } - } - return $this->survey->post($items, $validate); - } - - /** - * Checks if a displayed (rendered and visible) item exists in an array of items - * - * @param Item[] $items - * @return boolean - */ - protected function displayedItemExists(&$items) { - foreach ($items as $item) { - if ($item->isRendered() && !$item->hidden && $item->type !== 'submit') { - return true; - } - } - return false; - } - - private function redirectToPage($page) { - $redirect = $this->getPageUrl($page); - redirect_to($redirect); - } - - private function getPageUrl($page) { - if ($page < 0) { - $page = 1; - } - $params = array_diff_key($_REQUEST, $_POST); - unset($params['route'], $params['run_name'], $params['code'], $params['_rsi_']); - return run_url($this->run->name, $page, $params); - } + $buttons = ''; + for ($i = 1; $i < $pageNo; $i++) { + $buttons .= Template::replace('%{page_no}', array( + 'run_url' => $this->getPageUrl($i), + 'page_no' => $i + )); + } + + return Template::replace($tpl, array( + 'page' => $pageNo, + 'max_page' => $this->getMaxPage(), + 'name' => self::FMR_PAGE_ELEMENT, + 'value' => $pageNo, + 'buttons' => $buttons ? ' Back to Page ' . $buttons : null, + 'progress_attributes' => implode(' ', $progressAttribs), + )); + } + + protected function emptyPreviousPageExists($pageNo) { + $prev = $pageNo - 1; + if ($prev < 1) { + return false; + } + + $query = array( + 'survey_items_display.page <=' => $prev, + 'survey_items_display.session_id' => $this->unitSession->id + ); + + $select = $this->db->select('survey_items_display.item_id, survey_items_display.page'); + $select->from('survey_items_display'); + $select->where($query); + $select->where('survey_items_display.answered IS NULL'); + $select->order('survey_items_display.page', 'ASC'); + $select->limit(1); + $row = $select->statement()->fetch(PDO::FETCH_ASSOC); + if ($row) { + return $row['page']; + } + return false; + } + + /** + * All items that don't require connecting to openCPU and don't require user input are posted immediately. + * Examples: get parameters, browser, ip. + * + * @param Item[] $items + * @return array Returns items that may have to be sent to openCPU or be rendered for user input + */ + protected function processAutomaticItems($items) { + $hiddenItems = array(); + foreach ($items as $name => $item) { + if (!$item->needsDynamicValue() && !$item->requiresUserInput()) { + $hiddenItems[$name] = $item->getComputedValue(); + //unset($items[$name]); + continue; + } + } + + // save these values + if ($hiddenItems) { + $this->saveSuryeyItems($hiddenItems, false); + } + + // return possibly shortened item array + return $items; + } + + /** + * Process show-ifs and dynamic values for a given set of items in survey + * @note: All dynamic values are processed (even for those we don't know if they will be shown) + * + * @param Item[] $items + * @return array + */ + protected function processDynamicValuesAndShowIfs(&$items) { + // In this loop we gather all show-ifs and dynamic-values that need processing and all values. + $code = array(); + $save = array(); + + /* @var $item Item */ + foreach ($items as $name => &$item) { + // 1. Check item's show-if + $showif = $item->getShowIf(); + if ($showif) { + $siname = "si.{$name}"; + $showif = str_replace("\n", "\n\t", $showif); + $code[$siname] = "{$siname} = (function(){ \n {$showif} \n})()"; + } + + // 2. Check item's value + if ($item->needsDynamicValue()) { + $val = str_replace("\n", "\n\t", $item->getValue($this->survey)); + $code[$name] = "{$name} = (function(){ \n {$val} \n })()"; + if ($showif) { + $code[$name] = "if({$siname}) { \n {$code[$name]} \n }"; + } + // If item is to be shown (rendered), return evaluated dynamic value, else keep dynamic value as string + } + } + + if (!$code) { + return $items; + } + + $ocpu_session = opencpu_multiparse_showif($this->survey, $code, true); + if (!$ocpu_session || $ocpu_session->hasError()) { + notify_user_error(opencpu_debug($ocpu_session), "There was a problem evaluating showifs using openCPU."); + foreach ($items as $name => &$item) { + $item->alwaysInvalid(); + } + } else { + print_hidden_opencpu_debug_message($ocpu_session, "OpenCPU debugger for dynamic values and showifs."); + $results = $ocpu_session->getJSONObject(); + $updateVisibility = $this->db->prepare("UPDATE `survey_items_display` SET hidden = :hidden WHERE item_id = :item_id AND session_id = :session_id"); + $updateVisibility->bindValue(":session_id", $this->unitSession->id); + + $definitelyShownItems = 0; + foreach ($items as $item_name => &$item) { + // set show-if visibility for items + $siname = "si.{$item->name}"; + $isVisible = $item->setVisibility(array_val($results, $siname)); + + // three possible states: 1 = hidden, 0 = shown, null = depends on JS on the page, render anyway + if ($isVisible === null) { + // we only render it, if there are some items before it on which its display could depend + // otherwise it's hidden for good + $hidden = $definitelyShownItems > 0 ? null : 1; + } else { + $hidden = (int) !$isVisible; + } + $item->hidden = $hidden; + $updateVisibility->bindValue(':item_id', $item->id); + $updateVisibility->bindValue(':hidden', $hidden); + $updateVisibility->execute(); + + if ($hidden === 1) { // gone for good + //unset($items[$item_name]); // we remove items that are definitely hidden from consideration + unset($item->parent_attributes['data-show']); + $item->hidden = null; + $item->hide(); + continue; // don't increment counter + } else { + // set dynamic values for items + $val = array_val($results, $item->name, null); + $item->setDynamicValue($val); + // save dynamic value + // if a. we have a value b. this item does not require user input (e.g. calculate) + if (array_key_exists($item->name, $results) && !$item->requiresUserInput()) { + $save[$item->name] = $item->getComputedValue(); + //unset($items[$item_name]); // we remove items that are immediately written from consideration + continue; // don't increment counter + } + $this->markItemAsShown($item); + } + $definitelyShownItems++; // track whether there are any items certain to be shown + } + $this->saveSuryeyItems($save, false); + } + + return $items; + } + + protected function processDynamicLabelsAndChoices(&$items) { + // Gather choice lists + $lists_to_fetch = $strings_to_parse = array(); + $session_labels = array(); + foreach ($items as $name => &$item) { + if ($item->choice_list) { + $lists_to_fetch[] = $item->choice_list; + } + + if ($item->needsDynamicLabel($this->survey)) { + $items[$name]->label_parsed = opencpu_string_key(count($strings_to_parse)); + $strings_to_parse[] = $item->label; + } + } + + // gather and format choice_lists and save all choice labels that need parsing + $choices = $this->survey->getChoices($lists_to_fetch, null); + $choice_lists = array(); + foreach ($choices as $i => $choice) { + if ($choice['label_parsed'] === null) { + $choices[$i]['label_parsed'] = opencpu_string_key(count($strings_to_parse)); + $strings_to_parse[] = $choice['label']; + } + + if (!isset($choice_lists[$choice['list_name']])) { + $choice_lists[$choice['list_name']] = array(); + } + $choice_lists[$choice['list_name']][$choice['name']] = $choices[$i]['label_parsed']; + } + + // Now that we have the items and the choices, If there was anything left to parse, we do so here! + if ($strings_to_parse) { + $parsed_strings = opencpu_multistring_parse($this->survey, $strings_to_parse); + // Replace parsed strings in $choice_list array + opencpu_substitute_parsed_strings($choice_lists, $parsed_strings); + // Replace parsed strings in unanswered items array + opencpu_substitute_parsed_strings($items, $parsed_strings); + } + + // Merge parsed choice lists into items + foreach ($items as $name => &$item) { + $choice_list = $item->choice_list; + if (isset($choice_lists[$choice_list])) { + $list = $choice_lists[$choice_list]; + $list = array_filter($list, 'is_formr_truthy'); + $items[$name]->setChoices($list); + } + $session_labels[$name] = $item->label_parsed; + //$items[$name]->refresh($item, array('label_parsed')); + } + + Session::set('labels', $session_labels); + return $items; + } + + /** + * Mark as item as "to be shown" + * + * @param Item $item + * @return Item + */ + protected function markItemAsShown(&$item) { + if ($item->hidden === 0) { + $item->parent_attributes['data-show'] = true; + } + $item->data_showif = $item->js_showif ? true : false; + return $item; + } + + /** + * Save Survey Items + * + * @param Item[] $items + * @param boolean $validate + */ + protected function saveSuryeyItems($items, $validate = true) { + if (!$items) { + return false; + } + + if (!$validate) { + foreach ($items as &$item) { + if ($item instanceof Item) { + $item->skip_validation = true; + } + } + } + return $this->survey->post($items, $validate); + } + + /** + * Checks if a displayed (rendered and visible) item exists in an array of items + * + * @param Item[] $items + * @return boolean + */ + protected function displayedItemExists(&$items) { + foreach ($items as $item) { + if ($item->isRendered() && !$item->hidden && $item->type !== 'submit') { + return true; + } + } + return false; + } + + private function redirectToPage($page) { + $redirect = $this->getPageUrl($page); + redirect_to($redirect); + } + + private function getPageUrl($page) { + if ($page < 0) { + $page = 1; + } + $params = array_diff_key($_REQUEST, $_POST); + unset($params['route'], $params['run_name'], $params['code'], $params['_rsi_']); + return run_url($this->run->name, $page, $params); + } } diff --git a/application/Library/AnimalName.php b/application/Library/AnimalName.php index 637260b98..a0bc69838 100644 --- a/application/Library/AnimalName.php +++ b/application/Library/AnimalName.php @@ -1,6 +1,8 @@ findFile($class)) { - if (class_exists($class, false)) { - return true; - } + /** + * unregister class instance as an autoloader + */ + public function unregister() { + spl_autoload_unregister(array($this, 'loadClass')); + } - include $file; - if (!class_exists($class)) { - $message = sprintf("Autloader expected class %s to be defined in file %s. The file was found but the class was not in it", $class, $file); - throw new RuntimeException($message); - } - return true; - } - return false; - } + /** + * Loads the given class + * + * @param string $class The name of the class + * @return bool Returns TRUE if class is loaded + * @throws RuntimeException + */ + public function loadClass($class) { + if ($file = $this->findFile($class)) { + if (class_exists($class, false)) { + return true; + } - /** - * Finds the path associated to the class name - * - * @param string $class - * @return string|null - */ - private function findFile($class) { - if (!defined('APPLICATION_ROOT')) { - define('APPLICATION_ROOT', realpath(dirname(__FILE__) . '../../') . '/'); - } + include $file; + if (!class_exists($class)) { + $message = sprintf("Autloader expected class %s to be defined in file %s. The file was found but the class was not in it", $class, $file); + throw new RuntimeException($message); + } + return true; + } + return false; + } - if (!defined('APPLICATION_PATH')) { - define('APPLICATION_PATH', APPLICATION_ROOT . 'application/'); - } + /** + * Finds the path associated to the class name + * + * @param string $class + * @return string|null + */ + private function findFile($class) { + if (!defined('APPLICATION_ROOT')) { + define('APPLICATION_ROOT', realpath(dirname(__FILE__) . '../../') . '/'); + } - $class = $this->classNameToPath($class); - $libraryPath = APPLICATION_PATH . "Library/{$class}.php"; - $modelPath = APPLICATION_PATH . "Model/{$class}.php"; + if (!defined('APPLICATION_PATH')) { + define('APPLICATION_PATH', APPLICATION_ROOT . 'application/'); + } - if (strstr($class, 'Controller') !== false) { - $file = APPLICATION_PATH . "Controller/{$class}.php"; - } elseif (strstr($class, 'Helper') !== false) { - $file = APPLICATION_PATH . "Helper/{$class}.php"; - } elseif (file_exists($modelPath)) { - $file = $modelPath; - } elseif (file_exists($libraryPath)) { - $file = $libraryPath; - } + $class = $this->classNameToPath($class); + $libraryPath = APPLICATION_PATH . "Library/{$class}.php"; + $modelPath = APPLICATION_PATH . "Model/{$class}.php"; - if (!empty($file)) { - return $file; - } - return false; - } + if (strstr($class, 'Controller') !== false) { + $file = APPLICATION_PATH . "Controller/{$class}.php"; + } elseif (strstr($class, 'Helper') !== false) { + $file = APPLICATION_PATH . "Helper/{$class}.php"; + } elseif (file_exists($modelPath)) { + $file = $modelPath; + } elseif (file_exists($libraryPath)) { + $file = $libraryPath; + } - protected function classNameToPath($class) { - if (strstr($class, '_') !== false) { - $pieces = array_reverse(explode('_', $class)); - $class = implode('/', $pieces); - } - if (substr($class, -7) === 'Factory') { - $class = str_replace('Factory', '', $class); - } - return $class; - } + if (!empty($file)) { + return $file; + } + return false; + } - public static function getLoader() { - if (self::$loader === null) { - /* @var $loader Autoload */ - $loader = new self(); - $loader->register(); - self::$loader = $loader; - } - return self::$loader; - } + protected function classNameToPath($class) { + if (strstr($class, '_') !== false) { + $pieces = array_reverse(explode('_', $class)); + $class = implode('/', $pieces); + } + if (substr($class, -7) === 'Factory') { + $class = str_replace('Factory', '', $class); + } + return $class; + } + + public static function getLoader() { + if (self::$loader === null) { + /* @var $loader Autoload */ + $loader = new self(); + $loader->register(); + self::$loader = $loader; + } + return self::$loader; + } } diff --git a/application/Library/CURL.php b/application/Library/CURL.php index 709b7113d..aba5da50d 100644 --- a/application/Library/CURL.php +++ b/application/Library/CURL.php @@ -2,388 +2,388 @@ class CURL { - /** - * constant for HttpRequest UserAgent string - */ - const USERAGENT = 'formr-curl/1.0'; - - /** - * Encoding used by translit() call - */ - const ENCODING = 'utf-8'; - - /** - * Array key used to retrieve response headers with HttpRequest from $info - */ - const RESPONSE_HEADERS = "\theaders[]"; - - /** - * Array key name for previous headers in response - */ - const RESPONSE_PREVIOUS_HEADERS = "\tprevious-headers[]"; - const HTTP_METHOD_GET = 'GET'; - const HTTP_METHOD_POST = 'POST'; - const HTTP_METHOD_HEAD = 'HEAD'; - const HTTP_METHOD_PUT = 'PUT'; - const HTTP_METHOD_DELETE = 'DELETE'; - - /** - * Download Filters - */ - const DOWNLOAD_FILTERS = 'DOWNLOAD_FILTERS'; - const DOWNLOAD_FILTER_MAXSIZE = 'maxsize'; - const DOWNLOAD_FILTER_CONTENT_TYPE = 'content-type'; - const DOWNLOAD_FILTER_KEEP_LAST_MODIFIED = 'keep-last-modified'; - - /** - * Default cURL options. - * - * @var array - */ - private static $curlOptions = array( - // The number of seconds to wait while trying to connect. Use 0 to wait indefinitely. - CURLOPT_CONNECTTIMEOUT => 20, - // The maximum number of seconds to allow cURL functions to execute. - CURLOPT_TIMEOUT => 120, - // TRUE to return the transfer as a string of the return value of - // curl_exec() instead of outputting it out directly. - CURLOPT_RETURNTRANSFER => 1, - // TRUE to follow any "Location: " header that the server sends as part - // of the HTTP header (note this is recursive, PHP will follow as many - // "Location: " headers that it is sent, unless CURLOPT_MAXREDIRS is - // set). - CURLOPT_FOLLOWLOCATION => true, - // FALSE to stop cURL from verifying the peer's certificate. - // Alternate certificates to verify against can be specified with the - // CURLOPT_CAINFO option or a certificate directory can be specified - // with the CURLOPT_CAPATH option. CURLOPT_SSL_VERIFYHOST may also need - // to be TRUE or FALSE if CURLOPT_SSL_VERIFYPEER is disabled (it defaults to 2) - CURLOPT_SSL_VERIFYPEER => true, - // 1 to check the existence of a common name in the SSL peer certificate. - // 2 to check the existence of a common name and also verify that it matches the hostname provided. - CURLOPT_SSL_VERIFYHOST => 2, - // The contents of the "User-Agent: " header to be used in a HTTP request. - CURLOPT_USERAGENT => self::USERAGENT, - ); - - /** - * Create cURL resource initialized with common options - * Requires curl php extension to be present - * - * Parameters in POST method can be passed in two modes: - * - application/x-www-form-urlencoded - * - multipart/form-data - * Default is 'multipart/form-data' - * - * While PHP can parse both modes just fine, certain servers accept only one. One such example is Twitter API. - * To enforce encoding with 'application/x-www-form-urlencoded', pass parameters as string, like 'para1=val1¶2=val2&...' - * See {@link http://php.net/manual/en/function.curl-setopt.php curl_setopt} for CURLOPT_POSTFIELDS. - * - * @static - * @throws Exception - * @param string $url url to request - * @param array $params request parameters (GET or POST) - * @param string $method http method (GET/POST) - * @param array $options curl extra options - * @param array $info = null curl_getinfo() results stored here - * @return string content - */ - public static function HttpRequest($url, $params = array(), $method = self::HTTP_METHOD_GET, $options = array(), &$info = null) { - static $have_curl; - if ($have_curl === null) { - $have_curl = extension_loaded('curl'); - } - if ($have_curl === false) { - throw new Exception("cURL extension not loaded."); - } - - $curl = curl_init(); - if ($curl === false) { - throw new Exception("Unable to initialize cURL."); - } - - if (!$options) { - $options = array(); - } - - $curlConfigOptions = Config::get('curl', array()); - $curlConfigOptions += self::$curlOptions; - $options += $curlConfigOptions; - - if ($method == self::HTTP_METHOD_POST) { - $options[CURLOPT_POST] = true; - $options[CURLOPT_POSTFIELDS] = $params; - } elseif ($method === self::HTTP_METHOD_PUT) { - $options[CURLOPT_RETURNTRANSFER] = true; - $options[CURLOPT_CUSTOMREQUEST] = self::HTTP_METHOD_PUT; - } elseif ($method == self::HTTP_METHOD_GET || $method == self::HTTP_METHOD_HEAD) { - if ($method == self::HTTP_METHOD_GET) { - $options[CURLOPT_HTTPGET] = true; - } else if ($method == self::HTTP_METHOD_HEAD) { - // make HEAD request - $options[CURLOPT_NOBODY] = true; - // enable header to capture response headers - $options[CURLOPT_HEADER] = true; - } - - if ($params) { - if (!is_array($params)) { - // undefined how you pass key=value pairs with params being 'string'. - throw new LogicException("Can't do GET and params of type '" . gettype($params) . "', use POST instead"); - } - $url = self::urljoin($url, $params); - } - } - $options[CURLOPT_URL] = $url; - curl_setopt_array($curl, $options); + /** + * constant for HttpRequest UserAgent string + */ + const USERAGENT = 'formr-curl/1.0'; + + /** + * Encoding used by translit() call + */ + const ENCODING = 'utf-8'; + + /** + * Array key used to retrieve response headers with HttpRequest from $info + */ + const RESPONSE_HEADERS = "\theaders[]"; + + /** + * Array key name for previous headers in response + */ + const RESPONSE_PREVIOUS_HEADERS = "\tprevious-headers[]"; + const HTTP_METHOD_GET = 'GET'; + const HTTP_METHOD_POST = 'POST'; + const HTTP_METHOD_HEAD = 'HEAD'; + const HTTP_METHOD_PUT = 'PUT'; + const HTTP_METHOD_DELETE = 'DELETE'; + + /** + * Download Filters + */ + const DOWNLOAD_FILTERS = 'DOWNLOAD_FILTERS'; + const DOWNLOAD_FILTER_MAXSIZE = 'maxsize'; + const DOWNLOAD_FILTER_CONTENT_TYPE = 'content-type'; + const DOWNLOAD_FILTER_KEEP_LAST_MODIFIED = 'keep-last-modified'; + + /** + * Default cURL options. + * + * @var array + */ + private static $curlOptions = array( + // The number of seconds to wait while trying to connect. Use 0 to wait indefinitely. + CURLOPT_CONNECTTIMEOUT => 20, + // The maximum number of seconds to allow cURL functions to execute. + CURLOPT_TIMEOUT => 120, + // TRUE to return the transfer as a string of the return value of + // curl_exec() instead of outputting it out directly. + CURLOPT_RETURNTRANSFER => 1, + // TRUE to follow any "Location: " header that the server sends as part + // of the HTTP header (note this is recursive, PHP will follow as many + // "Location: " headers that it is sent, unless CURLOPT_MAXREDIRS is + // set). + CURLOPT_FOLLOWLOCATION => true, + // FALSE to stop cURL from verifying the peer's certificate. + // Alternate certificates to verify against can be specified with the + // CURLOPT_CAINFO option or a certificate directory can be specified + // with the CURLOPT_CAPATH option. CURLOPT_SSL_VERIFYHOST may also need + // to be TRUE or FALSE if CURLOPT_SSL_VERIFYPEER is disabled (it defaults to 2) + CURLOPT_SSL_VERIFYPEER => true, + // 1 to check the existence of a common name in the SSL peer certificate. + // 2 to check the existence of a common name and also verify that it matches the hostname provided. + CURLOPT_SSL_VERIFYHOST => 2, + // The contents of the "User-Agent: " header to be used in a HTTP request. + CURLOPT_USERAGENT => self::USERAGENT, + ); + + /** + * Create cURL resource initialized with common options + * Requires curl php extension to be present + * + * Parameters in POST method can be passed in two modes: + * - application/x-www-form-urlencoded + * - multipart/form-data + * Default is 'multipart/form-data' + * + * While PHP can parse both modes just fine, certain servers accept only one. One such example is Twitter API. + * To enforce encoding with 'application/x-www-form-urlencoded', pass parameters as string, like 'para1=val1¶2=val2&...' + * See {@link http://php.net/manual/en/function.curl-setopt.php curl_setopt} for CURLOPT_POSTFIELDS. + * + * @static + * @throws Exception + * @param string $url url to request + * @param array $params request parameters (GET or POST) + * @param string $method http method (GET/POST) + * @param array $options curl extra options + * @param array $info = null curl_getinfo() results stored here + * @return string content + */ + public static function HttpRequest($url, $params = array(), $method = self::HTTP_METHOD_GET, $options = array(), &$info = null) { + static $have_curl; + if ($have_curl === null) { + $have_curl = extension_loaded('curl'); + } + if ($have_curl === false) { + throw new Exception("cURL extension not loaded."); + } + + $curl = curl_init(); + if ($curl === false) { + throw new Exception("Unable to initialize cURL."); + } + + if (!$options) { + $options = array(); + } + + $curlConfigOptions = Config::get('curl', array()); + $curlConfigOptions += self::$curlOptions; + $options += $curlConfigOptions; + + if ($method == self::HTTP_METHOD_POST) { + $options[CURLOPT_POST] = true; + $options[CURLOPT_POSTFIELDS] = $params; + } elseif ($method === self::HTTP_METHOD_PUT) { + $options[CURLOPT_RETURNTRANSFER] = true; + $options[CURLOPT_CUSTOMREQUEST] = self::HTTP_METHOD_PUT; + } elseif ($method == self::HTTP_METHOD_GET || $method == self::HTTP_METHOD_HEAD) { + if ($method == self::HTTP_METHOD_GET) { + $options[CURLOPT_HTTPGET] = true; + } else if ($method == self::HTTP_METHOD_HEAD) { + // make HEAD request + $options[CURLOPT_NOBODY] = true; + // enable header to capture response headers + $options[CURLOPT_HEADER] = true; + } + + if ($params) { + if (!is_array($params)) { + // undefined how you pass key=value pairs with params being 'string'. + throw new LogicException("Can't do GET and params of type '" . gettype($params) . "', use POST instead"); + } + $url = self::urljoin($url, $params); + } + } + $options[CURLOPT_URL] = $url; + curl_setopt_array($curl, $options); // curl_setopt($curl, CURLOPT_HTTPHEADER,array("Expect:")); - $res = curl_exec($curl); - $info = curl_getinfo($curl); - - if ($res === false) { - $error = curl_error($curl); - curl_close($curl); - throw new Exception("cURL error: {$error} in {$info['total_time']}"); - } - curl_close($curl); - - // convert to array in case of headers wanted - $return = $res; - if (!empty($options[CURLOPT_HEADER])) { - $info[self::RESPONSE_HEADERS] = self::chopHttpHeaders($res, $info); - $return = substr($return, $info['header_size']); - } - - return $return; - } - - /** - * Retrieve URL using cURL. Decodes resonse as JSON. - * - * @param string $url url to download - * @param array $params request parameters (GET or POST) - * @param string $method http method (GET/POST) - * @param array $options curl extra options - * @param array &$info curl_getinfo() results stored here - * @param bool $assoc [optional] When true, returned objects will be converted into associative arrays. - * @throws Exception when url can't be retrieved or json does not parse - * @return object - */ - public static function JsonRequest($url, $params = array(), $method = self::HTTP_METHOD_GET, $options = array(), &$info = null, $assoc = false) { - $res = self::HttpRequest($url, $params, $method, $options, $info); - - $json = json_decode($res, $assoc); - // we'll assume nobody wants to return NULL - if ($json === null) { - if (function_exists('json_last_error_msg')) { - // PHP 5.5 - $error = json_last_error_msg(); - } elseif (function_exists('json_last_error')) { - // PHP 5.3 - $error = 'error code ' . json_last_error(); - } else { - $error = 'no more info available'; - } - throw new Exception('Unable decode json response from [' . $url . '][http code ' . $info['http_code'] . ']: ' . $error); - } - - return $json; - } - - /** - * Append parameters to URL. URL may already contain parameters. - * - * $quote_style = ENT_QUOTES for '&' to became '&' - * $quote_style = ENT_NOQUOTES for '&' to became '&' - * - * @static - * @param $url - * @param array $params - * @param int $quote_style = ENT_NOQUOTES - * @return string - */ - public static function urljoin($url, $params = array(), $quote_style = ENT_NOQUOTES) { - if ($params) { - $amp = $quote_style == ENT_QUOTES ? '&' : '&'; - $args = http_build_query($params, null, $amp); - if ($args) { - $q = strstr($url, '?') ? $amp : '?'; - $url .= $q . $args; - } - } - - return $url; - } - - /** - * - * @param type $filename - * @param type $postname - * @return mixed Returns a string if CURLFile class is not present else returns an instance of curl file - * - * @todo Detect mime type automatically - */ - public static function getPostFileParam($filename, $postname = 'filename') { - if (class_exists('CURLFile', false)) { - return new CURLFile($filename, null, $postname); - } - return "@$filename"; - } - - /** - * Download file from URL using cURL. The file is streamed so it does not occupy lots of memory for large downloads. - * File is downloaded to temporary file and renamed on success. on failure temporary file is cleaned up. - * - * @param string $url url to download - * @param string $output_file path there to save the result - * @param Array $ params request parameters (GET or POST) - * @param string $method http method (GET/POST) - * @param array $options curl extra options - * @param array &$info curl_getinfo() results stored here - * @throws Exception - */ - public static function DownloadUrl($url, $output_file, $params = array(), $method = self::HTTP_METHOD_GET, $options = array(), &$info = null) { - $last_modified = null; - - // if content filters present check those first - if (isset($options[self::DOWNLOAD_FILTERS])) { - // make copy of filters, do not pass it in real HttpRequest - $filters = $options[self::DOWNLOAD_FILTERS]; - unset($options[self::DOWNLOAD_FILTERS]); - - // pre-check with HEAD request - self::HttpRequest($url, $params, self::HTTP_METHOD_HEAD, $options, $info); - - // extract Last-Modified header to save timestamp later - if (!empty($filters[self::DOWNLOAD_FILTER_KEEP_LAST_MODIFIED])) { - if (isset($info[self::RESPONSE_HEADERS]['Last-Modified'])) { - $last_modified = strtotime($info[self::RESPONSE_HEADERS]['Last-Modified']); - } - } - - if (isset($filters[self::DOWNLOAD_FILTER_CONTENT_TYPE])) { - if (empty($info['content_type'])) { - throw new Exception("Didn't get Content-Type from HEAD request"); - } - if (!in_array($info['content_type'], $filters[self::DOWNLOAD_FILTER_CONTENT_TYPE])) { - throw new Exception("Wrong Content-Type: {$info['content_type']}"); - } - } - - if (isset($filters[self::DOWNLOAD_FILTER_MAXSIZE])) { - if (empty($info['download_content_length'])) { - throw new Exception("Didn't get Content-Length from HEAD request"); - } - // TODO: handle -1 - // http://stackoverflow.com/questions/5518323/curl-getinfo-returning-1-as-content-length - if ($info['download_content_length'] > $filters[self::DOWNLOAD_FILTER_MAXSIZE]) { - throw new Exception("File too large: {$info['download_content_length']} bytes"); - } - } - } - - $tmpfile = tempnam(dirname($output_file), basename($output_file)); - $fp = fopen($tmpfile, 'wb+'); - if ($fp == false) { - throw new Exception("Failed to create: $tmpfile"); - } - - // set defaults for this method - $options += array( - CURLOPT_TIMEOUT => 10 * 60, - ); - - // overrride, must be this or the method will fail - $options[CURLOPT_RETURNTRANSFER] = false; - $options[CURLOPT_FILE] = $fp; - - $res = self::HttpRequest($url, $params, $method, $options, $info); - if (fclose($fp) !== true) { - throw new Exception("Unable to save download result"); - } - - if ($res !== true) { - unlink($tmpfile); - throw new Exception("Download error (in {$info['total_time']})"); - } - - // expect 2xx status code - if ($info['http_code'] < 200 || $info['http_code'] > 300) { - unlink($tmpfile); - throw new Exception($url . ': bad statuscode ' . $info['http_code'], $info['http_code']); - } - - // restore timestamp, if available - if ($last_modified) { - touch($tmpfile, $last_modified); - } - - $rename = rename($tmpfile, $output_file); - if ($rename !== true) { - throw new Exception("Unable to rename temporary file"); - } - } - - /** - * Parse HTTP Headers out of cURL $response, it modifies $response while doing so - * - * Uses header splitter from - * {@link http://www.sitepoint.com/forums/showthread.php?590248-Getting-response-header-in-PHP-cURL-request here} - * - * @param string $response - * @param array $info - * @return array - */ - private static function chopHttpHeaders(&$response, &$info) { - $headerstext = substr($response, 0, $info['header_size']); - $info['raw_header'] = $headerstext; - - // 'download_content_length' is -1 for HEAD request - if ($info['download_content_length'] >= 0) { - $response = substr($response, -$info['download_content_length']); - } else { - $response = ''; - } - $headersets = explode("\r\n\r\n", $headerstext); - - // http_parse_headers() php implementation from here: - // http://php.net/manual/en/function.http-parse-headers.php - $res = null; - foreach ($headersets as $i => $headerset) { - if (empty($headerset)) { - continue; - } - - $headerlist = explode("\r\n", $headerset); - $headers = array(); - // fill status line as 'Status' header, it's identical to what 'Status' actual header would be used in CGI scripts - $headers['Status'] = array_shift($headerlist); - foreach ($headerlist as $line) { - $line = rtrim($line); - if (empty($line)) { - continue; - } - - list($header, $value) = preg_split('/:\s*/', $line, 2); - - // Do Camel-Case to header name - $header = str_replace(" ", "-", ucwords(strtolower(str_replace("-", " ", $header)))); - - // add as array if duplicate - if (isset($headers[$header])) { - $headers[$header] = array($headers[$header], $value); - } else { - $headers[$header] = $value; - } - } - - if (isset($res)) { - $headers[self::RESPONSE_PREVIOUS_HEADERS] = $res; - } - $res = $headers; - } - - return $res; - } + $res = curl_exec($curl); + $info = curl_getinfo($curl); + + if ($res === false) { + $error = curl_error($curl); + curl_close($curl); + throw new Exception("cURL error: {$error} in {$info['total_time']}"); + } + curl_close($curl); + + // convert to array in case of headers wanted + $return = $res; + if (!empty($options[CURLOPT_HEADER])) { + $info[self::RESPONSE_HEADERS] = self::chopHttpHeaders($res, $info); + $return = substr($return, $info['header_size']); + } + + return $return; + } + + /** + * Retrieve URL using cURL. Decodes resonse as JSON. + * + * @param string $url url to download + * @param array $params request parameters (GET or POST) + * @param string $method http method (GET/POST) + * @param array $options curl extra options + * @param array &$info curl_getinfo() results stored here + * @param bool $assoc [optional] When true, returned objects will be converted into associative arrays. + * @throws Exception when url can't be retrieved or json does not parse + * @return object + */ + public static function JsonRequest($url, $params = array(), $method = self::HTTP_METHOD_GET, $options = array(), &$info = null, $assoc = false) { + $res = self::HttpRequest($url, $params, $method, $options, $info); + + $json = json_decode($res, $assoc); + // we'll assume nobody wants to return NULL + if ($json === null) { + if (function_exists('json_last_error_msg')) { + // PHP 5.5 + $error = json_last_error_msg(); + } elseif (function_exists('json_last_error')) { + // PHP 5.3 + $error = 'error code ' . json_last_error(); + } else { + $error = 'no more info available'; + } + throw new Exception('Unable decode json response from [' . $url . '][http code ' . $info['http_code'] . ']: ' . $error); + } + + return $json; + } + + /** + * Append parameters to URL. URL may already contain parameters. + * + * $quote_style = ENT_QUOTES for '&' to became '&' + * $quote_style = ENT_NOQUOTES for '&' to became '&' + * + * @static + * @param $url + * @param array $params + * @param int $quote_style = ENT_NOQUOTES + * @return string + */ + public static function urljoin($url, $params = array(), $quote_style = ENT_NOQUOTES) { + if ($params) { + $amp = $quote_style == ENT_QUOTES ? '&' : '&'; + $args = http_build_query($params, null, $amp); + if ($args) { + $q = strstr($url, '?') ? $amp : '?'; + $url .= $q . $args; + } + } + + return $url; + } + + /** + * + * @param type $filename + * @param type $postname + * @return mixed Returns a string if CURLFile class is not present else returns an instance of curl file + * + * @todo Detect mime type automatically + */ + public static function getPostFileParam($filename, $postname = 'filename') { + if (class_exists('CURLFile', false)) { + return new CURLFile($filename, null, $postname); + } + return "@$filename"; + } + + /** + * Download file from URL using cURL. The file is streamed so it does not occupy lots of memory for large downloads. + * File is downloaded to temporary file and renamed on success. on failure temporary file is cleaned up. + * + * @param string $url url to download + * @param string $output_file path there to save the result + * @param Array $ params request parameters (GET or POST) + * @param string $method http method (GET/POST) + * @param array $options curl extra options + * @param array &$info curl_getinfo() results stored here + * @throws Exception + */ + public static function DownloadUrl($url, $output_file, $params = array(), $method = self::HTTP_METHOD_GET, $options = array(), &$info = null) { + $last_modified = null; + + // if content filters present check those first + if (isset($options[self::DOWNLOAD_FILTERS])) { + // make copy of filters, do not pass it in real HttpRequest + $filters = $options[self::DOWNLOAD_FILTERS]; + unset($options[self::DOWNLOAD_FILTERS]); + + // pre-check with HEAD request + self::HttpRequest($url, $params, self::HTTP_METHOD_HEAD, $options, $info); + + // extract Last-Modified header to save timestamp later + if (!empty($filters[self::DOWNLOAD_FILTER_KEEP_LAST_MODIFIED])) { + if (isset($info[self::RESPONSE_HEADERS]['Last-Modified'])) { + $last_modified = strtotime($info[self::RESPONSE_HEADERS]['Last-Modified']); + } + } + + if (isset($filters[self::DOWNLOAD_FILTER_CONTENT_TYPE])) { + if (empty($info['content_type'])) { + throw new Exception("Didn't get Content-Type from HEAD request"); + } + if (!in_array($info['content_type'], $filters[self::DOWNLOAD_FILTER_CONTENT_TYPE])) { + throw new Exception("Wrong Content-Type: {$info['content_type']}"); + } + } + + if (isset($filters[self::DOWNLOAD_FILTER_MAXSIZE])) { + if (empty($info['download_content_length'])) { + throw new Exception("Didn't get Content-Length from HEAD request"); + } + // TODO: handle -1 + // http://stackoverflow.com/questions/5518323/curl-getinfo-returning-1-as-content-length + if ($info['download_content_length'] > $filters[self::DOWNLOAD_FILTER_MAXSIZE]) { + throw new Exception("File too large: {$info['download_content_length']} bytes"); + } + } + } + + $tmpfile = tempnam(dirname($output_file), basename($output_file)); + $fp = fopen($tmpfile, 'wb+'); + if ($fp == false) { + throw new Exception("Failed to create: $tmpfile"); + } + + // set defaults for this method + $options += array( + CURLOPT_TIMEOUT => 10 * 60, + ); + + // overrride, must be this or the method will fail + $options[CURLOPT_RETURNTRANSFER] = false; + $options[CURLOPT_FILE] = $fp; + + $res = self::HttpRequest($url, $params, $method, $options, $info); + if (fclose($fp) !== true) { + throw new Exception("Unable to save download result"); + } + + if ($res !== true) { + unlink($tmpfile); + throw new Exception("Download error (in {$info['total_time']})"); + } + + // expect 2xx status code + if ($info['http_code'] < 200 || $info['http_code'] > 300) { + unlink($tmpfile); + throw new Exception($url . ': bad statuscode ' . $info['http_code'], $info['http_code']); + } + + // restore timestamp, if available + if ($last_modified) { + touch($tmpfile, $last_modified); + } + + $rename = rename($tmpfile, $output_file); + if ($rename !== true) { + throw new Exception("Unable to rename temporary file"); + } + } + + /** + * Parse HTTP Headers out of cURL $response, it modifies $response while doing so + * + * Uses header splitter from + * {@link http://www.sitepoint.com/forums/showthread.php?590248-Getting-response-header-in-PHP-cURL-request here} + * + * @param string $response + * @param array $info + * @return array + */ + private static function chopHttpHeaders(&$response, &$info) { + $headerstext = substr($response, 0, $info['header_size']); + $info['raw_header'] = $headerstext; + + // 'download_content_length' is -1 for HEAD request + if ($info['download_content_length'] >= 0) { + $response = substr($response, -$info['download_content_length']); + } else { + $response = ''; + } + $headersets = explode("\r\n\r\n", $headerstext); + + // http_parse_headers() php implementation from here: + // http://php.net/manual/en/function.http-parse-headers.php + $res = null; + foreach ($headersets as $i => $headerset) { + if (empty($headerset)) { + continue; + } + + $headerlist = explode("\r\n", $headerset); + $headers = array(); + // fill status line as 'Status' header, it's identical to what 'Status' actual header would be used in CGI scripts + $headers['Status'] = array_shift($headerlist); + foreach ($headerlist as $line) { + $line = rtrim($line); + if (empty($line)) { + continue; + } + + list($header, $value) = preg_split('/:\s*/', $line, 2); + + // Do Camel-Case to header name + $header = str_replace(" ", "-", ucwords(strtolower(str_replace("-", " ", $header)))); + + // add as array if duplicate + if (isset($headers[$header])) { + $headers[$header] = array($headers[$header], $value); + } else { + $headers[$header] = $value; + } + } + + if (isset($res)) { + $headers[self::RESPONSE_PREVIOUS_HEADERS] = $res; + } + $res = $headers; + } + + return $res; + } } diff --git a/application/Library/Cache.php b/application/Library/Cache.php index 0549ea568..3b33bd955 100644 --- a/application/Library/Cache.php +++ b/application/Library/Cache.php @@ -1,80 +1,77 @@ name = $name; - $clear = false; - - if ($file) { - $this->file = md5($file); - } elseif (isset($_COOKIE[$name])) { - $this->file = $_COOKIE[$name]; - } elseif (isset($_POST[self::REQUEST_NAME])) { - // Load cookie from POSTed data if browser cookie already expired - // to avoid data loss on submission - $this->file = $_POST[self::REQUEST_NAME]; - $clear = true; // cookie not sent by browser but by post request - $this->isBrowserCookie = false; - } else { - $this->file = md5(crypto_token(48)); - } - - $this->filename = APPLICATION_ROOT . 'tmp/sessions/sess_' . $this->file . '.json'; - if ($this->exists()) { - $this->data = $this->getData(); - } - if ($clear) { - //$this->deleteFile(); - } - } - - public function create($data, $expire = 0, $path = '/', $domain = null, $secure = false, $httponly = false) { - if (file_exists($this->filename)) { - unset($data['created'], $data['expires']); - return $this->saveData($data); - } - - $data['path'] = $path; - $data['domain'] = $domain; - $data['secure'] = $secure; - $data['httponly'] = $httponly; - - $this->saveData($data); - return $this->set($expire, $path, $domain, $secure, $httponly); - } - - public function saveData($data) { - if (!file_exists($this->filename) && !isset($data['expires'])) { - return; - } - - $this->data = array_merge($this->data, $data); - $this->data['modified'] = time(); - return file_put_contents($this->filename, json_encode($this->data, JSON_PRETTY_PRINT)); - } - - public function getData($field = null, $default = null) { - if (!$this->data && file_exists($this->filename)) { - $contents = file_get_contents($this->filename); - $this->data = @json_decode($contents, true); - } - - if ($field !== null) { - return array_val($this->data, $field, $default); - } else { - return $this->data; - } - } - - public function clearData() { - $this->data = array(); - } - - public function exists() { - return isset($_COOKIE[$this->name]) || isset($_POST[self::REQUEST_NAME]); - } - - public function set($expire = 0, $path = '/', $domain = null, $secure = false, $httponly = false) { - return setcookie($this->name, $this->file, $expire, $path, $domain, $secure, $httponly); - } - - public function destroy() { - setcookie($this->name, '', time() - 3600); - $this->deleteFile(); - } - - public function isExpired() { - if (empty($this->data['expires'])) { - return false; - } - - return $this->data['expires'] < time(); - } - - public function getRequestToken() { - $token = sha1(mt_rand()); - if (!$tokens = $this->getData(self::REQUEST_TOKENS)) { - $tokens = array($token => 1); - } else { - $tokens[$token] = 1; - } - - $this->saveData(array(self::REQUEST_TOKENS => $tokens)); - return $token; - } - - public function canValidateRequestToken(Request $request) { - $token = $request->getParam(self::REQUEST_TOKENS); - $tokens = $this->getData(self::REQUEST_TOKENS, array()); - //$cookie = $request->getParam(self::REQUEST_NAME); - $code = $request->getParam(self::REQUEST_USER_CODE); - $validated = false; - - if (!$this->isBrowserCookie) { - $validated = $code == $this->getData('code') && !empty($tokens[$token]); - error_log('Validated: ' . $validated); - error_log('POSTed cookie: ' . print_r($this->getData(), 1)); - } else { - $validated = !empty($tokens[$token]); - if ($validated) { - unset($tokens[$token]); - $this->saveData(array(self::REQUEST_TOKENS => $tokens)); - } - error_log('Browser cookie: ' . print_r($this->getData(), 1)); - } - - return $validated; - } - - public function getName() { - return $this->name; - } - - public function getFile() { - return $this->file; - } - - public function deleteFile() { - if (file_exists($this->filename)) { - @unlink($this->filename); - } - } - - public function setExpiration($timestamp) { - if (!file_exists($this->filename) || !$this->isBrowserCookie) { - return false; - } - - $path = $this->getData('path', '/'); - $domain = $this->getData('domain'); - $secure = $this->getData('secure', SSL); - $httponly = $this->getData('httponly', true); - $this->saveData(array('expires' => $timestamp)); - - return $this->set($timestamp, $path, $domain, $secure, $httponly); - } + protected $data = array(); + protected $name; + protected $value; + protected $filename; + protected $file; + protected $isBrowserCookie = true; + + const REQUEST_TOKENS = '_formr_request_tokens'; + const REQUEST_USER_CODE = '_formr_code'; + const REQUEST_NAME = '_formr_cookie'; + + public function __construct($name, $file = null) { + $this->name = $name; + $clear = false; + + if ($file) { + $this->file = md5($file); + } elseif (isset($_COOKIE[$name])) { + $this->file = $_COOKIE[$name]; + } elseif (isset($_POST[self::REQUEST_NAME])) { + // Load cookie from POSTed data if browser cookie already expired + // to avoid data loss on submission + $this->file = $_POST[self::REQUEST_NAME]; + $clear = true; // cookie not sent by browser but by post request + $this->isBrowserCookie = false; + } else { + $this->file = md5(crypto_token(48)); + } + + $this->filename = APPLICATION_ROOT . 'tmp/sessions/sess_' . $this->file . '.json'; + if ($this->exists()) { + $this->data = $this->getData(); + } + if ($clear) { + //$this->deleteFile(); + } + } + + public function create($data, $expire = 0, $path = '/', $domain = null, $secure = false, $httponly = false) { + if (file_exists($this->filename)) { + unset($data['created'], $data['expires']); + return $this->saveData($data); + } + + $data['path'] = $path; + $data['domain'] = $domain; + $data['secure'] = $secure; + $data['httponly'] = $httponly; + + $this->saveData($data); + return $this->set($expire, $path, $domain, $secure, $httponly); + } + + public function saveData($data) { + if (!file_exists($this->filename) && !isset($data['expires'])) { + return; + } + + $this->data = array_merge($this->data, $data); + $this->data['modified'] = time(); + return file_put_contents($this->filename, json_encode($this->data, JSON_PRETTY_PRINT)); + } + + public function getData($field = null, $default = null) { + if (!$this->data && file_exists($this->filename)) { + $contents = file_get_contents($this->filename); + $this->data = @json_decode($contents, true); + } + + if ($field !== null) { + return array_val($this->data, $field, $default); + } else { + return $this->data; + } + } + + public function clearData() { + $this->data = array(); + } + + public function exists() { + return isset($_COOKIE[$this->name]) || isset($_POST[self::REQUEST_NAME]); + } + + public function set($expire = 0, $path = '/', $domain = null, $secure = false, $httponly = false) { + return setcookie($this->name, $this->file, $expire, $path, $domain, $secure, $httponly); + } + + public function destroy() { + setcookie($this->name, '', time() - 3600); + $this->deleteFile(); + } + + public function isExpired() { + if (empty($this->data['expires'])) { + return false; + } + + return $this->data['expires'] < time(); + } + + public function getRequestToken() { + $token = sha1(mt_rand()); + if (!$tokens = $this->getData(self::REQUEST_TOKENS)) { + $tokens = array($token => 1); + } else { + $tokens[$token] = 1; + } + + $this->saveData(array(self::REQUEST_TOKENS => $tokens)); + return $token; + } + + public function canValidateRequestToken(Request $request) { + $token = $request->getParam(self::REQUEST_TOKENS); + $tokens = $this->getData(self::REQUEST_TOKENS, array()); + //$cookie = $request->getParam(self::REQUEST_NAME); + $code = $request->getParam(self::REQUEST_USER_CODE); + $validated = false; + + if (!$this->isBrowserCookie) { + $validated = $code == $this->getData('code') && !empty($tokens[$token]); + error_log('Validated: ' . $validated); + error_log('POSTed cookie: ' . print_r($this->getData(), 1)); + } else { + $validated = !empty($tokens[$token]); + if ($validated) { + unset($tokens[$token]); + $this->saveData(array(self::REQUEST_TOKENS => $tokens)); + } + error_log('Browser cookie: ' . print_r($this->getData(), 1)); + } + + return $validated; + } + + public function getName() { + return $this->name; + } + + public function getFile() { + return $this->file; + } + + public function deleteFile() { + if (file_exists($this->filename)) { + @unlink($this->filename); + } + } + + public function setExpiration($timestamp) { + if (!file_exists($this->filename) || !$this->isBrowserCookie) { + return false; + } + + $path = $this->getData('path', '/'); + $domain = $this->getData('domain'); + $secure = $this->getData('secure', SSL); + $httponly = $this->getData('httponly', true); + $this->saveData(array('expires' => $timestamp)); + + return $this->set($timestamp, $path, $domain, $secure, $httponly); + } } - diff --git a/application/Library/Crypto.php b/application/Library/Crypto.php index d24f19568..01b35c0fe 100644 --- a/application/Library/Crypto.php +++ b/application/Library/Crypto.php @@ -2,84 +2,85 @@ class Crypto { - const KEY_FILE = APPPLICATION_CRYPTO_KEY_FILE; - - private static $key = null; + const KEY_FILE = APPPLICATION_CRYPTO_KEY_FILE; - public static function setup() { - if (! \ParagonIE\Halite\Halite::isLibsodiumSetupCorrectly()) { - throw new Exception('The libsodium extension is required. Please see https://paragonie.com/book/pecl-libsodium/read/00-intro.md#installing-libsodium on how to install'); - } - - if (!file_exists(self::getKeyFile())) { - $enc_key = \ParagonIE\Halite\KeyFactory::generateEncryptionKey(); - \ParagonIE\Halite\KeyFactory::save($enc_key, self::getKeyFile()); - } - } + private static $key = null; - protected static function getKeyFile() { - if (Config::get('encryption_key_file')) { - return Config::get('encryption_key_file'); - } - return self::KEY_FILE; - } + public static function setup() { + if (!\ParagonIE\Halite\Halite::isLibsodiumSetupCorrectly()) { + throw new Exception('The libsodium extension is required. Please see https://paragonie.com/book/pecl-libsodium/read/00-intro.md#installing-libsodium on how to install'); + } - /** - * Get saved encryption key - * - * @return \ParagonIE\Halite\Symmetric\EncryptionKey | null - */ - protected static function getKey() { - if (self::$key === null) { - try { - self::$key = \ParagonIE\Halite\KeyFactory::loadEncryptionKey(self::getKeyFile()); - } catch (Exception $e) { - formr_log_exception($e, 'ParagonIE\Halite'); - } - } - return self::$key; - } + if (!file_exists(self::getKeyFile())) { + $enc_key = \ParagonIE\Halite\KeyFactory::generateEncryptionKey(); + \ParagonIE\Halite\KeyFactory::save($enc_key, self::getKeyFile()); + } + } - private static function doWeNeedHiddenStrings() { - if ( class_exists('\ParagonIE\Halite\HiddenString') ) { - return true; - } else { - return false; - } - } + protected static function getKeyFile() { + if (Config::get('encryption_key_file')) { + return Config::get('encryption_key_file'); + } + return self::KEY_FILE; + } - /** - * Encrypt Data - * - * @param string|array $data String or an array of strongs - * @param string $glue If array is provided as first parameter, this will be used to glue the elements to form a string - * @return string|null - */ - public static function encrypt($data, $glue = '') { - if (is_array($data)) { - $data = implode($glue, $data); - } - if ( self::doWeNeedHiddenStrings() && ! $data instanceof \ParagonIE\Halite\HiddenString ) { - $data = new \ParagonIE\Halite\HiddenString($data, true); - } - try { - return \ParagonIE\Halite\Symmetric\Crypto::encrypt($data, self::getKey()); - } catch (Exception $e) { - formr_log_exception($e, 'ParagonIE\Halite'); - } - } + /** + * Get saved encryption key + * + * @return \ParagonIE\Halite\Symmetric\EncryptionKey | null + */ + protected static function getKey() { + if (self::$key === null) { + try { + self::$key = \ParagonIE\Halite\KeyFactory::loadEncryptionKey(self::getKeyFile()); + } catch (Exception $e) { + formr_log_exception($e, 'ParagonIE\Halite'); + } + } + return self::$key; + } + + private static function doWeNeedHiddenStrings() { + if (class_exists('\ParagonIE\Halite\HiddenString')) { + return true; + } else { + return false; + } + } + + /** + * Encrypt Data + * + * @param string|array $data String or an array of strongs + * @param string $glue If array is provided as first parameter, this will be used to glue the elements to form a string + * @return string|null + */ + public static function encrypt($data, $glue = '') { + if (is_array($data)) { + $data = implode($glue, $data); + } + if (self::doWeNeedHiddenStrings() && !$data instanceof \ParagonIE\Halite\HiddenString) { + $data = new \ParagonIE\Halite\HiddenString($data, true); + } + try { + return \ParagonIE\Halite\Symmetric\Crypto::encrypt($data, self::getKey()); + } catch (Exception $e) { + formr_log_exception($e, 'ParagonIE\Halite'); + } + } + + /** + * Decrypt cipher text + * + * @param string $ciphertext + * @return string|null + */ + public static function decrypt($ciphertext) { + try { + return \ParagonIE\Halite\Symmetric\Crypto::decrypt($ciphertext, self::getKey()); + } catch (Exception $e) { + formr_log_exception($e, 'ParagonIE\Halite'); + } + } - /** - * Decrypt cipher text - * - * @param string $ciphertext - * @return string|null - */ - public static function decrypt($ciphertext) { - try { - return \ParagonIE\Halite\Symmetric\Crypto::decrypt($ciphertext, self::getKey()); - } catch (Exception $e) { - formr_log_exception($e, 'ParagonIE\Halite'); - } - } } diff --git a/application/Library/DB.php b/application/Library/DB.php index a48b5e47e..5562f46d6 100644 --- a/application/Library/DB.php +++ b/application/Library/DB.php @@ -2,860 +2,860 @@ class DB { - /** - * @var DB - */ - protected static $instance = null; - - /** - * - * @var array - */ - protected $types = array( - // Interger types - 'int' => PDO::PARAM_INT, - 'integer' => PDO::PARAM_INT, - // String Types - 'str' => PDO::PARAM_STR, - 'string' => PDO::PARAM_STR, - // Boolean types - 'bool' => PDO::PARAM_BOOL, - 'boolean' => PDO::PARAM_BOOL, - // NULL type - 'null' => PDO::PARAM_NULL, - ); - - /** - * Default data-type - * - * @var interger - */ - protected $default_type = PDO::PARAM_STR; - - /** - * Get a DB instance - * - * @return DB - */ - public static function getInstance() { - if (self::$instance == null) { - self::$instance = new self(); - } - return self::$instance; - } - - /** - * @var PDO - */ - protected $PDO; - - protected function __construct() { - $params = (array) Config::get('database'); - - $options = array( - 'host' => $params['host'], - 'dbname' => $params['database'], - 'charset' => 'utf8', - ); - if (!empty($params['port'])) { - $options['port'] = $params['port']; - } - - $dsn = 'mysql:' . http_build_query($options, null, ';'); - $this->PDO = new PDO($dsn, $params['login'], $params['password'], array( - PDO::ATTR_EMULATE_PREPARES => false, - PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, - PDO::MYSQL_ATTR_INIT_COMMAND => 'SET NAMES utf8', - )); - - $dt = new DateTime(); - $offset = $dt->format("P"); - - $this->PDO->exec("SET time_zone='$offset';"); - $this->PDO->exec("SET SESSION sql_mode='STRICT_ALL_TABLES';"); - } - - /** - * Execute any query with parameters and get results - * - * @param string $query Query string with optional placeholders - * @param array $params An array of parameters to bind to PDO statement - * @param bool $fetchcol - * @param bool $fetchrow - * @return array Returns an associative array of results - */ - public function execute($query, $params = array(), $fetchcol = false, $fetchrow = false) { - $data = self::parseWhereBindParams($params); - $params = $data['params']; - $stmt = $this->PDO->prepare($query); - $stmt->execute($params); - if ($fetchcol) { - return $stmt->fetchColumn(); - } - if ($fetchrow) { - return $stmt->fetch(PDO::FETCH_ASSOC); - } - return $stmt->fetchAll(PDO::FETCH_ASSOC); - } - - /** - * Used for INSERT, UPDATE and DELETE - * - * @param string $query MySQL query with placeholders - * @param array $data Optional associative array of parameters that will be bound to the query - * @return int Returns the number of affected rows of the query - */ - public function exec($query, array $data = array()) { - if ($data) { - $data = self::parseWhereBindParams($data); - $params = $data['params']; - $sth = $this->PDO->prepare($query); - $sth->execute($params); - return $sth->rowCount(); - } - return $this->PDO->exec($query); - } - - /** - * Used for SELECT or 'non-modify' queries - * - * @param string $query SQL query to execute - * @param bool $return_statemnt [optional] If set to true, PDOStatement is always returned - * @return mixed Returns a PDOStatement if not selecting else returns selected results in an associative array - */ - public function query($query, $return_statemnt = false) { - $stmt = $this->PDO->query($query); - if (preg_match('/^select/', strtolower($query)) && $return_statemnt === false) { - return $stmt->fetchAll(PDO::FETCH_ASSOC); - } - return $stmt; - } - - /** - * @param string $query - * - * @return PDOStatement - */ - public function rquery($query) { //secured query with prepare and execute - $args = func_get_args(); - array_shift($args); //first element is not an argument but the query itself, should removed - - $stmt = $this->PDO->prepare($query); - $stmt->execute($args); - return $stmt; - } - - /** - * Get the number of rows in a result - * - * @param string $query - * @return mixed - */ - public function num_rows($query) { - # create a prepared statement - $stmt = $this->PDO->prepare($query); - $stmt->execute(); - return $stmt->rowCount(); - } - - public function table_exists($table) { - return $this->num_rows("SHOW TABLES LIKE '" . $table . "'") > 0; - } - - public function __destruct() { - $this->PDO = null; - } - - public function getError() { - return $this->PDO->errorInfo(); - } - - /** - * Find a set of records from db - * - * @param string $table_name - * @param string|aray $where - * @param array $params - * @return array - */ - public function find($table_name, $where = null, $params = array()) { - if (empty($params['cols'])) { - $params['cols'] = array(); - } - $select = $this->select($params['cols']); - $select->from($table_name); - - if ($where) { - $select->where($where); - } - - if (!empty($params['order']) && !empty($params['order_by'])) { - $select->order($params['order_by'], $params['order']); - } - - if (!empty($params['limit'])) { - $offset = isset($params['offset']) ? $params['offset'] : 0; - $select->limit($params['limit'], $offset); - } - - // unset all the shit that is not necessary for binding - unset($params['cols'], $params['order_by'], $params['order'], $params['limit'], $params['offset']); - if ($params) { - $params = self::parseWhereBindParams($params); - $select->setParams($params['binds']); - } - - return $select->fetchAll(); - } - - public function findRow($table_name, $where = null, $cols = array()) { - return $this->select($cols)->from($table_name)->where($where)->limit(1)->fetch(); - } - - public function findValue($table_name, $where = null, $cols = array()) { - return $this->select($cols)->from($table_name)->where($where)->limit(1)->fetchColumn(); - } - - public function select($cols = array()) { - if (is_string($cols)) { - $cols = explode(',', $cols); - } - return new DB_Select($this->PDO, $cols); - } - - /** - * Count - * - * @param string $table_name - * @param array|string $where If a string is given, it must be properly escaped - * @param string $col specify some column name to count - * @return int - */ - public function count($table_name, $where = array(), $col = '*') { - $query = "SELECT count({$col}) FROM {$table_name}"; - $params = array(); - if ($where && is_array($where)) { - $wc = self::parseWhereBindParams($where); - $query .= " WHERE {$wc['clauses_str']}"; - $params = $wc['params']; - } elseif ($where && is_string($where)) { - $query .= " WHERE $where"; - } - - $stmt = $this->PDO->prepare($query); - $stmt->execute($params); - return $stmt->fetchColumn(); - } - - /** - * Assert the existence of rows in a table - * - * @param string $table_name - * @param array|string $where If a string is given, it must be properly escaped - * @return boolean - */ - public function entry_exists($table_name, $where) { - return $this->count($table_name, $where) > 0; - } - - /** - * Insert Data into a MySQL Table - * - * @param string $table_name Table Name - * @param array $data An associative array of data with keys representing column names - * @param array $types A numerically indexed array representing the data-types of the value in the $data array. - * @throws Exception - * @return mix Returns an integer if last insert id was set else returns null - */ - public function insert($table_name, array $data, array $types = array()) { - if (!$this->checkTypeCount($data, $types)) { - throw new Exception("Array count for data and data-types do not match"); - } - - $keys = array_map(array('DB', 'pkey'), array_keys($data)); - $cols = array_map(array('DB', 'quoteCol'), array_keys($data)); - - $query = self::replace("INSERT INTO %{table_name} (%{cols}) VALUES (%{values})", array( - 'cols' => implode(', ', $cols), - 'values' => implode(', ', $keys), - 'table_name' => $table_name, - )); - - /* @var $stmt PDOStatement */ - $stmt = $this->PDO->prepare($query); - $stmt = $this->bindValues($stmt, $data, array_values($types), false, true); - $stmt->execute(); - return $this->lastInsertId(); - } - - /** - * Insert data and update values on duplicate keys - * - * @param string $table Table name - * @param array $data An associative array of key,value pairs representing data to insert - * @param array $updates [optional] Optional column names representing what values to update - * @return int Returns number of affected rows - */ - public function insert_update($table, array $data, array $updates = array()) { - $columns = array_map(array('DB', 'quoteCol'), array_keys($data)); - $values = array_map(array('DB', 'pkey'), array_keys($data)); - if (!$updates) { - $updates = array_keys($data); - } - $table = self::quoteCol($table); - - $columns_str = implode(',', $columns); - $values_str = implode(',', $values); - $updates_str = $this->getDuplicateUpdateString($updates); - - $query = "INSERT INTO $table ($columns_str) VALUES ($values_str) ON DUPLICATE KEY UPDATE $updates_str"; - return $this->exec($query, $data); - } - - public function update($table_name, array $data, array $where, array $data_types = array(), array $where_types = array()) { - if (!$this->checkTypeCount($data, $data_types)) { - throw new Exception("Array count for data and data-types do not match"); - } - - if (!$this->checkTypeCount($where, $where_types)) { - throw new Exception("Array count for where clause and where clause data-types do not match"); - } - - // remove signs from where clauses (<, >, <= , >=, != can be used in array keys of $where array. E.g $where['id >'] = 45) - $cols_where = array_keys($where); - $signs = array(); - foreach ($cols_where as $i => $col_condition) { - $col_condition = trim($col_condition); - $col_condition = preg_replace('/\s+/', ' ', $col_condition); - $parts = array_filter(explode(' ', $col_condition)); - if (count($parts) === 1) { - $signs[$i] = '='; - } else { - $signs[$i] = $parts[1]; - } - $cols_where[$i] = $parts[0]; - } - - $cols = array_map(array('DB', 'quoteCol'), array_keys($data)); - $cols_where = array_map(array('DB', 'quoteCol'), $cols_where); - $set_values = $where_values = array(); - - foreach ($cols as $col) { - $set_values[] = "$col = ?"; - } - - foreach ($cols_where as $i => $col) { - $sign = !empty($signs[$i]) ? $signs[$i] : '='; - $where_values[] = "$col $sign ?"; - } - - $query = self::replace("UPDATE %{table_name} SET %{set_values} WHERE (%{where_values})", array( - 'where_values' => implode(' AND ', $where_values), - 'set_values' => implode(', ', $set_values), - 'table_name' => $table_name, - )); - - /* @var $stmt PDOStatement */ - $stmt = $this->PDO->prepare($query); - $stmt = $this->bindValues($stmt, $data, array_values($data_types), true, true); - $stmt = $this->bindValues($stmt, $where, array_values($where_types)); - $stmt->execute(); - return $stmt->rowCount(); - } - - public function delete($table_name, array $data, array $types = array()) { - $cols = array_map(array('DB', 'pCol'), array_keys($data)); - $query = self::replace("DELETE FROM %{table_name} WHERE (%{values})", array( - 'values' => implode(' AND ', $cols), - 'table_name' => self::quoteCol($table_name), - )); - - /* @var $stmt PDOStatement */ - $stmt = $this->PDO->prepare($query); - $stmt = $this->bindValues($stmt, $data, array_values($types), false, true); - $stmt->execute(); - return $stmt->rowCount(); - } - - /** - * @param string $query - * @param array $options [optional] driver options - * @return PDOStatement - */ - public function prepare($query, $options = array()) { - return $this->PDO->prepare($query, $options); - } - - /** - * Quote a string for sql query - * - * @param string $string - * @return string - */ - public function quote($string) { - return $this->PDO->quote($string); - } - - /** - * @return PDO - */ - public function pdo() { - return $this->PDO; - } - - public function lastInsertId() { - return $this->PDO->lastInsertId(); - } - - /** - * - * @param PDOStatement $stmt - * @param array $data - * @param array $types - * @param bool $numeric - * #param bool $reset - * @return PDOStatement - */ - private function bindValues($stmt, $data, $types, $numeric = true, $reset = false) { - static $i; - if ($reset || $i === null) { - $i = 0; - } - foreach ($data as $key => $value) { - $type = $this->default_type; - if (isset($types[$i]) && isset($this->types[$types[$i]])) { - $type = $this->types[$types[$i]]; - } - $stmt->bindValue(($numeric ? $i + 1 : self::pkey($key)), $value, $type); - $i++; - } - return $stmt; - } - - private function checkTypeCount(array $data, array $types) { - if (!$types) { - return true; - } - return count($types) === count($data); - } - - public function getDuplicateUpdateString($columns) { - foreach ($columns as $i => $column) { - if (is_numeric($i)) { - $column = trim($column, '`'); - $columns[$i] = "`$column` = VALUES(`$column`)"; - } else { - $value = strstr($column, '::') !== false ? str_replace('::', '', $column) : $this->PDO->quote($column); - $column = trim($i, '`'); - $columns[$i] = "`$column` = $value"; - } - } - return implode(', ', $columns); - } - - public static function pkey($key) { - $key = trim($key); - $key = trim($key, '`:'); - return ':' . $key; - } - - public static function pCol($col) { - $col = trim($col); - $col = trim($col, '`:'); - $col = "`$col` = :$col"; - return $col; - } - - public static function quoteCol($col, $table = null) { - $col = trim($col); - $col = trim($col, '`'); - return $table !== null ? "`$table`.`$col`" : "`$col`"; - } - - public static function parseColName($string) { - if (strpos($string, '.') !== false) { - $string = explode('.', $string, 2); - $tableName = self::quoteCol($string[0]); - $fieldName = self::quoteCol($string[1]); - return $tableName . '.' . $fieldName; - } - return self::quoteCol($string); - } - - public static function parseWhereBindParams(array $array) { - $cols = array_keys($array); - $values = array_values($array); - - $clauses = array_map(array('DB', 'pCol'), $cols); - $params = array(); - - foreach ($cols as $i => $col) { - $params[self::pkey($col)] = $values[$i]; - } - return array( - 'clauses' => $clauses, - 'clauses_str' => implode(' AND ', $clauses), - 'params' => $params, - ); - } - - public static function replace($string, $params = array()) { - foreach ($params as $key => $value) { - $key = "%{" . $key . "}"; - $string = str_replace($key, $value, $string); - } - return $string; - } - - /** - * For transactions just use same calls - */ - - /** - * {inherit PDO doc} - */ - public function beginTransaction() { - return $this->PDO->beginTransaction(); - } - - /** - * {inherit PDO doc} - */ - public function commit() { - return $this->PDO->commit(); - } - - /** - * {inherit PDO doc} - */ - public function rollBack() { - return $this->PDO->rollBack(); - } - - public function getTableDefinition($table, $property = null) { - if (!$this->table_exists($table)) { - return array(); - } - - $query = "SHOW COLUMNS FROM `$table`"; - $stmt = $this->PDO->query($query); - $cols = $stmt->fetchAll(PDO::FETCH_ASSOC); - if ($property === null) { - return $cols; - } - $filtered = array(); - foreach ($cols as $col) { - $filtered[$col[$property]] = $col; - } - return $filtered; - } + /** + * @var DB + */ + protected static $instance = null; + + /** + * + * @var array + */ + protected $types = array( + // Interger types + 'int' => PDO::PARAM_INT, + 'integer' => PDO::PARAM_INT, + // String Types + 'str' => PDO::PARAM_STR, + 'string' => PDO::PARAM_STR, + // Boolean types + 'bool' => PDO::PARAM_BOOL, + 'boolean' => PDO::PARAM_BOOL, + // NULL type + 'null' => PDO::PARAM_NULL, + ); + + /** + * Default data-type + * + * @var interger + */ + protected $default_type = PDO::PARAM_STR; + + /** + * Get a DB instance + * + * @return DB + */ + public static function getInstance() { + if (self::$instance == null) { + self::$instance = new self(); + } + return self::$instance; + } + + /** + * @var PDO + */ + protected $PDO; + + protected function __construct() { + $params = (array) Config::get('database'); + + $options = array( + 'host' => $params['host'], + 'dbname' => $params['database'], + 'charset' => 'utf8', + ); + if (!empty($params['port'])) { + $options['port'] = $params['port']; + } + + $dsn = 'mysql:' . http_build_query($options, null, ';'); + $this->PDO = new PDO($dsn, $params['login'], $params['password'], array( + PDO::ATTR_EMULATE_PREPARES => false, + PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, + PDO::MYSQL_ATTR_INIT_COMMAND => 'SET NAMES utf8', + )); + + $dt = new DateTime(); + $offset = $dt->format("P"); + + $this->PDO->exec("SET time_zone='$offset';"); + $this->PDO->exec("SET SESSION sql_mode='STRICT_ALL_TABLES';"); + } + + /** + * Execute any query with parameters and get results + * + * @param string $query Query string with optional placeholders + * @param array $params An array of parameters to bind to PDO statement + * @param bool $fetchcol + * @param bool $fetchrow + * @return array Returns an associative array of results + */ + public function execute($query, $params = array(), $fetchcol = false, $fetchrow = false) { + $data = self::parseWhereBindParams($params); + $params = $data['params']; + $stmt = $this->PDO->prepare($query); + $stmt->execute($params); + if ($fetchcol) { + return $stmt->fetchColumn(); + } + if ($fetchrow) { + return $stmt->fetch(PDO::FETCH_ASSOC); + } + return $stmt->fetchAll(PDO::FETCH_ASSOC); + } + + /** + * Used for INSERT, UPDATE and DELETE + * + * @param string $query MySQL query with placeholders + * @param array $data Optional associative array of parameters that will be bound to the query + * @return int Returns the number of affected rows of the query + */ + public function exec($query, array $data = array()) { + if ($data) { + $data = self::parseWhereBindParams($data); + $params = $data['params']; + $sth = $this->PDO->prepare($query); + $sth->execute($params); + return $sth->rowCount(); + } + return $this->PDO->exec($query); + } + + /** + * Used for SELECT or 'non-modify' queries + * + * @param string $query SQL query to execute + * @param bool $return_statemnt [optional] If set to true, PDOStatement is always returned + * @return mixed Returns a PDOStatement if not selecting else returns selected results in an associative array + */ + public function query($query, $return_statemnt = false) { + $stmt = $this->PDO->query($query); + if (preg_match('/^select/', strtolower($query)) && $return_statemnt === false) { + return $stmt->fetchAll(PDO::FETCH_ASSOC); + } + return $stmt; + } + + /** + * @param string $query + * + * @return PDOStatement + */ + public function rquery($query) { //secured query with prepare and execute + $args = func_get_args(); + array_shift($args); //first element is not an argument but the query itself, should removed + + $stmt = $this->PDO->prepare($query); + $stmt->execute($args); + return $stmt; + } + + /** + * Get the number of rows in a result + * + * @param string $query + * @return mixed + */ + public function num_rows($query) { + # create a prepared statement + $stmt = $this->PDO->prepare($query); + $stmt->execute(); + return $stmt->rowCount(); + } + + public function table_exists($table) { + return $this->num_rows("SHOW TABLES LIKE '" . $table . "'") > 0; + } + + public function __destruct() { + $this->PDO = null; + } + + public function getError() { + return $this->PDO->errorInfo(); + } + + /** + * Find a set of records from db + * + * @param string $table_name + * @param string|aray $where + * @param array $params + * @return array + */ + public function find($table_name, $where = null, $params = array()) { + if (empty($params['cols'])) { + $params['cols'] = array(); + } + $select = $this->select($params['cols']); + $select->from($table_name); + + if ($where) { + $select->where($where); + } + + if (!empty($params['order']) && !empty($params['order_by'])) { + $select->order($params['order_by'], $params['order']); + } + + if (!empty($params['limit'])) { + $offset = isset($params['offset']) ? $params['offset'] : 0; + $select->limit($params['limit'], $offset); + } + + // unset all the shit that is not necessary for binding + unset($params['cols'], $params['order_by'], $params['order'], $params['limit'], $params['offset']); + if ($params) { + $params = self::parseWhereBindParams($params); + $select->setParams($params['binds']); + } + + return $select->fetchAll(); + } + + public function findRow($table_name, $where = null, $cols = array()) { + return $this->select($cols)->from($table_name)->where($where)->limit(1)->fetch(); + } + + public function findValue($table_name, $where = null, $cols = array()) { + return $this->select($cols)->from($table_name)->where($where)->limit(1)->fetchColumn(); + } + + public function select($cols = array()) { + if (is_string($cols)) { + $cols = explode(',', $cols); + } + return new DB_Select($this->PDO, $cols); + } + + /** + * Count + * + * @param string $table_name + * @param array|string $where If a string is given, it must be properly escaped + * @param string $col specify some column name to count + * @return int + */ + public function count($table_name, $where = array(), $col = '*') { + $query = "SELECT count({$col}) FROM {$table_name}"; + $params = array(); + if ($where && is_array($where)) { + $wc = self::parseWhereBindParams($where); + $query .= " WHERE {$wc['clauses_str']}"; + $params = $wc['params']; + } elseif ($where && is_string($where)) { + $query .= " WHERE $where"; + } + + $stmt = $this->PDO->prepare($query); + $stmt->execute($params); + return $stmt->fetchColumn(); + } + + /** + * Assert the existence of rows in a table + * + * @param string $table_name + * @param array|string $where If a string is given, it must be properly escaped + * @return boolean + */ + public function entry_exists($table_name, $where) { + return $this->count($table_name, $where) > 0; + } + + /** + * Insert Data into a MySQL Table + * + * @param string $table_name Table Name + * @param array $data An associative array of data with keys representing column names + * @param array $types A numerically indexed array representing the data-types of the value in the $data array. + * @throws Exception + * @return mix Returns an integer if last insert id was set else returns null + */ + public function insert($table_name, array $data, array $types = array()) { + if (!$this->checkTypeCount($data, $types)) { + throw new Exception("Array count for data and data-types do not match"); + } + + $keys = array_map(array('DB', 'pkey'), array_keys($data)); + $cols = array_map(array('DB', 'quoteCol'), array_keys($data)); + + $query = self::replace("INSERT INTO %{table_name} (%{cols}) VALUES (%{values})", array( + 'cols' => implode(', ', $cols), + 'values' => implode(', ', $keys), + 'table_name' => $table_name, + )); + + /* @var $stmt PDOStatement */ + $stmt = $this->PDO->prepare($query); + $stmt = $this->bindValues($stmt, $data, array_values($types), false, true); + $stmt->execute(); + return $this->lastInsertId(); + } + + /** + * Insert data and update values on duplicate keys + * + * @param string $table Table name + * @param array $data An associative array of key,value pairs representing data to insert + * @param array $updates [optional] Optional column names representing what values to update + * @return int Returns number of affected rows + */ + public function insert_update($table, array $data, array $updates = array()) { + $columns = array_map(array('DB', 'quoteCol'), array_keys($data)); + $values = array_map(array('DB', 'pkey'), array_keys($data)); + if (!$updates) { + $updates = array_keys($data); + } + $table = self::quoteCol($table); + + $columns_str = implode(',', $columns); + $values_str = implode(',', $values); + $updates_str = $this->getDuplicateUpdateString($updates); + + $query = "INSERT INTO $table ($columns_str) VALUES ($values_str) ON DUPLICATE KEY UPDATE $updates_str"; + return $this->exec($query, $data); + } + + public function update($table_name, array $data, array $where, array $data_types = array(), array $where_types = array()) { + if (!$this->checkTypeCount($data, $data_types)) { + throw new Exception("Array count for data and data-types do not match"); + } + + if (!$this->checkTypeCount($where, $where_types)) { + throw new Exception("Array count for where clause and where clause data-types do not match"); + } + + // remove signs from where clauses (<, >, <= , >=, != can be used in array keys of $where array. E.g $where['id >'] = 45) + $cols_where = array_keys($where); + $signs = array(); + foreach ($cols_where as $i => $col_condition) { + $col_condition = trim($col_condition); + $col_condition = preg_replace('/\s+/', ' ', $col_condition); + $parts = array_filter(explode(' ', $col_condition)); + if (count($parts) === 1) { + $signs[$i] = '='; + } else { + $signs[$i] = $parts[1]; + } + $cols_where[$i] = $parts[0]; + } + + $cols = array_map(array('DB', 'quoteCol'), array_keys($data)); + $cols_where = array_map(array('DB', 'quoteCol'), $cols_where); + $set_values = $where_values = array(); + + foreach ($cols as $col) { + $set_values[] = "$col = ?"; + } + + foreach ($cols_where as $i => $col) { + $sign = !empty($signs[$i]) ? $signs[$i] : '='; + $where_values[] = "$col $sign ?"; + } + + $query = self::replace("UPDATE %{table_name} SET %{set_values} WHERE (%{where_values})", array( + 'where_values' => implode(' AND ', $where_values), + 'set_values' => implode(', ', $set_values), + 'table_name' => $table_name, + )); + + /* @var $stmt PDOStatement */ + $stmt = $this->PDO->prepare($query); + $stmt = $this->bindValues($stmt, $data, array_values($data_types), true, true); + $stmt = $this->bindValues($stmt, $where, array_values($where_types)); + $stmt->execute(); + return $stmt->rowCount(); + } + + public function delete($table_name, array $data, array $types = array()) { + $cols = array_map(array('DB', 'pCol'), array_keys($data)); + $query = self::replace("DELETE FROM %{table_name} WHERE (%{values})", array( + 'values' => implode(' AND ', $cols), + 'table_name' => self::quoteCol($table_name), + )); + + /* @var $stmt PDOStatement */ + $stmt = $this->PDO->prepare($query); + $stmt = $this->bindValues($stmt, $data, array_values($types), false, true); + $stmt->execute(); + return $stmt->rowCount(); + } + + /** + * @param string $query + * @param array $options [optional] driver options + * @return PDOStatement + */ + public function prepare($query, $options = array()) { + return $this->PDO->prepare($query, $options); + } + + /** + * Quote a string for sql query + * + * @param string $string + * @return string + */ + public function quote($string) { + return $this->PDO->quote($string); + } + + /** + * @return PDO + */ + public function pdo() { + return $this->PDO; + } + + public function lastInsertId() { + return $this->PDO->lastInsertId(); + } + + /** + * + * @param PDOStatement $stmt + * @param array $data + * @param array $types + * @param bool $numeric + * #param bool $reset + * @return PDOStatement + */ + private function bindValues($stmt, $data, $types, $numeric = true, $reset = false) { + static $i; + if ($reset || $i === null) { + $i = 0; + } + foreach ($data as $key => $value) { + $type = $this->default_type; + if (isset($types[$i]) && isset($this->types[$types[$i]])) { + $type = $this->types[$types[$i]]; + } + $stmt->bindValue(($numeric ? $i + 1 : self::pkey($key)), $value, $type); + $i++; + } + return $stmt; + } + + private function checkTypeCount(array $data, array $types) { + if (!$types) { + return true; + } + return count($types) === count($data); + } + + public function getDuplicateUpdateString($columns) { + foreach ($columns as $i => $column) { + if (is_numeric($i)) { + $column = trim($column, '`'); + $columns[$i] = "`$column` = VALUES(`$column`)"; + } else { + $value = strstr($column, '::') !== false ? str_replace('::', '', $column) : $this->PDO->quote($column); + $column = trim($i, '`'); + $columns[$i] = "`$column` = $value"; + } + } + return implode(', ', $columns); + } + + public static function pkey($key) { + $key = trim($key); + $key = trim($key, '`:'); + return ':' . $key; + } + + public static function pCol($col) { + $col = trim($col); + $col = trim($col, '`:'); + $col = "`$col` = :$col"; + return $col; + } + + public static function quoteCol($col, $table = null) { + $col = trim($col); + $col = trim($col, '`'); + return $table !== null ? "`$table`.`$col`" : "`$col`"; + } + + public static function parseColName($string) { + if (strpos($string, '.') !== false) { + $string = explode('.', $string, 2); + $tableName = self::quoteCol($string[0]); + $fieldName = self::quoteCol($string[1]); + return $tableName . '.' . $fieldName; + } + return self::quoteCol($string); + } + + public static function parseWhereBindParams(array $array) { + $cols = array_keys($array); + $values = array_values($array); + + $clauses = array_map(array('DB', 'pCol'), $cols); + $params = array(); + + foreach ($cols as $i => $col) { + $params[self::pkey($col)] = $values[$i]; + } + return array( + 'clauses' => $clauses, + 'clauses_str' => implode(' AND ', $clauses), + 'params' => $params, + ); + } + + public static function replace($string, $params = array()) { + foreach ($params as $key => $value) { + $key = "%{" . $key . "}"; + $string = str_replace($key, $value, $string); + } + return $string; + } + + /** + * For transactions just use same calls + */ + + /** + * {inherit PDO doc} + */ + public function beginTransaction() { + return $this->PDO->beginTransaction(); + } + + /** + * {inherit PDO doc} + */ + public function commit() { + return $this->PDO->commit(); + } + + /** + * {inherit PDO doc} + */ + public function rollBack() { + return $this->PDO->rollBack(); + } + + public function getTableDefinition($table, $property = null) { + if (!$this->table_exists($table)) { + return array(); + } + + $query = "SHOW COLUMNS FROM `$table`"; + $stmt = $this->PDO->query($query); + $cols = $stmt->fetchAll(PDO::FETCH_ASSOC); + if ($property === null) { + return $cols; + } + $filtered = array(); + foreach ($cols as $col) { + $filtered[$col[$property]] = $col; + } + return $filtered; + } } class DB_Select { - /** - * @var PDO - */ - protected $PDO; - - /** - * Constructed SQL statement - * - * @var string - */ - protected $query; - protected $where = array(); - protected $or_where = array(); - protected $joins = array(); - protected $columns = array('*'); - protected $params = array(); - protected $order = array(); - protected $limit; - protected $offset; - protected $table; - - public function __construct(PDO $pdo, array $cols = array()) { - $this->PDO = $pdo; - $this->columns($cols); - } - - public function __destruct() { - $this->PDO = null; - } - - public function columns(array $cols) { - if ($cols) { - $this->columns = $this->parseCols($cols); - } - } - - public function from($table) { - $this->table = DB::quoteCol($table); - return $this; - } - - public function leftJoin($table, $condition) { - $table = DB::quoteCol($table); - $condition = $this->parseJoinConditions(func_get_args()); - $this->joins[] = " LEFT JOIN $table ON ($condition)"; - return $this; - } - - public function rightJoin($table, $condition) { - $table = DB::quoteCol($table); - $condition = $this->parseJoinConditions(func_get_args()); - $this->joins[] = " RIGHT JOIN $table ON ($condition)"; - return $this; - } - - public function join($table, $condition) { - $table = DB::quoteCol($table); - $condition = $this->parseJoinConditions(func_get_args()); - $this->joins[] = " INNER JOIN $table ON ($condition)"; - return $this; - } - - public function where($where) { - if (is_array($where)) { - $whereParsed = $this->parseWhere($where); - $this->where = array_merge($this->where, $whereParsed['clauses']); - $this->params = array_merge($this->params, $whereParsed['params']); - } elseif (is_string($where)) { - $this->where[] = $where; - } - return $this; - } - - public function whereIn($field, array $values) { - $field = $this->parseColName($field); - $values = array_map(array($this->PDO, 'quote'), $values); - $this->where[] = "{$field} IN (" . implode(',', $values) . ")"; - } - - public function like($colname, $value, $pad = 'both') { - $colname = $this->parseColName($colname); - if ($pad === 'right') { - $value = "$value%"; - } elseif ($pad === 'left') { - $value = "%$value"; - } else { - $value = "%$value%"; - } - $this->PDO->quote($value); - $this->where("$colname LIKE '$value'"); - return $this; - } - - public function limit($limit, $offset = 0) { - $this->limit = (int) $limit; - $this->offset = (int) $offset; - return $this; - } - - public function order($by, $order = 'asc') { - if ($by === 'RAND') { - $this->order[] = 'RAND()'; - return $this; - } - - if ($order === null) { - $this->order[] = $by; - return $this; - } - - $order = strtoupper($order); - if (!in_array($order, array('ASC', 'DESC'))) { - throw new Exception("Invalid Order"); - } - $by = $this->parseColName($by); - $this->order[] = "$by $order"; - return $this; - } - - /** - * {inherit PDO doc} - */ - public function fetchAll($fetch_style = PDO::FETCH_ASSOC) { - $this->constructQuery(); - $query = $this->trimQuery(); - $stmt = $this->PDO->prepare($query); - $stmt->execute($this->params); - return $stmt->fetchAll($fetch_style); - } - - /** - * {inherit PDO doc} - */ - public function fetch($fetch_style = PDO::FETCH_ASSOC) { - $this->constructQuery(); - $query = $this->trimQuery(); - $stmt = $this->PDO->prepare($query); - $stmt->execute($this->params); - return $stmt->fetch($fetch_style); - } - - /** - * {inherit PDO doc} - */ - public function fetchColumn() { - $this->constructQuery(); - $query = $this->trimQuery(); - $stmt = $this->PDO->prepare($query); - $stmt->execute($this->params); - return $stmt->fetchColumn(); - } - - /** - * Returns executed PDO statement of current query - * - * @return PDOStatement - */ - public function statement() { - $this->constructQuery(); - $query = $this->trimQuery(); - $stmt = $this->PDO->prepare($query); - if ($this->params) { - foreach ($this->params as $key => $value) { - $stmt->bindValue($key, $value); - } - } - $stmt->execute(); - return $stmt; - } - - public function getParams() { - return $this->params; - } - - public function setParams(array $params) { - $this->params = $params; - return $this; - } - - public function bindParams(array $params) { - $params = $this->parseWhere($params); - $this->params = array_merge($this->params, $params['params']); - return $this; - } - - public function lastQuery() { - $this->constructQuery(); - return $this->query; - } - - private function trimQuery() { - return str_replace("\n", "", $this->query); - } - - /** - * @todo Add or_where and or_like clauses - */ - private function constructQuery() { - $columns = implode(', ', $this->columns); - - $query = "SELECT $columns FROM {$this->table} \n"; - if ($this->joins) { - $query .= implode(" \n", $this->joins); - } - - if ($this->where) { - $where = implode(' AND ', $this->where); - $query .= " WHERE ($where)"; - } - - if ($this->order) { - $order = implode(', ', $this->order); - $query .= " \nORDER BY " . $order; - } - - if ($this->limit) { - $query .= " \nLIMIT {$this->offset}, {$this->limit}"; - } - $this->query = $query; - } - - private function parseJoinConditions($conditions) { - array_shift($conditions); // first arguement is the table name - $parsed = array(); - foreach ($conditions as $condition) { - $parsed[] = $this->parseJoinCondition($condition); - } - return implode(' AND ', $parsed); - } - - private function parseJoinCondition($condition) { - $conditions = explode('=', $condition, 2); - if (count($conditions) != 2) { - throw new Exception("Unable to get join condition clauses"); - } - $conditions = $this->parseCols($conditions); - return implode(' = ', $conditions); - } - - private function parseCols(array $cols) { - $select = array(); - foreach ($cols as $key => $val) { - if (is_numeric($key)) { - $select[] = $this->parseColName($val); - } else { - $select[] = $this->parseColName($key) . ' AS ' . $this->parseColName($val); - } - } - return $select; - } - - private function parseColName($string) { - $string = trim($string); - // If the column is not one of these then maybe some mysql func or so is called - if (preg_match('/[^a-zA-Z0-9_\.\`]/i', $string, $matches)) { - return $string; - } - - $string = trim($string, '`'); - - if (strpos($string, '.') !== false) { - $string = explode('.', $string, 2); - $tableName = DB::quoteCol($string[0]); - $fieldName = DB::quoteCol($string[1]); - return $tableName . '.' . $fieldName; - } - return DB::quoteCol($string); - } - - private function parseWhere(array $array) { - $cols = array_keys($array); - $values = array_values($array); - - $signs = array(); - foreach ($cols as $i => $col_condition) { - $col_condition = preg_replace('/\s+/', ' ', trim($col_condition)); - $parts = array_filter(explode(' ', $col_condition)); - if (count($parts) === 1) { - $signs[$i] = '='; - } else { - $signs[$i] = $parts[1]; - } - $cols[$i] = $parts[0]; - } - - $clauses = array(); //array_map(array('DB', 'pCol'), $cols); - $params = array(); - foreach ($cols as $i => $col) { - $sign = !empty($signs[$i]) ? $signs[$i] : '='; - $param = $col; - if (strstr($col, '.') !== false) { - list($c, $param) = explode('.', $col, 2); - } - $col = $this->parseColName($col); - $clauses[] = "$col $sign :$param"; - $params[DB::pkey($param)] = $values[$i]; - } - - return array( - 'clauses' => $clauses, - 'params' => $params, - ); - } + /** + * @var PDO + */ + protected $PDO; + + /** + * Constructed SQL statement + * + * @var string + */ + protected $query; + protected $where = array(); + protected $or_where = array(); + protected $joins = array(); + protected $columns = array('*'); + protected $params = array(); + protected $order = array(); + protected $limit; + protected $offset; + protected $table; + + public function __construct(PDO $pdo, array $cols = array()) { + $this->PDO = $pdo; + $this->columns($cols); + } + + public function __destruct() { + $this->PDO = null; + } + + public function columns(array $cols) { + if ($cols) { + $this->columns = $this->parseCols($cols); + } + } + + public function from($table) { + $this->table = DB::quoteCol($table); + return $this; + } + + public function leftJoin($table, $condition) { + $table = DB::quoteCol($table); + $condition = $this->parseJoinConditions(func_get_args()); + $this->joins[] = " LEFT JOIN $table ON ($condition)"; + return $this; + } + + public function rightJoin($table, $condition) { + $table = DB::quoteCol($table); + $condition = $this->parseJoinConditions(func_get_args()); + $this->joins[] = " RIGHT JOIN $table ON ($condition)"; + return $this; + } + + public function join($table, $condition) { + $table = DB::quoteCol($table); + $condition = $this->parseJoinConditions(func_get_args()); + $this->joins[] = " INNER JOIN $table ON ($condition)"; + return $this; + } + + public function where($where) { + if (is_array($where)) { + $whereParsed = $this->parseWhere($where); + $this->where = array_merge($this->where, $whereParsed['clauses']); + $this->params = array_merge($this->params, $whereParsed['params']); + } elseif (is_string($where)) { + $this->where[] = $where; + } + return $this; + } + + public function whereIn($field, array $values) { + $field = $this->parseColName($field); + $values = array_map(array($this->PDO, 'quote'), $values); + $this->where[] = "{$field} IN (" . implode(',', $values) . ")"; + } + + public function like($colname, $value, $pad = 'both') { + $colname = $this->parseColName($colname); + if ($pad === 'right') { + $value = "$value%"; + } elseif ($pad === 'left') { + $value = "%$value"; + } else { + $value = "%$value%"; + } + $this->PDO->quote($value); + $this->where("$colname LIKE '$value'"); + return $this; + } + + public function limit($limit, $offset = 0) { + $this->limit = (int) $limit; + $this->offset = (int) $offset; + return $this; + } + + public function order($by, $order = 'asc') { + if ($by === 'RAND') { + $this->order[] = 'RAND()'; + return $this; + } + + if ($order === null) { + $this->order[] = $by; + return $this; + } + + $order = strtoupper($order); + if (!in_array($order, array('ASC', 'DESC'))) { + throw new Exception("Invalid Order"); + } + $by = $this->parseColName($by); + $this->order[] = "$by $order"; + return $this; + } + + /** + * {inherit PDO doc} + */ + public function fetchAll($fetch_style = PDO::FETCH_ASSOC) { + $this->constructQuery(); + $query = $this->trimQuery(); + $stmt = $this->PDO->prepare($query); + $stmt->execute($this->params); + return $stmt->fetchAll($fetch_style); + } + + /** + * {inherit PDO doc} + */ + public function fetch($fetch_style = PDO::FETCH_ASSOC) { + $this->constructQuery(); + $query = $this->trimQuery(); + $stmt = $this->PDO->prepare($query); + $stmt->execute($this->params); + return $stmt->fetch($fetch_style); + } + + /** + * {inherit PDO doc} + */ + public function fetchColumn() { + $this->constructQuery(); + $query = $this->trimQuery(); + $stmt = $this->PDO->prepare($query); + $stmt->execute($this->params); + return $stmt->fetchColumn(); + } + + /** + * Returns executed PDO statement of current query + * + * @return PDOStatement + */ + public function statement() { + $this->constructQuery(); + $query = $this->trimQuery(); + $stmt = $this->PDO->prepare($query); + if ($this->params) { + foreach ($this->params as $key => $value) { + $stmt->bindValue($key, $value); + } + } + $stmt->execute(); + return $stmt; + } + + public function getParams() { + return $this->params; + } + + public function setParams(array $params) { + $this->params = $params; + return $this; + } + + public function bindParams(array $params) { + $params = $this->parseWhere($params); + $this->params = array_merge($this->params, $params['params']); + return $this; + } + + public function lastQuery() { + $this->constructQuery(); + return $this->query; + } + + private function trimQuery() { + return str_replace("\n", "", $this->query); + } + + /** + * @todo Add or_where and or_like clauses + */ + private function constructQuery() { + $columns = implode(', ', $this->columns); + + $query = "SELECT $columns FROM {$this->table} \n"; + if ($this->joins) { + $query .= implode(" \n", $this->joins); + } + + if ($this->where) { + $where = implode(' AND ', $this->where); + $query .= " WHERE ($where)"; + } + + if ($this->order) { + $order = implode(', ', $this->order); + $query .= " \nORDER BY " . $order; + } + + if ($this->limit) { + $query .= " \nLIMIT {$this->offset}, {$this->limit}"; + } + $this->query = $query; + } + + private function parseJoinConditions($conditions) { + array_shift($conditions); // first arguement is the table name + $parsed = array(); + foreach ($conditions as $condition) { + $parsed[] = $this->parseJoinCondition($condition); + } + return implode(' AND ', $parsed); + } + + private function parseJoinCondition($condition) { + $conditions = explode('=', $condition, 2); + if (count($conditions) != 2) { + throw new Exception("Unable to get join condition clauses"); + } + $conditions = $this->parseCols($conditions); + return implode(' = ', $conditions); + } + + private function parseCols(array $cols) { + $select = array(); + foreach ($cols as $key => $val) { + if (is_numeric($key)) { + $select[] = $this->parseColName($val); + } else { + $select[] = $this->parseColName($key) . ' AS ' . $this->parseColName($val); + } + } + return $select; + } + + private function parseColName($string) { + $string = trim($string); + // If the column is not one of these then maybe some mysql func or so is called + if (preg_match('/[^a-zA-Z0-9_\.\`]/i', $string, $matches)) { + return $string; + } + + $string = trim($string, '`'); + + if (strpos($string, '.') !== false) { + $string = explode('.', $string, 2); + $tableName = DB::quoteCol($string[0]); + $fieldName = DB::quoteCol($string[1]); + return $tableName . '.' . $fieldName; + } + return DB::quoteCol($string); + } + + private function parseWhere(array $array) { + $cols = array_keys($array); + $values = array_values($array); + + $signs = array(); + foreach ($cols as $i => $col_condition) { + $col_condition = preg_replace('/\s+/', ' ', trim($col_condition)); + $parts = array_filter(explode(' ', $col_condition)); + if (count($parts) === 1) { + $signs[$i] = '='; + } else { + $signs[$i] = $parts[1]; + } + $cols[$i] = $parts[0]; + } + + $clauses = array(); //array_map(array('DB', 'pCol'), $cols); + $params = array(); + foreach ($cols as $i => $col) { + $sign = !empty($signs[$i]) ? $signs[$i] : '='; + $param = $col; + if (strstr($col, '.') !== false) { + list($c, $param) = explode('.', $col, 2); + } + $col = $this->parseColName($col); + $clauses[] = "$col $sign :$param"; + $params[DB::pkey($param)] = $values[$i]; + } + + return array( + 'clauses' => $clauses, + 'params' => $params, + ); + } } diff --git a/application/Library/Deamon.php b/application/Library/Deamon.php index 0ccce643f..fe82b47aa 100644 --- a/application/Library/Deamon.php +++ b/application/Library/Deamon.php @@ -4,229 +4,225 @@ * Deamon class that processes runs and run-sessions in background * */ - - class Deamon { - /** - * Database handle - * - * @var DB - */ - protected $db; - - /** - * - * @var string - */ - protected $lockFile; - - /** - * Number of seconds to expire before run is fetched from DB for processing - * - * @var int - */ - protected $runExpireTime; - - /** - * Interval in seconds for which loop should be rested - * - * @var int - */ - protected $loopInterval; - - /** - * Flag used to exit or stay in run loop - * - * @var boolean - */ - protected $out = false; - - /** - * - * @var GearmanClient - */ - protected $gearmanClient = null; - - public static $dbg = false; - - public function __construct(DB $db) { - $this->db = $db; - $this->lockFile = APPLICATION_ROOT . 'tmp/deamon.lock'; - $this->runExpireTime = Config::get('deamon.run_expire_time', 10 * 60); - $this->loopInterval = Config::get('deamon.loop_interval', 60); - - // Register signal handlers that should be able to kill the cron in case some other weird shit happens - // apart from cron exiting cleanly - // declare signal handlers - if (extension_loaded('pcntl')) { - declare(ticks = 1); - - pcntl_signal(SIGINT, array(&$this, 'interrupt')); - pcntl_signal(SIGTERM, array(&$this, 'interrupt')); - pcntl_signal(SIGUSR1, array(&$this, 'interrupt')); - } else { - self::$dbg = true; - self::dbg('pcntl extension is not loaded'); - } - } - - public function run() { - $runExpireTime = $time = $id = 0; - $fetchStmt = $this->db->prepare('select id, name, last_deamon_access from survey_runs where last_deamon_access < :run_expiration_time and cron_active = 1 order by rand()'); - $fetchStmt->bindParam(':run_expiration_time', $runExpireTime, PDO::PARAM_INT); - - $updateStmt = $this->db->prepare('update survey_runs set last_deamon_access = :time where id = :id'); - $updateStmt->bindParam(':time', $time); - $updateStmt->bindParam(':id', $id); - - // loop forever until terminated by SIGINT - while (!$this->out) { - try { - file_put_contents($this->lockFile, date('r')); - // loop until terminated but with taking some nap - while (!$this->out && $this->rested()) { - - $runExpireTime = time() - $this->runExpireTime; - $fetchStmt->execute(); - $runs = $fetchStmt->fetchAll(PDO::FETCH_ASSOC); - $gearmanClient = $this->getGearmanClient(); - - if (!$runs) { - self::dbg('No runs to be processed at this time.. waiting to retry'); - sleep(10); - continue; - } - - foreach ($runs as $run) { - $time = time(); - $id = $run['id']; - self::dbg("Process run '%s'. Last access: %s", $run['name'], date('r', $run['last_deamon_access'])); - $updateStmt->execute(); - $gearmanClient->doHighBackground('process_run', json_encode($run), $run['name']); - } - - } - } catch (Exception $e) { - // if connection disappeared - try to restore it - $error_code = $e->getCode(); - if ($error_code != 1053 && $error_code != 2006 && $error_code != 2013 && $error_code != 2003) { - throw $e; - } - - self::dbg($e->getMessage() . "[" . $error_code . "]"); - - self::dbg("Unable to connect. waiting 5 seconds before reconnect."); - sleep(5); - } - } - - echo getmypid(), " Terminating...\n"; - } - - public function cronize() { - $time = $id = null; - $fetchStmt = $this->db->prepare('select id, name, last_deamon_access from survey_runs where cron_active = 1 order by rand()'); - $updateStmt = $this->db->prepare('update survey_runs set last_deamon_access = :time where id = :id'); - $updateStmt->bindParam(':time', $time); - $updateStmt->bindParam(':id', $id); - - $fetchStmt->execute(); - $runs = $fetchStmt->fetchAll(PDO::FETCH_ASSOC); - - if (!$runs) { - self::dbg('No runs to be processed at this time.. waiting to retry'); - return; - } - - $gearmanClient = $this->getGearmanClient(); - foreach ($runs as $run) { - $time = time(); - $id = $run['id']; - self::dbg("Process run [cr.d] '%s'. Last access: %s", $run['name'], date('r', $run['last_deamon_access'])); - $updateStmt->execute(); - $gearmanClient->doHighBackground('process_run', json_encode($run), $run['name']); - } - } - - /** - * Signal handler - * - * @param integer $signo - */ - public function interrupt($signo) { - switch ($signo) { - // Set terminated flag to be able to terminate program securely - // to prevent from terminating in the middle of the process - // Use Ctrl+C to send interruption signal to a running program - case SIGINT: - case SIGTERM: - $this->out = true; - self::dbg("%s Received termination signal", getmypid()); - $this->cleanup('SIGINT|SIGTERM'); - break; - - // switch the debug mode on/off - // @example: $ kill -s SIGUSR1 - case SIGUSR1: - if ((self::$dbg = !self::$dbg)) { - self::dbg("\nEntering debug mode...\n"); - } else { - self::dbg("\nLeaving debug mode...\n"); - } - $this->cleanup('SIGUSR1'); - break; - } - } - - protected function cleanup($interrupt = null) { - if (file_exists($this->lockFile)) { - unlink($this->lockFile); - self::dbg("Lockfile cleanup complete"); - } - } - - private function rested() { - static $last_access; - if (!is_null($last_access) && $this->loopInterval > ($usleep = (microtime(true) - $last_access))) { - usleep(1000000 * ($this->loopInterval - $usleep)); - } - - $last_access = microtime(true); - return true; - } - - /** - * - * @param boolean $refresh - * @return \MHlavac\Gearman\Client - */ - private function getGearmanClient($refresh = false) { - if ($this->gearmanClient === null || $refresh === true) { - $client = new GearmanClient(); - $servers = Config::get('deamon.gearman_servers'); - foreach ($servers as $server) { - list($host, $port) = explode(':', $server, 2); - $client->addServer($host, $port); - } - $this->gearmanClient = $client; - } - return $this->gearmanClient; - } - - /** - * Debug output - * - * @param string $str - */ - private static function dbg($str) { - $args = func_get_args(); - if (count($args) > 1) { - $str = vsprintf(array_shift($args), $args); - } - - $str = date('Y-m-d H:i:s') . ' ' . $str . PHP_EOL; - return error_log($str, 3, get_log_file('deamon.log')); - } + /** + * Database handle + * + * @var DB + */ + protected $db; + + /** + * + * @var string + */ + protected $lockFile; + + /** + * Number of seconds to expire before run is fetched from DB for processing + * + * @var int + */ + protected $runExpireTime; + + /** + * Interval in seconds for which loop should be rested + * + * @var int + */ + protected $loopInterval; + + /** + * Flag used to exit or stay in run loop + * + * @var boolean + */ + protected $out = false; + + /** + * + * @var GearmanClient + */ + protected $gearmanClient = null; + public static $dbg = false; + + public function __construct(DB $db) { + $this->db = $db; + $this->lockFile = APPLICATION_ROOT . 'tmp/deamon.lock'; + $this->runExpireTime = Config::get('deamon.run_expire_time', 10 * 60); + $this->loopInterval = Config::get('deamon.loop_interval', 60); + + // Register signal handlers that should be able to kill the cron in case some other weird shit happens + // apart from cron exiting cleanly + // declare signal handlers + if (extension_loaded('pcntl')) { + declare(ticks = 1); + + pcntl_signal(SIGINT, array(&$this, 'interrupt')); + pcntl_signal(SIGTERM, array(&$this, 'interrupt')); + pcntl_signal(SIGUSR1, array(&$this, 'interrupt')); + } else { + self::$dbg = true; + self::dbg('pcntl extension is not loaded'); + } + } + + public function run() { + $runExpireTime = $time = $id = 0; + $fetchStmt = $this->db->prepare('select id, name, last_deamon_access from survey_runs where last_deamon_access < :run_expiration_time and cron_active = 1 order by rand()'); + $fetchStmt->bindParam(':run_expiration_time', $runExpireTime, PDO::PARAM_INT); + + $updateStmt = $this->db->prepare('update survey_runs set last_deamon_access = :time where id = :id'); + $updateStmt->bindParam(':time', $time); + $updateStmt->bindParam(':id', $id); + + // loop forever until terminated by SIGINT + while (!$this->out) { + try { + file_put_contents($this->lockFile, date('r')); + // loop until terminated but with taking some nap + while (!$this->out && $this->rested()) { + + $runExpireTime = time() - $this->runExpireTime; + $fetchStmt->execute(); + $runs = $fetchStmt->fetchAll(PDO::FETCH_ASSOC); + $gearmanClient = $this->getGearmanClient(); + + if (!$runs) { + self::dbg('No runs to be processed at this time.. waiting to retry'); + sleep(10); + continue; + } + + foreach ($runs as $run) { + $time = time(); + $id = $run['id']; + self::dbg("Process run '%s'. Last access: %s", $run['name'], date('r', $run['last_deamon_access'])); + $updateStmt->execute(); + $gearmanClient->doHighBackground('process_run', json_encode($run), $run['name']); + } + } + } catch (Exception $e) { + // if connection disappeared - try to restore it + $error_code = $e->getCode(); + if ($error_code != 1053 && $error_code != 2006 && $error_code != 2013 && $error_code != 2003) { + throw $e; + } + + self::dbg($e->getMessage() . "[" . $error_code . "]"); + + self::dbg("Unable to connect. waiting 5 seconds before reconnect."); + sleep(5); + } + } + + echo getmypid(), " Terminating...\n"; + } + + public function cronize() { + $time = $id = null; + $fetchStmt = $this->db->prepare('select id, name, last_deamon_access from survey_runs where cron_active = 1 order by rand()'); + $updateStmt = $this->db->prepare('update survey_runs set last_deamon_access = :time where id = :id'); + $updateStmt->bindParam(':time', $time); + $updateStmt->bindParam(':id', $id); + + $fetchStmt->execute(); + $runs = $fetchStmt->fetchAll(PDO::FETCH_ASSOC); + + if (!$runs) { + self::dbg('No runs to be processed at this time.. waiting to retry'); + return; + } + + $gearmanClient = $this->getGearmanClient(); + foreach ($runs as $run) { + $time = time(); + $id = $run['id']; + self::dbg("Process run [cr.d] '%s'. Last access: %s", $run['name'], date('r', $run['last_deamon_access'])); + $updateStmt->execute(); + $gearmanClient->doHighBackground('process_run', json_encode($run), $run['name']); + } + } + + /** + * Signal handler + * + * @param integer $signo + */ + public function interrupt($signo) { + switch ($signo) { + // Set terminated flag to be able to terminate program securely + // to prevent from terminating in the middle of the process + // Use Ctrl+C to send interruption signal to a running program + case SIGINT: + case SIGTERM: + $this->out = true; + self::dbg("%s Received termination signal", getmypid()); + $this->cleanup('SIGINT|SIGTERM'); + break; + + // switch the debug mode on/off + // @example: $ kill -s SIGUSR1 + case SIGUSR1: + if ((self::$dbg = !self::$dbg)) { + self::dbg("\nEntering debug mode...\n"); + } else { + self::dbg("\nLeaving debug mode...\n"); + } + $this->cleanup('SIGUSR1'); + break; + } + } + + protected function cleanup($interrupt = null) { + if (file_exists($this->lockFile)) { + unlink($this->lockFile); + self::dbg("Lockfile cleanup complete"); + } + } + + private function rested() { + static $last_access; + if (!is_null($last_access) && $this->loopInterval > ($usleep = (microtime(true) - $last_access))) { + usleep(1000000 * ($this->loopInterval - $usleep)); + } + + $last_access = microtime(true); + return true; + } + + /** + * + * @param boolean $refresh + * @return \MHlavac\Gearman\Client + */ + private function getGearmanClient($refresh = false) { + if ($this->gearmanClient === null || $refresh === true) { + $client = new GearmanClient(); + $servers = Config::get('deamon.gearman_servers'); + foreach ($servers as $server) { + list($host, $port) = explode(':', $server, 2); + $client->addServer($host, $port); + } + $this->gearmanClient = $client; + } + return $this->gearmanClient; + } + + /** + * Debug output + * + * @param string $str + */ + private static function dbg($str) { + $args = func_get_args(); + if (count($args) > 1) { + $str = vsprintf(array_shift($args), $args); + } + + $str = date('Y-m-d H:i:s') . ' ' . $str . PHP_EOL; + return error_log($str, 3, get_log_file('deamon.log')); + } } diff --git a/application/Library/EmailQueue.php b/application/Library/EmailQueue.php index 1ae743f8a..27954d012 100644 --- a/application/Library/EmailQueue.php +++ b/application/Library/EmailQueue.php @@ -5,275 +5,272 @@ * Process emails in email_queue * */ - class EmailQueue extends Queue { - /** - * - * @var PHPMailer[] - */ - protected $connections = array(); - - /** - * - * @var int[] - */ - protected $failures = array(); - - /** - * Number of seconds item should last in queue - * - * @var int - */ - protected $itemTtl; + /** + * + * @var PHPMailer[] + */ + protected $connections = array(); - /** - * Number of times the queue can re-try to send the email in case of failures - * @var int - */ - protected $itemTries; + /** + * + * @var int[] + */ + protected $failures = array(); - protected $logFile = 'email-queue.log'; + /** + * Number of seconds item should last in queue + * + * @var int + */ + protected $itemTtl; - protected $name = 'Email-Queue'; + /** + * Number of times the queue can re-try to send the email in case of failures + * @var int + */ + protected $itemTries; + protected $logFile = 'email-queue.log'; + protected $name = 'Email-Queue'; - public function __construct(DB $db, array $config) { - parent::__construct($db, $config); - $this->itemTtl = array_val($this->config, 'queue_item_ttl', 20 * 60); - $this->itemTries = array_val($this->config, 'queue_item_tries', 4); - } + public function __construct(DB $db, array $config) { + parent::__construct($db, $config); + $this->itemTtl = array_val($this->config, 'queue_item_ttl', 20 * 60); + $this->itemTries = array_val($this->config, 'queue_item_tries', 4); + } - /** - * - * @return PDOStatement - */ - protected function getEmailAccountsStatement($account_id) { - $WHERE = ''; - if ($account_id) { - $WHERE .= 'account_id = ' . (int) $account_id; - } + /** + * + * @return PDOStatement + */ + protected function getEmailAccountsStatement($account_id) { + $WHERE = ''; + if ($account_id) { + $WHERE .= 'account_id = ' . (int) $account_id; + } - $query = "SELECT account_id, `from`, from_name, host, port, tls, username, password, auth_key + $query = "SELECT account_id, `from`, from_name, host, port, tls, username, password, auth_key FROM survey_email_queue LEFT JOIN survey_email_accounts ON survey_email_accounts.id = survey_email_queue.account_id {$WHERE} GROUP BY account_id ORDER BY RAND() "; - return $this->db->rquery($query); - } + return $this->db->rquery($query); + } - /** - * - * @param int $account_id - * @return PDOStatement - */ - protected function getEmailsStatement($account_id) { - $query = 'SELECT id, subject, message, recipient, created, meta FROM survey_email_queue WHERE account_id = ' . (int) $account_id; - return $this->db->rquery($query); - } + /** + * + * @param int $account_id + * @return PDOStatement + */ + protected function getEmailsStatement($account_id) { + $query = 'SELECT id, subject, message, recipient, created, meta FROM survey_email_queue WHERE account_id = ' . (int) $account_id; + return $this->db->rquery($query); + } - /** - * - * @param array $account - * @return PHPMailer - */ - protected function getSMTPConnection($account) { - $account_id = $account['account_id']; - if (!isset($this->connections[$account_id])) { - $mail = new PHPMailer(); - $mail->SetLanguage("de", "/"); + /** + * + * @param array $account + * @return PHPMailer + */ + protected function getSMTPConnection($account) { + $account_id = $account['account_id']; + if (!isset($this->connections[$account_id])) { + $mail = new PHPMailer(); + $mail->SetLanguage("de", "/"); - $mail->isSMTP(); - $mail->SMTPAuth = true; - $mail->SMTPKeepAlive = true; - $mail->Mailer = "smtp"; - $mail->Host = $account['host']; - $mail->Port = $account['port']; - if ($account['tls']) { - $mail->SMTPSecure = 'tls'; - } else { - $mail->SMTPSecure = 'ssl'; - } - if (isset($account['username'])) { - $mail->Username = $account['username']; - $mail->Password = $account['password']; - } else { - $mail->SMTPAuth = false; - $mail->SMTPSecure = false; - } - $mail->setFrom($account['from'], $account['from_name']); - $mail->AddReplyTo($account['from'], $account['from_name']); - $mail->CharSet = "utf-8"; - $mail->WordWrap = 65; - $mail->AllowEmpty = true; - if (is_array(Config::get('email.smtp_options'))) { - $mail->SMTPOptions = array_merge($mail->SMTPOptions, Config::get('email.smtp_options')); - } + $mail->isSMTP(); + $mail->SMTPAuth = true; + $mail->SMTPKeepAlive = true; + $mail->Mailer = "smtp"; + $mail->Host = $account['host']; + $mail->Port = $account['port']; + if ($account['tls']) { + $mail->SMTPSecure = 'tls'; + } else { + $mail->SMTPSecure = 'ssl'; + } + if (isset($account['username'])) { + $mail->Username = $account['username']; + $mail->Password = $account['password']; + } else { + $mail->SMTPAuth = false; + $mail->SMTPSecure = false; + } + $mail->setFrom($account['from'], $account['from_name']); + $mail->AddReplyTo($account['from'], $account['from_name']); + $mail->CharSet = "utf-8"; + $mail->WordWrap = 65; + $mail->AllowEmpty = true; + if (is_array(Config::get('email.smtp_options'))) { + $mail->SMTPOptions = array_merge($mail->SMTPOptions, Config::get('email.smtp_options')); + } - $this->connections[$account_id] = $mail; - } - return $this->connections[$account_id]; - } + $this->connections[$account_id] = $mail; + } + return $this->connections[$account_id]; + } - protected function closeSMTPConnection($account_id) { - if (isset($this->connections[$account_id])) { - unset($this->connections[$account_id]); - } - } + protected function closeSMTPConnection($account_id) { + if (isset($this->connections[$account_id])) { + unset($this->connections[$account_id]); + } + } - protected function processQueue($account_id = null) { - $emailAccountsStatement = $this->getEmailAccountsStatement($account_id); - if ($emailAccountsStatement->rowCount() <= 0) { - $emailAccountsStatement->closeCursor(); - return false; - } + protected function processQueue($account_id = null) { + $emailAccountsStatement = $this->getEmailAccountsStatement($account_id); + if ($emailAccountsStatement->rowCount() <= 0) { + $emailAccountsStatement->closeCursor(); + return false; + } - while ($account = $emailAccountsStatement->fetch(PDO::FETCH_ASSOC)) { - if (!filter_var($account['from'], FILTER_VALIDATE_EMAIL)) { - $this->db->exec('DELETE FROM survey_email_queue WHERE account_id = ' . (int) $account['account_id']); - continue; - } + while ($account = $emailAccountsStatement->fetch(PDO::FETCH_ASSOC)) { + if (!filter_var($account['from'], FILTER_VALIDATE_EMAIL)) { + $this->db->exec('DELETE FROM survey_email_queue WHERE account_id = ' . (int) $account['account_id']); + continue; + } - list($username, $password) = explode(EmailAccount::AK_GLUE, Crypto::decrypt($account['auth_key']), 2); - $account['username'] = $username; - $account['password'] = $password; + list($username, $password) = explode(EmailAccount::AK_GLUE, Crypto::decrypt($account['auth_key']), 2); + $account['username'] = $username; + $account['password'] = $password; - $mailer = $this->getSMTPConnection($account); - $emailsStatement = $this->getEmailsStatement($account['account_id']); - while ($email = $emailsStatement->fetch(PDO::FETCH_ASSOC)) { - if (!filter_var($email['recipient'], FILTER_VALIDATE_EMAIL) || !$email['subject']) { - $this->registerFailure($email); - continue; - } + $mailer = $this->getSMTPConnection($account); + $emailsStatement = $this->getEmailsStatement($account['account_id']); + while ($email = $emailsStatement->fetch(PDO::FETCH_ASSOC)) { + if (!filter_var($email['recipient'], FILTER_VALIDATE_EMAIL) || !$email['subject']) { + $this->registerFailure($email); + continue; + } - $meta = json_decode($email['meta'], true); - $debugInfo = json_encode(array('id' => $email['id'], 's' => $email['subject'], 'r' => $email['recipient'], 'f' => $account['from'])); + $meta = json_decode($email['meta'], true); + $debugInfo = json_encode(array('id' => $email['id'], 's' => $email['subject'], 'r' => $email['recipient'], 'f' => $account['from'])); - $mailer->Subject = $email['subject']; - $mailer->msgHTML($email['message']); - $mailer->addAddress($email['recipient']); - $files = array(); - // add emdedded images - if (!empty($meta['embedded_images'])) { - foreach ($meta['embedded_images'] as $imageId => $image) { - $localImage = APPLICATION_ROOT . 'tmp/formrEA' . uniqid() . $imageId; - copy($image, $localImage); - $files[] = $localImage; - if (!$mailer->addEmbeddedImage($localImage, $imageId, $imageId, 'base64', 'image/png')) { - $this->dbg("Unable to attach image: " . $mailer->ErrorInfo . ".\n {$debugInfo}"); - } - } - } - // add attachments (attachments MUST be paths to local file - if (!empty($meta['attachments'])) { - foreach ($meta['attachments'] as $attachment) { - $files[] = $attachment; - if (!$mailer->addAttachment($attachment, basename($attachment))) { - $this->dbg("Unable to add attachment {$attachment} \n" . $mailer->ErrorInfo . ".\n {$debugInfo}"); - } - } - } + $mailer->Subject = $email['subject']; + $mailer->msgHTML($email['message']); + $mailer->addAddress($email['recipient']); + $files = array(); + // add emdedded images + if (!empty($meta['embedded_images'])) { + foreach ($meta['embedded_images'] as $imageId => $image) { + $localImage = APPLICATION_ROOT . 'tmp/formrEA' . uniqid() . $imageId; + copy($image, $localImage); + $files[] = $localImage; + if (!$mailer->addEmbeddedImage($localImage, $imageId, $imageId, 'base64', 'image/png')) { + $this->dbg("Unable to attach image: " . $mailer->ErrorInfo . ".\n {$debugInfo}"); + } + } + } + // add attachments (attachments MUST be paths to local file + if (!empty($meta['attachments'])) { + foreach ($meta['attachments'] as $attachment) { + $files[] = $attachment; + if (!$mailer->addAttachment($attachment, basename($attachment))) { + $this->dbg("Unable to add attachment {$attachment} \n" . $mailer->ErrorInfo . ".\n {$debugInfo}"); + } + } + } - // Send mail - try { - if (($sent = $mailer->send())) { - $this->db->exec("DELETE FROM survey_email_queue WHERE id = " . (int) $email['id']); - $query = "INSERT INTO `survey_email_log` (session_id, email_id, created, recipient, sent) VALUES (:session_id, :email_id, NOW(), :recipient, :sent)"; - $this->db->exec($query, array( - 'session_id' => $meta['session_id'], - 'email_id' => $meta['email_id'], - 'recipient' => $email['recipient'], - 'sent' => (int) $sent, - )); - $this->dbg("Send Success. \n {$debugInfo}"); - } else { - throw new Exception($mailer->ErrorInfo); - } - } catch (Exception $e) { - //formr_log_exception($e, 'EmailQueue ' . $debugInfo); - $this->dbg("Send Failure: " . $mailer->ErrorInfo . ".\n {$debugInfo}"); - $this->registerFailure($email); - // reset php mailer object for this account if smtp sending failed. Probably some limits have been hit - $this->closeSMTPConnection($account['account_id']); - $mailer = $this->getSMTPConnection($account); - } + // Send mail + try { + if (($sent = $mailer->send())) { + $this->db->exec("DELETE FROM survey_email_queue WHERE id = " . (int) $email['id']); + $query = "INSERT INTO `survey_email_log` (session_id, email_id, created, recipient, sent) VALUES (:session_id, :email_id, NOW(), :recipient, :sent)"; + $this->db->exec($query, array( + 'session_id' => $meta['session_id'], + 'email_id' => $meta['email_id'], + 'recipient' => $email['recipient'], + 'sent' => (int) $sent, + )); + $this->dbg("Send Success. \n {$debugInfo}"); + } else { + throw new Exception($mailer->ErrorInfo); + } + } catch (Exception $e) { + //formr_log_exception($e, 'EmailQueue ' . $debugInfo); + $this->dbg("Send Failure: " . $mailer->ErrorInfo . ".\n {$debugInfo}"); + $this->registerFailure($email); + // reset php mailer object for this account if smtp sending failed. Probably some limits have been hit + $this->closeSMTPConnection($account['account_id']); + $mailer = $this->getSMTPConnection($account); + } - $mailer->clearAddresses(); - $mailer->clearAttachments(); - $mailer->clearAllRecipients(); - $this->clearFiles($files); - } - // close sql emails cursor after processing batch - $emailsStatement->closeCursor(); - // check if smtp connection is lost and kill object - if (!$mailer->getSMTPInstance()->connected()) { - $this->closeSMTPConnection($account['account_id']); - } - } - $emailAccountsStatement->closeCursor(); - return true; - } + $mailer->clearAddresses(); + $mailer->clearAttachments(); + $mailer->clearAllRecipients(); + $this->clearFiles($files); + } + // close sql emails cursor after processing batch + $emailsStatement->closeCursor(); + // check if smtp connection is lost and kill object + if (!$mailer->getSMTPInstance()->connected()) { + $this->closeSMTPConnection($account['account_id']); + } + } + $emailAccountsStatement->closeCursor(); + return true; + } - /** - * Register email send failure and/or remove expired emails - * - * @param array $email @array(id, subject, message, recipient, created, meta) - */ - protected function registerFailure($email) { - $id = $email['id']; - if (!isset($this->failures[$id])) { - $this->failures[$id] = 0; - } - $this->failures[$id]++; - if ($this->failures[$id] > $this->itemTries || (time() - strtotime($email['created'])) > $this->itemTtl) { - $this->db->exec('DELETE FROM survey_email_queue WHERE id = ' . (int) $id); - } - } + /** + * Register email send failure and/or remove expired emails + * + * @param array $email @array(id, subject, message, recipient, created, meta) + */ + protected function registerFailure($email) { + $id = $email['id']; + if (!isset($this->failures[$id])) { + $this->failures[$id] = 0; + } + $this->failures[$id] ++; + if ($this->failures[$id] > $this->itemTries || (time() - strtotime($email['created'])) > $this->itemTtl) { + $this->db->exec('DELETE FROM survey_email_queue WHERE id = ' . (int) $id); + } + } - protected function clearFiles($files) { - if (!empty($files)) { - foreach ($files as $file) { - if (is_file($file)) { - @unlink($file); - } - } - } - } + protected function clearFiles($files) { + if (!empty($files)) { + foreach ($files as $file) { + if (is_file($file)) { + @unlink($file); + } + } + } + } - public function run() { - $account_id = array_val($this->config, 'account_id', null); + public function run() { + $account_id = array_val($this->config, 'account_id', null); - // loop forever until terminated by SIGINT - while (!$this->out) { - try { - // loop until terminated but with taking some nap - $sleeps = 0; - while (!$this->out && $this->rested()) { - if ($this->processQueue($account_id) === false) { - // if there is nothing to process in the queue sleep for sometime - $this->dbg("Sleeping because nothing was found in queue"); - sleep($this->sleep); - $sleeps++; - } - if ($sleeps > $this->allowedSleeps) { - // exit to restart supervisor process - $this->out = true; - } - } - } catch (Exception $e) { - // if connection disappeared - try to restore it - $error_code = $e->getCode(); - if ($error_code != 1053 && $error_code != 2006 && $error_code != 2013 && $error_code != 2003) { - throw $e; - } + // loop forever until terminated by SIGINT + while (!$this->out) { + try { + // loop until terminated but with taking some nap + $sleeps = 0; + while (!$this->out && $this->rested()) { + if ($this->processQueue($account_id) === false) { + // if there is nothing to process in the queue sleep for sometime + $this->dbg("Sleeping because nothing was found in queue"); + sleep($this->sleep); + $sleeps++; + } + if ($sleeps > $this->allowedSleeps) { + // exit to restart supervisor process + $this->out = true; + } + } + } catch (Exception $e) { + // if connection disappeared - try to restore it + $error_code = $e->getCode(); + if ($error_code != 1053 && $error_code != 2006 && $error_code != 2013 && $error_code != 2003) { + throw $e; + } - $this->dbg($e->getMessage() . "[" . $error_code . "]"); + $this->dbg($e->getMessage() . "[" . $error_code . "]"); - $this->dbg("Unable to connect. waiting 5 seconds before reconnect."); - sleep(5); - } - } - } + $this->dbg("Unable to connect. waiting 5 seconds before reconnect."); + sleep(5); + } + } + } } diff --git a/application/Library/Functions.php b/application/Library/Functions.php index ea60a2cb1..e335056c8 100644 --- a/application/Library/Functions.php +++ b/application/Library/Functions.php @@ -5,307 +5,307 @@ */ function formr_log($msg, $type = '') {// shorthand - $msg = print_r($msg, true); - $msg = date('Y-m-d H:i:s') . ' ' . $msg; - if ($type) { - $msg = "[$type] $msg"; - } -/* - if (DEBUG) { - alert('
' . $msg . '
', 'alert-danger'); - } -*/ - error_log($msg . "\n", 3, get_log_file('errors.log')); + $msg = print_r($msg, true); + $msg = date('Y-m-d H:i:s') . ' ' . $msg; + if ($type) { + $msg = "[$type] $msg"; + } + /* + if (DEBUG) { + alert('
' . $msg . '
', 'alert-danger'); + } + */ + error_log($msg . "\n", 3, get_log_file('errors.log')); } function formr_log_exception(Exception $e, $prefix = '', $debug_data = null) { - $msg = $prefix . ' Exception: ' . $e->getMessage() . "\n" . $e->getTraceAsString(); - formr_log($msg); - if ($debug_data !== null) { - formr_log('Debug Data: ' . print_r($debug_data, 1)); - } + $msg = $prefix . ' Exception: ' . $e->getMessage() . "\n" . $e->getTraceAsString(); + formr_log($msg); + if ($debug_data !== null) { + formr_log('Debug Data: ' . print_r($debug_data, 1)); + } } function get_log_file($filename) { - return APPLICATION_ROOT . "tmp/logs/$filename"; + return APPLICATION_ROOT . "tmp/logs/$filename"; } function alert($msg, $class = 'alert-warning', $dismissable = true) { // shorthand - global $site; - if (!is_object($site)) { - $site = Site::getInstance(); - } - $site->alert($msg, $class, $dismissable); + global $site; + if (!is_object($site)) { + $site = Site::getInstance(); + } + $site->alert($msg, $class, $dismissable); } function notify_user_error($error, $public_message = '') { - $run_session = Site::getInstance()->getRunSession(); - $date = date('Y-m-d H:i:s'); + $run_session = Site::getInstance()->getRunSession(); + $date = date('Y-m-d H:i:s'); - $message = $date . ': ' . $public_message . "
"; + $message = $date . ': ' . $public_message . "
"; - if (DEBUG || ($run_session && !$run_session->isCron() && $run_session->isTesting()) ) { - if ($error instanceof Exception) { - $message .= $error->getMessage(); - } else { - $message .= $error; - } - } - alert($message, 'alert-danger'); + if (DEBUG || ($run_session && !$run_session->isCron() && $run_session->isTesting())) { + if ($error instanceof Exception) { + $message .= $error->getMessage(); + } else { + $message .= $error; + } + } + alert($message, 'alert-danger'); } function print_hidden_opencpu_debug_message($ocpu_req, $public_message = '') { - $run_session = Site::getInstance()->getRunSession(); - if (DEBUG || ($run_session && !$run_session->isCron() && $run_session->isTesting()) ) { - $date = date('Y-m-d H:i:s'); + $run_session = Site::getInstance()->getRunSession(); + if (DEBUG || ($run_session && !$run_session->isCron() && $run_session->isTesting())) { + $date = date('Y-m-d H:i:s'); - $message = $date . ': ' . $public_message . "
"; + $message = $date . ': ' . $public_message . "
"; - $message .= opencpu_debug($ocpu_req); - alert($message, 'alert-info hidden_debug_message hidden'); - } + $message .= opencpu_debug($ocpu_req); + alert($message, 'alert-info hidden_debug_message hidden'); + } } function redirect_to($location = '', $params = array()) { - $location = str_replace(PHP_EOL, '', $location); - if (strpos($location, 'index') !== false) { - $location = ''; - } - - if (mb_substr($location, 0, 4) != 'http') { - $base = WEBROOT; - if (mb_substr($location, 0, 1) == '/') { - $location = $base . mb_substr($location, 1); - } else { - $location = $base . $location; - } - } - if ($params) { - $location .= '?' . http_build_query($params); - } - - Session::globalRefresh(); - Session::over(); - header("Location: $location"); - exit; + $location = str_replace(PHP_EOL, '', $location); + if (strpos($location, 'index') !== false) { + $location = ''; + } + + if (mb_substr($location, 0, 4) != 'http') { + $base = WEBROOT; + if (mb_substr($location, 0, 1) == '/') { + $location = $base . mb_substr($location, 1); + } else { + $location = $base . $location; + } + } + if ($params) { + $location .= '?' . http_build_query($params); + } + + Session::globalRefresh(); + Session::over(); + header("Location: $location"); + exit; } function session_over($site, $user) { - static $closed; - if ($closed) { - return false; - } -/* - $_SESSION['site'] = $site; - $_SESSION['user'] = serialize($user); -*/ - session_write_close(); - $closed = true; - return true; + static $closed; + if ($closed) { + return false; + } + /* + $_SESSION['site'] = $site; + $_SESSION['user'] = serialize($user); + */ + session_write_close(); + $closed = true; + return true; } function access_denied() { -/* - global $site, $user; - $_SESSION['site'] = $site; - $_SESSION['user'] = serialize($user); -*/ - redirect_to('error/403'); + /* + global $site, $user; + $_SESSION['site'] = $site; + $_SESSION['user'] = serialize($user); + */ + redirect_to('error/403'); } function not_found() { -/* - global $site, $user; - $_SESSION['site'] = $site; - $_SESSION['user'] = serialize($user); -*/ - redirect_to('error/404'); + /* + global $site, $user; + $_SESSION['site'] = $site; + $_SESSION['user'] = serialize($user); + */ + redirect_to('error/404'); } function bad_request() { -/* - global $site, $user; - $_SESSION['site'] = $site; - $_SESSION['user'] = serialize($user); -*/ - redirect_to('error/500'); + /* + global $site, $user; + $_SESSION['site'] = $site; + $_SESSION['user'] = serialize($user); + */ + redirect_to('error/500'); } function bad_request_header() { - header('HTTP/1.0 500 Bad Request'); + header('HTTP/1.0 500 Bad Request'); } function formr_error($code = 500, $title = 'Bad Request', $text = 'Request could not be processed', $hint = null, $link = null, $link_text = null) { - $code = $code ? $code : 500; - header("HTTP/1.0 {$code} {$title}"); - if ($link === null) { - $link = site_url(); - } - - if ($link_text === null) { - $link_text = 'Go to Site'; - } - - if (php_sapi_name() == 'cli') { - echo date('r') . " Error {$code}: {$text} \n"; - exit; - } - - Template::load('public/error', array( - 'code' => $code, - 'title' => $hint ? $hint : $title, - 'text' => $text, - 'link' => $link, - 'link_text' => $link_text, - )); - exit; + $code = $code ? $code : 500; + header("HTTP/1.0 {$code} {$title}"); + if ($link === null) { + $link = site_url(); + } + + if ($link_text === null) { + $link_text = 'Go to Site'; + } + + if (php_sapi_name() == 'cli') { + echo date('r') . " Error {$code}: {$text} \n"; + exit; + } + + Template::load('public/error', array( + 'code' => $code, + 'title' => $hint ? $hint : $title, + 'text' => $text, + 'link' => $link, + 'link_text' => $link_text, + )); + exit; } function json_header() { - header('Content-Type: application/json'); + header('Content-Type: application/json'); } function is_ajax_request() { - return strtolower(env('HTTP_X_REQUESTED_WITH')) === 'xmlhttprequest'; + return strtolower(env('HTTP_X_REQUESTED_WITH')) === 'xmlhttprequest'; } function h($text) { - return htmlspecialchars($text); + return htmlspecialchars($text); } function debug($string) { - if (DEBUG) { - echo "
";
-		print_r($string);
-		echo "
"; - } + if (DEBUG) { + echo "
";
+        print_r($string);
+        echo "
"; + } } function pr($string, $log = false) { - if (DEBUG > 0 && !$log) { - echo "
";
-		var_dump($string);
-		echo "
"; - } else { - formr_log(print_r($string, true)); - } + if (DEBUG > 0 && !$log) { + echo "
";
+        var_dump($string);
+        echo "
"; + } else { + formr_log(print_r($string, true)); + } } function prb($string = null) { - static $output = ""; - if ($string === null) { - if (DEBUG > 0) { - echo "
";
-			var_dump($string);
-			#		print_r(	debug_backtrace());
-			echo "
"; - } else { - formr_log($string); - } - } else { - $output .= "
" . $string; - } + static $output = ""; + if ($string === null) { + if (DEBUG > 0) { + echo "
";
+            var_dump($string);
+            #		print_r(	debug_backtrace());
+            echo "
"; + } else { + formr_log($string); + } + } else { + $output .= "
" . $string; + } } if (!function_exists('_')) { - function _($text) { - return $text; - } + function _($text) { + return $text; + } } function used_opencpu($echo = false) { - static $used; - if ($echo): - pr("Requests: " . $used); - return $used; - endif; - if (isset($used)) { - $used++; - } else { - $used = 1; - } - return $used; + static $used; + if ($echo): + pr("Requests: " . $used); + return $used; + endif; + if (isset($used)) { + $used++; + } else { + $used = 1; + } + return $used; } function used_cache($echo = false) { - static $used; - if ($echo): - pr("Hashcache: " . $used); - return $used; - endif; - if (isset($used)) { - $used++; - } else { - $used = 1; - } - return $used; + static $used; + if ($echo): + pr("Hashcache: " . $used); + return $used; + endif; + if (isset($used)) { + $used++; + } else { + $used = 1; + } + return $used; } function used_nginx_cache($echo = false) { - static $used; - if ($echo): - pr("Nginx: " . $used); - return $used; - endif; - if (isset($used)) { - $used++; - } else { - $used = 1; - } - return $used; + static $used; + if ($echo): + pr("Nginx: " . $used); + return $used; + endif; + if (isset($used)) { + $used++; + } else { + $used = 1; + } + return $used; } if (!function_exists('__')) { - /** - taken from cakePHP - */ - function __($singular, $args = null) { - if (!$singular) { - return; - } + /** + taken from cakePHP + */ + function __($singular, $args = null) { + if (!$singular) { + return; + } - $translated = _($singular); - if ($args === null) { - return $translated; - } elseif (!is_array($args)) { - $args = array_slice(func_get_args(), 1); - } - return vsprintf($translated, $args); - } + $translated = _($singular); + if ($args === null) { + return $translated; + } elseif (!is_array($args)) { + $args = array_slice(func_get_args(), 1); + } + return vsprintf($translated, $args); + } } if (!function_exists('__n')) { - /** - taken from cakePHP - */ - function __n($singular, $plural, $count, $args = null) { - if (!$singular) { - return; - } + /** + taken from cakePHP + */ + function __n($singular, $plural, $count, $args = null) { + if (!$singular) { + return; + } - $translated = ngettext($singular, $plural, null, 6, $count); - if ($args === null) { - return $translated; - } elseif (!is_array($args)) { - $args = array_slice(func_get_args(), 3); - } - return vsprintf($translated, $args); - } + $translated = ngettext($singular, $plural, null, 6, $count); + if ($args === null) { + return $translated; + } elseif (!is_array($args)) { + $args = array_slice(func_get_args(), 3); + } + return vsprintf($translated, $args); + } } function endsWith($haystack, $needle) { - $length = strlen($needle); - if ($length == 0) { - return true; - } + $length = strlen($needle); + if ($length == 0) { + return true; + } - return (mb_substr($haystack, -$length) === $needle); + return (mb_substr($haystack, -$length) === $needle); } /** @@ -319,167 +319,167 @@ function endsWith($haystack, $needle) { * @link http://book.cakephp.org/2.0/en/core-libraries/global-constants-and-functions.html#env */ function env($key) { - if ($key === 'HTTPS') { - if (isset($_SERVER['HTTPS'])) { - return (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off'); - } - return (mb_strpos(env('SCRIPT_URI'), 'https://') === 0); - } - - if ($key === 'SCRIPT_NAME') { - if (env('CGI_MODE') && isset($_ENV['SCRIPT_URL'])) { - $key = 'SCRIPT_URL'; - } - } - - $val = null; - if (isset($_SERVER[$key])) { - $val = $_SERVER[$key]; - } elseif (isset($_ENV[$key])) { - $val = $_ENV[$key]; - } elseif (getenv($key) !== false) { - $val = getenv($key); - } - - if ($key === 'REMOTE_ADDR' && $val === env('SERVER_ADDR')) { - $addr = env('HTTP_PC_REMOTE_ADDR'); - if ($addr !== null) { - $val = $addr; - } - } - - if ($val !== null) { - return $val; - } - - switch ($key) { - case 'SCRIPT_FILENAME': - if (defined('SERVER_IIS') && SERVER_IIS === true) { - return str_replace('\\\\', '\\', env('PATH_TRANSLATED')); - } - break; - case 'DOCUMENT_ROOT': - $name = env('SCRIPT_NAME'); - $filename = env('SCRIPT_FILENAME'); - $offset = 0; - if (!mb_strpos($name, '.php')) { - $offset = 4; - } - return mb_substr($filename, 0, -(strlen($name) + $offset)); - case 'PHP_SELF': - return str_replace(env('DOCUMENT_ROOT'), '', env('SCRIPT_FILENAME')); - case 'CGI_MODE': - return (PHP_SAPI === 'cgi'); - case 'HTTP_BASE': - $host = env('HTTP_HOST'); - $parts = explode('.', $host); - $count = count($parts); - - if ($count === 1) { - return '.' . $host; - } elseif ($count === 2) { - return '.' . $host; - } elseif ($count === 3) { - $gTLD = array( - 'aero', - 'asia', - 'biz', - 'cat', - 'com', - 'coop', - 'edu', - 'gov', - 'info', - 'int', - 'jobs', - 'mil', - 'mobi', - 'museum', - 'name', - 'net', - 'org', - 'pro', - 'tel', - 'travel', - 'xxx' - ); - if (in_array($parts[1], $gTLD)) { - return '.' . $host; - } - } - array_shift($parts); - return '.' . implode('.', $parts); - } - return null; + if ($key === 'HTTPS') { + if (isset($_SERVER['HTTPS'])) { + return (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off'); + } + return (mb_strpos(env('SCRIPT_URI'), 'https://') === 0); + } + + if ($key === 'SCRIPT_NAME') { + if (env('CGI_MODE') && isset($_ENV['SCRIPT_URL'])) { + $key = 'SCRIPT_URL'; + } + } + + $val = null; + if (isset($_SERVER[$key])) { + $val = $_SERVER[$key]; + } elseif (isset($_ENV[$key])) { + $val = $_ENV[$key]; + } elseif (getenv($key) !== false) { + $val = getenv($key); + } + + if ($key === 'REMOTE_ADDR' && $val === env('SERVER_ADDR')) { + $addr = env('HTTP_PC_REMOTE_ADDR'); + if ($addr !== null) { + $val = $addr; + } + } + + if ($val !== null) { + return $val; + } + + switch ($key) { + case 'SCRIPT_FILENAME': + if (defined('SERVER_IIS') && SERVER_IIS === true) { + return str_replace('\\\\', '\\', env('PATH_TRANSLATED')); + } + break; + case 'DOCUMENT_ROOT': + $name = env('SCRIPT_NAME'); + $filename = env('SCRIPT_FILENAME'); + $offset = 0; + if (!mb_strpos($name, '.php')) { + $offset = 4; + } + return mb_substr($filename, 0, -(strlen($name) + $offset)); + case 'PHP_SELF': + return str_replace(env('DOCUMENT_ROOT'), '', env('SCRIPT_FILENAME')); + case 'CGI_MODE': + return (PHP_SAPI === 'cgi'); + case 'HTTP_BASE': + $host = env('HTTP_HOST'); + $parts = explode('.', $host); + $count = count($parts); + + if ($count === 1) { + return '.' . $host; + } elseif ($count === 2) { + return '.' . $host; + } elseif ($count === 3) { + $gTLD = array( + 'aero', + 'asia', + 'biz', + 'cat', + 'com', + 'coop', + 'edu', + 'gov', + 'info', + 'int', + 'jobs', + 'mil', + 'mobi', + 'museum', + 'name', + 'net', + 'org', + 'pro', + 'tel', + 'travel', + 'xxx' + ); + if (in_array($parts[1], $gTLD)) { + return '.' . $host; + } + } + array_shift($parts); + return '.' . implode('.', $parts); + } + return null; } function emptyNull(&$x) { - $x = ($x == '') ? null : $x; + $x = ($x == '') ? null : $x; } function stringBool($x) { - if ($x === false) { - return 'false'; - } elseif ($x === true) { - return 'true'; - } elseif ($x === null) { - return 'null'; - } elseif ($x === 0) { - return '0'; - } elseif(is_array($x) AND empty($x)) { - return "NA"; - } + if ($x === false) { + return 'false'; + } elseif ($x === true) { + return 'true'; + } elseif ($x === null) { + return 'null'; + } elseif ($x === 0) { + return '0'; + } elseif (is_array($x) AND empty($x)) { + return "NA"; + } - return $x; + return $x; } function hardTrueFalse($x) { - if ($x === false) { - return 'FALSE'; - } elseif ($x === true) { - return 'TRUE'; + if ($x === false) { + return 'FALSE'; + } elseif ($x === true) { + return 'TRUE'; # elseif($x===null) return 'NULL'; - } elseif ($x === 0) { - return '0'; - } + } elseif ($x === 0) { + return '0'; + } - return $x; + return $x; } if (!function_exists('http_parse_headers')) { - function http_parse_headers($raw_headers) { - $headers = array(); - $key = ''; // [+] - - foreach (explode("\n", $raw_headers) as $i => $h) { - $h = explode(':', $h, 2); - - if (isset($h[1])) { - if (!isset($headers[$h[0]])) { - $headers[$h[0]] = trim($h[1]); - } elseif (is_array($headers[$h[0]])) { - // $tmp = array_merge($headers[$h[0]], array(trim($h[1]))); // [-] - // $headers[$h[0]] = $tmp; // [-] - $headers[$h[0]] = array_merge($headers[$h[0]], array(trim($h[1]))); // [+] - } else { - // $tmp = array_merge(array($headers[$h[0]]), array(trim($h[1]))); // [-] - // $headers[$h[0]] = $tmp; // [-] - $headers[$h[0]] = array_merge(array($headers[$h[0]]), array(trim($h[1]))); // [+] - } - - $key = $h[0]; // [+] - } else { // [+] // [+] - if (mb_substr($h[0], 0, 1) == "\t") { // [+] - $headers[$key] .= "\r\n\t" . trim($h[0]); // [+] - } elseif (!$key) { // [+] - $headers[0] = trim($h[0]); // [+] - } - } // [+] - } - - return $headers; - } + function http_parse_headers($raw_headers) { + $headers = array(); + $key = ''; // [+] + + foreach (explode("\n", $raw_headers) as $i => $h) { + $h = explode(':', $h, 2); + + if (isset($h[1])) { + if (!isset($headers[$h[0]])) { + $headers[$h[0]] = trim($h[1]); + } elseif (is_array($headers[$h[0]])) { + // $tmp = array_merge($headers[$h[0]], array(trim($h[1]))); // [-] + // $headers[$h[0]] = $tmp; // [-] + $headers[$h[0]] = array_merge($headers[$h[0]], array(trim($h[1]))); // [+] + } else { + // $tmp = array_merge(array($headers[$h[0]]), array(trim($h[1]))); // [-] + // $headers[$h[0]] = $tmp; // [-] + $headers[$h[0]] = array_merge(array($headers[$h[0]]), array(trim($h[1]))); // [+] + } + + $key = $h[0]; // [+] + } else { // [+] // [+] + if (mb_substr($h[0], 0, 1) == "\t") { // [+] + $headers[$key] .= "\r\n\t" . trim($h[0]); // [+] + } elseif (!$key) { // [+] + $headers[0] = trim($h[0]); // [+] + } + } // [+] + } + + return $headers; + } } @@ -490,96 +490,96 @@ function http_parse_headers($raw_headers) { * @return string */ function timetostr($timestamp) { - if ($timestamp === false) { - return ""; - } - $age = time() - $timestamp; - - $future = ($age <= 0); - $age = abs($age); - - $age = (int) ($age / 60); // minutes ago - if ($age == 0) { - return $future ? "a moment" : "just now"; - } - - $scales = [ - ["minute", "minutes", 60], - ["hour", "hours", 24], - ["day", "days", 7], - ["week", "weeks", 4.348214286], // average with leap year every 4 years - ["month", "months", 12], - ["year", "years", 10], - ["decade", "decades", 10], - ["century", "centuries", 1000], - ["millenium", "millenia", PHP_INT_MAX] - ]; - - foreach ($scales as $scale) { - list($singular, $plural, $factor) = $scale; - if ($age == 0) { - return $future ? "less than 1 $singular" : "less than 1 $singular ago"; - } - if ($age == 1) { - return $future ? "1 $singular" : "1 $singular ago"; - } - if ($age < $factor) { - return $future ? "$age $plural" : "$age $plural ago"; - } - - $age = (int) ($age / $factor); - } + if ($timestamp === false) { + return ""; + } + $age = time() - $timestamp; + + $future = ($age <= 0); + $age = abs($age); + + $age = (int) ($age / 60); // minutes ago + if ($age == 0) { + return $future ? "a moment" : "just now"; + } + + $scales = [ + ["minute", "minutes", 60], + ["hour", "hours", 24], + ["day", "days", 7], + ["week", "weeks", 4.348214286], // average with leap year every 4 years + ["month", "months", 12], + ["year", "years", 10], + ["decade", "decades", 10], + ["century", "centuries", 1000], + ["millenium", "millenia", PHP_INT_MAX] + ]; + + foreach ($scales as $scale) { + list($singular, $plural, $factor) = $scale; + if ($age == 0) { + return $future ? "less than 1 $singular" : "less than 1 $singular ago"; + } + if ($age == 1) { + return $future ? "1 $singular" : "1 $singular ago"; + } + if ($age < $factor) { + return $future ? "$age $plural" : "$age $plural ago"; + } + + $age = (int) ($age / $factor); + } } // from http://de1.php.net/manual/en/function.filesize.php function human_filesize($bytes, $decimals = 2) { - $sz = 'BKMGTP'; - $factor = floor((strlen($bytes) - 1) / 3); - return sprintf("%.{$decimals}f", $bytes / pow(1024, $factor)) . @$sz[$factor]; + $sz = 'BKMGTP'; + $factor = floor((strlen($bytes) - 1) / 3); + return sprintf("%.{$decimals}f", $bytes / pow(1024, $factor)) . @$sz[$factor]; } function cr2nl($string) { - return str_replace("\r\n", "\n", $string); + return str_replace("\r\n", "\n", $string); } function time_point($line, $file) { - static $times, $points; - if (empty($times)) { - $times = array($_SERVER["REQUEST_TIME_FLOAT"]); - $points = array("REQUEST TIME " . round($_SERVER["REQUEST_TIME_FLOAT"] / 60, 6)); - } - $took = $times[count($times) - 1]; - $times[] = microtime(true); - $took = round(($times[count($times) - 1] - $took) / 60, 6); - $points[] = "took $took minutes to get to line " . $line . " in file: " . $file; - return $points; + static $times, $points; + if (empty($times)) { + $times = array($_SERVER["REQUEST_TIME_FLOAT"]); + $points = array("REQUEST TIME " . round($_SERVER["REQUEST_TIME_FLOAT"] / 60, 6)); + } + $took = $times[count($times) - 1]; + $times[] = microtime(true); + $took = round(($times[count($times) - 1] - $took) / 60, 6); + $points[] = "took $took minutes to get to line " . $line . " in file: " . $file; + return $points; } function echo_time_points($points) { // echo ""; } function crypto_token($length, $url = true) { - $bytes = openssl_random_pseudo_bytes($length, $crypto_strong); - $base64 = base64_url_encode($bytes); - if (!$crypto_strong) { - formr_error(500, 'Internal Server Error', 'Generated cryptographic tokens are not strong.', 'Cryptographic Error'); - } - return $base64; + $bytes = openssl_random_pseudo_bytes($length, $crypto_strong); + $base64 = base64_url_encode($bytes); + if (!$crypto_strong) { + formr_error(500, 'Internal Server Error', 'Generated cryptographic tokens are not strong.', 'Cryptographic Error'); + } + return $base64; } function base64_url_encode($data) { - return strtr(base64_encode($data), '+/=', '-_~'); + return strtr(base64_encode($data), '+/=', '-_~'); } function base64_url_decode($data) { - return base64_decode(strtr($data, '-_~', '+/=')); + return base64_decode(strtr($data, '-_~', '+/=')); } /** @@ -595,43 +595,43 @@ function base64_url_decode($data) { * @return string */ function url_title($str, $separator = '-', $lowercase = false) { - if ($separator == 'dash') { - $separator = '-'; - } else if ($separator == 'underscore') { - $separator = '_'; - } - $q_separator = preg_quote($separator); - $trans = array( - '&.+?;' => '', - '[^a-z0-9 _-]' => '', - '\s+' => $separator, - '(' . $q_separator . ')+' => $separator - ); - $str = strip_tags($str); - foreach ($trans as $key => $val) { - $str = preg_replace("#" . $key . "#i", $val, $str); - } - - if ($lowercase === true) { - $str = strtolower($str); - } - - return trim($str, $separator); + if ($separator == 'dash') { + $separator = '-'; + } else if ($separator == 'underscore') { + $separator = '_'; + } + $q_separator = preg_quote($separator); + $trans = array( + '&.+?;' => '', + '[^a-z0-9 _-]' => '', + '\s+' => $separator, + '(' . $q_separator . ')+' => $separator + ); + $str = strip_tags($str); + foreach ($trans as $key => $val) { + $str = preg_replace("#" . $key . "#i", $val, $str); + } + + if ($lowercase === true) { + $str = strtolower($str); + } + + return trim($str, $separator); } function empty_column($col, $arr) { - $empty = true; - $last = null; - foreach ($arr AS $row): - if (!(empty($row->$col)) OR // not empty column? (also treats 0 and empty strings as empty) - $last != $row->$col OR // any variation in this column? - ! (!is_array($row->$col) AND trim($row->$col) == '')): - $empty = false; - break; - endif; - $last = $row->$col; - endforeach; - return $empty; + $empty = true; + $last = null; + foreach ($arr AS $row): + if (!(empty($row->$col)) OR // not empty column? (also treats 0 and empty strings as empty) + $last != $row->$col OR // any variation in this column? + ! (!is_array($row->$col) AND trim($row->$col) == '')): + $empty = false; + break; + endif; + $last = $row->$col; + endforeach; + return $empty; } /** @@ -641,24 +641,24 @@ function empty_column($col, $arr) { * @return mixed Returns an array if all is well or FALSE otherwise */ function get_run_dir_contents($dir) { - if (!$dir || !is_dir($dir) || !is_readable($dir)) { - return false; - } - - $files = glob($dir . '/*.json'); - if (!$files) { - return false; - } - - $contents = array(); - foreach ($files as $file) { - $file_contents = file_get_contents($file); - $json = json_decode($file_contents); - if ($json) { - $contents[basename($file)] = $json->name; - } - } - return $contents; + if (!$dir || !is_dir($dir) || !is_readable($dir)) { + return false; + } + + $files = glob($dir . '/*.json'); + if (!$files) { + return false; + } + + $contents = array(); + foreach ($files as $file) { + $file_contents = file_get_contents($file); + $json = json_decode($file_contents); + if ($json) { + $contents[basename($file)] = $json->name; + } + } + return $contents; } /** @@ -669,17 +669,17 @@ function get_run_dir_contents($dir) { * @return mixed Returns the mime type as a string or FALSE otherwise */ function get_file_mime($filename) { - $constant = defined('FILEINFO_MIME_TYPE') ? FILEINFO_MIME_TYPE : FILEINFO_MIME; - $finfo = finfo_open($constant); - $info = finfo_file($finfo, $filename); - finfo_close($finfo); - $mime = explode(';', $info); - if (!$mime) { - return false; - } + $constant = defined('FILEINFO_MIME_TYPE') ? FILEINFO_MIME_TYPE : FILEINFO_MIME; + $finfo = finfo_open($constant); + $info = finfo_file($finfo, $filename); + finfo_close($finfo); + $mime = explode(';', $info); + if (!$mime) { + return false; + } - $mime_type = $mime[0]; - return $mime_type; + $mime_type = $mime[0]; + return $mime_type; } /** @@ -690,34 +690,34 @@ function get_file_mime($filename) { * @todo implement caching stuff */ function download_file($file, $unlink = false) { - $type = get_file_mime($file); - $filename = basename($file); - $filesize = filesize($file); - header('Content-Description: File Transfer'); - header('Content-Type: ' . $type); - header('Content-Disposition: attachment; filename = "' . $filename . '"'); - header('Content-Transfer-Encoding: binary'); - header('Expires: 0'); - header('Cache-Control: must-revalidate, post-check=0, pre-check=0'); - if ($filesize) { - header('Content-Length: ' . $filesize); - } - readfile($file); - if ($unlink) { - unlink($file); - } - exit(0); + $type = get_file_mime($file); + $filename = basename($file); + $filesize = filesize($file); + header('Content-Description: File Transfer'); + header('Content-Type: ' . $type); + header('Content-Disposition: attachment; filename = "' . $filename . '"'); + header('Content-Transfer-Encoding: binary'); + header('Expires: 0'); + header('Cache-Control: must-revalidate, post-check=0, pre-check=0'); + if ($filesize) { + header('Content-Length: ' . $filesize); + } + readfile($file); + if ($unlink) { + unlink($file); + } + exit(0); } /** * @deprecated */ function get_duplicate_update_string($columns) { - foreach ($columns as $i => $column) { - $column = trim($column, '`'); - $columns[$i] = "`$column` = VALUES(`$column`)"; - } - return $columns; + foreach ($columns as $i => $column) { + $column = trim($column, '`'); + $columns[$i] = "`$column` = VALUES(`$column`)"; + } + return $columns; } /** @@ -727,10 +727,10 @@ function get_duplicate_update_string($columns) { * @return string */ function mysql_datetime($time = null) { - if ($time === null) { - $time = time(); - } - return date('Y-m-d H:i:s', $time); + if ($time === null) { + $time = time(); + } + return date('Y-m-d H:i:s', $time); } /** @@ -739,7 +739,7 @@ function mysql_datetime($time = null) { * @return string */ function mysql_now() { - return mysql_datetime(); + return mysql_datetime(); } /** @@ -749,106 +749,105 @@ function mysql_now() { * @return string */ function mysql_interval($interval) { - if (($time = strtotime($interval)) === false) { - throw new Exception("Invalid time interval given to strtotime '$interval'"); - } - return mysql_datetime($time); + if (($time = strtotime($interval)) === false) { + throw new Exception("Invalid time interval given to strtotime '$interval'"); + } + return mysql_datetime($time); } function site_url($uri = '', $params = array()) { - $url = WEBROOT; - if ($uri) { - $url .= $uri . '/'; - } - if ($params) { - $url .= '?' . http_build_query($params); - } - return trim($url, '\/\\'); + $url = WEBROOT; + if ($uri) { + $url .= $uri . '/'; + } + if ($params) { + $url .= '?' . http_build_query($params); + } + return trim($url, '\/\\'); } function admin_url($uri = '', $params = array()) { - if ($uri) { - $uri = '/' . $uri; - } - return site_url('admin' . $uri, $params); + if ($uri) { + $uri = '/' . $uri; + } + return site_url('admin' . $uri, $params); } function run_url($name = '', $action = '', $params = array()) { - if ($name === Run::TEST_RUN) { - return site_url('run/' . $name . '/' . $action); - } - - $protocol = Config::get('define_root.protocol'); - $domain = trim(Config::get('define_root.doc_root'), "\/\\"); - $subdomain = null; - if (Config::get('use_study_subdomains')) { - $subdomain = strtolower($name) . '.'; - } else { - $domain .= '/' . $name; - } - $url = $protocol . $subdomain . $domain; - if ($action) { - $action = trim($action, "\/\\"); - $url .= '/' . $action . '/'; - } - if ($params) { - $url .= '?' . http_build_query($params); - } - return $url; + if ($name === Run::TEST_RUN) { + return site_url('run/' . $name . '/' . $action); + } + + $protocol = Config::get('define_root.protocol'); + $domain = trim(Config::get('define_root.doc_root'), "\/\\"); + $subdomain = null; + if (Config::get('use_study_subdomains')) { + $subdomain = strtolower($name) . '.'; + } else { + $domain .= '/' . $name; + } + $url = $protocol . $subdomain . $domain; + if ($action) { + $action = trim($action, "\/\\"); + $url .= '/' . $action . '/'; + } + if ($params) { + $url .= '?' . http_build_query($params); + } + return $url; } function admin_study_url($name = '', $action = '', $params = array()) { - if ($action) { - $name = $name . '/' . $action; - } - return admin_url('survey/' . $name, $params); + if ($action) { + $name = $name . '/' . $action; + } + return admin_url('survey/' . $name, $params); } function admin_run_url($name = '', $action = '', $params = array()) { - if ($action) { - $name = $name . '/' . $action; - } - return admin_url('run/' . $name, $params); + if ($action) { + $name = $name . '/' . $action; + } + return admin_url('run/' . $name, $params); } - /** -* modified from https://stackoverflow.com/questions/118884/what-is-an-elegant-way-to-force-browsers-to-reload-cached-css-js-files?rq=1 + * modified from https://stackoverflow.com/questions/118884/what-is-an-elegant-way-to-force-browsers-to-reload-cached-css-js-files?rq=1 * Given a file, i.e. /css/base.css, replaces it with a string containing the * file's mtime, i.e. /css/base.1221534296.css. * * @param $file The file to be loaded. Must not start with a slash. */ function asset_url($file) { - if (strpos($file, 'http') !== false || strpos($file, '//') === 0) { - return $file; - } - if (strpos($file, 'assets') === false) { - $file = 'assets/' . $file; - } - $mtime = @filemtime(APPLICATION_ROOT . "webroot/" . $file); - if(!$mtime) { - return site_url($file); - } - return site_url($file . "?v" . $mtime); + if (strpos($file, 'http') !== false || strpos($file, '//') === 0) { + return $file; + } + if (strpos($file, 'assets') === false) { + $file = 'assets/' . $file; + } + $mtime = @filemtime(APPLICATION_ROOT . "webroot/" . $file); + if (!$mtime) { + return site_url($file); + } + return site_url($file . "?v" . $mtime); } function monkeybar_url($run_name, $action = '', $params = array()) { - return run_url($run_name, 'monkey-bar/' . $action, $params); + return run_url($run_name, 'monkey-bar/' . $action, $params); } function array_to_accordion($array) { - $rand = mt_rand(0, 10000); - $acc = '
'; - $first = ' in'; + $rand = mt_rand(0, 10000); + $acc = '
'; + $first = ' in'; - foreach ($array as $title => $content): - if ($content == null) { - $content = stringBool($content); - } - $id = 'collapse' . str_replace(' ', '', $rand . $title); + foreach ($array as $title => $content): + if ($content == null) { + $content = stringBool($content); + } + $id = 'collapse' . str_replace(' ', '', $rand . $title); - $acc .= ' + $acc .= '
'; - $first = ''; - endforeach; + $first = ''; + endforeach; - $acc .= '
'; - return $acc; + $acc .= ''; + return $acc; } function array_to_orderedlist($array, $olclass = null, $liclass = null) { - $ol = '
    '; - foreach ($array as $title => $label) { - if (is_formr_truthy($label)) { - $ol .= '
  1. ' . $label . '
  2. '; - } - } - $ol .= '
'; - return $ol; + $ol = '
    '; + foreach ($array as $title => $label) { + if (is_formr_truthy($label)) { + $ol .= '
  1. ' . $label . '
  2. '; + } + } + $ol .= '
'; + return $ol; } function is_formr_truthy($value) { - $value = (string) $value; - return $value || $value === '0'; + $value = (string) $value; + return $value || $value === '0'; } /** @@ -893,30 +892,30 @@ function is_formr_truthy($value) { * @return string Returns R variables */ function opencpu_define_vars(array $data, $context = null) { - $vars = ''; - if (!$data) { - return $vars; - } - - // Set datasets - if (isset($data['datasets']) && is_array($data['datasets'])) { - foreach ($data['datasets'] as $data_frame => $content) { - $vars .= $data_frame . ' = as.data.frame(jsonlite::fromJSON("' . addslashes(json_encode($content, JSON_UNESCAPED_UNICODE)) . '"), stringsAsFactors=F) + $vars = ''; + if (!$data) { + return $vars; + } + + // Set datasets + if (isset($data['datasets']) && is_array($data['datasets'])) { + foreach ($data['datasets'] as $data_frame => $content) { + $vars .= $data_frame . ' = as.data.frame(jsonlite::fromJSON("' . addslashes(json_encode($content, JSON_UNESCAPED_UNICODE)) . '"), stringsAsFactors=F) '; - if($context === $data_frame) { - $vars .= 'attach(tail(' . $context . ', 1)) + if ($context === $data_frame) { + $vars .= 'attach(tail(' . $context . ', 1)) '; - } - } - } - unset($data['datasets']); - - // set other variables - foreach ($data as $var_name => $var_value) { - $vars .= $var_name . ' = ' . $var_value . ' + } + } + } + unset($data['datasets']); + + // set other variables + foreach ($data as $var_name => $var_value) { + $vars .= $var_name . ' = ' . $var_value . ' '; - } - return $vars; + } + return $vars; } /** @@ -929,21 +928,21 @@ function opencpu_define_vars(array $data, $context = null) { * @return string|OpenCPU_Session|null Returns null if an error occured so check the return value using the equivalence operator (===) */ function opencpu_get($location, $return_format = 'json', $context = null, $return_session = false) { - $uri = $location . $return_format; - try { - $session = OpenCPU::getInstance()->get($uri); - if ($return_session === true) { - return $session; - } - - if ($session->hasError()) { - throw new OpenCPU_Exception($session->getError()); - } - return $return_format === 'json' ? $session->getJSONObject() : $session->getObject($return_format); - } catch (OpenCPU_Exception $e) { - opencpu_log($e); - return null; - } + $uri = $location . $return_format; + try { + $session = OpenCPU::getInstance()->get($uri); + if ($return_session === true) { + return $session; + } + + if ($session->hasError()) { + throw new OpenCPU_Exception($session->getError()); + } + return $return_format === 'json' ? $session->getJSONObject() : $session->getObject($return_format); + } catch (OpenCPU_Exception $e) { + opencpu_log($e); + return null; + } } /** @@ -957,45 +956,44 @@ function opencpu_get($location, $return_format = 'json', $context = null, $retur * @return string|OpenCPU_Session|null Returns null if an error occured so check the return value using the equivalence operator (===) */ function opencpu_evaluate($code, $variables = null, $return_format = 'json', $context = null, $return_session = false) { - if ($return_session !== true) { - $result = shortcut_without_opencpu($code, $variables); - if($result !== null) { - return current($result); - } - } - - if (!is_string($variables)) { - $variables = opencpu_define_vars($variables, $context); - } - - $params = array('x' => '{ + if ($return_session !== true) { + $result = shortcut_without_opencpu($code, $variables); + if ($result !== null) { + return current($result); + } + } + + if (!is_string($variables)) { + $variables = opencpu_define_vars($variables, $context); + } + + $params = array('x' => '{ (function() { library(formr) ' . $variables . ' ' . $code . ' })() }'); - $uri = '/base/R/identity/' . $return_format; - try { - $session = OpenCPU::getInstance()->post($uri, $params); - if ($return_session === true) { - return $session; - } + $uri = '/base/R/identity/' . $return_format; + try { + $session = OpenCPU::getInstance()->post($uri, $params); + if ($return_session === true) { + return $session; + } - if ($session->hasError()) { - throw new OpenCPU_Exception(opencpu_debug($session)); - } else { - print_hidden_opencpu_debug_message($session, "OpenCPU debugger for run R code."); - } - - return $return_format === 'json' ? $session->getJSONObject() : $session->getObject($return_format); - } catch (OpenCPU_Exception $e) { - notify_user_error($e, "There was a problem dynamically evaluating a value using openCPU."); - opencpu_log($e); - return null; - } -} + if ($session->hasError()) { + throw new OpenCPU_Exception(opencpu_debug($session)); + } else { + print_hidden_opencpu_debug_message($session, "OpenCPU debugger for run R code."); + } + return $return_format === 'json' ? $session->getJSONObject() : $session->getObject($return_format); + } catch (OpenCPU_Exception $e) { + notify_user_error($e, "There was a problem dynamically evaluating a value using openCPU."); + opencpu_log($e); + return null; + } +} /** * In one common, well-defined case, we just skip calling openCPU @@ -1005,20 +1003,19 @@ function opencpu_evaluate($code, $variables = null, $return_format = 'json', $co * @return mixed|null Returns null if things aren't simple, so check the return value using the equivalence operator (===) */ function shortcut_without_opencpu($code, $data) { - if($code === 'tail(survey_unit_sessions$created,1)') { - return array(end($data['datasets']['survey_unit_sessions']['created'])); - } elseif(preg_match("/^([a-zA-Z0-9_]+)\\\$([a-zA-Z0-9_]+)$/", $code, $matches)) { - $survey = $matches[1]; - $variable = $matches[2]; - if(!empty($data['datasets'][$survey][$variable]) && count($data['datasets'][$survey][$variable]) == 1) { - return $data['datasets'][$survey][$variable]; - } - } + if ($code === 'tail(survey_unit_sessions$created,1)') { + return array(end($data['datasets']['survey_unit_sessions']['created'])); + } elseif (preg_match("/^([a-zA-Z0-9_]+)\\\$([a-zA-Z0-9_]+)$/", $code, $matches)) { + $survey = $matches[1]; + $variable = $matches[2]; + if (!empty($data['datasets'][$survey][$variable]) && count($data['datasets'][$survey][$variable]) == 1) { + return $data['datasets'][$survey][$variable]; + } + } - return null; + return null; } - /** * Call knit() function from the knitr R package * @@ -1028,47 +1025,46 @@ function shortcut_without_opencpu($code, $data) { * @return string|null */ function opencpu_knit($code, $return_format = 'json', $self_contained = 1, $return_session = false) { - $params = array('text' => "'" . addslashes($code) . "'"); - $uri = '/knitr/R/knit/' . $return_format; - try { - $session = OpenCPU::getInstance()->post($uri, $params); - if ($return_session === true) { - return $session; - } - - if ($session->hasError()) { - throw new OpenCPU_Exception(opencpu_debug($session)); - } - return $return_format === 'json' ? $session->getJSONObject() : $session->getObject($return_format); - } catch (OpenCPU_Exception $e) { - notify_user_error($e, "There was a problem dynamically knitting something using openCPU."); - opencpu_log($e); - return null; - } + $params = array('text' => "'" . addslashes($code) . "'"); + $uri = '/knitr/R/knit/' . $return_format; + try { + $session = OpenCPU::getInstance()->post($uri, $params); + if ($return_session === true) { + return $session; + } + + if ($session->hasError()) { + throw new OpenCPU_Exception(opencpu_debug($session)); + } + return $return_format === 'json' ? $session->getJSONObject() : $session->getObject($return_format); + } catch (OpenCPU_Exception $e) { + notify_user_error($e, "There was a problem dynamically knitting something using openCPU."); + opencpu_log($e); + return null; + } } - function opencpu_knit_plaintext($source, $variables = null, $return_session = false, $context = null) { - if (!is_string($variables)) { - $variables = opencpu_define_vars($variables, $context); - } - - $run_session = Site::getInstance()->getRunSession(); - - $show_errors = 'F'; - if (!$run_session OR $run_session->isTesting() ) { - $show_errors = 'T'; - } - $source = '```{r settings,warning='. $show_errors .',message='. $show_errors .',error='. $show_errors .',echo=F} + if (!is_string($variables)) { + $variables = opencpu_define_vars($variables, $context); + } + + $run_session = Site::getInstance()->getRunSession(); + + $show_errors = 'F'; + if (!$run_session OR $run_session->isTesting()) { + $show_errors = 'T'; + } + $source = '```{r settings,warning=' . $show_errors . ',message=' . $show_errors . ',error=' . $show_errors . ',echo=F} library(knitr); library(formr) -opts_chunk$set(warning='. $show_errors .',message='. $show_errors .',error='. $show_errors .',echo=F,fig.height=7,fig.width=10) -opts_knit$set(base.url="'.OpenCPU::TEMP_BASE_URL.'") +opts_chunk$set(warning=' . $show_errors . ',message=' . $show_errors . ',error=' . $show_errors . ',echo=F,fig.height=7,fig.width=10) +opts_knit$set(base.url="' . OpenCPU::TEMP_BASE_URL . '") ' . $variables . ' ``` ' . -$source; + $source; - return opencpu_knit($source, 'json', 0, $return_session); + return opencpu_knit($source, 'json', 0, $return_session); } /** @@ -1081,62 +1077,61 @@ function opencpu_knit_plaintext($source, $variables = null, $return_session = fa * @return string|null */ function opencpu_knit2html($source, $return_format = 'json', $self_contained = 1, $return_session = false) { - $params = array('text' => "'" . addslashes($source) . "'", 'self_contained' => $self_contained); - $uri = '/formr/R/formr_render_commonmark/' . $return_format; - - $uri = '/formr/R/formr_inline_render/' . $return_format; - try { - $session = OpenCPU::getInstance()->post($uri, $params); - if ($return_session === true) { - return $session; - } - - if ($session->hasError()) { - throw new OpenCPU_Exception(opencpu_debug($session)); - } - - return $return_format === 'json' ? $session->getJSONObject() : $session->getObject($return_format); - } catch (OpenCPU_Exception $e) { - notify_user_error($e, "There was a problem dynamically knitting something to HTML using openCPU."); - opencpu_log($e); - return null; - } -} + $params = array('text' => "'" . addslashes($source) . "'", 'self_contained' => $self_contained); + $uri = '/formr/R/formr_render_commonmark/' . $return_format; + + $uri = '/formr/R/formr_inline_render/' . $return_format; + try { + $session = OpenCPU::getInstance()->post($uri, $params); + if ($return_session === true) { + return $session; + } + if ($session->hasError()) { + throw new OpenCPU_Exception(opencpu_debug($session)); + } + + return $return_format === 'json' ? $session->getJSONObject() : $session->getObject($return_format); + } catch (OpenCPU_Exception $e) { + notify_user_error($e, "There was a problem dynamically knitting something to HTML using openCPU."); + opencpu_log($e); + return null; + } +} function opencpu_knit_iframe($source, $variables = null, $return_session = false, $context = null, $description = '', $footer_text = '') { - if (!is_string($variables)) { - $variables = opencpu_define_vars($variables, $context); - } - - $run_session = Site::getInstance()->getRunSession(); - - $show_errors = 'F'; - if (!$run_session OR $run_session->isTesting() ) { - $show_errors = 'T'; - } - $yaml = ""; - $yaml_lines = '/^\-\-\-/um'; - if(preg_match_all($yaml_lines, $source) >= 2) { - $parts = preg_split($yaml_lines, $source, 3); - $yaml = "---" . $parts[1] . "---\n\n"; - $source = $parts[2]; - } - - $source = $yaml . -'```{r settings,warning='. $show_errors .',message='. $show_errors .',error='. $show_errors .',echo=F} + if (!is_string($variables)) { + $variables = opencpu_define_vars($variables, $context); + } + + $run_session = Site::getInstance()->getRunSession(); + + $show_errors = 'F'; + if (!$run_session OR $run_session->isTesting()) { + $show_errors = 'T'; + } + $yaml = ""; + $yaml_lines = '/^\-\-\-/um'; + if (preg_match_all($yaml_lines, $source) >= 2) { + $parts = preg_split($yaml_lines, $source, 3); + $yaml = "---" . $parts[1] . "---\n\n"; + $source = $parts[2]; + } + + $source = $yaml . + '```{r settings,warning=' . $show_errors . ',message=' . $show_errors . ',error=' . $show_errors . ',echo=F} library(knitr); library(formr) -opts_chunk$set(warning='. $show_errors .',message='. $show_errors .',error=T,echo=F,fig.height=7,fig.width=10) +opts_chunk$set(warning=' . $show_errors . ',message=' . $show_errors . ',error=T,echo=F,fig.height=7,fig.width=10) ' . $variables . ' ``` -'. -$description .' +' . + $description . ' ' . -$source . -" + $source . + " @@ -1144,106 +1139,106 @@ function opencpu_knit_iframe($source, $variables = null, $return_session = false " . $footer_text; - $params = array('text' => "'" . addslashes($source) . "'"); - - $uri = '/formr/R/formr_render/'; - try { - $session = OpenCPU::getInstance()->post($uri, $params); - if ($return_session === true) { - return $session; - } - - if ($session->hasError()) { - throw new OpenCPU_Exception(opencpu_debug($session)); - } - - return $session->getJSONObject(); - } catch (OpenCPU_Exception $e) { - notify_user_error($e, "There was a problem dynamically knitting something to HTML using openCPU."); - opencpu_log($e); - return null; - } + $params = array('text' => "'" . addslashes($source) . "'"); + + $uri = '/formr/R/formr_render/'; + try { + $session = OpenCPU::getInstance()->post($uri, $params); + if ($return_session === true) { + return $session; + } + + if ($session->hasError()) { + throw new OpenCPU_Exception(opencpu_debug($session)); + } + + return $session->getJSONObject(); + } catch (OpenCPU_Exception $e) { + notify_user_error($e, "There was a problem dynamically knitting something to HTML using openCPU."); + opencpu_log($e); + return null; + } } function opencpu_knitdisplay($source, $variables = null, $return_session = false, $context = null) { - if (!is_string($variables)) { - $variables = opencpu_define_vars($variables, $context); - } - - $run_session = Site::getInstance()->getRunSession(); - - $show_errors = 'F'; - if (!$run_session OR $run_session->isTesting() ) { - $show_errors = 'T'; - } - $source = '```{r settings,warning='. $show_errors .',message='. $show_errors .',error='. $show_errors .',echo=F} + if (!is_string($variables)) { + $variables = opencpu_define_vars($variables, $context); + } + + $run_session = Site::getInstance()->getRunSession(); + + $show_errors = 'F'; + if (!$run_session OR $run_session->isTesting()) { + $show_errors = 'T'; + } + $source = '```{r settings,warning=' . $show_errors . ',message=' . $show_errors . ',error=' . $show_errors . ',echo=F} library(knitr); library(formr) -opts_chunk$set(warning='. $show_errors .',message='. $show_errors .',error='. $show_errors .',echo=F,fig.height=7,fig.width=10) -opts_knit$set(base.url="'.OpenCPU::TEMP_BASE_URL.'") +opts_chunk$set(warning=' . $show_errors . ',message=' . $show_errors . ',error=' . $show_errors . ',echo=F,fig.height=7,fig.width=10) +opts_knit$set(base.url="' . OpenCPU::TEMP_BASE_URL . '") ' . $variables . ' ``` ' . -$source; + $source; - return opencpu_knit2html($source, 'json', 0, $return_session); + return opencpu_knit2html($source, 'json', 0, $return_session); } function opencpu_knitadmin($source, $variables = null, $return_session = false) { - if (!is_string($variables)) { - $variables = opencpu_define_vars($variables); - } - - $run_session = Site::getInstance()->getRunSession(); - - $show_errors = 'F'; - if (!$run_session OR $run_session->isTesting() ) { - $show_errors = 'T'; - } - $source = '```{r settings,warning='. $show_errors .',message='. $show_errors .',error='. $show_errors .',echo=F} + if (!is_string($variables)) { + $variables = opencpu_define_vars($variables); + } + + $run_session = Site::getInstance()->getRunSession(); + + $show_errors = 'F'; + if (!$run_session OR $run_session->isTesting()) { + $show_errors = 'T'; + } + $source = '```{r settings,warning=' . $show_errors . ',message=' . $show_errors . ',error=' . $show_errors . ',echo=F} library(knitr); library(formr) -opts_chunk$set(warning='. $show_errors .',message='. $show_errors .',error='. $show_errors .',echo=F,fig.height=7,fig.width=10) -opts_knit$set(base.url="'.OpenCPU::TEMP_BASE_URL.'") +opts_chunk$set(warning=' . $show_errors . ',message=' . $show_errors . ',error=' . $show_errors . ',echo=F,fig.height=7,fig.width=10) +opts_knit$set(base.url="' . OpenCPU::TEMP_BASE_URL . '") ' . $variables . ' ``` ' . -$source; + $source; - return opencpu_knit2html($source, 'json', 0, $return_session); + return opencpu_knit2html($source, 'json', 0, $return_session); } function opencpu_knitemail($source, array $variables = null, $return_format = 'json', $return_session = false) { - if (!is_string($variables)) { - $variables = opencpu_define_vars($variables); - } - $run_session = Site::getInstance()->getRunSession(); - - $show_errors = 'F'; - if (!$run_session OR $run_session->isTesting() ) { - $show_errors = 'T'; - } - - $source = '```{r settings,warning='. $show_errors .',message='. $show_errors .',error='. $show_errors .',echo=F} + if (!is_string($variables)) { + $variables = opencpu_define_vars($variables); + } + $run_session = Site::getInstance()->getRunSession(); + + $show_errors = 'F'; + if (!$run_session OR $run_session->isTesting()) { + $show_errors = 'T'; + } + + $source = '```{r settings,warning=' . $show_errors . ',message=' . $show_errors . ',error=' . $show_errors . ',echo=F} library(knitr); library(formr) -opts_chunk$set(warning='. $show_errors .',message='. $show_errors .',error='. $show_errors .',echo=F,fig.retina=2) +opts_chunk$set(warning=' . $show_errors . ',message=' . $show_errors . ',error=' . $show_errors . ',echo=F,fig.retina=2) opts_knit$set(upload.fun=function(x) { paste0("cid:", URLencode(basename(x))) }) ' . $variables . ' ``` ' . -$source; + $source; - return opencpu_knit2html($source, $return_format, 0, $return_session); + return opencpu_knit2html($source, $return_format, 0, $return_session); } function opencpu_string_key($index) { - return 'formr-ocpu-label-' . $index; + return 'formr-ocpu-label-' . $index; } function opencpu_string_key_parsing($strings) { - $ret = array(); - foreach ($strings as $index => $string) { - $ret['formr-ocpu-label-' . $index] = $string; - } - return $ret; + $ret = array(); + foreach ($strings as $index => $string) { + $ret['formr-ocpu-label-' . $index] = $string; + } + return $ret; } /** @@ -1254,20 +1249,20 @@ function opencpu_string_key_parsing($strings) { * @return array Returns an array of parsed labels indexed by the label-key to be substituted */ function opencpu_multistring_parse(Survey $survey, array $string_templates) { - $markdown = implode(OpenCPU::STRING_DELIMITER, $string_templates); - $opencpu_vars = $survey->getUserDataInRun($markdown, $survey->name); - $session = opencpu_knitdisplay($markdown, $opencpu_vars, true, $survey->name); - - if($session AND !$session->hasError()) { - print_hidden_opencpu_debug_message($session, "OpenCPU debugger for dynamic values and showifs."); - $parsed_strings = $session->getJSONObject(); - $strings = explode(OpenCPU::STRING_DELIMITER_PARSED, $parsed_strings); - $strings = array_map("remove_tag_wrapper", $strings); - return opencpu_string_key_parsing($strings); - } else { - notify_user_error(opencpu_debug($session), "There was a problem dynamically knitting something to HTML using openCPU."); - return fill_array(opencpu_string_key_parsing($string_templates)); - } + $markdown = implode(OpenCPU::STRING_DELIMITER, $string_templates); + $opencpu_vars = $survey->getUserDataInRun($markdown, $survey->name); + $session = opencpu_knitdisplay($markdown, $opencpu_vars, true, $survey->name); + + if ($session AND ! $session->hasError()) { + print_hidden_opencpu_debug_message($session, "OpenCPU debugger for dynamic values and showifs."); + $parsed_strings = $session->getJSONObject(); + $strings = explode(OpenCPU::STRING_DELIMITER_PARSED, $parsed_strings); + $strings = array_map("remove_tag_wrapper", $strings); + return opencpu_string_key_parsing($strings); + } else { + notify_user_error(opencpu_debug($session), "There was a problem dynamically knitting something to HTML using openCPU."); + return fill_array(opencpu_string_key_parsing($string_templates)); + } } /** @@ -1278,166 +1273,165 @@ function opencpu_multistring_parse(Survey $survey, array $string_templates) { * @param array $array An array of data contaning label templates * @param array $parsed_strings An array of parsed labels */ -function opencpu_substitute_parsed_strings (array &$array, array $parsed_strings) { - foreach ($array as $key => &$value) { - if (is_array($array[$key])) { - opencpu_substitute_parsed_strings($array[$key], $parsed_strings); - } elseif (is_object($value) && property_exists($value, 'label_parsed')) { - $value->label_parsed = isset($parsed_strings[$value->label_parsed]) ? $parsed_strings[$value->label_parsed] : $value->label_parsed; - $array[$key] = $value; - } elseif (isset($parsed_strings[$value])) { - $array[$key] = $parsed_strings[$value]; - } - } +function opencpu_substitute_parsed_strings(array &$array, array $parsed_strings) { + foreach ($array as $key => &$value) { + if (is_array($array[$key])) { + opencpu_substitute_parsed_strings($array[$key], $parsed_strings); + } elseif (is_object($value) && property_exists($value, 'label_parsed')) { + $value->label_parsed = isset($parsed_strings[$value->label_parsed]) ? $parsed_strings[$value->label_parsed] : $value->label_parsed; + $array[$key] = $value; + } elseif (isset($parsed_strings[$value])) { + $array[$key] = $parsed_strings[$value]; + } + } } function opencpu_multiparse_showif(Survey $survey, array $showifs, $return_session = false) { - $code = "(function() {with(tail({$survey->name}, 1), {\n"; - $code .= "formr.showifs = list();\n"; - $code .= "within(formr.showifs, { \n"; - $code .= implode("\n", $showifs) . "\n"; - $code .= "})\n"; - $code .= "})})()\n"; + $code = "(function() {with(tail({$survey->name}, 1), {\n"; + $code .= "formr.showifs = list();\n"; + $code .= "within(formr.showifs, { \n"; + $code .= implode("\n", $showifs) . "\n"; + $code .= "})\n"; + $code .= "})})()\n"; - $variables = $survey->getUserDataInRun($code, $survey->name); - return opencpu_evaluate($code, $variables, 'json', null, $return_session); + $variables = $survey->getUserDataInRun($code, $survey->name); + return opencpu_evaluate($code, $variables, 'json', null, $return_session); } function opencpu_multiparse_values(Survey $survey, array $values, $return_session = false) { - $code = "(function() {with(tail({$survey->name}, 1), {\n"; - $code .= "list(\n" . implode(",\n", $values) . "\n)"; - $code .= "})})()\n"; + $code = "(function() {with(tail({$survey->name}, 1), {\n"; + $code .= "list(\n" . implode(",\n", $values) . "\n)"; + $code .= "})})()\n"; - $variables = $survey->getUserDataInRun($code, $survey->name); - return opencpu_evaluate($code, $variables, 'json', null, $return_session); + $variables = $survey->getUserDataInRun($code, $survey->name); + return opencpu_evaluate($code, $variables, 'json', null, $return_session); } function opencpu_debug($session, OpenCPU $ocpu = null, $rtype = 'json') { - $debug = array(); - if (empty($session)) { - $debug['Response'] = 'No OpenCPU_Session found. Server may be down.'; - if ($ocpu !== null) { - $request = $ocpu->getRequest(); - $debug['Request'] = (string) $request; - $reponse_info = $ocpu->getRequestInfo(); - $debug['Request Headers'] = pre_htmlescape(print_r($reponse_info['request_header'], 1)); - } - } else { - - try { - $request = $session->getRequest(); - $params = $request->getParams(); - if(isset($params['text'])) { - $debug['R Markdown'] = ' + $debug = array(); + if (empty($session)) { + $debug['Response'] = 'No OpenCPU_Session found. Server may be down.'; + if ($ocpu !== null) { + $request = $ocpu->getRequest(); + $debug['Request'] = (string) $request; + $reponse_info = $ocpu->getRequestInfo(); + $debug['Request Headers'] = pre_htmlescape(print_r($reponse_info['request_header'], 1)); + } + } else { + + try { + $request = $session->getRequest(); + $params = $request->getParams(); + if (isset($params['text'])) { + $debug['R Markdown'] = '
Download R Markdown file to debug.
- '; - } elseif(isset($params['x'])) { - $debug['R Code'] = ' + '; + } elseif (isset($params['x'])) { + $debug['R Code'] = ' Download R code file to debug.
- '; - } - if ($session->hasError()) { - $debug['Response'] = pre_htmlescape($session->getError()); - } else { - if(($files = $session->getFiles("knit.html"))) { - $iframesrc = $files['knit.html']; - $debug['Response'] = ' + '; + } + if ($session->hasError()) { + $debug['Response'] = pre_htmlescape($session->getError()); + } else { + if (($files = $session->getFiles("knit.html"))) { + $iframesrc = $files['knit.html']; + $debug['Response'] = '

- Open in new window + Open in new window

'; - } - else if (isset($params['text']) || $rtype === 'text') { - $debug['Response'] = stringBool($session->getObject('text')); - } else { - $debug['Response'] = pre_htmlescape(json_encode($session->getJSONObject(), JSON_PRETTY_PRINT + JSON_UNESCAPED_UNICODE)); - } - } - - $urls = $session->getResponsePathsAsLinks(); - if (!$session->hasError() AND ! empty($urls)) { - $locations = ''; - foreach ($urls AS $path => $link) { - $path = str_replace('/ocpu/tmp/' . $session->getKey(), '', $path); - $locations .= "$path
"; - } - $debug['Locations'] = $locations; - } - $debug['Session Info'] = pre_htmlescape($session->getInfo()); - $debug['Session Console'] = pre_htmlescape($session->getConsole()); - $debug['Session Stdout'] = pre_htmlescape($session->getStdout()); - $debug['Request'] = pre_htmlescape((string) $request); - - $reponse_headers = $session->getResponseHeaders(); - $debug['Response Headers'] = pre_htmlescape(print_r($reponse_headers, 1)); - - $reponse_info = $session->caller()->getRequestInfo(); - $debug['Request Headers'] = pre_htmlescape(print_r($reponse_info['request_header'], 1)); - } catch (Exception $e) { - $debug['Response'] = 'An error occured: ' . $e->getMessage(); - } - } - - return array_to_accordion($debug); + } else if (isset($params['text']) || $rtype === 'text') { + $debug['Response'] = stringBool($session->getObject('text')); + } else { + $debug['Response'] = pre_htmlescape(json_encode($session->getJSONObject(), JSON_PRETTY_PRINT + JSON_UNESCAPED_UNICODE)); + } + } + + $urls = $session->getResponsePathsAsLinks(); + if (!$session->hasError() AND ! empty($urls)) { + $locations = ''; + foreach ($urls AS $path => $link) { + $path = str_replace('/ocpu/tmp/' . $session->getKey(), '', $path); + $locations .= "$path
"; + } + $debug['Locations'] = $locations; + } + $debug['Session Info'] = pre_htmlescape($session->getInfo()); + $debug['Session Console'] = pre_htmlescape($session->getConsole()); + $debug['Session Stdout'] = pre_htmlescape($session->getStdout()); + $debug['Request'] = pre_htmlescape((string) $request); + + $reponse_headers = $session->getResponseHeaders(); + $debug['Response Headers'] = pre_htmlescape(print_r($reponse_headers, 1)); + + $reponse_info = $session->caller()->getRequestInfo(); + $debug['Request Headers'] = pre_htmlescape(print_r($reponse_info['request_header'], 1)); + } catch (Exception $e) { + $debug['Response'] = 'An error occured: ' . $e->getMessage(); + } + } + + return array_to_accordion($debug); } function opencpu_log($msg) { - $log = ''; - if ($msg instanceof Exception) { - $log .= $msg->getMessage() . "\n" . $msg->getTraceAsString(); - } else { - $log .= $msg; - } - error_log($log . "\n", 3, get_log_file('opencpu.log')); + $log = ''; + if ($msg instanceof Exception) { + $log .= $msg->getMessage() . "\n" . $msg->getTraceAsString(); + } else { + $log .= $msg; + } + error_log($log . "\n", 3, get_log_file('opencpu.log')); } function pre_htmlescape($str) { - return '
' . htmlspecialchars($str) . '
'; + return '
' . htmlspecialchars($str) . '
'; } function array_val($array, $key, $default = "") { - if (!is_array($array)) { - return false; - } - if (array_key_exists($key, $array)) { - return $array[$key]; - } - return $default; + if (!is_array($array)) { + return false; + } + if (array_key_exists($key, $array)) { + return $array[$key]; + } + return $default; } function shutdown_formr_org() { - $user = Site::getCurrentUser(); - if (is_object($user) && $user->cron) { - return; - } + $user = Site::getCurrentUser(); + if (is_object($user) && $user->cron) { + return; + } - $error = error_get_last(); - if($error !== null && $error['type'] === E_ERROR && !DEBUG) { - $errno = $error["type"]; - $errfile = $error["file"]; - $errline = $error["line"]; - $errstr = $error["message"]; + $error = error_get_last(); + if ($error !== null && $error['type'] === E_ERROR && !DEBUG) { + $errno = $error["type"]; + $errfile = $error["file"]; + $errline = $error["line"]; + $errstr = $error["message"]; - $msg = "A fatal error occured and your request could not be completed. Contact site admins with these details \n"; - $msg .= "Error [$errno] in $errfile line $errline \n $errstr"; - //alert($msg, 'alert-danger'); + $msg = "A fatal error occured and your request could not be completed. Contact site admins with these details \n"; + $msg .= "Error [$errno] in $errfile line $errline \n $errstr"; + //alert($msg, 'alert-danger'); - formr_error(500, 'Internal Server Error', nl2br($msg), 'Fatal Error'); - } + formr_error(500, 'Internal Server Error', nl2br($msg), 'Fatal Error'); + } } function remove_tag_wrapper($text, $tag = 'p') { - $text = trim($text); - if (preg_match("@^<{$tag}>(.+)$@", $text, $matches)) { - $text = isset($matches[1]) ? $matches[1] : $text; - } - return $text; + $text = trim($text); + if (preg_match("@^<{$tag}>(.+)$@", $text, $matches)) { + $text = isset($matches[1]) ? $matches[1] : $text; + } + return $text; } function delete_tmp_file($file) { - // unlink tmp file especially for the case of google sheets - if (!empty($file['tmp_name']) && file_exists($file['tmp_name'])) { - @unlink($file['tmp_name']); - } + // unlink tmp file especially for the case of google sheets + if (!empty($file['tmp_name']) && file_exists($file['tmp_name'])) { + @unlink($file['tmp_name']); + } } /** @@ -1448,44 +1442,44 @@ function delete_tmp_file($file) { * @return array|boolean Returns an array similar to that of an 'uploaded-php-file' or FALSE otherwise; */ function google_download_survey_sheet($survey_name, $google_link) { - $google_id = google_get_sheet_id($google_link); - if (!$google_id) { - return false; - } - - $destination_file = Config::get('survey_upload_dir') . '/googledownload-' . $google_id . '.xlsx'; - $google_download_link = "http://docs.google.com/spreadsheets/d/{$google_id}/export?format=xlsx&{$google_id}"; - $info = array(); - - try { - if (!is_writable(dirname($destination_file))) { - throw new Exception("The survey backup directory is not writable"); - } - $options = array( - CURLOPT_SSL_VERIFYHOST => 0, - CURLOPT_SSL_VERIFYPEER => 0, - ); - - CURL::DownloadUrl($google_download_link, $destination_file, null, CURL::HTTP_METHOD_GET, $options, $info); - if (empty($info['http_code']) || $info['http_code'] < 200 || $info['http_code'] > 302 || strstr($info['content_type'], "text/html") !== false) { - $link = google_get_sheet_link($google_id); - throw new Exception("The google sheet at {$link} could not be downloaded. Please make sure everyone with the link can access the sheet!"); - } - - $ret = array( - 'name' => $survey_name . '.xlsx', - 'tmp_name' => $destination_file, - 'size' => filesize($destination_file), - 'google_id' => $google_id, - 'google_link' => google_get_sheet_link($google_id), - 'google_download_link' => $google_download_link, - ); - } catch (Exception $e) { - formr_log_exception($e, 'CURL_DOWNLOAD', $google_link); - alert($e->getMessage(), 'alert-danger'); - $ret = false; - } - return $ret; + $google_id = google_get_sheet_id($google_link); + if (!$google_id) { + return false; + } + + $destination_file = Config::get('survey_upload_dir') . '/googledownload-' . $google_id . '.xlsx'; + $google_download_link = "http://docs.google.com/spreadsheets/d/{$google_id}/export?format=xlsx&{$google_id}"; + $info = array(); + + try { + if (!is_writable(dirname($destination_file))) { + throw new Exception("The survey backup directory is not writable"); + } + $options = array( + CURLOPT_SSL_VERIFYHOST => 0, + CURLOPT_SSL_VERIFYPEER => 0, + ); + + CURL::DownloadUrl($google_download_link, $destination_file, null, CURL::HTTP_METHOD_GET, $options, $info); + if (empty($info['http_code']) || $info['http_code'] < 200 || $info['http_code'] > 302 || strstr($info['content_type'], "text/html") !== false) { + $link = google_get_sheet_link($google_id); + throw new Exception("The google sheet at {$link} could not be downloaded. Please make sure everyone with the link can access the sheet!"); + } + + $ret = array( + 'name' => $survey_name . '.xlsx', + 'tmp_name' => $destination_file, + 'size' => filesize($destination_file), + 'google_id' => $google_id, + 'google_link' => google_get_sheet_link($google_id), + 'google_download_link' => $google_download_link, + ); + } catch (Exception $e) { + formr_log_exception($e, 'CURL_DOWNLOAD', $google_link); + alert($e->getMessage(), 'alert-danger'); + $ret = false; + } + return $ret; } /** @@ -1495,12 +1489,12 @@ function google_download_survey_sheet($survey_name, $google_link) { * @return string|null */ function google_get_sheet_id($link) { - $matches = array(); - preg_match('/spreadsheets\/d\/(.*)\/edit/', $link, $matches); - if (!empty($matches[1])) { - return $matches[1]; - } - return null; + $matches = array(); + preg_match('/spreadsheets\/d\/(.*)\/edit/', $link, $matches); + if (!empty($matches[1])) { + return $matches[1]; + } + return null; } /** @@ -1510,183 +1504,183 @@ function google_get_sheet_id($link) { * @return string */ function google_get_sheet_link($id) { - return "https://docs.google.com/spreadsheets/d/{$id}/edit"; + return "https://docs.google.com/spreadsheets/d/{$id}/edit"; } function strt_replace($str, $params) { - foreach ($params as $key => $value) { - $str = str_replace('%{'.$key.'}', $value, $str); - $str = str_replace('{'.$key.'}', $value, $str); - } - return $str; + foreach ($params as $key => $value) { + $str = str_replace('%{' . $key . '}', $value, $str); + $str = str_replace('{' . $key . '}', $value, $str); + } + return $str; } function fill_array($array, $value = '') { - foreach ($array as $key => $v) { - $array[$key] = $value; - } - return $array; + foreach ($array as $key => $v) { + $array[$key] = $value; + } + return $array; } function files_are_equal($a, $b) { - if (!file_exists($a) || !file_exists($b)) - return false; - - // Check if filesize is different - if (filesize($a) !== filesize($b)) - return false; + if (!file_exists($a) || !file_exists($b)) + return false; + + // Check if filesize is different + if (filesize($a) !== filesize($b)) + return false; - if (sha1_file($a) !== sha1_file($b)) - return false; + if (sha1_file($a) !== sha1_file($b)) + return false; - return true; + return true; } function create_zip_archive($files, $destination, $overwrite = false) { - $zip = new ZipArchive(); + $zip = new ZipArchive(); - if ($zip->open($destination, $overwrite ? ZIPARCHIVE::OVERWRITE : ZIPARCHIVE::CREATE) !== true) { - return false; - } + if ($zip->open($destination, $overwrite ? ZIPARCHIVE::OVERWRITE : ZIPARCHIVE::CREATE) !== true) { + return false; + } - //add the files - foreach($files as $file) { - if (is_file($file)) { - $zip->addFile($file, basename($file)); - } - } - $zip->close(); + //add the files + foreach ($files as $file) { + if (is_file($file)) { + $zip->addFile($file, basename($file)); + } + } + $zip->close(); - //check to make sure the file exists - return file_exists($destination); + //check to make sure the file exists + return file_exists($destination); } function create_ini_file($assoc, $filepath) { - file_put_contents($filepath, ''); - foreach ($assoc as $section => $fields) { - file_put_contents($filepath, "[{$section}]\n", FILE_APPEND); - foreach ($fields as $key => $value) { - file_put_contents($filepath, "{$key} = {$value}\n", FILE_APPEND); - } - file_put_contents($filepath, "\n", FILE_APPEND); - } - return file_exists($filepath); + file_put_contents($filepath, ''); + foreach ($assoc as $section => $fields) { + file_put_contents($filepath, "[{$section}]\n", FILE_APPEND); + foreach ($fields as $key => $value) { + file_put_contents($filepath, "{$key} = {$value}\n", FILE_APPEND); + } + file_put_contents($filepath, "\n", FILE_APPEND); + } + return file_exists($filepath); } function deletefiles($files) { - foreach($files as $file) { - if(is_file($file)) { - @unlink($file); - } - } + foreach ($files as $file) { + if (is_file($file)) { + @unlink($file); + } + } } function get_default_assets($config = 'site') { - if (DEBUG) { - return Config::get("default_assets.dev.{$config}"); - } else { - return Config::get("default_assets.prod.{$config}"); - } + if (DEBUG) { + return Config::get("default_assets.dev.{$config}"); + } else { + return Config::get("default_assets.prod.{$config}"); + } } function get_assets() { - return get_default_assets('assets'); + return get_default_assets('assets'); } function print_stylesheets($files, $id = null) { - foreach ($files as $i => $file) { - $id = 'css-' . $i . $id; - echo '' . "\n"; - } + foreach ($files as $i => $file) { + $id = 'css-' . $i . $id; + echo '' . "\n"; + } } function print_scripts($files, $id = null) { - foreach ($files as $i => $file) { - $id = 'js-' . $i . $id; - echo '' . "\n"; - } + foreach ($files as $i => $file) { + $id = 'js-' . $i . $id; + echo '' . "\n"; + } } function fwrite_json($handle, $data) { - if ($handle) { - fseek($handle, 0, SEEK_END); - if (ftell($handle) > 0) { - fseek($handle, -1, SEEK_END); - fwrite($handle, ',', 1); - fwrite($handle, "\n" . json_encode($data) . "]"); - } else { - fwrite($handle, json_encode(array($data))); - } - } + if ($handle) { + fseek($handle, 0, SEEK_END); + if (ftell($handle) > 0) { + fseek($handle, -1, SEEK_END); + fwrite($handle, ',', 1); + fwrite($handle, "\n" . json_encode($data) . "]"); + } else { + fwrite($handle, json_encode(array($data))); + } + } } function do_run_shortcodes($text, $run_name, $sess_code) { - $link_tpl = '%{text}'; - if ($run_name) { - $login_url = run_url($run_name, null, array('code' => $sess_code)); - $logout_url = run_url($run_name, 'logout', array('code' => $sess_code)); - $settings_url = run_url($run_name, 'settings', array('code' => $sess_code)); - } else { - $login_url = $settings_url = site_url(); - $logout_url = site_url('logout'); - //alert("Generated a login link, but no run was specified", 'alert-danger'); - } - - - $settings_link = Template::replace($link_tpl, array('url' => $settings_url, 'text' => 'Settings Link')); - $login_link = Template::replace($link_tpl, array('url' => $login_url, 'text' => 'Login Link')); - $logout_link = Template::replace($link_tpl, array('url' => $logout_url, 'text' => 'Logout Link')); - - $text = str_replace("{{login_link}}", $login_link, $text); - $text = str_replace("{{login_url}}", $login_url, $text); - $text = str_replace("{{login_code}}", urlencode($sess_code), $text); - $text = str_replace("{{settings_link}}", $settings_link, $text); - $text = str_replace("{{settings_url}}", $settings_url, $text); - $text = str_replace("{{logout_link}}", $logout_link, $text); - $text = str_replace("{{logout_url}}", $logout_url, $text); - $text = str_replace(urlencode("{{login_url}}"), $login_url, $text); - $text = str_replace(urlencode("{{login_code}}"), urlencode($sess_code), $text); - $text = str_replace(urlencode("{{settings_url}}"), $settings_url, $text); - $text = str_replace(urlencode("{{logout_url}}"), $logout_url, $text); - - return $text; + $link_tpl = '%{text}'; + if ($run_name) { + $login_url = run_url($run_name, null, array('code' => $sess_code)); + $logout_url = run_url($run_name, 'logout', array('code' => $sess_code)); + $settings_url = run_url($run_name, 'settings', array('code' => $sess_code)); + } else { + $login_url = $settings_url = site_url(); + $logout_url = site_url('logout'); + //alert("Generated a login link, but no run was specified", 'alert-danger'); + } + + + $settings_link = Template::replace($link_tpl, array('url' => $settings_url, 'text' => 'Settings Link')); + $login_link = Template::replace($link_tpl, array('url' => $login_url, 'text' => 'Login Link')); + $logout_link = Template::replace($link_tpl, array('url' => $logout_url, 'text' => 'Logout Link')); + + $text = str_replace("{{login_link}}", $login_link, $text); + $text = str_replace("{{login_url}}", $login_url, $text); + $text = str_replace("{{login_code}}", urlencode($sess_code), $text); + $text = str_replace("{{settings_link}}", $settings_link, $text); + $text = str_replace("{{settings_url}}", $settings_url, $text); + $text = str_replace("{{logout_link}}", $logout_link, $text); + $text = str_replace("{{logout_url}}", $logout_url, $text); + $text = str_replace(urlencode("{{login_url}}"), $login_url, $text); + $text = str_replace(urlencode("{{login_code}}"), urlencode($sess_code), $text); + $text = str_replace(urlencode("{{settings_url}}"), $settings_url, $text); + $text = str_replace(urlencode("{{logout_url}}"), $logout_url, $text); + + return $text; } function factortosecs($value, $unit) { - $factors = array( - 'seconds' => 1, - 'minutes' => 60, - 'hours' => 3600, - 'days' => 86400, - 'months' => 30 * 86400, - 'years' => 365 * 86400, - ); - - if (isset($factors[$unit])) { - return $value * $factors[$unit]; - } else { - return null; - } + $factors = array( + 'seconds' => 1, + 'minutes' => 60, + 'hours' => 3600, + 'days' => 86400, + 'months' => 30 * 86400, + 'years' => 365 * 86400, + ); + + if (isset($factors[$unit])) { + return $value * $factors[$unit]; + } else { + return null; + } } function secstofactor($seconds) { - if (!$seconds) { - return null; - } - - $factors = array( - 'years' => 365 * 86400, - 'months' => 30 * 86400, - 'days' => 86400, - 'hours' => 3600, - 'minutes' => 60, - 'seconds' => 1, - ); - - foreach ($factors as $unit => $factor) { - if ($seconds % $factor === 0) { - return array($seconds / $factor, $unit); - } - } - return array($seconds, 'seconds'); -} \ No newline at end of file + if (!$seconds) { + return null; + } + + $factors = array( + 'years' => 365 * 86400, + 'months' => 30 * 86400, + 'days' => 86400, + 'hours' => 3600, + 'minutes' => 60, + 'seconds' => 1, + ); + + foreach ($factors as $unit => $factor) { + if ($seconds % $factor === 0) { + return array($seconds / $factor, $unit); + } + } + return array($seconds, 'seconds'); +} diff --git a/application/Library/LogParser.php b/application/Library/LogParser.php index 28c3477a2..236eaa90c 100644 --- a/application/Library/LogParser.php +++ b/application/Library/LogParser.php @@ -1,101 +1,96 @@ '; - const LOG_MARKER_ALERTS_END = ''; - - const LOG_MARKER_START_GM = 'Processing run >>>'; + const LOG_MARKER = '----------'; + const LOG_MARKER_START = 'cron-run call start'; + const LOG_MARKER_END = 'cron-run call end'; + const LOG_MARKER_ALERTS_START = ''; + const LOG_MARKER_ALERTS_END = ''; + const LOG_MARKER_START_GM = 'Processing run >>>'; - public function __construct() { - - } + public function __construct() { + + } - public function getCronLogFiles() { - $search = APPLICATION_ROOT . 'tmp/logs/cron/*.log'; - $files = array(); - foreach (glob($search) as $file) { - $filename = str_replace('cron-run-', '', basename($file)); - $files[$filename] = $file; - } - return $files; - } + public function getCronLogFiles() { + $search = APPLICATION_ROOT . 'tmp/logs/cron/*.log'; + $files = array(); + foreach (glob($search) as $file) { + $filename = str_replace('cron-run-', '', basename($file)); + $files[$filename] = $file; + } + return $files; + } - public function printCronLogFile($file, $expand = false) { - $file = APPLICATION_ROOT . 'tmp/logs/cron/cron-run-' . $file; - if (!file_exists($file)) { - return null; - } + public function printCronLogFile($file, $expand = false) { + $file = APPLICATION_ROOT . 'tmp/logs/cron/cron-run-' . $file; + if (!file_exists($file)) { + return null; + } - $handle = fopen($file, "r"); - $id = 1; - $class = $expand ? ' in' : null; - $openRow = false; - if ($handle) { - while (($line = fgets($handle)) !== false) { - $line = trim($line); - if (!$line) { - continue; - } + $handle = fopen($file, "r"); + $id = 1; + $class = $expand ? ' in' : null; + $openRow = false; + if ($handle) { + while (($line = fgets($handle)) !== false) { + $line = trim($line); + if (!$line) { + continue; + } - if (strstr($line, self::LOG_MARKER)) { - $id++; - continue; - } + if (strstr($line, self::LOG_MARKER)) { + $id++; + continue; + } - if (strstr($line, self::LOG_MARKER_START) !== false || strstr($line, self::LOG_MARKER_START_GM) !== false) { - if ($openRow === true) { - echo ''; - $id++; - } - $openRow = true; - $id_str = 'log-entry-' . $id; - $time = strtotime($this->stripLineTime($line, true)); - echo '
'; - - echo '
'; - $date = 'Cron run: ' . date('Y-m-d H:i', $time); - echo ''; - echo '
'; - - echo '
'; - } elseif (strstr($line, self::LOG_MARKER_ALERTS_START) !== false) { - echo '
'; - } elseif (strstr($line, self::LOG_MARKER_ALERTS_END) !== false) { - echo '
'; - } else { - echo $this->stripLineTime($line); - } - } + if (strstr($line, self::LOG_MARKER_START) !== false || strstr($line, self::LOG_MARKER_START_GM) !== false) { + if ($openRow === true) { + echo ''; + $id++; + } + $openRow = true; + $id_str = 'log-entry-' . $id; + $time = strtotime($this->stripLineTime($line, true)); + echo '
'; - fclose($handle); - } else { - echo 'an error occured while trying to open log file'; - } - } + echo '
'; + $date = 'Cron run: ' . date('Y-m-d H:i', $time); + echo ''; + echo '
'; - protected function stripLineTime($line, $returntime = false) { - $pattern = '/(?P[0-9]{1,4}-[0-9]{1,2}-[0-9]{1,2} [0-9]{1,2}:[0-9]{1,2}:[0-9]{1,2})/'; - $matches = array(); - if ($returntime === true) { - preg_match($pattern, $line, $matches); - if (isset($matches['datetime'])) { - return $matches['datetime']; - } - return 0; - } - - return preg_replace($pattern, '', str_replace('
', '', $line)); - } + echo '
'; + } elseif (strstr($line, self::LOG_MARKER_ALERTS_START) !== false) { + echo '
'; + } elseif (strstr($line, self::LOG_MARKER_ALERTS_END) !== false) { + echo '
'; + } else { + echo $this->stripLineTime($line); + } + } + + fclose($handle); + } else { + echo 'an error occured while trying to open log file'; + } + } + + protected function stripLineTime($line, $returntime = false) { + $pattern = '/(?P[0-9]{1,4}-[0-9]{1,2}-[0-9]{1,2} [0-9]{1,2}:[0-9]{1,2}:[0-9]{1,2})/'; + $matches = array(); + if ($returntime === true) { + preg_match($pattern, $line, $matches); + if (isset($matches['datetime'])) { + return $matches['datetime']; + } + return 0; + } + + return preg_replace($pattern, '', str_replace('
', '', $line)); + } } diff --git a/application/Library/OSF.php b/application/Library/OSF.php index e2fd5f512..e8e2cfb03 100644 --- a/application/Library/OSF.php +++ b/application/Library/OSF.php @@ -5,386 +5,378 @@ * Class responsible for handling oauth and node operations * */ - class OSF { - /** - * OSF API entry point - * - * @var string - */ - protected $api = 'https://api.osf.io/v2'; - - /** - * URI to exchange for access token - * - * @var string - */ - protected $token_uri = 'https://accounts.osf.io/oauth2/token'; - - /** - * URL to redirect to for authorization - * - * @var string - */ - protected $authorization_uri = 'https://accounts.osf.io/oauth2/authorize'; - - /** - * Application client id - * - * @var string - */ - protected $client_id; - - /** - * Application client secret - * - * @var string - */ - protected $client_secret; - - /** - * Access token object stored here after authorization - * - * @var array - */ - protected $access_token = array(); - - protected $redirect_url; - - protected $scope; - - protected $state = 'formr-osf-ugaAJAuTlg'; - - /** - * Are we authenticated from an https protocol - * - * @var boolean - */ - protected $is_https = false; - - /** - * Class can be initialized with a set of config parameters - * - * @param array $config - */ - public function __construct($config = array()) { - if ($config && is_array($config)) { - foreach ($config as $property => $value) { - if (property_exists($this, $property)) { - $this->{$property} = $value; - } - } - } - } - - /** - * Set access token. Array should contain @access_token and @expires entries - * - * @param array $access_token - */ - public function setAccessToken(array $access_token) { - $this->access_token = $access_token; - } - - /** - * Method to request access token, called from login() - * Extending Adapters may override this. - * - * @param array $params - * @return array - */ - protected function getAccessToken($params) { - return (array )$this->fetch($this->token_uri, $params, CURL::HTTP_METHOD_POST); - } - - /** - * Login method called from web with adapter specific parameters. - * - * After this method is called a valid session should be created that - * can be passed as parameter to profile() method. - * - * @param $params - * @param $token_params - * @return array parameters containing key => val pairs that uniquely identify the session - */ - public function login($params, array $token_params = array()) { - if (!empty($params['error'])) { - throw new OSF_Exception($params['error']); - } - - if (empty($params['code'])) { - throw new OSF_Exception("code is required"); - } - - // Set paramters tha are necessary for getting access token - if (!$token_params) { - $token_params = array( - 'client_id' => $this->client_id, - 'client_secret' => $this->client_secret, - 'grant_type' => 'authorization_code', - 'code' => $params['code'], - 'redirect_uri' => $this->getConnectUrl(), - ); - } - - // pack params urlencoded instead of array, or curl will use multipart/form-data as Content-Type - $token_params = http_build_query($token_params, null, '&'); - // TODO: To access API service after access token expired, need to get another access token with refresh token. - $result = $this->getAccessToken($token_params); - if (isset($result['expires'])) { - // convert to timestamp, as relative expire has no use later - $result['expires'] = time() + $result['expires']; - } elseif (isset($result['expires_in'])) { - // convert to timestamp, as relative expire has no use later - $result['expires'] = time() + $result['expires_in']; - } - - // save for debugging - $result['code'] = $params['code']; - - return $result; - } - - /** - * Fetch URL, decodes json response and checks for basic errors - * - * @throws OSF_Exception - * @param string $url - * @param array $params Url Parameters - * @param string $method GET or POST - * @param bool $json - * @return mixed json decoded data - */ - protected function fetch($url, $params = array(), $method = CURL::HTTP_METHOD_GET, $json = true, $curlopts = array()) { - $curlopts += $this->curlOpts(); - - $content = CURL::HttpRequest($url, $params, $method, $curlopts); - - /** @var $result mixed */ - $result = null; - if ($json) { - $result = json_decode($content); - } else { - // For Adapters that don't return a json object but a query string - parse_str($content, $result); - $result = (object)$result; - } - - if ($result === null) { - $url = CURL::urljoin($url, $params); - throw new OSF_Exception("Failed to parse response"); - } - - if (!empty($result->error)) { - if (isset($result->error->subcode)) { - throw new OSF_Exception($result->error->message, $result->error->code, $result->error->subcode, $result->error->type); - - } elseif (isset($result->error->code)) { - throw new OSF_Exception($result->error->message, $result->error->code, 0, $result->error->type); - - } elseif (isset($result->error->type)) { - throw new OSF_Exception($result->error->message, 0, 0, $result->error->type); - - } elseif (isset($result->error->description)) { - throw new OSF_Exception($result->error->description); - - } elseif (isset($result->error_description)) { - throw new OSF_Exception($result->error_description); - - } else { - throw new OSF_Exception($result->error); - } - } - - return $result; - } - - /** - * Get url where adapter should return on connecting. - * - * @return string - */ - public function getConnectUrl() { - return $this->redirect_url; - } - - /** - * URL to which user should be redirected to for login - * @return string - */ - public function getLoginUrl() { - $url = CURL::urljoin($this->authorization_uri, array( - 'client_id' => $this->client_id, - 'scope' => $this->scope, - 'response_type' => 'code', - 'redirect_uri' => $this->getConnectUrl(), - 'state' => $this->state, - 'display' => 'popup', - )); - return $url; - } - - public static function getUserAccessToken(User $user) { - $db = DB::getInstance(); - $row = $db->findRow('osf', array('user_id' => $user->id)); - if (!$row || $row['access_token_expires'] < time()) { - return false; - } - return array( - 'access_token' => $row['access_token'], - 'expires' => $row['access_token_expires'], - ); - } - - public static function setUserAccessToken(User $user, $token) { - $db = DB::getInstance(); - $db->insert_update('osf', array( - 'user_id' => $user->id, - 'access_token' => $token['access_token'], - 'access_token_expires' => $token['expires'], - ), array( - 'access_token', 'access_token_expires' - )); - } - - /** - * Upload a file under a particular OSF node - * - * @param string $node_id OSF node id - * @param string $file absolute path to file - * @param string $osf_file name of file on the OSF server - * - * @uses access_token - * @return OSF_Response - * @throws OSF_Exception - */ - public function upload($node_id, $file, $osf_file) { - if (!file_exists($file)) { - throw new OSF_Exception("Requested file not found"); - } - - $info = null; - $params = array('format' => 'json', '_'=>time()); - $url = $this->api . '/nodes/' . $node_id . '/files/'; - try { - $files_json = CURL::HttpRequest($url, $params, CURL::HTTP_METHOD_GET, $this->curlOpts(), $info); - } catch (Exception $e) { - $files_json = $this->wrapError($e->getMessage()); - } - - $response = new OSF_Response($files_json, $info); - if ($response->hasError()) { - return $response; - } - - $links = $response->getJSON()->data[0]->links; - $curlopts = $this->curlOpts(); - $curlopts[CURLOPT_POSTFIELDS] = file_get_contents($file); - $upload_url = $links->upload . '?' . http_build_query(array('kind' => 'file', 'name' => $osf_file)); - $uploaded = CURL::HttpRequest($upload_url, array('file' => CURL::getPostFileParam($file)), CURL::HTTP_METHOD_PUT, $curlopts, $info); - - return new OSF_Response($uploaded, $info); - } - - /** - * Retrieve project list of particular user - * - * @param string $user OSF id of that user. Defaults to 'me' for authenticated user - * @return OSF_Response - * @throws OSF_Exception - */ - public function getProjects($user = 'me') { - $params = array('format' => 'json'); - $info = null; - - try { - // first get user information to obtain nodes_api for that user - $url = $this->api . '/users/' . $user; - $json = CURL::HttpRequest($url, $params, CURL::HTTP_METHOD_GET, $this->curlOpts(), $info); - $userResponse = new OSF_Response($json, $info); - if ($userResponse->hasError()) { - return $userResponse; - } - $nodesApi = $userResponse->getJSON()->data->links->self . 'nodes/'; - - // get project list from user's nodes - $params['filter'] = array('category' => 'project'); - $json = CURL::HttpRequest($nodesApi, $params, CURL::HTTP_METHOD_GET, $this->curlOpts(), $info); - } catch (Exception $e) { - $json = $this->wrapError($e->getMessage()); - } - - return new OSF_Response($json, $info); - } - - protected function curlOpts() { - $curlopts = array(); - - if (!$this->is_https) { - $curlopts[CURLOPT_SSL_VERIFYHOST] = 0; - $curlopts[CURLOPT_SSL_VERIFYPEER] = 0; - } - - if ($this->access_token) { - $curlopts[CURLOPT_HTTPHEADER] = array("Authorization: Bearer {$this->access_token['access_token']}"); - } - return $curlopts; - } - - private function wrapError($error) { - return json_encode(array('errors' => array(array('detail' => $error)))); - } + /** + * OSF API entry point + * + * @var string + */ + protected $api = 'https://api.osf.io/v2'; + + /** + * URI to exchange for access token + * + * @var string + */ + protected $token_uri = 'https://accounts.osf.io/oauth2/token'; + + /** + * URL to redirect to for authorization + * + * @var string + */ + protected $authorization_uri = 'https://accounts.osf.io/oauth2/authorize'; + + /** + * Application client id + * + * @var string + */ + protected $client_id; + + /** + * Application client secret + * + * @var string + */ + protected $client_secret; + + /** + * Access token object stored here after authorization + * + * @var array + */ + protected $access_token = array(); + protected $redirect_url; + protected $scope; + protected $state = 'formr-osf-ugaAJAuTlg'; + + /** + * Are we authenticated from an https protocol + * + * @var boolean + */ + protected $is_https = false; + + /** + * Class can be initialized with a set of config parameters + * + * @param array $config + */ + public function __construct($config = array()) { + if ($config && is_array($config)) { + foreach ($config as $property => $value) { + if (property_exists($this, $property)) { + $this->{$property} = $value; + } + } + } + } + + /** + * Set access token. Array should contain @access_token and @expires entries + * + * @param array $access_token + */ + public function setAccessToken(array $access_token) { + $this->access_token = $access_token; + } + + /** + * Method to request access token, called from login() + * Extending Adapters may override this. + * + * @param array $params + * @return array + */ + protected function getAccessToken($params) { + return (array) $this->fetch($this->token_uri, $params, CURL::HTTP_METHOD_POST); + } + + /** + * Login method called from web with adapter specific parameters. + * + * After this method is called a valid session should be created that + * can be passed as parameter to profile() method. + * + * @param $params + * @param $token_params + * @return array parameters containing key => val pairs that uniquely identify the session + */ + public function login($params, array $token_params = array()) { + if (!empty($params['error'])) { + throw new OSF_Exception($params['error']); + } + + if (empty($params['code'])) { + throw new OSF_Exception("code is required"); + } + + // Set paramters tha are necessary for getting access token + if (!$token_params) { + $token_params = array( + 'client_id' => $this->client_id, + 'client_secret' => $this->client_secret, + 'grant_type' => 'authorization_code', + 'code' => $params['code'], + 'redirect_uri' => $this->getConnectUrl(), + ); + } + + // pack params urlencoded instead of array, or curl will use multipart/form-data as Content-Type + $token_params = http_build_query($token_params, null, '&'); + // TODO: To access API service after access token expired, need to get another access token with refresh token. + $result = $this->getAccessToken($token_params); + if (isset($result['expires'])) { + // convert to timestamp, as relative expire has no use later + $result['expires'] = time() + $result['expires']; + } elseif (isset($result['expires_in'])) { + // convert to timestamp, as relative expire has no use later + $result['expires'] = time() + $result['expires_in']; + } + + // save for debugging + $result['code'] = $params['code']; + + return $result; + } + + /** + * Fetch URL, decodes json response and checks for basic errors + * + * @throws OSF_Exception + * @param string $url + * @param array $params Url Parameters + * @param string $method GET or POST + * @param bool $json + * @return mixed json decoded data + */ + protected function fetch($url, $params = array(), $method = CURL::HTTP_METHOD_GET, $json = true, $curlopts = array()) { + $curlopts += $this->curlOpts(); + + $content = CURL::HttpRequest($url, $params, $method, $curlopts); + + /** @var $result mixed */ + $result = null; + if ($json) { + $result = json_decode($content); + } else { + // For Adapters that don't return a json object but a query string + parse_str($content, $result); + $result = (object) $result; + } + + if ($result === null) { + $url = CURL::urljoin($url, $params); + throw new OSF_Exception("Failed to parse response"); + } + + if (!empty($result->error)) { + if (isset($result->error->subcode)) { + throw new OSF_Exception($result->error->message, $result->error->code, $result->error->subcode, $result->error->type); + } elseif (isset($result->error->code)) { + throw new OSF_Exception($result->error->message, $result->error->code, 0, $result->error->type); + } elseif (isset($result->error->type)) { + throw new OSF_Exception($result->error->message, 0, 0, $result->error->type); + } elseif (isset($result->error->description)) { + throw new OSF_Exception($result->error->description); + } elseif (isset($result->error_description)) { + throw new OSF_Exception($result->error_description); + } else { + throw new OSF_Exception($result->error); + } + } + + return $result; + } + + /** + * Get url where adapter should return on connecting. + * + * @return string + */ + public function getConnectUrl() { + return $this->redirect_url; + } + + /** + * URL to which user should be redirected to for login + * @return string + */ + public function getLoginUrl() { + $url = CURL::urljoin($this->authorization_uri, array( + 'client_id' => $this->client_id, + 'scope' => $this->scope, + 'response_type' => 'code', + 'redirect_uri' => $this->getConnectUrl(), + 'state' => $this->state, + 'display' => 'popup', + )); + return $url; + } + + public static function getUserAccessToken(User $user) { + $db = DB::getInstance(); + $row = $db->findRow('osf', array('user_id' => $user->id)); + if (!$row || $row['access_token_expires'] < time()) { + return false; + } + return array( + 'access_token' => $row['access_token'], + 'expires' => $row['access_token_expires'], + ); + } + + public static function setUserAccessToken(User $user, $token) { + $db = DB::getInstance(); + $db->insert_update('osf', array( + 'user_id' => $user->id, + 'access_token' => $token['access_token'], + 'access_token_expires' => $token['expires'], + ), array( + 'access_token', 'access_token_expires' + )); + } + + /** + * Upload a file under a particular OSF node + * + * @param string $node_id OSF node id + * @param string $file absolute path to file + * @param string $osf_file name of file on the OSF server + * + * @uses access_token + * @return OSF_Response + * @throws OSF_Exception + */ + public function upload($node_id, $file, $osf_file) { + if (!file_exists($file)) { + throw new OSF_Exception("Requested file not found"); + } + + $info = null; + $params = array('format' => 'json', '_' => time()); + $url = $this->api . '/nodes/' . $node_id . '/files/'; + try { + $files_json = CURL::HttpRequest($url, $params, CURL::HTTP_METHOD_GET, $this->curlOpts(), $info); + } catch (Exception $e) { + $files_json = $this->wrapError($e->getMessage()); + } + + $response = new OSF_Response($files_json, $info); + if ($response->hasError()) { + return $response; + } + + $links = $response->getJSON()->data[0]->links; + $curlopts = $this->curlOpts(); + $curlopts[CURLOPT_POSTFIELDS] = file_get_contents($file); + $upload_url = $links->upload . '?' . http_build_query(array('kind' => 'file', 'name' => $osf_file)); + $uploaded = CURL::HttpRequest($upload_url, array('file' => CURL::getPostFileParam($file)), CURL::HTTP_METHOD_PUT, $curlopts, $info); + + return new OSF_Response($uploaded, $info); + } + + /** + * Retrieve project list of particular user + * + * @param string $user OSF id of that user. Defaults to 'me' for authenticated user + * @return OSF_Response + * @throws OSF_Exception + */ + public function getProjects($user = 'me') { + $params = array('format' => 'json'); + $info = null; + + try { + // first get user information to obtain nodes_api for that user + $url = $this->api . '/users/' . $user; + $json = CURL::HttpRequest($url, $params, CURL::HTTP_METHOD_GET, $this->curlOpts(), $info); + $userResponse = new OSF_Response($json, $info); + if ($userResponse->hasError()) { + return $userResponse; + } + $nodesApi = $userResponse->getJSON()->data->links->self . 'nodes/'; + + // get project list from user's nodes + $params['filter'] = array('category' => 'project'); + $json = CURL::HttpRequest($nodesApi, $params, CURL::HTTP_METHOD_GET, $this->curlOpts(), $info); + } catch (Exception $e) { + $json = $this->wrapError($e->getMessage()); + } + + return new OSF_Response($json, $info); + } + + protected function curlOpts() { + $curlopts = array(); + + if (!$this->is_https) { + $curlopts[CURLOPT_SSL_VERIFYHOST] = 0; + $curlopts[CURLOPT_SSL_VERIFYPEER] = 0; + } + + if ($this->access_token) { + $curlopts[CURLOPT_HTTPHEADER] = array("Authorization: Bearer {$this->access_token['access_token']}"); + } + return $curlopts; + } + + private function wrapError($error) { + return json_encode(array('errors' => array(array('detail' => $error)))); + } + } class OSF_Response { - protected $json; - - protected $json_string; - - protected $http_info = array(); - - public function __construct($string, array $http_info = array()) { - $this->json_string = $string; - $this->json = @json_decode($string); - $this->http_info = $http_info; - } - - public function hasError() { - if (!empty($this->json->errors)) { - return true; - } - return isset($this->http_info['http_code']) && ($this->http_info['http_code'] < 200 || $this->http_info['http_code'] > 302); - } - - public function getError() { - if (!empty($this->json->errors)) { - $err = array(); - foreach ($this->json->errors as $error) { - $err[] = $error->detail; - } - return implode(".\n ", $err); - } - return isset($this->json->message) ? $this->json->message : null; - } - - public function getErrorCode() { - return isset($this->json->code) ? $this->json->code : null; - } - - public function getJSON() { - return $this->json; - } - - public function getJSONString() { - $this->json_string; - } - - public function getHttpInfo() { - return $this->http_info; - } -} + protected $json; + protected $json_string; + protected $http_info = array(); + + public function __construct($string, array $http_info = array()) { + $this->json_string = $string; + $this->json = @json_decode($string); + $this->http_info = $http_info; + } + + public function hasError() { + if (!empty($this->json->errors)) { + return true; + } + return isset($this->http_info['http_code']) && ($this->http_info['http_code'] < 200 || $this->http_info['http_code'] > 302); + } + + public function getError() { + if (!empty($this->json->errors)) { + $err = array(); + foreach ($this->json->errors as $error) { + $err[] = $error->detail; + } + return implode(".\n ", $err); + } + return isset($this->json->message) ? $this->json->message : null; + } + + public function getErrorCode() { + return isset($this->json->code) ? $this->json->code : null; + } + + public function getJSON() { + return $this->json; + } + + public function getJSONString() { + $this->json_string; + } + + public function getHttpInfo() { + return $this->http_info; + } -class OSF_Exception extends Exception {} +} +class OSF_Exception extends Exception { + +} diff --git a/application/Library/OpenCPU.php b/application/Library/OpenCPU.php index b470c928e..1f7c4c451 100644 --- a/application/Library/OpenCPU.php +++ b/application/Library/OpenCPU.php @@ -2,523 +2,527 @@ class OpenCPU { - protected $baseUrl = 'https://public.opencpu.org'; - protected $libUri = '/ocpu/library'; - protected $last_message = null; - protected $rLibPath = '/usr/local/lib/R/site-library'; - - const STRING_DELIMITER = "\n\n==========formr=opencpu=string=delimiter==========\n\n"; - const TEMP_BASE_URL = "__formr_opencpu_session_url__"; - const STRING_DELIMITER_PARSED = "

==========formr=opencpu=string=delimiter==========

"; - - /** - * @var OpenCPU[] - */ - protected static $instances = array(); - /** - * @var OpenCPU_Session[] - */ - protected $cache = array(); - - /** - * Additional curl options to set when making curl request - * - * @var Array - */ - private $curl_opts = array( - CURLINFO_HEADER_OUT => true, - CURLOPT_IPRESOLVE => CURL_IPRESOLVE_V4, - CURLOPT_HEADER => true, - CURLOPT_ENCODING => "" - ); - - private $curl_info = array(); - - /** - * @var OpenCPU_Request - */ - protected $request; - - /** - * Get an instance of OpenCPU class - * - * @param string $instance Config item name that holds opencpu base URL - * @return OpenCPU - */ - public static function getInstance($instance = 'opencpu_instance') { - if (!isset(self::$instances[$instance])) { - self::$instances[$instance] = new self($instance); - } - return self::$instances[$instance]; - } - - protected function __construct($instance) { - $config = (array) Config::get($instance); - foreach ($config as $key => $value) { - $property = lcfirst(preg_replace('/\s+/', '', ucwords(str_replace('_', ' ', $key)))); - if (property_exists($this, $property)) { - $this->{$property} = $value; - } - } - } - - /** - * @param string $baseUrl - */ - public function setBaseUrl($baseUrl) { - if ($baseUrl) { - $baseUrl = rtrim($baseUrl, "/"); - $this->baseUrl = $baseUrl; - } - } - - public function getBaseUrl() { - return $this->baseUrl; - } - - public function getRLibPath() { - return $this->rLibPath; - } - public function getRTempBaseUrl() { - return self::TEMP_BASE_URL; - } - - public function setLibUrl($libUri) { - $libUri = trim($libUri, "/"); - $this->libUri = '/' . $libUri; - } - - public function getLibUri() { - return $this->libUri; - } - - public function getLastMessage() { - return $this->last_message; - } - - /** - * @return OpenCPU_Request - */ - public function getRequest() { - return $this->request; - } - - /** - * Get Response headers of opencpu request - * - * @return null|array - */ - public function getResponseHeaders() { - if (isset($this->curl_info[CURL::RESPONSE_HEADERS])) { - return $this->curl_info[CURL::RESPONSE_HEADERS]; - } elseif(isset($this->curl_info['raw_header'])) { - return http_parse_headers($this->curl_info['raw_header']); - } - return null; - } - - public function getRequestInfo($item = null) { - if ($item && isset($this->curl_info[$item])) { - return $this->curl_info[$item]; - } elseif ($item) { - return null; - } - return $this->curl_info; - } - - private function call($uri = '', $params = array(), $method = CURL::HTTP_METHOD_GET) { - $cachekey = md5(serialize(func_get_args())); - if (isset($this->cache[$cachekey])) { - return $this->cache[$cachekey]; - } - - - if ($uri && strstr($uri, $this->baseUrl) === false) { - $uri = "/" . ltrim($uri, "/"); - $url = $this->baseUrl . $this->libUri . $uri; - } else { - $url = $uri; - } - - // set global props - $this->curl_info = array(); - $this->request = new OpenCPU_Request($url, $method, $params); - - $curl_opts = $this->curl_opts; - // encode request - if ($method === CURL::HTTP_METHOD_POST) { - $params = array_map(array($this, 'cr2nl'), $params); - $params = http_build_query($params); - $curl_opts = $this->curl_opts + array(CURLOPT_HTTPHEADER => array( - 'Content-Length: ' . strlen($params), - )); - } - - // Maybe something bad happen in CURL request just throw it with OpenCPU_Exception with message returned from CURL - try { - $results = CURL::HttpRequest($url, $params, $method, $curl_opts, $this->curl_info); - } catch (Exception $e) { - throw new OpenCPU_Exception($e->getMessage(), -1, $e); - } - - if ($this->curl_info['http_code'] == 400) { - $results = "R Error: $results"; - return new OpenCPU_Session(null, null, $results, $this); - } elseif ($this->curl_info['http_code'] < 200 || $this->curl_info['http_code'] > 302) { - if (!$results) { - $results = "OpenCPU server '{$this->baseUrl}' could not be contacted"; - } - throw new OpenCPU_Exception($results, $this->curl_info['http_code']); - } - - $headers = $this->getResponseHeaders(); - if ($method === CURL::HTTP_METHOD_GET) { - $headers['Location'] = $url; - if(preg_match("@/(x0[a-z0-9-_~]+)/@",$url, $matches)): - $headers['X-Ocpu-Session'] = $matches[1]; - endif; - } - if (!$headers || empty($headers['Location']) || empty($headers['X-Ocpu-Session'])) { - $request = sprintf('[uri %s] %s', $uri, print_r($params, 1)); - throw new OpenCPU_Exception("Response headers not gotten from request $request"); - } - - $this->cache[$cachekey] = new OpenCPU_Session($headers['Location'], $headers['X-Ocpu-Session'], $results, $this); - return $this->cache[$cachekey]; - } - - /** - * Send a POST request to OpenCPU - * - * @param string $uri A uri that is relative to openCPU's library entry point for example '/markdown/R/render' - * @param array $params An array of parameters to pass - * @throws OpenCPU_Exception - * @return OpenCPU_Session - */ - public function post($uri = '', $params = array()) { - return $this->call($uri, $params, CURL::HTTP_METHOD_POST); - } - - /** - * Send a GET request to OpenCPU - * - * @param string $uri A uri that is relative to openCPU's library entry point for example '/markdown/R/render' - * @param array $params An array of parameters to pass - * @throws OpenCPU_Exception - * @return OpenCPU_Session - */ - public function get($uri = '', $params = array()) { - return $this->call($uri, $params, CURL::HTTP_METHOD_GET); - } - - /** - * Execute a snippet of R code - * - * @param string $code - * @return OpenCPU_Session - */ - public function snippet($code) { - $params = array('x' => '{ + protected $baseUrl = 'https://public.opencpu.org'; + protected $libUri = '/ocpu/library'; + protected $last_message = null; + protected $rLibPath = '/usr/local/lib/R/site-library'; + + const STRING_DELIMITER = "\n\n==========formr=opencpu=string=delimiter==========\n\n"; + const TEMP_BASE_URL = "__formr_opencpu_session_url__"; + const STRING_DELIMITER_PARSED = "

==========formr=opencpu=string=delimiter==========

"; + + /** + * @var OpenCPU[] + */ + protected static $instances = array(); + + /** + * @var OpenCPU_Session[] + */ + protected $cache = array(); + + /** + * Additional curl options to set when making curl request + * + * @var Array + */ + private $curl_opts = array( + CURLINFO_HEADER_OUT => true, + CURLOPT_IPRESOLVE => CURL_IPRESOLVE_V4, + CURLOPT_HEADER => true, + CURLOPT_ENCODING => "" + ); + private $curl_info = array(); + + /** + * @var OpenCPU_Request + */ + protected $request; + + /** + * Get an instance of OpenCPU class + * + * @param string $instance Config item name that holds opencpu base URL + * @return OpenCPU + */ + public static function getInstance($instance = 'opencpu_instance') { + if (!isset(self::$instances[$instance])) { + self::$instances[$instance] = new self($instance); + } + return self::$instances[$instance]; + } + + protected function __construct($instance) { + $config = (array) Config::get($instance); + foreach ($config as $key => $value) { + $property = lcfirst(preg_replace('/\s+/', '', ucwords(str_replace('_', ' ', $key)))); + if (property_exists($this, $property)) { + $this->{$property} = $value; + } + } + } + + /** + * @param string $baseUrl + */ + public function setBaseUrl($baseUrl) { + if ($baseUrl) { + $baseUrl = rtrim($baseUrl, "/"); + $this->baseUrl = $baseUrl; + } + } + + public function getBaseUrl() { + return $this->baseUrl; + } + + public function getRLibPath() { + return $this->rLibPath; + } + + public function getRTempBaseUrl() { + return self::TEMP_BASE_URL; + } + + public function setLibUrl($libUri) { + $libUri = trim($libUri, "/"); + $this->libUri = '/' . $libUri; + } + + public function getLibUri() { + return $this->libUri; + } + + public function getLastMessage() { + return $this->last_message; + } + + /** + * @return OpenCPU_Request + */ + public function getRequest() { + return $this->request; + } + + /** + * Get Response headers of opencpu request + * + * @return null|array + */ + public function getResponseHeaders() { + if (isset($this->curl_info[CURL::RESPONSE_HEADERS])) { + return $this->curl_info[CURL::RESPONSE_HEADERS]; + } elseif (isset($this->curl_info['raw_header'])) { + return http_parse_headers($this->curl_info['raw_header']); + } + return null; + } + + public function getRequestInfo($item = null) { + if ($item && isset($this->curl_info[$item])) { + return $this->curl_info[$item]; + } elseif ($item) { + return null; + } + return $this->curl_info; + } + + private function call($uri = '', $params = array(), $method = CURL::HTTP_METHOD_GET) { + $cachekey = md5(serialize(func_get_args())); + if (isset($this->cache[$cachekey])) { + return $this->cache[$cachekey]; + } + + + if ($uri && strstr($uri, $this->baseUrl) === false) { + $uri = "/" . ltrim($uri, "/"); + $url = $this->baseUrl . $this->libUri . $uri; + } else { + $url = $uri; + } + + // set global props + $this->curl_info = array(); + $this->request = new OpenCPU_Request($url, $method, $params); + + $curl_opts = $this->curl_opts; + // encode request + if ($method === CURL::HTTP_METHOD_POST) { + $params = array_map(array($this, 'cr2nl'), $params); + $params = http_build_query($params); + $curl_opts = $this->curl_opts + array(CURLOPT_HTTPHEADER => array( + 'Content-Length: ' . strlen($params), + )); + } + + // Maybe something bad happen in CURL request just throw it with OpenCPU_Exception with message returned from CURL + try { + $results = CURL::HttpRequest($url, $params, $method, $curl_opts, $this->curl_info); + } catch (Exception $e) { + throw new OpenCPU_Exception($e->getMessage(), -1, $e); + } + + if ($this->curl_info['http_code'] == 400) { + $results = "R Error: $results"; + return new OpenCPU_Session(null, null, $results, $this); + } elseif ($this->curl_info['http_code'] < 200 || $this->curl_info['http_code'] > 302) { + if (!$results) { + $results = "OpenCPU server '{$this->baseUrl}' could not be contacted"; + } + throw new OpenCPU_Exception($results, $this->curl_info['http_code']); + } + + $headers = $this->getResponseHeaders(); + if ($method === CURL::HTTP_METHOD_GET) { + $headers['Location'] = $url; + if (preg_match("@/(x0[a-z0-9-_~]+)/@", $url, $matches)): + $headers['X-Ocpu-Session'] = $matches[1]; + endif; + } + if (!$headers || empty($headers['Location']) || empty($headers['X-Ocpu-Session'])) { + $request = sprintf('[uri %s] %s', $uri, print_r($params, 1)); + throw new OpenCPU_Exception("Response headers not gotten from request $request"); + } + + $this->cache[$cachekey] = new OpenCPU_Session($headers['Location'], $headers['X-Ocpu-Session'], $results, $this); + return $this->cache[$cachekey]; + } + + /** + * Send a POST request to OpenCPU + * + * @param string $uri A uri that is relative to openCPU's library entry point for example '/markdown/R/render' + * @param array $params An array of parameters to pass + * @throws OpenCPU_Exception + * @return OpenCPU_Session + */ + public function post($uri = '', $params = array()) { + return $this->call($uri, $params, CURL::HTTP_METHOD_POST); + } + + /** + * Send a GET request to OpenCPU + * + * @param string $uri A uri that is relative to openCPU's library entry point for example '/markdown/R/render' + * @param array $params An array of parameters to pass + * @throws OpenCPU_Exception + * @return OpenCPU_Session + */ + public function get($uri = '', $params = array()) { + return $this->call($uri, $params, CURL::HTTP_METHOD_GET); + } + + /** + * Execute a snippet of R code + * + * @param string $code + * @return OpenCPU_Session + */ + public function snippet($code) { + $params = array('x' => '{ (function() { ' . $code . ' })()}'); - return $this->post('/base/R/identity', $params); - } + return $this->post('/base/R/identity', $params); + } - private function cr2nl($string) { - return str_replace("\r\n", "\n", $string); - } + private function cr2nl($string) { + return str_replace("\r\n", "\n", $string); + } } class OpenCPU_Session { - /** - * @var string - */ - protected $raw_result; - - /** - * @var string - */ - protected $key; - - /** - * @var string - */ - protected $location; - - /** - * @var OpenCPU - */ - private $ocpu; - - /** - * @var integer - */ - private $object_length = null; - - public function __construct($location, $key, $raw_result, OpenCPU $ocpu = null) { - $this->raw_result = $raw_result; - $this->key = $key; - $this->location = $location; - $this->ocpu = $ocpu; - } - - /** - * Returns the list of returned paths as a string separated by newline char - * - * @return string - */ - public function getRawResult() { - return $this->raw_result; - } - - /** - * @return OpenCPU_Request - */ - public function getRequest() { - return $this->ocpu->getRequest(); - } - - public function isJSONResult() { - return ($this->ocpu->getRequestInfo("content_type") === "application/json"); - } - - /** - * Returns the list of returned paths as a string separated by newline char - * - * @param bool $as_array If TRUE, paths will be returned in an array - * @return string|array - */ - public function getResponse($as_array = false) { - if ($as_array === true) { - return explode("\n", $this->raw_result); - } - return $this->raw_result; - } - - public function getKey() { - return $this->key; - } - - /** - * Get an array of files present in current session - * - * @param string $match You can match only files with some slug in the path name - * @param string $baseURL URL segment to prepend to paths - * @return array - */ - public function getFiles($match = '/files/', $baseURL = null) { - if (!$this->key) { - return null; - } - - $files = array(); - $result = explode("\n", $this->raw_result); - foreach ($result as $path) { - if (!$path || strpos($path, $match) === false) { - continue; - } - - $id = basename($path); - $files[$id] = $baseURL ? $baseURL . $path : $this->getResponsePath($path); - } - return $files; - } - - /** - * Get absolute URLs of all resources in the response - * - * @return array - */ - public function getResponsePaths() { - if (!$this->key || $this->isJSONResult()) { - return null; - } - - $result = explode("\n", $this->raw_result); - $files = array(); - foreach ($result as $id => $path) { - $files[$id] = $this->getResponsePath($path); - } - return $files; - } - public function getResponsePathsAsLinks() { - if (!$this->key || $this->isJSONResult()) { - return null; - } - - $result = explode("\n", $this->raw_result); - $files = array(); - foreach ($result as $path) { - $files[$path] = $this->getResponsePath($path); - } - return $files; - } - - public function getLocation() { - return $this->location; - } - - public function getFileURL($path) { - return $this->getResponsePath('/files/' . $path); - } - - public function getObject($name = 'json', $params = array()) { - if (!$this->key) { - return null; - } - - $url = $this->getLocation() . 'R/.val/' . $name; - $info = array(); // just in case needed in the furture to get curl info - $object = CURL::HttpRequest($url, $params, $method = CURL::HTTP_METHOD_GET, array(), $info); - if ($name === 'json') { - $object = $this->getJSONObject($object); - } - if(is_string($object)) { - $object = str_replace($this->ocpu->getRLibPath(), $this->getBaseUrl() . $this->ocpu->getLibUri(), $object); - return str_replace($this->ocpu->getRTempBaseUrl() , $this->getLocation().'files/', $object); - } - - return $object; - } - - public function getJSONObject($string = null, $as_assoc = true) { - if (!$this->key) { - return null; - } - - if ($string === null) { - $string = $this->raw_result; - } - $json = json_decode($string, $as_assoc); - $this->object_length = count($json); - // if decoded object is a non-empty array, get it's first element - if (is_array($json) && array_key_exists(0, $json)) { - if(is_string($json[0])) { - $string = str_replace($this->ocpu->getRLibPath(), $this->getBaseUrl() . $this->ocpu->getLibUri(), $json[0]); - return str_replace($this->ocpu->getRTempBaseUrl() , $this->getLocation().'files/', $string); - } - return $json[0]; - } - - return $json; - } - - public function getObjectLength() { - return $this->object_length; - } - - public function getStdout() { - if (!$this->key) { - return null; - } - - $url = $this->getLocation() . 'stdout'; - $info = array(); // just in case needed in the furture to get curl info - return CURL::HttpRequest($url, null, $method = CURL::HTTP_METHOD_GET, array(), $info); - } - - public function getConsole() { - if (!$this->key) { - return null; - } - - $url = $this->getLocation() . 'console'; - $info = array(); // just in case needed in the furture to get curl info - return CURL::HttpRequest($url, null, $method = CURL::HTTP_METHOD_GET, array(), $info); - } - - public function getInfo() { - if (!$this->key) { - return null; - } - - $url = $this->getLocation() . 'info'; - $info = array(); // just in case needed in the furture to get curl info - return CURL::HttpRequest($url, null, $method = CURL::HTTP_METHOD_GET, array(), $info); - } - - public function hasError() { - return $this->ocpu->getRequestInfo('http_code') >= 400; - } - - public function getError() { - if (!$this->hasError()) { - return null; - } - return $this->raw_result; - } - - /** - * @return OpenCPU - */ - public function caller() { - return $this->ocpu; - } - - public function getResponseHeaders() { - return $this->caller()->getResponseHeaders(); - } - - public function getBaseUrl() { - return $this->caller()->getBaseUrl(); - } - - protected function getResponsePath($path) { - return $this->caller()->getBaseUrl() . $path; - } + /** + * @var string + */ + protected $raw_result; + + /** + * @var string + */ + protected $key; + + /** + * @var string + */ + protected $location; + + /** + * @var OpenCPU + */ + private $ocpu; + + /** + * @var integer + */ + private $object_length = null; + + public function __construct($location, $key, $raw_result, OpenCPU $ocpu = null) { + $this->raw_result = $raw_result; + $this->key = $key; + $this->location = $location; + $this->ocpu = $ocpu; + } + + /** + * Returns the list of returned paths as a string separated by newline char + * + * @return string + */ + public function getRawResult() { + return $this->raw_result; + } + + /** + * @return OpenCPU_Request + */ + public function getRequest() { + return $this->ocpu->getRequest(); + } + + public function isJSONResult() { + return ($this->ocpu->getRequestInfo("content_type") === "application/json"); + } + + /** + * Returns the list of returned paths as a string separated by newline char + * + * @param bool $as_array If TRUE, paths will be returned in an array + * @return string|array + */ + public function getResponse($as_array = false) { + if ($as_array === true) { + return explode("\n", $this->raw_result); + } + return $this->raw_result; + } + + public function getKey() { + return $this->key; + } + + /** + * Get an array of files present in current session + * + * @param string $match You can match only files with some slug in the path name + * @param string $baseURL URL segment to prepend to paths + * @return array + */ + public function getFiles($match = '/files/', $baseURL = null) { + if (!$this->key) { + return null; + } + + $files = array(); + $result = explode("\n", $this->raw_result); + foreach ($result as $path) { + if (!$path || strpos($path, $match) === false) { + continue; + } + + $id = basename($path); + $files[$id] = $baseURL ? $baseURL . $path : $this->getResponsePath($path); + } + return $files; + } + + /** + * Get absolute URLs of all resources in the response + * + * @return array + */ + public function getResponsePaths() { + if (!$this->key || $this->isJSONResult()) { + return null; + } + + $result = explode("\n", $this->raw_result); + $files = array(); + foreach ($result as $id => $path) { + $files[$id] = $this->getResponsePath($path); + } + return $files; + } + + public function getResponsePathsAsLinks() { + if (!$this->key || $this->isJSONResult()) { + return null; + } + + $result = explode("\n", $this->raw_result); + $files = array(); + foreach ($result as $path) { + $files[$path] = $this->getResponsePath($path); + } + return $files; + } + + public function getLocation() { + return $this->location; + } + + public function getFileURL($path) { + return $this->getResponsePath('/files/' . $path); + } + + public function getObject($name = 'json', $params = array()) { + if (!$this->key) { + return null; + } + + $url = $this->getLocation() . 'R/.val/' . $name; + $info = array(); // just in case needed in the furture to get curl info + $object = CURL::HttpRequest($url, $params, $method = CURL::HTTP_METHOD_GET, array(), $info); + if ($name === 'json') { + $object = $this->getJSONObject($object); + } + if (is_string($object)) { + $object = str_replace($this->ocpu->getRLibPath(), $this->getBaseUrl() . $this->ocpu->getLibUri(), $object); + return str_replace($this->ocpu->getRTempBaseUrl(), $this->getLocation() . 'files/', $object); + } + + return $object; + } + + public function getJSONObject($string = null, $as_assoc = true) { + if (!$this->key) { + return null; + } + + if ($string === null) { + $string = $this->raw_result; + } + $json = json_decode($string, $as_assoc); + $this->object_length = count($json); + // if decoded object is a non-empty array, get it's first element + if (is_array($json) && array_key_exists(0, $json)) { + if (is_string($json[0])) { + $string = str_replace($this->ocpu->getRLibPath(), $this->getBaseUrl() . $this->ocpu->getLibUri(), $json[0]); + return str_replace($this->ocpu->getRTempBaseUrl(), $this->getLocation() . 'files/', $string); + } + return $json[0]; + } + + return $json; + } + + public function getObjectLength() { + return $this->object_length; + } + + public function getStdout() { + if (!$this->key) { + return null; + } + + $url = $this->getLocation() . 'stdout'; + $info = array(); // just in case needed in the furture to get curl info + return CURL::HttpRequest($url, null, $method = CURL::HTTP_METHOD_GET, array(), $info); + } + + public function getConsole() { + if (!$this->key) { + return null; + } + + $url = $this->getLocation() . 'console'; + $info = array(); // just in case needed in the furture to get curl info + return CURL::HttpRequest($url, null, $method = CURL::HTTP_METHOD_GET, array(), $info); + } + + public function getInfo() { + if (!$this->key) { + return null; + } + + $url = $this->getLocation() . 'info'; + $info = array(); // just in case needed in the furture to get curl info + return CURL::HttpRequest($url, null, $method = CURL::HTTP_METHOD_GET, array(), $info); + } + + public function hasError() { + return $this->ocpu->getRequestInfo('http_code') >= 400; + } + + public function getError() { + if (!$this->hasError()) { + return null; + } + return $this->raw_result; + } + + /** + * @return OpenCPU + */ + public function caller() { + return $this->ocpu; + } + + public function getResponseHeaders() { + return $this->caller()->getResponseHeaders(); + } + + public function getBaseUrl() { + return $this->caller()->getBaseUrl(); + } + + protected function getResponsePath($path) { + return $this->caller()->getBaseUrl() . $path; + } + } class OpenCPU_Request { - protected $url; - - protected $params; - - protected $method; - - public function __construct($url, $method, $params = null) { - $this->url = $url; - $this->method = $method; - $this->params = $params; - } - - public function getUrl() { - return $this->url; - } - - public function getMethod() { - return $this->method; - } + protected $url; + protected $params; + protected $method; + + public function __construct($url, $method, $params = null) { + $this->url = $url; + $this->method = $method; + $this->params = $params; + } + + public function getUrl() { + return $this->url; + } + + public function getMethod() { + return $this->method; + } + + public function getParams() { + return $this->params; + } + + public function __toString() { + $request = array("METHOD: {$this->method}", "URL: {$this->url}", "PARAMS: " . $this->stringify($this->params)); + return implode("\n", $request); + } + + protected function stringify($object) { + if (is_string($object)) { + return $object; + } + + $string = "\n"; + if (is_array($object)) { + foreach ($object as $key => $value) { + $value = $this->stringify($value); + $string .= "{$key} = {$value} \n"; + } + } else { + $string .= (string) $object; + } + + return $string; + } - public function getParams() { - return $this->params; - } - - public function __toString() { - $request = array("METHOD: {$this->method}", "URL: {$this->url}", "PARAMS: " . $this->stringify($this->params)); - return implode("\n", $request); - } - - protected function stringify($object) { - if (is_string($object)) { - return $object; - } - - $string = "\n"; - if (is_array($object)) { - foreach ($object as $key => $value) { - $value = $this->stringify($value); - $string .= "{$key} = {$value} \n"; - } - } else { - $string .= (string) $object; - } - - return $string; - } } -class OpenCPU_Exception extends Exception {} +class OpenCPU_Exception extends Exception { + +} diff --git a/application/Library/Queue.php b/application/Library/Queue.php index f48b6e0aa..425fa8ab2 100644 --- a/application/Library/Queue.php +++ b/application/Library/Queue.php @@ -6,127 +6,125 @@ */ class Queue { - /** - * - * @var DB - */ - protected $db; - - /** - * Interval in seconds for which loop should be rested - * - * @var int - */ - protected $loopInterval; - - /** - * Flag used to exit or stay in run loop - * - * @var boolean - */ - protected $out = false; - - /** - * Number of times mailer is allowed to sleep before exiting - * - * @var int - */ - protected $allowedSleeps = 120; - - /** - * Number of seconds mailer should sleep before checking if there is something in queue - * - * @var int - */ - protected $sleep = 15; - - protected $logFile = 'queue.log'; - - protected $name = 'Formr-Queue'; - - /** - * Configuration passed to queue - * - * @var array - */ - protected $config = array(); - - protected $debug = false; - - public function __construct(DB $db, array $config) { - $this->db = $db; - $this->config = $config; - $this->loopInterval = array_val($this->config, 'queue_loop_interval', 5); - $this->debug = array_val($this->config, 'debug', false); - - // Register signal handlers that should be able to kill the cron in case some other weird shit happens - // apart from cron exiting cleanly - // declare signal handlers - if (extension_loaded('pcntl')) { - declare(ticks = 1); - - pcntl_signal(SIGINT, array(&$this, 'interrupt')); - pcntl_signal(SIGTERM, array(&$this, 'interrupt')); - pcntl_signal(SIGUSR1, array(&$this, 'interrupt')); - } else { - $this->debug = true; - $this->dbg('pcntl extension is not loaded'); - } - } - - public function run() { - return true; - } - - protected function dbg($str) { - $args = func_get_args(); - if (count($args) > 1) { - $str = vsprintf(array_shift($args), $args); - } - - $str = date('Y-m-d H:i:s') . ' '. $this->name .': ' . $str . PHP_EOL; - if (DEBUG) { - echo $str; - return; - } - return error_log($str, 3, get_log_file($this->logFile)); - } - - protected function rested() { - static $last_access; - if (!is_null($last_access) && $this->loopInterval > ($usleep = (microtime(true) - $last_access))) { - usleep(1000000 * ($this->loopInterval - $usleep)); - } - - $last_access = microtime(true); - return true; - } - - /** - * Signal handler - * - * @param integer $signo - */ - public function interrupt($signo) { - switch ($signo) { - // Set terminated flag to be able to terminate program securely - // to prevent from terminating in the middle of the process - // Use Ctrl+C to send interruption signal to a running program - case SIGINT: - case SIGTERM: - $this->out = true; - $this->dbg("%s Received termination signal", getmypid()); - break; - - // switch the debug mode on/off - // @example: $ kill -s SIGUSR1 - case SIGUSR1: - if (($this->debug = !$this->debug)) { - $this->dbg("\nEntering debug mode...\n"); - } else { - $this->dbg("\nLeaving debug mode...\n"); - } - break; - } - } + /** + * + * @var DB + */ + protected $db; + + /** + * Interval in seconds for which loop should be rested + * + * @var int + */ + protected $loopInterval; + + /** + * Flag used to exit or stay in run loop + * + * @var boolean + */ + protected $out = false; + + /** + * Number of times mailer is allowed to sleep before exiting + * + * @var int + */ + protected $allowedSleeps = 120; + + /** + * Number of seconds mailer should sleep before checking if there is something in queue + * + * @var int + */ + protected $sleep = 15; + protected $logFile = 'queue.log'; + protected $name = 'Formr-Queue'; + + /** + * Configuration passed to queue + * + * @var array + */ + protected $config = array(); + protected $debug = false; + + public function __construct(DB $db, array $config) { + $this->db = $db; + $this->config = $config; + $this->loopInterval = array_val($this->config, 'queue_loop_interval', 5); + $this->debug = array_val($this->config, 'debug', false); + + // Register signal handlers that should be able to kill the cron in case some other weird shit happens + // apart from cron exiting cleanly + // declare signal handlers + if (extension_loaded('pcntl')) { + declare(ticks = 1); + + pcntl_signal(SIGINT, array(&$this, 'interrupt')); + pcntl_signal(SIGTERM, array(&$this, 'interrupt')); + pcntl_signal(SIGUSR1, array(&$this, 'interrupt')); + } else { + $this->debug = true; + $this->dbg('pcntl extension is not loaded'); + } + } + + public function run() { + return true; + } + + protected function dbg($str) { + $args = func_get_args(); + if (count($args) > 1) { + $str = vsprintf(array_shift($args), $args); + } + + $str = date('Y-m-d H:i:s') . ' ' . $this->name . ': ' . $str . PHP_EOL; + if (DEBUG) { + echo $str; + return; + } + return error_log($str, 3, get_log_file($this->logFile)); + } + + protected function rested() { + static $last_access; + if (!is_null($last_access) && $this->loopInterval > ($usleep = (microtime(true) - $last_access))) { + usleep(1000000 * ($this->loopInterval - $usleep)); + } + + $last_access = microtime(true); + return true; + } + + /** + * Signal handler + * + * @param integer $signo + */ + public function interrupt($signo) { + switch ($signo) { + // Set terminated flag to be able to terminate program securely + // to prevent from terminating in the middle of the process + // Use Ctrl+C to send interruption signal to a running program + case SIGINT: + case SIGTERM: + $this->out = true; + $this->dbg("%s Received termination signal", getmypid()); + break; + + // switch the debug mode on/off + // @example: $ kill -s SIGUSR1 + case SIGUSR1: + if (($this->debug = !$this->debug)) { + $this->dbg("\nEntering debug mode...\n"); + } else { + $this->dbg("\nLeaving debug mode...\n"); + } + break; + } + } + } diff --git a/application/Library/Request.php b/application/Library/Request.php index a79d06781..9d92ae55e 100644 --- a/application/Library/Request.php +++ b/application/Library/Request.php @@ -2,212 +2,230 @@ class Request { - private $data = array(); - - private static $globals = array(); - - /** - * @param array $data - */ - public function __construct($data = null) { - if ($data === null) { - $data = $_REQUEST; - } - - if (is_array($data)) { - foreach ($data as $key => $value) { - $this->__set($key, $value); - } - } - } - - /** - * @param string $name - */ - public function __isset($name) { - return isset($this->data[$name]); - } - - /** - * @param string $name - * @param mixed $value - */ - public function __set($name, $value) { - $this->data[$name] = self::stripslashes(self::stripControlChars($value)); - } - - /** - * @param string $name - * @return mixed - */ - public function __get($name) { - if ($this->__isset($name)) { - return $this->data[$name]; - } - - return null; - } - - /** - * Get all parameters from $_REQUEST variable - * - * @return array - */ - public function getParams() { - return $this->data; - } - - /** - * Get parameter - * - * @param string $name - * @return mixed - */ - public function getParam($name, $default = null) { - $param = $this->__get($name); - if ($param === null) { - $param = $default; - } - return $param; - } - - /** - * Recursively input clean control characters (low bits in ASCII table) - * - * @param array|mixed|string $value - * @return array|mixed|string - */ - public function stripControlChars($value) { - if (is_array($value)) { - foreach ($value as $key => $val) { - $value[$key] = self::stripControlChars($val); - } - } else { - // strip control chars, backspace and delete (including \r) - $value = preg_replace('/[\x00-\x08\x0b-\x1f\x7f]/', '', $value); - } - - return $value; - } - - /** - * Access a request parameter as int - * - * @param string $name Parameter name - * @param mixed $default Default to return if parameter isn't set or is an array - * @param bool $nonempty Return $default if parameter is set but empty() - * @return int - */ - public function int($name, $default = 0, $nonempty = false) { - if (!isset($this->data[$name])) return $default; - if (is_array($this->data[$name])) return $default; - if ($this->data[$name] === '') return $default; - if ($nonempty && empty($this->data[$name])) return $default; - - return (int)$this->data[$name]; - } - - /** - * Access a request parameter as string - * - * @param string $name Parameter name - * @param mixed $default Default to return if parameter isn't set or is an array - * @param bool $nonempty Return $default if parameter is set but empty() - * @return string - */ - public function str($name, $default = '', $nonempty = false) { - if (!isset($this->data[$name])) return $default; - if (is_array($this->data[$name])) return $default; - if ($nonempty && empty($this->data[$name])) return $default; - - return (string)$this->data[$name]; - } - - /** - * Access a request parameter as bool - * - * Note: $nonempty is here for interface consistency and makes not much sense for booleans - * - * @param string $name Parameter name - * @param mixed $default Default to return if parameter isn't set - * @param bool $nonempty Return $default if parameter is set but empty() - * @return bool - */ - public function bool($name, $default = false, $nonempty = false) { - if (!isset($this->data[$name])) return $default; - if (is_array($this->data[$name])) return $default; - if ($this->data[$name] === '') return $default; - if ($nonempty && empty($this->data[$name])) return $default; - - return (bool)$this->data[$name]; - } - - /** - * Access a request parameter as array - * - * @param string $name Parameter name - * @param mixed $default Default to return if parameter isn't set - * @param bool $nonempty Return $default if parameter is set but empty() - * @return array - */ - public function arr($name, $default = array(), $nonempty = false) { - if (!isset($this->data[$name])) return $default; - if (!is_array($this->data[$name])) return $default; - if ($nonempty && empty($this->data[$name])) return $default; - - return (array)$this->data[$name]; - } - - /** - * Access a request parameter as float - * - * @param string $name Parameter name - * @param mixed $default Default to return if parameter isn't set or is an array - * @param bool $nonempty Return $default if parameter is set but empty() - * @return float - */ - public function float($name, $default = 0, $nonempty = false) { - if (!isset($this->data[$name])) return $default; - if (is_array($this->data[$name])) return $default; - if ($this->data[$name] === '') return $default; - if ($nonempty && empty($this->data[$name])) return $default; - - return (float)$this->data[$name]; - } - - public static function isHTTPPostRequest() { - return strtolower($_SERVER['REQUEST_METHOD']) === 'post'; - } - - public static function isHTTPGetRequest() { - return strtolower($_SERVER['REQUEST_METHOD']) === 'get'; - } - - public static function isAjaxRequest() { - return strtolower(env('HTTP_X_REQUESTED_WITH')) === 'xmlhttprequest'; - } - - public static function setGlobals($key, $value) { - self::$globals[$key] = $value; - } - - public static function getGlobals($key, $default = null) { - return isset(self::$globals[$key]) ? self::$globals[$key] : $default; - } - - private static function stripslashes($value) { - // skip objects (object.toString() results in wrong output) - if (!is_object($value) && !is_array($value)) { - if (get_magic_quotes_gpc() == 1) { - $value = stripslashes($value); - } - // object is array - } elseif (is_array($value)) { - foreach ($value as $k => $v) { - $value[$k] = self::stripslashes($v); - } - } - - return $value; - } + private $data = array(); + private static $globals = array(); + + /** + * @param array $data + */ + public function __construct($data = null) { + if ($data === null) { + $data = $_REQUEST; + } + + if (is_array($data)) { + foreach ($data as $key => $value) { + $this->__set($key, $value); + } + } + } + + /** + * @param string $name + */ + public function __isset($name) { + return isset($this->data[$name]); + } + + /** + * @param string $name + * @param mixed $value + */ + public function __set($name, $value) { + $this->data[$name] = self::stripslashes(self::stripControlChars($value)); + } + + /** + * @param string $name + * @return mixed + */ + public function __get($name) { + if ($this->__isset($name)) { + return $this->data[$name]; + } + + return null; + } + + /** + * Get all parameters from $_REQUEST variable + * + * @return array + */ + public function getParams() { + return $this->data; + } + + /** + * Get parameter + * + * @param string $name + * @return mixed + */ + public function getParam($name, $default = null) { + $param = $this->__get($name); + if ($param === null) { + $param = $default; + } + return $param; + } + + /** + * Recursively input clean control characters (low bits in ASCII table) + * + * @param array|mixed|string $value + * @return array|mixed|string + */ + public function stripControlChars($value) { + if (is_array($value)) { + foreach ($value as $key => $val) { + $value[$key] = self::stripControlChars($val); + } + } else { + // strip control chars, backspace and delete (including \r) + $value = preg_replace('/[\x00-\x08\x0b-\x1f\x7f]/', '', $value); + } + + return $value; + } + + /** + * Access a request parameter as int + * + * @param string $name Parameter name + * @param mixed $default Default to return if parameter isn't set or is an array + * @param bool $nonempty Return $default if parameter is set but empty() + * @return int + */ + public function int($name, $default = 0, $nonempty = false) { + if(!isset($this-> data[$name])) + return $default; + if (is_array($this->data[$name])) + return $default; + if ($this->data[$name] === '') + return $default; + if ($nonempty && empty($this->data[$name])) + return $default; + + return (int) $this->data[$name]; + } + + /** + * Access a request parameter as string + * + * @param string $name Parameter name + * @param mixed $default Default to return if parameter isn't set or is an array + * @param bool $nonempty Return $default if parameter is set but empty() + * @return string + */ + public function str($name, $default = '', $nonempty = false) { + if (!isset($this->data[$name])) + return $default; + if (is_array($this->data[$name])) + return $default; + if ($nonempty && empty($this->data[$name])) + return $default; + + return (string) $this->data[$name]; + } + + /** + * Access a request parameter as bool + * + * Note: $nonempty is here for interface consistency and makes not much sense for booleans + * + * @param string $name Parameter name + * @param mixed $default Default to return if parameter isn't set + * @param bool $nonempty Return $default if parameter is set but empty() + * @return bool + */ + public function bool($name, $default = false, $nonempty = false) { + if(!isset($this-> data[$name])) + return $default; + if (is_array($this->data[$name])) + return $default; + if ($this->data[$name] === '') + return $default; + if ($nonempty && empty($this->data[$name])) + return $default; + + return (bool) $this->data[$name]; + } + + /** + * Access a request parameter as array + * + * @param string $name Parameter name + * @param mixed $default Default to return if parameter isn't set + * @param bool $nonempty Return $default if parameter is set but empty() + * @return array + */ + public function arr($name, $default = array(), $nonempty = false) { + if (!isset($this->data[$name])) + return $default; + if (!is_array($this->data[$name])) + return $default; + if ($nonempty && empty($this->data[$name])) + return $default; + + return (array) $this->data[$name]; + } + + /** + * Access a request parameter as float + * + * @param string $name Parameter name + * @param mixed $default Default to return if parameter isn't set or is an array + * @param bool $nonempty Return $default if parameter is set but empty() + * @return float + */ + public function float($name, $default = 0, $nonempty = false) { + if(!isset($this-> data[$name])) + return $default; + if (is_array($this->data[$name])) + return $default; + if ($this->data[$name] === '') + return $default; + if ($nonempty && empty($this->data[$name])) + return $default; + + return (float) $this->data[$name]; + } + + public static function isHTTPPostRequest() { + return strtolower($_SERVER['REQUEST_METHOD']) === 'post'; + } + + public static function isHTTPGetRequest() { + return strtolower($_SERVER['REQUEST_METHOD']) === 'get'; + } + + public static function isAjaxRequest() { + return strtolower(env('HTTP_X_REQUESTED_WITH')) === 'xmlhttprequest'; + } + + public static function setGlobals($key, $value) { + self::$globals[$key] = $value; + } + + public static function getGlobals($key, $default = null) { + return isset(self::$globals[$key]) ? self::$globals[$key] : $default; + } + + private static function stripslashes($value) { + // skip objects (object.toString() results in wrong output) + if (!is_object($value) && !is_array($value)) { + if (get_magic_quotes_gpc() == 1) { + $value = stripslashes($value); + } + // object is array + } elseif (is_array($value)) { + foreach ($value as $k => $v) { + $value[$k] = self::stripslashes($v); + } + } + + return $value; + } + } diff --git a/application/Library/Response.php b/application/Library/Response.php index dd1cf325b..a2b97aea2 100644 --- a/application/Library/Response.php +++ b/application/Library/Response.php @@ -2,277 +2,277 @@ class Response { - /** HTTP status codes */ - const STATUS_OK = 200; - const STATUS_CREATED = 201; - const STATUS_NO_CONTENT = 204; - const STATUS_NOT_MODIFIED = 304; - const STATUS_TEMPORARY_REDIRECT = 307; - const STATUS_PERMANENT_REDIRECT = 308; - const STATUS_BAD_REQUEST = 400; - const STATUS_UNAUTHORIZED = 401; - const STATUS_FORBIDDEN = 403; - const STATUS_NOT_FOUND = 404; - const STATUS_METHOD_NOT_ALLOWED = 405; - const STATUS_UNPROCESSABLE_ENTITY = 422; - const STATUS_INTERNAL_SERVER_ERROR = 500; - const STATUS_SERVICE_UNAVAILABLE = 503; - const STATUS_GATEWAY_TIMEOUT = 504; - - /** - * @var string - */ - protected $content; - - /** - * @var string - */ - protected $contentType; - - /** - * @var array - */ - protected $config; - - /** - * @param array $config - */ - public function __construct($config = array()) { - $this->config = $config; - } - - /** - * Sets the response status code. - * - * @param integer $code HTTP status code - * @param mixed $text HTTP status text - * @return Response - */ - public function setStatusCode($code, $text = null) { - $code = (int) $code; - $text = $text ? $text : "Status $code"; - // status - header(sprintf('HTTP/1.0 %s %s', $code, $text)); - - return $this; - } - - /** - * Resource not found - * @return void - */ - public function notFound() { - $this->setStatusCode(self::STATUS_NOT_FOUND, 'Resource Not Found'); - $this->setHeader('Pragma', 'no-cache'); - exit; - } - - /** - * Bad request: may be cached, it won't improve if fetched again - * - * @param $message string - * @return void - */ - public function badRequest($message = "") { - $this->setStatusCode(self::STATUS_BAD_REQUEST, 'Bad Request'); - if ($message) { - echo "

{$message}

"; - } - exit; - } - - /** - * Not modified: the request was verified to be identical with previous cache - * - * @param $etag - * @return void - */ - public function notModified($etag) { - // if not modified, save some cpu and bandwidth - $this->setStatusCode(self::STATUS_NOT_MODIFIED, 'Not Modified'); - $this->setEtag($etag); - exit; - } - - /** - * Forbidden, access to this resource is not allowed - * - * @return void - */ - public function forbidden() { - $this->setStatusCode(self::STATUS_FORBIDDEN, 'Forbidden'); - exit; - } - - /** - * Internal/Fatal Error - * - * @return void - */ - public function fatalError() { - $this->setStatusCode(self::STATUS_INTERNAL_SERVER_ERROR, 'Internal Error'); - exit; - } - - /** - * Gateway timeout - * - * @return void - */ - public function gatewayTimeout() { - $this->setStatusCode(self::STATUS_GATEWAY_TIMEOUT, 'Gateway Timeout'); - exit; - } - - public function badMethod($message = '') { - $this->setStatusCode(self::STATUS_METHOD_NOT_ALLOWED, 'Method Not Allowed'); - if ($message) { - echo "

{$message}

"; - } - exit; - } - - /** - * Set Content-Type HTTP header - * - * @param string $content_type - * @return Response - */ - public function setContentType($content_type) { - $this->contentType = $content_type; - return $this->setHeader('Content-Type', $content_type); - } - - /** - * Get Content-Type HTTP header - * - * @return string - * @since 1.3 - */ - public function getContentType() { - return $this->contentType; - } - - /** - * Set Content-Length HTTP header - * - * @param int $length - * @return Response - */ - public function setContentLength($length) { - return $this->setHeader('Content-Length', $length); - } - - /** - * Sets the response content. - * - * Valid types are strings, numbers, and objects that implement a __toString() method. - * - * @param mixed $content - * @return Response - * @throws UnexpectedValueException - */ - public function setContent($content) { - if (null !== $content && !is_string($content) && !is_numeric($content) && !is_callable(array($content, '__toString'))) { - throw new UnexpectedValueException('The Response content must be a string or object implementing __toString(), "' . gettype($content) . '" given.'); - } - - $this->content = (string) $content; - - return $this; - } - - /** - * Set JSON response content - * - * @param mixed $content Can be of any type except a resource. - * @return Response - * @throws UnexpectedValueException - */ - public function setJsonContent($content) { - if (is_string($content)) { - return $this->setContent($content); - } - - if (!$content = json_encode($content)) { - throw new UnexpectedValueException('The Response content cannot be json encoded'); - } - - return $this->setContent($content); - } - - /** - * Gets the current response content. - * - * @return string Content - */ - public function getContent() { - return $this->content; - } - - /** - * Sets the ETag value. - * - * @param string $etag The ETag unique identifier - * @return Response - */ - public function setEtag($etag) { - return $this->setHeader('Etag', '"' . $etag . '"'); - } - - /** - * Set HTTP Response header. - * - * @param string $header - * @param string $value - * @param bool $replace - * @return Response - */ - public function setHeader($header, $value, $replace = true) { - header("$header: $value", $replace); - - return $this; - } - - /** - * Marks the response as "private". - * It makes the response ineligible for serving other clients. - * - * @return Response - */ - public function setPrivate() { - return $this->setHeader('Cache-Control', 'private'); - } - - /** - * Send caching header - * - * @param string $duration Any string supported my PHP's strtotime() function (http://php.net/manual/en/function.strtotime.php) - * @param bool $public - * @return Response - */ - public function setCacheHeaders($duration, $public = true) { - $time = strtotime($duration); - if (!$time) { - return $this; - } - - $max_age = $time - time(); - $cache = $public ? 'public' : 'private'; - return $this->setHeader('Cache-Control', $cache . ', max-age=' . $max_age); - } - - /** - * Send out Response content. - * Note that this ends the request - * - * @return void - */ - public function send() { - if ($this->content) { - echo $this->content; - } - exit; - } + /** HTTP status codes */ + const STATUS_OK = 200; + const STATUS_CREATED = 201; + const STATUS_NO_CONTENT = 204; + const STATUS_NOT_MODIFIED = 304; + const STATUS_TEMPORARY_REDIRECT = 307; + const STATUS_PERMANENT_REDIRECT = 308; + const STATUS_BAD_REQUEST = 400; + const STATUS_UNAUTHORIZED = 401; + const STATUS_FORBIDDEN = 403; + const STATUS_NOT_FOUND = 404; + const STATUS_METHOD_NOT_ALLOWED = 405; + const STATUS_UNPROCESSABLE_ENTITY = 422; + const STATUS_INTERNAL_SERVER_ERROR = 500; + const STATUS_SERVICE_UNAVAILABLE = 503; + const STATUS_GATEWAY_TIMEOUT = 504; + + /** + * @var string + */ + protected $content; + + /** + * @var string + */ + protected $contentType; + + /** + * @var array + */ + protected $config; + + /** + * @param array $config + */ + public function __construct($config = array()) { + $this->config = $config; + } + + /** + * Sets the response status code. + * + * @param integer $code HTTP status code + * @param mixed $text HTTP status text + * @return Response + */ + public function setStatusCode($code, $text = null) { + $code = (int) $code; + $text = $text ? $text : "Status $code"; + // status + header(sprintf('HTTP/1.0 %s %s', $code, $text)); + + return $this; + } + + /** + * Resource not found + * @return void + */ + public function notFound() { + $this->setStatusCode(self::STATUS_NOT_FOUND, 'Resource Not Found'); + $this->setHeader('Pragma', 'no-cache'); + exit; + } + + /** + * Bad request: may be cached, it won't improve if fetched again + * + * @param $message string + * @return void + */ + public function badRequest($message = "") { + $this->setStatusCode(self::STATUS_BAD_REQUEST, 'Bad Request'); + if ($message) { + echo "

{$message}

"; + } + exit; + } + + /** + * Not modified: the request was verified to be identical with previous cache + * + * @param $etag + * @return void + */ + public function notModified($etag) { + // if not modified, save some cpu and bandwidth + $this->setStatusCode(self::STATUS_NOT_MODIFIED, 'Not Modified'); + $this->setEtag($etag); + exit; + } + + /** + * Forbidden, access to this resource is not allowed + * + * @return void + */ + public function forbidden() { + $this->setStatusCode(self::STATUS_FORBIDDEN, 'Forbidden'); + exit; + } + + /** + * Internal/Fatal Error + * + * @return void + */ + public function fatalError() { + $this->setStatusCode(self::STATUS_INTERNAL_SERVER_ERROR, 'Internal Error'); + exit; + } + + /** + * Gateway timeout + * + * @return void + */ + public function gatewayTimeout() { + $this->setStatusCode(self::STATUS_GATEWAY_TIMEOUT, 'Gateway Timeout'); + exit; + } + + public function badMethod($message = '') { + $this->setStatusCode(self::STATUS_METHOD_NOT_ALLOWED, 'Method Not Allowed'); + if ($message) { + echo "

{$message}

"; + } + exit; + } + + /** + * Set Content-Type HTTP header + * + * @param string $content_type + * @return Response + */ + public function setContentType($content_type) { + $this->contentType = $content_type; + return $this->setHeader('Content-Type', $content_type); + } + + /** + * Get Content-Type HTTP header + * + * @return string + * @since 1.3 + */ + public function getContentType() { + return $this->contentType; + } + + /** + * Set Content-Length HTTP header + * + * @param int $length + * @return Response + */ + public function setContentLength($length) { + return $this->setHeader('Content-Length', $length); + } + + /** + * Sets the response content. + * + * Valid types are strings, numbers, and objects that implement a __toString() method. + * + * @param mixed $content + * @return Response + * @throws UnexpectedValueException + */ + public function setContent($content) { + if (null !== $content && !is_string($content) && !is_numeric($content) && !is_callable(array($content, '__toString'))) { + throw new UnexpectedValueException('The Response content must be a string or object implementing __toString(), "' . gettype($content) . '" given.'); + } + + $this->content = (string) $content; + + return $this; + } + + /** + * Set JSON response content + * + * @param mixed $content Can be of any type except a resource. + * @return Response + * @throws UnexpectedValueException + */ + public function setJsonContent($content) { + if (is_string($content)) { + return $this->setContent($content); + } + + if (!$content = json_encode($content)) { + throw new UnexpectedValueException('The Response content cannot be json encoded'); + } + + return $this->setContent($content); + } + + /** + * Gets the current response content. + * + * @return string Content + */ + public function getContent() { + return $this->content; + } + + /** + * Sets the ETag value. + * + * @param string $etag The ETag unique identifier + * @return Response + */ + public function setEtag($etag) { + return $this->setHeader('Etag', '"' . $etag . '"'); + } + + /** + * Set HTTP Response header. + * + * @param string $header + * @param string $value + * @param bool $replace + * @return Response + */ + public function setHeader($header, $value, $replace = true) { + header("$header: $value", $replace); + + return $this; + } + + /** + * Marks the response as "private". + * It makes the response ineligible for serving other clients. + * + * @return Response + */ + public function setPrivate() { + return $this->setHeader('Cache-Control', 'private'); + } + + /** + * Send caching header + * + * @param string $duration Any string supported my PHP's strtotime() function (http://php.net/manual/en/function.strtotime.php) + * @param bool $public + * @return Response + */ + public function setCacheHeaders($duration, $public = true) { + $time = strtotime($duration); + if (!$time) { + return $this; + } + + $max_age = $time - time(); + $cache = $public ? 'public' : 'private'; + return $this->setHeader('Cache-Control', $cache . ', max-age=' . $max_age); + } + + /** + * Send out Response content. + * Note that this ends the request + * + * @return void + */ + public function send() { + if ($this->content) { + echo $this->content; + } + exit; + } } diff --git a/application/Library/Router.php b/application/Library/Router.php index ffd7a688c..fb807e372 100644 --- a/application/Library/Router.php +++ b/application/Library/Router.php @@ -1,188 +1,181 @@ site = $site; - $this->routes = Config::get('routes'); - $this->usingSubDomain = Config::get('use_study_subdomains'); - $this->serverName = isset($_SERVER['SERVER_NAME']) ? $_SERVER['SERVER_NAME'] : null; - if (!$this->serverName) { - throw new Exception('Server name not explicitly defined'); - } + $this->routes = Config::get('routes'); + $this->usingSubDomain = Config::get('use_study_subdomains'); + $this->serverName = isset($_SERVER['SERVER_NAME']) ? $_SERVER['SERVER_NAME'] : null; + if (!$this->serverName) { + throw new Exception('Server name not explicitly defined'); + } } /** * @return Router */ public function route() { - $route = $this->site->request->str('route'); - $routeSlugs = $this->extractRouteSlugs($route); - $params = null; - - // First try to get controller path from the route which is one of those configured routes - foreach ($routeSlugs as $slug) { - if (isset($this->routes[$slug])) { - $controller = $this->routes[$slug]; - $params = $this->getParamsFromRoute($route, $slug); - break; - } - } - - if (empty($controller)) { - $controller = $this->getControllerName('public'); - } - - if ($params === null) { - $params = $this->getParamsFromRoute($route, ''); - } - - $action = array_shift($params); - if (!$action) { - $action = 'index'; - } - - // Check if action exists in controller and if it doesn't assume we are looking at a run - // @todo validate runs not to have controller action names (especially PublicController) - $controllerName = $controller; - $actionName = $this->getActionName($action); - if (!class_exists($controllerName, true)) { - throw new Exception ("Controller $controllerName does not exist"); - } - - // Sub-domains for now are used only for accessing studies - if ($this->usingSubDomain && FMRSD_CONTEXT) { - list($controllerName, $actionName) = $this->getStudyRoute(); - $runName = $this->getRunFromSubDomain(); - if ($action !== 'index') { - array_unshift($params, $action); - } - array_unshift($params, $runName); - } - - if (!method_exists($controllerName, $actionName)) { - // Assume at this point user is trying to access a private action via the indexAction - list($controllerName, $actionName) = $this->shiftAction($controllerName); - // push back the $action as an action parameter - array_unshift($params, $action); - } - - $this->controller = $controllerName; - $this->action = $actionName; - $this->params = $params; - $this->site->setPath($route); - return $this; + $route = $this->site->request->str('route'); + $routeSlugs = $this->extractRouteSlugs($route); + $params = null; + + // First try to get controller path from the route which is one of those configured routes + foreach ($routeSlugs as $slug) { + if (isset($this->routes[$slug])) { + $controller = $this->routes[$slug]; + $params = $this->getParamsFromRoute($route, $slug); + break; + } + } + + if (empty($controller)) { + $controller = $this->getControllerName('public'); + } + + if ($params === null) { + $params = $this->getParamsFromRoute($route, ''); + } + + $action = array_shift($params); + if (!$action) { + $action = 'index'; + } + + // Check if action exists in controller and if it doesn't assume we are looking at a run + // @todo validate runs not to have controller action names (especially PublicController) + $controllerName = $controller; + $actionName = $this->getActionName($action); + if (!class_exists($controllerName, true)) { + throw new Exception("Controller $controllerName does not exist"); + } + + // Sub-domains for now are used only for accessing studies + if ($this->usingSubDomain && FMRSD_CONTEXT) { + list($controllerName, $actionName) = $this->getStudyRoute(); + $runName = $this->getRunFromSubDomain(); + if ($action !== 'index') { + array_unshift($params, $action); + } + array_unshift($params, $runName); + } + + if (!method_exists($controllerName, $actionName)) { + // Assume at this point user is trying to access a private action via the indexAction + list($controllerName, $actionName) = $this->shiftAction($controllerName); + // push back the $action as an action parameter + array_unshift($params, $action); + } + + $this->controller = $controllerName; + $this->action = $actionName; + $this->params = $params; + $this->site->setPath($route); + return $this; + } + + private function getControllerName($controllerPath) { + $parts = array_filter(explode('/', $controllerPath)); + $parts = array_map('ucwords', array_map('strtolower', $parts)); + return implode('', $parts) . 'Controller'; + } + + private function getActionName($action) { + if (strpos($action, '-') !== false) { + $parts = array_filter(explode('-', $action)); + } else { + $parts = array_filter(explode('_', $action)); + } + $action = array_shift($parts); + foreach ($parts as $part) { + $action .= ucwords(strtolower($part)); + } + return $action . 'Action'; + } + + /** + * Some hack method to shift blame when we can't find action in controller + * + * @param string $controller + * @return string + */ + private function shiftAction($controller) { + if ($controller === 'PublicController') { + return $this->getStudyRoute(); + } + return array($controller, 'indexAction'); + } + + /** + * Some hack method to shift blame when we can't find action in controller + * + * @return array + */ + private function getStudyRoute() { + return array('RunController', 'indexAction'); + } + + /** + * Get Run name from sub-domain + * + * @return string + */ + private function getRunFromSubDomain() { + $host = explode('.', $this->serverName); + $subdomains = array_slice($host, 0, count($host) - 2); + return $subdomains[0]; + } + + private function extractRouteSlugs($route) { + $parts = explode('/', $route); + $slugs = array(); + $prev = ''; + foreach ($parts as $r) { + $slug = trim(implode('/', array($prev, $r)), '\/'); + $slugs[] = $slug; + $prev = $slug; + } + + return array_reverse($slugs); + } + + private function getParamsFromRoute($route, $base) { + $route = '/' . trim($route, "\\\/") . '/'; + $base = '/' . trim($base, "\\\/") . '/'; + $params = (array) array_filter(explode('/', str_replace($base, '', $route))); + + return $params; } - private function getControllerName($controllerPath) { - $parts = array_filter(explode('/', $controllerPath)); - $parts = array_map('ucwords', array_map('strtolower', $parts)); - return implode('', $parts) . 'Controller'; - } - - private function getActionName($action) { - if (strpos($action, '-') !== false) { - $parts = array_filter(explode('-', $action)); - } else { - $parts = array_filter(explode('_', $action)); - } - $action = array_shift($parts); - foreach ($parts as $part) { - $action .= ucwords(strtolower($part)); - } - return $action . 'Action'; - } - - /** - * Some hack method to shift blame when we can't find action in controller - * - * @param string $controller - * @return string - */ - private function shiftAction($controller) { - if ($controller === 'PublicController') { - return $this->getStudyRoute(); - } - return array($controller, 'indexAction'); - } - - /** - * Some hack method to shift blame when we can't find action in controller - * - * @return array - */ - private function getStudyRoute() { - return array('RunController', 'indexAction'); - } - - /** - * Get Run name from sub-domain - * - * @return string - */ - private function getRunFromSubDomain() { - $host = explode('.', $this->serverName); - $subdomains = array_slice($host, 0, count($host) - 2); - return $subdomains[0]; - } - - private function extractRouteSlugs($route) { - $parts = explode('/', $route); - $slugs = array(); - $prev = ''; - foreach ($parts as $r) { - $slug = trim(implode('/', array($prev, $r)), '\/'); - $slugs[] = $slug; - $prev = $slug; - } - - return array_reverse($slugs); - } - - private function getParamsFromRoute($route, $base) { - $route = '/' . trim($route, "\\\/") . '/'; - $base = '/' . trim($base, "\\\/") . '/'; - $params = (array)array_filter(explode('/', str_replace($base, '', $route))); - - return $params; - } - - public function execute() { - $controller_ = $this->controller; - $action = $this->action; - - $controller = new $controller_($this->site); - if (!method_exists($controller, $action)) { - throw new Exception("Action $action not found in $controller_"); - } - - return call_user_func_array(array($controller, $action), $this->params); - } + public function execute() { + $controller_ = $this->controller; + $action = $this->action; + + $controller = new $controller_($this->site); + if (!method_exists($controller, $action)) { + throw new Exception("Action $action not found in $controller_"); + } + + return call_user_func_array(array($controller, $action), $this->params); + } /** * @return Router @@ -202,5 +195,5 @@ public static function getWebRoot() { public static function isWebRootDir($name) { return is_dir(self::getWebRoot() . '/' . $name); } -} +} diff --git a/application/Library/Session.php b/application/Library/Session.php index 08fc2b0a2..79b46f313 100644 --- a/application/Library/Session.php +++ b/application/Library/Session.php @@ -1,109 +1,102 @@ $value) { - self::$key = $value; - } - } - - /** - * Start a PHP session - */ - public static function start() { - session_name(self::$name); + protected static $name = 'formr_session'; + protected static $lifetime; + protected static $path = '/'; + protected static $domain = null; + protected static $secure = false; + protected static $httponly = true; + + const REQUEST_TOKENS = '_formr_request_tokens'; + const REQUEST_USER_CODE = '_formr_code'; + const REQUEST_NAME = '_formr_cookie'; + + public static function configure($config = array()) { + self::$lifetime = Config::get('session_cookie_lifetime'); + self::$secure = SSL; + foreach ($config as $key => $value) { + self::$key = $value; + } + } + + /** + * Start a PHP session + */ + public static function start() { + session_name(self::$name); session_set_cookie_params(self::$lifetime, self::$path, self::$domain, self::$secure, self::$httponly); session_start(); - } + } - public static function destroy() { - setcookie(session_name(), '', time() - 3600, '/'); - session_unset(); + public static function destroy() { + setcookie(session_name(), '', time() - 3600, '/'); + session_unset(); session_destroy(); - session_write_close(); - } - - public static function over() { - static $closed; - if ($closed) { - return false; - } - session_write_close(); - $closed = true; - return true; - } + session_write_close(); + } + + public static function over() { + static $closed; + if ($closed) { + return false; + } + session_write_close(); + $closed = true; + return true; + } + + public static function isExpired($expiry) { + return isset($_SESSION['last_activity']) && (time() - $_SESSION['last_activity'] > $expiry); + } + + public static function set($key, $value) { + $_SESSION[$key] = $value; + } + + public static function get($key, $default = null) { + return isset($_SESSION[$key]) ? $_SESSION[$key] : $default; + } + + public static function delete($key) { + if (isset($_SESSION[$key])) { + unset($_SESSION[$key]); + } + } + + public static function globalRefresh() { + global $user, $site; + self::set('user', serialize($user)); + self::set('site', $site); + } + + public static function getRequestToken() { + $token = sha1(mt_rand()); + if (!$tokens = self::get(self::REQUEST_TOKENS)) { + $tokens = array($token => 1); + } else { + $tokens[$token] = 1; + } + self::set(self::REQUEST_TOKENS, $tokens); + return $token; + } + + public static function canValidateRequestToken(Request $request) { + $token = $request->getParam(self::REQUEST_TOKENS); + $tokens = self::get(self::REQUEST_TOKENS, array()); + if (!empty($tokens[$token])) { + // a valid request token dies after it's validity is retrived :P + unset($tokens[$token]); + self::set(self::REQUEST_TOKENS, $tokens); + return true; + } + return false; + } - public static function isExpired($expiry) { - return isset($_SESSION['last_activity']) && (time() - $_SESSION['last_activity'] > $expiry); - } - - public static function set($key, $value) { - $_SESSION[$key] = $value; - } - - public static function get($key, $default = null) { - return isset($_SESSION[$key]) ? $_SESSION[$key] : $default; - } - - public static function delete($key) { - if (isset($_SESSION[$key])) { - unset($_SESSION[$key]); - } - } - - public static function globalRefresh() { - global $user, $site; - self::set('user', serialize($user)); - self::set('site', $site); - } - - public static function getRequestToken() { - $token = sha1(mt_rand()); - if (!$tokens = self::get(self::REQUEST_TOKENS)) { - $tokens = array($token => 1); - } else { - $tokens[$token] = 1; - } - self::set(self::REQUEST_TOKENS, $tokens); - return $token; - } - - public static function canValidateRequestToken(Request $request) { - $token = $request->getParam(self::REQUEST_TOKENS); - $tokens = self::get(self::REQUEST_TOKENS, array()); - if (!empty($tokens[$token])) { - // a valid request token dies after it's validity is retrived :P - unset($tokens[$token]); - self::set(self::REQUEST_TOKENS, $tokens); - return true; - } - return false; - } } - - diff --git a/application/Library/Template.php b/application/Library/Template.php index fb0683836..1d451bb2e 100644 --- a/application/Library/Template.php +++ b/application/Library/Template.php @@ -6,62 +6,62 @@ class Template { - /** - * Load and display a template - * - * @param string $template - * @param array $vars As associative array representing variables to be passed to templates - */ - public static function load($template, $vars = array()) { - global $site, $user, $fdb, $study, $run, $css, $js; - if (strstr($template, '.') === false) { - $template .= '.php'; - } - $file = APPLICATION_PATH . 'View/' . $template; + /** + * Load and display a template + * + * @param string $template + * @param array $vars As associative array representing variables to be passed to templates + */ + public static function load($template, $vars = array()) { + global $site, $user, $fdb, $study, $run, $css, $js; + if (strstr($template, '.') === false) { + $template .= '.php'; + } + $file = APPLICATION_PATH . 'View/' . $template; - if (file_exists($file)) { - $vars = array_merge(Request::getGlobals('variables', array()), $vars); - extract($vars); - include $file; - } - } + if (file_exists($file)) { + $vars = array_merge(Request::getGlobals('variables', array()), $vars); + extract($vars); + include $file; + } + } - public static function get($template, $vars = array()) { - ob_start(); - Template::load($template, $vars); - return ob_get_clean(); - } + public static function get($template, $vars = array()) { + ob_start(); + Template::load($template, $vars); + return ob_get_clean(); + } - public static function get_replace($template, $params = array(), $vars = array()) { - $text = self::get($template, $vars); - return self::replace($text, $params); - } + public static function get_replace($template, $params = array(), $vars = array()) { + $text = self::get($template, $vars); + return self::replace($text, $params); + } - public static function replace($template, $params, $rnl = false) { - if (empty($template) || empty($params)) { - return $template; - } + public static function replace($template, $params, $rnl = false) { + if (empty($template) || empty($params)) { + return $template; + } - if ($rnl === true) { - $template = str_replace("\n", '', $template); - } + if ($rnl === true) { + $template = str_replace("\n", '', $template); + } - $res = preg_match_all('/%{[^}]+}/', $template, $matches, PREG_OFFSET_CAPTURE); - if (!$res) { - return $template; - } + $res = preg_match_all('/%{[^}]+}/', $template, $matches, PREG_OFFSET_CAPTURE); + if (!$res) { + return $template; + } - $offset = 0; - $res = ''; - foreach ($matches[0] as $match) { - $res .= substr($template, $offset, $match[1] - $offset); - $key = substr($match[0], 2, -1); - $value = isset($params[$key]) ? $params[$key] : ''; - $res .= $value; - $offset = $match[1] + strlen($match[0]); - } - $res .= substr($template, $offset); - return $res; - } + $offset = 0; + $res = ''; + foreach ($matches[0] as $match) { + $res .= substr($template, $offset, $match[1] - $offset); + $key = substr($match[0], 2, -1); + $value = isset($params[$key]) ? $params[$key] : ''; + $res .= $value; + $offset = $match[1] + strlen($match[0]); + } + $res .= substr($template, $offset); + return $res; + } } diff --git a/application/Library/UnitSessionQueue.php b/application/Library/UnitSessionQueue.php index d7cbf26da..1ae9bdefa 100644 --- a/application/Library/UnitSessionQueue.php +++ b/application/Library/UnitSessionQueue.php @@ -5,183 +5,177 @@ * Process unit sessions in unit_sessions_queue * */ - - -class UnitSessionQueue extends Queue{ - - protected $name = 'UnitSession-Queue'; - - /** - * Maximum sessions to be processed by each PHP process started for a queue operation - * - * @var int - */ - protected $maxSessionsPerProcess; - - /** - * Array to hold push query values (50 inserted at once) - * - * @var array - */ - protected $pushQueries = array(); - - protected $logFile = 'session-queue.log'; - - protected $cache; - - public function __construct(DB $db, array $config) { - parent::__construct($db, $config); - } - - public function run() { - if (empty($this->config['use_queue'])) { - throw new Exception('Explicitely configure $settings[unit_session] to TRUE in order to use DB queuing.'); - } - - // loop forever until terminated by SIGINT - while (!$this->out) { - try { - // loop until terminated but with taking some nap - $sleeps = 0; - while (!$this->out && $this->rested()) { - if ($this->processQueue() === false) { - // if there is nothing to process in the queue sleep for sometime - $this->dbg("Sleeping because nothing was found in queue"); - sleep($this->sleep); - $sleeps++; - } - if ($sleeps > $this->allowedSleeps) { - // exit to restart supervisor process - $this->dbg('Exit and restart process because you have slept alot'); - $this->out = true; - } - } - } catch (Exception $e) { - // if connection disappeared - try to restore it - $error_code = $e->getCode(); - if ($error_code != 1053 && $error_code != 2006 && $error_code != 2013 && $error_code != 2003) { - throw $e; - } - - $this->dbg($e->getMessage() . "[" . $error_code . "]"); - - $this->dbg("Unable to connect. waiting 5 seconds before reconnect."); - sleep(5); - } - } - } - - /** - * - * @param int $account_id - * @return PDOStatement - */ - protected function getSessionsStatement() { - $now = time(); - $query = "SELECT session, unit_session_id, run_session_id, unit_id, expires, counter, run +class UnitSessionQueue extends Queue { + + protected $name = 'UnitSession-Queue'; + + /** + * Maximum sessions to be processed by each PHP process started for a queue operation + * + * @var int + */ + protected $maxSessionsPerProcess; + + /** + * Array to hold push query values (50 inserted at once) + * + * @var array + */ + protected $pushQueries = array(); + protected $logFile = 'session-queue.log'; + protected $cache; + + public function __construct(DB $db, array $config) { + parent::__construct($db, $config); + } + + public function run() { + if (empty($this->config['use_queue'])) { + throw new Exception('Explicitely configure $settings[unit_session] to TRUE in order to use DB queuing.'); + } + + // loop forever until terminated by SIGINT + while (!$this->out) { + try { + // loop until terminated but with taking some nap + $sleeps = 0; + while (!$this->out && $this->rested()) { + if ($this->processQueue() === false) { + // if there is nothing to process in the queue sleep for sometime + $this->dbg("Sleeping because nothing was found in queue"); + sleep($this->sleep); + $sleeps++; + } + if ($sleeps > $this->allowedSleeps) { + // exit to restart supervisor process + $this->dbg('Exit and restart process because you have slept alot'); + $this->out = true; + } + } + } catch (Exception $e) { + // if connection disappeared - try to restore it + $error_code = $e->getCode(); + if ($error_code != 1053 && $error_code != 2006 && $error_code != 2013 && $error_code != 2003) { + throw $e; + } + + $this->dbg($e->getMessage() . "[" . $error_code . "]"); + + $this->dbg("Unable to connect. waiting 5 seconds before reconnect."); + sleep(5); + } + } + } + + /** + * + * @param int $account_id + * @return PDOStatement + */ + protected function getSessionsStatement() { + $now = time(); + $query = "SELECT session, unit_session_id, run_session_id, unit_id, expires, counter, run FROM survey_sessions_queue LEFT JOIN survey_run_sessions ON survey_sessions_queue.run_session_id = survey_run_sessions.id WHERE survey_sessions_queue.expires <= {$now} ORDER BY expires ASC"; - return $this->db->rquery($query); - } - - protected function processQueue() { - $sessionsStmt = $this->getSessionsStatement(); - if ($sessionsStmt->rowCount() <= 0) { - $sessionsStmt->closeCursor(); - return false; - } - - while ($session = $sessionsStmt->fetch(PDO::FETCH_ASSOC)) { - if (!$session['session']) { - $this->dbg('A session could not be found for item in queue: ' . print_r($session, 1)); - self::removeItem($session['unit_session_id'], $session['unit_id']); - continue; - } - - $run = $this->getRun($session['run']); - $runSession = new RunSession($this->db, $run->id, 'cron', $session['session'], $run); - - // Execute session again by getting current unit - // This action might end or expire a session, thereby removing it from queue - // or session might be re-queued to expire in x minutes - $rsUnit = $runSession->getUnit(); - if ($this->debug) { - $this->dbg('Proccessed: ' . print_r($session, 1)); - } - } - } - - protected function setCache($type, $key, $value) { - if (!isset($this->cache[$type])) { - $this->cache[$type] = array(); - } - $this->cache[$type][$key] = $value; - } - - protected function getCache($type, $key) { - if (!empty($this->cache[$type])) { - return array_val($this->cache[$type], $key, null); - } - return null; - } - - /** - * Get Run Object - * - * @param string $runName - * @return \Run - */ - protected function getRun($runName) { - $run = $this->getCache('run', $runName); - if (!$run) { - $run = new Run($this->db, $runName); - $this->setCache('run', $runName, $run); - } - return $run; - } - - /** - * Remove item from session queue - * - * @param int $unitSessionId ID of the unit session - * @param int $runUnitId ID of the Run unit - * @return booelan - */ - public static function removeItem($unitSessionId, $runUnitId) { - $db = DB::getInstance(); - $removed = $db->exec( - "DELETE FROM `survey_sessions_queue` WHERE `unit_session_id` = :unit_session_id AND `unit_id` = :unit_id", - array('unit_session_id' => (int)$unitSessionId, 'unit_id' => (int)$runUnitId) - ); - - return (bool)$removed; - } - - /** - * Add item to session queue - * - * @param UnitSession $unitSession - * @param RunUnit $runUnit - * @param mixed $execResults - */ - public static function addItem(UnitSession $unitSession, RunUnit $runUnit, $execResults) { - $helper = RunUnitHelper::getInstance(); - if ($expires = (int)$helper->getUnitSessionExpiration($unitSession, $runUnit, $execResults)) { - $q = array( - 'unit_session_id' => $unitSession->id, - 'run_session_id' => $unitSession->run_session_id, - 'unit_id' => $runUnit->id, - 'expires' => $expires, - 'run' => $runUnit->run->name, - 'counter' => 1, - ); - - $db = DB::getInstance(); - $db->insert_update('survey_sessions_queue', $q, array('expires', 'counter' => '::counter + 1')); - } - - } + return $this->db->rquery($query); + } + + protected function processQueue() { + $sessionsStmt = $this->getSessionsStatement(); + if ($sessionsStmt->rowCount() <= 0) { + $sessionsStmt->closeCursor(); + return false; + } + + while ($session = $sessionsStmt->fetch(PDO::FETCH_ASSOC)) { + if (!$session['session']) { + $this->dbg('A session could not be found for item in queue: ' . print_r($session, 1)); + self::removeItem($session['unit_session_id'], $session['unit_id']); + continue; + } + + $run = $this->getRun($session['run']); + $runSession = new RunSession($this->db, $run->id, 'cron', $session['session'], $run); + + // Execute session again by getting current unit + // This action might end or expire a session, thereby removing it from queue + // or session might be re-queued to expire in x minutes + $rsUnit = $runSession->getUnit(); + if ($this->debug) { + $this->dbg('Proccessed: ' . print_r($session, 1)); + } + } + } + + protected function setCache($type, $key, $value) { + if (!isset($this->cache[$type])) { + $this->cache[$type] = array(); + } + $this->cache[$type][$key] = $value; + } + + protected function getCache($type, $key) { + if (!empty($this->cache[$type])) { + return array_val($this->cache[$type], $key, null); + } + return null; + } + + /** + * Get Run Object + * + * @param string $runName + * @return \Run + */ + protected function getRun($runName) { + $run = $this->getCache('run', $runName); + if (!$run) { + $run = new Run($this->db, $runName); + $this->setCache('run', $runName, $run); + } + return $run; + } + + /** + * Remove item from session queue + * + * @param int $unitSessionId ID of the unit session + * @param int $runUnitId ID of the Run unit + * @return booelan + */ + public static function removeItem($unitSessionId, $runUnitId) { + $db = DB::getInstance(); + $removed = $db->exec( + "DELETE FROM `survey_sessions_queue` WHERE `unit_session_id` = :unit_session_id AND `unit_id` = :unit_id", array('unit_session_id' => (int) $unitSessionId, 'unit_id' => (int) $runUnitId) + ); + + return (bool) $removed; + } + + /** + * Add item to session queue + * + * @param UnitSession $unitSession + * @param RunUnit $runUnit + * @param mixed $execResults + */ + public static function addItem(UnitSession $unitSession, RunUnit $runUnit, $execResults) { + $helper = RunUnitHelper::getInstance(); + if ($expires = (int) $helper->getUnitSessionExpiration($unitSession, $runUnit, $execResults)) { + $q = array( + 'unit_session_id' => $unitSession->id, + 'run_session_id' => $unitSession->run_session_id, + 'unit_id' => $runUnit->id, + 'expires' => $expires, + 'run' => $runUnit->run->name, + 'counter' => 1, + ); + + $db = DB::getInstance(); + $db->insert_update('survey_sessions_queue', $q, array('expires', 'counter' => '::counter + 1')); + } + } } diff --git a/application/Model/Branch.php b/application/Model/Branch.php index f6a9efc4a..cab2b4ee3 100644 --- a/application/Model/Branch.php +++ b/application/Model/Branch.php @@ -2,96 +2,96 @@ class Branch extends RunUnit { - public $errors = array(); - public $id = null; - public $session = null; - public $unit = null; - protected $condition = null; - protected $if_true = null; - protected $automatically_jump = 1; - protected $automatically_go_on = 1; - public $type = 'Branch'; - public $icon = 'fa-code-fork fa-flip-vertical'; - - public function __construct($fdb, $session = null, $unit = null, $run_session = NULL, $run = NULL) { - parent::__construct($fdb, $session, $unit, $run_session, $run); - - if ($this->id): - $vars = $this->dbh->select('id, condition, if_true, automatically_jump, automatically_go_on') - ->from('survey_branches') - ->where(array('id' => $this->id)) - ->fetch(); - if ($vars): - array_walk($vars, "emptyNull"); - $this->condition = $vars['condition']; - $this->if_true = $vars['if_true']; - $this->automatically_jump = $vars['automatically_jump']; - $this->automatically_go_on = $vars['automatically_go_on']; - $this->valid = true; - endif; - endif; - } - - public function create($options) { - $this->dbh->beginTransaction(); - if (!$this->id) { - $this->id = parent::create($this->type); - } else { - $this->modify($options); - } - - if (isset($options['condition'])) { - array_walk($options, "emptyNull"); - $this->condition = $options['condition']; - if (isset($options['if_true'])) { - $this->if_true = $options['if_true']; - } - if (isset($options['automatically_jump'])) { - $this->automatically_jump = $options['automatically_jump']; - } - if (isset($options['automatically_go_on'])) { - $this->automatically_go_on = $options['automatically_go_on']; - } - } - $this->condition = cr2nl($this->condition); - - $this->dbh->insert_update('survey_branches', array( - 'id' => $this->id, - 'condition' => $this->condition, - 'if_true' => $this->if_true, - 'automatically_jump' => $this->automatically_jump, - 'automatically_go_on' => $this->automatically_go_on - )); - $this->dbh->commit(); - $this->valid = true; - - return true; - } - - public function displayForRun($prepend = '') { - $dialog = Template::get($this->getUnitTemplatePath(), array( - 'prepend' => $prepend, - 'condition' => $this->condition, - 'position' => $this->position, - 'ifTrue' => $this->if_true, - 'jump' => $this->automatically_jump, - 'goOn' => $this->automatically_go_on, - )); - - return parent::runDialog($dialog); - } - - public function removeFromRun($special = null) { - return $this->delete($special); - } - - public function test() { - $results = $this->getSampleSessions(); - if (!$results) { - return false; - } - - $test_tpl = ' + public $errors = array(); + public $id = null; + public $session = null; + public $unit = null; + protected $condition = null; + protected $if_true = null; + protected $automatically_jump = 1; + protected $automatically_go_on = 1; + public $type = 'Branch'; + public $icon = 'fa-code-fork fa-flip-vertical'; + + public function __construct($fdb, $session = null, $unit = null, $run_session = NULL, $run = NULL) { + parent::__construct($fdb, $session, $unit, $run_session, $run); + + if ($this->id): + $vars = $this->dbh->select('id, condition, if_true, automatically_jump, automatically_go_on') + ->from('survey_branches') + ->where(array('id' => $this->id)) + ->fetch(); + if ($vars): + array_walk($vars, "emptyNull"); + $this->condition = $vars['condition']; + $this->if_true = $vars['if_true']; + $this->automatically_jump = $vars['automatically_jump']; + $this->automatically_go_on = $vars['automatically_go_on']; + $this->valid = true; + endif; + endif; + } + + public function create($options) { + $this->dbh->beginTransaction(); + if (!$this->id) { + $this->id = parent::create($this->type); + } else { + $this->modify($options); + } + + if (isset($options['condition'])) { + array_walk($options, "emptyNull"); + $this->condition = $options['condition']; + if (isset($options['if_true'])) { + $this->if_true = $options['if_true']; + } + if (isset($options['automatically_jump'])) { + $this->automatically_jump = $options['automatically_jump']; + } + if (isset($options['automatically_go_on'])) { + $this->automatically_go_on = $options['automatically_go_on']; + } + } + $this->condition = cr2nl($this->condition); + + $this->dbh->insert_update('survey_branches', array( + 'id' => $this->id, + 'condition' => $this->condition, + 'if_true' => $this->if_true, + 'automatically_jump' => $this->automatically_jump, + 'automatically_go_on' => $this->automatically_go_on + )); + $this->dbh->commit(); + $this->valid = true; + + return true; + } + + public function displayForRun($prepend = '') { + $dialog = Template::get($this->getUnitTemplatePath(), array( + 'prepend' => $prepend, + 'condition' => $this->condition, + 'position' => $this->position, + 'ifTrue' => $this->if_true, + 'jump' => $this->automatically_jump, + 'goOn' => $this->automatically_go_on, + )); + + return parent::runDialog($dialog); + } + + public function removeFromRun($special = null) { + return $this->delete($special); + } + + public function test() { + $results = $this->getSampleSessions(); + if (!$results) { + return false; + } + + $test_tpl = ' @@ -103,62 +103,62 @@ public function test() {
'; - $row_tpl = ' + $row_tpl = ' %{session} (%{position}) %{result} '; - $this->run_session_id = current($results)['id']; - $opencpu_vars = $this->getUserDataInRun($this->condition); - $ocpu_session = opencpu_evaluate($this->condition, $opencpu_vars, 'text', null, true); - echo opencpu_debug($ocpu_session, null, 'text'); - - // Maybe there is a way that we prevent 'calling opencpu' in a loop by gathering what is needed to be evaluated - // at opencpu in some 'box' and sending one request (also create new func in formr R package to open this box, evaluate what is inside and return the box) - $rows = ''; - foreach ($results as $row) { - $this->run_session_id = $row['id']; - $opencpu_vars = $this->getUserDataInRun($this->condition); - $eval = opencpu_evaluate($this->condition, $opencpu_vars); - $rows .= Template::replace($row_tpl, array( - 'session' => $row['session'], - 'position' => $row['position'], - 'result' => stringBool($eval), - )); - } - - echo Template::replace($test_tpl, array('rows' => $rows)); - $this->run_session_id = null; - } - - public function exec() { - $opencpu_vars = $this->getUserDataInRun($this->condition); - $eval = opencpu_evaluate($this->condition, $opencpu_vars); - if ($eval === null) { - return true; // don't go anywhere, wait for the error to be fixed! - } - - // If execution returned a timestamp greater that now() queue it - if (($time = strtotime($eval)) && $time > time()) { - $this->execData['expire_timestamp'] = $time; - return true; - } - - $result = (bool)$eval; - // if condition is true and we're set to jump automatically, or if the user reacted - if ($result && ($this->automatically_jump || !$this->called_by_cron)): - if ($this->run_session->session): - $this->end(); - return !$this->run_session->runTo($this->if_true); - endif; - elseif (!$result && ($this->automatically_go_on || !$this->called_by_cron)): // the condition is false and it goes on - $this->end(); - return false; - else: // we wait for the condition to turn true or false, depends. - return true; - endif; - } + $this->run_session_id = current($results)['id']; + $opencpu_vars = $this->getUserDataInRun($this->condition); + $ocpu_session = opencpu_evaluate($this->condition, $opencpu_vars, 'text', null, true); + echo opencpu_debug($ocpu_session, null, 'text'); + + // Maybe there is a way that we prevent 'calling opencpu' in a loop by gathering what is needed to be evaluated + // at opencpu in some 'box' and sending one request (also create new func in formr R package to open this box, evaluate what is inside and return the box) + $rows = ''; + foreach ($results as $row) { + $this->run_session_id = $row['id']; + $opencpu_vars = $this->getUserDataInRun($this->condition); + $eval = opencpu_evaluate($this->condition, $opencpu_vars); + $rows .= Template::replace($row_tpl, array( + 'session' => $row['session'], + 'position' => $row['position'], + 'result' => stringBool($eval), + )); + } + + echo Template::replace($test_tpl, array('rows' => $rows)); + $this->run_session_id = null; + } + + public function exec() { + $opencpu_vars = $this->getUserDataInRun($this->condition); + $eval = opencpu_evaluate($this->condition, $opencpu_vars); + if ($eval === null) { + return true; // don't go anywhere, wait for the error to be fixed! + } + + // If execution returned a timestamp greater that now() queue it + if (($time = strtotime($eval)) && $time > time()) { + $this->execData['expire_timestamp'] = $time; + return true; + } + + $result = (bool) $eval; + // if condition is true and we're set to jump automatically, or if the user reacted + if ($result && ($this->automatically_jump || !$this->called_by_cron)): + if ($this->run_session->session): + $this->end(); + return !$this->run_session->runTo($this->if_true); + endif; + elseif (!$result && ($this->automatically_go_on || !$this->called_by_cron)): // the condition is false and it goes on + $this->end(); + return false; + else: // we wait for the condition to turn true or false, depends. + return true; + endif; + } } diff --git a/application/Model/Email.php b/application/Model/Email.php index 29c539eff..8946bda1a 100644 --- a/application/Model/Email.php +++ b/application/Model/Email.php @@ -2,186 +2,185 @@ class Email extends RunUnit { - public $errors = array(); - public $id = null; - public $session = null; - public $unit = null; - protected $mail_sent = false; - protected $body = null; - protected $body_parsed = null; - protected $account_id = null; - protected $images = array(); - protected $subject = null; - protected $recipient_field; - protected $recipient; - protected $html = 1; - protected $cron_only = 0; - public $icon = "fa-envelope"; - public $type = "Email"; - protected $subject_parsed = null; - protected $mostrecent = "most recent reported address"; - - /** - * An array of unit's exportable attributes - * @var array - */ - public $export_attribs = array('type', 'description', 'position', 'special', 'subject', 'account_id', 'recipient_field', 'body', 'cron_only'); - - - public function __construct($fdb, $session = null, $unit = null, $run_session = NULL, $run = NULL) { - parent::__construct($fdb, $session, $unit, $run_session, $run); - - if ($this->id): - $vars = $this->dbh->findRow('survey_emails', array('id' => $this->id)); - if ($vars): - $this->account_id = $vars['account_id']; - $this->recipient_field = $vars['recipient_field']; - $this->body = $vars['body']; - $this->body_parsed = $vars['body_parsed']; - $this->subject = $vars['subject']; + public $errors = array(); + public $id = null; + public $session = null; + public $unit = null; + protected $mail_sent = false; + protected $body = null; + protected $body_parsed = null; + protected $account_id = null; + protected $images = array(); + protected $subject = null; + protected $recipient_field; + protected $recipient; + protected $html = 1; + protected $cron_only = 0; + public $icon = "fa-envelope"; + public $type = "Email"; + protected $subject_parsed = null; + protected $mostrecent = "most recent reported address"; + + /** + * An array of unit's exportable attributes + * @var array + */ + public $export_attribs = array('type', 'description', 'position', 'special', 'subject', 'account_id', 'recipient_field', 'body', 'cron_only'); + + public function __construct($fdb, $session = null, $unit = null, $run_session = NULL, $run = NULL) { + parent::__construct($fdb, $session, $unit, $run_session, $run); + + if ($this->id): + $vars = $this->dbh->findRow('survey_emails', array('id' => $this->id)); + if ($vars): + $this->account_id = $vars['account_id']; + $this->recipient_field = $vars['recipient_field']; + $this->body = $vars['body']; + $this->body_parsed = $vars['body_parsed']; + $this->subject = $vars['subject']; // $this->html = $vars['html'] ? 1:0; - $this->html = 1; - $this->cron_only = (int)$vars['cron_only']; - - $this->valid = true; - endif; - endif; - } - - public function create($options) { - if (!$this->id) { - $this->id = parent::create('Email'); - } else { - $this->modify($options); - } - - $parsedown = new ParsedownExtra(); - if (isset($options['body'])) { - $this->recipient_field = $options['recipient_field']; - $this->body = $options['body']; - $this->subject = $options['subject']; - if (isset($options['account_id']) && is_numeric($options['account_id'])) { - $this->account_id = (int) $options['account_id']; - } + $this->html = 1; + $this->cron_only = (int) $vars['cron_only']; + + $this->valid = true; + endif; + endif; + } + + public function create($options) { + if (!$this->id) { + $this->id = parent::create('Email'); + } else { + $this->modify($options); + } + + $parsedown = new ParsedownExtra(); + if (isset($options['body'])) { + $this->recipient_field = $options['recipient_field']; + $this->body = $options['body']; + $this->subject = $options['subject']; + if (isset($options['account_id']) && is_numeric($options['account_id'])) { + $this->account_id = (int) $options['account_id']; + } // $this->html = $options['html'] ? 1:0; - $this->html = 1; - $this->cron_only = isset($options['cron_only']) ? 1 : 0; - } - if ($this->account_id === null): - $email_accounts = Site::getCurrentUser()->getEmailAccounts(); - if (count($email_accounts) > 0): - $this->account_id = current($email_accounts)['id']; - endif; - endif; - - if (!$this->knittingNeeded($this->body)) { - $this->body_parsed = $parsedown->text($this->body); - } - - $this->dbh->insert_update('survey_emails', array( - 'id' => $this->id, - 'account_id' => $this->account_id, - 'recipient_field' => $this->recipient_field, - 'body' => $this->body, - 'body_parsed' => $this->body_parsed, - 'subject' => $this->subject, - 'html' => $this->html, - 'cron_only' => $this->cron_only, - )); - - $this->valid = true; - - return true; - } - - public function getSubject() { - if ($this->subject_parsed === NULL): - if ($this->knittingNeeded($this->subject)): - if ($this->run_session_id): - $this->subject_parsed = $this->getParsedText($this->subject); - else: - return false; - endif; - else: - return $this->subject; - endif; - endif; - return $this->subject_parsed; - } - - protected function substituteLinks($body) { - $sess = null; - $run_name = null; - if (isset($this->run_name)) { - $run_name = $this->run_name; - $sess = isset($this->session) ? $this->session : "TESTCODE"; - } - - $body = do_run_shortcodes($body, $run_name, $sess); - return $body; - } - - protected function getBody($embed_email = true) { - if ($this->run_session_id): - $response = $this->getParsedBody($this->body, true); - if($response === false): - return false; - else: - if (isset($response['body'])): - $this->body_parsed = $response['body']; - endif; - if (isset($response['images'])): - $this->images = $response['images']; - endif; - endif; - - $this->body_parsed = $this->substituteLinks($this->body_parsed); // once more, in case it was pre-parsed - - return $this->body_parsed; - else: - alert("Session ID for email recipient is missing.", "alert-danger"); - return false; - endif; - } - - protected function getPotentialRecipientFields() { - $get_recips = $this->dbh->prepare("SELECT survey_studies.name AS survey,survey_items.name AS item FROM survey_items + $this->html = 1; + $this->cron_only = isset($options['cron_only']) ? 1 : 0; + } + if ($this->account_id === null): + $email_accounts = Site::getCurrentUser()->getEmailAccounts(); + if (count($email_accounts) > 0): + $this->account_id = current($email_accounts)['id']; + endif; + endif; + + if (!$this->knittingNeeded($this->body)) { + $this->body_parsed = $parsedown->text($this->body); + } + + $this->dbh->insert_update('survey_emails', array( + 'id' => $this->id, + 'account_id' => $this->account_id, + 'recipient_field' => $this->recipient_field, + 'body' => $this->body, + 'body_parsed' => $this->body_parsed, + 'subject' => $this->subject, + 'html' => $this->html, + 'cron_only' => $this->cron_only, + )); + + $this->valid = true; + + return true; + } + + public function getSubject() { + if ($this->subject_parsed === NULL): + if ($this->knittingNeeded($this->subject)): + if ($this->run_session_id): + $this->subject_parsed = $this->getParsedText($this->subject); + else: + return false; + endif; + else: + return $this->subject; + endif; + endif; + return $this->subject_parsed; + } + + protected function substituteLinks($body) { + $sess = null; + $run_name = null; + if (isset($this->run_name)) { + $run_name = $this->run_name; + $sess = isset($this->session) ? $this->session : "TESTCODE"; + } + + $body = do_run_shortcodes($body, $run_name, $sess); + return $body; + } + + protected function getBody($embed_email = true) { + if ($this->run_session_id): + $response = $this->getParsedBody($this->body, true); + if ($response === false): + return false; + else: + if (isset($response['body'])): + $this->body_parsed = $response['body']; + endif; + if (isset($response['images'])): + $this->images = $response['images']; + endif; + endif; + + $this->body_parsed = $this->substituteLinks($this->body_parsed); // once more, in case it was pre-parsed + + return $this->body_parsed; + else: + alert("Session ID for email recipient is missing.", "alert-danger"); + return false; + endif; + } + + protected function getPotentialRecipientFields() { + $get_recips = $this->dbh->prepare("SELECT survey_studies.name AS survey,survey_items.name AS item FROM survey_items LEFT JOIN survey_studies ON survey_studies.id = survey_items.study_id LEFT JOIN survey_run_units ON survey_studies.id = survey_run_units.unit_id LEFT JOIN survey_runs ON survey_runs.id = survey_run_units.run_id WHERE survey_runs.id = :run_id AND survey_items.type = 'email'"); - // fixme: if the last reported email thing is known to work, show only linked email addresses here. - $get_recips->bindValue(':run_id', $this->run_id); - $get_recips->execute(); - - $recips = array( array( "id" => $this->mostrecent, "text" => $this->mostrecent )); - while($res = $get_recips->fetch(PDO::FETCH_ASSOC)): - $email = $res['survey'] . "$" . $res['item']; - $recips[] = array("id" => $email, "text" => $email); - endwhile; - return $recips; - } - - public function displayForRun($prepend = '') { - $dialog = Template::get($this->getUnitTemplatePath(), array( - 'email' => $this, - 'prepend' => $prepend, - 'email_accounts' => Site::getCurrentUser()->getEmailAccounts(), - 'body' => $this->body, - 'subject' => $this->subject, - 'account_id' => $this->account_id, - 'cron_only' => $this->cron_only, - 'recipient_field' => $this->recipient_field, - 'potentialRecipientFields' => $this->getPotentialRecipientFields(), - )); - - return parent::runDialog($dialog); - } - - public function getRecipientField($return_format = 'json', $return_session = false) { - if (empty($this->recipient_field) || $this->recipient_field === $this->mostrecent) { - $recent_email_query = " + // fixme: if the last reported email thing is known to work, show only linked email addresses here. + $get_recips->bindValue(':run_id', $this->run_id); + $get_recips->execute(); + + $recips = array(array("id" => $this->mostrecent, "text" => $this->mostrecent)); + while ($res = $get_recips->fetch(PDO::FETCH_ASSOC)): + $email = $res['survey'] . "$" . $res['item']; + $recips[] = array("id" => $email, "text" => $email); + endwhile; + return $recips; + } + + public function displayForRun($prepend = '') { + $dialog = Template::get($this->getUnitTemplatePath(), array( + 'email' => $this, + 'prepend' => $prepend, + 'email_accounts' => Site::getCurrentUser()->getEmailAccounts(), + 'body' => $this->body, + 'subject' => $this->subject, + 'account_id' => $this->account_id, + 'cron_only' => $this->cron_only, + 'recipient_field' => $this->recipient_field, + 'potentialRecipientFields' => $this->getPotentialRecipientFields(), + )); + + return parent::runDialog($dialog); + } + + public function getRecipientField($return_format = 'json', $return_session = false) { + if (empty($this->recipient_field) || $this->recipient_field === $this->mostrecent) { + $recent_email_query = " SELECT survey_items_display.answer AS email FROM survey_unit_sessions LEFT JOIN survey_units ON survey_units.id = survey_unit_sessions.unit_id AND survey_units.type = 'Survey' LEFT JOIN survey_run_units ON survey_run_units.unit_id = survey_units.id @@ -195,154 +194,154 @@ public function getRecipientField($return_format = 'json', $return_session = fal LIMIT 1 "; - $get_recip = $this->dbh->prepare($recent_email_query); - $get_recip->bindValue(':run_id', $this->run_id); - $get_recip->bindValue(':run_session_id', $this->run_session_id); - $get_recip->execute(); - - $res = $get_recip->fetch(PDO::FETCH_ASSOC); - $recipient = array_val($res, 'email', null); - } else { - $opencpu_vars = $this->getUserDataInRun($this->recipient_field); - $recipient = opencpu_evaluate($this->recipient_field, $opencpu_vars, $return_format, null, $return_session); - } - - return $recipient; - } - - public function sendMail($who = NULL) { - $this->mail_sent = false; - - if ($who === null): - $this->recipient = $this->getRecipientField(); - else: - $this->recipient = $who; - endif; - - if ($this->recipient == null): - //formr_log("Email recipient could not be determined from this field definition " . $this->recipient_field); - alert("We could not find an email recipient. Session: {$this->session}", 'alert-danger'); - return false; - endif; - - if ($this->account_id === null): - alert("The study administrator (you?) did not set up an email account. Do it now and then select the account in the email dropdown.", 'alert-danger'); - return false; - endif; - - $run_session = $this->run_session; - - $testing = !$run_session || $run_session->isTesting(); - - $acc = new EmailAccount($this->dbh, $this->account_id, null); - $mailing_themselves = (is_array($acc->account) && $acc->account["from"] === $this->recipient) || - (($user = Site::getCurrentUser()) && $user->email === $this->recipient) || - ($this->run && $this->run->getOwner()->email === $this->recipient); - - $mails_sent = $this->numberOfEmailsSent(); - $error = null; - $warning = null; - if(!$mailing_themselves): - if ($mails_sent['in_last_1m'] > 0): - if($mails_sent['in_last_1m'] < 3 && $testing): - $warning = sprintf("We already sent %d mail to this recipient in the last minute. An email was sent, because you're currently testing, but it would have been delayed for a real user, to avoid allegations of spamming.", $mails_sent['in_last_1m']); - else: - $error = sprintf("We already sent %d mail to this recipient in the last minute. No email was sent.", $mails_sent['in_last_1m']); - endif; - elseif ($mails_sent['in_last_10m'] > 1): - if($mails_sent['in_last_10m'] < 10 && $testing): - $warning = sprintf("We already sent %d mail to this recipient in the last 10 minutes. An email was sent, because you're currently testing, but it would have been delayed for a real user, to avoid allegations of spamming.", $mails_sent['in_last_10m']); - else: - $error = sprintf("We already sent %d mail to this recipient in the last 10 minutes. No email was sent.", $mails_sent['in_last_10m']); - endif; - elseif ($mails_sent['in_last_1h'] > 2): - if($mails_sent['in_last_1h'] < 10 && $testing): - $warning = sprintf("We already sent %d mails to this recipient in the last hour. An email was sent, because you're currently testing, but it would have been delayed for a real user, to avoid allegations of spamming.", $mails_sent['in_last_1h']); - else: - $error = sprintf("We already sent %d mails to this recipient in the last hour. No email was sent.", $mails_sent['in_last_1h']); - endif; - elseif ($mails_sent['in_last_1d'] > 9 && !$testing): - $error = sprintf("We already sent %d mails to this recipient in the last day. No email was sent.", $mails_sent['in_last_1d']); - elseif ($mails_sent['in_last_1w'] > 60 && !$testing): - $error = sprintf("We already sent %d mails to this recipient in the last week. No email was sent.", $mails_sent['in_last_1w']); - endif; - else: - if ($mails_sent['in_last_1m'] > 1 || $mails_sent['in_last_1d'] > 100): - $error = sprintf("Too many emails are being sent to the study administrator, %d mails today. Please wait a little.", $mails_sent['in_last_1d']); - endif; - endif; - - if ($error !== null) { - $error = "Session: {$this->session}:\n {$error}"; - alert(nl2br($error), 'alert-danger'); - return false; - } - - if ($warning !== null) { - $warning = "Session: {$this->session}:\n {$warning}"; - alert(nl2br($warning), 'alert-info'); - } - - // if formr is configured to use the email queue then add mail to queue and return - if (Config::get('email.use_queue', false) === true && filter_var($this->recipient, FILTER_VALIDATE_EMAIL)) { - $this->mail_sent = $this->dbh->insert('survey_email_queue', array( - 'subject' => $this->getSubject(), - 'message' => $this->getBody(), - 'recipient' => $this->recipient, - 'created' => mysql_datetime(), - 'account_id' => (int) $this->account_id, - 'meta' => json_encode(array( - 'session_id' => $this->session_id, - 'email_id' => $this->id, - 'embedded_images' => $this->images, - 'attachments' => '' - )), - )); - return $this->mail_sent; - } - - $mail = $acc->makeMailer(); + $get_recip = $this->dbh->prepare($recent_email_query); + $get_recip->bindValue(':run_id', $this->run_id); + $get_recip->bindValue(':run_session_id', $this->run_session_id); + $get_recip->execute(); + + $res = $get_recip->fetch(PDO::FETCH_ASSOC); + $recipient = array_val($res, 'email', null); + } else { + $opencpu_vars = $this->getUserDataInRun($this->recipient_field); + $recipient = opencpu_evaluate($this->recipient_field, $opencpu_vars, $return_format, null, $return_session); + } + + return $recipient; + } + + public function sendMail($who = NULL) { + $this->mail_sent = false; + + if ($who === null): + $this->recipient = $this->getRecipientField(); + else: + $this->recipient = $who; + endif; + + if ($this->recipient == null): + //formr_log("Email recipient could not be determined from this field definition " . $this->recipient_field); + alert("We could not find an email recipient. Session: {$this->session}", 'alert-danger'); + return false; + endif; + + if ($this->account_id === null): + alert("The study administrator (you?) did not set up an email account. Do it now and then select the account in the email dropdown.", 'alert-danger'); + return false; + endif; + + $run_session = $this->run_session; + + $testing = !$run_session || $run_session->isTesting(); + + $acc = new EmailAccount($this->dbh, $this->account_id, null); + $mailing_themselves = (is_array($acc->account) && $acc->account["from"] === $this->recipient) || + (($user = Site::getCurrentUser()) && $user->email === $this->recipient) || + ($this->run && $this->run->getOwner()->email === $this->recipient); + + $mails_sent = $this->numberOfEmailsSent(); + $error = null; + $warning = null; + if (!$mailing_themselves): + if ($mails_sent['in_last_1m'] > 0): + if ($mails_sent['in_last_1m'] < 3 && $testing): + $warning = sprintf("We already sent %d mail to this recipient in the last minute. An email was sent, because you're currently testing, but it would have been delayed for a real user, to avoid allegations of spamming.", $mails_sent['in_last_1m']); + else: + $error = sprintf("We already sent %d mail to this recipient in the last minute. No email was sent.", $mails_sent['in_last_1m']); + endif; + elseif ($mails_sent['in_last_10m'] > 1): + if ($mails_sent['in_last_10m'] < 10 && $testing): + $warning = sprintf("We already sent %d mail to this recipient in the last 10 minutes. An email was sent, because you're currently testing, but it would have been delayed for a real user, to avoid allegations of spamming.", $mails_sent['in_last_10m']); + else: + $error = sprintf("We already sent %d mail to this recipient in the last 10 minutes. No email was sent.", $mails_sent['in_last_10m']); + endif; + elseif ($mails_sent['in_last_1h'] > 2): + if ($mails_sent['in_last_1h'] < 10 && $testing): + $warning = sprintf("We already sent %d mails to this recipient in the last hour. An email was sent, because you're currently testing, but it would have been delayed for a real user, to avoid allegations of spamming.", $mails_sent['in_last_1h']); + else: + $error = sprintf("We already sent %d mails to this recipient in the last hour. No email was sent.", $mails_sent['in_last_1h']); + endif; + elseif ($mails_sent['in_last_1d'] > 9 && !$testing): + $error = sprintf("We already sent %d mails to this recipient in the last day. No email was sent.", $mails_sent['in_last_1d']); + elseif ($mails_sent['in_last_1w'] > 60 && !$testing): + $error = sprintf("We already sent %d mails to this recipient in the last week. No email was sent.", $mails_sent['in_last_1w']); + endif; + else: + if ($mails_sent['in_last_1m'] > 1 || $mails_sent['in_last_1d'] > 100): + $error = sprintf("Too many emails are being sent to the study administrator, %d mails today. Please wait a little.", $mails_sent['in_last_1d']); + endif; + endif; + + if ($error !== null) { + $error = "Session: {$this->session}:\n {$error}"; + alert(nl2br($error), 'alert-danger'); + return false; + } + + if ($warning !== null) { + $warning = "Session: {$this->session}:\n {$warning}"; + alert(nl2br($warning), 'alert-info'); + } + + // if formr is configured to use the email queue then add mail to queue and return + if (Config::get('email.use_queue', false) === true && filter_var($this->recipient, FILTER_VALIDATE_EMAIL)) { + $this->mail_sent = $this->dbh->insert('survey_email_queue', array( + 'subject' => $this->getSubject(), + 'message' => $this->getBody(), + 'recipient' => $this->recipient, + 'created' => mysql_datetime(), + 'account_id' => (int) $this->account_id, + 'meta' => json_encode(array( + 'session_id' => $this->session_id, + 'email_id' => $this->id, + 'embedded_images' => $this->images, + 'attachments' => '' + )), + )); + return $this->mail_sent; + } + + $mail = $acc->makeMailer(); // if($this->html) - $mail->IsHTML(true); - - $mail->AddAddress($this->recipient); - $mail->Subject = $this->getSubject(); - $mail->Body = $this->getBody(); - - if(filter_var($this->recipient, FILTER_VALIDATE_EMAIL) AND $mail->Body !== false AND $mail->Subject !== false): - foreach ($this->images AS $image_id => $image): - $local_image = APPLICATION_ROOT . 'tmp/' . uniqid() . $image_id; - copy($image, $local_image); - register_shutdown_function(create_function('', "unlink('{$local_image}');")); - - if (!$mail->AddEmbeddedImage($local_image, $image_id, $image_id, 'base64', 'image/png' )): - alert('Email with the subject "' . h($mail->Subject) . '" was not sent to ' . h($this->recipient) . ':
' . $mail->ErrorInfo, 'alert-danger'); - endif; - endforeach; - - if (!$mail->Send()): - alert('Email with the subject "' . h($mail->Subject) . '" was not sent to ' . h($this->recipient) . ':
' . $mail->ErrorInfo, 'alert-danger'); - else: - $this->mail_sent = true; - $this->logMail(); - endif; - else: - if(!filter_var($this->recipient, FILTER_VALIDATE_EMAIL)): - alert('Intended recipient was not a valid email address: '. $this->recipient, 'alert-danger'); - endif; - if($mail->Body === false): - alert('Email body empty or could not be dynamically generated.', 'alert-danger'); - endif; - if($mail->Subject === false): - alert('Email subject empty or could not be dynamically generated.', 'alert-danger'); - endif; - endif; - return $this->mail_sent; - } - - protected function numberOfEmailsSent() { - $log = $this->dbh->prepare("SELECT + $mail->IsHTML(true); + + $mail->AddAddress($this->recipient); + $mail->Subject = $this->getSubject(); + $mail->Body = $this->getBody(); + + if (filter_var($this->recipient, FILTER_VALIDATE_EMAIL) AND $mail->Body !== false AND $mail->Subject !== false): + foreach ($this->images AS $image_id => $image): + $local_image = APPLICATION_ROOT . 'tmp/' . uniqid() . $image_id; + copy($image, $local_image); + register_shutdown_function(create_function('', "unlink('{$local_image}');")); + + if (!$mail->AddEmbeddedImage($local_image, $image_id, $image_id, 'base64', 'image/png')): + alert('Email with the subject "' . h($mail->Subject) . '" was not sent to ' . h($this->recipient) . ':
' . $mail->ErrorInfo, 'alert-danger'); + endif; + endforeach; + + if (!$mail->Send()): + alert('Email with the subject "' . h($mail->Subject) . '" was not sent to ' . h($this->recipient) . ':
' . $mail->ErrorInfo, 'alert-danger'); + else: + $this->mail_sent = true; + $this->logMail(); + endif; + else: + if (!filter_var($this->recipient, FILTER_VALIDATE_EMAIL)): + alert('Intended recipient was not a valid email address: ' . $this->recipient, 'alert-danger'); + endif; + if ($mail->Body === false): + alert('Email body empty or could not be dynamically generated.', 'alert-danger'); + endif; + if ($mail->Subject === false): + alert('Email subject empty or could not be dynamically generated.', 'alert-danger'); + endif; + endif; + return $this->mail_sent; + } + + protected function numberOfEmailsSent() { + $log = $this->dbh->prepare("SELECT SUM(created > DATE_SUB(NOW(), INTERVAL 1 MINUTE)) AS in_last_1m, SUM(created > DATE_SUB(NOW(), INTERVAL 10 MINUTE)) AS in_last_10m, SUM(created > DATE_SUB(NOW(), INTERVAL 1 HOUR)) AS in_last_1h, @@ -350,66 +349,66 @@ protected function numberOfEmailsSent() { SUM(1) AS in_last_1w FROM `survey_email_log` WHERE recipient = :recipient AND created > DATE_SUB(NOW(), INTERVAL 7 DAY)"); - $log->bindParam(':recipient', $this->recipient); - $log->execute(); - return $log->fetch(PDO::FETCH_ASSOC); - } - - protected function logMail() { - if (!$this->session_id && $this->run_session) { - $unit = $this->run_session->getCurrentUnit(); - $session_id = $unit ? $unit['session_id'] : null; - } else { - $session_id = $this->session_id; - } - $query = "INSERT INTO `survey_email_log` (session_id, email_id, created, recipient) VALUES (:session_id, :email_id, NOW(), :recipient)"; - $this->dbh->exec($query, array( - 'session_id' => $session_id, - 'email_id' => $this->id, - 'recipient' => $this->recipient, - )); - } - - public function test() { - if (!$this->grabRandomSession()) { - return false; - } - - $user = Site::getCurrentUser(); - $receiver = $user->getEmail(); - - echo "

Recipient

"; - $recipient_field = $this->getRecipientField('', true); - if($recipient_field instanceof OpenCPU_Session) { - echo opencpu_debug($recipient_field, null, 'text'); - } else { - echo $this->mostrecent . ": " . $recipient_field; - } - - echo "

Subject

"; - if($this->knittingNeeded($this->subject)) { - echo $this->getParsedTextAdmin($this->subject); - } else { - echo $this->getSubject(); - } - - echo "

Body

"; - echo $this->getParsedBodyAdmin($this->body); - - echo "

Attempt to send email

"; - if($this->sendMail($receiver)) { - echo "

An email was sent to your own email address (". h($receiver). ").

"; - } else { - echo "

No email sent.

"; - } - - $results = $this->getSampleSessions(); - if ($results) { - if ($this->recipient_field === null OR trim($this->recipient_field) == '') { - $this->recipient_field = 'survey_users$email'; - } - - $test_tpl = ' + $log->bindParam(':recipient', $this->recipient); + $log->execute(); + return $log->fetch(PDO::FETCH_ASSOC); + } + + protected function logMail() { + if (!$this->session_id && $this->run_session) { + $unit = $this->run_session->getCurrentUnit(); + $session_id = $unit ? $unit['session_id'] : null; + } else { + $session_id = $this->session_id; + } + $query = "INSERT INTO `survey_email_log` (session_id, email_id, created, recipient) VALUES (:session_id, :email_id, NOW(), :recipient)"; + $this->dbh->exec($query, array( + 'session_id' => $session_id, + 'email_id' => $this->id, + 'recipient' => $this->recipient, + )); + } + + public function test() { + if (!$this->grabRandomSession()) { + return false; + } + + $user = Site::getCurrentUser(); + $receiver = $user->getEmail(); + + echo "

Recipient

"; + $recipient_field = $this->getRecipientField('', true); + if ($recipient_field instanceof OpenCPU_Session) { + echo opencpu_debug($recipient_field, null, 'text'); + } else { + echo $this->mostrecent . ": " . $recipient_field; + } + + echo "

Subject

"; + if ($this->knittingNeeded($this->subject)) { + echo $this->getParsedTextAdmin($this->subject); + } else { + echo $this->getSubject(); + } + + echo "

Body

"; + echo $this->getParsedBodyAdmin($this->body); + + echo "

Attempt to send email

"; + if ($this->sendMail($receiver)) { + echo "

An email was sent to your own email address (" . h($receiver) . ").

"; + } else { + echo "

No email sent.

"; + } + + $results = $this->getSampleSessions(); + if ($results) { + if ($this->recipient_field === null OR trim($this->recipient_field) == '') { + $this->recipient_field = 'survey_users$email'; + } + + $test_tpl = ' @@ -421,71 +420,71 @@ public function test() {
'; - $row_tpl = ' + $row_tpl = ' %{session} (%{position}) %{result} '; - $rows = ''; - foreach ($results as $row) { - $this->run_session_id = $row['id']; - $email = stringBool($this->getRecipientField()); - $class = filter_var($email, FILTER_VALIDATE_EMAIL) ? '' : 'text-warning'; - $rows .= Template::replace($row_tpl, array( - 'session' => $row['session'], - 'position' => $row['position'], - 'result' => stringBool($email), - 'class' => $class, - )); - } - - echo Template::replace($test_tpl, array('rows' => $rows)); - } - $this->run_session_id = null; - } - - protected function sessionCanReceiveMails() { - // If not executing under a run session or no_mail is null the user can receive email - if (!$this->run_session || $this->run_session->no_mail === null) { - return true; - } - - // If no mail is 0 then user has choose not to receive emails - if ((int)$this->run_session->no_mail === 0) { - return false; - } - - // If no_mail is set && the timestamp is less that current time then the snooze period has expired - if ($this->run_session->no_mail <= time()) { - // modify subscription settings - $this->run_session->saveSettings(array('no_email' => '1'), array('no_email' => null)); - return true; - } - - return false; - } - - public function exec() { - // If emails should be sent only when cron is active and unit is not called by cron, then end it and move on - if ($this->cron_only && !$this->called_by_cron) { - $this->end(); - return false; - } - - // Check if user is enabled to receive emails - if (!$this->sessionCanReceiveMails()) { - return array('body' => "

Session {$this->session} cannot receive mails at this time

"); - } - - // Try to send email - $err = $this->sendMail(); - if ($this->mail_sent) { - $this->end(); - return false; - } - return array('body' => $err); - } + $rows = ''; + foreach ($results as $row) { + $this->run_session_id = $row['id']; + $email = stringBool($this->getRecipientField()); + $class = filter_var($email, FILTER_VALIDATE_EMAIL) ? '' : 'text-warning'; + $rows .= Template::replace($row_tpl, array( + 'session' => $row['session'], + 'position' => $row['position'], + 'result' => stringBool($email), + 'class' => $class, + )); + } + + echo Template::replace($test_tpl, array('rows' => $rows)); + } + $this->run_session_id = null; + } + + protected function sessionCanReceiveMails() { + // If not executing under a run session or no_mail is null the user can receive email + if (!$this->run_session || $this->run_session->no_mail === null) { + return true; + } + + // If no mail is 0 then user has choose not to receive emails + if ((int) $this->run_session->no_mail === 0) { + return false; + } + + // If no_mail is set && the timestamp is less that current time then the snooze period has expired + if ($this->run_session->no_mail <= time()) { + // modify subscription settings + $this->run_session->saveSettings(array('no_email' => '1'), array('no_email' => null)); + return true; + } + + return false; + } + + public function exec() { + // If emails should be sent only when cron is active and unit is not called by cron, then end it and move on + if ($this->cron_only && !$this->called_by_cron) { + $this->end(); + return false; + } + + // Check if user is enabled to receive emails + if (!$this->sessionCanReceiveMails()) { + return array('body' => "

Session {$this->session} cannot receive mails at this time

"); + } + + // Try to send email + $err = $this->sendMail(); + if ($this->mail_sent) { + $this->end(); + return false; + } + return array('body' => $err); + } } diff --git a/application/Model/EmailAccount.php b/application/Model/EmailAccount.php index c0536420a..e265da962 100644 --- a/application/Model/EmailAccount.php +++ b/application/Model/EmailAccount.php @@ -2,133 +2,133 @@ class EmailAccount { - public $id = null; - public $user_id = null; - public $valid = null; - public $account = array(); - - /** - * @var DB - */ - private $dbh; - - const AK_GLUE = ':fmr:'; - - public function __construct($fdb, $id, $user_id) { - $this->dbh = $fdb; - $this->id = (int) $id; - $this->user_id = (int) $user_id; - - if ($id) { - $this->load(); - } - } - - protected function load() { - $this->account = $this->dbh->findRow('survey_email_accounts', array('id' => $this->id)); - if ($this->account) { - $this->valid = true; - $this->user_id = (int) $this->account['user_id']; - if ($this->account['auth_key']) { - list($username, $password) = explode(self::AK_GLUE, Crypto::decrypt($this->account['auth_key']), 2); - $this->account['username'] = $username; - $this->account['password'] = $password; - } - } - } - - public function create() { - $this->id = $this->dbh->insert('survey_email_accounts', array('user_id' => $this->user_id, 'auth_key' => '')); - $this->load(); - return $this->id; - } - - public function changeSettings($posted) { - $old_password = $this->account['password']; - $this->account = $posted; - - $params = array( - 'id' => $this->id, - 'fromm' => $this->account['from'], - 'from_name' => $this->account['from_name'], - 'host' => $this->account['host'], - 'port' => $this->account['port'], - 'tls' => $this->account['tls'], - 'username' => $this->account['username'], - 'password' => $old_password, - ); - - if (trim($posted['password']) != '') { - $params['password'] = $this->account['password']; - } - - $params['auth_key'] = Crypto::encrypt(array($params['username'], $params['password']), self::AK_GLUE); - if (!$params['auth_key']) { - return false; - } - $params['password'] = ''; - - $query = "UPDATE `survey_email_accounts` + public $id = null; + public $user_id = null; + public $valid = null; + public $account = array(); + + /** + * @var DB + */ + private $dbh; + + const AK_GLUE = ':fmr:'; + + public function __construct($fdb, $id, $user_id) { + $this->dbh = $fdb; + $this->id = (int) $id; + $this->user_id = (int) $user_id; + + if ($id) { + $this->load(); + } + } + + protected function load() { + $this->account = $this->dbh->findRow('survey_email_accounts', array('id' => $this->id)); + if ($this->account) { + $this->valid = true; + $this->user_id = (int) $this->account['user_id']; + if ($this->account['auth_key']) { + list($username, $password) = explode(self::AK_GLUE, Crypto::decrypt($this->account['auth_key']), 2); + $this->account['username'] = $username; + $this->account['password'] = $password; + } + } + } + + public function create() { + $this->id = $this->dbh->insert('survey_email_accounts', array('user_id' => $this->user_id, 'auth_key' => '')); + $this->load(); + return $this->id; + } + + public function changeSettings($posted) { + $old_password = $this->account['password']; + $this->account = $posted; + + $params = array( + 'id' => $this->id, + 'fromm' => $this->account['from'], + 'from_name' => $this->account['from_name'], + 'host' => $this->account['host'], + 'port' => $this->account['port'], + 'tls' => $this->account['tls'], + 'username' => $this->account['username'], + 'password' => $old_password, + ); + + if (trim($posted['password']) != '') { + $params['password'] = $this->account['password']; + } + + $params['auth_key'] = Crypto::encrypt(array($params['username'], $params['password']), self::AK_GLUE); + if (!$params['auth_key']) { + return false; + } + $params['password'] = ''; + + $query = "UPDATE `survey_email_accounts` SET `from` = :fromm, `from_name` = :from_name, `host` = :host, `port` = :port, `tls` = :tls, `username` = :username, `password` = :password, `auth_key` = :auth_key WHERE id = :id LIMIT 1"; - $this->dbh->exec($query, $params); - $this->load(); - return true; - } - - public function test() { - $receiver = $this->account['from']; - $mail = $this->makeMailer(); - - $mail->AddAddress($receiver); - $mail->Subject = 'formr: account test success'; - $mail->Body = Template::get('email/test-account.txt'); - - if (!$mail->Send()) { - alert('Account Test Failed: ' . $mail->ErrorInfo, 'alert-danger'); - return false; - } else { - alert("An email was sent to {$receiver}. Please confirm that you received this email.", 'alert-success'); - return true; - } - } - - public function makeMailer() { - $mail = new PHPMailer(); - $mail->SetLanguage("de", "/"); - - $mail->IsSMTP(); // telling the class to use SMTP - $mail->Mailer = "smtp"; - $mail->Host = $this->account['host']; - $mail->Port = $this->account['port']; - if ($this->account['tls']) { - $mail->SMTPSecure = 'tls'; - } else { - $mail->SMTPSecure = 'ssl'; - } - if (isset($this->account['username'])) { - $mail->SMTPAuth = true; // turn on SMTP authentication - $mail->Username = $this->account['username']; // SMTP username - $mail->Password = $this->account['password']; // SMTP password - } else { - $mail->SMTPAuth = false; - $mail->SMTPSecure = false; - } - $mail->From = $this->account['from']; - $mail->FromName = $this->account['from_name']; - $mail->AddReplyTo($this->account['from'], $this->account['from_name']); - $mail->CharSet = "utf-8"; - $mail->WordWrap = 65; // set word wrap to 65 characters - if (is_array(Config::get('email.smtp_options'))) { - $mail->SMTPOptions = array_merge($mail->SMTPOptions, Config::get('email.smtp_options')); - } - - return $mail; - } - - public function delete() { - return $this->dbh->update('survey_email_accounts', array('deleted' => 1), array('id' => $this->id)); - } + $this->dbh->exec($query, $params); + $this->load(); + return true; + } + + public function test() { + $receiver = $this->account['from']; + $mail = $this->makeMailer(); + + $mail->AddAddress($receiver); + $mail->Subject = 'formr: account test success'; + $mail->Body = Template::get('email/test-account.txt'); + + if (!$mail->Send()) { + alert('Account Test Failed: ' . $mail->ErrorInfo, 'alert-danger'); + return false; + } else { + alert("An email was sent to {$receiver}. Please confirm that you received this email.", 'alert-success'); + return true; + } + } + + public function makeMailer() { + $mail = new PHPMailer(); + $mail->SetLanguage("de", "/"); + + $mail->IsSMTP(); // telling the class to use SMTP + $mail->Mailer = "smtp"; + $mail->Host = $this->account['host']; + $mail->Port = $this->account['port']; + if ($this->account['tls']) { + $mail->SMTPSecure = 'tls'; + } else { + $mail->SMTPSecure = 'ssl'; + } + if (isset($this->account['username'])) { + $mail->SMTPAuth = true; // turn on SMTP authentication + $mail->Username = $this->account['username']; // SMTP username + $mail->Password = $this->account['password']; // SMTP password + } else { + $mail->SMTPAuth = false; + $mail->SMTPSecure = false; + } + $mail->From = $this->account['from']; + $mail->FromName = $this->account['from_name']; + $mail->AddReplyTo($this->account['from'], $this->account['from_name']); + $mail->CharSet = "utf-8"; + $mail->WordWrap = 65; // set word wrap to 65 characters + if (is_array(Config::get('email.smtp_options'))) { + $mail->SMTPOptions = array_merge($mail->SMTPOptions, Config::get('email.smtp_options')); + } + + return $mail; + } + + public function delete() { + return $this->dbh->update('survey_email_accounts', array('deleted' => 1), array('id' => $this->id)); + } } diff --git a/application/Model/External.php b/application/Model/External.php index 12577a288..dfd9c6af5 100644 --- a/application/Model/External.php +++ b/application/Model/External.php @@ -2,180 +2,180 @@ class External extends RunUnit { - public $errors = array(); - public $id = null; - public $session = null; - public $unit = null; - protected $address = null; - protected $api_end = 0; - protected $expire_after = 0; - public $icon = "fa-external-link-square"; - public $type = "External"; - - /** - * An array of unit's exportable attributes - * @var array - */ - public $export_attribs = array('type', 'description', 'position', 'special', 'address', 'api_end'); - - public function __construct($fdb, $session = null, $unit = null, $run_session = NULL, $run = NULL) { - parent::__construct($fdb, $session, $unit, $run_session, $run); - - if ($this->id): - $vars = $this->dbh->findRow('survey_externals', array('id' => $this->id), 'id, address, api_end, expire_after'); - if ($vars): - $this->address = $vars['address']; - $this->api_end = $vars['api_end'] ? 1 : 0; - $this->expire_after = (int) $vars['expire_after']; - $this->valid = true; - endif; - endif; - } - - public function create($options) { - $this->dbh->beginTransaction(); - if (!$this->id) { - $this->id = parent::create('External'); - } else { - $this->modify($options); - } - - if (isset($options['external_link'])) { - $this->address = $options['external_link']; - $this->api_end = $options['api_end'] ? 1 : 0; - $this->expire_after = (int) $options['expire_after']; - } - - $this->dbh->insert_update('survey_externals', array( - 'id' => $this->id, - 'address' => $this->address, - 'api_end' => $this->api_end, - 'expire_after' => $this->expire_after, - )); - $this->dbh->commit(); - $this->valid = true; - - return true; - } - - public function displayForRun($prepend = '') { - $dialog = Template::get($this->getUnitTemplatePath(), array( - 'prepend' => $prepend, - 'address' => $this->address, - 'expire_after' => $this->expire_after, - 'api_end' => $this->api_end, - )); - - return parent::runDialog($dialog); - } - - public function removeFromRun($special = null) { - return $this->delete($special); - } - - private function isR($address) { - if (substr($address, 0, 4) == "http") { - return false; - } - return true; - } - - private function isAddress($address) { - return !$this->isR($address); - } - - private function makeAddress($address) { - $login_link = run_url($this->run_name, null, array('code' => $this->session)); - $address = str_replace("{{login_link}}", $login_link, $address); - $address = str_replace("{{login_code}}", $this->session, $address); - return $address; - } - - public function test() { - if ($this->isR($this->address)) { - if ($results = $this->getSampleSessions()) { - if (!$results) { - return false; - } - - $this->run_session_id = current($results)['id']; - - $opencpu_vars = $this->getUserDataInRun($this->address); - $ocpu_session = opencpu_evaluate($this->address, $opencpu_vars, '', null, true); - $output = opencpu_debug($ocpu_session, null, 'text'); - } else { - $output = ''; - } - } else { - $output = Template::replace('%{address}', array('address' => $this->address)); - } - - $this->session = "TESTCODE"; - echo $this->makeAddress($output); - } - - private function hasExpired() { - $expire = (int)$this->expire_after; - if ($expire === 0) { - return false; - } else { - $last = $this->run_session->unit_session->created; - if (!$last || !strtotime($last)) { - return false; - } - - $expire_ts = strtotime($last) + ($expire * 60); - if (($expired = $expire_ts < time())) { - return true; - } else { - $this->execData['expire_timestamp'] = $expire_ts; - return false; - } - } - } - - public function exec() { - // never redirect, if we're just in the cronjob. just text for expiry - $expired = $this->hasExpired(); - if ($this->called_by_cron) { - if ($expired) { - $this->expire(); - return false; - } - } - - // if it's the user, redirect them or do the call - if ($this->isR($this->address)) { - $goto = null; - $opencpu_vars = $this->getUserDataInRun($this->address); - $result = opencpu_evaluate($this->address, $opencpu_vars); - - if ($result === null) { - return true; // don't go anywhere, wait for the error to be fixed! - } elseif($result === false) { - $this->end(); - return false; // go on, no redirect - } elseif($this->isAddress($result)) { - $goto = $result; - } - } else { // the simplest case, just an address - $goto = $this->address; - } - - // replace the code placeholder, if any - $goto = $this->makeAddress($goto); - - // never redirect if we're just in the cron job - if (!$this->called_by_cron) { - // sometimes we aren't able to control the other end - if (!$this->api_end) { - $this->end(); - } - - redirect_to($goto); - return false; - } - return true; - } + public $errors = array(); + public $id = null; + public $session = null; + public $unit = null; + protected $address = null; + protected $api_end = 0; + protected $expire_after = 0; + public $icon = "fa-external-link-square"; + public $type = "External"; + + /** + * An array of unit's exportable attributes + * @var array + */ + public $export_attribs = array('type', 'description', 'position', 'special', 'address', 'api_end'); + + public function __construct($fdb, $session = null, $unit = null, $run_session = NULL, $run = NULL) { + parent::__construct($fdb, $session, $unit, $run_session, $run); + + if ($this->id): + $vars = $this->dbh->findRow('survey_externals', array('id' => $this->id), 'id, address, api_end, expire_after'); + if ($vars): + $this->address = $vars['address']; + $this->api_end = $vars['api_end'] ? 1 : 0; + $this->expire_after = (int) $vars['expire_after']; + $this->valid = true; + endif; + endif; + } + + public function create($options) { + $this->dbh->beginTransaction(); + if (!$this->id) { + $this->id = parent::create('External'); + } else { + $this->modify($options); + } + + if (isset($options['external_link'])) { + $this->address = $options['external_link']; + $this->api_end = $options['api_end'] ? 1 : 0; + $this->expire_after = (int) $options['expire_after']; + } + + $this->dbh->insert_update('survey_externals', array( + 'id' => $this->id, + 'address' => $this->address, + 'api_end' => $this->api_end, + 'expire_after' => $this->expire_after, + )); + $this->dbh->commit(); + $this->valid = true; + + return true; + } + + public function displayForRun($prepend = '') { + $dialog = Template::get($this->getUnitTemplatePath(), array( + 'prepend' => $prepend, + 'address' => $this->address, + 'expire_after' => $this->expire_after, + 'api_end' => $this->api_end, + )); + + return parent::runDialog($dialog); + } + + public function removeFromRun($special = null) { + return $this->delete($special); + } + + private function isR($address) { + if (substr($address, 0, 4) == "http") { + return false; + } + return true; + } + + private function isAddress($address) { + return !$this->isR($address); + } + + private function makeAddress($address) { + $login_link = run_url($this->run_name, null, array('code' => $this->session)); + $address = str_replace("{{login_link}}", $login_link, $address); + $address = str_replace("{{login_code}}", $this->session, $address); + return $address; + } + + public function test() { + if ($this->isR($this->address)) { + if ($results = $this->getSampleSessions()) { + if (!$results) { + return false; + } + + $this->run_session_id = current($results)['id']; + + $opencpu_vars = $this->getUserDataInRun($this->address); + $ocpu_session = opencpu_evaluate($this->address, $opencpu_vars, '', null, true); + $output = opencpu_debug($ocpu_session, null, 'text'); + } else { + $output = ''; + } + } else { + $output = Template::replace('%{address}', array('address' => $this->address)); + } + + $this->session = "TESTCODE"; + echo $this->makeAddress($output); + } + + private function hasExpired() { + $expire = (int) $this->expire_after; + if ($expire === 0) { + return false; + } else { + $last = $this->run_session->unit_session->created; + if (!$last || !strtotime($last)) { + return false; + } + + $expire_ts = strtotime($last) + ($expire * 60); + if (($expired = $expire_ts < time())) { + return true; + } else { + $this->execData['expire_timestamp'] = $expire_ts; + return false; + } + } + } + + public function exec() { + // never redirect, if we're just in the cronjob. just text for expiry + $expired = $this->hasExpired(); + if ($this->called_by_cron) { + if ($expired) { + $this->expire(); + return false; + } + } + + // if it's the user, redirect them or do the call + if ($this->isR($this->address)) { + $goto = null; + $opencpu_vars = $this->getUserDataInRun($this->address); + $result = opencpu_evaluate($this->address, $opencpu_vars); + + if ($result === null) { + return true; // don't go anywhere, wait for the error to be fixed! + } elseif ($result === false) { + $this->end(); + return false; // go on, no redirect + } elseif ($this->isAddress($result)) { + $goto = $result; + } + } else { // the simplest case, just an address + $goto = $this->address; + } + + // replace the code placeholder, if any + $goto = $this->makeAddress($goto); + + // never redirect if we're just in the cron job + if (!$this->called_by_cron) { + // sometimes we aren't able to control the other end + if (!$this->api_end) { + $this->end(); + } + + redirect_to($goto); + return false; + } + return true; + } } diff --git a/application/Model/Item.php b/application/Model/Item.php index 1d2bb6f54..393fec195 100644 --- a/application/Model/Item.php +++ b/application/Model/Item.php @@ -2,53 +2,53 @@ class ItemFactory { - public $errors; - public $showifs = array(); - public $openCPU_errors = array(); - private $choice_lists = array(); - private $used_choice_lists = array(); - - function __construct($choice_lists) { - $this->choice_lists = $choice_lists; - } - - public function make($item) { - $type = ""; - if (isset($item['type'])) { - $type = $item['type']; - } - - if (!empty($item['choice_list'])) { // if it has choices - if (isset($this->choice_lists[$item['choice_list']])) { // if this choice_list exists - $item['choices'] = $this->choice_lists[$item['choice_list']]; // take it - $this->used_choice_lists[] = $item['choice_list']; // check it as used - } else { - $item['val_errors'] = array(__("Choice list %s does not exist, but is specified for item %s", $item['choice_list'], $item['name'])); - } - } - - $type = str_replace('-', '_', $type); - $class = $this->getItemClass($type); - - if (!class_exists($class, true)) { - return false; - } - - return new $class($item); - } - - public function unusedChoiceLists() { - return array_diff( - array_keys($this->choice_lists), $this->used_choice_lists - ); - } - - protected function getItemClass($type) { - $parts = explode('_', $type); - $parts = array_map('ucwords', $parts); - $item = implode('', $parts); - return $item . '_Item'; - } + public $errors; + public $showifs = array(); + public $openCPU_errors = array(); + private $choice_lists = array(); + private $used_choice_lists = array(); + + function __construct($choice_lists) { + $this->choice_lists = $choice_lists; + } + + public function make($item) { + $type = ""; + if (isset($item['type'])) { + $type = $item['type']; + } + + if (!empty($item['choice_list'])) { // if it has choices + if (isset($this->choice_lists[$item['choice_list']])) { // if this choice_list exists + $item['choices'] = $this->choice_lists[$item['choice_list']]; // take it + $this->used_choice_lists[] = $item['choice_list']; // check it as used + } else { + $item['val_errors'] = array(__("Choice list %s does not exist, but is specified for item %s", $item['choice_list'], $item['name'])); + } + } + + $type = str_replace('-', '_', $type); + $class = $this->getItemClass($type); + + if (!class_exists($class, true)) { + return false; + } + + return new $class($item); + } + + public function unusedChoiceLists() { + return array_diff( + array_keys($this->choice_lists), $this->used_choice_lists + ); + } + + protected function getItemClass($type) { + $parts = explode('_', $type); + $parts = array_map('ucwords', $parts); + $item = implode('', $parts); + return $item . '_Item'; + } } @@ -58,330 +58,328 @@ protected function getItemClass($type) { * The base class should also work for inputs like date, datetime which are either native or polyfilled but don't require special handling here * */ - class Item { - public $id = null; - public $name = null; - public $type = null; - public $type_options = null; - public $choice_list = null; - public $label = null; - public $label_parsed = null; - public $optional = 0; - public $class = null; - public $showif = null; - public $js_showif = null; - public $value = null; // syntax for sticky value - public $value_validated = null; - public $order = null; - public $block_order = null; - public $item_order = null; - public $displaycount = null; - public $error = null; - public $dont_validate = null; - public $reply = null; - public $val_errors = array(); - public $val_warnings = array(); - public $mysql_field = 'TEXT DEFAULT NULL'; - public $choices = array(); - public $hidden = false; - public $no_user_input_required = false; - public $save_in_results_table = true; - public $input_attributes = array(); // so that the pre-set value can be set externally - public $parent_attributes = array(); - public $presetValue = null; - public $allowed_classes = array(); - public $skip_validation = false; - public $data_showif = false; - - protected $prepend = null; - protected $append = null; - protected $type_options_array = array(); - protected $hasChoices = false; - protected $classes_controls = array('controls'); - protected $classes_wrapper = array('form-group', 'form-row'); - protected $classes_input = array(); - protected $classes_label = array('control-label'); - protected $presetValues = array(); - protected $probably_render = null; - protected $js_hidden = false; - - /** - * Minimized attributes - * - * @var array - */ - protected $_minimizedAttributes = array( - 'compact', 'checked', 'declare', 'readonly', 'disabled', 'selected', - 'defer', 'ismap', 'nohref', 'noshade', 'nowrap', 'multiple', 'noresize', - 'autoplay', 'controls', 'loop', 'muted', 'required', 'novalidate', 'formnovalidate' - ); - - /** - * Format to attribute - * - * @var string - */ - protected $_attributeFormat = '%s="%s"'; - - /** - * Format to attribute - * - * @var string - */ - protected $_minimizedAttributeFormat = '%s="%s"'; - - public function __construct($options = array()) { - // Load options to object - $optional = $this->optional; - foreach ($options as $property => $value) { - if (property_exists($this, $property)) { - $this->{$property} = $value; - } - } - - // Assign needed defaults - $this->id = isset($options['id']) ? $options['id'] : 0; - $this->label = isset($options['label']) ? $options['label'] : ''; - $this->label_parsed = isset($options['label_parsed']) ? $options['label_parsed'] : null; - $this->allowed_classes = Config::get('css_classes', array()); - - if (isset($options['type_options'])) { - $this->type_options = trim($options['type_options']); - $this->type_options_array = array($options['type_options']); - } - - if (empty($this->choice_list) && $this->hasChoices && $this->type_options != '') { - $lc = explode(' ', trim($this->type_options)); - if (count($lc) > 1) { - $this->choice_list = end($lc); - } - } - - if (!empty($options['val_error'])) { - $this->val_error = $options['val_error']; - } - - if (!empty($options['error'])) { - $this->error = $options['error']; - $this->classes_wrapper[] = "has-error"; - } - - if (isset($options['displaycount']) && $options['displaycount'] !== null) { - $this->displaycount = $options['displaycount']; - } - - $this->input_attributes['name'] = $this->name; - - $this->setMoreOptions(); - - // after the easily overridden setMoreOptions, some post-processing that is universal to all items. - if ($this->type == 'note') { - // notes can not be "required" - unset($options['optional']); - } - - if (isset($options['optional']) && $options['optional']) { - $this->optional = 1; - unset($options['optional']); - } elseif (isset($options['optional']) && !$options['optional']) { - $this->optional = 0; - } else { - $this->optional = $optional; - } - - if (!$this->optional) { - $this->classes_wrapper[] = 'required'; - $this->input_attributes['required'] = 'required'; - } else { - $this->classes_wrapper[] = 'optional'; - } - - if (!empty($options['class'])) { - $this->classes_wrapper = array_merge($this->classes_wrapper, explode(' ', $options['class'])); - $this->class = $options['class']; - } - - $this->classes_wrapper[] = 'item-' . $this->type; - - if (!isset($this->input_attributes['type'])) { - $this->input_attributes['type'] = $this->type; - } - - $this->input_attributes['class'] = implode(' ', $this->classes_input); - - $this->input_attributes['id'] = "item{$this->id}"; - - if (in_array('label_as_placeholder', $this->classes_wrapper)) { - $this->input_attributes['placeholder'] = $this->label; - } - - if ($this->showif) { - // primitive R to JS translation - $this->js_showif = preg_replace("/current\(\s*(\w+)\s*\)/", "$1", $this->showif); // remove current function - $this->js_showif = preg_replace("/tail\(\s*(\w+)\s*, 1\)/", "$1", $this->js_showif); // remove current function, JS evaluation is always in session - // all other R functions may break - $this->js_showif = preg_replace("/(^|[^&])(\&)([^&]|$)/", "$1&$3", $this->js_showif); // & operators, only single ones need to be doubled - $this->js_showif = preg_replace("/(^|[^|])(\|)([^|]|$)/", "$1|$3", $this->js_showif); // | operators, only single ones need to be doubled - $this->js_showif = preg_replace("/FALSE/", "false", $this->js_showif); // uppercase, R, FALSE, to lowercase, JS, false - $this->js_showif = preg_replace("/TRUE/", "true", $this->js_showif); // uppercase, R, TRUE, to lowercase, JS, true - $quoted_string = "([\"'])((\\\\{2})*|(.*?[^\\\\](\\\\{2})*))\\1"; - $this->js_showif = preg_replace("/\s*\%contains\%\s*" . $quoted_string . "/", ".toString().indexOf($1$2$1) > -1", $this->js_showif); - $this->js_showif = preg_replace("/\s*\%contains_word\%\s*" . $quoted_string . "/", ".toString().match(/\\b$2\\b/) !== null", $this->js_showif); - $this->js_showif = preg_replace("/\s*\%begins_with\%\s*" . $quoted_string . "/", ".toString().indexOf($1$2$1) === 0", $this->js_showif); - $this->js_showif = preg_replace("/\s*\%starts_with\%\s*" . $quoted_string . "/", ".toString().indexOf($1$2$1) === 0", $this->js_showif); - $this->js_showif = preg_replace("/\s*\%ends_with\%\s*" . $quoted_string . "/", ".toString().endsWith($1$2$1)", $this->js_showif); - $this->js_showif = preg_replace("/\s*stringr::str_length\(([a-zA-Z0-9_'\"]+)\)/", "$1.length", $this->js_showif); - - if (strstr($this->showif, "//js_only") !== false) { - $this->setVisibility(array(null)); - } - } - } - - public function refresh($options = array(), $properties = array()) { - foreach ($properties as $property) { - if (property_exists($this, $property) && isset($options[$property])) { - $this->{$property} = $options[$property]; - } - } - - $this->setMoreOptions(); - $this->classes_wrapper = array_merge($this->classes_wrapper, array('item-' . $this->type)); - return $this; - } - - public function hasBeenRendered() { - return $this->displaycount !== null; - } - - public function hasBeenViewed() { - return $this->displaycount > 0; - } - - protected function chooseResultFieldBasedOnChoices() { - if ($this->mysql_field == null) { - return; - } - $choices = array_keys($this->choices); - - $len = count($choices); - if ($len == count(array_filter($choices, 'is_numeric'))) { - $this->mysql_field = 'TINYINT UNSIGNED DEFAULT NULL'; - - $min = min($choices); - $max = max($choices); - - if ($min < 0) { - $this->mysql_field = str_replace('UNSIGNED ', '', $this->mysql_field); - } - - if (abs($min) > 32767 OR abs($max) > 32767) { - $this->mysql_field = str_replace("TINYINT", "MEDIUMINT", $this->mysql_field); - } elseif (abs($min) > 126 OR abs($min) > 126) { - $this->mysql_field = str_replace("TINYINT", "SMALLINT", $this->mysql_field); - } elseif (count(array_filter($choices, "is_float"))) { - $this->mysql_field = str_replace("TINYINT", "FLOAT", $this->mysql_field); - } - } else { - $lengths = array_map("strlen", $choices); - $maxlen = max($lengths); - $this->mysql_field = 'VARCHAR (' . $maxlen . ') DEFAULT NULL'; - } - } - - public function isStoredInResultsTable() { - return $this->save_in_results_table; - } - - public function getResultField() { - if (!empty($this->choices)) { - $this->chooseResultFieldBasedOnChoices(); - } - - if ($this->mysql_field !== null){ - return "`{$this->name}` {$this->mysql_field}"; - } else { - return null; - } - } - - public function validate() { - if (!$this->hasChoices && ($this->choice_list !== null || count($this->choices))) { - $this->val_errors[] = "'{$this->name}' You defined choices for this item, even though this type doesn't have choices."; - } elseif ($this->hasChoices && ($this->choice_list === null && count($this->choices) === 0) && $this->type !== "select_or_add_multiple") { - $this->val_errors[] = "'{$this->name}' You forgot to define choices for this item."; - } elseif ($this->hasChoices && count(array_unique($this->choices)) < count($this->choices)) { - $dups = implode(array_diff_assoc($this->choices, array_unique($this->choices)), ", "); - $this->val_errors[] = "'{$this->name}' You defined duplicated choices (" . h($dups) . ") for this item."; - } - - if (!preg_match('/^[A-Za-z][A-Za-z0-9_]+$/', $this->name)) { - $this->val_errors[] = "'{$this->name}' The variable name can contain a to Z, 0 to 9 and the underscore. It needs to start with a letter. You cannot use spaces, dots, or dashes."; - } - - if (trim($this->type) == '') { - $this->val_errors[] = "{$this->name}: The type column must not be empty."; - } - - $defined_classes = array_map('trim', explode(" ", $this->class)); - $missing_classes = array_diff($defined_classes, $this->allowed_classes); - if (count($missing_classes) > 0) { - $this->val_warnings[] = "'{$this->name}' You used CSS classes that aren't part of the standard set (but maybe you defined them yourself): " . implode(", ", $missing_classes); - } - - return array('val_errors' => $this->val_errors, 'val_warnings' => $this->val_warnings); - } - - public function validateInput($reply) { - $this->reply = $reply; - - if (!$this->optional && (($reply === null || $reply === false || $reply === array() || $reply === '') || (is_array($reply) && count($reply) === 1 && current($reply) === ''))) { - // missed a required field - $this->error = __("You missed entering some required information"); - } elseif ($this->optional && $reply == '') { - $reply = null; - } - return $reply; - } - - protected function setMoreOptions() { - - } - - protected function render_label() { - $template = ''; - - return Template::replace($template, array( - 'class' => implode(' ', $this->classes_label), - 'error' => $this->render_error_tip(), - 'text' => $this->label_parsed, - 'id' => $this->id, - )); - } - - protected function render_prepended() { - $template = $this->prepend ? '' : ''; - return sprintf($template, $this->prepend); - } - - protected function render_input() { - if ($this->value_validated !== null) { - $this->input_attributes['value'] = $this->value_validated; - } - - return sprintf('', self::_parseAttributes($this->input_attributes)); - } - - protected function render_appended() { - $template = $this->append ? '' : ''; - return sprintf($template, $this->append); - } - - protected function render_inner() { - $template = $this->render_label(); - $template .= ' + public $id = null; + public $name = null; + public $type = null; + public $type_options = null; + public $choice_list = null; + public $label = null; + public $label_parsed = null; + public $optional = 0; + public $class = null; + public $showif = null; + public $js_showif = null; + public $value = null; // syntax for sticky value + public $value_validated = null; + public $order = null; + public $block_order = null; + public $item_order = null; + public $displaycount = null; + public $error = null; + public $dont_validate = null; + public $reply = null; + public $val_errors = array(); + public $val_warnings = array(); + public $mysql_field = 'TEXT DEFAULT NULL'; + public $choices = array(); + public $hidden = false; + public $no_user_input_required = false; + public $save_in_results_table = true; + public $input_attributes = array(); // so that the pre-set value can be set externally + public $parent_attributes = array(); + public $presetValue = null; + public $allowed_classes = array(); + public $skip_validation = false; + public $data_showif = false; + protected $prepend = null; + protected $append = null; + protected $type_options_array = array(); + protected $hasChoices = false; + protected $classes_controls = array('controls'); + protected $classes_wrapper = array('form-group', 'form-row'); + protected $classes_input = array(); + protected $classes_label = array('control-label'); + protected $presetValues = array(); + protected $probably_render = null; + protected $js_hidden = false; + + /** + * Minimized attributes + * + * @var array + */ + protected $_minimizedAttributes = array( + 'compact', 'checked', 'declare', 'readonly', 'disabled', 'selected', + 'defer', 'ismap', 'nohref', 'noshade', 'nowrap', 'multiple', 'noresize', + 'autoplay', 'controls', 'loop', 'muted', 'required', 'novalidate', 'formnovalidate' + ); + + /** + * Format to attribute + * + * @var string + */ + protected $_attributeFormat = '%s="%s"'; + + /** + * Format to attribute + * + * @var string + */ + protected $_minimizedAttributeFormat = '%s="%s"'; + + public function __construct($options = array()) { + // Load options to object + $optional = $this->optional; + foreach ($options as $property => $value) { + if (property_exists($this, $property)) { + $this->{$property} = $value; + } + } + + // Assign needed defaults + $this->id = isset($options['id']) ? $options['id'] : 0; + $this->label = isset($options['label']) ? $options['label'] : ''; + $this->label_parsed = isset($options['label_parsed']) ? $options['label_parsed'] : null; + $this->allowed_classes = Config::get('css_classes', array()); + + if (isset($options['type_options'])) { + $this->type_options = trim($options['type_options']); + $this->type_options_array = array($options['type_options']); + } + + if (empty($this->choice_list) && $this->hasChoices && $this->type_options != '') { + $lc = explode(' ', trim($this->type_options)); + if (count($lc) > 1) { + $this->choice_list = end($lc); + } + } + + if (!empty($options['val_error'])) { + $this->val_error = $options['val_error']; + } + + if (!empty($options['error'])) { + $this->error = $options['error']; + $this->classes_wrapper[] = "has-error"; + } + + if (isset($options['displaycount']) && $options['displaycount'] !== null) { + $this->displaycount = $options['displaycount']; + } + + $this->input_attributes['name'] = $this->name; + + $this->setMoreOptions(); + + // after the easily overridden setMoreOptions, some post-processing that is universal to all items. + if ($this->type == 'note') { + // notes can not be "required" + unset($options['optional']); + } + + if (isset($options['optional']) && $options['optional']) { + $this->optional = 1; + unset($options['optional']); + } elseif (isset($options['optional']) && !$options['optional']) { + $this->optional = 0; + } else { + $this->optional = $optional; + } + + if (!$this->optional) { + $this->classes_wrapper[] = 'required'; + $this->input_attributes['required'] = 'required'; + } else { + $this->classes_wrapper[] = 'optional'; + } + + if (!empty($options['class'])) { + $this->classes_wrapper = array_merge($this->classes_wrapper, explode(' ', $options['class'])); + $this->class = $options['class']; + } + + $this->classes_wrapper[] = 'item-' . $this->type; + + if (!isset($this->input_attributes['type'])) { + $this->input_attributes['type'] = $this->type; + } + + $this->input_attributes['class'] = implode(' ', $this->classes_input); + + $this->input_attributes['id'] = "item{$this->id}"; + + if (in_array('label_as_placeholder', $this->classes_wrapper)) { + $this->input_attributes['placeholder'] = $this->label; + } + + if ($this->showif) { + // primitive R to JS translation + $this->js_showif = preg_replace("/current\(\s*(\w+)\s*\)/", "$1", $this->showif); // remove current function + $this->js_showif = preg_replace("/tail\(\s*(\w+)\s*, 1\)/", "$1", $this->js_showif); // remove current function, JS evaluation is always in session + // all other R functions may break + $this->js_showif = preg_replace("/(^|[^&])(\&)([^&]|$)/", "$1&$3", $this->js_showif); // & operators, only single ones need to be doubled + $this->js_showif = preg_replace("/(^|[^|])(\|)([^|]|$)/", "$1|$3", $this->js_showif); // | operators, only single ones need to be doubled + $this->js_showif = preg_replace("/FALSE/", "false", $this->js_showif); // uppercase, R, FALSE, to lowercase, JS, false + $this->js_showif = preg_replace("/TRUE/", "true", $this->js_showif); // uppercase, R, TRUE, to lowercase, JS, true + $quoted_string = "([\"'])((\\\\{2})*|(.*?[^\\\\](\\\\{2})*))\\1"; + $this->js_showif = preg_replace("/\s*\%contains\%\s*" . $quoted_string . "/", ".toString().indexOf($1$2$1) > -1", $this->js_showif); + $this->js_showif = preg_replace("/\s*\%contains_word\%\s*" . $quoted_string . "/", ".toString().match(/\\b$2\\b/) !== null", $this->js_showif); + $this->js_showif = preg_replace("/\s*\%begins_with\%\s*" . $quoted_string . "/", ".toString().indexOf($1$2$1) === 0", $this->js_showif); + $this->js_showif = preg_replace("/\s*\%starts_with\%\s*" . $quoted_string . "/", ".toString().indexOf($1$2$1) === 0", $this->js_showif); + $this->js_showif = preg_replace("/\s*\%ends_with\%\s*" . $quoted_string . "/", ".toString().endsWith($1$2$1)", $this->js_showif); + $this->js_showif = preg_replace("/\s*stringr::str_length\(([a-zA-Z0-9_'\"]+)\)/", "$1.length", $this->js_showif); + + if (strstr($this->showif, "//js_only") !== false) { + $this->setVisibility(array(null)); + } + } + } + + public function refresh($options = array(), $properties = array()) { + foreach ($properties as $property) { + if (property_exists($this, $property) && isset($options[$property])) { + $this->{$property} = $options[$property]; + } + } + + $this->setMoreOptions(); + $this->classes_wrapper = array_merge($this->classes_wrapper, array('item-' . $this->type)); + return $this; + } + + public function hasBeenRendered() { + return $this->displaycount !== null; + } + + public function hasBeenViewed() { + return $this->displaycount > 0; + } + + protected function chooseResultFieldBasedOnChoices() { + if ($this->mysql_field == null) { + return; + } + $choices = array_keys($this->choices); + + $len = count($choices); + if ($len == count(array_filter($choices, 'is_numeric'))) { + $this->mysql_field = 'TINYINT UNSIGNED DEFAULT NULL'; + + $min = min($choices); + $max = max($choices); + + if ($min < 0) { + $this->mysql_field = str_replace('UNSIGNED ', '', $this->mysql_field); + } + + if (abs($min) > 32767 OR abs($max) > 32767) { + $this->mysql_field = str_replace("TINYINT", "MEDIUMINT", $this->mysql_field); + } elseif (abs($min) > 126 OR abs($min) > 126) { + $this->mysql_field = str_replace("TINYINT", "SMALLINT", $this->mysql_field); + } elseif (count(array_filter($choices, "is_float"))) { + $this->mysql_field = str_replace("TINYINT", "FLOAT", $this->mysql_field); + } + } else { + $lengths = array_map("strlen", $choices); + $maxlen = max($lengths); + $this->mysql_field = 'VARCHAR (' . $maxlen . ') DEFAULT NULL'; + } + } + + public function isStoredInResultsTable() { + return $this->save_in_results_table; + } + + public function getResultField() { + if (!empty($this->choices)) { + $this->chooseResultFieldBasedOnChoices(); + } + + if ($this->mysql_field !== null) { + return "`{$this->name}` {$this->mysql_field}"; + } else { + return null; + } + } + + public function validate() { + if (!$this->hasChoices && ($this->choice_list !== null || count($this->choices))) { + $this->val_errors[] = "'{$this->name}' You defined choices for this item, even though this type doesn't have choices."; + } elseif ($this->hasChoices && ($this->choice_list === null && count($this->choices) === 0) && $this->type !== "select_or_add_multiple") { + $this->val_errors[] = "'{$this->name}' You forgot to define choices for this item."; + } elseif ($this->hasChoices && count(array_unique($this->choices)) < count($this->choices)) { + $dups = implode(array_diff_assoc($this->choices, array_unique($this->choices)), ", "); + $this->val_errors[] = "'{$this->name}' You defined duplicated choices (" . h($dups) . ") for this item."; + } + + if (!preg_match('/^[A-Za-z][A-Za-z0-9_]+$/', $this->name)) { + $this->val_errors[] = "'{$this->name}' The variable name can contain a to Z, 0 to 9 and the underscore. It needs to start with a letter. You cannot use spaces, dots, or dashes."; + } + + if (trim($this->type) == '') { + $this->val_errors[] = "{$this->name}: The type column must not be empty."; + } + + $defined_classes = array_map('trim', explode(" ", $this->class)); + $missing_classes = array_diff($defined_classes, $this->allowed_classes); + if (count($missing_classes) > 0) { + $this->val_warnings[] = "'{$this->name}' You used CSS classes that aren't part of the standard set (but maybe you defined them yourself): " . implode(", ", $missing_classes); + } + + return array('val_errors' => $this->val_errors, 'val_warnings' => $this->val_warnings); + } + + public function validateInput($reply) { + $this->reply = $reply; + + if (!$this->optional && (($reply === null || $reply === false || $reply === array() || $reply === '') || (is_array($reply) && count($reply) === 1 && current($reply) === ''))) { + // missed a required field + $this->error = __("You missed entering some required information"); + } elseif ($this->optional && $reply == '') { + $reply = null; + } + return $reply; + } + + protected function setMoreOptions() { + + } + + protected function render_label() { + $template = ''; + + return Template::replace($template, array( + 'class' => implode(' ', $this->classes_label), + 'error' => $this->render_error_tip(), + 'text' => $this->label_parsed, + 'id' => $this->id, + )); + } + + protected function render_prepended() { + $template = $this->prepend ? '' : ''; + return sprintf($template, $this->prepend); + } + + protected function render_input() { + if ($this->value_validated !== null) { + $this->input_attributes['value'] = $this->value_validated; + } + + return sprintf('', self::_parseAttributes($this->input_attributes)); + } + + protected function render_appended() { + $template = $this->append ? '' : ''; + return sprintf($template, $this->append); + } + + protected function render_inner() { + $template = $this->render_label(); + $template .= '
%{input_group_open} @@ -390,20 +388,20 @@ protected function render_inner() {
'; - - $inputgroup = isset($this->prepend) || isset($this->append); - return Template::replace($template, array( - 'classes_controls' => implode(' ', $this->classes_controls), - 'input_group_open' => $inputgroup ? '
' : '', - 'input_group_close' => $inputgroup ? '
' : '', - 'prepended' => $this->render_prepended(), - 'input' => $this->render_input(), - 'appended' => $this->render_appended(), - )); - } - - protected function render_item_view_input() { - $template = ' + + $inputgroup = isset($this->prepend) || isset($this->append); + return Template::replace($template, array( + 'classes_controls' => implode(' ', $this->classes_controls), + 'input_group_open' => $inputgroup ? '
' : '', + 'input_group_close' => $inputgroup ? '
' : '', + 'prepended' => $this->render_prepended(), + 'input' => $this->render_input(), + 'appended' => $this->render_appended(), + )); + } + + protected function render_item_view_input() { + $template = ' @@ -411,18 +409,18 @@ protected function render_item_view_input() { '; - return Template::replace($template, array('id' => $this->id)); - } - - public function render() { - if ($this->error) { - $this->classes_wrapper[] = "has-error"; - } - $this->classes_wrapper = array_unique($this->classes_wrapper); - if ($this->data_showif) { - $this->parent_attributes['data-showif'] = $this->js_showif; - } - $template = ' + return Template::replace($template, array('id' => $this->id)); + } + + public function render() { + if ($this->error) { + $this->classes_wrapper[] = "has-error"; + } + $this->classes_wrapper = array_unique($this->classes_wrapper); + if ($this->data_showif) { + $this->parent_attributes['data-showif'] = $this->js_showif; + } + $template = '
%{item_content}
'; - return Template::replace($template, array( - 'classes_wrapper' => implode(' ', $this->classes_wrapper), - 'parent_attributes' => self::_parseAttributes($this->parent_attributes), - 'item_content' => $this->render_inner() . $this->render_item_view_input(), - 'title' => h($this->js_showif), - 'name' => h($this->name), - )); - } - - public function render_error_tip() { - $format = $this->error ? ' ' : ''; - return sprintf($format, h($this->error)); - } - - protected function splitValues() { - if (isset($this->value_validated)) { - if (is_array($this->value_validated)) { - $this->presetValues = $this->value_validated; - } else { - $this->presetValues = array_map("trim", explode(",", $this->value_validated)); - } - unset($this->input_attributes['value']); - } elseif (isset($this->input_attributes['value'])) { - $this->presetValues[] = $this->input_attributes['value']; - } else { - $this->presetValues = array(); - } - } - - public function hide() { - if (!$this->hidden) { - $this->classes_wrapper[] = "hidden"; - $this->data_showif = true; - $this->input_attributes['disabled'] = true; ## so it isn't submitted or validated - $this->hidden = true; ## so it isn't submitted or validated - } - } - - public function alwaysInvalid() { - $this->error = _('There were problems with openCPU.'); - if (!isset($this->input_attributes['class'])) { - $this->input_attributes['class'] = ''; - } - $this->input_attributes['class'] .= " always_invalid"; - } - - public function needsDynamicLabel($survey = null) { - return $this->label_parsed === null; - } - - public function getShowIf() { - if (strstr($this->showif, "//js_only") !== false) { - return "NA"; - } - - if ($this->hidden !== null) { - return false; - } - if (trim($this->showif) != "") { - return $this->showif; - } - return false; - } - - public function needsDynamicValue() { - $this->value = trim($this->value); - if (!(is_formr_truthy($this->value))) { - $this->presetValue = null; - return false; - } - if (is_numeric($this->value)) { - $this->input_attributes['value'] = $this->value; - return false; - } - - return true; - } - - public function evaluateDynamicValue(Survey $survey) { - - } - - public function getValue(Survey $survey = null) { - if ($survey && $this->value === 'sticky') { - $this->value = "tail(na.omit({$survey->name}\${$this->name}),1)"; - } - return trim($this->value); - } - - /** - * Set the visibility of an item based on show-if results returned from opencpu - * $showif_result Can be an array or an interger value returned by ocpu. If a non-empty array then $showif_result[0] can have the following values - * - NULL if the variable in $showif is Not Avaliable, - * - TRUE if it avalaible and true, - * - FALSE if it avalaible and not true - * - An empty array if a problem occured with opencpu - * - * @param array|int $showif_result - * @return null - */ - public function setVisibility($showif_result) { - if (!$showif_result) { - return true; - } - - $result = true; - if (is_array($showif_result) && array_key_exists(0, $showif_result)) { - $result = $showif_result[0]; - } elseif ($showif_result === array()) { - notify_user_error("You made a mistake, writing a showif " . $this->showif . " that returns an element of length 0. The most common reason for this is to e.g. refer to data that does not exist. Valid return values for a showif are TRUE, FALSE and NULL/NA.", " There are programming problems in this survey."); - $this->alwaysInvalid(); - $this->error = _('Incorrectly defined showif.'); - } - - if (!$result) { - $this->hide(); - $this->probably_render = false; - } - // null means we can't determine clearly if item should be visible or not - if ($result === null) { - $this->probably_render = true; - } - return $result; - } - - /** - * Set the dynamic value computed on opencpu - * - * @param mixed $value Value - * @return null - */ - public function setDynamicValue($value) { - if (!$value) { - return; - } - - $currentValue = $this->getValue(); - if ($value === array()) { - notify_user_error("You made a mistake, writing a dynamic value " . h($currentValue) . " that returns an element of length 0. The most common reason for this is to e.g. refer to data that does not exist, e.g. misspell an item. Valid values need to have a length of one.", " There are programming problems related to zero-length dynamic values in this survey."); - $this->openCPU_errors[$value] = _('Incorrectly defined value (zero length).'); - $this->alwaysInvalid(); - $value = null; - } elseif ($value === null) { - notify_user_error("You made a mistake, writing a dynamic value " . h($currentValue) . " that returns NA (missing). The most common reason for this is to e.g. refer to data that is not yet set, i.e. referring to questions that haven't been answered yet. To circumvent this, add a showif to your item, checking whether the item is answered yet using is.na(). Valid values need to have a length of one.", " There are programming problems related to null dynamic values in this survey."); - $this->openCPU_errors[$value] = _('Incorrectly defined value (null).'); - $this->alwaysInvalid(); - } elseif (is_array($value) && array_key_exists(0, $value)) { - $value = $value[0]; - } - - $this->input_attributes['value'] = $value; - } - - public function getComputedValue() { - if (isset($this->input_attributes['value'])) { - return $this->input_attributes['value']; - } else { - return null; - } - } - - /** - * Says if an item is visible or not. An item is visible if: - * - It's hidden property is FALSE OR - * - It's state cannot be determined at time of rendering - * - * @return boolean - */ - public function isRendered() { - return $this->requiresUserInput() && (!$this->hidden || $this->probably_render); - } - - /** - * Says if an item requires user input. - * - * @return boolean - */ - public function requiresUserInput() { - return !$this->no_user_input_required; - } - - /** - * Is an element hidden in DOM but rendered? - * - * @return boolean - */ - public function isHiddenButRendered() { - return $this->hidden && $this->probably_render; - } - - public function setChoices($choices) { - $this->choices = $choices; - } - - public function getReply($reply) { - return $reply; - } - - /** - * Returns a space-delimited string with items of the $options array. If a key - * of $options array happens to be one of those listed in `Helper::$_minimizedAttributes` - * - * And its value is one of: - * - * - '1' (string) - * - 1 (integer) - * - true (boolean) - * - 'true' (string) - * - * Then the value will be reset to be identical with key's name. - * If the value is not one of these 3, the parameter is not output. - * - * 'escape' is a special option in that it controls the conversion of - * attributes to their html-entity encoded equivalents. Set to false to disable html-encoding. - * - * If value for any option key is set to `null` or `false`, that option will be excluded from output. - * - * @param array $options Array of options. - * @param array $exclude Array of options to be excluded, the options here will not be part of the return. - * @param string $insertBefore String to be inserted before options. - * @param string $insertAfter String to be inserted after options. - * @return string Composed attributes. - */ - protected function _parseAttributes($options, $exclude = null, $insertBefore = ' ', $insertAfter = null) { - if (!is_string($options)) { - $options = (array) $options + array('escape' => true); - - if (!is_array($exclude)) { - $exclude = array(); - } - - $exclude = array('escape' => true) + array_flip($exclude); - $escape = $options['escape']; - $attributes = array(); - - foreach ($options as $key => $value) { - if (!isset($exclude[$key]) && $value !== false && $value !== null) { - $attributes[] = $this->_formatAttribute($key, $value, $escape); - } - } - $out = implode(' ', $attributes); - } else { - $out = $options; - } - return $out ? $insertBefore . $out . $insertAfter : ''; - } - - /** - * Formats an individual attribute, and returns the string value of the composed attribute. - * Works with minimized attributes that have the same value as their name such as 'disabled' and 'checked' - * - * @param string $key The name of the attribute to create - * @param string $value The value of the attribute to create. - * @param boolean $escape Define if the value must be escaped - * @return string The composed attribute. - */ - protected function _formatAttribute($key, $value, $escape = true) { - if (is_array($value)) { - $value = implode(' ', $value); - } - if (is_numeric($key)) { - return sprintf($this->_minimizedAttributeFormat, $value, $value); - } - $truthy = array(1, '1', true, 'true', $key); - $isMinimized = in_array($key, $this->_minimizedAttributes); - if ($isMinimized && in_array($value, $truthy, true)) { - return sprintf($this->_minimizedAttributeFormat, $key, $key); - } - if ($isMinimized) { - return ''; - } - return sprintf($this->_attributeFormat, $key, ($escape ? h($value) : $value)); - } - - protected function isSelectedOptionValue($expected = null, $actual = null) { - if ($expected !== null && $actual !== null && $expected == $actual) { - return true; - } - return false; - } + return Template::replace($template, array( + 'classes_wrapper' => implode(' ', $this->classes_wrapper), + 'parent_attributes' => self::_parseAttributes($this->parent_attributes), + 'item_content' => $this->render_inner() . $this->render_item_view_input(), + 'title' => h($this->js_showif), + 'name' => h($this->name), + )); + } + + public function render_error_tip() { + $format = $this->error ? ' ' : ''; + return sprintf($format, h($this->error)); + } + + protected function splitValues() { + if (isset($this->value_validated)) { + if (is_array($this->value_validated)) { + $this->presetValues = $this->value_validated; + } else { + $this->presetValues = array_map("trim", explode(",", $this->value_validated)); + } + unset($this->input_attributes['value']); + } elseif (isset($this->input_attributes['value'])) { + $this->presetValues[] = $this->input_attributes['value']; + } else { + $this->presetValues = array(); + } + } + + public function hide() { + if (!$this->hidden) { + $this->classes_wrapper[] = "hidden"; + $this->data_showif = true; + $this->input_attributes['disabled'] = true; ## so it isn't submitted or validated + $this->hidden = true; ## so it isn't submitted or validated + } + } + + public function alwaysInvalid() { + $this->error = _('There were problems with openCPU.'); + if (!isset($this->input_attributes['class'])) { + $this->input_attributes['class'] = ''; + } + $this->input_attributes['class'] .= " always_invalid"; + } + + public function needsDynamicLabel($survey = null) { + return $this->label_parsed === null; + } + + public function getShowIf() { + if (strstr($this->showif, "//js_only") !== false) { + return "NA"; + } + + if ($this->hidden !== null) { + return false; + } + if (trim($this->showif) != "") { + return $this->showif; + } + return false; + } + + public function needsDynamicValue() { + $this->value = trim($this->value); + if (!(is_formr_truthy($this->value))) { + $this->presetValue = null; + return false; + } + if (is_numeric($this->value)) { + $this->input_attributes['value'] = $this->value; + return false; + } + + return true; + } + + public function evaluateDynamicValue(Survey $survey) { + + } + + public function getValue(Survey $survey = null) { + if ($survey && $this->value === 'sticky') { + $this->value = "tail(na.omit({$survey->name}\${$this->name}),1)"; + } + return trim($this->value); + } + + /** + * Set the visibility of an item based on show-if results returned from opencpu + * $showif_result Can be an array or an interger value returned by ocpu. If a non-empty array then $showif_result[0] can have the following values + * - NULL if the variable in $showif is Not Avaliable, + * - TRUE if it avalaible and true, + * - FALSE if it avalaible and not true + * - An empty array if a problem occured with opencpu + * + * @param array|int $showif_result + * @return null + */ + public function setVisibility($showif_result) { + if (!$showif_result) { + return true; + } + + $result = true; + if (is_array($showif_result) && array_key_exists(0, $showif_result)) { + $result = $showif_result[0]; + } elseif ($showif_result === array()) { + notify_user_error("You made a mistake, writing a showif " . $this->showif . " that returns an element of length 0. The most common reason for this is to e.g. refer to data that does not exist. Valid return values for a showif are TRUE, FALSE and NULL/NA.", " There are programming problems in this survey."); + $this->alwaysInvalid(); + $this->error = _('Incorrectly defined showif.'); + } + + if (!$result) { + $this->hide(); + $this->probably_render = false; + } + // null means we can't determine clearly if item should be visible or not + if ($result === null) { + $this->probably_render = true; + } + return $result; + } + + /** + * Set the dynamic value computed on opencpu + * + * @param mixed $value Value + * @return null + */ + public function setDynamicValue($value) { + if (!$value) { + return; + } + + $currentValue = $this->getValue(); + if ($value === array()) { + notify_user_error("You made a mistake, writing a dynamic value " . h($currentValue) . " that returns an element of length 0. The most common reason for this is to e.g. refer to data that does not exist, e.g. misspell an item. Valid values need to have a length of one.", " There are programming problems related to zero-length dynamic values in this survey."); + $this->openCPU_errors[$value] = _('Incorrectly defined value (zero length).'); + $this->alwaysInvalid(); + $value = null; + } elseif ($value === null) { + notify_user_error("You made a mistake, writing a dynamic value " . h($currentValue) . " that returns NA (missing). The most common reason for this is to e.g. refer to data that is not yet set, i.e. referring to questions that haven't been answered yet. To circumvent this, add a showif to your item, checking whether the item is answered yet using is.na(). Valid values need to have a length of one.", " There are programming problems related to null dynamic values in this survey."); + $this->openCPU_errors[$value] = _('Incorrectly defined value (null).'); + $this->alwaysInvalid(); + } elseif (is_array($value) && array_key_exists(0, $value)) { + $value = $value[0]; + } + + $this->input_attributes['value'] = $value; + } + + public function getComputedValue() { + if (isset($this->input_attributes['value'])) { + return $this->input_attributes['value']; + } else { + return null; + } + } + + /** + * Says if an item is visible or not. An item is visible if: + * - It's hidden property is FALSE OR + * - It's state cannot be determined at time of rendering + * + * @return boolean + */ + public function isRendered() { + return $this->requiresUserInput() && (!$this->hidden || $this->probably_render); + } + + /** + * Says if an item requires user input. + * + * @return boolean + */ + public function requiresUserInput() { + return !$this->no_user_input_required; + } + + /** + * Is an element hidden in DOM but rendered? + * + * @return boolean + */ + public function isHiddenButRendered() { + return $this->hidden && $this->probably_render; + } + + public function setChoices($choices) { + $this->choices = $choices; + } + + public function getReply($reply) { + return $reply; + } + + /** + * Returns a space-delimited string with items of the $options array. If a key + * of $options array happens to be one of those listed in `Helper::$_minimizedAttributes` + * + * And its value is one of: + * + * - '1' (string) + * - 1 (integer) + * - true (boolean) + * - 'true' (string) + * + * Then the value will be reset to be identical with key's name. + * If the value is not one of these 3, the parameter is not output. + * + * 'escape' is a special option in that it controls the conversion of + * attributes to their html-entity encoded equivalents. Set to false to disable html-encoding. + * + * If value for any option key is set to `null` or `false`, that option will be excluded from output. + * + * @param array $options Array of options. + * @param array $exclude Array of options to be excluded, the options here will not be part of the return. + * @param string $insertBefore String to be inserted before options. + * @param string $insertAfter String to be inserted after options. + * @return string Composed attributes. + */ + protected function _parseAttributes($options, $exclude = null, $insertBefore = ' ', $insertAfter = null) { + if (!is_string($options)) { + $options = (array) $options + array('escape' => true); + + if (!is_array($exclude)) { + $exclude = array(); + } + + $exclude = array('escape' => true) + array_flip($exclude); + $escape = $options['escape']; + $attributes = array(); + + foreach ($options as $key => $value) { + if (!isset($exclude[$key]) && $value !== false && $value !== null) { + $attributes[] = $this->_formatAttribute($key, $value, $escape); + } + } + $out = implode(' ', $attributes); + } else { + $out = $options; + } + return $out ? $insertBefore . $out . $insertAfter : ''; + } + + /** + * Formats an individual attribute, and returns the string value of the composed attribute. + * Works with minimized attributes that have the same value as their name such as 'disabled' and 'checked' + * + * @param string $key The name of the attribute to create + * @param string $value The value of the attribute to create. + * @param boolean $escape Define if the value must be escaped + * @return string The composed attribute. + */ + protected function _formatAttribute($key, $value, $escape = true) { + if (is_array($value)) { + $value = implode(' ', $value); + } + if (is_numeric($key)) { + return sprintf($this->_minimizedAttributeFormat, $value, $value); + } + $truthy = array(1, '1', true, 'true', $key); + $isMinimized = in_array($key, $this->_minimizedAttributes); + if ($isMinimized && in_array($value, $truthy, true)) { + return sprintf($this->_minimizedAttributeFormat, $key, $key); + } + if ($isMinimized) { + return ''; + } + return sprintf($this->_attributeFormat, $key, ($escape ? h($value) : $value)); + } + + protected function isSelectedOptionValue($expected = null, $actual = null) { + if ($expected !== null && $actual !== null && $expected == $actual) { + return true; + } + return false; + } } diff --git a/application/Model/Item/Blank.php b/application/Model/Item/Blank.php index 9f4d99862..858b3c345 100644 --- a/application/Model/Item/Blank.php +++ b/application/Model/Item/Blank.php @@ -2,21 +2,20 @@ class Blank_Item extends Text_Item { - public $type = 'blank'; - public $mysql_field = 'TEXT DEFAULT NULL'; + public $type = 'blank'; + public $mysql_field = 'TEXT DEFAULT NULL'; - public function render() { - if ($this->error) { - $this->classes_wrapper[] = "has-error"; - } - $template = '
%{text}
'; + public function render() { + if ($this->error) { + $this->classes_wrapper[] = "has-error"; + } + $template = '
%{text}
'; - return Template::replace($template, array( - 'classes_wrapper' => implode(' ', $this->classes_wrapper), - 'showif' => $this->data_showif ? sprintf('data-showif="%s"', h($this->js_showif)) : '', - 'text' => $this->label_parsed, - )); - - } + return Template::replace($template, array( + 'classes_wrapper' => implode(' ', $this->classes_wrapper), + 'showif' => $this->data_showif ? sprintf('data-showif="%s"', h($this->js_showif)) : '', + 'text' => $this->label_parsed, + )); + } } diff --git a/application/Model/Item/Block.php b/application/Model/Item/Block.php index 117a43bb9..50a95a242 100644 --- a/application/Model/Item/Block.php +++ b/application/Model/Item/Block.php @@ -2,12 +2,12 @@ class Block_Item extends Note_Item { - public $type = 'block'; - public $input_attributes = array('type' => 'checkbox'); - public $optional = 0; + public $type = 'block'; + public $input_attributes = array('type' => 'checkbox'); + public $optional = 0; - public function setMoreOptions() { - $this->classes_wrapper[] = 'alert alert-danger'; - } + public function setMoreOptions() { + $this->classes_wrapper[] = 'alert alert-danger'; + } } diff --git a/application/Model/Item/Browser.php b/application/Model/Item/Browser.php index 1b9b2b607..dfa83e154 100644 --- a/application/Model/Item/Browser.php +++ b/application/Model/Item/Browser.php @@ -1,3 +1,5 @@ 'hidden'); - public $no_user_input_required = true; - public $mysql_field = 'TEXT DEFAULT NULL'; - - public function render() { - return $this->render_input(); - } + public $type = 'calculate'; + public $input_attributes = array('type' => 'hidden'); + public $no_user_input_required = true; + public $mysql_field = 'TEXT DEFAULT NULL'; + + public function render() { + return $this->render_input(); + } } diff --git a/application/Model/Item/Cc.php b/application/Model/Item/Cc.php index e5e18e40b..408d9458b 100644 --- a/application/Model/Item/Cc.php +++ b/application/Model/Item/Cc.php @@ -6,14 +6,13 @@ class Cc_Item extends Text_Item { - public $type = 'cc'; - public $input_attributes = array('type' => 'cc', "data-luhn" => ""); - public $mysql_field = 'VARCHAR(255) DEFAULT NULL'; + public $type = 'cc'; + public $input_attributes = array('type' => 'cc', "data-luhn" => ""); + public $mysql_field = 'VARCHAR(255) DEFAULT NULL'; + protected $prepend = 'fa-credit-card'; - protected $prepend = 'fa-credit-card'; - - protected function setMoreOptions() { - $this->classes_input[] = 'form-control'; - } + protected function setMoreOptions() { + $this->classes_input[] = 'form-control'; + } } diff --git a/application/Model/Item/Check.php b/application/Model/Item/Check.php index 010b15162..b4edf178a 100644 --- a/application/Model/Item/Check.php +++ b/application/Model/Item/Check.php @@ -6,59 +6,59 @@ class Check_Item extends McMultiple_Item { - public $mysql_field = 'TINYINT UNSIGNED DEFAULT NULL'; - public $choice_list = NULL; - protected $hasChoices = false; + public $mysql_field = 'TINYINT UNSIGNED DEFAULT NULL'; + public $choice_list = NULL; + protected $hasChoices = false; - protected function setMoreOptions() { - parent::setMoreOptions(); - $this->input_attributes['name'] = $this->name; - } + protected function setMoreOptions() { + parent::setMoreOptions(); + $this->input_attributes['name'] = $this->name; + } - protected function render_label() { - $template = ''; + protected function render_label() { + $template = ''; - return Template::replace($template, array( - 'class' => implode(' ', $this->classes_label), - 'error' => $this->render_error_tip(), - 'text' => $this->label_parsed, - 'id' => $this->id, - )); - } + return Template::replace($template, array( + 'class' => implode(' ', $this->classes_label), + 'error' => $this->render_error_tip(), + 'text' => $this->label_parsed, + 'id' => $this->id, + )); + } - public function validateInput($reply) { - if (!in_array($reply, array(0, 1))) { - $this->error = __("You chose an option '%s' that is not permitted.", h($reply)); - } - $reply = parent::validateInput($reply); - return $reply ? 1 : 0; - } + public function validateInput($reply) { + if (!in_array($reply, array(0, 1))) { + $this->error = __("You chose an option '%s' that is not permitted.", h($reply)); + } + $reply = parent::validateInput($reply); + return $reply ? 1 : 0; + } - public function getReply($reply) { - return $reply ? 1 : 0; - } + public function getReply($reply) { + return $reply ? 1 : 0; + } - protected function render_input() { - if (!empty($this->input_attributes['value']) || !empty($this->value_validated)) { - $this->input_attributes['checked'] = true; - } else { - $this->input_attributes['checked'] = false; - } - unset($this->input_attributes['value']); + protected function render_input() { + if (!empty($this->input_attributes['value']) || !empty($this->value_validated)) { + $this->input_attributes['checked'] = true; + } else { + $this->input_attributes['checked'] = false; + } + unset($this->input_attributes['value']); - $template = ' + $template = '
'; - return Template::replace($template, array( - 'id' => $this->id, - 'class' => $this->js_hidden ? ' js_hidden' : '', - 'attributes' => self::_parseAttributes($this->input_attributes, array('id', 'type', 'required')), - 'input_attributes' => self::_parseAttributes($this->input_attributes, array('id')), - )); - } + return Template::replace($template, array( + 'id' => $this->id, + 'class' => $this->js_hidden ? ' js_hidden' : '', + 'attributes' => self::_parseAttributes($this->input_attributes, array('id', 'type', 'required')), + 'input_attributes' => self::_parseAttributes($this->input_attributes, array('id')), + )); + } } diff --git a/application/Model/Item/CheckButton.php b/application/Model/Item/CheckButton.php index deb9db9f5..9a2b41f60 100644 --- a/application/Model/Item/CheckButton.php +++ b/application/Model/Item/CheckButton.php @@ -2,29 +2,29 @@ class CheckButton_Item extends Check_Item { - public $mysql_field = 'TINYINT UNSIGNED DEFAULT NULL'; - protected $js_hidden = true; + public $mysql_field = 'TINYINT UNSIGNED DEFAULT NULL'; + protected $js_hidden = true; - protected function setMoreOptions() { - parent::setMoreOptions(); - $this->classes_wrapper[] = 'btn-check'; - } + protected function setMoreOptions() { + parent::setMoreOptions(); + $this->classes_wrapper[] = 'btn-check'; + } - protected function render_label() { - $template = ''; - return Template::replace($template, array( - 'class' => implode(' ', $this->classes_label), - 'error' => $this->render_error_tip(), - 'text' => $this->label_parsed, - )); - } + protected function render_label() { + $template = ''; + return Template::replace($template, array( + 'class' => implode(' ', $this->classes_label), + 'error' => $this->render_error_tip(), + 'text' => $this->label_parsed, + )); + } - protected function render_appended() { - $template = ' + protected function render_appended() { + $template = '
'; - return sprintf($template, $this->id); - } + return sprintf($template, $this->id); + } } diff --git a/application/Model/Item/ChooseTwoWeekdays.php b/application/Model/Item/ChooseTwoWeekdays.php index f859eb367..b1961c40f 100644 --- a/application/Model/Item/ChooseTwoWeekdays.php +++ b/application/Model/Item/ChooseTwoWeekdays.php @@ -2,10 +2,10 @@ class ChooseTwoWeekdays_Item extends McMultiple_Item { - protected function setMoreOptions() { - $this->optional = 0; - $this->classes_input[] = 'choose2days'; - $this->input_attributes['name'] = $this->name . '[]'; - } + protected function setMoreOptions() { + $this->optional = 0; + $this->classes_input[] = 'choose2days'; + $this->input_attributes['name'] = $this->name . '[]'; + } } diff --git a/application/Model/Item/Color.php b/application/Model/Item/Color.php index 779f54b2c..d0a487fcb 100644 --- a/application/Model/Item/Color.php +++ b/application/Model/Item/Color.php @@ -2,25 +2,25 @@ class Color_Item extends Item { - public $type = 'color'; - public $input_attributes = array('type' => 'color'); - public $mysql_field = 'CHAR(7) DEFAULT NULL'; - protected $prepend = 'fa-tint'; + public $type = 'color'; + public $input_attributes = array('type' => 'color'); + public $mysql_field = 'CHAR(7) DEFAULT NULL'; + protected $prepend = 'fa-tint'; - protected function setMoreOptions() { - $this->classes_input[] = 'form-control'; - } + protected function setMoreOptions() { + $this->classes_input[] = 'form-control'; + } - public function validateInput($reply) { - if ($this->optional && trim($reply) == '') { - return parent::validateInput($reply); - } else { - if (!preg_match("/^#[0-9A-Fa-f]{6}$/", $reply)) { - $this->error = __('The color %s is not valid', h($reply)); - } - } + public function validateInput($reply) { + if ($this->optional && trim($reply) == '') { + return parent::validateInput($reply); + } else { + if (!preg_match("/^#[0-9A-Fa-f]{6}$/", $reply)) { + $this->error = __('The color %s is not valid', h($reply)); + } + } - return $reply; - } + return $reply; + } } diff --git a/application/Model/Item/Date.php b/application/Model/Item/Date.php index bc5891dfc..b345df022 100644 --- a/application/Model/Item/Date.php +++ b/application/Model/Item/Date.php @@ -2,10 +2,10 @@ class Date_Item extends Datetime_Item { - public $type = 'date'; - public $input_attributes = array('type' => 'date' , 'placeholder' => 'yyyy-mm-dd'); - public $mysql_field = 'DATE DEFAULT NULL'; - //protected $prepend = 'fa-calendar'; - protected $html5_date_format = 'Y-m-d'; + public $type = 'date'; + public $input_attributes = array('type' => 'date', 'placeholder' => 'yyyy-mm-dd'); + public $mysql_field = 'DATE DEFAULT NULL'; + //protected $prepend = 'fa-calendar'; + protected $html5_date_format = 'Y-m-d'; } diff --git a/application/Model/Item/Datetime.php b/application/Model/Item/Datetime.php index d444f5fd6..e1f91a78c 100644 --- a/application/Model/Item/Datetime.php +++ b/application/Model/Item/Datetime.php @@ -2,54 +2,54 @@ class Datetime_Item extends Item { - public $type = 'datetime'; - public $input_attributes = array('type' => 'datetime'); - public $mysql_field = 'DATETIME DEFAULT NULL'; - //protected $prepend = 'fa-calendar'; - protected $html5_date_format = 'Y-m-d\TH:i'; + public $type = 'datetime'; + public $input_attributes = array('type' => 'datetime'); + public $mysql_field = 'DATETIME DEFAULT NULL'; + //protected $prepend = 'fa-calendar'; + protected $html5_date_format = 'Y-m-d\TH:i'; - protected function setMoreOptions() { + protected function setMoreOptions() { # $this->input_attributes['step'] = 'any'; - $this->classes_input[] = 'form-control'; - - if (isset($this->type_options) && trim($this->type_options) != "") { - $this->type_options_array = explode(",", $this->type_options, 3); - - $min = trim(reset($this->type_options_array)); - if (strtotime($min)) { - $this->input_attributes['min'] = date($this->html5_date_format, strtotime($min)); - } - - $max = trim(next($this->type_options_array)); - if (strtotime($max)) { - $this->input_attributes['max'] = date($this->html5_date_format, strtotime($max)); - } - } - } - - public function validateInput($reply) { - if (!($this->optional && $reply == '')) { - - $time_reply = strtotime($reply); - if ($time_reply === false) { - $this->error = _('You did not enter a valid date.'); - } - - if (isset($this->input_attributes['min']) && $time_reply < strtotime($this->input_attributes['min'])) { // lower number than allowed - $this->error = __("The minimum is %d", $this->input_attributes['min']); - } elseif (isset($this->input_attributes['max']) && $time_reply > strtotime($this->input_attributes['max'])) { // larger number than allowed - $this->error = __("The maximum is %d", $this->input_attributes['max']); - } - } - return parent::validateInput($reply); - } - - public function getReply($reply) { - if (!$reply) { - return null; - } - $time_reply = strtotime($reply); - return date($this->html5_date_format, $time_reply); - } + $this->classes_input[] = 'form-control'; + + if (isset($this->type_options) && trim($this->type_options) != "") { + $this->type_options_array = explode(",", $this->type_options, 3); + + $min = trim(reset($this->type_options_array)); + if (strtotime($min)) { + $this->input_attributes['min'] = date($this->html5_date_format, strtotime($min)); + } + + $max = trim(next($this->type_options_array)); + if (strtotime($max)) { + $this->input_attributes['max'] = date($this->html5_date_format, strtotime($max)); + } + } + } + + public function validateInput($reply) { + if (!($this->optional && $reply == '')) { + + $time_reply = strtotime($reply); + if ($time_reply === false) { + $this->error = _('You did not enter a valid date.'); + } + + if (isset($this->input_attributes['min']) && $time_reply < strtotime($this->input_attributes['min'])) { // lower number than allowed + $this->error = __("The minimum is %d", $this->input_attributes['min']); + } elseif (isset($this->input_attributes['max']) && $time_reply > strtotime($this->input_attributes['max'])) { // larger number than allowed + $this->error = __("The maximum is %d", $this->input_attributes['max']); + } + } + return parent::validateInput($reply); + } + + public function getReply($reply) { + if (!$reply) { + return null; + } + $time_reply = strtotime($reply); + return date($this->html5_date_format, $time_reply); + } } diff --git a/application/Model/Item/DatetimeLocal.php b/application/Model/Item/DatetimeLocal.php index e163c88a0..5a9d80618 100644 --- a/application/Model/Item/DatetimeLocal.php +++ b/application/Model/Item/DatetimeLocal.php @@ -2,7 +2,7 @@ class DatetimeLocal_Item extends Datetime_Item { - public $type = 'datetime-local'; - public $input_attributes = array('type' => 'datetime-local'); + public $type = 'datetime-local'; + public $input_attributes = array('type' => 'datetime-local'); } diff --git a/application/Model/Item/Email.php b/application/Model/Item/Email.php index 75a49a75b..5beeece74 100644 --- a/application/Model/Item/Email.php +++ b/application/Model/Item/Email.php @@ -5,23 +5,22 @@ */ class Email_Item extends Text_Item { - public $type = 'email'; - public $input_attributes = array('type' => 'email', 'maxlength' => 255); - public $mysql_field = 'VARCHAR(255) DEFAULT NULL'; + public $type = 'email'; + public $input_attributes = array('type' => 'email', 'maxlength' => 255); + public $mysql_field = 'VARCHAR(255) DEFAULT NULL'; + protected $prepend = 'fa-envelope'; - protected $prepend = 'fa-envelope'; + public function validateInput($reply) { + if ($this->optional && trim($reply) == '') { + return parent::validateInput($reply); + } else { + $reply_valid = filter_var($reply, FILTER_VALIDATE_EMAIL); + if (!$reply_valid) { + $this->error = __('The email address %s is not valid', h($reply)); + } + } - public function validateInput($reply) { - if ($this->optional && trim($reply) == '') { - return parent::validateInput($reply); - } else { - $reply_valid = filter_var($reply, FILTER_VALIDATE_EMAIL); - if (!$reply_valid) { - $this->error = __('The email address %s is not valid', h($reply)); - } - } - - return $reply_valid; - } + return $reply_valid; + } } diff --git a/application/Model/Item/File.php b/application/Model/Item/File.php index aaa9f6e14..11a0d9d0e 100644 --- a/application/Model/Item/File.php +++ b/application/Model/Item/File.php @@ -2,59 +2,59 @@ class File_Item extends Item { - public $type = 'file'; - public $input_attributes = array('type' => 'file', 'accept' => "image/*,video/*,audio/*,text/*;capture=camera"); - public $mysql_field = 'VARCHAR(1000) DEFAULT NULL'; - protected $file_endings = array( - 'image/jpeg' => '.jpg', 'image/png' => '.png', 'image/gif' => '.gif', 'image/tiff' => '.tif', - 'video/mpeg' => '.mpg', 'video/quicktime' => '.mov', 'video/x-flv' => '.flv', 'video/x-f4v' => '.f4v', 'video/x-msvideo' => '.avi', - 'audio/mpeg' => '.mp3', - 'text/csv' => '.csv', 'text/css' => '.css', 'text/tab-separated-values' => '.tsv', 'text/plain' => '.txt' - ); - protected $embed_html = '%s'; - protected $max_size = 16777219; + public $type = 'file'; + public $input_attributes = array('type' => 'file', 'accept' => "image/*,video/*,audio/*,text/*;capture=camera"); + public $mysql_field = 'VARCHAR(1000) DEFAULT NULL'; + protected $file_endings = array( + 'image/jpeg' => '.jpg', 'image/png' => '.png', 'image/gif' => '.gif', 'image/tiff' => '.tif', + 'video/mpeg' => '.mpg', 'video/quicktime' => '.mov', 'video/x-flv' => '.flv', 'video/x-f4v' => '.f4v', 'video/x-msvideo' => '.avi', + 'audio/mpeg' => '.mp3', + 'text/csv' => '.csv', 'text/css' => '.css', 'text/tab-separated-values' => '.tsv', 'text/plain' => '.txt' + ); + protected $embed_html = '%s'; + protected $max_size = 16777219; - protected function setMoreOptions() { - if (is_array($this->type_options_array) && count($this->type_options_array) == 1) { - $val = (int) trim(current($this->type_options_array)); - if (is_numeric($val)) { - $bytes = $val * 1048576; # size is provided in MB - $this->max_size = $bytes; - } - } - } + protected function setMoreOptions() { + if (is_array($this->type_options_array) && count($this->type_options_array) == 1) { + $val = (int) trim(current($this->type_options_array)); + if (is_numeric($val)) { + $bytes = $val * 1048576; # size is provided in MB + $this->max_size = $bytes; + } + } + } - public function validateInput($reply) { - if ($reply['error'] === 0) { // verify maximum length and no errors - if (filesize($reply['tmp_name']) < $this->max_size) { - $finfo = new finfo(FILEINFO_MIME_TYPE); - $mime = $finfo->file($reply['tmp_name']); - $new_file_name = crypto_token(66) . $this->file_endings[$mime]; - if (!in_array($mime, array_keys($this->file_endings))) { - $this->error = 'Files of type' . $mime . ' are not allowed to be uploaded.'; - } elseif (move_uploaded_file($reply['tmp_name'], APPLICATION_ROOT . 'webroot/assets/tmp/' . $new_file_name)) { - $public_path = WEBROOT . 'assets/tmp/' . $new_file_name; - $reply = __($this->embed_html, $public_path); - } else { - $this->error = __("Unable to save uploaded file"); - } - } else { - $this->error = __("This file is too big the maximum is %d megabytes.", round($this->max_size / 1048576, 2)); - $reply = null; - } - } elseif($reply['error'] === 4 && $this->optional) { - $reply = null; - } else { - $this->error = __("Error uploading file. (Code: %s)", $reply['error']); - $reply = null; - } + public function validateInput($reply) { + if ($reply['error'] === 0) { // verify maximum length and no errors + if (filesize($reply['tmp_name']) < $this->max_size) { + $finfo = new finfo(FILEINFO_MIME_TYPE); + $mime = $finfo->file($reply['tmp_name']); + $new_file_name = crypto_token(66) . $this->file_endings[$mime]; + if (!in_array($mime, array_keys($this->file_endings))) { + $this->error = 'Files of type' . $mime . ' are not allowed to be uploaded.'; + } elseif (move_uploaded_file($reply['tmp_name'], APPLICATION_ROOT . 'webroot/assets/tmp/' . $new_file_name)) { + $public_path = WEBROOT . 'assets/tmp/' . $new_file_name; + $reply = __($this->embed_html, $public_path); + } else { + $this->error = __("Unable to save uploaded file"); + } + } else { + $this->error = __("This file is too big the maximum is %d megabytes.", round($this->max_size / 1048576, 2)); + $reply = null; + } + } elseif ($reply['error'] === 4 && $this->optional) { + $reply = null; + } else { + $this->error = __("Error uploading file. (Code: %s)", $reply['error']); + $reply = null; + } - $this->reply = parent::validateInput($reply); - return $this->reply; - } + $this->reply = parent::validateInput($reply); + return $this->reply; + } - public function getReply($reply) { - return $this->reply; - } + public function getReply($reply) { + return $this->reply; + } } diff --git a/application/Model/Item/Geopoint.php b/application/Model/Item/Geopoint.php index c4248e0e6..00d81f214 100644 --- a/application/Model/Item/Geopoint.php +++ b/application/Model/Item/Geopoint.php @@ -2,34 +2,34 @@ class Geopoint_Item extends Item { - public $type = 'geopoint'; - public $input_attributes = array('type' => 'text', 'readonly'); - public $mysql_field = 'TEXT DEFAULT NULL'; - protected $append = true; + public $type = 'geopoint'; + public $input_attributes = array('type' => 'text', 'readonly'); + public $mysql_field = 'TEXT DEFAULT NULL'; + protected $append = true; - protected function setMoreOptions() { - $this->input_attributes['name'] = $this->name . '[]'; - $this->classes_input[] = "form-control"; - } + protected function setMoreOptions() { + $this->input_attributes['name'] = $this->name . '[]'; + $this->classes_input[] = "form-control"; + } - public function getReply($reply) { - if (is_array($reply)): - $reply = array_filter($reply); - $reply = end($reply); - endif; - return $reply; - } + public function getReply($reply) { + if (is_array($reply)): + $reply = array_filter($reply); + $reply = end($reply); + endif; + return $reply; + } - protected function render_prepended() { - $ret = ' + protected function render_prepended() { + $ret = '
'; - return sprintf($ret, $this->name); - } + return sprintf($ret, $this->name); + } - protected function render_appended() { - $ret = ' + protected function render_appended() { + $ret = '
'; - return sprintf($ret, $this->id); - } + return sprintf($ret, $this->id); + } } diff --git a/application/Model/Item/Get.php b/application/Model/Item/Get.php index cf394cb4d..29c21a572 100644 --- a/application/Model/Item/Get.php +++ b/application/Model/Item/Get.php @@ -2,43 +2,43 @@ class Get_Item extends Item { - public $type = 'get'; - public $input_attributes = array('type' => 'hidden'); - public $no_user_input_required = true; - public $probably_render = true; - public $mysql_field = 'TEXT DEFAULT NULL'; - protected $hasChoices = false; - private $get_var = 'referred_by'; - - protected function setMoreOptions() { - if (isset($this->type_options_array) && is_array($this->type_options_array)) { - if (count($this->type_options_array) == 1) { - $this->get_var = trim(current($this->type_options_array)); - } - } - - $this->input_attributes['value'] = ''; - $request = new Request($_GET); - if (($value = $request->getParam($this->get_var)) !== null) { - $this->input_attributes['value'] = $value; - $this->value = $value; - $this->value_validated = $value; - } - } - - public function validate() { - if (!preg_match('/^[A-Za-z0-9_]+$/', $this->get_var)) { - $this->val_errors[] = __('Problem with variable %s "get %s". The part after get can only contain a-Z0-9 and the underscore.', $this->name, $this->get_var); - } - return parent::validate(); - } - - public function render() { - return $this->render_input(); - } - - public function needsDynamicValue() { - return false; - } + public $type = 'get'; + public $input_attributes = array('type' => 'hidden'); + public $no_user_input_required = true; + public $probably_render = true; + public $mysql_field = 'TEXT DEFAULT NULL'; + protected $hasChoices = false; + private $get_var = 'referred_by'; + + protected function setMoreOptions() { + if (isset($this->type_options_array) && is_array($this->type_options_array)) { + if (count($this->type_options_array) == 1) { + $this->get_var = trim(current($this->type_options_array)); + } + } + + $this->input_attributes['value'] = ''; + $request = new Request($_GET); + if (($value = $request->getParam($this->get_var)) !== null) { + $this->input_attributes['value'] = $value; + $this->value = $value; + $this->value_validated = $value; + } + } + + public function validate() { + if (!preg_match('/^[A-Za-z0-9_]+$/', $this->get_var)) { + $this->val_errors[] = __('Problem with variable %s "get %s". The part after get can only contain a-Z0-9 and the underscore.', $this->name, $this->get_var); + } + return parent::validate(); + } + + public function render() { + return $this->render_input(); + } + + public function needsDynamicValue() { + return false; + } } diff --git a/application/Model/Item/Hidden.php b/application/Model/Item/Hidden.php index 58a5dc2b3..b3ab6b792 100644 --- a/application/Model/Item/Hidden.php +++ b/application/Model/Item/Hidden.php @@ -2,18 +2,18 @@ class Hidden_Item extends Item { - public $type = 'hidden'; - public $mysql_field = 'TEXT DEFAULT NULL'; - public $input_attributes = array('type' => 'hidden'); - public $optional = 1; + public $type = 'hidden'; + public $mysql_field = 'TEXT DEFAULT NULL'; + public $input_attributes = array('type' => 'hidden'); + public $optional = 1; - public function setMoreOptions() { - unset($this->input_attributes["required"]); - $this->classes_wrapper[] = "hidden"; - } + public function setMoreOptions() { + unset($this->input_attributes["required"]); + $this->classes_wrapper[] = "hidden"; + } - public function render_inner() { - return $this->render_input(); - } + public function render_inner() { + return $this->render_input(); + } } diff --git a/application/Model/Item/Image.php b/application/Model/Item/Image.php index 6f04dbfba..2cd9b66ad 100644 --- a/application/Model/Item/Image.php +++ b/application/Model/Item/Image.php @@ -2,11 +2,11 @@ class Image_Item extends File_Item { - public $type = 'image'; - public $input_attributes = array('type' => 'file', 'accept' => "image/*;capture=camera"); - public $mysql_field = 'VARCHAR(1000) DEFAULT NULL'; - protected $file_endings = array('image/jpeg' => '.jpg', 'image/png' => '.png', 'image/gif' => '.gif', 'image/tiff' => '.tif'); - protected $embed_html = ''; - protected $max_size = 16777219; + public $type = 'image'; + public $input_attributes = array('type' => 'file', 'accept' => "image/*;capture=camera"); + public $mysql_field = 'VARCHAR(1000) DEFAULT NULL'; + protected $file_endings = array('image/jpeg' => '.jpg', 'image/png' => '.png', 'image/gif' => '.gif', 'image/tiff' => '.tif'); + protected $embed_html = ''; + protected $max_size = 16777219; } diff --git a/application/Model/Item/Ip.php b/application/Model/Item/Ip.php index b9d81645f..5d1d4d43f 100644 --- a/application/Model/Item/Ip.php +++ b/application/Model/Item/Ip.php @@ -2,22 +2,22 @@ class Ip_Item extends Item { - public $type = 'ip'; - public $input_attributes = array('type' => 'hidden'); - public $mysql_field = 'VARCHAR(46) DEFAULT NULL'; - public $no_user_input_required = true; - public $optional = 1; + public $type = 'ip'; + public $input_attributes = array('type' => 'hidden'); + public $mysql_field = 'VARCHAR(46) DEFAULT NULL'; + public $no_user_input_required = true; + public $optional = 1; - protected function setMoreOptions() { - $this->input_attributes['value'] = $_SERVER["REMOTE_ADDR"]; - } + protected function setMoreOptions() { + $this->input_attributes['value'] = $_SERVER["REMOTE_ADDR"]; + } - public function getReply($reply) { - return isset($_SERVER["REMOTE_ADDR"]) ? $_SERVER["REMOTE_ADDR"] : null; - } + public function getReply($reply) { + return isset($_SERVER["REMOTE_ADDR"]) ? $_SERVER["REMOTE_ADDR"] : null; + } - public function render() { - return $this->render_input(); - } + public function render() { + return $this->render_input(); + } } diff --git a/application/Model/Item/Letters.php b/application/Model/Item/Letters.php index a9e266e04..6d9ef666b 100644 --- a/application/Model/Item/Letters.php +++ b/application/Model/Item/Letters.php @@ -2,13 +2,13 @@ class Letters_Item extends Text_Item { - public $type = 'letters'; - public $input_attributes = array('type' => 'text'); - public $mysql_field = 'TEXT DEFAULT NULL'; - - protected function setMoreOptions() { - $this->input_attributes['pattern'] = "[A-Za-züäöß.;,!: ]+"; - return parent::setMoreOptions(); - } + public $type = 'letters'; + public $input_attributes = array('type' => 'text'); + public $mysql_field = 'TEXT DEFAULT NULL'; + + protected function setMoreOptions() { + $this->input_attributes['pattern'] = "[A-Za-züäöß.;,!: ]+"; + return parent::setMoreOptions(); + } } diff --git a/application/Model/Item/Mc.php b/application/Model/Item/Mc.php index 5482f5b78..7dd1afb68 100644 --- a/application/Model/Item/Mc.php +++ b/application/Model/Item/Mc.php @@ -5,113 +5,112 @@ */ class Mc_Item extends Item { - public $type = 'mc'; - public $lower_text = ''; - public $upper_text = ''; - public $input_attributes = array('type' => 'radio'); - public $mysql_field = 'TINYINT UNSIGNED DEFAULT NULL'; - protected $hasChoices = true; - - public function validateInput($reply) { - if (!($this->optional && $reply == '') && !empty($this->choices) && // check - ( is_string($reply) && !in_array($reply, array_keys($this->choices)) ) || // mc - ( is_array($reply) && ($diff = array_diff($reply, array_keys($this->choices))) && !empty($diff) && current($diff) !== '' ) // mc_multiple - ) { // invalid multiple choice answer - if (isset($diff)) { - $problem = $diff; - } else { - $problem = $reply; - } - - if (is_array($problem)) { - $problem = implode("', '", $problem); - } - - $this->error = __("You chose an option '%s' that is not permitted.", h($problem)); - } - return parent::validateInput($reply); - } - - protected function render_label() { - $template = ''; - - return Template::replace($template, array( - 'class' => implode(' ', $this->classes_label), - 'error' => $this->render_error_tip(), - 'text' => $this->label_parsed, - )); - } - - protected function render_input() { - - $this->splitValues(); - - $ret = ' + public $type = 'mc'; + public $lower_text = ''; + public $upper_text = ''; + public $input_attributes = array('type' => 'radio'); + public $mysql_field = 'TINYINT UNSIGNED DEFAULT NULL'; + protected $hasChoices = true; + + public function validateInput($reply) { + if (!($this->optional && $reply == '') && !empty($this->choices) && // check + ( is_string($reply) && !in_array($reply, array_keys($this->choices)) ) || // mc + ( is_array($reply) && ($diff = array_diff($reply, array_keys($this->choices))) && !empty($diff) && current($diff) !== '' ) // mc_multiple + ) { // invalid multiple choice answer + if (isset($diff)) { + $problem = $diff; + } else { + $problem = $reply; + } + + if (is_array($problem)) { + $problem = implode("', '", $problem); + } + + $this->error = __("You chose an option '%s' that is not permitted.", h($problem)); + } + return parent::validateInput($reply); + } + + protected function render_label() { + $template = ''; + + return Template::replace($template, array( + 'class' => implode(' ', $this->classes_label), + 'error' => $this->render_error_tip(), + 'text' => $this->label_parsed, + )); + } + + protected function render_input() { + + $this->splitValues(); + + $ret = '
'; - $opt_values = array_count_values($this->choices); - if (isset($opt_values['']) && /* $opt_values[''] > 0 && */ current($this->choices) != '') { // and the first option isn't empty - $this->label_first = true; // the first option label will be rendered before the radio button instead of after it. - } else { - $this->label_first = false; - } - - if (mb_strpos(implode(' ', $this->classes_wrapper), 'mc-first-left') !== false) { - $this->label_first = true; - } - $all_left = false; - if (mb_strpos(implode(' ', $this->classes_wrapper), 'mc-all-left') !== false) { - $all_left = true; - } - - if ($this->value_validated) { - $this->presetValues[] = $this->value_validated; - } - - foreach ($this->choices as $value => $option) { - // determine whether options needs to be checked - if (in_array($value, $this->presetValues)) { - $this->input_attributes['checked'] = true; - } else { - $this->input_attributes['checked'] = false; - } - - $label = ' + $opt_values = array_count_values($this->choices); + if (isset($opt_values['']) && /* $opt_values[''] > 0 && */ current($this->choices) != '') { // and the first option isn't empty + $this->label_first = true; // the first option label will be rendered before the radio button instead of after it. + } else { + $this->label_first = false; + } + + if (mb_strpos(implode(' ', $this->classes_wrapper), 'mc-first-left') !== false) { + $this->label_first = true; + } + $all_left = false; + if (mb_strpos(implode(' ', $this->classes_wrapper), 'mc-all-left') !== false) { + $all_left = true; + } + + if ($this->value_validated) { + $this->presetValues[] = $this->value_validated; + } + + foreach ($this->choices as $value => $option) { + // determine whether options needs to be checked + if (in_array($value, $this->presetValues)) { + $this->input_attributes['checked'] = true; + } else { + $this->input_attributes['checked'] = false; + } + + $label = ' '; - $label = Template::replace($label, array( - 'id' => $this->id, - 'value' => $value, - 'input_attributes' => self::_parseAttributes($this->input_attributes, array('id')), - 'left_span' => ($this->label_first || $all_left) ? '' . $option . '' : '', - 'right_span' => ($this->label_first || $all_left) ? ' ' : ' ' . $option . '' - )); - - if (in_array('mc_vertical', $this->classes_wrapper)) { - $ret .= '
' . $label . '
'; - } else { - $ret .= $label; - } - - if ($this->label_first) { - $this->label_first = false; - } - - } - - $ret .= '
'; - - return Template::replace($ret, array( - 'js_hidden' => $this->js_hidden ? ' js_hidden' : '', - 'id' => $this->id, - 'input_attributes' => self::_parseAttributes($this->input_attributes, array('type', 'id', 'required')), - )); - } + $label = Template::replace($label, array( + 'id' => $this->id, + 'value' => $value, + 'input_attributes' => self::_parseAttributes($this->input_attributes, array('id')), + 'left_span' => ($this->label_first || $all_left) ? '' . $option . '' : '', + 'right_span' => ($this->label_first || $all_left) ? ' ' : ' ' . $option . '' + )); + + if (in_array('mc_vertical', $this->classes_wrapper)) { + $ret .= '
' . $label . '
'; + } else { + $ret .= $label; + } + + if ($this->label_first) { + $this->label_first = false; + } + } + + $ret .= ''; + + return Template::replace($ret, array( + 'js_hidden' => $this->js_hidden ? ' js_hidden' : '', + 'id' => $this->id, + 'input_attributes' => self::_parseAttributes($this->input_attributes, array('type', 'id', 'required')), + )); + } } diff --git a/application/Model/Item/McButton.php b/application/Model/Item/McButton.php index b40c81425..ab709c96f 100644 --- a/application/Model/Item/McButton.php +++ b/application/Model/Item/McButton.php @@ -2,27 +2,27 @@ class McButton_Item extends Mc_Item { - public $mysql_field = 'TINYINT UNSIGNED DEFAULT NULL'; - protected $js_hidden = true; + public $mysql_field = 'TINYINT UNSIGNED DEFAULT NULL'; + protected $js_hidden = true; - protected function setMoreOptions() { - parent::setMoreOptions(); - $this->classes_wrapper[] = 'btn-radio'; - } + protected function setMoreOptions() { + parent::setMoreOptions(); + $this->classes_wrapper[] = 'btn-radio'; + } - protected function render_appended() { - $ret = '
'; - foreach ($this->choices as $value => $option) { - $tpl = ' + protected function render_appended() { + $ret = '
'; + foreach ($this->choices as $value => $option) { + $tpl = ' '; - $ret .= Template::replace($tpl, array('id' => $this->id, 'value' => $value, 'option' => $option)); - } - $ret .= '
'; + $ret .= Template::replace($tpl, array('id' => $this->id, 'value' => $value, 'option' => $option)); + } + $ret .= '
'; - return $ret; - } + return $ret; + } } diff --git a/application/Model/Item/McHeading.php b/application/Model/Item/McHeading.php index 269c84cb3..d0f1acee2 100644 --- a/application/Model/Item/McHeading.php +++ b/application/Model/Item/McHeading.php @@ -2,71 +2,70 @@ class McHeading_Item extends Mc_Item { - public $type = 'mc_heading'; - public $mysql_field = null; - public $save_in_results_table = false; + public $type = 'mc_heading'; + public $mysql_field = null; + public $save_in_results_table = false; - protected function setMoreOptions() { - $this->input_attributes['disabled'] = 'disabled'; - } + protected function setMoreOptions() { + $this->input_attributes['disabled'] = 'disabled'; + } - protected function render_label() { - $template = ' + protected function render_label() { + $template = '
%{error} %{text}
'; - return Template::replace($template, array( - 'class' => implode(' ', $this->classes_label), - 'error' => $this->render_error_tip(), - 'text' => $this->label_parsed, - 'name' => $this->name, - )); - } + return Template::replace($template, array( + 'class' => implode(' ', $this->classes_label), + 'error' => $this->render_error_tip(), + 'text' => $this->label_parsed, + 'name' => $this->name, + )); + } - protected function render_input() { - $ret = '
'; + protected function render_input() { + $ret = '
'; - $this->input_attributes['type'] = 'radio'; - $opt_values = array_count_values($this->choices); - if (isset($opt_values['']) && /* // if there are empty options $opt_values[''] > 0 && */ current($this->choices) != '') { // and the first option isn't empty - $this->label_first = true; // the first option label will be rendered before the radio button instead of after it. - } else { - $this->label_first = false; - } + $this->input_attributes['type'] = 'radio'; + $opt_values = array_count_values($this->choices); + if (isset($opt_values['']) && /* // if there are empty options $opt_values[''] > 0 && */ current($this->choices) != '') { // and the first option isn't empty + $this->label_first = true; // the first option label will be rendered before the radio button instead of after it. + } else { + $this->label_first = false; + } - if (mb_strpos(implode(" ", $this->classes_wrapper), 'mc_first_left') !== false) { - $this->label_first = true; - } - $all_left = false; - if (mb_strpos(implode(" ", $this->classes_wrapper), 'mc_all_left') !== false) { - $all_left = true; - } + if (mb_strpos(implode(" ", $this->classes_wrapper), 'mc_first_left') !== false) { + $this->label_first = true; + } + $all_left = false; + if (mb_strpos(implode(" ", $this->classes_wrapper), 'mc_all_left') !== false) { + $all_left = true; + } - foreach ($this->choices as $value => $option) { - $this->input_attributes['selected'] = $this->isSelectedOptionValue($value, $this->value_validated); - $label = ' + foreach ($this->choices as $value => $option) { + $this->input_attributes['selected'] = $this->isSelectedOptionValue($value, $this->value_validated); + $label = ' '; - $ret .= Template::replace($label, array( - 'id' => $this->id, - 'value' => $value, - 'input_attributes' => self::_parseAttributes($this->input_attributes, array('id')), - 'left_span' => ($this->label_first || $all_left) ? $option . ' ' : '', - 'right_span' => ($this->label_first || $all_left) ? " " : ' ' . $option, - )); + $ret .= Template::replace($label, array( + 'id' => $this->id, + 'value' => $value, + 'input_attributes' => self::_parseAttributes($this->input_attributes, array('id')), + 'left_span' => ($this->label_first || $all_left) ? $option . ' ' : '', + 'right_span' => ($this->label_first || $all_left) ? " " : ' ' . $option, + )); + } - } + $ret .= '
'; - $ret .= '
'; - - return $ret; - } + return $ret; + } } diff --git a/application/Model/Item/McMultiple.php b/application/Model/Item/McMultiple.php index 6fadcfb5b..de488a328 100644 --- a/application/Model/Item/McMultiple.php +++ b/application/Model/Item/McMultiple.php @@ -6,84 +6,84 @@ */ class McMultiple_Item extends Mc_Item { - public $type = 'mc_multiple'; - public $input_attributes = array('type' => 'checkbox'); - public $optional = 1; - public $mysql_field = 'VARCHAR(40) DEFAULT NULL'; + public $type = 'mc_multiple'; + public $input_attributes = array('type' => 'checkbox'); + public $optional = 1; + public $mysql_field = 'VARCHAR(40) DEFAULT NULL'; - protected function setMoreOptions() { - $this->input_attributes['name'] = $this->name . '[]'; - } + protected function setMoreOptions() { + $this->input_attributes['name'] = $this->name . '[]'; + } - protected function chooseResultFieldBasedOnChoices() { - $choices = array_keys($this->choices); - $max = implode(", ", array_filter($choices)); - $maxlen = strlen($max); - $this->mysql_field = 'VARCHAR (' . $maxlen . ') DEFAULT NULL'; - } + protected function chooseResultFieldBasedOnChoices() { + $choices = array_keys($this->choices); + $max = implode(", ", array_filter($choices)); + $maxlen = strlen($max); + $this->mysql_field = 'VARCHAR (' . $maxlen . ') DEFAULT NULL'; + } - protected function render_input() { - if (!$this->optional) { - $this->input_attributes['data-grouprequired'] = ''; - } - $this->splitValues(); + protected function render_input() { + if (!$this->optional) { + $this->input_attributes['data-grouprequired'] = ''; + } + $this->splitValues(); - $ret = ' + $ret = '
'; - if (!$this->optional) { - // this is a kludge, but if I don't add this, checkboxes are always circled red - $ret .= ''; - } + if (!$this->optional) { + // this is a kludge, but if I don't add this, checkboxes are always circled red + $ret .= ''; + } - if ($this->value_validated) { - $this->presetValues[] = $this->value_validated; - } + if ($this->value_validated) { + $this->presetValues[] = $this->value_validated; + } - foreach ($this->choices AS $value => $option) { - // determine whether options needs to be checked - if (in_array($value, $this->presetValues)) { - $this->input_attributes['checked'] = true; - } else { - $this->input_attributes['checked'] = false; - } + foreach ($this->choices AS $value => $option) { + // determine whether options needs to be checked + if (in_array($value, $this->presetValues)) { + $this->input_attributes['checked'] = true; + } else { + $this->input_attributes['checked'] = false; + } - $label = ' + $label = ' '; - $label = Template::replace($label, array( - 'id' => $this->id, - 'value' => $value, - 'option' => $option, - 'input_attributes' => self::_parseAttributes($this->input_attributes, array('id', 'required', 'data-grouprequired')), - )); + $label = Template::replace($label, array( + 'id' => $this->id, + 'value' => $value, + 'option' => $option, + 'input_attributes' => self::_parseAttributes($this->input_attributes, array('id', 'required', 'data-grouprequired')), + )); - if (in_array('mc_vertical', $this->classes_wrapper)) { - $ret .= '
' . $label . '
'; - } else { - $ret .= $label; - } - } + if (in_array('mc_vertical', $this->classes_wrapper)) { + $ret .= '
' . $label . '
'; + } else { + $ret .= $label; + } + } - $ret .= '
'; + $ret .= ''; - return Template::replace($ret, array( - 'js_hidden' => $this->js_hidden ? ' js_hidden' : '', - 'id' => $this->id, - 'input_attributes' => self::_parseAttributes($this->input_attributes, array('id', 'type', 'required', 'data-grouprequired')), - 'input_attributes_' => self::_parseAttributes($this->input_attributes, array('class', 'id', 'required')), - )); - } + return Template::replace($ret, array( + 'js_hidden' => $this->js_hidden ? ' js_hidden' : '', + 'id' => $this->id, + 'input_attributes' => self::_parseAttributes($this->input_attributes, array('id', 'type', 'required', 'data-grouprequired')), + 'input_attributes_' => self::_parseAttributes($this->input_attributes, array('class', 'id', 'required')), + )); + } - public function getReply($reply) { - if (is_array($reply)) { - $reply = implode(", ", array_filter($reply)); - } - return $reply; - } + public function getReply($reply) { + if (is_array($reply)) { + $reply = implode(", ", array_filter($reply)); + } + return $reply; + } } diff --git a/application/Model/Item/McMultipleButton.php b/application/Model/Item/McMultipleButton.php index 71823fc63..93dee8a99 100644 --- a/application/Model/Item/McMultipleButton.php +++ b/application/Model/Item/McMultipleButton.php @@ -2,28 +2,28 @@ class McMultipleButton_Item extends McMultiple_Item { - public $mysql_field = 'VARCHAR(40) DEFAULT NULL'; - public $type = "mc_multiple_button"; - protected $js_hidden = true; + public $mysql_field = 'VARCHAR(40) DEFAULT NULL'; + public $type = "mc_multiple_button"; + protected $js_hidden = true; - protected function setMoreOptions() { - parent::setMoreOptions(); - $this->classes_wrapper[] = 'btn-checkbox'; - } + protected function setMoreOptions() { + parent::setMoreOptions(); + $this->classes_wrapper[] = 'btn-checkbox'; + } - protected function render_appended() { - $ret = '
'; - foreach ($this->choices as $value => $option) { - $tpl = ' + protected function render_appended() { + $ret = '
'; + foreach ($this->choices as $value => $option) { + $tpl = ' '; - $ret .= Template::replace($tpl, array('id' => $this->id, 'value' => $value, 'option' => $option)); - } - $ret .= '
'; + $ret .= Template::replace($tpl, array('id' => $this->id, 'value' => $value, 'option' => $option)); + } + $ret .= '
'; - return $ret; - } + return $ret; + } } diff --git a/application/Model/Item/Month.php b/application/Model/Item/Month.php index eb012ca69..bba37f32e 100644 --- a/application/Model/Item/Month.php +++ b/application/Model/Item/Month.php @@ -2,10 +2,8 @@ class Month_Item extends Yearmonth_Item { - public $type = 'month'; - public $input_attributes = array('type' => 'month' , 'placeholder' => 'yyyy-mm'); - - //protected $prepend = 'fa-calendar-o'; + public $type = 'month'; + public $input_attributes = array('type' => 'month', 'placeholder' => 'yyyy-mm'); + //protected $prepend = 'fa-calendar-o'; } - diff --git a/application/Model/Item/Note.php b/application/Model/Item/Note.php index df690d87c..bd7a4842a 100644 --- a/application/Model/Item/Note.php +++ b/application/Model/Item/Note.php @@ -3,27 +3,27 @@ // notes are rendered at full width class Note_Item extends Item { - public $type = 'note'; - public $mysql_field = null; - public $input_attributes = array('type' => 'hidden', "value" => 1); - public $save_in_results_table = false; - public $optional = 1; + public $type = 'note'; + public $mysql_field = null; + public $input_attributes = array('type' => 'hidden', "value" => 1); + public $save_in_results_table = false; + public $optional = 1; - protected function render_label() { - $template = '
%{error} %{text}
'; + protected function render_label() { + $template = '
%{error} %{text}
'; - return Template::replace($template, array( - 'class' => implode(' ', $this->classes_label), - 'error' => $this->render_error_tip(), - 'text' => $this->label_parsed, - )); - } + return Template::replace($template, array( + 'class' => implode(' ', $this->classes_label), + 'error' => $this->render_error_tip(), + 'text' => $this->label_parsed, + )); + } - public function validateInput($reply) { - if ($reply != 1) { - $this->error = _("You can only answer notes by viewing them."); - } - return $reply; - } + public function validateInput($reply) { + if ($reply != 1) { + $this->error = _("You can only answer notes by viewing them."); + } + return $reply; + } } diff --git a/application/Model/Item/NoteIframe.php b/application/Model/Item/NoteIframe.php index 6ba4ae155..5df58ff09 100644 --- a/application/Model/Item/NoteIframe.php +++ b/application/Model/Item/NoteIframe.php @@ -3,25 +3,25 @@ // notes are rendered at full width class NoteIframe_Item extends Note_Item { - public $type = 'note_iframe'; - public $mysql_field = null; - public $input_attributes = array('type' => 'hidden', "value" => 1); - public $save_in_results_table = false; + public $type = 'note_iframe'; + public $mysql_field = null; + public $input_attributes = array('type' => 'hidden', "value" => 1); + public $save_in_results_table = false; - public function needsDynamicLabel($survey = null) { - $ocpu_vars = $survey->getUserDataInRun($this->label, $survey->name); - $ocpu_session = opencpu_knit_iframe($this->label, $ocpu_vars, true, $survey->name); - if ($ocpu_session && !$ocpu_session->hasError()) { - $iframesrc = $ocpu_session->getFiles("knit.html")['knit.html']; - $this->label_parsed = ''. - '
+ public function needsDynamicLabel($survey = null) { + $ocpu_vars = $survey->getUserDataInRun($this->label, $survey->name); + $ocpu_session = opencpu_knit_iframe($this->label, $ocpu_vars, true, $survey->name); + if ($ocpu_session && !$ocpu_session->hasError()) { + $iframesrc = $ocpu_session->getFiles("knit.html")['knit.html']; + $this->label_parsed = '' . + '
'; - } + } - return false; - } + return false; + } } diff --git a/application/Model/Item/Number.php b/application/Model/Item/Number.php index b9c8ec939..2af77dcc5 100644 --- a/application/Model/Item/Number.php +++ b/application/Model/Item/Number.php @@ -5,121 +5,120 @@ * - the min/max stuff is confusing and will fail for real big numbers * - spinbox is polyfilled in browsers that lack it */ - class Number_Item extends Item { - public $type = 'number'; - public $input_attributes = array('type' => 'number', 'min' => 0, 'max' => 10000000, 'step' => 1); - public $mysql_field = 'INT UNSIGNED DEFAULT NULL'; - - protected function setMoreOptions() { - $this->classes_input[] = 'form-control'; - if (isset($this->type_options) && trim($this->type_options) != "") { - $this->type_options_array = explode(",", $this->type_options, 3); - - $min = trim(reset($this->type_options_array)); - if (is_numeric($min) OR $min === 'any') { - $this->input_attributes['min'] = $min; - } - - $max = trim(next($this->type_options_array)); - if (is_numeric($max) OR $max === 'any') { - $this->input_attributes['max'] = $max; - } - - $step = trim(next($this->type_options_array)); - if (is_numeric($step) OR $step === 'any') { - $this->input_attributes['step'] = $step; - } - } - - $multiply = 2; - if ($this->input_attributes['min'] < 0) : - $this->mysql_field = str_replace("UNSIGNED", "", $this->mysql_field); - $multiply = 1; - endif; - - if ($this->input_attributes['step'] === 'any' OR $this->input_attributes['min'] === 'any' OR $this->input_attributes['max'] === 'any'): // is any any - $this->mysql_field = str_replace(array("INT"), "FLOAT", $this->mysql_field); // use FLOATing point accuracy - else: - if ( - (abs($this->input_attributes['min']) < ($multiply * 127) ) && ( abs($this->input_attributes['max']) < ($multiply * 127) ) - ): - $this->mysql_field = preg_replace("/^INT\s/", "TINYINT ", $this->mysql_field); - elseif ( - (abs($this->input_attributes['min']) < ($multiply * 32767) ) && ( abs($this->input_attributes['max']) < ($multiply * 32767) ) - ): - $this->mysql_field = preg_replace("/^INT\s/", "SMALLINT ", $this->mysql_field); - elseif ( - (abs($this->input_attributes['min']) < ($multiply * 8388608) ) && ( abs($this->input_attributes['max']) < ($multiply * 8388608) ) - ): - $this->mysql_field = preg_replace("/^INT\s/", "MEDIUMINT ", $this->mysql_field); - elseif ( - (abs($this->input_attributes['min']) < ($multiply * 2147483648) ) && ( abs($this->input_attributes['max']) < ($multiply * 2147483648) ) - ): - $this->mysql_field = str_replace("INT", "INT", $this->mysql_field); - elseif ( - (abs($this->input_attributes['min']) < ($multiply * 9223372036854775808) ) && ( abs($this->input_attributes['max']) < ($multiply * 9223372036854775808) ) - ): - $this->mysql_field = preg_replace("/^INT\s/", "BIGINT ", $this->mysql_field); - endif; - - // FIXME: why not use is_int()? why casting to int before strlen? - if ((string) (int) $this->input_attributes['step'] != $this->input_attributes['step']): // step is integer? - $before_point = max(strlen((int) $this->input_attributes['min']), strlen((int) $this->input_attributes['max'])); // use decimal with this many digits - $after_point = strlen($this->input_attributes['step']) - 2; - $d = $before_point + $after_point; - - $this->mysql_field = str_replace(array("TINYINT", "SMALLINT", "MEDIUMINT", "INT", "BIGINT"), "DECIMAL($d, $after_point)", $this->mysql_field); - endif; - endif; - } - - public function validateInput($reply) { // fixme: input is not re-displayed after this - $reply = trim(str_replace(",", ".", $reply)); - if (!$reply && $reply !== 0 && $this->optional) { - return null; - } - - if ($this->input_attributes['min'] !== 'any' && $reply < $this->input_attributes['min']) { // lower number than allowed - $this->error = __("The minimum is %d.", $this->input_attributes['min']); - } elseif ($this->input_attributes['max'] !== 'any' && $reply > $this->input_attributes['max']) { // larger number than allowed - $this->error = __("The maximum is %d.", $this->input_attributes['max']); - } elseif ($this->input_attributes['step'] !== 'any' AND - abs( - (round($reply / $this->input_attributes['step']) * $this->input_attributes['step']) // divide, round and multiply by step - - $reply // should be equal to reply - ) > 0.000000001 // with floats I have to leave a small margin of error - ) { - $this->error = __("Numbers have to be in steps of at least %d.", $this->input_attributes['step']); - } - - return parent::validateInput($reply); - } - - public function getReply($reply) { - $reply = trim(str_replace(",", ".", $reply)); - if (!$reply && $reply !== 0 && $this->optional) { - return null; - } - return $reply; - } - - public function needsDynamicValue() { - /** - * If item is serving as a counter and already has a saved value then use that - */ - if ($this->value_validated !== null && $this->isCounter()) { - $this->input_attributes['value'] = $this->value_validated; - return false; - } else { - // Use the default calulation - return parent::needsDynamicValue(); - } - } - - protected function isCounter() { - return in_array('counter', $this->classes_wrapper); - } + public $type = 'number'; + public $input_attributes = array('type' => 'number', 'min' => 0, 'max' => 10000000, 'step' => 1); + public $mysql_field = 'INT UNSIGNED DEFAULT NULL'; + + protected function setMoreOptions() { + $this->classes_input[] = 'form-control'; + if (isset($this->type_options) && trim($this->type_options) != "") { + $this->type_options_array = explode(",", $this->type_options, 3); + + $min = trim(reset($this->type_options_array)); + if (is_numeric($min) OR $min === 'any') { + $this->input_attributes['min'] = $min; + } + + $max = trim(next($this->type_options_array)); + if (is_numeric($max) OR $max === 'any') { + $this->input_attributes['max'] = $max; + } + + $step = trim(next($this->type_options_array)); + if (is_numeric($step) OR $step === 'any') { + $this->input_attributes['step'] = $step; + } + } + + $multiply = 2; + if ($this->input_attributes['min'] < 0) : + $this->mysql_field = str_replace("UNSIGNED", "", $this->mysql_field); + $multiply = 1; + endif; + + if ($this->input_attributes['step'] === 'any' OR $this->input_attributes['min'] === 'any' OR $this->input_attributes['max'] === 'any'): // is any any + $this->mysql_field = str_replace(array("INT"), "FLOAT", $this->mysql_field); // use FLOATing point accuracy + else: + if ( + (abs($this->input_attributes['min']) < ($multiply * 127) ) && ( abs($this->input_attributes['max']) < ($multiply * 127) ) + ): + $this->mysql_field = preg_replace("/^INT\s/", "TINYINT ", $this->mysql_field); + elseif ( + (abs($this->input_attributes['min']) < ($multiply * 32767) ) && ( abs($this->input_attributes['max']) < ($multiply * 32767) ) + ): + $this->mysql_field = preg_replace("/^INT\s/", "SMALLINT ", $this->mysql_field); + elseif ( + (abs($this->input_attributes['min']) < ($multiply * 8388608) ) && ( abs($this->input_attributes['max']) < ($multiply * 8388608) ) + ): + $this->mysql_field = preg_replace("/^INT\s/", "MEDIUMINT ", $this->mysql_field); + elseif ( + (abs($this->input_attributes['min']) < ($multiply * 2147483648) ) && ( abs($this->input_attributes['max']) < ($multiply * 2147483648) ) + ): + $this->mysql_field = str_replace("INT", "INT", $this->mysql_field); + elseif ( + (abs($this->input_attributes['min']) < ($multiply * 9223372036854775808) ) && ( abs($this->input_attributes['max']) < ($multiply * 9223372036854775808) ) + ): + $this->mysql_field = preg_replace("/^INT\s/", "BIGINT ", $this->mysql_field); + endif; + + // FIXME: why not use is_int()? why casting to int before strlen? + if ((string) (int) $this->input_attributes['step'] != $this->input_attributes['step']): // step is integer? + $before_point = max(strlen((int) $this->input_attributes['min']), strlen((int) $this->input_attributes['max'])); // use decimal with this many digits + $after_point = strlen($this->input_attributes['step']) - 2; + $d = $before_point + $after_point; + + $this->mysql_field = str_replace(array("TINYINT", "SMALLINT", "MEDIUMINT", "INT", "BIGINT"), "DECIMAL($d, $after_point)", $this->mysql_field); + endif; + endif; + } + + public function validateInput($reply) { // fixme: input is not re-displayed after this + $reply = trim(str_replace(",", ".", $reply)); + if (!$reply && $reply !== 0 && $this->optional) { + return null; + } + + if ($this->input_attributes['min'] !== 'any' && $reply < $this->input_attributes['min']) { // lower number than allowed + $this->error = __("The minimum is %d.", $this->input_attributes['min']); + } elseif ($this->input_attributes['max'] !== 'any' && $reply > $this->input_attributes['max']) { // larger number than allowed + $this->error = __("The maximum is %d.", $this->input_attributes['max']); + } elseif ($this->input_attributes['step'] !== 'any' AND + abs( + (round($reply / $this->input_attributes['step']) * $this->input_attributes['step']) // divide, round and multiply by step + - $reply // should be equal to reply + ) > 0.000000001 // with floats I have to leave a small margin of error + ) { + $this->error = __("Numbers have to be in steps of at least %d.", $this->input_attributes['step']); + } + + return parent::validateInput($reply); + } + + public function getReply($reply) { + $reply = trim(str_replace(",", ".", $reply)); + if (!$reply && $reply !== 0 && $this->optional) { + return null; + } + return $reply; + } + + public function needsDynamicValue() { + /** + * If item is serving as a counter and already has a saved value then use that + */ + if ($this->value_validated !== null && $this->isCounter()) { + $this->input_attributes['value'] = $this->value_validated; + return false; + } else { + // Use the default calulation + return parent::needsDynamicValue(); + } + } + + protected function isCounter() { + return in_array('counter', $this->classes_wrapper); + } } diff --git a/application/Model/Item/Random.php b/application/Model/Item/Random.php index 13dfed914..4334674b6 100644 --- a/application/Model/Item/Random.php +++ b/application/Model/Item/Random.php @@ -2,29 +2,29 @@ class Random_Item extends Number_Item { - public $type = 'random'; - public $input_attributes = array('type' => 'hidden', 'step' => 1); - public $mysql_field = 'INT UNSIGNED DEFAULT NULL'; - public $no_user_input_required = true; + public $type = 'random'; + public $input_attributes = array('type' => 'hidden', 'step' => 1); + public $mysql_field = 'INT UNSIGNED DEFAULT NULL'; + public $no_user_input_required = true; - protected function setMoreOptions() { - parent::setMoreOptions(); - $this->input_attributes['value'] = $this->validateInput(); - } + protected function setMoreOptions() { + parent::setMoreOptions(); + $this->input_attributes['value'] = $this->validateInput(); + } - public function validateInput($reply = '') { - if (isset($this->input_attributes['min']) && isset($this->input_attributes['max'])) { // both limits specified - $reply = mt_rand($this->input_attributes['min'], $this->input_attributes['max']); - } elseif (!isset($this->input_attributes['min']) && !isset($this->input_attributes['max'])) { // neither limit specified - $reply = mt_rand(0, 1); - } else { - $this->error = __("Both random minimum and maximum need to be specified"); - } - return $reply; - } + public function validateInput($reply = '') { + if (isset($this->input_attributes['min']) && isset($this->input_attributes['max'])) { // both limits specified + $reply = mt_rand($this->input_attributes['min'], $this->input_attributes['max']); + } elseif (!isset($this->input_attributes['min']) && !isset($this->input_attributes['max'])) { // neither limit specified + $reply = mt_rand(0, 1); + } else { + $this->error = __("Both random minimum and maximum need to be specified"); + } + return $reply; + } - public function getReply($reply) { - return $this->input_attributes['value']; - } + public function getReply($reply) { + return $this->input_attributes['value']; + } } diff --git a/application/Model/Item/Range.php b/application/Model/Item/Range.php index 3854ab156..b6fea07cb 100644 --- a/application/Model/Item/Range.php +++ b/application/Model/Item/Range.php @@ -2,39 +2,38 @@ class Range_Item extends Number_Item { - public $type = 'range'; - public $input_attributes = array('type' => 'range', 'min' => 0, 'max' => 100, 'step' => 1); - public $mysql_field = 'INT UNSIGNED DEFAULT NULL'; - - protected $hasChoices = true; - - protected function setMoreOptions() { - $this->lower_text = current($this->choices); - $this->upper_text = next($this->choices); - parent::setMoreOptions(); - - $this->classes_input = array_diff($this->classes_input, array('form-control')); - } - - protected function render_input() { - if ($this->value_validated) { - $this->input_attributes['value'] = $this->value_validated; - } - - $tpl = '%{left_label} %{right_label}'; - - return Template::replace($tpl, array( - 'left_label' => $this->render_pad_label(1, 'right'), - 'input_attributes' => self::_parseAttributes($this->input_attributes, array('required')), - 'right_label' => $this->render_pad_label(2, 'left'), - )); - } - - private function render_pad_label($choice, $pad) { - if (isset($this->choices[$choice])) { - return sprintf('', $pad, $this->choices[$choice]); - } - return ''; - } + public $type = 'range'; + public $input_attributes = array('type' => 'range', 'min' => 0, 'max' => 100, 'step' => 1); + public $mysql_field = 'INT UNSIGNED DEFAULT NULL'; + protected $hasChoices = true; + + protected function setMoreOptions() { + $this->lower_text = current($this->choices); + $this->upper_text = next($this->choices); + parent::setMoreOptions(); + + $this->classes_input = array_diff($this->classes_input, array('form-control')); + } + + protected function render_input() { + if ($this->value_validated) { + $this->input_attributes['value'] = $this->value_validated; + } + + $tpl = '%{left_label} %{right_label}'; + + return Template::replace($tpl, array( + 'left_label' => $this->render_pad_label(1, 'right'), + 'input_attributes' => self::_parseAttributes($this->input_attributes, array('required')), + 'right_label' => $this->render_pad_label(2, 'left'), + )); + } + + private function render_pad_label($choice, $pad) { + if (isset($this->choices[$choice])) { + return sprintf('', $pad, $this->choices[$choice]); + } + return ''; + } } diff --git a/application/Model/Item/RangeTicks.php b/application/Model/Item/RangeTicks.php index a71b091f8..83679fbd6 100644 --- a/application/Model/Item/RangeTicks.php +++ b/application/Model/Item/RangeTicks.php @@ -5,42 +5,40 @@ */ class RangeTicks_Item extends Number_Item { - public $type = 'range_ticks'; - public $input_attributes = array('type' => 'range', 'step' => 1); - - protected $labels = array(); - protected $left_label = ''; - protected $right_labeel = ''; + public $type = 'range_ticks'; + public $input_attributes = array('type' => 'range', 'step' => 1); + protected $labels = array(); + protected $left_label = ''; + protected $right_labeel = ''; + protected $hasChoices = true; - protected $hasChoices = true; - - public function setChoices($choices) { - $this->labels = $choices; - $this->left_label = $this->render_pad_label(1, 'right'); - $this->right_labeel = $this->render_pad_label(2, 'left'); - } + public function setChoices($choices) { + $this->labels = $choices; + $this->left_label = $this->render_pad_label(1, 'right'); + $this->right_labeel = $this->render_pad_label(2, 'left'); + } - protected function setMoreOptions() { - $this->input_attributes['min'] = 0; - $this->input_attributes['max'] = 100; - $this->input_attributes['list'] = 'dlist' . $this->id; - $this->input_attributes['data-range'] = '{"animate": true, "classes": "show-activevaluetooltip"}'; - $this->classes_input[] = "range-list"; + protected function setMoreOptions() { + $this->input_attributes['min'] = 0; + $this->input_attributes['max'] = 100; + $this->input_attributes['list'] = 'dlist' . $this->id; + $this->input_attributes['data-range'] = '{"animate": true, "classes": "show-activevaluetooltip"}'; + $this->classes_input[] = "range-list"; - $this->classes_wrapper[] = 'range_ticks_output'; + $this->classes_wrapper[] = 'range_ticks_output'; - parent::setMoreOptions(); - $this->classes_input = array_diff($this->classes_input, array('form-control')); + parent::setMoreOptions(); + $this->classes_input = array_diff($this->classes_input, array('form-control')); - // Set actual choices based on defined range and step - $this->choices = array(); - for ($i = $this->input_attributes['min']; $i <= $this->input_attributes['max']; $i = $i + $this->input_attributes['step']) { - $this->choices[$i] = $i; - } - } + // Set actual choices based on defined range and step + $this->choices = array(); + for ($i = $this->input_attributes['min']; $i <= $this->input_attributes['max']; $i = $i + $this->input_attributes['step']) { + $this->choices[$i] = $i; + } + } - protected function render_input() { - $tpl = ' + protected function render_input() { + $tpl = ' %{left_label} @@ -51,25 +49,25 @@ protected function render_input() { %{right_label} '; - $options = ''; - for ($i = $this->input_attributes['min']; $i <= $this->input_attributes['max']; $i = $i + $this->input_attributes['step']) { - $options .= sprintf('', $i, $i); - } + $options = ''; + for ($i = $this->input_attributes['min']; $i <= $this->input_attributes['max']; $i = $i + $this->input_attributes['step']) { + $options .= sprintf('', $i, $i); + } - return Template::replace($tpl, array( - 'left_label' => $this->left_label, - 'input_attributes' => self::_parseAttributes($this->input_attributes, array('required')), - 'id' => $this->id, - 'options' => $options, - 'right_label' => $this->right_labeel, - )); - } + return Template::replace($tpl, array( + 'left_label' => $this->left_label, + 'input_attributes' => self::_parseAttributes($this->input_attributes, array('required')), + 'id' => $this->id, + 'options' => $options, + 'right_label' => $this->right_labeel, + )); + } - private function render_pad_label($choice, $pad) { - if (isset($this->labels[$choice])) { - return sprintf('', $pad, $this->labels[$choice]); - } - return ''; - } + private function render_pad_label($choice, $pad) { + if (isset($this->labels[$choice])) { + return sprintf('', $pad, $this->labels[$choice]); + } + return ''; + } } diff --git a/application/Model/Item/RatingButton.php b/application/Model/Item/RatingButton.php index af840b276..c0ef44ffc 100644 --- a/application/Model/Item/RatingButton.php +++ b/application/Model/Item/RatingButton.php @@ -2,60 +2,60 @@ class RatingButton_Item extends McButton_Item { - public $mysql_field = 'SMALLINT DEFAULT NULL'; - public $type = "rating_button"; - private $step = 1; - private $lower_limit = 1; - private $upper_limit = 5; - - protected function setMoreOptions() { - parent::setMoreOptions(); - $this->step = 1; - $this->lower_limit = 1; - $this->upper_limit = 5; - - if (isset($this->type_options_array) && is_array($this->type_options_array)) { - if (count($this->type_options_array) == 1) { - $this->type_options_array = explode(",", current($this->type_options_array)); - } - - if (count($this->type_options_array) == 1) { - $this->upper_limit = (int) trim($this->type_options_array[0]); - } elseif (count($this->type_options_array) == 2) { - $this->lower_limit = (int) trim($this->type_options_array[0]); - $this->upper_limit = (int) trim($this->type_options_array[1]); - } elseif (count($this->type_options_array) == 3) { - $this->lower_limit = (int) trim($this->type_options_array[0]); - $this->upper_limit = (int) trim($this->type_options_array[1]); - $this->step = (int) trim($this->type_options_array[2]); - } - } - - /** - * For obvious reason $this->choices can still be empty at this point (if user doesn't have choice1, choice2 columns but used a choice_list instead) - * So get labels from choice list which should be gotten from last item in options array - */ - // force step to be a non-zero positive number less than or equal to upper limit - $step = $this->step; - if ($this->upper_limit < $this->lower_limit && $step > 0) { - $step = -1 * $this->step; - } - - $choices = @range($this->lower_limit, $this->upper_limit, $step); - if ($choices && is_array($choices)) { - $this->choices = array_combine($choices, $choices); - } - } - - public function setChoices($choices) { - $this->lower_text = current($choices); - $this->upper_text = next($choices); - } - - protected function render_input() { - - $this->splitValues(); - $tpl = ' + public $mysql_field = 'SMALLINT DEFAULT NULL'; + public $type = "rating_button"; + private $step = 1; + private $lower_limit = 1; + private $upper_limit = 5; + + protected function setMoreOptions() { + parent::setMoreOptions(); + $this->step = 1; + $this->lower_limit = 1; + $this->upper_limit = 5; + + if (isset($this->type_options_array) && is_array($this->type_options_array)) { + if (count($this->type_options_array) == 1) { + $this->type_options_array = explode(",", current($this->type_options_array)); + } + + if (count($this->type_options_array) == 1) { + $this->upper_limit = (int) trim($this->type_options_array[0]); + } elseif (count($this->type_options_array) == 2) { + $this->lower_limit = (int) trim($this->type_options_array[0]); + $this->upper_limit = (int) trim($this->type_options_array[1]); + } elseif (count($this->type_options_array) == 3) { + $this->lower_limit = (int) trim($this->type_options_array[0]); + $this->upper_limit = (int) trim($this->type_options_array[1]); + $this->step = (int) trim($this->type_options_array[2]); + } + } + + /** + * For obvious reason $this->choices can still be empty at this point (if user doesn't have choice1, choice2 columns but used a choice_list instead) + * So get labels from choice list which should be gotten from last item in options array + */ + // force step to be a non-zero positive number less than or equal to upper limit + $step = $this->step; + if ($this->upper_limit < $this->lower_limit && $step > 0) { + $step = -1 * $this->step; + } + + $choices = @range($this->lower_limit, $this->upper_limit, $step); + if ($choices && is_array($choices)) { + $this->choices = array_combine($choices, $choices); + } + } + + public function setChoices($choices) { + $this->lower_text = current($choices); + $this->upper_text = next($choices); + } + + protected function render_input() { + + $this->splitValues(); + $tpl = ' @@ -63,39 +63,40 @@ protected function render_input() { '; - if ($this->value_validated) { - $this->presetValues[] = $this->value_validated; - } - - $labels = ''; - foreach ($this->choices as $option) { - // determine whether options needs to be checked - if (in_array($option, $this->presetValues)) { - $this->input_attributes['checked'] = true; - } else { - $this->input_attributes['checked'] = false; - } - - $label = ' '; - $labels .= Template::replace($label, array( - 'id' => $this->id, - 'option' => $option, - 'input_attributes' => self::_parseAttributes($this->input_attributes, array('id')), - )); - } - - return Template::replace($tpl, array( - 'id' => $this->id, - 'lower_text' => $this->lower_text, - 'labels' => $labels, - 'input_attributes' => self::_parseAttributes($this->input_attributes, array('type', 'id', 'required')), - )); - } - - protected function render_appended() { - $ret = parent::render_appended(); - $ret .= " "; - - return $ret; - } + if ($this->value_validated) { + $this->presetValues[] = $this->value_validated; + } + + $labels = ''; + foreach ($this->choices as $option) { + // determine whether options needs to be checked + if (in_array($option, $this->presetValues)) { + $this->input_attributes['checked'] = true; + } else { + $this->input_attributes['checked'] = false; + } + + $label = ' '; + $labels .= Template::replace($label, array( + 'id' => $this->id, + 'option' => $option, + 'input_attributes' => self::_parseAttributes($this->input_attributes, array('id')), + )); + } + + return Template::replace($tpl, array( + 'id' => $this->id, + 'lower_text' => $this->lower_text, + 'labels' => $labels, + 'input_attributes' => self::_parseAttributes($this->input_attributes, array('type', 'id', 'required')), + )); + } + + protected function render_appended() { + $ret = parent::render_appended(); + $ret .= " "; + + return $ret; + } + } diff --git a/application/Model/Item/Referrer.php b/application/Model/Item/Referrer.php index 699f91452..40b8ccd90 100644 --- a/application/Model/Item/Referrer.php +++ b/application/Model/Item/Referrer.php @@ -2,26 +2,26 @@ class Referrer_Item extends Item { - public $type = 'referrer'; - public $input_attributes = array('type' => 'hidden'); - public $mysql_field = 'TEXT DEFAULT NULL'; - public $no_user_input_required = true; - public $optional = 1; + public $type = 'referrer'; + public $input_attributes = array('type' => 'hidden'); + public $mysql_field = 'TEXT DEFAULT NULL'; + public $no_user_input_required = true; + public $optional = 1; - protected function setMoreOptions() { - $this->input_attributes['value'] = Site::getInstance()->last_outside_referrer; - } + protected function setMoreOptions() { + $this->input_attributes['value'] = Site::getInstance()->last_outside_referrer; + } - public function validateInput($reply) { - return $reply; - } + public function validateInput($reply) { + return $reply; + } - public function getReply($reply) { - return Site::getInstance()->last_outside_referrer; - } + public function getReply($reply) { + return Site::getInstance()->last_outside_referrer; + } - public function render() { - return $this->render_input(); - } + public function render() { + return $this->render_input(); + } } diff --git a/application/Model/Item/SelectMultiple.php b/application/Model/Item/SelectMultiple.php index d0b5a1808..04182f698 100644 --- a/application/Model/Item/SelectMultiple.php +++ b/application/Model/Item/SelectMultiple.php @@ -5,27 +5,27 @@ */ class SelectMultiple_Item extends SelectOne_Item { - public $type = 'select_multiple'; - public $mysql_field = 'VARCHAR(40) DEFAULT NULL'; + public $type = 'select_multiple'; + public $mysql_field = 'VARCHAR(40) DEFAULT NULL'; - protected function chooseResultFieldBasedOnChoices() { - $choices = array_keys($this->choices); - $max = implode(", ", array_filter($choices)); - $maxlen = strlen($max); - $this->mysql_field = 'VARCHAR (' . $maxlen . ') DEFAULT NULL'; - } + protected function chooseResultFieldBasedOnChoices() { + $choices = array_keys($this->choices); + $max = implode(", ", array_filter($choices)); + $maxlen = strlen($max); + $this->mysql_field = 'VARCHAR (' . $maxlen . ') DEFAULT NULL'; + } - protected function setMoreOptions() { - parent::setMoreOptions(); - $this->input_attributes['multiple'] = true; - $this->input_attributes['name'] = $this->name . '[]'; - } - public function getReply($reply) { - if (is_array($reply)) { - $reply = implode(", ", array_filter($reply)); - } - return $reply; - } -} + protected function setMoreOptions() { + parent::setMoreOptions(); + $this->input_attributes['multiple'] = true; + $this->input_attributes['name'] = $this->name . '[]'; + } + public function getReply($reply) { + if (is_array($reply)) { + $reply = implode(", ", array_filter($reply)); + } + return $reply; + } +} diff --git a/application/Model/Item/SelectOne.php b/application/Model/Item/SelectOne.php index 9e1a38df5..871d12669 100644 --- a/application/Model/Item/SelectOne.php +++ b/application/Model/Item/SelectOne.php @@ -3,21 +3,20 @@ /** * Dropdown to choose one item */ - class SelectOne_Item extends Item { - public $type = 'select'; - public $mysql_field = 'TEXT DEFAULT NULL'; - public $input_attributes = array('type' => 'select'); - protected $hasChoices = true; + public $type = 'select'; + public $mysql_field = 'TEXT DEFAULT NULL'; + public $input_attributes = array('type' => 'select'); + protected $hasChoices = true; - protected function setMoreOptions() { - $this->classes_input[] = "form-control"; - } + protected function setMoreOptions() { + $this->classes_input[] = "form-control"; + } - protected function render_input() { - $this->splitValues(); - $tpl = ' + protected function render_input() { + $this->splitValues(); + $tpl = ' '; - if ($this->value_validated) { - $this->presetValues[] = $this->value_validated; - } - - // Hack to split choices if comma separated and have only one element - // ASSUMPTION: choices are not suppose to have commas (weirdo) - $choice = current($this->choices); - if (count($this->choices) == 1 && strpos($choice, ',') !== false) { - $choices = explode(',', $choice); - $this->choices = array_combine($choices, $choices); - } - - $options = ''; - foreach ($this->choices as $value => $option) { - // determine whether options needs to be checked - $selected = ''; - if (in_array($value, $this->presetValues)) { - $selected = ' selected="selected"'; - } - $options .= sprintf('', $value, $selected, $option); - } + if ($this->value_validated) { + $this->presetValues[] = $this->value_validated; + } - return Template::replace($tpl, array( - 'id' => $this->id, - 'empty_option' => !isset($this->input_attributes['multiple']) ? '' : '', - 'options' => $options, - 'input_attributes' => self::_parseAttributes($this->input_attributes, array('id', 'type', 'required','multiple')), - 'select_attributes' => self::_parseAttributes($this->input_attributes, array('type')), - )); - } + // Hack to split choices if comma separated and have only one element + // ASSUMPTION: choices are not suppose to have commas (weirdo) + $choice = current($this->choices); + if (count($this->choices) == 1 && strpos($choice, ',') !== false) { + $choices = explode(',', $choice); + $this->choices = array_combine($choices, $choices); + } - protected function chooseResultFieldBasedOnChoices() { - if (count($this->choices) == count(array_filter($this->choices, 'is_numeric'))) { - return parent::chooseResultFieldBasedOnChoices(); - } - } + $options = ''; + foreach ($this->choices as $value => $option) { + // determine whether options needs to be checked + $selected = ''; + if (in_array($value, $this->presetValues)) { + $selected = ' selected="selected"'; + } + $options .= sprintf('', $value, $selected, $option); + } -} + return Template::replace($tpl, array( + 'id' => $this->id, + 'empty_option' => !isset($this->input_attributes['multiple']) ? '' : '', + 'options' => $options, + 'input_attributes' => self::_parseAttributes($this->input_attributes, array('id', 'type', 'required', 'multiple')), + 'select_attributes' => self::_parseAttributes($this->input_attributes, array('type')), + )); + } + protected function chooseResultFieldBasedOnChoices() { + if (count($this->choices) == count(array_filter($this->choices, 'is_numeric'))) { + return parent::chooseResultFieldBasedOnChoices(); + } + } +} diff --git a/application/Model/Item/SelectOrAddMultiple.php b/application/Model/Item/SelectOrAddMultiple.php index a20208c34..757c7ecd8 100644 --- a/application/Model/Item/SelectOrAddMultiple.php +++ b/application/Model/Item/SelectOrAddMultiple.php @@ -2,23 +2,25 @@ class SelectOrAddMultiple_Item extends SelectOrAddOne_Item { - public $type = 'select_or_add_multiple'; - public $mysql_field = 'TEXT DEFAULT NULL'; - public $input_attributes = array('type' => 'text'); + public $type = 'select_or_add_multiple'; + public $mysql_field = 'TEXT DEFAULT NULL'; + public $input_attributes = array('type' => 'text'); - protected function setMoreOptions() { - parent::setMoreOptions(); - $this->text_choices = true; - $this->input_attributes['data-select2multiple'] = 1; - } + protected function setMoreOptions() { + parent::setMoreOptions(); + $this->text_choices = true; + $this->input_attributes['data-select2multiple'] = 1; + } - public function getReply($reply) { - if (is_array($reply)) { - $reply = implode("\n", array_filter($reply)); - } - return $reply; - } + public function getReply($reply) { + if (is_array($reply)) { + $reply = implode("\n", array_filter($reply)); + } + return $reply; + } - protected function chooseResultFieldBasedOnChoices() {} + protected function chooseResultFieldBasedOnChoices() { + + } } diff --git a/application/Model/Item/SelectOrAddOne.php b/application/Model/Item/SelectOrAddOne.php index b92e06ca4..2caabfb31 100644 --- a/application/Model/Item/SelectOrAddOne.php +++ b/application/Model/Item/SelectOrAddOne.php @@ -2,58 +2,58 @@ class SelectOrAddOne_Item extends Item { - public $type = 'select_or_add_one'; - public $mysql_field = 'TEXT DEFAULT NULL'; - public $input_attributes = array('type' => 'text'); - protected $hasChoices = true; - private $maxSelect = 0; - private $maxType = 255; - - protected function setMoreOptions() { - parent::setMoreOptions(); - - if (isset($this->type_options) && trim($this->type_options) != "") { - $this->type_options_array = explode(",", $this->type_options, 3); - - $this->maxType = trim(reset($this->type_options_array)); - if (!is_numeric($this->maxType)) { - $this->maxType = 255; - } - - if (count($this->type_options_array) > 1) { - $this->maxSelect = trim(next($this->type_options_array)); - } - if (!isset($this->maxSelect) || !is_numeric($this->maxSelect)) { - $this->maxSelect = 0; - } - } - - $this->classes_input[] = 'select2add'; - $this->classes_input[] = 'form-control'; - } - - public function setChoices($choices) { - $this->choices = $choices; - // Hack to split choices if comma separated and have only one element - // ASSUMPTION: choices are not suppose to have commas (weirdo) - $choice = current($this->choices); - if (count($this->choices) == 1 && strpos($choice, ',') !== false) { - $this->choices = explode(',', $choice); - } - $for_select2 = array(); - - foreach ($this->choices AS $option) { - $for_select2[] = array('id' => $option, 'text' => $option); - } - - $this->input_attributes['data-select2add'] = json_encode($for_select2, JSON_UNESCAPED_UNICODE); - $this->input_attributes['data-select2maximum-selection-size'] = (int) $this->maxSelect; - $this->input_attributes['data-select2maximum-input-length'] = (int) $this->maxType; - } - - protected function chooseResultFieldBasedOnChoices() { - - } + public $type = 'select_or_add_one'; + public $mysql_field = 'TEXT DEFAULT NULL'; + public $input_attributes = array('type' => 'text'); + protected $hasChoices = true; + private $maxSelect = 0; + private $maxType = 255; + + protected function setMoreOptions() { + parent::setMoreOptions(); + + if (isset($this->type_options) && trim($this->type_options) != "") { + $this->type_options_array = explode(",", $this->type_options, 3); + + $this->maxType = trim(reset($this->type_options_array)); + if (!is_numeric($this->maxType)) { + $this->maxType = 255; + } + + if (count($this->type_options_array) > 1) { + $this->maxSelect = trim(next($this->type_options_array)); + } + if (!isset($this->maxSelect) || !is_numeric($this->maxSelect)) { + $this->maxSelect = 0; + } + } + + $this->classes_input[] = 'select2add'; + $this->classes_input[] = 'form-control'; + } + + public function setChoices($choices) { + $this->choices = $choices; + // Hack to split choices if comma separated and have only one element + // ASSUMPTION: choices are not suppose to have commas (weirdo) + $choice = current($this->choices); + if (count($this->choices) == 1 && strpos($choice, ',') !== false) { + $this->choices = explode(',', $choice); + } + $for_select2 = array(); + + foreach ($this->choices AS $option) { + $for_select2[] = array('id' => $option, 'text' => $option); + } + + $this->input_attributes['data-select2add'] = json_encode($for_select2, JSON_UNESCAPED_UNICODE); + $this->input_attributes['data-select2maximum-selection-size'] = (int) $this->maxSelect; + $this->input_attributes['data-select2maximum-input-length'] = (int) $this->maxType; + } + + protected function chooseResultFieldBasedOnChoices() { + + } // override parent } diff --git a/application/Model/Item/Server.php b/application/Model/Item/Server.php index c6c7bef47..b96359489 100644 --- a/application/Model/Item/Server.php +++ b/application/Model/Item/Server.php @@ -2,50 +2,50 @@ class Server_Item extends Item { - public $type = 'server'; - public $input_attributes = array('type' => 'hidden'); - public $mysql_field = 'TEXT DEFAULT NULL'; - public $no_user_input_required = true; - public $optional = 1; - private $get_var = 'HTTP_USER_AGENT'; - - protected function setMoreOptions() { - if (isset($this->type_options_array) && is_array($this->type_options_array)) { - if (count($this->type_options_array) == 1) { - $this->get_var = trim(current($this->type_options_array)); - } - } - $this->input_attributes['value'] = array_val($_SERVER, $this->get_var); - } - - public function getReply($reply) { - return $this->input_attributes['value']; - } - - public function validate() { - $vars = array( - 'HTTP_USER_AGENT', - 'HTTP_ACCEPT', - 'HTTP_ACCEPT_CHARSET', - 'HTTP_ACCEPT_ENCODING', - 'HTTP_ACCEPT_LANGUAGE', - 'HTTP_CONNECTION', - 'HTTP_HOST', - 'QUERY_STRING', - 'REQUEST_TIME', - 'REQUEST_TIME_FLOAT' - ); - - if (!in_array($this->get_var, $vars)) { - $this->val_errors[] = __('The server variable %s with the value %s cannot be saved', $this->name, $this->get_var); - return parent::validate(); - } - - return $this->val_errors; - } - - public function render() { - return $this->render_input(); - } + public $type = 'server'; + public $input_attributes = array('type' => 'hidden'); + public $mysql_field = 'TEXT DEFAULT NULL'; + public $no_user_input_required = true; + public $optional = 1; + private $get_var = 'HTTP_USER_AGENT'; + + protected function setMoreOptions() { + if (isset($this->type_options_array) && is_array($this->type_options_array)) { + if (count($this->type_options_array) == 1) { + $this->get_var = trim(current($this->type_options_array)); + } + } + $this->input_attributes['value'] = array_val($_SERVER, $this->get_var); + } + + public function getReply($reply) { + return $this->input_attributes['value']; + } + + public function validate() { + $vars = array( + 'HTTP_USER_AGENT', + 'HTTP_ACCEPT', + 'HTTP_ACCEPT_CHARSET', + 'HTTP_ACCEPT_ENCODING', + 'HTTP_ACCEPT_LANGUAGE', + 'HTTP_CONNECTION', + 'HTTP_HOST', + 'QUERY_STRING', + 'REQUEST_TIME', + 'REQUEST_TIME_FLOAT' + ); + + if (!in_array($this->get_var, $vars)) { + $this->val_errors[] = __('The server variable %s with the value %s cannot be saved', $this->name, $this->get_var); + return parent::validate(); + } + + return $this->val_errors; + } + + public function render() { + return $this->render_input(); + } } diff --git a/application/Model/Item/Sex.php b/application/Model/Item/Sex.php index 8d9ede225..72de07101 100644 --- a/application/Model/Item/Sex.php +++ b/application/Model/Item/Sex.php @@ -2,15 +2,15 @@ class Sex_Item extends McButton_Item { - public $mysql_field = 'TINYINT UNSIGNED DEFAULT NULL'; + public $mysql_field = 'TINYINT UNSIGNED DEFAULT NULL'; - protected function setMoreOptions() { - parent::setMoreOptions(); - $this->setChoices(array()); - } + protected function setMoreOptions() { + parent::setMoreOptions(); + $this->setChoices(array()); + } - public function setChoices($choices) { - $this->choices = array(1 => '♂', 2 => '♀'); - } + public function setChoices($choices) { + $this->choices = array(1 => '♂', 2 => '♀'); + } } diff --git a/application/Model/Item/Submit.php b/application/Model/Item/Submit.php index 6412884fb..cc2c15ae3 100644 --- a/application/Model/Item/Submit.php +++ b/application/Model/Item/Submit.php @@ -2,28 +2,28 @@ class Submit_Item extends Item { - public $type = 'submit'; - public $input_attributes = array('type' => 'submit', 'value' => 1); - public $mysql_field = null; - public $save_in_results_table = false; + public $type = 'submit'; + public $input_attributes = array('type' => 'submit', 'value' => 1); + public $mysql_field = null; + public $save_in_results_table = false; - protected function setMoreOptions() { - $this->classes_input[] = 'btn'; - $this->classes_input[] = 'btn-lg'; - $this->classes_input[] = 'btn-info'; - if ($this->type_options !== NULL && is_numeric($this->type_options)) { - $this->input_attributes["data-timeout"] = $this->type_options; - $this->classes_input[] = "submit_automatically_after_timeout"; - } - $this->input_attributes['value'] = $this->label_parsed; - } + protected function setMoreOptions() { + $this->classes_input[] = 'btn'; + $this->classes_input[] = 'btn-lg'; + $this->classes_input[] = 'btn-info'; + if ($this->type_options !== NULL && is_numeric($this->type_options)) { + $this->input_attributes["data-timeout"] = $this->type_options; + $this->classes_input[] = "submit_automatically_after_timeout"; + } + $this->input_attributes['value'] = $this->label_parsed; + } - protected function render_inner() { - return Template::replace(' %{time_out}', array( - 'label' => $this->label_parsed, - 'input_attributes' => self::_parseAttributes($this->input_attributes, array('required')), - 'time_out' => isset($this->input_attributes["data-timeout"]) ? '
' : '', - )); - } + protected function render_inner() { + return Template::replace(' %{time_out}', array( + 'label' => $this->label_parsed, + 'input_attributes' => self::_parseAttributes($this->input_attributes, array('required')), + 'time_out' => isset($this->input_attributes["data-timeout"]) ? '
' : '', + )); + } } diff --git a/application/Model/Item/Tel.php b/application/Model/Item/Tel.php index 6032dfea3..5c7a1a2d9 100644 --- a/application/Model/Item/Tel.php +++ b/application/Model/Item/Tel.php @@ -2,14 +2,13 @@ class Tel_Item extends Text_Item { - public $type = 'tel'; - public $input_attributes = array('type' => 'tel'); - public $mysql_field = 'VARCHAR(100) DEFAULT NULL'; - - protected $prepend = 'fa-phone'; - - protected function setMoreOptions() { - $this->classes_input[] = 'form-control'; - } + public $type = 'tel'; + public $input_attributes = array('type' => 'tel'); + public $mysql_field = 'VARCHAR(100) DEFAULT NULL'; + protected $prepend = 'fa-phone'; + + protected function setMoreOptions() { + $this->classes_input[] = 'form-control'; + } } diff --git a/application/Model/Item/Text.php b/application/Model/Item/Text.php index 2f24d5315..943bd4e6f 100644 --- a/application/Model/Item/Text.php +++ b/application/Model/Item/Text.php @@ -2,27 +2,27 @@ class Text_Item extends Item { - public $type = 'text'; - public $input_attributes = array('type' => 'text'); - public $mysql_field = 'TEXT DEFAULT NULL'; + public $type = 'text'; + public $input_attributes = array('type' => 'text'); + public $mysql_field = 'TEXT DEFAULT NULL'; - protected function setMoreOptions() { - if (is_array($this->type_options_array) && count($this->type_options_array) == 1) { - $val = trim(current($this->type_options_array)); - if (is_numeric($val)) { - $this->input_attributes['maxlength'] = (int) $val; - } else if (trim(current($this->type_options_array))) { - $this->input_attributes['pattern'] = trim(current($this->type_options_array)); - } - } - $this->classes_input[] = 'form-control'; - } + protected function setMoreOptions() { + if (is_array($this->type_options_array) && count($this->type_options_array) == 1) { + $val = trim(current($this->type_options_array)); + if (is_numeric($val)) { + $this->input_attributes['maxlength'] = (int) $val; + } else if (trim(current($this->type_options_array))) { + $this->input_attributes['pattern'] = trim(current($this->type_options_array)); + } + } + $this->classes_input[] = 'form-control'; + } - public function validateInput($reply) { - if (isset($this->input_attributes['maxlength']) && $this->input_attributes['maxlength'] > 0 && strlen($reply) > $this->input_attributes['maxlength']) { // verify maximum length - $this->error = __("You can't use that many characters. The maximum is %d", $this->input_attributes['maxlength']); - } - return parent::validateInput($reply); - } + public function validateInput($reply) { + if (isset($this->input_attributes['maxlength']) && $this->input_attributes['maxlength'] > 0 && strlen($reply) > $this->input_attributes['maxlength']) { // verify maximum length + $this->error = __("You can't use that many characters. The maximum is %d", $this->input_attributes['maxlength']); + } + return parent::validateInput($reply); + } } diff --git a/application/Model/Item/Textarea.php b/application/Model/Item/Textarea.php index e7a058f67..ea11acf7e 100644 --- a/application/Model/Item/Textarea.php +++ b/application/Model/Item/Textarea.php @@ -2,21 +2,21 @@ class Textarea_Item extends Item { - public $type = 'textarea'; - public $mysql_field = 'TEXT DEFAULT NULL'; // change to mediumtext to get 64KiB to 16MiB + public $type = 'textarea'; + public $mysql_field = 'TEXT DEFAULT NULL'; // change to mediumtext to get 64KiB to 16MiB - protected function setMoreOptions() { - $this->classes_input[] = 'form-control'; - } + protected function setMoreOptions() { + $this->classes_input[] = 'form-control'; + } - protected function render_input() { - if ($this->value_validated) { - $this->input_attributes['value'] = $this->value_validated; - } + protected function render_input() { + if ($this->value_validated) { + $this->input_attributes['value'] = $this->value_validated; + } - $value = array_val($this->input_attributes, 'value'); - unset($this->input_attributes['value']); - return sprintf('', self::_parseAttributes($this->input_attributes, array('type')), $value); - } + $value = array_val($this->input_attributes, 'value'); + unset($this->input_attributes['value']); + return sprintf('', self::_parseAttributes($this->input_attributes, array('type')), $value); + } } diff --git a/application/Model/Item/Time.php b/application/Model/Item/Time.php index fca666451..92c084261 100644 --- a/application/Model/Item/Time.php +++ b/application/Model/Item/Time.php @@ -3,11 +3,10 @@ // time is polyfilled, we prepended a clock class Time_Item extends Datetime_Item { - public $type = 'time'; - public $input_attributes = array('type' => 'time', 'style' => 'width:160px'); - public $mysql_field = 'TIME DEFAULT NULL'; - - //protected $prepend = 'fa-clock-o'; - protected $html5_date_format = 'H:i'; + public $type = 'time'; + public $input_attributes = array('type' => 'time', 'style' => 'width:160px'); + public $mysql_field = 'TIME DEFAULT NULL'; + //protected $prepend = 'fa-clock-o'; + protected $html5_date_format = 'H:i'; } diff --git a/application/Model/Item/Timezone.php b/application/Model/Item/Timezone.php index 87861b653..4378d8729 100644 --- a/application/Model/Item/Timezone.php +++ b/application/Model/Item/Timezone.php @@ -2,53 +2,53 @@ class Timezone_Item extends SelectOne_Item { - public $mysql_field = 'VARCHAR(255)'; - public $choice_list = '*'; - - protected function chooseResultFieldBasedOnChoices() { - - } - - protected function setMoreOptions() { - $this->classes_input[] = 'select2zone'; - - parent::setMoreOptions(); - $this->setChoices(array()); - } - - public function setChoices($choices) { - $zonenames = timezone_identifiers_list(); - asort($zonenames); - $zones = array(); - $offsets = array(); - foreach ($zonenames AS $zonename): - $zone = timezone_open($zonename); - $offsets[] = timezone_offset_get($zone, date_create()); - $zones[] = str_replace("/", " - ", str_replace("_", " ", $zonename)); - endforeach; - $this->choices = $zones; - $this->offsets = $offsets; - } - - protected function render_input() { - $tpl = ' + public $mysql_field = 'VARCHAR(255)'; + public $choice_list = '*'; + + protected function chooseResultFieldBasedOnChoices() { + + } + + protected function setMoreOptions() { + $this->classes_input[] = 'select2zone'; + + parent::setMoreOptions(); + $this->setChoices(array()); + } + + public function setChoices($choices) { + $zonenames = timezone_identifiers_list(); + asort($zonenames); + $zones = array(); + $offsets = array(); + foreach ($zonenames AS $zonename): + $zone = timezone_open($zonename); + $offsets[] = timezone_offset_get($zone, date_create()); + $zones[] = str_replace("/", " - ", str_replace("_", " ", $zonename)); + endforeach; + $this->choices = $zones; + $this->offsets = $offsets; + } + + protected function render_input() { + $tpl = ' '; - $options = ''; - foreach ($this->choices as $value => $option) { - $selected = array('selected' => $this->isSelectedOptionValue($value, $this->value_validated)); - $options .= sprintf('', $value, self::_parseAttributes($selected, array('type')), $option); - } - - return Template::replace($tpl, array( - 'empty_option' => !isset($this->input_attributes['multiple']) ? '' : '', - 'options' => $options, - 'select_attributes' => self::_parseAttributes($this->input_attributes, array('type')), - )); - } + $options = ''; + foreach ($this->choices as $value => $option) { + $selected = array('selected' => $this->isSelectedOptionValue($value, $this->value_validated)); + $options .= sprintf('', $value, self::_parseAttributes($selected, array('type')), $option); + } + + return Template::replace($tpl, array( + 'empty_option' => !isset($this->input_attributes['multiple']) ? '' : '', + 'options' => $options, + 'select_attributes' => self::_parseAttributes($this->input_attributes, array('type')), + )); + } } diff --git a/application/Model/Item/Url.php b/application/Model/Item/Url.php index 7c8059364..8f4ec6806 100644 --- a/application/Model/Item/Url.php +++ b/application/Model/Item/Url.php @@ -2,26 +2,25 @@ class Url_Item extends Text_Item { - public $type = 'url'; - public $input_attributes = array('type' => 'url'); - public $mysql_field = 'VARCHAR(255) DEFAULT NULL'; + public $type = 'url'; + public $input_attributes = array('type' => 'url'); + public $mysql_field = 'VARCHAR(255) DEFAULT NULL'; + protected $prepend = 'fa-link'; - protected $prepend = 'fa-link'; + public function validateInput($reply) { + if ($this->optional && trim($reply) == ''): + return parent::validateInput($reply); + else: + $reply_valid = filter_var($reply, FILTER_VALIDATE_URL); + if (!$reply_valid): + $this->error = __('The URL %s is not valid', h($reply)); + endif; + endif; + return $reply_valid; + } - public function validateInput($reply) { - if ($this->optional && trim($reply) == ''): - return parent::validateInput($reply); - else: - $reply_valid = filter_var($reply, FILTER_VALIDATE_URL); - if (!$reply_valid): - $this->error = __('The URL %s is not valid', h($reply)); - endif; - endif; - return $reply_valid; - } - - protected function setMoreOptions() { - $this->classes_input[] = 'form-control'; - } + protected function setMoreOptions() { + $this->classes_input[] = 'form-control'; + } } diff --git a/application/Model/Item/Week.php b/application/Model/Item/Week.php index 0e9bf9fbd..56a8bf431 100644 --- a/application/Model/Item/Week.php +++ b/application/Model/Item/Week.php @@ -2,13 +2,10 @@ class Week_Item extends Datetime_Item { - public $type = 'week'; - public $input_attributes = array('type' => 'week'); - public $mysql_field = 'VARCHAR(9) DEFAULT NULL'; - - protected $html5_date_format = 'Y-mW'; - //protected $prepend = 'fa-calendar-o'; + public $type = 'week'; + public $input_attributes = array('type' => 'week'); + public $mysql_field = 'VARCHAR(9) DEFAULT NULL'; + protected $html5_date_format = 'Y-mW'; + //protected $prepend = 'fa-calendar-o'; } - - diff --git a/application/Model/Item/Year.php b/application/Model/Item/Year.php index 83cb42f52..0befa5ffc 100644 --- a/application/Model/Item/Year.php +++ b/application/Model/Item/Year.php @@ -2,11 +2,10 @@ class Year_Item extends Datetime_Item { - public $type = 'year'; - public $input_attributes = array('type' => 'year'); - public $mysql_field = 'YEAR DEFAULT NULL'; - - protected $html5_date_format = 'Y'; - protected $prepend = 'fa-calendar-o'; + public $type = 'year'; + public $input_attributes = array('type' => 'year'); + public $mysql_field = 'YEAR DEFAULT NULL'; + protected $html5_date_format = 'Y'; + protected $prepend = 'fa-calendar-o'; } diff --git a/application/Model/Item/Yearmonth.php b/application/Model/Item/Yearmonth.php index 93ddf902a..7bf2053a0 100644 --- a/application/Model/Item/Yearmonth.php +++ b/application/Model/Item/Yearmonth.php @@ -2,22 +2,20 @@ class Yearmonth_Item extends Datetime_Item { - public $type = 'yearmonth'; - public $input_attributes = array('type' => 'yearmonth'); - public $mysql_field = 'DATE DEFAULT NULL'; - - //protected $prepend = 'fa-calendar-o'; - protected $html5_date_format = 'Y-m-01'; + public $type = 'yearmonth'; + public $input_attributes = array('type' => 'yearmonth'); + public $mysql_field = 'DATE DEFAULT NULL'; + //protected $prepend = 'fa-calendar-o'; + protected $html5_date_format = 'Y-m-01'; - protected function render_input() { - if ($this->value_validated !== null) { - $parts = explode('-', $this->value_validated); - array_pop($parts); - $this->input_attributes['value'] = implode('-', $parts); - } + protected function render_input() { + if ($this->value_validated !== null) { + $parts = explode('-', $this->value_validated); + array_pop($parts); + $this->input_attributes['value'] = implode('-', $parts); + } - return sprintf('', self::_parseAttributes($this->input_attributes)); - } + return sprintf('', self::_parseAttributes($this->input_attributes)); + } } - diff --git a/application/Model/Page.php b/application/Model/Page.php index 6f2a6e681..2dbd39935 100644 --- a/application/Model/Page.php +++ b/application/Model/Page.php @@ -2,117 +2,117 @@ class Page extends RunUnit { - public $errors = array(); - public $id = null; - public $session = null; - public $unit = null; - protected $body = ''; - protected $body_parsed = ''; - public $title = ''; - private $can_be_ended = 0; - public $ended = false; - public $type = 'Endpage'; - public $icon = "fa-stop"; - - /** - * An array of unit's exportable attributes - * @var array - */ - public $export_attribs = array('type', 'description', 'position', 'special', 'body'); - - public function __construct($fdb, $session = null, $unit = null, $run_session = NULL, $run = NULL) { - parent::__construct($fdb, $session, $unit, $run_session, $run); - - if ($this->id): - $vars = $this->dbh->findRow('survey_pages', array('id' => $this->id), 'title, body, body_parsed'); - if ($vars): - $this->body = $vars['body']; - $this->body_parsed = $vars['body_parsed']; - $this->title = $vars['title']; + public $errors = array(); + public $id = null; + public $session = null; + public $unit = null; + protected $body = ''; + protected $body_parsed = ''; + public $title = ''; + private $can_be_ended = 0; + public $ended = false; + public $type = 'Endpage'; + public $icon = "fa-stop"; + + /** + * An array of unit's exportable attributes + * @var array + */ + public $export_attribs = array('type', 'description', 'position', 'special', 'body'); + + public function __construct($fdb, $session = null, $unit = null, $run_session = NULL, $run = NULL) { + parent::__construct($fdb, $session, $unit, $run_session, $run); + + if ($this->id): + $vars = $this->dbh->findRow('survey_pages', array('id' => $this->id), 'title, body, body_parsed'); + if ($vars): + $this->body = $vars['body']; + $this->body_parsed = $vars['body_parsed']; + $this->title = $vars['title']; # $this->can_be_ended = $vars['end'] ? 1:0; - $this->can_be_ended = 0; - - $this->valid = true; - endif; - endif; - - if (!empty($_POST) AND isset($_POST['page_submit'])) { - unset($_POST['page_submit']); - $this->end(); - } - } - - public function create($options) { - if (!$this->id) { - $this->id = parent::create('Page'); - } else { - $this->modify($options); - } - - if (isset($options['body'])) { - $this->body = $options['body']; + $this->can_be_ended = 0; + + $this->valid = true; + endif; + endif; + + if (!empty($_POST) AND isset($_POST['page_submit'])) { + unset($_POST['page_submit']); + $this->end(); + } + } + + public function create($options) { + if (!$this->id) { + $this->id = parent::create('Page'); + } else { + $this->modify($options); + } + + if (isset($options['body'])) { + $this->body = $options['body']; // $this->title = $options['title']; // $this->can_be_ended = $options['end'] ? 1:0; - $this->can_be_ended = 0; - } - - $parsedown = new ParsedownExtra(); - $this->body_parsed = $parsedown - ->setBreaksEnabled(true) - ->text($this->body); // transform upon insertion into db instead of at runtime - - $this->dbh->insert_update('survey_pages', array( - 'id' => $this->id, - 'body' => $this->body, - 'body_parsed' => $this->body_parsed, - 'title' => $this->title, - 'end' => (int) $this->can_be_ended, - )); - $this->valid = true; - - return true; - } - - public function displayForRun($prepend = '') { - $dialog = Template::get($this->getUnitTemplatePath(), array( - 'prepend' => $prepend, - 'body' => $this->body, - )); - - return parent::runDialog($dialog); - } - - public function removeFromRun($special = null) { - return $this->delete($special); - } - - public function test() { - echo $this->getParsedBodyAdmin($this->body); - } - - public function exec() { - if ($this->called_by_cron) { - $this->getParsedBody($this->body); // make report before showing it to the user, so they don't have to wait - return true; // never show to the cronjob - } - - $run_name = $sess_code = null; - if ($this->run_session) { - $run_name = $this->run_session->run_name; - $sess_code = $this->run_session->session; - $this->run_session->end(); - } - - $this->body_parsed = $this->getParsedBody($this->body); - if ($this->body_parsed === false) { - return true; // wait for openCPU to be fixed! - } - - $body = do_run_shortcodes($this->body_parsed, $run_name, $sess_code); - - return array( - 'body' => $body, - ); - } + $this->can_be_ended = 0; + } + + $parsedown = new ParsedownExtra(); + $this->body_parsed = $parsedown + ->setBreaksEnabled(true) + ->text($this->body); // transform upon insertion into db instead of at runtime + + $this->dbh->insert_update('survey_pages', array( + 'id' => $this->id, + 'body' => $this->body, + 'body_parsed' => $this->body_parsed, + 'title' => $this->title, + 'end' => (int) $this->can_be_ended, + )); + $this->valid = true; + + return true; + } + + public function displayForRun($prepend = '') { + $dialog = Template::get($this->getUnitTemplatePath(), array( + 'prepend' => $prepend, + 'body' => $this->body, + )); + + return parent::runDialog($dialog); + } + + public function removeFromRun($special = null) { + return $this->delete($special); + } + + public function test() { + echo $this->getParsedBodyAdmin($this->body); + } + + public function exec() { + if ($this->called_by_cron) { + $this->getParsedBody($this->body); // make report before showing it to the user, so they don't have to wait + return true; // never show to the cronjob + } + + $run_name = $sess_code = null; + if ($this->run_session) { + $run_name = $this->run_session->run_name; + $sess_code = $this->run_session->session; + $this->run_session->end(); + } + + $this->body_parsed = $this->getParsedBody($this->body); + if ($this->body_parsed === false) { + return true; // wait for openCPU to be fixed! + } + + $body = do_run_shortcodes($this->body_parsed, $run_name, $sess_code); + + return array( + 'body' => $body, + ); + } } diff --git a/application/Model/Pagination.php b/application/Model/Pagination.php index 16d84aad7..554ace5e0 100644 --- a/application/Model/Pagination.php +++ b/application/Model/Pagination.php @@ -2,107 +2,105 @@ class Pagination { - public $maximum = 0; - - private $maximum_page; - private $start = 0; - private $per_page = 100; - private $page = 0; - private $enable_show_all = false; - - public function __construct($maximum, $per_page = 100, $enable_show_all = false) { - if (is_numeric($per_page)) { - $this->per_page = (int) $per_page; - } - $this->enable_show_all = $enable_show_all; - - $this->setMaximum($maximum); - } - - public function getLimits() { - if (isset($_GET['page']) and is_numeric($_GET['page'])): - $this->page = (int) $_GET['page'] - 1; - if ($this->page > $this->maximum_page): - $this->page = $this->maximum_page; - elseif ($this->page === -1 AND $this->enable_show_all): - return "0," . $this->maximum; - elseif ($this->page < 0 AND ! $this->enable_show_all): - $this->page = 0; - endif; - $this->start = $this->page * $this->per_page; - endif; - - return $this->start . "," . $this->per_page; - } - - public function setPage($page) { - if (is_numeric($page)) { - $this->page = (int) $page; - } - - if ($this->page < 0) { - throw new Exception("Pagination page number must be positive."); - } - } - - - private function setMaximum($maximum) { - $maximum = (int)$maximum; - if (!$maximum) { - $this->maximum = 0; - $this->maximum_page = 0; - } - - $this->maximum = (int)$maximum; - $this->maximum_page = (int) ceil(($this->maximum - $this->per_page) / $this->per_page); // get the last page that can display this many x, divide it by x, - if ($this->maximum_page < 0): - $this->maximum_page = 0; // but if it's negative make it 0 - endif; - } - - public function render($url) { - if ($this->maximum_page === 0) { - return ''; - } - - $params = $_GET; - unset($params['route']); - - $pages = range(0, $this->maximum_page); // all pages - if ($this->maximum_page > 15) { - $pagination_class = 'pagination pagination-sm'; - } elseif ($this->maximum_page < 5) { - $pagination_class = 'pagination pagination-lg'; - } else { - $pagination_class = 'pagination'; - } - - $html = ' + public $maximum = 0; + private $maximum_page; + private $start = 0; + private $per_page = 100; + private $page = 0; + private $enable_show_all = false; + + public function __construct($maximum, $per_page = 100, $enable_show_all = false) { + if (is_numeric($per_page)) { + $this->per_page = (int) $per_page; + } + $this->enable_show_all = $enable_show_all; + + $this->setMaximum($maximum); + } + + public function getLimits() { + if (isset($_GET['page']) and is_numeric($_GET['page'])): + $this->page = (int) $_GET['page'] - 1; + if ($this->page > $this->maximum_page): + $this->page = $this->maximum_page; + elseif ($this->page === -1 AND $this->enable_show_all): + return "0," . $this->maximum; + elseif ($this->page < 0 AND ! $this->enable_show_all): + $this->page = 0; + endif; + $this->start = $this->page * $this->per_page; + endif; + + return $this->start . "," . $this->per_page; + } + + public function setPage($page) { + if (is_numeric($page)) { + $this->page = (int) $page; + } + + if ($this->page < 0) { + throw new Exception("Pagination page number must be positive."); + } + } + + private function setMaximum($maximum) { + $maximum = (int) $maximum; + if (!$maximum) { + $this->maximum = 0; + $this->maximum_page = 0; + } + + $this->maximum = (int) $maximum; + $this->maximum_page = (int) ceil(($this->maximum - $this->per_page) / $this->per_page); // get the last page that can display this many x, divide it by x, + if ($this->maximum_page < 0): + $this->maximum_page = 0; // but if it's negative make it 0 + endif; + } + + public function render($url) { + if ($this->maximum_page === 0) { + return ''; + } + + $params = $_GET; + unset($params['route']); + + $pages = range(0, $this->maximum_page); // all pages + if ($this->maximum_page > 15) { + $pagination_class = 'pagination pagination-sm'; + } elseif ($this->maximum_page < 5) { + $pagination_class = 'pagination pagination-lg'; + } else { + $pagination_class = 'pagination'; + } + + $html = '
    %{pages}
'; - $ps = ''; - - foreach ($pages as $page) { - $active = $page == $this->page ? ' class="active"' : null; - $page++; - $params['page'] = $page; - $href = site_url($url, $params); - $ps .= "
  • {$page}
  • "; - } - - if ($this->enable_show_all) { - $active = $this->page === -1 ? ' class="active"' : null; - $params['page'] = 0; - $href = site_url($url, $params); - $ps .= "
  • Show all
  • "; - } - - echo Template::replace($html, array( - 'pagination_class' => $pagination_class, - 'pages' => $ps, - )); - } + $ps = ''; + + foreach ($pages as $page) { + $active = $page == $this->page ? ' class="active"' : null; + $page++; + $params['page'] = $page; + $href = site_url($url, $params); + $ps .= "
  • {$page}
  • "; + } + + if ($this->enable_show_all) { + $active = $this->page === -1 ? ' class="active"' : null; + $params['page'] = 0; + $href = site_url($url, $params); + $ps .= "
  • Show all
  • "; + } + + echo Template::replace($html, array( + 'pagination_class' => $pagination_class, + 'pages' => $ps, + )); + } } diff --git a/application/Model/Pause.php b/application/Model/Pause.php index c6261fef2..fc88c3279 100644 --- a/application/Model/Pause.php +++ b/application/Model/Pause.php @@ -2,299 +2,296 @@ class Pause extends RunUnit { - public $errors = array(); - public $id = null; - public $session = null; - public $unit = null; - public $ended = false; - public $type = "Pause"; - public $icon = "fa-pause"; - - protected $body = ''; - protected $body_parsed = ''; - protected $relative_to = null; - protected $wait_minutes = null; - protected $wait_until_time = null; - protected $wait_until_date = null; - - - private $has_relative_to = false; - private $has_wait_minutes = false; - private $relative_to_result = null; - - /** - * An array of unit's exportable attributes - * @var array - */ - public $export_attribs = array('type', 'description', 'position', 'special', 'wait_until_time', 'wait_until_date', 'wait_minutes', 'relative_to', 'body'); - - public function __construct($fdb, $session = null, $unit = null, $run_session = NULL, $run = NULL) { - parent::__construct($fdb, $session, $unit, $run_session, $run); - - if ($this->id): - $vars = $this->dbh->select('id, body, body_parsed, wait_until_time, wait_minutes, wait_until_date, relative_to') - ->from('survey_pauses') - ->where(array('id' => $this->id)) - ->limit(1)->fetch(); - - if ($vars): - array_walk($vars, "emptyNull"); - $this->body = $vars['body']; - $this->body_parsed = $vars['body_parsed']; - $this->wait_until_time = $vars['wait_until_time']; - $this->wait_until_date = $vars['wait_until_date']; - $this->wait_minutes = $vars['wait_minutes']; - $this->relative_to = $vars['relative_to']; - - $this->valid = true; - endif; - endif; - } - - public function create($options) { - $this->dbh->beginTransaction(); - if (!$this->id) { - $this->id = parent::create($this->type); - } else { - $this->modify($options); - } - - if (isset($options['body'])) { - array_walk($options, "emptyNull"); - $this->body = $options['body']; - $this->wait_until_time = $options['wait_until_time']; - $this->wait_until_date = $options['wait_until_date']; - $this->wait_minutes = $options['wait_minutes']; - $this->relative_to = $options['relative_to']; - } - - $parsedown = new ParsedownExtra(); - $parsedown->setBreaksEnabled(true); - - if (!$this->knittingNeeded($this->body)) { - $this->body_parsed = $parsedown->text($this->body); // transform upon insertion into db instead of at runtime - } - - $this->dbh->insert_update('survey_pauses', array( - 'id' => $this->id, - 'body' => $this->body, - 'body_parsed' => $this->body_parsed, - 'wait_until_time' => $this->wait_until_time, - 'wait_until_date' => $this->wait_until_date, - 'wait_minutes' => $this->wait_minutes, - 'relative_to' => $this->relative_to, - )); - $this->dbh->commit(); - $this->valid = true; - - return true; - } - - public function displayForRun($prepend = '') { - $dialog = Template::get($this->getUnitTemplatePath(), array( - 'prepend' => $prepend, - 'wait_until_time' => $this->wait_until_time, - 'wait_until_date' => $this->wait_until_date, - 'wait_minutes' => $this->wait_minutes, - 'relative_to' => $this->relative_to, - 'body' => $this->body, - )); - - return parent::runDialog($dialog); - } - - public function removeFromRun($special = null) { - return $this->delete($special); - } - - protected function checkRelativeTo() { - $this->relative_to = trim($this->relative_to); - $this->wait_minutes = trim($this->wait_minutes); - $this->has_wait_minutes = !($this->wait_minutes === null || $this->wait_minutes == ''); - $this->has_relative_to = !($this->relative_to === null || $this->relative_to == ''); - - // disambiguate what user meant - if ($this->has_wait_minutes && !$this->has_relative_to) { - // If user specified waiting minutes but did not specify relative to which timestamp, - // we imply we are waiting relative to when the user arrived at the pause - $this->relative_to = 'tail(survey_unit_sessions$created,1)'; - $this->has_relative_to = true; - } - - return $this->has_relative_to; - } - - protected function checkWhetherPauseIsOver() { - $this->execData['check_failed'] = false; - $this->execData['expire_relatively'] = null; - - // if a relative_to has been defined by user or automatically, we need to retrieve its value - if ($this->has_relative_to) { - $opencpu_vars = $this->getUserDataInRun($this->relative_to); - $result = opencpu_evaluate($this->relative_to, $opencpu_vars, 'json'); - - if ($result === null) { - $this->execData['check_failed'] = true; - return false; - } - $this->relative_to_result = $relative_to = $result; - } - - $bind_relative_to = false; - $conditions = array(); - - if (!$this->has_wait_minutes && $this->has_relative_to) { - // if no wait minutes but a relative to was defined, we just use this as the param (useful for complex R expressions) - if ($relative_to === true) { - $conditions['relative_to'] = '1=1'; - $this->execData['expire_relatively'] = true; - } elseif ($relative_to === false) { - $conditions['relative_to'] = '0=1'; - $this->execData['expire_relatively'] = false; - } elseif (!is_array($relative_to) && strtotime($relative_to)) { - $conditions['relative_to'] = ':relative_to <= NOW()'; - $bind_relative_to = true; - $this->execData['expire_timestamp'] = strtotime($relative_to); - // If there was a wait_time, set the timestamp to have this time - if ($time = $this->parseWaitTime(true)) { - $ts = $this->execData['expire_timestamp']; - $this->execData['expire_timestamp'] = mktime($time[0], $time[1], 0, date('m', $ts), date('d', $ts), date('Y', $ts)); - } - } else { - alert("Pause {$this->position}: Relative to yields neither true nor false, nor a date, nor a time. " . print_r($relative_to, true), 'alert-warning'); - $this->execData['check_failed'] = true; - return false; - } - } elseif ($this->has_wait_minutes) { - if (!is_array($relative_to) && strtotime($relative_to)) { - $conditions['minute'] = "DATE_ADD(:relative_to, INTERVAL :wait_minutes MINUTE) <= NOW()"; - $bind_relative_to = true; - $this->execData['expire_timestamp'] = strtotime($relative_to) + ($this->wait_minutes * 60); - } else { - alert("Pause {$this->position}: Relative to yields neither a date, nor a time. " . print_r($relative_to, true), 'alert-warning'); - $this->execData['check_failed'] = true; - return false; - } - } - - if ($this->wait_until_date && $this->wait_until_date != '0000-00-00') { - $wait_date = $this->wait_until_date; - } - - if ($this->wait_until_time && $this->wait_until_time != '00:00:00') { - $wait_time = $this->wait_until_time; - } - - $wait_date = $this->parseWaitDate(); - $wait_time = $this->parseWaitTime(); - - if (!empty($wait_date) && empty($wait_time)) { - $wait_time = '00:00:01'; - } - - if (!empty($wait_time) && empty($wait_date)) { - $wait_date = date('Y-m-d'); - } - - if (!empty($wait_date) && !empty($wait_time) && empty($this->execData['expire_timestamp'])) { - $wait_datetime = $wait_date . ' ' . $wait_time; - $this->execData['expire_timestamp'] = strtotime($wait_datetime); - - // If the expiration hour already passed before the user entered the pause, set expiration to the next day (in 24 hours) - $exp_ts = $this->execData['expire_timestamp']; - $created_ts = strtotime($this->run_session->unit_session->created); - $exp_hour_min = mktime(date('G', $exp_ts), date('i', $exp_ts), 0); - if ($created_ts > $exp_hour_min) { - $this->execData['expire_timestamp'] += 24 * 60 * 60; - return false; - } -/* - // Check if this unit already expired today for current run_session_id - $q = ' - SELECT 1 AS finished FROM `survey_unit_sessions` - WHERE `survey_unit_sessions`.unit_id = :id AND `survey_unit_sessions`.run_session_id = :run_session_id AND DATE(`survey_unit_sessions`.ended) = CURDATE() - LIMIT 1 - '; - $stmt = $this->dbh->prepare($q); - $stmt->bindValue(':id', $this->id); - $stmt->bindValue(':run_session_id', $this->run_session_id); - $stmt->execute(); - if ($stmt->rowCount() > 0) { - $this->execData['expire_timestamp'] = strtotime('+1 day', $this->execData['expire_timestamp']); - return false; - } -*/ - - $conditions['datetime'] = ':wait_datetime <= NOW()'; - } - - $result = !empty($this->execData['expire_timestamp']) && $this->execData['expire_timestamp'] <= time(); - - if ($conditions) { - $condition = implode(' AND ', $conditions); - $stmt = $this->dbh->prepare("SELECT {$condition} AS test LIMIT 1"); - if ($bind_relative_to) { - $stmt->bindValue(':relative_to', $relative_to); - } - if (isset($conditions['minute'])) { - $stmt->bindValue(':wait_minutes', $this->wait_minutes); - } - if (isset($conditions['datetime'])) { - $stmt->bindValue(':wait_datetime', $wait_datetime); - } - - $stmt->execute(); - if ($stmt->rowCount() === 1 && ($row = $stmt->fetch(PDO::FETCH_ASSOC))) { - $result = (bool)$row['test']; - } - } else { - $result = true; - } - - $this->execData['pause_over'] = $result; - return $result; - } - - protected function parseWaitTime($parts = false) { - if ($this->wait_until_time && $this->wait_until_time != '00:00:00') { - return $parts ? explode(':', $this->wait_until_time) : $this->wait_until_time; - } - - return null; - } - - protected function parseWaitDate($parts = false) { - if ($this->wait_until_date && $this->wait_until_date != '0000-00-00') { - return $parts ? explode('-', $this->wait_until_date) : $this->wait_until_date; - } - - return null; - } - - public function test() { - $results = $this->getSampleSessions(); - if (!$results) { - return false; - } - - // take the first sample session - $sess = current($results); - $this->run_session_id = $sess['id']; - - - echo "

    Pause message

    "; - echo $this->getParsedBodyAdmin($this->body); - - if ($this->checkRelativeTo()) { - echo "

    Pause relative to

    "; - $opencpu_vars = $this->getUserDataInRun($this->relative_to); - $session = opencpu_evaluate($this->relative_to, $opencpu_vars, 'json', null, true); - echo opencpu_debug($session); - } - - if (!empty($results) && (empty($session) || !$session->hasError())) { - - $test_tpl = ' + public $errors = array(); + public $id = null; + public $session = null; + public $unit = null; + public $ended = false; + public $type = "Pause"; + public $icon = "fa-pause"; + protected $body = ''; + protected $body_parsed = ''; + protected $relative_to = null; + protected $wait_minutes = null; + protected $wait_until_time = null; + protected $wait_until_date = null; + private $has_relative_to = false; + private $has_wait_minutes = false; + private $relative_to_result = null; + + /** + * An array of unit's exportable attributes + * @var array + */ + public $export_attribs = array('type', 'description', 'position', 'special', 'wait_until_time', 'wait_until_date', 'wait_minutes', 'relative_to', 'body'); + + public function __construct($fdb, $session = null, $unit = null, $run_session = NULL, $run = NULL) { + parent::__construct($fdb, $session, $unit, $run_session, $run); + + if ($this->id): + $vars = $this->dbh->select('id, body, body_parsed, wait_until_time, wait_minutes, wait_until_date, relative_to') + ->from('survey_pauses') + ->where(array('id' => $this->id)) + ->limit(1)->fetch(); + + if ($vars): + array_walk($vars, "emptyNull"); + $this->body = $vars['body']; + $this->body_parsed = $vars['body_parsed']; + $this->wait_until_time = $vars['wait_until_time']; + $this->wait_until_date = $vars['wait_until_date']; + $this->wait_minutes = $vars['wait_minutes']; + $this->relative_to = $vars['relative_to']; + + $this->valid = true; + endif; + endif; + } + + public function create($options) { + $this->dbh->beginTransaction(); + if (!$this->id) { + $this->id = parent::create($this->type); + } else { + $this->modify($options); + } + + if (isset($options['body'])) { + array_walk($options, "emptyNull"); + $this->body = $options['body']; + $this->wait_until_time = $options['wait_until_time']; + $this->wait_until_date = $options['wait_until_date']; + $this->wait_minutes = $options['wait_minutes']; + $this->relative_to = $options['relative_to']; + } + + $parsedown = new ParsedownExtra(); + $parsedown->setBreaksEnabled(true); + + if (!$this->knittingNeeded($this->body)) { + $this->body_parsed = $parsedown->text($this->body); // transform upon insertion into db instead of at runtime + } + + $this->dbh->insert_update('survey_pauses', array( + 'id' => $this->id, + 'body' => $this->body, + 'body_parsed' => $this->body_parsed, + 'wait_until_time' => $this->wait_until_time, + 'wait_until_date' => $this->wait_until_date, + 'wait_minutes' => $this->wait_minutes, + 'relative_to' => $this->relative_to, + )); + $this->dbh->commit(); + $this->valid = true; + + return true; + } + + public function displayForRun($prepend = '') { + $dialog = Template::get($this->getUnitTemplatePath(), array( + 'prepend' => $prepend, + 'wait_until_time' => $this->wait_until_time, + 'wait_until_date' => $this->wait_until_date, + 'wait_minutes' => $this->wait_minutes, + 'relative_to' => $this->relative_to, + 'body' => $this->body, + )); + + return parent::runDialog($dialog); + } + + public function removeFromRun($special = null) { + return $this->delete($special); + } + + protected function checkRelativeTo() { + $this->relative_to = trim($this->relative_to); + $this->wait_minutes = trim($this->wait_minutes); + $this->has_wait_minutes = !($this->wait_minutes === null || $this->wait_minutes == ''); + $this->has_relative_to = !($this->relative_to === null || $this->relative_to == ''); + + // disambiguate what user meant + if ($this->has_wait_minutes && !$this->has_relative_to) { + // If user specified waiting minutes but did not specify relative to which timestamp, + // we imply we are waiting relative to when the user arrived at the pause + $this->relative_to = 'tail(survey_unit_sessions$created,1)'; + $this->has_relative_to = true; + } + + return $this->has_relative_to; + } + + protected function checkWhetherPauseIsOver() { + $this->execData['check_failed'] = false; + $this->execData['expire_relatively'] = null; + + // if a relative_to has been defined by user or automatically, we need to retrieve its value + if ($this->has_relative_to) { + $opencpu_vars = $this->getUserDataInRun($this->relative_to); + $result = opencpu_evaluate($this->relative_to, $opencpu_vars, 'json'); + + if ($result === null) { + $this->execData['check_failed'] = true; + return false; + } + $this->relative_to_result = $relative_to = $result; + } + + $bind_relative_to = false; + $conditions = array(); + + if (!$this->has_wait_minutes && $this->has_relative_to) { + // if no wait minutes but a relative to was defined, we just use this as the param (useful for complex R expressions) + if ($relative_to === true) { + $conditions['relative_to'] = '1=1'; + $this->execData['expire_relatively'] = true; + } elseif ($relative_to === false) { + $conditions['relative_to'] = '0=1'; + $this->execData['expire_relatively'] = false; + } elseif (!is_array($relative_to) && strtotime($relative_to)) { + $conditions['relative_to'] = ':relative_to <= NOW()'; + $bind_relative_to = true; + $this->execData['expire_timestamp'] = strtotime($relative_to); + // If there was a wait_time, set the timestamp to have this time + if ($time = $this->parseWaitTime(true)) { + $ts = $this->execData['expire_timestamp']; + $this->execData['expire_timestamp'] = mktime($time[0], $time[1], 0, date('m', $ts), date('d', $ts), date('Y', $ts)); + } + } else { + alert("Pause {$this->position}: Relative to yields neither true nor false, nor a date, nor a time. " . print_r($relative_to, true), 'alert-warning'); + $this->execData['check_failed'] = true; + return false; + } + } elseif ($this->has_wait_minutes) { + if (!is_array($relative_to) && strtotime($relative_to)) { + $conditions['minute'] = "DATE_ADD(:relative_to, INTERVAL :wait_minutes MINUTE) <= NOW()"; + $bind_relative_to = true; + $this->execData['expire_timestamp'] = strtotime($relative_to) + ($this->wait_minutes * 60); + } else { + alert("Pause {$this->position}: Relative to yields neither a date, nor a time. " . print_r($relative_to, true), 'alert-warning'); + $this->execData['check_failed'] = true; + return false; + } + } + + if ($this->wait_until_date && $this->wait_until_date != '0000-00-00') { + $wait_date = $this->wait_until_date; + } + + if ($this->wait_until_time && $this->wait_until_time != '00:00:00') { + $wait_time = $this->wait_until_time; + } + + $wait_date = $this->parseWaitDate(); + $wait_time = $this->parseWaitTime(); + + if (!empty($wait_date) && empty($wait_time)) { + $wait_time = '00:00:01'; + } + + if (!empty($wait_time) && empty($wait_date)) { + $wait_date = date('Y-m-d'); + } + + if (!empty($wait_date) && !empty($wait_time) && empty($this->execData['expire_timestamp'])) { + $wait_datetime = $wait_date . ' ' . $wait_time; + $this->execData['expire_timestamp'] = strtotime($wait_datetime); + + // If the expiration hour already passed before the user entered the pause, set expiration to the next day (in 24 hours) + $exp_ts = $this->execData['expire_timestamp']; + $created_ts = strtotime($this->run_session->unit_session->created); + $exp_hour_min = mktime(date('G', $exp_ts), date('i', $exp_ts), 0); + if ($created_ts > $exp_hour_min) { + $this->execData['expire_timestamp'] += 24 * 60 * 60; + return false; + } + /* + // Check if this unit already expired today for current run_session_id + $q = ' + SELECT 1 AS finished FROM `survey_unit_sessions` + WHERE `survey_unit_sessions`.unit_id = :id AND `survey_unit_sessions`.run_session_id = :run_session_id AND DATE(`survey_unit_sessions`.ended) = CURDATE() + LIMIT 1 + '; + $stmt = $this->dbh->prepare($q); + $stmt->bindValue(':id', $this->id); + $stmt->bindValue(':run_session_id', $this->run_session_id); + $stmt->execute(); + if ($stmt->rowCount() > 0) { + $this->execData['expire_timestamp'] = strtotime('+1 day', $this->execData['expire_timestamp']); + return false; + } + */ + + $conditions['datetime'] = ':wait_datetime <= NOW()'; + } + + $result = !empty($this->execData['expire_timestamp']) && $this->execData['expire_timestamp'] <= time(); + + if ($conditions) { + $condition = implode(' AND ', $conditions); + $stmt = $this->dbh->prepare("SELECT {$condition} AS test LIMIT 1"); + if ($bind_relative_to) { + $stmt->bindValue(':relative_to', $relative_to); + } + if (isset($conditions['minute'])) { + $stmt->bindValue(':wait_minutes', $this->wait_minutes); + } + if (isset($conditions['datetime'])) { + $stmt->bindValue(':wait_datetime', $wait_datetime); + } + + $stmt->execute(); + if ($stmt->rowCount() === 1 && ($row = $stmt->fetch(PDO::FETCH_ASSOC))) { + $result = (bool) $row['test']; + } + } else { + $result = true; + } + + $this->execData['pause_over'] = $result; + return $result; + } + + protected function parseWaitTime($parts = false) { + if ($this->wait_until_time && $this->wait_until_time != '00:00:00') { + return $parts ? explode(':', $this->wait_until_time) : $this->wait_until_time; + } + + return null; + } + + protected function parseWaitDate($parts = false) { + if ($this->wait_until_date && $this->wait_until_date != '0000-00-00') { + return $parts ? explode('-', $this->wait_until_date) : $this->wait_until_date; + } + + return null; + } + + public function test() { + $results = $this->getSampleSessions(); + if (!$results) { + return false; + } + + // take the first sample session + $sess = current($results); + $this->run_session_id = $sess['id']; + + + echo "

    Pause message

    "; + echo $this->getParsedBodyAdmin($this->body); + + if ($this->checkRelativeTo()) { + echo "

    Pause relative to

    "; + $opencpu_vars = $this->getUserDataInRun($this->relative_to); + $session = opencpu_evaluate($this->relative_to, $opencpu_vars, 'json', null, true); + echo opencpu_debug($session); + } + + if (!empty($results) && (empty($session) || !$session->hasError())) { + + $test_tpl = ' @@ -306,8 +303,8 @@ public function test() {
    '; - - $row_tpl = ' + + $row_tpl = ' %{session} (%{position}) %{relative_to} @@ -315,39 +312,39 @@ public function test() { '; - $rows = ''; - foreach ($results as $row) { - $this->run_session_id = $row['id']; - $runSession = new RunSession($this->dbh, $this->run->id, Site::getCurrentUser()->id, $row['session'], $this->run); - $runSession->unit_session = new UnitSession($this->dbh, $this->run_session_id, $this->id); - $this->run_session = $runSession; - - $rows .= Template::replace($row_tpl, array( - 'session' => $row['session'], - 'position' => $row['position'], - 'relative_to' => stringBool($this->relative_to_result), - 'pause_over' => stringBool($this->checkWhetherPauseIsOver()), - )); - } - - echo Template::replace($test_tpl, array('rows' => $rows)); - } - } - - public function exec() { - $this->checkRelativeTo(); - if ($this->checkWhetherPauseIsOver()) { - $this->end(); - return false; - } else { - $body = $this->getParsedBody($this->body); - if ($body === false) { - return true; // openCPU errors - } - return array( - 'body' => $body - ); - } - } + $rows = ''; + foreach ($results as $row) { + $this->run_session_id = $row['id']; + $runSession = new RunSession($this->dbh, $this->run->id, Site::getCurrentUser()->id, $row['session'], $this->run); + $runSession->unit_session = new UnitSession($this->dbh, $this->run_session_id, $this->id); + $this->run_session = $runSession; + + $rows .= Template::replace($row_tpl, array( + 'session' => $row['session'], + 'position' => $row['position'], + 'relative_to' => stringBool($this->relative_to_result), + 'pause_over' => stringBool($this->checkWhetherPauseIsOver()), + )); + } + + echo Template::replace($test_tpl, array('rows' => $rows)); + } + } + + public function exec() { + $this->checkRelativeTo(); + if ($this->checkWhetherPauseIsOver()) { + $this->end(); + return false; + } else { + $body = $this->getParsedBody($this->body); + if ($body === false) { + return true; // openCPU errors + } + return array( + 'body' => $body + ); + } + } } diff --git a/application/Model/Run.php b/application/Model/Run.php index 9ab3a01cf..db3476da4 100644 --- a/application/Model/Run.php +++ b/application/Model/Run.php @@ -27,368 +27,365 @@ */ class Run { - public $id = null; - public $name = null; - public $valid = false; - public $public = false; - public $cron_active = true; - public $cron_fork = false; - public $live = false; - public $user_id = null; - public $being_serviced = false; - public $locked = false; - public $errors = array(); - public $messages = array(); - public $custom_css_path = null; - public $custom_js_path = null; - public $header_image_path = null; - public $title = null; - public $description = null; - public $osf_project_id = null; - public $footer_text = null; - public $public_blurb = null; - public $use_material_design = false; - public $expire_cookie = 0; - - public $expire_cookie_value = 0; - public $expire_cookie_unit; - public $expire_cookie_units = array( - 'seconds' => 'Seconds', - 'minutes' => 'Minutes', - 'hours' => 'Hours', - 'days' => 'Days', - 'months' => 'Months', - 'years' => 'Years', - ); - - private $description_parsed = null; - private $footer_text_parsed = null; - private $public_blurb_parsed = null; - private $api_secret_hash = null; - private $owner = null; - private $run_settings = array( - "header_image_path", "title", "description", - "footer_text", "public_blurb", "custom_css", - "custom_js", "cron_active", "osf_project_id", - "use_material_design", "expire_cookie", - "expire_cookie_value", "expire_cookie_unit", - ); - public $renderedDescAndFooterAlready = false; - - /** - * @var DB - */ - private $dbh; - - /** - * - * @var RunSession - */ - public $activeRunSession; - - const TEST_RUN = 'formr-test-run'; - - public function __construct($fdb, $name) { - $this->dbh = $fdb; - - if ($name == self::TEST_RUN) { - $this->name = $name; - $this->valid = true; - $this->user_id = -1; - $this->id = -1; - return true; - } - - if ($name !== null) { - $this->name = $name; - $this->load(); - } - } - - protected function load() { - if (in_array($this->name, Config::get('reserved_run_names', array()))) { - return; - } - - $columns = "id, user_id, created, modified, name, api_secret_hash, public, cron_active, cron_fork, locked, header_image_path, title, description, description_parsed, footer_text, footer_text_parsed, public_blurb, public_blurb_parsed, custom_css_path, custom_js_path, osf_project_id, use_material_design, expire_cookie"; - $vars = $this->dbh->findRow('survey_runs', array('name' => $this->name), $columns); - - if ($vars) { - $this->id = $vars['id']; - $this->user_id = (int) $vars['user_id']; - $this->api_secret_hash = $vars['api_secret_hash']; - $this->created = (int)$vars['created']; - $this->modified = (int)$vars['modified']; - $this->public = (int)$vars['public']; - $this->cron_active = (int)$vars['cron_active']; - $this->cron_fork = (int)$vars['cron_fork']; - $this->locked = (int)$vars['locked']; - $this->header_image_path = $vars['header_image_path']; - $this->title = $vars['title']; - $this->description = $vars['description']; - $this->description_parsed = $vars['description_parsed']; - $this->footer_text = $vars['footer_text']; - $this->footer_text_parsed = $vars['footer_text_parsed']; - $this->public_blurb = $vars['public_blurb']; - $this->public_blurb_parsed = $vars['public_blurb_parsed']; - $this->custom_css_path = $vars['custom_css_path']; - $this->custom_js_path = $vars['custom_js_path']; - $this->osf_project_id = $vars['osf_project_id']; - $this->use_material_design = (bool)$vars['use_material_design']; - $this->expire_cookie = (int)$vars['expire_cookie']; - $this->valid = true; - $this->setExpireCookieUnits(); - } - } - - public function getCronDues() { - $sessions = $this->dbh->select('session') - ->from('survey_run_sessions') - ->where(array('run_id' => $this->id)) - ->order('RAND') - ->statement(); - $dues = array(); - while ($run_session = $sessions->fetch(PDO::FETCH_ASSOC)) { - $dues[] = $run_session['session']; - } - return $dues; - } - - /* ADMIN functions */ - - public function getApiSecret($user) { - if ($user->isAdmin()) { - return $this->api_secret_hash; - } - return false; - } - - public function hasApiAccess($secret) { - return $this->api_secret_hash === $secret; - } - - public function rename($new_name) { - $name = trim($new_name); - $this->dbh->update('survey_runs', array('name' => $name), array('id' => $this->id)); - return true; - } - - public function delete() { - try { - $this->dbh->delete('survey_runs', array('id' => $this->id)); - alert("Success. Successfully deleted run '{$this->name}'.", 'alert-success'); - redirect_to(admin_url()); - } catch (Exception $e) { - formr_log_exception($e, __CLASS__); - alert(__('Could not delete run %s. This is probably because there are still run units present. For safety\'s sake you\'ll first need to delete each unit individually.', $this->name), 'alert-danger'); - } - } - - public function togglePublic($public) { - if (!in_array($public, range(0, 3))) { - return false; - } - - $updated = $this->dbh->update('survey_runs', array('public' => $public), array('id' => $this->id)); - return $updated !== false; - } - - public function toggleLocked($on) { - $on = (int) $on; - $updated = $this->dbh->update('survey_runs', array('locked' => $on), array('id' => $this->id)); - return $updated !== false; - } - - public function create($options) { - $name = trim($options['run_name']); - - // create run db entry - $new_secret = crypto_token(66); - $this->dbh->insert('survey_runs', array( - 'user_id' => $options['user_id'], - 'name' => $name, - 'title' => $name, - 'created' => mysql_now(), - 'modified' => mysql_now(), - 'api_secret_hash' => $new_secret, - 'cron_active' => 1, - 'use_material_design' => 1, - 'expire_cookie' => 0, - 'public' => 0, - 'footer_text' => "Remember to add your contact info here! Contact the [study administration](mailto:email@example.com) in case of questions.", - 'footer_text_parsed' => "Remember to add your contact info here! Contact the study administration in case of questions.", - )); - $this->id = $this->dbh->pdo()->lastInsertId(); - $this->name = $name; - $this->load(); - - // create default run service message - $factory = new RunUnitFactory(); - $options = RunUnit::getDefaults('ServiceMessagePage'); - $unit = $factory->make($this->dbh, null, $options, null, $this); - $unit->create($options); - $unit->addToRun($this->id, 0, $options); - return $name; - } - - public function getUploadedFiles() { - return $this->dbh->select('id, created, original_file_name, new_file_path') - ->from('survey_uploaded_files') - ->where(array('run_id' => $this->id)) - ->order('created', 'desc') - ->fetchAll(); - } - - public $file_endings = array( - 'image/jpeg' => '.jpg', 'image/png' => '.png', 'image/gif' => '.gif', 'image/tiff' => '.tif', - 'video/mpeg' => '.mpg', 'video/quicktime' => '.mov', 'video/x-flv' => '.flv', 'video/x-f4v' => '.f4v', 'video/x-msvideo' => '.avi', - 'audio/mpeg' => '.mp3', - 'application/pdf' => '.pdf', - 'text/csv' => '.csv', 'text/css' => '.css', 'text/tab-separated-values' => '.tsv', 'text/plain' => '.txt' - ); - - public function uploadFiles($files) { - $max_size_upload = Config::get('admin_maximum_size_of_uploaded_files'); - // make lookup array - $existing_files = $this->getUploadedFiles(); - $files_by_names = array(); - foreach ($existing_files as $existing_file) { - $files_by_names[$existing_file['original_file_name']] = $existing_file['new_file_path']; - } - - // loop through files and modify them if necessary - for ($i = 0; $i < count($files['tmp_name']); $i++) { - // validate if any error occured on upload - if ($files['error'][$i]) { - $this->errors[] = __("An error occured uploading file '%s'. ERROR CODE: PFUL-%d", $files['name'][$i], $files['error'][$i]); - continue; - } - - // validate file size - $size = (int)$files['size'][$i]; - if (!$size || ($size > $max_size_upload * 1048576)) { - $this->errors[] = __("The file '%s' is too big or the size could not be determined. The allowed maximum size is %d megabytes.", $files['name'][$i], round($max_size_upload, 2)); - continue; - } - - // validate mime type - $finfo = new finfo(FILEINFO_MIME_TYPE); - $mime = $finfo->file($files['tmp_name'][$i]); - if (!isset($this->file_endings[$mime])) { - $this->errors[] = __('The file "%s" has the MIME type %s and is not allowed to be uploaded.', $files['name'][$i], $mime); - continue; - } - - // validation was OK - $original_file_name = $files['name'][$i]; - if (isset($files_by_names[$original_file_name])) { - // override existing path - $new_file_path = $files_by_names[$original_file_name]; - $this->messages[] = __('The file "%s" was overriden.', $original_file_name); - } else { - $new_file_path = 'assets/tmp/admin/' . crypto_token(33, true) . $this->file_endings[$mime]; - } - - // save file - $destination_dir = APPLICATION_ROOT . 'webroot/' . $new_file_path; - if (move_uploaded_file($files['tmp_name'][$i], $destination_dir)) { - $this->dbh->insert_update('survey_uploaded_files', array( - 'run_id' => $this->id, - 'created' => mysql_now(), - 'original_file_name' => $original_file_name, - 'new_file_path' => $new_file_path, - ), array( - 'modified' => mysql_now() - )); - } else { - $this->errors[] = __("Unable to move uploaded file '%s' to storage location.", $files['name'][$i]); - } - - } - - return empty($this->errors); - } - - public function deleteFile($id, $filename) { - $where = array('id' => (int) $id, 'original_file_name' => $filename); - $filepath = $this->dbh->findValue('survey_uploaded_files', $where, 'new_file_path'); - $deleted = $this->dbh->delete('survey_uploaded_files', $where); - $physicalfile = APPLICATION_ROOT . "webroot/" . $filepath; - if ($deleted && file_exists($physicalfile)) { - @unlink($physicalfile); - } - return $deleted; - } - - public static function nameExists($name) { - return DB::getInstance()->entry_exists('survey_runs', array('name' => $name)); - } - - public function reorder($positions) { - $run_unit_id = null; - $pos = null; - $update = "UPDATE `survey_run_units` SET position = :position WHERE run_id = :run_id AND id = :run_unit_id"; - $reorder = $this->dbh->prepare($update); - $reorder->bindParam(':run_id', $this->id); - $reorder->bindParam(':run_unit_id', $run_unit_id); - $reorder->bindParam(':position', $pos); - - foreach ($positions as $run_unit_id => $pos) { - $reorder->execute(); - } - return true; - } - - public function getAllUnitIds() { - return $this->dbh->select(array('id' => 'run_unit_id', 'unit_id', 'position')) - ->from('survey_run_units') - ->where(array('run_id' => $this->id)) - ->order('position') - ->fetchAll(); - } - - public function getAllUnitTypes() { - $select = $this->dbh->select(array('survey_run_units.id' => 'run_unit_id', 'unit_id', 'position', 'type', 'description')); - $select->from('survey_run_units'); - $select->join('survey_units', 'survey_units.id = survey_run_units.unit_id'); - $select->where(array('run_id' => $this->id))->order('position'); - - return $select->fetchAll(); - } - - public function getOverviewScript() { - return $this->getSpecialUnit('OverviewScriptPage'); - } - - public function getServiceMessage() { - return $this->getSpecialUnit('ServiceMessagePage'); - } - - public function getNumberOfSessionsInRun() { - $g_users = $this->dbh->prepare( - "SELECT COUNT(`survey_run_sessions`.id) AS sessions, AVG(`survey_run_sessions`.position) AS avg_position + public $id = null; + public $name = null; + public $valid = false; + public $public = false; + public $cron_active = true; + public $cron_fork = false; + public $live = false; + public $user_id = null; + public $being_serviced = false; + public $locked = false; + public $errors = array(); + public $messages = array(); + public $custom_css_path = null; + public $custom_js_path = null; + public $header_image_path = null; + public $title = null; + public $description = null; + public $osf_project_id = null; + public $footer_text = null; + public $public_blurb = null; + public $use_material_design = false; + public $expire_cookie = 0; + public $expire_cookie_value = 0; + public $expire_cookie_unit; + public $expire_cookie_units = array( + 'seconds' => 'Seconds', + 'minutes' => 'Minutes', + 'hours' => 'Hours', + 'days' => 'Days', + 'months' => 'Months', + 'years' => 'Years', + ); + private $description_parsed = null; + private $footer_text_parsed = null; + private $public_blurb_parsed = null; + private $api_secret_hash = null; + private $owner = null; + private $run_settings = array( + "header_image_path", "title", "description", + "footer_text", "public_blurb", "custom_css", + "custom_js", "cron_active", "osf_project_id", + "use_material_design", "expire_cookie", + "expire_cookie_value", "expire_cookie_unit", + ); + public $renderedDescAndFooterAlready = false; + + /** + * @var DB + */ + private $dbh; + + /** + * + * @var RunSession + */ + public $activeRunSession; + + const TEST_RUN = 'formr-test-run'; + + public function __construct($fdb, $name) { + $this->dbh = $fdb; + + if ($name == self::TEST_RUN) { + $this->name = $name; + $this->valid = true; + $this->user_id = -1; + $this->id = -1; + return true; + } + + if ($name !== null) { + $this->name = $name; + $this->load(); + } + } + + protected function load() { + if (in_array($this->name, Config::get('reserved_run_names', array()))) { + return; + } + + $columns = "id, user_id, created, modified, name, api_secret_hash, public, cron_active, cron_fork, locked, header_image_path, title, description, description_parsed, footer_text, footer_text_parsed, public_blurb, public_blurb_parsed, custom_css_path, custom_js_path, osf_project_id, use_material_design, expire_cookie"; + $vars = $this->dbh->findRow('survey_runs', array('name' => $this->name), $columns); + + if ($vars) { + $this->id = $vars['id']; + $this->user_id = (int) $vars['user_id']; + $this->api_secret_hash = $vars['api_secret_hash']; + $this->created = (int) $vars['created']; + $this->modified = (int) $vars['modified']; + $this->public = (int) $vars['public']; + $this->cron_active = (int) $vars['cron_active']; + $this->cron_fork = (int) $vars['cron_fork']; + $this->locked = (int) $vars['locked']; + $this->header_image_path = $vars['header_image_path']; + $this->title = $vars['title']; + $this->description = $vars['description']; + $this->description_parsed = $vars['description_parsed']; + $this->footer_text = $vars['footer_text']; + $this->footer_text_parsed = $vars['footer_text_parsed']; + $this->public_blurb = $vars['public_blurb']; + $this->public_blurb_parsed = $vars['public_blurb_parsed']; + $this->custom_css_path = $vars['custom_css_path']; + $this->custom_js_path = $vars['custom_js_path']; + $this->osf_project_id = $vars['osf_project_id']; + $this->use_material_design = (bool) $vars['use_material_design']; + $this->expire_cookie = (int) $vars['expire_cookie']; + $this->valid = true; + $this->setExpireCookieUnits(); + } + } + + public function getCronDues() { + $sessions = $this->dbh->select('session') + ->from('survey_run_sessions') + ->where(array('run_id' => $this->id)) + ->order('RAND') + ->statement(); + $dues = array(); + while ($run_session = $sessions->fetch(PDO::FETCH_ASSOC)) { + $dues[] = $run_session['session']; + } + return $dues; + } + + /* ADMIN functions */ + + public function getApiSecret($user) { + if ($user->isAdmin()) { + return $this->api_secret_hash; + } + return false; + } + + public function hasApiAccess($secret) { + return $this->api_secret_hash === $secret; + } + + public function rename($new_name) { + $name = trim($new_name); + $this->dbh->update('survey_runs', array('name' => $name), array('id' => $this->id)); + return true; + } + + public function delete() { + try { + $this->dbh->delete('survey_runs', array('id' => $this->id)); + alert("Success. Successfully deleted run '{$this->name}'.", 'alert-success'); + redirect_to(admin_url()); + } catch (Exception $e) { + formr_log_exception($e, __CLASS__); + alert(__('Could not delete run %s. This is probably because there are still run units present. For safety\'s sake you\'ll first need to delete each unit individually.', $this->name), 'alert-danger'); + } + } + + public function togglePublic($public) { + if (!in_array($public, range(0, 3))) { + return false; + } + + $updated = $this->dbh->update('survey_runs', array('public' => $public), array('id' => $this->id)); + return $updated !== false; + } + + public function toggleLocked($on) { + $on = (int) $on; + $updated = $this->dbh->update('survey_runs', array('locked' => $on), array('id' => $this->id)); + return $updated !== false; + } + + public function create($options) { + $name = trim($options['run_name']); + + // create run db entry + $new_secret = crypto_token(66); + $this->dbh->insert('survey_runs', array( + 'user_id' => $options['user_id'], + 'name' => $name, + 'title' => $name, + 'created' => mysql_now(), + 'modified' => mysql_now(), + 'api_secret_hash' => $new_secret, + 'cron_active' => 1, + 'use_material_design' => 1, + 'expire_cookie' => 0, + 'public' => 0, + 'footer_text' => "Remember to add your contact info here! Contact the [study administration](mailto:email@example.com) in case of questions.", + 'footer_text_parsed' => "Remember to add your contact info here! Contact the study administration in case of questions.", + )); + $this->id = $this->dbh->pdo()->lastInsertId(); + $this->name = $name; + $this->load(); + + // create default run service message + $factory = new RunUnitFactory(); + $options = RunUnit::getDefaults('ServiceMessagePage'); + $unit = $factory->make($this->dbh, null, $options, null, $this); + $unit->create($options); + $unit->addToRun($this->id, 0, $options); + return $name; + } + + public function getUploadedFiles() { + return $this->dbh->select('id, created, original_file_name, new_file_path') + ->from('survey_uploaded_files') + ->where(array('run_id' => $this->id)) + ->order('created', 'desc') + ->fetchAll(); + } + + public $file_endings = array( + 'image/jpeg' => '.jpg', 'image/png' => '.png', 'image/gif' => '.gif', 'image/tiff' => '.tif', + 'video/mpeg' => '.mpg', 'video/quicktime' => '.mov', 'video/x-flv' => '.flv', 'video/x-f4v' => '.f4v', 'video/x-msvideo' => '.avi', + 'audio/mpeg' => '.mp3', + 'application/pdf' => '.pdf', + 'text/csv' => '.csv', 'text/css' => '.css', 'text/tab-separated-values' => '.tsv', 'text/plain' => '.txt' + ); + + public function uploadFiles($files) { + $max_size_upload = Config::get('admin_maximum_size_of_uploaded_files'); + // make lookup array + $existing_files = $this->getUploadedFiles(); + $files_by_names = array(); + foreach ($existing_files as $existing_file) { + $files_by_names[$existing_file['original_file_name']] = $existing_file['new_file_path']; + } + + // loop through files and modify them if necessary + for ($i = 0; $i < count($files['tmp_name']); $i++) { + // validate if any error occured on upload + if ($files['error'][$i]) { + $this->errors[] = __("An error occured uploading file '%s'. ERROR CODE: PFUL-%d", $files['name'][$i], $files['error'][$i]); + continue; + } + + // validate file size + $size = (int) $files['size'][$i]; + if (!$size || ($size > $max_size_upload * 1048576)) { + $this->errors[] = __("The file '%s' is too big or the size could not be determined. The allowed maximum size is %d megabytes.", $files['name'][$i], round($max_size_upload, 2)); + continue; + } + + // validate mime type + $finfo = new finfo(FILEINFO_MIME_TYPE); + $mime = $finfo->file($files['tmp_name'][$i]); + if (!isset($this->file_endings[$mime])) { + $this->errors[] = __('The file "%s" has the MIME type %s and is not allowed to be uploaded.', $files['name'][$i], $mime); + continue; + } + + // validation was OK + $original_file_name = $files['name'][$i]; + if (isset($files_by_names[$original_file_name])) { + // override existing path + $new_file_path = $files_by_names[$original_file_name]; + $this->messages[] = __('The file "%s" was overriden.', $original_file_name); + } else { + $new_file_path = 'assets/tmp/admin/' . crypto_token(33, true) . $this->file_endings[$mime]; + } + + // save file + $destination_dir = APPLICATION_ROOT . 'webroot/' . $new_file_path; + if (move_uploaded_file($files['tmp_name'][$i], $destination_dir)) { + $this->dbh->insert_update('survey_uploaded_files', array( + 'run_id' => $this->id, + 'created' => mysql_now(), + 'original_file_name' => $original_file_name, + 'new_file_path' => $new_file_path, + ), array( + 'modified' => mysql_now() + )); + } else { + $this->errors[] = __("Unable to move uploaded file '%s' to storage location.", $files['name'][$i]); + } + } + + return empty($this->errors); + } + + public function deleteFile($id, $filename) { + $where = array('id' => (int) $id, 'original_file_name' => $filename); + $filepath = $this->dbh->findValue('survey_uploaded_files', $where, 'new_file_path'); + $deleted = $this->dbh->delete('survey_uploaded_files', $where); + $physicalfile = APPLICATION_ROOT . "webroot/" . $filepath; + if ($deleted && file_exists($physicalfile)) { + @unlink($physicalfile); + } + return $deleted; + } + + public static function nameExists($name) { + return DB::getInstance()->entry_exists('survey_runs', array('name' => $name)); + } + + public function reorder($positions) { + $run_unit_id = null; + $pos = null; + $update = "UPDATE `survey_run_units` SET position = :position WHERE run_id = :run_id AND id = :run_unit_id"; + $reorder = $this->dbh->prepare($update); + $reorder->bindParam(':run_id', $this->id); + $reorder->bindParam(':run_unit_id', $run_unit_id); + $reorder->bindParam(':position', $pos); + + foreach ($positions as $run_unit_id => $pos) { + $reorder->execute(); + } + return true; + } + + public function getAllUnitIds() { + return $this->dbh->select(array('id' => 'run_unit_id', 'unit_id', 'position')) + ->from('survey_run_units') + ->where(array('run_id' => $this->id)) + ->order('position') + ->fetchAll(); + } + + public function getAllUnitTypes() { + $select = $this->dbh->select(array('survey_run_units.id' => 'run_unit_id', 'unit_id', 'position', 'type', 'description')); + $select->from('survey_run_units'); + $select->join('survey_units', 'survey_units.id = survey_run_units.unit_id'); + $select->where(array('run_id' => $this->id))->order('position'); + + return $select->fetchAll(); + } + + public function getOverviewScript() { + return $this->getSpecialUnit('OverviewScriptPage'); + } + + public function getServiceMessage() { + return $this->getSpecialUnit('ServiceMessagePage'); + } + + public function getNumberOfSessionsInRun() { + $g_users = $this->dbh->prepare( + "SELECT COUNT(`survey_run_sessions`.id) AS sessions, AVG(`survey_run_sessions`.position) AS avg_position FROM `survey_run_sessions` WHERE `survey_run_sessions`.run_id = :run_id;" - ); - $g_users->bindParam(':run_id', $this->id); - $g_users->execute(); - return $g_users->fetch(PDO::FETCH_ASSOC); - } - - /** - * - * @return \User - */ - public function getOwner() { - if (!$this->owner) { - $this->owner = new User($this->dbh, $this->user_id); - } - return $this->owner; - } - - public function getUserCounts() { - $g_users = $this->dbh->prepare( - "SELECT COUNT(`id`) AS users_total, + ); + $g_users->bindParam(':run_id', $this->id); + $g_users->execute(); + return $g_users->fetch(PDO::FETCH_ASSOC); + } + + /** + * + * @return \User + */ + public function getOwner() { + if (!$this->owner) { + $this->owner = new User($this->dbh, $this->user_id); + } + return $this->owner; + } + + public function getUserCounts() { + $g_users = $this->dbh->prepare( + "SELECT COUNT(`id`) AS users_total, SUM(`ended` IS NOT NULL) AS users_finished, SUM(`ended` IS NULL AND `last_access` >= DATE_SUB(NOW(), INTERVAL 1 DAY) ) AS users_active_today, SUM(`ended` IS NULL AND `last_access` >= DATE_SUB(NOW(), INTERVAL 7 DAY) ) AS users_active, @@ -396,215 +393,215 @@ public function getUserCounts() { FROM `survey_run_sessions` WHERE `survey_run_sessions`.run_id = :run_id;"); - $g_users->bindParam(':run_id', $this->id); - $g_users->execute(); - return $g_users->fetch(PDO::FETCH_ASSOC); - } - - public function emptySelf() { - $surveys = $this->getAllSurveys(); - $unit_factory = new RunUnitFactory(); - foreach ($surveys AS $survey) { - /* @var $unit Survey */ - $unit = $unit_factory->make($this->dbh, null, $survey, null, $this); - if (!$unit->backupResults()) { - alert('Could not backup results of survey ' . $unit->name, 'alert-danger'); - return false; - } - } - $rows = $this->dbh->delete('survey_run_sessions', array('run_id' => $this->id)); - alert('Run was emptied. ' . $rows . ' were deleted.', 'alert-info'); - return $rows; - } - - public function getSpecialUnit($xtype, $id = null) { - $units = $this->getSpecialUnits(false, $xtype, $id); - if (empty($units)) { - return null; - } - $factory = new RunUnitFactory(); - return $factory->make($this->dbh, null, $units[0], null, $this); - } - - public function getSpecialUnits($render = false, $xtype = null, $id = null) { - $cols = array( - 'survey_run_special_units.id' => 'unit_id', 'survey_run_special_units.run_id', 'survey_run_special_units.type' => 'xtype', 'survey_run_special_units.description', - 'survey_units.type', 'survey_units.created', 'survey_units.modified' - ); - $select = $this->dbh->select($cols); - $select->from('survey_run_special_units'); - $select->join('survey_units', 'survey_units.id = survey_run_special_units.id'); - $select->where('survey_run_special_units.run_id = :run_id'); - $select->order('survey_units.id', 'desc'); - $params = array('run_id' => $this->id); - if ($xtype !== null) { - $select->where('survey_run_special_units.type = :xtype'); - $params['xtype'] = $xtype; - } - if ($id !== null) { - $select->where('survey_run_special_units.id = :id'); - $params['id'] = $id; - } - $select->bindParams($params); - - if ($render === false) { - return $select->fetchAll(); - } else { - $units = array(); - foreach ($select->fetchAll() as $unit) { - $units[] = array( - 'id' => $unit['unit_id'], - 'html_units' => array(array( - 'special' => $unit['xtype'], - 'run_unit_id' => $unit['unit_id'], - 'unit_id' => $unit['unit_id'] - )), - ); - } - return $units; - } - } - - public function getReminder($reminder_id, $session, $run_session_id) { - // create a unit_session here and get a session_id and pass it when making the unit - $unitSession = new UnitSession($this->dbh, $run_session_id, $reminder_id); - $session_id = $unitSession->create(); - $unit_factory = new RunUnitFactory(); - $unit = $unit_factory->make($this->dbh, $session, array( - 'type' => "Email", - "unit_id" => $reminder_id, - "run_name" => $this->name, - "run_id" => $this->id, - "run_session_id" => $run_session_id, - "session_id" => $session_id, - ), null, $this); - return $unit; - } - - public function getCustomCSS() { - if ($this->custom_css_path != null) { - return $this->getFileContent($this->custom_css_path); - } - - return ""; - } - - public function getCustomJS() { - if ($this->custom_js_path != null) { - return $this->getFileContent($this->custom_js_path); - } - - return ""; - } - - private function getFileContent($path) { - $path = new SplFileInfo(APPLICATION_ROOT . "webroot/" . $path); - $exists = file_exists($path->getPathname()); - if ($exists) { - $file = $path->openFile('c+'); - $data = ''; - $file->next(); - while ($file->valid()) { - $data .= $file->current(); - $file->next(); - } - return $data; - } - - return ''; - } - - public function saveSettings($posted) { - $parsedown = new ParsedownExtra(); - $parsedown->setBreaksEnabled(true); - $successes = array(); - if (isset($posted['description'])) { - $posted['description_parsed'] = $parsedown->text($posted['description']); - $this->run_settings[] = 'description_parsed'; - } - if (isset($posted['public_blurb'])){ - $posted['public_blurb_parsed'] = $parsedown->text($posted['public_blurb']); - $this->run_settings[] = 'public_blurb_parsed'; - } - if (isset($posted['footer_text'])) { - $posted['footer_text_parsed'] = $parsedown->text($posted['footer_text']); - $this->run_settings[] = 'footer_text_parsed'; - } - - $cookie_units = array_keys($this->expire_cookie_units); - if (isset($posted['expire_cookie_value']) && is_numeric($posted['expire_cookie_value']) && - isset($posted['expire_cookie_unit']) && in_array($posted['expire_cookie_unit'], $cookie_units)) { - $posted['expire_cookie'] = factortosecs($posted['expire_cookie_value'], $posted['expire_cookie_unit']); - } elseif (!isset($posted['expire_cookie'])) { - $posted['expire_cookie'] = $this->expire_cookie; - } - unset($posted['expire_cookie_value'], $posted['expire_cookie_unit']); - - $updates = array(); - foreach ($posted as $name => $value) { - $value = trim($value); - - if (!in_array($name, $this->run_settings)) { - $this->errors[] = "Invalid setting " . h($name); - continue; - } - - if ($name == "custom_js" || $name == "custom_css") { - if ($name == "custom_js") { - $asset_path = $this->custom_js_path; - $file_ending = '.js'; - } else { - $asset_path = $this->custom_css_path; - $file_ending = '.css'; - } - - $name = $name . "_path"; - $asset_file = APPLICATION_ROOT . "webroot/" . $asset_path; - // Delete old file if css/js was emptied - if (!$value && $asset_path) { - if (file_exists($asset_file) && !unlink($asset_file)) { - alert("Could not delete old file ({$asset_path}).", 'alert-warning'); - } - $value = null; - } elseif ($value) { - // if $asset_path has not been set it means neither JS or CSS has been entered so create a new path - if (!$asset_path) { - $asset_path = 'assets/tmp/admin/' . crypto_token(33, true) . $file_ending; - $asset_file = APPLICATION_ROOT . 'webroot/' . $asset_path; - } - - $path = new SplFileInfo($asset_file); - if (file_exists($path->getPathname())): - $file = $path->openFile('c+'); - $file->rewind(); - $file->ftruncate(0); // truncate any existing file - else: - $file = $path->openFile('c+'); - endif; - $file->fwrite($value); - $file->fflush(); - $value = $asset_path; - } - } - - $updates[$name] = $value; - } - - if ($updates) { - $updates['modified'] = mysql_now(); - $this->dbh->update('survey_runs', $updates, array('id' => $this->id)); - } - - if (!in_array(false, $successes)) { - return true; - } - - return false; - } - - public function getUnitAdmin($id, $special = false) { - if (!$special) { - $unit = $this->dbh->select(' + $g_users->bindParam(':run_id', $this->id); + $g_users->execute(); + return $g_users->fetch(PDO::FETCH_ASSOC); + } + + public function emptySelf() { + $surveys = $this->getAllSurveys(); + $unit_factory = new RunUnitFactory(); + foreach ($surveys AS $survey) { + /* @var $unit Survey */ + $unit = $unit_factory->make($this->dbh, null, $survey, null, $this); + if (!$unit->backupResults()) { + alert('Could not backup results of survey ' . $unit->name, 'alert-danger'); + return false; + } + } + $rows = $this->dbh->delete('survey_run_sessions', array('run_id' => $this->id)); + alert('Run was emptied. ' . $rows . ' were deleted.', 'alert-info'); + return $rows; + } + + public function getSpecialUnit($xtype, $id = null) { + $units = $this->getSpecialUnits(false, $xtype, $id); + if (empty($units)) { + return null; + } + $factory = new RunUnitFactory(); + return $factory->make($this->dbh, null, $units[0], null, $this); + } + + public function getSpecialUnits($render = false, $xtype = null, $id = null) { + $cols = array( + 'survey_run_special_units.id' => 'unit_id', 'survey_run_special_units.run_id', 'survey_run_special_units.type' => 'xtype', 'survey_run_special_units.description', + 'survey_units.type', 'survey_units.created', 'survey_units.modified' + ); + $select = $this->dbh->select($cols); + $select->from('survey_run_special_units'); + $select->join('survey_units', 'survey_units.id = survey_run_special_units.id'); + $select->where('survey_run_special_units.run_id = :run_id'); + $select->order('survey_units.id', 'desc'); + $params = array('run_id' => $this->id); + if ($xtype !== null) { + $select->where('survey_run_special_units.type = :xtype'); + $params['xtype'] = $xtype; + } + if ($id !== null) { + $select->where('survey_run_special_units.id = :id'); + $params['id'] = $id; + } + $select->bindParams($params); + + if ($render === false) { + return $select->fetchAll(); + } else { + $units = array(); + foreach ($select->fetchAll() as $unit) { + $units[] = array( + 'id' => $unit['unit_id'], + 'html_units' => array(array( + 'special' => $unit['xtype'], + 'run_unit_id' => $unit['unit_id'], + 'unit_id' => $unit['unit_id'] + )), + ); + } + return $units; + } + } + + public function getReminder($reminder_id, $session, $run_session_id) { + // create a unit_session here and get a session_id and pass it when making the unit + $unitSession = new UnitSession($this->dbh, $run_session_id, $reminder_id); + $session_id = $unitSession->create(); + $unit_factory = new RunUnitFactory(); + $unit = $unit_factory->make($this->dbh, $session, array( + 'type' => "Email", + "unit_id" => $reminder_id, + "run_name" => $this->name, + "run_id" => $this->id, + "run_session_id" => $run_session_id, + "session_id" => $session_id, + ), null, $this); + return $unit; + } + + public function getCustomCSS() { + if ($this->custom_css_path != null) { + return $this->getFileContent($this->custom_css_path); + } + + return ""; + } + + public function getCustomJS() { + if ($this->custom_js_path != null) { + return $this->getFileContent($this->custom_js_path); + } + + return ""; + } + + private function getFileContent($path) { + $path = new SplFileInfo(APPLICATION_ROOT . "webroot/" . $path); + $exists = file_exists($path->getPathname()); + if ($exists) { + $file = $path->openFile('c+'); + $data = ''; + $file->next(); + while ($file->valid()) { + $data .= $file->current(); + $file->next(); + } + return $data; + } + + return ''; + } + + public function saveSettings($posted) { + $parsedown = new ParsedownExtra(); + $parsedown->setBreaksEnabled(true); + $successes = array(); + if (isset($posted['description'])) { + $posted['description_parsed'] = $parsedown->text($posted['description']); + $this->run_settings[] = 'description_parsed'; + } + if (isset($posted['public_blurb'])) { + $posted['public_blurb_parsed'] = $parsedown->text($posted['public_blurb']); + $this->run_settings[] = 'public_blurb_parsed'; + } + if (isset($posted['footer_text'])) { + $posted['footer_text_parsed'] = $parsedown->text($posted['footer_text']); + $this->run_settings[] = 'footer_text_parsed'; + } + + $cookie_units = array_keys($this->expire_cookie_units); + if (isset($posted['expire_cookie_value']) && is_numeric($posted['expire_cookie_value']) && + isset($posted['expire_cookie_unit']) && in_array($posted['expire_cookie_unit'], $cookie_units)) { + $posted['expire_cookie'] = factortosecs($posted['expire_cookie_value'], $posted['expire_cookie_unit']); + } elseif (!isset($posted['expire_cookie'])) { + $posted['expire_cookie'] = $this->expire_cookie; + } + unset($posted['expire_cookie_value'], $posted['expire_cookie_unit']); + + $updates = array(); + foreach ($posted as $name => $value) { + $value = trim($value); + + if (!in_array($name, $this->run_settings)) { + $this->errors[] = "Invalid setting " . h($name); + continue; + } + + if ($name == "custom_js" || $name == "custom_css") { + if ($name == "custom_js") { + $asset_path = $this->custom_js_path; + $file_ending = '.js'; + } else { + $asset_path = $this->custom_css_path; + $file_ending = '.css'; + } + + $name = $name . "_path"; + $asset_file = APPLICATION_ROOT . "webroot/" . $asset_path; + // Delete old file if css/js was emptied + if (!$value && $asset_path) { + if (file_exists($asset_file) && !unlink($asset_file)) { + alert("Could not delete old file ({$asset_path}).", 'alert-warning'); + } + $value = null; + } elseif ($value) { + // if $asset_path has not been set it means neither JS or CSS has been entered so create a new path + if (!$asset_path) { + $asset_path = 'assets/tmp/admin/' . crypto_token(33, true) . $file_ending; + $asset_file = APPLICATION_ROOT . 'webroot/' . $asset_path; + } + + $path = new SplFileInfo($asset_file); + if (file_exists($path->getPathname())): + $file = $path->openFile('c+'); + $file->rewind(); + $file->ftruncate(0); // truncate any existing file + else: + $file = $path->openFile('c+'); + endif; + $file->fwrite($value); + $file->fflush(); + $value = $asset_path; + } + } + + $updates[$name] = $value; + } + + if ($updates) { + $updates['modified'] = mysql_now(); + $this->dbh->update('survey_runs', $updates, array('id' => $this->id)); + } + + if (!in_array(false, $successes)) { + return true; + } + + return false; + } + + public function getUnitAdmin($id, $special = false) { + if (!$special) { + $unit = $this->dbh->select(' `survey_run_units`.id, `survey_run_units`.run_id, `survey_run_units`.unit_id, @@ -613,19 +610,19 @@ public function getUnitAdmin($id, $special = false) { `survey_units`.type, `survey_units`.created, `survey_units`.modified') - ->from('survey_run_units') - ->leftJoin('survey_units', 'survey_units.id = survey_run_units.unit_id') - ->where('survey_run_units.run_id = :run_id') - ->where('survey_run_units.id = :id') - ->bindParams(array('run_id' => $this->id, 'id' => $id)) - ->limit(1)->fetch(); - } else { - $specials = array('ServiceMessagePage', 'OverviewScriptPage', 'ReminderEmail'); - if (!in_array($special, $specials)) { - die("Special unit not allowed"); - } - - $unit = $this->dbh->select(" + ->from('survey_run_units') + ->leftJoin('survey_units', 'survey_units.id = survey_run_units.unit_id') + ->where('survey_run_units.run_id = :run_id') + ->where('survey_run_units.id = :id') + ->bindParams(array('run_id' => $this->id, 'id' => $id)) + ->limit(1)->fetch(); + } else { + $specials = array('ServiceMessagePage', 'OverviewScriptPage', 'ReminderEmail'); + if (!in_array($special, $specials)) { + die("Special unit not allowed"); + } + + $unit = $this->dbh->select(" `survey_run_special_units`.`id` AS unit_id, `survey_run_special_units`.`run_id`, `survey_run_special_units`.`description`, @@ -633,51 +630,52 @@ public function getUnitAdmin($id, $special = false) { `survey_units`.type, `survey_units`.created, `survey_units`.modified") - ->from('survey_run_special_units') - ->leftJoin('survey_units', "survey_units.id = `survey_run_special_units`.`id`") - ->where('survey_run_special_units.run_id = :run_id') - ->where("`survey_run_special_units`.`id` = :unit_id") - ->bindParams(array('run_id' => $this->id, 'unit_id' => $id)) - ->limit(1)->fetch(); - $unit["special"] = $special; - } - - if ($unit === false) { // or maybe we've got a problem - alert("Missing unit! $id", 'alert-danger'); - return false; - } - - - $unit['run_name'] = $this->name; - return $unit; - } - - public function getAllSurveys() { - // first, generate a master list of the search set (all the surveys that are part of the run) - return $this->dbh->select(array('COALESCE(`survey_studies`.`results_table`,`survey_studies`.`name`)' => 'results_table', 'survey_studies.name', 'survey_studies.id')) - ->from('survey_studies') - ->leftJoin('survey_run_units', 'survey_studies.id = survey_run_units.unit_id') - ->leftJoin('survey_runs', 'survey_runs.id = survey_run_units.run_id') - ->where('survey_runs.id = :run_id') - ->bindParams(array('run_id' => $this->id)) - ->fetchAll(); - } - public function getAllLinkedSurveys() { - // first, generate a master list of the search set (all the surveys that are part of the run) - return $this->dbh->select(array('COALESCE(`survey_studies`.`results_table`,`survey_studies`.`name`)' => 'results_table', 'survey_studies.name', 'survey_studies.id')) - ->from('survey_studies') - ->leftJoin('survey_run_units', 'survey_studies.id = survey_run_units.unit_id') - ->leftJoin('survey_runs', 'survey_runs.id = survey_run_units.run_id') - ->where('survey_runs.id = :run_id') - ->where('survey_studies.unlinked = 0') - ->bindParams(array('run_id' => $this->id)) - ->fetchAll(); - } - - public function getData($rstmt = false) { - ini_set('memory_limit', Config::get('memory_limit.run_get_data')); - $fdb = $this->dbh; - $collect = $fdb->prepare("SELECT + ->from('survey_run_special_units') + ->leftJoin('survey_units', "survey_units.id = `survey_run_special_units`.`id`") + ->where('survey_run_special_units.run_id = :run_id') + ->where("`survey_run_special_units`.`id` = :unit_id") + ->bindParams(array('run_id' => $this->id, 'unit_id' => $id)) + ->limit(1)->fetch(); + $unit["special"] = $special; + } + + if ($unit === false) { // or maybe we've got a problem + alert("Missing unit! $id", 'alert-danger'); + return false; + } + + + $unit['run_name'] = $this->name; + return $unit; + } + + public function getAllSurveys() { + // first, generate a master list of the search set (all the surveys that are part of the run) + return $this->dbh->select(array('COALESCE(`survey_studies`.`results_table`,`survey_studies`.`name`)' => 'results_table', 'survey_studies.name', 'survey_studies.id')) + ->from('survey_studies') + ->leftJoin('survey_run_units', 'survey_studies.id = survey_run_units.unit_id') + ->leftJoin('survey_runs', 'survey_runs.id = survey_run_units.run_id') + ->where('survey_runs.id = :run_id') + ->bindParams(array('run_id' => $this->id)) + ->fetchAll(); + } + + public function getAllLinkedSurveys() { + // first, generate a master list of the search set (all the surveys that are part of the run) + return $this->dbh->select(array('COALESCE(`survey_studies`.`results_table`,`survey_studies`.`name`)' => 'results_table', 'survey_studies.name', 'survey_studies.id')) + ->from('survey_studies') + ->leftJoin('survey_run_units', 'survey_studies.id = survey_run_units.unit_id') + ->leftJoin('survey_runs', 'survey_runs.id = survey_run_units.run_id') + ->where('survey_runs.id = :run_id') + ->where('survey_studies.unlinked = 0') + ->bindParams(array('run_id' => $this->id)) + ->fetchAll(); + } + + public function getData($rstmt = false) { + ini_set('memory_limit', Config::get('memory_limit.run_get_data')); + $fdb = $this->dbh; + $collect = $fdb->prepare("SELECT `survey_studies`.name AS survey_name, `survey_run_units`.position AS unit_position, `survey_unit_sessions`.id AS unit_session_id, @@ -705,21 +703,21 @@ public function getData($rstmt = false) { LEFT JOIN `survey_run_units` ON `survey_studies`.id = `survey_run_units`.unit_id WHERE `survey_run_sessions`.run_id = :id AND `survey_studies`.unlinked = 0"); - $collect->bindValue(":id", $this->id); - $collect->execute(); - if ($rstmt === true) { - return $collect; - } - - $results = array(); - while ($row = $collect->fetch(PDO::FETCH_ASSOC)) { - $results[] = $row; - } - return $results; - } - - public function getRandomGroups() { - $g_users = $this->dbh->prepare("SELECT + $collect->bindValue(":id", $this->id); + $collect->execute(); + if ($rstmt === true) { + return $collect; + } + + $results = array(); + while ($row = $collect->fetch(PDO::FETCH_ASSOC)) { + $results[] = $row; + } + return $results; + } + + public function getRandomGroups() { + $g_users = $this->dbh->prepare("SELECT `survey_run_sessions`.session, `survey_unit_sessions`.id AS session_id, `survey_runs`.name AS run_name, @@ -738,291 +736,287 @@ public function getRandomGroups() { WHERE `survey_run_sessions`.run_id = :run_id AND `survey_units`.type = 'Shuffle' ORDER BY `survey_run_sessions`.id DESC,`survey_unit_sessions`.id ASC;"); - $g_users->bindParam(':run_id', $this->id); - $g_users->execute(); - return $g_users; - } - - private function isFakeTestRun() { - return $this->name === self::TEST_RUN; - } - - private function fakeTestRun() { - if ($session = Session::get('dummy_survey_session')): - $run_session = $this->makeTestRunSession(); - $unit = new Survey($this->dbh, null, $session, $run_session, $this); - $output = $unit->exec(); - $this->activeRunSession = $run_session; - - if (!$output): - $output['title'] = 'Finish'; - $output['body'] = " + $g_users->bindParam(':run_id', $this->id); + $g_users->execute(); + return $g_users; + } + + private function isFakeTestRun() { + return $this->name === self::TEST_RUN; + } + + private function fakeTestRun() { + if ($session = Session::get('dummy_survey_session')): + $run_session = $this->makeTestRunSession(); + $unit = new Survey($this->dbh, null, $session, $run_session, $this); + $output = $unit->exec(); + $this->activeRunSession = $run_session; + + if (!$output): + $output['title'] = 'Finish'; + $output['body'] = "

    Finish

    You're finished with testing this survey.

    Back to the admin control panel."; - Session::delete('dummy_survey_session'); - endif; - return compact("output", "run_session"); - else: - alert("Error: Nothing to test-drive.", 'alert-danger'); - redirect_to("/index"); - return false; - endif; - } - - public function makeTestRunSession($testing = 1) { - $animal_name = AnimalName::haikunate(["tokenLength" => 0, "delimiter" => "",]) . "XXX"; - $animal_name = str_replace(" ", "", $animal_name); - $test_code = crypto_token(48 - floor(3 / 4 * strlen($animal_name))); - $test_code = $animal_name . substr($test_code, 0, 64 - strlen($animal_name)); - $run_session = new RunSession($this->dbh, $this->id, NULL, $test_code, $this); // does this user have a session? - $run_session->create($test_code, $testing); - - return $run_session; - } - - - public function addNamedRunSession($name, $testing = 0) { - $name = str_replace(" ", "_", $name); - if ($name && !preg_match('/^[a-zA-Z0-9_-~]{0,32}$/', $name)) { - alert("Invalid characters in suggested name. Only a-z, numbers, _ - and ~ are allowed. Spaces are automatically replaced by a _.", 'alert-danger'); - return false; - } - - if ($name) { - $name .= 'XXX'; - } - - $new_code = crypto_token(48 - floor(3 / 4 * strlen($name))); - $new_code = $name . substr($new_code, 0, 64 - strlen($name)); - $run_session = new RunSession($this->dbh, $this->id, null, $new_code, $this); // does this user have a session? - $run_session->create($new_code, $testing); - - return $run_session; - - } - - public function exec(User $user) { - if (!$this->valid) { - formr_error(404, 'Not Found', __("Run '%s' is broken or does not exist.", $this->name), 'Study Not Found'); - return false; - } elseif ($this->name == self::TEST_RUN) { - $test = $this->fakeTestRun(); - extract($test); - } else { - - $run_session = new RunSession($this->dbh, $this->id, $user->id, $user->user_code, $this); // does this user have a session? - - if (($this->getOwner()->user_code == $user->user_code || // owner always has access - $run_session->isTesting()) || // testers always have access - ($this->public >= 1 && $run_session->id) || // already enrolled - ($this->public >= 2)) { // anyone with link can access - - if ($run_session->id === null) { - $run_session->create($user->user_code, (int) $user->created($this)); // generating access code for those who don't have it but need it - } - - Session::globalRefresh(); - $output = $run_session->getUnit(); - - } else { - $output = $this->getServiceMessage()->exec(); - alert("Sorry: You cannot currently access this run.", 'alert-warning'); - } - - $run_session->setLastAccess(); - $this->activeRunSession = $run_session; - } - - if (!$output) { - return; - } - - global $title; - $css = $js = array(); - - if (isset($output['title'])) { - $title = $output['title']; - } else { - $title = $this->title ? $this->title : $this->name; - } - - if ($this->custom_css_path) { - $css[] = asset_url($this->custom_css_path); - } - if ($this->custom_js_path) { - $js[] = asset_url($this->custom_js_path); - } - - $run_content = ''; - - if (! $this->renderedDescAndFooterAlready && trim($this->description_parsed)) { - $run_content .= $this->description_parsed; - } - - if (isset($output['body'])) { - $run_content .= $output['body']; - } - if (! $this->renderedDescAndFooterAlready && trim($this->footer_text_parsed)) { - $run_content .= $this->footer_text_parsed; - } - - if ($run_session->isTesting()) { - $animal_end = strpos($user->user_code, "XXX"); - if ($animal_end === false) { - $animal_end = 10; - } - - //$js .= ''; - //$js[] = DEBUG ? asset_url('common/js/run_users.js') : asset_url('build/js/run_users.min.js'); - - $run_content .= Template::get('admin/run/monkey_bar', array( - 'user' => $user, - 'run' => $this, - 'run_session' => $run_session, - 'short_code' => substr($user->user_code, 0, $animal_end), - 'icon' => $user->created($this) ? "fa-user-md" : "fa-stethoscope", - 'disable_class' => $this->isFakeTestRun() ? " disabled " : "", - )); - } - - return array( - 'title' => $title, - 'css' => $css, - 'js' => $js, - 'run_session' => $run_session, - 'run_content' => $run_content, - 'run' => $this, - ); - } - - /** - * Export RUN units - * - * @param string $name The name that will be assigned to export - * @param array $units - * @param boolean $inc_survey Should survey data be included in export? - * @return mixed Returns an array of its two inputs. - */ - public function export($name, array $units, $inc_survey) { - $SPR = new SpreadsheetReader(); - // Save run units - foreach ($units as $i => &$unit) { - if ($inc_survey && $unit->type === 'Survey') { - $survey = Survey::loadById($unit->unit_id); - $unit->survey_data = $SPR->exportItemTableJSON($survey, true); - } - unset($unit->unit_id, $unit->run_unit_id); - } - // Save run settings - $settings = array( - 'header_image_path' => $this->header_image_path, - 'description' => $this->description, - 'footer_text' => $this->footer_text, - 'public_blurb' => $this->public_blurb, - 'cron_active' => (int) $this->cron_active, - 'custom_js' => $this->getCustomJS(), - 'custom_css' => $this->getCustomCSS(), - ); - - // save run files - $files = array(); - $uploads = $this->getUploadedFiles(); - foreach ($uploads as $file) { - $files[] = site_url('file_download/' . $this->id . '/' . $file['original_file_name']); - } - - $export = array( - 'name' => $name, - 'units' => array_values($units), - 'settings' => $settings, - 'files' => $files, - ); - return $export; - } - - /** - * Import a set of run units into current run by parsing a valid json string. - * Existing exported run units are read from configured dir $settings[run_exports_dir] - * Foreach unit item there is a check for at least for 'type' and 'position' attributes - * - * @param string $json_string JSON string of run units - * @param int $start_position Start position to be assigned to units. Defaults to 1. - * @return array Returns an array on rendered units indexed by position - */ - public function importUnits($json_string, $start_position = 0) { - ini_set('memory_limit', Config::get('memory_limit.run_import_units')); - if (!$start_position) { - $start_position = 0; - } else { - $start_position = (int) $start_position - 10; - } - $json = json_decode($json_string); - $existingUnits = $this->getAllUnitIds(); - if ($existingUnits) { - $last = end($existingUnits); - $start_position = $last['position'] + 10; - } - - if (empty($json->units)) { - alert("Error Invalid json string provided.", 'alert-danger'); - return false; - } - - $units = (array) $json->units; - $createdUnits = array(); - $runFactory = new RunUnitFactory(); - - foreach ($units as $unit) { - if (isset($unit->position) && !empty($unit->type)) { - $unit->position = $start_position + $unit->position; - // for some reason Endpage replaces Page - if (strpos($unit->type, 'page') !== false) { - $unit->type = 'Page'; - } - - if (strpos($unit->type, 'Survey') !== false) { - $unit->mock = true; - } - - if (strpos($unit->type, 'Skip') !== false) { - $unit->if_true = $unit->if_true + $start_position; - } - - if (strpos($unit->type, 'Email') !== false) { - $unit->account_id = null; - } - - $unitObj = $runFactory->make($this->dbh, null, (array) $unit, null, $this); - $unit = (array) $unit; - $unitObj->create($unit); - if ($unitObj->valid) { - $unitObj->addToRun($this->id, $unitObj->position, $unit); - // @todo check how to manage this because they are echoed only on next page load - //alert('Success. '.ucfirst($unitObj->type).' unit was created.','alert-success'); - $createdUnits[$unitObj->position] = $unitObj->displayForRun(Site::getInstance()->renderAlerts()); - } - } - } - - // try importing settings - if (!empty($json->settings)) { - $this->saveSettings((array) $json->settings); - } - return $createdUnits; - } - - protected function setExpireCookieUnits() { - $unit = secstofactor($this->expire_cookie); - if ($unit) { - $this->expire_cookie_unit = $unit[1]; - $this->expire_cookie_value = $unit[0]; - } - } - - public function getCookieName() { - return sprintf('FRS_%s', $this->id); - } + Session::delete('dummy_survey_session'); + endif; + return compact("output", "run_session"); + else: + alert("Error: Nothing to test-drive.", 'alert-danger'); + redirect_to("/index"); + return false; + endif; + } + + public function makeTestRunSession($testing = 1) { + $animal_name = AnimalName::haikunate(["tokenLength" => 0, "delimiter" => "",]) . "XXX"; + $animal_name = str_replace(" ", "", $animal_name); + $test_code = crypto_token(48 - floor(3 / 4 * strlen($animal_name))); + $test_code = $animal_name . substr($test_code, 0, 64 - strlen($animal_name)); + $run_session = new RunSession($this->dbh, $this->id, NULL, $test_code, $this); // does this user have a session? + $run_session->create($test_code, $testing); + + return $run_session; + } + + public function addNamedRunSession($name, $testing = 0) { + $name = str_replace(" ", "_", $name); + if ($name && !preg_match('/^[a-zA-Z0-9_-~]{0,32}$/', $name)) { + alert("Invalid characters in suggested name. Only a-z, numbers, _ - and ~ are allowed. Spaces are automatically replaced by a _.", 'alert-danger'); + return false; + } + + if ($name) { + $name .= 'XXX'; + } + + $new_code = crypto_token(48 - floor(3 / 4 * strlen($name))); + $new_code = $name . substr($new_code, 0, 64 - strlen($name)); + $run_session = new RunSession($this->dbh, $this->id, null, $new_code, $this); // does this user have a session? + $run_session->create($new_code, $testing); + + return $run_session; + } + + public function exec(User $user) { + if (!$this->valid) { + formr_error(404, 'Not Found', __("Run '%s' is broken or does not exist.", $this->name), 'Study Not Found'); + return false; + } elseif ($this->name == self::TEST_RUN) { + $test = $this->fakeTestRun(); + extract($test); + } else { + + $run_session = new RunSession($this->dbh, $this->id, $user->id, $user->user_code, $this); // does this user have a session? + + if (($this->getOwner()->user_code == $user->user_code || // owner always has access + $run_session->isTesting()) || // testers always have access + ($this->public >= 1 && $run_session->id) || // already enrolled + ($this->public >= 2)) { // anyone with link can access + if ($run_session->id === null) { + $run_session->create($user->user_code, (int) $user->created($this)); // generating access code for those who don't have it but need it + } + + Session::globalRefresh(); + $output = $run_session->getUnit(); + } else { + $output = $this->getServiceMessage()->exec(); + alert("Sorry: You cannot currently access this run.", 'alert-warning'); + } + + $run_session->setLastAccess(); + $this->activeRunSession = $run_session; + } + + if (!$output) { + return; + } + + global $title; + $css = $js = array(); + + if (isset($output['title'])) { + $title = $output['title']; + } else { + $title = $this->title ? $this->title : $this->name; + } + + if ($this->custom_css_path) { + $css[] = asset_url($this->custom_css_path); + } + if ($this->custom_js_path) { + $js[] = asset_url($this->custom_js_path); + } + + $run_content = ''; + + if (!$this->renderedDescAndFooterAlready && trim($this->description_parsed)) { + $run_content .= $this->description_parsed; + } + + if (isset($output['body'])) { + $run_content .= $output['body']; + } + if (!$this->renderedDescAndFooterAlready && trim($this->footer_text_parsed)) { + $run_content .= $this->footer_text_parsed; + } + + if ($run_session->isTesting()) { + $animal_end = strpos($user->user_code, "XXX"); + if ($animal_end === false) { + $animal_end = 10; + } + + //$js .= ''; + //$js[] = DEBUG ? asset_url('common/js/run_users.js') : asset_url('build/js/run_users.min.js'); + + $run_content .= Template::get('admin/run/monkey_bar', array( + 'user' => $user, + 'run' => $this, + 'run_session' => $run_session, + 'short_code' => substr($user->user_code, 0, $animal_end), + 'icon' => $user->created($this) ? "fa-user-md" : "fa-stethoscope", + 'disable_class' => $this->isFakeTestRun() ? " disabled " : "", + )); + } + + return array( + 'title' => $title, + 'css' => $css, + 'js' => $js, + 'run_session' => $run_session, + 'run_content' => $run_content, + 'run' => $this, + ); + } + + /** + * Export RUN units + * + * @param string $name The name that will be assigned to export + * @param array $units + * @param boolean $inc_survey Should survey data be included in export? + * @return mixed Returns an array of its two inputs. + */ + public function export($name, array $units, $inc_survey) { + $SPR = new SpreadsheetReader(); + // Save run units + foreach ($units as $i => &$unit) { + if ($inc_survey && $unit->type === 'Survey') { + $survey = Survey::loadById($unit->unit_id); + $unit->survey_data = $SPR->exportItemTableJSON($survey, true); + } + unset($unit->unit_id, $unit->run_unit_id); + } + // Save run settings + $settings = array( + 'header_image_path' => $this->header_image_path, + 'description' => $this->description, + 'footer_text' => $this->footer_text, + 'public_blurb' => $this->public_blurb, + 'cron_active' => (int) $this->cron_active, + 'custom_js' => $this->getCustomJS(), + 'custom_css' => $this->getCustomCSS(), + ); + + // save run files + $files = array(); + $uploads = $this->getUploadedFiles(); + foreach ($uploads as $file) { + $files[] = site_url('file_download/' . $this->id . '/' . $file['original_file_name']); + } + + $export = array( + 'name' => $name, + 'units' => array_values($units), + 'settings' => $settings, + 'files' => $files, + ); + return $export; + } + + /** + * Import a set of run units into current run by parsing a valid json string. + * Existing exported run units are read from configured dir $settings[run_exports_dir] + * Foreach unit item there is a check for at least for 'type' and 'position' attributes + * + * @param string $json_string JSON string of run units + * @param int $start_position Start position to be assigned to units. Defaults to 1. + * @return array Returns an array on rendered units indexed by position + */ + public function importUnits($json_string, $start_position = 0) { + ini_set('memory_limit', Config::get('memory_limit.run_import_units')); + if (!$start_position) { + $start_position = 0; + } else { + $start_position = (int) $start_position - 10; + } + $json = json_decode($json_string); + $existingUnits = $this->getAllUnitIds(); + if ($existingUnits) { + $last = end($existingUnits); + $start_position = $last['position'] + 10; + } + + if (empty($json->units)) { + alert("Error Invalid json string provided.", 'alert-danger'); + return false; + } + + $units = (array) $json->units; + $createdUnits = array(); + $runFactory = new RunUnitFactory(); + + foreach ($units as $unit) { + if (isset($unit->position) && !empty($unit->type)) { + $unit->position = $start_position + $unit->position; + // for some reason Endpage replaces Page + if (strpos($unit->type, 'page') !== false) { + $unit->type = 'Page'; + } + + if (strpos($unit->type, 'Survey') !== false) { + $unit->mock = true; + } + + if (strpos($unit->type, 'Skip') !== false) { + $unit->if_true = $unit->if_true + $start_position; + } + + if (strpos($unit->type, 'Email') !== false) { + $unit->account_id = null; + } + + $unitObj = $runFactory->make($this->dbh, null, (array) $unit, null, $this); + $unit = (array) $unit; + $unitObj->create($unit); + if ($unitObj->valid) { + $unitObj->addToRun($this->id, $unitObj->position, $unit); + // @todo check how to manage this because they are echoed only on next page load + //alert('Success. '.ucfirst($unitObj->type).' unit was created.','alert-success'); + $createdUnits[$unitObj->position] = $unitObj->displayForRun(Site::getInstance()->renderAlerts()); + } + } + } + + // try importing settings + if (!empty($json->settings)) { + $this->saveSettings((array) $json->settings); + } + return $createdUnits; + } + + protected function setExpireCookieUnits() { + $unit = secstofactor($this->expire_cookie); + if ($unit) { + $this->expire_cookie_unit = $unit[1]; + $this->expire_cookie_value = $unit[0]; + } + } + + public function getCookieName() { + return sprintf('FRS_%s', $this->id); + } } diff --git a/application/Model/RunSession.php b/application/Model/RunSession.php index 4fed9712e..25d297586 100644 --- a/application/Model/RunSession.php +++ b/application/Model/RunSession.php @@ -2,59 +2,59 @@ class RunSession { - public $id; - public $run_id; - public $user_id; - public $session = null; - public $created; - public $ended; - public $last_access; - public $position; - public $current_unit_id; - public $deactivated; - public $no_mail; - public $current_unit_type; - public $run_name; - public $run_owner_id; - public $run; - public $unit_session = false; - private $cron = false; - private $is_testing = false; - private $test_run = false; - - /** - * @var DB - */ - private $dbh; - - public function __construct($fdb, $run_id, $user_id, $session, $run = NULL) { - $this->dbh = $fdb; - $this->session = $session; - $this->run_id = $run_id; - $this->run = $run; - if ($user_id == 'cron') { - $this->cron = true; - } else { - $this->user_id = $user_id; - } - - if($run_id == -1) { - $this->test_run = true; - $this->id = -1; - $this->session = $session; - $this->run_id = $run_id; - $this->user_id = $user_id; - $this->run_name = $run->name; - $this->run_owner_id = $user_id; - $this->is_testing = true; - Site::getInstance()->setRunSession($this); - } else if ($this->session && $this->run_id) {// called with null in constructor if they have no session yet - $this->load(); - } - } - - private function load() { - $sess_array = $this->dbh->select(' + public $id; + public $run_id; + public $user_id; + public $session = null; + public $created; + public $ended; + public $last_access; + public $position; + public $current_unit_id; + public $deactivated; + public $no_mail; + public $current_unit_type; + public $run_name; + public $run_owner_id; + public $run; + public $unit_session = false; + private $cron = false; + private $is_testing = false; + private $test_run = false; + + /** + * @var DB + */ + private $dbh; + + public function __construct($fdb, $run_id, $user_id, $session, $run = NULL) { + $this->dbh = $fdb; + $this->session = $session; + $this->run_id = $run_id; + $this->run = $run; + if ($user_id == 'cron') { + $this->cron = true; + } else { + $this->user_id = $user_id; + } + + if ($run_id == -1) { + $this->test_run = true; + $this->id = -1; + $this->session = $session; + $this->run_id = $run_id; + $this->user_id = $user_id; + $this->run_name = $run->name; + $this->run_owner_id = $user_id; + $this->is_testing = true; + Site::getInstance()->setRunSession($this); + } else if ($this->session && $this->run_id) {// called with null in constructor if they have no session yet + $this->load(); + } + } + + private function load() { + $sess_array = $this->dbh->select(' `survey_run_sessions`.id, `survey_run_sessions`.session, `survey_run_sessions`.user_id, @@ -67,332 +67,333 @@ private function load() { `survey_run_sessions`.testing, `survey_runs`.name AS run_name, `survey_runs`.user_id AS run_owner_id') - ->from('survey_run_sessions') - ->leftJoin('survey_runs', 'survey_runs.id = survey_run_sessions.run_id') - ->where(array('run_id' => $this->run_id, 'session' => $this->session)) - ->limit(1)->fetch(); - - if ($sess_array) { - $this->id = $sess_array['id']; - $this->session = $sess_array['session']; - $this->run_id = $sess_array['run_id']; - $this->user_id = $sess_array['user_id']; - $this->created = $sess_array['created']; - $this->ended = $sess_array['ended']; - $this->position = $sess_array['position']; - $this->run_name = $sess_array['run_name']; - $this->run_owner_id = $sess_array['run_owner_id']; - $this->last_access = $sess_array['last_access']; - $this->no_mail = $sess_array['no_email']; - $this->is_testing = (bool)$sess_array['testing']; - - Site::getInstance()->setRunSession($this); - return true; - } - - return false; - } - - public function getLastAccess() { - return $this->dbh->findValue('survey_run_sessions', array('id' => $this->id), array('last_access')); - } - - public function setLastAccess() { - if (!$this->cron && (int)$this->id > 0) { - $this->dbh->update('survey_run_sessions', array('last_access' => mysql_now()), array('id' => (int)$this->id)); - } - } - - public function runAccessExpired() { - if (!$this->run || !($last_access = $this->getLastAccess())) { - return false; - } - - if (($timestamp = strtotime($last_access)) && $this->run->expire_cookie) { - return $timestamp + $this->run->expire_cookie < time(); - } - - return false; - } - - public function create($session = NULL, $testing = 0) { - if($this->run_id === -1) { - return false; - } - if ($session !== NULL) { - if (strlen($session) != 64) { - alert("Error. Session tokens need to be exactly 64 characters long.", 'alert-danger'); - return false; - } - } else { - $session = crypto_token(48); - } - - $this->dbh->insert_update('survey_run_sessions', array( - 'run_id' => $this->run_id, - 'user_id' => $this->user_id, - 'session' => $session, - 'created' => mysql_now(), - 'testing' => $testing - ), array('user_id')); - $this->session = $session; - return $this->load(); - } - - public function getUnit() { - $i = 0; - $done = array(); - $unit_factory = new RunUnitFactory(); - - $output = false; - while (!$output): // only when there is something to display, stop. - $i++; - if ($i > 80) { - global $user; - if ($user->isCron() || $user->isAdmin()) { - if (isset($unit)) - alert(print_r($unit, true), 'alert-danger'); - } - alert('Nesting too deep. Could there be an infinite loop or maybe no landing page?', 'alert-danger'); - return false; - } - - $unit_info = $this->getCurrentUnit(); // get first unit in line - if ($unit_info) { // if there is one, spin that shit - if ($this->cron) { - $unit_info['cron'] = true; - } - - $unit = $unit_factory->make($this->dbh, $this->session, $unit_info, $this, $this->run); - $this->current_unit_type = $unit->type; - $output = $unit->exec(); - - //@TODO check whether output is set or NOT - $queue = $this->unit_session->id && !$unit->ended && !$unit->expired; - if ($queue) { - $queued = UnitSessionQueue::addItem($this->unit_session, $unit, $output); - } - - if (!$output && is_object($unit)) { - if (!isset($done[$unit->type])) { - $done[$unit->type] = 0; - } - $done[$unit->type]++; - } - } else { - if (!$this->runToNextUnit()) { // if there is nothing in line yet, add the next one in run order - return array('body' => ''); // if that fails because the run is wrongly configured, return nothing - } - } - endwhile; - - if ($this->cron) { - return $done; - } - - return $output; - } - - public function getUnitIdAtPosition($position) { - $unit_id = $this->dbh->findValue('survey_run_units', array('run_id' => $this->run_id, 'position' => $position), 'unit_id'); - if (!$unit_id) { - return false; - } - return $unit_id; - } - - public function endUnitSession() { - $unit = $this->getCurrentUnit(); // get first unit in line - if ($unit) { - $unit_factory = new RunUnitFactory(); - $unit = $unit_factory->make($this->dbh, null, $unit, $this, $this->run); - if ($unit->type == "Survey" || $unit->type == "External") { - $unit->expire(); - } else { - $unit->end(); // cancel it - } - return true; - } - return false; - } - - public function forceTo($position) { - // If there a unit for current position, then end the unit's session before moving - if ($this->getUnitIdAtPosition($this->position)) { - $this->endUnitSession(); - } - return $this->runTo($position); - } - - public function runTo($position, $unit_id = null) { - if ($unit_id === null) { - $unit_id = $this->getUnitIdAtPosition($position); - } - - if ($unit_id): - - $this->unit_session = new UnitSession($this->dbh, $this->id, $unit_id); - if (!$this->unit_session->id) { - $this->unit_session->create(); - } - $_SESSION['session'] = $this->session; - - if ($this->unit_session->id): - $updated = $this->dbh->update('survey_run_sessions', array('position' => $position), array('id' => $this->id)); - $this->position = (int) $position; - return true; - else: - alert(__('Error. Could not create unit session for unit %s at pos. %s.', $unit_id, $position), 'alert-danger'); - endif; - elseif ($unit_id !== null AND $position): - alert(__('Error. The run position %s does not exist.', $position), 'alert-danger'); - else: - alert('Error. You tried to jump to a non-existing run position or forgot to specify one entirely.', 'alert-danger'); - endif; - return false; - } - - public function getCurrentUnit() { - $query = $this->dbh->select(' + ->from('survey_run_sessions') + ->leftJoin('survey_runs', 'survey_runs.id = survey_run_sessions.run_id') + ->where(array('run_id' => $this->run_id, 'session' => $this->session)) + ->limit(1)->fetch(); + + if ($sess_array) { + $this->id = $sess_array['id']; + $this->session = $sess_array['session']; + $this->run_id = $sess_array['run_id']; + $this->user_id = $sess_array['user_id']; + $this->created = $sess_array['created']; + $this->ended = $sess_array['ended']; + $this->position = $sess_array['position']; + $this->run_name = $sess_array['run_name']; + $this->run_owner_id = $sess_array['run_owner_id']; + $this->last_access = $sess_array['last_access']; + $this->no_mail = $sess_array['no_email']; + $this->is_testing = (bool) $sess_array['testing']; + + Site::getInstance()->setRunSession($this); + return true; + } + + return false; + } + + public function getLastAccess() { + return $this->dbh->findValue('survey_run_sessions', array('id' => $this->id), array('last_access')); + } + + public function setLastAccess() { + if (!$this->cron && (int) $this->id > 0) { + $this->dbh->update('survey_run_sessions', array('last_access' => mysql_now()), array('id' => (int) $this->id)); + } + } + + public function runAccessExpired() { + if (!$this->run || !($last_access = $this->getLastAccess())) { + return false; + } + + if (($timestamp = strtotime($last_access)) && $this->run->expire_cookie) { + return $timestamp + $this->run->expire_cookie < time(); + } + + return false; + } + + public function create($session = NULL, $testing = 0) { + if ($this->run_id === -1) { + return false; + } + if ($session !== NULL) { + if (strlen($session) != 64) { + alert("Error. Session tokens need to be exactly 64 characters long.", 'alert-danger'); + return false; + } + } else { + $session = crypto_token(48); + } + + $this->dbh->insert_update('survey_run_sessions', array( + 'run_id' => $this->run_id, + 'user_id' => $this->user_id, + 'session' => $session, + 'created' => mysql_now(), + 'testing' => $testing + ), array('user_id')); + $this->session = $session; + return $this->load(); + } + + public function getUnit() { + $i = 0; + $done = array(); + $unit_factory = new RunUnitFactory(); + + $output = false; + while (!$output): // only when there is something to display, stop. + $i++; + if ($i > 80) { + global $user; + if ($user->isCron() || $user->isAdmin()) { + if (isset($unit)) + alert(print_r($unit, true), 'alert-danger'); + } + alert('Nesting too deep. Could there be an infinite loop or maybe no landing page?', 'alert-danger'); + return false; + } + + $unit_info = $this->getCurrentUnit(); // get first unit in line + if ($unit_info) { // if there is one, spin that shit + if ($this->cron) { + $unit_info['cron'] = true; + } + + $unit = $unit_factory->make($this->dbh, $this->session, $unit_info, $this, $this->run); + $this->current_unit_type = $unit->type; + $output = $unit->exec(); + + //@TODO check whether output is set or NOT + $queue = $this->unit_session->id && !$unit->ended && !$unit->expired; + if ($queue) { + $queued = UnitSessionQueue::addItem($this->unit_session, $unit, $output); + } + + if (!$output && is_object($unit)) { + if (!isset($done[$unit->type])) { + $done[$unit->type] = 0; + } + $done[$unit->type] ++; + } + } else { + if (!$this->runToNextUnit()) { // if there is nothing in line yet, add the next one in run order + return array('body' => ''); // if that fails because the run is wrongly configured, return nothing + } + } + endwhile; + + if ($this->cron) { + return $done; + } + + return $output; + } + + public function getUnitIdAtPosition($position) { + $unit_id = $this->dbh->findValue('survey_run_units', array('run_id' => $this->run_id, 'position' => $position), 'unit_id'); + if (!$unit_id) { + return false; + } + return $unit_id; + } + + public function endUnitSession() { + $unit = $this->getCurrentUnit(); // get first unit in line + if ($unit) { + $unit_factory = new RunUnitFactory(); + $unit = $unit_factory->make($this->dbh, null, $unit, $this, $this->run); + if ($unit->type == "Survey" || $unit->type == "External") { + $unit->expire(); + } else { + $unit->end(); // cancel it + } + return true; + } + return false; + } + + public function forceTo($position) { + // If there a unit for current position, then end the unit's session before moving + if ($this->getUnitIdAtPosition($this->position)) { + $this->endUnitSession(); + } + return $this->runTo($position); + } + + public function runTo($position, $unit_id = null) { + if ($unit_id === null) { + $unit_id = $this->getUnitIdAtPosition($position); + } + + if ($unit_id): + + $this->unit_session = new UnitSession($this->dbh, $this->id, $unit_id); + if (!$this->unit_session->id) { + $this->unit_session->create(); + } + $_SESSION['session'] = $this->session; + + if ($this->unit_session->id): + $updated = $this->dbh->update('survey_run_sessions', array('position' => $position), array('id' => $this->id)); + $this->position = (int) $position; + return true; + else: + alert(__('Error. Could not create unit session for unit %s at pos. %s.', $unit_id, $position), 'alert-danger'); + endif; + elseif ($unit_id !== null AND $position): + alert(__('Error. The run position %s does not exist.', $position), 'alert-danger'); + else: + alert('Error. You tried to jump to a non-existing run position or forgot to specify one entirely.', 'alert-danger'); + endif; + return false; + } + + public function getCurrentUnit() { + $query = $this->dbh->select(' `survey_unit_sessions`.unit_id, `survey_unit_sessions`.id AS session_id, `survey_unit_sessions`.created, `survey_units`.type') - ->from('survey_unit_sessions') - ->leftJoin('survey_units', 'survey_unit_sessions.unit_id = survey_units.id') - ->where('survey_unit_sessions.run_session_id = :run_session_id') - ->where('survey_unit_sessions.unit_id = :unit_id') - ->where('survey_unit_sessions.ended IS NULL AND survey_unit_sessions.expired IS NULL') //so we know when to runToNextUnit - ->bindParams(array('run_session_id' => $this->id, 'unit_id' => $this->getUnitIdAtPosition($this->position))) - ->order('survey_unit_sessions`.id', 'desc') - ->limit(1); - - $unit = $query->fetch(); - - if ($unit) { - // unit needs: - # run_id - # run_name - # unit_id - # session_id - # run_session_id - # type - # session? - $unit['run_id'] = $this->run_id; - $unit['run_name'] = $this->run_name; - $unit['run_session_id'] = $this->id; - $this->unit_session = new UnitSession($this->dbh, $this->id, $unit['unit_id'], $unit['session_id']); - - return $unit; - } else { - return false; - } - } - - public function getUnitSession() { - if (!$this->unit_session) { - $this->getCurrentUnit(); - } - - $this->unit_session; - } - - public function runToNextUnit() { - $select = $this->dbh->select('unit_id, position') - ->from('survey_run_units') - ->where('run_id = :run_id') - ->where('position > :position') - ->order('position', 'asc') - ->limit(1); - - $position = -1000000; - if ($this->position !== null) { - $position = $this->position; - } - - $select->bindParams(array('run_id' => $this->run_id, 'position' => $position)); - $next = $select->fetch(); - if (!$next) { - alert('Run ' . $this->run_name . ': Oops, this study\'s creator forgot to give it a proper ending, user ' . h($this->session) . ' is dangling at the end.', 'alert-danger'); - return false; - } - return $this->runTo($next['position'], $next['unit_id']); - } - - public function endLastExternal() { - $query = " + ->from('survey_unit_sessions') + ->leftJoin('survey_units', 'survey_unit_sessions.unit_id = survey_units.id') + ->where('survey_unit_sessions.run_session_id = :run_session_id') + ->where('survey_unit_sessions.unit_id = :unit_id') + ->where('survey_unit_sessions.ended IS NULL AND survey_unit_sessions.expired IS NULL') //so we know when to runToNextUnit + ->bindParams(array('run_session_id' => $this->id, 'unit_id' => $this->getUnitIdAtPosition($this->position))) + ->order('survey_unit_sessions`.id', 'desc') + ->limit(1); + + $unit = $query->fetch(); + + if ($unit) { + // unit needs: + # run_id + # run_name + # unit_id + # session_id + # run_session_id + # type + # session? + $unit['run_id'] = $this->run_id; + $unit['run_name'] = $this->run_name; + $unit['run_session_id'] = $this->id; + $this->unit_session = new UnitSession($this->dbh, $this->id, $unit['unit_id'], $unit['session_id']); + + return $unit; + } else { + return false; + } + } + + public function getUnitSession() { + if (!$this->unit_session) { + $this->getCurrentUnit(); + } + + $this->unit_session; + } + + public function runToNextUnit() { + $select = $this->dbh->select('unit_id, position') + ->from('survey_run_units') + ->where('run_id = :run_id') + ->where('position > :position') + ->order('position', 'asc') + ->limit(1); + + $position = -1000000; + if ($this->position !== null) { + $position = $this->position; + } + + $select->bindParams(array('run_id' => $this->run_id, 'position' => $position)); + $next = $select->fetch(); + if (!$next) { + alert('Run ' . $this->run_name . ': Oops, this study\'s creator forgot to give it a proper ending, user ' . h($this->session) . ' is dangling at the end.', 'alert-danger'); + return false; + } + return $this->runTo($next['position'], $next['unit_id']); + } + + public function endLastExternal() { + $query = " UPDATE `survey_unit_sessions` LEFT JOIN `survey_units` ON `survey_unit_sessions`.unit_id = `survey_units`.id SET `survey_unit_sessions`.`ended` = NOW() WHERE `survey_unit_sessions`.run_session_id = :id AND `survey_units`.type = 'External' AND `survey_unit_sessions`.ended IS NULL AND `survey_unit_sessions`.expired IS NULL;"; - $updated = $this->dbh->exec($query, array('id' => $this->id)); - $success = $updated !== false; - return $success; - } - - public function end() { - $query = "UPDATE `survey_run_sessions` SET `ended` = NOW() WHERE `id` = :id AND `ended` IS NULL"; - $updated = $this->dbh->exec($query, array('id' => $this->id)); - - if ($updated === 1) { - $this->ended = true; - return true; - } - - return false; - } - - public function setTestingStatus($status = 0) { - $this->dbh->update("survey_run_sessions", array('testing' => $status), array('id' => $this->id)); - } - - public function isTesting() { - return $this->is_testing; - } - public function isCron() { - return $this->cron; - } - - /** - * Check if current run session is a test - * - * @param User $user - * @return boolean True if current user in run is testing. False otherwise - */ - public function isTest(User $user) { - return $this->run_owner_id == $user->id; - } - - public function __sleep() { - return array('id', 'session', 'run_id'); - } - - public function saveSettings($settings, $update = null) { - if (!empty($update)) { - $this->dbh->update('survey_run_sessions', $update, array('id' => $this->id)); - } - - $oldSettings = $this->getSettings(); - unset($oldSettings['code']); - if ($oldSettings) { - $settings = array_merge($oldSettings, $settings); - } - - $this->dbh->insert_update('survey_run_settings', array( - 'run_session_id' => $this->id, - 'settings' => json_encode($settings), - )); - } - - public function getSettings() { - $settings = array(); - $row = $this->dbh->findRow('survey_run_settings', array('run_session_id' => $this->id)); - if ($row) { - $settings = (array)json_decode($row['settings']); - } - $settings['code'] = $this->session; - return $settings; - } + $updated = $this->dbh->exec($query, array('id' => $this->id)); + $success = $updated !== false; + return $success; + } + + public function end() { + $query = "UPDATE `survey_run_sessions` SET `ended` = NOW() WHERE `id` = :id AND `ended` IS NULL"; + $updated = $this->dbh->exec($query, array('id' => $this->id)); + + if ($updated === 1) { + $this->ended = true; + return true; + } + + return false; + } + + public function setTestingStatus($status = 0) { + $this->dbh->update("survey_run_sessions", array('testing' => $status), array('id' => $this->id)); + } + + public function isTesting() { + return $this->is_testing; + } + + public function isCron() { + return $this->cron; + } + + /** + * Check if current run session is a test + * + * @param User $user + * @return boolean True if current user in run is testing. False otherwise + */ + public function isTest(User $user) { + return $this->run_owner_id == $user->id; + } + + public function __sleep() { + return array('id', 'session', 'run_id'); + } + + public function saveSettings($settings, $update = null) { + if (!empty($update)) { + $this->dbh->update('survey_run_sessions', $update, array('id' => $this->id)); + } + + $oldSettings = $this->getSettings(); + unset($oldSettings['code']); + if ($oldSettings) { + $settings = array_merge($oldSettings, $settings); + } + + $this->dbh->insert_update('survey_run_settings', array( + 'run_session_id' => $this->id, + 'settings' => json_encode($settings), + )); + } + + public function getSettings() { + $settings = array(); + $row = $this->dbh->findRow('survey_run_settings', array('run_session_id' => $this->id)); + if ($row) { + $settings = (array) json_decode($row['settings']); + } + $settings['code'] = $this->session; + return $settings; + } } diff --git a/application/Model/RunUnit.php b/application/Model/RunUnit.php index 59575375c..2b701363e 100644 --- a/application/Model/RunUnit.php +++ b/application/Model/RunUnit.php @@ -2,829 +2,821 @@ class RunUnitFactory { - protected $supported = array('Survey', 'Pause', 'Email', 'External', 'Page', 'SkipBackward', 'SkipForward', 'Shuffle'); - - /** - * Create a RunUnit object based on supported types - * - * @param DB $dbh - * @param string $session - * @param array $unit - * @param RunSession $run_session - * @param Run $run - * @return RunUnit - * @throws Exception - */ - public function make($dbh, $session, $unit, $run_session = NULL, $run = NULL) { - if (empty($unit['type'])) { - $unit['type'] = 'Survey'; - } - $type = $unit['type']; - - if (!in_array($type, $this->supported)) { - throw new Exception("Unsupported unit type '$type'"); - } - - return new $type($dbh, $session, $unit, $run_session, $run); - } - - public function getSupportedUnits() { - return $this->supported; - } + protected $supported = array('Survey', 'Pause', 'Email', 'External', 'Page', 'SkipBackward', 'SkipForward', 'Shuffle'); + + /** + * Create a RunUnit object based on supported types + * + * @param DB $dbh + * @param string $session + * @param array $unit + * @param RunSession $run_session + * @param Run $run + * @return RunUnit + * @throws Exception + */ + public function make($dbh, $session, $unit, $run_session = NULL, $run = NULL) { + if (empty($unit['type'])) { + $unit['type'] = 'Survey'; + } + $type = $unit['type']; + + if (!in_array($type, $this->supported)) { + throw new Exception("Unsupported unit type '$type'"); + } + + return new $type($dbh, $session, $unit, $run_session, $run); + } + + public function getSupportedUnits() { + return $this->supported; + } } class RunUnit { - public $errors = array(); - public $id = null; - public $user_id = null; - public $run_unit_id = null; // this is the ID of the unit-to-run-link entry - public $session = null; - public $unit = null; - public $ended = false; - public $expired = false; - public $position; - public $called_by_cron = false; - public $knitr = false; - public $session_id = null; - public $run_session_id = null; - public $type = ''; - public $icon = 'fa-wrench'; - public $special = false; - public $valid; - public $run_id; - public $description = ""; - protected $had_major_changes = false; - - /** - * An array of unit's exportable attributes - * @var array - */ - public $export_attribs = array('type', 'description', 'position', 'special'); - - /** - * @var RunSession - */ - public $run_session; - - /** - * @var Run - */ - public $run; - - /** - * parsed body if unit - * @var string - */ - protected $body_parsed = null; - - /** - * Array of tables that contain user/study data to be used when parsing variables - * indexed by table names with values being an array of columns of interest - * - * @var array - */ - protected $non_user_tables = array( - 'survey_users' => array("created","modified", "user_code","email","email_verified","mobile_number", "mobile_verified"), - 'survey_run_sessions' => array("session","created","last_access","position","current_unit_id", "deactivated","no_email"), - 'survey_unit_sessions' => array("created","ended",'expired',"unit_id", "position", "type"), - 'externals' => array("created","ended",'expired', "position"), - 'survey_items_display' => array("created","answered_time","answered","displaycount","item_id"), - 'survey_email_log' => array("email_id","created","recipient"), - 'shuffle' => array("unit_id","created","group"), - ); - - /** - * Array of tables that contain system session data to be used when parsing variables - * - * @var array - */ - protected $non_session_tables = array('survey_users', 'survey_run_sessions', 'survey_unit_sessions'); - - /** - * collects strings to parse on opencpu before rendering the unit - * @var array - */ - protected $strings_to_parse = array(); - - /** - * @var DB - */ - protected $dbh; - - /** - * To hold temporary data gathered during unit execution. - * This data can then be used after execution e.g. during queueing. - * - * @var array - */ - public $execData = array(); - - - public function __construct($fdb, $session = null, $unit = null, $run_session = null, $run = NULL) { - $this->dbh = $fdb; - $this->session = $session; - $this->unit = $unit; - $this->run_session = $run_session; - $this->run = $run; - - if (isset($unit['run_id'])) { - $this->run_id = $unit['run_id']; - } - - if (isset($unit['run_unit_id'])) { - $this->run_unit_id = $unit['run_unit_id']; - } elseif (isset($unit['id'])) { - $this->run_unit_id = $unit['id']; - } - - if (isset($unit['run_name'])) { - $this->run_name = $unit['run_name']; - } - - if (isset($unit['session_id'])) { - $this->session_id = $unit['session_id']; - } - - if (isset($unit['run_session_id'])) { - $this->run_session_id = $unit['run_session_id']; - } - - if (isset($this->unit['unit_id'])) { - $this->id = $this->unit['unit_id']; - $vars = $this->dbh->findRow('survey_units', array('id' => $this->id), 'created, modified, type'); - if ($vars): - $this->modified = $vars['modified']; - $this->created = $vars['created']; - endif; - } - - if (isset($this->unit['position'])) { - $this->position = (int) $this->unit['position']; - } - if (isset($this->unit['description'])) { - $this->description = $this->unit['description']; - } - - if (isset($this->unit['special'])) { - $this->special = $this->unit['special']; - } - - if (isset($this->unit['cron'])) { - $this->called_by_cron = true; - } - } - - public function create($type) { - $id = $this->dbh->insert('survey_units', array( - 'type' => $type, - 'created' => mysql_now(), - 'modified' => mysql_now(), - )); - $this->unit_id = $id; - return $this->unit_id; - } - - public function modify($options = array()) { - $change = array('modified' => mysql_now()); - $table = empty($options['special']) ? 'survey_run_units' : 'survey_run_special_units'; - if($this->run_unit_id && isset($options['description'])): - $this->dbh->update($table, array("description" => $options['description']), array('id' => $this->run_unit_id )); - $this->description = $options['description']; - endif; - return $this->dbh->update('survey_units', $change, array('id' => $this->id)); - } - - protected function beingTestedByOwner() { - if ($this->run_session === null OR $this->run_session->user_id == $this->run_session->run_owner_id) { - return true; - } - return false; - } - - public function linkToRun() { - // todo: set run modified - return $this->dbh->update('survey_run_units', - array('unit_id' => $this->id), - array('id' => $this->run_unit_id), - array('int'), array('int') - ); - } - - public function addToRun($run_id, $position = 10, $options = array("description" => '')) { - // todo: set run modified - if (!is_numeric($position)) { - $position = 10; - } - $this->position = (int) $position; - if(!isset($options['description'])) { - $options['description'] = ''; - } - - if ($this->special) { - return $this->dbh->insert('survey_run_special_units', array( - 'id' => $this->id, - 'run_id' => $run_id, - 'type' => $this->special, - 'description' => $options['description'] - )); - } - $this->run_unit_id = $this->dbh->insert('survey_run_units', array( - 'unit_id' => $this->id, - 'run_id' => $run_id, - 'position' => $position, - 'description' => $options['description'] - )); - return $this->run_unit_id; - } - - public function removeFromRun($special = null) { - // todo: set run modified - if ($special !== null) { - return $this->dbh->delete('survey_run_special_units', array('id' => $this->run_unit_id, 'type' => $special)); - } else { - return $this->dbh->delete('survey_run_units', array('id' => $this->run_unit_id)); - } - } - - public function delete($special = null) { - // todo: set run modified - if ($special !== null) { - return $this->dbh->delete('survey_run_special_units', array('id' => $this->run_unit_id, 'type' => $special)); - } else { - $affected = $this->dbh->delete('survey_units', array('id' => $this->id)); - if ($affected) { // remove from all runs - $affected += $this->dbh->delete('survey_run_units', array('unit_id' => $this->id)); - } - } - return $affected; - } - - public function end() { // todo: logically this should be part of the Unit Session Model, but I messed up my logic somehow - $ended = $this->dbh->exec( + public $errors = array(); + public $id = null; + public $user_id = null; + public $run_unit_id = null; // this is the ID of the unit-to-run-link entry + public $session = null; + public $unit = null; + public $ended = false; + public $expired = false; + public $position; + public $called_by_cron = false; + public $knitr = false; + public $session_id = null; + public $run_session_id = null; + public $type = ''; + public $icon = 'fa-wrench'; + public $special = false; + public $valid; + public $run_id; + public $description = ""; + protected $had_major_changes = false; + + /** + * An array of unit's exportable attributes + * @var array + */ + public $export_attribs = array('type', 'description', 'position', 'special'); + + /** + * @var RunSession + */ + public $run_session; + + /** + * @var Run + */ + public $run; + + /** + * parsed body if unit + * @var string + */ + protected $body_parsed = null; + + /** + * Array of tables that contain user/study data to be used when parsing variables + * indexed by table names with values being an array of columns of interest + * + * @var array + */ + protected $non_user_tables = array( + 'survey_users' => array("created", "modified", "user_code", "email", "email_verified", "mobile_number", "mobile_verified"), + 'survey_run_sessions' => array("session", "created", "last_access", "position", "current_unit_id", "deactivated", "no_email"), + 'survey_unit_sessions' => array("created", "ended", 'expired', "unit_id", "position", "type"), + 'externals' => array("created", "ended", 'expired', "position"), + 'survey_items_display' => array("created", "answered_time", "answered", "displaycount", "item_id"), + 'survey_email_log' => array("email_id", "created", "recipient"), + 'shuffle' => array("unit_id", "created", "group"), + ); + + /** + * Array of tables that contain system session data to be used when parsing variables + * + * @var array + */ + protected $non_session_tables = array('survey_users', 'survey_run_sessions', 'survey_unit_sessions'); + + /** + * collects strings to parse on opencpu before rendering the unit + * @var array + */ + protected $strings_to_parse = array(); + + /** + * @var DB + */ + protected $dbh; + + /** + * To hold temporary data gathered during unit execution. + * This data can then be used after execution e.g. during queueing. + * + * @var array + */ + public $execData = array(); + + public function __construct($fdb, $session = null, $unit = null, $run_session = null, $run = NULL) { + $this->dbh = $fdb; + $this->session = $session; + $this->unit = $unit; + $this->run_session = $run_session; + $this->run = $run; + + if (isset($unit['run_id'])) { + $this->run_id = $unit['run_id']; + } + + if (isset($unit['run_unit_id'])) { + $this->run_unit_id = $unit['run_unit_id']; + } elseif (isset($unit['id'])) { + $this->run_unit_id = $unit['id']; + } + + if (isset($unit['run_name'])) { + $this->run_name = $unit['run_name']; + } + + if (isset($unit['session_id'])) { + $this->session_id = $unit['session_id']; + } + + if (isset($unit['run_session_id'])) { + $this->run_session_id = $unit['run_session_id']; + } + + if (isset($this->unit['unit_id'])) { + $this->id = $this->unit['unit_id']; + $vars = $this->dbh->findRow('survey_units', array('id' => $this->id), 'created, modified, type'); + if ($vars): + $this->modified = $vars['modified']; + $this->created = $vars['created']; + endif; + } + + if (isset($this->unit['position'])) { + $this->position = (int) $this->unit['position']; + } + if (isset($this->unit['description'])) { + $this->description = $this->unit['description']; + } + + if (isset($this->unit['special'])) { + $this->special = $this->unit['special']; + } + + if (isset($this->unit['cron'])) { + $this->called_by_cron = true; + } + } + + public function create($type) { + $id = $this->dbh->insert('survey_units', array( + 'type' => $type, + 'created' => mysql_now(), + 'modified' => mysql_now(), + )); + $this->unit_id = $id; + return $this->unit_id; + } + + public function modify($options = array()) { + $change = array('modified' => mysql_now()); + $table = empty($options['special']) ? 'survey_run_units' : 'survey_run_special_units'; + if ($this->run_unit_id && isset($options['description'])): + $this->dbh->update($table, array("description" => $options['description']), array('id' => $this->run_unit_id)); + $this->description = $options['description']; + endif; + return $this->dbh->update('survey_units', $change, array('id' => $this->id)); + } + + protected function beingTestedByOwner() { + if ($this->run_session === null OR $this->run_session->user_id == $this->run_session->run_owner_id) { + return true; + } + return false; + } + + public function linkToRun() { + // todo: set run modified + return $this->dbh->update('survey_run_units', array('unit_id' => $this->id), array('id' => $this->run_unit_id), array('int'), array('int') + ); + } + + public function addToRun($run_id, $position = 10, $options = array("description" => '')) { + // todo: set run modified + if (!is_numeric($position)) { + $position = 10; + } + $this->position = (int) $position; + if (!isset($options['description'])) { + $options['description'] = ''; + } + + if ($this->special) { + return $this->dbh->insert('survey_run_special_units', array( + 'id' => $this->id, + 'run_id' => $run_id, + 'type' => $this->special, + 'description' => $options['description'] + )); + } + $this->run_unit_id = $this->dbh->insert('survey_run_units', array( + 'unit_id' => $this->id, + 'run_id' => $run_id, + 'position' => $position, + 'description' => $options['description'] + )); + return $this->run_unit_id; + } + + public function removeFromRun($special = null) { + // todo: set run modified + if ($special !== null) { + return $this->dbh->delete('survey_run_special_units', array('id' => $this->run_unit_id, 'type' => $special)); + } else { + return $this->dbh->delete('survey_run_units', array('id' => $this->run_unit_id)); + } + } + + public function delete($special = null) { + // todo: set run modified + if ($special !== null) { + return $this->dbh->delete('survey_run_special_units', array('id' => $this->run_unit_id, 'type' => $special)); + } else { + $affected = $this->dbh->delete('survey_units', array('id' => $this->id)); + if ($affected) { // remove from all runs + $affected += $this->dbh->delete('survey_run_units', array('unit_id' => $this->id)); + } + } + return $affected; + } + + public function end() { // todo: logically this should be part of the Unit Session Model, but I messed up my logic somehow + $ended = $this->dbh->exec( "UPDATE `survey_unit_sessions` SET `ended` = NOW() WHERE `id` = :session_id AND `unit_id` = :unit_id AND `ended` IS NULL LIMIT 1", array('session_id' => $this->session_id, 'unit_id' => $this->id) - ); + ); - if ($ended === 1) { - $this->ended = true; - UnitSessionQueue::removeItem($this->session_id, $this->id); - return true; - } + if ($ended === 1) { + $this->ended = true; + UnitSessionQueue::removeItem($this->session_id, $this->id); + return true; + } - return false; - } + return false; + } - public function expire() { // todo: logically this should be part of the Unit Session Model, but I messed up my logic somehow - $expired = $this->dbh->exec( + public function expire() { // todo: logically this should be part of the Unit Session Model, but I messed up my logic somehow + $expired = $this->dbh->exec( "UPDATE `survey_unit_sessions` SET `expired` = NOW() WHERE `id` = :session_id AND `unit_id` = :unit_id AND `ended` IS NULL LIMIT 1", array('session_id' => $this->session_id, 'unit_id' => $this->id) - ); - - if ($expired === 1) { - $this->expired = true; - UnitSessionQueue::removeItem($this->session_id, $this->id); - return true; - } - - return false; - } - - protected function getSampleSessions() { - $current_position = -9999999; - if (isset($this->unit['position'])) { - $current_position = $this->unit['position']; - } - $results = $this->dbh->select('session, id, position') - ->from('survey_run_sessions') - ->order('position', 'desc')->order('RAND') - ->where(array('run_id' => $this->run_id, 'position >=' => $current_position)) - ->limit(20)->fetchAll(); - - if (!$results) { - alert('No data to compare to yet. Create some test data by sending guinea pigs through the run using the "Test run" function on the left.','alert-info'); - return false; - } - return $results; - } - - protected function grabRandomSession() { - if ($this->run_session_id === NULL) { - $current_position = -9999999; - if (isset($this->unit['position'])) { - $current_position = $this->unit['position']; - } - - $temp_user = $this->dbh->select('session, id, position') - ->from('survey_run_sessions') - ->order('position', 'desc')->order('RAND') - ->where(array('run_id' => $this->run_id, 'position >=' => $current_position)) - ->limit(1) - ->fetch(); - - if (!$temp_user) { - alert('No data to compare to yet. Create some test data by sending guinea pigs through the run using the "Test run" function on the left.','alert-info'); - return false; - } - - $this->run_session_id = $temp_user['id']; - } - - return $this->run_session_id; - } - - public function howManyReachedItNumbers() { - $reached = $this->dbh->select(array('SUM(`survey_unit_sessions`.ended IS NULL AND `survey_unit_sessions`.expired IS NULL)' => 'begun', 'SUM(`survey_unit_sessions`.ended IS NOT NULL)' => 'finished', 'SUM(`survey_unit_sessions`.expired IS NOT NULL)' => 'expired')) - ->from('survey_unit_sessions') - ->leftJoin('survey_run_sessions', 'survey_run_sessions.id = survey_unit_sessions.run_session_id') - ->where('survey_unit_sessions.unit_id = :unit_id') - ->where('survey_run_sessions.run_id = :run_id') - ->bindParams(array('unit_id' => $this->id, 'run_id' => $this->run_id)) - ->fetch(); - - return $reached; - } - - public function howManyReachedIt() { - $reached = $this->howManyReachedItNumbers(); - if ($reached['begun'] === "0") { - $reached['begun'] = ""; - } - if ($reached['finished'] === "0") { - $reached['finished'] = ""; - } - if ($reached['expired'] === "0") { - $reached['expired'] = ""; - } - return " - " . $reached['begun'] . " - " . $reached['expired'] . " - " . $reached['finished'] . " + ); + + if ($expired === 1) { + $this->expired = true; + UnitSessionQueue::removeItem($this->session_id, $this->id); + return true; + } + + return false; + } + + protected function getSampleSessions() { + $current_position = -9999999; + if (isset($this->unit['position'])) { + $current_position = $this->unit['position']; + } + $results = $this->dbh->select('session, id, position') + ->from('survey_run_sessions') + ->order('position', 'desc')->order('RAND') + ->where(array('run_id' => $this->run_id, 'position >=' => $current_position)) + ->limit(20)->fetchAll(); + + if (!$results) { + alert('No data to compare to yet. Create some test data by sending guinea pigs through the run using the "Test run" function on the left.', 'alert-info'); + return false; + } + return $results; + } + + protected function grabRandomSession() { + if ($this->run_session_id === NULL) { + $current_position = -9999999; + if (isset($this->unit['position'])) { + $current_position = $this->unit['position']; + } + + $temp_user = $this->dbh->select('session, id, position') + ->from('survey_run_sessions') + ->order('position', 'desc')->order('RAND') + ->where(array('run_id' => $this->run_id, 'position >=' => $current_position)) + ->limit(1) + ->fetch(); + + if (!$temp_user) { + alert('No data to compare to yet. Create some test data by sending guinea pigs through the run using the "Test run" function on the left.', 'alert-info'); + return false; + } + + $this->run_session_id = $temp_user['id']; + } + + return $this->run_session_id; + } + + public function howManyReachedItNumbers() { + $reached = $this->dbh->select(array('SUM(`survey_unit_sessions`.ended IS NULL AND `survey_unit_sessions`.expired IS NULL)' => 'begun', 'SUM(`survey_unit_sessions`.ended IS NOT NULL)' => 'finished', 'SUM(`survey_unit_sessions`.expired IS NOT NULL)' => 'expired')) + ->from('survey_unit_sessions') + ->leftJoin('survey_run_sessions', 'survey_run_sessions.id = survey_unit_sessions.run_session_id') + ->where('survey_unit_sessions.unit_id = :unit_id') + ->where('survey_run_sessions.run_id = :run_id') + ->bindParams(array('unit_id' => $this->id, 'run_id' => $this->run_id)) + ->fetch(); + + return $reached; + } + + public function howManyReachedIt() { + $reached = $this->howManyReachedItNumbers(); + if ($reached['begun'] === "0") { + $reached['begun'] = ""; + } + if ($reached['finished'] === "0") { + $reached['finished'] = ""; + } + if ($reached['expired'] === "0") { + $reached['expired'] = ""; + } + return " + " . $reached['begun'] . " + " . $reached['expired'] . " + " . $reached['finished'] . " "; - } - - public function runDialog($dialog) { - $tpl = $this->getUnitTemplatePath('unit'); - return Template::get($tpl, array('dialog' => $dialog, 'unit' => $this)); - } - - public function hadMajorChanges() { - return $this->had_major_changes; - } - protected function majorChange() { - $this->had_major_changes = true; - } - - public function displayForRun($prepend = '') { - return $this->runDialog($prepend); // FIXME: This class has no parent - } - - protected $survey_results; - - /** - * Get user data needed to execute a query/request (mainly used in opencpu requests) - * - * @param string $q - * @param string $required - * @return array - */ - public function getUserDataInRun($q, $required = null) { - $cache_key = Cache::makeKey($q, $required, $this->session_id, $this->run_session_id); - if (($data = Cache::get($cache_key))) { - return $data; - } - - $needed = $this->dataNeeded($q, $required); - $surveys = $needed['matches']; - $results_tables = $needed['matches_results_tables']; - $matches_variable_names = $needed['matches_variable_names']; - $this->survey_results = array('datasets' => array()); - - foreach($surveys AS $study_id => $survey_name) { - if (isset($this->survey_results['datasets'][$survey_name])) { - continue; - } - - $results_table = $results_tables[$survey_name]; - $variables = array(); - if(empty($matches_variable_names[ $survey_name ])) { - $variables[] = "NULL AS formr_dummy"; - } else { - if($results_table === "survey_unit_sessions") { - if(($key = array_search('position', $matches_variable_names[$survey_name])) !== false) { - unset($matches_variable_names[$survey_name][$key]); - $variables[] = '`survey_run_units`.`position`'; - } - if(($key = array_search('type', $matches_variable_names[$survey_name])) !== false) { - unset($matches_variable_names[$survey_name][$key]); - $variables[] = '`survey_units`.`type`'; - } - } - - if (!empty($matches_variable_names[$survey_name])) { - foreach ($matches_variable_names[$survey_name] as $k => $v) { - $variables[] = DB::quoteCol($v, $results_table); - } - } - } - - $variables = implode(', ', $variables); - $select = "SELECT $variables"; - if($this->run_session_id === NULL AND !in_array($results_table, $this->non_session_tables)) { // todo: what to do with session_id tables in faketestrun - $where = " WHERE `$results_table`.session_id = :session_id"; // just for testing surveys - } else { - $where = " WHERE `survey_run_sessions`.id = :run_session_id"; - if($survey_name === "externals") { - $where .= " AND `survey_units`.`type` = 'External'"; - } - } - - if(!in_array($results_table, $this->non_session_tables )) { - $joins = " + } + + public function runDialog($dialog) { + $tpl = $this->getUnitTemplatePath('unit'); + return Template::get($tpl, array('dialog' => $dialog, 'unit' => $this)); + } + + public function hadMajorChanges() { + return $this->had_major_changes; + } + + protected function majorChange() { + $this->had_major_changes = true; + } + + public function displayForRun($prepend = '') { + return $this->runDialog($prepend); // FIXME: This class has no parent + } + + protected $survey_results; + + /** + * Get user data needed to execute a query/request (mainly used in opencpu requests) + * + * @param string $q + * @param string $required + * @return array + */ + public function getUserDataInRun($q, $required = null) { + $cache_key = Cache::makeKey($q, $required, $this->session_id, $this->run_session_id); + if (($data = Cache::get($cache_key))) { + return $data; + } + + $needed = $this->dataNeeded($q, $required); + $surveys = $needed['matches']; + $results_tables = $needed['matches_results_tables']; + $matches_variable_names = $needed['matches_variable_names']; + $this->survey_results = array('datasets' => array()); + + foreach ($surveys AS $study_id => $survey_name) { + if (isset($this->survey_results['datasets'][$survey_name])) { + continue; + } + + $results_table = $results_tables[$survey_name]; + $variables = array(); + if (empty($matches_variable_names[$survey_name])) { + $variables[] = "NULL AS formr_dummy"; + } else { + if ($results_table === "survey_unit_sessions") { + if (($key = array_search('position', $matches_variable_names[$survey_name])) !== false) { + unset($matches_variable_names[$survey_name][$key]); + $variables[] = '`survey_run_units`.`position`'; + } + if (($key = array_search('type', $matches_variable_names[$survey_name])) !== false) { + unset($matches_variable_names[$survey_name][$key]); + $variables[] = '`survey_units`.`type`'; + } + } + + if (!empty($matches_variable_names[$survey_name])) { + foreach ($matches_variable_names[$survey_name] as $k => $v) { + $variables[] = DB::quoteCol($v, $results_table); + } + } + } + + $variables = implode(', ', $variables); + $select = "SELECT $variables"; + if ($this->run_session_id === NULL AND ! in_array($results_table, $this->non_session_tables)) { // todo: what to do with session_id tables in faketestrun + $where = " WHERE `$results_table`.session_id = :session_id"; // just for testing surveys + } else { + $where = " WHERE `survey_run_sessions`.id = :run_session_id"; + if ($survey_name === "externals") { + $where .= " AND `survey_units`.`type` = 'External'"; + } + } + + if (!in_array($results_table, $this->non_session_tables)) { + $joins = " LEFT JOIN `survey_unit_sessions` ON `$results_table`.session_id = `survey_unit_sessions`.id LEFT JOIN `survey_run_sessions` ON `survey_run_sessions`.id = `survey_unit_sessions`.run_session_id "; - } elseif($results_table == 'survey_unit_sessions'){ - $joins = "LEFT JOIN `survey_run_sessions` ON `survey_run_sessions`.id = `survey_unit_sessions`.run_session_id + } elseif ($results_table == 'survey_unit_sessions') { + $joins = "LEFT JOIN `survey_run_sessions` ON `survey_run_sessions`.id = `survey_unit_sessions`.run_session_id LEFT JOIN `survey_units` ON `survey_unit_sessions`.unit_id = `survey_units`.id LEFT JOIN `survey_run_units` ON `survey_unit_sessions`.unit_id = `survey_run_units`.unit_id LEFT JOIN `survey_runs` ON `survey_runs`.id = `survey_run_units`.run_id "; - $where .= " AND `survey_runs`.id = :run_id"; - } elseif($results_table == 'survey_run_sessions') { - $joins = ""; - } elseif($results_table == 'survey_users') { - $joins = "LEFT JOIN `survey_run_sessions` ON `survey_users`.id = `survey_run_sessions`.user_id"; - } - - $select .= " FROM `$results_table` "; - - $q = $select . $joins . $where . ";"; - $get_results = $this->dbh->prepare($q); - if($this->run_session_id === NULL) { - $get_results->bindValue(':session_id', $this->session_id); - } else { - $get_results->bindValue(':run_session_id', $this->run_session_id); - } - if($results_table == 'survey_unit_sessions') { - $get_results->bindValue(':run_id', $this->run_id); - } - $get_results->execute(); - - $this->survey_results['datasets'][$survey_name] = array(); - while($res = $get_results->fetch(PDO::FETCH_ASSOC)) { - foreach($res AS $var => $val) { - if(!isset($this->survey_results['datasets'][$survey_name][$var])) { - $this->survey_results['datasets'][$survey_name][$var] = array(); - } - $this->survey_results['datasets'][$survey_name][$var][] = $val; - } - } - } - - if(!empty($needed['variables'])) { - if(in_array('formr_last_action_date', $needed['variables']) || in_array('formr_last_action_time', $needed['variables'])) { - $this->survey_results['.formr$last_action_date'] = "NA"; - $this->survey_results['.formr$last_action_time'] = "NA"; - $last_action = $this->dbh->execute( - "SELECT `survey_unit_sessions`.`created` FROM `survey_unit_sessions` + $where .= " AND `survey_runs`.id = :run_id"; + } elseif ($results_table == 'survey_run_sessions') { + $joins = ""; + } elseif ($results_table == 'survey_users') { + $joins = "LEFT JOIN `survey_run_sessions` ON `survey_users`.id = `survey_run_sessions`.user_id"; + } + + $select .= " FROM `$results_table` "; + + $q = $select . $joins . $where . ";"; + $get_results = $this->dbh->prepare($q); + if ($this->run_session_id === NULL) { + $get_results->bindValue(':session_id', $this->session_id); + } else { + $get_results->bindValue(':run_session_id', $this->run_session_id); + } + if ($results_table == 'survey_unit_sessions') { + $get_results->bindValue(':run_id', $this->run_id); + } + $get_results->execute(); + + $this->survey_results['datasets'][$survey_name] = array(); + while ($res = $get_results->fetch(PDO::FETCH_ASSOC)) { + foreach ($res AS $var => $val) { + if (!isset($this->survey_results['datasets'][$survey_name][$var])) { + $this->survey_results['datasets'][$survey_name][$var] = array(); + } + $this->survey_results['datasets'][$survey_name][$var][] = $val; + } + } + } + + if (!empty($needed['variables'])) { + if (in_array('formr_last_action_date', $needed['variables']) || in_array('formr_last_action_time', $needed['variables'])) { + $this->survey_results['.formr$last_action_date'] = "NA"; + $this->survey_results['.formr$last_action_time'] = "NA"; + $last_action = $this->dbh->execute( + "SELECT `survey_unit_sessions`.`created` FROM `survey_unit_sessions` LEFT JOIN `survey_run_sessions` ON `survey_run_sessions`.id = `survey_unit_sessions`.run_session_id - WHERE `survey_run_sessions`.id = :run_session_id AND `unit_id` = :unit_id AND `survey_unit_sessions`.`ended` IS NULL LIMIT 1", - array('run_session_id' => $this->run_session_id, 'unit_id' => $this->id), - true - ); - if($last_action !== false) { - $last_action_time = strtotime($last_action); - if(in_array('formr_last_action_date', $needed['variables'])) { - $this->survey_results['.formr$last_action_date'] = "as.POSIXct('".date("Y-m-d", $last_action_time)."')"; - } - if(in_array('formr_last_action_time', $needed['variables'])) { - $this->survey_results['.formr$last_action_time'] = "as.POSIXct('".date("Y-m-d H:i:s T", $last_action_time)."')"; - } - } - } - - if(in_array('formr_login_link', $needed['variables'])) { - $this->survey_results['.formr$login_link'] = "'" . run_url($this->run_name, null, array('code' => $this->session)) . "'"; - } - if(in_array('formr_login_code', $needed['variables'])) { - $this->survey_results['.formr$login_code'] = "'" . $this->session . "'"; - } - if(in_array('formr_nr_of_participants', $needed['variables'])) { - $count = (int)$this->dbh->count('survey_run_sessions', array('run_id' => $this->run_id), 'id'); - $this->survey_results['.formr$nr_of_participants'] = (int)$count; - } - if(in_array('formr_session_last_active', $needed['variables']) && $this->run_session_id) { - $last_access = $this->dbh->findValue('survey_run_sessions', array('id' => $this->run_session_id), 'last_access'); - if ($last_access) { - $this->survey_results['.formr$session_last_active'] = "as.POSIXct('".date("Y-m-d H:i:s T", strtotime($last_access))."')"; - } - } - } - - if ($needed['token_add'] !== null AND ! isset($this->survey_results['datasets'][$needed['token_add']])): - $this->survey_results['datasets'][$needed['token_add']] = array(); - endif; - - Cache::set($cache_key, $this->survey_results); - return $this->survey_results; - } - - protected function knittingNeeded($source) { - if (mb_strpos($source, '`r ') !== false OR mb_strpos($source, '```{r') !== false) { - return true; - } - return false; - } - - protected function dataNeeded($q, $token_add = null) { - $cache_key = Cache::makeKey($q, $token_add); - if (($data = Cache::get($cache_key))) { - return $data; - } - - $matches_variable_names = $variable_names_in_table = $matches = $matches_results_tables = $results_tables = $tables = array(); + WHERE `survey_run_sessions`.id = :run_session_id AND `unit_id` = :unit_id AND `survey_unit_sessions`.`ended` IS NULL LIMIT 1", array('run_session_id' => $this->run_session_id, 'unit_id' => $this->id), true + ); + if ($last_action !== false) { + $last_action_time = strtotime($last_action); + if (in_array('formr_last_action_date', $needed['variables'])) { + $this->survey_results['.formr$last_action_date'] = "as.POSIXct('" . date("Y-m-d", $last_action_time) . "')"; + } + if (in_array('formr_last_action_time', $needed['variables'])) { + $this->survey_results['.formr$last_action_time'] = "as.POSIXct('" . date("Y-m-d H:i:s T", $last_action_time) . "')"; + } + } + } + + if (in_array('formr_login_link', $needed['variables'])) { + $this->survey_results['.formr$login_link'] = "'" . run_url($this->run_name, null, array('code' => $this->session)) . "'"; + } + if (in_array('formr_login_code', $needed['variables'])) { + $this->survey_results['.formr$login_code'] = "'" . $this->session . "'"; + } + if (in_array('formr_nr_of_participants', $needed['variables'])) { + $count = (int) $this->dbh->count('survey_run_sessions', array('run_id' => $this->run_id), 'id'); + $this->survey_results['.formr$nr_of_participants'] = (int) $count; + } + if (in_array('formr_session_last_active', $needed['variables']) && $this->run_session_id) { + $last_access = $this->dbh->findValue('survey_run_sessions', array('id' => $this->run_session_id), 'last_access'); + if ($last_access) { + $this->survey_results['.formr$session_last_active'] = "as.POSIXct('" . date("Y-m-d H:i:s T", strtotime($last_access)) . "')"; + } + } + } + + if ($needed['token_add'] !== null AND ! isset($this->survey_results['datasets'][$needed['token_add']])): + $this->survey_results['datasets'][$needed['token_add']] = array(); + endif; + + Cache::set($cache_key, $this->survey_results); + return $this->survey_results; + } + + protected function knittingNeeded($source) { + if (mb_strpos($source, '`r ') !== false OR mb_strpos($source, '```{r') !== false) { + return true; + } + return false; + } + + protected function dataNeeded($q, $token_add = null) { + $cache_key = Cache::makeKey($q, $token_add); + if (($data = Cache::get($cache_key))) { + return $data; + } + + $matches_variable_names = $variable_names_in_table = $matches = $matches_results_tables = $results_tables = $tables = array(); // $results = $this->run->getAllLinkedSurveys(); // fixme -> if the last reported email thing is known to work, we can turn this on - $results = $this->run->getAllSurveys(); - - // also add some "global" formr tables - $non_user_tables = array_keys($this->non_user_tables); - $tables = $non_user_tables; - $table_ids = $non_user_tables; - $results_tables = array_combine($non_user_tables, $non_user_tables); - if(isset($results_tables['externals'])) { - $results_tables['externals'] = 'survey_unit_sessions'; - } - - if($token_add !== null): // send along this table if necessary, always as the first one, since we attach it - $table_ids[] = $this->id; - $tables[] = $this->name; - $results_tables[ $this->name ] = $this->results_table; - endif; - - // map table ID to the name that the user sees (because tables in the DB are prefixed with the user ID, so they're unique) - foreach ($results as $res) { - if($res['name'] !== $token_add): - $table_ids[] = $res['id']; - $tables[] = $res['name']; // FIXME: ID can overwrite the non_user_tables - $results_tables[$res['name']] = $res['results_table']; - endif; - } - - foreach($tables AS $index => $table_name): - $study_id = $table_ids[$index]; - - // For preg_match, study name appears as word, matches nrow(survey), survey$item, survey[row,], but not survey_2 - if($table_name == $token_add OR preg_match("/\b$table_name\b/", $q)) { - $matches[ $study_id ] = $table_name; - $matches_results_tables[ $table_name ] = $results_tables[ $table_name ]; - } - - endforeach; - - // loop through any studies that are mentioned in the command - foreach($matches AS $study_id => $table_name): - - // generate a search set of variable names for each study - if(array_key_exists($table_name, $this->non_user_tables)) { - $variable_names_in_table[$table_name] = $this->non_user_tables[$table_name]; - } else { - $items = $this->dbh->select('name')->from('survey_items') - ->where(array('study_id' => $study_id)) - ->where("type NOT IN ('mc_heading', 'note', 'submit', 'block', 'note_iframe')") - ->fetchAll(); - - $variable_names_in_table[ $table_name ] = array("created", "modified", "ended"); // should avoid modified, sucks for caching - foreach ($items as $res) { - $variable_names_in_table[ $table_name ][] = $res['name']; // search set for user defined tables - } - } - - $matches_variable_names[ $table_name ] = array(); - // generate match list for variable names - foreach($variable_names_in_table[ $table_name ] AS $variable_name) { - // try to match scales too, extraversion_1 + extraversion_2 - extraversion_3R - extraversion_4r = extraversion (script might mention the construct name, but not its item constituents) - $variable_name_base = preg_replace("/_?[0-9]{1,3}R?$/i","", $variable_name); - // don't match very short variable name bases - if(strlen($variable_name_base) < 3) { - $variable_name_base = $variable_name; - } - // item name appears as word, matches survey$item, survey[, "item"], but not item_2 for item-scale unfortunately - if(preg_match("/\b$variable_name\b/",$q) OR preg_match("/\b$variable_name_base\b/",$q)) { - $matches_variable_names[ $table_name ][] = $variable_name; - } - } + $results = $this->run->getAllSurveys(); + + // also add some "global" formr tables + $non_user_tables = array_keys($this->non_user_tables); + $tables = $non_user_tables; + $table_ids = $non_user_tables; + $results_tables = array_combine($non_user_tables, $non_user_tables); + if (isset($results_tables['externals'])) { + $results_tables['externals'] = 'survey_unit_sessions'; + } + + if ($token_add !== null): // send along this table if necessary, always as the first one, since we attach it + $table_ids[] = $this->id; + $tables[] = $this->name; + $results_tables[$this->name] = $this->results_table; + endif; + + // map table ID to the name that the user sees (because tables in the DB are prefixed with the user ID, so they're unique) + foreach ($results as $res) { + if ($res['name'] !== $token_add): + $table_ids[] = $res['id']; + $tables[] = $res['name']; // FIXME: ID can overwrite the non_user_tables + $results_tables[$res['name']] = $res['results_table']; + endif; + } + + foreach ($tables AS $index => $table_name): + $study_id = $table_ids[$index]; + + // For preg_match, study name appears as word, matches nrow(survey), survey$item, survey[row,], but not survey_2 + if ($table_name == $token_add OR preg_match("/\b$table_name\b/", $q)) { + $matches[$study_id] = $table_name; + $matches_results_tables[$table_name] = $results_tables[$table_name]; + } + + endforeach; + + // loop through any studies that are mentioned in the command + foreach ($matches AS $study_id => $table_name): + + // generate a search set of variable names for each study + if (array_key_exists($table_name, $this->non_user_tables)) { + $variable_names_in_table[$table_name] = $this->non_user_tables[$table_name]; + } else { + $items = $this->dbh->select('name')->from('survey_items') + ->where(array('study_id' => $study_id)) + ->where("type NOT IN ('mc_heading', 'note', 'submit', 'block', 'note_iframe')") + ->fetchAll(); + + $variable_names_in_table[$table_name] = array("created", "modified", "ended"); // should avoid modified, sucks for caching + foreach ($items as $res) { + $variable_names_in_table[$table_name][] = $res['name']; // search set for user defined tables + } + } + + $matches_variable_names[$table_name] = array(); + // generate match list for variable names + foreach ($variable_names_in_table[$table_name] AS $variable_name) { + // try to match scales too, extraversion_1 + extraversion_2 - extraversion_3R - extraversion_4r = extraversion (script might mention the construct name, but not its item constituents) + $variable_name_base = preg_replace("/_?[0-9]{1,3}R?$/i", "", $variable_name); + // don't match very short variable name bases + if (strlen($variable_name_base) < 3) { + $variable_name_base = $variable_name; + } + // item name appears as word, matches survey$item, survey[, "item"], but not item_2 for item-scale unfortunately + if (preg_match("/\b$variable_name\b/", $q) OR preg_match("/\b$variable_name_base\b/", $q)) { + $matches_variable_names[$table_name][] = $variable_name; + } + } // if(empty($matches_variable_names[ $table_name ])): // unset($matches_variable_names[ $table_name ]); // unset($variable_names_in_table[ $table_name ]); // unset($matches[ $study_id ]); // endif; - endforeach; - - $variables = array(); - if(preg_match("/\btime_passed\b/",$q)) { - $variables[] = 'formr_last_action_time'; - } - if(preg_match("/\bnext_day\b/",$q)) { - $variables[] = 'formr_last_action_date'; - } - if(strstr($q, '.formr$login_code') !== false) { - $variables[] = 'formr_login_code'; - } - if(strstr($q, '.formr$login_link') !== false) { - $variables[] = 'formr_login_link'; - } - if(strstr($q, '.formr$nr_of_participants') !== false) { - $variables[] = 'formr_nr_of_participants'; - } - if(strstr($q, '.formr$session_last_active') !== false) { - $variables[] = 'formr_session_last_active'; - } - - $data = compact("matches","matches_results_tables", "matches_variable_names", "token_add", "variables"); - Cache::set($cache_key, $data); - return $data; - } - - public function parseBodySpecial() { - $ocpu_vars = array();//$this->getUserDataInRun($this->body); - return $this->getParsedBodyAdmin($this->body, false, false); - } - - public function getParsedText($source) { - $ocpu_vars = $this->getUserDataInRun($source); - return opencpu_knit_plaintext($source, $ocpu_vars, false); - } - - public function getParsedTextAdmin($source) { - if (!$this->grabRandomSession()) { - return false; - } - $ocpu_vars = $this->getUserDataInRun($source); - return opencpu_debug(opencpu_knit_plaintext($source, $ocpu_vars, true)); - } - - public function getParsedBodyAdmin($source, $email_embed = false, $pick_session = true) { - if ($this->knittingNeeded($source)) { - if ($pick_session && !$this->grabRandomSession()) { - return false; - } - - $session = $this->getParsedBody($source, $email_embed, true, $pick_session); - - $body = opencpu_debug($session); - } else { - $body = $this->body_parsed; - - } - if ($email_embed) { - $report = array('body' => $this->body_parsed, 'images' => array()); - } else { - $report = $body; - } - - return $report; - } - - public function getParsedBody($source, $email_embed = false, $admin = false, $has_session_data = true) { - - if (!$this->knittingNeeded($source)) { - if($email_embed) { - return array('body' => $this->body_parsed, 'images' => array()); - } else { - return $this->body_parsed; - } - } - - /* @var $session OpenCPU_Session */ - $session = null; - $cache_session = false; - - if(!$admin) { - $opencpu_url = $this->dbh->findValue('survey_reports', array( - 'unit_id' => $this->id, - 'session_id' => $this->session_id, - 'created >=' => $this->modified // if the definition of the unit changed, don't use old reports - ), array('opencpu_url')); - - // If there is a cache of opencpu, check if it still exists - if($opencpu_url) { - if ($this->called_by_cron) { - return false; // don't regenerate once we once had a report for this feedback, if it's only the cronjob - } - - $session = opencpu_get($opencpu_url, '' , null, true); - $files = $session->getFiles('files/', $opencpu_url); - $images = $session->getFiles('/figure-html', $opencpu_url); - } - } - - // If there no session or old session (from aquired url) has an error for some reason, then get a new one for current request - if (empty($session) || $session->hasError()) { - $ocpu_vars = $has_session_data ? $this->getUserDataInRun($source) : array(); - $session = $email_embed - ? opencpu_knitemail($source, $ocpu_vars, '', true) - : opencpu_knit_iframe($source, $ocpu_vars, true, null, $this->run->description, $this->run->footer_text); - - $files = $session->getFiles('knit.html'); - $images = $session->getFiles('/figure-html'); - $cache_session = true; - } - - // At this stage we are sure to have an OpenCPU_Session in $session. If there is an error in the session return FALSE - if(empty($session)) { - alert('OpenCPU is probably down or inaccessible. Please retry in a few minutes.', 'alert-danger'); - return false; - } elseif ($session->hasError()) { - $where = ''; - if(isset($this->run_name)) { - $where = "Run: ". $this->run_name. " (".$this->position."-". $this->type.") "; - } - notify_user_error(opencpu_debug($session), 'There was a problem with OpenCPU for session ' . h($this->session)); - return false; - } elseif ($admin) { - - return $session; - } else { - print_hidden_opencpu_debug_message($session, "OpenCPU debugger for run R code in {$this->type} at {$this->position}."); - - $opencpu_url = $session->getLocation(); - if($email_embed) { - $report = array( - 'body' => $session->getObject(), - 'images' => $images, - ); - } else { - $this->run->renderedDescAndFooterAlready = true; - $iframesrc = $files['knit.html']; - $report = '' . - '
    -
    '; - } + } - if($this->session_id && $cache_session) { - $set_report = $this->dbh->prepare( - "INSERT INTO `survey_reports` (`session_id`, `unit_id`, `opencpu_url`, `created`, `last_viewed`) + if ($this->session_id && $cache_session) { + $set_report = $this->dbh->prepare( + "INSERT INTO `survey_reports` (`session_id`, `unit_id`, `opencpu_url`, `created`, `last_viewed`) VALUES (:session_id, :unit_id, :opencpu_url, NOW(), NOW() ) ON DUPLICATE KEY UPDATE opencpu_url = VALUES(opencpu_url), created = VALUES(created)"); - $set_report->bindParam(":unit_id", $this->id); - $set_report->bindParam(":opencpu_url", $opencpu_url); - $set_report->bindParam(":session_id", $this->session_id); - $set_report->execute(); - } - - return $report; - } - } - - public function getExportUnit() { - $unit = array(); - foreach ($this->export_attribs as $property) { - if (property_exists($this, $property)) { - $unit[$property] = $this->{$property}; - } - } - return $unit; - } - - public static function getDefaults($type) { - $defaults = array(); - $defaults['ServiceMessagePage'] = array( - 'type' => 'Page', - 'title' => 'Service message', - 'special' => 'ServiceMessagePage', - 'description' => 'Service Message ' . date('d.m.Y'), - 'body' => "# Service message \n This study is currently being serviced. Please return at a later time." - ); - - $defaults['OverviewScriptPage'] = array( - 'type' => 'Page', - 'title' => 'Overview script', - 'special' => 'OverviewScriptPage', - 'body' => "# Intersperse Markdown with R + $set_report->bindParam(":unit_id", $this->id); + $set_report->bindParam(":opencpu_url", $opencpu_url); + $set_report->bindParam(":session_id", $this->session_id); + $set_report->execute(); + } + + return $report; + } + } + + public function getExportUnit() { + $unit = array(); + foreach ($this->export_attribs as $property) { + if (property_exists($this, $property)) { + $unit[$property] = $this->{$property}; + } + } + return $unit; + } + + public static function getDefaults($type) { + $defaults = array(); + $defaults['ServiceMessagePage'] = array( + 'type' => 'Page', + 'title' => 'Service message', + 'special' => 'ServiceMessagePage', + 'description' => 'Service Message ' . date('d.m.Y'), + 'body' => "# Service message \n This study is currently being serviced. Please return at a later time." + ); + + $defaults['OverviewScriptPage'] = array( + 'type' => 'Page', + 'title' => 'Overview script', + 'special' => 'OverviewScriptPage', + 'body' => "# Intersperse Markdown with R ```{r} plot(cars) ```"); - $defaults['ReminderEmail'] = array( - 'type' => 'Email', - 'subject' => 'Reminder', - 'special' => 'ReminderEmail', - 'recipient_field' => '', - 'body' => "\nPlease take part in our study at {{login_link}}.", - ); - - return array_val($defaults, $type, array()); - } - - protected function getUnitTemplatePath($tpl = null) { - return 'admin/run/units/' . ($tpl ? $tpl : strtolower($this->type)); - } + $defaults['ReminderEmail'] = array( + 'type' => 'Email', + 'subject' => 'Reminder', + 'special' => 'ReminderEmail', + 'recipient_field' => '', + 'body' => "\nPlease take part in our study at {{login_link}}.", + ); + + return array_val($defaults, $type, array()); + } + + protected function getUnitTemplatePath($tpl = null) { + return 'admin/run/units/' . ($tpl ? $tpl : strtolower($this->type)); + } } diff --git a/application/Model/Shuffle.php b/application/Model/Shuffle.php index a165e052c..7ea67c76a 100644 --- a/application/Model/Shuffle.php +++ b/application/Model/Shuffle.php @@ -2,75 +2,75 @@ class Shuffle extends RunUnit { - public $errors = array(); - public $id = null; - public $session = null; - public $unit = null; - public $ended = false; - public $type = 'Shuffle'; - public $icon = "fa-random"; - protected $groups = 2; - - /** - * An array of unit's exportable attributes - * @var array - */ - public $export_attribs = array('type', 'description', 'position', 'special', 'groups'); - - public function __construct($fdb, $session = null, $unit = null, $run_session = NULL, $run = NULL) { - parent::__construct($fdb, $session, $unit, $run_session, $run); - - if ($this->id): - $groups = $this->dbh->findValue('survey_shuffles', array('id' => $this->id), array('groups')); - if ($groups): - $this->groups = $groups; - $this->valid = true; - endif; - endif; - } - - public function create($options) { - $this->dbh->beginTransaction(); - if (!$this->id) { - $this->id = parent::create('Shuffle'); - } else { - $this->modify($options); - } - - if (isset($options['groups'])) { - $this->groups = $options['groups']; - } - - $this->dbh->insert_update('survey_shuffles', array( - 'id' => $this->id, - 'groups' => $this->groups, - )); - $this->dbh->commit(); - $this->valid = true; - - return true; - } - - public function displayForRun($prepend = '') { - - $dialog = Template::get($this->getUnitTemplatePath(), array( - 'prepend' => $prepend, - 'groups' => $this->groups - )); - - return parent::runDialog($dialog); - } - - public function removeFromRun($special = null) { - return $this->delete($special); - } - - public function randomise_into_group() { - return mt_rand(1, $this->groups); - } - - public function test() { - $test_tpl = ' + public $errors = array(); + public $id = null; + public $session = null; + public $unit = null; + public $ended = false; + public $type = 'Shuffle'; + public $icon = "fa-random"; + protected $groups = 2; + + /** + * An array of unit's exportable attributes + * @var array + */ + public $export_attribs = array('type', 'description', 'position', 'special', 'groups'); + + public function __construct($fdb, $session = null, $unit = null, $run_session = NULL, $run = NULL) { + parent::__construct($fdb, $session, $unit, $run_session, $run); + + if ($this->id): + $groups = $this->dbh->findValue('survey_shuffles', array('id' => $this->id), array('groups')); + if ($groups): + $this->groups = $groups; + $this->valid = true; + endif; + endif; + } + + public function create($options) { + $this->dbh->beginTransaction(); + if (!$this->id) { + $this->id = parent::create('Shuffle'); + } else { + $this->modify($options); + } + + if (isset($options['groups'])) { + $this->groups = $options['groups']; + } + + $this->dbh->insert_update('survey_shuffles', array( + 'id' => $this->id, + 'groups' => $this->groups, + )); + $this->dbh->commit(); + $this->valid = true; + + return true; + } + + public function displayForRun($prepend = '') { + + $dialog = Template::get($this->getUnitTemplatePath(), array( + 'prepend' => $prepend, + 'groups' => $this->groups + )); + + return parent::runDialog($dialog); + } + + public function removeFromRun($special = null) { + return $this->delete($special); + } + + public function randomise_into_group() { + return mt_rand(1, $this->groups); + } + + public function test() { + $test_tpl = '

    Randomisation

    We just generated fifty random group assignments:

    %{groups}
    @@ -80,24 +80,24 @@ public function test() { but usually you shouldn\'t do this.

    '; - $groups = ''; - for ($i = 0; $i < 50; $i++) { - $groups .= $this->randomise_into_group() . '  '; - } - - echo Template::replace($test_tpl, array('groups' => $groups)); - } - - public function exec() { - $group = $this->randomise_into_group(); - $this->dbh->insert('shuffle', array( - 'session_id' => $this->session_id, - 'unit_id' => $this->id, - 'group' => $group, - 'created' => mysql_now() - )); - $this->end(); - return false; - } + $groups = ''; + for ($i = 0; $i < 50; $i++) { + $groups .= $this->randomise_into_group() . '  '; + } + + echo Template::replace($test_tpl, array('groups' => $groups)); + } + + public function exec() { + $group = $this->randomise_into_group(); + $this->dbh->insert('shuffle', array( + 'session_id' => $this->session_id, + 'unit_id' => $this->id, + 'group' => $group, + 'created' => mysql_now() + )); + $this->end(); + return false; + } } diff --git a/application/Model/Site.php b/application/Model/Site.php index 4ed36e5a9..48cb95ff4 100644 --- a/application/Model/Site.php +++ b/application/Model/Site.php @@ -2,337 +2,337 @@ class Site { - public $alerts = array(); - public $alert_types = array("alert-warning" => 0, "alert-success" => 0, "alert-info" => 0, "alert-danger" => 0); - public $last_outside_referrer; - - /** - * @var Request - */ - public $request; - - /** - * @var Site - */ - protected static $instance = null; - - /** - * @var string - */ - protected $path; - - /** - * @var RunSession[] - */ - protected $runSessions = array(); - - protected function __construct() { - $this->updateRequestObject(); - } - - /** - * @return Site - */ - public static function getInstance() { - if (self::$instance === null) { - self::$instance = new self(); - } - return self::$instance; - } - - public function refresh() { - $this->lastOutsideReferrer(); - } - - public function renderAlerts() { - $now_handled = $this->alerts; - $this->alerts = array(); - $this->alert_types = array("alert-warning" => 0, "alert-success" => 0, "alert-info" => 0, "alert-danger" => 0); - return implode($now_handled); - } - - public function updateRequestObject($path = null) { - $this->request = new Request(); - $this->path = $path; - } - - public function setPath($path) { - $this->path = $path; - } - - public function getPath() { - return $this->path; - } - - public function alert($msg, $class = 'alert-warning', $dismissable = true) { - if (isset($this->alert_types[$class])): // count types of alerts - $this->alert_types[$class] ++; - else: - $this->alert_types[$class] = 1; - endif; - if (is_array($msg)) { - $msg = $msg['body']; - } - - if ($class == 'alert-warning') { - $class_logo = 'exclamation-triangle'; - } elseif ($class == 'alert-danger') { - $class_logo = 'bolt'; - } elseif ($class == 'alert-info') { - $class_logo = 'info-circle'; - } else { // if($class == 'alert-success') - $class_logo = 'thumbs-up'; - } - - $msg = str_replace(APPLICATION_ROOT, '', $msg); - $logo = '  '; - $this->alerts[] = "
    " . $logo . '' . "$msg
    "; - } - - public function inSuperAdminArea() { - return strpos($this->path, 'superadmin/') !== FALSE; - } - - public function inAdminArea() { - return strpos($this->path, 'admin/') !== FALSE; - } - - public function inAdminRunArea() { - return strpos($this->path, 'admin/run') !== FALSE; - } - - public function inAdminSurveyArea() { - return strpos($this->path, 'admin/survey') !== FALSE; - } - - public function isFrontEndStudyArea() { - return strpos($this->path, basename(RUNROOT) . '/') !== FALSE; - } - - public function lastOutsideReferrer() { - $ref = isset($_SERVER['HTTP_REFERER']) ? $_SERVER['HTTP_REFERER'] : ''; - if (mb_strpos($ref, WEBROOT) !== 0) { - $this->last_outside_referrer = $ref; - } - } - - public function makeAdminMailer() { - global $settings; - $mail = new PHPMailer(); - $mail->SetLanguage("de", "/"); - - $mail->IsSMTP(); // telling the class to use SMTP - $mail->Mailer = "smtp"; - $mail->Host = $settings['email']['host']; - $mail->Port = $settings['email']['port']; - if ($settings['email']['tls']) { - $mail->SMTPSecure = 'tls'; - } else { - $mail->SMTPSecure = 'ssl'; - } - if (isset($settings['email']['username'])) { - $mail->SMTPAuth = true; // turn on SMTP authentication - $mail->Username = $settings['email']['username']; // SMTP username - $mail->Password = $settings['email']['password']; // SMTP password - } else { - $mail->SMTPAuth = false; - $mail->SMTPSecure = false; - } - $mail->From = $settings['email']['from']; - $mail->FromName = $settings['email']['from_name']; - $mail->AddReplyTo($settings['email']['from'], $settings['email']['from_name']); - $mail->CharSet = "utf-8"; - $mail->WordWrap = 65; // set word wrap to 65 characters - if (is_array(Config::get('email.smtp_options'))) { - $mail->SMTPOptions = array_merge($mail->SMTPOptions, Config::get('email.smtp_options')); - } - - return $mail; - } - - public function expire_session($expiry) { - if (Session::isExpired($expiry)) { - // last request was more than 30 minutes ago - alert("You were logged out automatically, because you were last active " . timetostr(Session::get('last_activity')) . '.', 'alert-info'); - Session::destroy(); - return true; - } - return false; - } - - public function loginUser($user) { - // came here with a login link - if (isset($_GET['run_name']) && isset($_GET['code']) && strlen($_GET['code']) == 64) { - $login_code = $_GET['code']; - // this user came here with a session code that he wasn't using before. - // this will always be true if the user is - // (a) new (auto-assigned code by site) - // (b) already logged in with a different account - if ($user->user_code !== $login_code): - if ($user->loggedIn()) { - // if the user is new and has an auto-assigned code, there's no need to talk about the behind-the-scenes change - // but if he's logged in we should alert them - alert("You switched sessions, because you came here with a login link and were already logged in as someone else.", 'alert-info'); - } - $user = new User(DB::getInstance(), null, $login_code); - // a special case are admins. if they are not already logged in, verified through password, they should not be able to obtain access so easily. but because we only create a mock user account, this is no problem. the admin flags are only set/privileges are only given if they legitimately log in - endif; - } elseif (isset($_GET['run_name']) && isset($user->user_code)) { - return $user; - } else { - alert("Sorry. Something went wrong when you tried to access.", 'alert-danger'); - redirect_to("index"); - } - - return $user; - } - - public function makeTitle() { - global $title; - if (trim($title)) { - return $title; - } - - $path = ''; - if (isset($_SERVER['REDIRECT_URL'])) { - $path = $_SERVER['REDIRECT_URL']; - } else if (isset($_SERVER['SCRIPT_NAME'])) { - $path = $_SERVER['SCRIPT_NAME']; - } - - $path = preg_replace(array( - "@var/www/@", - "@formr/@", - "@webroot/@", - "@\.php$@", - "@index$@", - "@^/@", - "@/$@", - ), "", $path); - - if ($path != ''): - $title = "formr /" . $path; - $title = str_replace(array('_', '/'), array(' ', ' / '), $title); - endif; - return isset($title) ? $title : 'formr survey framework'; - } - - public function getCurrentRoute() { - return $this->request->getParam('route'); - } - - public function setRunSession(RunSession $runSession) { - $session = $runSession->session; - if ($session) { - Session::set('current_run_session_code', $session); - $id = md5($session); - $this->runSessions[$id] = $runSession; - } - } - - public function getRunSession($session = null) { - if ($session === null) { - // if $id is null, get from current session - $session = Session::get('current_run_session_code'); - } - $id = md5($session); - return isset($this->runSessions[$id]) ? $this->runSessions[$id] : null; - } - - /** - * @return DB - */ - public static function getDb() { - return DB::getInstance(); - } - - /** - * - * @global User $user - * @return User - */ - public static function getCurrentUser() { - global $user; - return $user; - } - - /** - * Get Site user from current Session - * - * @return User|null - */ - public function getSessionUser() { - $expiry = Config::get('expire_unregistered_session'); - $db = self::getDb(); - $user = null; - - if (($usr = Session::get('user'))) { - $user = unserialize($usr); - // This segment basically checks whether the user-specific expiry time was met - // If user session is expired, user is logged out and redirected - if (!empty($user->id)) { // logged in user - // refresh user object if not expired - $expiry = Config::get('expire_registered_session'); - $user = new User($db, $user->id, $user->user_code); - // admins have a different expiry, can only be lower - if ($user->isAdmin()) { - $expiry = Config::get('expire_admin_session'); - } - } elseif (!empty($user->user_code)) { // visitor - // refresh user object - $user = new User($db, null, $user->user_code); - } - } - - if($this->expire_session($expiry)) { - $user = null; - } - - if (empty($user->user_code)) { - $user = new User($db, null, null); - } - - return $user; - } - - /** - * @return \OAuth2\Server - */ - public static function getOauthServer() { - static $server; - if ($server != null) { - return $server; - } - - // Setup DB connection for oauth - $db_config = (array) Config::get('database'); - $options = array( - 'host' => $db_config['host'], - 'dbname' => $db_config['database'], - 'charset' => 'utf8', - ); - if (!empty($db_config['port'])) { - $options['port'] = $db_config['port']; - } - - $dsn = 'mysql:' . http_build_query($options, null, ';'); - $username = $db_config['login']; - $password = $db_config['password']; - - OAuth2\Autoloader::register(); - - // $dsn is the Data Source Name for your database, for exmaple "mysql:dbname=my_oauth2_db;host=localhost" - $storage = new OAuth2\Storage\Pdo(array('dsn' => $dsn, 'username' => $username, 'password' => $password)); - - // Pass a storage object or array of storage objects to the OAuth2 server class - $server = new OAuth2\Server($storage); - - // Add the "Client Credentials" grant type (it is the simplest of the grant types) - $server->addGrantType(new OAuth2\GrantType\ClientCredentials($storage)); - - // Add the "Authorization Code" grant type (this is where the oauth magic happens) - $server->addGrantType(new OAuth2\GrantType\AuthorizationCode($storage)); - return $server; - } + public $alerts = array(); + public $alert_types = array("alert-warning" => 0, "alert-success" => 0, "alert-info" => 0, "alert-danger" => 0); + public $last_outside_referrer; + + /** + * @var Request + */ + public $request; + + /** + * @var Site + */ + protected static $instance = null; + + /** + * @var string + */ + protected $path; + + /** + * @var RunSession[] + */ + protected $runSessions = array(); + + protected function __construct() { + $this->updateRequestObject(); + } + + /** + * @return Site + */ + public static function getInstance() { + if (self::$instance === null) { + self::$instance = new self(); + } + return self::$instance; + } + + public function refresh() { + $this->lastOutsideReferrer(); + } + + public function renderAlerts() { + $now_handled = $this->alerts; + $this->alerts = array(); + $this->alert_types = array("alert-warning" => 0, "alert-success" => 0, "alert-info" => 0, "alert-danger" => 0); + return implode($now_handled); + } + + public function updateRequestObject($path = null) { + $this->request = new Request(); + $this->path = $path; + } + + public function setPath($path) { + $this->path = $path; + } + + public function getPath() { + return $this->path; + } + + public function alert($msg, $class = 'alert-warning', $dismissable = true) { + if (isset($this->alert_types[$class])): // count types of alerts + $this->alert_types[$class] ++; + else: + $this->alert_types[$class] = 1; + endif; + if (is_array($msg)) { + $msg = $msg['body']; + } + + if ($class == 'alert-warning') { + $class_logo = 'exclamation-triangle'; + } elseif ($class == 'alert-danger') { + $class_logo = 'bolt'; + } elseif ($class == 'alert-info') { + $class_logo = 'info-circle'; + } else { // if($class == 'alert-success') + $class_logo = 'thumbs-up'; + } + + $msg = str_replace(APPLICATION_ROOT, '', $msg); + $logo = '  '; + $this->alerts[] = "
    " . $logo . '' . "$msg
    "; + } + + public function inSuperAdminArea() { + return strpos($this->path, 'superadmin/') !== FALSE; + } + + public function inAdminArea() { + return strpos($this->path, 'admin/') !== FALSE; + } + + public function inAdminRunArea() { + return strpos($this->path, 'admin/run') !== FALSE; + } + + public function inAdminSurveyArea() { + return strpos($this->path, 'admin/survey') !== FALSE; + } + + public function isFrontEndStudyArea() { + return strpos($this->path, basename(RUNROOT) . '/') !== FALSE; + } + + public function lastOutsideReferrer() { + $ref = isset($_SERVER['HTTP_REFERER']) ? $_SERVER['HTTP_REFERER'] : ''; + if (mb_strpos($ref, WEBROOT) !== 0) { + $this->last_outside_referrer = $ref; + } + } + + public function makeAdminMailer() { + global $settings; + $mail = new PHPMailer(); + $mail->SetLanguage("de", "/"); + + $mail->IsSMTP(); // telling the class to use SMTP + $mail->Mailer = "smtp"; + $mail->Host = $settings['email']['host']; + $mail->Port = $settings['email']['port']; + if ($settings['email']['tls']) { + $mail->SMTPSecure = 'tls'; + } else { + $mail->SMTPSecure = 'ssl'; + } + if (isset($settings['email']['username'])) { + $mail->SMTPAuth = true; // turn on SMTP authentication + $mail->Username = $settings['email']['username']; // SMTP username + $mail->Password = $settings['email']['password']; // SMTP password + } else { + $mail->SMTPAuth = false; + $mail->SMTPSecure = false; + } + $mail->From = $settings['email']['from']; + $mail->FromName = $settings['email']['from_name']; + $mail->AddReplyTo($settings['email']['from'], $settings['email']['from_name']); + $mail->CharSet = "utf-8"; + $mail->WordWrap = 65; // set word wrap to 65 characters + if (is_array(Config::get('email.smtp_options'))) { + $mail->SMTPOptions = array_merge($mail->SMTPOptions, Config::get('email.smtp_options')); + } + + return $mail; + } + + public function expire_session($expiry) { + if (Session::isExpired($expiry)) { + // last request was more than 30 minutes ago + alert("You were logged out automatically, because you were last active " . timetostr(Session::get('last_activity')) . '.', 'alert-info'); + Session::destroy(); + return true; + } + return false; + } + + public function loginUser($user) { + // came here with a login link + if (isset($_GET['run_name']) && isset($_GET['code']) && strlen($_GET['code']) == 64) { + $login_code = $_GET['code']; + // this user came here with a session code that he wasn't using before. + // this will always be true if the user is + // (a) new (auto-assigned code by site) + // (b) already logged in with a different account + if ($user->user_code !== $login_code): + if ($user->loggedIn()) { + // if the user is new and has an auto-assigned code, there's no need to talk about the behind-the-scenes change + // but if he's logged in we should alert them + alert("You switched sessions, because you came here with a login link and were already logged in as someone else.", 'alert-info'); + } + $user = new User(DB::getInstance(), null, $login_code); + // a special case are admins. if they are not already logged in, verified through password, they should not be able to obtain access so easily. but because we only create a mock user account, this is no problem. the admin flags are only set/privileges are only given if they legitimately log in + endif; + } elseif (isset($_GET['run_name']) && isset($user->user_code)) { + return $user; + } else { + alert("Sorry. Something went wrong when you tried to access.", 'alert-danger'); + redirect_to("index"); + } + + return $user; + } + + public function makeTitle() { + global $title; + if (trim($title)) { + return $title; + } + + $path = ''; + if (isset($_SERVER['REDIRECT_URL'])) { + $path = $_SERVER['REDIRECT_URL']; + } else if (isset($_SERVER['SCRIPT_NAME'])) { + $path = $_SERVER['SCRIPT_NAME']; + } + + $path = preg_replace(array( + "@var/www/@", + "@formr/@", + "@webroot/@", + "@\.php$@", + "@index$@", + "@^/@", + "@/$@", + ), "", $path); + + if ($path != ''): + $title = "formr /" . $path; + $title = str_replace(array('_', '/'), array(' ', ' / '), $title); + endif; + return isset($title) ? $title : 'formr survey framework'; + } + + public function getCurrentRoute() { + return $this->request->getParam('route'); + } + + public function setRunSession(RunSession $runSession) { + $session = $runSession->session; + if ($session) { + Session::set('current_run_session_code', $session); + $id = md5($session); + $this->runSessions[$id] = $runSession; + } + } + + public function getRunSession($session = null) { + if ($session === null) { + // if $id is null, get from current session + $session = Session::get('current_run_session_code'); + } + $id = md5($session); + return isset($this->runSessions[$id]) ? $this->runSessions[$id] : null; + } + + /** + * @return DB + */ + public static function getDb() { + return DB::getInstance(); + } + + /** + * + * @global User $user + * @return User + */ + public static function getCurrentUser() { + global $user; + return $user; + } + + /** + * Get Site user from current Session + * + * @return User|null + */ + public function getSessionUser() { + $expiry = Config::get('expire_unregistered_session'); + $db = self::getDb(); + $user = null; + + if (($usr = Session::get('user'))) { + $user = unserialize($usr); + // This segment basically checks whether the user-specific expiry time was met + // If user session is expired, user is logged out and redirected + if (!empty($user->id)) { // logged in user + // refresh user object if not expired + $expiry = Config::get('expire_registered_session'); + $user = new User($db, $user->id, $user->user_code); + // admins have a different expiry, can only be lower + if ($user->isAdmin()) { + $expiry = Config::get('expire_admin_session'); + } + } elseif (!empty($user->user_code)) { // visitor + // refresh user object + $user = new User($db, null, $user->user_code); + } + } + + if ($this->expire_session($expiry)) { + $user = null; + } + + if (empty($user->user_code)) { + $user = new User($db, null, null); + } + + return $user; + } + + /** + * @return \OAuth2\Server + */ + public static function getOauthServer() { + static $server; + if ($server != null) { + return $server; + } + + // Setup DB connection for oauth + $db_config = (array) Config::get('database'); + $options = array( + 'host' => $db_config['host'], + 'dbname' => $db_config['database'], + 'charset' => 'utf8', + ); + if (!empty($db_config['port'])) { + $options['port'] = $db_config['port']; + } + + $dsn = 'mysql:' . http_build_query($options, null, ';'); + $username = $db_config['login']; + $password = $db_config['password']; + + OAuth2\Autoloader::register(); + + // $dsn is the Data Source Name for your database, for exmaple "mysql:dbname=my_oauth2_db;host=localhost" + $storage = new OAuth2\Storage\Pdo(array('dsn' => $dsn, 'username' => $username, 'password' => $password)); + + // Pass a storage object or array of storage objects to the OAuth2 server class + $server = new OAuth2\Server($storage); + + // Add the "Client Credentials" grant type (it is the simplest of the grant types) + $server->addGrantType(new OAuth2\GrantType\ClientCredentials($storage)); + + // Add the "Authorization Code" grant type (this is where the oauth magic happens) + $server->addGrantType(new OAuth2\GrantType\AuthorizationCode($storage)); + return $server; + } } diff --git a/application/Model/SkipBackward.php b/application/Model/SkipBackward.php index 78106de77..8f0f0c371 100644 --- a/application/Model/SkipBackward.php +++ b/application/Model/SkipBackward.php @@ -2,23 +2,24 @@ class SkipBackward extends Branch { - public $type = 'SkipBackward'; - public $icon = 'fa-backward'; - /** - * An array of unit's exportable attributes - * @var array - */ - public $export_attribs = array('type', 'description', 'position', 'special', 'condition', 'if_true'); + public $type = 'SkipBackward'; + public $icon = 'fa-backward'; - public function displayForRun($prepend = '') { - $dialog = Template::get($this->getUnitTemplatePath(), array( - 'prepend' => $prepend, - 'condition' => $this->condition, - 'position' => $this->position, - 'ifTrue' => $this->if_true, - )); + /** + * An array of unit's exportable attributes + * @var array + */ + public $export_attribs = array('type', 'description', 'position', 'special', 'condition', 'if_true'); - return parent::runDialog($dialog); - } + public function displayForRun($prepend = '') { + $dialog = Template::get($this->getUnitTemplatePath(), array( + 'prepend' => $prepend, + 'condition' => $this->condition, + 'position' => $this->position, + 'ifTrue' => $this->if_true, + )); + + return parent::runDialog($dialog); + } } diff --git a/application/Model/SkipForward.php b/application/Model/SkipForward.php index 7380cf31a..07275de43 100644 --- a/application/Model/SkipForward.php +++ b/application/Model/SkipForward.php @@ -2,13 +2,13 @@ class SkipForward extends Branch { - public $type = 'SkipForward'; - public $icon = 'fa-forward'; - - /** - * An array of unit's exportable attributes - * @var array - */ - public $export_attribs = array('type', 'description', 'position', 'special', 'automatically_jump', 'if_true', 'automatically_go_on'); + public $type = 'SkipForward'; + public $icon = 'fa-forward'; + + /** + * An array of unit's exportable attributes + * @var array + */ + public $export_attribs = array('type', 'description', 'position', 'special', 'automatically_jump', 'if_true', 'automatically_go_on'); } diff --git a/application/Model/SpreadsheetReader.php b/application/Model/SpreadsheetReader.php index 1cc630a23..bf30253dd 100644 --- a/application/Model/SpreadsheetReader.php +++ b/application/Model/SpreadsheetReader.php @@ -2,833 +2,826 @@ class SpreadsheetReader { - private $choices_columns = array('list_name', 'name', 'label'); - private $survey_columns = array('name', 'type', 'label', 'optional', 'class', 'showif', 'choice1', 'choice2', 'choice3', 'choice4', 'choice5', 'choice6', 'choice7', 'choice8', 'choice9', 'choice10', 'choice11', 'choice12', 'choice13', 'choice14', 'value', 'order', 'block_order', 'item_order', - # legacy - 'variablenname', 'wortlaut', 'typ', 'ratinguntererpol', 'ratingobererpol', 'mcalt1', 'mcalt2', 'mcalt3', 'mcalt4', 'mcalt5', 'mcalt6', 'mcalt7', 'mcalt8', 'mcalt9', 'mcalt10', 'mcalt11', 'mcalt12', 'mcalt13', 'mcalt14',); - private $internal_columns = array('choice_list', 'type_options', 'label_parsed'); - private $existing_choice_lists = array(); - - public $messages = array(); - public $errors = array(); - public $warnings = array(); - public $survey = array(); - public $choices = array(); - public static $exportFormats = array('csv', 'csv_german', 'tsv', 'xlsx', 'xls', 'json'); - - public static function verifyExportFormat($formatstring) { - if (!in_array($formatstring, static::$exportFormats)) { - formr_error(400, 'Bad Request', 'Unsupported export format requested.'); - } - } - - public function backupTSV($array, $filename) { - $objPhpSpreadsheet = $this->objectFromArray($array); - - $objWriter = \PhpOffice\PhpSpreadsheet\IOFactory::createWriter($objPhpSpreadsheet, 'Csv'); - $objWriter->setDelimiter("\t"); - $objWriter->setEnclosure(""); - - try { - $objWriter->save($filename); - return true; - } catch (Exception $e) { - formr_log_exception($e, __CLASS__); - alert("Couldn't save file.", 'alert-danger'); - return false; - } - } - - protected function objectFromArray($array) { - set_time_limit(300); # defaults to 30 - ini_set('memory_limit', Config::get('memory_limit.spr_object_array')); - - $objPhpSpreadsheet = new \PhpOffice\PhpSpreadsheet\Spreadsheet(); - $current = current($array); - if (!$current) { - return $objPhpSpreadsheet; - } - array_unshift($array, array_keys($current)); - $objPhpSpreadsheet->getSheet(0)->fromArray($array); - - return $objPhpSpreadsheet; - } - - /** - * - * @param PDOStatement $stmt - * @return \PhpOffice\PhpSpreadsheet\Spreadsheet; - */ - protected function objectFromPDOStatement(PDOStatement $stmt) { - $PhpSpreadsheet = new \PhpOffice\PhpSpreadsheet\Spreadsheet(); - $PhpSpreadsheetSheet = $PhpSpreadsheet->getSheet(0); - - list ($startColumn, $startRow) = \PhpOffice\PhpSpreadsheet\Cell\Coordinate::coordinateFromString('A1'); - $writeColumns = true; - while($row = $stmt->fetch(PDO::FETCH_ASSOC)) { - if ($writeColumns) { - $columns = array_keys($row); - $currentColumn = $startColumn; - foreach ($columns as $cellValue) { - $PhpSpreadsheetSheet->getCell($currentColumn . $startRow)->setValue($cellValue); - ++$currentColumn; - } - ++$startRow; - $writeColumns = false; - } - $currentColumn = $startColumn; - foreach ($row as $cellValue) { - $PhpSpreadsheetSheet->getCell($currentColumn . $startRow)->setValue($cellValue); - ++$currentColumn; - } - ++$startRow; - } - return $PhpSpreadsheet; - } - public function exportInRequestedFormat(PDOStatement $resultsStmt, $filename, $filetype) { - self::verifyExportFormat($filetype); - - switch ($filetype) { - case 'xlsx': - $download_successfull = $this->exportXLSX($resultsStmt, $filename); - break; - case 'xls': - $download_successfull = $this->exportXLS($resultsStmt, $filename); - break; - case 'csv_german': - $download_successfull = $this->exportCSV_german($resultsStmt, $filename); - break; - case 'tsv': - $download_successfull = $this->exportTSV($resultsStmt, $filename); - break; - case 'json': - $download_successfull = $this->exportJSON($resultsStmt, $filename); - break; - default: - $download_successfull = $this->exportCSV($resultsStmt, $filename); - break; - } - - - return $download_successfull; - } - - public function exportCSV(PDOStatement $stmt, $filename) { - if (!$stmt->columnCount()) { - formr_log('Debug: column count is not set'); - return false; - } - - try { - $phpSpreadsheet = $this->objectFromPDOStatement($stmt); - $phpSpreadsheetWriter = \PhpOffice\PhpSpreadsheet\IOFactory::createWriter($phpSpreadsheet, 'Csv'); - - header('Content-Disposition: attachment;filename="' . $filename . '.csv"'); - header('Cache-Control: max-age=0'); - header('Content-Type: text/csv'); - $phpSpreadsheetWriter->save('php://output'); - exit; - } catch (Exception $e) { - formr_log_exception($e, __METHOD__); - alert('Couldn\'t save file.', 'alert-danger'); - return false; - } - } - - public function exportJSON($object, $filename) { - set_time_limit(300); - if ($object instanceof PDOStatement) { - $file = APPLICATION_ROOT . "tmp/downloads/{$filename}.json"; - file_put_contents($file, ''); - - $handle = fopen($file, 'w+'); - while ($row = $object->fetch(PDO::FETCH_ASSOC)) { - fwrite_json($handle, $row); - } - fclose($handle); - - header('Content-Disposition: attachment;filename="' . $filename . '.json"'); - header('Cache-Control: max-age=0'); - header('Content-type: application/json; charset=utf-8'); - Config::get('use_xsendfile') ? header('X-Sendfile: ' . $file) : readfile($file); - exit; - } else { - header('Content-Disposition: attachment;filename="' . $filename . '.json"'); - header('Cache-Control: max-age=0'); - header('Content-type: application/json; charset=utf-8'); - echo json_encode($object, JSON_PRETTY_PRINT + JSON_UNESCAPED_UNICODE + JSON_NUMERIC_CHECK); - exit; - - } - } - - public function exportTSV(PDOStatement $stmt, $filename, $savefile = null) { - if (!$stmt->columnCount()) { - return false; - } - - try { - $phpSpreadsheet = $this->objectFromPDOStatement($stmt); - $phpSpreadsheetWriter = \PhpOffice\PhpSpreadsheet\IOFactory::createWriter($phpSpreadsheet, 'Csv'); - $phpSpreadsheetWriter->setDelimiter("\t"); - $phpSpreadsheetWriter->setEnclosure(""); - - if ($savefile === null) { - header('Content-Disposition: attachment;filename="' . $filename . '.tab"'); - header('Cache-Control: max-age=0'); - header('Content-Type: text/csv'); // or maybe text/tab-separated-values? - $phpSpreadsheetWriter->save('php://output'); - exit; - } else { - $phpSpreadsheetWriter->save($savefile); - return true; - } - } catch (Exception $e) { - formr_log_exception($e, __METHOD__); - alert('Couldn\'t save file.', 'alert-danger'); - return false; - } - } - - public function exportCSV_german(PDOStatement $stmt, $filename, $savefile = null) { - if (!$stmt->columnCount()) { - return false; - } - - try { - $phpSpreadsheet = $this->objectFromPDOStatement($stmt); - $phpSpreadsheetWriter = \PhpOffice\PhpSpreadsheet\IOFactory::createWriter($phpSpreadsheet, 'Csv'); - $phpSpreadsheetWriter->setDelimiter(';'); - $phpSpreadsheetWriter->setEnclosure('"'); - - if ($savefile === null) { - header('Content-Disposition: attachment;filename="' . $filename . '.csv"'); - header('Cache-Control: max-age=0'); - header('Content-Type: text/csv'); - $phpSpreadsheetWriter->save('php://output'); - exit; - } else { - $phpSpreadsheetWriter->save($savefile); - return true; - } - } catch (Exception $e) { - formr_log_exception($e, __METHOD__); - alert('Couldn\'t save file.', 'alert-danger'); - return false; - } - } - - public function exportXLS(PDOStatement $stmt, $filename) { - if (!$stmt->columnCount()) { - return false; - } - - try { - $phpSpreadsheet = $this->objectFromPDOStatement($stmt); - $phpSpreadsheetWriter = \PhpOffice\PhpSpreadsheet\IOFactory::createWriter($phpSpreadsheet, 'Xls'); - header('Content-Disposition: attachment;filename="' . $filename . '.xls"'); - header('Cache-Control: max-age=0'); - header('Content-Type: application/vnd.ms-excel'); - $phpSpreadsheetWriter->save('php://output'); - exit; - } catch (Exception $e) { - formr_log_exception($e, __METHOD__); - alert('Couldn\'t save file.', 'alert-danger'); - return false; - } - } - - public function exportXLSX(PDOStatement $stmt, $filename) { - if (!$stmt->columnCount()) { - return false; - } - - try { - $phpSpreadsheet = $this->objectFromPDOStatement($stmt); - $phpSpreadsheetWriter = \PhpOffice\PhpSpreadsheet\IOFactory::createWriter($phpSpreadsheet, 'Xlsx'); - header('Content-Disposition: attachment;filename="' . $filename . '.xlsx"'); - header('Cache-Control: max-age=0'); - header('Content-Type: application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'); - $phpSpreadsheetWriter->save('php://output'); - exit; - } catch (Exception $e) { - formr_log_exception($e, __METHOD__); - alert('Couldn\'t save file.', 'alert-danger'); - return false; - } - } - - private function getSheetsFromArrays($items, $choices = array(), $settings = array()) { - set_time_limit(300); # defaults to 30 - ini_set('memory_limit', Config::get('memory_limit.spr_sheets_array')); - - $objPhpSpreadsheet = new \PhpOffice\PhpSpreadsheet\Spreadsheet(); - $objPhpSpreadsheet->getDefaultStyle()->getFont()->setName('Helvetica'); - $objPhpSpreadsheet->getDefaultStyle()->getFont()->setSize(16); - $objPhpSpreadsheet->getDefaultStyle()->getAlignment()->setWrapText(true); - $sheet_index = $objPhpSpreadsheet->getSheetCount() - 1; - - if (is_array($choices) && count($choices) > 0): - $objPhpSpreadsheet->createSheet(); - $sheet_index++; - array_unshift($choices, array_keys(current($choices))); - $objPhpSpreadsheet->getSheet($sheet_index)->getDefaultColumnDimension()->setWidth(20); - $objPhpSpreadsheet->getSheet($sheet_index)->getColumnDimension('A')->setWidth(20); # list_name - $objPhpSpreadsheet->getSheet($sheet_index)->getColumnDimension('B')->setWidth(20); # name - $objPhpSpreadsheet->getSheet($sheet_index)->getColumnDimension('C')->setWidth(30); # label - - $objPhpSpreadsheet->getSheet($sheet_index)->fromArray($choices); - $objPhpSpreadsheet->getSheet($sheet_index)->setTitle('choices'); - $objPhpSpreadsheet->getSheet($sheet_index)->getStyle('A1:C1')->applyFromArray(array('font' => array('bold' => true))); - endif; - - if (is_array($settings) && count($settings) > 0): - // put settings in a suitable format for excel sheet - $sttgs = array(array('item', 'value')); - foreach ($settings as $item => $value) { - $sttgs[] = array('item' => $item, 'value' => (string)$value); - } - - $objPhpSpreadsheet->createSheet(); - $sheet_index++; - $objPhpSpreadsheet->getSheet($sheet_index)->getDefaultColumnDimension()->setWidth(20); - $objPhpSpreadsheet->getSheet($sheet_index)->getColumnDimension('A')->setWidth(20); # item - $objPhpSpreadsheet->getSheet($sheet_index)->getColumnDimension('B')->setWidth(20); # value - - $objPhpSpreadsheet->getSheet($sheet_index)->fromArray($sttgs); - $objPhpSpreadsheet->getSheet($sheet_index)->setTitle('settings'); - $objPhpSpreadsheet->getSheet($sheet_index)->getStyle('A1:C1')->applyFromArray(array('font' => array('bold' => true))); - endif; - - array_unshift($items, array_keys(current($items))); - $objPhpSpreadsheet->getSheet(0)->getColumnDimension('A')->setWidth(20); # type - $objPhpSpreadsheet->getSheet(0)->getColumnDimension('B')->setWidth(20); # name - $objPhpSpreadsheet->getSheet(0)->getColumnDimension('C')->setWidth(30); # label - $objPhpSpreadsheet->getSheet(0)->getColumnDimension('D')->setWidth(3); # optional - $objPhpSpreadsheet->getSheet(0)->getStyle('D1')->getAlignment()->setWrapText(false); - - $objPhpSpreadsheet->getSheet(0)->fromArray($items); - $objPhpSpreadsheet->getSheet(0)->setTitle('survey'); - $objPhpSpreadsheet->getSheet(0)->getStyle('A1:H1')->applyFromArray(array('font' => array('bold' => true))); - - return $objPhpSpreadsheet; - } - - public function exportItemTableXLSX(Survey $study) { - $items = $study->getItemsForSheet(); - $choices = $study->getChoicesForSheet(); - $filename = $study->name; - - try { - $objPhpSpreadsheet = $this->getSheetsFromArrays($items, $choices, $study->settings); - $objWriter = \PhpOffice\PhpSpreadsheet\IOFactory::createWriter($objPhpSpreadsheet, 'Xlsx'); - - header('Content-Disposition: attachment;filename="' . $filename . '.xlsx"'); - header('Cache-Control: max-age=0'); - header('Content-Type: application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'); - - $objWriter->save('php://output'); - exit; - } catch (Exception $e) { - formr_log_exception($e, __CLASS__); - alert("Couldn't save file.", 'alert-danger'); - return false; - } - } - - public function exportItemTableXLS(Survey $study) { - $items = $study->getItemsForSheet(); - $choices = $study->getChoicesForSheet(); - $filename = $study->name; - - try { - $objPhpSpreadsheet = $this->getSheetsFromArrays($items, $choices, $study->settings); - - $objWriter = \PhpOffice\PhpSpreadsheet\IOFactory::createWriter($objPhpSpreadsheet, 'Xls'); - - header('Content-Disposition: attachment;filename="' . $filename . '.xls"'); - header('Cache-Control: max-age=0'); - header('Content-Type: application/vnd.ms-excel; charset=utf-8'); - $objWriter->save('php://output'); - exit; - } catch (Exception $e) { - formr_log_exception($e, __CLASS__); - alert("Couldn't save file.", 'alert-danger'); - return false; - } - } - - public function exportItemTableJSON(Survey $study, $return_object = false) { - $items = $study->getItems(); - $choices = $study->getChoices(); - $filename = $study->name; - - foreach ($items as $i => $val) { - unset($items[$i]['id'], $items[$i]['study_id']); - if (isset($val["choice_list"]) && isset($choices[$val["choice_list"]])) { - $items[$i]["choices"] = $choices[$val["choice_list"]]; - $items[$i]["choice_list"] = $items[$i]["name"]; - } - } - - $object = array( - 'name' => $study->name, - 'items' => $items, - 'settings' => $study->settings, - ); - - if ($google_id = $study->getGoogleFileId()) { - $object['google_sheet'] = google_get_sheet_link($google_id); - } - - if ($return_object === true) { - return $object; - } - - header('Content-Disposition: attachment;filename="' . $filename . '.json"'); - header('Cache-Control: max-age=0'); - header('Content-type: application/json; charset=utf-8'); - - try { - echo json_encode($object, JSON_PRETTY_PRINT + JSON_UNESCAPED_UNICODE + JSON_NUMERIC_CHECK); - exit; - } catch (Exception $e) { - formr_log_exception($e, __CLASS__); - alert("Couldn't save file.", 'alert-danger'); - return false; - } - } - - private function translateLegacyColumn($col) { - $col = trim(mb_strtolower($col)); - $translations = array( - 'variablenname' => 'name', - 'typ' => 'type', - 'wortlaut' => 'label', - 'text' => 'label', - 'ratinguntererpol' => 'choice1', - 'ratingobererpol' => 'choice2', - ); - - if (mb_substr($col, 0, 5) == 'mcalt') { - return 'choice' . mb_substr($col, 5); - } else { - return isset($translations[$col]) ? $translations[$col] : $col; - } - } - - private function translateLegacyType($type) { - $type = trim(mb_strtolower($type)); - $translations = array( - 'offen' => 'text', - 'instruktion' => 'note', - 'instruction' => 'note', - 'fork' => 'note', - 'rating' => 'rating_button', - 'mmc' => 'mc_multiple', - 'select' => 'select_one', - 'mselect' => 'select_multiple', - 'select_or_add' => 'select_or_add_one', - 'mselect_add' => 'select_or_add_multiple', - 'btnrating' => 'rating_button', - 'range_list' => 'range_ticks', - 'btnradio' => 'mc_button', - 'btncheckbox' => 'mc_multiple_button', - 'btncheck' => 'check_button', - 'geolocation' => 'geopoint', - 'mcnt' => 'mc', - ); - - return isset($translations[$type]) ? $translations[$type] : $type; - } - - public function addSurveyItem(array $row) { - // @todo validate items in $data - if (empty($row['name'])) { - $this->warnings[] = "Skipping row with no 'item name' specified"; - return; - } - - foreach ($row as $key => $value) { - if (!in_array($key, $this->survey_columns) && !in_array($key, $this->internal_columns)) { - $this->errors[] = "Column name '{$key}' for item '{$row['name']}' is not allowed"; - return; - } - } - $this->survey[] = $row; - } - - public function readItemTableFile($filepath) { - ini_set('max_execution_time', 360); - - $this->errors = $this->messages = $this->warnings = array(); - if (!file_exists($filepath)) { - $this->errors[] = 'Item table file does not exist'; - return; - } - - try { - // Identify the type of $filepath and create \PhpOffice\PhpSpreadsheet\Spreadsheet object from a read-only reader - $filetype = \PhpOffice\PhpSpreadsheet\IOFactory::identify($filepath); - $phpSpreadsheetReader = \PhpOffice\PhpSpreadsheet\IOFactory::createReader($filetype); - $phpSpreadsheetReader->setReadDataOnly(true); - - /* @var $phpSpreadsheet \PhpOffice\PhpSpreadsheet\Spreadsheet */ - $phpSpreadsheet = $phpSpreadsheetReader->load($filepath); - - // Gather sheets to be read - if ($phpSpreadsheet->sheetNameExists('survey')) { - $surveySheet = $phpSpreadsheet->getSheetByName('survey'); - } else { - $surveySheet = $phpSpreadsheet->getSheet(0); - } - - if ($phpSpreadsheet->sheetNameExists('choices') && $phpSpreadsheet->getSheetCount() > 1) { - $choicesSheet = $phpSpreadsheet->getSheetByName('choices'); - } elseif ($phpSpreadsheet->getSheetCount() > 1) { - $choicesSheet = $phpSpreadsheet->getSheet(1); - } - - if (isset($choicesSheet)) { - $this->readChoicesSheet($choicesSheet); - } - - $this->readSurveySheet($surveySheet); - - } catch (\PhpOffice\PhpSpreadsheet\Exception $e) { - $this->errors[] = "An error occured reading your excel file. Please check your file or report to admin"; - $this->errors[] = $e->getMessage(); - formr_log_exception($e, __CLASS__, $filepath); - return; - } - } - - private function readChoicesSheet(\PhpOffice\PhpSpreadsheet\Worksheet\Worksheet $worksheet) { - // Get worksheet dimensions - // non-allowed columns will be ignored, allows to specify auxiliary information if needed - $skippedColumns = $columns = array(); - $colCount = \PhpOffice\PhpSpreadsheet\Cell\Coordinate::columnIndexFromString($worksheet->getHighestDataColumn()); - $rowCount = $worksheet->getHighestDataRow(); - - for ($i = 1; $i <= $colCount; $i++) { - $colName = mb_strtolower($worksheet->getCellByColumnAndRow($i, 1)->getValue()); - if (in_array($colName, $this->choices_columns)) { - $columns[$i] = $colName; - } else { - $skippedColumns[$i] = $colName; - } - } - - if (!in_array('list_name', $columns)) { - $this->errors[] = 'You forgot to define the "list_name" column on the choices sheet.'; - } - if (!in_array('name', $columns)) { - $this->errors[] = 'You forgot to define the "name" column on the choices sheet'; - } - if (!in_array('label', $columns)) { - $this->errors[] = 'You forgot to define the "label" column on the choices sheet.'; - } - - if ($this->errors) { - return false; - } - - if ($skippedColumns) { - $this->warnings[] = sprintf('Choices worksheet "%s" skipped columns: %s', $worksheet->getTitle(), implode($skippedColumns, ", ")); - } - $this->messages[] = sprintf('Choices worksheet "%s" used columns: %s', $worksheet->getTitle(), implode($columns, ", ")); - - $data = array(); - $choiceNames = array(); - $inheritedListNames = array(); - - foreach ($worksheet->getRowIterator(1, $rowCount) as $row) { - /* @var $row \PhpOffice\PhpSpreadsheet\Worksheet\Row */ - $rowNumber = $row->getRowIndex(); - if ($rowNumber == 1) { - // skip table head - continue; - } - if($rowNumber > $rowCount) break; - - $data[$rowNumber] = array(); - $cellIterator = $row->getCellIterator('A', $worksheet->getHighestDataColumn()); - $cellIterator->setIterateOnlyExistingCells(false); - foreach ($cellIterator as $cell) { - /* @var $cell \PhpOffice\PhpSpreadsheet\Cell\Cell */ - if (is_null($cell)) { - continue; - } - - $colNumber = \PhpOffice\PhpSpreadsheet\Cell\Coordinate::columnIndexFromString($cell->getColumn()); - if (!isset($columns[$colNumber])) { - continue; // not a column of interest - } - $colName = $columns[$colNumber]; - $cellValue = hardTrueFalse(Normalizer::normalize($cell->getValue(), Normalizer::FORM_C)); - $cellValue = trim($cellValue); - - if ($colName == 'list_name') { - if ($cellValue && !preg_match("/^[a-zA-Z0-9_]{1,255}$/", $cellValue)) { - $this->errors[] = __("The list name '%s' is invalid. It has to be between 1 and 255 characters long. It may not contain anything other than the characters from a to Z, 0 to 9 and the underscore.", $cellValue); - } - - if (!$cellValue && !isset($lastListName)) { - $this->warnings[] = __('Skipping Row %s of choices sheet', $rowNumber); - unset($data[$rowNumber]); - continue 2; - } elseif (!$cellValue && isset($lastListName)) { - $cellValue = $lastListName; - if (!isset($inheritedListNames[$cellValue])) { - $inheritedListNames[$cellValue] = array(); - } - $inheritedListNames[$cellValue][] = $rowNumber; - } - - if (!in_array($cellValue, $this->existing_choice_lists)) { - $this->existing_choice_lists[] = $cellValue; - $data[$rowNumber]['list_name'] = $cellValue; - } elseif (in_array($cellValue, $this->existing_choice_lists) && $lastListName != $cellValue) { - $this->errors[] = __("We found a discontinuous list: the same list name ('%s') was used before row %s, but other lists came in between.", $cellValue, $rowNumber); - } else { - //$data[$rowNumber]['list_name'] = $cellValue; - } - - $lastListName = $cellValue; - } elseif ($colName == 'name') { - if (!is_formr_truthy($cellValue)) { - $this->warnings[] = __("Skipping Row %s of choices sheet: Choice name empty, but content in other columns.", $rowNumber); - if (isset($inheritedListNames[$data[$rowNumber]['list_name']])) { - // remove this row from bookmarks - $bmk = &$inheritedListNames[$data[$rowNumber]['list_name']]; - if (($key = array_search($rowNumber, $bmk)) !== false) { - unset($bmk[$key]); - } - } - unset($data[$rowNumber]); - continue 2; - } - if (!preg_match("/^.{1,255}$/", $cellValue)) { - $this->errors[] = __("The choice name '%s' is invalid. It has to be between 1 and 255 characters long.", $cellValue); - } - //$data[$rowNumber]['name'] = $cellValue; - - } elseif ($colName == 'label') { - if (!$cellValue && isset($data[$rowNumber]['name'])) { - $cellValue = $data[$rowNumber]['name']; - } - //$data[$rowNumber]['label'] = $cellValue; - } - - // Stop processing if we have any errors - if ($this->errors) { - $error = sprintf("Error in cell %s%s (Choices sheet): \n %s", $cell->getColumn(), $rowNumber, implode("\n", $this->errors)); - throw new \PhpOffice\PhpSpreadsheet\Exception($error); - } - - // Save cell value - $data[$rowNumber][$colName] = $cellValue; - } // Cell loop - - } // Rows loop - - // Data has been gathered, group lists by list_name and check if there are duplicates for each list. - foreach ($data as $rowNumber => $row) { - if (!isset($choiceNames[$row['list_name']])) { - $choiceNames[$row['list_name']] = array(); - } - if (isset($choiceNames[$row['list_name']][$row['name']])) { - throw new \PhpOffice\PhpSpreadsheet\Exception(sprintf("'%s' has already been used as a 'name' for the list '%s'", $row['name'], $row['list_name'])); - } - $choiceNames[$row['list_name']][$row['name']] = $row['label']; - } - - // Announce rows that inherited list_names - $msgs = array(); - foreach ($inheritedListNames as $name => $rows) { - $msgs[] = $rows ? sprintf("%s: This list name was assigned to rows %s - %s automatically, because they had an empty list name and followed in this list.", $name, min($rows), max($rows)) : null; - } - if ($msgs = array_filter($msgs)) { - $this->messages[] = '
    • ' . implode('
    • ', $msgs) . '
    '; - } - - $this->choices = $data; - } - - private function readSurveySheet(\PhpOffice\PhpSpreadsheet\Worksheet\Worksheet $worksheet) { - $callStartTime = microtime(true); - // non-allowed columns will be ignored, allows to specify auxiliary information if needed - - $skippedColumns = $columns = array(); - $colCount = \PhpOffice\PhpSpreadsheet\Cell\Coordinate::columnIndexFromString($worksheet->getHighestDataColumn()); - $rowCount = $worksheet->getHighestDataRow(); - - if ($colCount > 30) { - $this->warnings[] = __('Only the first 30 columns out of %d were read.', $colCount); - $colCount = 30; - } - - $blankColCount = 0; // why should this be set to 1 by default? - for ($i = 1; $i <= $colCount; $i++) { - $colName = trim(mb_strtolower($worksheet->getCellByColumnAndRow($i, 1)->getValue())); - if (!$colName) { - $blankColCount++; - continue; - } - if (in_array($colName, $this->survey_columns)) { - $trColName = $this->translateLegacyColumn($colName); - if ($colName != $trColName) { - $this->warnings[] = __('The column "%s" is deprecated and was automatically translated to "%s"', $colName, $trColName); - } - $columns[$i] = $trColName; - } else { - $skippedColumns[$i] = $colName; - } - - if ($colName == 'choice1' && (!in_array('name', $columns) || !in_array('type', $columns))) { - $this->errors[] = "The 'name' and 'type' column have to be placed to the left of all choice columns."; - return false; - } - } - - if ($blankColCount) { - $this->warnings[] = __('Your survey sheet appears to contain %d columns without names (given in the first row).', $blankColCount); - } - if ($skippedColumns) { - $this->warnings[] = __('These survey sheet columns were skipped: %s', implode($skippedColumns, ', ')); - } - - $data = $skippedRows = $emptyRows = $variableNames = array(); - - foreach ($worksheet->getRowIterator(1, $rowCount) as $row) { - /* @var $row \PhpOffice\PhpSpreadsheet\Worksheet\Row */ - $rowNumber = $row->getRowIndex(); - if ($rowNumber == 1) { - // skip table head - continue; - } - if($rowNumber > $rowCount) break; - - $data[$rowNumber] = array(); - $cellIterator = $row->getCellIterator('A', $worksheet->getHighestDataColumn()); - $cellIterator->setIterateOnlyExistingCells(false); - - foreach ($cellIterator as $cell) { - /* @var $cell \PhpOffice\PhpSpreadsheet\Cell\Cell */ - if (is_null($cell)) { - continue; - } - - $colNumber = \PhpOffice\PhpSpreadsheet\Cell\Coordinate::columnIndexFromString($cell->getColumn()); - if (!isset($columns[$colNumber])) { - continue; // not a column of interest - } - $colName = $columns[$colNumber]; - if (isset($data[$rowNumber][$colName])) { - continue; // dont overwrite set columns - } - - $cellValue = trim(hardTrueFalse(Normalizer::normalize($cell->getValue(), Normalizer::FORM_C))); - - if ($colName == 'name') { - if (!$cellValue) { - if (!empty($data[$rowNumber])) { - $skippedRows[] = $rowNumber; - } else { - $emptyRows[] = $rowNumber; - } - unset($data[$rowNumber]); - continue 2; // Skip row with no item name - } elseif (!preg_match('/^[a-zA-Z][a-zA-Z0-9_]{1,64}$/', $cellValue)) { - $this->errors[] = __("The variable name '%s' is invalid. It has to be between 1 and 64 characters. It needs to start with a letter and can only contain the characters from a to Z, 0 to 9 and the underscore.", $cellValue); - } - - if (in_array($cellValue, array('session_id', 'created', 'modified', 'ended'))) { - $this->errors[] = __("Row %s: variable name '%s' is not permitted.", $rowNumber, $cellValue); - } - - if (($existingRow = array_search(mb_strtolower($cellValue), $variableNames)) === false) { - $variableNames[$rowNumber] = mb_strtolower($cellValue); - } else { - $this->errors[] = __("Row %s: Variable name '%s' already appeared in row %s", $rowNumber, $cellValue, $existingRow); - } - - } elseif ($colName == 'type') { - if (mb_strpos($cellValue, ' ') !== false) { - $typeOptions = explode(' ', trim(preg_replace('/\s+/', ' ', $cellValue))); // get real type and options - $type = $typeOptions[0]; - unset($typeOptions[0]); - if (!empty($typeOptions[1]) && - !in_array($type, array('server', 'get', 'text', 'textarea', 'letters', 'file', 'image', 'rating_button', 'submit')) && - preg_match('/^[A-Za-z0-9_]{1,20}$/', trim($typeOptions[1]))) { - $data[$rowNumber]['choice_list'] = trim($typeOptions[1]); - unset($typeOptions[1]); - } - $data[$rowNumber]['type_options'] = implode(' ', $typeOptions); - $cellValue = $type; - } - - $trType = $this->translateLegacyType($cellValue); - if ($trType != $cellValue) { - $this->warnings[] = __('The type "%s" is deprecated and was automatically translated to "%s"', $cellValue, $trType); - } - $cellValue = $trType; - - } elseif ($colName == 'optional') { - if ($cellValue === '*') { - $cellValue = 1; - } elseif ($cellValue === '!') { - $cellValue = 0; - } else { - $cellValue = null; - } - - } elseif (strpos($colName, 'choice') === 0 && is_formr_truthy($cellValue) && isset($data[$rowNumber])) { - $choiceValue = substr($colName, 6); - $this->choices[] = array( - 'list_name' => $data[$rowNumber]['name'], - 'name' => $choiceValue, - 'label' => $cellValue, - ); - - if (!isset($data[$rowNumber]['choice_list'])) { - $data[$rowNumber]['choice_list'] = $data[$rowNumber]['name']; - } elseif (isset($data[$rowNumber]['choice_list']) && $choiceValue == 1) { - $this->errors[] = __("Row %s: You defined both a named choice_list '%s' for item '%s' and a nonempty choice1 column. Choose one.", $rowNumber, $data[$rowNumber]['choice_list'], $data[$rowNumber]['name']); - } - } - - // Stop processing if we have any errors - if ($this->errors) { - $error = sprintf("Error in cell %s%s (Survey Sheet): \n %s", $cell->getColumn(), $rowNumber, implode("\n", $this->errors)); - throw new \PhpOffice\PhpSpreadsheet\Exception($error); - } - - // Save cell value - $data[$rowNumber][$colName] = $cellValue; - - } // Cell Loop - - $data[$rowNumber]['order'] = $rowNumber - 1; - // if no order is entered, use row_number - if(!isset($data[$rowNumber]['item_order']) || !is_formr_truthy($data[$rowNumber]['item_order'])) { - $data[$rowNumber]['item_order'] = $data[$rowNumber]['order']; - } - } // Rows Loop - - $callEndTime = microtime(true); - $callTime = $callEndTime - $callStartTime; - $this->messages[] = 'Survey worksheet - ' . $worksheet->getTitle() . ' (' . count($data) . ' non-empty rows, ' . $colCount . ' columns). These columns were used: ' . implode($columns, ", "); - - if (!empty($emptyRows)) { - $this->messages[] = __('Empty rows (no variable name): %s', implode($emptyRows, ", ")); - } - - if (!empty($skippedRows)) { - $this->warnings[] = __('Skipped rows (no variable name): %s. Variable name empty, but other columns had content. Double-check that you did not forget to define a variable name for a proper item.', implode($skippedRows, ", ")); - } - - $this->survey = $data; - } + private $choices_columns = array('list_name', 'name', 'label'); + private $survey_columns = array('name', 'type', 'label', 'optional', 'class', 'showif', 'choice1', 'choice2', 'choice3', 'choice4', 'choice5', 'choice6', 'choice7', 'choice8', 'choice9', 'choice10', 'choice11', 'choice12', 'choice13', 'choice14', 'value', 'order', 'block_order', 'item_order', + # legacy + 'variablenname', 'wortlaut', 'typ', 'ratinguntererpol', 'ratingobererpol', 'mcalt1', 'mcalt2', 'mcalt3', 'mcalt4', 'mcalt5', 'mcalt6', 'mcalt7', 'mcalt8', 'mcalt9', 'mcalt10', 'mcalt11', 'mcalt12', 'mcalt13', 'mcalt14',); + private $internal_columns = array('choice_list', 'type_options', 'label_parsed'); + private $existing_choice_lists = array(); + public $messages = array(); + public $errors = array(); + public $warnings = array(); + public $survey = array(); + public $choices = array(); + public static $exportFormats = array('csv', 'csv_german', 'tsv', 'xlsx', 'xls', 'json'); + + public static function verifyExportFormat($formatstring) { + if (!in_array($formatstring, static::$exportFormats)) { + formr_error(400, 'Bad Request', 'Unsupported export format requested.'); + } + } + + public function backupTSV($array, $filename) { + $objPhpSpreadsheet = $this->objectFromArray($array); + + $objWriter = \PhpOffice\PhpSpreadsheet\IOFactory::createWriter($objPhpSpreadsheet, 'Csv'); + $objWriter->setDelimiter("\t"); + $objWriter->setEnclosure(""); + + try { + $objWriter->save($filename); + return true; + } catch (Exception $e) { + formr_log_exception($e, __CLASS__); + alert("Couldn't save file.", 'alert-danger'); + return false; + } + } + + protected function objectFromArray($array) { + set_time_limit(300); # defaults to 30 + ini_set('memory_limit', Config::get('memory_limit.spr_object_array')); + + $objPhpSpreadsheet = new \PhpOffice\PhpSpreadsheet\Spreadsheet(); + $current = current($array); + if (!$current) { + return $objPhpSpreadsheet; + } + array_unshift($array, array_keys($current)); + $objPhpSpreadsheet->getSheet(0)->fromArray($array); + + return $objPhpSpreadsheet; + } + + /** + * + * @param PDOStatement $stmt + * @return \PhpOffice\PhpSpreadsheet\Spreadsheet; + */ + protected function objectFromPDOStatement(PDOStatement $stmt) { + $PhpSpreadsheet = new \PhpOffice\PhpSpreadsheet\Spreadsheet(); + $PhpSpreadsheetSheet = $PhpSpreadsheet->getSheet(0); + + list ($startColumn, $startRow) = \PhpOffice\PhpSpreadsheet\Cell\Coordinate::coordinateFromString('A1'); + $writeColumns = true; + while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) { + if ($writeColumns) { + $columns = array_keys($row); + $currentColumn = $startColumn; + foreach ($columns as $cellValue) { + $PhpSpreadsheetSheet->getCell($currentColumn . $startRow)->setValue($cellValue); + ++$currentColumn; + } + ++$startRow; + $writeColumns = false; + } + $currentColumn = $startColumn; + foreach ($row as $cellValue) { + $PhpSpreadsheetSheet->getCell($currentColumn . $startRow)->setValue($cellValue); + ++$currentColumn; + } + ++$startRow; + } + return $PhpSpreadsheet; + } + + public function exportInRequestedFormat(PDOStatement $resultsStmt, $filename, $filetype) { + self::verifyExportFormat($filetype); + + switch ($filetype) { + case 'xlsx': + $download_successfull = $this->exportXLSX($resultsStmt, $filename); + break; + case 'xls': + $download_successfull = $this->exportXLS($resultsStmt, $filename); + break; + case 'csv_german': + $download_successfull = $this->exportCSV_german($resultsStmt, $filename); + break; + case 'tsv': + $download_successfull = $this->exportTSV($resultsStmt, $filename); + break; + case 'json': + $download_successfull = $this->exportJSON($resultsStmt, $filename); + break; + default: + $download_successfull = $this->exportCSV($resultsStmt, $filename); + break; + } + + + return $download_successfull; + } + + public function exportCSV(PDOStatement $stmt, $filename) { + if (!$stmt->columnCount()) { + formr_log('Debug: column count is not set'); + return false; + } + + try { + $phpSpreadsheet = $this->objectFromPDOStatement($stmt); + $phpSpreadsheetWriter = \PhpOffice\PhpSpreadsheet\IOFactory::createWriter($phpSpreadsheet, 'Csv'); + + header('Content-Disposition: attachment;filename="' . $filename . '.csv"'); + header('Cache-Control: max-age=0'); + header('Content-Type: text/csv'); + $phpSpreadsheetWriter->save('php://output'); + exit; + } catch (Exception $e) { + formr_log_exception($e, __METHOD__); + alert('Couldn\'t save file.', 'alert-danger'); + return false; + } + } + + public function exportJSON($object, $filename) { + set_time_limit(300); + if ($object instanceof PDOStatement) { + $file = APPLICATION_ROOT . "tmp/downloads/{$filename}.json"; + file_put_contents($file, ''); + + $handle = fopen($file, 'w+'); + while ($row = $object->fetch(PDO::FETCH_ASSOC)) { + fwrite_json($handle, $row); + } + fclose($handle); + + header('Content-Disposition: attachment;filename="' . $filename . '.json"'); + header('Cache-Control: max-age=0'); + header('Content-type: application/json; charset=utf-8'); + Config::get('use_xsendfile') ? header('X-Sendfile: ' . $file) : readfile($file); + exit; + } else { + header('Content-Disposition: attachment;filename="' . $filename . '.json"'); + header('Cache-Control: max-age=0'); + header('Content-type: application/json; charset=utf-8'); + echo json_encode($object, JSON_PRETTY_PRINT + JSON_UNESCAPED_UNICODE + JSON_NUMERIC_CHECK); + exit; + } + } + + public function exportTSV(PDOStatement $stmt, $filename, $savefile = null) { + if (!$stmt->columnCount()) { + return false; + } + + try { + $phpSpreadsheet = $this->objectFromPDOStatement($stmt); + $phpSpreadsheetWriter = \PhpOffice\PhpSpreadsheet\IOFactory::createWriter($phpSpreadsheet, 'Csv'); + $phpSpreadsheetWriter->setDelimiter("\t"); + $phpSpreadsheetWriter->setEnclosure(""); + + if ($savefile === null) { + header('Content-Disposition: attachment;filename="' . $filename . '.tab"'); + header('Cache-Control: max-age=0'); + header('Content-Type: text/csv'); // or maybe text/tab-separated-values? + $phpSpreadsheetWriter->save('php://output'); + exit; + } else { + $phpSpreadsheetWriter->save($savefile); + return true; + } + } catch (Exception $e) { + formr_log_exception($e, __METHOD__); + alert('Couldn\'t save file.', 'alert-danger'); + return false; + } + } + + public function exportCSV_german(PDOStatement $stmt, $filename, $savefile = null) { + if (!$stmt->columnCount()) { + return false; + } + + try { + $phpSpreadsheet = $this->objectFromPDOStatement($stmt); + $phpSpreadsheetWriter = \PhpOffice\PhpSpreadsheet\IOFactory::createWriter($phpSpreadsheet, 'Csv'); + $phpSpreadsheetWriter->setDelimiter(';'); + $phpSpreadsheetWriter->setEnclosure('"'); + + if ($savefile === null) { + header('Content-Disposition: attachment;filename="' . $filename . '.csv"'); + header('Cache-Control: max-age=0'); + header('Content-Type: text/csv'); + $phpSpreadsheetWriter->save('php://output'); + exit; + } else { + $phpSpreadsheetWriter->save($savefile); + return true; + } + } catch (Exception $e) { + formr_log_exception($e, __METHOD__); + alert('Couldn\'t save file.', 'alert-danger'); + return false; + } + } + + public function exportXLS(PDOStatement $stmt, $filename) { + if (!$stmt->columnCount()) { + return false; + } + + try { + $phpSpreadsheet = $this->objectFromPDOStatement($stmt); + $phpSpreadsheetWriter = \PhpOffice\PhpSpreadsheet\IOFactory::createWriter($phpSpreadsheet, 'Xls'); + header('Content-Disposition: attachment;filename="' . $filename . '.xls"'); + header('Cache-Control: max-age=0'); + header('Content-Type: application/vnd.ms-excel'); + $phpSpreadsheetWriter->save('php://output'); + exit; + } catch (Exception $e) { + formr_log_exception($e, __METHOD__); + alert('Couldn\'t save file.', 'alert-danger'); + return false; + } + } + + public function exportXLSX(PDOStatement $stmt, $filename) { + if (!$stmt->columnCount()) { + return false; + } + + try { + $phpSpreadsheet = $this->objectFromPDOStatement($stmt); + $phpSpreadsheetWriter = \PhpOffice\PhpSpreadsheet\IOFactory::createWriter($phpSpreadsheet, 'Xlsx'); + header('Content-Disposition: attachment;filename="' . $filename . '.xlsx"'); + header('Cache-Control: max-age=0'); + header('Content-Type: application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'); + $phpSpreadsheetWriter->save('php://output'); + exit; + } catch (Exception $e) { + formr_log_exception($e, __METHOD__); + alert('Couldn\'t save file.', 'alert-danger'); + return false; + } + } + + private function getSheetsFromArrays($items, $choices = array(), $settings = array()) { + set_time_limit(300); # defaults to 30 + ini_set('memory_limit', Config::get('memory_limit.spr_sheets_array')); + + $objPhpSpreadsheet = new \PhpOffice\PhpSpreadsheet\Spreadsheet(); + $objPhpSpreadsheet->getDefaultStyle()->getFont()->setName('Helvetica'); + $objPhpSpreadsheet->getDefaultStyle()->getFont()->setSize(16); + $objPhpSpreadsheet->getDefaultStyle()->getAlignment()->setWrapText(true); + $sheet_index = $objPhpSpreadsheet->getSheetCount() - 1; + + if (is_array($choices) && count($choices) > 0): + $objPhpSpreadsheet->createSheet(); + $sheet_index++; + array_unshift($choices, array_keys(current($choices))); + $objPhpSpreadsheet->getSheet($sheet_index)->getDefaultColumnDimension()->setWidth(20); + $objPhpSpreadsheet->getSheet($sheet_index)->getColumnDimension('A')->setWidth(20); # list_name + $objPhpSpreadsheet->getSheet($sheet_index)->getColumnDimension('B')->setWidth(20); # name + $objPhpSpreadsheet->getSheet($sheet_index)->getColumnDimension('C')->setWidth(30); # label + + $objPhpSpreadsheet->getSheet($sheet_index)->fromArray($choices); + $objPhpSpreadsheet->getSheet($sheet_index)->setTitle('choices'); + $objPhpSpreadsheet->getSheet($sheet_index)->getStyle('A1:C1')->applyFromArray(array('font' => array('bold' => true))); + endif; + + if (is_array($settings) && count($settings) > 0): + // put settings in a suitable format for excel sheet + $sttgs = array(array('item', 'value')); + foreach ($settings as $item => $value) { + $sttgs[] = array('item' => $item, 'value' => (string) $value); + } + + $objPhpSpreadsheet->createSheet(); + $sheet_index++; + $objPhpSpreadsheet->getSheet($sheet_index)->getDefaultColumnDimension()->setWidth(20); + $objPhpSpreadsheet->getSheet($sheet_index)->getColumnDimension('A')->setWidth(20); # item + $objPhpSpreadsheet->getSheet($sheet_index)->getColumnDimension('B')->setWidth(20); # value + + $objPhpSpreadsheet->getSheet($sheet_index)->fromArray($sttgs); + $objPhpSpreadsheet->getSheet($sheet_index)->setTitle('settings'); + $objPhpSpreadsheet->getSheet($sheet_index)->getStyle('A1:C1')->applyFromArray(array('font' => array('bold' => true))); + endif; + + array_unshift($items, array_keys(current($items))); + $objPhpSpreadsheet->getSheet(0)->getColumnDimension('A')->setWidth(20); # type + $objPhpSpreadsheet->getSheet(0)->getColumnDimension('B')->setWidth(20); # name + $objPhpSpreadsheet->getSheet(0)->getColumnDimension('C')->setWidth(30); # label + $objPhpSpreadsheet->getSheet(0)->getColumnDimension('D')->setWidth(3); # optional + $objPhpSpreadsheet->getSheet(0)->getStyle('D1')->getAlignment()->setWrapText(false); + + $objPhpSpreadsheet->getSheet(0)->fromArray($items); + $objPhpSpreadsheet->getSheet(0)->setTitle('survey'); + $objPhpSpreadsheet->getSheet(0)->getStyle('A1:H1')->applyFromArray(array('font' => array('bold' => true))); + + return $objPhpSpreadsheet; + } + + public function exportItemTableXLSX(Survey $study) { + $items = $study->getItemsForSheet(); + $choices = $study->getChoicesForSheet(); + $filename = $study->name; + + try { + $objPhpSpreadsheet = $this->getSheetsFromArrays($items, $choices, $study->settings); + $objWriter = \PhpOffice\PhpSpreadsheet\IOFactory::createWriter($objPhpSpreadsheet, 'Xlsx'); + + header('Content-Disposition: attachment;filename="' . $filename . '.xlsx"'); + header('Cache-Control: max-age=0'); + header('Content-Type: application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'); + + $objWriter->save('php://output'); + exit; + } catch (Exception $e) { + formr_log_exception($e, __CLASS__); + alert("Couldn't save file.", 'alert-danger'); + return false; + } + } + + public function exportItemTableXLS(Survey $study) { + $items = $study->getItemsForSheet(); + $choices = $study->getChoicesForSheet(); + $filename = $study->name; + + try { + $objPhpSpreadsheet = $this->getSheetsFromArrays($items, $choices, $study->settings); + + $objWriter = \PhpOffice\PhpSpreadsheet\IOFactory::createWriter($objPhpSpreadsheet, 'Xls'); + + header('Content-Disposition: attachment;filename="' . $filename . '.xls"'); + header('Cache-Control: max-age=0'); + header('Content-Type: application/vnd.ms-excel; charset=utf-8'); + $objWriter->save('php://output'); + exit; + } catch (Exception $e) { + formr_log_exception($e, __CLASS__); + alert("Couldn't save file.", 'alert-danger'); + return false; + } + } + + public function exportItemTableJSON(Survey $study, $return_object = false) { + $items = $study->getItems(); + $choices = $study->getChoices(); + $filename = $study->name; + + foreach ($items as $i => $val) { + unset($items[$i]['id'], $items[$i]['study_id']); + if (isset($val["choice_list"]) && isset($choices[$val["choice_list"]])) { + $items[$i]["choices"] = $choices[$val["choice_list"]]; + $items[$i]["choice_list"] = $items[$i]["name"]; + } + } + + $object = array( + 'name' => $study->name, + 'items' => $items, + 'settings' => $study->settings, + ); + + if ($google_id = $study->getGoogleFileId()) { + $object['google_sheet'] = google_get_sheet_link($google_id); + } + + if ($return_object === true) { + return $object; + } + + header('Content-Disposition: attachment;filename="' . $filename . '.json"'); + header('Cache-Control: max-age=0'); + header('Content-type: application/json; charset=utf-8'); + + try { + echo json_encode($object, JSON_PRETTY_PRINT + JSON_UNESCAPED_UNICODE + JSON_NUMERIC_CHECK); + exit; + } catch (Exception $e) { + formr_log_exception($e, __CLASS__); + alert("Couldn't save file.", 'alert-danger'); + return false; + } + } + + private function translateLegacyColumn($col) { + $col = trim(mb_strtolower($col)); + $translations = array( + 'variablenname' => 'name', + 'typ' => 'type', + 'wortlaut' => 'label', + 'text' => 'label', + 'ratinguntererpol' => 'choice1', + 'ratingobererpol' => 'choice2', + ); + + if (mb_substr($col, 0, 5) == 'mcalt') { + return 'choice' . mb_substr($col, 5); + } else { + return isset($translations[$col]) ? $translations[$col] : $col; + } + } + + private function translateLegacyType($type) { + $type = trim(mb_strtolower($type)); + $translations = array( + 'offen' => 'text', + 'instruktion' => 'note', + 'instruction' => 'note', + 'fork' => 'note', + 'rating' => 'rating_button', + 'mmc' => 'mc_multiple', + 'select' => 'select_one', + 'mselect' => 'select_multiple', + 'select_or_add' => 'select_or_add_one', + 'mselect_add' => 'select_or_add_multiple', + 'btnrating' => 'rating_button', + 'range_list' => 'range_ticks', + 'btnradio' => 'mc_button', + 'btncheckbox' => 'mc_multiple_button', + 'btncheck' => 'check_button', + 'geolocation' => 'geopoint', + 'mcnt' => 'mc', + ); + + return isset($translations[$type]) ? $translations[$type] : $type; + } + + public function addSurveyItem(array $row) { + // @todo validate items in $data + if (empty($row['name'])) { + $this->warnings[] = "Skipping row with no 'item name' specified"; + return; + } + + foreach ($row as $key => $value) { + if (!in_array($key, $this->survey_columns) && !in_array($key, $this->internal_columns)) { + $this->errors[] = "Column name '{$key}' for item '{$row['name']}' is not allowed"; + return; + } + } + $this->survey[] = $row; + } + + public function readItemTableFile($filepath) { + ini_set('max_execution_time', 360); + + $this->errors = $this->messages = $this->warnings = array(); + if (!file_exists($filepath)) { + $this->errors[] = 'Item table file does not exist'; + return; + } + + try { + // Identify the type of $filepath and create \PhpOffice\PhpSpreadsheet\Spreadsheet object from a read-only reader + $filetype = \PhpOffice\PhpSpreadsheet\IOFactory::identify($filepath); + $phpSpreadsheetReader = \PhpOffice\PhpSpreadsheet\IOFactory::createReader($filetype); + $phpSpreadsheetReader->setReadDataOnly(true); + + /* @var $phpSpreadsheet \PhpOffice\PhpSpreadsheet\Spreadsheet */ + $phpSpreadsheet = $phpSpreadsheetReader->load($filepath); + + // Gather sheets to be read + if ($phpSpreadsheet->sheetNameExists('survey')) { + $surveySheet = $phpSpreadsheet->getSheetByName('survey'); + } else { + $surveySheet = $phpSpreadsheet->getSheet(0); + } + + if ($phpSpreadsheet->sheetNameExists('choices') && $phpSpreadsheet->getSheetCount() > 1) { + $choicesSheet = $phpSpreadsheet->getSheetByName('choices'); + } elseif ($phpSpreadsheet->getSheetCount() > 1) { + $choicesSheet = $phpSpreadsheet->getSheet(1); + } + + if (isset($choicesSheet)) { + $this->readChoicesSheet($choicesSheet); + } + + $this->readSurveySheet($surveySheet); + } catch (\PhpOffice\PhpSpreadsheet\Exception $e) { + $this->errors[] = "An error occured reading your excel file. Please check your file or report to admin"; + $this->errors[] = $e->getMessage(); + formr_log_exception($e, __CLASS__, $filepath); + return; + } + } + + private function readChoicesSheet(\PhpOffice\PhpSpreadsheet\Worksheet\Worksheet $worksheet) { + // Get worksheet dimensions + // non-allowed columns will be ignored, allows to specify auxiliary information if needed + $skippedColumns = $columns = array(); + $colCount = \PhpOffice\PhpSpreadsheet\Cell\Coordinate::columnIndexFromString($worksheet->getHighestDataColumn()); + $rowCount = $worksheet->getHighestDataRow(); + + for ($i = 1; $i <= $colCount; $i++) { + $colName = mb_strtolower($worksheet->getCellByColumnAndRow($i, 1)->getValue()); + if (in_array($colName, $this->choices_columns)) { + $columns[$i] = $colName; + } else { + $skippedColumns[$i] = $colName; + } + } + + if (!in_array('list_name', $columns)) { + $this->errors[] = 'You forgot to define the "list_name" column on the choices sheet.'; + } + if (!in_array('name', $columns)) { + $this->errors[] = 'You forgot to define the "name" column on the choices sheet'; + } + if (!in_array('label', $columns)) { + $this->errors[] = 'You forgot to define the "label" column on the choices sheet.'; + } + + if ($this->errors) { + return false; + } + + if ($skippedColumns) { + $this->warnings[] = sprintf('Choices worksheet "%s" skipped columns: %s', $worksheet->getTitle(), implode($skippedColumns, ", ")); + } + $this->messages[] = sprintf('Choices worksheet "%s" used columns: %s', $worksheet->getTitle(), implode($columns, ", ")); + + $data = array(); + $choiceNames = array(); + $inheritedListNames = array(); + + foreach ($worksheet->getRowIterator(1, $rowCount) as $row) { + /* @var $row \PhpOffice\PhpSpreadsheet\Worksheet\Row */ + $rowNumber = $row->getRowIndex(); + if ($rowNumber == 1) { + // skip table head + continue; + } + if ($rowNumber > $rowCount) + break; + + $data[$rowNumber] = array(); + $cellIterator = $row->getCellIterator('A', $worksheet->getHighestDataColumn()); + $cellIterator->setIterateOnlyExistingCells(false); + foreach ($cellIterator as $cell) { + /* @var $cell \PhpOffice\PhpSpreadsheet\Cell\Cell */ + if (is_null($cell)) { + continue; + } + + $colNumber = \PhpOffice\PhpSpreadsheet\Cell\Coordinate::columnIndexFromString($cell->getColumn()); + if (!isset($columns[$colNumber])) { + continue; // not a column of interest + } + $colName = $columns[$colNumber]; + $cellValue = hardTrueFalse(Normalizer::normalize($cell->getValue(), Normalizer::FORM_C)); + $cellValue = trim($cellValue); + + if ($colName == 'list_name') { + if ($cellValue && !preg_match("/^[a-zA-Z0-9_]{1,255}$/", $cellValue)) { + $this->errors[] = __("The list name '%s' is invalid. It has to be between 1 and 255 characters long. It may not contain anything other than the characters from a to Z, 0 to 9 and the underscore.", $cellValue); + } + + if (!$cellValue && !isset($lastListName)) { + $this->warnings[] = __('Skipping Row %s of choices sheet', $rowNumber); + unset($data[$rowNumber]); + continue 2; + } elseif (!$cellValue && isset($lastListName)) { + $cellValue = $lastListName; + if (!isset($inheritedListNames[$cellValue])) { + $inheritedListNames[$cellValue] = array(); + } + $inheritedListNames[$cellValue][] = $rowNumber; + } + + if (!in_array($cellValue, $this->existing_choice_lists)) { + $this->existing_choice_lists[] = $cellValue; + $data[$rowNumber]['list_name'] = $cellValue; + } elseif (in_array($cellValue, $this->existing_choice_lists) && $lastListName != $cellValue) { + $this->errors[] = __("We found a discontinuous list: the same list name ('%s') was used before row %s, but other lists came in between.", $cellValue, $rowNumber); + } else { + //$data[$rowNumber]['list_name'] = $cellValue; + } + + $lastListName = $cellValue; + } elseif ($colName == 'name') { + if (!is_formr_truthy($cellValue)) { + $this->warnings[] = __("Skipping Row %s of choices sheet: Choice name empty, but content in other columns.", $rowNumber); + if (isset($inheritedListNames[$data[$rowNumber]['list_name']])) { + // remove this row from bookmarks + $bmk = &$inheritedListNames[$data[$rowNumber]['list_name']]; + if (($key = array_search($rowNumber, $bmk)) !== false) { + unset($bmk[$key]); + } + } + unset($data[$rowNumber]); + continue 2; + } + if (!preg_match("/^.{1,255}$/", $cellValue)) { + $this->errors[] = __("The choice name '%s' is invalid. It has to be between 1 and 255 characters long.", $cellValue); + } + //$data[$rowNumber]['name'] = $cellValue; + } elseif ($colName == 'label') { + if (!$cellValue && isset($data[$rowNumber]['name'])) { + $cellValue = $data[$rowNumber]['name']; + } + //$data[$rowNumber]['label'] = $cellValue; + } + + // Stop processing if we have any errors + if ($this->errors) { + $error = sprintf("Error in cell %s%s (Choices sheet): \n %s", $cell->getColumn(), $rowNumber, implode("\n", $this->errors)); + throw new \PhpOffice\PhpSpreadsheet\Exception($error); + } + + // Save cell value + $data[$rowNumber][$colName] = $cellValue; + } // Cell loop + } // Rows loop + // Data has been gathered, group lists by list_name and check if there are duplicates for each list. + foreach ($data as $rowNumber => $row) { + if (!isset($choiceNames[$row['list_name']])) { + $choiceNames[$row['list_name']] = array(); + } + if (isset($choiceNames[$row['list_name']][$row['name']])) { + throw new \PhpOffice\PhpSpreadsheet\Exception(sprintf("'%s' has already been used as a 'name' for the list '%s'", $row['name'], $row['list_name'])); + } + $choiceNames[$row['list_name']][$row['name']] = $row['label']; + } + + // Announce rows that inherited list_names + $msgs = array(); + foreach ($inheritedListNames as $name => $rows) { + $msgs[] = $rows ? sprintf("%s: This list name was assigned to rows %s - %s automatically, because they had an empty list name and followed in this list.", $name, min($rows), max($rows)) : null; + } + if ($msgs = array_filter($msgs)) { + $this->messages[] = '
    • ' . implode('
    • ', $msgs) . '
    '; + } + + $this->choices = $data; + } + + private function readSurveySheet(\PhpOffice\PhpSpreadsheet\Worksheet\Worksheet $worksheet) { + $callStartTime = microtime(true); + // non-allowed columns will be ignored, allows to specify auxiliary information if needed + + $skippedColumns = $columns = array(); + $colCount = \PhpOffice\PhpSpreadsheet\Cell\Coordinate::columnIndexFromString($worksheet->getHighestDataColumn()); + $rowCount = $worksheet->getHighestDataRow(); + + if ($colCount > 30) { + $this->warnings[] = __('Only the first 30 columns out of %d were read.', $colCount); + $colCount = 30; + } + + $blankColCount = 0; // why should this be set to 1 by default? + for ($i = 1; $i <= $colCount; $i++) { + $colName = trim(mb_strtolower($worksheet->getCellByColumnAndRow($i, 1)->getValue())); + if (!$colName) { + $blankColCount++; + continue; + } + if (in_array($colName, $this->survey_columns)) { + $trColName = $this->translateLegacyColumn($colName); + if ($colName != $trColName) { + $this->warnings[] = __('The column "%s" is deprecated and was automatically translated to "%s"', $colName, $trColName); + } + $columns[$i] = $trColName; + } else { + $skippedColumns[$i] = $colName; + } + + if ($colName == 'choice1' && (!in_array('name', $columns) || !in_array('type', $columns))) { + $this->errors[] = "The 'name' and 'type' column have to be placed to the left of all choice columns."; + return false; + } + } + + if ($blankColCount) { + $this->warnings[] = __('Your survey sheet appears to contain %d columns without names (given in the first row).', $blankColCount); + } + if ($skippedColumns) { + $this->warnings[] = __('These survey sheet columns were skipped: %s', implode($skippedColumns, ', ')); + } + + $data = $skippedRows = $emptyRows = $variableNames = array(); + + foreach ($worksheet->getRowIterator(1, $rowCount) as $row) { + /* @var $row \PhpOffice\PhpSpreadsheet\Worksheet\Row */ + $rowNumber = $row->getRowIndex(); + if ($rowNumber == 1) { + // skip table head + continue; + } + if ($rowNumber > $rowCount) + break; + + $data[$rowNumber] = array(); + $cellIterator = $row->getCellIterator('A', $worksheet->getHighestDataColumn()); + $cellIterator->setIterateOnlyExistingCells(false); + + foreach ($cellIterator as $cell) { + /* @var $cell \PhpOffice\PhpSpreadsheet\Cell\Cell */ + if (is_null($cell)) { + continue; + } + + $colNumber = \PhpOffice\PhpSpreadsheet\Cell\Coordinate::columnIndexFromString($cell->getColumn()); + if (!isset($columns[$colNumber])) { + continue; // not a column of interest + } + $colName = $columns[$colNumber]; + if (isset($data[$rowNumber][$colName])) { + continue; // dont overwrite set columns + } + + $cellValue = trim(hardTrueFalse(Normalizer::normalize($cell->getValue(), Normalizer::FORM_C))); + + if ($colName == 'name') { + if (!$cellValue) { + if (!empty($data[$rowNumber])) { + $skippedRows[] = $rowNumber; + } else { + $emptyRows[] = $rowNumber; + } + unset($data[$rowNumber]); + continue 2; // Skip row with no item name + } elseif (!preg_match('/^[a-zA-Z][a-zA-Z0-9_]{1,64}$/', $cellValue)) { + $this->errors[] = __("The variable name '%s' is invalid. It has to be between 1 and 64 characters. It needs to start with a letter and can only contain the characters from a to Z, 0 to 9 and the underscore.", $cellValue); + } + + if (in_array($cellValue, array('session_id', 'created', 'modified', 'ended'))) { + $this->errors[] = __("Row %s: variable name '%s' is not permitted.", $rowNumber, $cellValue); + } + + if (($existingRow = array_search(mb_strtolower($cellValue), $variableNames)) === false) { + $variableNames[$rowNumber] = mb_strtolower($cellValue); + } else { + $this->errors[] = __("Row %s: Variable name '%s' already appeared in row %s", $rowNumber, $cellValue, $existingRow); + } + } elseif ($colName == 'type') { + if (mb_strpos($cellValue, ' ') !== false) { + $typeOptions = explode(' ', trim(preg_replace('/\s+/', ' ', $cellValue))); // get real type and options + $type = $typeOptions[0]; + unset($typeOptions[0]); + if (!empty($typeOptions[1]) && + !in_array($type, array('server', 'get', 'text', 'textarea', 'letters', 'file', 'image', 'rating_button', 'submit')) && + preg_match('/^[A-Za-z0-9_]{1,20}$/', trim($typeOptions[1]))) { + $data[$rowNumber]['choice_list'] = trim($typeOptions[1]); + unset($typeOptions[1]); + } + $data[$rowNumber]['type_options'] = implode(' ', $typeOptions); + $cellValue = $type; + } + + $trType = $this->translateLegacyType($cellValue); + if ($trType != $cellValue) { + $this->warnings[] = __('The type "%s" is deprecated and was automatically translated to "%s"', $cellValue, $trType); + } + $cellValue = $trType; + } elseif ($colName == 'optional') { + if ($cellValue === '*') { + $cellValue = 1; + } elseif ($cellValue === '!') { + $cellValue = 0; + } else { + $cellValue = null; + } + } elseif (strpos($colName, 'choice') === 0 && is_formr_truthy($cellValue) && isset($data[$rowNumber])) { + $choiceValue = substr($colName, 6); + $this->choices[] = array( + 'list_name' => $data[$rowNumber]['name'], + 'name' => $choiceValue, + 'label' => $cellValue, + ); + + if (!isset($data[$rowNumber]['choice_list'])) { + $data[$rowNumber]['choice_list'] = $data[$rowNumber]['name']; + } elseif (isset($data[$rowNumber]['choice_list']) && $choiceValue == 1) { + $this->errors[] = __("Row %s: You defined both a named choice_list '%s' for item '%s' and a nonempty choice1 column. Choose one.", $rowNumber, $data[$rowNumber]['choice_list'], $data[$rowNumber]['name']); + } + } + + // Stop processing if we have any errors + if ($this->errors) { + $error = sprintf("Error in cell %s%s (Survey Sheet): \n %s", $cell->getColumn(), $rowNumber, implode("\n", $this->errors)); + throw new \PhpOffice\PhpSpreadsheet\Exception($error); + } + + // Save cell value + $data[$rowNumber][$colName] = $cellValue; + } // Cell Loop + + $data[$rowNumber]['order'] = $rowNumber - 1; + // if no order is entered, use row_number + if (!isset($data[$rowNumber]['item_order']) || !is_formr_truthy($data[$rowNumber]['item_order'])) { + $data[$rowNumber]['item_order'] = $data[$rowNumber]['order']; + } + } // Rows Loop + + $callEndTime = microtime(true); + $callTime = $callEndTime - $callStartTime; + $this->messages[] = 'Survey worksheet - ' . $worksheet->getTitle() . ' (' . count($data) . ' non-empty rows, ' . $colCount . ' columns). These columns were used: ' . implode($columns, ", "); + + if (!empty($emptyRows)) { + $this->messages[] = __('Empty rows (no variable name): %s', implode($emptyRows, ", ")); + } + + if (!empty($skippedRows)) { + $this->warnings[] = __('Skipped rows (no variable name): %s. Variable name empty, but other columns had content. Double-check that you did not forget to define a variable name for a proper item.', implode($skippedRows, ", ")); + } + + $this->survey = $data; + } } diff --git a/application/Model/Survey.php b/application/Model/Survey.php index 49903a6c4..76a582de0 100644 --- a/application/Model/Survey.php +++ b/application/Model/Survey.php @@ -1,4 +1,5 @@ 0, - 'not_answered' => 0, - 'hidden_but_rendered' => 0, - 'not_rendered' => 0, - 'visible_on_current_page' => 0, - 'hidden_but_rendered_on_current_page' => 0, - 'not_answered_on_current_page' => 0 - ); - - /** - * An array of unit's exportable attributes - * @var array - */ - public $export_attribs = array('type', 'description', 'position', 'special'); - - /** - * @var DB - */ - public $dbh; - - /** - * @var ParsedownExtra - */ - public $parsedown; - - - public function __construct($fdb, $session, $unit, $run_session = null, $run = null) { - $this->dbh = $fdb; - if (isset($unit['name']) && !isset($unit['unit_id'])) { //when called via URL - $this->load($unit['name']); - // parent::__construct needs this - $unit['unit_id'] = $this->id; - } elseif (isset($unit['unit_id'])) { - $this->id = (int) $unit['unit_id']; - } - - parent::__construct($fdb, $session, $unit, $run_session, $run); - - // $this->valid means survey has been loaded from DB so no need to re-load it - if ($this->id && !$this->valid): - $this->load(); - endif; - } - - /** - * Get survey by id - * - * @param int $id - * @return Survey - */ - public static function loadById($id) { - $unit = array('unit_id' => (int) $id); - return new Survey(DB::getInstance(), null, $unit); - } - - /** - * Get survey by name - * - * @param string $name - * @return Survey - */ - public static function loadByName($name) { - $db = Site::getDb(); - $id = $db->findValue('survey_studies', array('name' => $name), 'id'); - return self::loadById($id); - } - - public static function loadByUserAndName(User $user, $name) { - $db = Site::getDb(); - $id = $db->findValue('survey_studies', array('user_id' => $user->id, 'name' => $name), 'id'); - return self::loadById($id); - } - - private function load($survey_name = null) { - global $user; - if ($survey_name !== null) { - $vars = $this->dbh->findRow('survey_studies', array('name' => $survey_name, 'user_id' => $user->id)); - } else { - $vars = $this->dbh->findRow('survey_studies', array('id' => $this->id)); - } - if ($vars): - $this->id = $vars['id']; - $this->name = $vars['name']; - $this->user_id = (int) $vars['user_id']; - if (!isset($vars['results_table']) OR $vars['results_table'] == null) { - $this->results_table = $this->name; - } else { - $this->results_table = $vars['results_table']; - } - - $this->settings['maximum_number_displayed'] = (int) array_val($vars, 'maximum_number_displayed', null); - $this->settings['displayed_percentage_maximum'] = (int) array_val($vars, 'displayed_percentage_maximum'); - $this->settings['add_percentage_points'] = (int) array_val($vars, 'add_percentage_points'); - $this->settings['enable_instant_validation'] = (int) array_val($vars, 'enable_instant_validation'); - $this->settings['expire_after'] = (int) array_val($vars, 'expire_after'); - $this->settings['google_file_id'] = array_val($vars, 'google_file_id'); - $this->settings['unlinked'] = array_val($vars, 'unlinked'); - $this->settings['expire_invitation_after'] = (int) array_val($vars, 'expire_invitation_after'); - $this->settings['expire_invitation_grace'] = (int) array_val($vars, 'expire_invitation_grace'); - $this->settings['hide_results'] = (int) array_val($vars, 'hide_results'); - $this->settings['use_paging'] = (int) array_val($vars, 'use_paging'); - - $this->valid = true; - endif; - } - - public function create($options) { - $old_name = $this->name; - // If survey_data is present (an object with "name", "items", "settings" entries) - // then create/update the survey and set $options[unit_id] as Survey's ID - if (!empty($options['survey_data'])) { - if ($created = $this->createFromData($options['survey_data'])) { - $options = array_merge($options, $created); - $this->id = $created['id']; - $this->name = $created['name']; - $this->results_table = $created['results_table']; - } - } - - // this unit type is a bit special - // all other unit types are created only within runs - // but surveys are semi-independent of runs - // so it is possible to add a survey, without specifying which one at first - // and to then choose one. - // thus, we "mock" a survey at first - if (count($options) === 1 || isset($options['mock'])) { - $this->valid = true; - } else { // and link it to the run only later - if (!empty($options['unit_id'])) { - $this->id = (int) $options['unit_id']; - if ($this->linkToRun()) { - $this->majorChange(); - $this->load(); - if(empty($options['description']) || $options['description'] === $old_name) { - $options['description'] = $this->name; - } - } - } - $this->modify($options); - $this->valid = true; - } - } - - /** - * Create survey from data the $data object has the following fields - * $data = { - * name: a string representing the name of the survey - * items: an array of objects representing items in the survey (fields as in excel sheet) - * settings: an object with settings of the survey - * } - * - * @param object $data - * @param boolean $return_spr flag as to whether just an SPR object should be returned - * @return array|bool|SpreadsheetReader Returns an array with info on created survey, SpreadsheetReader if indicated by second parameter or FALSE on failure - * - * @todo process items - */ - protected function createFromData($data, $return_spr = false) { - if (empty($data->name) || empty($data->items)) { - return false; - } - - if (empty($data->settings)) { - $data->settings = array(); - } - - $created = array(); - // check if survey exists by name even if it belongs to current user. If that is the case then use existing ID. - $survey = Survey::loadByName($data->name); - if ($survey->valid && Site::getCurrentUser()->created($survey)) { - $created['id'] = $survey->id; - $created['unit_id'] = $survey->id; - } else { - $unit = array( - 'user_id' => Site::getCurrentUser()->id, - 'name' => $data->name, - ); - $survey = new Survey(DB::getInstance(), null, $unit); - if ($survey->createIndependently((array)$data->settings)) { - $created['unit_id'] = $survey->id; - $created['id'] = $survey->id; - } - } - - $created['results_table'] = $survey->results_table; - $created['name'] = $survey->name; - - // Mock SpreadSheetReader to use existing mechanism of creating survey items - $SPR = new SpreadsheetReader(); - $i = 1; - foreach ($data->items as $item) { - if (!empty($item->choices) && !empty($item->choice_list)) { - foreach ($item->choices as $name => $label) { - $SPR->choices[] = array( - 'list_name' => $item->choice_list, - 'name' => $name, - 'label' => $label, - ); - } - unset($item->choices); - } - $SPR->addSurveyItem((array)$item); - } - - if ($return_spr === true) { - return $SPR; - } - - if (!$survey->createSurvey($SPR)) { - alert("Unable to import survey items in survey '{$survey->name}'. You may need to independently create this survey and attach it to run", 'alert-warning'); - $errors = array_merge($survey->errors, $SPR->errors, $SPR->warnings); - if ($errors) { - alert(nl2br(implode("\n", $errors)), 'alert-warning'); - } - return false; - } - - return $created; - } - - public function render($form_action = null, $form_append = null) { - $ret = ' + public $id = null; + public $name = null; + public $run_name = null; + public $items = array(); + public $items_validated = array(); + public $session = null; + public $results_table = null; + public $run_session_id = null; + public $settings = array(); + public $valid = false; + public $public = false; + public $errors = array(); + public $validation_errors = array(); + public $messages = array(); + public $warnings = array(); + public $position; + public $rendered_items = array(); + private $SPR; + public $openCPU = null; + public $icon = "fa-pencil-square-o"; + public $type = "Survey"; + private $confirmed_deletion = false; + private $created_new = false; + public $item_factory = null; + public $unanswered = array(); + public $to_render = array(); + public $study_name_pattern = "/[a-zA-Z][a-zA-Z0-9_]{2,64}/"; + private $result_count = null; + + /** + * Counts for progress computation + * @var int {collection} + */ + public $progress = 0; + public $progress_counts = array( + 'already_answered' => 0, + 'not_answered' => 0, + 'hidden_but_rendered' => 0, + 'not_rendered' => 0, + 'visible_on_current_page' => 0, + 'hidden_but_rendered_on_current_page' => 0, + 'not_answered_on_current_page' => 0 + ); + + /** + * An array of unit's exportable attributes + * @var array + */ + public $export_attribs = array('type', 'description', 'position', 'special'); + + /** + * @var DB + */ + public $dbh; + + /** + * @var ParsedownExtra + */ + public $parsedown; + + public function __construct($fdb, $session, $unit, $run_session = null, $run = null) { + $this->dbh = $fdb; + if (isset($unit['name']) && !isset($unit['unit_id'])) { //when called via URL + $this->load($unit['name']); + // parent::__construct needs this + $unit['unit_id'] = $this->id; + } elseif (isset($unit['unit_id'])) { + $this->id = (int) $unit['unit_id']; + } + + parent::__construct($fdb, $session, $unit, $run_session, $run); + + // $this->valid means survey has been loaded from DB so no need to re-load it + if ($this->id && !$this->valid): + $this->load(); + endif; + } + + /** + * Get survey by id + * + * @param int $id + * @return Survey + */ + public static function loadById($id) { + $unit = array('unit_id' => (int) $id); + return new Survey(DB::getInstance(), null, $unit); + } + + /** + * Get survey by name + * + * @param string $name + * @return Survey + */ + public static function loadByName($name) { + $db = Site::getDb(); + $id = $db->findValue('survey_studies', array('name' => $name), 'id'); + return self::loadById($id); + } + + public static function loadByUserAndName(User $user, $name) { + $db = Site::getDb(); + $id = $db->findValue('survey_studies', array('user_id' => $user->id, 'name' => $name), 'id'); + return self::loadById($id); + } + + private function load($survey_name = null) { + global $user; + if ($survey_name !== null) { + $vars = $this->dbh->findRow('survey_studies', array('name' => $survey_name, 'user_id' => $user->id)); + } else { + $vars = $this->dbh->findRow('survey_studies', array('id' => $this->id)); + } + if ($vars): + $this->id = $vars['id']; + $this->name = $vars['name']; + $this->user_id = (int) $vars['user_id']; + if (!isset($vars['results_table']) OR $vars['results_table'] == null) { + $this->results_table = $this->name; + } else { + $this->results_table = $vars['results_table']; + } + + $this->settings['maximum_number_displayed'] = (int) array_val($vars, 'maximum_number_displayed', null); + $this->settings['displayed_percentage_maximum'] = (int) array_val($vars, 'displayed_percentage_maximum'); + $this->settings['add_percentage_points'] = (int) array_val($vars, 'add_percentage_points'); + $this->settings['enable_instant_validation'] = (int) array_val($vars, 'enable_instant_validation'); + $this->settings['expire_after'] = (int) array_val($vars, 'expire_after'); + $this->settings['google_file_id'] = array_val($vars, 'google_file_id'); + $this->settings['unlinked'] = array_val($vars, 'unlinked'); + $this->settings['expire_invitation_after'] = (int) array_val($vars, 'expire_invitation_after'); + $this->settings['expire_invitation_grace'] = (int) array_val($vars, 'expire_invitation_grace'); + $this->settings['hide_results'] = (int) array_val($vars, 'hide_results'); + $this->settings['use_paging'] = (int) array_val($vars, 'use_paging'); + + $this->valid = true; + endif; + } + + public function create($options) { + $old_name = $this->name; + // If survey_data is present (an object with "name", "items", "settings" entries) + // then create/update the survey and set $options[unit_id] as Survey's ID + if (!empty($options['survey_data'])) { + if ($created = $this->createFromData($options['survey_data'])) { + $options = array_merge($options, $created); + $this->id = $created['id']; + $this->name = $created['name']; + $this->results_table = $created['results_table']; + } + } + + // this unit type is a bit special + // all other unit types are created only within runs + // but surveys are semi-independent of runs + // so it is possible to add a survey, without specifying which one at first + // and to then choose one. + // thus, we "mock" a survey at first + if (count($options) === 1 || isset($options['mock'])) { + $this->valid = true; + } else { // and link it to the run only later + if (!empty($options['unit_id'])) { + $this->id = (int) $options['unit_id']; + if ($this->linkToRun()) { + $this->majorChange(); + $this->load(); + if (empty($options['description']) || $options['description'] === $old_name) { + $options['description'] = $this->name; + } + } + } + $this->modify($options); + $this->valid = true; + } + } + + /** + * Create survey from data the $data object has the following fields + * $data = { + * name: a string representing the name of the survey + * items: an array of objects representing items in the survey (fields as in excel sheet) + * settings: an object with settings of the survey + * } + * + * @param object $data + * @param boolean $return_spr flag as to whether just an SPR object should be returned + * @return array|bool|SpreadsheetReader Returns an array with info on created survey, SpreadsheetReader if indicated by second parameter or FALSE on failure + * + * @todo process items + */ + protected function createFromData($data, $return_spr = false) { + if (empty($data->name) || empty($data->items)) { + return false; + } + + if (empty($data->settings)) { + $data->settings = array(); + } + + $created = array(); + // check if survey exists by name even if it belongs to current user. If that is the case then use existing ID. + $survey = Survey::loadByName($data->name); + if ($survey->valid && Site::getCurrentUser()->created($survey)) { + $created['id'] = $survey->id; + $created['unit_id'] = $survey->id; + } else { + $unit = array( + 'user_id' => Site::getCurrentUser()->id, + 'name' => $data->name, + ); + $survey = new Survey(DB::getInstance(), null, $unit); + if ($survey->createIndependently((array) $data->settings)) { + $created['unit_id'] = $survey->id; + $created['id'] = $survey->id; + } + } + + $created['results_table'] = $survey->results_table; + $created['name'] = $survey->name; + + // Mock SpreadSheetReader to use existing mechanism of creating survey items + $SPR = new SpreadsheetReader(); + $i = 1; + foreach ($data->items as $item) { + if (!empty($item->choices) && !empty($item->choice_list)) { + foreach ($item->choices as $name => $label) { + $SPR->choices[] = array( + 'list_name' => $item->choice_list, + 'name' => $name, + 'label' => $label, + ); + } + unset($item->choices); + } + $SPR->addSurveyItem((array) $item); + } + + if ($return_spr === true) { + return $SPR; + } + + if (!$survey->createSurvey($SPR)) { + alert("Unable to import survey items in survey '{$survey->name}'. You may need to independently create this survey and attach it to run", 'alert-warning'); + $errors = array_merge($survey->errors, $SPR->errors, $SPR->warnings); + if ($errors) { + alert(nl2br(implode("\n", $errors)), 'alert-warning'); + } + return false; + } + + return $created; + } + + public function render($form_action = null, $form_append = null) { + $ret = '
    '; - $ret .= $this->render_form_header($form_action) . - $this->render_items() . - $form_append . - $this->render_form_footer(); - $ret .= ' + $ret .= $this->render_form_header($form_action) . + $this->render_items() . + $form_append . + $this->render_form_footer(); + $ret .= '
    '; - //$this->dbh = null; - return $ret; - } - - protected function startEntry() { - if (!$this->dbh->table_exists($this->results_table)) { - alert('A results table for this survey could not be found', 'alert-danger'); - throw new Exception("Results table '{$this->results_table}' not found!"); - } - - $this->dbh->insert_update($this->results_table, array( - 'session_id' => $this->session_id, - 'study_id' => $this->id, + //$this->dbh = null; + return $ret; + } + + protected function startEntry() { + if (!$this->dbh->table_exists($this->results_table)) { + alert('A results table for this survey could not be found', 'alert-danger'); + throw new Exception("Results table '{$this->results_table}' not found!"); + } + + $this->dbh->insert_update($this->results_table, array( + 'session_id' => $this->session_id, + 'study_id' => $this->id, 'created' => mysql_now()), array( - 'modified' => mysql_now(), - )); - - // Check if session already has enough entries in the items_display table for this survey - if($this->allItemsHaveAnOrder()) { - return; - } else { - // get the definition of the order - list($item_ids, $item_types) = $this->getOrderedItemsIds(); - - // define paramers to bind parameters - $display_order = null; - $item_id = null; - $page = 1; - - $survey_items_display = $this->dbh->prepare( - "INSERT INTO `survey_items_display` (`item_id`, `session_id`, `display_order`, `page`) VALUES (:item_id, :session_id, :display_order, :page) + 'modified' => mysql_now(), + )); + + // Check if session already has enough entries in the items_display table for this survey + if ($this->allItemsHaveAnOrder()) { + return; + } else { + // get the definition of the order + list($item_ids, $item_types) = $this->getOrderedItemsIds(); + + // define paramers to bind parameters + $display_order = null; + $item_id = null; + $page = 1; + + $survey_items_display = $this->dbh->prepare( + "INSERT INTO `survey_items_display` (`item_id`, `session_id`, `display_order`, `page`) VALUES (:item_id, :session_id, :display_order, :page) ON DUPLICATE KEY UPDATE `display_order` = VALUES(`display_order`), `page` = VALUES(`page`)" - ); - $survey_items_display->bindParam(":session_id", $this->session_id); - $survey_items_display->bindParam(":item_id", $item_id); - $survey_items_display->bindParam(":display_order", $display_order); - $survey_items_display->bindParam(":page", $page); - - foreach ($item_ids as $display_order => $item_id) { - $survey_items_display->execute(); - // set page number when submit button is hit or we reached max_items_per_page for survey - if ($item_types[$item_id] === 'submit') { - $page++; - } - } - } - } - - protected function allItemsHaveAnOrder() { - /* - we have cascading deletes for items->item_display so we only need to worry whether the item_display is short of items - 12 items - 12 ordered items - - scenario A - 1 deleted - 11 items - 11 ordered items - -> don't reorder - - scenario B - 1 added - 13 items - 12 ordered items - - -> reorder - scenario C - 1 added, 1 deleted - 12 items - 11 ordered items - -> reorder - - */ - $nr_items = $this->dbh->count('survey_items', array('study_id' => $this->id), 'id'); - $nr_display_items = $this->dbh->count('survey_items_display', array('session_id' => $this->session_id), 'id'); - - return $nr_display_items === $nr_items; - } - - protected function getOrderedItemsIds() { - $get_items = $this->dbh->select(' + ); + $survey_items_display->bindParam(":session_id", $this->session_id); + $survey_items_display->bindParam(":item_id", $item_id); + $survey_items_display->bindParam(":display_order", $display_order); + $survey_items_display->bindParam(":page", $page); + + foreach ($item_ids as $display_order => $item_id) { + $survey_items_display->execute(); + // set page number when submit button is hit or we reached max_items_per_page for survey + if ($item_types[$item_id] === 'submit') { + $page++; + } + } + } + } + + protected function allItemsHaveAnOrder() { + /* + we have cascading deletes for items->item_display so we only need to worry whether the item_display is short of items + 12 items + 12 ordered items + + scenario A + 1 deleted + 11 items + 11 ordered items + -> don't reorder + + scenario B + 1 added + 13 items + 12 ordered items + + -> reorder + scenario C + 1 added, 1 deleted + 12 items + 11 ordered items + -> reorder + + */ + $nr_items = $this->dbh->count('survey_items', array('study_id' => $this->id), 'id'); + $nr_display_items = $this->dbh->count('survey_items_display', array('session_id' => $this->session_id), 'id'); + + return $nr_display_items === $nr_items; + } + + protected function getOrderedItemsIds() { + $get_items = $this->dbh->select(' `survey_items`.id, `survey_items`.`type`, `survey_items`.`item_order`, `survey_items`.`block_order`') - ->from('survey_items') - ->where("`survey_items`.`study_id` = :study_id") - ->order("`survey_items`.order") - ->bindParams(array('`study_id`' => $this->id)) - ->statement(); - - // sort blocks randomly (if they are consecutive), then by item number and if the latter are identical, randomly - $block_segment = $block_order = $item_order = $random_order = $block_numbers = $item_ids = array(); - $types = array(); - - $last_block = ""; - $block_nr = 0; - $block_segment_i = 0; - - while ($item = $get_items->fetch(PDO::FETCH_ASSOC)) { - if ($item['block_order'] == "") { // not blocked - $item['block_order'] = ""; // ? why is this necessary - $block_order[] = $block_nr; - } else { - if (!array_key_exists($item['block_order'], $block_numbers)) { // new block - if ($last_block === "") { // new segment of blocks - $block_segment_i = 0; - $block_segment = range($block_nr, $block_nr + 10000); // by choosing this range, the next non-block segment is forced to follow - shuffle($block_segment); - $block_nr = $block_nr + 10001; - } - - $rand_block_number = $block_segment[$block_segment_i]; - $block_numbers[$item['block_order']] = $rand_block_number; - $block_segment_i++; - } - $block_order[] = $block_numbers[$item['block_order']]; // get stored block order - } // sort the blocks with each other - // but keep the order within blocks if desired - $item_order[] = $item['item_order']; // after sorting by block, sort by item order - $item_ids[] = $item['id']; - $last_block = $item['block_order']; - - $types[$item['id']] = $item['type']; - } - - $random_order = range(1, count($item_ids)); // if item order is identical, sort randomly (within block) - shuffle($random_order); - array_multisort($block_order, $item_order, $random_order, $item_ids); - // order is already sufficiently defined at least by random_order, but this is a simple way to sort $item_ids is sorted accordingly - - return array($item_ids, $types); - } - - /** - * Save posted survey data to database - * - * @param array $posted - * @param bool $validate Should items be validated before posted? - * @return boolean Returns TRUE if all data was successfully validated and saved or FALSE otherwise - * @throws Exception - */ - public function post($posted, $validate = true) { - // remove variables user is not allowed to overrite (they should not be sent to user in the first place if not used in request) - unset($posted['id'], $posted['session'], $posted['session_id'], $posted['study_id'], $posted['created'], $posted['modified'], $posted['ended']); - - if (!$posted) { - return false; - } - - if (isset($posted["_item_views"]["shown"])): - $posted["_item_views"]["shown"] = array_filter($posted["_item_views"]["shown"]); - $posted["_item_views"]["shown_relative"] = array_filter($posted["_item_views"]["shown_relative"]); - $posted["_item_views"]["answered"] = array_filter($posted["_item_views"]["answered"]); - $posted["_item_views"]["answered_relative"] = array_filter($posted["_item_views"]["answered_relative"]); - endif; - - /** - * The concept of 'save all possible data' is not so correct - * ALL data on current page must valid before any database operation or saves are made - * This should help avoid inconsistencies or having notes and headings spread across pages - */ - - // Get items from database that are related to what is being posted - $items = $this->getItemsWithChoices(null, array( - 'field' => 'name', - 'values' => array_keys($posted), - )); - - // Validate items and if any fails return user to same page with all unansered items and error messages - // This loop also accumulates potential update data - $update_data = array(); - foreach ($posted as $item_name => $item_value) { - if (!isset($items[$item_name])) { - continue; - } - - /** @var $item Item */ - if ($item_value instanceof Item) { - $item = $item_value; - $item_value = $item->value_validated; - } else { - $item = $items[$item_name]; - } - - $validInput = ($validate && !$item->skip_validation) ? $item->validateInput($item_value) : $item_value; - if ($item->save_in_results_table) { - if ($item->error) { - $this->validation_errors[$item_name] = $item->error; - } else { - $update_data[$item_name] = $item->getReply($validInput); - } - $item->value_validated = $item_value; - $items[$item_name] = $item; - } - } - - if (!empty($this->validation_errors)) { - $this->items_validated = $items; - return false; - } - - $survey_items_display = $this->dbh->prepare( - "UPDATE `survey_items_display` SET + ->from('survey_items') + ->where("`survey_items`.`study_id` = :study_id") + ->order("`survey_items`.order") + ->bindParams(array('`study_id`' => $this->id)) + ->statement(); + + // sort blocks randomly (if they are consecutive), then by item number and if the latter are identical, randomly + $block_segment = $block_order = $item_order = $random_order = $block_numbers = $item_ids = array(); + $types = array(); + + $last_block = ""; + $block_nr = 0; + $block_segment_i = 0; + + while ($item = $get_items->fetch(PDO::FETCH_ASSOC)) { + if ($item['block_order'] == "") { // not blocked + $item['block_order'] = ""; // ? why is this necessary + $block_order[] = $block_nr; + } else { + if (!array_key_exists($item['block_order'], $block_numbers)) { // new block + if ($last_block === "") { // new segment of blocks + $block_segment_i = 0; + $block_segment = range($block_nr, $block_nr + 10000); // by choosing this range, the next non-block segment is forced to follow + shuffle($block_segment); + $block_nr = $block_nr + 10001; + } + + $rand_block_number = $block_segment[$block_segment_i]; + $block_numbers[$item['block_order']] = $rand_block_number; + $block_segment_i++; + } + $block_order[] = $block_numbers[$item['block_order']]; // get stored block order + } // sort the blocks with each other + // but keep the order within blocks if desired + $item_order[] = $item['item_order']; // after sorting by block, sort by item order + $item_ids[] = $item['id']; + $last_block = $item['block_order']; + + $types[$item['id']] = $item['type']; + } + + $random_order = range(1, count($item_ids)); // if item order is identical, sort randomly (within block) + shuffle($random_order); + array_multisort($block_order, $item_order, $random_order, $item_ids); + // order is already sufficiently defined at least by random_order, but this is a simple way to sort $item_ids is sorted accordingly + + return array($item_ids, $types); + } + + /** + * Save posted survey data to database + * + * @param array $posted + * @param bool $validate Should items be validated before posted? + * @return boolean Returns TRUE if all data was successfully validated and saved or FALSE otherwise + * @throws Exception + */ + public function post($posted, $validate = true) { + // remove variables user is not allowed to overrite (they should not be sent to user in the first place if not used in request) + unset($posted['id'], $posted['session'], $posted['session_id'], $posted['study_id'], $posted['created'], $posted['modified'], $posted['ended']); + + if (!$posted) { + return false; + } + + if (isset($posted["_item_views"]["shown"])): + $posted["_item_views"]["shown"] = array_filter($posted["_item_views"]["shown"]); + $posted["_item_views"]["shown_relative"] = array_filter($posted["_item_views"]["shown_relative"]); + $posted["_item_views"]["answered"] = array_filter($posted["_item_views"]["answered"]); + $posted["_item_views"]["answered_relative"] = array_filter($posted["_item_views"]["answered_relative"]); + endif; + + /** + * The concept of 'save all possible data' is not so correct + * ALL data on current page must valid before any database operation or saves are made + * This should help avoid inconsistencies or having notes and headings spread across pages + */ + // Get items from database that are related to what is being posted + $items = $this->getItemsWithChoices(null, array( + 'field' => 'name', + 'values' => array_keys($posted), + )); + + // Validate items and if any fails return user to same page with all unansered items and error messages + // This loop also accumulates potential update data + $update_data = array(); + foreach ($posted as $item_name => $item_value) { + if (!isset($items[$item_name])) { + continue; + } + + /** @var $item Item */ + if ($item_value instanceof Item) { + $item = $item_value; + $item_value = $item->value_validated; + } else { + $item = $items[$item_name]; + } + + $validInput = ($validate && !$item->skip_validation) ? $item->validateInput($item_value) : $item_value; + if ($item->save_in_results_table) { + if ($item->error) { + $this->validation_errors[$item_name] = $item->error; + } else { + $update_data[$item_name] = $item->getReply($validInput); + } + $item->value_validated = $item_value; + $items[$item_name] = $item; + } + } + + if (!empty($this->validation_errors)) { + $this->items_validated = $items; + return false; + } + + $survey_items_display = $this->dbh->prepare( + "UPDATE `survey_items_display` SET created = COALESCE(created,NOW()), answer = :answer, saved = :saved, @@ -490,325 +489,323 @@ public function post($posted, $validate = true) { displaycount = COALESCE(displaycount,1), hidden = :hidden WHERE item_id = :item_id AND session_id = :session_id"); # fixme: displaycount starts at 2 - $survey_items_display->bindParam(":session_id", $this->session_id); - - try { - $this->dbh->beginTransaction(); - - // update item_display table for each posted item using prepared statement - foreach ($posted AS $name => $value) { - if (!isset($items[$name])) { - continue; - } - - /* @var $item Item */ - if ($value instanceof Item) { - $item = $value; - $value = $item->value_validated; - } else { - $item = $items[$name]; - } - - if (isset($posted["_item_views"]["shown"][$item->id], $posted["_item_views"]["shown_relative"][$item->id])) { - $shown = $posted["_item_views"]["shown"][$item->id]; - $shown_relative = $posted["_item_views"]["shown_relative"][$item->id]; - } else { - $shown = mysql_now(); - $shown_relative = null; // and where this is null, performance.now wasn't available - } - - if (isset($posted["_item_views"]["answered"][$item->id], // separately to "shown" because of items like "note" - $posted["_item_views"]["answered_relative"][$item->id])) { - $answered = $posted["_item_views"]["answered"][$item->id]; - $answered_relative = $posted["_item_views"]["answered_relative"][$item->id]; - } else { - $answered = $shown; // this way we can identify items where JS time failed because answered and show time are exactly identical - $answered_relative = null; - } - - $survey_items_display->bindValue(":item_id", $item->id); - $survey_items_display->bindValue(":answer", $item->getReply($value)); - $survey_items_display->bindValue(":hidden", $item->skip_validation ? (int)$item->hidden : 0); // an item that was answered has to have been shown - $survey_items_display->bindValue(":saved", mysql_now()); - $survey_items_display->bindParam(":shown", $shown); - $survey_items_display->bindParam(":shown_relative", $shown_relative); - $survey_items_display->bindParam(":answered", $answered); - $survey_items_display->bindParam(":answered_relative", $answered_relative); - $item_answered = $survey_items_display->execute(); - - if (!$item_answered) { - throw new Exception("Survey item '$name' could not be saved with value '$value' in table '{$this->results_table}' (FieldType: {$this->unanswered[$name]->getResultField()})"); - } - unset($this->unanswered[$name]); //?? FIX ME - } //endforeach - - // Update results table in one query - if ($update_data) { - $update_where = array( - 'study_id' => $this->id, - 'session_id' => $this->session_id, - ); - $this->dbh->update($this->results_table, $update_data, $update_where); - } - $this->dbh->commit(); - } catch (Exception $e) { - $this->dbh->rollBack(); - notify_user_error($e, 'An error occurred while trying to save your survey data. Please notify the author of this survey with this date and time'); - formr_log_exception($e, __CLASS__); - //$redirect = false; - return false; - } - - // If all was well and we are re-directing then do so - /* - if ($redirect) { - redirect_to($this->run_name); - } - */ - return true; - } - - protected function getProgress() { - $answered = $this->dbh->select(array('COUNT(`survey_items_display`.saved)' => 'count', 'study_id', 'session_id')) - ->from('survey_items') - ->leftJoin('survey_items_display', 'survey_items_display.session_id = :session_id', 'survey_items.id = survey_items_display.item_id') - ->where('survey_items_display.session_id IS NOT NULL') - ->where('survey_items.study_id = :study_id') - ->where("survey_items.type NOT IN ('submit')") - ->where("`survey_items_display`.saved IS NOT NULL") - ->bindParams(array('session_id' => $this->session_id, 'study_id' => $this->id)) - ->fetch(); - - $this->progress_counts['already_answered'] = $answered['count']; - - /** @var Item $item */ - foreach ($this->unanswered as $item) { - // count only rendered items, not skipped ones - if ($item->isRendered($this)) { - $this->progress_counts['not_answered'] ++; - } - // count those items that were hidden but rendered (ie. those relying on missing data for their showif) - if ($item->isHiddenButRendered($this)) { - $this->progress_counts['hidden_but_rendered'] ++; - } - } - /** @var Item $item */ - foreach ($this->to_render as $item) { - // On current page, count only rendered items, not skipped ones - if ($item->isRendered()) { - $this->progress_counts['visible_on_current_page'] ++; - } - // On current page, count those items that were hidden but rendered (ie. those relying on missing data for their showif) - if ($item->isHiddenButRendered()) { - $this->progress_counts['hidden_but_rendered_on_current_page'] ++; - } - } - - $this->progress_counts['not_answered_on_current_page'] = $this->progress_counts['not_answered'] - $this->progress_counts['visible_on_current_page']; - - $all_items = $this->progress_counts['already_answered'] + $this->progress_counts['not_answered']; - - if ($all_items !== 0) { - $this->progress = $this->progress_counts['already_answered'] / $all_items; - } else { - $this->errors[] = _('Something went wrong, there are no items in this survey!'); - $this->progress = 0; - } - - // if there only hidden items, that have no way of becoming visible (no other items) - if ($this->progress_counts['not_answered'] === $this->progress_counts['hidden_but_rendered']) { - $this->progress = 1; - } - return $this->progress; - } - - /** - * Process show-ifs and dynamic values for a given set of items in survey - * @note: All dynamic values are processed (even for those we don't know if they will be shown) - * - * @param Item[] $items - * @return array - */ - protected function processDynamicValuesAndShowIfs(&$items) { - // In this loop we gather all show-ifs and dynamic-values that need processing and all values. - $code = array(); - - /* @var $item Item */ - foreach ($items as $name => &$item) { - // 1. Check item's show-if - $showif = $item->getShowIf(); - if($showif) { - $siname = "si.{$name}"; - $showif = str_replace("\n", "\n\t", $showif); - $code[$siname] = "{$siname} = (function(){ + $survey_items_display->bindParam(":session_id", $this->session_id); + + try { + $this->dbh->beginTransaction(); + + // update item_display table for each posted item using prepared statement + foreach ($posted AS $name => $value) { + if (!isset($items[$name])) { + continue; + } + + /* @var $item Item */ + if ($value instanceof Item) { + $item = $value; + $value = $item->value_validated; + } else { + $item = $items[$name]; + } + + if (isset($posted["_item_views"]["shown"][$item->id], $posted["_item_views"]["shown_relative"][$item->id])) { + $shown = $posted["_item_views"]["shown"][$item->id]; + $shown_relative = $posted["_item_views"]["shown_relative"][$item->id]; + } else { + $shown = mysql_now(); + $shown_relative = null; // and where this is null, performance.now wasn't available + } + + if (isset($posted["_item_views"]["answered"][$item->id], // separately to "shown" because of items like "note" + $posted["_item_views"]["answered_relative"][$item->id])) { + $answered = $posted["_item_views"]["answered"][$item->id]; + $answered_relative = $posted["_item_views"]["answered_relative"][$item->id]; + } else { + $answered = $shown; // this way we can identify items where JS time failed because answered and show time are exactly identical + $answered_relative = null; + } + + $survey_items_display->bindValue(":item_id", $item->id); + $survey_items_display->bindValue(":answer", $item->getReply($value)); + $survey_items_display->bindValue(":hidden", $item->skip_validation ? (int) $item->hidden : 0); // an item that was answered has to have been shown + $survey_items_display->bindValue(":saved", mysql_now()); + $survey_items_display->bindParam(":shown", $shown); + $survey_items_display->bindParam(":shown_relative", $shown_relative); + $survey_items_display->bindParam(":answered", $answered); + $survey_items_display->bindParam(":answered_relative", $answered_relative); + $item_answered = $survey_items_display->execute(); + + if (!$item_answered) { + throw new Exception("Survey item '$name' could not be saved with value '$value' in table '{$this->results_table}' (FieldType: {$this->unanswered[$name]->getResultField()})"); + } + unset($this->unanswered[$name]); //?? FIX ME + } //endforeach + // Update results table in one query + if ($update_data) { + $update_where = array( + 'study_id' => $this->id, + 'session_id' => $this->session_id, + ); + $this->dbh->update($this->results_table, $update_data, $update_where); + } + $this->dbh->commit(); + } catch (Exception $e) { + $this->dbh->rollBack(); + notify_user_error($e, 'An error occurred while trying to save your survey data. Please notify the author of this survey with this date and time'); + formr_log_exception($e, __CLASS__); + //$redirect = false; + return false; + } + + // If all was well and we are re-directing then do so + /* + if ($redirect) { + redirect_to($this->run_name); + } + */ + return true; + } + + protected function getProgress() { + $answered = $this->dbh->select(array('COUNT(`survey_items_display`.saved)' => 'count', 'study_id', 'session_id')) + ->from('survey_items') + ->leftJoin('survey_items_display', 'survey_items_display.session_id = :session_id', 'survey_items.id = survey_items_display.item_id') + ->where('survey_items_display.session_id IS NOT NULL') + ->where('survey_items.study_id = :study_id') + ->where("survey_items.type NOT IN ('submit')") + ->where("`survey_items_display`.saved IS NOT NULL") + ->bindParams(array('session_id' => $this->session_id, 'study_id' => $this->id)) + ->fetch(); + + $this->progress_counts['already_answered'] = $answered['count']; + + /** @var Item $item */ + foreach ($this->unanswered as $item) { + // count only rendered items, not skipped ones + if ($item->isRendered($this)) { + $this->progress_counts['not_answered'] ++; + } + // count those items that were hidden but rendered (ie. those relying on missing data for their showif) + if ($item->isHiddenButRendered($this)) { + $this->progress_counts['hidden_but_rendered'] ++; + } + } + /** @var Item $item */ + foreach ($this->to_render as $item) { + // On current page, count only rendered items, not skipped ones + if ($item->isRendered()) { + $this->progress_counts['visible_on_current_page'] ++; + } + // On current page, count those items that were hidden but rendered (ie. those relying on missing data for their showif) + if ($item->isHiddenButRendered()) { + $this->progress_counts['hidden_but_rendered_on_current_page'] ++; + } + } + + $this->progress_counts['not_answered_on_current_page'] = $this->progress_counts['not_answered'] - $this->progress_counts['visible_on_current_page']; + + $all_items = $this->progress_counts['already_answered'] + $this->progress_counts['not_answered']; + + if ($all_items !== 0) { + $this->progress = $this->progress_counts['already_answered'] / $all_items; + } else { + $this->errors[] = _('Something went wrong, there are no items in this survey!'); + $this->progress = 0; + } + + // if there only hidden items, that have no way of becoming visible (no other items) + if ($this->progress_counts['not_answered'] === $this->progress_counts['hidden_but_rendered']) { + $this->progress = 1; + } + return $this->progress; + } + + /** + * Process show-ifs and dynamic values for a given set of items in survey + * @note: All dynamic values are processed (even for those we don't know if they will be shown) + * + * @param Item[] $items + * @return array + */ + protected function processDynamicValuesAndShowIfs(&$items) { + // In this loop we gather all show-ifs and dynamic-values that need processing and all values. + $code = array(); + + /* @var $item Item */ + foreach ($items as $name => &$item) { + // 1. Check item's show-if + $showif = $item->getShowIf(); + if ($showif) { + $siname = "si.{$name}"; + $showif = str_replace("\n", "\n\t", $showif); + $code[$siname] = "{$siname} = (function(){ {$showif} })()"; - } + } - // 2. Check item's value - if ($item->needsDynamicValue()) { - $val = str_replace("\n","\n\t",$item->getValue($this)); - $code[$name] = "{$name} = (function(){ + // 2. Check item's value + if ($item->needsDynamicValue()) { + $val = str_replace("\n", "\n\t", $item->getValue($this)); + $code[$name] = "{$name} = (function(){ {$val} })()"; - if($showif) { - $code[$name] = "if({$siname}) { - ". $code[$name] ." + if ($showif) { + $code[$name] = "if({$siname}) { + " . $code[$name] . " }"; - } - // If item is to be shown (rendered), return evaluated dynamic value, else keep dynamic value as string - } - - } - - if (!$code) { - return $items; - } - - $ocpu_session = opencpu_multiparse_showif($this, $code, true); - if (!$ocpu_session || $ocpu_session->hasError()) { - notify_user_error(opencpu_debug($ocpu_session), "There was a problem evaluating showifs using openCPU."); - foreach($items as $name => &$item) { - $item->alwaysInvalid(); - } - } else { - print_hidden_opencpu_debug_message($ocpu_session, "OpenCPU debugger for dynamic values and showifs."); - $results = $ocpu_session->getJSONObject(); - $updateVisibility = $this->dbh->prepare("UPDATE `survey_items_display` SET hidden = :hidden WHERE item_id = :item_id AND session_id = :session_id"); - $updateVisibility->bindValue(":session_id", $this->session_id); - - $save = array(); - - $definitelyShownItems = 0; - foreach ($items as $item_name => &$item) { - // set show-if visibility for items - $siname = "si.{$item->name}"; - $isVisible = $item->setVisibility(array_val($results, $siname)); - // three possible states: 1 = hidden, 0 = shown, null = depends on JS on the page, render anyway - if($isVisible === null) { - // we only render it, if there are some items before it on which its display could depend - // otherwise it's hidden for good - $hidden = $definitelyShownItems > 0 ? null : 1; - } else { - $hidden = (int)!$isVisible; - } - $updateVisibility->bindValue(":item_id", $item->id); - $updateVisibility->bindValue(":hidden", $hidden); - $updateVisibility->execute(); - - if($hidden === 1) { // gone for good - unset($items[$item_name]); // we remove items that are definitely hidden from consideration - continue; // don't increment counter - } else { - // set dynamic values for items - $val = array_val($results, $item->name, null); - $item->setDynamicValue($val); - // save dynamic value - // if a. we have a value b. this item does not require user input (e.g. calculate) - if (array_key_exists($item->name, $results) && !$item->requiresUserInput()) { - $save[$item->name] = $item->getComputedValue(); - unset($items[$item_name]); // we remove items that are immediately written from consideration - continue; // don't increment counter - } - } - $definitelyShownItems++; // track whether there are any items certain to be shown - } - $this->post($save, false); - } - - return $items; - } - - protected function processDynamicLabelsAndChoices(&$items) { - // Gather choice lists - $lists_to_fetch = $strings_to_parse = array(); - $session_labels = array(); - - foreach ($items as $name => &$item) { - if ($item->choice_list) { - $lists_to_fetch[] = $item->choice_list; - } - - if ($item->needsDynamicLabel($this) ) { - $items[$name]->label_parsed = opencpu_string_key(count($strings_to_parse)); - $strings_to_parse[] = $item->label; - } - } - - // gather and format choice_lists and save all choice labels that need parsing - $choices = $this->getChoices($lists_to_fetch, null); - $choice_lists = array(); - foreach ($choices as $i => $choice) { - if ($choice['label_parsed'] === null) { - $choices[$i]['label_parsed'] = opencpu_string_key(count($strings_to_parse)); - $strings_to_parse[] = $choice['label']; - } - - if (!isset($choice_lists[$choice['list_name']])) { - $choice_lists[$choice['list_name']] = array(); - } - $choice_lists[$choice['list_name']][$choice['name']] = $choices[$i]['label_parsed']; - } - - // Now that we have the items and the choices, If there was anything left to parse, we do so here! - if ($strings_to_parse) { - $parsed_strings = opencpu_multistring_parse($this, $strings_to_parse); - // Replace parsed strings in $choice_list array - opencpu_substitute_parsed_strings($choice_lists, $parsed_strings); - // Replace parsed strings in unanswered items array - opencpu_substitute_parsed_strings($items, $parsed_strings); - } - - // Merge parsed choice lists into items - foreach ($items as $name => &$item) { - $choice_list = $item->choice_list; - if (isset($choice_lists[$choice_list])) { - $list = $choice_lists[$choice_list]; - $list = array_filter($list, 'is_formr_truthy'); - $items[$name]->setChoices($list); - } - //$items[$name]->refresh($item, array('label_parsed')); - $session_labels[$name] = $item->label_parsed; - } - - Session::set('labels', $session_labels); - return $items; - } - - /** - * All items that don't require connecting to openCPU and don't require user input are posted immediately. - * Examples: get parameters, browser, ip. - * - * @param Item[] $items - * @return array Returns items that may have to be sent to openCPU or be rendered for user input - */ - protected function processAutomaticItems($items) { - $hiddenItems = array(); - foreach ($items as $name => $item) { - if (!$item->requiresUserInput() && !$item->needsDynamicValue()) { - $hiddenItems[$name] = $item->getComputedValue(); - unset($items[$name]); - continue; - } - } - - // save these values - if ($hiddenItems) { - $this->post($hiddenItems, false); - } - - // return possibly shortened item array - return $items; - } - - /** - * Get the next items to be possibly displayed in the survey - * - * @return array Returns items that can be possibly shown on current page - */ - protected function getNextItems() { - $this->unanswered = array(); - $select = $this->dbh->select(' + } + // If item is to be shown (rendered), return evaluated dynamic value, else keep dynamic value as string + } + } + + if (!$code) { + return $items; + } + + $ocpu_session = opencpu_multiparse_showif($this, $code, true); + if (!$ocpu_session || $ocpu_session->hasError()) { + notify_user_error(opencpu_debug($ocpu_session), "There was a problem evaluating showifs using openCPU."); + foreach ($items as $name => &$item) { + $item->alwaysInvalid(); + } + } else { + print_hidden_opencpu_debug_message($ocpu_session, "OpenCPU debugger for dynamic values and showifs."); + $results = $ocpu_session->getJSONObject(); + $updateVisibility = $this->dbh->prepare("UPDATE `survey_items_display` SET hidden = :hidden WHERE item_id = :item_id AND session_id = :session_id"); + $updateVisibility->bindValue(":session_id", $this->session_id); + + $save = array(); + + $definitelyShownItems = 0; + foreach ($items as $item_name => &$item) { + // set show-if visibility for items + $siname = "si.{$item->name}"; + $isVisible = $item->setVisibility(array_val($results, $siname)); + // three possible states: 1 = hidden, 0 = shown, null = depends on JS on the page, render anyway + if ($isVisible === null) { + // we only render it, if there are some items before it on which its display could depend + // otherwise it's hidden for good + $hidden = $definitelyShownItems > 0 ? null : 1; + } else { + $hidden = (int) !$isVisible; + } + $updateVisibility->bindValue(":item_id", $item->id); + $updateVisibility->bindValue(":hidden", $hidden); + $updateVisibility->execute(); + + if ($hidden === 1) { // gone for good + unset($items[$item_name]); // we remove items that are definitely hidden from consideration + continue; // don't increment counter + } else { + // set dynamic values for items + $val = array_val($results, $item->name, null); + $item->setDynamicValue($val); + // save dynamic value + // if a. we have a value b. this item does not require user input (e.g. calculate) + if (array_key_exists($item->name, $results) && !$item->requiresUserInput()) { + $save[$item->name] = $item->getComputedValue(); + unset($items[$item_name]); // we remove items that are immediately written from consideration + continue; // don't increment counter + } + } + $definitelyShownItems++; // track whether there are any items certain to be shown + } + $this->post($save, false); + } + + return $items; + } + + protected function processDynamicLabelsAndChoices(&$items) { + // Gather choice lists + $lists_to_fetch = $strings_to_parse = array(); + $session_labels = array(); + + foreach ($items as $name => &$item) { + if ($item->choice_list) { + $lists_to_fetch[] = $item->choice_list; + } + + if ($item->needsDynamicLabel($this)) { + $items[$name]->label_parsed = opencpu_string_key(count($strings_to_parse)); + $strings_to_parse[] = $item->label; + } + } + + // gather and format choice_lists and save all choice labels that need parsing + $choices = $this->getChoices($lists_to_fetch, null); + $choice_lists = array(); + foreach ($choices as $i => $choice) { + if ($choice['label_parsed'] === null) { + $choices[$i]['label_parsed'] = opencpu_string_key(count($strings_to_parse)); + $strings_to_parse[] = $choice['label']; + } + + if (!isset($choice_lists[$choice['list_name']])) { + $choice_lists[$choice['list_name']] = array(); + } + $choice_lists[$choice['list_name']][$choice['name']] = $choices[$i]['label_parsed']; + } + + // Now that we have the items and the choices, If there was anything left to parse, we do so here! + if ($strings_to_parse) { + $parsed_strings = opencpu_multistring_parse($this, $strings_to_parse); + // Replace parsed strings in $choice_list array + opencpu_substitute_parsed_strings($choice_lists, $parsed_strings); + // Replace parsed strings in unanswered items array + opencpu_substitute_parsed_strings($items, $parsed_strings); + } + + // Merge parsed choice lists into items + foreach ($items as $name => &$item) { + $choice_list = $item->choice_list; + if (isset($choice_lists[$choice_list])) { + $list = $choice_lists[$choice_list]; + $list = array_filter($list, 'is_formr_truthy'); + $items[$name]->setChoices($list); + } + //$items[$name]->refresh($item, array('label_parsed')); + $session_labels[$name] = $item->label_parsed; + } + + Session::set('labels', $session_labels); + return $items; + } + + /** + * All items that don't require connecting to openCPU and don't require user input are posted immediately. + * Examples: get parameters, browser, ip. + * + * @param Item[] $items + * @return array Returns items that may have to be sent to openCPU or be rendered for user input + */ + protected function processAutomaticItems($items) { + $hiddenItems = array(); + foreach ($items as $name => $item) { + if (!$item->requiresUserInput() && !$item->needsDynamicValue()) { + $hiddenItems[$name] = $item->getComputedValue(); + unset($items[$name]); + continue; + } + } + + // save these values + if ($hiddenItems) { + $this->post($hiddenItems, false); + } + + // return possibly shortened item array + return $items; + } + + /** + * Get the next items to be possibly displayed in the survey + * + * @return array Returns items that can be possibly shown on current page + */ + protected function getNextItems() { + $this->unanswered = array(); + $select = $this->dbh->select(' `survey_items`.id, `survey_items`.study_id, `survey_items`.type, @@ -827,86 +824,86 @@ protected function getNextItems() { `survey_items_display`.`display_order`, `survey_items_display`.`hidden`, `survey_items_display`.answered') - ->from('survey_items') - ->leftJoin('survey_items_display', 'survey_items_display.session_id = :session_id', 'survey_items.id = survey_items_display.item_id') - ->where('(survey_items.study_id = :study_id) AND + ->from('survey_items') + ->leftJoin('survey_items_display', 'survey_items_display.session_id = :session_id', 'survey_items.id = survey_items_display.item_id') + ->where('(survey_items.study_id = :study_id) AND (survey_items_display.saved IS null) AND (survey_items_display.hidden IS NULL OR survey_items_display.hidden = 0)') - ->order('`survey_items_display`.`display_order`', 'asc') - ->order('survey_items.`order`', 'asc') // only needed for transfer - ->order('survey_items.id', 'asc'); + ->order('`survey_items_display`.`display_order`', 'asc') + ->order('survey_items.`order`', 'asc') // only needed for transfer + ->order('survey_items.id', 'asc'); - $get_items = $select->bindParams(array('session_id' => $this->session_id, 'study_id' => $this->id))->statement(); + $get_items = $select->bindParams(array('session_id' => $this->session_id, 'study_id' => $this->id))->statement(); - // We initialise item factory with no choice list because we don't know which choices will be used yet. - // This assumes choices are not required for show-ifs and dynamic values (hope so) - $itemFactory = new ItemFactory(array()); - $pageItems = array(); - $inPage = true; + // We initialise item factory with no choice list because we don't know which choices will be used yet. + // This assumes choices are not required for show-ifs and dynamic values (hope so) + $itemFactory = new ItemFactory(array()); + $pageItems = array(); + $inPage = true; - while ($item = $get_items->fetch(PDO::FETCH_ASSOC)) { - /* @var $oItem Item */ - $oItem = $itemFactory->make($item); - $this->unanswered[$oItem->name] = $oItem; + while ($item = $get_items->fetch(PDO::FETCH_ASSOC)) { + /* @var $oItem Item */ + $oItem = $itemFactory->make($item); + $this->unanswered[$oItem->name] = $oItem; - // If no user input is required and item can be on current page, then save it to be shown - if ($inPage) { - $pageItems[$oItem->name] = $oItem; - } + // If no user input is required and item can be on current page, then save it to be shown + if ($inPage) { + $pageItems[$oItem->name] = $oItem; + } - if ($oItem->type === 'submit') { - $inPage = false; - } - } + if ($oItem->type === 'submit') { + $inPage = false; + } + } - return $pageItems; - } + return $pageItems; + } - protected function renderNextItems() { + protected function renderNextItems() { - $this->dbh->beginTransaction(); + $this->dbh->beginTransaction(); - $view_query = " + $view_query = " UPDATE `survey_items_display` SET displaycount = COALESCE(displaycount,0) + 1, created = COALESCE(created, NOW()) WHERE item_id = :item_id AND session_id = :session_id"; - $view_update = $this->dbh->prepare($view_query); - $view_update->bindValue(":session_id", $this->session_id); - - $itemsDisplayed = 0; - - $this->rendered_items = array(); - try { - foreach ($this->to_render as &$item) { - if ($this->settings['maximum_number_displayed'] && $this->settings['maximum_number_displayed'] === $itemsDisplayed) { - break; - } else if ($item->isRendered()) { - // if it's rendered, we send it along here or update display count - $view_update->bindParam(":item_id", $item->id); - $view_update->execute(); - - if (!$item->hidden) { - $itemsDisplayed++; - } - - $this->rendered_items[] = $item; - } - } - - $this->dbh->commit(); - } catch (Exception $e) { - $this->dbh->rollBack(); - formr_log_exception($e, __CLASS__); - return false; - } - } - - protected function render_form_header($action = null) { - //$cookie = Request::getGlobals('COOKIE'); - $action = $action !== null ? $action : run_url($this->run_name); - $enctype = 'multipart/form-data'; # maybe make this conditional application/x-www-form-urlencoded - - $tpl = ' + $view_update = $this->dbh->prepare($view_query); + $view_update->bindValue(":session_id", $this->session_id); + + $itemsDisplayed = 0; + + $this->rendered_items = array(); + try { + foreach ($this->to_render as &$item) { + if ($this->settings['maximum_number_displayed'] && $this->settings['maximum_number_displayed'] === $itemsDisplayed) { + break; + } else if ($item->isRendered()) { + // if it's rendered, we send it along here or update display count + $view_update->bindParam(":item_id", $item->id); + $view_update->execute(); + + if (!$item->hidden) { + $itemsDisplayed++; + } + + $this->rendered_items[] = $item; + } + } + + $this->dbh->commit(); + } catch (Exception $e) { + $this->dbh->rollBack(); + formr_log_exception($e, __CLASS__); + return false; + } + } + + protected function render_form_header($action = null) { + //$cookie = Request::getGlobals('COOKIE'); + $action = $action !== null ? $action : run_url($this->run_name); + $enctype = 'multipart/form-data'; # maybe make this conditional application/x-www-form-urlencoded + + $tpl = '
    @@ -924,7 +921,7 @@ protected function render_form_header($action = null) { %{errors_tpl} '; - $errors_tpl = ' + $errors_tpl = '
    @@ -932,748 +929,746 @@ protected function render_form_header($action = null) {
    '; - if (!isset($this->settings["displayed_percentage_maximum"]) OR $this->settings["displayed_percentage_maximum"] == 0) { - $this->settings["displayed_percentage_maximum"] = 100; - } - - $prog = $this->progress * // the fraction of this survey that was completed - ($this->settings["displayed_percentage_maximum"] - // is multiplied with the stretch of percentage that it was accorded - $this->settings["add_percentage_points"]); - - if (isset($this->settings["add_percentage_points"])) { - $prog += $this->settings["add_percentage_points"]; - } - - if ($prog > $this->settings["displayed_percentage_maximum"]) { - $prog = $this->settings["displayed_percentage_maximum"]; - } - - $prog = round($prog); - - $tpl_vars = array( - 'action' => $action, - 'class' => 'form-horizontal main_formr_survey' . ($this->settings['enable_instant_validation'] ? ' ws-validate' : ''), - 'enctype' => $enctype, - 'session_id' => $this->session_id, - 'name_request_tokens' => Session::REQUEST_TOKENS, - 'name_user_code' => Session::REQUEST_USER_CODE, - 'name_cookie' => Session::REQUEST_NAME, - 'request_tokens' => Session::getRequestToken(), //$cookie->getRequestToken(), - 'user_code' => h(Site::getCurrentUser()->user_code), //h($cookie->getData('code')), - 'cookie' => '', //$cookie->getFile(), - 'progress' => $prog, - 'add_percentage_points' => $this->settings["add_percentage_points"], - 'displayed_percentage_maximum' => $this->settings["displayed_percentage_maximum"], - 'already_answered' => $this->progress_counts['already_answered'], - 'not_answered_on_current_page' => $this->progress_counts['not_answered_on_current_page'], - 'items_on_page' => $this->progress_counts['not_answered'] - $this->progress_counts['not_answered_on_current_page'], - 'hidden_but_rendered' => $this->progress_counts['hidden_but_rendered_on_current_page'], - - 'errors_tpl' => !empty($this->validation_errors) ? Template::replace($errors_tpl, array('errors' => $this->render_errors($this->validation_errors))) : null, - ); - - return Template::replace($tpl, $tpl_vars); - } - - protected function render_items() { - $ret = ''; - - foreach ($this->rendered_items AS $item) { - if (!empty($this->validation_errors[$item->name])) { - $item->error = $this->validation_errors[$item->name]; - } - if (!empty($this->items_validated[$item->name])) { - $item->value_validated = $this->items_validated[$item->name]->value_validated; - } - $ret .= $item->render(); - } - - // if the last item was not a submit button, add a default one - if (isset($item) && ($item->type !== "submit" || $item->hidden)) { - $sub_sets = array( - 'label_parsed' => ' Go on to the
    next page!', - 'classes_input' => array('btn-info default_formr_button'), - ); - $item = new Submit_Item($sub_sets); - $ret .= $item->render(); - } - - return $ret; - } - - protected function render_form_footer() { - return '
    '; - } - - /** - * - * @param Item[] $items - * @return string - */ - protected function render_errors($items) { - $labels = Session::get('labels', array()); - $tpl = - '
  • + if (!isset($this->settings["displayed_percentage_maximum"]) OR $this->settings["displayed_percentage_maximum"] == 0) { + $this->settings["displayed_percentage_maximum"] = 100; + } + + $prog = $this->progress * // the fraction of this survey that was completed + ($this->settings["displayed_percentage_maximum"] - // is multiplied with the stretch of percentage that it was accorded + $this->settings["add_percentage_points"]); + + if (isset($this->settings["add_percentage_points"])) { + $prog += $this->settings["add_percentage_points"]; + } + + if ($prog > $this->settings["displayed_percentage_maximum"]) { + $prog = $this->settings["displayed_percentage_maximum"]; + } + + $prog = round($prog); + + $tpl_vars = array( + 'action' => $action, + 'class' => 'form-horizontal main_formr_survey' . ($this->settings['enable_instant_validation'] ? ' ws-validate' : ''), + 'enctype' => $enctype, + 'session_id' => $this->session_id, + 'name_request_tokens' => Session::REQUEST_TOKENS, + 'name_user_code' => Session::REQUEST_USER_CODE, + 'name_cookie' => Session::REQUEST_NAME, + 'request_tokens' => Session::getRequestToken(), //$cookie->getRequestToken(), + 'user_code' => h(Site::getCurrentUser()->user_code), //h($cookie->getData('code')), + 'cookie' => '', //$cookie->getFile(), + 'progress' => $prog, + 'add_percentage_points' => $this->settings["add_percentage_points"], + 'displayed_percentage_maximum' => $this->settings["displayed_percentage_maximum"], + 'already_answered' => $this->progress_counts['already_answered'], + 'not_answered_on_current_page' => $this->progress_counts['not_answered_on_current_page'], + 'items_on_page' => $this->progress_counts['not_answered'] - $this->progress_counts['not_answered_on_current_page'], + 'hidden_but_rendered' => $this->progress_counts['hidden_but_rendered_on_current_page'], + 'errors_tpl' => !empty($this->validation_errors) ? Template::replace($errors_tpl, array('errors' => $this->render_errors($this->validation_errors))) : null, + ); + + return Template::replace($tpl, $tpl_vars); + } + + protected function render_items() { + $ret = ''; + + foreach ($this->rendered_items AS $item) { + if (!empty($this->validation_errors[$item->name])) { + $item->error = $this->validation_errors[$item->name]; + } + if (!empty($this->items_validated[$item->name])) { + $item->value_validated = $this->items_validated[$item->name]->value_validated; + } + $ret .= $item->render(); + } + + // if the last item was not a submit button, add a default one + if (isset($item) && ($item->type !== "submit" || $item->hidden)) { + $sub_sets = array( + 'label_parsed' => ' Go on to the
    next page!', + 'classes_input' => array('btn-info default_formr_button'), + ); + $item = new Submit_Item($sub_sets); + $ret .= $item->render(); + } + + return $ret; + } + + protected function render_form_footer() { + return ''; + } + + /** + * + * @param Item[] $items + * @return string + */ + protected function render_errors($items) { + $labels = Session::get('labels', array()); + $tpl = ' +
  • Question/Code: %{question}
    Error: %{error}
  • '; - $errors = ''; - - foreach ($items as $name => $error) { - if ($error) { - $errors .= Template::replace($tpl, array( - 'question' => strip_tags(array_val($labels, $name, strtoupper($name))), - 'error' => $error, - )); - } - } - Session::delete('labels'); - return '
      ' . $errors . '
    '; - } - - - public function end() { - $query = "UPDATE `{$this->results_table}` SET `ended` = NOW() WHERE `session_id` = :session_id AND `study_id` = :study_id AND `ended` IS null"; - $params = array('session_id' => $this->session_id, 'study_id' => $this->id); - $ended = $this->dbh->exec($query, $params); - - return parent::end(); - } - - public function getTimeWhenLastViewedItem() { - // use created (item render time) if viewed time is lacking - $arr = $this->dbh->select(array('COALESCE(`survey_items_display`.shown,`survey_items_display`.created)' => 'last_viewed')) - ->from('survey_items_display') - ->leftJoin('survey_items', 'survey_items_display.session_id = :session_id', 'survey_items.id = survey_items_display.item_id') - ->where('survey_items_display.session_id IS NOT NULL') - ->where('survey_items.study_id = :study_id') - ->order('survey_items_display.shown', 'desc') - ->order('survey_items_display.created', 'desc') - ->limit(1) - ->bindParams(array('session_id' => $this->session_id, 'study_id' => $this->id)) - ->fetch(); - - return isset($arr['last_viewed']) ? $arr['last_viewed'] : null; - } - - /** - * @see https://github.com/rubenarslan/formr.org/wiki/Expiry - * @return boolean - */ - private function hasExpired() { - if (empty($this->run_session->unit_session)) { - return false; - } - - $expire_invitation = (int) $this->settings['expire_invitation_after']; - $grace_period = (int) $this->settings['expire_invitation_grace']; - $expire_inactivity = (int) $this->settings['expire_after']; - if ($expire_inactivity === 0 && $expire_invitation === 0) { - return false; - } else { - $now = time(); - - $last_active = $this->getTimeWhenLastViewedItem(); // when was the user last active on the study - $expire_invitation_time = $expire_inactivity_time = 0; // default to 0 (means: other values supervene. users only get here if at least one value is nonzero) - if($expire_inactivity !== 0 && $last_active != null) { - $expire_inactivity_time = strtotime($last_active) + $expire_inactivity * 60; - } - $invitation_sent = $this->run_session->unit_session->created; - if($expire_invitation !== 0 && $invitation_sent) { - $expire_invitation_time = strtotime($invitation_sent) + $expire_invitation * 60; - if($grace_period !== 0 && $last_active) { - $expire_invitation_time = $expire_invitation_time + $grace_period * 60; - } - } - $expire = max($expire_inactivity_time, $expire_invitation_time); - $this->execData['expire_timestamp'] = $expire; - - return ($expire > 0) && ($now > $expire); // when we switch to the new scheduler, we need to return the timestamp here - } - } - - public function exec() { - // never show to the cronjob - $expired = $this->hasExpired(); - if ($this->called_by_cron) { - if ($expired) { - $this->expire(); - return false; - } - return true; - } - - // execute survey unit in a try catch block - // @todo Do same for other run units - try { - $request = new Request($_POST); - //$cookie = Request::getGlobals('COOKIE'); - //check if user session has a valid form token for POST requests - //if (Request::isHTTPPostRequest() && $cookie && !$cookie->canValidateRequestToken($request)) { - // redirect_to(run_url($this->run_name)); - //} - if (Request::isHTTPPostRequest() && !Session::canValidateRequestToken($request)) { - redirect_to(run_url($this->run_name)); - } - $this->startEntry(); - - // Use SurveyHelper if study is configured to use pages - if ($this->settings['use_paging']) { - $surveyHelper = new SurveyHelper(new Request(array_merge($_POST, $_FILES)), $this, new Run($this->dbh, $this->run_name)); - $surveyHelper->savePageItems($this->session_id); - if (($renderSurvey = $surveyHelper->renderSurvey($this->session_id)) !== false) { - return array('body' => $renderSurvey); - } else { - // Survey ended - return false; - } - } - - // POST items only if request is a post request - if (Request::isHTTPPostRequest()) { - $posted = $this->post(array_merge($request->getParams(), $_FILES)); - if ($posted) { - redirect_to(run_url($this->run_name)); - } - } - - $loops = 0; - while(($items = $this->getNextItems())) { - // exit loop if it has ran more than x times and log remaining items - $loops++; - if ($loops > Config::get('allowed_empty_pages', 80)) { - alert('Too many empty pages in this survey. Please alert an administrator.', 'alert-danger'); - formr_log("Survey::exec() '{$this->run_name} > {$this->name}' terminated with an infinite loop for items: "); - formr_log(array_keys($items)); - break; - } - // process automatic values (such as get, browser) - $items = $this->processAutomaticItems($items); - // process showifs, dynamic values for these items - $items = $this->processDynamicValuesAndShowIfs($items); - // If no items survived all the processing then move on - if (!$items) { - continue; - } - $lastItem = end($items); - - // If no items ended up to be on the page but for a submit button, make it hidden and continue - // else render processed items - if(count($items) == 1 && $lastItem->type === 'submit') { - $sess_item = array( - 'session_id' => $this->session_id, - 'item_id' => $lastItem->id, - ); - $this->dbh->update('survey_items_display', array('hidden' => 1), $sess_item); - continue; - } else { - $this->to_render = $this->processDynamicLabelsAndChoices($items); - break; - } - } - - if ($this->getProgress() === 1) { - $this->end(); - return false; - } - - $this->renderNextItems(); - - return array('body' => $this->render()); - } catch (Exception $e) { - formr_log_exception($e, __CLASS__); - return array('body' => ''); - } - } - - public function changeSettings($key_value_pairs) { - $errors = false; - array_walk($key_value_pairs, function (&$value, $key) { - if ($key !== 'google_file_id') { - $value = (int) $value; - } - }); - if (isset($key_value_pairs['maximum_number_displayed']) && $key_value_pairs['maximum_number_displayed'] > 3000 || $key_value_pairs['maximum_number_displayed'] < 0) { - alert("Maximum number displayed has to be between 1 and 3000", 'alert-warning'); - $errors = true; - } - - if (isset($key_value_pairs['displayed_percentage_maximum']) && $key_value_pairs['displayed_percentage_maximum'] > 100 || $key_value_pairs['displayed_percentage_maximum'] < 1) { - alert("Percentage maximum has to be between 1 and 100.", 'alert-warning'); - $errors = true; - } - - if (isset($key_value_pairs['add_percentage_points']) && $key_value_pairs['add_percentage_points'] > 100 || $key_value_pairs['add_percentage_points'] < 0) { - alert("Percentage points added has to be between 0 and 100.", 'alert-warning'); - $errors = true; - } - - $key_value_pairs['enable_instant_validation'] = (int)(isset($key_value_pairs['enable_instant_validation']) && $key_value_pairs['enable_instant_validation'] == 1); - $key_value_pairs['hide_results'] = (int)(isset($key_value_pairs['hide_results']) && $key_value_pairs['hide_results'] === 1); - $key_value_pairs['use_paging'] = (int)(isset($key_value_pairs['use_paging']) && $key_value_pairs['use_paging'] === 1); - $key_value_pairs['unlinked'] = (int)(isset($key_value_pairs['unlinked']) && $key_value_pairs['unlinked'] === 1); - - // user can't revert unlinking - if($key_value_pairs['unlinked'] < $this->settings['unlinked']) { - alert("Once a survey has been unlinked, it cannot be relinked.", 'alert-warning'); - $errors = true; - } - - // user can't revert preventing results display - if($key_value_pairs['hide_results'] < $this->settings['hide_results']) { - alert("Once results display is disabled, it cannot be re-enabled.", 'alert-warning'); - $errors = true; - } - - // user can't revert preventing results display - if($key_value_pairs['use_paging'] < $this->settings['use_paging']) { - alert("Once you have enabled the use of custom paging, you can't revert this setting.", 'alert-warning'); - $errors = true; - } - - if (isset($key_value_pairs['expire_after']) && $key_value_pairs['expire_after'] > 3153600) { - alert("Survey expiry time (in minutes) has to be below 3153600.", 'alert-warning'); - $errors = true; - } - - if (array_diff(array_keys($key_value_pairs), array_keys($this->settings))) { // any settings that aren't in the settings array? - alert("Invalid settings.", 'alert-danger'); - $errors = true; - } - - if ($errors) { - return false; - } - - $this->dbh->update('survey_studies', $key_value_pairs, array('id' => $this->id)); - - alert('Survey settings updated', 'alert-success', true); - } + $errors = ''; + + foreach ($items as $name => $error) { + if ($error) { + $errors .= Template::replace($tpl, array( + 'question' => strip_tags(array_val($labels, $name, strtoupper($name))), + 'error' => $error, + )); + } + } + Session::delete('labels'); + return '
      ' . $errors . '
    '; + } + + public function end() { + $query = "UPDATE `{$this->results_table}` SET `ended` = NOW() WHERE `session_id` = :session_id AND `study_id` = :study_id AND `ended` IS null"; + $params = array('session_id' => $this->session_id, 'study_id' => $this->id); + $ended = $this->dbh->exec($query, $params); + + return parent::end(); + } + + public function getTimeWhenLastViewedItem() { + // use created (item render time) if viewed time is lacking + $arr = $this->dbh->select(array('COALESCE(`survey_items_display`.shown,`survey_items_display`.created)' => 'last_viewed')) + ->from('survey_items_display') + ->leftJoin('survey_items', 'survey_items_display.session_id = :session_id', 'survey_items.id = survey_items_display.item_id') + ->where('survey_items_display.session_id IS NOT NULL') + ->where('survey_items.study_id = :study_id') + ->order('survey_items_display.shown', 'desc') + ->order('survey_items_display.created', 'desc') + ->limit(1) + ->bindParams(array('session_id' => $this->session_id, 'study_id' => $this->id)) + ->fetch(); + + return isset($arr['last_viewed']) ? $arr['last_viewed'] : null; + } + + /** + * @see https://github.com/rubenarslan/formr.org/wiki/Expiry + * @return boolean + */ + private function hasExpired() { + if (empty($this->run_session->unit_session)) { + return false; + } + + $expire_invitation = (int) $this->settings['expire_invitation_after']; + $grace_period = (int) $this->settings['expire_invitation_grace']; + $expire_inactivity = (int) $this->settings['expire_after']; + if ($expire_inactivity === 0 && $expire_invitation === 0) { + return false; + } else { + $now = time(); + + $last_active = $this->getTimeWhenLastViewedItem(); // when was the user last active on the study + $expire_invitation_time = $expire_inactivity_time = 0; // default to 0 (means: other values supervene. users only get here if at least one value is nonzero) + if ($expire_inactivity !== 0 && $last_active != null) { + $expire_inactivity_time = strtotime($last_active) + $expire_inactivity * 60; + } + $invitation_sent = $this->run_session->unit_session->created; + if ($expire_invitation !== 0 && $invitation_sent) { + $expire_invitation_time = strtotime($invitation_sent) + $expire_invitation * 60; + if ($grace_period !== 0 && $last_active) { + $expire_invitation_time = $expire_invitation_time + $grace_period * 60; + } + } + $expire = max($expire_inactivity_time, $expire_invitation_time); + $this->execData['expire_timestamp'] = $expire; + + return ($expire > 0) && ($now > $expire); // when we switch to the new scheduler, we need to return the timestamp here + } + } + + public function exec() { + // never show to the cronjob + $expired = $this->hasExpired(); + if ($this->called_by_cron) { + if ($expired) { + $this->expire(); + return false; + } + return true; + } + + // execute survey unit in a try catch block + // @todo Do same for other run units + try { + $request = new Request($_POST); + //$cookie = Request::getGlobals('COOKIE'); + //check if user session has a valid form token for POST requests + //if (Request::isHTTPPostRequest() && $cookie && !$cookie->canValidateRequestToken($request)) { + // redirect_to(run_url($this->run_name)); + //} + if (Request::isHTTPPostRequest() && !Session::canValidateRequestToken($request)) { + redirect_to(run_url($this->run_name)); + } + $this->startEntry(); + + // Use SurveyHelper if study is configured to use pages + if ($this->settings['use_paging']) { + $surveyHelper = new SurveyHelper(new Request(array_merge($_POST, $_FILES)), $this, new Run($this->dbh, $this->run_name)); + $surveyHelper->savePageItems($this->session_id); + if (($renderSurvey = $surveyHelper->renderSurvey($this->session_id)) !== false) { + return array('body' => $renderSurvey); + } else { + // Survey ended + return false; + } + } + + // POST items only if request is a post request + if (Request::isHTTPPostRequest()) { + $posted = $this->post(array_merge($request->getParams(), $_FILES)); + if ($posted) { + redirect_to(run_url($this->run_name)); + } + } + + $loops = 0; + while (($items = $this->getNextItems())) { + // exit loop if it has ran more than x times and log remaining items + $loops++; + if ($loops > Config::get('allowed_empty_pages', 80)) { + alert('Too many empty pages in this survey. Please alert an administrator.', 'alert-danger'); + formr_log("Survey::exec() '{$this->run_name} > {$this->name}' terminated with an infinite loop for items: "); + formr_log(array_keys($items)); + break; + } + // process automatic values (such as get, browser) + $items = $this->processAutomaticItems($items); + // process showifs, dynamic values for these items + $items = $this->processDynamicValuesAndShowIfs($items); + // If no items survived all the processing then move on + if (!$items) { + continue; + } + $lastItem = end($items); + + // If no items ended up to be on the page but for a submit button, make it hidden and continue + // else render processed items + if (count($items) == 1 && $lastItem->type === 'submit') { + $sess_item = array( + 'session_id' => $this->session_id, + 'item_id' => $lastItem->id, + ); + $this->dbh->update('survey_items_display', array('hidden' => 1), $sess_item); + continue; + } else { + $this->to_render = $this->processDynamicLabelsAndChoices($items); + break; + } + } + + if ($this->getProgress() === 1) { + $this->end(); + return false; + } + + $this->renderNextItems(); + + return array('body' => $this->render()); + } catch (Exception $e) { + formr_log_exception($e, __CLASS__); + return array('body' => ''); + } + } + + public function changeSettings($key_value_pairs) { + $errors = false; + array_walk($key_value_pairs, function (&$value, $key) { + if ($key !== 'google_file_id') { + $value = (int) $value; + } + }); + if (isset($key_value_pairs['maximum_number_displayed']) && $key_value_pairs['maximum_number_displayed'] > 3000 || $key_value_pairs['maximum_number_displayed'] < 0) { + alert("Maximum number displayed has to be between 1 and 3000", 'alert-warning'); + $errors = true; + } + + if (isset($key_value_pairs['displayed_percentage_maximum']) && $key_value_pairs['displayed_percentage_maximum'] > 100 || $key_value_pairs['displayed_percentage_maximum'] < 1) { + alert("Percentage maximum has to be between 1 and 100.", 'alert-warning'); + $errors = true; + } + + if (isset($key_value_pairs['add_percentage_points']) && $key_value_pairs['add_percentage_points'] > 100 || $key_value_pairs['add_percentage_points'] < 0) { + alert("Percentage points added has to be between 0 and 100.", 'alert-warning'); + $errors = true; + } + + $key_value_pairs['enable_instant_validation'] = (int) (isset($key_value_pairs['enable_instant_validation']) && $key_value_pairs['enable_instant_validation'] == 1); + $key_value_pairs['hide_results'] = (int) (isset($key_value_pairs['hide_results']) && $key_value_pairs['hide_results'] === 1); + $key_value_pairs['use_paging'] = (int) (isset($key_value_pairs['use_paging']) && $key_value_pairs['use_paging'] === 1); + $key_value_pairs['unlinked'] = (int) (isset($key_value_pairs['unlinked']) && $key_value_pairs['unlinked'] === 1); + + // user can't revert unlinking + if ($key_value_pairs['unlinked'] < $this->settings['unlinked']) { + alert("Once a survey has been unlinked, it cannot be relinked.", 'alert-warning'); + $errors = true; + } + + // user can't revert preventing results display + if ($key_value_pairs['hide_results'] < $this->settings['hide_results']) { + alert("Once results display is disabled, it cannot be re-enabled.", 'alert-warning'); + $errors = true; + } + + // user can't revert preventing results display + if ($key_value_pairs['use_paging'] < $this->settings['use_paging']) { + alert("Once you have enabled the use of custom paging, you can't revert this setting.", 'alert-warning'); + $errors = true; + } + + if (isset($key_value_pairs['expire_after']) && $key_value_pairs['expire_after'] > 3153600) { + alert("Survey expiry time (in minutes) has to be below 3153600.", 'alert-warning'); + $errors = true; + } + + if (array_diff(array_keys($key_value_pairs), array_keys($this->settings))) { // any settings that aren't in the settings array? + alert("Invalid settings.", 'alert-danger'); + $errors = true; + } + + if ($errors) { + return false; + } + + $this->dbh->update('survey_studies', $key_value_pairs, array('id' => $this->id)); + + alert('Survey settings updated', 'alert-success', true); + } // - public function uploadItemTable($file, $confirmed_deletion, $updates = array(), $created_new = false) { - umask(0002); - ini_set('memory_limit', Config::get('memory_limit.survey_upload_items')); - $target = $file['tmp_name']; - $filename = $file['name']; - - $this->confirmed_deletion = $confirmed_deletion; - $this->created_new = $created_new; - - $this->messages[] = "File $filename was uploaded to survey {$this->name}."; - - // @todo FIXME: This check is fakish because for some reason finfo_file doesn't deal with excel sheets exported from formr - // survey uploaded via JSON? - if (preg_match('/^([a-zA-Z][a-zA-Z0-9_]{2,64})(-[a-z0-9A-Z]+)?\.json$/', $filename)) { - $data = @json_decode(file_get_contents($target)); - $SPR = $this->createFromData($data, true); - } else { - // survey uploaded via excel - $SPR = new SpreadsheetReader(); - $SPR->readItemTableFile($target); - } - - if (!$SPR || !is_object($SPR)) { - alert('Spreadsheet object could not be created!', 'alert-danger'); - return false; - } - - $this->errors = array_merge($this->errors, $SPR->errors); - $this->warnings = array_merge($this->warnings, $SPR->warnings); - $this->messages = array_merge($this->messages, $SPR->messages); - $this->messages = array_unique($this->messages); - $this->warnings = array_unique($this->warnings); - - // if items are ok, make actual survey - if (empty($this->errors) && $this->createSurvey($SPR)): - if (!empty($this->warnings)) { - alert('
    • ' . implode("
    • ", $this->warnings) . '
    ', 'alert-warning'); - } - - if (!empty($this->messages)) { - alert('
    • ' . implode("
    • ", $this->messages) . '
    ', 'alert-info'); - } - - // save original survey sheet - $filename = 'formr-survey-' . Site::getCurrentUser()->id . '-' . $filename; - $file = Config::get('survey_upload_dir') . '/' . $filename; - if (file_exists($target) && (move_uploaded_file($target, $file) || rename($target, $file))) { - $updates['original_file'] = $filename; - } else { - alert('Unable to save original uploaded file', 'alert-warning'); - } - - // update db entry if necessary - if ($updates) { - $this->dbh->update('survey_studies', $updates, array('id' => $this->id)); - } - - return true; - else: - alert('
    • ' . implode("
    • ", $this->errors) . '
    ', 'alert-danger'); - return false; - endif; - } - - protected function resultsTableExists() { - return $this->dbh->table_exists($this->results_table); - } - - /* ADMIN functions */ - - public function checkName($name) { - if (!$name) { - alert(_("Error: The study name (the name of the file you uploaded) can only contain the characters from a to Z, 0 to 9 and the underscore. The name has to at least 2, at most 64 characters long. It needs to start with a letter. No dots, no spaces, no dashes, no umlauts please. The file can have version numbers after a dash, like this survey_1-v2.xlsx, but they will be ignored."), 'alert-danger'); - return false; - } elseif (!preg_match($this->study_name_pattern, $name)) { - alert('Error: The study name (the name of the file you uploaded) can only contain the characters from a to Z, 0 to 9 and the underscore. It needs to start with a letter. The file can have version numbers after a dash, like this survey_1-v2.xlsx.', 'alert-danger'); - return false; - } else { - $study_exists = $this->dbh->entry_exists('survey_studies', array('name' => $name, 'user_id' => $this->unit['user_id'])); - if ($study_exists) { - alert(__("Error: The survey name %s is already taken.", h($name)), 'alert-danger'); - return false; - } - } - - return true; - } - - public function createIndependently($settings = array(), $updates = array()) { - $name = trim($this->unit['name']); - $check_name = $this->checkName($name); - if(!$check_name) { - return false; - } - $this->id = parent::create('Survey'); - $this->name = $name; - - $results_table = substr("s" . $this->id . '_' . $name, 0, 64); - - if($this->dbh->table_exists($results_table)) { - alert("Results table name conflict. This shouldn't happen. Please alert the formr admins.", 'alert-danger'); - return false; - } - $this->results_table = $results_table; - - $study = array_merge(array( - 'id' => $this->id, - 'created' => mysql_now(), - 'modified' => mysql_now(), - 'user_id' => $this->unit['user_id'], - 'name' => $this->name, - 'results_table' => $this->results_table, - ), $updates); - - $this->dbh->insert('survey_studies', $study); - $this->load(); - - $this->changeSettings(array_merge(array( - "maximum_number_displayed" => 0, - "displayed_percentage_maximum" => 100, - "add_percentage_points" => 0, - ), $settings)); - - return true; - } - - protected $user_defined_columns = array( - 'name', 'label', 'label_parsed', 'type', 'type_options', 'choice_list', 'optional', 'class', 'showif', 'value', 'block_order', 'item_order', 'order' // study_id is not among the user_defined columns - ); - protected $choices_user_defined_columns = array( - 'list_name', 'name', 'label', 'label_parsed' // study_id is not among the user_defined columns - ); - - /** - * Get All choice lists in this survey with associated items - * - * @param array $specific An array if list_names which if defined, only lists specified in the array will be returned - * @param string $label - * @return $array Returns an array indexed by list name; - */ - public function getChoices($specific = null, $label = 'label') { - $select = $this->dbh->select('list_name, name, label, label_parsed'); - $select->from('survey_item_choices'); - $select->where(array('study_id' => $this->id)); - - if (!$specific && $specific !== null) { - return array(); - } elseif ($specific !== null) { - $select->whereIn('list_name', $specific); - } - $select->order('id', 'ASC'); - - $lists = array(); - $stmt = $select->statement(); - - // If we are not hunting for a particular field name return list as is - if (!$label) { - return $stmt->fetchAll(PDO::FETCH_ASSOC); - } - - while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) { - if (!isset($lists[$row['list_name']])) { - $lists[$row['list_name']] = array(); - } - $lists[$row['list_name']][$row['name']] = $row[$label]; - } - - return $lists; - } - - public function getChoicesForSheet() { - return $this->dbh->select('list_name, name, label') - ->from('survey_item_choices') - ->where(array('study_id' => $this->id)) - ->order('id', 'ASC')->fetchAll(); - } - - /** - * Create a new survey using items uploaded by the spreadsheet reader - * Cases: - * 0. new creation -> do it - * 1. is the upload identical to the existing -> do nothing - * 2. are there any results at all? -> nuke existing shit, do fresh start - * 3. does the upload entail text/minor changes to the item table -> simply do changes - * 4. does the upload entail changes to results (i.e. will there be deletion) -> confirm deletion - * 4a. deletion confirmed: backup results, modify results table, drop stuff - * 4b. deletion not confirmed: cancel the whole shit. - * - * @param SpreadsheetReader $SPR - * @return boolean - */ - public function createSurvey($SPR) { - $this->SPR = $SPR; - $this->parsedown = new ParsedownExtra(); - $this->parsedown = $this->parsedown->setBreaksEnabled(true)->setUrlsLinked(true); - - // Get old choice lists for getting old items - $choice_lists = $this->getChoices(); - $this->item_factory = new ItemFactory($choice_lists); - - // Get old items, mark them as false meaning all are vulnerable for delete. - // When the loop over survey items ends you will know which should be deleted. - $old_items = array(); - $old_items_in_results = array(); - - foreach ($this->getItems() as $item) { - if (($object = $this->item_factory->make($item)) !== false) { - $old_items[$item['name']] = $object->getResultField(); - if ($object->isStoredInResultsTable()) { - $old_items_in_results[] = $item['name']; - } - } - } - - try { - $this->dbh->beginTransaction(); - $data = $this->addItems(); - $new_items = $data['new_items']; - $result_columns = $data['result_columns']; - - $added = array_diff_assoc($new_items, $old_items); - $deleted = array_diff_assoc($old_items, $new_items); - $unused = $this->item_factory->unusedChoiceLists(); - if ($unused) { - $this->warnings[] = __("These choice lists were not used: '%s'", implode("', '", $unused)); - } - - // If there are items to delete, check if user confirmed deletion and if so check if back up succeeded - if(count($deleted) > 0) { - if($this->realDataExists() && !$this->confirmed_deletion) { - $deleted_columns_string = implode(array_keys($deleted), ", "); - $this->errors[] = "No permission to delete data. Enter the survey name, if you are okay with data being deleted from the following items: " . $deleted_columns_string; - } - if($this->realDataExists() && $this->confirmed_deletion && !$this->backupResults($old_items_in_results)) { - $this->errors[] = "Back up failed. Deletions would have been necessary, but backing up the item table failed, so no modification was carried out."; - } - } - - // If there are errors at this point then terminate to rollback all changes inluding adding choices and inserting items - if (!empty($this->errors)) { - throw new Exception('Process terminated prematurely due to errors'); - } - - $actually_deleted = array_diff(array_keys($deleted), array_keys($added)); - if ($deleted && $actually_deleted) { - // some items were just re-typed, they only have to be deleted from the wide format table which has inflexible types - $toDelete = implode(',', array_map(array($this->dbh, 'quote'), $actually_deleted)); - $studyId = (int) $this->id; - $delQ = "DELETE FROM survey_items WHERE `name` IN ($toDelete) AND study_id = $studyId"; - $this->dbh->query($delQ); - } - - // we start fresh if it's a new creation, no results table exist or it is completely empty - if ($this->created_new || !$this->resultsTableExists() || !$this->dataExists()) { - if($this->created_new && $this->resultsTableExists()) { - throw new Exception("Results table name conflict. This shouldn't happen. Please alert the formr admins"); - } - // step 2 - $this->messages[] = "The results table was newly created, because there were no results and test sessions."; - // if there is no results table or no existing data at all, drop table, create anew - // i.e. where possible prefer nuclear option - $new_syntax = $this->getResultsTableSyntax($result_columns); - if(!$this->createResultsTable($new_syntax)) { - throw new Exception('Unable to create a data table for survey results'); - } - } else { - // this will never happen if deletion was not confirmed, because this would raise an error - // 2 and 4a - $merge = $this->alterResultsTable($added, $deleted); - if(!$merge) { - throw new Exception('Required modifications could not be made to the survey results table'); - } - } - - $this->dbh->commit(); - return true; - } catch (Exception $e) { - $this->dbh->rollBack(); - $this->errors[] = 'Error: ' . $e->getMessage(); - formr_log_exception($e, __CLASS__, $this->errors); - return false; - } - - } - - /** - * Prepares the statement to insert new items and returns an associative containing - * the SQL definition of the new items and the new result columns - * - * @see createSurvey() - * @return array array(new_items, result_columns) - */ - protected function addItems() { - // Save new choices and re-build the item factory - $this->addChoices(); - $choice_lists = $this->getChoices(); - $this->item_factory = new ItemFactory($choice_lists); - - // Prepare SQL statement for adding items - $UPDATES = implode(', ', get_duplicate_update_string($this->user_defined_columns)); - $addStmt = $this->dbh->prepare( - "INSERT INTO `survey_items` (study_id, name, label, label_parsed, type, type_options, choice_list, optional, class, showif, value, `block_order`,`item_order`, `order`) + public function uploadItemTable($file, $confirmed_deletion, $updates = array(), $created_new = false) { + umask(0002); + ini_set('memory_limit', Config::get('memory_limit.survey_upload_items')); + $target = $file['tmp_name']; + $filename = $file['name']; + + $this->confirmed_deletion = $confirmed_deletion; + $this->created_new = $created_new; + + $this->messages[] = "File $filename was uploaded to survey {$this->name}."; + + // @todo FIXME: This check is fakish because for some reason finfo_file doesn't deal with excel sheets exported from formr + // survey uploaded via JSON? + if (preg_match('/^([a-zA-Z][a-zA-Z0-9_]{2,64})(-[a-z0-9A-Z]+)?\.json$/', $filename)) { + $data = @json_decode(file_get_contents($target)); + $SPR = $this->createFromData($data, true); + } else { + // survey uploaded via excel + $SPR = new SpreadsheetReader(); + $SPR->readItemTableFile($target); + } + + if (!$SPR || !is_object($SPR)) { + alert('Spreadsheet object could not be created!', 'alert-danger'); + return false; + } + + $this->errors = array_merge($this->errors, $SPR->errors); + $this->warnings = array_merge($this->warnings, $SPR->warnings); + $this->messages = array_merge($this->messages, $SPR->messages); + $this->messages = array_unique($this->messages); + $this->warnings = array_unique($this->warnings); + + // if items are ok, make actual survey + if (empty($this->errors) && $this->createSurvey($SPR)): + if (!empty($this->warnings)) { + alert('
    • ' . implode("
    • ", $this->warnings) . '
    ', 'alert-warning'); + } + + if (!empty($this->messages)) { + alert('
    • ' . implode("
    • ", $this->messages) . '
    ', 'alert-info'); + } + + // save original survey sheet + $filename = 'formr-survey-' . Site::getCurrentUser()->id . '-' . $filename; + $file = Config::get('survey_upload_dir') . '/' . $filename; + if (file_exists($target) && (move_uploaded_file($target, $file) || rename($target, $file))) { + $updates['original_file'] = $filename; + } else { + alert('Unable to save original uploaded file', 'alert-warning'); + } + + // update db entry if necessary + if ($updates) { + $this->dbh->update('survey_studies', $updates, array('id' => $this->id)); + } + + return true; + else: + alert('
    • ' . implode("
    • ", $this->errors) . '
    ', 'alert-danger'); + return false; + endif; + } + + protected function resultsTableExists() { + return $this->dbh->table_exists($this->results_table); + } + + /* ADMIN functions */ + + public function checkName($name) { + if (!$name) { + alert(_("Error: The study name (the name of the file you uploaded) can only contain the characters from a to Z, 0 to 9 and the underscore. The name has to at least 2, at most 64 characters long. It needs to start with a letter. No dots, no spaces, no dashes, no umlauts please. The file can have version numbers after a dash, like this survey_1-v2.xlsx, but they will be ignored."), 'alert-danger'); + return false; + } elseif (!preg_match($this->study_name_pattern, $name)) { + alert('Error: The study name (the name of the file you uploaded) can only contain the characters from a to Z, 0 to 9 and the underscore. It needs to start with a letter. The file can have version numbers after a dash, like this survey_1-v2.xlsx.', 'alert-danger'); + return false; + } else { + $study_exists = $this->dbh->entry_exists('survey_studies', array('name' => $name, 'user_id' => $this->unit['user_id'])); + if ($study_exists) { + alert(__("Error: The survey name %s is already taken.", h($name)), 'alert-danger'); + return false; + } + } + + return true; + } + + public function createIndependently($settings = array(), $updates = array()) { + $name = trim($this->unit['name']); + $check_name = $this->checkName($name); + if (!$check_name) { + return false; + } + $this->id = parent::create('Survey'); + $this->name = $name; + + $results_table = substr("s" . $this->id . '_' . $name, 0, 64); + + if ($this->dbh->table_exists($results_table)) { + alert("Results table name conflict. This shouldn't happen. Please alert the formr admins.", 'alert-danger'); + return false; + } + $this->results_table = $results_table; + + $study = array_merge(array( + 'id' => $this->id, + 'created' => mysql_now(), + 'modified' => mysql_now(), + 'user_id' => $this->unit['user_id'], + 'name' => $this->name, + 'results_table' => $this->results_table, + ), $updates); + + $this->dbh->insert('survey_studies', $study); + $this->load(); + + $this->changeSettings(array_merge(array( + "maximum_number_displayed" => 0, + "displayed_percentage_maximum" => 100, + "add_percentage_points" => 0, + ), $settings)); + + return true; + } + + protected $user_defined_columns = array( + 'name', 'label', 'label_parsed', 'type', 'type_options', 'choice_list', 'optional', 'class', 'showif', 'value', 'block_order', 'item_order', 'order' // study_id is not among the user_defined columns + ); + protected $choices_user_defined_columns = array( + 'list_name', 'name', 'label', 'label_parsed' // study_id is not among the user_defined columns + ); + + /** + * Get All choice lists in this survey with associated items + * + * @param array $specific An array if list_names which if defined, only lists specified in the array will be returned + * @param string $label + * @return $array Returns an array indexed by list name; + */ + public function getChoices($specific = null, $label = 'label') { + $select = $this->dbh->select('list_name, name, label, label_parsed'); + $select->from('survey_item_choices'); + $select->where(array('study_id' => $this->id)); + + if (!$specific && $specific !== null) { + return array(); + } elseif ($specific !== null) { + $select->whereIn('list_name', $specific); + } + $select->order('id', 'ASC'); + + $lists = array(); + $stmt = $select->statement(); + + // If we are not hunting for a particular field name return list as is + if (!$label) { + return $stmt->fetchAll(PDO::FETCH_ASSOC); + } + + while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) { + if (!isset($lists[$row['list_name']])) { + $lists[$row['list_name']] = array(); + } + $lists[$row['list_name']][$row['name']] = $row[$label]; + } + + return $lists; + } + + public function getChoicesForSheet() { + return $this->dbh->select('list_name, name, label') + ->from('survey_item_choices') + ->where(array('study_id' => $this->id)) + ->order('id', 'ASC')->fetchAll(); + } + + /** + * Create a new survey using items uploaded by the spreadsheet reader + * Cases: + * 0. new creation -> do it + * 1. is the upload identical to the existing -> do nothing + * 2. are there any results at all? -> nuke existing shit, do fresh start + * 3. does the upload entail text/minor changes to the item table -> simply do changes + * 4. does the upload entail changes to results (i.e. will there be deletion) -> confirm deletion + * 4a. deletion confirmed: backup results, modify results table, drop stuff + * 4b. deletion not confirmed: cancel the whole shit. + * + * @param SpreadsheetReader $SPR + * @return boolean + */ + public function createSurvey($SPR) { + $this->SPR = $SPR; + $this->parsedown = new ParsedownExtra(); + $this->parsedown = $this->parsedown->setBreaksEnabled(true)->setUrlsLinked(true); + + // Get old choice lists for getting old items + $choice_lists = $this->getChoices(); + $this->item_factory = new ItemFactory($choice_lists); + + // Get old items, mark them as false meaning all are vulnerable for delete. + // When the loop over survey items ends you will know which should be deleted. + $old_items = array(); + $old_items_in_results = array(); + + foreach ($this->getItems() as $item) { + if (($object = $this->item_factory->make($item)) !== false) { + $old_items[$item['name']] = $object->getResultField(); + if ($object->isStoredInResultsTable()) { + $old_items_in_results[] = $item['name']; + } + } + } + + try { + $this->dbh->beginTransaction(); + $data = $this->addItems(); + $new_items = $data['new_items']; + $result_columns = $data['result_columns']; + + $added = array_diff_assoc($new_items, $old_items); + $deleted = array_diff_assoc($old_items, $new_items); + $unused = $this->item_factory->unusedChoiceLists(); + if ($unused) { + $this->warnings[] = __("These choice lists were not used: '%s'", implode("', '", $unused)); + } + + // If there are items to delete, check if user confirmed deletion and if so check if back up succeeded + if (count($deleted) > 0) { + if ($this->realDataExists() && !$this->confirmed_deletion) { + $deleted_columns_string = implode(array_keys($deleted), ", "); + $this->errors[] = "No permission to delete data. Enter the survey name, if you are okay with data being deleted from the following items: " . $deleted_columns_string; + } + if ($this->realDataExists() && $this->confirmed_deletion && !$this->backupResults($old_items_in_results)) { + $this->errors[] = "Back up failed. Deletions would have been necessary, but backing up the item table failed, so no modification was carried out."; + } + } + + // If there are errors at this point then terminate to rollback all changes inluding adding choices and inserting items + if (!empty($this->errors)) { + throw new Exception('Process terminated prematurely due to errors'); + } + + $actually_deleted = array_diff(array_keys($deleted), array_keys($added)); + if ($deleted && $actually_deleted) { + // some items were just re-typed, they only have to be deleted from the wide format table which has inflexible types + $toDelete = implode(',', array_map(array($this->dbh, 'quote'), $actually_deleted)); + $studyId = (int) $this->id; + $delQ = "DELETE FROM survey_items WHERE `name` IN ($toDelete) AND study_id = $studyId"; + $this->dbh->query($delQ); + } + + // we start fresh if it's a new creation, no results table exist or it is completely empty + if ($this->created_new || !$this->resultsTableExists() || !$this->dataExists()) { + if ($this->created_new && $this->resultsTableExists()) { + throw new Exception("Results table name conflict. This shouldn't happen. Please alert the formr admins"); + } + // step 2 + $this->messages[] = "The results table was newly created, because there were no results and test sessions."; + // if there is no results table or no existing data at all, drop table, create anew + // i.e. where possible prefer nuclear option + $new_syntax = $this->getResultsTableSyntax($result_columns); + if (!$this->createResultsTable($new_syntax)) { + throw new Exception('Unable to create a data table for survey results'); + } + } else { + // this will never happen if deletion was not confirmed, because this would raise an error + // 2 and 4a + $merge = $this->alterResultsTable($added, $deleted); + if (!$merge) { + throw new Exception('Required modifications could not be made to the survey results table'); + } + } + + $this->dbh->commit(); + return true; + } catch (Exception $e) { + $this->dbh->rollBack(); + $this->errors[] = 'Error: ' . $e->getMessage(); + formr_log_exception($e, __CLASS__, $this->errors); + return false; + } + } + + /** + * Prepares the statement to insert new items and returns an associative containing + * the SQL definition of the new items and the new result columns + * + * @see createSurvey() + * @return array array(new_items, result_columns) + */ + protected function addItems() { + // Save new choices and re-build the item factory + $this->addChoices(); + $choice_lists = $this->getChoices(); + $this->item_factory = new ItemFactory($choice_lists); + + // Prepare SQL statement for adding items + $UPDATES = implode(', ', get_duplicate_update_string($this->user_defined_columns)); + $addStmt = $this->dbh->prepare( + "INSERT INTO `survey_items` (study_id, name, label, label_parsed, type, type_options, choice_list, optional, class, showif, value, `block_order`,`item_order`, `order`) VALUES (:study_id, :name, :label, :label_parsed, :type, :type_options, :choice_list, :optional, :class, :showif, :value, :block_order, :item_order, :order) ON DUPLICATE KEY UPDATE $UPDATES"); - $addStmt->bindParam(":study_id", $this->id); - - $ret = array( - 'new_items' => array(), - 'result_columns' => array(), - ); - - foreach ($this->SPR->survey as $row_number => $row) { - $item = $this->item_factory->make($row); - if (!$item) { - $this->errors[] = __("Row %s: Type %s is invalid.", $row_number, array_val($this->SPR->survey[$row_number], 'type')); - unset($this->SPR->survey[$row_number]); - continue; - } - - $val_results = $item->validate(); - if (!empty($val_results['val_errors'])) { - $this->errors = $this->errors + $val_results['val_errors']; - unset($this->SPR->survey[$row_number]); - continue; - } - if(!empty($val_results['val_warnings'])) { - $this->warnings = $this->warnings + $val_results['val_warnings']; - } - - // if the parsed label is constant or exists - if (!$this->knittingNeeded($item->label) && !$item->label_parsed) { - $markdown = $this->parsedown->text($item->label); - $item->label_parsed = $markdown; - if (mb_substr_count($markdown, "

    ") === 1 AND preg_match("@^

    (.+)

    $@", trim($markdown), $matches)) { - $item->label_parsed = $matches[1]; - } - } - - foreach ($this->user_defined_columns as $param) { - $addStmt->bindValue(":$param", $item->$param); - } - - $result_field = $item->getResultField(); - $ret['new_items'][$item->name] = $result_field; - $ret['result_columns'][] = $result_field; - - $addStmt->execute(); - } - - return $ret; - } - - public function getItemsWithChoices($columns = null, $whereIn = null) { - if($this->resultsTableExists()) { - $choice_lists = $this->getChoices(); - $this->item_factory = new ItemFactory($choice_lists); - - $raw_items = $this->getItems($columns, $whereIn); - - $items = array(); - foreach ($raw_items as $row) { - $item = $this->item_factory->make($row); - $items[$item->name] = $item; - } - return $items; - } else { - return array(); - } - } - public function getItemsInResultsTable() { - $items = $this->getItems(); - $names = array(); - $itemFactory = new ItemFactory(array()); - foreach($items AS $item) { - $item = $itemFactory->make($item); - if($item->isStoredInResultsTable()) { - $names[] = $item->name; - } - } - return $names; - } - - private function addChoices() { - // delete cascades to item display ?? FIXME so maybe not a good idea to delete then - $deleted = $this->dbh->delete('survey_item_choices', array('study_id' => $this->id)); - $addChoiceStmt = $this->dbh->prepare( - 'INSERT INTO `survey_item_choices` (study_id, list_name, name, label, label_parsed) + $addStmt->bindParam(":study_id", $this->id); + + $ret = array( + 'new_items' => array(), + 'result_columns' => array(), + ); + + foreach ($this->SPR->survey as $row_number => $row) { + $item = $this->item_factory->make($row); + if (!$item) { + $this->errors[] = __("Row %s: Type %s is invalid.", $row_number, array_val($this->SPR->survey[$row_number], 'type')); + unset($this->SPR->survey[$row_number]); + continue; + } + + $val_results = $item->validate(); + if (!empty($val_results['val_errors'])) { + $this->errors = $this->errors + $val_results['val_errors']; + unset($this->SPR->survey[$row_number]); + continue; + } + if (!empty($val_results['val_warnings'])) { + $this->warnings = $this->warnings + $val_results['val_warnings']; + } + + // if the parsed label is constant or exists + if (!$this->knittingNeeded($item->label) && !$item->label_parsed) { + $markdown = $this->parsedown->text($item->label); + $item->label_parsed = $markdown; + if (mb_substr_count($markdown, "

    ") === 1 AND preg_match("@^

    (.+)

    $@", trim($markdown), $matches)) { + $item->label_parsed = $matches[1]; + } + } + + foreach ($this->user_defined_columns as $param) { + $addStmt->bindValue(":$param", $item->$param); + } + + $result_field = $item->getResultField(); + $ret['new_items'][$item->name] = $result_field; + $ret['result_columns'][] = $result_field; + + $addStmt->execute(); + } + + return $ret; + } + + public function getItemsWithChoices($columns = null, $whereIn = null) { + if ($this->resultsTableExists()) { + $choice_lists = $this->getChoices(); + $this->item_factory = new ItemFactory($choice_lists); + + $raw_items = $this->getItems($columns, $whereIn); + + $items = array(); + foreach ($raw_items as $row) { + $item = $this->item_factory->make($row); + $items[$item->name] = $item; + } + return $items; + } else { + return array(); + } + } + + public function getItemsInResultsTable() { + $items = $this->getItems(); + $names = array(); + $itemFactory = new ItemFactory(array()); + foreach ($items AS $item) { + $item = $itemFactory->make($item); + if ($item->isStoredInResultsTable()) { + $names[] = $item->name; + } + } + return $names; + } + + private function addChoices() { + // delete cascades to item display ?? FIXME so maybe not a good idea to delete then + $deleted = $this->dbh->delete('survey_item_choices', array('study_id' => $this->id)); + $addChoiceStmt = $this->dbh->prepare( + 'INSERT INTO `survey_item_choices` (study_id, list_name, name, label, label_parsed) VALUES (:study_id, :list_name, :name, :label, :label_parsed )' - ); - $addChoiceStmt->bindParam(":study_id", $this->id); - - foreach ($this->SPR->choices as $choice) { - if (isset($choice['list_name']) && isset($choice['name']) && isset($choice['label'])) { - if (!$this->knittingNeeded($choice['label']) && empty($choice['label_parsed'])) { // if the parsed label is constant - $markdown = $this->parsedown->text($choice['label']); // transform upon insertion into db instead of at runtime - $choice['label_parsed'] = $markdown; - if (mb_substr_count($markdown, "

    ") === 1 AND preg_match("@^

    (.+)

    $@", trim($markdown), $matches)) { - $choice['label_parsed'] = $matches[1]; - } - } - - foreach ($this->choices_user_defined_columns as $param) { - $addChoiceStmt->bindValue(":$param", $choice[$param]); - } - $addChoiceStmt->execute(); - } - } - - return true; - } - - private function getResultsTableSyntax($columns) { - $columns = array_filter($columns); // remove null, false, '' values (note, fork, submit, ...) - - if (empty($columns)) { - $columns_string = ''; # create a results tabel with only the access times - } else { - $columns_string = implode(",\n", $columns) . ","; - } - - $create = " + ); + $addChoiceStmt->bindParam(":study_id", $this->id); + + foreach ($this->SPR->choices as $choice) { + if (isset($choice['list_name']) && isset($choice['name']) && isset($choice['label'])) { + if (!$this->knittingNeeded($choice['label']) && empty($choice['label_parsed'])) { // if the parsed label is constant + $markdown = $this->parsedown->text($choice['label']); // transform upon insertion into db instead of at runtime + $choice['label_parsed'] = $markdown; + if (mb_substr_count($markdown, "

    ") === 1 AND preg_match("@^

    (.+)

    $@", trim($markdown), $matches)) { + $choice['label_parsed'] = $matches[1]; + } + } + + foreach ($this->choices_user_defined_columns as $param) { + $addChoiceStmt->bindValue(":$param", $choice[$param]); + } + $addChoiceStmt->execute(); + } + } + + return true; + } + + private function getResultsTableSyntax($columns) { + $columns = array_filter($columns); // remove null, false, '' values (note, fork, submit, ...) + + if (empty($columns)) { + $columns_string = ''; # create a results tabel with only the access times + } else { + $columns_string = implode(",\n", $columns) . ","; + } + + $create = " CREATE TABLE `{$this->results_table}` ( `session_id` INT UNSIGNED NOT NULL , `study_id` INT UNSIGNED NOT NULL , @@ -1697,159 +1692,160 @@ private function getResultsTableSyntax($columns) { ON DELETE NO ACTION ON UPDATE NO ACTION) ENGINE = InnoDB"; - return $create; - } - - private function createResultsTable($syntax) { - if ($this->deleteResults()) { - $drop = $this->dbh->query("DROP TABLE IF EXISTS `{$this->results_table}` ;"); - $drop->execute(); - } else { - return false; - } - - $create_table = $this->dbh->query($syntax); - if ($create_table) { - return true; - } - return false; - } - - public function getItems($columns = null, $whereIn = null) { - if ($columns === null) { - $columns = "id, study_id, type, choice_list, type_options, name, label, label_parsed, optional, class, showif, value, block_order,item_order"; - } - - $select = $this->dbh->select($columns); - $select->from('survey_items'); - $select->where(array('study_id' => $this->id)); - if ($whereIn) { - $select->whereIn($whereIn['field'], $whereIn['values']); - } - $select->order("order"); - return $select->fetchAll(); - } - - public function getItemsForSheet() { - $get_items = $this->dbh->select('type, type_options, choice_list, name, label, optional, class, showif, value, block_order, item_order') - ->from('survey_items') - ->where(array('study_id' => $this->id)) - ->order("`survey_items`.order") - ->statement(); - - $results = array(); - while ($row = $get_items->fetch(PDO::FETCH_ASSOC)): - $row["type"] = $row["type"] . " " . $row["type_options"] . " " . $row["choice_list"]; - unset($row["choice_list"], $row["type_options"]); //FIXME: why unset here? - $results[] = $row; - endwhile; - - return $results; - } - - public function getResults($items = null, $filter = null, array $paginate = null, $runId = null, $rstmt = false) { - if ($this->resultsTableExists()) { - ini_set('memory_limit', Config::get('memory_limit.survey_get_results')); - - $results_table = $this->results_table; - if (!$items) { - $items = $this->getItemsInResultsTable(); - } - - $count = $this->getResultCount(); - $get_all = true; - if($this->settings['unlinked'] && $count['real_users'] <= 10) { - if($count['real_users'] > 0) { - alert("You cannot see the real results yet. It will only be possible after 10 real users have registered.", 'alert-warning'); - } - $get_all = false; - } - if($this->settings['unlinked']) { - $columns = array(); - // considered showing data for test sessions, but then researchers could set real users to "test" to identify them -/* $columns = array( - "IF(survey_run_sessions.testing, survey_run_sessions.session, '') AS session", - "IF(survey_run_sessions.testing, `{$results_table}`.`created`, '') AS created", - "IF(survey_run_sessions.testing, `{$results_table}`.`modified`, '') AS modified", - "IF(survey_run_sessions.testing, `{$results_table}`.`ended`, '') AS ended", - ); -*/ } else { - $columns = array('survey_run_sessions.session', "`{$results_table}`.`created`", "`{$results_table}`.`modified`", "`{$results_table}`.`ended`, `survey_unit_sessions`.`expired`"); - } - foreach ($items as $item) { - $columns[] = "{$results_table}.{$item}"; - } - - $select = $this->dbh->select($columns) - ->from($results_table) - ->leftJoin('survey_unit_sessions', "{$results_table}.session_id = survey_unit_sessions.id") - ->leftJoin('survey_run_sessions', 'survey_unit_sessions.run_session_id = survey_run_sessions.id'); - if (!$get_all) { - $select->where('survey_run_sessions.testing = 1'); - } - - if ($runId !== null) { - $select->where("survey_run_sessions.run_id = {$runId}"); - } - - if ($paginate && isset($paginate['offset'])) { - $order = isset($paginate['order']) ? $paginate['order'] : 'asc'; - $order_by = isset($paginate['order_by']) ? $paginate['order_by'] : '{$results_table}.session_id'; - if($this->settings['unlinked']) { - $order_by = "RAND()"; - } - $select->order($order_by, $order); - $select->limit($paginate['limit'], $paginate['offset']); - } - - if (!empty($filter['session'])) { - $session = $filter['session']; - strlen($session) == 64 ? $select->where("survey_run_sessions.session = '$session'") : $select->like('survey_run_sessions.session', $session, 'right'); - } - - if (!empty($filter['results']) && ($res_filter = $this->getResultsFilter($filter['results']))) { - $res_where = Template::replace($res_filter['query'], array('table' => $results_table)); - $select->where($res_where); - } - - $stmt = $select->statement(); - if ($rstmt === true) { - return $stmt; - } - - $results = array(); - while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) { - unset($row['study_id']); - $results[] = $row; - } - - return $results; - } else { - return array(); - } - } - - /** - * Get Results from the item display table - * - * @param array $items An array of item names that are required in the survey - * @param string $session If specified, only results of that particular session will be returned - * @param array $paginate Pagination parameters [offset, limit] - * @param boolean $rstmt If TRUE, PDOStament will be returned instead - * @return array|PDOStatement - */ - public function getItemDisplayResults($items = array(), $filter = null, array $paginate = null, $rstmt = false) { - ini_set('memory_limit', Config::get('memory_limit.survey_get_results')); - - $count = $this->getResultCount(); - if($this->settings['unlinked']) { - if($count['real_users'] > 0) { - alert("You cannot see the long-form results yet. It will only be possible after 10 real users have registered.", 'alert-warning'); - } - return array(); - } - - $select = $this->dbh->select("`survey_run_sessions`.session, + return $create; + } + + private function createResultsTable($syntax) { + if ($this->deleteResults()) { + $drop = $this->dbh->query("DROP TABLE IF EXISTS `{$this->results_table}` ;"); + $drop->execute(); + } else { + return false; + } + + $create_table = $this->dbh->query($syntax); + if ($create_table) { + return true; + } + return false; + } + + public function getItems($columns = null, $whereIn = null) { + if ($columns === null) { + $columns = "id, study_id, type, choice_list, type_options, name, label, label_parsed, optional, class, showif, value, block_order,item_order"; + } + + $select = $this->dbh->select($columns); + $select->from('survey_items'); + $select->where(array('study_id' => $this->id)); + if ($whereIn) { + $select->whereIn($whereIn['field'], $whereIn['values']); + } + $select->order("order"); + return $select->fetchAll(); + } + + public function getItemsForSheet() { + $get_items = $this->dbh->select('type, type_options, choice_list, name, label, optional, class, showif, value, block_order, item_order') + ->from('survey_items') + ->where(array('study_id' => $this->id)) + ->order("`survey_items`.order") + ->statement(); + + $results = array(); + while ($row = $get_items->fetch(PDO::FETCH_ASSOC)): + $row["type"] = $row["type"] . " " . $row["type_options"] . " " . $row["choice_list"]; + unset($row["choice_list"], $row["type_options"]); //FIXME: why unset here? + $results[] = $row; + endwhile; + + return $results; + } + + public function getResults($items = null, $filter = null, array $paginate = null, $runId = null, $rstmt = false) { + if ($this->resultsTableExists()) { + ini_set('memory_limit', Config::get('memory_limit.survey_get_results')); + + $results_table = $this->results_table; + if (!$items) { + $items = $this->getItemsInResultsTable(); + } + + $count = $this->getResultCount(); + $get_all = true; + if ($this->settings['unlinked'] && $count['real_users'] <= 10) { + if ($count['real_users'] > 0) { + alert("You cannot see the real results yet. It will only be possible after 10 real users have registered.", 'alert-warning'); + } + $get_all = false; + } + if ($this->settings['unlinked']) { + $columns = array(); + // considered showing data for test sessions, but then researchers could set real users to "test" to identify them + /* $columns = array( + "IF(survey_run_sessions.testing, survey_run_sessions.session, '') AS session", + "IF(survey_run_sessions.testing, `{$results_table}`.`created`, '') AS created", + "IF(survey_run_sessions.testing, `{$results_table}`.`modified`, '') AS modified", + "IF(survey_run_sessions.testing, `{$results_table}`.`ended`, '') AS ended", + ); + */ + } else { + $columns = array('survey_run_sessions.session', "`{$results_table}`.`created`", "`{$results_table}`.`modified`", "`{$results_table}`.`ended`, `survey_unit_sessions`.`expired`"); + } + foreach ($items as $item) { + $columns[] = "{$results_table}.{$item}"; + } + + $select = $this->dbh->select($columns) + ->from($results_table) + ->leftJoin('survey_unit_sessions', "{$results_table}.session_id = survey_unit_sessions.id") + ->leftJoin('survey_run_sessions', 'survey_unit_sessions.run_session_id = survey_run_sessions.id'); + if (!$get_all) { + $select->where('survey_run_sessions.testing = 1'); + } + + if ($runId !== null) { + $select->where("survey_run_sessions.run_id = {$runId}"); + } + + if ($paginate && isset($paginate['offset'])) { + $order = isset($paginate['order']) ? $paginate['order'] : 'asc'; + $order_by = isset($paginate['order_by']) ? $paginate['order_by'] : '{$results_table}.session_id'; + if ($this->settings['unlinked']) { + $order_by = "RAND()"; + } + $select->order($order_by, $order); + $select->limit($paginate['limit'], $paginate['offset']); + } + + if (!empty($filter['session'])) { + $session = $filter['session']; + strlen($session) == 64 ? $select->where("survey_run_sessions.session = '$session'") : $select->like('survey_run_sessions.session', $session, 'right'); + } + + if (!empty($filter['results']) && ($res_filter = $this->getResultsFilter($filter['results']))) { + $res_where = Template::replace($res_filter['query'], array('table' => $results_table)); + $select->where($res_where); + } + + $stmt = $select->statement(); + if ($rstmt === true) { + return $stmt; + } + + $results = array(); + while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) { + unset($row['study_id']); + $results[] = $row; + } + + return $results; + } else { + return array(); + } + } + + /** + * Get Results from the item display table + * + * @param array $items An array of item names that are required in the survey + * @param string $session If specified, only results of that particular session will be returned + * @param array $paginate Pagination parameters [offset, limit] + * @param boolean $rstmt If TRUE, PDOStament will be returned instead + * @return array|PDOStatement + */ + public function getItemDisplayResults($items = array(), $filter = null, array $paginate = null, $rstmt = false) { + ini_set('memory_limit', Config::get('memory_limit.survey_get_results')); + + $count = $this->getResultCount(); + if ($this->settings['unlinked']) { + if ($count['real_users'] > 0) { + alert("You cannot see the long-form results yet. It will only be possible after 10 real users have registered.", 'alert-warning'); + } + return array(); + } + + $select = $this->dbh->select("`survey_run_sessions`.session, `survey_items_display`.`session_id` as `unit_session_id`, `survey_items_display`.`item_id`, `survey_items`.`name` as `item_name`, @@ -1864,74 +1860,74 @@ public function getItemDisplayResults($items = array(), $filter = null, array $p `survey_items_display`.`display_order`, `survey_items_display`.`hidden`"); - $select->from('survey_items_display') - ->leftJoin('survey_unit_sessions', 'survey_unit_sessions.id = survey_items_display.session_id') - ->leftJoin('survey_run_sessions', 'survey_run_sessions.id = survey_unit_sessions.run_session_id') - ->leftJoin('survey_items', 'survey_items_display.item_id = survey_items.id') - ->where('survey_items.study_id = :study_id') - ->order('survey_run_sessions.session') - ->order('survey_run_sessions.created') - ->order('survey_unit_sessions.created') - ->order('survey_items_display.display_order') - ->bindParams(array('study_id' => $this->id)); - - if ($items) { - $select->whereIn('survey_items.name', $items); - } - - $session = array_val($filter, 'session', null); - if ($session) { - if(strlen($session) == 64) { - $select->where("survey_run_sessions.session = :session"); - } else { - $select->where("survey_run_sessions.session LIKE :session"); - $session .= "%"; - } - $select->bindParams(array("session" => $session)); - } - - if ($paginate && isset($paginate['offset'])) { - $select->limit($paginate['limit'], $paginate['offset']); - } - - if ($rstmt === true) { - return $select->statement(); - } - return $select->fetchAll(); - } - - public function getResultsByItemsPerSession($items = array(), $filter = null, array $paginate = null, $rstmt = false) { - if($this->settings['unlinked']) { - return array(); - } - ini_set('memory_limit', Config::get('memory_limit.survey_get_results')); - - $filter_select = $this->dbh->select('session_id'); - $filter_select->from($this->results_table); - $filter_select->leftJoin('survey_unit_sessions', "{$this->results_table}.session_id = survey_unit_sessions.id"); - $filter_select->leftJoin('survey_run_sessions', 'survey_unit_sessions.run_session_id = survey_run_sessions.id'); - - if (!empty($filter['session'])) { - $session = $filter['session']; - strlen($session) == 64 ? $filter_select->where("survey_run_sessions.session = '$session'") : $filter_select->like('survey_run_sessions.session', $session, 'right'); - } - - if (!empty($filter['results']) && ($res_filter = $this->getResultsFilter($filter['results']))) { - $res_where = Template::replace($res_filter['query'], array('table' => $this->results_table)); - $filter_select->where($res_where); - } - $filter_select->order('session_id'); - if ($paginate && isset($paginate['offset'])) { - $filter_select->limit($paginate['limit'], $paginate['offset']); - } - $stmt = $filter_select->statement(); - $session_ids = ''; - while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) { - $session_ids .= "{$row['session_id']},"; - } - $session_ids = trim($session_ids, ','); - - $select = $this->dbh->select(" + $select->from('survey_items_display') + ->leftJoin('survey_unit_sessions', 'survey_unit_sessions.id = survey_items_display.session_id') + ->leftJoin('survey_run_sessions', 'survey_run_sessions.id = survey_unit_sessions.run_session_id') + ->leftJoin('survey_items', 'survey_items_display.item_id = survey_items.id') + ->where('survey_items.study_id = :study_id') + ->order('survey_run_sessions.session') + ->order('survey_run_sessions.created') + ->order('survey_unit_sessions.created') + ->order('survey_items_display.display_order') + ->bindParams(array('study_id' => $this->id)); + + if ($items) { + $select->whereIn('survey_items.name', $items); + } + + $session = array_val($filter, 'session', null); + if ($session) { + if (strlen($session) == 64) { + $select->where("survey_run_sessions.session = :session"); + } else { + $select->where("survey_run_sessions.session LIKE :session"); + $session .= "%"; + } + $select->bindParams(array("session" => $session)); + } + + if ($paginate && isset($paginate['offset'])) { + $select->limit($paginate['limit'], $paginate['offset']); + } + + if ($rstmt === true) { + return $select->statement(); + } + return $select->fetchAll(); + } + + public function getResultsByItemsPerSession($items = array(), $filter = null, array $paginate = null, $rstmt = false) { + if ($this->settings['unlinked']) { + return array(); + } + ini_set('memory_limit', Config::get('memory_limit.survey_get_results')); + + $filter_select = $this->dbh->select('session_id'); + $filter_select->from($this->results_table); + $filter_select->leftJoin('survey_unit_sessions', "{$this->results_table}.session_id = survey_unit_sessions.id"); + $filter_select->leftJoin('survey_run_sessions', 'survey_unit_sessions.run_session_id = survey_run_sessions.id'); + + if (!empty($filter['session'])) { + $session = $filter['session']; + strlen($session) == 64 ? $filter_select->where("survey_run_sessions.session = '$session'") : $filter_select->like('survey_run_sessions.session', $session, 'right'); + } + + if (!empty($filter['results']) && ($res_filter = $this->getResultsFilter($filter['results']))) { + $res_where = Template::replace($res_filter['query'], array('table' => $this->results_table)); + $filter_select->where($res_where); + } + $filter_select->order('session_id'); + if ($paginate && isset($paginate['offset'])) { + $filter_select->limit($paginate['limit'], $paginate['offset']); + } + $stmt = $filter_select->statement(); + $session_ids = ''; + while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) { + $session_ids .= "{$row['session_id']},"; + } + $session_ids = trim($session_ids, ','); + + $select = $this->dbh->select(" `survey_run_sessions`.session, `survey_items_display`.`session_id` as `unit_session_id`, `survey_items`.`name` as `item_name`, @@ -1947,156 +1943,156 @@ public function getResultsByItemsPerSession($items = array(), $filter = null, ar `survey_items_display`.`display_order`, `survey_items_display`.`hidden`"); - $select->from('survey_items_display') - ->leftJoin('survey_unit_sessions', 'survey_unit_sessions.id = survey_items_display.session_id') - ->leftJoin('survey_run_sessions', 'survey_run_sessions.id = survey_unit_sessions.run_session_id') - ->leftJoin('survey_items', 'survey_items.id = survey_items_display.item_id') - ->where('survey_items.study_id = :study_id') - ->where('survey_items_display.session_id IN ('.$session_ids.')') - ->order('survey_items_display.session_id') - ->order('survey_items_display.display_order') - ->bindParams(array('study_id' => $this->id)); - - if ($items) { - $select->whereIn('survey_items.name', $items); - } - - if ($rstmt === true) { - return $select->statement(); - } - return $select->fetchAll(); - } - - /** - * Get Results from the item display table - * - * @param array $items An array of item names that are required in the survey - * @param array $sessions If specified, only results of that particular session will be returned - * @return array - */ - public function getResultsByItemAndSession($items = array(), $sessions = null) { - $select = $this->dbh->select(' + $select->from('survey_items_display') + ->leftJoin('survey_unit_sessions', 'survey_unit_sessions.id = survey_items_display.session_id') + ->leftJoin('survey_run_sessions', 'survey_run_sessions.id = survey_unit_sessions.run_session_id') + ->leftJoin('survey_items', 'survey_items.id = survey_items_display.item_id') + ->where('survey_items.study_id = :study_id') + ->where('survey_items_display.session_id IN (' . $session_ids . ')') + ->order('survey_items_display.session_id') + ->order('survey_items_display.display_order') + ->bindParams(array('study_id' => $this->id)); + + if ($items) { + $select->whereIn('survey_items.name', $items); + } + + if ($rstmt === true) { + return $select->statement(); + } + return $select->fetchAll(); + } + + /** + * Get Results from the item display table + * + * @param array $items An array of item names that are required in the survey + * @param array $sessions If specified, only results of that particular session will be returned + * @return array + */ + public function getResultsByItemAndSession($items = array(), $sessions = null) { + $select = $this->dbh->select(' `survey_run_sessions`.session, `survey_items`.name, `survey_items_display`.answer'); - $select->from('survey_items_display') - ->leftJoin('survey_unit_sessions', 'survey_unit_sessions.id = survey_items_display.session_id') - ->leftJoin('survey_run_sessions', 'survey_run_sessions.id = survey_unit_sessions.run_session_id') - ->leftJoin('survey_items', 'survey_items_display.item_id = survey_items.id') - ->where('survey_items.study_id = :study_id') - ->order('survey_run_sessions.session') - ->order('survey_run_sessions.created') - ->order('survey_unit_sessions.created') - ->order('survey_items_display.display_order') - ->bindParams(array('study_id' => $this->id)); - - if (!empty($items)) { - $select->whereIn('survey_items.name', $items); - } - - if (!empty($sessions)) { - $select->whereIn('survey_items.name', $sessions); - } - - return $select->fetchAll(); - } - - protected function dataExists($min = 0) { - $this->result_count = $this->getResultCount(); - if(($this->result_count["real_users"] + $this->result_count['testers']) > 0) { - return true; - } else { - return false; - } - } - - protected function realDataExists($min = 0) { - $this->result_count = $this->getResultCount(); - if($this->result_count["real_users"] > 1) { - return true; - } else { - return false; - } - } - - public function deleteResults($run_id = null) { - $this->result_count = $this->getResultCount($run_id); - - if (array_sum($this->result_count) === 0) { - return true; - } elseif ($run_id !== null) { - //@todo implement deleting results only for a particular run - $this->error[] = 'Deleting run specific results for a survey is not yet implemented'; - return false; - } elseif ($this->backupResults()) { - $delete = $this->dbh->query("TRUNCATE TABLE `{$this->results_table}`"); - $delete_item_disp = $this->dbh->delete('survey_unit_sessions', array('unit_id' => $this->id)); - return $delete && $delete_item_disp; - } else { - $this->errors[] = __("Backup of %s result rows failed. Deletion cancelled.", array_sum($this->result_count)); - return false; - } - } - - public function backupResults($itemNames = null) { - $this->result_count = $this->getResultCount(); - if ($this->realDataExists()) { - $this->messages[] = __("Backed up. The old results were backed up in a file (%s results)", array_sum($this->result_count)); - - $filename = $this->results_table . date('YmdHis') . ".tab"; - if (isset($this->user_id)) { - $filename = "user" . $this->user_id . $filename; - } - $filename = APPLICATION_ROOT . "tmp/backups/results/" . $filename; - - $SPR = new SpreadsheetReader(); - return $SPR->backupTSV($this->getResults($itemNames), $filename); - } else { // if we have no real data, no need for backup - return true; - } - } - - public function getResultCount($run_id = null, $filter = array()) { - // If there is no filter and results have been saved in a previous operation then that - if ($this->result_count !== null && !$filter) { - return $this->result_count; - } - - $count = array('finished' => 0, 'begun' => 0, 'testers' => 0, 'real_users' => 0); - if ($this->resultsTableExists()) { - $results_table = $this->results_table; - $select = $this->dbh->select(array( - "SUM(`survey_run_sessions`.`testing` IS NOT NULL AND `survey_run_sessions`.`testing` = 0 AND `{$results_table}`.ended IS null)" => 'begun', - "SUM(`survey_run_sessions`.`testing` IS NOT NULL AND `survey_run_sessions`.`testing` = 0 AND `{$results_table}`.ended IS NOT NULL)" => 'finished', - "SUM(`survey_run_sessions`.`testing` IS NULL OR `survey_run_sessions`.`testing` = 1)" => 'testers', - "SUM(`survey_run_sessions`.`testing` IS NOT NULL AND `survey_run_sessions`.`testing` = 0)" => 'real_users' - ))->from($results_table) - ->leftJoin('survey_unit_sessions', "survey_unit_sessions.id = {$results_table}.session_id") - ->leftJoin('survey_run_sessions', "survey_unit_sessions.run_session_id = survey_run_sessions.id"); - - if ($run_id) { - $select->where("survey_run_sessions.run_id = {$run_id}"); - } - if (!empty($filter['session'])) { - $session = $filter['session']; - strlen($session) == 64 ? $select->where("survey_run_sessions.session = '$session'") : $select->like('survey_run_sessions.session', $session, 'right'); - } - - if (!empty($filter['results']) && ($res_filter = $this->getResultsFilter($filter['results']))) { - $res_where = Template::replace($res_filter['query'], array('table' => $results_table)); - $select->where($res_where); - } - - $count = $select->fetch(); - } - - return $count; - } - - public function getAverageTimeItTakes() { - if($this->resultsTableExists()) { - $get = "SELECT AVG(middle_values) AS 'median' FROM ( + $select->from('survey_items_display') + ->leftJoin('survey_unit_sessions', 'survey_unit_sessions.id = survey_items_display.session_id') + ->leftJoin('survey_run_sessions', 'survey_run_sessions.id = survey_unit_sessions.run_session_id') + ->leftJoin('survey_items', 'survey_items_display.item_id = survey_items.id') + ->where('survey_items.study_id = :study_id') + ->order('survey_run_sessions.session') + ->order('survey_run_sessions.created') + ->order('survey_unit_sessions.created') + ->order('survey_items_display.display_order') + ->bindParams(array('study_id' => $this->id)); + + if (!empty($items)) { + $select->whereIn('survey_items.name', $items); + } + + if (!empty($sessions)) { + $select->whereIn('survey_items.name', $sessions); + } + + return $select->fetchAll(); + } + + protected function dataExists($min = 0) { + $this->result_count = $this->getResultCount(); + if (($this->result_count["real_users"] + $this->result_count['testers']) > 0) { + return true; + } else { + return false; + } + } + + protected function realDataExists($min = 0) { + $this->result_count = $this->getResultCount(); + if ($this->result_count["real_users"] > 1) { + return true; + } else { + return false; + } + } + + public function deleteResults($run_id = null) { + $this->result_count = $this->getResultCount($run_id); + + if (array_sum($this->result_count) === 0) { + return true; + } elseif ($run_id !== null) { + //@todo implement deleting results only for a particular run + $this->error[] = 'Deleting run specific results for a survey is not yet implemented'; + return false; + } elseif ($this->backupResults()) { + $delete = $this->dbh->query("TRUNCATE TABLE `{$this->results_table}`"); + $delete_item_disp = $this->dbh->delete('survey_unit_sessions', array('unit_id' => $this->id)); + return $delete && $delete_item_disp; + } else { + $this->errors[] = __("Backup of %s result rows failed. Deletion cancelled.", array_sum($this->result_count)); + return false; + } + } + + public function backupResults($itemNames = null) { + $this->result_count = $this->getResultCount(); + if ($this->realDataExists()) { + $this->messages[] = __("Backed up. The old results were backed up in a file (%s results)", array_sum($this->result_count)); + + $filename = $this->results_table . date('YmdHis') . ".tab"; + if (isset($this->user_id)) { + $filename = "user" . $this->user_id . $filename; + } + $filename = APPLICATION_ROOT . "tmp/backups/results/" . $filename; + + $SPR = new SpreadsheetReader(); + return $SPR->backupTSV($this->getResults($itemNames), $filename); + } else { // if we have no real data, no need for backup + return true; + } + } + + public function getResultCount($run_id = null, $filter = array()) { + // If there is no filter and results have been saved in a previous operation then that + if ($this->result_count !== null && !$filter) { + return $this->result_count; + } + + $count = array('finished' => 0, 'begun' => 0, 'testers' => 0, 'real_users' => 0); + if ($this->resultsTableExists()) { + $results_table = $this->results_table; + $select = $this->dbh->select(array( + "SUM(`survey_run_sessions`.`testing` IS NOT NULL AND `survey_run_sessions`.`testing` = 0 AND `{$results_table}`.ended IS null)" => 'begun', + "SUM(`survey_run_sessions`.`testing` IS NOT NULL AND `survey_run_sessions`.`testing` = 0 AND `{$results_table}`.ended IS NOT NULL)" => 'finished', + "SUM(`survey_run_sessions`.`testing` IS NULL OR `survey_run_sessions`.`testing` = 1)" => 'testers', + "SUM(`survey_run_sessions`.`testing` IS NOT NULL AND `survey_run_sessions`.`testing` = 0)" => 'real_users' + ))->from($results_table) + ->leftJoin('survey_unit_sessions', "survey_unit_sessions.id = {$results_table}.session_id") + ->leftJoin('survey_run_sessions', "survey_unit_sessions.run_session_id = survey_run_sessions.id"); + + if ($run_id) { + $select->where("survey_run_sessions.run_id = {$run_id}"); + } + if (!empty($filter['session'])) { + $session = $filter['session']; + strlen($session) == 64 ? $select->where("survey_run_sessions.session = '$session'") : $select->like('survey_run_sessions.session', $session, 'right'); + } + + if (!empty($filter['results']) && ($res_filter = $this->getResultsFilter($filter['results']))) { + $res_where = Template::replace($res_filter['query'], array('table' => $results_table)); + $select->where($res_where); + } + + $count = $select->fetch(); + } + + return $count; + } + + public function getAverageTimeItTakes() { + if ($this->resultsTableExists()) { + $get = "SELECT AVG(middle_values) AS 'median' FROM ( SELECT took AS 'middle_values' FROM ( SELECT @row:=@row+1 as `row`, (x.ended - x.created) AS took @@ -2114,137 +2110,138 @@ public function getAverageTimeItTakes() { -- the following condition will return 1 record for odd number sets, or 2 records for even number sets. WHERE t1.row >= t2.count/2 and t1.row <= ((t2.count/2) +1)) AS t3;"; - $get = $this->dbh->query($get, true); - $time = $get->fetch(PDO::FETCH_NUM); - $time = round($time[0] / 60, 3); # seconds to minutes - - return $time; - } - return ''; - } - - public function rename($new_name) { - if($this->checkName($new_name)) { - $mod = $this->dbh->update('survey_studies', array('name' => $new_name), array( - 'id' => $this->id, - )); - if($mod) { - $this->name = $new_name; - } - return $mod; - } - } - - public function delete($special = null) { - if ($this->deleteResults()): // always back up - $this->dbh->query("DROP TABLE IF EXISTS `{$this->results_table}`"); - if (($filename = $this->getOriginalFileName())) { - @unlink(Config::get('survey_upload_dir') . '/' . $filename); - } - return parent::delete($special); - endif; - return false; - } - - public function displayForRun($prepend = '') { - $dialog = Template::get($this->getUnitTemplatePath(), array( - 'survey' => $this, - 'studies' => $this->dbh->select('id, name')->from('survey_studies')->where(array('user_id' => Site::getCurrentUser()->id))->fetchAll(), - 'prepend' => $prepend, - 'resultCount' => $this->id ? $this->howManyReachedItNumbers() : null, - 'time' => $this->id ? $this->getAverageTimeItTakes() : null, - )); - - return parent::runDialog($dialog); - } - - /** - * Merge survey items. Each parameter is an associative array indexed by the names of the items in the survey, with the - * mysql field definition as the value. - * new items are added, old items are deleted, items that changed type are deleted from the results table but not the item_display_table - * All non null entries represent the MySQL data type definition of the fields as they should be in the survey results table - * NOTE: All the DB queries here should be in a transaction of calling function - * - * @param array $newItems - * @param array $deleteItems - * @return bool; - */ - private function alterResultsTable(array $newItems, array $deleteItems) { - $actions = $toAdd = $toDelete = array(); - $deleteQuery = $addQuery = array(); - $addQ = $delQ = null; - - // just for safety checking that there is something to be deleted (in case of aborted earlier tries) - $existingColumns = $this->dbh->getTableDefinition($this->results_table, 'Field'); - - // Create query to drop items in existing table - foreach ($deleteItems as $name => $result_field) { - if ($result_field !== null && isset($existingColumns[$name])) { - $deleteQuery[] = " DROP `{$name}`"; - } - $toDelete[] = $name; - } - // Create query for adding items to existing table - foreach ($newItems as $name => $result_field) { - if ($result_field !== null) { - $addQuery[] = " ADD $result_field"; - } - $toAdd[] = $name; - } - - // prepare these strings for feedback - $added_columns_string = implode($toAdd, ", "); - $deleted_columns_string = implode($toDelete, ", "); - - - // if something should be deleted - if ($deleteQuery) { - $q = "ALTER TABLE `{$this->results_table}`" . implode(',', $deleteQuery); - $this->dbh->query($q); - $actions[] = "Deleted columns: $deleted_columns_string."; - } - - // we only get here if the deletion stuff was harmless, allowed or did not happen - if ($addQuery) { - $q = "ALTER TABLE `{$this->results_table}`" . implode(',', $addQuery); - $this->dbh->query($q); - $actions[] = "Added columns: $added_columns_string."; - } - - if(!empty($actions)) { - $this->messages[] = "The results table was modified."; - $this->messages = array_merge($this->messages, $actions); - } else { - $this->messages[] = "The results table did not need to be modified."; - } - - return true; - } - - public function getOriginalFileName() { - return $this->dbh->findValue('survey_studies', array('id' => $this->id), 'original_file'); - } - - public function getGoogleFileId() { - return $this->dbh->findValue('survey_studies', array('id' => $this->id), 'google_file_id'); - } - - public function getResultsFilter($f = null) { - $filter = array( - 'all' => array( - 'title' => 'Show All', - 'query' => null, - ), - 'incomplete' => array( - 'title' => 'Incomplete', - 'query' => '(%{table}.created <> %{table}.modified or %{table}.modified is null) and %{table}.ended is null', - ), - 'complete' => array( - 'title' => 'Complete', - 'query' => '%{table}.ended is not null', - ), - ); - - return $f !== null ? array_val($filter, $f, null) : $filter; - } + $get = $this->dbh->query($get, true); + $time = $get->fetch(PDO::FETCH_NUM); + $time = round($time[0] / 60, 3); # seconds to minutes + + return $time; + } + return ''; + } + + public function rename($new_name) { + if ($this->checkName($new_name)) { + $mod = $this->dbh->update('survey_studies', array('name' => $new_name), array( + 'id' => $this->id, + )); + if ($mod) { + $this->name = $new_name; + } + return $mod; + } + } + + public function delete($special = null) { + if ($this->deleteResults()): // always back up + $this->dbh->query("DROP TABLE IF EXISTS `{$this->results_table}`"); + if (($filename = $this->getOriginalFileName())) { + @unlink(Config::get('survey_upload_dir') . '/' . $filename); + } + return parent::delete($special); + endif; + return false; + } + + public function displayForRun($prepend = '') { + $dialog = Template::get($this->getUnitTemplatePath(), array( + 'survey' => $this, + 'studies' => $this->dbh->select('id, name')->from('survey_studies')->where(array('user_id' => Site::getCurrentUser()->id))->fetchAll(), + 'prepend' => $prepend, + 'resultCount' => $this->id ? $this->howManyReachedItNumbers() : null, + 'time' => $this->id ? $this->getAverageTimeItTakes() : null, + )); + + return parent::runDialog($dialog); + } + + /** + * Merge survey items. Each parameter is an associative array indexed by the names of the items in the survey, with the + * mysql field definition as the value. + * new items are added, old items are deleted, items that changed type are deleted from the results table but not the item_display_table + * All non null entries represent the MySQL data type definition of the fields as they should be in the survey results table + * NOTE: All the DB queries here should be in a transaction of calling function + * + * @param array $newItems + * @param array $deleteItems + * @return bool; + */ + private function alterResultsTable(array $newItems, array $deleteItems) { + $actions = $toAdd = $toDelete = array(); + $deleteQuery = $addQuery = array(); + $addQ = $delQ = null; + + // just for safety checking that there is something to be deleted (in case of aborted earlier tries) + $existingColumns = $this->dbh->getTableDefinition($this->results_table, 'Field'); + + // Create query to drop items in existing table + foreach ($deleteItems as $name => $result_field) { + if ($result_field !== null && isset($existingColumns[$name])) { + $deleteQuery[] = " DROP `{$name}`"; + } + $toDelete[] = $name; + } + // Create query for adding items to existing table + foreach ($newItems as $name => $result_field) { + if ($result_field !== null) { + $addQuery[] = " ADD $result_field"; + } + $toAdd[] = $name; + } + + // prepare these strings for feedback + $added_columns_string = implode($toAdd, ", "); + $deleted_columns_string = implode($toDelete, ", "); + + + // if something should be deleted + if ($deleteQuery) { + $q = "ALTER TABLE `{$this->results_table}`" . implode(',', $deleteQuery); + $this->dbh->query($q); + $actions[] = "Deleted columns: $deleted_columns_string."; + } + + // we only get here if the deletion stuff was harmless, allowed or did not happen + if ($addQuery) { + $q = "ALTER TABLE `{$this->results_table}`" . implode(',', $addQuery); + $this->dbh->query($q); + $actions[] = "Added columns: $added_columns_string."; + } + + if (!empty($actions)) { + $this->messages[] = "The results table was modified."; + $this->messages = array_merge($this->messages, $actions); + } else { + $this->messages[] = "The results table did not need to be modified."; + } + + return true; + } + + public function getOriginalFileName() { + return $this->dbh->findValue('survey_studies', array('id' => $this->id), 'original_file'); + } + + public function getGoogleFileId() { + return $this->dbh->findValue('survey_studies', array('id' => $this->id), 'google_file_id'); + } + + public function getResultsFilter($f = null) { + $filter = array( + 'all' => array( + 'title' => 'Show All', + 'query' => null, + ), + 'incomplete' => array( + 'title' => 'Incomplete', + 'query' => '(%{table}.created <> %{table}.modified or %{table}.modified is null) and %{table}.ended is null', + ), + 'complete' => array( + 'title' => 'Complete', + 'query' => '%{table}.ended is not null', + ), + ); + + return $f !== null ? array_val($filter, $f, null) : $filter; + } + } diff --git a/application/Model/UnitSession.php b/application/Model/UnitSession.php index 1d4458337..8aec57ce2 100644 --- a/application/Model/UnitSession.php +++ b/application/Model/UnitSession.php @@ -2,67 +2,67 @@ class UnitSession { - public $session = null; - public $id; - public $unit_id; - public $created; - public $ended; - public $expired; - public $run_session_id; + public $session = null; + public $id; + public $unit_id; + public $created; + public $ended; + public $expired; + public $run_session_id; - /** - * @var DB - */ - private $dbh; + /** + * @var DB + */ + private $dbh; - public function __construct($fdb, $run_session_id, $unit_id, $unit_session_id = null) { - $this->dbh = $fdb; - $this->unit_id = $unit_id; - $this->run_session_id = $run_session_id; - $this->id = $unit_session_id; + public function __construct($fdb, $run_session_id, $unit_id, $unit_session_id = null) { + $this->dbh = $fdb; + $this->unit_id = $unit_id; + $this->run_session_id = $run_session_id; + $this->id = $unit_session_id; - $this->load(); - } + $this->load(); + } - public function create() { - $now = mysql_now(); - $this->id = $this->dbh->insert('survey_unit_sessions', array( - 'unit_id' => $this->unit_id, - 'run_session_id' => $this->run_session_id, - 'created' => $now - )); - $this->created = $now; - return $this->id; - } + public function create() { + $now = mysql_now(); + $this->id = $this->dbh->insert('survey_unit_sessions', array( + 'unit_id' => $this->unit_id, + 'run_session_id' => $this->run_session_id, + 'created' => $now + )); + $this->created = $now; + return $this->id; + } - public function load() { - if($this->id !== null) { - $vars = $this->dbh->select('id, created, unit_id, run_session_id, ended') - ->from('survey_unit_sessions') - ->where(array('id' => $this->id)) - ->fetch(); - } else { - $vars = $this->dbh->select('id, created, unit_id, run_session_id, ended') - ->from('survey_unit_sessions') - ->where(array('run_session_id' => $this->run_session_id, 'unit_id' => $this->unit_id)) - ->where('ended IS NULL AND expired IS NULL') - ->order('created', 'desc')->limit(1) - ->fetch(); - } + public function load() { + if ($this->id !== null) { + $vars = $this->dbh->select('id, created, unit_id, run_session_id, ended') + ->from('survey_unit_sessions') + ->where(array('id' => $this->id)) + ->fetch(); + } else { + $vars = $this->dbh->select('id, created, unit_id, run_session_id, ended') + ->from('survey_unit_sessions') + ->where(array('run_session_id' => $this->run_session_id, 'unit_id' => $this->unit_id)) + ->where('ended IS NULL AND expired IS NULL') + ->order('created', 'desc')->limit(1) + ->fetch(); + } - if (!$vars) { - return; - } + if (!$vars) { + return; + } - foreach ($vars as $property => $value) { - if (property_exists($this, $property)) { - $this->{$property} = $value; - } - } - } + foreach ($vars as $property => $value) { + if (property_exists($this, $property)) { + $this->{$property} = $value; + } + } + } - public function __sleep() { - return array('id', 'session', 'unit_id', 'created'); - } + public function __sleep() { + return array('id', 'session', 'unit_id', 'created'); + } } diff --git a/application/Model/User.php b/application/Model/User.php index 4156dea44..2424a16ba 100644 --- a/application/Model/User.php +++ b/application/Model/User.php @@ -2,444 +2,439 @@ class User { - public $id = null; - public $email = null; - public $user_code = null; - public $first_name = null; - public $last_name = null; - public $affiliation = null; - public $created = 0; - public $email_verified = 0; - public $settings = array(); - public $errors = array(); - public $messages = array(); - public $cron = false; - - private $logged_in = false; - private $admin = false; - private $referrer_code = null; - // todo: time zone, etc. - - - /** - * @var DB - */ - private $dbh; - - public function __construct($fdb, $id = null, $user_code = null) { - $this->dbh = $fdb; - - if ($id !== null): // if there is a registered, logged in user - $this->id = (int) $id; - $this->load(); // load his stuff - elseif ($user_code !== null): - $this->user_code = $user_code; // if there is someone who has been browsing the site - else: - $this->user_code = crypto_token(48); // a new arrival - endif; - } - - public function __sleep() { - return array('id', 'user_code'); - } - - private function load() { - $user = $this->dbh->select('id, email, password, admin, user_code, referrer_code, first_name, last_name, affiliation, created, email_verified') - ->from('survey_users') - ->where(array('id' => $this->id)) - ->limit(1) - ->fetch(); - - if ($user) { - $this->logged_in = true; - $this->email = $user['email']; - $this->id = (int) $user['id']; - $this->user_code = $user['user_code']; - $this->admin = $user['admin']; - $this->referrer_code = $user['referrer_code']; - $this->first_name = $user['first_name']; - $this->last_name = $user['last_name']; - $this->affiliation = $user['affiliation']; - $this->created = $user['created']; - $this->email_verified = $user['email_verified']; - return true; - } - - return false; - } - - public function loggedIn() { - return $this->logged_in; - } - - public function isCron() { - return $this->cron; - } - - public function isAdmin() { - return $this->admin >= 1; - } - - public function isSuperAdmin() { - return $this->admin >= 10; - } - - public function getAdminLevel() { - return $this->admin; - } - public function getEmail() { - return $this->email; - } - - public function created($object) { - return (int) $this->id === (int) $object->user_id; - } - - public function register($info) { - $email = array_val($info, 'email'); - $password = array_val($info, 'password'); - $referrer_code = array_val($info, 'referrer_code'); - - $hash = password_hash($password, PASSWORD_DEFAULT); - - $user_exists = $this->dbh->entry_exists('survey_users', array('email' => $email)); - if ($user_exists) { - $this->errors[] = 'This e-mail address is already associated to an existing account.'; - return false; - } - - if ($this->user_code === null) { - $this->user_code = crypto_token(48); - } - - $this->referrer_code = $referrer_code; - - if ($hash) : - $inserted = $this->dbh->insert('survey_users', array( - 'email' => $email, - 'created' => mysql_now(), - 'password' => $hash, - 'user_code' => $this->user_code, - 'referrer_code' => $this->referrer_code - )); - - if (!$inserted) { - throw new Exception("Unable create user account"); - } - - //$login = $this->login($email, $password); - $this->email = $email; - $this->id = $inserted; - $this->needToVerifyMail(); - return true; - - else: - alert('Error! Hash error.', 'alert-danger'); - return false; - endif; - } - - public function needToVerifyMail() { - $token = crypto_token(48); - $token_hash = password_hash($token, PASSWORD_DEFAULT); - $this->dbh->update('survey_users', array('email_verification_hash' => $token_hash, 'email_verified' => 0), array('id' => $this->id)); - - $verify_link = site_url('verify_email', array( - 'email' => $this->email, - 'verification_token' => $token - )); - - global $site; - $mail = $site->makeAdminMailer(); - $mail->AddAddress($this->email); - $mail->Subject = 'formr: confirm your email address'; - $mail->Body = Template::get_replace('email/verify-email.txt', array( - 'site_url' => site_url(), - 'documentation_url' => site_url('documentation#help'), - 'verify_link' => $verify_link, - )); - - if (!$mail->Send()) { - alert($mail->ErrorInfo, 'alert-danger'); - } else { - alert("You were sent an email to verify your address.", 'alert-info'); - } - $this->id = null; - } - - public function login($info) { - $email = array_val($info, 'email'); - $password = array_val($info, 'password'); - - $user = $this->dbh->select('id, password, admin, user_code, email_verified, email_verification_hash') - ->from('survey_users') - ->where(array('email' => $email)) - ->limit(1)->fetch(); - - if ($user && !$user['email_verified']) { - $verification_link = site_url('verify-email', array('token' => $user['email_verification_hash'])); - $this->id = $user['id']; - $this->email = $email; - $this->errors[] = sprintf('Please verify your email address by clicking on the verification link that was sent to you, ' - . 'Resend Link', $verification_link); - return false; - } elseif ($user && password_verify($password, $user['password'])) { - if (password_needs_rehash($user['password'], PASSWORD_DEFAULT)) { - $hash = password_hash($password, PASSWORD_DEFAULT); - /* Store new hash in db */ - if ($hash) { - $this->dbh->update('survey_users', array('password' => $hash), array('email' => $email)); - } else { - $this->errors[] = 'An error occurred verifying your password. Please contact site administrators!'; - return false; - } - } - - $this->id = (int)$user['id']; - $this->load(); - return true; - } else { - $this->errors[] = 'Your login credentials were incorrect!'; - return false; - } - } - - public function setAdminLevelTo($level) { - if (!Site::getCurrentUser()->isSuperAdmin()) { - throw new Exception("You need more admin rights to effect this change"); - } - - $level = (int) $level; - if ($level !== 0 && $level !== 1) { - if ($level > 1) { - $level = 1; - } else { - $level = 0; - } - } - - return $this->dbh->update('survey_users', array('admin' => $level), array('id' => $this->id, 'admin <' => 100)); - } - - public function forgot_password($email) { - $user_exists = $this->dbh->entry_exists('survey_users', array('email' => $email)); - - if (!$user_exists): - alert("This email address is not registered here.", "alert-danger"); - return false; - else: - $token = crypto_token(48); - $hash = password_hash($token, PASSWORD_DEFAULT); - - $this->dbh->update('survey_users', array('reset_token_hash' => $hash, 'reset_token_expiry' => mysql_interval('+2 days')), array('email' => $email)); - - $reset_link = site_url('reset_password', array( - 'email' => $email, - 'reset_token' => $token - )); - - global $site; - $mail = $site->makeAdminMailer(); - $mail->AddAddress($email); - $mail->Subject = 'formr: forgot password'; - $mail->Body = Template::get_replace('email/forgot-password.txt', array( - 'site_url' => site_url(), - 'reset_link' => $reset_link, - )); - - if (!$mail->Send()): - alert($mail->ErrorInfo, 'alert-danger'); - else: - alert("You were sent a password reset link.", 'alert-info'); - redirect_to("forgot_password"); - endif; - - endif; - } - - function logout() { - $this->logged_in = false; - Session::destroy(); - } - - public function changePassword($password, $new_password) { - if (!$this->login($this->email, $password)) { - $this->errors = array('The old password you entered is not correct.'); - return false; - } - - $hash = password_hash($new_password, PASSWORD_DEFAULT); - /* Store new hash in db */ - if ($hash) { - $this->dbh->update('survey_users', array('password' => $hash), array('email' => $this->email)); - return true; - } else { - $this->errors[] = 'Unable to generate new password'; - return false; - } - } - - public function changeData($password, $data) { - if (!$this->login($this->email, $password)) { - $this->errors = array('The old password you entered is not correct.'); - return false; - } - - $verificationRequired = false; - $update = array(); - $update['email'] = array_val($data, 'new_email'); - $update['first_name'] = array_val($data, 'first_name'); - $update['last_name'] = array_val($data, 'last_name'); - $update['affiliation'] = array_val($data, 'affiliation'); - - if (!$update['email']) { - $this->errors[] = 'Please provide a valid email address'; - return false; - }elseif ($update['email'] !== $this->email) { - $verificationRequired = true; - $update['email_verified'] = 0; - // check if email already exists - $exists = $this->dbh->entry_exists('survey_users', array('email' => $update['email'])); - if ($exists) { - $this->errors[] = 'The provided email address is already in use!'; - return false; - } - } - - $this->dbh->update('survey_users', $update, array('id' => $this->id)); - $this->email = $update['email']; - if ($verificationRequired) { - $this->needToVerifyMail(); - } - $this->load(); - return true; - } - - public function reset_password($info) { - $email = array_val($info, 'email'); - $token = array_val($info, 'reset_token'); - $new_password = array_val($info, 'new_password'); - $new_password_confirm = array_val($info, 'new_password_confirm'); - - if ($new_password !== $new_password_confirm) { - alert('The passwords you entered do not match', 'alert-danger'); - return false; - } - - $reset_token_hash = $this->dbh->findValue('survey_users', array('email' => $email), array('reset_token_hash')); - - if ($reset_token_hash) { - if (password_verify($token, $reset_token_hash)) { - $password_hash = password_hash($new_password, PASSWORD_DEFAULT); - $this->dbh->update('survey_users', - array('password' => $password_hash, 'reset_token_hash' => null, 'reset_token_expiry' => null), - array('email' => $email), - array('str', 'int', 'int') - ); - $login_anchor = 'login'; - alert("Your password was successfully changed. You can now use it to {$login_anchor}.", 'alert-success'); - return true; - } - } - - alert("Incorrect token or email address.", "alert-danger"); - return false; - } - - public function verify_email($email, $token) { - $verify_data = $this->dbh->findRow('survey_users', array('email' => $email), array('email_verification_hash', 'referrer_code')); - if (!$verify_data) { - alert('Incorrect token or email address.', 'alert-danger'); - return false; - } - - if (password_verify($token, $verify_data['email_verification_hash'])) { - $this->dbh->update('survey_users', - array('email_verification_hash' => null, 'email_verified' => 1), - array('email' => $email), - array('int', 'int') - ); - alert('Your email was successfully verified!', 'alert-success'); - - if(in_array($verify_data['referrer_code'], Config::get('referrer_codes'))) { - $this->dbh->update('survey_users', - array('admin' => 1), - array('email' => $email) - ); - alert('You now have the rights to create your own studies!', 'alert-success'); - } - return true; - } else { - alert('Your email verification token was invalid or oudated. Please try copy-pasting the link in your email and removing any spaces.', 'alert-danger'); - return false; - } - } - - public function resendVerificationEmail($verificationHash) { - $verify_data = $this->dbh->findRow('survey_users', array('email_verification_hash' => $verificationHash), array('id', 'email_verification_hash', 'email')); - if (!$verify_data) { - alert('Incorrect token.', 'alert-danger'); - return false; - } - - $this->id = (int)$verify_data['id']; - $this->email = $verify_data['email']; - $this->needToVerifyMail(); - $this->id = $this->email = null; - return true; - } - - public function getStudies($order = 'id DESC', $limit = null) { - if ($this->isAdmin()) { - $select = $this->dbh->select(); - $select->from('survey_studies'); - $select->order($order, null); - if ($limit) { - $select->limit($limit); - } - $select->where(array('user_id' => $this->id)); - return $select->fetchAll(); - } - return array(); - } - - public function getEmailAccounts() { - if ($this->isAdmin()): - $accs = $this->dbh->find('survey_email_accounts', array('user_id' => $this->id, 'deleted' => 0), array('cols' => 'id, from')); - - $results = array(); - foreach ($accs as $acc) { - if ($acc['from'] == null) { - $acc['from'] = 'New.'; - } - $results[] = $acc; - } - return $results; - endif; - - return false; - } - - public function getRuns($order = 'id DESC', $limit = null) { - if ($this->isAdmin()) { - $select = $this->dbh->select(); - $select->from('survey_runs'); - $select->order($order, null); - if ($limit) { - $select->limit($limit); - } - $select->where(array('user_id' => $this->id)); - return $select->fetchAll(); - } - return array(); - } - - public function getAvailableRuns() { - return $this->dbh->select('name,title, public_blurb_parsed') - ->from('survey_runs') - ->where('public > 2') - ->fetchAll(); - } - - + public $id = null; + public $email = null; + public $user_code = null; + public $first_name = null; + public $last_name = null; + public $affiliation = null; + public $created = 0; + public $email_verified = 0; + public $settings = array(); + public $errors = array(); + public $messages = array(); + public $cron = false; + private $logged_in = false; + private $admin = false; + private $referrer_code = null; + // todo: time zone, etc. + + /** + * @var DB + */ + private $dbh; + + public function __construct($fdb, $id = null, $user_code = null) { + $this->dbh = $fdb; + + if ($id !== null): // if there is a registered, logged in user + $this->id = (int) $id; + $this->load(); // load his stuff + elseif ($user_code !== null): + $this->user_code = $user_code; // if there is someone who has been browsing the site + else: + $this->user_code = crypto_token(48); // a new arrival + endif; + } + + public function __sleep() { + return array('id', 'user_code'); + } + + private function load() { + $user = $this->dbh->select('id, email, password, admin, user_code, referrer_code, first_name, last_name, affiliation, created, email_verified') + ->from('survey_users') + ->where(array('id' => $this->id)) + ->limit(1) + ->fetch(); + + if ($user) { + $this->logged_in = true; + $this->email = $user['email']; + $this->id = (int) $user['id']; + $this->user_code = $user['user_code']; + $this->admin = $user['admin']; + $this->referrer_code = $user['referrer_code']; + $this->first_name = $user['first_name']; + $this->last_name = $user['last_name']; + $this->affiliation = $user['affiliation']; + $this->created = $user['created']; + $this->email_verified = $user['email_verified']; + return true; + } + + return false; + } + + public function loggedIn() { + return $this->logged_in; + } + + public function isCron() { + return $this->cron; + } + + public function isAdmin() { + return $this->admin >= 1; + } + + public function isSuperAdmin() { + return $this->admin >= 10; + } + + public function getAdminLevel() { + return $this->admin; + } + + public function getEmail() { + return $this->email; + } + + public function created($object) { + return (int) $this->id === (int) $object->user_id; + } + + public function register($info) { + $email = array_val($info, 'email'); + $password = array_val($info, 'password'); + $referrer_code = array_val($info, 'referrer_code'); + + $hash = password_hash($password, PASSWORD_DEFAULT); + + $user_exists = $this->dbh->entry_exists('survey_users', array('email' => $email)); + if ($user_exists) { + $this->errors[] = 'This e-mail address is already associated to an existing account.'; + return false; + } + + if ($this->user_code === null) { + $this->user_code = crypto_token(48); + } + + $this->referrer_code = $referrer_code; + + if ($hash) : + $inserted = $this->dbh->insert('survey_users', array( + 'email' => $email, + 'created' => mysql_now(), + 'password' => $hash, + 'user_code' => $this->user_code, + 'referrer_code' => $this->referrer_code + )); + + if (!$inserted) { + throw new Exception("Unable create user account"); + } + + //$login = $this->login($email, $password); + $this->email = $email; + $this->id = $inserted; + $this->needToVerifyMail(); + return true; + + else: + alert('Error! Hash error.', 'alert-danger'); + return false; + endif; + } + + public function needToVerifyMail() { + $token = crypto_token(48); + $token_hash = password_hash($token, PASSWORD_DEFAULT); + $this->dbh->update('survey_users', array('email_verification_hash' => $token_hash, 'email_verified' => 0), array('id' => $this->id)); + + $verify_link = site_url('verify_email', array( + 'email' => $this->email, + 'verification_token' => $token + )); + + global $site; + $mail = $site->makeAdminMailer(); + $mail->AddAddress($this->email); + $mail->Subject = 'formr: confirm your email address'; + $mail->Body = Template::get_replace('email/verify-email.txt', array( + 'site_url' => site_url(), + 'documentation_url' => site_url('documentation#help'), + 'verify_link' => $verify_link, + )); + + if (!$mail->Send()) { + alert($mail->ErrorInfo, 'alert-danger'); + } else { + alert("You were sent an email to verify your address.", 'alert-info'); + } + $this->id = null; + } + + public function login($info) { + $email = array_val($info, 'email'); + $password = array_val($info, 'password'); + + $user = $this->dbh->select('id, password, admin, user_code, email_verified, email_verification_hash') + ->from('survey_users') + ->where(array('email' => $email)) + ->limit(1)->fetch(); + + if ($user && !$user['email_verified']) { + $verification_link = site_url('verify-email', array('token' => $user['email_verification_hash'])); + $this->id = $user['id']; + $this->email = $email; + $this->errors[] = sprintf('Please verify your email address by clicking on the verification link that was sent to you, ' + . 'Resend Link', $verification_link); + return false; + } elseif ($user && password_verify($password, $user['password'])) { + if (password_needs_rehash($user['password'], PASSWORD_DEFAULT)) { + $hash = password_hash($password, PASSWORD_DEFAULT); + /* Store new hash in db */ + if ($hash) { + $this->dbh->update('survey_users', array('password' => $hash), array('email' => $email)); + } else { + $this->errors[] = 'An error occurred verifying your password. Please contact site administrators!'; + return false; + } + } + + $this->id = (int) $user['id']; + $this->load(); + return true; + } else { + $this->errors[] = 'Your login credentials were incorrect!'; + return false; + } + } + + public function setAdminLevelTo($level) { + if (!Site::getCurrentUser()->isSuperAdmin()) { + throw new Exception("You need more admin rights to effect this change"); + } + + $level = (int) $level; + if ($level !== 0 && $level !== 1) { + if ($level > 1) { + $level = 1; + } else { + $level = 0; + } + } + + return $this->dbh->update('survey_users', array('admin' => $level), array('id' => $this->id, 'admin <' => 100)); + } + + public function forgot_password($email) { + $user_exists = $this->dbh->entry_exists('survey_users', array('email' => $email)); + + if (!$user_exists): + alert("This email address is not registered here.", "alert-danger"); + return false; + else: + $token = crypto_token(48); + $hash = password_hash($token, PASSWORD_DEFAULT); + + $this->dbh->update('survey_users', array('reset_token_hash' => $hash, 'reset_token_expiry' => mysql_interval('+2 days')), array('email' => $email)); + + $reset_link = site_url('reset_password', array( + 'email' => $email, + 'reset_token' => $token + )); + + global $site; + $mail = $site->makeAdminMailer(); + $mail->AddAddress($email); + $mail->Subject = 'formr: forgot password'; + $mail->Body = Template::get_replace('email/forgot-password.txt', array( + 'site_url' => site_url(), + 'reset_link' => $reset_link, + )); + + if (!$mail->Send()): + alert($mail->ErrorInfo, 'alert-danger'); + else: + alert("You were sent a password reset link.", 'alert-info'); + redirect_to("forgot_password"); + endif; + + endif; + } + + function logout() { + $this->logged_in = false; + Session::destroy(); + } + + public function changePassword($password, $new_password) { + if (!$this->login($this->email, $password)) { + $this->errors = array('The old password you entered is not correct.'); + return false; + } + + $hash = password_hash($new_password, PASSWORD_DEFAULT); + /* Store new hash in db */ + if ($hash) { + $this->dbh->update('survey_users', array('password' => $hash), array('email' => $this->email)); + return true; + } else { + $this->errors[] = 'Unable to generate new password'; + return false; + } + } + + public function changeData($password, $data) { + if (!$this->login($this->email, $password)) { + $this->errors = array('The old password you entered is not correct.'); + return false; + } + + $verificationRequired = false; + $update = array(); + $update['email'] = array_val($data, 'new_email'); + $update['first_name'] = array_val($data, 'first_name'); + $update['last_name'] = array_val($data, 'last_name'); + $update['affiliation'] = array_val($data, 'affiliation'); + + if (!$update['email']) { + $this->errors[] = 'Please provide a valid email address'; + return false; + } elseif ($update['email'] !== $this->email) { + $verificationRequired = true; + $update['email_verified'] = 0; + // check if email already exists + $exists = $this->dbh->entry_exists('survey_users', array('email' => $update['email'])); + if ($exists) { + $this->errors[] = 'The provided email address is already in use!'; + return false; + } + } + + $this->dbh->update('survey_users', $update, array('id' => $this->id)); + $this->email = $update['email']; + if ($verificationRequired) { + $this->needToVerifyMail(); + } + $this->load(); + return true; + } + + public function reset_password($info) { + $email = array_val($info, 'email'); + $token = array_val($info, 'reset_token'); + $new_password = array_val($info, 'new_password'); + $new_password_confirm = array_val($info, 'new_password_confirm'); + + if ($new_password !== $new_password_confirm) { + alert('The passwords you entered do not match', 'alert-danger'); + return false; + } + + $reset_token_hash = $this->dbh->findValue('survey_users', array('email' => $email), array('reset_token_hash')); + + if ($reset_token_hash) { + if (password_verify($token, $reset_token_hash)) { + $password_hash = password_hash($new_password, PASSWORD_DEFAULT); + $this->dbh->update( + 'survey_users', + array('password' => $password_hash, 'reset_token_hash' => null, 'reset_token_expiry' => null), + array('email' => $email), + array('str', 'int', 'int') + ); + + $login_anchor = 'login'; + alert("Your password was successfully changed. You can now use it to {$login_anchor}.", 'alert-success'); + return true; + } + } + + alert("Incorrect token or email address.", "alert-danger"); + return false; + } + + public function verify_email($email, $token) { + $verify_data = $this->dbh->findRow('survey_users', array('email' => $email), array('email_verification_hash', 'referrer_code')); + if (!$verify_data) { + alert('Incorrect token or email address.', 'alert-danger'); + return false; + } + + if (password_verify($token, $verify_data['email_verification_hash'])) { + $this->dbh->update('survey_users', array('email_verification_hash' => null, 'email_verified' => 1), array('email' => $email), array('int', 'int') + ); + alert('Your email was successfully verified!', 'alert-success'); + + if (in_array($verify_data['referrer_code'], Config::get('referrer_codes'))) { + $this->dbh->update('survey_users', array('admin' => 1), array('email' => $email) + ); + alert('You now have the rights to create your own studies!', 'alert-success'); + } + return true; + } else { + alert('Your email verification token was invalid or oudated. Please try copy-pasting the link in your email and removing any spaces.', 'alert-danger'); + return false; + } + } + + public function resendVerificationEmail($verificationHash) { + $verify_data = $this->dbh->findRow('survey_users', array('email_verification_hash' => $verificationHash), array('id', 'email_verification_hash', 'email')); + if (!$verify_data) { + alert('Incorrect token.', 'alert-danger'); + return false; + } + + $this->id = (int) $verify_data['id']; + $this->email = $verify_data['email']; + $this->needToVerifyMail(); + $this->id = $this->email = null; + return true; + } + + public function getStudies($order = 'id DESC', $limit = null) { + if ($this->isAdmin()) { + $select = $this->dbh->select(); + $select->from('survey_studies'); + $select->order($order, null); + if ($limit) { + $select->limit($limit); + } + $select->where(array('user_id' => $this->id)); + return $select->fetchAll(); + } + return array(); + } + + public function getEmailAccounts() { + if ($this->isAdmin()): + $accs = $this->dbh->find('survey_email_accounts', array('user_id' => $this->id, 'deleted' => 0), array('cols' => 'id, from')); + + $results = array(); + foreach ($accs as $acc) { + if ($acc['from'] == null) { + $acc['from'] = 'New.'; + } + $results[] = $acc; + } + return $results; + endif; + + return false; + } + + public function getRuns($order = 'id DESC', $limit = null) { + if ($this->isAdmin()) { + $select = $this->dbh->select(); + $select->from('survey_runs'); + $select->order($order, null); + if ($limit) { + $select->limit($limit); + } + $select->where(array('user_id' => $this->id)); + return $select->fetchAll(); + } + return array(); + } + + public function getAvailableRuns() { + return $this->dbh->select('name,title, public_blurb_parsed') + ->from('survey_runs') + ->where('public > 2') + ->fetchAll(); + } + } diff --git a/application/View/admin/header.php b/application/View/admin/header.php index 7a370c7bf..0de1fc1be 100755 --- a/application/View/admin/header.php +++ b/application/View/admin/header.php @@ -6,21 +6,21 @@ formr admin - $files) { - print_stylesheets($files, $id); - } - foreach ($js as $id => $files) { - print_scripts($files, $id); - } - ?> + $files) { + print_stylesheets($files, $id); + } + foreach ($js as $id => $files) { + print_scripts($files, $id); + } + ?> - getRuns('id DESC', 5); - $studies = $user->getStudies('id DESC', 5); - } - ?> + getRuns('id DESC', 5); + $studies = $user->getStudies('id DESC', 5); + } + ?> @@ -44,16 +44,16 @@
  • Mail Accounts
  • @@ -79,17 +79,17 @@
  • Github repository
  • R package on Github
  • isSuperAdmin()): ?> -
  • cron log
  • -
  • manage users
  • -
  • active users
  • -
  • manage runs
  • - - +
  • cron log
  • +
  • manage users
  • +
  • active users
  • +
  • manage runs
  • + +
    -
    +
    diff --git a/application/View/admin/home.php b/application/View/admin/home.php index 295f72144..6cb1e1593 100755 --- a/application/View/admin/home.php +++ b/application/View/admin/home.php @@ -1,172 +1,172 @@
    -
    -

    Dashboard Quick Links

    -
    +
    +

    Dashboard Quick Links

    +
    -
    +
    - - + +
    + + +
    +
    +
    +
    +
    +

    Recent Runs

    +
    + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + +
    # IDNameCreatedStatusCronLock
    # PRIVATE'; + } elseif ($d_run['public'] == 1) { + echo 'ACCESS CODE ONLY'; + } elseif ($d_run['public'] == 2) { + echo 'LINK ONLY'; + } elseif ($d_run['public'] == 3) { + echo 'PUBLIC'; + } + ?>
    +
    + +
    + +
    +
    +
    +
    +
    +

    Recent Surveys

    +
    +
    +
    + + + + + + + + + + + + + + + + + + + + + +
    # IDNameCreatedModifiedGoogle Sheet
    # + + ... + +
    +
    + +
    + +
    + +
    +
    +
    -
    - - -
    -
    -
    -
    -
    -

    Recent Runs

    -
    - -
    -
    - - - - - - - - - - - - - - - - - - - - - - - -
    # IDNameCreatedStatusCronLock
    # PRIVATE'; - } elseif ($d_run['public'] == 1) { - echo 'ACCESS CODE ONLY'; - } elseif ($d_run['public'] == 2) { - echo 'LINK ONLY'; - } elseif ($d_run['public'] == 3) { - echo 'PUBLIC'; - } - ?>
    -
    - -
    - -
    -
    -
    -
    -
    -

    Recent Surveys

    -
    -
    -
    - - - - - - - - - - - - - - - - - - - - - -
    # IDNameCreatedModifiedGoogle Sheet
    # - - ... - -
    -
    - -
    - -
    -
    -
    -
    - -
    - \ No newline at end of file + \ No newline at end of file diff --git a/application/View/admin/mail/edit.php b/application/View/admin/mail/edit.php index 7992c5281..b834873ea 100644 --- a/application/View/admin/mail/edit.php +++ b/application/View/admin/mail/edit.php @@ -3,79 +3,79 @@ Template::load('acp_nav'); ?>
    -

    edit email account

    -
    -
    - -
    - -
    -
    +

    edit email account

    + +
    + +
    + +
    +
    -
    - -
    - -
    -
    +
    + +
    + +
    +
    -
    - -
    - -
    -
    +
    + +
    + +
    +
    -
    - -
    - -
    -
    +
    + +
    + +
    +
    -
    - -
    - - account['tls']) ? 'checked' : ''; ?>> -
    -
    +
    + +
    + + account['tls']) ? 'checked' : ''; ?>> +
    +
    -
    - -
    - -
    -
    +
    + +
    + +
    +
    -
    - -
    - -
    -
    +
    + +
    + +
    +
    -
    -
    - "> - -
    -
    -
    +
    +
    + "> + +
    +
    +
    - -
    -

    E-Mail Accounts

    -
    + +
    +

    E-Mail Accounts

    +
    - -
    -
    - -
    + +
    +
    + +
    -
    -
    -

    Current Accounts

    -
    - -
    -
    -
    - - - -
    - -
    - Create New Account - - -
    +
    +
    +

    Current Accounts

    +
    + +
    +
    +
    + + + +
    + +
    + Create New Account -
    -
    -
    -

    -
    -
    -
    - -
    - -
    - -
    -
    -
    - -
    - -
    -
    -
    - -
    - -
    -
    -
    - -
    - -
    -
    -
    - -
    -
    - -
    -
    -
    + +
    +
    +
    +
    +

    +
    + +
    + +
    + +
    + +
    +
    +
    + +
    + +
    +
    +
    + +
    + +
    +
    +
    + +
    + +
    +
    +
    + +
    +
    + +
    +
    +
    -
    - -
    - -
    -
    -
    - -
    - -
    -
    -
    -
    - -
    -
    - -
    -
    -
    -
    - -
    - -
    -
    -
    - - +
    + +
    + +
    +
    +
    + +
    + +
    +
    +
    +
    + +
    +
    + +
    +
    +
    +
    + +
    + +
    +
    -
    -
    -
    +
    + + -
    - +
    +
    +
    + + + diff --git a/application/View/admin/misc/info.php b/application/View/admin/misc/info.php index f35a8efd6..83f154933 100644 --- a/application/View/admin/misc/info.php +++ b/application/View/admin/misc/info.php @@ -1,3 +1,3 @@
    - -
    -

    Open Science Framework «» FORMR actions

    -
    + +
    +

    Open Science Framework «» FORMR actions

    +
    - -
    -
    -
    -

    - Export run structure to OSF project -

    + +
    +
    + -
    -
    -
    - - - - - - - - - - - - - - - - - -
    Fromr Projects OSF Projects 
    -
    - -
    - -
    -
    -

    - Create an formr project (run) -

    -
    -
    - -
    - -
    -
    -

    - Create an OSF project -

    -
    - - -
    -
    -
    -
    -
    -
    - +
    +
    +
    +
    + + + + + + + + + + + + + + + + + +
    Fromr Projects OSF Projects 
    +
    + +
    + +
    +
    +

    + Create an formr project (run) +

    +
    +
    + +
    + +
    +
    +

    + Create an OSF project +

    +
    + + +
    +
    +
    +
    +
    +
    +
    diff --git a/application/View/admin/misc/test_opencpu.php b/application/View/admin/misc/test_opencpu.php index ce4157a90..0123d0e5a 100644 --- a/application/View/admin/misc/test_opencpu.php +++ b/application/View/admin/misc/test_opencpu.php @@ -1,4 +1,5 @@ OpenCPU test"; -echo '
    testing '.Config::get('alternative_opencpu_instance').'
    '; +echo '
    testing ' . Config::get('alternative_opencpu_instance') . '
    '; $source = '{ library(knitr) knit2html(text = "' . addslashes("__Hello__ World `r 1` ```{r} -# ".$nocache." +# " . $nocache . " library(ggplot2) qplot(rnorm(1000)) qplot(rnorm(10000), rnorm(10000)) @@ -27,14 +28,14 @@ }'; $before = microtime(true); -$results = $openCPU->identity(array('x' => $source),'', true); +$results = $openCPU->identity(array('x' => $source), '', true); $alert_type = 'alert-success'; -if($openCPU->http_status > 302 OR $openCPU->http_status === 0) { - $alert_type = 'alert-danger'; +if ($openCPU->http_status > 302 OR $openCPU->http_status === 0) { + $alert_type = 'alert-danger'; } -alert("1. HTTP status: ".$openCPU->http_status,$alert_type); +alert("1. HTTP status: " . $openCPU->http_status, $alert_type); $accordion = $openCPU->debugCall($results); @@ -49,14 +50,14 @@ rnorm(10) }'; $before = microtime(true); -$results = $openCPU->identity(array('x' => $source),'', true); +$results = $openCPU->identity(array('x' => $source), '', true); $alert_type = 'alert-success'; -if($openCPU->http_status > 302 OR $openCPU->http_status === 0) { - $alert_type = 'alert-danger'; +if ($openCPU->http_status > 302 OR $openCPU->http_status === 0) { + $alert_type = 'alert-danger'; } -alert("2. HTTP status: ".$openCPU->http_status,$alert_type); +alert("2. HTTP status: " . $openCPU->http_status, $alert_type); $accordion2 = $openCPU->debugCall($results); alert("2. Request took " . round((microtime(true) - $before) / 60, 4) . " minutes", 'alert-info'); @@ -65,7 +66,7 @@ $openCPU = new OpenCPU(Config::get('alternative_opencpu_instance')); $source = '{ (function() { -# '.$nocache.' +# ' . $nocache . ' survey_unit_sessions = as.data.frame(jsonlite::fromJSON("{\"session\":[\"2uy43834873467gejdsgdfujgcv8\",\"2uy43834873467gejdsgdfujgcv8\",\"2uy43834873467gejdsgdfujgcv8\",\"2uy43834873467gejdsgdfujgcv8\",\"2uy43834873467gejdsgdfujgcv8\",\"2uy43834873467gejdsgdfujgcv8\",\"2uy43834873467gejdsgdfujgcv8\",\"2uy43834873467gejdsgdfujgcv8\",\"2uy43834873467gejdsgdfujgcv8\",\"2uy43834873467gejdsgdfujgcv8\",\"2uy43834873467gejdsgdfujgcv8\",\"2uy43834873467gejdsgdfujgcv8\",\"2uy43834873467gejdsgdfujgcv8\",\"2uy43834873467gejdsgdfujgcv8\",\"2uy43834873467gejdsgdfujgcv8\",\"2uy43834873467gejdsgdfujgcv8\",\"2uy43834873467gejdsgdfujgcv8\",\"2uy43834873467gejdsgdfujgcv8\",\"2uy43834873467gejdsgdfujgcv8\",\"2uy43834873467gejdsgdfujgcv8\",\"2uy43834873467gejdsgdfujgcv8\",\"2uy43834873467gejdsgdfujgcv8\",\"2uy43834873467gejdsgdfujgcv8\",\"2uy43834873467gejdsgdfujgcv8\",\"2uy43834873467gejdsgdfujgcv8\",\"2uy43834873467gejdsgdfujgcv8\",\"2uy43834873467gejdsgdfujgcv8\",\"2uy43834873467gejdsgdfujgcv8\",\"2uy43834873467gejdsgdfujgcv8\",\"2uy43834873467gejdsgdfujgcv8\",\"2uy43834873467gejdsgdfujgcv8\",\"2uy43834873467gejdsgdfujgcv8\",\"2uy43834873467gejdsgdfujgcv8\",\"2uy43834873467gejdsgdfujgcv8\",\"2uy43834873467gejdsgdfujgcv8\",\"2uy43834873467gejdsgdfujgcv8\",\"2uy43834873467gejdsgdfujgcv8\",\"2uy43834873467gejdsgdfujgcv8\",\"2uy43834873467gejdsgdfujgcv8\",\"2uy43834873467gejdsgdfujgcv8\",\"2uy43834873467gejdsgdfujgcv8\",\"2uy43834873467gejdsgdfujgcv8\",\"2uy43834873467gejdsgdfujgcv8\",\"2uy43834873467gejdsgdfujgcv8\",\"2uy43834873467gejdsgdfujgcv8\",\"2uy43834873467gejdsgdfujgcv8\",\"2uy43834873467gejdsgdfujgcv8\",\"2uy43834873467gejdsgdfujgcv8\",\"2uy43834873467gejdsgdfujgcv8\",\"2uy43834873467gejdsgdfujgcv8\",\"2uy43834873467gejdsgdfujgcv8\",\"2uy43834873467gejdsgdfujgcv8\",\"2uy43834873467gejdsgdfujgcv8\",\"2uy43834873467gejdsgdfujgcv8\",\"2uy43834873467gejdsgdfujgcv8\",\"2uy43834873467gejdsgdfujgcv8\",\"2uy43834873467gejdsgdfujgcv8\",\"2uy43834873467gejdsgdfujgcv8\",\"2uy43834873467gejdsgdfujgcv8\",\"2uy43834873467gejdsgdfujgcv8\",\"2uy43834873467gejdsgdfujgcv8\",\"2uy43834873467gejdsgdfujgcv8\",\"2uy43834873467gejdsgdfujgcv8\",\"2uy43834873467gejdsgdfujgcv8\",\"2uy43834873467gejdsgdfujgcv8\",\"2uy43834873467gejdsgdfujgcv8\",\"2uy43834873467gejdsgdfujgcv8\",\"2uy43834873467gejdsgdfujgcv8\",\"2uy43834873467gejdsgdfujgcv8\",\"2uy43834873467gejdsgdfujgcv8\",\"2uy43834873467gejdsgdfujgcv8\",\"2uy43834873467gejdsgdfujgcv8\",\"2uy43834873467gejdsgdfujgcv8\",\"2uy43834873467gejdsgdfujgcv8\",\"2uy43834873467gejdsgdfujgcv8\",\"2uy43834873467gejdsgdfujgcv8\",\"2uy43834873467gejdsgdfujgcv8\",\"2uy43834873467gejdsgdfujgcv8\",\"2uy43834873467gejdsgdfujgcv8\",\"2uy43834873467gejdsgdfujgcv8\",\"2uy43834873467gejdsgdfujgcv8\"],\"id\":[1420,1422,1423,1427,1428,1432,1433,1434,1435,1436,1437,1439,1440,1442,1443,1445,1446,1465,1466,1468,1469,1470,1479,1480,1481,1483,1484,1485,1486,1494,1495,1496,1497,1539,1540,1541,1542,1543,1544,1545,1547,1574,1575,1576,1577,1578,1579,1580,1581,1582,1583,1584,1585,1586,1587,1591,1592,1596,1597,1598,1599,1600,1601,1602,1603,1604,1766,1916,1920,1921,1922,1923,1924,1925,1926,1927,1928,1929,1930,1931,1932],\"unit_id\":[224,224,225,224,225,224,225,225,221,222,223,224,225,222,223,224,225,221,222,224,221,222,221,222,223,224,221,222,223,224,221,222,223,225,225,225,224,221,222,223,224,224,221,222,224,221,222,221,222,221,222,221,222,221,221,224,220,224,221,222,227,229,228,227,229,228,227,229,221,222,223,219,224,221,222,223,219,224,220,220,226],\"run_session_id\":[472,472,472,472,472,472,472,472,472,472,472,472,472,472,472,472,472,472,472,472,472,472,472,472,472,472,472,472,472,472,472,472,472,472,472,472,472,472,472,472,472,472,472,472,472,472,472,472,472,472,472,472,472,472,472,472,472,472,472,472,472,472,472,472,472,472,472,472,472,472,472,472,472,472,472,472,472,472,472,472,472],\"created\":[\"2011-01-04 19:40:45\",\"2011-01-04 19:43:16\",\"2011-01-04 19:43:17\",\"2011-01-04 19:46:26\",\"2011-01-04 19:46:28\",\"2011-01-04 19:59:45\",\"2011-01-04 19:59:45\",\"2011-01-04 20:00:34\",\"2011-01-04 20:00:45\",\"2011-01-04 20:00:48\",\"2011-01-04 20:00:53\",\"2011-01-04 20:02:26\",\"2011-01-04 20:02:27\",\"2011-01-04 20:02:51\",\"2011-01-04 20:02:58\",\"2011-01-04 20:04:05\",\"2011-01-04 20:04:06\",\"2011-01-04 21:08:32\",\"2011-01-04 21:08:32\",\"2011-01-04 21:11:11\",\"2011-01-04 21:11:12\",\"2011-01-04 21:11:12\",\"2011-01-04 21:27:41\",\"2011-01-04 21:27:41\",\"2011-01-04 21:27:45\",\"2011-01-04 21:29:13\",\"2011-01-04 21:29:14\",\"2011-01-04 21:29:14\",\"2011-01-04 21:29:17\",\"2011-01-04 21:30:56\",\"2011-01-04 21:30:57\",\"2011-01-04 21:30:57\",\"2011-01-04 21:31:01\",\"2011-01-05 14:00:17\",\"2011-01-05 14:00:35\",\"2011-01-05 14:02:00\",\"2011-01-05 14:25:11\",\"2011-01-05 14:25:15\",\"2011-01-05 14:26:12\",\"2011-01-05 14:26:21\",\"2011-01-05 14:31:26\",\"2011-01-05 18:16:15\",\"2011-01-05 18:16:16\",\"2011-01-05 18:16:16\",\"2011-01-05 18:16:53\",\"2011-01-05 18:16:56\",\"2011-01-05 18:16:56\",\"2011-01-05 18:17:00\",\"2011-01-05 18:17:02\",\"2011-01-05 18:10:57\",\"2011-01-05 18:21:02\",\"2011-01-05 18:21:39\",\"2011-01-05 18:23:09\",\"2011-01-05 18:23:19\",\"2011-01-05 18:37:00\",\"2011-01-05 18:48:57\",\"2011-01-05 18:49:00\",\"2011-01-05 19:34:52\",\"2011-01-05 19:34:54\",\"2011-01-05 19:34:54\",\"2011-01-05 19:35:23\",\"2011-01-05 19:35:36\",\"2011-01-05 19:35:36\",\"2011-01-05 19:37:34\",\"2011-01-05 19:37:47\",\"2011-01-05 19:37:47\",\"2011-01-08 10:01:38\",\"2011-01-09 19:34:11\",\"2011-01-09 19:40:35\",\"2011-01-09 19:40:35\",\"2011-01-09 19:40:38\",\"2011-01-09 19:40:39\",\"2011-01-09 19:44:08\",\"2011-01-09 19:44:09\",\"2011-01-09 19:44:09\",\"2011-01-09 19:44:12\",\"2011-01-09 19:44:13\",\"2011-01-09 19:45:33\",\"2011-01-09 19:45:34\",\"2011-01-09 19:45:34\",\"2011-01-09 19:47:36\"],\"ended\":[\"2011-01-04 19:41:36\",\"2011-01-04 19:43:16\",\"2011-01-04 19:44:30\",\"2011-01-04 19:46:28\",\"2011-01-04 19:56:57\",\"2011-01-04 19:59:45\",\"2011-01-04 20:00:34\",\"2011-01-04 20:00:45\",\"2011-01-04 20:00:48\",\"2011-01-04 20:00:53\",\"2011-01-04 20:00:53\",\"2011-01-04 20:02:27\",\"2011-01-04 20:02:37\",\"2011-01-04 20:02:58\",\"2011-01-04 20:02:58\",\"2011-01-04 20:04:06\",\"2011-01-04 20:58:29\",\"2011-01-04 21:08:32\",\"2011-01-04 21:09:10\",\"2011-01-04 21:11:12\",\"2011-01-04 21:11:12\",\"2011-01-04 21:24:17\",\"2011-01-04 21:27:41\",\"2011-01-04 21:27:45\",\"2011-01-04 21:27:46\",\"2011-01-04 21:29:14\",\"2011-01-04 21:29:14\",\"2011-01-04 21:29:17\",\"2011-01-04 21:29:18\",\"2011-01-04 21:30:57\",\"2011-01-04 21:30:57\",\"2011-01-04 21:31:01\",\"2011-01-04 21:31:02\",\"2011-01-05 14:00:35\",\"2011-01-05 14:02:00\",\"2011-01-05 14:25:11\",\"2011-01-05 14:25:15\",\"2011-01-05 14:26:12\",\"2011-01-05 14:26:21\",\"2011-01-05 14:26:21\",\"2011-01-05 17:38:17\",\"2011-01-05 18:16:16\",\"2011-01-05 18:16:16\",\"2011-01-05 18:16:53\",\"2011-01-05 18:16:56\",\"2011-01-05 18:16:56\",\"2011-01-05 18:17:00\",\"2011-01-05 18:17:02\",\"2011-01-05 18:10:57\",\"2011-01-05 18:21:02\",\"2011-01-05 18:21:39\",\"2011-01-05 18:23:09\",\"2011-01-05 18:23:19\",\"2011-01-05 18:37:00\",\"2011-01-05 18:47:09\",\"2011-01-05 18:49:00\",\"2011-01-05 19:33:50\",\"2011-01-05 19:34:54\",\"2011-01-05 19:34:54\",\"2011-01-05 19:35:23\",\"2011-01-05 19:35:36\",\"2011-01-05 19:35:36\",\"2011-01-05 19:37:34\",\"2011-01-05 19:37:47\",\"2011-01-05 19:37:47\",\"2011-01-08 10:01:38\",\"2011-01-09 19:34:11\",\"2011-01-09 19:34:11\",\"2011-01-09 19:40:35\",\"2011-01-09 19:40:38\",\"2011-01-09 19:40:39\",\"2011-01-09 19:44:08\",\"2011-01-09 19:44:09\",\"2011-01-09 19:44:09\",\"2011-01-09 19:44:12\",\"2011-01-09 19:44:13\",\"2011-01-09 19:45:33\",\"2011-01-09 19:45:34\",\"2011-01-09 19:47:36\",\"2011-01-09 19:47:22\",null]}"), stringsAsFactors=F) survey_unit_sessions = as.data.frame(jsonlite::fromJSON("{\"session\":[\"2uy43834873467gejdsgdfujgcv8\",\"2uy43834873467gejdsgdfujgcv8\",\"2uy43834873467gejdsgdfujgcv8\",\"2uy43834873467gejdsgdfujgcv8\",\"2uy43834873467gejdsgdfujgcv8\",\"2uy43834873467gejdsgdfujgcv8\",\"2uy43834873467gejdsgdfujgcv8\",\"2uy43834873467gejdsgdfujgcv8\",\"2uy43834873467gejdsgdfujgcv8\",\"2uy43834873467gejdsgdfujgcv8\",\"2uy43834873467gejdsgdfujgcv8\",\"2uy43834873467gejdsgdfujgcv8\",\"2uy43834873467gejdsgdfujgcv8\",\"2uy43834873467gejdsgdfujgcv8\",\"2uy43834873467gejdsgdfujgcv8\",\"2uy43834873467gejdsgdfujgcv8\",\"2uy43834873467gejdsgdfujgcv8\",\"2uy43834873467gejdsgdfujgcv8\",\"2uy43834873467gejdsgdfujgcv8\",\"2uy43834873467gejdsgdfujgcv8\",\"2uy43834873467gejdsgdfujgcv8\",\"2uy43834873467gejdsgdfujgcv8\",\"2uy43834873467gejdsgdfujgcv8\",\"2uy43834873467gejdsgdfujgcv8\",\"2uy43834873467gejdsgdfujgcv8\",\"2uy43834873467gejdsgdfujgcv8\",\"2uy43834873467gejdsgdfujgcv8\",\"2uy43834873467gejdsgdfujgcv8\",\"2uy43834873467gejdsgdfujgcv8\",\"2uy43834873467gejdsgdfujgcv8\",\"2uy43834873467gejdsgdfujgcv8\",\"2uy43834873467gejdsgdfujgcv8\",\"2uy43834873467gejdsgdfujgcv8\",\"2uy43834873467gejdsgdfujgcv8\",\"2uy43834873467gejdsgdfujgcv8\",\"2uy43834873467gejdsgdfujgcv8\",\"2uy43834873467gejdsgdfujgcv8\",\"2uy43834873467gejdsgdfujgcv8\",\"2uy43834873467gejdsgdfujgcv8\",\"2uy43834873467gejdsgdfujgcv8\",\"2uy43834873467gejdsgdfujgcv8\",\"2uy43834873467gejdsgdfujgcv8\",\"2uy43834873467gejdsgdfujgcv8\",\"2uy43834873467gejdsgdfujgcv8\",\"2uy43834873467gejdsgdfujgcv8\",\"2uy43834873467gejdsgdfujgcv8\",\"2uy43834873467gejdsgdfujgcv8\",\"2uy43834873467gejdsgdfujgcv8\",\"2uy43834873467gejdsgdfujgcv8\",\"2uy43834873467gejdsgdfujgcv8\",\"2uy43834873467gejdsgdfujgcv8\",\"2uy43834873467gejdsgdfujgcv8\",\"2uy43834873467gejdsgdfujgcv8\",\"2uy43834873467gejdsgdfujgcv8\",\"2uy43834873467gejdsgdfujgcv8\",\"2uy43834873467gejdsgdfujgcv8\",\"2uy43834873467gejdsgdfujgcv8\",\"2uy43834873467gejdsgdfujgcv8\",\"2uy43834873467gejdsgdfujgcv8\",\"2uy43834873467gejdsgdfujgcv8\",\"2uy43834873467gejdsgdfujgcv8\",\"2uy43834873467gejdsgdfujgcv8\",\"2uy43834873467gejdsgdfujgcv8\",\"2uy43834873467gejdsgdfujgcv8\",\"2uy43834873467gejdsgdfujgcv8\",\"2uy43834873467gejdsgdfujgcv8\",\"2uy43834873467gejdsgdfujgcv8\",\"2uy43834873467gejdsgdfujgcv8\",\"2uy43834873467gejdsgdfujgcv8\",\"2uy43834873467gejdsgdfujgcv8\",\"2uy43834873467gejdsgdfujgcv8\",\"2uy43834873467gejdsgdfujgcv8\",\"2uy43834873467gejdsgdfujgcv8\",\"2uy43834873467gejdsgdfujgcv8\",\"2uy43834873467gejdsgdfujgcv8\",\"2uy43834873467gejdsgdfujgcv8\",\"2uy43834873467gejdsgdfujgcv8\",\"2uy43834873467gejdsgdfujgcv8\",\"2uy43834873467gejdsgdfujgcv8\",\"2uy43834873467gejdsgdfujgcv8\",\"2uy43834873467gejdsgdfujgcv8\"],\"id\":[1420,1422,1423,1427,1428,1432,1433,1434,1435,1436,1437,1439,1440,1442,1443,1445,1446,1465,1466,1468,1469,1470,1479,1480,1481,1483,1484,1485,1486,1494,1495,1496,1497,1539,1540,1541,1542,1543,1544,1545,1547,1574,1575,1576,1577,1578,1579,1580,1581,1582,1583,1584,1585,1586,1587,1591,1592,1596,1597,1598,1599,1600,1601,1602,1603,1604,1766,1916,1920,1921,1922,1923,1924,1925,1926,1927,1928,1929,1930,1931,1932],\"unit_id\":[224,224,225,224,225,224,225,225,221,222,223,224,225,222,223,224,225,221,222,224,221,222,221,222,223,224,221,222,223,224,221,222,223,225,225,225,224,221,222,223,224,224,221,222,224,221,222,221,222,221,222,221,222,221,221,224,220,224,221,222,227,229,228,227,229,228,227,229,221,222,223,219,224,221,222,223,219,224,220,220,226],\"run_session_id\":[472,472,472,472,472,472,472,472,472,472,472,472,472,472,472,472,472,472,472,472,472,472,472,472,472,472,472,472,472,472,472,472,472,472,472,472,472,472,472,472,472,472,472,472,472,472,472,472,472,472,472,472,472,472,472,472,472,472,472,472,472,472,472,472,472,472,472,472,472,472,472,472,472,472,472,472,472,472,472,472,472],\"created\":[\"2011-01-04 19:40:45\",\"2011-01-04 19:43:16\",\"2011-01-04 19:43:17\",\"2011-01-04 19:46:26\",\"2011-01-04 19:46:28\",\"2011-01-04 19:59:45\",\"2011-01-04 19:59:45\",\"2011-01-04 20:00:34\",\"2011-01-04 20:00:45\",\"2011-01-04 20:00:48\",\"2011-01-04 20:00:53\",\"2011-01-04 20:02:26\",\"2011-01-04 20:02:27\",\"2011-01-04 20:02:51\",\"2011-01-04 20:02:58\",\"2011-01-04 20:04:05\",\"2011-01-04 20:04:06\",\"2011-01-04 21:08:32\",\"2011-01-04 21:08:32\",\"2011-01-04 21:11:11\",\"2011-01-04 21:11:12\",\"2011-01-04 21:11:12\",\"2011-01-04 21:27:41\",\"2011-01-04 21:27:41\",\"2011-01-04 21:27:45\",\"2011-01-04 21:29:13\",\"2011-01-04 21:29:14\",\"2011-01-04 21:29:14\",\"2011-01-04 21:29:17\",\"2011-01-04 21:30:56\",\"2011-01-04 21:30:57\",\"2011-01-04 21:30:57\",\"2011-01-04 21:31:01\",\"2011-01-05 14:00:17\",\"2011-01-05 14:00:35\",\"2011-01-05 14:02:00\",\"2011-01-05 14:25:11\",\"2011-01-05 14:25:15\",\"2011-01-05 14:26:12\",\"2011-01-05 14:26:21\",\"2011-01-05 14:31:26\",\"2011-01-05 18:16:15\",\"2011-01-05 18:16:16\",\"2011-01-05 18:16:16\",\"2011-01-05 18:16:53\",\"2011-01-05 18:16:56\",\"2011-01-05 18:16:56\",\"2011-01-05 18:17:00\",\"2011-01-05 18:17:02\",\"2011-01-05 18:10:57\",\"2011-01-05 18:21:02\",\"2011-01-05 18:21:39\",\"2011-01-05 18:23:09\",\"2011-01-05 18:23:19\",\"2011-01-05 18:37:00\",\"2011-01-05 18:48:57\",\"2011-01-05 18:49:00\",\"2011-01-05 19:34:52\",\"2011-01-05 19:34:54\",\"2011-01-05 19:34:54\",\"2011-01-05 19:35:23\",\"2011-01-05 19:35:36\",\"2011-01-05 19:35:36\",\"2011-01-05 19:37:34\",\"2011-01-05 19:37:47\",\"2011-01-05 19:37:47\",\"2011-01-08 10:01:38\",\"2011-01-09 19:34:11\",\"2011-01-09 19:40:35\",\"2011-01-09 19:40:35\",\"2011-01-09 19:40:38\",\"2011-01-09 19:40:39\",\"2011-01-09 19:44:08\",\"2011-01-09 19:44:09\",\"2011-01-09 19:44:09\",\"2011-01-09 19:44:12\",\"2011-01-09 19:44:13\",\"2011-01-09 19:45:33\",\"2011-01-09 19:45:34\",\"2011-01-09 19:45:34\",\"2011-01-09 19:47:36\"],\"ended\":[\"2011-01-04 19:41:36\",\"2011-01-04 19:43:16\",\"2011-01-04 19:44:30\",\"2011-01-04 19:46:28\",\"2011-01-04 19:56:57\",\"2011-01-04 19:59:45\",\"2011-01-04 20:00:34\",\"2011-01-04 20:00:45\",\"2011-01-04 20:00:48\",\"2011-01-04 20:00:53\",\"2011-01-04 20:00:53\",\"2011-01-04 20:02:27\",\"2011-01-04 20:02:37\",\"2011-01-04 20:02:58\",\"2011-01-04 20:02:58\",\"2011-01-04 20:04:06\",\"2011-01-04 20:58:29\",\"2011-01-04 21:08:32\",\"2011-01-04 21:09:10\",\"2011-01-04 21:11:12\",\"2011-01-04 21:11:12\",\"2011-01-04 21:24:17\",\"2011-01-04 21:27:41\",\"2011-01-04 21:27:45\",\"2011-01-04 21:27:46\",\"2011-01-04 21:29:14\",\"2011-01-04 21:29:14\",\"2011-01-04 21:29:17\",\"2011-01-04 21:29:18\",\"2011-01-04 21:30:57\",\"2011-01-04 21:30:57\",\"2011-01-04 21:31:01\",\"2011-01-04 21:31:02\",\"2011-01-05 14:00:35\",\"2011-01-05 14:02:00\",\"2011-01-05 14:25:11\",\"2011-01-05 14:25:15\",\"2011-01-05 14:26:12\",\"2011-01-05 14:26:21\",\"2011-01-05 14:26:21\",\"2011-01-05 17:38:17\",\"2011-01-05 18:16:16\",\"2011-01-05 18:16:16\",\"2011-01-05 18:16:53\",\"2011-01-05 18:16:56\",\"2011-01-05 18:16:56\",\"2011-01-05 18:17:00\",\"2011-01-05 18:17:02\",\"2011-01-05 18:10:57\",\"2011-01-05 18:21:02\",\"2011-01-05 18:21:39\",\"2011-01-05 18:23:09\",\"2011-01-05 18:23:19\",\"2011-01-05 18:37:00\",\"2011-01-05 18:47:09\",\"2011-01-05 18:49:00\",\"2011-01-05 19:33:50\",\"2011-01-05 19:34:54\",\"2011-01-05 19:34:54\",\"2011-01-05 19:35:23\",\"2011-01-05 19:35:36\",\"2011-01-05 19:35:36\",\"2011-01-05 19:37:34\",\"2011-01-05 19:37:47\",\"2011-01-05 19:37:47\",\"2011-01-08 10:01:38\",\"2011-01-09 19:34:11\",\"2011-01-09 19:34:11\",\"2011-01-09 19:40:35\",\"2011-01-09 19:40:38\",\"2011-01-09 19:40:39\",\"2011-01-09 19:44:08\",\"2011-01-09 19:44:09\",\"2011-01-09 19:44:09\",\"2011-01-09 19:44:12\",\"2011-01-09 19:44:13\",\"2011-01-09 19:45:33\",\"2011-01-09 19:45:34\",\"2011-01-09 19:47:36\",\"2011-01-09 19:47:22\",null]}"), stringsAsFactors=F) @@ -90,16 +91,16 @@ # wenn der taegliche fragebogen seltener als 20 mal und ueber einen kuerzeren zeitraum als 35 tage ausgefuellt wurde, muss er weiter ausgefuellt werden. })() }'; $before = microtime(true); -$results = $openCPU->identity(array('x' => $source),'', true); +$results = $openCPU->identity(array('x' => $source), '', true); $alert_type = 'alert-success'; -if($openCPU->http_status > 302 OR $openCPU->http_status === 0) { - $alert_type = 'alert-danger'; +if ($openCPU->http_status > 302 OR $openCPU->http_status === 0) { + $alert_type = 'alert-danger'; } -alert("3. HTTP status: ".$openCPU->http_status,$alert_type); +alert("3. HTTP status: " . $openCPU->http_status, $alert_type); $accordion3 = $openCPU->debugCall($results); -alert("3. Request took " . round((microtime(true) - $before) / 60, 4) . " minutes and was ".human_filesize(strlen($source)), 'alert-info'); +alert("3. Request took " . round((microtime(true) - $before) / 60, 4) . " minutes and was " . human_filesize(strlen($source)), 'alert-info'); // =============================================== @@ -107,7 +108,7 @@ $openCPU = new OpenCPU(Config::get('alternative_opencpu_instance')); $source = '{ (function() { -# '.$nocache.' +# ' . $nocache . ' Taeglicher_Fragebogen_1 = as.data.frame(jsonlite::fromJSON("{\"session\":[\"2uy43834873467gejdsgdfujgcv8\",\"2uy43834873467gejdsgdfujgcv8\"],\"session_id\":[1923,1928],\"study_id\":[219,219],\"modified\":[\"2011-01-09 19:44:08\",\"2011-01-09 19:45:33\"],\"created\":[\"2011-01-09 19:40:39\",\"2011-01-09 19:44:13\"],\"ended\":[\"2011-01-09 19:44:08\",\"2011-01-09 19:45:33\"]}"), stringsAsFactors=F); library(lubridate); @@ -115,24 +116,24 @@ ( as.Date(head(Taeglicher_Fragebogen_1$created, 1)) + days(35) ) < today() })() }'; $before = microtime(true); -$results = $openCPU->identity(array('x' => $source),'', true); +$results = $openCPU->identity(array('x' => $source), '', true); $alert_type = 'alert-success'; -if($openCPU->http_status > 302 OR $openCPU->http_status === 0) { - $alert_type = 'alert-danger'; +if ($openCPU->http_status > 302 OR $openCPU->http_status === 0) { + $alert_type = 'alert-danger'; } -alert("4. HTTP status: ".$openCPU->http_status,$alert_type); +alert("4. HTTP status: " . $openCPU->http_status, $alert_type); $accordion4 = $openCPU->debugCall($results); alert("4. Request took " . round((microtime(true) - $before) / 60, 4) . " minutes", 'alert-info'); // ===== RESULTS ===== // $alerts = $site->renderAlerts(); -if(!empty($alerts)): - echo '
    '; - echo $alerts; - echo '
    '; +if (!empty($alerts)): + echo '
    '; + echo $alerts; + echo '
    '; endif; echo "

    test knitr with plot

    "; @@ -151,4 +152,4 @@ echo $accordion4; -Template::load('footer'); \ No newline at end of file +Template::load('footer'); diff --git a/application/View/admin/misc/test_opencpu_speed.php b/application/View/admin/misc/test_opencpu_speed.php index 077a28608..3a64e989d 100644 --- a/application/View/admin/misc/test_opencpu_speed.php +++ b/application/View/admin/misc/test_opencpu_speed.php @@ -1,20 +1,21 @@ OpenCPU test"; -echo '
    testing '.Config::get('alternative_opencpu_instance').'
    '; +echo '
    testing ' . Config::get('alternative_opencpu_instance') . '
    '; $max = 30; -for($i = 0; $i < $max; $i++): - $openCPU->clearUserData(); - $source = '{'. - mt_rand().' - '. - str_repeat(" ",$i). - ' +for ($i = 0; $i < $max; $i++): + $openCPU->clearUserData(); + $source = '{' . + mt_rand() . ' + ' . + str_repeat(" ", $i) . + ' library(knitr) knit2html(text = "' . addslashes("__Hello__ World `r 1` ```{r} @@ -27,33 +28,33 @@ ") . '", fragment.only = T, options=c("base64_images","smartypants") ) - '. - str_repeat(" ",$max-$i). - ' + ' . + str_repeat(" ", $max - $i) . + ' }'; - $start_time = microtime(true); - $results = $openCPU->identity(array('x' => $source), '', true); - $responseHeaders = $openCPU->responseHeaders(); + $start_time = microtime(true); + $results = $openCPU->identity(array('x' => $source), '', true); + $responseHeaders = $openCPU->responseHeaders(); - $alert_type = 'alert-success'; - if($openCPU->http_status > 302 || $openCPU->http_status === 0) { - $alert_type = 'alert-danger'; - } + $alert_type = 'alert-success'; + if ($openCPU->http_status > 302 || $openCPU->http_status === 0) { + $alert_type = 'alert-danger'; + } - alert('1. HTTP status: ' . $openCPU->http_status, $alert_type); + alert('1. HTTP status: ' . $openCPU->http_status, $alert_type); - $accordion = $openCPU->debugCall($results); - $responseHeaders['total_time_php'] = round(microtime(true) - $start_time, 3); + $accordion = $openCPU->debugCall($results); + $responseHeaders['total_time_php'] = round(microtime(true) - $start_time, 3); - if(isset($times)): - $times['total_time'][] = $responseHeaders['total_time']; - $times['total_time_php'][] = $responseHeaders['total_time_php']; - else: - $times = array(); - $times['total_time'] = array($responseHeaders['total_time']); - $times['total_time_php'] = array($responseHeaders['total_time_php']); - endif; + if (isset($times)): + $times['total_time'][] = $responseHeaders['total_time']; + $times['total_time_php'][] = $responseHeaders['total_time_php']; + else: + $times = array(); + $times['total_time'] = array($responseHeaders['total_time']); + $times['total_time_php'] = array($responseHeaders['total_time_php']); + endif; endfor; @@ -75,21 +76,21 @@ unset($times['certinfo']); $openCPU->addUserData(array('datasets' => $datasets)); -$accordion = $openCPU->knitForAdminDebug( $source); +$accordion = $openCPU->knitForAdminDebug($source); $alert_type = 'alert-success'; -if($openCPU->http_status > 302 OR $openCPU->http_status === 0) { - $alert_type = 'alert-danger'; +if ($openCPU->http_status > 302 OR $openCPU->http_status === 0) { + $alert_type = 'alert-danger'; } alert('1. HTTP status: ' . $openCPU->http_status, $alert_type); echo $accordion; $alerts = $site->renderAlerts(); -if(!empty($alerts)): - echo '
    '; - echo $alerts; - echo '
    '; +if (!empty($alerts)): + echo '
    '; + echo $alerts; + echo '
    '; endif; Template::load('footer'); diff --git a/application/View/admin/run/add_run.php b/application/View/admin/run/add_run.php index 2f68bc666..6bd228744 100644 --- a/application/View/admin/run/add_run.php +++ b/application/View/admin/run/add_run.php @@ -2,51 +2,51 @@
    - -
    -

    Runs Add New

    -
    - - -
    -
    -
    -
    -

    Create new run

    + +
    +

    Runs Add New

    +
    + + +
    +
    +
    +
    +

    Create new run

    +
    + +
    +
    + + +
    +

    Enter Run shorthand

    +
      +
    • This is the name that users will see in their browser's address bar for your study, possibly elsewhere too.
    • +
    • It can be changed later, but it also changes the link to your study, so don't change it once you're live.
    • +
    • Ideally, it should be the memorable name of your study.
    • +
    • Name should contain only alpha-numeric characters and no spaces. It needs to start with a letter.
    • +
    +
    +
    +
    - - -
    - - -
    -

    Enter Run shorthand

    -
      -
    • This is the name that users will see in their browser's address bar for your study, possibly elsewhere too.
    • -
    • It can be changed later, but it also changes the link to your study, so don't change it once you're live.
    • -
    • Ideally, it should be the memorable name of your study.
    • -
    • Name should contain only alpha-numeric characters and no spaces. It needs to start with a letter.
    • -
    -
    -
    - -
    -
    -
    - - - - -
    -

     

    - more help on creating runs
    -
    -
    - + + + + +
    +

     

    + more help on creating runs +
    + +
    +
    +
    diff --git a/application/View/admin/run/create_new_named_session.php b/application/View/admin/run/create_new_named_session.php index 6bfc48c16..4c1025a00 100644 --- a/application/View/admin/run/create_new_named_session.php +++ b/application/View/admin/run/create_new_named_session.php @@ -1,53 +1,53 @@
    - -
    -

    name; ?> name, null, null) ?>

    -
    + +
    +

    name; ?> name, null, null) ?>

    +
    - -
    -
    -
    - -
    -
    -
    -
    -

    Create Named Session

    -
    -
    -
    - + +
    +
    +
    + +
    +
    +
    +
    +

    Create Named Session

    +
    + +
    + - -
    -
    -
    - - -
    -
    -
    -
    - + +
    +
    +
    + + +
    +
    +
    +
    + - - + + -
    -
    -
    +
    +
    +
    -
    -
    - +
    + +
    \ No newline at end of file diff --git a/application/View/admin/run/cron_log.php b/application/View/admin/run/cron_log.php index ed5ee71af..d67fa3f3c 100755 --- a/application/View/admin/run/cron_log.php +++ b/application/View/admin/run/cron_log.php @@ -1,63 +1,64 @@

    cron log

    - The cron job runs every x minutes, to evaluate whether somebody needs to be sent a mail. This usually happens if a pause is over. It will then skip forward or backward, send emails and shuffle participants, but will stop at surveys and pages, because those should be viewed by the user. + The cron job runs every x minutes, to evaluate whether somebody needs to be sent a mail. This usually happens if a pause is over. It will then skip forward or backward, send emails and shuffle participants, but will stop at surveys and pages, because those should be viewed by the user.

    - - - - $value): - if($field=='skipbackwards') - $field = ''; - elseif($field=='skipforwards') - $field = ''; - elseif($field=='pauses') - $field = ''; - elseif($field=='emails') - $field = ''; - elseif($field=='shuffles') - $field = ''; - elseif($field=='sessions') - $field = ''; - elseif($field=='errors') - $field = ''; - elseif($field=='warnings') - $field = ''; - elseif($field=='notices') - $field = ''; - - echo ""; - endforeach; - ?> - - - $cell"; - endforeach; + +
    {$field}
    + + $value): + if ($field == 'skipbackwards') + $field = ''; + elseif ($field == 'skipforwards') + $field = ''; + elseif ($field == 'pauses') + $field = ''; + elseif ($field == 'emails') + $field = ''; + elseif ($field == 'shuffles') + $field = ''; + elseif ($field == 'sessions') + $field = ''; + elseif ($field == 'errors') + $field = ''; + elseif ($field == 'warnings') + $field = ''; + elseif ($field == 'notices') + $field = ''; - echo "\n"; - endforeach; - ?> + echo ""; + endforeach; + ?> + + +
    {$field}
    - render("admin/run/".$run->name."/cron_log"); - } else { - echo "No cron jobs yet. Maybe you disabled them in the settings."; - } - ?> - + // printing table rows + foreach ($cronlogs AS $row): + foreach ($row as $cell): + echo "$cell"; + endforeach; + + echo "\n"; + endforeach; + ?> + + + render("admin/run/" . $run->name . "/cron_log"); +} else { + echo "No cron jobs yet. Maybe you disabled them in the settings."; +} +?> + - -
    - -
    -

    name; ?> name, null, null) ?>

    -
    + +
    +

    name; ?> name, null, null) ?>

    +
    - -
    -
    -
    - -
    -
    -
    -
    -

    Cron Log

    -
    -
    -

    - The cron job runs every x minutes, to evaluate whether somebody needs to be sent a mail. This usually happens if a pause is over. It will then skip forward or backward, send emails and shuffle participants, but will stop at surveys and pages, because those should be viewed by the user. -

    -
    -
    - printCronLogFile($parse); - } - ?> -
    -
    + +
    +
    +
    + +
    +
    +
    +
    +

    Cron Log

    +
    +
    +

    + The cron job runs every x minutes, to evaluate whether somebody needs to be sent a mail. This usually happens if a pause is over. It will then skip forward or backward, send emails and shuffle participants, but will stop at surveys and pages, because those should be viewed by the user. +

    +
    +
    + printCronLogFile($parse); + } + ?> +
    +
    - -
    -
    + +
    +
    -
    -
    +
    +
    -
    -
    - +
    + +
    \ No newline at end of file diff --git a/application/View/admin/run/delete_run.php b/application/View/admin/run/delete_run.php index 953fe052e..380c36551 100644 --- a/application/View/admin/run/delete_run.php +++ b/application/View/admin/run/delete_run.php @@ -1,53 +1,53 @@
    - -
    -

    name; ?> name, null, null) ?>

    -
    + +
    +

    name; ?> name, null, null) ?>

    +
    - -
    -
    -
    - -
    -
    -
    -
    -

    Delete Run

    -
    -
    -
    - + +
    +
    +
    + +
    +
    +
    +
    +

    Delete Run

    +
    + +
    + -

    Type run name to confirm it's deletion

    -
    -

    Type the run's name to confirm that you want delete all existing users who progressed on average to position

    -
    -
    -
    -
    - - -
    -
    -
    -
    - +

    Type run name to confirm it's deletion

    +
    +

    Type the run's name to confirm that you want delete all existing users who progressed on average to position

    +
    +
    +
    +
    + + +
    +
    +
    +
    + - - -
    + + +
    -
    -
    +
    +
    -
    -
    - +
    + +
    \ No newline at end of file diff --git a/application/View/admin/run/email_log.php b/application/View/admin/run/email_log.php index 9cfb6b303..3b5b49167 100755 --- a/application/View/admin/run/email_log.php +++ b/application/View/admin/run/email_log.php @@ -1,63 +1,63 @@
    - -
    -

    name; ?> name, null, null) ?>

    -
    + +
    +

    name; ?> name, null, null) ?>

    +
    - -
    -
    -
    - -
    -
    -
    -
    -

    Email Log

    -
    -
    - + +
    +
    +
    + +
    +
    +
    +
    +

    Email Log

    +
    +
    + - - - - $value) { - echo ""; - } - ?> - - - - "; - foreach ($row as $cell) { - echo ""; - } - echo "\n"; - }; - ?> - -
    {$field}
    $cell
    - - -
    No E-mails yet
    - -
    -
    + + + + $value) { + echo ""; + } + ?> + + + + "; + foreach ($row as $cell) { + echo ""; + } + echo "\n"; + }; + ?> + +
    {$field}
    $cell
    + + +
    No E-mails yet
    + +
    +
    -
    -
    +
    +
    -
    -
    - +
    + +
    \ No newline at end of file diff --git a/application/View/admin/run/empty_run.php b/application/View/admin/run/empty_run.php index 7d04b82b9..232cd944c 100644 --- a/application/View/admin/run/empty_run.php +++ b/application/View/admin/run/empty_run.php @@ -1,54 +1,54 @@
    - -
    -

    name; ?> name, null, null) ?>

    -
    - - -
    -
    -
    - -
    -
    -
    -
    -

    Empty Run

    -
    -
    -
    - - -
    -

    Type the run's name to confirm that you want to delete all existing users who progressed on average to position .

    -

    You should only use this feature before the study goes live, to get rid of testing remnants! Please backup your survey data individually before emptying a run.

    - -
    -
    -
    -
    - - -
    -
    -
    -
    - - - -
    -
    - -
    -
    - -
    -
    - + +
    +

    name; ?> name, null, null) ?>

    +
    + + +
    +
    +
    + +
    +
    +
    +
    +

    Empty Run

    +
    +
    +
    + + +
    +

    Type the run's name to confirm that you want to delete all existing users who progressed on average to position .

    +

    You should only use this feature before the study goes live, to get rid of testing remnants! Please backup your survey data individually before emptying a run.

    + +
    +
    +
    +
    + + +
    +
    +
    +
    + + + +
    +
    + +
    +
    + +
    +
    +
    \ No newline at end of file diff --git a/application/View/admin/run/index.php b/application/View/admin/run/index.php index beac58472..c4bf83476 100644 --- a/application/View/admin/run/index.php +++ b/application/View/admin/run/index.php @@ -1,123 +1,123 @@
    - -
    -

    name; ?> name, null, null) ?>

    -
    + +
    +

    name; ?> name, null, null) ?>

    +
    - -
    -
    -
    - -
    -
    -
    -
    -

    - Edit Run - I am panicking :-( -

    -
    -
    - -
    - -
    - -
    Publicness:  
    - + +
    +
    +
    + +
    +
    +
    +
    +

    + Edit Run + I am panicking :-( +

    +
    +
    + + + + - -
    -
    +
    Publicness:  
    + -
    - -
    - -
    -
    - -
    click one of the symbols above to add a module
    -
    -
    - + + + + + + + + + + + + +
    +
    + +
    +
    -
    -
    -
    -
    - +
    + +
    + +
    +
    + +
    click one of the symbols above to add a module
    + +
    +
    + +
    +
    +
    +
    + +
    +
    -
    -
    +
    +
    -
    -
    - +
    + +
    - array())); - Template::load('admin/footer'); + array())); +Template::load('admin/footer'); ?> \ No newline at end of file diff --git a/application/View/admin/run/list.php b/application/View/admin/run/list.php index 745612389..5a345ab8b 100644 --- a/application/View/admin/run/list.php +++ b/application/View/admin/run/list.php @@ -1,78 +1,78 @@
    - -
    -

    Runs

    -
    + +
    +

    Runs

    +
    - -
    -
    -
    -
    -
    -

    Runs Listing

    - -
    -
    - -
    - - - - - - - - - - - - - - - - - - - - - - - -
    # IDNameCreatedStatusCronLock
    # PRIVATE'; - } elseif ($d_run['public'] == 1) { - echo 'ACCESS CODE ONLY'; - } elseif ($d_run['public'] == 2) { - echo 'LINK ONLY'; - } elseif ($d_run['public'] == 3) { - echo 'PUBLIC'; - } - ?>
    -
    + +
    +
    +
    +
    +
    +

    Runs Listing

    + +
    +
    + +
    + + + + + + + + + + + + + + + + + + + + + + + +
    # IDNameCreatedStatusCronLock
    # PRIVATE'; + } elseif ($d_run['public'] == 1) { + echo 'ACCESS CODE ONLY'; + } elseif ($d_run['public'] == 2) { + echo 'LINK ONLY'; + } elseif ($d_run['public'] == 3) { + echo 'PUBLIC'; + } + ?>
    +
    -
    -
    +
    +
    -
    -
    -
    -
    - -
    -
    +
    +
    +
    +
    + +
    +
    -
    -
    +
    +
    -
    -
    - +
    + +
    -
    -

    Configuration

    -
    - -
    -
    - - +
    +

    Configuration

    +
    + +
    +
    + +
    -
    -

    Testing & Management

    -
    - -
    -
    - +
    -
    -

    Danger Zone

    -
    - -
    -
    - +
    Add Run diff --git a/application/View/admin/run/monkey_bar.php b/application/View/admin/run/monkey_bar.php index 67ab610df..d5e8f16b3 100644 --- a/application/View/admin/run/monkey_bar.php +++ b/application/View/admin/run/monkey_bar.php @@ -1,91 +1,91 @@
    -
    -
    - -
    - - +
    +
    + + + + - + - + - - - "> - - - + + + "> + + + - - - - - - - - - - - - -
    + + + + + + + + + + + + +
    - + - -
    + +
    diff --git a/application/View/admin/run/overview.php b/application/View/admin/run/overview.php index ae3190a13..0d8f7ac7e 100644 --- a/application/View/admin/run/overview.php +++ b/application/View/admin/run/overview.php @@ -1,50 +1,50 @@
    - -
    -

    name; ?> name, null, null) ?>

    -
    + +
    +

    name; ?> name, null, null) ?>

    +
    - -
    -
    -
    - -
    -
    -
    -
    -

    Run Overview

    -
    -
    - + +
    +
    +
    + +
    +
    +
    +
    +

    Run Overview

    +
    +
    + - -
    -

    - title ?> - - finished users, - active users, - waiting users - -

    - parseBodySpecial(); ?> -
    - -

    Add an overview script

    - + +
    +

    + title ?> + + finished users, + active users, + waiting users + +

    + parseBodySpecial(); ?> +
    + +

    Add an overview script

    + -
    +
    -
    -
    -
    +
    +
    +
    -
    -
    - +
    + +
    \ No newline at end of file diff --git a/application/View/admin/run/random_groups.php b/application/View/admin/run/random_groups.php index c40ac4c5c..6c453037b 100644 --- a/application/View/admin/run/random_groups.php +++ b/application/View/admin/run/random_groups.php @@ -1,118 +1,118 @@
    - -
    -

    name; ?> name, null, null) ?>

    -
    + +
    +

    name; ?> name, null, null) ?>

    +
    - -
    -
    -
    - -
    -
    -
    -
    -

    Randomization Results

    -
    - Export -
    -
    -
    - - - - - - - $value): - echo ""; - endforeach; - ?> - - - - '; - foreach ($row as $cell) { - echo ""; - } + +
    +
    +
    + +
    +
    +
    +
    +

    Randomization Results

    +
    + Export +
    +
    +
    + - echo "
    \n"; - } - ?> + +
    {$field}
    $cell
    + + + $value): + echo ""; + endforeach; + ?> + + + + '; + foreach ($row as $cell) { + echo ""; + } - -
    {$field}
    $cell
    + echo "\n"; + } + ?> - -
    No users to randomize
    - -
    -
    + + -
    -
    + +
    No users to randomize
    + +
    + -
    - - + + + +
    + + \ No newline at end of file diff --git a/application/View/admin/run/rename_run.php b/application/View/admin/run/rename_run.php index f16fd0010..6bb1ae8b1 100644 --- a/application/View/admin/run/rename_run.php +++ b/application/View/admin/run/rename_run.php @@ -1,57 +1,57 @@
    - -
    -

    name; ?> name, null, null) ?>

    -
    + +
    +

    name; ?> name, null, null) ?>

    +
    - -
    -
    -
    - -
    -
    -
    -
    -

    Empty Run

    -
    -
    -
    - + +
    +
    +
    + +
    +
    +
    +
    +

    Empty Run

    +
    + +
    + -

    Enter new run shorthand

    -
    -
      -
    • This is the name that users will see in their browser's address bar for your study, possibly elsewhere too.
    • -
    • It can be changed later, but it also changes the link to your study, so you probably won't want to change it once you're live.
    • -
    • Ideally, it should be the memorable name of your study.
    • -
    -
    -
    -
    -
    - - -
    -
    -
    -
    - +

    Enter new run shorthand

    +
    +
      +
    • This is the name that users will see in their browser's address bar for your study, possibly elsewhere too.
    • +
    • It can be changed later, but it also changes the link to your study, so you probably won't want to change it once you're live.
    • +
    • Ideally, it should be the memorable name of your study.
    • +
    +
    +
    +
    +
    + + +
    +
    +
    +
    + - - -
    + + +
    -
    -
    +
    +
    -
    -
    - +
    + +
    \ No newline at end of file diff --git a/application/View/admin/run/run_import_dialog.php b/application/View/admin/run/run_import_dialog.php index d1ce81b23..c85e172a2 100644 --- a/application/View/admin/run/run_import_dialog.php +++ b/application/View/admin/run/run_import_dialog.php @@ -1,25 +1,25 @@
    -
    - -
    - - -
    -
    +
    + +
    + + +
    +
    -
    -
    - -
    - Select a json file - -
    -
    -
    +
    +
    + +
    + Select a json file + +
    +
    +
    diff --git a/application/View/admin/run/settings.php b/application/View/admin/run/settings.php index 86821e06b..1027414be 100644 --- a/application/View/admin/run/settings.php +++ b/application/View/admin/run/settings.php @@ -1,312 +1,312 @@
    - -
    -

    name; ?> name, null, null) ?>

    -
    + +
    +

    name; ?> name, null, null) ?>

    +
    - -
    -
    -
    - -
    -
    -
    -
    -

    Settings

    -
    + +
    +
    +
    + +
    +
    +
    +
    +

    Settings

    +
    -
    - - +
    + -
    +
    -
    -
    +
    +
    -
    -
    - +
    + +
    -
    - - -
    -

    - -
    - - -
    -
    - -
    - - -
    - -
    - - -
    -

    {{login_link}} will be replaced by a personalised link to this run, {{login_code}} will be replaced with this user's session code.

    -
    -
    - -
    - -
    - -
    - Save - Test -
    +
    + + +
    +

    + +
    + + +
    +
    + +
    + + +
    + +
    + + +
    +

    {{login_link}} will be replaced by a personalised link to this run, {{login_code}} will be replaced with this user's session code.

    +
    +
    + +
    + +
    + +
    + Save + Test +
    -
    No email accounts. Add some here.
    +
    No email accounts. Add some here.
    \ No newline at end of file diff --git a/application/View/admin/run/units/endpage.php b/application/View/admin/run/units/endpage.php index 551acb526..1ddcad83f 100644 --- a/application/View/admin/run/units/endpage.php +++ b/application/View/admin/run/units/endpage.php @@ -1,12 +1,12 @@

    - +

    - Save - Test + Save + Test

    \ No newline at end of file diff --git a/application/View/admin/run/units/external.php b/application/View/admin/run/units/external.php index 0b3f8ccc2..d8f79914b 100644 --- a/application/View/admin/run/units/external.php +++ b/application/View/admin/run/units/external.php @@ -1,25 +1,25 @@

    - +

    - - + +

    - +

    - Enter a URL like http://example.org?code={{login_code}} and the user will be sent to that URL, - replacing {{login_code}} with that user's code.
    - Enter R-code to e.g. send more data along:
    paste0('http:example.org?code={{login_link}}&age=', demographics$age). + Enter a URL like http://example.org?code={{login_code}} and the user will be sent to that URL, + replacing {{login_code}} with that user's code.
    + Enter R-code to e.g. send more data along:
    paste0('http:example.org?code={{login_link}}&age=', demographics$age).

    - Save - Test + Save + Test

    diff --git a/application/View/admin/run/units/pause.php b/application/View/admin/run/units/pause.php index 664b7f7b0..b877a3a2b 100644 --- a/application/View/admin/run/units/pause.php +++ b/application/View/admin/run/units/pause.php @@ -1,39 +1,39 @@

    - - and + + and

    - - and + + and

    - - - - - - -
    - + + + + + + +
    +

    - +

    - Save - Test + Save + Test

    \ No newline at end of file diff --git a/application/View/admin/run/units/shuffle.php b/application/View/admin/run/units/shuffle.php index d1e346797..310a9879a 100644 --- a/application/View/admin/run/units/shuffle.php +++ b/application/View/admin/run/units/shuffle.php @@ -1,18 +1,18 @@
    - Randomly assign to one of - - groups counting from one. + Randomly assign to one of + + groups counting from one.
    - You can later read the assigned group using shuffle$group.
    - You can then for example use a SkipForward to send one group to a different arm/path in the run or use a show-if in a survey to show certain items/stimuli to one group only. + You can later read the assigned group using shuffle$group.
    + You can then for example use a SkipForward to send one group to a different arm/path in the run or use a show-if in a survey to show certain items/stimuli to one group only.

    - Save - Test + Save + Test

    \ No newline at end of file diff --git a/application/View/admin/run/units/skipbackward.php b/application/View/admin/run/units/skipbackward.php index 1db42e1e4..65c19a314 100644 --- a/application/View/admin/run/units/skipbackward.php +++ b/application/View/admin/run/units/skipbackward.php @@ -1,18 +1,18 @@

    - +

    - +

    - Save - Test + Save + Test

    \ No newline at end of file diff --git a/application/View/admin/run/units/skipforward.php b/application/View/admin/run/units/skipforward.php index 59079d375..98aa7f804 100644 --- a/application/View/admin/run/units/skipforward.php +++ b/application/View/admin/run/units/skipforward.php @@ -1,30 +1,30 @@
    -
    - - - -
    - - else - - - go on +
    + + + +
    + + else + + + go on

    - Save - Test + Save + Test
    \ No newline at end of file diff --git a/application/View/admin/run/units/survey.php b/application/View/admin/run/units/survey.php index 863ce2c61..d61f2b741 100644 --- a/application/View/admin/run/units/survey.php +++ b/application/View/admin/run/units/survey.php @@ -2,44 +2,45 @@ -
    - - - id): ?> -

    - complete results, - begun (in ~ m) -

    -

    - View items - Upload items -

    -
    -

    - Save - Test -

    - -

    -

    - Save -
    -

    - - -
    +
    + + + id): ?> +

    + complete results, + begun (in ~ m) +

    +

    + View items + Upload items +

    +
    +

    + Save + Test +

    + +

    +

    + Save +
    +

    + + +
    -
    No studies. Add some first
    +
    No studies. Add some first
    diff --git a/application/View/admin/run/units/unit.php b/application/View/admin/run/units/unit.php index a7660f09e..0af194bce 100644 --- a/application/View/admin/run/units/unit.php +++ b/application/View/admin/run/units/unit.php @@ -1,18 +1,18 @@
    -
    -

    -
    -
    -

    - howManyReachedIt() ?>
    -
    -
    -
    - - - - - -
    +
    +

    +
    +
    +

    + howManyReachedIt() ?>
    +
    +
    +
    + + + + + +
    \ No newline at end of file diff --git a/application/View/admin/run/upload_files.php b/application/View/admin/run/upload_files.php index d26ada372..d8ee4fe32 100644 --- a/application/View/admin/run/upload_files.php +++ b/application/View/admin/run/upload_files.php @@ -1,83 +1,83 @@
    - -
    -

    name; ?> name, null, null) ?>

    -
    + +
    +

    name; ?> name, null, null) ?>

    +
    - -
    -
    -
    - -
    -
    -
    -
    -

    Upload Files

    -
    + +
    +
    +
    + +
    +
    +
    +
    +

    Upload Files

    +
    -
    - -
    -
      -
    • Choose as many files as you'd like.
    • -
    • You will be able to browse them by name here, but you'll have to copy a randomly-generated link to embed them.
    • -
    • To embed images, use the following Markdown syntax: ![image description for blind users](image link), so in a concrete example ![Picture of a guitar](https://formr.org/assets/tmp/admin/mkWpDTv5Um2ijGs1SJbH1uw9Bn2ctysD8N3tbkuwalOM.png). You can embed images anywhere you can use Markdown (e.g. in item and choice labels, feedback, emails).
    • -
    • We do not prevent users from sharing the links with others. - If your users see an image/video, there is no way of preventing them from re-sharing it, if you're not looking over their shoulders.
      - Users can always take a photo of the screen, even if you could prevent screenshots. Hence, we saw no point in generating single-use links for the images (so that users can't share the picture directly). Please be aware of this and don't use formr to show confidential information in an un-supervised setting. However, because the links are large random numbers, it's fairly safe to use formr to upload confidential information to be shown in the lab, the images cannot be discovered by people who don't have access to the study.
    • -
    -
    +
    + +
    +
      +
    • Choose as many files as you'd like.
    • +
    • You will be able to browse them by name here, but you'll have to copy a randomly-generated link to embed them.
    • +
    • To embed images, use the following Markdown syntax: ![image description for blind users](image link), so in a concrete example ![Picture of a guitar](https://formr.org/assets/tmp/admin/mkWpDTv5Um2ijGs1SJbH1uw9Bn2ctysD8N3tbkuwalOM.png). You can embed images anywhere you can use Markdown (e.g. in item and choice labels, feedback, emails).
    • +
    • We do not prevent users from sharing the links with others. + If your users see an image/video, there is no way of preventing them from re-sharing it, if you're not looking over their shoulders.
      + Users can always take a photo of the screen, even if you could prevent screenshots. Hence, we saw no point in generating single-use links for the images (so that users can't share the picture directly). Please be aware of this and don't use formr to show confidential information in an un-supervised setting. However, because the links are large random numbers, it's fairly safe to use formr to upload confidential information to be shown in the lab, the images cannot be discovered by people who don't have access to the study.
    • +
    +
    -

    Files to upload:

    -
    -
    - -
    +

    Files to upload:

    + +
    + +
    - -
    + + -
    -

    Files uploaded in this run

    - - - - - - - - - - - - - - - - - - -
    File NameCreatedActions
    - View File - Copy URL - Delete File -
    - -

     

    -
    - +
    +

    Files uploaded in this run

    + + + + + + + + + + + + + + + + + + +
    File NameCreatedActions
    + View File + Copy URL + Delete File +
    + +

     

    +
    + -
    +
    -
    -
    +
    +
    -
    -
    - +
    + +
    \ No newline at end of file diff --git a/application/View/admin/run/user_detail.php b/application/View/admin/run/user_detail.php index a1a1793e4..b04feb572 100755 --- a/application/View/admin/run/user_detail.php +++ b/application/View/admin/run/user_detail.php @@ -1,128 +1,128 @@
    - -
    -

    name; ?> name, null, null) ?>

    -
    + +
    +

    name; ?> name, null, null) ?>

    +
    - -
    -
    -
    - -
    -
    -
    -
    -

    Log of user activity

    - + +
    +
    +
    + +
    +
    +
    + -
    -

    - Here you can see users' history of participation, i.e. when they got to certain point in a study, how long they stayed at each station and so forth. Earliest participants come first. -

    -
    -
    - - -
    -
    - -
    -
    SEARCH
    - -
    +
    +
    +

    + Here you can see users' history of participation, i.e. when they got to certain point in a study, how long they stayed at each station and so forth. Earliest participants come first. +

    +
    +
    + - -
    -
    - -
    +
    + + +
    +
    SEARCH
    + +
    - -
    - -
    + +
    +
    + +
    + +
    + +
    - - -
    - - - - - - - - - - - - - - - - - - - - - - - - - - -
    Unit in RunModule DescriptionSessionEnteredStayedLeftDelete
    () - -
    - - + + +
    -
    -
    + + + + + + + + + + + + + + + + + + + + + + + + + + +
    Unit in RunModule DescriptionSessionEnteredStayedLeftDelete
    () + +
    + + -
    -
    +
    +
    -
    -
    - +
    + + +
    + + !empty($reminders) ? $reminders : array())); ?> diff --git a/application/View/admin/run/user_overview.php b/application/View/admin/run/user_overview.php index d91887e91..196d5b88f 100755 --- a/application/View/admin/run/user_overview.php +++ b/application/View/admin/run/user_overview.php @@ -1,205 +1,205 @@
    - -
    -

    name; ?> name, null, null) ?>

    -
    - - -
    -
    -
    - -
    -
    -
    - -
    - - -

    - Here you can see users' progress (on which station they currently are). - If you're not happy with their progress, you can send manual reminders, customisable here.
    You can also shove them to a different position in a run if they veer off-track. -

    -

    - Participants who have been stuck at the same survey, external link or email for 2 days or more are highlighted in yellow at the top. Being stuck at an email module usually means that the user somehow ended up there without a valid email address, so that the email cannot be sent. Being stuck at a survey or external link usually means that the user interrupted the survey/external part before completion, you probably want to remind them manually (if you have the means to do so). -

    -

    - You can manually create new test users or real users. Test users are useful to test your run. They are like normal users, but have animal names to make them easy to re-identify and you get a bunch of tools to help you fill out faster and skip over pauses. You may want to create real users if a) you want to send users a link containing an identifier to link them up with other data sources b) you are manually enrolling participants, i.e. participants cannot enrol automatically. The identifier you choose will be displayed in the table below, making it easier to administrate users with a specific cipher/code. -

    - -
    - -
    - -
    -
    SEARCH
    - -
    -
    -
    SEARCH
    - -
    -
    - - -
    -
    - -
    - - -
    - -
    - - -
    - - - - - - - - - - - - - - - - - - - - - - - - - - -
    Run positionDescriptionSessionCreatedLast AccessAction
    - - - - - - - user_code == $user['session']): ?> - - - - - - - - -
    - - - - - ' - title='Toggle testing status'>'> - - " title="Remind this user" data-session=""> - - - - - - " title="Delete this user and all their data (you'll have to confirm)" data-session=""> - - - -
    -
    -
    -
    - Do with selected: - - - - - - - - - -
    -

     

    -
    - - - -
    - -
    -
    -
    - -
    -
    - + +
    +

    name; ?> name, null, null) ?>

    +
    + + +
    +
    +
    + +
    +
    +
    + +
    + + +

    + Here you can see users' progress (on which station they currently are). + If you're not happy with their progress, you can send manual reminders, customisable here.
    You can also shove them to a different position in a run if they veer off-track. +

    +

    + Participants who have been stuck at the same survey, external link or email for 2 days or more are highlighted in yellow at the top. Being stuck at an email module usually means that the user somehow ended up there without a valid email address, so that the email cannot be sent. Being stuck at a survey or external link usually means that the user interrupted the survey/external part before completion, you probably want to remind them manually (if you have the means to do so). +

    +

    + You can manually create new test users or real users. Test users are useful to test your run. They are like normal users, but have animal names to make them easy to re-identify and you get a bunch of tools to help you fill out faster and skip over pauses. You may want to create real users if a) you want to send users a link containing an identifier to link them up with other data sources b) you are manually enrolling participants, i.e. participants cannot enrol automatically. The identifier you choose will be displayed in the table below, making it easier to administrate users with a specific cipher/code. +

    + +
    + +
    + +
    +
    SEARCH
    + +
    +
    +
    SEARCH
    + +
    +
    + + +
    +
    + +
    + + +
    + +
    + + +
    + + + + + + + + + + + + + + + + + + + + + + + + + + +
    Run positionDescriptionSessionCreatedLast AccessAction
    + + + + + + + user_code == $user['session']): ?> + + + + + + + + +
    + + + + + ' + title='Toggle testing status'>'> + + " title="Remind this user" data-session=""> + + + + + + " title="Delete this user and all their data (you'll have to confirm)" data-session=""> + + + +
    +
    +
    +
    + Do with selected: + + + + + + + + + +
    +

     

    +
    + + + +
    + +
    +
    +
    + +
    +
    +
    - $reminders)); Template::load('admin/footer'); ?> diff --git a/application/View/admin/survey/add_survey.php b/application/View/admin/survey/add_survey.php index 26ef3a298..4c4cdbfd5 100755 --- a/application/View/admin/survey/add_survey.php +++ b/application/View/admin/survey/add_survey.php @@ -2,90 +2,90 @@
    - -
    -

    Surveys Add New

    -
    + +
    +

    Surveys Add New

    +
    - -
    + +
    -
    -

    Please keep this in mind when uploading surveys!

    -
      -
    • - The format must be one of .xls, .xlsx, .ods, .xml, .txt, or .csv. -
    • +
      +

      Please keep this in mind when uploading surveys!

      +
        +
      • + The format must be one of .xls, .xlsx, .ods, .xml, .txt, or .csv. +
      • -
      • - The survey shorthand will be derived from the filename. -
          -
        • If your spreadsheet was named survey_1-v2.xlsx the name would be survey_1.
        • -
        • The name can contain a to Z, 0 to 9 and the underscore. The name has to at least 2, at most 64 characters long. You can't use spaces, periods or dashes in the name.
        • -
        • It needs to start with a letter.
        • -
        • As shown above, you can add version numbers (or anything) after a dash, they will be ignored.
        • -
        -
      • -
      • - The name you choose here cannot be changed. It will be used to refer to this survey's results in many places.
        - Make it meaningful. -
      • -
      -
      - -
      -

      Upload an item table

      -
      -
      -

      Select a file

      -
      -
      -
      +
    • + The survey shorthand will be derived from the filename. +
        +
      • If your spreadsheet was named survey_1-v2.xlsx the name would be survey_1.
      • +
      • The name can contain a to Z, 0 to 9 and the underscore. The name has to at least 2, at most 64 characters long. You can't use spaces, periods or dashes in the name.
      • +
      • It needs to start with a letter.
      • +
      • As shown above, you can add version numbers (or anything) after a dash, they will be ignored.
      • +
      +
    • +
    • + The name you choose here cannot be changed. It will be used to refer to this survey's results in many places.
      + Make it meaningful. +
    • +
    +
    + +
    +

    Upload an item table

    +
    +
    +

    Select a file

    +
    + +
    -
    - - - Did you know, that on many computers you can also drag and drop a file on this box instead of navigating there through the file browser? -
    -
    - +
    + + + Did you know, that on many computers you can also drag and drop a file on this box instead of navigating there through the file browser? +
    +
    + - - -
    -

     

    - more help on creating survey sheets -
    -

    OR

    -
    -

    Import a Googlesheet

    -
    -
    -
    -
    - - - Enter a survey name following the hints above.. -
    -
    - - - Make sure this sheet is accessible by anyone with the link -
    -
    - + +
    +
    +

     

    + more help on creating survey sheets +
    +

    OR

    +
    +

    Import a Googlesheet

    +
    +
    +
    +
    + + + Enter a survey name following the hints above.. +
    +
    + + + Make sure this sheet is accessible by anyone with the link +
    +
    + - -
    -
    -
    -
    - - + + + + +
    + + diff --git a/application/View/admin/survey/delete_results.php b/application/View/admin/survey/delete_results.php index bc9400f27..fe36cf350 100755 --- a/application/View/admin/survey/delete_results.php +++ b/application/View/admin/survey/delete_results.php @@ -2,58 +2,58 @@
    -
    -

    name ?> Survey ID: id ?>

    -
    - -
    -
    -
    - -
    - -
    -
    -
    -

    Delete Results complete, begun

    -
    -
    -
    - - ' . $msg . '
    '; - } - if((int)$resultCount['finished'] > 10) { - echo '
    +
    +

    name ?> Survey ID: id ?>

    +
    + +
    +
    +
    + +
    + +
    +
    +
    +

    Delete Results complete, begun

    +
    + +
    + + ' . $msg . '
    '; + } + if ((int) $resultCount['finished'] > 10) { + echo '

    Warning!

    - Please review the existing results before deleting them. + Please review the existing results before deleting them.
    '; - } - ?> -

    Type survey name to confirm it's deletion

    -
    -
    -
    - - -
    -
    -
    -
    - - - - -
    - -
    -
    -
    - -
    + } + ?> +

    Type survey name to confirm it's deletion

    +
    +
    +
    + + +
    +
    +
    +
    + + + + + + + +
    + + + diff --git a/application/View/admin/survey/delete_study.php b/application/View/admin/survey/delete_study.php index ee6d2d37b..8f4161690 100755 --- a/application/View/admin/survey/delete_study.php +++ b/application/View/admin/survey/delete_study.php @@ -2,50 +2,52 @@
    -
    -

    name ?> Survey ID: id ?>

    -
    - -
    -
    -
    - -
    - -
    -
    -
    -

    Delete Survey with result rows

    -
    -
    -
    - - ' . $msg . '
    '; - } ?> -

    Type survey name to confirm it's deletion

    -
    -
    -
    - - -
    -
    -
    -
    - - - - -
    - -
    -
    -
    - - +
    +

    name ?> Survey ID: id ?>

    +
    + +
    +
    +
    + +
    + +
    +
    +
    +

    Delete Survey with result rows

    +
    +
    +
    + + ' . $msg . '
    '; + } + ?> +

    Type survey name to confirm it's deletion

    +
    +
    +
    + + +
    +
    +
    +
    + + + + +
    + +
    +
    + + +
    diff --git a/application/View/admin/survey/google_sheet_import.php b/application/View/admin/survey/google_sheet_import.php index 98a54c463..8bba9c6ee 100644 --- a/application/View/admin/survey/google_sheet_import.php +++ b/application/View/admin/survey/google_sheet_import.php @@ -1,33 +1,33 @@ diff --git a/application/View/admin/survey/index.php b/application/View/admin/survey/index.php index 79b481947..a446ac4e9 100755 --- a/application/View/admin/survey/index.php +++ b/application/View/admin/survey/index.php @@ -2,194 +2,194 @@
    -
    -

    name ?> Survey ID:

    -
    - -
    -
    -
    - -
    - -
    -
    -
    -

    Survey Shortcuts

    -
    - -
    -
    -

    Survey Settings

    -
    - -
    - - -
    -

    These are some settings for advanced users. You'll mostly need the "Import items" and the "Export results" options to the left.

    -
    - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    - - - Do you want a certain number of items on each page? We prefer speciyfing pages manually (by adding submit buttons items when we want a pagebreaks) because this gives us greater manual control - - - - - - - - - - Instant validation means that users will be alerted if their survey input is invalid right after entering their information. Otherwise, validation messages will only be shown once the user tries to submit. - -
    - -
    -
    - - - Sometimes, in complex studies where several surveys are linked, you'll want to let the progress bar that the user sees only vary in a given range (e.g. first survey 0-40, second survey 40-100). - -
    -
    -
    from
    - -
    to
    -
    %
    -
    -
    -
    - - - - Unlinking a survey means that the results will only be shown in random order, without session codes and dates and only after a minimum of 10 results are in. This is meant as a way to anonymise personally identifiable data and separate it from the survey data that you will analyze. - You can't change this settings once you select this option. - -
    - -
    -
    - - - Selecting this option will disable displaying the data of this survey in formr. However the data will still be available for use. - You can't change this settings once you select this option. - -
    - -
    - -

    Survey access window

    - - - - How big should the access window be for your survey? Here, you define the time a user can start the survey (usually after receiving an email invitation). By setting the second value to a value other than zero, you are saying that the user has to finish with the survey x minutes after the access window closed.
    - The sum of these values is the maximum time someone can spend on this unit, giving you more predictability than the snooze button (see below). To allow a user to keep editing indefinitely, set the finishing time and inactivity expiration to 0. If inactivity expiration is also set, a survey can expire before the end of the finish time. - More information. -
    -
    -
    -
    Start editing within
    - -
    minutes
    -
    finishing editing within
    - -
    minutes after the access window closed
    -
    - -
    -
    - - - If a user is inactive in the survey for x minutes, should the survey expire? Specify 0 if not. If a user inactive for x minutes, the run will automatically move on. If the invitation is still valid (see above), this value doesn't count. Beware: much like with the snooze button on your alarm clock, a user can theoretically snooze indefinitely. - -
    -
    - -
    Minutes
    -
    -
    -

    Survey Paging

    - - - - By enabling custom dynamic paging, your survey items will be "grouped" in pages depending on how your Submit Items are defined in the items sheet. That is, each page ends at a defined submit button. - Enabling this option nullifies the above "Items Per Page" setting, which means the number of items on a page will be determined by where Submit Items are placed in your items sheet. - You can't change this settings once you select this option. - -
    - -
    -
    - - -
    - -
    - - - -
    -
    -
    -
    -
    - -
    - +
    +

    name ?> Survey ID:

    +
    + +
    +
    +
    + +
    + +
    +
    +
    +

    Survey Shortcuts

    +
    + +
    +
    +

    Survey Settings

    +
    + +
    + + +
    +

    These are some settings for advanced users. You'll mostly need the "Import items" and the "Export results" options to the left.

    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    + + + Do you want a certain number of items on each page? We prefer speciyfing pages manually (by adding submit buttons items when we want a pagebreaks) because this gives us greater manual control + + + + + + + + + + Instant validation means that users will be alerted if their survey input is invalid right after entering their information. Otherwise, validation messages will only be shown once the user tries to submit. + +
    + +
    +
    + + + Sometimes, in complex studies where several surveys are linked, you'll want to let the progress bar that the user sees only vary in a given range (e.g. first survey 0-40, second survey 40-100). + +
    +
    +
    from
    + +
    to
    +
    %
    +
    +
    +
    + + + + Unlinking a survey means that the results will only be shown in random order, without session codes and dates and only after a minimum of 10 results are in. This is meant as a way to anonymise personally identifiable data and separate it from the survey data that you will analyze. + You can't change this settings once you select this option. + +
    + +
    +
    + + + Selecting this option will disable displaying the data of this survey in formr. However the data will still be available for use. + You can't change this settings once you select this option. + +
    + +
    + +

    Survey access window

    + + + + How big should the access window be for your survey? Here, you define the time a user can start the survey (usually after receiving an email invitation). By setting the second value to a value other than zero, you are saying that the user has to finish with the survey x minutes after the access window closed.
    + The sum of these values is the maximum time someone can spend on this unit, giving you more predictability than the snooze button (see below). To allow a user to keep editing indefinitely, set the finishing time and inactivity expiration to 0. If inactivity expiration is also set, a survey can expire before the end of the finish time. + More information. +
    +
    +
    +
    Start editing within
    + +
    minutes
    +
    finishing editing within
    + +
    minutes after the access window closed
    +
    + +
    +
    + + + If a user is inactive in the survey for x minutes, should the survey expire? Specify 0 if not. If a user inactive for x minutes, the run will automatically move on. If the invitation is still valid (see above), this value doesn't count. Beware: much like with the snooze button on your alarm clock, a user can theoretically snooze indefinitely. + +
    +
    + +
    Minutes
    +
    +
    +

    Survey Paging

    + + + + By enabling custom dynamic paging, your survey items will be "grouped" in pages depending on how your Submit Items are defined in the items sheet. That is, each page ends at a defined submit button. + Enabling this option nullifies the above "Items Per Page" setting, which means the number of items on a page will be determined by where Submit Items are placed in your items sheet. + You can't change this settings once you select this option. + +
    + +
    +
    + + +
    + +
    + + + +
    +
    +
    +
    +
    + +
    +
    diff --git a/application/View/admin/survey/list.php b/application/View/admin/survey/list.php index 2f8c3c015..bb1996510 100644 --- a/application/View/admin/survey/list.php +++ b/application/View/admin/survey/list.php @@ -1,70 +1,70 @@
    - -
    -

    Surveys

    -
    + +
    +

    Surveys

    +
    - -
    -
    -
    -
    -
    -

    Survey Listing

    - -
    -
    - -
    - - - - - - - - - - - - - - - - - - - - - -
    # IDNameCreatedModifiedGoogle Sheet
    # - - ... - -
    -
    + +
    +
    +
    +
    +
    +

    Survey Listing

    + +
    +
    + +
    + + + + + + + + + + + + + + + + + + + + + +
    # IDNameCreatedModifiedGoogle Sheet
    # + + ... + +
    +
    -
    -
    +
    +
    -
    - + -
    +
    +
    -
    -
    - +
    + +
    -
    -

    Configuration

    -
    - -
    -
    - - +
    +

    Configuration

    +
    + +
    +
    + +
    -
    -

    Testing & Management

    -
    - -
    -
    - - +
    +

    Testing & Management

    +
    + +
    +
    + +
    -
    -

    Danger Zone

    -
    - -
    -
    - - +
    +

    Danger Zone

    +
    + +
    +
    + +
    -getResultCount(); - } - if (trim($study->settings['google_file_id']) && (int)$resultCount['real_users'] === 0): - $google_link = google_get_sheet_link($study->settings['google_file_id']); -?> -
    - - +getResultCount(); +} +if (trim($study->settings['google_file_id']) && (int) $resultCount['real_users'] === 0): + $google_link = google_get_sheet_link($study->settings['google_file_id']); + ?> + + + - -
    + +
    Add Survey diff --git a/application/View/admin/survey/rename_study.php b/application/View/admin/survey/rename_study.php index 5606fc11e..8438670d3 100755 --- a/application/View/admin/survey/rename_study.php +++ b/application/View/admin/survey/rename_study.php @@ -2,51 +2,51 @@
    -
    -

    name ?> Survey ID: id ?>

    -
    - -
    -
    -
    - -
    - -
    -
    -
    -

    Rename Survey

    -
    -
    -
    - - ' . $msg . '
    '; - } - ?> -

    Choose a new name for your study

    -
    -
    -
    - - -
    -
    -
    -
    - - - -
    - -
    -
    -
    - - +
    +

    name ?> Survey ID: id ?>

    +
    + +
    +
    +
    + +
    + +
    +
    +
    +

    Rename Survey

    +
    +
    +
    + + ' . $msg . '
    '; + } + ?> +

    Choose a new name for your study

    +
    +
    +
    + + +
    +
    +
    +
    + + + +
    + +
    +
    + + +
    diff --git a/application/View/admin/survey/show_item_table.php b/application/View/admin/survey/show_item_table.php index 5411a7f8b..533221f23 100755 --- a/application/View/admin/survey/show_item_table.php +++ b/application/View/admin/survey/show_item_table.php @@ -2,131 +2,131 @@
    -
    -

    name ?> Survey ID: id ?>

    -
    - -
    -
    -
    - -
    - -
    -
    -
    -

    Survey Items

    -
    - -
    - -
    -

    - Click the "Show Items" button to show an overview table of your items. To leave the overview, press esc or click the close button in the top or bottom right. -

    -

    - You can download your item table in different formats (.xls, .xlsx, .json), but take care: The downloaded table - may not exactly match the uploaded table (most importantly, choice labels are always relegated to a second sheet). -

    -
    - - -
    -
    - - -
    -
    - -
    - +
    +

    name ?> Survey ID: id ?>

    +
    + +
    +
    +
    + +
    + +
    +
    +
    +

    Survey Items

    +
    + +
    + +
    +

    + Click the "Show Items" button to show an overview table of your items. To leave the overview, press esc or click the close button in the top or bottom right. +

    +

    + You can download your item table in different formats (.xls, .xlsx, .json), but take care: The downloaded table + may not exactly match the uploaded table (most importantly, choice labels are always relegated to a second sheet). +

    +
    + + +
    +
    + + +
    +
    + +
    +
    diff --git a/application/View/admin/survey/show_item_table_table.php b/application/View/admin/survey/show_item_table_table.php index d48f9958a..1356a6423 100644 --- a/application/View/admin/survey/show_item_table_table.php +++ b/application/View/admin/survey/show_item_table_table.php @@ -1,45 +1,47 @@
    - - - - $value): - if (in_array($field, $display_columns) AND ! empty_column($field, $results)): - array_push($use_columns, $field); - echo ""; - endif; - endforeach; - ?> - - - - - - - type = implode(" ", array('' . $row->type . '', ($row->choice_list == $row->name) ? '' : $row->choice_list, '' . $row->type_options . '')); - $row->name = $row->name . ($row->optional ? "*" : ""); - foreach ($use_columns AS $field): - echo ''; - endforeach; - ?> - - - -
    {$field}
    '; - $cell = $row->$field; - if (strtolower($field) == 'choices') { - $cell = array_to_orderedlist($cell); - } elseif ($field == 'label_parsed' AND $cell === null) { - $cell = $row->label; - } elseif (($field == 'value' || $field == 'showif') && $cell != '') { - $cell = "
    $cell
    "; - } - echo $cell; - echo '
    + + + + $value): + if (in_array($field, $display_columns) AND ! empty_column($field, $results)): + array_push($use_columns, $field); + echo ""; + endif; + endforeach; + ?> + + + + + + + type = implode(" ", array('' . $row->type . '', ($row->choice_list == $row->name) ? '' : $row->choice_list, '' . $row->type_options . '')); + $row->name = $row->name . ($row->optional ? "*" : ""); + foreach ($use_columns AS $field): + echo ''; + endforeach; + ?> + + + +
    {$field}
    '; + $cell = $row->$field; + if (strtolower($field) == 'choices') { + $cell = array_to_orderedlist($cell); + } elseif ($field == 'label_parsed' AND $cell === null) { + $cell = $row->label; + } elseif (($field == 'value' || $field == 'showif') && $cell != '') { + $cell = "
    $cell
    "; + } + echo $cell; + echo '
    \ No newline at end of file diff --git a/application/View/admin/survey/show_itemdisplay.php b/application/View/admin/survey/show_itemdisplay.php index 45ee86bca..fade62258 100755 --- a/application/View/admin/survey/show_itemdisplay.php +++ b/application/View/admin/survey/show_itemdisplay.php @@ -2,117 +2,117 @@
    -
    -

    name ?> Survey ID: id ?>

    -
    - -
    -
    -
    - -
    - -
    -
    -
    -

    Survey Results complete, begun

    - - -
    - -
    - -
    -
    -
    - Search by session - - Filter Results - - - - -
    -
    -
    - - - - - - $value): - if (in_array($field, array("shown_relative", "answered_relative", "item_id", "display_order", "hidden"))) { - continue; - } - echo ""; - endforeach; - ?> - - - - ' . timetostr(strtotime($row['created'])) . ''; - $row['shown'] = '' . timetostr(strtotime($row['shown'])) . ' '; - - if ($row['hidden'] === 1) { - $row['shown'] .= "not shown"; - } elseif ($row['hidden'] === null) { - $row['shown'] .= $row['shown'] . "not yet"; - } - - // truncate session code - if ($row['session']) { - if (($animal_end = strpos($row['session'], "XXX")) === false) { - $animal_end = 10; - } - $short_session = substr($row['session'], 0, $animal_end); - $row['session'] = '' . $short_session . '…'; - } - $row['saved'] = '' . timetostr(strtotime($row['saved'])) . ''; - $row['answered'] = '' . timetostr(strtotime($row['answered'])) . ''; - unset($row['shown_relative'], $row['answered_relative'], $row['item_id'], $row['display_order'], $row['hidden']); - - // open row - echo $last_sess == $row['unit_session_id'] ? '' : ''; - $last_sess = $row['unit_session_id']; - - // print cells of row - // $row is array... foreach( .. ) puts every element of $row to $cell variable - foreach ($row as $cell): - echo ""; - endforeach; - - // close row - echo "\n"; - endforeach; - ?> - -
    {$field}
    $cell
    - - -
    -
    - -
    -
    -
    - -
    - +
    +

    name ?> Survey ID: id ?>

    +
    + +
    +
    +
    + +
    + +
    +
    +
    +

    Survey Results complete, begun

    + + +
    + +
    + +
    +
    +
    + Search by session + + Filter Results + + + + +
    +
    +
    + + + + + + $value): + if (in_array($field, array("shown_relative", "answered_relative", "item_id", "display_order", "hidden"))) { + continue; + } + echo ""; + endforeach; + ?> + + + + ' . timetostr(strtotime($row['created'])) . ''; + $row['shown'] = '' . timetostr(strtotime($row['shown'])) . ' '; + + if ($row['hidden'] === 1) { + $row['shown'] .= "not shown"; + } elseif ($row['hidden'] === null) { + $row['shown'] .= $row['shown'] . "not yet"; + } + + // truncate session code + if ($row['session']) { + if (($animal_end = strpos($row['session'], "XXX")) === false) { + $animal_end = 10; + } + $short_session = substr($row['session'], 0, $animal_end); + $row['session'] = '' . $short_session . '…'; + } + $row['saved'] = '' . timetostr(strtotime($row['saved'])) . ''; + $row['answered'] = '' . timetostr(strtotime($row['answered'])) . ''; + unset($row['shown_relative'], $row['answered_relative'], $row['item_id'], $row['display_order'], $row['hidden']); + + // open row + echo $last_sess == $row['unit_session_id'] ? '' : ''; + $last_sess = $row['unit_session_id']; + + // print cells of row + // $row is array... foreach( .. ) puts every element of $row to $cell variable + foreach ($row as $cell): + echo ""; + endforeach; + + // close row + echo "\n"; + endforeach; + ?> + +
    {$field}
    $cell
    + + +
    +
    + +
    +
    +
    + +
    +
    @@ -120,52 +120,52 @@ diff --git a/application/View/admin/survey/show_results.php b/application/View/admin/survey/show_results.php index e5717ee91..010dce0a4 100755 --- a/application/View/admin/survey/show_results.php +++ b/application/View/admin/survey/show_results.php @@ -2,98 +2,98 @@
    -
    -

    name ?> Survey ID: id ?>

    -
    - -
    -
    -
    - -
    - -
    -
    -
    -

    Survey Results complete, begun, testers

    -
    - settings['hide_results']): ?> - Export - Detailed Results - -
    -
    - - settings['hide_results']): ?> -
    -

     

    -

    Displaying results has been disabled for this survey.

    -
    -
    - - -
    - -
    -
    -
    - Search by session - - Filter Results - - - - -
    -
    -
    - - - '; - foreach ($row as $field => $value) { - echo ''; - } - echo ''; - echo ''; - $print_header = false; - } - - if(isset($row['created'])): - $row['created'] = ''.timetostr(strtotime($row['created'])).''; - $row['ended'] = ''.timetostr(strtotime($row['ended'])).''; - $row['modified'] = ''.timetostr(strtotime($row['modified'])).''; - $row['expired'] = ''.timetostr(strtotime($row['expired'])).''; - endif; - echo ''; - foreach($row as $cell) { - echo ''; - } - echo ''; - } - echo ''; - ?> -
    ' . $field . '
    ' . $cell . '
    - -
    -
    - -
    -
    -
    - -
    - +
    +

    name ?> Survey ID: id ?>

    +
    + +
    +
    +
    + +
    + +
    +
    +
    +

    Survey Results complete, begun, testers

    +
    + settings['hide_results']): ?> + Export + Detailed Results + +
    +
    + + settings['hide_results']): ?> +
    +

     

    +

    Displaying results has been disabled for this survey.

    +
    +
    + + +
    + +
    +
    +
    + Search by session + + Filter Results + + + + +
    +
    +
    + + + '; + foreach ($row as $field => $value) { + echo ''; + } + echo ''; + echo ''; + $print_header = false; + } + + if (isset($row['created'])): + $row['created'] = '' . timetostr(strtotime($row['created'])) . ''; + $row['ended'] = '' . timetostr(strtotime($row['ended'])) . ''; + $row['modified'] = '' . timetostr(strtotime($row['modified'])) . ''; + $row['expired'] = '' . timetostr(strtotime($row['expired'])) . ''; + endif; + echo ''; + foreach ($row as $cell) { + echo ''; + } + echo ''; + } + echo ''; + ?> +
    ' . $field . '
    ' . $cell . '
    + +
    +
    + +
    +
    +
    + +
    +
    @@ -101,59 +101,59 @@ diff --git a/application/View/admin/survey/upload_items.php b/application/View/admin/survey/upload_items.php index d071a423f..b33a121d1 100755 --- a/application/View/admin/survey/upload_items.php +++ b/application/View/admin/survey/upload_items.php @@ -5,122 +5,122 @@
    -
    -

    name ?> Survey ID: id ?>

    -
    +
    +

    name ?> Survey ID: id ?>

    +
    -
    -
    -
    - -
    +
    +
    +
    + +
    -
    -
    -
    -

    Import Survey Items

    -
    -
    - -
    - - -
    -

    Please keep this in mind when uploading surveys!

    -
      -
    • - The format must be one of .xls, .xlsx, .ods, .xml, .txt, or .csv. -
    • -
    • - Existing results should be preserved if you did not remove, rename or re-type items .
      - Changes to labels and choice labels are okay (fixing typos etc.).
      - If you keep the confirmation box below empty, the changes will only happen, if the results can be preserved.
      - To possibly overwrite results by uploading a new item table, you will have to enter the study's name into the box.
      - Always back up your data, before doing the latter. -
    • -
    • - The name you chose for this survey is now locked. -
        -
      • - The uploaded file's name has to match name ?>, so you cannot accidentally upload the wrong item table. -
      • -
      • - You can, however, put version numbers behind a dash at the end: name ?>-v2.xlsx. The information after the dash and the file format are ignored. -
      • -
      -
    • -
    -
    +
    +
    +
    +

    Import Survey Items

    +
    + + +
    + -

    Upload an item table

    -
    - - Did you know, that on many computers you can also drag and drop a file on this box instead of navigating there through the file browser? -
    +
    +

    Please keep this in mind when uploading surveys!

    +
      +
    • + The format must be one of .xls, .xlsx, .ods, .xml, .txt, or .csv. +
    • +
    • + Existing results should be preserved if you did not remove, rename or re-type items .
      + Changes to labels and choice labels are okay (fixing typos etc.).
      + If you keep the confirmation box below empty, the changes will only happen, if the results can be preserved.
      + To possibly overwrite results by uploading a new item table, you will have to enter the study's name into the box.
      + Always back up your data, before doing the latter. +
    • +
    • + The name you chose for this survey is now locked. +
        +
      • + The uploaded file's name has to match name ?>, so you cannot accidentally upload the wrong item table. +
      • +
      • + You can, however, put version numbers behind a dash at the end: name ?>-v2.xlsx. The information after the dash and the file format are ignored. +
      • +
      +
    • +
    +
    -

    - Or use - this - - a - Googlesheet -

    -
    - - - Make sure this sheet is accessible by anyone with the link -
    +

    Upload an item table

    +
    + + Did you know, that on many computers you can also drag and drop a file on this box instead of navigating there through the file browser? +
    - 0): ?> -
    -

    Delete Results Confirmation

    - -
    -
    - - -
    -
    -
    -
    - - - -
    - +

    + Or use + this + + a + Googlesheet +

    +
    + + + Make sure this sheet is accessible by anyone with the link +
    - - -
    -

     

    - more help on creating survey sheets + 0): ?> +
    +

    Delete Results Confirmation

    + +
    +
    + + +
    +
    +
    +
    + + + +
    + -
    -
    -
    + + +
    +

     

    + more help on creating survey sheets -
    - +
    +
    +
    + + + diff --git a/application/View/public/about.php b/application/View/public/about.php index fed23618b..77a0f8eaf 100644 --- a/application/View/public/about.php +++ b/application/View/public/about.php @@ -1,127 +1,127 @@ $bodyClass, - 'headerClass' => 'fmr-small-header', - )); +Template::load('public/header', array( + 'bodyClass' => $bodyClass, + 'headerClass' => 'fmr-small-header', +)); ?>
    -
    -
    -
    -

    about formr

    -

    - formr was made by Ruben C. Arslan and Cyril S. Tata. Matthias P. Walther recently joined the team. -

    -
    -
    -
    -
    -
    - Cyril Tata -
    -

    Cyril

    -
    -
    -
    -
    - Ruben Arslan -
    -

    Ruben

    -
    -
    -
    -
    - Matthias Walther -
    -

    Matthias

    -
    -
    - -
    - -
    -
    +
    +
    +
    +

    about formr

    +

    + formr was made by Ruben C. Arslan and Cyril S. Tata. Matthias P. Walther recently joined the team. +

    +
    +
    +
    +
    +
    + Cyril Tata +
    +

    Cyril

    +
    +
    +
    +
    + Ruben Arslan +
    +

    Ruben

    +
    +
    +
    +
    + Matthias Walther +
    +

    Matthias

    +
    +
    + +
    + +
    +
    -
    -
    -
    -

    Credit

    -

     

    -
    -
    -
    -
    -
    -

    Citation

    -
    -

    - If you are publishing research conducted using formr, please cite -

    -
    - Arslan, R.C., Tata, C.S. & Walther, M.P. (2018). formr: A study framework allowing for automated feedback generation and complex longitudinal experience sampling studies using R. (version ). DOI -
    -
    -

    - Cite the version that was active while you ran your study. Zenodo will keep backups of each major release, so that the software used for your study is preserved when we update it and if Github ceases to exist. This ensures reproducibility and allows us to trace papers affected by major bugs, should we discover any in the future. -

    -
    -

    - If you used the accompanying R package, you should cite it too, because it is independent of the rest of the software and independently versioned. -

    -
    - Arslan, R.C. (2017). formr R package (Version 0.4.1). DOI -
    -
    - -

    Funding

    -

    Friedrich-Schiller-University Jena – DFG project "Kompass", PIs: Julia Zimmermann, Franz J. Neyer -

    -

    Georg August University Göttingen – Lars Penke, current hosting

    -

    Center for Open ScienceOpen Contributor Grant to Ruben Arslan and Cyril Tata. - - -

    -
    -
    -
    -

    Team

    -

    - formr was made by Ruben C. Arslan and Cyril S. Tata. -

    The current incarnation of the survey framework draws on prior work by Linus Neumann, prior funding by Jaap J. A. Denissen, ideas, testing, and feedback by Sarah J. Lennartz, Isabelle Habedank, and Tanja M. Gerlach.

    -

    - - -
    - -
    -

    - Uni Göttingen logo
    - Georg August University Göttingen -

    -
    - -
    -

    - Uni Jena logo
    - Friedrich Schiller University Jena -

    -
    -
    - - -

    Other credit

    -

    - formr is open source software and uses a lot of other free software, see the Github repository for some due credit. Most importantly, formr uses OpenCPU as its R backend. -

    -
    -
    -
    -
    -
    +
    +
    +
    +

    Credit

    +

     

    +
    +
    +
    +
    +
    +

    Citation

    +
    +

    + If you are publishing research conducted using formr, please cite +

    +
    + Arslan, R.C., Tata, C.S. & Walther, M.P. (2018). formr: A study framework allowing for automated feedback generation and complex longitudinal experience sampling studies using R. (version ). DOI +
    +
    +

    + Cite the version that was active while you ran your study. Zenodo will keep backups of each major release, so that the software used for your study is preserved when we update it and if Github ceases to exist. This ensures reproducibility and allows us to trace papers affected by major bugs, should we discover any in the future. +

    +
    +

    + If you used the accompanying R package, you should cite it too, because it is independent of the rest of the software and independently versioned. +

    +
    + Arslan, R.C. (2017). formr R package (Version 0.4.1). DOI +
    +
    + +

    Funding

    +

    Friedrich-Schiller-University Jena – DFG project "Kompass", PIs: Julia Zimmermann, Franz J. Neyer +

    +

    Georg August University Göttingen – Lars Penke, current hosting

    +

    Center for Open ScienceOpen Contributor Grant to Ruben Arslan and Cyril Tata. + + +

    +
    +
    +
    +

    Team

    +

    + formr was made by Ruben C. Arslan and Cyril S. Tata. +

    The current incarnation of the survey framework draws on prior work by Linus Neumann, prior funding by Jaap J. A. Denissen, ideas, testing, and feedback by Sarah J. Lennartz, Isabelle Habedank, and Tanja M. Gerlach.

    +

    + + +
    + +
    +

    + Uni Göttingen logo
    + Georg August University Göttingen +

    +
    + +
    +

    + Uni Jena logo
    + Friedrich Schiller University Jena +

    +
    +
    + + +

    Other credit

    +

    + formr is open source software and uses a lot of other free software, see the Github repository for some due credit. Most importantly, formr uses OpenCPU as its R backend. +

    +
    +
    +
    +
    +
    @@ -129,46 +129,46 @@
    -

    Hosting & Security

    -
    -
    -

    Security

    -

    - Your (and your participants') connection to this site is encrypted using state-of-the-art security using HTTPS (also called HTTP over TLS). This protects against eavesdropping on survey responses and tampering with the content of our site. -

    -

    - We have taken several measures to make it very unlikely that sensitive participant's data is divulged. It is not possible for participants to retrieve their answers to past responses, unless those are incorporated in a feedback somewhere by you, the researcher. Therefore, care should be taken not to incorporate sensitive information into the feedback and to alert participants to any possible privacy gray areas in the feedback (e.g. incorporating participant responses about their employer in a feedback mailed to a work email address or incorporating feedback on romantic activity in a study where it's likely that the participant's partner has access to their device). -

    -

    - Participants get an access token for the study, which functions as a strong password. However, an access token is stored on participant's devices/browsers by default and (if you set this up) emails can be sent to their email addresses, so the protection is only as strong as security for access to their device or their email account. -

    -

    - It is very important that you, as the study administator, choose a strong password for the admin account and the email address that it is linked to. Here's some good advice on choosing a strong password. Do not share the password with your collaborators via unencrypted channels (e.g. email) and don't share the password via any medium together with the information for which account and website it is. Keep your password in a safe place (your mind, a good password manager) and make sure your collaborators do the same. -

    -

    - The same precautions, of course, should be respected for the data that you collected.
    - Should you plan to release the collected data openly, please make sure that the data are not sensitive and not (re-)identifiable. -

    - -
    -
    -
    -
    -

    Hosting

    -

    - This instance of the formr.org is hosted on servers at the Georg August University Göttingen. It implements a security model that individually and uniquely protects the various entities of the platform, the application, it's data and the R interface (OpenCPU). These entities communicate only within a local network whose access is restricted to the IT administators of the Georg-Elias-Müller-Institute of Psychology. -

    -

    - Our entire database is backed up nightly. Whenever real data is deleted by you in bulk, formr backs it up as well, right before deletion. No backup is made if you delete single users/single survey entries. We do not specifically back up run units and survey files, but you can redownload the most recently uploaded version of a survey file and download files with the run structure to your computer or to the openscienceframework. -

    - - - -
    -
    - -
    -
    +

    Hosting & Security

    +
    +
    +

    Security

    +

    + Your (and your participants') connection to this site is encrypted using state-of-the-art security using HTTPS (also called HTTP over TLS). This protects against eavesdropping on survey responses and tampering with the content of our site. +

    +

    + We have taken several measures to make it very unlikely that sensitive participant's data is divulged. It is not possible for participants to retrieve their answers to past responses, unless those are incorporated in a feedback somewhere by you, the researcher. Therefore, care should be taken not to incorporate sensitive information into the feedback and to alert participants to any possible privacy gray areas in the feedback (e.g. incorporating participant responses about their employer in a feedback mailed to a work email address or incorporating feedback on romantic activity in a study where it's likely that the participant's partner has access to their device). +

    +

    + Participants get an access token for the study, which functions as a strong password. However, an access token is stored on participant's devices/browsers by default and (if you set this up) emails can be sent to their email addresses, so the protection is only as strong as security for access to their device or their email account. +

    +

    + It is very important that you, as the study administator, choose a strong password for the admin account and the email address that it is linked to. Here's some good advice on choosing a strong password. Do not share the password with your collaborators via unencrypted channels (e.g. email) and don't share the password via any medium together with the information for which account and website it is. Keep your password in a safe place (your mind, a good password manager) and make sure your collaborators do the same. +

    +

    + The same precautions, of course, should be respected for the data that you collected.
    + Should you plan to release the collected data openly, please make sure that the data are not sensitive and not (re-)identifiable. +

    + +
    +
    +
    +
    +

    Hosting

    +

    + This instance of the formr.org is hosted on servers at the Georg August University Göttingen. It implements a security model that individually and uniquely protects the various entities of the platform, the application, it's data and the R interface (OpenCPU). These entities communicate only within a local network whose access is restricted to the IT administators of the Georg-Elias-Müller-Institute of Psychology. +

    +

    + Our entire database is backed up nightly. Whenever real data is deleted by you in bulk, formr backs it up as well, right before deletion. No backup is made if you delete single users/single survey entries. We do not specifically back up run units and survey files, but you can redownload the most recently uploaded version of a survey file and download files with the run structure to your computer or to the openscienceframework. +

    + + + +
    +
    + +
    +
    diff --git a/application/View/public/account.php b/application/View/public/account.php index 7276a557f..0723c23cb 100644 --- a/application/View/public/account.php +++ b/application/View/public/account.php @@ -3,91 +3,91 @@ ?>
    -
    +
    diff --git a/application/View/public/alerts.php b/application/View/public/alerts.php index 8c26432c0..ec1a68feb 100644 --- a/application/View/public/alerts.php +++ b/application/View/public/alerts.php @@ -1,9 +1,10 @@ renderAlerts(); + $alerts = $site->renderAlerts(); } -if (!empty($alerts)): ?> -
    - -
    +if (!empty($alerts)): + ?> +
    + +
    diff --git a/application/View/public/disclaimer.php b/application/View/public/disclaimer.php index 58d435cb7..ce7ef34b5 100644 --- a/application/View/public/disclaimer.php +++ b/application/View/public/disclaimer.php @@ -1,18 +1,18 @@
    -
    -
    -
    -

    Disclaimer

    -
    -
    -
    -
    -
    -

    This is pretty much brand new software and is supplied for free, open-source. As such, it doesn't come with a warranty of any kind. Still, if you let us know when formr causes you trouble or headaches, we will try to help you resolve the problem and you will get our heartfelt apologies. -

    If you're the technical type (or employ one), you might consider hosting a stable release of formr yourself, because this version of formr tracks the most recent pre-release and will thus sometimes have kinks. -

    -
    -
    -
    -
    +
    +
    +
    +

    Disclaimer

    +
    +
    +
    +
    +
    +

    This is pretty much brand new software and is supplied for free, open-source. As such, it doesn't come with a warranty of any kind. Still, if you let us know when formr causes you trouble or headaches, we will try to help you resolve the problem and you will get our heartfelt apologies. +

    If you're the technical type (or employ one), you might consider hosting a stable release of formr yourself, because this version of formr tracks the most recent pre-release and will thus sometimes have kinks. +

    +
    +
    +
    +
    \ No newline at end of file diff --git a/application/View/public/documentation.php b/application/View/public/documentation.php index ca74b09af..93c6471f5 100644 --- a/application/View/public/documentation.php +++ b/application/View/public/documentation.php @@ -1,74 +1,74 @@ - 'fmr-small-header', - )); + 'fmr-small-header', +)); ?>
    -
    -
    -
    -

    formR documentation

    -

    - chain simple forms into longer runs, - use the power of R to generate pretty feedback and complex designs -

    -

    - Most documentation is inside formr – you can just get going and it will be waiting for you where you need it.
    - If something just doesn't make sense or if you run into errors, please let us know. -

    -
    -
    -

    Contents

    -
    - +
    +
    +
    +

    formR documentation

    +

    + chain simple forms into longer runs, + use the power of R to generate pretty feedback and complex designs +

    +

    + Most documentation is inside formr – you can just get going and it will be waiting for you where you need it.
    + If something just doesn't make sense or if you run into errors, please let us know. +

    +
    + -
    -
    - -
    -
    - -
    -
    - -
    -
    - -
    -
    - -
    -
    - -
    -
    - -
    -
    - -
    -
    - -
    -
    - -
    -
    -
    -
    +
    +
    +
    + +
    +
    + +
    +
    + +
    +
    + +
    +
    + +
    +
    + +
    +
    + +
    +
    + +
    +
    + +
    +
    + +
    +
    +
    +
    diff --git a/application/View/public/documentation/api.php b/application/View/public/documentation/api.php index af67d12b9..c14cef347 100644 --- a/application/View/public/documentation/api.php +++ b/application/View/public/documentation/api.php @@ -1,32 +1,32 @@

    formr API


    -The formr API is the primary way to get data/results out of the platform. It's a low-level HTTP-based API that you can use principally to get results of a -study for specified participants (sessions) + The formr API is the primary way to get data/results out of the platform. It's a low-level HTTP-based API that you can use principally to get results of a + study for specified participants (sessions)

    -Resource requests to the formr API require that you have a valid access token which you can obtain by providing API credentials (a client id and a client -secret) + Resource requests to the formr API require that you have a valid access token which you can obtain by providing API credentials (a client id and a client + secret)

    -API base URL:
    -https://api.formr.org + API base URL:
    + https://api.formr.org

    Obtaining Client ID and Client Secret

    -Access to the API is restricted, so only the administrators of formr are able to provide API credentials to formr users. To -obtain API credentials, send an email to Cyril and your credentials will be sent to you. + Access to the API is restricted, so only the administrators of formr are able to provide API credentials to formr users. To + obtain API credentials, send an email to Cyril and your credentials will be sent to you.

    Obtaining An Access Token

    -An access token is an opaque string that identifies a formr user and can be used to make API calls without further authentication. formr API access tokens -are short-lived and have a life span of about an hour. + An access token is an opaque string that identifies a formr user and can be used to make API calls without further authentication. formr API access tokens + are short-lived and have a life span of about an hour.

    - +

    To generate an access token you need to make an HTTP POST request to the token endpoint of the API

    @@ -39,9 +39,9 @@
     

    -This call will return a JSON object containing an access token which can be used to access the API without further authentication required. -
    -Sample successful response + This call will return a JSON object containing an access token which can be used to access the API without further authentication required. +
    + Sample successful response

    @@ -92,18 +92,18 @@
     

    - Notes:
    -

      -
    • survey_name1 and survey_name2 should be the actual survey names
    • -
    • If you want to get results for all surveys in the run you can omit the survey parameter
    • -
    • If you want to get all items from a survey, keep the items list empty.
    • -
    + Notes:
    +
      +
    • survey_name1 and survey_name2 should be the actual survey names
    • +
    • If you want to get results for all surveys in the run you can omit the survey parameter
    • +
    • If you want to get all items from a survey, keep the items list empty.
    • +

    RESPONSE

    -The response to a results request is a JSON object. The keys of this JSON structure are the names of the survey that were indicated in the requested and the value associated to each survey entry is an array of objects representing the results collected for that survey for all the requested sessions. An example of a response object could be the following: + The response to a results request is a JSON object. The keys of this JSON structure are the names of the survey that were indicated in the requested and the value associated to each survey entry is an array of objects representing the results collected for that survey for all the requested sessions. An example of a response object could be the following:

    diff --git a/application/View/public/documentation/features.php b/application/View/public/documentation/features.php
    index cc32673bc..bde7e0f04 100644
    --- a/application/View/public/documentation/features.php
    +++ b/application/View/public/documentation/features.php
    @@ -1,92 +1,92 @@
     

    Features


    - The following designs and many more are possible: + The following designs and many more are possible:

      -
    • simple surveys with and without feedback -
    • -
    • complex surveys (using skipping logic, personalised text, complex feedback) -
    • -
    • surveys with eligibility limitations -
    • -
    • diary studies including completely flexible automated email/text message reminders
    • -
    • longitudinal studies (e.g. automatically re-contact participants after they return from their exchange year). The items of later waves need not exist in final form at wave 1.
    • -
    • longitudinal social networks and other studies that require rating a variable number of things or persons
    • +
    • simple surveys with and without feedback +
    • +
    • complex surveys (using skipping logic, personalised text, complex feedback) +
    • +
    • surveys with eligibility limitations +
    • +
    • diary studies including completely flexible automated email/text message reminders
    • +
    • longitudinal studies (e.g. automatically re-contact participants after they return from their exchange year). The items of later waves need not exist in final form at wave 1.
    • +
    • longitudinal social networks and other studies that require rating a variable number of things or persons

    - Core strengths + Core strengths

      -
    • - generates very pretty feedback live, including ggplot2, and interactive ggvis plots and htmlwidgets. We find that this greatly increases interest and retention in our studies. -
    • -
    • - automates complex experience sampling, diary and training studies, including automated reminders via email or text message -
    • -
    • - looks nice on a phone (about 30-40% of participants fill out our surveys on a mobile device) -
    • -
    • - easily share, swap and combine surveys (they're simply spreadsheets) and runs (you can share complete designs, e.g. "daily diary study") -
    • -
    • - you can use R to do basically anything that R can do (i.e. complicated stuff, like using a sentiment analysis of a participant's Twitter feed to decide when the survey happens) -
    • -
    • - not jealous at all – feel free to integrate other components (other survey engines, reaction time tasks, whatever you are used to) with formr, we tried our best to make it easy. -
    • +
    • + generates very pretty feedback live, including ggplot2, and interactive ggvis plots and htmlwidgets. We find that this greatly increases interest and retention in our studies. +
    • +
    • + automates complex experience sampling, diary and training studies, including automated reminders via email or text message +
    • +
    • + looks nice on a phone (about 30-40% of participants fill out our surveys on a mobile device) +
    • +
    • + easily share, swap and combine surveys (they're simply spreadsheets) and runs (you can share complete designs, e.g. "daily diary study") +
    • +
    • + you can use R to do basically anything that R can do (i.e. complicated stuff, like using a sentiment analysis of a participant's Twitter feed to decide when the survey happens) +
    • +
    • + not jealous at all – feel free to integrate other components (other survey engines, reaction time tasks, whatever you are used to) with formr, we tried our best to make it easy. +

    - Features + Features

      -
    • - manage access to and eligibility for studies -
    • -
    • - longitudinal studies -
    • -
    • - send text messages (see the HowTo) -
    • -
    • - works on all somewhat modern devices and degrades gracefully where it doesn't -
    • -
    • - formats text using Github-flavoured Markdown (a.k.a. the easiest and least bothersome way to mark up text) -
    • -
    • - file, image, video, sound uploads for users (as survey items) and admins (to supply study materials) -
    • -
    • - complex conditional items -
    • -
    • - a dedicated formr R package: makes pretty feedback graphs and complex run logic even simpler. Simplifies data wrangling (importing, aggregating, simulating data from surveys). -
    • -
    • - a nice editor, Ace, for editing Markdown & R in runs. -
    • - +
    • + manage access to and eligibility for studies +
    • +
    • + longitudinal studies +
    • +
    • + send text messages (see the HowTo) +
    • +
    • + works on all somewhat modern devices and degrades gracefully where it doesn't +
    • +
    • + formats text using Github-flavoured Markdown (a.k.a. the easiest and least bothersome way to mark up text) +
    • +
    • + file, image, video, sound uploads for users (as survey items) and admins (to supply study materials) +
    • +
    • + complex conditional items +
    • +
    • + a dedicated formr R package: makes pretty feedback graphs and complex run logic even simpler. Simplifies data wrangling (importing, aggregating, simulating data from surveys). +
    • +
    • + a nice editor, Ace, for editing Markdown & R in runs. +
    • +

    - Plans: + Plans:

      -
    • - work offline on mobile phones and other devices with intermittent internet access (in the meantime enketo is pretty good and free too, but geared towards humanitarian aid) -
    • +
    • + work offline on mobile phones and other devices with intermittent internet access (in the meantime enketo is pretty good and free too, but geared towards humanitarian aid) +
    • -
    • - a better API (some basics are there) -
    • -
    • - social networks, round robin studies - at the moment they can be implemented, but are a bit bothersome at first. There is a dedicated module already which might also get released as open source if there's time. -
    • -
    • - more planned enhancements on Github -
    • +
    • + a better API (some basics are there) +
    • +
    • + social networks, round robin studies - at the moment they can be implemented, but are a bit bothersome at first. There is a dedicated module already which might also get released as open source if there's time. +
    • +
    • + more planned enhancements on Github +
    diff --git a/application/View/public/documentation/get_help.php b/application/View/public/documentation/get_help.php index fbaf04e86..d8b52194d 100644 --- a/application/View/public/documentation/get_help.php +++ b/application/View/public/documentation/get_help.php @@ -4,30 +4,30 @@

    If you're a participant in one of the studies implemented in formr, please reach out to the person running the study.

    If you're running a study yourself, there's several places to look.

    \ No newline at end of file diff --git a/application/View/public/documentation/getting_started.php b/application/View/public/documentation/getting_started.php index b05b02e27..c81215785 100644 --- a/application/View/public/documentation/getting_started.php +++ b/application/View/public/documentation/getting_started.php @@ -2,41 +2,41 @@

    Creating Studies

    - To begin creating studies using formr, you need to sign-up with your email and obtain an administrator account. - An administrator account is obtained by sending a request via email to rubenarslan@gmail.com or cyril.tata@gmail.com. - Studies in formr are created using spreadsheets. As a good starting point, you can clone the following Google spreadsheet - and study it to get versed with the definitions of the item types formr supports. + To begin creating studies using formr, you need to sign-up with your email and obtain an administrator account. + An administrator account is obtained by sending a request via email to rubenarslan@gmail.com or cyril.tata@gmail.com. + Studies in formr are created using spreadsheets. As a good starting point, you can clone the following Google spreadsheet + and study it to get versed with the definitions of the item types formr supports.

    - 1. Upload the items: - With your spreadsheet ready, login to formr admin and go to Surveys > Create new Surveys.
    - You can either upload your spreadsheet if it was stored locally on your computer using the form Upload an item table or - you could import a Google spreadsheet via it's visible share link, using the form Import a Googlesheet. When importing a Googlesheet, - you will need to manually specify the name of your survey where's if uploading a spreadsheet, the name of your survey is obtained using the filename - of the spreadsheet. + 1. Upload the items: + With your spreadsheet ready, login to formr admin and go to Surveys > Create new Surveys.
    + You can either upload your spreadsheet if it was stored locally on your computer using the form Upload an item table or + you could import a Google spreadsheet via it's visible share link, using the form Import a Googlesheet. When importing a Googlesheet, + you will need to manually specify the name of your survey where's if uploading a spreadsheet, the name of your survey is obtained using the filename + of the spreadsheet.

    - 2. Manage your survey: - If your spreadsheet was well formed (as described here) and the items were successfully uploaded, you survey will be added - to the Surveys menu. To manage your created survey, go to Surveys > YourSurveyName.
    - In the survey admin area you can test your study, change some survey settings, view and download results, upload and delete survey items etc. - The survey menu to the left in the survey admin area contains hints that are self explanatory. + 2. Manage your survey: + If your spreadsheet was well formed (as described here) and the items were successfully uploaded, you survey will be added + to the Surveys menu. To manage your created survey, go to Surveys > YourSurveyName.
    + In the survey admin area you can test your study, change some survey settings, view and download results, upload and delete survey items etc. + The survey menu to the left in the survey admin area contains hints that are self explanatory.

    - 3.Create a Run: - A formr "run" contains your study's complete design. Designs can range from the simple (a single survey or a randomized experiment) to the complex (like a diary study with daily reminders by email and text message or a longitudinal study tracking social network changes). - It is recommended you read more about runs before you begin. To create a run go to Runs > Create a new Run. Enter a meaningful run name which should contain only of alphanumeric characters. - If the run was creates successfully, it will be added to the Runs menu and you will be redirected to the run admin area. - Here you can add Run Units, design the complexity of your study and test your study. To modify your study definition later, you can go to Runs > YourRunName. - Your run is the entry point of your study. For participants to access your study, you need to set you run as public or protected in the admin area and it will be accessible under the URL YourRunName + 3.Create a Run: + A formr "run" contains your study's complete design. Designs can range from the simple (a single survey or a randomized experiment) to the complex (like a diary study with daily reminders by email and text message or a longitudinal study tracking social network changes). + It is recommended you read more about runs before you begin. To create a run go to Runs > Create a new Run. Enter a meaningful run name which should contain only of alphanumeric characters. + If the run was creates successfully, it will be added to the Runs menu and you will be redirected to the run admin area. + Here you can add Run Units, design the complexity of your study and test your study. To modify your study definition later, you can go to Runs > YourRunName. + Your run is the entry point of your study. For participants to access your study, you need to set you run as public or protected in the admin area and it will be accessible under the URL YourRunName

    Setting up your own formr instance

    - If you wish to set up your own instance of formr please follow the guidelines in our installation guide. + If you wish to set up your own instance of formr please follow the guidelines in our installation guide.

    - There is always help if you need assistance. + There is always help if you need assistance.

    \ No newline at end of file diff --git a/application/View/public/documentation/item_types.php b/application/View/public/documentation/item_types.php index 8401ea330..020a6d67c 100644 --- a/application/View/public/documentation/item_types.php +++ b/application/View/public/documentation/item_types.php @@ -1,193 +1,193 @@

    Survey Item Types


    There are a lot of item types, in the beginning you will probably only need a few though. To see them in action, -try using the following Google spreadsheet or fill it out yourself. It contains example uses of nearly every item there is. -

    Plain display types

    -
    -
    - note -
    -
    - display text. Notes are only displayed once, you can think of them as being "answered" simple by submitting. -
    -
    - note_iframe -
    -
    - If you want to render complex rmarkdown htmlwidgets, use this. -
    -
    - submit timeout -
    -
    - display a submit button. No items are displayed after the submit button, until all of the ones preceding it have been answered. This is useful for pagination and to ensure that answers required for showif or for dynamically generating item text have been given. If you specify the optional timeout (an integer, milliseconds), the submit button will automatically submit after that time has passed. However, if not all items are answered or optional, the user will end up on the same page. Together with optional items, this is a way to use timed submissions. The data in the item display table can be used to check how long an item was displayed and whether this matches with the server's time for when it sent the item and received the response. -
    -
    -

    Simple input family

    -
    -
    - text max_length -
    -
    - allows you to enter a text in a single-line input field. Adding a number text 100 defines the maximum number of characters that may be entered. -
    -
    - textarea max_length -
    -
    - displays a multi-line input field -
    -
    - number min, max, step -
    -
    - for numbers. step defaults to 1, using any will allow any decimals. -
    -
    - letters max_length -
    -
    - like text, allows only letters (A-Za-züäöß.;,!: ), no numbers. -
    -
    - email -
    -
    - for email addresses. They will be validated for syntax, but they won't be verified unless you say so in the run. -
    -
    -

    Sliders

    -
    -
    - range min,max,step -
    -
    - these are sliders. The numeric value chosen is not displayed. Text to be shown to the left and right of the slider can be defined using the choice1 and choice2 fields. Defaults are 1,100,1. -
    -
    - range_ticks min,max,step -
    -
    - like range but the individual steps are visually indicated using ticks and the chosen number is shown to the right. -
    -
    - -

    Datetime family

    -
    -
    - date min,max -
    -
    - for dates (displays a date picker). Input can be constrained using the min,max parameters. Allowed values would e.g. be 2013-01-01,2014-01-01 or -2years,now. -
    -
    - time min,max -
    -
    - for times (displays an input with hours and minutes). Input can also be constrained using min,max, e.g. 12:00,17:00 -
    -
    -

    Fancy family

    -
    -
    - geopoint -
    -
    - displays a button next to a text field. If you press the button (which has the location icon on it) and agree to share your location, the GPS coordinates will be saved. If you deny access or if GPS positioning fails, you can enter a location manually. -
    -
    - color -
    -
    - allows you to pick a color, using the operating system color picker (or one polyfilled by Webshims) -
    -
    -

    Multiple choice family

    -

    The, by far, biggest family of items. Please note, that there is some variability in how the answers are stored. You need to know about this, if you (a) intend to analyse the data in a certain way, for example you will want to store numbers for Likert scale choices, but text for timezones and cities (b) if you plan to use conditions in the run or in showif or somewhere else where R is executed. (b) is especially important, because you might not notice if demographics$sex == 'male' never turns true because sex is stored as 0/1 and you're testing as female.

    -
    -
    - mc choice_list -
    -
    - multipe choice (radio buttons), you can choose only one. -
    -
    - mc_button choice_list -
    -
    - like mc but instead of the text appearing next to a small button, a big button contains each choice label -
    - -
    - mc_multiple choice_list -
    -
    - multiple multiple choice (check boxes), you can choose several. Choices defined as above. -
    -
    - mc_multiple_button -
    -
    - like mc_multiple and mc_button -
    - -
    - check -
    -
    - a single check box for confirmation of a statement. -
    -
    - check_button -
    -
    - a bigger button to check. -
    - -
    - rating_button
    min, max, step -
    -
    - This shows the choice1 label to the left, the choice2 label to the right and a series of numbered buttons as defined by min,max,step in between. Defaults to 1,5,1. -
    -
    - sex -
    -
    - shorthand for mc_button with the ♂, ♀ symbols as choices -
    -
    - select_one choice_list -
    -
    - a dropdown, you can choose only one -
    -
    - select_multiple choice_list -
    -
    - a list in which, you can choose several options -
    -
    - select_or_add_one
    choice_list, maxType -
    -
    - like select_one, but it allows users to choose an option not given. Uses Select2. maxType can be used to set an upper limit on the length of the user-added option. Defaults to 255. -
    -
    - select_or_add_multiple
    choice_list, maxType,
    maxChoose
    -
    -
    - like select_multiple and select_or_add_one, allows users to add options not given. maxChoose can be used to place an upper limit on the number of chooseable options. -
    -
    - mc_heading choice_list -
    -
    - This type permits you to show the labels for mc or mc_multiple choices only once.
    - To get the necessary tabular look, assign a constant width to the choices (using e.g. mc-width100), give the heading the same choices as the mcs, and give the following mcs (or mc_multiples) the same classes + hide_label.
    - On small screens the mc_heading will be hidden and labels will automatically be displayed again, because the tabular layout would otherwise break down. -
    +try using the following Google spreadsheet or fill it out yourself. It contains example uses of nearly every item there is. +

    Plain display types

    +
    +
    + note +
    +
    + display text. Notes are only displayed once, you can think of them as being "answered" simple by submitting. +
    +
    + note_iframe +
    +
    + If you want to render complex rmarkdown htmlwidgets, use this. +
    +
    + submit timeout +
    +
    + display a submit button. No items are displayed after the submit button, until all of the ones preceding it have been answered. This is useful for pagination and to ensure that answers required for showif or for dynamically generating item text have been given. If you specify the optional timeout (an integer, milliseconds), the submit button will automatically submit after that time has passed. However, if not all items are answered or optional, the user will end up on the same page. Together with optional items, this is a way to use timed submissions. The data in the item display table can be used to check how long an item was displayed and whether this matches with the server's time for when it sent the item and received the response. +
    +
    +

    Simple input family

    +
    +
    + text max_length +
    +
    + allows you to enter a text in a single-line input field. Adding a number text 100 defines the maximum number of characters that may be entered. +
    +
    + textarea max_length +
    +
    + displays a multi-line input field +
    +
    + number min, max, step +
    +
    + for numbers. step defaults to 1, using any will allow any decimals. +
    +
    + letters max_length +
    +
    + like text, allows only letters (A-Za-züäöß.;,!: ), no numbers. +
    +
    + email +
    +
    + for email addresses. They will be validated for syntax, but they won't be verified unless you say so in the run. +
    +
    +

    Sliders

    +
    +
    + range min,max,step +
    +
    + these are sliders. The numeric value chosen is not displayed. Text to be shown to the left and right of the slider can be defined using the choice1 and choice2 fields. Defaults are 1,100,1. +
    +
    + range_ticks min,max,step +
    +
    + like range but the individual steps are visually indicated using ticks and the chosen number is shown to the right. +
    +
    + +

    Datetime family

    +
    +
    + date min,max +
    +
    + for dates (displays a date picker). Input can be constrained using the min,max parameters. Allowed values would e.g. be 2013-01-01,2014-01-01 or -2years,now. +
    +
    + time min,max +
    +
    + for times (displays an input with hours and minutes). Input can also be constrained using min,max, e.g. 12:00,17:00 +
    +
    +

    Fancy family

    +
    +
    + geopoint +
    +
    + displays a button next to a text field. If you press the button (which has the location icon on it) and agree to share your location, the GPS coordinates will be saved. If you deny access or if GPS positioning fails, you can enter a location manually. +
    +
    + color +
    +
    + allows you to pick a color, using the operating system color picker (or one polyfilled by Webshims) +
    +
    +

    Multiple choice family

    +

    The, by far, biggest family of items. Please note, that there is some variability in how the answers are stored. You need to know about this, if you (a) intend to analyse the data in a certain way, for example you will want to store numbers for Likert scale choices, but text for timezones and cities (b) if you plan to use conditions in the run or in showif or somewhere else where R is executed. (b) is especially important, because you might not notice if demographics$sex == 'male' never turns true because sex is stored as 0/1 and you're testing as female.

    +
    +
    + mc choice_list +
    +
    + multipe choice (radio buttons), you can choose only one. +
    +
    + mc_button choice_list +
    +
    + like mc but instead of the text appearing next to a small button, a big button contains each choice label +
    + +
    + mc_multiple choice_list +
    +
    + multiple multiple choice (check boxes), you can choose several. Choices defined as above. +
    +
    + mc_multiple_button +
    +
    + like mc_multiple and mc_button +
    + +
    + check +
    +
    + a single check box for confirmation of a statement. +
    +
    + check_button +
    +
    + a bigger button to check. +
    + +
    + rating_button
    min, max, step +
    +
    + This shows the choice1 label to the left, the choice2 label to the right and a series of numbered buttons as defined by min,max,step in between. Defaults to 1,5,1. +
    +
    + sex +
    +
    + shorthand for mc_button with the ♂, ♀ symbols as choices +
    +
    + select_one choice_list +
    +
    + a dropdown, you can choose only one +
    +
    + select_multiple choice_list +
    +
    + a list in which, you can choose several options +
    +
    + select_or_add_one
    choice_list, maxType +
    +
    + like select_one, but it allows users to choose an option not given. Uses Select2. maxType can be used to set an upper limit on the length of the user-added option. Defaults to 255. +
    +
    + select_or_add_multiple
    choice_list, maxType,
    maxChoose
    +
    +
    + like select_multiple and select_or_add_one, allows users to add options not given. maxChoose can be used to place an upper limit on the number of chooseable options. +
    +
    + mc_heading choice_list +
    +
    + This type permits you to show the labels for mc or mc_multiple choices only once.
    + To get the necessary tabular look, assign a constant width to the choices (using e.g. mc-width100), give the heading the same choices as the mcs, and give the following mcs (or mc_multiples) the same classes + hide_label.
    + On small screens the mc_heading will be hidden and labels will automatically be displayed again, because the tabular layout would otherwise break down. +
    @@ -195,53 +195,53 @@

    Hidden family

    These items don't require the user to do anything, so including them simply means that the relevant value will be stored. If you have exclusively hidden items in a form, things will wrap up immediately and move to the next element in the run. This can be useful for hooking up with other software which sends data over the query string i.e. https://formr.org/run_name?param1=10&user_id=29
    -
    - calculate -
    -
    - in the value column you can specify an R expression, the result of which will be saved into this variable. Useful to pull in external data or to forestall recalculating something repeatedly that you want to refer to later. If the calculation is based on values from the same module, you can insert the calculate item in the last line of the sheet behind the last submit button and its result will be stored in the database for use in further modules. -
    -
    - ip -
    -
    - saves your IP address. You should probably not do this covertly but explicitly announce it. -
    -
    - referrer -
    -
    - saves the last outside referrer (if any), ie. from which website you came to formr -
    -
    - server var -
    -
    - saves the $_SERVER value with the index given by var. Can be used to store one of 'HTTP_USER_AGENT', 'HTTP_ACCEPT', 'HTTP_ACCEPT_CHARSET', 'HTTP_ACCEPT_ENCODING', 'HTTP_ACCEPT_LANGUAGE', 'HTTP_CONNECTION', 'HTTP_HOST', 'QUERY_STRING', 'REQUEST_TIME', 'REQUEST_TIME_FLOAT'. In English: the browser, some stuff about browser language information, some server stuff, and access time. -
    -
    - get var -
    -
    - saves the var from the query string, so in the example above get param1 would lead to 10 being stored. -
    -
    - random min,max -
    -
    - generates a random number for later use (e.g. randomisation in experiments). Minimum and maximum default to 0 and 1 respectively. If you specify them, you have to specify both. -
    -
    - hidden -
    -
    - you can use this item with a pre-set value, if you need to use data from previous pages together with data on the same page for a showif -
    -
    - block -
    -
    - Blocks progress. You can give this item a showif such as (item1 + item2) > 100 to add further requirements. -
    +
    + calculate +
    +
    + in the value column you can specify an R expression, the result of which will be saved into this variable. Useful to pull in external data or to forestall recalculating something repeatedly that you want to refer to later. If the calculation is based on values from the same module, you can insert the calculate item in the last line of the sheet behind the last submit button and its result will be stored in the database for use in further modules. +
    +
    + ip +
    +
    + saves your IP address. You should probably not do this covertly but explicitly announce it. +
    +
    + referrer +
    +
    + saves the last outside referrer (if any), ie. from which website you came to formr +
    +
    + server var +
    +
    + saves the $_SERVER value with the index given by var. Can be used to store one of 'HTTP_USER_AGENT', 'HTTP_ACCEPT', 'HTTP_ACCEPT_CHARSET', 'HTTP_ACCEPT_ENCODING', 'HTTP_ACCEPT_LANGUAGE', 'HTTP_CONNECTION', 'HTTP_HOST', 'QUERY_STRING', 'REQUEST_TIME', 'REQUEST_TIME_FLOAT'. In English: the browser, some stuff about browser language information, some server stuff, and access time. +
    +
    + get var +
    +
    + saves the var from the query string, so in the example above get param1 would lead to 10 being stored. +
    +
    + random min,max +
    +
    + generates a random number for later use (e.g. randomisation in experiments). Minimum and maximum default to 0 and 1 respectively. If you specify them, you have to specify both. +
    +
    + hidden +
    +
    + you can use this item with a pre-set value, if you need to use data from previous pages together with data on the same page for a showif +
    +
    + block +
    +
    + Blocks progress. You can give this item a showif such as (item1 + item2) > 100 to add further requirements. +
    diff --git a/application/View/public/documentation/knitr_markdown.php b/application/View/public/documentation/knitr_markdown.php index b532b8216..5a945a6cf 100644 --- a/application/View/public/documentation/knitr_markdown.php +++ b/application/View/public/documentation/knitr_markdown.php @@ -1,70 +1,70 @@

    Knit R & Markdown


    - +

    - This section gives some guidance on how to format and customise text in formr. In many cases you'll do it right by default. + This section gives some guidance on how to format and customise text in formr. In many cases you'll do it right by default.

    - Markdown + Markdown

    - You can format text/feedback everywhere (i.e. item labels, choice labels, the feedback shown in pauses, stops, in emails) in a natural fashion using Github-flavoured Markdown.
    - The philosophy is that you write like you would in a plain-text email and Markdown turns it nice.
    - In most cases, characters with special meaning won't entail unintended side effects if you use them normally, but if you ever need to specify that they shouldn't have side effects, escape it with a backslash: \*10\* doesn't turn italic. + You can format text/feedback everywhere (i.e. item labels, choice labels, the feedback shown in pauses, stops, in emails) in a natural fashion using Github-flavoured Markdown.
    + The philosophy is that you write like you would in a plain-text email and Markdown turns it nice.
    + In most cases, characters with special meaning won't entail unintended side effects if you use them normally, but if you ever need to specify that they shouldn't have side effects, escape it with a backslash: \*10\* doesn't turn italic.

     * list item 1
     * list item 2
     

    - will turn into a nice bulleted list. + will turn into a nice bulleted list.

      -
    • list item 1 -
    • -
    • list item 2 -
    • +
    • list item 1 +
    • +
    • list item 2 +

    - # at the beginning of a line turns it into a large headline, ## up to ###### turn it into smaller ones. + # at the beginning of a line turns it into a large headline, ## up to ###### turn it into smaller ones.

    - *italics* and __bold__ are also easy to do. + *italics* and __bold__ are also easy to do.

    [Named links](http://yihui.name/knitr/) and embedded images ![image description](http://imgur.com/imagelink) are easy. If you simply paste a link, it will be clickable automatically too, even easier. Email addresses are a bit special, you need the "mailto:" prefix: [Contact us](mailto:contact_email@example.com).

    - You can quote something by placing a > at the beginning of the line. + You can quote something by placing a > at the beginning of the line.

    - If you're already familiar with HTML you can also use that instead, though it is a little less readable for humans. Or mix it with Markdown! You may for example use it to go beyond Markdown's features and e.g. add icons to your text using <i class="fa fa-smile-o"></i> to get for instance. Check the full set of available icons at Font Awesome. + If you're already familiar with HTML you can also use that instead, though it is a little less readable for humans. Or mix it with Markdown! You may for example use it to go beyond Markdown's features and e.g. add icons to your text using <i class="fa fa-smile-o"></i> to get for instance. Check the full set of available icons at Font Awesome.

    - Knitr + Knitr

    - If you want to customise the text or generate custom feedback, including plots, you can use Knitr. Thanks to Knitr you can freely mix Markdown and chunks of R. Some examples: + If you want to customise the text or generate custom feedback, including plots, you can use Knitr. Thanks to Knitr you can freely mix Markdown and chunks of R. Some examples:

      -
    • - - - Today is `r date()` shows today's date.
      -
    • -
    • - - - Hello `r demographics$name` greets someone using the variable "name" from the survey "demographics".
      -
    • -
    • - - - Dear `r ifelse(demographics$sex == 1, 'Sir', 'Madam')` greets someone differently based on the variable "sex" from the survey "demographics".
      -
    • -
    • - - You can also plot someone's extraversion on the standard normal distribution. -
      ```{r}
      +    
    • + + + Today is `r date()` shows today's date.
      +
    • +
    • + + + Hello `r demographics$name` greets someone using the variable "name" from the survey "demographics".
      +
    • +
    • + + + Dear `r ifelse(demographics$sex == 1, 'Sir', 'Madam')` greets someone differently based on the variable "sex" from the survey "demographics".
      +
    • +
    • + + You can also plot someone's extraversion on the standard normal distribution. +
      ```{r}
       library(formr)
       # build scales automatically
       big5 = formr_aggregate(results = big5)
      @@ -74,7 +74,7 @@
       # plot
       qplot_on_normal(big$extraversion, xlab = "Extraversion")
       ```
      -
      yields
      - Graph of extraversion bell curve feedback -
    • +
      yields
      + Graph of extraversion bell curve feedback +
    \ No newline at end of file diff --git a/application/View/public/documentation/r_helpers.php b/application/View/public/documentation/r_helpers.php index d61b23051..0d3e14da3 100644 --- a/application/View/public/documentation/r_helpers.php +++ b/application/View/public/documentation/r_helpers.php @@ -1,23 +1,23 @@

    R Helpers


    - Wherever you use R in formr you can also use the functions in its R package. If you want to use the package in a different environment, - you'll need to install it using these two lines of code. + Wherever you use R in formr you can also use the functions in its R package. If you want to use the package in a different environment, + you'll need to install it using these two lines of code.

    install.packages("devtools")
     devtools::install_github("rubenarslan/formr")

    The package currently has the following feature sets

      -
    • Some shorthand functions for frequently needed operations on the site: -
      first(cars); 
      +    
    • Some shorthand functions for frequently needed operations on the site: +
      first(cars); 
       last(cars); 
       current(cars); 
       "formr." %contains% "mr."
    • -
    • Some helper functions to make it easier to correctly deal with dates and times: -
      time_passed(hours = 7); 
      +    
    • Some helper functions to make it easier to correctly deal with dates and times: +
      time_passed(hours = 7); 
       next_day(); 
       in_time_window(time1, time2);
    • -
    • Connecting to formr, importing your data, correctly typing all variables, automatically aggregating scales.
    • -
    • Easily making feedback plots e.g.
      qplot_on_normal(0.8, "Extraversion")
      - The package also has a function to simulate possible data, so you can try to make feedback plots ahead of collecting data.
    • +
    • Connecting to formr, importing your data, correctly typing all variables, automatically aggregating scales.
    • +
    • Easily making feedback plots e.g.
      qplot_on_normal(0.8, "Extraversion")
      + The package also has a function to simulate possible data, so you can try to make feedback plots ahead of collecting data.
    \ No newline at end of file diff --git a/application/View/public/documentation/run_module_explanations.php b/application/View/public/documentation/run_module_explanations.php index aff5e0df1..324a9a46c 100644 --- a/application/View/public/documentation/run_module_explanations.php +++ b/application/View/public/documentation/run_module_explanations.php @@ -1,412 +1,412 @@

    formr Runs


    - A formr "run" contains your study's complete design. Designs can range from the simple (a single survey or a randomised experiment) to the complex (like a diary study with daily reminders by email and text message or a longitudinal study tracking social network changes). + A formr "run" contains your study's complete design. Designs can range from the simple (a single survey or a randomised experiment) to the complex (like a diary study with daily reminders by email and text message or a longitudinal study tracking social network changes).

    -
    - -
    -
    -

    - Inside a run, participants' data can be connected, so you can track how many times a participant filled out her diary or whether her social network grew in size since the first measurement timepoint. -

    -

    - So, why "run"? In formr, runs consist of simple modules that are chained together linearly. Because most modules are boombox-themed, it may help to think of a tape running. Using controls such as the skip backward button, the pause button and the stop button, you control the participant's progression along the run. Surveys can be thought of as the record button: whenever you place a survey in your run, the participant can input data. -

    -

    - Because data are supplied on-the-fly to the statistics programming language R, you can dynamically generate feedback graphics for your participants with minimal programming knowledge. With more programming knowledge, nothing keeps you from making full use of R. You could for example conduct complex sentiment analyses on participants' tweets and invite them to follow-up surveys only if they express anger. -

    -

    - Since runs contain your study's complete design, it makes sense that runs' administration side is where every user management-related action takes place. There is an overview of users, where you can see at which position in the run each participant is and when they were last active. Here, you can send people custom reminders (if they are running late), shove them to a different position in the run (if they get lost somewhere due to an unforeseen complication) or see what the study looks like for them (if they report problems). -

    -

    - Runs are also where you customise your study's look, upload files (such as images), control access and enrollment. In addition, there are logs of every email sent, every position a participant has visited and of automatic progressions (the cron job). -

    -
    -
    -
    +
    + +
    +
    +

    + Inside a run, participants' data can be connected, so you can track how many times a participant filled out her diary or whether her social network grew in size since the first measurement timepoint. +

    +

    + So, why "run"? In formr, runs consist of simple modules that are chained together linearly. Because most modules are boombox-themed, it may help to think of a tape running. Using controls such as the skip backward button, the pause button and the stop button, you control the participant's progression along the run. Surveys can be thought of as the record button: whenever you place a survey in your run, the participant can input data. +

    +

    + Because data are supplied on-the-fly to the statistics programming language R, you can dynamically generate feedback graphics for your participants with minimal programming knowledge. With more programming knowledge, nothing keeps you from making full use of R. You could for example conduct complex sentiment analyses on participants' tweets and invite them to follow-up surveys only if they express anger. +

    +

    + Since runs contain your study's complete design, it makes sense that runs' administration side is where every user management-related action takes place. There is an overview of users, where you can see at which position in the run each participant is and when they were last active. Here, you can send people custom reminders (if they are running late), shove them to a different position in the run (if they get lost somewhere due to an unforeseen complication) or see what the study looks like for them (if they report problems). +

    +

    + Runs are also where you customise your study's look, upload files (such as images), control access and enrollment. In addition, there are logs of every email sent, every position a participant has visited and of automatic progressions (the cron job). +

    +
    +
    +

    - Module explanations + Module explanations

    -
    - -
    -
    -

    - Surveys are series of questions (or other items) that are created using simple spreadsheets/item tables (e.g. Excel). -

    -

    - Survey item tables are just spreadsheets – and they can just as easily be shared, reused, recycled and collaboratively edited using e.g. Google Sheets. -

    -

    - Surveys can remain fairly simple: a bunch of items that belong together and that a participant can respond to in one sitting. For some people, simple surveys are all they need, but in formr a survey always has to be part of a (simple) run. -

    -

    - Surveys can feature various items, allowing e.g. numeric, textual input, agreement on a Likert scale, geolocation and so on. -

    -

    - Items can be optionally shown depending on the participant's responses in the same survey, in previous surveys and entirely different data sources (e.g. data gleaned from Facebook activity). Item labels and choice labels can also be customised using ">knitr, so you can e.g. refer to a participant's pet or last holiday location by name or address men and women differently. -

    -

    - For R-savvy personality psychologists, formr includes a few nice timesavers. Data import can be automated without any funny format business and items will be correctly typed according to the item table, not according to flawed heuristics.
    - If you name your items according to the schema BFI_extra_2R, items with an R at the end can be automatically reversed and items ending on consecutive numbers with the same prefix will be aggregated to a mean score (with the name of the prefix). Internal consistency analyses and item frequency plots can also be automatically generated. Hence, some tedious manual data wrangling can be avoided and as an added benefit, you will start giving your items meaningful and memorable names early on. The relevant functions can be found in the R package on Github. The functions are also always available, whenever you use R inside formr runs and surveys. -

    -
    -
    -
    -
    - -
    -
    -

    - These are external links - use them to send participants to other, specialised data collection modules, such as a social network generator, a reaction time task, another survey software (we won't be too sad), anything really. However, you can also simply call upon external functionality without sending the participant anywhere – one popular application of this is sending text messages. -

    -

    - If you insert the placeholder {{login_code}}, it will be replaced by the participant's run session code, allowing you to link data later (but only if your external module picks this variable up!). -

    -

    - Sometimes, you may find yourself wanting to do more complicated stuff like (a) sending along more data, the participant's age or sex for example, (b) calling an API to do some operations before the participant is sent off (e.g. making sure the other end is ready to receive, this is useful if you plan to integrate formr tightly with some other software) (c) redirecting the participant to a large number of custom links (e.g. you want to redirect participants to the profile of the person who last commented on their Facebook wall to assess closeness) (d) you want to optionally redirect participants back to the run (e.g. as a fallback or to do complicated stuff in formr). -

    -

    - You can either choose to "finish/wrap up" this component before the participant is redirected (the simple way) or enable your external module to call our API to close it only once the external component is finished (the proper way). If you do the latter, the participant will always be redirected to the external page until that page makes the call that the required input has been made. -

    -
    -
    -
    -
    - - -
    -
    - -
    -
    -

    - Skip backward allows you to jump back in the run, if a specific condition is fulfilled. -

    -

    - This way, you can create a loop. Loops, especially in combination with reminder emails are useful for diary, training, and experience sampling studies.
    -

    -

    - The condition is specified in R and all necessary survey data is automatically available. The simplest condition would be TRUE – always skip back, no matter what. A slightly more complex one is nrow(diary) < 14, this means that the diary must have been filled out at least fourteen times. Even more complex: nrow(diary) < 14 | !time_passed(days = 20, time = first(diary$created)), this means that at least 20 days must have passed since the first diary was done and that at least 14 diaries must have been filled out. But any complexity is possible, as shown in Example 2. -

    -
    - Example 1: -
    -

    - A simple diary. Let's say your run contains -

    -
      -
    • - Pos. 10. a survey in which you find out the participant's email address -
    • -
    • - Pos. 20. a pause which always waits until 6PM on the next day -
    • -
    • - Pos. 30. an email invitation -
    • -
    • - Pos. 40. a survey called diary containing your diary questions -
    • -
    • - Pos. 50. You would now add a Skip Backward with the following condition: nrow(diary) < 14 and the instructions to jump back to position 20, the pause, if that is true. -
    • -
    • - Pos. 60. At this position you could then use a Stop point, marking the end of your diary study. -
    • -
    -
    - What would happen? -
    -

    - Starting at 20, participants would receive their first invitation to the diary at 6PM the next day after enrolling. After completion, the Skip Backward would send them back to the pause, where you could thank them for completing today's diary and instruct them to close their web browser. Automatically, once it is 6PM the next day, they would receive another invitation, complete another diary etc. Once this cycle repeated 14 times, the condition would no longer be true and they would progress to position 60, where they might receive feedback on their mood fluctuation in the diary. -

    -
    - Example 2: -
    -

    - But you can also make a loop that doesn't involve user action, to periodically check for external events: -

    -
      -
    • - Pos. 10. a short survey called location that mostly just asks for the participants' GPS coordinates and contact info -
    • -
    • - Pos. 20. a pause which always waits one day -
    • -
    • - Pos. 30. A Skip Backward checks which checks the weather at the participant's GPS coordinates. If no thunderstorm occurred there, it jumps back to the pause at position 20. If a storm occurred, however, it progresses. -
    • -
    • - Pos. 40. an email invitation -
    • -
    • - Pos. 50. a survey called storm_mood containing your questions regarding the participant's experience of the storm. -
    • -
    • - Pos. 60. A stop button, ending the study. -
    • -
    -
    - What would happen? -
    -

    - In this scenario, the participant takes part in the short survey first. We obtain the geolocation, which can be used to retrieve the local weather using API calls to weather information services in the Skip Backward at position 30. The weather gets checked once each day (pause at 20) and if there ever is a thunderstorm in the area, the participant is invited via email (40) to take a survey (50) detailing their experience of the thunderstorm. This way, the participants only get invited when necessary, we don't have to ask them to report weather events on a daily basis and risk driving them away. -

    -
    -
    -
    -
    - -
    -
    -

    - This simple component allows you to delay the continuation of the run, be it
    - until a certain date (01.01.2014 for research on new year's hangovers),
    - time of day (asking participants to sum up their day in their diary after 7PM)
    - or to wait relative to a date that a participant specified (such as her graduation date or the last time he cut his nails). -

    -
      -
    • - Pos. 10. a survey collecting personality and contact info + the graduation data of university students -
    • -
    • - Pos. 20. a pause which waits until 4 months after graduation -
    • -
    • - Pos. 30. an email invitation -
    • -
    • - Pos. 40. another personality survey -
    • -
    • - Pos. 50. A stop button, ending the study. On this last page, the students get feedback on how their personality has changed after graduation. -
    • -
    -

    - See the Knitr & Markdown section to find out how to personalise the text shown while waiting. -

    -
    -
    -
    -
    - -
    -
    -

    - Skip forward allows you to jump forward in the run, if a specific condition is fulfilled. There are many simple but also complicated applications for this. Skip forward modules can do three actions: 1. skip to the specified position 2. go on (to the next position) 3. stay and re-evaluate again in a few minutes. The last action can be specified to take place if the participant hasn't reacted yet. -

    -

    - Example 1: a filter/screening -

    -

    - Let's say your run contains -

    -
      -
    • - Pos. 10. a survey (depression) which has an item about suicidality -
    • -
    • - Pos. 20. a Skip Forward which checks depression$suicidal != 1. If the person is not suicidal, it skips forward to pos 40. -
    • -
    • - Pos. 30. At this position you would use a Stop point. Here you could give the participant the numbers for suicide hotlines and tell them they're not eligible to participate. -
    • -
    • Pos. 40. Here you could do your real survey. -
    • -
    • - Pos. 50. A stop button, ending the study. -
    • -
    -
    - What would happen? -
    -

    - Starting at 10, participants would complete a survey on depression. If they indicated suicidal tendencies, they would receive the numbers for suicide hotlines at which point the run would end for them. If they did not indicate suicidal tendencies, they would be eligible to participate in the main survey. -

    -

    - Example 2: different paths -

    -

    - Let's say your run contains -

    -
      -
    • Pos. 10. a survey on optimism (optimism) -
    • -
    • Pos. 20. a Skip Forward which checks optimism$pessimist == 1. If the person is a pessimist, it skips forward to pos 5. -
    • -
    • Pos. 30. a survey tailored to optimists -
    • -
    • Pos. 40. a Skip Forward which checks TRUE, so it always skips forward to pos 6. -
    • -
    • Pos. 50. a survey tailored to pessimists -
    • -
    • - Pos. 60. At this position you would thank both optimists and pessimists for their participation. -
    • -
    -
    - What would happen? -
    -

    - Starting at 10, participants would complete a survey on optimism. If they indicated that they are pessimists, they fill out a different survey than if they are optimists. Both groups receive the same feedback at the end. It is important to note that we have to let the optimists jump over the survey tailored to pessimists at position 40, so that they do not have to take both surveys. -

    -

    - Example 3: reminders and access windows -

    -

    - Let's say your run contains -

    -
      -
    • Pos. 10. a waiting period (e.g. let's say we know when exchange students will arrive in their host country, and do not ask questions before they've been there one week)
    • -
    • Pos. 20. Now we have to send our exchange students an email to invite them to do the survey.
    • -
    • Pos. 30. a Skip Forward which checks ! time_passed(weeks = 2), i.e. "two weeks have not passed yet". The first dropdown (if two weeks have not passed) is set to "if user reacts", only then jump to pos. 60 (the survey). The second (else if two weeks have passed) is set to go on "automatically".
    • -
    • Pos. 40. This is our email reminder for the students who did not react after one week.
    • -
    • Pos. 50. a Skip Forward which checks time_passed(weeks = 8), .e. "eight weeks have passed". The first dropdown (if eight weeks have passed) is set to "automatically", only then jump to pos. 70 (wait for return home), the second (else if: "while eight weeks have not yet passed") is set to go on only "if user reacts".
    • -
    • Pos. 60. the survey we want the exchange students to fill out
    • -
    • Pos. 70. Because this is a longitudinal study, we now wait for our exchange students to return home. The rest is left out.
    • +
      + +
      +
      +

      + Surveys are series of questions (or other items) that are created using simple spreadsheets/item tables (e.g. Excel). +

      +

      + Survey item tables are just spreadsheets – and they can just as easily be shared, reused, recycled and collaboratively edited using e.g. Google Sheets. +

      +

      + Surveys can remain fairly simple: a bunch of items that belong together and that a participant can respond to in one sitting. For some people, simple surveys are all they need, but in formr a survey always has to be part of a (simple) run. +

      +

      + Surveys can feature various items, allowing e.g. numeric, textual input, agreement on a Likert scale, geolocation and so on. +

      +

      + Items can be optionally shown depending on the participant's responses in the same survey, in previous surveys and entirely different data sources (e.g. data gleaned from Facebook activity). Item labels and choice labels can also be customised using ">knitr, so you can e.g. refer to a participant's pet or last holiday location by name or address men and women differently. +

      +

      + For R-savvy personality psychologists, formr includes a few nice timesavers. Data import can be automated without any funny format business and items will be correctly typed according to the item table, not according to flawed heuristics.
      + If you name your items according to the schema BFI_extra_2R, items with an R at the end can be automatically reversed and items ending on consecutive numbers with the same prefix will be aggregated to a mean score (with the name of the prefix). Internal consistency analyses and item frequency plots can also be automatically generated. Hence, some tedious manual data wrangling can be avoided and as an added benefit, you will start giving your items meaningful and memorable names early on. The relevant functions can be found in the R package on Github. The functions are also always available, whenever you use R inside formr runs and surveys. +

      +
      +
      +
      +
      + +
      +
      +

      + These are external links - use them to send participants to other, specialised data collection modules, such as a social network generator, a reaction time task, another survey software (we won't be too sad), anything really. However, you can also simply call upon external functionality without sending the participant anywhere – one popular application of this is sending text messages. +

      +

      + If you insert the placeholder {{login_code}}, it will be replaced by the participant's run session code, allowing you to link data later (but only if your external module picks this variable up!). +

      +

      + Sometimes, you may find yourself wanting to do more complicated stuff like (a) sending along more data, the participant's age or sex for example, (b) calling an API to do some operations before the participant is sent off (e.g. making sure the other end is ready to receive, this is useful if you plan to integrate formr tightly with some other software) (c) redirecting the participant to a large number of custom links (e.g. you want to redirect participants to the profile of the person who last commented on their Facebook wall to assess closeness) (d) you want to optionally redirect participants back to the run (e.g. as a fallback or to do complicated stuff in formr). +

      +

      + You can either choose to "finish/wrap up" this component before the participant is redirected (the simple way) or enable your external module to call our API to close it only once the external component is finished (the proper way). If you do the latter, the participant will always be redirected to the external page until that page makes the call that the required input has been made. +

      +
      +
      +
      +
      + + +
      +
      + +
      +
      +

      + Skip backward allows you to jump back in the run, if a specific condition is fulfilled. +

      +

      + This way, you can create a loop. Loops, especially in combination with reminder emails are useful for diary, training, and experience sampling studies.
      +

      +

      + The condition is specified in R and all necessary survey data is automatically available. The simplest condition would be TRUE – always skip back, no matter what. A slightly more complex one is nrow(diary) < 14, this means that the diary must have been filled out at least fourteen times. Even more complex: nrow(diary) < 14 | !time_passed(days = 20, time = first(diary$created)), this means that at least 20 days must have passed since the first diary was done and that at least 14 diaries must have been filled out. But any complexity is possible, as shown in Example 2. +

      +
      + Example 1: +
      +

      + A simple diary. Let's say your run contains +

      +
        +
      • + Pos. 10. a survey in which you find out the participant's email address +
      • +
      • + Pos. 20. a pause which always waits until 6PM on the next day +
      • +
      • + Pos. 30. an email invitation +
      • +
      • + Pos. 40. a survey called diary containing your diary questions +
      • +
      • + Pos. 50. You would now add a Skip Backward with the following condition: nrow(diary) < 14 and the instructions to jump back to position 20, the pause, if that is true. +
      • +
      • + Pos. 60. At this position you could then use a Stop point, marking the end of your diary study. +
      • +
      +
      + What would happen? +
      +

      + Starting at 20, participants would receive their first invitation to the diary at 6PM the next day after enrolling. After completion, the Skip Backward would send them back to the pause, where you could thank them for completing today's diary and instruct them to close their web browser. Automatically, once it is 6PM the next day, they would receive another invitation, complete another diary etc. Once this cycle repeated 14 times, the condition would no longer be true and they would progress to position 60, where they might receive feedback on their mood fluctuation in the diary. +

      +
      + Example 2: +
      +

      + But you can also make a loop that doesn't involve user action, to periodically check for external events: +

      +
        +
      • + Pos. 10. a short survey called location that mostly just asks for the participants' GPS coordinates and contact info +
      • +
      • + Pos. 20. a pause which always waits one day +
      • +
      • + Pos. 30. A Skip Backward checks which checks the weather at the participant's GPS coordinates. If no thunderstorm occurred there, it jumps back to the pause at position 20. If a storm occurred, however, it progresses. +
      • +
      • + Pos. 40. an email invitation +
      • +
      • + Pos. 50. a survey called storm_mood containing your questions regarding the participant's experience of the storm. +
      • +
      • + Pos. 60. A stop button, ending the study. +
      • +
      +
      + What would happen? +
      +

      + In this scenario, the participant takes part in the short survey first. We obtain the geolocation, which can be used to retrieve the local weather using API calls to weather information services in the Skip Backward at position 30. The weather gets checked once each day (pause at 20) and if there ever is a thunderstorm in the area, the participant is invited via email (40) to take a survey (50) detailing their experience of the thunderstorm. This way, the participants only get invited when necessary, we don't have to ask them to report weather events on a daily basis and risk driving them away. +

      +
      +
      +
      +
      + +
      +
      +

      + This simple component allows you to delay the continuation of the run, be it
      + until a certain date (01.01.2014 for research on new year's hangovers),
      + time of day (asking participants to sum up their day in their diary after 7PM)
      + or to wait relative to a date that a participant specified (such as her graduation date or the last time he cut his nails). +

      +
        +
      • + Pos. 10. a survey collecting personality and contact info + the graduation data of university students +
      • +
      • + Pos. 20. a pause which waits until 4 months after graduation +
      • +
      • + Pos. 30. an email invitation +
      • +
      • + Pos. 40. another personality survey +
      • +
      • + Pos. 50. A stop button, ending the study. On this last page, the students get feedback on how their personality has changed after graduation. +
      • +
      +

      + See the Knitr & Markdown section to find out how to personalise the text shown while waiting. +

      +
      +
      +
      +
      + +
      +
      +

      + Skip forward allows you to jump forward in the run, if a specific condition is fulfilled. There are many simple but also complicated applications for this. Skip forward modules can do three actions: 1. skip to the specified position 2. go on (to the next position) 3. stay and re-evaluate again in a few minutes. The last action can be specified to take place if the participant hasn't reacted yet. +

      +

      + Example 1: a filter/screening +

      +

      + Let's say your run contains +

      +
        +
      • + Pos. 10. a survey (depression) which has an item about suicidality +
      • +
      • + Pos. 20. a Skip Forward which checks depression$suicidal != 1. If the person is not suicidal, it skips forward to pos 40. +
      • +
      • + Pos. 30. At this position you would use a Stop point. Here you could give the participant the numbers for suicide hotlines and tell them they're not eligible to participate. +
      • +
      • Pos. 40. Here you could do your real survey. +
      • +
      • + Pos. 50. A stop button, ending the study. +
      • +
      +
      + What would happen? +
      +

      + Starting at 10, participants would complete a survey on depression. If they indicated suicidal tendencies, they would receive the numbers for suicide hotlines at which point the run would end for them. If they did not indicate suicidal tendencies, they would be eligible to participate in the main survey. +

      +

      + Example 2: different paths +

      +

      + Let's say your run contains +

      +
        +
      • Pos. 10. a survey on optimism (optimism) +
      • +
      • Pos. 20. a Skip Forward which checks optimism$pessimist == 1. If the person is a pessimist, it skips forward to pos 5. +
      • +
      • Pos. 30. a survey tailored to optimists +
      • +
      • Pos. 40. a Skip Forward which checks TRUE, so it always skips forward to pos 6. +
      • +
      • Pos. 50. a survey tailored to pessimists +
      • +
      • + Pos. 60. At this position you would thank both optimists and pessimists for their participation. +
      • +
      +
      + What would happen? +
      +

      + Starting at 10, participants would complete a survey on optimism. If they indicated that they are pessimists, they fill out a different survey than if they are optimists. Both groups receive the same feedback at the end. It is important to note that we have to let the optimists jump over the survey tailored to pessimists at position 40, so that they do not have to take both surveys. +

      +

      + Example 3: reminders and access windows +

      +

      + Let's say your run contains +

      +
        +
      • Pos. 10. a waiting period (e.g. let's say we know when exchange students will arrive in their host country, and do not ask questions before they've been there one week)
      • +
      • Pos. 20. Now we have to send our exchange students an email to invite them to do the survey.
      • +
      • Pos. 30. a Skip Forward which checks ! time_passed(weeks = 2), i.e. "two weeks have not passed yet". The first dropdown (if two weeks have not passed) is set to "if user reacts", only then jump to pos. 60 (the survey). The second (else if two weeks have passed) is set to go on "automatically".
      • +
      • Pos. 40. This is our email reminder for the students who did not react after one week.
      • +
      • Pos. 50. a Skip Forward which checks time_passed(weeks = 8), .e. "eight weeks have passed". The first dropdown (if eight weeks have passed) is set to "automatically", only then jump to pos. 70 (wait for return home), the second (else if: "while eight weeks have not yet passed") is set to go on only "if user reacts".
      • +
      • Pos. 60. the survey we want the exchange students to fill out
      • +
      • Pos. 70. Because this is a longitudinal study, we now wait for our exchange students to return home. The rest is left out.
      • -
      -
      - What would happen? -
      -

      - The pause would simply lead to all exchange students being invited once they've been in their host country for a week (we left out the part where we obtained or entered the necessary information). After the invitation, however, we don't just give up, if they don't react. After another week has passed (two weeks in the host country), we remind them.
      - How is this done? It's just a little tricky:
      - The condition at pos. 30 says "if two weeks have not passed". This is true directly after we sent the email.
      - Therefore we set the first dropdown to "if user reacts" (usually it's set to "automatically").
      - Now if he doesn't answer for two weeks, the condition will become false and the run will automatically go on to 40 (the else conditon), to our email reminder (tentatively titled "Oh lover boy..."). We hope the participant clicks on the link in our invitation email before then though.
      - If he does, he will jump to the survey at position 60.
      - If he still doesn't answer, we will patiently wait for another eight weeks. This time, it's the other way around, if the condition is false (eight weeks have not passed) and the user reacts, he goes on to the survey. If the condition turns true, we automatically jump to position 70, which stands for waiting for return home, i.e. we gave up on getting a reaction in the first wave (but we still have "Baby, oh baby, My sweet baby, you're the one" up our sleeve). -

      -
      -
      -
      -
      - -
      -
      -

      - You will always need at least one. These are stop points in your run, where you can give short or complex feedback, ranging from "You're not eligible to participate." to "This is the scatter plot of your mood and your alcohol consumption across the last two weeks". -

      -

      - If you combine these end points with Skip Forward, you can have several in your run: You would use the Skip Forward to check whether participants are eligible, and if so, skip over the stop point between the Skip Forward and the survey that they are eligible for. This way, ineligible participants end up in a dead end before the survey. In the edit run interface, you can see green counts of the number of people on this position on the left, so you can see easily how many people are ineligible by checking the count.
      - See the Knitr & Markdown section to find out how to generate personalised feedback, including plots. -

      -
      -
      -
      -
      - -
      -
      -

      - This is a very simple component. You simply choose how many groups you want to randomly assign your participants to. We start counting at one (1), so if you have two groups you will check shuffle$group == 1 and shuffle$group == 2. You can read a person's group using shuffle$group. If you generate random groups at more than one point in a run, you might have to use the last one tail(shuffle$group,1) or check the unit id shuffle$unit_id, but usually you needn't do this. -

      -

      - If you combine a Shuffle with Skip Forward, you could send one group to an entirely different arm/path of the study. But maybe you just want to randomly switch on a specific item in a survey - then you would use a "showif" in the survey item table containing e.g. shuffle$group == 2. The randomisation always has to occur before you try to use the number, but the participants won't notice it unless you tell them somehow (for example by switching on a note telling them which group they've been assigned to). -

      -
      -
      -
      +
    +
    + What would happen? +
    +

    + The pause would simply lead to all exchange students being invited once they've been in their host country for a week (we left out the part where we obtained or entered the necessary information). After the invitation, however, we don't just give up, if they don't react. After another week has passed (two weeks in the host country), we remind them.
    + How is this done? It's just a little tricky:
    + The condition at pos. 30 says "if two weeks have not passed". This is true directly after we sent the email.
    + Therefore we set the first dropdown to "if user reacts" (usually it's set to "automatically").
    + Now if he doesn't answer for two weeks, the condition will become false and the run will automatically go on to 40 (the else conditon), to our email reminder (tentatively titled "Oh lover boy..."). We hope the participant clicks on the link in our invitation email before then though.
    + If he does, he will jump to the survey at position 60.
    + If he still doesn't answer, we will patiently wait for another eight weeks. This time, it's the other way around, if the condition is false (eight weeks have not passed) and the user reacts, he goes on to the survey. If the condition turns true, we automatically jump to position 70, which stands for waiting for return home, i.e. we gave up on getting a reaction in the first wave (but we still have "Baby, oh baby, My sweet baby, you're the one" up our sleeve). +

    +
    +
    +
    +
    + +
    +
    +

    + You will always need at least one. These are stop points in your run, where you can give short or complex feedback, ranging from "You're not eligible to participate." to "This is the scatter plot of your mood and your alcohol consumption across the last two weeks". +

    +

    + If you combine these end points with Skip Forward, you can have several in your run: You would use the Skip Forward to check whether participants are eligible, and if so, skip over the stop point between the Skip Forward and the survey that they are eligible for. This way, ineligible participants end up in a dead end before the survey. In the edit run interface, you can see green counts of the number of people on this position on the left, so you can see easily how many people are ineligible by checking the count.
    + See the Knitr & Markdown section to find out how to generate personalised feedback, including plots. +

    +
    +
    +
    +
    + +
    +
    +

    + This is a very simple component. You simply choose how many groups you want to randomly assign your participants to. We start counting at one (1), so if you have two groups you will check shuffle$group == 1 and shuffle$group == 2. You can read a person's group using shuffle$group. If you generate random groups at more than one point in a run, you might have to use the last one tail(shuffle$group,1) or check the unit id shuffle$unit_id, but usually you needn't do this. +

    +

    + If you combine a Shuffle with Skip Forward, you could send one group to an entirely different arm/path of the study. But maybe you just want to randomly switch on a specific item in a survey - then you would use a "showif" in the survey item table containing e.g. shuffle$group == 2. The randomisation always has to occur before you try to use the number, but the participants won't notice it unless you tell them somehow (for example by switching on a note telling them which group they've been assigned to). +

    +
    +
    +
    diff --git a/application/View/public/documentation/sample_choices_sheet.php b/application/View/public/documentation/sample_choices_sheet.php index 1823139dd..4c3eadd87 100644 --- a/application/View/public/documentation/sample_choices_sheet.php +++ b/application/View/public/documentation/sample_choices_sheet.php @@ -2,74 +2,74 @@ You can clone a Google spreadsheet to get started. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    - list_name - - name - - label -
    - agreement - - 1 - - disagree completely -
    - agreement - - 2 - - rather disagree -
    - agreement - - 3 - - neither agree nor disagree -
    - agreement - - 4 - - rather agree -
    - agreement - - 5 - - agree completely -
    \ No newline at end of file + + + + list_name + + + name + + + label + + + + + + + agreement + + + 1 + + + disagree completely + + + + + agreement + + + 2 + + + rather disagree + + + + + agreement + + + 3 + + + neither agree nor disagree + + + + + agreement + + + 4 + + + rather agree + + + + + agreement + + + 5 + + + agree completely + + + + \ No newline at end of file diff --git a/application/View/public/documentation/sample_survey_sheet.php b/application/View/public/documentation/sample_survey_sheet.php index 475a89214..5f08b7742 100644 --- a/application/View/public/documentation/sample_survey_sheet.php +++ b/application/View/public/documentation/sample_survey_sheet.php @@ -3,291 +3,291 @@

    You can clone a Google spreadsheet to get started.

    Some helpful tips:

      -
    • - You may want to make linebreaks in Excel to format your text. In Microsoft Excel on Macs, you need to press Command ⌘+Option ⌥+Enter ↩, on Windows it is ctrl+Enter ↩. We suggest you start working from the provided sample sheet, because it already has the proper formatting and settings. In Google Spreadsheets, the combination is Option ⌥+Enter ↩. -
    • -
    • - Make text bold using __bold__, make it italic using *italic*. -
    • +
    • + You may want to make linebreaks in Excel to format your text. In Microsoft Excel on Macs, you need to press Command ⌘+Option ⌥+Enter ↩, on Windows it is ctrl+Enter ↩. We suggest you start working from the provided sample sheet, because it already has the proper formatting and settings. In Google Spreadsheets, the combination is Option ⌥+Enter ↩. +
    • +
    • + Make text bold using __bold__, make it italic using *italic*. +
    - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + - - - - - - - + + + + + + + - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + +
    - type - - name - - label - - optional - - showif -
    - text - - name - - Please enter your name - - * - -
    - number 1,130,1 - - age - - How old are you? - - -
    + type + + name + + label + + optional + + showif +
    + text + + name + + Please enter your name + + * + +
    - mc agreement - - emotional_stability1R - - I worry a lot. - - age >= 18 -
    + number 1,130,1 + + age + + How old are you? + + +
    - mc agreement - - emotional_stability2R - - I easily get nervous and unsure of myself. - - - age >= 18 -
    - mc agreement - - emotional_stability3 - - I am relaxed and not easily stressed. - - - age >= 18 -
    + mc agreement + + emotional_stability1R + + I worry a lot. + + age >= 18 +
    + mc agreement + + emotional_stability2R + + I easily get nervous and unsure of myself. + + + age >= 18 +
    + mc agreement + + emotional_stability3 + + I am relaxed and not easily stressed. + + + age >= 18 +

    Available columns

    You can use more columns than the ones shown above. Unknown column types are simply ignored, so you can use them for other information.

    The following column types exist:

    -
    - type -
    -
    - set the item type (see item types tab) -
    -
    - name -
    -
    - this is simply the name of the item. You'll use it to refer to the item in your data analysis and when making complex conditions, so adhere to a systematic naming scheme (we recommend scale1, scale2, scale3R for Likert-type items). -
    -
    - label -
    -
    - This column is the text that will be shown on the left-hand side of the answer choices (for most items). You can use Markdown formatting here. -
    -
    - showif -
    -
    - If you leave this empty, the item will always be shown. If it contains a condition, such as sex == 1, it will only be shown if that condition is true. Conditions are written in R and can be arbitrarily complex. You should always test them well. It is also possible to refer to data in other surveys using e.g. other_survey$item_name != 2. If you refer to data on the same page, items will also be shown dynamically using Javascript. -
    -
    - optional -
    -
    - Nearly all items are mandatory by default. By using * in this column, you can turns items optional instead. Using ! requires a response to items that are optional by default (check, check_button). -
    -
    - value -
    -
    - Sometimes you may want a value to be pre-set when users first fill out a form. This can be especially handy in longitudinal studies, where you want to check that e.g. contact information is still up-to-date or when you want to highlight changes across days. You can, again, use arbitrarily complex R code (e.g. a_different_survey$item1 + a_different_survey$item2) to pre-set a value, but you can also simply use 1 to choose the option with the value 1 (remember that choices for mc-family items are saved as numbers, not as the choice labels per default). There is one special word, sticky, which always pre-sets the items value to the most recently chosen value. You also have to keep in mind when pre-setting strings, that they have to be marked up in R, like this "I am text" (preferably do not use single quotes because Excel will mess them up). -
    -
    - class -
    -
    - This column can optionally be added to visually style items. Find the available classes below. -
    -
    +
    + type +
    +
    + set the item type (see item types tab) +
    +
    + name +
    +
    + this is simply the name of the item. You'll use it to refer to the item in your data analysis and when making complex conditions, so adhere to a systematic naming scheme (we recommend scale1, scale2, scale3R for Likert-type items). +
    +
    + label +
    +
    + This column is the text that will be shown on the left-hand side of the answer choices (for most items). You can use Markdown formatting here. +
    +
    + showif +
    +
    + If you leave this empty, the item will always be shown. If it contains a condition, such as sex == 1, it will only be shown if that condition is true. Conditions are written in R and can be arbitrarily complex. You should always test them well. It is also possible to refer to data in other surveys using e.g. other_survey$item_name != 2. If you refer to data on the same page, items will also be shown dynamically using Javascript. +
    +
    + optional +
    +
    + Nearly all items are mandatory by default. By using * in this column, you can turns items optional instead. Using ! requires a response to items that are optional by default (check, check_button). +
    +
    + value +
    +
    + Sometimes you may want a value to be pre-set when users first fill out a form. This can be especially handy in longitudinal studies, where you want to check that e.g. contact information is still up-to-date or when you want to highlight changes across days. You can, again, use arbitrarily complex R code (e.g. a_different_survey$item1 + a_different_survey$item2) to pre-set a value, but you can also simply use 1 to choose the option with the value 1 (remember that choices for mc-family items are saved as numbers, not as the choice labels per default). There is one special word, sticky, which always pre-sets the items value to the most recently chosen value. You also have to keep in mind when pre-setting strings, that they have to be marked up in R, like this "I am text" (preferably do not use single quotes because Excel will mess them up). +
    +
    + class +
    +
    + This column can optionally be added to visually style items. Find the available classes below. +
    +

    Optional classes for visual styling

    You might want to tinker with the look of certain form items. To do so you can use a variety of pre-set CSS classes. This is a fancy way of saying that if you make a new column in your survey sheet, call it "class" and add space-separated magic words, stuff will look different.

    These are the available styling classes:

    -
    - left100, (200, …, 900) -
    -
    - controls the width of the left-hand column (labels). The default is left300, you have 100 pixel increments to choose from. -
    -
    - right100, (200, …, 900) -
    -
    - controls the width of the right-hand column (answers). There is no default here, usually the right-hand column will extend in accordance with the display width. -
    -
    - right_offset0, (100, …, 900) -
    -
    - controls the offset (distance) of the right-hand column to the left (not the label column, just the left). This is 300 pixels (+20 extra) by default. Analogously with left_offset100 etc. (defaults to 0). -
    - -
    - label_align_left
    label_align_center
    label_align_right -
    -
    - controls the text alignment of the left-hand column (labels), by default it is aligned to the right. -
    -
    - answer_align_left
    answer_align_center
    answer_align_right -
    -
    - controls the text alignment of the right-hand column (answers), by default it is aligned to the left. -
    - -
    - answer_below_label -
    -
    - This leads to answers stacking below labels, instead of them being side-by-side (the default). It entails zero offsets and left alignment. Can be overridden with offsets and the alignment classes. -
    - -
    - hide_label -
    -
    - This hides the labels for mc and mc_multiple replies. Useful in combination with a fixed width for mc, mc_multiple labels and mc_heading – this way you can achieve a tabular layout. On small screens labels will automatically be displayed again, because the tabular layout cannot be maintained then. -
    - -
    - show_value_instead_of_label -
    -
    - This hides the labels for mc_button and mc_multiple_button, instead it shows their values (useful numbers from 1 to x). Useful in combination with mc_heading – this way you can achieve a tabular layout. On small screens labels will automatically be displayed again, because the tabular layout cannot be maintained then. -
    - -
    - rotate_label45, rotate_label30,
    rotate_label90 -
    -
    - This rotates the labels for mc and mc_multiple replies. Useful if some have long words, that would lead to exaggerated widths for one answer column. -
    - - -
    - mc_block -
    -
    - This turns answer labels for mc-family items into blocks, so that lines break before and after the label. -
    - -
    - mc_vertical -
    -
    - This makes answer labels for mc-family items stack up. Useful if you have so many options, that they jut out of the viewport. If you have very long list, consider using select-type items instead (they come with a search function). -
    +
    + left100, (200, …, 900) +
    +
    + controls the width of the left-hand column (labels). The default is left300, you have 100 pixel increments to choose from. +
    +
    + right100, (200, …, 900) +
    +
    + controls the width of the right-hand column (answers). There is no default here, usually the right-hand column will extend in accordance with the display width. +
    +
    + right_offset0, (100, …, 900) +
    +
    + controls the offset (distance) of the right-hand column to the left (not the label column, just the left). This is 300 pixels (+20 extra) by default. Analogously with left_offset100 etc. (defaults to 0). +
    + +
    + label_align_left
    label_align_center
    label_align_right +
    +
    + controls the text alignment of the left-hand column (labels), by default it is aligned to the right. +
    +
    + answer_align_left
    answer_align_center
    answer_align_right +
    +
    + controls the text alignment of the right-hand column (answers), by default it is aligned to the left. +
    + +
    + answer_below_label +
    +
    + This leads to answers stacking below labels, instead of them being side-by-side (the default). It entails zero offsets and left alignment. Can be overridden with offsets and the alignment classes. +
    + +
    + hide_label +
    +
    + This hides the labels for mc and mc_multiple replies. Useful in combination with a fixed width for mc, mc_multiple labels and mc_heading – this way you can achieve a tabular layout. On small screens labels will automatically be displayed again, because the tabular layout cannot be maintained then. +
    + +
    + show_value_instead_of_label +
    +
    + This hides the labels for mc_button and mc_multiple_button, instead it shows their values (useful numbers from 1 to x). Useful in combination with mc_heading – this way you can achieve a tabular layout. On small screens labels will automatically be displayed again, because the tabular layout cannot be maintained then. +
    + +
    + rotate_label45, rotate_label30,
    rotate_label90 +
    +
    + This rotates the labels for mc and mc_multiple replies. Useful if some have long words, that would lead to exaggerated widths for one answer column. +
    + + +
    + mc_block +
    +
    + This turns answer labels for mc-family items into blocks, so that lines break before and after the label. +
    + +
    + mc_vertical +
    +
    + This makes answer labels for mc-family items stack up. Useful if you have so many options, that they jut out of the viewport. If you have very long list, consider using select-type items instead (they come with a search function). +
    + +
    + mc_horizontal +
    +
    + This makes answer labels for mc-family items stay horizontal on small screens. +
    + + +
    + mc_equal_widths +
    +
    + This makes answer labels for mc-family items have equal widths, even though their contents would lead them to have different widths. This won't work in combination with every other option for mc-styling and if your widest elements are very wide, the choices might jut out of the viewport. +
    + -
    - mc_horizontal -
    -
    - This makes answer labels for mc-family items stay horizontal on small screens. -
    - - -
    - mc_equal_widths -
    -
    - This makes answer labels for mc-family items have equal widths, even though their contents would lead them to have different widths. This won't work in combination with every other option for mc-styling and if your widest elements are very wide, the choices might jut out of the viewport. -
    +
    + mc_width50 (60, … ,
    100, 150, 200) +
    +
    + This makes choice labels and choice buttons for mc-family items have fixed widths. If one choice has text wider than that width, it might jut out or ignore the fixed width, depending on the browser. +
    +
    + rating_button_label_width50
    (60, … , 100, 150, 200) +
    +
    + This makes the labels for rating_button items have fixed widths. This can be useful to align rating_buttons buttons with each other even though the end points are labelled differently. A more flexible solution would be to horizontally center the choices using answer_align_center. +
    -
    - mc_width50 (60, … ,
    100, 150, 200) -
    -
    - This makes choice labels and choice buttons for mc-family items have fixed widths. If one choice has text wider than that width, it might jut out or ignore the fixed width, depending on the browser. -
    - -
    - rating_button_label_width50
    (60, … , 100, 150, 200) -
    -
    - This makes the labels for rating_button items have fixed widths. This can be useful to align rating_buttons buttons with each other even though the end points are labelled differently. A more flexible solution would be to horizontally center the choices using answer_align_center. -
    - -
    - space_bottom_10
    (10, 20, … , 60) -
    -
    - Controls the space after an item. Default value is 15. -
    -
    - space_label_answer_vertical_10
    (10, 20, … , 60) -
    -
    - Controls the vertical space between label and choices, if you've set answer_below_label. Default value is 15. -
    -
    - clickable_map -
    -
    - If you use this class for a text type item, with one image in the label, this image will become a clickable image, with the four outer corners selectable (the selection will be stored in the text field). Will probably require customisation for your purposes. -
    -
    \ No newline at end of file +
    + space_bottom_10
    (10, 20, … , 60) +
    +
    + Controls the space after an item. Default value is 15. +
    +
    + space_label_answer_vertical_10
    (10, 20, … , 60) +
    +
    + Controls the vertical space between label and choices, if you've set answer_below_label. Default value is 15. +
    +
    + clickable_map +
    +
    + If you use this class for a text type item, with one image in the label, this image will become a clickable image, with the four outer corners selectable (the selection will be stored in the text field). Will probably require customisation for your purposes. +
    + \ No newline at end of file diff --git a/application/View/public/error.php b/application/View/public/error.php index dd683f8e0..aff24d83d 100644 --- a/application/View/public/error.php +++ b/application/View/public/error.php @@ -1,32 +1,32 @@ - - - <?php echo $title; ?> - formr.org - - - - -
    -

    -

    -
    - renderAlerts() : null; ?> -
    -

    - -

    - -
    - - + + + <?php echo $title; ?> - formr.org + + + + +
    +

    +

    +
    + renderAlerts() : null; ?> +
    +

    + +

    + +
    + + diff --git a/application/View/public/forgot_password.php b/application/View/public/forgot_password.php index 2edf1ba46..b97bfa4a5 100755 --- a/application/View/public/forgot_password.php +++ b/application/View/public/forgot_password.php @@ -1,33 +1,33 @@
    -
    -
    -
    -
    -
    -
    - -
    -
    -
    -
    -

     

    -
    -
    -
    -
    +
    +
    +
    +
    +
    +
    + +
    +
    +
    +
    +

     

    +
    +
    +
    +
    \ No newline at end of file diff --git a/application/View/public/head.php b/application/View/public/head.php index 8e6597813..3741393b3 100644 --- a/application/View/public/head.php +++ b/application/View/public/head.php @@ -1,4 +1,6 @@ - + <?php echo $site->makeTitle(); ?> @@ -25,10 +27,10 @@ $files) { - print_stylesheets($files, $id); + print_stylesheets($files, $id); } foreach ($js as $id => $files) { - print_scripts($files, $id); + print_scripts($files, $id); } ?> \ No newline at end of file diff --git a/application/View/public/header.php b/application/View/public/header.php index f9189ccff..663e1bae1 100755 --- a/application/View/public/header.php +++ b/application/View/public/header.php @@ -1,18 +1,18 @@ - + - + + +
    + +
    +
    + +
    +
    -
    - -
    -
    - -
    -
    - diff --git a/application/View/public/home.php b/application/View/public/home.php index 4e9521ab9..037a59c42 100755 --- a/application/View/public/home.php +++ b/application/View/public/home.php @@ -3,168 +3,168 @@
    -
    -
    -
    -
    -
    -

    formr survey framework

    - -

    chain simple surveys into long runs, use the power of R to generate pretty feedback and complex designs

    -

    Sign up (it's all free)

    -
    -
    -
    -
    -
    - - - -
    +
    +
    +
    +
    +
    +

    formr survey framework

    + +

    chain simple surveys into long runs, use the power of R to generate pretty feedback and complex designs

    +

    Sign up (it's all free)

    +
    +
    +
    +
    +
    + + + +
    - -
    -
    -
    -

    Core Strengths

    -

    The core strengths of formr

    -
    -
    -
    -
    -
    -
    - -
    -

    Live Feedback

    -

    generates live and interactive feedback, including ggplot2, interactive ggvis and htmlwidgets. In our studies, this increases interest and retention. See examples.

    -
    -
    -
    -
    -
    - -
    -

    Reminders, invitations

    -

    sends automated reminders via email or text message, you can generate custom text and feedback here too

    -
    -
    -
    -
    -
    - -
    -

    Responsive Layout

    -

    all platforms and device sizes are supported (about 30-40% of participants fill out our surveys on a mobile device)

    -
    -
    -
    + +
    +
    +
    +

    Core Strengths

    +

    The core strengths of formr

    +
    +
    +
    +
    +
    +
    + +
    +

    Live Feedback

    +

    generates live and interactive feedback, including ggplot2, interactive ggvis and htmlwidgets. In our studies, this increases interest and retention. See examples.

    +
    +
    +
    +
    +
    + +
    +

    Reminders, invitations

    +

    sends automated reminders via email or text message, you can generate custom text and feedback here too

    +
    +
    +
    +
    +
    + +
    +

    Responsive Layout

    +

    all platforms and device sizes are supported (about 30-40% of participants fill out our surveys on a mobile device)

    +
    +
    +
    -
    -
    -
    - -
    -

    Share & Remix

    -

    easily share, swap and remix surveys (they're just spreadsheets) and runs (they're just JSON). Track version changes in these files with e.g. the OSF.

    -
    -
    -
    -
    -
    -
    - -
    -

    Use R

    -

    use R to do anything it can do (plot a graph or even use a sentiment analysis of a participant's Twitter feed to decide which questions to ask)

    -
    -
    -
    -
    -
    - -
    -

    Secure and open source

    -

    Participants can only connect via an SSL-encrypted connection. Learn more about security on formr.

    -
    -
    -
    -
    -
    +
    +
    +
    + +
    +

    Share & Remix

    +

    easily share, swap and remix surveys (they're just spreadsheets) and runs (they're just JSON). Track version changes in these files with e.g. the OSF.

    +
    +
    +
    +
    +
    +
    + +
    +

    Use R

    +

    use R to do anything it can do (plot a graph or even use a sentiment analysis of a participant's Twitter feed to decide which questions to ask)

    +
    +
    +
    +
    +
    + +
    +

    Secure and open source

    +

    Participants can only connect via an SSL-encrypted connection. Learn more about security on formr.

    +
    +
    +
    +
    +
    -
    -
    -
    -

    What formr can do

    -

    These study types (and others) can be implemented in formr.org

    -
    -
    -
    -
    -
    - Image -
    -

    Surveys with feedback

    -

    generate simple surveys via spreadsheets. With and without feedback. Use R to generate feedback

    -
    -
    -
    -
    -
    - Image -
    -

    Complex Surveys

    -

    complex surveys (using skipping logic, personalised text, complex feedback)

    -
    -
    -
    +
    +
    +
    +

    What formr can do

    +

    These study types (and others) can be implemented in formr.org

    +
    +
    +
    +
    +
    + Image +
    +

    Surveys with feedback

    +

    generate simple surveys via spreadsheets. With and without feedback. Use R to generate feedback

    +
    +
    +
    +
    +
    + Image +
    +

    Complex Surveys

    +

    complex surveys (using skipping logic, personalised text, complex feedback)

    +
    +
    +
    -
    -
    - Image -
    -

    Eligibility Limitations

    -

    filter your participants with eligibility criteria.

    -
    -
    -
    -
    -
    - Image -
    -

    Diary Studies

    -

    do diary studies with flexible automated email/text message reminders

    -
    -
    -
    -
    -
    - Image -
    -

    Longitudinal Studies

    -

    do longitudinal studies. The items of later waves need not exist in final form at wave 1.

    -
    -
    -
    -
    -
    - Image -
    -

    Longitudinal Social Networks

    -

    let people rate their social networks and track changes in them

    -
    -
    -
    +
    +
    + Image +
    +

    Eligibility Limitations

    +

    filter your participants with eligibility criteria.

    +
    +
    +
    +
    +
    + Image +
    +

    Diary Studies

    +

    do diary studies with flexible automated email/text message reminders

    +
    +
    +
    +
    +
    + Image +
    +

    Longitudinal Studies

    +

    do longitudinal studies. The items of later waves need not exist in final form at wave 1.

    +
    +
    +
    +
    +
    + Image +
    +

    Longitudinal Social Networks

    +

    let people rate their social networks and track changes in them

    +
    +
    +
    -
    -
    +
    +
    diff --git a/application/View/public/login.php b/application/View/public/login.php index 7cf2878f7..519a6f3e1 100755 --- a/application/View/public/login.php +++ b/application/View/public/login.php @@ -3,47 +3,47 @@ ?>
    -
    -
    -
    -
    -
    -
    -
    - -
    -
    -
    -
    -
    -
    -
    -

    Login

    -

    Login to manage your existing studies or create new studies

    -
    -
    -
    -
    -
    -
    -
    +
    +
    +
    +
    +
    +
    +
    + +
    +
    +
    +
    +
    +
    +
    +

    Login

    +

    Login to manage your existing studies or create new studies

    +
    +
    +
    +
    +
    +
    +
    diff --git a/application/View/public/navigation.php b/application/View/public/navigation.php index 4c8d57eef..57175c74a 100644 --- a/application/View/public/navigation.php +++ b/application/View/public/navigation.php @@ -1,34 +1,34 @@ diff --git a/application/View/public/publications.php b/application/View/public/publications.php index b4f825db1..1579274bc 100644 --- a/application/View/public/publications.php +++ b/application/View/public/publications.php @@ -1,57 +1,57 @@ 'fmr-small-header', + 'headerClass' => 'fmr-small-header', )); ?>
    -
    -
    -
    -

    Publications

    -

    Publications using data collected with the formr.org software

    -
    -
    -
    -
    -
    -

    +

    +
    +
    +

    Publications

    +

    Publications using data collected with the formr.org software

    +
    +
    +
    +
    +
    +

    -   If you are publishing research conducted using formr, please cite both the preprint and the version of the software that was active when you ran your study. -

    - -
    - Arslan, R. C., Walther, M., & Tata, C. (2018, September 4). formr: A study framework allowing for automated feedback generation and complex longitudinal experience sampling studies using R. https://doi.org/10.31234/osf.io/pjasu -
    - -
    - Arslan, R.C., Tata, C.S. & Walther, M.P. (2018). formr: A study framework allowing for automated feedback generation and complex longitudinal experience sampling studies using R. (version ). DOI -
    -

    - Once your research is published, - you can send to us by email to rubenarslan@gmail.com or cyril.tata@gmail.com - and it will be added to the list of publications. -

    -
    -

     

    -
    -
    - -
    -

     

    - -
    -
    +   If you are publishing research conducted using formr, please cite both the preprint and the version of the software that was active when you ran your study. +

    + +
    + Arslan, R. C., Walther, M., & Tata, C. (2018, September 4). formr: A study framework allowing for automated feedback generation and complex longitudinal experience sampling studies using R. https://doi.org/10.31234/osf.io/pjasu +
    + +
    + Arslan, R.C., Tata, C.S. & Walther, M.P. (2018). formr: A study framework allowing for automated feedback generation and complex longitudinal experience sampling studies using R. (version ). DOI +
    +

    + Once your research is published, + you can send to us by email to rubenarslan@gmail.com or cyril.tata@gmail.com + and it will be added to the list of publications. +

    +
    +

     

    +
    +
    + +
    +

     

    + +
    +
    diff --git a/application/View/public/register.php b/application/View/public/register.php index 1bca37d4c..c841a4f3e 100755 --- a/application/View/public/register.php +++ b/application/View/public/register.php @@ -1,48 +1,48 @@ -
    -
    -
    -
    -
    +
    +
    +
    +
    -
    -
    -
    - -
    -
    -
    -

     

    -
    -
    -
    -
    -

    Sign-Up

    -

    It's free, we don't spam

    -

    If you don't have a referral token, sign up first and then write us an &body= +

    +

     

    +
    +
    +
    +
    +

    Sign-Up

    +

    It's free, we don't spam

    +

    If you don't have a referral token, sign up first and then write us an &body=">email to ask for the right to create studies. If you have a token, you'll be able to create studies once you confirm your email address.

    -
    -
    -
    -
    -
    -
    -
    +"); ?>">email to ask for the right to create studies. If you have a token, you'll be able to create studies once you confirm your email address.

    +
    +
    +
    +
    +
    +
    +
    diff --git a/application/View/public/reset_password.php b/application/View/public/reset_password.php index ab52a3c15..0a081f20b 100755 --- a/application/View/public/reset_password.php +++ b/application/View/public/reset_password.php @@ -3,42 +3,42 @@ ?>
    -
    -
    -
    -
    -
    -
    -
    - +
    +
    +
    +
    +
    +

     

    +
    +
    +
    +
    diff --git a/application/View/public/run/index.php b/application/View/public/run/index.php index 645a03151..143657db3 100644 --- a/application/View/public/run/index.php +++ b/application/View/public/run/index.php @@ -1,46 +1,46 @@ - + - +
    - header_image_path): ?> - <?php echo $run->name; ?> header image - + header_image_path): ?> + <?php echo $run->name; ?> header image +
    -
    - -
    +
    + +
    - +
    - + \ No newline at end of file diff --git a/application/View/public/run/settings.php b/application/View/public/run/settings.php index 6adf594a9..4ae15b6f0 100755 --- a/application/View/public/run/settings.php +++ b/application/View/public/run/settings.php @@ -1,63 +1,63 @@ 'fmr-small-header', + 'headerClass' => 'fmr-small-header', )); ?>
    -
    -
    -
    -

    Settings for 'name; ?>'

    - +
    +
    +
    +

    Settings for 'name; ?>'

    + -
    - - - - - - - - - - - - - - - - - -
    Setting 
    - Email subscription
    - Subscribe / Unsubscribe to receiving emails from this study -
    - -
    - Delete Survey Session
    - Your session cookie will be deleted, so your session will no longer be accessible from this computer, but your data will still be saved.
    - To re-activate your session you can use the login link, if you have one.
    -
    -
    - -
    -
    - - -
    -
    -
    -
    +
    + + + + + + + + + + + + + + + + + +
    Setting 
    + Email subscription
    + Subscribe / Unsubscribe to receiving emails from this study +
    + +
    + Delete Survey Session
    + Your session cookie will be deleted, so your session will no longer be accessible from this computer, but your data will still be saved.
    + To re-activate your session you can use the login link, if you have one.
    +
    +
    + +
    +
    + + +
    +
    +
    +
    diff --git a/application/View/public/social_share.php b/application/View/public/social_share.php index e050e90d2..81b498c44 100644 --- a/application/View/public/social_share.php +++ b/application/View/public/social_share.php @@ -1,15 +1,15 @@ $data): - $href = strt_replace($data['url'], array('title' => $title, 'url' => $url)); - ?> - + $href = strt_replace($data['url'], array('title' => $title, 'url' => $url)); + ?> + diff --git a/application/View/public/studies.php b/application/View/public/studies.php index 7849c0b78..9ba1915c9 100644 --- a/application/View/public/studies.php +++ b/application/View/public/studies.php @@ -1,42 +1,42 @@ - 'fmr-small-header', - )); + 'fmr-small-header', +)); ?>
    -
    -
    -
    -

    Studies

    -

    Some studies currently running on formr.

    -
    -
    -
    +
    +
    +
    +

    Studies

    +

    Some studies currently running on formr.

    +
    +
    +
    - -
    -
    -

    -
    -  

    ' ?> -
    -
    -
    - -
    -
    - Participate - -
    -
    -
    - + +
    +
    +

    +
    +  

    ' ?> +
    +
    +
    + +
    +
    + Participate + +
    +
    +
    + -
    -
    +
    +
    diff --git a/application/View/superadmin/active_users.php b/application/View/superadmin/active_users.php index febbf7010..d86df7b37 100644 --- a/application/View/superadmin/active_users.php +++ b/application/View/superadmin/active_users.php @@ -1,61 +1,61 @@
    - -
    -

    User Management Superadmin

    -
    + +
    +

    User Management Superadmin

    +
    - -
    -
    -
    -
    -
    -

    Formr active users

    -
    -
    - - - - - $value): - echo ""; - endforeach; - ?> - - - - "; - foreach ($row as $cell): - echo ""; - endforeach; - echo ""; - endforeach; - ?> - -
    {$field}
    $cell
    - + +
    +
    +
    +
    +
    +

    Formr active users

    +
    +
    + + + + + $value): + echo ""; + endforeach; + ?> + + + + "; + foreach ($row as $cell): + echo ""; + endforeach; + echo ""; + endforeach; + ?> + +
    {$field}
    $cell
    + - + -
    -
    +
    +
    -
    -
    +
    +
    -
    -
    - +
    + +
    \ No newline at end of file diff --git a/application/View/superadmin/cron_log.php b/application/View/superadmin/cron_log.php index d03240971..3df95d684 100755 --- a/application/View/superadmin/cron_log.php +++ b/application/View/superadmin/cron_log.php @@ -1,58 +1,59 @@

    cron log

    - The cron job runs every x minutes, to evaluate whether somebody needs to be sent a mail. This usually happens if a pause is over. It will then skip forward or backward, send emails and shuffle participants, but will stop at surveys and pages, because those should be viewed by the user. + The cron job runs every x minutes, to evaluate whether somebody needs to be sent a mail. This usually happens if a pause is over. It will then skip forward or backward, send emails and shuffle participants, but will stop at surveys and pages, because those should be viewed by the user.

    - - - - $value): - if($field=='skipbackwards') - $field = ''; - elseif($field=='skipforwards') - $field = ''; - elseif($field=='pauses') - $field = ''; - elseif($field=='emails') - $field = ''; - elseif($field=='shuffles') - $field = ''; - elseif($field=='sessions') - $field = ''; - elseif($field=='errors') - $field = ''; - elseif($field=='warnings') - $field = ''; - elseif($field=='notices') - $field = ''; - - echo ""; - endforeach; - ?> - - - $cell"; - endforeach; + +
    {$field}
    + + $value): + if ($field == 'skipbackwards') + $field = ''; + elseif ($field == 'skipforwards') + $field = ''; + elseif ($field == 'pauses') + $field = ''; + elseif ($field == 'emails') + $field = ''; + elseif ($field == 'shuffles') + $field = ''; + elseif ($field == 'sessions') + $field = ''; + elseif ($field == 'errors') + $field = ''; + elseif ($field == 'warnings') + $field = ''; + elseif ($field == 'notices') + $field = ''; - echo "\n"; - endforeach; - } - ?> + echo ""; + endforeach; + ?> + + +
    {$field}
    -
    - render("superadmin/cron_log"); ?> + // printing table rows + foreach ($cronlogs AS $row): + foreach ($row as $cell): + echo "$cell"; + endforeach; + + echo "\n"; + endforeach; + } + ?> + + + +render("superadmin/cron_log"); ?> - -
    - -
    -

    Cron Logs Superadmin

    -
    + +
    +

    Cron Logs Superadmin

    +
    - -
    -
    -
    -
    -
    -

    Logs

    -
    - -
    -
    -
    - -
    - -
    - -
    -
    -
    -
    -

    Cron Log

    -
    -
    -
    - printCronLogFile($parse, $expand_logs); - } - ?> -
    + +
    +
    +
    +
    +
    +

    Logs

    +
    + +
    +
    +
    + +
    + +
    + +
    +
    +
    +
    +

    Cron Log

    +
    +
    +
    + printCronLogFile($parse, $expand_logs); + } + ?> +
    - -
    -
    + +
    +
    -
    -
    +
    +
    -
    -
    - +
    + +
    \ No newline at end of file diff --git a/application/View/superadmin/runs_management.php b/application/View/superadmin/runs_management.php index 8de194256..f29a35280 100644 --- a/application/View/superadmin/runs_management.php +++ b/application/View/superadmin/runs_management.php @@ -1,77 +1,77 @@
    - -
    -

    User Management Superadmin

    -
    + +
    +

    User Management Superadmin

    +
    - -
    -
    -
    -
    -
    -

    Formr Runs ()

    -
    -
    - - + +
    +
    +
    +
    +
    +

    Formr Runs ()

    +
    +
    + + -
    - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    IDRun NameUserNo. SessionsCron ActiveCron ForkedLocked
    - - - /> - - - /> - - - /> -
    - -
    -
    - +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    IDRun NameUserNo. SessionsCron ActiveCron ForkedLocked
    + + + /> + + + /> + + + /> +
    + +
    +
    + -
    -
    +
    +
    -
    -
    +
    +
    -
    -
    - +
    + +
    \ No newline at end of file diff --git a/application/View/superadmin/user_management.php b/application/View/superadmin/user_management.php index 7aef37051..defadbf6a 100644 --- a/application/View/superadmin/user_management.php +++ b/application/View/superadmin/user_management.php @@ -1,94 +1,94 @@
    - -
    -

    User Management Superadmin

    -
    + +
    +

    User Management Superadmin

    +
    - -
    -
    -
    -
    -
    -

    Formr Users

    -
    -
    - - - - - $value): - echo ""; - endforeach; - ?> - - - - "; - foreach ($row as $cell): - echo ""; - endforeach; - echo ""; - endforeach; - ?> - -
    {$field}
    $cell
    - + +
    +
    +
    +
    +
    +

    Formr Users

    +
    +
    + + + + + $value): + echo ""; + endforeach; + ?> + + + + "; + foreach ($row as $cell): + echo ""; + endforeach; + echo ""; + endforeach; + ?> + +
    {$field}
    $cell
    + - + -
    -
    +
    +
    -
    -
    +
    +
    -
    -
    - +
    + +
    ",rE:!0,sL:["actionscript","javascript","handlebars","xml"]}},{cN:"meta",v:[{b:/<\?xml/,e:/\?>/,r:10},{b:/<\?\w+/,e:/\?>/}]},{cN:"tag",b:"",c:[{cN:"name",b:/[^\/><\s]+/,r:0},c]}]}}),hljs.registerLanguage("r",function(a){var b="([a-zA-Z]|\\.[a-zA-Z.])[a-zA-Z0-9._]*";return{c:[a.HCM,{b:b,l:b,k:{keyword:"function if in break next repeat else for return switch while try tryCatch stop warning require library attach detach source setMethod setGeneric setGroupGeneric setClass ...",literal:"NULL NA TRUE FALSE T F Inf NaN NA_integer_|10 NA_real_|10 NA_character_|10 NA_complex_|10"},r:0},{cN:"number",b:"0[xX][0-9a-fA-F]+[Li]?\\b",r:0},{cN:"number",b:"\\d+(?:[eE][+\\-]?\\d*)?L\\b",r:0},{cN:"number",b:"\\d+\\.(?!\\d)(?:i\\b)?",r:0},{cN:"number",b:"\\d+(?:\\.\\d*)?(?:[eE][+\\-]?\\d*)?i?\\b",r:0},{cN:"number",b:"\\.\\d+(?:[eE][+\\-]?\\d*)?i?\\b",r:0},{b:"`",e:"`",r:0},{cN:"string",c:[a.BE],v:[{b:'"',e:'"'},{b:"'",e:"'"}]}]}}),hljs.registerLanguage("markdown",function(a){return{aliases:["md","mkdown","mkd"],c:[{cN:"section",v:[{b:"^#{1,6}",e:"$"},{b:"^.+?\\n[=-]{2,}$"}]},{b:"<",e:">",sL:"xml",r:0},{cN:"bullet",b:"^([*+-]|(\\d+\\.))\\s+"},{cN:"strong",b:"[*_]{2}.+?[*_]{2}"},{cN:"emphasis",v:[{b:"\\*.+?\\*"},{b:"_.+?_",r:0}]},{cN:"quote",b:"^>\\s+",e:"$"},{cN:"code",v:[{b:"`.+?`"},{b:"^( {4}| )",e:"$",r:0}]},{b:"^[-\\*]{3,}",e:"$"},{b:"\\[.+?\\][\\(\\[].*?[\\)\\]]",rB:!0,c:[{cN:"string",b:"\\[",e:"\\]",eB:!0,rE:!0,r:0},{cN:"link",b:"\\]\\(",e:"\\)",eB:!0,eE:!0},{cN:"symbol",b:"\\]\\[",e:"\\]",eB:!0,eE:!0}],r:10},{b:"^\\[.+\\]:",rB:!0,c:[{cN:"symbol",b:"\\[",e:"\\]:",eB:!0,eE:!0,starts:{cN:"link",e:"$"}}]}]}}),window.performance=window.performance||{},performance.now=function(){return performance.now||performance.mozNow||performance.msNow||performance.oNow||performance.webkitNow}(),window.gccConfig={content:{message:"On our website we're using cookies to optimize user experience and to improve our website. By using our website you agree that cookies can be stored on your local computer."},palette:{popup:{background:"#333333",text:"#fff",link:"#fff"},button:{background:"#8dc63f",text:"#fff"}}},function(a){"use strict";!function(a){a.fn.stickyStuff=function(){var b=this,c=function(){var c=window.location.hash,d=c?'a[href="'+c+'"]':"li.active > a";if("tab"===a(d,b).data("toggle"))a(d,b).tab("show");else if("collapse"===a(d,b).data("toggle")){var e=c;a(e,b).collapse("show");var f=a(e,b).parents(".tab-pane");f&&!f.hasClass("active")&&a("a[href=#"+f.attr("id")+"]").tab("show")}};return c(b),a(window).on("hashchange",function(){c(b)}),a("a",b).on("click",function(a){history.pushState(null,null,this.href),c(b)}),this}}(jQuery),a(function(){if(1==a(".schmail").length){var b=a(".schmail").attr("href");b=b.replace("IMNOTSENDINGSPAMTO","").replace("that-big-googly-eyed-email-provider","gmail").replace(encodeURIComponent("If you are not a robot, I have high hopes that you can figure out how to get my proper email address from the above."),"").replace(encodeURIComponent("\r\n\r\n"),""),a(".schmail").attr("href",b)}if(a("*[title]").tooltip({container:"body"}),hljs.initHighlighting(),a(".nav-tabs, .tab-content").stickyStuff(),a(".navbar-toggle").attr("style","-ms-touch-action: manipulation; touch-action: manipulation;"),a("ul.menu-highlight a").each(function(){var b=a(this),c=b.attr("href");c===document.location.href&&b.parents("li").addClass("active")}),a(".social-share-icon").unbind("click").bind("click",function(){var b=a(this),c=b.attr("data-href");c&&(b.attr("data-target")?window.open(c,b.attr("data-target"),b.attr("data-width")?"width="+b.attr("data-width")+",height="+b.attr("data-height"):void 0):window.location.href=c)}),a(".copy_clipboard").click(function(){this.select();try{var b=document.execCommand("copy");b&&a(this).tooltip({title:"Link was copied to clipboard.",position:"top"}).tooltip("show")}catch(c){}}),a(".copy-url").click(function(){try{var b=a(this).data("url"),c=document.createElement("input");document.body.appendChild(c),c.value=b,c.select(),document.execCommand("copy"),document.body.removeChild(c),bootstrap_modal("URL Copied",b)}catch(d){}}),a(".monkey-bar-modal").length&&a(".monkey-bar-modal").appendTo("body"),!cookies_enabled()){var c="The use of cookies seems to have been disabled in your browser. ";c+="In order to be able to use formr and have a good user experience, you will need to enable cookies.",bootstrap_modal("Cookies Disabled",c)}})}(jQuery),function(a){"use strict";function b(b,c){a(c).click(function(b){b.preventDefault();var c=a(this),d=c.attr("href");return""===d?!1:(c.attr("href",""),a.ajax({type:"GET",url:d,dataType:"html"}).done(a.proxy(function(b){var c=a(this);c.attr("href",d),c.hasClass("danger")||c.css("color","green");var e=c.find("i.fa");e.hasClass("fa-stethoscope")?(e.addClass("fa-heartbeat"),e.removeClass("fa-stethoscope")):e.hasClass("fa-heartbeat")?(e.removeClass("fa-heartbeat"),e.addClass("fa-stethoscope")):bootstrap_modal("Alert",b,"tpl-feedback-modal"),c.hasClass("refresh_on_success")&&document.location.reload(!0)},this)).fail(a.proxy(function(b,c,e,f){a(this).attr("href",d),ajaxErrorHandling(b,c,e,f)},this)),!1)})}function c(b,c){a(c).submit(function(b){b.preventDefault();var c=a(this),d=c.find("button[type=submit].btn");return d.attr("disabled",!0),a.ajax({type:c.attr("method"),url:c.attr("action"),data:c.serialize(),dataType:"html"}).done(a.proxy(function(b){d.attr("disabled",!1),d.css("color","green"),a(".alerts-container").prepend(b),d.hasClass("refresh_on_success")&&document.location.reload(!0)},this)).fail(a.proxy(function(a,b,c,e){d.attr("disabled",!1),ajaxErrorHandling(a,b,c,e)},this)),!1})}function d(b){var c=parseInt(a(this).data("user"),10),d=a(this).data("email");if(c&&d){var f={user_id:c,user_email:d,user_api:!0,api_action:"get"};i(saAjaxUrl,f,function(a){a&&a.success&&e(a.data,f)})}}function e(b,c){var d=a(a.parseHTML(getHTMLTemplate("tpl-user-api",{user:" ("+b.user+")",client_id:b.client_id,client_secret:b.client_secret})));b.client_id?d.find(".api-create").remove():d.find(".api-change, .api-delete").remove(),d.on("shown.bs.modal",function(){d.find(".api-create").click(function(){if(confirm("Are you sure?")){var a={user_id:c.user_id,user_email:c.user_email,user_api:!0,api_action:"create"};i(saAjaxUrl,a,function(a){a&&a.success&&(d.modal("hide"),e(a.data,c))})}}),d.find(".api-change").click(function(){if(confirm("Are you sure?")){var a={user_id:c.user_id,user_email:c.user_email,user_api:!0,api_action:"change"};i(saAjaxUrl,a,function(a){a&&a.success&&(d.modal("hide"),e(a.data,c))})}}),d.find(".api-delete").click(function(){if(confirm("Are you sure?")){var a={user_id:c.user_id,user_email:c.user_email,user_api:!0,api_action:"delete"};i(saAjaxUrl,a,function(a){a&&a.success&&(d.modal("hide"),e({user:b.user,client_id:"",client_secret:""},c))})}})}).on("hidden.bs.modal",function(){d.remove()}).modal("show")}function f(b){var d=a(this),e=a(a.parseHTML(getHTMLTemplate("tpl-delete-run-session",{action:d.data("href"),session:d.data("session")}))),f=d.parents("tr");d.css("border-color","#ee5f5b"),d.css("color","#ee5f5b"),e.find("form").each(c).submit(function(){f.css("background-color","#ee5f5b"),e.modal("hide")}),e.on("hidden.bs.modal",function(){e.remove(),d.css("border-color","black"),d.css("color","black")}).modal("show")}function g(b){var c=a(this),d=a(a.parseHTML(getHTMLTemplate("tpl-confirmation",{content:c.data("msg"),yes_url:c.data("href"),no_url:"#"})));d.on("shown.bs.modal",function(){d.find(".btn-yes").click(function(){var b=c.data("href");a(this).append(bootstrap_spinner()),i(b,{ajax:!0},function(a){d.modal("hide");var b=bootstrap_modal("Activity Deleted",a,"tpl-feedback-modal");b.on("hidden.bs.modal",function(){d.remove(),document.location.reload(!0)}).modal("show")},"html")})}).on("hidden.bs.modal",function(){d.remove()}).modal("show")}function h(b){var c=a(this),d=a(a.parseHTML(getHTMLTemplate("tpl-remind-run-session",{action:c.data("href"),session:c.data("session")})));c.parents("tr");d.on("shown.bs.modal",function(){d.find(".send").click(function(){var b=c.data("href"),e=a(this).data("reminder");a(this).append(bootstrap_spinner()),i(b,{reminder:e},function(a){d.modal("hide"),bootstrap_modal("Send Reminder",a,"tpl-feedback-modal").modal("show"),c.css("border-color","green"),c.css("color","green")},"html")})}),d.on("hidden.bs.modal",function(){d.remove()}),a.get(c.data("href"),{session:c.data("session"),get_count:!0},function(a,b,c){d.find(".reminder-row-count").text("(0)");for(var e in a){var f=a[e];d.find(".reminder-row-count-"+e).text(" ("+f+")")}d.modal("show")})}function i(b,c,d,e,f){e=e||"json",f=f||function(){},a.ajax({type:"POST",url:b,data:c,dataType:e,success:function(a,b,c){d(a)},error:function(b,c){a(".alerts-container").prepend(c),f(b,c)},beforeSend:function(a){}})}function j(){var b=a(this);"single"===b.data("active")?(b.siblings(".single").addClass("hidden"),b.siblings(".multiple").removeClass("hidden"),b.data("active","multiple")):(b.siblings(".single").removeClass("hidden"),b.siblings(".multiple").addClass("hidden"),b.data("active","single"))}function k(b){var c=a(this),d=[];a("input.ba-select-session").each(function(){a(this).is(":checked")&&d.push(a(this).val())}),d.length&&"function"==typeof l[c.data("action")]&&l[c.data("action")](c.parents("form").attr("action"),d)}var l={toggleTest:function(b,c){var d=a(a.parseHTML(getHTMLTemplate("tpl-confirmation",{content:"

    Are you sure you want to perform this action?

    "})));d.on("shown.bs.modal",function(){d.find(".btn-yes").click(function(e){a(this).append(bootstrap_spinner()),i(b,{action:"toggleTest",sessions:c},function(c){return d.modal("hide"),c.success?void(document.location=b.replace("ajax_user_bulk_actions","user_overview")):(a(this).find(".fa-spin").remove(),void bootstrap_modal("Error",c.error,"tpl-feedback-modal").modal("show"))},"json")}),d.find(".btn-no").click(function(a){d.modal("hide")})}).on("hidden.bs.modal",function(){d.remove()}).modal("show")},sendReminder:function(b,c){var d=a(a.parseHTML(getHTMLTemplate("tpl-remind-run-session",{action:b,session:null})));d.on("shown.bs.modal",function(){d.find(".send").click(function(){var e=a(this).data("reminder");a(this).append(bootstrap_spinner()),i(b,{action:"sendReminder",sessions:c,reminder:e},function(a){return d.modal("hide"),a.success?void(document.location=b.replace("ajax_user_bulk_actions","user_overview")):void bootstrap_modal("Error",a.error,"tpl-feedback-modal").modal("show")},"json")})}).on("hidden.bs.modal",function(){d.remove()}).modal("show")},deleteSessions:function(b,c){var d=a(a.parseHTML(getHTMLTemplate("tpl-confirmation",{content:"

    Are you sure you want delete "+c.length+" session(s)?

    "})));d.on("shown.bs.modal",function(){d.find(".btn-yes").click(function(e){a(this).append(bootstrap_spinner()),i(b,{action:"deleteSessions",sessions:c},function(c){return d.modal("hide"),c.success?void(document.location=b.replace("ajax_user_bulk_actions","user_overview")):(a(this).find(".fa-spin").remove(),void bootstrap_modal("Error",c.error,"tpl-feedback-modal").modal("show"))},"json")}),d.find(".btn-no").click(function(a){d.modal("hide")})}).on("hidden.bs.modal",function(){d.remove()}).modal("show")},positionSessions:function(b,c){var d=parseInt(a("select[name=ba_new_position]").val());if(isNaN(d))return void alert("Bad position selected");var e=a(a.parseHTML(getHTMLTemplate("tpl-confirmation",{content:"

    Are you sure you want push "+c.length+" session(s) to position "+d+"?

    "})));e.on("shown.bs.modal",function(){e.find(".btn-yes").click(function(f){a(this).append(bootstrap_spinner()),i(b,{action:"positionSessions",sessions:c,pos:d},function(c){return e.modal("hide"),c.success?void(document.location=b.replace("ajax_user_bulk_actions","user_overview")):(a(this).find(".fa-spin").remove(),void bootstrap_modal("Error",c.error,"tpl-feedback-modal").modal("show"))},"json")}),e.find(".btn-no").click(function(a){e.modal("hide")})}).on("hidden.bs.modal",function(){e.remove()}).modal("show")}};a(function(){var e;a(".form-ajax").each(c),a(".link-ajax").each(b),a(".link-ajax .fa-pause").parent(".btn").mouseenter(function(){a(this).find(".fa").removeClass("fa-pause").addClass("fa-play")}).mouseleave(function(){a(this).find(".fa").addClass("fa-pause").removeClass("fa-play")}),a(".link-ajax .fa-stop").parent(".btn").mouseenter(function(){a(this).find(".fa").removeClass("fa-stop").addClass("fa-play")}).mouseleave(function(){a(this).find(".fa").addClass("fa-stop").removeClass("fa-play")}),a(".api-btn").click(d),a(".sessions-search-switch").click(j),a(".hidden_debug_message").length>0&&(a(".show_hidden_debugging_messages").click(function(){return a(".hidden_debug_message").toggleClass("hidden"),!1}),a(".show_hidden_debugging_messages").attr("disabled",!1)),a("abbr.abbreviated_session").click(function(){a(this).text()!==a(this).data("full-session")?a(this).text(a(this).data("full-session")):a(this).text(a(this).data("full-session").substr(0,10)+"…")}),a(".download_r_code").length>0&&a(".download_r_code").click(function(){return download_next_textarea(this)}),a(".removal_modal").on("show.bs.modal",function(c){e=a(c.relatedTarget);var d=a(this);e.parents("tr").css("background-color","#ee5f5b"),a(this).find(".danger").attr("href",e.data("href")),b(1,a(this).find(".danger")),a(this).find(".danger").click(function(a){e.css("color","#ee5f5b"),d.hasClass("refresh_on_success")&&window.setTimeout(function(){document.location.reload(!0)},200),d.modal("hide")})}).on("hide.bs.modal",function(a){e.parents("tr").css("background-color","transparent")}),a("a.delete-run-session").bind("click",f),a("a.delete-user-unit-session").bind("click",g),a("a.remind-run-session").bind("click",h),a("div.bulk-actions-ba").find(".ba").bind("click",k)})}(jQuery),function(a){"use strict";function b(b){this.run=b,this.block=a('
    '),b.form.find(".run_units").append(this.block)}function c(c){"undefined"==typeof this.autosaved&&(this.lastSave=a.now(),this.autosaved=!1),this.form=c,this.form.submit(function(){return!1}),this.name=this.form.find(".run_name").val(),this.url=this.form.prop("action"),this.units=[];for(var d=a.parseJSON(this.form.attr("data-units")),e=0;e0&&!c.select2("container").hasClass("select2-container")){var d,e=c.attr("data-select2init");d="object"!=typeof e?a.parseJSON(e):e,c.select2({createSearchChoice:function(b,c){return 0===a(c).filter(function(){return 0===this.text.localeCompare(b)}).length?{id:b,text:b}:void 0},initSelection:function(b,c){var e;e={id:b.val(),text:b.val()},a.each(d,function(a,c){return c.id===b.val()?(e=c,!1):void 0}),c(e)},data:d})}this.unsavedChanges=!1,this.save_button=this.block.find("a.unit_save"),this.block.find("button.from_days").click(function(b){b.preventDefault();var c=a(this).closest(".input-group").find("input[type=number]"),d=c.val();c.val(60*d*24).change()}),this.test_button=this.block.find("a.unit_test"),this.test_button.click(a.proxy(this.test,this)),this.remove_button=this.block.find("button.remove_unit_from_run"),this.remove_button.click(a.proxy(this.removeFromRun,this)).mouseenter(function(){a(this).addClass("btn-danger")}).mouseleave(function(){a(this).removeClass("btn-danger")});var f=this.block.find("textarea");f[0]&&(this.textarea=a(f[0]),this.session=this.hookAceToTextarea(this.textarea)),f[1]&&(this.textarea2=a(f[1]),this.session2=this.hookAceToTextarea(this.textarea2)),this.run.lock(this.run.lock_toggle.hasClass("btn-checked"),this.block),this.save_button.attr("disabled",!0).removeClass("btn-info").text("Saved").click(a.proxy(this.save,this))},b.prototype.position_changes=function(a){this.position_changed||(this.position_changed=!0,this.run.reorder_button.addClass("btn-info").removeAttr("disabled")),this.position.parent().addClass("pos_changed")},b.prototype.changes=function(a){this.unsavedChanges||(this.unsavedChanges=!0,this.save_button.addClass("btn-info").removeAttr("disabled").text("Save changes"),this.test_button.attr("disabled","disabled"))},b.prototype.test=function(b){b.preventDefault();var c=this.test_button.text();this.test_button.attr("disabled",!0).html(c+bootstrap_spinner());this.block;return a.ajax({url:this.run.url+"/"+this.test_button.attr("href"),dataType:"html",data:{run_unit_id:this.run_unit_id,special:this.special},method:"GET"}).done(a.proxy(function(b){var d=bootstrap_modal("Test Results",b);a(".opencpu_accordion",d).collapse({toggle:!0}),this.test_button.html(c).removeAttr("disabled");var e=d.find("pre code");Array.prototype.forEach.call(e,hljs.highlightBlock),d.find(".download_r_code").length>0&&d.find(".download_r_code").click(function(){return download_next_textarea(this)})},this)).fail(a.proxy(function(a,b,d,e){this.test_button.attr("disabled",!1).html(c),ajaxErrorHandling(a,b,d,e)},this)),!1},b.prototype.save=function(b){b.preventDefault();var c=this.save_button.text();this.save_button.attr("disabled","disabled").html(c+bootstrap_spinner()),this.session&&this.textarea.val(this.session.getValue()),this.session2&&this.textarea2.val(this.session2.getValue());this.block;return a.ajax({url:this.run.url+"/"+this.save_button.attr("href"),dataType:"html",data:this.save_inputs.serialize(),method:"POST"}).done(a.proxy(function(b){""!==b?a.proxy(this.init(b),this):(this.save_button.attr("disabled",!0).removeClass("btn-info").text("Saved").click(a.proxy(this.save,this)),this.unsavedChanges=!1,this.test_button.removeAttr("disabled"))},this)).fail(a.proxy(function(a,b,d,e){this.save_button.removeAttr("disabled").html(c),ajaxErrorHandling(a,b,d,e)},this)),!1},b.prototype.hookAceToTextarea=function(b){var c=b.data("editor"),d=a("
    ",{position:"absolute",width:b.width(),height:b.height(),"class":b.attr("class")}).insertBefore(b);b.css("display","none"),this.editor=ace.edit(d[0]),this.editor.setOptions({minLines:b.attr("rows")?b.attr("rows"):3,maxLines:30}),this.editor.setTheme("ace/theme/textmate"),this.editor.$blockScrolling=1/0;var e=this.editor.getSession();return e.setValue(b.val()),this.editor.renderer.setShowGutter(!1),e.setUseWrapMode(!0),e.setWrapLimitRange(42,42),e.setMode("ace/mode/"+c),this.editor.on("change",a.proxy(this.changes,this)),e},b.prototype.removeFromRun=function(b,c){b.preventDefault(),a(".tooltip").hide();var d,e=this,f=b,g=this.block,h=this.run.url+"/"+this.remove_button.attr("href"),i={run_unit_id:this.run_unit_id};return"yes"===c&&(i.confirm="yes"),g.hide(),a.ajax({url:h,dataType:"html",data:i,method:"POST"}).done(a.proxy(function(b){if(g.show(),b)if("warn"===b)d=a(a.parseHTML(getHTMLTemplate("tpl-confirmation",{content:"Are you sure you want to delete this run unit and all it's data?"}))),d.on("shown.bs.modal",function(){d.find(".btn-yes").click(function(a){e.removeFromRun(f,"yes"),d.modal("hide")}),d.find(".btn-no").click(function(a){d.modal("hide"),e.removeFromRun(f,"no")})}).on("hidden.bs.modal",function(){d.remove()}).modal("show");else{g.html(b),g.show();var c=this.run.units.indexOf(this);c>-1&&this.run.units.splice(c,1)}},this)).fail(function(a,b,c,d){g.show(),ajaxErrorHandling(a,b,c,d)}),!1},b.prototype.serialize=function(){var a=this.save_inputs.serializeArray(),b={};b.type=this.block.find(".run_unit_inner").data("type");for(var c=0;cb&&(b=d)}),null===b&&(b=0),b},c.prototype.loadUnit=function(b,c){a.ajax({url:this.url+"/ajax_get_unit",data:b,dataType:"html",success:a.proxy(function(a,b){c.init(a)},this)})},c.prototype.addUnit=function(c){var d=this.getMaxPosition(),e=new b(this);this.units.push(e),a.ajax({url:c,dataType:"html",method:"POST",data:{position:d+10}}).done(a.proxy(function(a){e.init(a)},this)).fail(ajaxErrorHandling)},c.prototype.exportUnits=function(){var b={},c=this.url,d=!1,e=a("
    "),f=this.lock_toggle.hasClass("btn-checked");f&&this.lock(!1,this.form);for(var g=0;g-1?(bootstrap_alert("You used the position "+c+" more than once, therefore the new order could not be saved. Click here to scroll to the duplicated position.","Error.",".run_units"),f=!0):(d[g.run_unit_id]=c,e.push(c))}),!f)return a.ajax({url:this.reorder_button.attr("href"),dataType:"html",method:"POST",data:{position:d}}).done(a.proxy(function(b){a(this.units).each(function(a,b){b.position_changed=!1}),this.reorder_button.removeClass("btn-info").attr("disabled","disabled");var c=e.join(","),d=e.sort(function(a,b){return a-b}).join(",");if(this.form.find(".pos_changed").removeClass("pos_changed"),c!=d){var f=this.form;a(this.units.sort(function(a,b){return+a.position.val()-+b.position.val()})).each(function(a,b){f.find(".run_units").append(b.block)})}},this)).fail(ajaxErrorHandling),!1}},c.prototype.lock=function(b,c){var d=this.units;for(var e in d)d[e].editor&&d[e].editor.setReadOnly(b);var f=c.find(".import_run_units, .run_unit_description, .position, .remove_unit_from_run, .reorder_units, .unit_save, .form-control, select, .from_days, .add_run_unit");f.each(function(c,d){b?(a("#run-unit-choices").hide(),d.onclick&&(d.onclick_disabled=d.onclick,d.onclick=function(a){return a.preventDefault(),!1}),a(d).attr("data-old_disabled",a(d).attr("disabled")),a(d).attr("disabled","disabled")):(a("#run-unit-choices").show(),d.onclick_disabled&&(d.onclick=d.onclick_disabled),a(d).attr("data-old-disabled")&&""!==a(d).attr("data-old-disabled")?a(d).attr("disabled",a(d).attr("data-old-disabled")):a(d).removeAttr("disabled"))})},c.prototype.publicToggle=function(b){var c=a(this);return c.parents(".btn-group").find(".btn-checked").removeClass("btn-checked"),c.toggleClass("btn-checked",1),a.ajax({url:c.attr("href"),dataType:"html",method:"POST"}).fail(ajaxErrorHandling),!1},c.prototype.panic=function(b){var c={content:"

    Don't panic!

    Are things not going your way? Users end up in the wrong places, too many emails being sent?

    This run will be made private, locked and cron tasks (automatic email and text messages) will be disabled. You will need to manually undo these actions later.

    This will essentially buy you some time to fix the bug or get help. By configuring the service message under settings, you can show your users an explanation.

    ",yes_url:this.url+"/panic",no_url:"javascript:void(0);"},d=a(a.parseHTML(getHTMLTemplate("tpl-confirmation",c))).attr("id","run-panic-dialog");d.on("shown.bs.modal",function(){d.find(".btn-yes").click(function(b){document.location=a(this).data("yes")})}).on("hidden.bs.modal",function(){d.remove()}).modal("show")},a(function(){a(".edit_run").each(function(b,d){new c(a(d))})})}(jQuery),function(a){"use strict";function b(b,c){var d=a(c),e=d.data("editor"),f=a("
    ",{position:"absolute",width:"100%",height:d.height(),"class":d.attr("class")}).insertBefore(d);d.css("display","none");var g=ace.edit(f[0]);g.setOptions({minLines:d.attr("rows")?d.attr("rows"):3,maxLines:200}),g.setTheme("ace/theme/textmate");var h=g.getSession();h.setValue(d.val()),h.setUseWrapMode(!0),h.setMode("ace/mode/"+e);var i=a(c).parents("form");g.on("change",function(){i.trigger("change")}),i.on("ajax_submission",function(){d.val(h.getValue())})}function c(b,c){a(c).prop("disabled",!0);var d=a(c).parents("form");d.change(function(){a(c).prop("disabled",!1)}).submit(function(){return!1}),a(c).click(function(b){return d.trigger("ajax_submission"),b.preventDefault(),a.ajax({url:d.attr("action"),dataType:"html",data:d.serialize(),method:"POST"}).done(function(b){""!==b&&a(b).insertBefore(d),a(c).prop("disabled",!0)}).fail(function(a,b,c,d){ajaxErrorHandling(a,b,c,d)}),!1})}a(function(){a("textarea.big_ace_editor").each(b),a(".save_settings").each(c)})}(jQuery),"undefined"==typeof jQuery)throw new Error("AdminLTE requires jQuery");$.AdminLTE={},$.AdminLTE.options={navbarMenuSlimscroll:!0,navbarMenuSlimscrollWidth:"3px",navbarMenuHeight:"200px",animationSpeed:500,sidebarToggleSelector:"[data-toggle='offcanvas']",sidebarPushMenu:!0,sidebarSlimScroll:!0,sidebarExpandOnHover:!1,enableBoxRefresh:!0,enableBSToppltip:!0,BSTooltipSelector:"[data-toggle='tooltip']",enableFastclick:!1,enableControlTreeView:!0,enableControlSidebar:!0,controlSidebarOptions:{toggleBtnSelector:"[data-toggle='control-sidebar']",selector:".control-sidebar",slide:!0},enableBoxWidget:!0,boxWidgetOptions:{boxWidgetIcons:{collapse:"fa-minus",open:"fa-plus",remove:"fa-times"},boxWidgetSelectors:{remove:'[data-widget="remove"]',collapse:'[data-widget="collapse"]'}},directChat:{enable:!0,contactToggleSelector:'[data-widget="chat-pane-toggle"]'},colors:{lightBlue:"#3c8dbc",red:"#f56954",green:"#00a65a",aqua:"#00c0ef",yellow:"#f39c12",blue:"#0073b7",navy:"#001F3F",teal:"#39CCCC", -olive:"#3D9970",lime:"#01FF70",orange:"#FF851B",fuchsia:"#F012BE",purple:"#8E24AA",maroon:"#D81B60",black:"#222222",gray:"#d2d6de"},screenSizes:{xs:480,sm:768,md:992,lg:1200}},$(function(){"use strict";$("body").removeClass("hold-transition"),"undefined"!=typeof AdminLTEOptions&&$.extend(!0,$.AdminLTE.options,AdminLTEOptions);var a=$.AdminLTE.options;_init(),$.AdminLTE.layout.activate(),a.enableControlTreeView&&$.AdminLTE.tree(".sidebar"),a.enableControlSidebar&&$.AdminLTE.controlSidebar.activate(),a.navbarMenuSlimscroll&&"undefined"!=typeof $.fn.slimscroll&&$(".navbar .menu").slimscroll({height:a.navbarMenuHeight,alwaysVisible:!1,size:a.navbarMenuSlimscrollWidth}).css("width","100%"),a.sidebarPushMenu&&$.AdminLTE.pushMenu.activate(a.sidebarToggleSelector),a.enableBSToppltip&&$("body").tooltip({selector:a.BSTooltipSelector,container:"body"}),a.enableBoxWidget&&$.AdminLTE.boxWidget.activate(),a.enableFastclick&&"undefined"!=typeof FastClick&&FastClick.attach(document.body),a.directChat.enable&&$(document).on("click",a.directChat.contactToggleSelector,function(){var a=$(this).parents(".direct-chat").first();a.toggleClass("direct-chat-contacts-open")}),$('.btn-group[data-toggle="btn-toggle"]').each(function(){var a=$(this);$(this).find(".btn").on("click",function(b){a.find(".btn.active").removeClass("active"),$(this).addClass("active"),b.preventDefault()})})}),function(a){"use strict";a.fn.boxRefresh=function(b){function c(a){a.append(f),e.onLoadStart.call(a)}function d(a){a.find(f).remove(),e.onLoadDone.call(a)}var e=a.extend({trigger:".refresh-btn",source:"",onLoadStart:function(a){return a},onLoadDone:function(a){return a}},b),f=a('
    ');return this.each(function(){if(""===e.source)return void(window.console&&window.console.log("Please specify a source first - boxRefresh()"));var b=a(this),f=b.find(e.trigger).first();f.on("click",function(a){a.preventDefault(),c(b),b.find(".box-body").load(e.source,function(){d(b)})})})}}(jQuery),function(a){"use strict";a.fn.activateBox=function(){a.AdminLTE.boxWidget.activate(this)},a.fn.toggleBox=function(){var b=a(a.AdminLTE.boxWidget.selectors.collapse,this);a.AdminLTE.boxWidget.collapse(b)},a.fn.removeBox=function(){var b=a(a.AdminLTE.boxWidget.selectors.remove,this);a.AdminLTE.boxWidget.remove(b)}}(jQuery),function(a){"use strict";a.fn.todolist=function(b){var c=a.extend({onCheck:function(a){return a},onUncheck:function(a){return a}},b);return this.each(function(){"undefined"!=typeof a.fn.iCheck?(a("input",this).on("ifChecked",function(){var b=a(this).parents("li").first();b.toggleClass("done"),c.onCheck.call(b)}),a("input",this).on("ifUnchecked",function(){var b=a(this).parents("li").first();b.toggleClass("done"),c.onUncheck.call(b)})):a("input",this).on("change",function(){var b=a(this).parents("li").first();b.toggleClass("done"),a("input",b).is(":checked")?c.onCheck.call(b):c.onUncheck.call(b)})})}}(jQuery),jQuery(document).ready(function(){$("#user-overview-select-all").click(function(){var a=$(this),b=a.parents("table").find(".ba-select-session");a.is(":checked")?b.prop("checked",!0):b.prop("checked",!1)}),$(".dashboard-link").each(function(a,b){var c=$(b).data("click");1==c&&$(b).trigger("click")})}); \ No newline at end of file +r:0}),e},a.CLCM=a.C("//","$"),a.CBCM=a.C("/\\*","\\*/"),a.HCM=a.C("#","$"),a.NM={cN:"number",b:a.NR,r:0},a.CNM={cN:"number",b:a.CNR,r:0},a.BNM={cN:"number",b:a.BNR,r:0},a.CSSNM={cN:"number",b:a.NR+"(%|em|ex|ch|rem|vw|vh|vmin|vmax|cm|mm|in|pt|pc|px|deg|grad|rad|turn|s|ms|Hz|kHz|dpi|dpcm|dppx)?",r:0},a.RM={cN:"regexp",b:/\//,e:/\/[gimuy]*/,i:/\n/,c:[a.BE,{b:/\[/,e:/\]/,r:0,c:[a.BE]}]},a.TM={cN:"title",b:a.IR,r:0},a.UTM={cN:"title",b:a.UIR,r:0},a.METHOD_GUARD={b:"\\.\\s*"+a.UIR,r:0},a}),hljs.registerLanguage("json",function(a){var b={literal:"true false null"},c=[a.QSM,a.CNM],d={e:",",eW:!0,eE:!0,c:c,k:b},e={b:"{",e:"}",c:[{cN:"attr",b:/"/,e:/"/,c:[a.BE],i:"\\n"},a.inherit(d,{b:/:/})],i:"\\S"},f={b:"\\[",e:"\\]",c:[a.inherit(d)],i:"\\S"};return c.splice(c.length,0,e,f),{c:c,k:b,i:"\\S"}}),hljs.registerLanguage("css",function(a){var b="[a-zA-Z-][a-zA-Z0-9_-]*",c={b:/[A-Z\_\.\-]+\s*:/,rB:!0,e:";",eW:!0,c:[{cN:"attribute",b:/\S/,e:":",eE:!0,starts:{eW:!0,eE:!0,c:[{b:/[\w-]+\(/,rB:!0,c:[{cN:"built_in",b:/[\w-]+/},{b:/\(/,e:/\)/,c:[a.ASM,a.QSM]}]},a.CSSNM,a.QSM,a.ASM,a.CBCM,{cN:"number",b:"#[0-9A-Fa-f]+"},{cN:"meta",b:"!important"}]}}]};return{cI:!0,i:/[=\/|'\$]/,c:[a.CBCM,{cN:"selector-id",b:/#[A-Za-z0-9_-]+/},{cN:"selector-class",b:/\.[A-Za-z0-9_-]+/},{cN:"selector-attr",b:/\[/,e:/\]/,i:"$"},{cN:"selector-pseudo",b:/:(:)?[a-zA-Z0-9\_\-\+\(\)"'.]+/},{b:"@(font-face|page)",l:"[a-z-]+",k:"font-face page"},{b:"@",e:"[{;]",c:[{cN:"keyword",b:/\S+/},{b:/\s/,eW:!0,eE:!0,r:0,c:[a.ASM,a.QSM,a.CSSNM]}]},{cN:"selector-tag",b:b,r:0},{b:"{",e:"}",i:/\S/,c:[a.CBCM,c]}]}}),hljs.registerLanguage("xml",function(a){var b="[A-Za-z0-9\\._:-]+",c={eW:!0,i:/]+/}]}]}]};return{aliases:["html","xhtml","rss","atom","xsl","plist"],cI:!0,c:[{cN:"meta",b:"",r:10,c:[{b:"\\[",e:"\\]"}]},a.C("",{r:10}),{b:"<\\!\\[CDATA\\[",e:"\\]\\]>",r:10},{b:/<\?(php)?/,e:/\?>/,sL:"php",c:[{b:"/\\*",e:"\\*/",skip:!0}]},{cN:"tag",b:"|$)",e:">",k:{name:"style"},c:[c],starts:{e:"",rE:!0,sL:["css","xml"]}},{cN:"tag",b:"|$)",e:">",k:{name:"script"},c:[c],starts:{e:"",rE:!0,sL:["actionscript","javascript","handlebars","xml"]}},{cN:"meta",v:[{b:/<\?xml/,e:/\?>/,r:10},{b:/<\?\w+/,e:/\?>/}]},{cN:"tag",b:"",c:[{cN:"name",b:/[^\/><\s]+/,r:0},c]}]}}),hljs.registerLanguage("r",function(a){var b="([a-zA-Z]|\\.[a-zA-Z.])[a-zA-Z0-9._]*";return{c:[a.HCM,{b:b,l:b,k:{keyword:"function if in break next repeat else for return switch while try tryCatch stop warning require library attach detach source setMethod setGeneric setGroupGeneric setClass ...",literal:"NULL NA TRUE FALSE T F Inf NaN NA_integer_|10 NA_real_|10 NA_character_|10 NA_complex_|10"},r:0},{cN:"number",b:"0[xX][0-9a-fA-F]+[Li]?\\b",r:0},{cN:"number",b:"\\d+(?:[eE][+\\-]?\\d*)?L\\b",r:0},{cN:"number",b:"\\d+\\.(?!\\d)(?:i\\b)?",r:0},{cN:"number",b:"\\d+(?:\\.\\d*)?(?:[eE][+\\-]?\\d*)?i?\\b",r:0},{cN:"number",b:"\\.\\d+(?:[eE][+\\-]?\\d*)?i?\\b",r:0},{b:"`",e:"`",r:0},{cN:"string",c:[a.BE],v:[{b:'"',e:'"'},{b:"'",e:"'"}]}]}}),hljs.registerLanguage("markdown",function(a){return{aliases:["md","mkdown","mkd"],c:[{cN:"section",v:[{b:"^#{1,6}",e:"$"},{b:"^.+?\\n[=-]{2,}$"}]},{b:"<",e:">",sL:"xml",r:0},{cN:"bullet",b:"^([*+-]|(\\d+\\.))\\s+"},{cN:"strong",b:"[*_]{2}.+?[*_]{2}"},{cN:"emphasis",v:[{b:"\\*.+?\\*"},{b:"_.+?_",r:0}]},{cN:"quote",b:"^>\\s+",e:"$"},{cN:"code",v:[{b:"`.+?`"},{b:"^( {4}| )",e:"$",r:0}]},{b:"^[-\\*]{3,}",e:"$"},{b:"\\[.+?\\][\\(\\[].*?[\\)\\]]",rB:!0,c:[{cN:"string",b:"\\[",e:"\\]",eB:!0,rE:!0,r:0},{cN:"link",b:"\\]\\(",e:"\\)",eB:!0,eE:!0},{cN:"symbol",b:"\\]\\[",e:"\\]",eB:!0,eE:!0}],r:10},{b:"^\\[.+\\]:",rB:!0,c:[{cN:"symbol",b:"\\[",e:"\\]:",eB:!0,eE:!0,starts:{cN:"link",e:"$"}}]}]}}),window.performance=window.performance||{},performance.now=function(){return performance.now||performance.mozNow||performance.msNow||performance.oNow||performance.webkitNow}(),window.gccConfig={content:{message:"On our website we're using cookies to optimize user experience and to improve our website. By using our website you agree that cookies can be stored on your local computer."},palette:{popup:{background:"#333333",text:"#fff",link:"#fff"},button:{background:"#8dc63f",text:"#fff"}}},function(a){"use strict";!function(a){a.fn.stickyStuff=function(){var b=this,c=function(){var c=window.location.hash,d=c?'a[href="'+c+'"]':"li.active > a";if("tab"===a(d,b).data("toggle"))a(d,b).tab("show");else if("collapse"===a(d,b).data("toggle")){var e=c;a(e,b).collapse("show");var f=a(e,b).parents(".tab-pane");f&&!f.hasClass("active")&&a("a[href=#"+f.attr("id")+"]").tab("show")}};return c(b),a(window).on("hashchange",function(){c(b)}),a("a",b).on("click",function(a){history.pushState(null,null,this.href),c(b)}),this}}(jQuery),a(function(){if(1==a(".schmail").length){var b=a(".schmail").attr("href");b=b.replace("IMNOTSENDINGSPAMTO","").replace("that-big-googly-eyed-email-provider","gmail").replace(encodeURIComponent("If you are not a robot, I have high hopes that you can figure out how to get my proper email address from the above."),"").replace(encodeURIComponent("\r\n\r\n"),""),a(".schmail").attr("href",b)}if(a("*[title]").tooltip({container:"body"}),hljs.initHighlighting(),a(".nav-tabs, .tab-content").stickyStuff(),a(".navbar-toggle").attr("style","-ms-touch-action: manipulation; touch-action: manipulation;"),a("ul.menu-highlight a").each(function(){var b=a(this),c=b.attr("href");c===document.location.href&&b.parents("li").addClass("active")}),a(".social-share-icon").unbind("click").bind("click",function(){var b=a(this),c=b.attr("data-href");c&&(b.attr("data-target")?window.open(c,b.attr("data-target"),b.attr("data-width")?"width="+b.attr("data-width")+",height="+b.attr("data-height"):void 0):window.location.href=c)}),a(".copy_clipboard").click(function(){this.select();try{var b=document.execCommand("copy");b&&a(this).tooltip({title:"Link was copied to clipboard.",position:"top"}).tooltip("show")}catch(c){}}),a(".copy-url").click(function(){try{var b=a(this).data("url"),c=document.createElement("input");document.body.appendChild(c),c.value=b,c.select(),document.execCommand("copy"),document.body.removeChild(c),bootstrap_modal("URL Copied",b)}catch(d){}}),a(".monkey-bar-modal").length&&a(".monkey-bar-modal").appendTo("body"),!cookies_enabled()){var c="The use of cookies seems to have been disabled in your browser. ";c+="In order to be able to use formr and have a good user experience, you will need to enable cookies.",bootstrap_modal("Cookies Disabled",c)}})}(jQuery),function(a){"use strict";function b(b,c){a(c).click(function(b){b.preventDefault();var c=a(this),d=c.attr("href");return""===d?!1:(c.attr("href",""),a.ajax({type:"GET",url:d,dataType:"html"}).done(a.proxy(function(b){var c=a(this);c.attr("href",d),c.hasClass("danger")||c.css("color","green");var e=c.find("i.fa");e.hasClass("fa-stethoscope")?(e.addClass("fa-heartbeat"),e.removeClass("fa-stethoscope")):e.hasClass("fa-heartbeat")?(e.removeClass("fa-heartbeat"),e.addClass("fa-stethoscope")):bootstrap_modal("Alert",b,"tpl-feedback-modal"),c.hasClass("refresh_on_success")&&document.location.reload(!0)},this)).fail(a.proxy(function(b,c,e,f){a(this).attr("href",d),ajaxErrorHandling(b,c,e,f)},this)),!1)})}function c(b,c){a(c).submit(function(b){b.preventDefault();var c=a(this),d=c.find("button[type=submit].btn");return d.attr("disabled",!0),a.ajax({type:c.attr("method"),url:c.attr("action"),data:c.serialize(),dataType:"html"}).done(a.proxy(function(b){d.attr("disabled",!1),d.css("color","green"),a(".alerts-container").prepend(b),d.hasClass("refresh_on_success")&&document.location.reload(!0)},this)).fail(a.proxy(function(a,b,c,e){d.attr("disabled",!1),ajaxErrorHandling(a,b,c,e)},this)),!1})}function d(b){var c=parseInt(a(this).data("user"),10),d=a(this).data("email");if(c&&d){var f={user_id:c,user_email:d,user_api:!0,api_action:"get"};i(saAjaxUrl,f,function(a){a&&a.success&&e(a.data,f)})}}function e(b,c){var d=a(a.parseHTML(getHTMLTemplate("tpl-user-api",{user:" ("+b.user+")",client_id:b.client_id,client_secret:b.client_secret})));b.client_id?d.find(".api-create").remove():d.find(".api-change, .api-delete").remove(),d.on("shown.bs.modal",function(){d.find(".api-create").click(function(){if(confirm("Are you sure?")){var a={user_id:c.user_id,user_email:c.user_email,user_api:!0,api_action:"create"};i(saAjaxUrl,a,function(a){a&&a.success&&(d.modal("hide"),e(a.data,c))})}}),d.find(".api-change").click(function(){if(confirm("Are you sure?")){var a={user_id:c.user_id,user_email:c.user_email,user_api:!0,api_action:"change"};i(saAjaxUrl,a,function(a){a&&a.success&&(d.modal("hide"),e(a.data,c))})}}),d.find(".api-delete").click(function(){if(confirm("Are you sure?")){var a={user_id:c.user_id,user_email:c.user_email,user_api:!0,api_action:"delete"};i(saAjaxUrl,a,function(a){a&&a.success&&(d.modal("hide"),e({user:b.user,client_id:"",client_secret:""},c))})}})}).on("hidden.bs.modal",function(){d.remove()}).modal("show")}function f(b){var d=a(this),e=a(a.parseHTML(getHTMLTemplate("tpl-delete-run-session",{action:d.data("href"),session:d.data("session")}))),f=d.parents("tr");d.css("border-color","#ee5f5b"),d.css("color","#ee5f5b"),e.find("form").each(c).submit(function(){f.css("background-color","#ee5f5b"),e.modal("hide")}),e.on("hidden.bs.modal",function(){e.remove(),d.css("border-color","black"),d.css("color","black")}).modal("show")}function g(b){var c=a(this),d=a(a.parseHTML(getHTMLTemplate("tpl-confirmation",{content:c.data("msg"),yes_url:c.data("href"),no_url:"#"})));d.on("shown.bs.modal",function(){d.find(".btn-yes").click(function(){var b=c.data("href");a(this).append(bootstrap_spinner()),i(b,{ajax:!0},function(a){d.modal("hide");var b=bootstrap_modal("Activity Deleted",a,"tpl-feedback-modal");b.on("hidden.bs.modal",function(){d.remove(),document.location.reload(!0)}).modal("show")},"html")})}).on("hidden.bs.modal",function(){d.remove()}).modal("show")}function h(b){var c=a(this),d=a(a.parseHTML(getHTMLTemplate("tpl-remind-run-session",{action:c.data("href"),session:c.data("session")})));c.parents("tr");d.on("shown.bs.modal",function(){d.find(".send").click(function(){var b=c.data("href"),e=a(this).data("reminder");a(this).append(bootstrap_spinner()),i(b,{reminder:e},function(a){d.modal("hide"),bootstrap_modal("Send Reminder",a,"tpl-feedback-modal").modal("show"),c.css("border-color","green"),c.css("color","green")},"html")})}),d.on("hidden.bs.modal",function(){d.remove()}),a.get(c.data("href"),{session:c.data("session"),get_count:!0},function(a,b,c){d.find(".reminder-row-count").text("(0)");for(var e in a){var f=a[e];d.find(".reminder-row-count-"+e).text(" ("+f+")")}d.modal("show")})}function i(b,c,d,e,f){e=e||"json",f=f||function(){},a.ajax({type:"POST",url:b,data:c,dataType:e,success:function(a,b,c){d(a)},error:function(b,c){a(".alerts-container").prepend(c),f(b,c)},beforeSend:function(a){}})}function j(){var b=a(this);"single"===b.data("active")?(b.siblings(".single").addClass("hidden"),b.siblings(".multiple").removeClass("hidden"),b.data("active","multiple")):(b.siblings(".single").removeClass("hidden"),b.siblings(".multiple").addClass("hidden"),b.data("active","single"))}function k(b){var c=a(this),d=[];a("input.ba-select-session").each(function(){a(this).is(":checked")&&d.push(a(this).val())}),d.length&&"function"==typeof l[c.data("action")]&&l[c.data("action")](c.parents("form").attr("action"),d)}var l={toggleTest:function(b,c){var d=a(a.parseHTML(getHTMLTemplate("tpl-confirmation",{content:"

    Are you sure you want to perform this action?

    "})));d.on("shown.bs.modal",function(){d.find(".btn-yes").click(function(e){a(this).append(bootstrap_spinner()),i(b,{action:"toggleTest",sessions:c},function(c){return d.modal("hide"),c.success?void(document.location=b.replace("ajax_user_bulk_actions","user_overview")):(a(this).find(".fa-spin").remove(),void bootstrap_modal("Error",c.error,"tpl-feedback-modal").modal("show"))},"json")}),d.find(".btn-no").click(function(a){d.modal("hide")})}).on("hidden.bs.modal",function(){d.remove()}).modal("show")},sendReminder:function(b,c){var d=a(a.parseHTML(getHTMLTemplate("tpl-remind-run-session",{action:b,session:null})));d.on("shown.bs.modal",function(){d.find(".send").click(function(){var e=a(this).data("reminder");a(this).append(bootstrap_spinner()),i(b,{action:"sendReminder",sessions:c,reminder:e},function(a){return d.modal("hide"),a.success?void(document.location=b.replace("ajax_user_bulk_actions","user_overview")):void bootstrap_modal("Error",a.error,"tpl-feedback-modal").modal("show")},"json")})}).on("hidden.bs.modal",function(){d.remove()}).modal("show")},deleteSessions:function(b,c){var d=a(a.parseHTML(getHTMLTemplate("tpl-confirmation",{content:"

    Are you sure you want delete "+c.length+" session(s)?

    "})));d.on("shown.bs.modal",function(){d.find(".btn-yes").click(function(e){a(this).append(bootstrap_spinner()),i(b,{action:"deleteSessions",sessions:c},function(c){return d.modal("hide"),c.success?void(document.location=b.replace("ajax_user_bulk_actions","user_overview")):(a(this).find(".fa-spin").remove(),void bootstrap_modal("Error",c.error,"tpl-feedback-modal").modal("show"))},"json")}),d.find(".btn-no").click(function(a){d.modal("hide")})}).on("hidden.bs.modal",function(){d.remove()}).modal("show")},positionSessions:function(b,c){var d=parseInt(a("select[name=ba_new_position]").val());if(isNaN(d))return void alert("Bad position selected");var e=a(a.parseHTML(getHTMLTemplate("tpl-confirmation",{content:"

    Are you sure you want push "+c.length+" session(s) to position "+d+"?

    "})));e.on("shown.bs.modal",function(){e.find(".btn-yes").click(function(f){a(this).append(bootstrap_spinner()),i(b,{action:"positionSessions",sessions:c,pos:d},function(c){return e.modal("hide"),c.success?void(document.location=b.replace("ajax_user_bulk_actions","user_overview")):(a(this).find(".fa-spin").remove(),void bootstrap_modal("Error",c.error,"tpl-feedback-modal").modal("show"))},"json")}),e.find(".btn-no").click(function(a){e.modal("hide")})}).on("hidden.bs.modal",function(){e.remove()}).modal("show")}};a(function(){var e;a(".form-ajax").each(c),a(".link-ajax").each(b),a(".link-ajax .fa-pause").parent(".btn").mouseenter(function(){a(this).find(".fa").removeClass("fa-pause").addClass("fa-play")}).mouseleave(function(){a(this).find(".fa").addClass("fa-pause").removeClass("fa-play")}),a(".link-ajax .fa-stop").parent(".btn").mouseenter(function(){a(this).find(".fa").removeClass("fa-stop").addClass("fa-play")}).mouseleave(function(){a(this).find(".fa").addClass("fa-stop").removeClass("fa-play")}),a(".api-btn").click(d),a(".sessions-search-switch").click(j),a(".hidden_debug_message").length>0&&(a(".show_hidden_debugging_messages").click(function(){return a(".hidden_debug_message").toggleClass("hidden"),!1}),a(".show_hidden_debugging_messages").attr("disabled",!1)),a("abbr.abbreviated_session").click(function(){a(this).text()!==a(this).data("full-session")?a(this).text(a(this).data("full-session")):a(this).text(a(this).data("full-session").substr(0,10)+"…")}),a(".download_r_code").length>0&&a(".download_r_code").click(function(){return download_next_textarea(this)}),a(".removal_modal").on("show.bs.modal",function(c){e=a(c.relatedTarget);var d=a(this);e.parents("tr").css("background-color","#ee5f5b"),a(this).find(".danger").attr("href",e.data("href")),b(1,a(this).find(".danger")),a(this).find(".danger").click(function(a){e.css("color","#ee5f5b"),d.hasClass("refresh_on_success")&&window.setTimeout(function(){document.location.reload(!0)},200),d.modal("hide")})}).on("hide.bs.modal",function(a){e.parents("tr").css("background-color","transparent")}),a("a.delete-run-session").bind("click",f),a("a.delete-user-unit-session").bind("click",g),a("a.remind-run-session").bind("click",h),a("div.bulk-actions-ba").find(".ba").bind("click",k)})}(jQuery),function(a){"use strict";function b(b){this.run=b,this.block=a('
    '),b.form.find(".run_units").append(this.block)}function c(c){"undefined"==typeof this.autosaved&&(this.lastSave=a.now(),this.autosaved=!1),this.form=c,this.form.submit(function(){return!1}),this.name=this.form.find(".run_name").val(),this.url=this.form.prop("action"),this.units=[];for(var d=a.parseJSON(this.form.attr("data-units")),e=0;e0&&!c.select2("container").hasClass("select2-container")){var d,e=c.attr("data-select2init");d="object"!=typeof e?a.parseJSON(e):e,c.select2({createSearchChoice:function(b,c){return 0===a(c).filter(function(){return 0===this.text.localeCompare(b)}).length?{id:b,text:b}:void 0},initSelection:function(b,c){var e;e={id:b.val(),text:b.val()},a.each(d,function(a,c){return c.id===b.val()?(e=c,!1):void 0}),c(e)},data:d})}this.unsavedChanges=!1,this.save_button=this.block.find("a.unit_save"),this.block.find("button.from_days").click(function(b){b.preventDefault();var c=a(this).closest(".input-group").find("input[type=number]"),d=c.val();c.val(60*d*24).change()}),this.test_button=this.block.find("a.unit_test"),this.test_button.click(a.proxy(this.test,this)),this.remove_button=this.block.find("button.remove_unit_from_run"),this.remove_button.click(a.proxy(this.removeFromRun,this)).mouseenter(function(){a(this).addClass("btn-danger")}).mouseleave(function(){a(this).removeClass("btn-danger")});var f=this.block.find("textarea");f[0]&&(this.textarea=a(f[0]),this.session=this.hookAceToTextarea(this.textarea)),f[1]&&(this.textarea2=a(f[1]),this.session2=this.hookAceToTextarea(this.textarea2)),this.run.lock(this.run.lock_toggle.hasClass("btn-checked"),this.block),this.save_button.unbind("click"),this.save_button.attr("disabled",!0).removeClass("btn-info").text("Saved").click(a.proxy(this.save,this))},b.prototype.position_changes=function(a){this.position_changed||(this.position_changed=!0,this.run.reorder_button.addClass("btn-info").removeAttr("disabled")),this.position.parent().addClass("pos_changed")},b.prototype.changes=function(a){this.unsavedChanges||(this.unsavedChanges=!0,this.save_button.addClass("btn-info").removeAttr("disabled").text("Save changes"),this.test_button.attr("disabled","disabled"))},b.prototype.test=function(b){b.preventDefault();var c=this.test_button.text();this.test_button.attr("disabled",!0).html(c+bootstrap_spinner());this.block;return a.ajax({url:this.run.url+"/"+this.test_button.attr("href"),dataType:"html",data:{run_unit_id:this.run_unit_id,special:this.special},method:"GET"}).done(a.proxy(function(b){var d=bootstrap_modal("Test Results",b);a(".opencpu_accordion",d).collapse({toggle:!0}),this.test_button.html(c).removeAttr("disabled");var e=d.find("pre code");Array.prototype.forEach.call(e,hljs.highlightBlock),d.find(".download_r_code").length>0&&d.find(".download_r_code").click(function(){return download_next_textarea(this)})},this)).fail(a.proxy(function(a,b,d,e){this.test_button.attr("disabled",!1).html(c),ajaxErrorHandling(a,b,d,e)},this)),!1},b.prototype.save=function(b){b.preventDefault();var c=this.save_button.text();this.save_button.attr("disabled","disabled").html(c+bootstrap_spinner()),this.session&&this.textarea.val(this.session.getValue()),this.session2&&this.textarea2.val(this.session2.getValue());this.block;return a.ajax({url:this.run.url+"/"+this.save_button.attr("href"),dataType:"html",data:this.save_inputs.serialize(),method:"POST"}).done(a.proxy(function(b){""!==b?a.proxy(this.init(b),this):(this.save_button.unbind("click"),this.save_button.attr("disabled",!0).removeClass("btn-info").text("Saved").click(a.proxy(this.save,this)),this.unsavedChanges=!1,this.test_button.removeAttr("disabled"))},this)).fail(a.proxy(function(a,b,d,e){this.save_button.removeAttr("disabled").html(c),ajaxErrorHandling(a,b,d,e)},this)),!1},b.prototype.hookAceToTextarea=function(b){var c=b.data("editor"),d=a("
    ",{position:"absolute",width:b.width(),height:b.height(),"class":b.attr("class")}).insertBefore(b);b.css("display","none"),this.editor=ace.edit(d[0]),this.editor.setOptions({minLines:b.attr("rows")?b.attr("rows"):3,maxLines:30}),this.editor.setTheme("ace/theme/textmate"),this.editor.$blockScrolling=1/0;var e=this.editor.getSession();return e.setValue(b.val()),this.editor.renderer.setShowGutter(!1),e.setUseWrapMode(!0),e.setWrapLimitRange(42,42),e.setMode("ace/mode/"+c),this.editor.on("change",a.proxy(this.changes,this)),e},b.prototype.removeFromRun=function(b,c){b.preventDefault(),a(".tooltip").hide();var d,e=this,f=b,g=this.block,h=this.run.url+"/"+this.remove_button.attr("href"),i={run_unit_id:this.run_unit_id};return"yes"===c&&(i.confirm="yes"),g.hide(),a.ajax({url:h,dataType:"html",data:i,method:"POST"}).done(a.proxy(function(b){if(g.show(),b)if("warn"===b)d=a(a.parseHTML(getHTMLTemplate("tpl-confirmation",{content:"Are you sure you want to delete this run unit and all it's data?"}))),d.on("shown.bs.modal",function(){d.find(".btn-yes").click(function(a){e.removeFromRun(f,"yes"),d.modal("hide")}),d.find(".btn-no").click(function(a){d.modal("hide"),e.removeFromRun(f,"no")})}).on("hidden.bs.modal",function(){d.remove()}).modal("show");else{g.html(b),g.show();var c=this.run.units.indexOf(this);c>-1&&this.run.units.splice(c,1)}},this)).fail(function(a,b,c,d){g.show(),ajaxErrorHandling(a,b,c,d)}),!1},b.prototype.serialize=function(){var a=this.save_inputs.serializeArray(),b={};b.type=this.block.find(".run_unit_inner").data("type");for(var c=0;cb&&(b=d)}),null===b&&(b=0),b},c.prototype.loadUnit=function(b,c){a.ajax({url:this.url+"/ajax_get_unit",data:b,dataType:"html",success:a.proxy(function(a,b){c.init(a)},this)})},c.prototype.addUnit=function(c){var d=this.getMaxPosition(),e=new b(this);this.units.push(e),a.ajax({url:c,dataType:"html",method:"POST",data:{position:d+10}}).done(a.proxy(function(a){e.init(a)},this)).fail(ajaxErrorHandling)},c.prototype.exportUnits=function(){var b={},c=this.url,d=!1,e=a("
    "),f=this.lock_toggle.hasClass("btn-checked");f&&this.lock(!1,this.form);for(var g=0;g-1?(bootstrap_alert("You used the position "+c+" more than once, therefore the new order could not be saved. Click here to scroll to the duplicated position.","Error.",".run_units"),f=!0):(d[g.run_unit_id]=c,e.push(c))}),!f)return a.ajax({url:this.reorder_button.attr("href"),dataType:"html",method:"POST",data:{position:d}}).done(a.proxy(function(b){a(this.units).each(function(a,b){b.position_changed=!1}),this.reorder_button.removeClass("btn-info").attr("disabled","disabled");var c=e.join(","),d=e.sort(function(a,b){return a-b}).join(",");if(this.form.find(".pos_changed").removeClass("pos_changed"),c!=d){var f=this.form;a(this.units.sort(function(a,b){return+a.position.val()-+b.position.val()})).each(function(a,b){f.find(".run_units").append(b.block)})}},this)).fail(ajaxErrorHandling),!1}},c.prototype.lock=function(b,c){var d=this.units;for(var e in d)d[e].editor&&d[e].editor.setReadOnly(b);var f=c.find(".import_run_units, .run_unit_description, .position, .remove_unit_from_run, .reorder_units, .unit_save, .form-control, select, .from_days, .add_run_unit");f.each(function(c,d){b?(a("#run-unit-choices").hide(),d.onclick&&(d.onclick_disabled=d.onclick,d.onclick=function(a){return a.preventDefault(),!1}),a(d).attr("data-old_disabled",a(d).attr("disabled")),a(d).attr("disabled","disabled")):(a("#run-unit-choices").show(),d.onclick_disabled&&(d.onclick=d.onclick_disabled),a(d).attr("data-old-disabled")&&""!==a(d).attr("data-old-disabled")?a(d).attr("disabled",a(d).attr("data-old-disabled")):a(d).removeAttr("disabled"))})},c.prototype.publicToggle=function(b){var c=a(this);return c.parents(".btn-group").find(".btn-checked").removeClass("btn-checked"),c.toggleClass("btn-checked",1),a.ajax({url:c.attr("href"),dataType:"html",method:"POST"}).fail(ajaxErrorHandling),!1},c.prototype.panic=function(b){var c={content:"

    Don't panic!

    Are things not going your way? Users end up in the wrong places, too many emails being sent?

    This run will be made private, locked and cron tasks (automatic email and text messages) will be disabled. You will need to manually undo these actions later.

    This will essentially buy you some time to fix the bug or get help. By configuring the service message under settings, you can show your users an explanation.

    ",yes_url:this.url+"/panic",no_url:"javascript:void(0);"},d=a(a.parseHTML(getHTMLTemplate("tpl-confirmation",c))).attr("id","run-panic-dialog");d.on("shown.bs.modal",function(){d.find(".btn-yes").click(function(b){document.location=a(this).data("yes")})}).on("hidden.bs.modal",function(){d.remove()}).modal("show")},a(function(){a(".edit_run").each(function(b,d){new c(a(d))})})}(jQuery),function(a){"use strict";function b(b,c){var d=a(c),e=d.data("editor"),f=a("
    ",{position:"absolute",width:"100%",height:d.height(),"class":d.attr("class")}).insertBefore(d);d.css("display","none");var g=ace.edit(f[0]);g.setOptions({minLines:d.attr("rows")?d.attr("rows"):3,maxLines:200}),g.setTheme("ace/theme/textmate");var h=g.getSession();h.setValue(d.val()),h.setUseWrapMode(!0),h.setMode("ace/mode/"+e);var i=a(c).parents("form");g.on("change",function(){i.trigger("change")}),i.on("ajax_submission",function(){d.val(h.getValue())})}function c(b,c){a(c).prop("disabled",!0);var d=a(c).parents("form");d.change(function(){a(c).prop("disabled",!1)}).submit(function(){return!1}),a(c).click(function(b){return d.trigger("ajax_submission"),b.preventDefault(),a.ajax({url:d.attr("action"),dataType:"html",data:d.serialize(),method:"POST"}).done(function(b){""!==b&&a(b).insertBefore(d),a(c).prop("disabled",!0)}).fail(function(a,b,c,d){ajaxErrorHandling(a,b,c,d)}),!1})}a(function(){a("textarea.big_ace_editor").each(b),a(".save_settings").each(c)})}(jQuery),"undefined"==typeof jQuery)throw new Error("AdminLTE requires jQuery");$.AdminLTE={},$.AdminLTE.options={navbarMenuSlimscroll:!0,navbarMenuSlimscrollWidth:"3px",navbarMenuHeight:"200px",animationSpeed:500,sidebarToggleSelector:"[data-toggle='offcanvas']",sidebarPushMenu:!0,sidebarSlimScroll:!0,sidebarExpandOnHover:!1,enableBoxRefresh:!0,enableBSToppltip:!0,BSTooltipSelector:"[data-toggle='tooltip']",enableFastclick:!1,enableControlTreeView:!0,enableControlSidebar:!0,controlSidebarOptions:{toggleBtnSelector:"[data-toggle='control-sidebar']",selector:".control-sidebar",slide:!0},enableBoxWidget:!0,boxWidgetOptions:{boxWidgetIcons:{collapse:"fa-minus",open:"fa-plus",remove:"fa-times"},boxWidgetSelectors:{remove:'[data-widget="remove"]',collapse:'[data-widget="collapse"]'}},directChat:{enable:!0,contactToggleSelector:'[data-widget="chat-pane-toggle"]'},colors:{lightBlue:"#3c8dbc",red:"#f56954",green:"#00a65a",aqua:"#00c0ef", +yellow:"#f39c12",blue:"#0073b7",navy:"#001F3F",teal:"#39CCCC",olive:"#3D9970",lime:"#01FF70",orange:"#FF851B",fuchsia:"#F012BE",purple:"#8E24AA",maroon:"#D81B60",black:"#222222",gray:"#d2d6de"},screenSizes:{xs:480,sm:768,md:992,lg:1200}},$(function(){"use strict";$("body").removeClass("hold-transition"),"undefined"!=typeof AdminLTEOptions&&$.extend(!0,$.AdminLTE.options,AdminLTEOptions);var a=$.AdminLTE.options;_init(),$.AdminLTE.layout.activate(),a.enableControlTreeView&&$.AdminLTE.tree(".sidebar"),a.enableControlSidebar&&$.AdminLTE.controlSidebar.activate(),a.navbarMenuSlimscroll&&"undefined"!=typeof $.fn.slimscroll&&$(".navbar .menu").slimscroll({height:a.navbarMenuHeight,alwaysVisible:!1,size:a.navbarMenuSlimscrollWidth}).css("width","100%"),a.sidebarPushMenu&&$.AdminLTE.pushMenu.activate(a.sidebarToggleSelector),a.enableBSToppltip&&$("body").tooltip({selector:a.BSTooltipSelector,container:"body"}),a.enableBoxWidget&&$.AdminLTE.boxWidget.activate(),a.enableFastclick&&"undefined"!=typeof FastClick&&FastClick.attach(document.body),a.directChat.enable&&$(document).on("click",a.directChat.contactToggleSelector,function(){var a=$(this).parents(".direct-chat").first();a.toggleClass("direct-chat-contacts-open")}),$('.btn-group[data-toggle="btn-toggle"]').each(function(){var a=$(this);$(this).find(".btn").on("click",function(b){a.find(".btn.active").removeClass("active"),$(this).addClass("active"),b.preventDefault()})})}),function(a){"use strict";a.fn.boxRefresh=function(b){function c(a){a.append(f),e.onLoadStart.call(a)}function d(a){a.find(f).remove(),e.onLoadDone.call(a)}var e=a.extend({trigger:".refresh-btn",source:"",onLoadStart:function(a){return a},onLoadDone:function(a){return a}},b),f=a('
    ');return this.each(function(){if(""===e.source)return void(window.console&&window.console.log("Please specify a source first - boxRefresh()"));var b=a(this),f=b.find(e.trigger).first();f.on("click",function(a){a.preventDefault(),c(b),b.find(".box-body").load(e.source,function(){d(b)})})})}}(jQuery),function(a){"use strict";a.fn.activateBox=function(){a.AdminLTE.boxWidget.activate(this)},a.fn.toggleBox=function(){var b=a(a.AdminLTE.boxWidget.selectors.collapse,this);a.AdminLTE.boxWidget.collapse(b)},a.fn.removeBox=function(){var b=a(a.AdminLTE.boxWidget.selectors.remove,this);a.AdminLTE.boxWidget.remove(b)}}(jQuery),function(a){"use strict";a.fn.todolist=function(b){var c=a.extend({onCheck:function(a){return a},onUncheck:function(a){return a}},b);return this.each(function(){"undefined"!=typeof a.fn.iCheck?(a("input",this).on("ifChecked",function(){var b=a(this).parents("li").first();b.toggleClass("done"),c.onCheck.call(b)}),a("input",this).on("ifUnchecked",function(){var b=a(this).parents("li").first();b.toggleClass("done"),c.onUncheck.call(b)})):a("input",this).on("change",function(){var b=a(this).parents("li").first();b.toggleClass("done"),a("input",b).is(":checked")?c.onCheck.call(b):c.onUncheck.call(b)})})}}(jQuery),jQuery(document).ready(function(){$("#user-overview-select-all").click(function(){var a=$(this),b=a.parents("table").find(".ba-select-session");a.is(":checked")?b.prop("checked",!0):b.prop("checked",!1)}),$(".dashboard-link").each(function(a,b){var c=$(b).data("click");1==c&&$(b).trigger("click")})}); \ No newline at end of file diff --git a/webroot/assets/build/js/formr-material.min.js b/webroot/assets/build/js/formr-material.min.js index 4550b4e27..00e19ec34 100644 --- a/webroot/assets/build/js/formr-material.min.js +++ b/webroot/assets/build/js/formr-material.min.js @@ -5,5 +5,5 @@ e.removeClass(["active",h].join(" ")),i.sliding=!1,setTimeout(function(){i.$elem dataFilter:m}))},basePath:d},support:k,bugs:{},modules:{},features:{},featureList:[],setOptions:function(b,c){"string"==typeof b&&arguments.length>1?q[b]=a.isPlainObject(c)?a.extend(!0,q[b]||{},c):c:"object"==typeof b&&a.extend(!0,q,b)},_getAutoEnhance:n,addPolyfill:function(b,c){c=c||{};var d=c.f||b;r[d]||(r[d]=[],f.featureList.push(d),q[d]={}),r[d].push(b),c.options=a.extend(q[d],c.options),y(b,c),c.methodNames&&a.each(c.methodNames,function(a,b){f.addMethodName(b)})},polyfill:function(){var a={};return function(b){if(b||(b=f.featureList,WSDEBUG&&f.warn("loading all features without specifing might be bad for performance")),"string"==typeof b&&(b=b.split(" ")),WSDEBUG)for(var c=0;ci;i++)k=!1,h=l[i],-1==a.inArray(h,b)&&("noCombo"!=q.debug&&a.each(v[h].c,m),k||c(v[h].src||h,h))}}(),makePath:function(a){return-1!=a.indexOf("//")||0===a.indexOf("/")?a:(-1==a.indexOf(".")&&(a+=".js"),q.addCacheBuster&&(a+=q.addCacheBuster),q.basePath+a)},loadCSS:function(){var b,c={};return function(d){d=this.makePath(d),c[d]||(b=b||a("link, style")[0]||a("script")[0],c[d]=1,a('').insertBefore(b).attr({href:d}))}}(),loadScript:function(){var b={};return function(c,d,e,f){if(f||(c=w.makePath(c)),!b[c]){var g=function(){d&&d(),e&&("string"==typeof e&&(e=e.split(" ")),a.each(e,function(a,b){v[b]&&(v[b].afterLoad&&v[b].afterLoad(),s(v[b].noAutoCallback?b+"FileLoaded":b,!0))}))};b[c]=1,q.loadScript(c,g,a.noop)}}}()}});var q=f.cfg,r=f.features,s=f.isReady,t=f.ready,u=f.addPolyfill,v=f.modules,w=f.loader,x=w.loadList,y=w.addModule,z=f.bugs,A=[],B={warn:1,error:1},C=a.fn,D=b("video");f.addMethodName=function(a){a=a.split(":");var b=a[1];1==a.length?(b=a[0],a=a[0]):a=a[0],C[a]=function(){return this.callProp(b,arguments)}},C.callProp=function(b,c){var d;return c||(c=[]),this.each(function(){var e=a.prop(this,b);if(e&&e.apply){if(d=e.apply(this,c),void 0!==d)return!1}else f.warn(b+" is not a method of "+this)}),void 0!==d?d:this},f.activeLang=function(){"language"in e||(e.language=e.browserLanguage||"");var b=a.attr(document.documentElement,"lang")||e.language;return t("webshimLocalization",function(){f.activeLang(b)}),function(a){if(a)if("string"==typeof a)b=a;else if("object"==typeof a){var c=arguments,d=this;t("webshimLocalization",function(){f.activeLang.apply(d,c)})}return b}}(),f.errorLog=[],a.each(["log","error","warn","info"],function(a,b){f[b]=function(a){(B[b]&&q.debug!==!1||q.debug)&&(f.errorLog.push(a),window.console&&console.log&&console[console[b]?b:"log"](a))}}),WSDEBUG&&(f._curScript.wsFoundCurrent||f.error("Could not detect currentScript! Use basePath to set script path.")),function(){a.isDOMReady=a.isReady;var b=function(){a.isDOMReady=!0,s("DOM",!0),setTimeout(function(){s("WINDOWLOAD",!0)},9999)};c=function(){if(!c.run){if(WSDEBUG&&a.mobile&&(a.mobile.textinput||a.mobile.rangeslider||a.mobile.button)&&(f.info('jQM textinput/rangeslider/button detected waitReady was set to false. Use webshims.ready("featurename") to script against polyfilled methods/properties'),q.readyEvt||f.error('in a jQuery mobile enviroment: you should change the readyEvt to "pageinit".'),q.waitReady&&f.error("in a jQuery mobile enviroment: you should change the waitReady to false.")),WSDEBUG&&q.waitReady&&a.isReady&&f.warn("Call webshims.polyfill before DOM-Ready or set waitReady to false."),!a.isDOMReady&&q.waitReady){var d=a.ready;a.ready=function(a){return a!==!0&&document.body&&b(),d.apply(this,arguments)},a.ready.promise=d.promise}q.readyEvt?a(document).one(q.readyEvt,b):a(b)}c.run=!0},a(window).on("load",function(){b(),setTimeout(function(){s("WINDOWLOAD",!0)},9)});var d=[],e=function(){1==this.nodeType&&f.triggerDomUpdate(this)};a.extend(f,{addReady:function(a){var b=function(b,c){f.ready("DOM",function(){a(b,c)})};d.push(b),q.wsdoc&&b(q.wsdoc,i)},triggerDomUpdate:function(b){if(!b||!b.nodeType)return void(b&&b.jquery&&b.each(function(){f.triggerDomUpdate(this)}));var c=b.nodeType;if(1==c||9==c){var e=b!==document?a(b):i;a.each(d,function(a,c){c(b,e)})}}}),C.clonePolyfill=C.clone,C.htmlPolyfill=function(b){if(!arguments.length)return a(this.clonePolyfill()).html();var c=C.html.call(this,b);return c===this&&a.isDOMReady&&this.each(e),c},C.jProp=function(){return this.pushStack(a(C.prop.apply(this,arguments)||[]))},a.each(["after","before","append","prepend","replaceWith"],function(b,c){C[c+"Polyfill"]=function(b){return b=a(b),C[c].call(this,b),a.isDOMReady&&b.each(e),this}}),a.each(["insertAfter","insertBefore","appendTo","prependTo","replaceAll"],function(b,c){C[c.replace(/[A-Z]/,function(a){return"Polyfill"+a})]=function(){return C[c].apply(this,arguments),a.isDOMReady&&f.triggerDomUpdate(this),this}}),C.updatePolyfill=function(){return a.isDOMReady&&f.triggerDomUpdate(this),this},a.each(["getNativeElement","getShadowElement","getShadowFocusElement"],function(a,b){C[b]=function(){return this.pushStack(this)}})}(),WSDEBUG&&(q.debug=!0),l.create&&(f.objectCreate=function(b,c,d){WSDEBUG&&c&&f.error("second argument for webshims.objectCreate is only available with DOM support");var e=l.create(b);return d&&(e.options=a.extend(!0,{},e.options||{},d),d=e.options),e._create&&a.isFunction(e._create)&&e._create(d),e}),y("swfmini",{test:function(){return window.swfobject&&!window.swfmini&&(window.swfmini=window.swfobject),"swfmini"in window},c:[16,7,2,8,1,12,23]}),v.swfmini.test(),y("sizzle",{test:a.expr.filters}),u("es5",{test:!(!k.ES5||!Function.prototype.bind),d:["sizzle"]}),u("dom-extend",{f:g,noAutoCallback:!0,d:["es5"],c:[16,7,2,15,30,3,8,4,9,10,25,31,34]}),b("picture"),u("picture",{test:"picturefill"in window||!!window.HTMLPictureElement||"respimage"in window,d:["matchMedia"],c:[18],loadInit:function(){s("picture",!0)}}),u("matchMedia",{test:!(!window.matchMedia||!matchMedia("all").addListener),c:[18]}),u("sticky",{test:-1!=(a(b("b")).attr("style","position: -webkit-sticky; position: sticky").css("position")||"").indexOf("sticky"),d:["es5","matchMedia"]}),u("es6",{test:!!(Math.imul&&Number.MIN_SAFE_INTEGER&&l.is&&window.Promise&&Promise.all),d:["es5"]}),u("geolocation",{test:"geolocation"in e,options:{destroyWrite:!0},c:[21]}),function(){u("canvas",{src:"excanvas",test:"getContext"in b("canvas"),options:{type:"flash"},noAutoCallback:!0,loadInit:function(){var a=this.options.type;!a||-1===a.indexOf("flash")||v.swfmini.test()&&!swfmini.hasFlashPlayerVersion("9.0.0")||(this.src="flash"==a?"FlashCanvas/flashcanvas":"FlashCanvasPro/flashcanvas")},methodNames:["getContext"],d:[g]})}();var E="getUserMedia"in e;u("usermedia-core",{f:"usermedia",test:E&&!!window.URL,d:["url",g]}),u("usermedia-shim",{f:"usermedia",test:!!(E||e.webkitGetUserMedia||e.mozGetUserMedia||e.msGetUserMedia),d:["url","mediaelement",g]}),u("mediacapture",{test:p,d:["swfmini","usermedia",g,"filereader","forms","canvas"]}),function(){var c,d,h="form-shim-extend",i="formvalidation",j="form-number-date-api",l=!1,m=!1,o=!1,p={},r=b("progress"),s=b("output"),t=function(){var d,f,g="1(",j=b("input");if(f=a('
    ", "alert-info"); - redirect_to($sess_url); - } - - private function createNewNamedSessionAction() { - - if (Request::isHTTPPostRequest()) { - $code_name = $this->request->getParam('code_name'); - $run_session = $this->run->addNamedRunSession($code_name); - - if ($run_session) { - $sess = $run_session->session; - $sess_url = run_url($this->run->name, null, array('code' => $sess)); - - //alert("You've added a user with the code name '{$code_name}'.
    Send them this link to participate
    ", "alert-info"); - redirect_to(admin_run_url($this->run->name, 'user_overview', array('session' => $sess))); - } - } - - $this->renderView('run/create_new_named_session'); - } - private function userDetailAction() { $run = $this->run; $fdb = $this->fdb; - - $search = ''; $querystring = array(); - $position_lt = '='; + $queryparams = array('run_id' => $run->id, 'position_operator' => '='); + + if ($this->request->position_lt && in_array($this->request->position_lt, array('=', '>', '<'))) { + $queryparams['position_operator'] = $this->request->position_lt; + $querystring['position_lt'] = $queryparams['position_operator']; + } + if ($this->request->session) { - $session = str_replace('…', '', $this->request->session); - $search .= 'AND `survey_run_sessions`.session LIKE :session '; - $search_session = "%" . $session . "%"; + $session = str_replace("…", "", $this->request->session); + $queryparams['session'] = "%" . $session . "%"; $querystring['session'] = $session; } if ($this->request->position) { - $position = $this->request->position; - if (in_array($this->request->position_lt, array('>', '=', '<'))) { - $position_lt = $this->request->position_lt; - } - $search .= 'AND `survey_run_units`.position ' . $position_lt . ' :position '; - $search_position = $position; - $querystring['position_lt'] = $position_lt; - $querystring['position'] = $position; + $queryparams['position'] = $this->request->position; + $querystring['position'] = $queryparams['position']; } - $user_count_query = " - SELECT COUNT(`survey_unit_sessions`.id) AS count FROM `survey_unit_sessions` - LEFT JOIN `survey_run_sessions` ON `survey_run_sessions`.id = `survey_unit_sessions`.run_session_id - LEFT JOIN `survey_run_units` ON `survey_unit_sessions`.`unit_id` = `survey_run_units`.`unit_id` - WHERE `survey_run_sessions`.run_id = :run_id $search"; - - $params = array(':run_id' => $run->id); - if (isset($search_session)) { - $params[':session'] = $search_session; - } - if (isset($search_position)) { - $params[':position'] = $search_position; - } - - $user_count = $fdb->execute($user_count_query, $params, true); - $pagination = new Pagination($user_count, 400, true); - $limits = $pagination->getLimits(); - - - $params[':run_id2'] = $params[':run_id']; - - $users_query = "SELECT - `survey_run_sessions`.session, - `survey_unit_sessions`.id AS session_id, - `survey_runs`.name AS run_name, - `survey_run_units`.position, - `survey_run_units`.description, - `survey_units`.type AS unit_type, - `survey_unit_sessions`.created, - `survey_unit_sessions`.ended, - `survey_unit_sessions`.expired - FROM `survey_unit_sessions` - LEFT JOIN `survey_run_sessions` ON `survey_run_sessions`.id = `survey_unit_sessions`.run_session_id - LEFT JOIN `survey_units` ON `survey_unit_sessions`.unit_id = `survey_units`.id - LEFT JOIN `survey_run_units` ON `survey_unit_sessions`.unit_id = `survey_run_units`.unit_id - LEFT JOIN `survey_runs` ON `survey_runs`.id = `survey_run_units`.run_id - WHERE `survey_runs`.id = :run_id2 AND `survey_run_sessions`.run_id = :run_id $search - ORDER BY `survey_run_sessions`.id DESC,`survey_unit_sessions`.id ASC LIMIT $limits"; - - $users = $fdb->execute($users_query, $params); + $helper = new RunHelper($run, $fdb, $this->request); + $table = $helper->getUserDetailTable($queryparams); + $users = $table['data']; foreach ($users as $i => $userx) { if ($userx['expired']) { @@ -318,8 +176,51 @@ private function userDetailAction() { $users[$i] = $userx; } - $vars = get_defined_vars(); - $this->renderView('run/user_detail', $vars); + $this->renderView('run/user_detail', array( + 'users' => $users, + 'pagination' => $table['pagination'], + 'position_lt' => $queryparams['position_operator'], + 'querystring' => $querystring, + )); + } + + private function exportUserDetailAction() { + $helper = new RunHelper($this->run, $this->fdb, $this->request); + $queryParams = array(':run_id' => $this->run->id, ':run_id2' => $this->run->id); + $exportStmt = $helper->getUserDetailExportPdoStatement($queryParams); + $SPR = new SpreadsheetReader(); + $download_successfull = $SPR->exportInRequestedFormat($exportStmt, $this->run->name . '_user_detail', $this->request->str('format')); + if (!$download_successfull) { + alert('An error occured during user detail download.', 'alert-danger'); + redirect_to(admin_run_url($this->run->name, 'user_detail')); + } + } + + private function createNewTestCodeAction() { + $run_session = $this->run->makeTestRunSession(); + $sess = $run_session->session; + $animal = substr($sess, 0, strpos($sess, "XXX")); + $sess_url = run_url($this->run->name, null, array('code' => $sess)); + + //alert("You've created a new guinea pig, ".h($animal).". Use this guinea pig to move through the run like a normal user with special powers (accessibly via the monkey bar at the bottom right). As a guinea pig, you can see more detailed error messages than real users, so it is easier to e.g. debug R problems. If you want someone else to be the guinea pig, just forward them this link:
    ", "alert-info"); + redirect_to($sess_url); + } + + private function createNewNamedSessionAction() { + if (Request::isHTTPPostRequest()) { + $code_name = $this->request->getParam('code_name'); + $run_session = $this->run->addNamedRunSession($code_name); + + if ($run_session) { + $sess = $run_session->session; + $sess_url = run_url($this->run->name, null, array('code' => $sess)); + + //alert("You've added a user with the code name '{$code_name}'.
    Send them this link to participate
    ", "alert-info"); + redirect_to(admin_run_url($this->run->name, 'user_overview', array('session' => $sess))); + } + } + + $this->renderView('run/create_new_named_session'); } private function uploadFilesAction() { @@ -581,12 +482,10 @@ private function randomGroupsAction() { } private function overviewAction() { - $run = $this->run; - $this->renderView('run/overview', array( - 'users' => $run->getNumberOfSessionsInRun(), - 'overview_script' => $run->getOverviewScript(), - 'user_overview' => $run->getUserCounts(), + 'users' => $this->run->getNumberOfSessionsInRun(), + 'overview_script' => $this->run->getOverviewScript(), + 'user_overview' => $this->run->getUserCounts(), )); } @@ -607,52 +506,14 @@ private function emptyRunAction() { } private function emailLogAction() { - $run = $this->run; - $fdb = $this->fdb; + $queryparams = array('run_id' => $this->run->id); + $helper = new RunHelper($this->run, $this->fdb, $this->request); + $table = $helper->getEmailLogTable($queryparams); - $email_count_query = "SELECT COUNT(`survey_email_log`.id) AS count - FROM `survey_email_log` - LEFT JOIN `survey_unit_sessions` ON `survey_unit_sessions`.id = `survey_email_log`.session_id - LEFT JOIN `survey_run_sessions` ON `survey_unit_sessions`.run_session_id = `survey_run_sessions`.id - WHERE `survey_run_sessions`.run_id = :run_id"; - - $email_count = $fdb->execute($email_count_query, array(':run_id' => $run->id), true); - $pagination = new Pagination($email_count, 50, true); - $limits = $pagination->getLimits(); - - $emails_query = "SELECT - `survey_email_accounts`.from_name, - `survey_email_accounts`.`from`, - `survey_email_log`.recipient AS `to`, - `survey_email_log`.`sent`, - `survey_emails`.subject, - `survey_emails`.body, - `survey_email_log`.created, - `survey_run_units`.position AS position_in_run - FROM `survey_email_log` - LEFT JOIN `survey_emails` ON `survey_email_log`.email_id = `survey_emails`.id - LEFT JOIN `survey_run_units` ON `survey_emails`.id = `survey_run_units`.unit_id - LEFT JOIN `survey_email_accounts` ON `survey_emails`.account_id = `survey_email_accounts`.id - LEFT JOIN `survey_unit_sessions` ON `survey_unit_sessions`.id = `survey_email_log`.session_id - LEFT JOIN `survey_run_sessions` ON `survey_unit_sessions`.run_session_id = `survey_run_sessions`.id - WHERE `survey_run_sessions`.run_id = :run_id - ORDER BY `survey_email_log`.id DESC LIMIT $limits ;"; - - $g_emails = $fdb->execute($emails_query, array(':run_id' => $run->id)); - $emails = array(); - foreach ($g_emails as $email) { - $email['from'] = "{$email['from_name']}
    {$email['from']}"; - unset($email['from_name']); - $email['to'] = $email['to'] . "
    at run position " . $email['position_in_run'] . ""; - $email['mail'] = $email['subject'] . "
    " . h(substr($email['body'], 0, 100)) . "…"; - $email['datetime'] = '' . timetostr(strtotime($email['created'])) . ' '; - $email['datetime'] .= $email['sent'] ? '' : ''; - unset($email['position_in_run'], $email['subject'], $email['body'], $email['created'], $email['sent']); - $emails[] = $email; - } - - $vars = get_defined_vars(); - $this->renderView('run/email_log', $vars); + $this->renderView('run/email_log', array( + 'emails' => $table['data'], + 'pagination' => $table['pagination'], + )); } private function deleteRunAction() { @@ -677,71 +538,6 @@ private function cronLogParsed() { private function cronLogAction() { return $this->cronLogParsed(); - // @todo: deprecate code - $run = $this->run; - $fdb = $this->fdb; - - $fdb->count('survey_cron_log', array('run_id' => $run->id)); - $cron_entries_count = $fdb->count('survey_cron_log', array('run_id' => $run->id)); - - $pagination = new Pagination($cron_entries_count); - $limits = $pagination->getLimits(); - - $cron_query = "SELECT - `survey_cron_log`.id, - `survey_cron_log`.run_id, - `survey_cron_log`.created, - `survey_cron_log`.ended - `survey_cron_log`.created AS time_in_seconds, - `survey_cron_log`.sessions, - `survey_cron_log`.skipbackwards, - `survey_cron_log`.skipforwards, - `survey_cron_log`.pauses, - `survey_cron_log`.emails, - `survey_cron_log`.shuffles, - `survey_cron_log`.errors, - `survey_cron_log`.warnings, - `survey_cron_log`.notices, - `survey_cron_log`.message - FROM `survey_cron_log` - WHERE `survey_cron_log`.run_id = :run_id - ORDER BY `survey_cron_log`.id DESC LIMIT $limits;"; - - $g_cron = $fdb->execute($cron_query, array(':run_id' => $run->id)); - - $cronlogs = array(); - foreach ($g_cron as $cronlog) { - $cronlog = array_reverse($cronlog, true); - $cronlog['Modules'] = ''; - - if ($cronlog['pauses'] > 0) - $cronlog['Modules'] .= $cronlog['pauses'] . ' '; - if ($cronlog['skipbackwards'] > 0) - $cronlog['Modules'] .= $cronlog['skipbackwards'] . ' '; - if ($cronlog['skipforwards'] > 0) - $cronlog['Modules'] .= $cronlog['skipforwards'] . ' '; - if ($cronlog['emails'] > 0) - $cronlog['Modules'] .= $cronlog['emails'] . ' '; - if ($cronlog['shuffles'] > 0) - $cronlog['Modules'] .= $cronlog['shuffles'] . ' '; - $cronlog['Modules'] .= ''; - $cronlog['took'] = '' . round($cronlog['time_in_seconds'] / 60, 2) . 'm'; - $cronlog['time'] = '' . timetostr(strtotime($cronlog['created'])) . ''; - $cronlog = array_reverse($cronlog, true); - unset($cronlog['created']); - unset($cronlog['time_in_seconds']); - unset($cronlog['skipforwards']); - unset($cronlog['skipbackwards']); - unset($cronlog['pauses']); - unset($cronlog['emails']); - unset($cronlog['shuffles']); - unset($cronlog['run_id']); - unset($cronlog['id']); - - $cronlogs[] = $cronlog; - } - - $vars = get_defined_vars(); - $this->renderView('run/cron_log', $vars); } private function setRun($name) { @@ -849,7 +645,7 @@ private function panicAction() { 'locked' => 1, 'cron_active' => 0, 'public' => 0, - //@todo maybe do more + //@todo maybe do more ); $updated = $this->fdb->update('survey_runs', $settings, array('id' => $this->run->id)); if ($updated) { diff --git a/application/Controller/PublicController.php b/application/Controller/PublicController.php index bdd7bfa2c..d85864a2b 100644 --- a/application/Controller/PublicController.php +++ b/application/Controller/PublicController.php @@ -19,7 +19,7 @@ public function documentationAction() { } public function studiesAction() { - $this->renderView('public/studies', array('runs' => $this->user->getAvailableRuns())); + $this->renderView('public/studies', array('runs' => RunHelper::getPublicRuns())); } public function aboutAction() { @@ -101,7 +101,8 @@ public function loginAction() { if ($this->user->login($info)) { alert('Success! You were logged in!', 'alert-success'); Session::set('user', serialize($this->user)); - $redirect = $this->user->isAdmin() ? redirect_to('admin') : redirect_to('account'); + $redirect = $this->user->isAdmin() ? 'admin' : 'account'; + redirect_to($redirect); } else { alert(implode($this->user->errors), 'alert-danger'); } @@ -166,7 +167,7 @@ public function verifyEmailAction() { alert("You need to follow the link you received in your verification mail."); redirect_to('login'); } else { - $user->verify_email($email, $verification_token); + $user->verifyEmail($email, $verification_token); redirect_to('login'); }; } @@ -177,7 +178,7 @@ public function forgotPasswordAction() { } if ($this->request->str('email')) { - $this->user->forgot_password($this->request->str('email')); + $this->user->forgotPassword($this->request->str('email')); } $this->registerAssets('bootstrap-material-design'); @@ -201,7 +202,7 @@ public function resetPasswordAction() { 'new_password' => $postRequest->str('new_password'), 'new_password_confirm' => $postRequest->str('new_password_c'), ); - if (($done = $user->reset_password($info))) { + if (($done = $user->resetPassword($info))) { redirect_to('forgot_password'); } } diff --git a/application/Controller/RunController.php b/application/Controller/RunController.php index 8958ca1f2..2404f9989 100644 --- a/application/Controller/RunController.php +++ b/application/Controller/RunController.php @@ -122,7 +122,7 @@ protected function monkeyBarAction($action = '') { $parts = explode('_', $action); $method = array_shift($parts) . str_replace(' ', '', ucwords(implode(' ', $parts))); - $runHelper = new RunHelper($this->request, $this->fdb, $run->name); + $runHelper = new RunHelper($run, $this->fdb, $this->request); // check if run session usedby the monkey bar is a test if not this action is not valid if (!($runSession = $runHelper->getRunSession()) || !$runSession->isTesting()) { diff --git a/application/Controller/SuperadminController.php b/application/Controller/SuperadminController.php index 25373375d..60bb146a1 100644 --- a/application/Controller/SuperadminController.php +++ b/application/Controller/SuperadminController.php @@ -43,7 +43,7 @@ private function setAdminLevel($user_id, $level) { } elseif ($level == $user->getAdminLevel()) { alert('User already has requested admin rights', 'alert-warning'); } else { - if (!$user->setAdminLevelTo($level)) : + if (!$user->setAdminLevel($level)) : alert('Something went wrong with the admin level change.', 'alert-danger'); bad_request_header(); else: @@ -116,197 +116,21 @@ public function cronLogParsed() { public function cronLogAction() { return $this->cronLogParsed(); - // @todo: deprecate code - $cron_entries_count = $this->fdb->count('survey_cron_log'); - $pagination = new Pagination($cron_entries_count); - $limits = $pagination->getLimits(); - - $cron_entries_query = "SELECT - `survey_cron_log`.id, - `survey_users`.email, - `survey_cron_log`.run_id, - `survey_cron_log`.created, - `survey_cron_log`.ended - `survey_cron_log`.created AS time_in_seconds, - `survey_cron_log`.sessions, - `survey_cron_log`.skipbackwards, - `survey_cron_log`.skipforwards, - `survey_cron_log`.pauses, - `survey_cron_log`.emails, - `survey_cron_log`.shuffles, - `survey_cron_log`.errors, - `survey_cron_log`.warnings, - `survey_cron_log`.notices, - `survey_cron_log`.message, - `survey_runs`.name AS run_name - FROM `survey_cron_log` LEFT JOIN `survey_runs` ON `survey_cron_log`.run_id = `survey_runs`.id - LEFT JOIN `survey_users` ON `survey_users`.id = `survey_runs`.user_id - ORDER BY `survey_cron_log`.id DESC LIMIT $limits;"; - - $g_cron = $this->fdb->execute($cron_entries_query, array('user_id', $this->user->id)); - $cronlogs = array(); - foreach ($g_cron as $cronlog) { - $cronlog = array_reverse($cronlog, true); - $cronlog['Modules'] = ''; - - if ($cronlog['pauses'] > 0) - $cronlog['Modules'] .= $cronlog['pauses'] . ' '; - if ($cronlog['skipbackwards'] > 0) - $cronlog['Modules'] .= $cronlog['skipbackwards'] . ' '; - if ($cronlog['skipforwards'] > 0) - $cronlog['Modules'] .= $cronlog['skipforwards'] . ' '; - if ($cronlog['emails'] > 0) - $cronlog['Modules'] .= $cronlog['emails'] . ' '; - if ($cronlog['shuffles'] > 0) - $cronlog['Modules'] .= $cronlog['shuffles'] . ' '; - $cronlog['Modules'] .= ''; - $cronlog['took'] = '' . round($cronlog['time_in_seconds'] / 60, 2) . 'm'; - $cronlog['time'] = '' . timetostr(strtotime($cronlog['created'])) . ''; - $cronlog['Run name'] = $cronlog['run_name']; - $cronlog['Owner'] = $cronlog['email']; - - $cronlog = array_reverse($cronlog, true); - unset($cronlog['run_name']); - unset($cronlog['created']); - unset($cronlog['time_in_seconds']); - unset($cronlog['skipforwards']); - unset($cronlog['skipbackwards']); - unset($cronlog['pauses']); - unset($cronlog['emails']); - unset($cronlog['shuffles']); - unset($cronlog['run_id']); - unset($cronlog['id']); - unset($cronlog['email']); - - $cronlogs[] = $cronlog; - } - - $this->renderView('superadmin/cron_log', array( - 'cronlogs' => $cronlogs, - 'pagination' => $pagination, - )); } public function userManagementAction() { - $user_count = $this->fdb->count('survey_users'); - $pagination = new Pagination($user_count, 200, true); - $limits = $pagination->getLimits(); - - $users_query = "SELECT - `survey_users`.id, - `survey_users`.created, - `survey_users`.modified, - `survey_users`.email, - `survey_users`.admin, - `survey_users`.email_verified - FROM `survey_users` - ORDER BY `survey_users`.id ASC LIMIT $limits;"; - - $g_users = $this->fdb->prepare($users_query); - $g_users->execute(); - - $users = array(); - while ($userx = $g_users->fetch(PDO::FETCH_ASSOC)) { - $userx['Email'] = '' . h($userx['email']) . '' . ($userx['email_verified'] ? " " : " "); - $userx['Created'] = "" . timetostr(strtotime($userx['created'])) . ""; - $userx['Modified'] = "" . timetostr(strtotime($userx['modified'])) . ""; - $userx['Admin'] = " -
    - - - - - - - -
    "; - $userx['API Access'] = ''; - - unset($userx['email'], $userx['created'], $userx['modified'], $userx['admin'], $userx['id'], $userx['email_verified']); - - $users[] = $userx; - } - - $this->renderView('superadmin/user_management', array( - 'users' => $users, - 'pagination' => $pagination, - )); + $table = UserHelper::getUserManagementTablePdoStatement(); + $this->renderView('superadmin/user_management', $table); } public function activeUsersAction() { - $user_count = $this->fdb->count('survey_users'); - $pagination = new Pagination($user_count, 200, true); - $limits = $pagination->getLimits(); - - $users_query = "SELECT - `survey_users`.id, - `survey_users`.created, - `survey_users`.modified, - `survey_users`.email, - `survey_users`.admin, - `survey_users`.email_verified, - `survey_runs`.name AS run_name, - `survey_runs`.cron_active, - `survey_runs`.public, - COUNT(`survey_run_sessions`.id) AS number_of_users_in_run, - MAX(`survey_run_sessions`.last_access) AS last_edit - FROM `survey_users` - LEFT JOIN `survey_runs` ON `survey_runs`.user_id = `survey_users`.id - LEFT JOIN `survey_run_sessions` ON `survey_runs`.id = `survey_run_sessions`.run_id - WHERE `survey_users`.admin > 0 - GROUP BY `survey_runs`.id - ORDER BY `survey_users`.id ASC, last_edit DESC LIMIT $limits;"; - - $g_users = $this->fdb->prepare($users_query); - $g_users->execute(); - - $users = array(); - $last_user = ""; - while ($userx = $g_users->fetch(PDO::FETCH_ASSOC)) { - $public_status = (int) $userx['public']; - $public_logo = ''; - if ($public_status === 0): - $public_logo = "fa-eject"; - elseif ($public_status === 1): - $public_logo = "fa-volume-off"; - elseif ($public_status === 2): - $public_logo = "fa-volume-down"; - elseif ($public_status === 3): - $public_logo = "fa-volume-up"; - endif; - if ($last_user !== $userx['id']): - $userx['Email'] = '' . h($userx['email']) . '' . ($userx['email_verified'] ? " " : " "); - $last_user = $userx['id']; - else: - $userx['Email'] = ''; - endif; - $userx['Created'] = "" . timetostr(strtotime($userx['created'])) . ""; - $userx['Modified'] = "" . timetostr(strtotime($userx['modified'])) . ""; - $userx['Run'] = h($userx['run_name']) . " " . - ($userx['cron_active'] ? "" : "") . ' ' . - ""; - $userx['Users'] = $userx['number_of_users_in_run']; - $userx['Last active'] = "" . timetostr(strtotime($userx['last_edit'])) . ""; - - unset($userx['email']); - unset($userx['created']); - unset($userx['modified']); - unset($userx['admin']); - unset($userx['number_of_users_in_run']); - unset($userx['public']); - unset($userx['cron_active']); - unset($userx['run_name']); - unset($userx['id']); - unset($userx['last_edit']); - unset($userx['email_verified']); - # $user['body'] = "". substr($user['body'],0,50). "…"; - - $users[] = $userx; - } - + $table = UserHelper::getActiveUsersTablePdoStatement(); $this->renderView('superadmin/active_users', array( - 'users' => $users, - 'pagination' => $pagination, + 'pdoStatement' => $table['pdoStatement'], + 'pagination' => $table['pagination'], + 'status_icons' => array(0 => 'fa-eject', 1 => 'fa-volume-off', 2 => 'fa-volume-down', 3 => 'fa-volume-up' ) )); + } public function runsManagementAction() { @@ -321,20 +145,11 @@ public function runsManagementAction() { $this->fdb->update('survey_runs', $update, array('id' => (int) $id)); } alert('Changes saved', 'alert-success'); - redirect_to('superadmin/runs_management'); + $qs = $this->request->page ? '/?page=' . $this->request->page : null; + redirect_to('superadmin/runs_management' . $qs); + } else { + $this->renderView('superadmin/runs_management', RunHelper::getRunsManagementTablePdoStatement()); } - - $q = 'SELECT survey_runs.id AS run_id, name, survey_runs.user_id, cron_active, cron_fork, locked, count(survey_run_sessions.session) AS sessions, survey_users.email - FROM survey_runs - LEFT JOIN survey_users ON survey_users.id = survey_runs.user_id - LEFT JOIN survey_run_sessions ON survey_run_sessions.run_id = survey_runs.id - GROUP BY survey_run_sessions.run_id - ORDER BY survey_runs.name ASC - '; - $this->renderView('superadmin/runs_management', array( - 'runs' => $this->fdb->query($q), - 'i' => 1, - )); } } diff --git a/application/Helper/RunHelper.php b/application/Helper/RunHelper.php index 1e4067727..e659f668b 100644 --- a/application/Helper/RunHelper.php +++ b/application/Helper/RunHelper.php @@ -4,7 +4,7 @@ class RunHelper { /** * - * @var Request + * @var Request */ protected $request; @@ -33,11 +33,11 @@ class RunHelper { protected $errors = array(); protected $message = null; - public function __construct(Request $r, DB $db, $run) { + public function __construct(Run $run, DB $db, Request $r) { $this->request = $r; - $this->run_name = $run; $this->db = $db; - $this->run = new Run($this->db, $run); + $this->run = $run; + $this->run_name = $run->name; if (!$this->run->valid) { throw new Exception("Run with name {$run} not found"); @@ -115,7 +115,7 @@ public function snipUnitSession() { $this->errors[] = "No unit session found"; endif; } - + public function getRunSession() { return $this->runSession; } @@ -128,4 +128,224 @@ public function getMessage() { return $this->message; } + public function getUserOverviewTable($queryParams, $page = null) { + $query = array(' `survey_run_sessions`.run_id = :run_id '); + if (!empty($queryParams['session'])) { + $query[] = ' `survey_run_sessions`.session LIKE :session '; + } + + if (!empty($queryParams['sessions'])) { + $query[] = ' `survey_run_sessions`.session IN (' . implode($queryParams['sessions'], ',') . ') '; + } + + if (!empty($queryParams['position'])) { + $query[] = " `survey_run_sessions`.position {$queryParams['position_operator']} :position "; + } + $adminCode = $queryParams['admin_code']; + unset($queryParams['position_operator'], $queryParams['admin_code']); + + $where = implode(' AND ', $query); + $count_query = "SELECT COUNT(`survey_run_sessions`.id) AS count FROM `survey_run_sessions` WHERE {$where}"; + $count = $this->db->execute($count_query, $queryParams, true); + + $pagination = new Pagination($count, 200, true); + $limits = $pagination->getLimits(); + $queryParams['admin_code'] = $adminCode; + + $itemsQuery = " + SELECT + `survey_run_sessions`.id AS run_session_id, + `survey_run_sessions`.session, + `survey_run_sessions`.position, + `survey_run_units`.description, + `survey_run_sessions`.last_access, + `survey_run_sessions`.created, + `survey_run_sessions`.testing, + `survey_runs`.name AS run_name, + `survey_units`.type AS unit_type, + `survey_run_sessions`.last_access, + (`survey_units`.type IN ('Survey','External','Email') AND DATEDIFF(NOW(), `survey_run_sessions`.last_access) >= 2) AS hang + FROM `survey_run_sessions` + LEFT JOIN `survey_runs` ON `survey_run_sessions`.run_id = `survey_runs`.id + LEFT JOIN `survey_run_units` ON `survey_run_sessions`.position = `survey_run_units`.position AND `survey_run_units`.run_id = `survey_run_sessions`.run_id + LEFT JOIN `survey_units` ON `survey_run_units`.unit_id = `survey_units`.id + WHERE {$where} + ORDER BY `survey_run_sessions`.session != :admin_code, hang DESC, `survey_run_sessions`.last_access DESC + LIMIT $limits + "; + + return array( + 'data' => $this->db->execute($itemsQuery, $queryParams), + 'pagination' => $pagination, + ); + } + + public function getUserOverviewExportPdoStatement($queryParams) { + $query = " + SELECT + `survey_run_sessions`.position, + `survey_units`.type AS unit_type, + `survey_run_units`.description, + `survey_run_sessions`.session, + `survey_run_sessions`.created, + `survey_run_sessions`.last_access, + (`survey_units`.type IN ('Survey','External','Email') AND DATEDIFF(NOW(), `survey_run_sessions`.last_access) >= 2) AS hang + FROM `survey_run_sessions` + LEFT JOIN `survey_runs` ON `survey_run_sessions`.run_id = `survey_runs`.id + LEFT JOIN `survey_run_units` ON `survey_run_sessions`.position = `survey_run_units`.position AND `survey_run_units`.run_id = `survey_run_sessions`.run_id + LEFT JOIN `survey_units` ON `survey_run_units`.unit_id = `survey_units`.id + WHERE `survey_run_sessions`.run_id = :run_id ORDER BY `survey_run_sessions`.session != :admin_code, hang DESC, `survey_run_sessions`.last_access DESC + "; + $stmt = $this->db->prepare($query); + $stmt->execute($queryParams); + + return $stmt; + } + + public function getUserDetailTable($queryParams, $page = null) { + $query = array(' `survey_run_sessions`.run_id = :run_id '); + if (!empty($queryParams['session'])) { + $query[] = ' `survey_run_sessions`.session LIKE :session '; + } + + if (!empty($queryParams['position'])) { + $query[] = " `survey_run_units`.position {$queryParams['position_operator']} :position "; + } + unset($queryParams['position_operator']); + + $where = implode(' AND ', $query); + $count_query = " + SELECT COUNT(`survey_unit_sessions`.id) AS count FROM `survey_unit_sessions` + LEFT JOIN `survey_run_sessions` ON `survey_run_sessions`.id = `survey_unit_sessions`.run_session_id + LEFT JOIN `survey_run_units` ON `survey_unit_sessions`.`unit_id` = `survey_run_units`.`unit_id` + WHERE {$where} + "; + $count = $this->db->execute($count_query, $queryParams, true); + $pagination = new Pagination($count, 200, true); + $limits = $pagination->getLimits(); + + $query[] = ' `survey_runs`.id = :run_id2 '; + $queryParams['run_id2'] = $queryParams['run_id']; + $where = implode(' AND ', $query); + + $itemsQuery = " + SELECT + `survey_run_sessions`.session, + `survey_unit_sessions`.id AS session_id, + `survey_runs`.name AS run_name, + `survey_run_units`.position, + `survey_run_units`.description, + `survey_units`.type AS unit_type, + `survey_unit_sessions`.created, + `survey_unit_sessions`.ended, + `survey_unit_sessions`.expired + FROM `survey_unit_sessions` + LEFT JOIN `survey_run_sessions` ON `survey_run_sessions`.id = `survey_unit_sessions`.run_session_id + LEFT JOIN `survey_units` ON `survey_unit_sessions`.unit_id = `survey_units`.id + LEFT JOIN `survey_run_units` ON `survey_unit_sessions`.unit_id = `survey_run_units`.unit_id + LEFT JOIN `survey_runs` ON `survey_runs`.id = `survey_run_units`.run_id + WHERE {$where} + ORDER BY `survey_run_sessions`.id DESC,`survey_unit_sessions`.id ASC LIMIT {$limits} + "; + + return array( + 'data' => $this->db->execute($itemsQuery, $queryParams), + 'pagination' => $pagination, + ); + } + + public function getUserDetailExportPdoStatement($queryParams) { + $query = " + SELECT + `survey_run_units`.position, + `survey_units`.type AS unit_type, + `survey_run_units`.description, + `survey_run_sessions`.session, + `survey_unit_sessions`.created AS entered, + IF (`survey_unit_sessions`.ended > 0, UNIX_TIMESTAMP(`survey_unit_sessions`.ended)-UNIX_TIMESTAMP(`survey_unit_sessions`.created), UNIX_TIMESTAMP(NOW())-UNIX_TIMESTAMP(`survey_unit_sessions`.created)) AS 'seconds_stayed', + `survey_unit_sessions`.ended AS 'left', + `survey_unit_sessions`.expired + FROM `survey_unit_sessions` + LEFT JOIN `survey_run_sessions` ON `survey_run_sessions`.id = `survey_unit_sessions`.run_session_id + LEFT JOIN `survey_units` ON `survey_unit_sessions`.unit_id = `survey_units`.id + LEFT JOIN `survey_run_units` ON `survey_unit_sessions`.unit_id = `survey_run_units`.unit_id + LEFT JOIN `survey_runs` ON `survey_runs`.id = `survey_run_units`.run_id + WHERE `survey_runs`.id = :run_id AND `survey_run_sessions`.run_id = :run_id2 + ORDER BY `survey_run_sessions`.id DESC,`survey_unit_sessions`.id ASC; + "; + $stmt = $this->db->prepare($query); + $stmt->execute($queryParams); + + return $stmt; + } + + public function getEmailLogTable($queryParams) { + $count_query = " + SELECT COUNT(`survey_email_log`.id) AS count + FROM `survey_email_log` + LEFT JOIN `survey_unit_sessions` ON `survey_unit_sessions`.id = `survey_email_log`.session_id + LEFT JOIN `survey_run_sessions` ON `survey_unit_sessions`.run_session_id = `survey_run_sessions`.id + WHERE `survey_run_sessions`.run_id = :run_id + "; + $count = $this->db->execute($count_query, $queryParams, true); + $pagination = new Pagination($count, 75, true); + $limits = $pagination->getLimits(); + + $itemsQuery = " + SELECT + `survey_email_accounts`.from_name, + `survey_email_accounts`.`from`, + `survey_email_log`.recipient AS `to`, + `survey_email_log`.`sent`, + `survey_emails`.subject, + `survey_emails`.body, + `survey_email_log`.created, + `survey_run_units`.position AS position_in_run + FROM `survey_email_log` + LEFT JOIN `survey_emails` ON `survey_email_log`.email_id = `survey_emails`.id + LEFT JOIN `survey_run_units` ON `survey_emails`.id = `survey_run_units`.unit_id + LEFT JOIN `survey_email_accounts` ON `survey_emails`.account_id = `survey_email_accounts`.id + LEFT JOIN `survey_unit_sessions` ON `survey_unit_sessions`.id = `survey_email_log`.session_id + LEFT JOIN `survey_run_sessions` ON `survey_unit_sessions`.run_session_id = `survey_run_sessions`.id + WHERE `survey_run_sessions`.run_id = :run_id + ORDER BY `survey_email_log`.id DESC LIMIT $limits + "; + + return array( + 'data' => $this->db->execute($itemsQuery, $queryParams), + 'pagination' => $pagination, + ); + } + + public static function getPublicRuns() { + return DB::getInstance()->select('name, title, public_blurb_parsed') + ->from('survey_runs') + ->where('public > 2') + ->fetchAll(); + } + + public static function getRunsManagementTablePdoStatement() { + $count = DB::getInstance()->count('survey_runs'); + $pagination = new Pagination($count, 100, true); + $limits = $pagination->getLimits(); + + $itemsQuery = " + SELECT survey_runs.id AS run_id, name, survey_runs.user_id, cron_active, cron_fork, locked, count(survey_run_sessions.id) AS sessions, survey_users.email + FROM survey_runs + LEFT JOIN survey_users ON survey_users.id = survey_runs.user_id + LEFT JOIN survey_run_sessions ON survey_run_sessions.run_id = survey_runs.id + GROUP BY survey_run_sessions.run_id + ORDER BY survey_runs.name ASC LIMIT $limits + "; + + $stmt = DB::getInstance()->prepare($itemsQuery); + $stmt->execute(); + + return array( + 'pdoStatement' => $stmt, + 'pagination' => $pagination, + 'count' => $count, + ); + } + } diff --git a/application/Helper/UserHelper.php b/application/Helper/UserHelper.php new file mode 100644 index 000000000..2bebd9420 --- /dev/null +++ b/application/Helper/UserHelper.php @@ -0,0 +1,66 @@ +count('survey_users'); + $pagination = new Pagination($count, 200, true); + $limits = $pagination->getLimits(); + + $itemsQuery = " + SELECT + `survey_users`.id, + `survey_users`.created, + `survey_users`.modified, + `survey_users`.email, + `survey_users`.admin, + `survey_users`.email_verified + FROM `survey_users` + ORDER BY `survey_users`.id ASC LIMIT $limits + "; + + $stmt = DB::getInstance()->prepare($itemsQuery); + $stmt->execute(); + + return array( + 'pdoStatement' => $stmt, + 'pagination' => $pagination, + ); + } + + public static function getActiveUsersTablePdoStatement() { + $count = DB::getInstance()->count('survey_users'); + $pagination = new Pagination($count, 200, true); + $limits = $pagination->getLimits(); + + $itemsQuery = " + SELECT + `survey_users`.id, + `survey_users`.created, + `survey_users`.modified, + `survey_users`.email, + `survey_users`.admin, + `survey_users`.email_verified, + `survey_runs`.name AS run_name, + `survey_runs`.cron_active, + `survey_runs`.public, + COUNT(`survey_run_sessions`.id) AS number_of_users_in_run, + MAX(`survey_run_sessions`.last_access) AS last_edit + FROM `survey_users` + LEFT JOIN `survey_runs` ON `survey_runs`.user_id = `survey_users`.id + LEFT JOIN `survey_run_sessions` ON `survey_runs`.id = `survey_run_sessions`.run_id + WHERE `survey_users`.admin > 0 + GROUP BY `survey_runs`.id + ORDER BY `survey_users`.id ASC, last_edit DESC LIMIT $limits + "; + + $stmt = DB::getInstance()->prepare($itemsQuery); + $stmt->execute(); + + return array( + 'pdoStatement' => $stmt, + 'pagination' => $pagination, + ); + } + +} diff --git a/application/Model/RunSession.php b/application/Model/RunSession.php index cb5a7fbc8..7d31216fb 100644 --- a/application/Model/RunSession.php +++ b/application/Model/RunSession.php @@ -409,4 +409,58 @@ public function getSettings() { return $settings; } + public static function toggleTestingStatus($sessions) { + $dbh = DB::getInstance(); + if (is_string($sessions)) { + $sessions = array($sessions); + } + + foreach ($sessions as $session) { + $qs[] = $dbh->quote($session); + } + + $query = 'UPDATE survey_run_sessions SET testing = 1 - testing WHERE session IN (' . implode(',', $qs) . ')'; + return $dbh->query($query)->rowCount(); + } + + public static function deleteSessions($sessions) { + $dbh = DB::getInstance(); + if (is_string($sessions)) { + $sessions = array($sessions); + } + + foreach ($sessions as $session) { + $qs[] = $dbh->quote($session); + } + + $query = 'DELETE FROM survey_run_sessions WHERE session IN (' . implode(',', $qs) . ')'; + return $dbh->query($query)->rowCount(); + } + + public static function positionSessions($sessions, $position) { + $dbh = DB::getInstance(); + if (is_string($sessions)) { + $sessions = array($sessions); + } + + foreach ($sessions as $session) { + $qs[] = $dbh->quote($session); + } + + $query = 'UPDATE survey_run_sessions SET position = ' . $position . ' WHERE session IN (' . implode(',', $qs) . ')'; + return $dbh->query($query)->rowCount(); + } + + public static function getSentRemindersBySessionId($id) { + $stmt = DB::getInstance()->prepare(' + SELECT survey_unit_sessions.id as unit_session_id, survey_run_special_units.id as unit_id FROM survey_unit_sessions + LEFT JOIN survey_units ON survey_unit_sessions.unit_id = survey_units.id + LEFT JOIN survey_run_special_units ON survey_run_special_units.id = survey_units.id + WHERE survey_unit_sessions.run_session_id = :run_session_id AND survey_run_special_units.type = "ReminderEmail" + '); + $stmt->bindValue('run_session_id', $id, PDO::PARAM_INT); + $stmt->execute(); + return $stmt->fetchAll(PDO::FETCH_ASSOC); + } + } diff --git a/application/Model/User.php b/application/Model/User.php index 2424a16ba..cabb977ea 100644 --- a/application/Model/User.php +++ b/application/Model/User.php @@ -113,7 +113,7 @@ public function register($info) { $this->referrer_code = $referrer_code; - if ($hash) : + if ($hash) { $inserted = $this->dbh->insert('survey_users', array( 'email' => $email, 'created' => mysql_now(), @@ -132,10 +132,10 @@ public function register($info) { $this->needToVerifyMail(); return true; - else: + } else { alert('Error! Hash error.', 'alert-danger'); return false; - endif; + } } public function needToVerifyMail() { @@ -153,9 +153,9 @@ public function needToVerifyMail() { $mail->AddAddress($this->email); $mail->Subject = 'formr: confirm your email address'; $mail->Body = Template::get_replace('email/verify-email.txt', array( - 'site_url' => site_url(), - 'documentation_url' => site_url('documentation#help'), - 'verify_link' => $verify_link, + 'site_url' => site_url(), + 'documentation_url' => site_url('documentation#help'), + 'verify_link' => $verify_link, )); if (!$mail->Send()) { @@ -179,8 +179,10 @@ public function login($info) { $verification_link = site_url('verify-email', array('token' => $user['email_verification_hash'])); $this->id = $user['id']; $this->email = $email; - $this->errors[] = sprintf('Please verify your email address by clicking on the verification link that was sent to you, ' - . 'Resend Link', $verification_link); + $this->errors[] = sprintf(' + Please verify your email address by clicking on the verification link that was sent to you, + Resend Link', + $verification_link); return false; } elseif ($user && password_verify($password, $user['password'])) { if (password_needs_rehash($user['password'], PASSWORD_DEFAULT)) { @@ -203,30 +205,22 @@ public function login($info) { } } - public function setAdminLevelTo($level) { + public function setAdminLevel($level) { if (!Site::getCurrentUser()->isSuperAdmin()) { throw new Exception("You need more admin rights to effect this change"); } $level = (int) $level; - if ($level !== 0 && $level !== 1) { - if ($level > 1) { - $level = 1; - } else { - $level = 0; - } - } + $level = max(array(0, $level)); + $level = $level > 1 ? 1 : $level; return $this->dbh->update('survey_users', array('admin' => $level), array('id' => $this->id, 'admin <' => 100)); } - public function forgot_password($email) { + public function forgotPassword($email) { $user_exists = $this->dbh->entry_exists('survey_users', array('email' => $email)); - if (!$user_exists): - alert("This email address is not registered here.", "alert-danger"); - return false; - else: + if ($user_exists) { $token = crypto_token(48); $hash = password_hash($token, PASSWORD_DEFAULT); @@ -237,23 +231,21 @@ public function forgot_password($email) { 'reset_token' => $token )); - global $site; - $mail = $site->makeAdminMailer(); + $mail = Site::getCurrentUser()->makeAdminMailer(); $mail->AddAddress($email); $mail->Subject = 'formr: forgot password'; $mail->Body = Template::get_replace('email/forgot-password.txt', array( - 'site_url' => site_url(), - 'reset_link' => $reset_link, + 'site_url' => site_url(), + 'reset_link' => $reset_link, )); - if (!$mail->Send()): + if (!$mail->Send()) { alert($mail->ErrorInfo, 'alert-danger'); - else: - alert("You were sent a password reset link.", 'alert-info'); + } else { + alert("If the provided email was registered with us, you will receive an email with instructions on how to reset your password.", 'alert-info'); redirect_to("forgot_password"); - endif; - - endif; + } + } } function logout() { @@ -314,7 +306,7 @@ public function changeData($password, $data) { return true; } - public function reset_password($info) { + public function resetPassword($info) { $email = array_val($info, 'email'); $token = array_val($info, 'reset_token'); $new_password = array_val($info, 'new_password'); @@ -347,7 +339,7 @@ public function reset_password($info) { return false; } - public function verify_email($email, $token) { + public function verifyEmail($email, $token) { $verify_data = $this->dbh->findRow('survey_users', array('email' => $email), array('email_verification_hash', 'referrer_code')); if (!$verify_data) { alert('Incorrect token or email address.', 'alert-danger'); @@ -355,13 +347,11 @@ public function verify_email($email, $token) { } if (password_verify($token, $verify_data['email_verification_hash'])) { - $this->dbh->update('survey_users', array('email_verification_hash' => null, 'email_verified' => 1), array('email' => $email), array('int', 'int') - ); + $this->dbh->update('survey_users', array('email_verification_hash' => null, 'email_verified' => 1), array('email' => $email), array('int', 'int')); alert('Your email was successfully verified!', 'alert-success'); if (in_array($verify_data['referrer_code'], Config::get('referrer_codes'))) { - $this->dbh->update('survey_users', array('admin' => 1), array('email' => $email) - ); + $this->dbh->update('survey_users', array('admin' => 1), array('email' => $email)); alert('You now have the rights to create your own studies!', 'alert-success'); } return true; @@ -400,9 +390,8 @@ public function getStudies($order = 'id DESC', $limit = null) { } public function getEmailAccounts() { - if ($this->isAdmin()): + if ($this->isAdmin()) { $accs = $this->dbh->find('survey_email_accounts', array('user_id' => $this->id, 'deleted' => 0), array('cols' => 'id, from')); - $results = array(); foreach ($accs as $acc) { if ($acc['from'] == null) { @@ -411,7 +400,7 @@ public function getEmailAccounts() { $results[] = $acc; } return $results; - endif; + } return false; } @@ -430,11 +419,4 @@ public function getRuns($order = 'id DESC', $limit = null) { return array(); } - public function getAvailableRuns() { - return $this->dbh->select('name,title, public_blurb_parsed') - ->from('survey_runs') - ->where('public > 2') - ->fetchAll(); - } - } diff --git a/application/View/admin/run/email_log.php b/application/View/admin/run/email_log.php index 3b5b49167..02595f00e 100755 --- a/application/View/admin/run/email_log.php +++ b/application/View/admin/run/email_log.php @@ -23,24 +23,24 @@ - $value) { - echo ""; - } - ?> + + + + - "; - foreach ($row as $cell) { - echo ""; - } - echo "\n"; - }; - ?> + + + + + + + +
    {$field}FromToMailDatetime
    $cell


    at run position

    + + ' : ''; ?> +