diff --git a/.gitignore b/.gitignore index d77b9ad78..bf058b808 100644 --- a/.gitignore +++ b/.gitignore @@ -27,10 +27,10 @@ formr-package/man webroot/assets/img tests/test.php nohup.out +webroot/*.html webroot/assets/*.mp4 webroot/assets/*.html - webroot/assets/bower_components webroot/assets/lib/bower_components - +/bin/migra.php /*.key diff --git a/CHANGELOG-v1.md b/CHANGELOG-v1.md new file mode 100644 index 000000000..d44582d81 --- /dev/null +++ b/CHANGELOG-v1.md @@ -0,0 +1,491 @@ +# Formr.org Change Log + +The format is based on [Keep a Changelog](http://keepachangelog.com/) and this project adheres to [Semantic Versioning](http://semver.org/). + +## [0.19.10] - 31.01.2022 +## [0.19.9] - 31.01.2022 +* Minor bug fixes + +## [0.18.0] - 27.05.2017 +### Added +- A new queuing system to process run sessions +- An overview of sessions waiting in queue +- A new Run Unit called Waiting Time (see documentation) + +### Changed +- Optimizing the getting of results over the API +- Some minor bug fixes + +## [0.17.21] - 25.02.2019 +* Minor bug fixes + +## [0.17.20] - 03.01.2019 +### Fixes +https://github.com/rubenarslan/formr.org/issues/405 +https://github.com/rubenarslan/formr.org/issues/404 +https://github.com/rubenarslan/formr.org/issues/398 +https://github.com/rubenarslan/formr.org/issues/395 +https://github.com/rubenarslan/formr.org/issues/392 +https://github.com/rubenarslan/formr.org/issues/391 +https://github.com/rubenarslan/formr.org/issues/390 +https://github.com/rubenarslan/formr.org/issues/389 +https://github.com/rubenarslan/formr.org/issues/382 + +## [0.17.18] - 21.09.2018 +### Fixes +- https://github.com/rubenarslan/formr.org/issues/379 +- https://github.com/rubenarslan/formr.org/issues/332 +- https://github.com/rubenarslan/formr.org/issues/370 +- https://github.com/rubenarslan/formr.org/issues/347 +- https://github.com/rubenarslan/formr.org/issues/355 +- https://github.com/rubenarslan/formr.org/issues/378 +- https://github.com/rubenarslan/formr.org/issues/376 +- https://github.com/rubenarslan/formr.org/issues/374 +- https://github.com/rubenarslan/formr.org/issues/319 +- https://github.com/rubenarslan/formr.org/issues/292 + +## [0.17.14] - 14.06.2018 +### Added +* Logout action for RUNs + +## [0.17.13] - 12.06.2018 +### Fixed +* Show-ifs: Use previous show-if value for paginated survey + +## [0.17.12] - 25.05.2018 +### Added +* Cookie consent alert + +### Changed +* Meta tag info + +## [0.17.11] - 15.05.2018 +### Fixed +* Fix 'sticky' value for choice item types + +## [0.17.10] - 07.05.2018 +### Fixed +* Optionally uploading a file for 'File' item-type. + +## [0.17.9] - 27.04.2018 +### Fixed +* Bug #342 +* Fix Number item-type value when it is used as counter + +## [0.17.8] - 25.04.2018 +### Fixed +* Connection to the Open Science Framework +* Patch Commit + +## [0.17.7] - 20.04.2018 +### Fixed +* Do not save values for hidden items +* RUN file uploads + +## [0.17.6] - 18.04.2018 +### Added +* Alert users if cookies are not activated +* GET parameter to force re-calculation of show-if for paged-surveys + +### Changed +* UI of the account page + +### Fixed +* Testing Email Accounts (send self-mail) +* Monkey bar session link #340 +* Default run export name #341 + +## [0.17.5] - 05.04.2018 +### Fixed +* Testing "Survey" for surveys with paging enabled + +## [0.17.4] - 22.03.2018 +### Changed +#### Bug Fixes +* Use webshim calendar widget in all browsers. +* Fix Check_Item "optional" flag glitch +* Add "in_maintenance" config flag to prevent user access when site is under maintenance +* "Quick Upload" fix for surveys + +## [0.17.3] - 19.03.2018 +### Added +* Surveys can be configured to be "paginated". Users can be provided with page numbers where they can click back and forth to make corrections. +* Uploaded files associated to a run can now be easily deleted and upload file URL can also be easily copied. +* Users can re-send the email verification link. + +## [0.17.2] - 28.02.2018 +### Changed +* Fix rating button glitch (range can be reversed) +* Issue #332 +* Issue #333 + +## [0.17.1] - 28.02.2018 +### Changed +* Replace underscores in run names to hyphens + +## [0.17.0] - 28.02.2018 +### Added +* Sub-domain definition for studies +* Option to hide survey results + +### Changed +* Architectural change. + +## [0.16.17] - 09.02.2018 +### Changed +* Architectural change. + +## [0.16.16] - 31.01.2018 +### Changed +* Add option to use [XSendFile] (https://tn123.org/mod_xsendfile/) for downloads + +## [0.16.15] - 26.01.2018 +### Changed +* Fixed PHP-side survey validation so that entered data is not lost, partially addressing #327 +* Added further ways to specify expiry #305 +* UI Changes in Survey settings + +## [0.16.14] - 11.07.2017 +### Changed +Bug fix release. +* See [Github](https://github.com/rubenarslan/formr.org/issues/314) for details +* Compute dynamic value using current survey object + +## [0.16.13] - 02.10.2017 +### Changed +Bug fix release. +* See [Github](https://github.com/rubenarslan/formr.org/issues?utf8=%E2%9C%93&q=milestone%3Av0.16.13%20) for details +* add mc_horizontal class +* fix some CSS bugs in new material design +* several UI fixes + +## [0.16.12] - 27.07.2017 +### Changed +* Publications moved to new page + +## [0.16.10] - 06.07.2017 +## [0.16.11] - 06.07.2017 +### Changed +* Fix sign-up bug +* Get right auth credentials for email queue accounts + +## [0.16.9] - 05.07.2017 +### Added +* Use paragonie/halite for user accounts encryption - https://paragonie.com/project/halite + +### Changed +* Fix bug allowing unverified email accounts to login. + +## [0.16.8] - 04.07.2017 +### Changed +* Fix bug preventing user email change + +## [0.16.6] - 02.06.2017 +### Added +* Config item for phpmailer SMTPOptions +* Quick links to runs and surveys on admin dash board + +### Changed +* Fix broken sign up referrer code functionality +* Remove newsletter subscription + +## [0.16.5] - 11.05.2017 +### Added +* CSRF protection + +## [0.16.4] - 05.05.2017 +### Added +* Variable to get number of participants in R `.formr$nr_of_participants` + +### Changed +* Minor Bug Fixes: Form error messages, missing variables, CSS. + +## [0.16.3] - 21.03.2017 +### Changed +* HTML meta-tags +* Fix broken run unit SkipForward link + +## [0.16.2] - 15.03.2017 +## [0.16.1] - 16.03.2017 +### Changed +* Fix CSS bugs + +## [0.16.0] - 14.03.2017 +### Changed +* New User Interface + +## [0.15.7] - 09.03.2017 +### Changed +* Bug fix: Long query optimization + +## [0.15.6] - 13.02.2017 +### Changed +* Bug fix: Fix PHP warnings + + +## [0.15.5] - 08.02.2017 +### Changed +* Bug fix: Fix PHP warnings +* Issues closed: + * https://github.com/rubenarslan/formr.org/issues/275 + * https://github.com/rubenarslan/formr.org/issues/276 + * https://github.com/rubenarslan/formr.org/issues/277 + * https://github.com/rubenarslan/formr.org/issues/278 + * https://github.com/rubenarslan/formr.org/issues/280 + +## [0.15.4] - 19.12.2016 +### Changed +* Bug fix: Clear email attachments after email is sent +* Increase timeout limits for CURL requests + +## [0.15.3] - 2.12.2016 +### Changed +* Bug fix: Cron should be processed if it is locked. + +## [0.15.2] - 1.12.2016 +## [0.15.1] - 1.12.2016 +### Changed +- Configure re-try limits for email queue items +- reset smtp connection if queue item fails + +## [0.15.0] - 1.12.2016 +### Added +* E-mail queuing + * Setting: `$settings['email']['use_queue']` - Should queue be used or not? + * Setting: `$settings['email']['queue_loop_interval']` - Number of seconds for which deamon loop should rest before getting next batch +* Run management interface for superadmin. In this interface the superadmin can determine which run is allowed to run in the cron and whose cron should be forked in an independent process. +* MySQL database v21: add email queue table, run property for process forking and a _sent_ flag to email log table. + +### Changed +* Memory limits to exports are now configurable. +* Increased weekly limit on how many e-mails an address can receive. +* Bug Fixes: + * https://github.com/rubenarslan/formr.org/issues/270 + * https://github.com/rubenarslan/formr.org/issues/244 + * https://github.com/rubenarslan/formr.org/issues/272 + +## [0.14.3] - 22.11.2016 +### Changed +* Fix range issues for Item_random + +## [0.14.2] - 17.11.2016 +### Changed +* Bug fix on OR operator + +## [0.14.1] - 7.11.2016 +### Changed +* Logout page should also logout anonymous run sessions +* A page to redirect to after logout can be specified using the get parameter *_rdir* + +## [0.14.0] - 1.11.2016 +### Added +* When sending 'run jobs' to the gearman server, a high priority is used. +* User unique identifies for gearman jobs to avoid duplicates + +### Changed +* Configurable memory limits +* Bug Fixes + * Fix query to get _most recent mail_ in the Email Run Unit + * properly parse cron logs generated when running gearman + +## [0.13.3] +### Changed +* Increased memory limit when exporting results in JSON +* When backing-up results, only items saved in results table are exported +* Minor bug fixes + +## [0.13.2] +### Changed +* Fixed PHP warnings + +## [0.13.1] +### Changed +* Fixed PHP warnings + +## [0.13.0] +### Added +* Reminders now processed as unit sessions. Researchers are able to see how many times a particular reminder has been sent to a participant. +* Actions in "User Overview" can be performed on multiple participants at the same time: + * Deleting Sessions + * Moving sessions to a position in the run + * Sending reminders + * Setting test status +* Users can choose to execute email units (i.e. send emails) only when user is not currently active on a study using the option in the email run unit. +* New panic button to temporarily de-activate run in case of emergency: Functions of the panic button: + * lock's run + * disable's run cron + * makes run private. +* [major feature] Background session processing is now distributed and make asynchronous by using Gearman + +### Changed +* Error reporting on survey upload improved to point out what individual item caused issues +* Modified study examples: Experience sampling and Longitudinal studies. +* Bug fixes: + * [Issue #262](https://github.com/rubenarslan/formr.org/issues/262) + * [Issue #265](https://github.com/rubenarslan/formr.org/issues/265) + * [Issue #266](https://github.com/rubenarslan/formr.org/issues/266) + + +## [0.12.7] +### Changed +* Update documentation +* Update sample studies +* improve handling of debugger with iframes + +## [0.12.6] +### Added +* Support for deprecated 'request' api parameter + +## [0.12.5] +### Changed +* Re-factor API to accept json formatted requests +* API bug fixes + +## [0.12.4] +* Edit documentation + +## [0.12.3] +* Fix bug on run users listing +* Edit publications documentation + +## [0.12.2] +* Fix bug on run users listing +* Edit publications documentation + +## [0.12.1] +* Minor bug fixes + +## [0.12.0] +### Added +* Introduced *special run units*: + * Reminders + * Service Message + * Overview Script +* Possibility to add multiple reminders. +* Reminder sending UI modified to support multiple reminders +* much more complex feedback (including JS-graphs etc.) now possible because we embed iframes + +### Changed +* run/survey import/export improved +* some UI improvements + +## [0.11.9] +### Added +* Use iframes for feedback + +## [0.11.8] +* Debug: Log session codes for problematic sessions +* Minor bug fixes + +## [0.11.7] +* Bug fixes of PHP warnings + +## [0.11.6] +* Minor bug fixes +* Revert previous login cookie logic + +## [0.11.5] +### Changed +* Re-factor user login procedure +* Non-registered users get no persistent cookie + +## [0.11.4] +* Tactical revert of forged dependencies + +## [0.11.3] +### Changed +* Run owner's email should be flagged as 'self-mailing' +* Add validation request tokens to survey forms + +## [0.11.2] +* Fix accessing external module over the API by specifying non-restrictive routes + +## [0.11.1] +### Added +* Show exired sessions in UI +* Email bug fixes + +## [0.11.0] +### Added +* Export run survey results +* Unlink Surveys to separate confidential from experimental data. +### Changed +* Major bug fixes +* pause module was simplified (if an hour of day is set, the pause will only expire once per day) for diaries +* improved external module to simplify integration with e.g. [SoSci Survey](https://github.com/rubenarslan/formr.org/wiki/How-to-combine-formr-with-surveys-from-SoSci-survey) (how to) +* surveys can be renamed, run unit modules description default to survey names + +## [0.10.4] +* Few bug fixes + +## [0.10.3] +* Few bug fixes + +## [0.10.2] +* Few bug fixes + +## [0.10.0] +# Changed +* Re-factor show-if evaluation functionality + +## [0.9.0] +### Added +* Open Science Framework API connection + * barring unforeseen changes on the side of osf.io it is now possible to connect runs to an OSF project. Currently, you can only export the Run-JSON there (in essence, saving all of the run structure and survey items). You can use this for pre-registration or version tracking. +### Changed +* improvements to testing + * when the test button is clicked in the edit run view, test emails are sent to your admin email address, not mailinator + * there are new buttons when testing surveys: a light bulb shows items that were hidden on the page, a magnifying glass shows debugging info (item names, requests sent to openCPU) + * our openCPU debugger (the accordion with info on the openCPU request) is now shown whenever there is an r problem (or you click the magnifying glass). it has the option to download the r code or the rmarkdown. That way you can edit it in Rstudio, which has much better code highlighting for debugging. +* improvements to the survey module + * now hidden items work consistently: every showif is only called once unless the return value in R is `NA`. + * only items on the currently visible page are evaluated (before this calculate items at the end of the survey were also evaluated) + * we've minimised costly openCPU requests +* there are no more flashes of unstyled content (where the mc_buttons only appear after a moment) + +## [0.8.2] +### Changed +* Changed URL slugs for public URLs +* CSS fixes + +## [0.8.1] +* bug fix release +* testing how zenodo mints DOIs for releases + +## [0.8.0] +### Added +* Items can be shuffled (in and between blocks) now. +* Timed submit buttons allow you to specify a minimum time in which to submit. +* Item tables can be imported straight from Google Docs, reducing the edit-download-upload-cycle previously necessary. Makes collaboration easier. +* A proper test mode + * you can now create test codes for runs. They're special in that they enable a bar with specific features (auto-filling forms, skipping to the next step in the run/ending a pause). + * you can now share test codes for runs before releasing the run to the public + * test codes have names like zanyElephant, which allow you to find them again if you're looking for a specific test case +* the first semblance of an OAuth2.0 API (manual activation needed) +* users can turn off emails from your study for a week or forever if you allow them to. + +## [0.7.2] +#### Changed +* Dynamic options are split by a comma + +## [0.7.1] +### Added +* Make 'time to last' for cron tasks configurable +* Pass page labels in one OpenCPU request +### Changed +* Append formr version to asset URLs for poisoning + +## [0.7.0] +### Changed +* It's now possible to present submit buttons by themselves on a page. +Sometimes this might happen unintentionally if all items on a page are skipped via a showif, but the submit button doesn't have the same showif. +In this case, previously, the submit button would never be displayed. We decided it would be more consistent to show it once. This shouldn't break anything, but might lead to an extraneous step in some very complex studies + +## [0.1.0 - 0.6.8] +- These release changes are preliminary and the mentioned versions are not recommended for use so ignored in this documentation. + +## [UNRELEASED] + - Architecture re-design(queue manager change, service-based continuous fetching, more TBD) + + + diff --git a/CHANGELOG.md b/CHANGELOG.md index 7a55ae548..f5daa0fb3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,474 +1,33 @@ -# Formr.org Change Log +# Formr.org Change Log (check previous change logs in CHANGELOG-v1.md) The format is based on [Keep a Changelog](http://keepachangelog.com/) and this project adheres to [Semantic Versioning](http://semver.org/). -## [0.17.20] - 03.01.2019 -### Fixes -https://github.com/rubenarslan/formr.org/issues/405 -https://github.com/rubenarslan/formr.org/issues/404 -https://github.com/rubenarslan/formr.org/issues/398 -https://github.com/rubenarslan/formr.org/issues/395 -https://github.com/rubenarslan/formr.org/issues/392 -https://github.com/rubenarslan/formr.org/issues/391 -https://github.com/rubenarslan/formr.org/issues/390 -https://github.com/rubenarslan/formr.org/issues/389 -https://github.com/rubenarslan/formr.org/issues/382 - -## [0.17.18] - 21.09.2018 -### Fixes -- https://github.com/rubenarslan/formr.org/issues/379 -- https://github.com/rubenarslan/formr.org/issues/332 -- https://github.com/rubenarslan/formr.org/issues/370 -- https://github.com/rubenarslan/formr.org/issues/347 -- https://github.com/rubenarslan/formr.org/issues/355 -- https://github.com/rubenarslan/formr.org/issues/378 -- https://github.com/rubenarslan/formr.org/issues/376 -- https://github.com/rubenarslan/formr.org/issues/374 -- https://github.com/rubenarslan/formr.org/issues/319 -- https://github.com/rubenarslan/formr.org/issues/292 - -## [0.17.14] - 14.06.2018 -### Added -* Logout action for RUNs - -## [0.17.13] - 12.06.2018 -### Fixed -* Show-ifs: Use previous show-if value for paginated survey - -## [0.17.12] - 25.05.2018 -### Added -* Cookie consent alert - -### Changed -* Meta tag info - -## [0.17.11] - 15.05.2018 -### Fixed -* Fix 'sticky' value for choice item types - -## [0.17.10] - 07.05.2018 -### Fixed -* Optionally uploading a file for 'File' item-type. - -## [0.17.9] - 27.04.2018 -### Fixed -* Bug #342 -* Fix Number item-type value when it is used as counter - -## [0.17.8] - 25.04.2018 -### Fixed -* Connection to the Open Science Framework -* Patch Commit - -## [0.17.7] - 20.04.2018 -### Fixed -* Do not save values for hidden items -* RUN file uploads - -## [0.17.6] - 18.04.2018 +## [v0.20.5] - 20.10.2022 ### Added -* Alert users if cookies are not activated -* GET parameter to force re-calculation of show-if for paged-surveys - -### Changed -* UI of the account page +* User search by email in admin +* User deletion ### Fixed -* Testing Email Accounts (send self-mail) -* Monkey bar session link #340 -* Default run export name #341 +* Various bug fixes -## [0.17.5] - 05.04.2018 +## [v0.20.4] - 13.09.2022 ### Fixed -* Testing "Survey" for surveys with paging enabled - -## [0.17.4] - 22.03.2018 -### Changed -#### Bug Fixes -* Use webshim calendar widget in all browsers. -* Fix Check_Item "optional" flag glitch -* Add "in_maintenance" config flag to prevent user access when site is under maintenance -* "Quick Upload" fix for surveys - -## [0.17.3] - 19.03.2018 -### Added -* Surveys can be configured to be "paginated". Users can be provided with page numbers where they can click back and forth to make corrections. -* Uploaded files associated to a run can now be easily deleted and upload file URL can also be easily copied. -* Users can re-send the email verification link. - -## [0.17.2] - 28.02.2018 -### Changed -* Fix rating button glitch (range can be reversed) -* Issue #332 -* Issue #333 - -## [0.17.1] - 28.02.2018 -### Changed -* Replace underscores in run names to hyphens - -## [0.17.0] - 28.02.2018 -### Added -* Sub-domain definition for studies -* Option to hide survey results - -### Changed -* Architectural change. - -## [0.16.17] - 09.02.2018 -### Changed -* Architectural change. - -## [0.16.16] - 31.01.2018 -### Changed -* Add option to use [XSendFile] (https://tn123.org/mod_xsendfile/) for downloads - -## [0.16.15] - 26.01.2018 -### Changed -* Fixed PHP-side survey validation so that entered data is not lost, partially addressing #327 -* Added further ways to specify expiry #305 -* UI Changes in Survey settings - -## [0.16.14] - 11.07.2017 -### Changed -Bug fix release. -* See [Github](https://github.com/rubenarslan/formr.org/issues/314) for details -* Compute dynamic value using current survey object +* Restart database transactions in case of lock wait timeout or deadlock. +* Check for orphan unit sessions before executing +* Deprecation warnings -## [0.16.13] - 02.10.2017 -### Changed -Bug fix release. -* See [Github](https://github.com/rubenarslan/formr.org/issues?utf8=%E2%9C%93&q=milestone%3Av0.16.13%20) for details -* add mc_horizontal class -* fix some CSS bugs in new material design -* several UI fixes - -## [0.16.12] - 27.07.2017 -### Changed -* Publications moved to new page - -## [0.16.10] - 06.07.2017 -## [0.16.11] - 06.07.2017 -### Changed -* Fix sign-up bug -* Get right auth credentials for email queue accounts - -## [0.16.9] - 05.07.2017 -### Added -* Use paragonie/halite for user accounts encryption - https://paragonie.com/project/halite - -### Changed -* Fix bug allowing unverified email accounts to login. - -## [0.16.8] - 04.07.2017 -### Changed -* Fix bug preventing user email change - -## [0.16.6] - 02.06.2017 -### Added -* Config item for phpmailer SMTPOptions -* Quick links to runs and surveys on admin dash board - -### Changed -* Fix broken sign up referrer code functionality -* Remove newsletter subscription - -## [0.16.5] - 11.05.2017 -### Added -* CSRF protection - -## [0.16.4] - 05.05.2017 -### Added -* Variable to get number of participants in R `.formr$nr_of_participants` - -### Changed -* Minor Bug Fixes: Form error messages, missing variables, CSS. - -## [0.16.3] - 21.03.2017 -### Changed -* HTML meta-tags -* Fix broken run unit SkipForward link - -## [0.16.2] - 15.03.2017 -## [0.16.1] - 16.03.2017 -### Changed -* Fix CSS bugs - -## [0.16.0] - 14.03.2017 -### Changed -* New User Interface - -## [0.15.7] - 09.03.2017 -### Changed -* Bug fix: Long query optimization - -## [0.15.6] - 13.02.2017 -### Changed -* Bug fix: Fix PHP warnings - - -## [0.15.5] - 08.02.2017 -### Changed -* Bug fix: Fix PHP warnings -* Issues closed: - * https://github.com/rubenarslan/formr.org/issues/275 - * https://github.com/rubenarslan/formr.org/issues/276 - * https://github.com/rubenarslan/formr.org/issues/277 - * https://github.com/rubenarslan/formr.org/issues/278 - * https://github.com/rubenarslan/formr.org/issues/280 - -## [0.15.4] - 19.12.2016 -### Changed -* Bug fix: Clear email attachments after email is sent -* Increase timeout limits for CURL requests - -## [0.15.3] - 2.12.2016 -### Changed -* Bug fix: Cron should be processed if it is locked. - -## [0.15.2] - 1.12.2016 -## [0.15.1] - 1.12.2016 -### Changed -- Configure re-try limits for email queue items -- reset smtp connection if queue item fails - -## [0.15.0] - 1.12.2016 -### Added -* E-mail queuing - * Setting: `$settings['email']['use_queue']` - Should queue be used or not? - * Setting: `$settings['email']['queue_loop_interval']` - Number of seconds for which deamon loop should rest before getting next batch -* Run management interface for superadmin. In this interface the superadmin can determine which run is allowed to run in the cron and whose cron should be forked in an independent process. -* MySQL database v21: add email queue table, run property for process forking and a _sent_ flag to email log table. - -### Changed -* Memory limits to exports are now configurable. -* Increased weekly limit on how many e-mails an address can receive. -* Bug Fixes: - * https://github.com/rubenarslan/formr.org/issues/270 - * https://github.com/rubenarslan/formr.org/issues/244 - * https://github.com/rubenarslan/formr.org/issues/272 - -## [0.14.3] - 22.11.2016 -### Changed -* Fix range issues for Item_random - -## [0.14.2] - 17.11.2016 -### Changed -* Bug fix on OR operator - -## [0.14.1] - 7.11.2016 -### Changed -* Logout page should also logout anonymous run sessions -* A page to redirect to after logout can be specified using the get parameter *_rdir* - -## [0.14.0] - 1.11.2016 -### Added -* When sending 'run jobs' to the gearman server, a high priority is used. -* User unique identifies for gearman jobs to avoid duplicates - -### Changed -* Configurable memory limits -* Bug Fixes - * Fix query to get _most recent mail_ in the Email Run Unit - * properly parse cron logs generated when running gearman - -## [0.13.3] -### Changed -* Increased memory limit when exporting results in JSON -* When backing-up results, only items saved in results table are exported -* Minor bug fixes - -## [0.13.2] -### Changed -* Fixed PHP warnings - -## [0.13.1] -### Changed -* Fixed PHP warnings - -## [0.13.0] -### Added -* Reminders now processed as unit sessions. Researchers are able to see how many times a particular reminder has been sent to a participant. -* Actions in "User Overview" can be performed on multiple participants at the same time: - * Deleting Sessions - * Moving sessions to a position in the run - * Sending reminders - * Setting test status -* Users can choose to execute email units (i.e. send emails) only when user is not currently active on a study using the option in the email run unit. -* New panic button to temporarily de-activate run in case of emergency: Functions of the panic button: - * lock's run - * disable's run cron - * makes run private. -* [major feature] Background session processing is now distributed and make asynchronous by using Gearman - -### Changed -* Error reporting on survey upload improved to point out what individual item caused issues -* Modified study examples: Experience sampling and Longitudinal studies. -* Bug fixes: - * [Issue #262](https://github.com/rubenarslan/formr.org/issues/262) - * [Issue #265](https://github.com/rubenarslan/formr.org/issues/265) - * [Issue #266](https://github.com/rubenarslan/formr.org/issues/266) - - -## [0.12.7] -### Changed -* Update documentation -* Update sample studies -* improve handling of debugger with iframes - -## [0.12.6] -### Added -* Support for deprecated 'request' api parameter - -## [0.12.5] -### Changed -* Re-factor API to accept json formatted requests -* API bug fixes - -## [0.12.4] -* Edit documentation - -## [0.12.3] -* Fix bug on run users listing -* Edit publications documentation - -## [0.12.2] -* Fix bug on run users listing -* Edit publications documentation - -## [0.12.1] -* Minor bug fixes - -## [0.12.0] -### Added -* Introduced *special run units*: - * Reminders - * Service Message - * Overview Script -* Possibility to add multiple reminders. -* Reminder sending UI modified to support multiple reminders -* much more complex feedback (including JS-graphs etc.) now possible because we embed iframes - -### Changed -* run/survey import/export improved -* some UI improvements - -## [0.11.9] -### Added -* Use iframes for feedback - -## [0.11.8] -* Debug: Log session codes for problematic sessions -* Minor bug fixes - -## [0.11.7] -* Bug fixes of PHP warnings - -## [0.11.6] -* Minor bug fixes -* Revert previous login cookie logic - -## [0.11.5] -### Changed -* Re-factor user login procedure -* Non-registered users get no persistent cookie - -## [0.11.4] -* Tactical revert of forged dependencies - -## [0.11.3] -### Changed -* Run owner's email should be flagged as 'self-mailing' -* Add validation request tokens to survey forms - -## [0.11.2] -* Fix accessing external module over the API by specifying non-restrictive routes - -## [0.11.1] -### Added -* Show exired sessions in UI -* Email bug fixes - -## [0.11.0] -### Added -* Export run survey results -* Unlink Surveys to separate confidential from experimental data. -### Changed -* Major bug fixes -* pause module was simplified (if an hour of day is set, the pause will only expire once per day) for diaries -* improved external module to simplify integration with e.g. [SoSci Survey](https://github.com/rubenarslan/formr.org/wiki/How-to-combine-formr-with-surveys-from-SoSci-survey) (how to) -* surveys can be renamed, run unit modules description default to survey names - -## [0.10.4] -* Few bug fixes - -## [0.10.3] -* Few bug fixes - -## [0.10.2] -* Few bug fixes - -## [0.10.0] -# Changed -* Re-factor show-if evaluation functionality - -## [0.9.0] -### Added -* Open Science Framework API connection - * barring unforeseen changes on the side of osf.io it is now possible to connect runs to an OSF project. Currently, you can only export the Run-JSON there (in essence, saving all of the run structure and survey items). You can use this for pre-registration or version tracking. -### Changed -* improvements to testing - * when the test button is clicked in the edit run view, test emails are sent to your admin email address, not mailinator - * there are new buttons when testing surveys: a light bulb shows items that were hidden on the page, a magnifying glass shows debugging info (item names, requests sent to openCPU) - * our openCPU debugger (the accordion with info on the openCPU request) is now shown whenever there is an r problem (or you click the magnifying glass). it has the option to download the r code or the rmarkdown. That way you can edit it in Rstudio, which has much better code highlighting for debugging. -* improvements to the survey module - * now hidden items work consistently: every showif is only called once unless the return value in R is `NA`. - * only items on the currently visible page are evaluated (before this calculate items at the end of the survey were also evaluated) - * we've minimised costly openCPU requests -* there are no more flashes of unstyled content (where the mc_buttons only appear after a moment) - -## [0.8.2] -### Changed -* Changed URL slugs for public URLs -* CSS fixes - -## [0.8.1] -* bug fix release -* testing how zenodo mints DOIs for releases - -## [0.8.0] -### Added -* Items can be shuffled (in and between blocks) now. -* Timed submit buttons allow you to specify a minimum time in which to submit. -* Item tables can be imported straight from Google Docs, reducing the edit-download-upload-cycle previously necessary. Makes collaboration easier. -* A proper test mode - * you can now create test codes for runs. They're special in that they enable a bar with specific features (auto-filling forms, skipping to the next step in the run/ending a pause). - * you can now share test codes for runs before releasing the run to the public - * test codes have names like zanyElephant, which allow you to find them again if you're looking for a specific test case -* the first semblance of an OAuth2.0 API (manual activation needed) -* users can turn off emails from your study for a week or forever if you allow them to. - -## [0.7.2] -#### Changed -* Dynamic options are split by a comma +## [v0.20.1] - 04.09.2022 +### Fixed +* Deprecation warnings. -## [0.7.1] +## [v0.20.1] - 03.09.2022 +## [v0.20.0] - 03.09.2022 ### Added -* Make 'time to last' for cron tasks configurable -* Pass page labels in one OpenCPU request -### Changed -* Append formr version to asset URLs for poisoning +* *Require PHP 8.1 or greater* +* Page content configuration (some menu pages can now be hidden and footer links / logo can be changed) +* Branding configurability. -## [0.7.0] ### Changed -* It's now possible to present submit buttons by themselves on a page. -Sometimes this might happen unintentionally if all items on a page are skipped via a showif, but the submit button doesn't have the same showif. -In this case, previously, the submit button would never be displayed. We decided it would be more consistent to show it once. This shouldn't break anything, but might lead to an extraneous step in some very complex studies - -## [0.1.0 - 0.6.8] -- These release changes are preliminary and the mentioned versions are not recommended for use so ignored in this documentation. - -## [UNRELEASED] - - Architecture re-design(queue manager change, service-based continuous fetching, more TBD) - - +* Re-factor queue-ing mechanism (run units should instruct run session on the next steps) +* Bug fixes diff --git a/INSTALLATION.md b/INSTALLATION.md index ce71bd8cb..d32820da8 100755 --- a/INSTALLATION.md +++ b/INSTALLATION.md @@ -1,49 +1,148 @@ # Setup instructions for formr -formr can run on Linux, Mac OS and Windows. However we recommend running formr on a linux environment. The installation instructions detailed -below are for a linux Environment but can be modified accordingly for other platforms. +formr can run on Linux, Mac OS and Windows. However, we only have experience with Linux and therefore recommend a Linux environment. +The installation instructions detailed below are for a Debian 9 Environment but can be modified accordingly for other platforms. -## Installing [R](http://www.r-project.org/) and [OpenCPU](https://public.opencpu.org/pages/) +## Virtual Machines -### Install R - -You can install and set-up the R software by following the [installation instructions](https://cran.r-project.org/bin/linux/) on the r-project website. +We recommend setting up OpenCPU in a separate virtual machine to the formr instance, mainly because +their load requirements may differ, and because you may at some point want to add a load balancer +to OpenCPU. It may also be advisable to separate out a database server for formr, but we will not +go into this here. +You should force all VMs to be accessible exclusively via https. Formr redirects to the SSL version +automatically and you need to make sure formr accesses OpenCPU via https. ### Install OpenCPU Visit the [OpenCPU](https://github.com/jeroenooms/opencpu/) repository and follow the [installation instructions](https://github.com/jeroenooms/opencpu/blob/master/README.md) there on how to set up and configure OpenCPU. -### Install and using the formr R-package +``` +#requires Ubuntu 16.04 (Xenial) or 18.04 (Bionic) +sudo add-apt-repository -y ppa:opencpu/opencpu-2.1 +sudo apt-get update +sudo apt-get upgrade + +#install opencpu server +sudo apt-get install -y opencpu-server +``` + +OpenCPU will automatically start running as a system service. It will be +available under the subfolder "ocpu" under the domain name for your VM. +You will need to set this domain name up in the formr settings. + +Now, you need to install a few packages. You'll need at least the formr +package, which you can install by executing `sudo -i R` and then running +`devtools::install_github("rubenarslan/formr", upgrade_dependencies = FALSE)`. + +Other packages you might wish to install: +`install.packages(c("codebook", "tidyverse", "pander"))`. For a longer list, +see [this usually up-to-date list](https://github.com/rubenarslan/formr.org/wiki/Packages-on-OpenCPU) of the stack maintained on our machine. + +We next recommend editing the following configuration files: + +Open `/etc/opencpu/server.conf` using e.g., `sudo nano`. Edit +the `key_length` setting to be longer. We use 50, OpenCPU uses 13. +We also set the following packages to `"preload": ["stringr", "dplyr","knitr", "lubridate","formr", "rmarkdown"]`. -Visit the [formr R-package repository](https://github.com/rubenarslan/formr) for installation and set up instructions. +Open `/etc/nginx/opencpu.d/ocpu.conf` using e.g., `sudo nano`. Remove the first `location` block and replace it with +``` +# OpenCPU API +location /ocpu { + proxy_pass http://ocpu/ocpu; + include /etc/nginx/opencpu.d/cache.conf; +} + +location ~* /ocpu/tmp/.+?/(messages|source|console|stdout|info|files/.+\.Rmd) { + allow IP_ADDRESS_OF_YOUR_FORMR_VM; + deny all; + proxy_pass http://ocpu; + include /etc/nginx/opencpu.d/cache.conf; +} +``` + +Take care to ensure the IP_ADDRESS_OF_YOUR_FORMR_VM is accurate. This step ensures that +the commands sent to OpenCPU are not readable by end users, which is important if you plan +to send along secret API tokens (e.g., for text messaging services). -## Installing an instance of the formr website +After packages are installed and configuration files edited, run `sudo service apache2 restart` and +check that OpenCPU runs as expected. + +### Installing an instance of the formr website These are the instructions to run a local or online copy of the formr.org distribution. It is much easier to install the [R package](https://github.com/rubenarslan/formr) if that's what you're looking for. Those who don't mind running on a frequently-updated server, we will probably also give you access to our hosted version at [formr.org](https://formr.org). -### 0. Requirements +#### 0. Requirements 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 ≥ 8.1 + * composer + * php-curl + * 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 (not needed in `develop` branch for libsodium23) * Apache >= 2.4 -* MySQL >= 5.6 +* 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) -* [Gearman](http://gearman.org/) (Server + Client) *OPTIONAL* (for running background jobs) -* [Supervisor](http://supervisord.org/) *OPTIONAL* + * 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 `develop` supports libsodium23 v1.0.16 which is the default version on most current distributions. +* [Supervisor](http://supervisord.org/) *OPTIONAL*. Though optional, we recommend using supervisor for sending out email notifications in queues and well as processing uni sessions. * [smysqlin](https://bitbucket.org/cyriltata/smysqlin) *OPTIONAL* (for managing database patches) -### 1. Clone the Git repository and checkout the *desired* release (version) +Paket list for copying: + +``` +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 php-xml 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. @@ -52,7 +151,7 @@ To see the existing releases of formr, go to https://github.com/rubenarslan/form ```sh git fetch --tags && git checkout -b ``` -Suppose for example you want to run release *v0.12.0* `git fetch --tags && git checkout v0.12.0 -b v0.12.0` +Suppose for example you want to run release *v0.18.0* `git fetch --tags && git checkout v0.18.0 -b v0.18.0` At this point you should have your formr files present in the installation directory. Go to the root of the installation directory and install the application dependencies by running @@ -60,8 +159,8 @@ At this point you should have your formr files present in the installation direc composer install ``` - -### 2. Create an empty MySQL database +  +#### 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 @@ -77,12 +176,14 @@ Import the initial required database structure mysql formr -uformr -pEnterPassword < /path/to/formr/sql/schema.sql ``` +__You'll need to apply patchsets 29 and 30 in sql/patches manually.__ + Optionally, you could use [smysqlin](https://bitbucket.org/cyriltata/smysqlin) to set up and manage patches to the formr mysql database. SQL patches are created with updates and found in the directory `/path/to/formr/sql/patches`. Any patch will be announced in the update release and you can either run this patch directly against your database or use smysqlin. -### 3. Configuration +#### 3. Configuration -#### Create the config folder +##### Create the config folder * Duplicate *(don't rename)* the folder `config-dist`, name it `config`. * Edit the /path/to/formr/config/settings.php to configure the right values for the various config items. The comments in the config file should help you identify the meaning of the config items. Some common items you need to modify are @@ -92,7 +193,7 @@ SQL patches are created with updates and found in the directory `/path/to/formr/ * email SMTP configuration * cron or deamon settings depending on how you want to process jobs in the background -#### Installing the formr cron and/or deamon +##### Installing the formr cron and/or deamon In other to process sessions in the background, you will have to setup the formr cron OR the formr deamon *BUT NOT BOTH*. The cronjob is necessary to send automated email reminders and the like. @@ -101,26 +202,26 @@ create a symbolic link to install the formr crontab in the system's crontab conf ```sh ln -s /path/to/formr/config/formr_crontab /etc/cron.d/formr ``` -* To use the formr deamon, you will need to install and setup [Gearman](http://gearman.org/) and [Supervisor](http://supervisord.org/). +* To use the formr queues (for email and unit session processing), you will need to install and setup [Supervisor](http://supervisord.org/). formr comes with a programs supervisor config in `/path/to/formr/config/supervisord.conf`. Edit this file to suit your needs. This config can be added to supervisor's default config by creating a symbolic link as follows or moving the config file to supervisor's default config directory ```sh ln -s /path/to/formr/config/supervisord.conf /etc/supervisor/conf.d/formr.conf ``` -#### Configure smysqlin for installing db patches +##### Configure smysqlin for installing db patches If you decided to use [smysqlin](https://bitbucket.org/cyriltata/smysqlin) to manage your database patches. The you could setup a formr configuration for smysqlin using the file in `/path/to/formr/config/formr.ini`. Modify it accordingly and add it to the smysql in configuration by copying file or creating a symbolic link ```sh ln -s /path/to/formr/config/formr.ini /etc/smysqlin/formr.ini ``` -#### Set paths and permissions +##### Set paths and permissions The followings folders (and their sub-folders) have to be writable: `/tmp` and `/webroot/assets/tmp`. You may need to modify the `.htaccess` files to suit your needs. See config item `define_root` to specify installation path, url and test mode. -### 4. Done +#### 4. Done You should be able to see your installation up and running by visiting the configured URL. @@ -129,4 +230,5 @@ You should be able to see your installation up and running by visiting the confi * is /tmp writable? * define_root has a hardcoded path at the time. * internal server errors: check permissions (tmp), case-sensitive paths, .htaccess path trouble -* [contact me](https://psych.uni-goettingen.de/en/biopers/team/arslan) \ No newline at end of file +* [contact us](https://groups.google.com/forum/#!forum/formr) +* If your layout seems broken, disable developer mode in the _settings.php_ diff --git a/application/AnimalName.php b/application/AnimalName.php new file mode 100644 index 000000000..a0bc69838 --- /dev/null +++ b/application/AnimalName.php @@ -0,0 +1,603 @@ +findFile($class)) { + if (class_exists($class, false)) { + return true; + } + + 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; + } + + /** + * 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__) . '../../') . '/'); + } + + if (!defined('APPLICATION_PATH')) { + define('APPLICATION_PATH', APPLICATION_ROOT . 'application/'); + } + + $class = $this->classNameToPath($class); + $paths = array( + APPLICATION_PATH . "{$class}.php", + APPLICATION_PATH . "Controller/{$class}.php", + APPLICATION_PATH . "Model/RunUnit/{$class}.php", + APPLICATION_PATH . "Model/Item/{$class}.php", + APPLICATION_PATH . "Model/{$class}.php", + APPLICATION_PATH . "View/{$class}.php", + APPLICATION_PATH . "Helper/{$class}.php", + APPLICATION_PATH . "Queue/{$class}.php", + APPLICATION_PATH . "Services/{$class}.php", + APPLICATION_PATH . "Spreadsheet/{$class}.php", + ); + + foreach ($paths as $path) { + if (file_exists($path) && is_readable($path)) { + $file = $path; + break; + } + } + + if (!empty($file)) { + return $file; + } + return false; + } + + 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; + } + +} + +return Autoload::getLoader(); diff --git a/application/CURL.php b/application/CURL.php new file mode 100644 index 000000000..f8f1f6678 --- /dev/null +++ b/application/CURL.php @@ -0,0 +1,389 @@ + 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, '', $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/Cache.php b/application/Cache.php new file mode 100644 index 000000000..4b9754a1c --- /dev/null +++ b/application/Cache.php @@ -0,0 +1,86 @@ + time() + $ttl, 'data' => $data); + return $key; + } + + /** + * Get cahced data + * + * @param string $key + * @param mixed $default Some default value to be returned if cached item is not found + * @return mixed + */ + public static function get($key = null, $default = null) { + if ($key === null) { + return self::$memory; + } + + if (isset(self::$memory[$key])) { + $cache = self::$memory[$key]; + if ($cache['ttl'] >= time()) { + $default = $cache['data']; + } + } + + return $default; + } + +} diff --git a/application/Library/Config.php b/application/Config.php similarity index 99% rename from application/Library/Config.php rename to application/Config.php index 50b152328..0d33fa769 100644 --- a/application/Library/Config.php +++ b/application/Config.php @@ -1,11 +1,12 @@ registerAssets($default_assets); + } + } + + public function indexAction() { + if (!$this->user->loggedIn()) { + alert('You need to be logged in to go here.', 'alert-info'); + $this->request->redirect('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("\n", $this->user->errors)), 'alert-danger'); + $vars['showform'] = 'show-form'; + } elseif ($oldEmail != $this->request->str('new_email')) { + $redirect = 'logout'; + } + + // Change password + $passwords = array( + 'email' => $this->user->email, + 'password' => $this->request->str('password'), + 'new_password' => $this->request->str('new_password'), + ); + if ($passwords['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($passwords)) { + 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) { + $this->request->redirect($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)'; + $vars['api_credentials'] = OAuthHelper::getInstance()->getClient($this->user); + $vars['survey_count'] = $this->fdb->count('survey_studies', ['user_id' => $this->user->id]); + $vars['run_count'] = $this->fdb->count('survey_runs', ['user_id' => $this->user->id]); + $vars['mail_count'] = $this->fdb->count('survey_email_accounts', ['user_id' => $this->user->id, 'deleted' => 0]); + + //$this->registerAssets('bootstrap-material-design'); + $this->setView('admin/account/index', $vars); + return $this->sendResponse(); + } + + public function loginAction() { + if ($this->user->loggedIn()) { + $this->request->redirect('admin/account'); + } + + if ($this->request->str('email') && $this->request->str('password') && filter_var($this->request->str('email'), FILTER_VALIDATE_EMAIL)) { + $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)); + Session::setAdminCookie($this->user); + + $redirect = $this->user->isAdmin() ? 'admin' : 'admin/account'; + $this->request->redirect($redirect); + } else { + alert(implode($this->user->errors), 'alert-danger'); + } + } + + $this->registerAssets('bootstrap-material-design'); + $this->setView('admin/account/login', array('alerts' => $this->site->renderAlerts())); + return $this->sendResponse(); + } + + 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->setView('admin/account/login', array('alerts' => $alerts)); + return $this->sendResponse(); + } else { + Session::destroy(); + $redirect_to = $this->request->getParam('_rdir'); + $this->request->redirect($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'); + $this->request->redirect('index'); + } + + if ($this->request->isHTTPPostRequest() && $site->request->str('email') && filter_var($this->request->str('email'), FILTER_VALIDATE_EMAIL)) { + if (!Session::canValidateRequestToken($site->request) || Site::getSettings('signup:allow', 'true') !== 'true') { + alert('Could not process your request please try again later', 'alert-danger'); + return $this->request->redirect('register'); + } + + + $info = array( + 'email' => $site->request->str('email'), + 'password' => $site->request->str('password'), + 'referrer_code' => $site->request->str('referrer_code'), + ); + if ($user->register($info)) { + $this->request->redirect('admin/account/register'); + } else { + alert(implode($user->errors), 'alert-danger'); + } + } + + $this->registerAssets('bootstrap-material-design'); + $this->setView('admin/account/register'); + return $this->sendResponse(); + } + + 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')); + $this->request->redirect('login'); + } elseif (!$verification_token || !$email) { + alert("You need to follow the link you received in your verification mail."); + $this->request->redirect('login'); + } else { + $user->verifyEmail($email, $verification_token); + $this->request->redirect('login'); + }; + } + + public function forgotPasswordAction() { + if ($this->user->loggedIn()) { + $this->request->redirect('index'); + } + + if ($this->request->str('email')) { + $this->user->forgotPassword($this->request->str('email')); + } + + $this->registerAssets('bootstrap-material-design'); + $this->setView('admin/account/forgot_password'); + return $this->sendResponse(); + } + + public function resetPasswordAction() { + $user = $this->user; + if ($user->loggedIn()) { + $this->request->redirect('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'); + $this->request->redirect('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->resetPassword($info))) { + $this->request->redirect('forgot_password'); + } + } + + $this->registerAssets('bootstrap-material-design'); + $this->setView('admin/account/reset_password', array( + 'reset_data_email' => $this->request->str('email'), + 'reset_data_token' => $this->request->str('reset_token'), + )); + return $this->sendResponse(); + } +} diff --git a/application/Controller/AdminAdvancedController.php b/application/Controller/AdminAdvancedController.php new file mode 100644 index 000000000..ccb13a491 --- /dev/null +++ b/application/Controller/AdminAdvancedController.php @@ -0,0 +1,365 @@ +request->redirect('/'); + } + + public function infoAction() { + $this->setView('advanced/info'); + return $this->sendResponse(); + } + + public function timingAction() { + $ocpu = OpenCPU::getInstance('opencpu_instance'); + + $db_time = $this->fdb->query("SELECT NOW();")[0]["NOW()"]; + + $ocpu_time = $ocpu->post('/base/R/Sys.time/json')->getRawResult(); + + $this->setView('advanced/timing', array( + "php" => mysql_datetime(), + "db" => $db_time, + "ocpu" => $ocpu_time)); + return $this->sendResponse(); + } + + public function testOpencpuAction() { + $this->setView('advanced/test_opencpu'); + return $this->sendResponse(); + } + + public function testOpencpuSpeedAction() { + $this->setView('advanced/test_opencpu_speed'); + return $this->sendResponse(); + } + + public function ajaxAdminAction() { + if (!Request::isAjaxRequest()) { + return $this->request->redirect('/'); + } + + $request = new Request($_POST); + + if ($request->user_id && is_numeric($request->admin_level)) { + $content = $this->setAdminLevel($request->user_id, $request->admin_level); + return $this->sendResponse($content); + } + + if ($request->user_api) { + $content = $this->apiUserManagement($request->user_id, $request->user_email, $request->api_action); + $this->response->setContentType('application/json'); + $this->response->setJsonContent($content); + return $this->sendResponse(); + } + + if ($request->user_delete) { + $user = (new User())->refresh(['id' => (int)$request->user_id, 'email' => $request->user_email]); + if ($user->isSuperAdmin()) { + alert('A super administrator cannot be deleted. Set admin level to 1 or below to delete', 'alert-danger'); + $this->response->setContentType('application/json'); + $this->response->setJsonContent(['success' => true]); + return $this->sendResponse(); + } + + $runs = $user->getRuns(); + foreach ($runs as $row) { + $run = new Run(null, $row['id']); + $run->emptySelf(); + $run->deleteUnits(); + $run->delete(); + } + + $studies = $user->getStudies('id DESC', null, 'id'); + foreach ($studies as $row) { + $study = new SurveyStudy($row['id']); + $study->delete(); + } + + $user->delete(); + + alert('User and associated data have been deleted.', 'alert-success'); + $this->response->setContentType('application/json'); + $this->response->setJsonContent(['success' => true]); + return $this->sendResponse(); + } + } + + private function setAdminLevel($user_id, $level) { + $level = (int) $level; + $allowed_levels = array(0, 1, 100); + $user = new User($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->setAdminLevel($level)) { + alert('Something went wrong with the admin level change.', 'alert-danger'); + $this->response->setStatusCode(500, 'Bad Request'); + } else { + alert('Level assigned to user.', 'alert-success'); + } + } + + return $this->site->renderAlerts(); + } + + private function apiUserManagement($user_id, $user_email, $action) { + $user = new User($user_id, null); + $content = array(); + + if ($user->email !== $user_email) { + $content = array('success' => false, 'message' => 'Invalid User'); + } elseif ($action === 'create') { + $client = OAuthHelper::getInstance()->createClient($user); + if (!$client) { + $content = array('success' => false, 'message' => 'Unable to create client'); + } else { + $client['user'] = $user->email; + $content = array('success' => true, 'data' => $client); + } + } elseif ($action === 'get') { + $client = OAuthHelper::getInstance()->getClient($user); + if (!$client) { + $content = array('success' => true, 'data' => array('client_id' => '', 'client_secret' => '', 'user' => $user->email)); + } else { + $client['user'] = $user->email; + $content = array('success' => true, 'data' => $client); + } + } elseif ($action === 'delete') { + if (OAuthHelper::getInstance()->deleteClient($user)) { + $content = array('success' => true, 'message' => 'Credentials revoked for user ' . $user->email); + } else { + $content = array('success' => false, 'message' => 'An error occured'); + } + } elseif ($action === 'change') { + $client = OAuthHelper::getInstance()->refreshToken($user); + if (!$client) { + $content = array('success' => false, 'message' => 'An error occured refereshing API secret.'); + } else { + $client['user'] = $user->email; + $content = array('success' => true, 'data' => $client); + } + } + + return $content; + } + + 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->setView('advanced/cron_log_parsed', array( + 'files' => $files, + 'parse' => $parse, + 'parser' => $parser, + 'expand_logs' => $expand, + )); + + return $this->sendResponse(); + } + + public function cronLogAction() { + return $this->cronLogParsed(); + } + + public function userManagementAction() { + $table = UserHelper::getUserManagementTablePdoStatement($this->request->getParams()); + $this->setView('advanced/user_management', $table); + + return $this->sendResponse(); + } + + public function activeUsersAction() { + $table = UserHelper::getActiveUsersTablePdoStatement(); + $this->setView('advanced/active_users', array( + 'pdoStatement' => $table['pdoStatement'], + 'pagination' => $table['pagination'], + 'status_icons' => array(0 => 'fa-eject', 1 => 'fa-volume-off', 2 => 'fa-volume-down', 3 => 'fa-volume-up') + )); + + return $this->sendResponse(); + } + + 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'); + $qs = $this->request->page ? '/?page=' . $this->request->page : null; + $this->request->redirect('admin/advanced/runs-management' . $qs); + } elseif ($id = $this->request->int('id')) { + $run = new Run(null, $id); + if (!$run->valid) { + formr_error(404, 'Not Found', 'Run Not Found'); + } + $this->setView('advanced/runs_management_queue', array( + 'stmt' => UnitSessionQueue::getRunItems($run), + 'run' => $run, + )); + return $this->sendResponse(); + } else { + $this->setView('advanced/runs_management', RunHelper::getRunsManagementTablePdoStatement()); + return $this->sendResponse(); + } + } + + public function contentSettingsAction() { + if (Request::isHTTPPostRequest()) { + $allowedSettings = array( + // 'true' set for checkboxes + 'content:about:show' => true, + 'content:docu:show' => true, 'content:docu:support_email', + 'content:studies:show' => true, + 'content:publications:show' => true, 'content:publications', + 'footer:link:policyurl', 'footer:link:logourl', 'footer:link:logolink', 'footer:imprint', + 'signup:allow','signup:message', + 'js:cookieconsent' + ); + + foreach ($allowedSettings as $setting => $is_checkbox) { + if ($is_checkbox !== true) { + $setting = $is_checkbox; + } + + if (($value = $this->request->getParam($setting)) !== null) { + $this->fdb->insert_update('survey_settings', array('setting' => $setting, 'value' => $value)); + } + } + + $this->fdb->insert_update('survey_settings', array('setting' => 'content.about.show', 'value' => 'true')); + + alert('Settings saved', 'alert-success'); + $this->sendResponse($this->site->renderAlerts()); + } + + $this->setView('advanced/settings', array('settings' => Site::getSettings())); + return $this->sendResponse(); + } + public function userDetailsAction() { + $querystring = array(); + $queryparams = array( '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->run_name) { + $queryparams['run_name'] = $this->request->run_name; + $querystring['run_name'] = $queryparams['run_name']; + } + + if ($this->request->session) { + $session = str_replace("…", "", $this->request->session); + $queryparams['session'] = "%" . $session . "%"; + $querystring['session'] = $session; + } + + if ($this->request->position) { + $queryparams['position'] = $this->request->position; + $querystring['position'] = $queryparams['position']; + } + + $table = $this->getUserDetailTable($queryparams); + $users = $table['data']; + + 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; + } + + $this->setView('advanced/user_detail', array( + 'users' => $users, + 'pagination' => $table['pagination'], + 'position_lt' => $queryparams['position_operator'], + 'querystring' => $querystring, + )); + + return $this->sendResponse(); + } + + private function getUserDetailTable($queryParams, $page = null) { + $query = array(); + if (!empty($queryParams['run_name'])) { + $query[] = ' `survey_runs`.name LIKE :run_name '; + } + + 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']); + + if(count($query) > 0 ) { + $where = "WHERE " . implode(' AND ', $query); + } else { + $where = ""; + } + + $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, + `survey_unit_sessions`.expires, + `survey_unit_sessions`.`queued`, + `survey_unit_sessions`.result, + `survey_unit_sessions`.result_log + 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} + ORDER BY `survey_run_sessions`.id DESC,`survey_unit_sessions`.id ASC LIMIT 1000 + "; + + return array( + 'data' => $this->fdb->execute($itemsQuery, $queryParams), + 'pagination' => "", + ); + } + +} diff --git a/application/Controller/AdminAjaxController.php b/application/Controller/AdminAjaxController.php index 84ed59127..e33a95d4d 100644 --- a/application/Controller/AdminAjaxController.php +++ b/application/Controller/AdminAjaxController.php @@ -8,519 +8,502 @@ */ 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 Response + */ + protected $response; + + /** + * + * @var DB + */ + protected $dbh; + + public function __construct(AdminController $controller) { + $this->controller = $controller; + $this->site = $controller->getSite(); + $this->dbh = $controller->getDB(); + $this->request = new Request(); + $this->response = new Response(); + } + + /** + * Execute the corresponding ajax method + * + * @param string $method + * @param AdminController $controller + * + * @return Response + */ + public static function call($method, AdminController $controller) { + $self = new self($controller); + $action = $self->getPrivateAction($method); + $self->$action(); + return $self->response; + } + + private function ajaxCreateRunUnit() { + if (!Request::isAjaxRequest()) { + 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'); + $content = $unit->displayForRun($this->site->renderAlerts()); + } else { + $this->response->setStatusCode(500, 'Bad Request'); + $msg = 'Sorry. Run unit could not be created.'; + $msg .= !empty($unit) ? implode("\n", $unit->errors) : ''; + alert($msg, 'alert-danger'); + $content = $this->site->renderAlerts(); + } + + return $this->response->setContent($content); + } + + private function ajaxGetUnit() { + if (!Request::isAjaxRequest()) { + formr_error(406, 'Not Acceptable'); + } + + $run = $this->controller->run; + + if ($id = $this->request->getParam('unit_id')) { + $params = $this->request->getParams(); + $params['id'] = (int) $id; + $unit = RunUnitFactory::make($run, $params); + $content = $unit->displayForRun(); + } else { + $this->response->setStatusCode(500, 'Bad Request'); + $msg = 'Sorry. Missing run unit.'; + $msg .= !empty($unit) ? implode("\n", $unit->errors) : ''; + alert($msg, 'alert-danger'); + $content = $this->site->renderAlerts(); + } + + $this->response->setContent($content); + } + + 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 - 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); - } + } + + $this->response->setContentType('application/json'); + return $this->response->setJsonContent($count); + } + + $run = $this->controller->run; + + // find the last email unit + $emailSession = $run->getReminderSession($this->request->getParam('reminder'), $this->request->getParam('session'), $this->request->getParam('run_session_id')); + if ($emailSession->execute() === false) { + alert('Something went wrong with the reminder. Run: ' . $run->name, 'alert-danger'); + } else { + alert('Reminder sent!', 'alert-success'); + } + + if (Request::isAjaxRequest()) { + $content = $this->site->renderAlerts(); + return $this->response->setContent($content); + } else { + $this->request->redirect(admin_run_url($run->name, 'user_overview')); + } + } + + private function ajaxToggleTesting() { + $run = $this->controller->run; + $dbh = $this->dbh; + + $run_session = new RunSession($this->request->getParam('session'), $run); + + $status = $this->request->getParam('toggle_on') ? 1 : 0; + $run_session->setTestingStatus($status); + + if (Request::isAjaxRequest()) { + $content = $this->site->renderAlerts(); + return $this->response->setContent($content); + } else { + $this->request->redirect(admin_run_url($run->name, 'user_overview')); + } + } + + private function ajaxSendToPosition() { + $run = $this->controller->run; + $dbh = $this->dbh; + + $runSession = new RunSession($this->request->str('session'), $run); + $position = $this->request->int('new_position'); + + if (!$runSession->forceTo($position)) { + alert('Something went wrong with the position change. Run: ' . $run->name, 'alert-danger'); + $this->response->setStatusCode(500, 'Bad Request'); + } else { + // execute current unit but don't return ouput + $runSession->execute(); + alert('Session has been moved to desired position. If moved to a Branching Unit the session might have moved on.', 'alert-info'); + } + + if (Request::isAjaxRequest()) { + $content = $this->site->renderAlerts(); + return $this->response->setContent($content); + } else { + $this->request->redirect(admin_run_url($run->name, 'user_overview')); + } + } + + private function ajaxNextInRun() { + $run = $this->controller->run; + $dbh = $this->dbh; + + $run_session = new RunSession($_GET['session'], $run); + + if (!$run_session->endCurrentUnitSession()) { + alert('Something went wrong with the unpause. in run ' . $run->name, 'alert-danger'); + $this->response->setStatusCode(500, 'Bad Request'); + } + + if (Request::isAjaxRequest()) { + $content = $this->site->renderAlerts(); + return $this->response->setContent($content); + } else { + $this->request->redirect(admin_run_url($run->name, 'user_overview')); + } + } + + private function ajaxSnipUnitSession() { + $run = $this->controller->run; + $dbh = $this->dbh; + $run_session = new RunSession($this->request->getParam('session'), $run); + + $unit_session = $run_session->getCurrentUnitSession(); + 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'); + $this->response->setStatusCode(500, 'Bad Request'); + } + } else { + alert("No unit session found", 'alert-danger'); + } + + if (Request::isAjaxRequest()) { + $content = $this->site->renderAlerts(); + return $this->response->setContent($content);; + } else { + $this->request->redirect(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'), 'run_id' => $run->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'); + $this->response->setStatusCode(500, 'Bad Request'); + } + + if (Request::isAjaxRequest()) { + $content = $this->site->renderAlerts(); + return $this->response->setContent($content); + } else { + $this->request->redirect(admin_run_url($run->name, 'user_overview')); + } + } + + private function ajaxDeleteUnitSession() { + $run = $this->controller->run; + $deleted = $this->dbh->delete('survey_unit_sessions', array('id' => $this->request->int('session_id'))); + + if ($deleted) { + alert('Success. You deleted this unit session.', 'alert-success'); + } else { + alert('Couldn\'t delete. ', 'alert-danger'); + $this->response->setStatusCode(500, 'Bad Request'); + } + + if (Request::isAjaxRequest()) { + $content = $this->site->renderAlerts(); + return $this->response->setContent($content);; + } else { + $this->request->redirect(admin_run_url($run->name, 'user_detail')); + } + } + + private function ajaxRemoveRunUnitFromRun() { + if (!Request::isAjaxRequest()) { + formr_error(406, 'Not Acceptable'); + } + + $run = $this->controller->run; + $dbh = $this->dbh; + + if (($run_unit_id = $this->request->getParam('run_unit_id'))) { + $unit = RunUnit::findByRunUnitId($run_unit_id, $this->request->getParams()); + if (!$unit) { + formr_error(404, 'Not Found', 'Requested Run Unit was not found'); + } + + $sess_key = __METHOD__ . $unit->id; + $results = $unit->getUnitSessionsCount(); + + $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); + $content = 'warn'; + return $this->response->setContent($content); + } elseif (!$has_sessions || (Session::get($sess_key) === $unit->id && $this->request->getParam('confirm') === 'yes')) { + if ($unit->removeFromRun($this->request->getParam('special'))) { + alert('Success. Unit with ID ' . $this->request->run_unit_id . ' was deleted.', 'alert-success'); + } else { + $this->response->setStatusCode(500, 'Bad Request'); + $alert_msg = 'Sorry, could not remove unit. '; + $alert_msg .= implode($unit->errors); + alert($alert_msg, 'alert-danger'); + } + } + } + + Session::delete($sess_key); + $content = $this->site->renderAlerts(); + return $this->response->setContent($content); + } + + private function ajaxReorder() { + if (!Request::isAjaxRequest()) { + formr_error(406, 'Not Acceptable'); + } + + $run = $this->controller->run; + $positions = $this->request->arr('position'); + if ($positions) { + $unit = $run->reorder($positions); + $content = ''; + } else { + $this->response->setStatusCode(500, 'Bad Request'); + $msg = 'Sorry. Re-ordering run units failed.'; + $msg .= !empty($unit) ? implode("\n", $unit->errors) : ''; + alert($msg, 'alert-danger'); + $content = $this->site->renderAlerts(); + } + + return $this->response->setContent($content); + } + + private function ajaxRunImport() { + $run = $this->controller->run; + $site = $this->site; + + if (!Request::isAjaxRequest()) { + 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(); + } + + $view = new View('admin/run/run_import_dialog', array( + 'exports' => $exports, + 'run' => $this->controller->run + )); + return $this->response->setContent($view->render()); + } + } + + private function ajaxRunLockedToggle() { + if (!Request::isAjaxRequest()) { + formr_error(406, 'Not Acceptable'); + } + $run = $this->controller->run; + $lock = $this->request->int('on'); + if (in_array($lock, array(0, 1))) { + return $run->toggleLocked($lock); + } + } + + private function ajaxRunPublicToggle() { + if (!Request::isAjaxRequest()) { + formr_error(406, 'Not Acceptable'); + } + $run = $this->controller->run; + $pub = $this->request->int('public'); + if (!$run->togglePublic($pub)) { + $this->response->setStatusCode(500, 'Bad Request'); + } + } + + private function ajaxSaveRunUnit() { + if (!Request::isAjaxRequest()) { + formr_error(406, 'Not Acceptable'); + } + + $run = $this->controller->run; + $content = ''; + + if ($id = $this->request->getParam('unit_id')) { + $params = $this->request->getParams(); + $params['id'] = (int) $id; + if (!empty($params['position']) && is_array($params['position'])) { + $params['position'] = array_shift($params['position']); + } + + $unit = RunUnitFactory::make($run, $params); + $unit->create($params); + + if ($unit->valid) { + $content = $unit->displayForRun($this->site->renderAlerts()); + } + } else { + $this->response->setStatusCode(500, 'Bad Request'); + $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'); + $content = $this->site->renderAlerts(); + } + + return $this->response->setContent($content); + } + + private function ajaxSaveSettings() { + if (!Request::isAjaxRequest()) { + formr_error(406, 'Not Acceptable'); + } + + $run = $this->controller->run; + $post = new Request($_POST); + if ($run->saveSettings($post->getParams())) { + alert('Settings saved', 'alert-success'); + } else { + $this->response->setStatusCode(500, 'Bad Request'); + alert('Error. ' . implode(".\n", $run->errors), 'alert-danger'); + } + + $content = $this->site->renderAlerts(); + return $this->response->setContent($content); + } + + private function ajaxTestUnit() { + if (!Request::isAjaxRequest()) { + formr_error(406, 'Not Acceptable'); + } + + if ($run_unit_id = $this->request->getParam('run_unit_id')) { + $unit = RunUnit::findByRunUnitId($run_unit_id, $this->request->getParams()); + $test_content = $unit->test(); + $content = $this->site->renderAlerts(); + $content .= $test_content; + } else { + $this->response->setStatusCode(500, 'Bad Request'); + $alert_msg = "Sorry. An error occured during the test."; + $alert_msg .= isset($unit) ? implode("\n", $unit->errors) : ''; + alert($alert_msg, 'alert-danger'); + $content = $this->site->renderAlerts(); + } + + return $this->response->setContent($content); + } + + private function ajaxUserBulkActions() { + if (!Request::isAjaxRequest()) { + formr_error(406, 'Not Acceptable'); + } + + $action = $this->request->str('action'); + $sessions = $this->request->arr('sessions'); + $qs = $res = array(); + if (!$action || !$sessions) { + $this->response->setStatusCode(500, 'Bad Request'); + return $this->response->setContent('Missing Parameters'); + } + + if ($action === 'toggleTest') { + $count = RunSession::toggleTestingStatus($sessions); + 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) { + $emailSession = $run->getReminderSession($this->request->int('reminder'), $sess, null); + if ($emailSession->execute() !== false) { + $count++; + } + //$email->end(); + } + + if ($count) { + alert("{$count} session(s) have been sent the reminder '{$emailSession->runUnit->getSubject($emailSession)}'", 'alert-success'); + $res['success'] = true; + } else { + $res['error'] = $this->site->renderAlerts(); + } + } elseif ($action === 'deleteSessions') { + $count = RunSession::deleteSessions($sessions); + alert("{$count} selected session(s) were successfully deleted", 'alert-success'); + $res['success'] = true; + } elseif ($action === 'positionSessions') { + $count = RunSession::positionSessions($this->controller->run, $sessions, $this->request->int('pos')); + alert("{$count} selected session(s) were successfully moved", 'alert-success'); + $res['success'] = true; + } + + $this->response->setContentType('application/json'); + return $this->response->setJsonContent($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) { + return RunSession::getSentRemindersBySessionId($run_session_id); + } } diff --git a/application/Controller/AdminController.php b/application/Controller/AdminController.php index b04d4214c..19aaa8602 100644 --- a/application/Controller/AdminController.php +++ b/application/Controller/AdminController.php @@ -2,144 +2,122 @@ 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->setView('home', array( + 'runs' => $this->user->getRuns('id DESC', 5), + 'studies' => $this->user->getStudies('id DESC', 5), + )); + return $this->sendResponse(); + } + + public function osfAction() { + if (!($token = OSF::getUserAccessToken($this->user))) { + $this->request->redirect('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->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(); + + /* @var RunUnit $u */ + foreach ($unitIds as $u) { + $unit = RunUnitFactory::make($run, $u); + $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')) { + $this->request->redirect($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->setView('misc/osf', array( + 'token' => $token, + 'runs' => $this->user->getRuns(), + 'run_selected' => $this->request->getParam('run'), + 'osf_projects' => $osf_projects, + )); + + return $this->sendResponse(); + } + + protected function setView($template, $vars = array()) { + $template = 'admin/' . $template; + parent::setView($template, $vars); + } + + protected function header() { + if (!($cookie = Session::getAdminCookie())) { + alert('You need to login to access the admin section', 'alert-warning'); + $this->request->redirect('login'); + } + + $this->user = new User($cookie[0], $cookie[1]); + + if (!$this->user->isAdmin()) { + $docLink = site_url('documentation/#get_started'); + alert('You need to request for an admin account in order to access this section. See Documentation.', 'alert-warning'); + $this->request->redirect('admin/account'); + } + + if ($this->site->inSuperAdminArea() && !$this->user->isSuperAdmin()) { + formr_error(403, 'Unauthorized', 'You are not authorized to access this section.'); + } + } + + public function createRunUnit($id = null) { + $data = array_merge($this->request->getParams(), RunUnit::getDefaults($this->request->type), RunUnit::getDefaults($this->request->special)); + if ($id) { + $data['id'] = $id; + } + + $unit = RunUnitFactory::make($this->run, $data); + + return $unit->create($data); + } } diff --git a/application/Controller/AdminMailController.php b/application/Controller/AdminMailController.php index 1d38059ef..3634e7645 100644 --- a/application/Controller/AdminMailController.php +++ b/application/Controller/AdminMailController.php @@ -2,77 +2,78 @@ 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(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->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->setView('mail/index', $vars); + return $this->sendResponse(); + } - // @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->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) { + $this->request->redirect('admin/mail'); + } else { + $this->request->redirect('admin/mail', array('account_id' => $acc->id)); + } + } } diff --git a/application/Controller/AdminRunController.php b/application/Controller/AdminRunController.php index ffbbb8450..f12dbb3af 100644 --- a/application/Controller/AdminRunController.php +++ b/application/Controller/AdminRunController.php @@ -2,871 +2,727 @@ 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 - `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 `survey_run_sessions`.run_id = :run_id $search - 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 - `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"; - - $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')); + 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) { + $this->response = AdminAjaxController::call($private_action, $this); + return $this->sendResponse(); + } + + $privateAction = $this->getPrivateAction($private_action); + return $this->$privateAction(); + } + + if (empty($this->run)) { + $this->request->redirect('admin/run/add_run'); + } + + $vars = array( + 'show_panic' => $this->showPanicButton(), + 'add_unit_buttons' => $this->getUnitAddButtons(), + ); + + $this->setView('run/index', $vars); + return $this->sendResponse(); + } + + public function listAction() { + $vars = array( + 'runs' => $this->user->getRuns('id DESC', null), + ); + + $this->setView('run/list', $vars); + return $this->sendResponse(); + } + + 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(); + $run->create([ + 'run_name' => $run_name, + 'user_id' => $this->user->id + ]); + if ($run->valid) { + alert("Success. Run '{$run->name}' was created.", 'alert-success'); + $this->request->redirect(admin_run_url($run->name)); + } else { + $error = 'An error creating your run please try again'; } - } - - private function exportUserDetailAction() { - $users_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;"; - - $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')); + } + + if (!empty($error)) { + alert("Error: {$error}", 'alert-danger'); + } + } + + $this->setView('run/add_run'); + return $this->sendResponse(); + } + + private function userOverviewAction() { + $run = $this->run; + $fdb = $this->fdb; + $querystring = array(); + $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); + $queryparams['session'] = "%" . $session . "%"; + $querystring['session'] = $session; + } + + if ($this->request->position) { + $queryparams['position'] = $this->request->position; + $querystring['position'] = $queryparams['position']; + } + + if ($this->request->sessions) { + $sessions = array(); + foreach (explode("\n", $this->request->sessions) as $session) { + $session = $this->fdb->quote($session); + if ($session) { + $sessions[] = $session; } - } - - 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; - } - - $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); - - 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(); - - $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); - } - - 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, - `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) { - 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; - } + } + $queryparams['sessions'] = $sessions; + $querystring['sessions'] = $this->request->sessions; + } + + $queryparams['admin_code'] = $this->user->user_code; + $helper = new RunHelper($run, $fdb, $this->request); + $table = $helper->getUserOverviewTable($queryparams); + + $this->setView('run/user_overview', array( + 'users' => $table['data'], + 'pagination' => $table['pagination'], + 'position_lt' => $queryparams['position_operator'], + 'currentUser' => $this->user, + 'unit_types' => $run->getAllUnitTypes(), + 'reminders' => $this->run->getSpecialUnits(false, 'ReminderEmail'), + 'querystring' => $querystring, + )); + + return $this->sendResponse(); + } + + private function exportUserOverviewAction() { + $helper = new RunHelper($this->run, $this->fdb, $this->request); + $queryParams = array('run_id' => $this->run->id, 'admin_code' => $this->user->user_code); + $exportStmt = $helper->getUserOverviewExportPdoStatement($queryParams); + $SPR = new SpreadsheetReader(); + $download_successfull = $SPR->exportInRequestedFormat($exportStmt, $this->run->name . '_user_overview', $this->request->str('format')); + if (!$download_successfull) { + alert('An error occured during user overview download.', 'alert-danger'); + $this->request->redirect(admin_run_url($this->run->name, 'user_overview')); + } + } + + private function userDetailAction() { + $run = $this->run; + $fdb = $this->fdb; + $querystring = array(); + $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); + $queryparams['session'] = "%" . $session . "%"; + $querystring['session'] = $session; + } + + if ($this->request->position) { + $queryparams['position'] = $this->request->position; + $querystring['position'] = $queryparams['position']; + } + + $helper = new RunHelper($run, $fdb, $this->request); + $table = $helper->getUserDetailTable($queryparams); + $users = $table['data']; + + 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; + } + + $this->setView('run/user_detail', array( + 'users' => $users, + 'pagination' => $table['pagination'], + 'position_lt' => $queryparams['position_operator'], + 'querystring' => $querystring, + )); + + return $this->sendResponse(); + } + + 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'); + $this->request->redirect(admin_run_url($this->run->name, 'user_detail')); + } + } + + private function createNewTestCodeAction() { + $run_session = RunSession::getTestSession($this->run); + $sess = $run_session->session; + $animal = substr($sess, 0, strpos($sess, "XXX")); + $sess_url = run_url($this->run->name, null, array('code' => $sess)); + + if (Config::get('use_study_subdomains')) { + $this->request->redirect($sess_url); + } else { + 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"); + $this->request->redirect(admin_run_url($this->run->name, 'user_overview')); + } + } + + private function createNewNamedSessionAction() { + if (Request::isHTTPPostRequest()) { + $code_name = $this->request->getParam('code_name'); + $run_session = RunSession::getNamedSession($this->run, $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"); + $this->request->redirect(admin_run_url($this->run->name, 'user_overview', array('session' => $sess))); + } + } + + $this->setView('run/create_new_named_session'); + return $this->sendResponse(); + } + + 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'); + } + $this->request->redirect(admin_run_url($run->name, 'upload_files')); + } else { + alert('Sorry, files could not be uploaded.
' . nl2br(implode("\n", $run->errors)), '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->setView('run/upload_files', array('files' => $run->getUploadedFiles())); + return $this->sendResponse(); + } + + 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'); + } + + $this->request->redirect(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->setView('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'), + )); + + return $this->sendResponse(); + } + + 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'); + $this->request->redirect(admin_run_url($run_name)); + } else { + $error = 'An error renaming your run please try again'; + } + } + + if (!empty($error)) { + alert("Error: {$error}", 'alert-danger'); + } + } + + $this->setView('run/rename_run'); + return $this->sendResponse(); + } + + 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'); + $this->request->redirect(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'); + $this->request->redirect(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'); + $this->request->redirect(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'); + $this->request->redirect(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'); + } + $this->request->redirect(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'); + $this->request->redirect(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'); + $this->request->redirect(admin_run_url($run->name)); + } + } + + private function randomGroupsAction() { + $run = $this->run; + $pdoStatement = $run->getRandomGroups(); + + $this->setView('run/random_groups', array('users' => $pdoStatement->fetchAll(PDO::FETCH_ASSOC))); + return $this->sendResponse(); + } + + private function overviewAction() { + $this->setView('run/overview', array( + 'users' => $this->run->getNumberOfSessionsInRun(), + 'overview_script' => $this->run->getOverviewScript(), + 'user_overview' => $this->run->getUserCounts(), + )); + + return $this->sendResponse(); + } + + private function emptyRunAction() { + $run = $this->run; + if ($this->request->isHTTPPostRequest()) { + if ($this->request->getParam('empty_confirm') === $run->name) { + $run->emptySelf(); + $this->request->redirect(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->setView('run/empty_run', array( 'users' => $run->getNumberOfSessionsInRun())); + return $this->sendResponse(); + + } + + private function emailLogAction() { + $queryparams = array('run_id' => $this->run->id); + $helper = new RunHelper($this->run, $this->fdb, $this->request); + $table = $helper->getEmailLogTable($queryparams); + + $this->setView('run/email_log', array( + 'emails' => $table['data'], + 'pagination' => $table['pagination'], + )); + + return $this->sendResponse(); + } + + private function deleteRunAction() { + $run = $this->run; + if (Request::isHTTPPostRequest() && $this->request->getParam('delete') && $this->request->getParam('delete_confirm') === $run->name) { + if($run->delete()) { + $this->request->redirect(admin_url()); + } + } elseif (Request::isHTTPPostRequest() && $this->request->getParam('delete')) { + alert("Error: You must type the run's name '{$run->name}' to delete it.", 'alert-danger'); + } + + $this->setView('run/delete_run', array( + 'users' => $run->getNumberOfSessionsInRun(), + )); + return $this->sendResponse(); + } + + private function cronLogAction() { + $parser = new LogParser(); + $parse = $this->run->name . '.log'; + $vars = get_defined_vars(); + + $this->setView('run/cron_log_parsed', $vars); + return $this->sendResponse(); + } + + private function sessionsQueueAction() { + $this->setView('run/sessions_queue', array( + 'stmt' => UnitSessionQueue::getRunItems($this->run), + 'run_name' => $this->run->name + )); + return $this->sendResponse(); + } + + private function setRun($name) { + if (!$name) { + return; + } + + $run = new Run($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($_POST['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'); + $this->request->redirect(admin_run_url($run->name)); + } + + if (!($export = $run->export($name, $units, $inc_survey))) { + $this->response->setStatusCode(500, 'Bad Request'); + return $this->sendResponse($site->renderAlerts()); + } else { + $SPR = new SpreadsheetReader(); + $SPR->exportJSON($export, $name); + } + } else { + alert('Run Export: Missing run units or invalid run name enterd.', 'alert-danger'); + $this->request->redirect(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 $this->request->redirect(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 $this->request->redirect(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 $this->request->redirect(admin_run_url($this->run->name)); + } + + $start_position = 10; + if ($this->run->importUnits($json_string, $start_position)) { + alert('Run modules imported successfully!', 'alert-success'); + } + + $this->request->redirect(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) { + alert('Run unit created', 'alert-success'); + } else { + alert('An unexpected error occured. Unit could not be created', 'alert-danger'); + } + $this->request->redirect(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'); + } + $this->request->redirect(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'); + } + $this->request->redirect("admin/run/{$this->run->name}"); + } + + private function showPanicButton() { + $on = $this->run->locked === 1 && + $this->run->cron_active === 0 && + $this->run->public === 0; + return !$on; + } + + private function getUnitAddButtons() { + return array( + 'Survey' => array( + 'title' => 'Add Survey', + 'icon' => 'fa-pencil-square', + ), + 'External' => array( + 'title' => 'Add External Link', + 'icon' => 'fa-external-link-square', + ), + 'Email' => array( + 'title' => 'Add Email', + 'icon' => 'fa-envelope', + ), + 'SkipBackward' => array( + 'title' => 'Add a loop (Skip Backwards)', + 'icon' => 'fa-backward', + ), + 'Pause' => array( + 'title' => 'Add a Pause', + 'icon' => 'fa-pause', + ), + 'SkipForward' => array( + 'title' => 'Add a jump (Skip Forward)', + 'icon' => 'fa-forward', + ), + 'Wait' => array( + 'title' => 'Add Waiting Time', + 'icon' => 'fa-hourglass-half', + ), + 'Shuffle' => array( + 'title' => 'Add shuffle (Randomise Participants)', + 'icon' => 'fa-random', + ), + 'Page' => array( + 'title' => 'Add a Stop Point', + 'icon' => 'fa-stop', + ), + ); + } + } diff --git a/application/Controller/AdminSurveyController.php b/application/Controller/AdminSurveyController.php index 6db1ed2d4..585125203 100644 --- a/application/Controller/AdminSurveyController.php +++ b/application/Controller/AdminSurveyController.php @@ -2,460 +2,555 @@ 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()) { + $this->changeSettings((new Request($_POST))->getParams()); + return $this->request->redirect(admin_study_url($this->study->name)); + } + + if (empty($this->study)) { + return $this->request->redirect(admin_url('survey/add_survey')); + } + + $this->setView('survey/index', array('survey_id' => $this->study->id)); + return $this->sendResponse(); + } + + public function listAction() { + $vars = array('studies' => $this->user->getStudies('id DESC', null),); + $this->setView('survey/list', $vars); + + return $this->sendResponse(); + } + + public function addSurveyAction() { + $settings = $updates = array(); + + if (Request::isHTTPPostRequest()) { + + if ($this->request->google_sheet && $this->request->survey_name) { + // Google sheet was used + $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'); + return $this->request->redirect('admin/survey'); + } + } elseif (isset($_FILES['uploaded'])) { + // Excel file was uploaded + $file = $_FILES['uploaded']; + } else { + // Nothing was uploaded + alert('Error: You have to select an item table file here.', 'alert-danger'); + return $this->request->redirect('admin/survey'); + } + + + if ($this->validateUploadedFile($file)) { + $study = new SurveyStudy(null); + if ($study->createFromFile($file)) { + // upload items + if ($study->uploadItems($file, true, true)) { + alert('Success! New survey created!', 'alert-success'); + $redirect = 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'); + $redirect = admin_study_url($study->name, 'upload_items'); + } + + delete_tmp_file($file); + return $this->request->redirect($redirect); + } + + } else { + delete_tmp_file($file); + return $this->request->redirect('admin/survey'); + } + + } + + $vars = array('google' => array()); + $this->setView('survey/add_survey', $vars); + + return $this->sendResponse(); + } + + protected function validateUploadedFile($file, $editing = false) { + if (empty($file['name'])) { + return false; + } + + $name = preg_filter("/^([a-zA-Z][a-zA-Z0-9_]{2,64})(-[a-z0-9A-Z]+)?\.[a-z]{3,4}$/", "$1", basename($file['name'])); + if (!preg_match("/[a-zA-Z][a-zA-Z0-9_]{2,64}/", (string)$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; + } + + $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'); + return false; + } + + if ($this->fdb->entry_exists('survey_studies', array('name' => $name, 'user_id' => $this->user->id)) && $editing === false) { + alert(__("Error: The survey name %s is already taken.", h($name)), 'alert-danger'); + return false; + } + + return true; + } + + private function uploadItemsAction() { + $updates = array(); + $study = $this->study; + $google_id = $study->google_file_id; + + $vars = array( + 'study_name' => $study->name, + 'google' => array( + 'id' => $google_id, + 'link' => google_get_sheet_link($google_id), + 'name' => $study->name + ), + ); + + if (Request::isHTTPPostRequest()) { + $can_delete = false; + if ($this->request->delete_confirm) { + if ($this->request->delete_confirm !== $study->name) { + 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'); + return $this->request->redirect(admin_study_url($study->name, 'upload_items')); + } + + $can_delete = true; + } + + if ($this->request->google_sheet) { + // Google sheet was used + $file = google_download_survey_sheet($study->name, $this->request->google_sheet); + if (!$file) { + alert("Unable to download the file at '{$this->request->google_sheet}'", 'alert-danger'); + return $this->request->redirect(admin_study_url($study->name, 'upload_items')); + } + + if ($study->google_file_id != $file['google_file_id']) { + $study->google_file_id = $file['google_file_id']; + $study->save(); + } + } elseif (isset($_FILES['uploaded'])) { + // Excel file was uploaded + $file = $_FILES['uploaded']; + } else { + // Nothing was uploaded + alert('Error: You have to select an item table file here.', 'alert-danger'); + return $this->request->redirect(admin_study_url($study->name, 'upload_items')); + } + + + if ($this->validateUploadedFile($file, true)) { + $survey_name = preg_filter("/^([a-zA-Z][a-zA-Z0-9_]{2,64})(-[a-z0-9A-Z]+)?\.[a-z]{3,4}$/", "$1", basename($file['name'])); + if ($study->name !== $survey_name) { + alert('Error: The uploaded file name ' . htmlspecialchars($survey_name) . ' did not match the study name ' . $study->name . '.', 'alert-danger'); + delete_tmp_file($file); + return $this->request->redirect(admin_study_url($study->name, 'upload_items')); + } + + 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'); + delete_tmp_file($file); + return $this->request->redirect(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')); + if ($study->uploadItems($file, $can_delete, false)) { + // upload items + alert('Success! Survey items uploaded!', 'alert-success'); + delete_tmp_file($file); + return $this->request->redirect(admin_study_url($study->name, 'upload_items')); } - } - - 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; - } + + } else { + delete_tmp_file($file); + return $this->request->redirect('admin/survey'); + } + + } + + $this->setView('survey/upload_items', $vars); + return $this->sendResponse(); + } + + private function accessAction() { + Session::set('test_study_data', array( + 'study_id' => $this->study->id, + 'study_name' => $this->study->name, + 'unit_id' => $this->study->id, + 'data' => $this->study->getItems('id, name, type'), + )); + + alert("Go ahead. You can test the study " . $this->study->name . " now.", 'alert-info'); + $this->request->redirect(run_url(Run::TEST_RUN)); + } + + 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'); + $this->request->redirect(admin_study_url($this->study->name, 'upload_items')); + } else { + $this->setView('survey/show_item_table', $vars); + return $this->sendResponse(); + } + } + + private function showItemdisplayAction() { + if ($this->study->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']; + } + + $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->setView('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(), + )); + + return $this->sendResponse(); + } + + private function showResultsAction() { + if ($this->study->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->setView('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(), + )); + + return $this->sendResponse(); + } + + private function hideResults() { + $this->setView('survey/show_results', array( + 'resultCount' => $this->study->getResultCount(), + 'results' => array(), + 'pagination' => new Pagination(1), + 'study_name' => $this->study->name, + )); + + return $this->sendResponse(); + } + + private function deleteResultsAction() { + $study = $this->study; + + if (Request::isHTTPPostRequest() && $this->request->getParam('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'); + } + $this->request->redirect(admin_study_url($study->name, 'delete_results')); + } elseif (Request::isHTTPPostRequest()) { + alert("Error: Survey's name must match '{$study->name}' to delete results.", 'alert-danger'); + } + + $this->setView('survey/delete_results', array( + 'resultCount' => $study->getResultCount(), + )); + + return $this->sendResponse(); + } + + private function deleteStudyAction() { + $study = $this->study; + + if (Request::isHTTPPostRequest() && $this->request->getParam('delete_confirm') === $study->name) { + $study->delete(); + alert("Success. Successfully deleted study '{$study->name}'.", 'alert-success'); + $this->request->redirect(admin_url()); + } elseif (Request::isHTTPPostRequest()) { + alert("Error: You must type the study's name '{$study->name}' to delete it.", 'alert-danger'); + } + + $this->setView('survey/delete_study', array( + 'resultCount' => $study->getResultCount(), + )); + + return $this->sendResponse(); + } + + private function renameStudyAction() { + $study = $this->study; + $new_name = $this->request->str('new_name'); + $old_name = $study->name; + + if ($new_name && $new_name !== $study->name) { + if (!preg_match("/[a-zA-Z][a-zA-Z0-9_]{2,64}/", $new_name) || + $this->fdb->entry_exists('survey_studies', array('name' => $new_name, 'user_id' => $this->user->id))) { + + alert('A study with this name already exists', 'alert-danger'); + return $this->request->redirect(admin_study_url($study->name, 'rename_study')); + } + + $study->name = $new_name; + $study->save(); + alert("Success. Successfully renamed study from '{$old_name}' to {$study->name}.", 'alert-success'); + + return $this->request->redirect(admin_study_url($study->name, 'rename_study')); + } + + $this->setView('survey/rename_study', array('study_name' => $study->name)); + return $this->sendResponse(); + } + + 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'); + $this->request->redirect(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'); + $this->request->redirect(admin_study_url($this->study->name, 'show_itemdisplay')); + } + } + + private function exportItemdisplayAction() { + if ($this->study->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'); + $this->request->redirect(admin_study_url($study->name, 'show_results')); + } + } + + private function exportResultsAction() { + if ($this->study->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'); + $this->request->redirect(admin_study_url($filename, 'show_results')); + } + } + + private function changeSettings($settings) { + $errors = false; + array_walk($settings, function (&$value, $key) { + if ($key !== 'google_file_id') { + $value = (int) $value; + } + }); + if (isset($settings['maximum_number_displayed']) && $settings['maximum_number_displayed'] > 3000 || $settings['maximum_number_displayed'] < 0) { + alert("Maximum number displayed has to be between 1 and 3000", 'alert-warning'); + $errors = true; + } + + if (isset($settings['displayed_percentage_maximum']) && $settings['displayed_percentage_maximum'] > 100 || $settings['displayed_percentage_maximum'] < 1) { + alert("Percentage maximum has to be between 1 and 100.", 'alert-warning'); + $errors = true; + } + + if (isset($settings['add_percentage_points']) && $settings['add_percentage_points'] > 100 || $settings['add_percentage_points'] < 0) { + alert("Percentage points added has to be between 0 and 100.", 'alert-warning'); + $errors = true; + } + + $settings['enable_instant_validation'] = (int) (isset($settings['enable_instant_validation']) && $settings['enable_instant_validation'] == 1); + $settings['hide_results'] = (int) (isset($settings['hide_results']) && $settings['hide_results'] === 1); + $settings['use_paging'] = (int) (isset($settings['use_paging']) && $settings['use_paging'] === 1); + $settings['unlinked'] = (int) (isset($settings['unlinked']) && $settings['unlinked'] === 1); + + // user can't revert unlinking + if ($settings['unlinked'] < $this->study->unlinked) { + alert("Once a survey has been unlinked, it cannot be relinked.", 'alert-warning'); + $errors = true; + } + + // user can't revert preventing results display + if ($settings['hide_results'] < $this->study->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 ($settings['use_paging'] < $this->study->use_paging) { + alert("Once you have enabled the use of custom paging, you can't revert this setting.", 'alert-warning'); + $errors = true; + } + + if (isset($settings['expire_after']) && $settings['expire_after'] > 3153600) { + alert("Survey expiry time (in minutes) has to be below 3153600.", 'alert-warning'); + $errors = true; + } + + if ($errors) { + return false; + } + + $this->study->update($settings); + + alert('Survey settings updated', 'alert-success', true); + } + + private function setStudy($name) { + if (!$name) { + return; + } + + $study = SurveyStudy::loadByUserAndName($this->user, $name); + if (!$study->valid) { + formr_error(404, 'Not Found', 'Requested Survey does not exist or has been moved'); + } + + $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..62e85fa46 100644 --- a/application/Controller/ApiController.php +++ b/application/Controller/ApiController.php @@ -1,217 +1,219 @@ 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; + + protected $unrestrictedActions = ['end-last-external']; + + public function __construct(Site &$site) { + parent::__construct($site); + $this->initialize(); + } + + public function indexAction() { + $this->respond(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->respond($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) { + if (!in_array($action, $this->unrestrictedActions)) { + $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->respond(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 respond($statusCode = Response::STATUS_OK, $statusText = 'OK', $response = null) { + $this->response->setStatusCode($statusCode, $statusText); + $this->response->setContentType('application/json'); + $this->response->setJsonContent($response); + return $this->sendResponse(); + } + + protected function initialize() { + $this->view = null; + $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..83ea327fc 100644 --- a/application/Controller/Controller.php +++ b/application/Controller/Controller.php @@ -2,185 +2,229 @@ 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 SurveyStudy + */ + public $study; + + /** + * + * @var Request + */ + protected $request; + + /** + * @var Response + */ + protected $response; + + /** + * @var View + */ + protected $view; + + 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; + $this->response = new Response(); + } + + protected function setView($template, $vars = array()) { + $global = 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(), + 'jsConfig' => $this->getJsConfig(), + ); + + $variables = array_merge($global, $this->vars, $vars); + $this->view = new View($template, $variables); + } + + protected function sendResponse($content = null) { + if ($content === null && $this->view) { + $content = $this->view->render(); + } + if ($content !== null) { + $this->response->setContent($content); + } + + $this->response->send(); + } + + 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."); + } + + $disabledFeatures = Config::get('disabled_features', array()); + if (in_array('SURVEY.'.$method, $disabledFeatures) || in_array('RUN.'.$method, $disabledFeatures)) { + formr_error_feature_unavailable(); + } + + + 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; + } + + protected function getJsConfig() { + // Initialize JS config + $config = array(); + // URLs + $config['site_url'] = site_url(); + $config['admin_url'] = admin_url(); + // Cookie consent + $cookieconsent = Site::getSettings('js:cookieconsent', '{}'); + if ($cookieconsent && ($decoded = json_decode($cookieconsent, true))) { + $config['cookieconsent'] = $decoded; + } + + return $config; + } + + public function errorAction($code = null, $text = null) { + formr_error($code, $text); + } } diff --git a/application/Controller/PublicController.php b/application/Controller/PublicController.php index 2142ebc52..cff251304 100644 --- a/application/Controller/PublicController.php +++ b/application/Controller/PublicController.php @@ -1,213 +1,99 @@ 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')) { - if($this->user->login($this->request->str('email'), $this->request->str('password'))) { - 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')) { - if($user->register($site->request->str('email'), $site->request->str('password'), $site->request->str('referrer_code'))) { - //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); - $email = $postRequest->str('email'); - $token = $postRequest->str('reset_token'); - $newPass = $postRequest->str('new_password'); - $newPassOK = $postRequest->str('new_password_c'); - if (($done = $user->reset_password($email, $token, $newPass, $newPassOK))) { - 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->setView('public/home'); + return $this->sendResponse(); + } + + public function documentationAction() { + if (Site::getSettings('content:docu:show', 'true') !== 'true') { + formr_error(403, 'Not Public', 'Page cannot be displayed'); + } + $this->setView('public/documentation', array('headerClass' => 'fmr-small-header')); + return $this->sendResponse(); + } + + public function studiesAction() { + if (Site::getSettings('content:studies:show', 'true') !== 'true') { + formr_error(403, 'Not Public', 'Page cannot be displayed'); + } + + $this->setView('public/studies', array('runs' => RunHelper::getPublicRuns())); + return $this->sendResponse(); + } + + public function aboutAction() { + if (Site::getSettings('content:about:show', 'true') !== 'true') { + formr_error(403, 'Not Public', 'Page cannot be displayed'); + } + + $this->setView('public/about', array( + 'bodyClass' => 'fmr-about', + 'headerClass' => 'fmr-small-header' + )); + return $this->sendResponse(); + } + + public function publicationsAction() { + if (Site::getSettings('content:publications:show', 'true') !== 'true') { + formr_error(403, 'Not Public', 'Page cannot be displayed'); + } + + $this->setView('public/publications', array('headerClass' => 'fmr-small-header')); + return $this->sendResponse(); + } + + public function loginAction() { + $this->request->redirect('admin/account/login'); + } + + public function logoutAction() { + $this->request->redirect('admin/account/logout'); + } + + public function registerAction() { + $this->request->redirect('admin/account/register'); + } + + public function accountAction() { + $this->request->redirect('admin/account'); + } + + public function resetPasswordAction() { + $this->request->redirect('admin/account/reset-password', array( + 'email' => $this->request->str('email'), + 'reset_token' => $this->request->str('reset_token'), + )); + } + + public function forgotPasswordAction() { + $this->request->redirect('admin/account/forgot-password'); + } + + public function verifyEmailAction() { + $this->request->redirect('admin/account/verify-email', array( + 'email' => $this->request->str('email'), + 'verification_token' => $this->request->str('verification_token'), + 'token' => $this->request->str('token', null), + )); + } + + 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 $this->request->redirect(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 ab9ec2955..b78e532d6 100644 --- a/application/Controller/RunController.php +++ b/application/Controller/RunController.php @@ -2,200 +2,285 @@ 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'); - } + public function __construct(Site &$site) { + + parent::__construct($site); + if (!Request::isAjaxRequest()) { + $default_assets = get_default_assets('site'); + $this->registerAssets($default_assets); + } + } - $this->user = $this->site->loginUser($this->user); - $this->run = $this->getRun(); - $run_vars = $this->run->exec($this->user); - $run_vars['bodyClass'] = 'fmr-run'; + public function indexAction($runName = '', $privateAction = null) { + // hack for run name + $_GET['run_name'] = $runName; + $this->site->request->run_name = $runName; + $pageNo = null; - $assset_vars = $this->filterAssets($run_vars); - unset($run_vars['css'], $run_vars['js']); - $this->renderView('public/run/index', array_merge($run_vars, $assset_vars)); - } + 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'); + } - protected function settingsAction() { - $run = $this->getRun(); - $run_name = $this->site->request->run_name; + $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(); - if (!$run->valid) { - formr_error(404, 'Not Found', 'Requested Run does not exist or has been moved'); - } + //Request::setGlobals('COOKIE', $this->setRunCookie()); - // 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->site->loginUser($this->user); - if ($this->user->user_code != $code) { - alert('Unable to login with the provided code', 'alert-warning'); - } - redirect_to(run_url($run_name, 'settings')); + $run_vars = $this->run->exec($this->user); + if (!$run_vars) { + formr_error(500, 'Invalid Execution', 'The execution generated no output'); } + + $run_vars['bodyClass'] = 'fmr-run'; + + if (!empty($run_vars['redirect'])) { + return $this->request->redirect($run_vars['redirect']); + } - // 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.'); - } + $assset_vars = $this->filterAssets($run_vars); + unset($run_vars['css'], $run_vars['js']); - $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->setView('run/index', array_merge($run_vars, $assset_vars)); - $this->run = $run; - $this->renderView('public/run/settings', array( - 'settings' => $session->getSettings(), - 'email_subscriptions' => Config::get('email_subscriptions'), - )); - } - - protected function logoutAction() { - $run = $this->getRun(); - 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($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"); - } + return $this->sendResponse(); + } - $parts = explode('_', $action); - $method = array_shift($parts) . str_replace(' ', '', ucwords(implode(' ', $parts))); - $runHelper = new RunHelper($this->request, $this->fdb, $run->name); + protected function settingsAction() { + $run = $this->getRun(); + $run_name = $this->site->request->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 (!$run->valid) { + formr_error(404, 'Not Found', 'Requested Run does not exist or has been moved'); + } - if (!method_exists($runHelper, $method)) { - throw new Exception("Invalid method {$action}"); - } + // 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()); - $runHelper->{$method}(); - if (($errors = $runHelper->getErrors())) { - $errors = implode("\n", $errors); - alert($errors, 'alert-danger'); - } + if ($this->user->user_code != $code) { + alert('Unable to login with the provided code', 'alert-warning'); + } + $this->request->redirect(run_url($run_name, 'settings')); + } - if (($message = $runHelper->getMessage())) { - alert($message, 'alert-info'); - } + // People who have no session in the run need not set anything + $session = new RunSession($this->user->user_code, $run); + if (!$session->id) { + formr_error(401, 'Unauthorized', 'You cannot create settings in a study you have not participated in.'); + } - 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.'); - } + $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'), + ); - return $run; - } + 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; + } - 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; - } + $session->saveSettings($settings, $update); + + alert('Settings saved successfully for survey "' . $run->name . '"', 'alert-success'); + if ($settings['delete_cookie']) { + Session::destroy(); + $this->request->redirect('index'); + } + $this->request->redirect(run_url($run_name, 'settings')); + } + + $this->run = $run; + $this->setView('run/settings', array( + 'settings' => $session->getSettings(), + 'email_subscriptions' => Config::get('email_subscriptions'), + )); + + return $this->sendResponse(); + } + + protected function logoutAction() { + $this->run = $this->getRun(); + $this->user = $this->loginUser(); + $cookie = $this->getRunCookie(); + $cookie->destroy(); + Session::destroy(false); + $hint = 'Session Ended'; + $text = 'Your session was successfully closed! You can restart a new session by clicking the link below.'; + $url = run_url($this->run->name); + if ($this->request->prev) { + //If user is loggin out from a test session, show button to create another test session + $prevRunSesson = new RunSession($this->request->prev, $this->run); + if ($prevRunSesson->testing) { + $url = admin_run_url($this->run->name, 'create_new_test_code'); + } + } + formr_error(200, 'OK', $text, $hint, $url, '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($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()) { + 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 (Request::isAjaxRequest()) { + $content = $this->site->renderAlerts(); + $this->sendResponse($content); + } else { + $this->request->redirect(''); + } + } + + private function getRun() { + $name = $this->request->str('run_name'); + $run = new Run($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); + $this->request->redirect($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) { + if ($action === null) { + return false; + } + + $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; - return $meta; - } + // 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 ($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($id, $loginCode); + } } diff --git a/application/Controller/SuperadminController.php b/application/Controller/SuperadminController.php deleted file mode 100644 index eb4c643fb..000000000 --- a/application/Controller/SuperadminController.php +++ /dev/null @@ -1,339 +0,0 @@ -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, - `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, - )); - } - - 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; - } - - $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 diff --git a/application/Cookie.php b/application/Cookie.php new file mode 100644 index 000000000..ad8b95e81 --- /dev/null +++ b/application/Cookie.php @@ -0,0 +1,169 @@ +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/Cron.php b/application/Cron.php new file mode 100644 index 000000000..77455ba26 --- /dev/null +++ b/application/Cron.php @@ -0,0 +1,243 @@ +db = $db; + $this->site = $site; + $this->user = $user; + $this->config = $config; + $this->lockfile = $params['lockfile']; + $this->logfile = $params['logfile']; + $this->process_run = $params['process_run']; + + $this->start_datastamp = date('r'); + + $this->setUp(); + } + + protected function setUp() { + // Check if lock exists and exit if it does to avoid running similar process + if ($this->lockExists()) { + exit(1); + } + + set_time_limit($this->config['ttl_cron']); + register_shutdown_function(array($this, 'cleanup')); + + // Register signal handlers that should be able to kill the cron in case some other weird stuff 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 execute() { + if (!$this->lock()) { + $this->dbg('Unable to LOCK cron.. exiting'); + exit(1); + } + + /** Do the Work */ + $this->dbg(".... Cron started .... {$this->start_datastamp}"); + $start_time = microtime(true); + + if ($this->process_run) { + $this->executeRun($this->process_run); + } else { + $this->executeList(); + } + + $minutes = round((microtime(true) - $start_time) / 60, 3); + $end_date = date('r'); + $this->dbg(".... Cron ended .... {$end_date}. Took ~{$minutes} minutes"); + + $this->unLock(); + $this->cleanup(); + } + + protected function executeList() { + try { + // Get all runs + $runs = $this->db->select('name')->from('survey_runs')->where('cron_active = 1')->order('cron_fork', 'DESC')->fetchAll(); + + foreach ($runs as $run_data) { + $run = new Run($run_data['name']); + if (!$run->valid) { + alert("This run '{$run_data['name']}' caused problems", 'alert-danger'); + continue; + } + + // If run is locked, do not process it + $lockfile = APPLICATION_ROOT . "tmp/cron-{$run->name}.lock"; + if ($this->lockExists($lockfile)) { + // log in cron run file + continue; + } + + $script = APPLICATION_ROOT . 'bin/cron.php'; + $stdout = get_log_file("cron/cron-run-{$run->name}.log"); + $command = PHP_BINARY . " $script -n {$run->name} >> {$stdout} 2>&1 &"; + $this->dbg("Execute Command Run: '{$command}'"); + exec($command, $output, $status); + if ($status != 0) { + $this->dbg("Command '{$command}' exited with status {$status}. Output: " . print_r($output, 1)); + } + } + } catch (Exception $e) { + error_log('Cron [Exception]: ' . $e->getMessage()); + error_log('Cron [Exception]: ' . $e->getTraceAsString()); + } + } + + protected function executeRun(Run $run) { + $this->dbg('----------'); + $this->dbg("cron-run call start for {$run->name}"); + + // get all session codes that have Branch, Pause, or Email lined up (not ended) + $sessions = $this->db->select('session')->from('survey_run_sessions') + ->where(array('run_id' => $run->id)) + ->order('RAND') + ->statement(); + + $done = array(); + $i = 0; + // Foreach session, execute all units + $run->getOwner(); + while ($row = $sessions->fetch(PDO::FETCH_ASSOC)) { + $session = $row['session']; + $runSession = new RunSession($session, $run); + $types = $runSession->execute(); // start looping thru their units. + $i++; + + if ($types === false) { + alert("This session '$session' caused problems", 'alert-danger'); + continue; + } + + foreach ($types as $type => $nr) { + if (!isset($done[$type])) { + $done[$type] = 0; + } + $done[$type] += (int)$nr; + } + } + + $executed_types = $this->parseExecutedUnitTypes($done); + + $msg = "$i sessions in the run " . $run->name . " were processed. {$executed_types}"; + $this->dbg($msg); + if (Site::getInstance()->alerts) { + $this->dbg("\n\n" . Site::getInstance()->renderAlerts() . "\n"); + } + + // log execution time + $this->dbg("cron-run call end for {$run->name}"); + return true; + } + + public function cleanup() { + $this->unLock(); + } + + protected function parseExecutedUnitTypes($types) { + $str = ''; + foreach ($types as $key => $value) { + $str .= " {$value} {$key}s,"; + } + return $str; + } + + protected function lock() { + return file_put_contents($this->lockfile, $this->start_datastamp); + } + + protected function unLock() { + if (file_exists($this->lockfile)) { + unlink($this->lockfile); + $this->dbg(".... Cronfile cleanup complete"); + } + } + + protected function lockExists($lockfile = null) { + if ($lockfile === null) { + $lockfile = $this->lockfile; + } + + if (file_exists($lockfile)) { + $started = file_get_contents($lockfile); + $this->dbg("Cron overlapped. Started: $started, Overlapped: {$this->start_datastamp}"); + + // hack to delete $lockfile if cron hangs for more that 30 mins + if ((strtotime($started) + ((int) $this->config['ttl_lockfile'] * 60)) < time()) { + $this->dbg("Forced delete of {$lockfile}"); + unlink($lockfile); + return false; + } + return true; + } else { + return false; + } + } + + /** + * 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->dbg("%s Received termination signal", getmypid()); + break; + + case SIGUSR1: + break; + } + } + + protected function dbg($message) { + $message = date('Y-m-d H:i:s') . ' ' . $message . "\n"; + if ($this->logfile) { + return error_log($message, 3, $this->logfile); + } + // else echo to STDOUT instead + echo $message; + } + +} diff --git a/application/Crypto.php b/application/Crypto.php new file mode 100644 index 000000000..63212f3ac --- /dev/null +++ b/application/Crypto.php @@ -0,0 +1,79 @@ +getString(); + } catch (Exception $e) { + formr_log_exception($e, 'ParagonIE\Halite'); + } + } + +} diff --git a/application/DB.php b/application/DB.php new file mode 100644 index 000000000..ea50115b6 --- /dev/null +++ b/application/DB.php @@ -0,0 +1,895 @@ + 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; + /** + * + * @var array(PDOStatement, array) + */ + protected $lastStatement; + + protected function __construct() { + $params = (array) Config::get('database'); + + $options = array( + 'host' => $params['host'], + 'dbname' => $params['database'], + 'charset' => $params['encoding'], + ); + if (!empty($params['port'])) { + $options['port'] = $params['port']; + } + + $dsn = 'mysql:' . http_build_query($options, '', ';'); + $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 '.$options['charset'], + )); + + $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); + $this->lastStatement = [$stmt, $params]; + $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); + $this->lastStatement = [$sth, $params]; + $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 + * @param array $params + * + * @return PDOStatement + */ + public function rquery($query, $params = array()) { //secured query with prepare and execute + $stmt = $this->PDO->prepare($query); + $this->lastStatement = [$stmt, $params]; + $stmt->execute($params); + + 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); + $this->lastStatement = [$stmt, null]; + $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); + $this->lastStatement = [$stmt, $params]; + $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); + $this->lastStatement = [$stmt, $data]; + $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)); + $this->lastStatement = [$stmt, $data]; + $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); + $this->lastStatement = [$stmt, $data]; + $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]]; + } + if (is_array($value)) { + $value = json_encode($value); + } + $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() { + if (!$this->PDO->inTransaction()) { + return $this->PDO->beginTransaction(); + } + } + + /** + * {inherit PDO doc} + */ + public function commit() { + if ($this->PDO->inTransaction()) { + return $this->PDO->commit(); + } + } + + /** + * {inherit PDO doc} + */ + public function rollBack() { + if ($this->PDO->inTransaction()) { + 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; + } + + public function retryTransaction(Exception $e) { + return strstr($e->getMessage(), 'try restarting transaction') !== false; + } + + public function logLastStatement(Exception $e) { + $interestingCodes = ['HY000', '23000', '40001']; + if (in_array($e->getCode(), $interestingCodes) && $this->lastStatement) { + formr_log($this->lastStatement[0]->queryString, 'MySQL_QUERY'); + formr_log($this->lastStatement[1], 'MySQL_PARAMS'); + } + } + +} + +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, + ); + } + +} diff --git a/application/Functions.php b/application/Functions.php new file mode 100644 index 000000000..2a765f551 --- /dev/null +++ b/application/Functions.php @@ -0,0 +1,1761 @@ +' . $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)); + } +} + +function get_log_file($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); +} + +function notify_user_error($error, $public_message = '') { + $run_session = Site::getInstance()->getRunSession(); + $date = date('Y-m-d H:i:s'); + + $message = $date . ': ' . $public_message . "
"; + + if ($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 ($run_session && !$run_session->isCron() && $run_session->isTesting()) { + $date = date('Y-m-d H:i:s'); + + $message = $date . ': ' . $public_message . "
"; + + $message .= opencpu_debug($ocpu_req); + alert($message, 'alert-info hidden_debug_message hidden'); + } +} + +function redirect_to($location = '', $params = array()) { + if (formr_in_console()) { + return; + } + + $location = str_replace(PHP_EOL, '', (string)$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; +} + +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; + $text = str_replace(APPLICATION_ROOT, '', $text); + 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; + } + + $view = new View('public/error', array( + 'code' => $code, + 'title' => $hint ? $hint : $title, + 'text' => $text, + 'link' => $link, + 'link_text' => $link_text, + )); + + $response = new Response(); + $response->setStatusCode($code, $title)->setContent($view->render())->send(); +} + +function formr_error_feature_unavailable() { + formr_error('503', 'Feature Unavailable', 'Sorry this feature is temporarily unavailable. Please try again later', '', 'javascript:history.back();', 'Go Back'); +} + +function h($text) { + if (!$text) { + return null; + } + + return htmlspecialchars($text); +} + +function debug($string) { + 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)); + } +} + +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; + } +} + +if (!function_exists('_')) { + + 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; +} + +function used_cache($echo = false) { + 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; +} + +if (!function_exists('__')) { + + /** + 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); + } + +} + +if (!function_exists('__n')) { + + /** + 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); + } + +} + +function endsWith($haystack, $needle) { + $length = strlen($needle); + if ($length == 0) { + return true; + } + + return (mb_substr($haystack, -$length) === $needle); +} + +/** + * Gets an environment variable from available sources, and provides emulation + * for unsupported or inconsistent environment variables (i.e. DOCUMENT_ROOT on + * IIS, or SCRIPT_NAME in CGI mode). Also exposes some additional custom + * environment information. + * + * @param string $key Environment variable name. + * @return string Environment variable setting. + * @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; +} + +function emptyNull(&$x) { + $x = is_formr_truthy($x) ? $x : null; +} + +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"; + } + + return $x; +} + +function hardTrueFalse($x) { + if ($x === false) { + return 'FALSE'; + } elseif ($x === true) { + return 'TRUE'; +# elseif($x===null) return 'NULL'; + } elseif ($x === 0) { + return '0'; + } + + 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; + } + +} + +/** + * Format a timestamp to display its age (5 days ago, in 3 days, etc.). + * + * @param int $timestamp + * @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); + } +} + +// 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]; +} + +function cr2nl($string) { + return str_replace("\r\n", "\n", (string)$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; +} + +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; +} + +function base64_url_encode($data) { + return strtr(base64_encode($data), '+/=', '-_~'); +} + +function base64_url_decode($data) { + return base64_decode(strtr($data, '-_~', '+/=')); +} + +/** + * Create URL Title + * + * Takes a "title" string as input and creates a + * human-friendly URL string with a "separator" string + * as the word separator. + * + * @param string the string + * @param string the separator + * @param strin $lowercase Should string be returned in lowecase letters + * @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); +} + +function empty_column($col, $arr) { + $empty = true; + $last = null; + foreach ($arr AS $row): + if (!(empty($row->$col)) || // not empty column? (also treats 0 and empty strings as empty) + $last != $row->$col || // any variation in this column? + !(!is_array($row->$col) && trim((string)$row->$col) == '')): + $empty = false; + break; + endif; + $last = $row->$col; + endforeach; + return $empty; +} + +/** + * Return an array of contents in the run export directory + * + * @param string $dir Absolute path to readable directory + * @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; +} + +/** + * Get the mime type of a file given filename using FileInfo + * @see http://php.net/manual/en/book.fileinfo.php + * + * @param string $filename + * @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; + } + + $mime_type = $mime[0]; + return $mime_type; +} + +/** + * Send a file for download to client + * + * @param string $file Absolute path to file + * @param boolean $unlink + * @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); +} + +/** + * @deprecated + */ +function get_duplicate_update_string($columns) { + foreach ($columns as $i => $column) { + $column = trim($column, '`'); + $columns[$i] = "`$column` = VALUES(`$column`)"; + } + return $columns; +} + +/** + * Returns a valid MySQL datetime string + * + * @param int $time [optional] Valid unix timestamp + * @return string + */ +function mysql_datetime($time = null) { + if ($time === null) { + $time = time(); + } + if (is_string($time)) { + $time = strtotime($time); + } + return date('Y-m-d H:i:s', $time); +} + +/** + * Returns a string equivalent to MySQL's NOW() function + * + * @return string + */ +function mysql_now() { + return mysql_datetime(); +} + +/** + * Returns formatted strings equivalent to expressions like NOW() + INTERVAL 2 DAY + * + * @param string A string defining an interval accepted by PHP's strtotime() function + * @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); +} + +function site_url($uri = '', $params = array()) { + $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); +} + +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')) { + $domain = Config::get('define_root.study_domain', $domain); # use different domain for studies if set + $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); +} + +function admin_run_url($name = '', $action = '', $params = array()) { + 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 + * 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); +} + +function monkeybar_url($run_name, $action = '', $params = array()) { + return run_url($run_name, 'monkey-bar/' . $action, $params); +} + +function array_to_accordion($array) { + $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); + + $acc .= ' +
+ +
+
+ ' . $content . ' +
+
+
'; + $first = ''; + endforeach; + + $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; +} + +function is_formr_truthy($value) { + if (is_array($value)) { + return $value; + } + $value = (string) $value; + $value = trim($value); + return $value || $value === '0'; +} + +/** + * Convert an array of data into variables for OpenCPU request + * The array parameter if it contains an entry called 'datasets', then these will be passed as R dataframes and other key/value pairs will be passed as R variables + * + * @param array $data + * @param string $context + * @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) +'; + 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 . ' +'; + } + return $vars; +} + +/** + * Execute a piece of code against OpenCPU + * + * @param string $location A previous openCPU session location + * @param string $return_format String like 'json' + * @param mixed $context If this paramter is set, $code will be evaluated with a context + * @param bool $return_session Should OpenCPU_Session object be returned + * @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; + } +} + +/** + * Execute a piece of code against OpenCPU + * + * @param string $code Each code line should be separated by a newline characted + * @param string|array $variables An array or string (separated by newline) of variables to be used in OpenCPU request + * @param string $return_format String like 'json' + * @param mixed $context If this paramter is set, $code will be evaluated with a context + * @param bool $return_session Should OpenCPU_Session object be returned + * @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' => '{ +(function() { + library(formr) + ' . $variables . ' + ' . $code . ' +})() }'); + + $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 computational error."); + opencpu_log($e); + return null; + } +} + +/** + * In one common, well-defined case, we just skip calling openCPU + * + * @param string code + * @param array data for openCPU + * @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_]+)$/", (string)$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; +} + +/** + * Call knit() function from the knitr R package + * + * @param string $code + * @param string $return_format + * @param bool $return_session Should OpenCPU_Session object be returned + * @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; + } +} + +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 = 'FALSE'; + $show_warnings = 'FALSE'; + if (!$run_session OR $run_session->isTesting()) { + $show_errors = 'FALSE'; + $show_warnings = 'TRUE'; + } + + $source = '```{r settings,warning=' . $show_warnings . ',message=' . $show_warnings . ',error=' . $show_errors . ',echo=F} +library(knitr); library(formr) +opts_chunk$set(warning=' . $show_warnings . ',message=' . $show_warnings . ',error=' . $show_errors . ',echo=F,fig.height=7,fig.width=10) +opts_knit$set(base.url="' . OpenCPU::TEMP_BASE_URL . '") +' . $variables . ' +``` +' . + $source; + + return opencpu_knit($source, 'json', 0, $return_session); +} + +/** + * knit R markdown to html + * + * @param string $source + * @param string $return_format + * @param int $self_contained + * @param bool $return_session Should OpenCPU_Session object be returned + * @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; + } +} + +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 = 'FALSE'; + $show_warnings = 'FALSE'; + if (!$run_session OR $run_session->isTesting()) { + $show_errors = 'TRUE'; + $show_warnings = 'TRUE'; + } + + $yaml = ""; + $yaml_lines = '/^\-\-\-/um'; + if (preg_match_all($yaml_lines, (string)$source) >= 2) { + $parts = preg_split($yaml_lines, $source, 3); + $yaml = "---" . $parts[1] . "---\n\n"; + $source = $parts[2]; + } + + $source = $yaml . + '```{r settings,warning=' . $show_warnings . ',message=' . $show_warnings . ',error=' . $show_errors . ',echo=' . $show_warnings . '} +library(knitr); library(formr) +opts_chunk$set(warning=' . $show_warnings . ',message=' . $show_warnings . ',error=' . $show_errors . ',echo=' . $show_warnings . ',fig.height=7,fig.width=10) +' . $variables . ' +``` + +' . + $description . ' + + +' . + $source . + " + + + +#   + +" . $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 computational error."); + 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 = 'FALSE'; + $show_warnings = 'FALSE'; + if (!$run_session OR $run_session->isTesting()) { + $show_errors = 'TRUE'; + $show_warnings = 'TRUE'; + } + + $source = '```{r settings,warning=' . $show_warnings . ',message=' . $show_warnings . ',error=' . $show_errors . ',echo=F} +library(knitr); library(formr) +opts_chunk$set(warning=' . $show_warnings . ',message=' . $show_warnings . ',error=' . $show_errors . ',echo=F,fig.height=7,fig.width=10) +opts_knit$set(base.url="' . OpenCPU::TEMP_BASE_URL . '") +' . $variables . ' +``` +' . + $source; + + 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 = 'FALSE'; + $show_warnings = 'FALSE'; + if (!$run_session OR $run_session->isTesting()) { + $show_errors = 'TRUE'; + $show_warnings = 'TRUE'; + } + + $source = '```{r settings,warning=' . $show_warnings . ',message=' . $show_warnings . ',error=' . $show_errors . ',echo=F} +library(knitr); library(formr) +opts_chunk$set(warning=' . $show_warnings . ',message=' . $show_warnings . ',error=' . $show_errors . ',echo=F) +opts_knit$set(base.url="' . OpenCPU::TEMP_BASE_URL . '") +' . $variables . ' +``` +' . + $source; + + return opencpu_knit2html($source, 'json', 0, $return_session); +} + +function opencpu_knit_email($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 = 'FALSE'; + $show_warnings = 'FALSE'; + if (!$run_session OR $run_session->isTesting()) { + $show_errors = 'TRUE'; + $show_warnings = 'TRUE'; + } + + $source = '```{r settings,warning=' . $show_warnings . ',message=' . $show_warnings . ',error=' . $show_errors . ',echo=F} +library(knitr); library(formr) +opts_chunk$set(warning=' . $show_warnings . ',message=' . $show_warnings . ',error=' . $show_errors . ',echo=F,fig.retina=2) +opts_knit$set(upload.fun=function(x) { paste0("cid:", URLencode(basename(x))) }) +' . $variables . ' +``` +' . + $source; + + return opencpu_knit2html($source, $return_format, 0, $return_session); +} + +function opencpu_string_key($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; +} + +/** + * Parse a bulk of strings in ocpu + * + * @param UnitSession $unitSession Unit session containing the data needed + * @param array $string_templates An array of strings to be parsed + * @return array Returns an array of parsed labels indexed by the label-key to be substituted + */ +function opencpu_multistring_parse(UnitSession $unitSession, array $string_templates) { + $survey = $unitSession->runUnit->surveyStudy; + $markdown = implode(OpenCPU::STRING_DELIMITER, $string_templates); + $opencpu_vars = $unitSession->getRunData($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)); + } +} + +/** + * Substitute parsed strings in the collection of items that were sent for parsing + * This function does not return anything as the collection of items is passed by reference + * For objects having the property 'label_parsed', they are checked and substituted + * + * @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_multiparse_showif(UnitSession $unitSession, array $showifs, $return_session = false) { + $survey = $unitSession->runUnit->surveyStudy; + $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 = $unitSession->getRunData($code, $survey->name); + return opencpu_evaluate($code, $variables, 'json', null, $return_session); +} + +function opencpu_multiparse_values(UnitSession $unitSession, array $values, $return_session = false) { + $survey = $unitSession->runUnit->surveyStudy; + $code = "(function() {with(tail({$survey->name}, 1), {\n"; + $code .= "list(\n" . implode(",\n", $values) . "\n)"; + $code .= "})})()\n"; + + $variables = $unitSession->getRunData($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'] = ' + Download R Markdown file to debug.
+ '; + } 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'] = ' +

+ 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); +} + +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')); +} + +function opencpu_formr_variables($q) { + $variables = []; + if (preg_match("/\btime_passed\b/", (string)$q)) { + $variables[] = 'formr_last_action_time'; + } + if (preg_match("/\bnext_day\b/", (string)$q)) { + $variables[] = 'formr_last_action_date'; + } + if (strstr((string)$q, '.formr$login_code') !== false) { + $variables[] = 'formr_login_code'; + } + if (preg_match("/\buser_id\b/", (string)$q)) { + $variables[] = 'user_id'; + } + if (strstr((string)$q, '.formr$login_link') !== false) { + $variables[] = 'formr_login_link'; + } + if (strstr((string)$q, '.formr$nr_of_participants') !== false) { + $variables[] = 'formr_nr_of_participants'; + } + if (strstr((string)$q, '.formr$session_last_active') !== false) { + $variables[] = 'formr_session_last_active'; + } + + return $variables; +} + +function pre_htmlescape($str) { + $str = (string) $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; +} + +function shutdown_formr_org() { + $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"]; + $code = strtoupper(AnimalName::haikunate()); + + $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 $code"; + + formr_log("$msg \n $errstr", $code); + formr_error(500, 'Internal Server Error', nl2br($msg), 'Fatal Error'); + } +} + +function remove_tag_wrapper($text, $tag = 'p') { + $text = trim((string)$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']); + } +} + +/** + * Hackathon to dwnload an excel sheet from google + * + * @param string $survey_name + * @param string $google_link The URL of the Google Sheet + * @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_file_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; +} + +/** + * preg-match the Google sheet ID from the google sheet link + * + * @param string $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; +} + +/** + * Returns the google sheet link given ID + * + * @param string $id + * @return string + */ +function google_get_sheet_link($id) { + 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; +} + +function fill_array($array, $value = '') { + 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 (sha1_file($a) !== sha1_file($b)) + return false; + + return true; +} + +function create_zip_archive($files, $destination, $overwrite = false) { + $zip = new ZipArchive(); + + 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(); + + //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); +} + +function deletefiles($files) { + 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}"); + } +} + +function get_assets() { + return get_default_assets('assets'); +} + +function print_stylesheets($files, $id = null) { + 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"; + } +} + +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))); + } + } +} + +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, (string)$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; + } +} + +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'); +} + +function knitting_needed($source) { + if (!$source) { + return false; + } + + if (mb_strpos($source, '`r ') !== false || mb_strpos($source, '```{r') !== false) { + return true; + } + + return false; +} + +function get_db_non_user_tables() { + return [ + '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"), + ]; +} + +function get_db_non_session_tables() { + return ['survey_users', 'survey_run_sessions', 'survey_unit_sessions']; +} + +function formr_check_maintenance() { + $ip = env('REMOTE_ADDR'); + + if (Config::get('in_maintenance') && !in_array($ip, Config::get('maintenance_ips', []))) { + formr_error(503, 'Service Unavailable', 'This website is currently undergoing maintenance. Please try again later.', 'Maintenance Mode', false); + } +} + +function formr_in_console() { + return php_sapi_name() === 'cli'; +} + +function formr_search_highlight($search, $subject) { + return str_replace($search, ''.$search.'', $subject); +} diff --git a/application/Helper/ApiHelper.php b/application/Helper/ApiHelper.php index 2e29f79e5..334550a8a 100644 --- a/application/Helper/ApiHelper.php +++ b/application/Helper/ApiHelper.php @@ -2,333 +2,314 @@ 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, - ); + /** + * @var DB + */ + protected $db; + + /** + * @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->db = $db; + $this->request = $request; + $this->user = OAuthHelper::getInstance()->getUserByAccessToken($this->request->getParam('access_token')); + } + + public function results() { + ini_set('memory_limit', Config::get('memory_limit.run_get_data')); + + // 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; + } + + // If sessions are still not available then run is empty + if (!$this->db->count('survey_run_sessions', array('run_id' => $run->id), 'id')) { + $this->setData(Response::STATUS_NOT_FOUND, 'Empty Run', null, 'No sessions were found in this run.'); + 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) { + $surveys[] = (object) array( + 'name' => $survey['name'], + 'items' => null, + ); + } + } + + // 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); + } + + $results = array(); + foreach ($surveys as $s) { + $results[$s->name] = $this->getSurveyResults($run, $s->name, $s->items, $requested_run->sessions); + } + + $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(null, $run); + $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($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($session_code, null); + + if ($run_session->id) { + $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($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(Run $run, $survey_name, $survey_items = null, $sessions = null) { + $results = array(); + $query = $query = $this->buildSurveyResultsQuery($run, $survey_name, $survey_items, $sessions); + $stmt = $this->db->query($query, true); + + while (($row = $stmt->fetch(PDO::FETCH_ASSOC))) { + $session_id = $row['session_id']; + if (!isset($results[$session_id])) { + $results[$session_id] = array( + 'session' => $row['run_session'], + 'created' => $row['created'], + 'current_position' => $row['current_position' + ]); + } + if ($row['created'] && !$results[$session_id]['created']) { + $results[$session_id]['created'] = $row['created']; } - } - - // 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); - } + $results[$session_id][$row['item_name']] = $row['answer']; + } + + return array_values($results); + } + + private function buildSurveyResultsQuery(Run $run, $survey_name, $survey_items = null, $sessions = null) { + $params = array( + 'run_id' => $run->id, + 'user_id' => $this->user->id, + 'survey_name' => $this->db->quote($survey_name), + 'WHERE_survey_items' => null, + 'WHERE_run_sessions' => null, + ); + + $q = ' + SELECT itms_display.item_id, itms_display.session_id, itms_display.answer, itms_display.created, + survey_items.name AS item_name, survey_run_sessions.session AS run_session, survey_run_sessions.position AS current_position + FROM survey_items_display AS itms_display + LEFT JOIN survey_unit_sessions ON survey_unit_sessions.id = itms_display.session_id + LEFT JOIN survey_run_sessions ON survey_run_sessions.id = survey_unit_sessions.run_session_id + LEFT JOIN survey_items ON survey_items.id = itms_display.item_id + LEFT JOIN survey_studies ON survey_studies.id = survey_items.study_id + WHERE survey_studies.name = %{survey_name} + AND survey_studies.user_id = %{user_id} + AND survey_run_sessions.run_id = %{run_id} + %{WHERE_survey_items} + %{WHERE_run_sessions} + '; + + if ($survey_items && is_string($survey_items)) { + $itms = array(); + foreach (explode(',', $survey_items) as $itm) { + $itms[] = $this->db->quote(trim($itm)); + } + $params['WHERE_survey_items'] = ' AND survey_items.name IN (' . implode(',', $itms) . ') '; + } + + if ($sessions && is_array($sessions)) { + $or_like = array(); + foreach ($sessions as $session) { + $or_like[] = " survey_run_sessions.session LIKE '{$session}%' "; + } + $params['WHERE_run_sessions'] = ' AND (' . implode('OR', $or_like) . ') '; + } + + return Template::replace($q, $params); + } } diff --git a/application/Helper/GearmanWorkerHelper.php b/application/Helper/GearmanWorkerHelper.php deleted file mode 100644 index 9cc403b9d..000000000 --- a/application/Helper/GearmanWorkerHelper.php +++ /dev/null @@ -1,248 +0,0 @@ -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); - } -} - -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); - } -} diff --git a/application/Helper/OAuthHelper.php b/application/Helper/OAuthHelper.php index 162dfa528..a65437447 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($user_id, null); + } + + /** + * 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..dd0ac782f 100644 --- a/application/Helper/RunHelper.php +++ b/application/Helper/RunHelper.php @@ -2,130 +2,362 @@ 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(Run $run, DB $db, Request $r) { + $this->request = $r; + $this->db = $db; + $this->run = $run; + $this->run_name = $run->name; + + if (!$this->run->valid) { + throw new Exception("Run with name {$run} not found"); + } + + if ($this->request->session) { + $this->runSession = new RunSession($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() { + $emailSession = $this->run->getReminderSession($this->request->reminder_id, $this->request->session, $this->request->run_session_id); + if ($emailSession->execute() === false) { + $this->errors[] = 'Something went wrong with the reminder. in run ' . $this->run->name; + return false; + } + + $this->message = 'Reminder sent'; + return true; + } + + public function nextInRun() { + if ($this->runSession->currentUnitSession && !$this->runSession->currentUnitSession->end('moved')) { + $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($session, $run); + + $unit_session = $run_session->getCurrentUnitSession(); + 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; + } + + public function getUserOverviewTable($queryParams, $page = null) { + ini_set('memory_limit', Config::get('memory_limit.run_get_data')); + + $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_run_sessions`.current_unit_session_id, + `survey_runs`.name AS run_name, + `survey_units`.type AS unit_type, + `survey_run_sessions`.last_access, + `us`.result, + `us`.result_log, + `us`.expires + 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 + LEFT JOIN `survey_unit_sessions` us ON `survey_run_sessions`.current_unit_session_id = `us`.id + WHERE {$where} + ORDER BY `survey_run_sessions`.session != :admin_code, `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`.current_unit_session_id, + `survey_run_sessions`.last_access, + `us`.result, + `us`.result_log, + `us`.expires + 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 + LEFT JOIN `survey_unit_sessions` us ON `survey_run_sessions`.current_unit_session_id = `us`.id + WHERE `survey_run_sessions`.run_id = :run_id ORDER BY `survey_run_sessions`.session != :admin_code,`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, + `survey_unit_sessions`.expires, + `survey_unit_sessions`.`queued`, + `survey_unit_sessions`.result, + `survey_unit_sessions`.result_log + 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_unit_sessions`.id AS session_id, + `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`.`status`, + `survey_email_log`.subject, + `survey_email_log`.sent, + `survey_email_log`.created, + `survey_unit_sessions`.result, + `survey_unit_sessions`.result_log, + `survey_run_units`.position AS position_in_run + FROM `survey_email_log` + LEFT JOIN `survey_run_units` ON `survey_email_log`.email_id = `survey_run_units`.unit_id + LEFT JOIN `survey_email_accounts` ON `survey_email_log`.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/SurveyHelper.php b/application/Helper/SurveyHelper.php deleted file mode 100644 index c0477e565..000000000 --- a/application/Helper/SurveyHelper.php +++ /dev/null @@ -1,622 +0,0 @@ -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 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 = '
- Page %{page}/%{max_page} - -
-
- %{buttons} -
-
- '; - $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/Helper/UserHelper.php b/application/Helper/UserHelper.php new file mode 100644 index 000000000..1160b67cf --- /dev/null +++ b/application/Helper/UserHelper.php @@ -0,0 +1,72 @@ +count('survey_users'); + $pagination = new Pagination($count, 200, true); + $limits = $pagination->getLimits(); + $where = " "; + if (!empty($params['email'])) { + $where = " WHERE email LIKE '%{$params['email']}%' "; + } + + $itemsQuery = " + SELECT + `survey_users`.id, + `survey_users`.created, + `survey_users`.modified, + `survey_users`.email, + `survey_users`.admin, + `survey_users`.email_verified + FROM `survey_users` + {$where} + ORDER BY `survey_users`.id ASC LIMIT $limits + "; + + $stmt = DB::getInstance()->prepare($itemsQuery); + $stmt->execute(); + + return array( + 'search_email' => $params['email'] ?? '', + '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/Library/AnimalName.php b/application/Library/AnimalName.php deleted file mode 100644 index 637260b98..000000000 --- a/application/Library/AnimalName.php +++ /dev/null @@ -1,600 +0,0 @@ -findFile($class)) { - if (class_exists($class, false)) { - return true; - } - - 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; - } - - /** - * 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__) . '../../') . '/'); - } - - if (!defined('APPLICATION_PATH')) { - define('APPLICATION_PATH', APPLICATION_ROOT . 'application/'); - } - - $class = $this->classNameToPath($class); - $libraryPath = APPLICATION_PATH . "Library/{$class}.php"; - $modelPath = APPLICATION_PATH . "Model/{$class}.php"; - - 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; - } - - if (!empty($file)) { - return $file; - } - return false; - } - - 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; - } - -} - -return Autoload::getLoader(); diff --git a/application/Library/CURL.php b/application/Library/CURL.php deleted file mode 100644 index 709b7113d..000000000 --- a/application/Library/CURL.php +++ /dev/null @@ -1,389 +0,0 @@ - 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; - } - -} diff --git a/application/Library/Cache.php b/application/Library/Cache.php deleted file mode 100644 index 0549ea568..000000000 --- a/application/Library/Cache.php +++ /dev/null @@ -1,80 +0,0 @@ - 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 = $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) { - $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, - ); - } - -} diff --git a/application/Library/Deamon.php b/application/Library/Deamon.php deleted file mode 100644 index 0ccce643f..000000000 --- a/application/Library/Deamon.php +++ /dev/null @@ -1,232 +0,0 @@ -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 deleted file mode 100644 index fd1ace8fc..000000000 --- a/application/Library/EmailQueue.php +++ /dev/null @@ -1,353 +0,0 @@ -db = $db; - $this->loopInterval = Config::get('email.queue_loop_interval', 5); - $this->itemTtl = Config::get('email.queue_item_ttl', 20 * 60); - $this->itemTries = Config::get('email.queue_item_tries', 4); - // 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'); - } - } - - /** - * - * @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 - 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); - } - - /** - * - * @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", "/"); - - $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'; - } - $mail->Username = $account['username']; - $mail->Password = $account['password']; - - $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]; - } - - 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; - } - - 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; - - $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'])); - - $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')) { - self::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))) { - self::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, - )); - self::dbg("Send Success. \n {$debugInfo}"); - } else { - throw new Exception($mailer->ErrorInfo); - } - } catch (Exception $e) { - //formr_log_exception($e, 'EmailQueue ' . $debugInfo); - self::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; - } - - /** - * 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); - } - } - } - } - - 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; - } - - 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') . ' Email-Queue: ' . $str . PHP_EOL; - if (DEBUG) { - echo $str; - return; - } - return error_log($str, 3, get_log_file('email-queue.log')); - } - - /** - * 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()); - 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"); - } - break; - } - } - - public function run($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 - self::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; - } - - self::dbg($e->getMessage() . "[" . $error_code . "]"); - - self::dbg("Unable to connect. waiting 5 seconds before reconnect."); - sleep(5); - } - } - } - -} diff --git a/application/Library/Functions.php b/application/Library/Functions.php deleted file mode 100644 index 5add2ae15..000000000 --- a/application/Library/Functions.php +++ /dev/null @@ -1,1654 +0,0 @@ -' . $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)); - } -} - -function get_log_file($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); -} - -function notify_user_error($error, $public_message = '') { - $run_session = Site::getInstance()->getRunSession(); - $date = date('Y-m-d H:i:s'); - - $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'); -} - -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'); - - $message = $date . ': ' . $public_message . "
"; - - $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; -} - -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; -} - -function access_denied() { -/* - 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'); -} - -function bad_request() { -/* - 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'); -} - -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; -} - -function json_header() { - header('Content-Type: application/json'); -} - -function is_ajax_request() { - return strtolower(env('HTTP_X_REQUESTED_WITH')) === 'xmlhttprequest'; -} - -function h($text) { - return htmlspecialchars($text); -} - -function debug($string) { - 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)); - } -} - -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; - } -} - -if (!function_exists('_')) { - - 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; -} - -function used_cache($echo = false) { - 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; -} - -if (!function_exists('__')) { - - /** - 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); - } - -} - -if (!function_exists('__n')) { - - /** - 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); - } - -} - -function endsWith($haystack, $needle) { - $length = strlen($needle); - if ($length == 0) { - return true; - } - - return (mb_substr($haystack, -$length) === $needle); -} - -/** - * Gets an environment variable from available sources, and provides emulation - * for unsupported or inconsistent environment variables (i.e. DOCUMENT_ROOT on - * IIS, or SCRIPT_NAME in CGI mode). Also exposes some additional custom - * environment information. - * - * @param string $key Environment variable name. - * @return string Environment variable setting. - * @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; -} - -function emptyNull(&$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"; - } - - return $x; -} - -function hardTrueFalse($x) { - if ($x === false) { - return 'FALSE'; - } elseif ($x === true) { - return 'TRUE'; -# elseif($x===null) return 'NULL'; - } elseif ($x === 0) { - return '0'; - } - - 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; - } - -} - -/** - * Format a timestamp to display its age (5 days ago, in 3 days, etc.). - * - * @param int $timestamp - * @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); - } -} - -// 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]; -} - -function cr2nl($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; -} - -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; -} - -function base64_url_encode($data) { - return strtr(base64_encode($data), '+/=', '-_~'); -} - -function base64_url_decode($data) { - return base64_decode(strtr($data, '-_~', '+/=')); -} - -/** - * Create URL Title - * - * Takes a "title" string as input and creates a - * human-friendly URL string with a "separator" string - * as the word separator. - * - * @param string the string - * @param string the separator - * @param strin $lowercase Should string be returned in lowecase letters - * @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); -} - -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; -} - -/** - * Return an array of contents in the run export directory - * - * @param string $dir Absolute path to readable directory - * @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; -} - -/** - * Get the mime type of a file given filename using FileInfo - * @see http://php.net/manual/en/book.fileinfo.php - * - * @param string $filename - * @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; - } - - $mime_type = $mime[0]; - return $mime_type; -} - -/** - * Send a file for download to client - * - * @param string $file Absolute path to file - * @param boolean $unlink - * @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); -} - -/** - * @deprecated - */ -function get_duplicate_update_string($columns) { - foreach ($columns as $i => $column) { - $column = trim($column, '`'); - $columns[$i] = "`$column` = VALUES(`$column`)"; - } - return $columns; -} - -/** - * Returns a valid MySQL datetime string - * - * @param int $time [optional] Valid unix timestamp - * @return string - */ -function mysql_datetime($time = null) { - if ($time === null) { - $time = time(); - } - return date('Y-m-d H:i:s', $time); -} - -/** - * Returns a string equivalent to MySQL's NOW() function - * - * @return string - */ -function mysql_now() { - return mysql_datetime(); -} - -/** - * Returns formatted strings equivalent to expressions like NOW() + INTERVAL 2 DAY - * - * @param string A string defining an interval accepted by PHP's strtotime() function - * @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); -} - -function site_url($uri = '', $params = array()) { - $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); -} - -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; -} - -function admin_study_url($name = '', $action = '', $params = array()) { - 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); -} - - -/** -* 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); -} - -function monkeybar_url($run_name, $action = '', $params = array()) { - return run_url($run_name, 'monkey-bar/' . $action, $params); -} - -function array_to_accordion($array) { - $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); - - $acc .= ' -
- -
-
- ' . $content . ' -
-
-
'; - $first = ''; - endforeach; - - $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; -} - -function is_formr_truthy($value) { - $value = (string) $value; - return $value || $value === '0'; -} - -/** - * Convert an array of data into variables for OpenCPU request - * The array parameter if it contains an entry called 'datasets', then these will be passed as R dataframes and other key/value pairs will be passed as R variables - * - * @param array $data - * @param string $context - * @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) -'; - 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 . ' -'; - } - return $vars; -} - -/** - * Execute a piece of code against OpenCPU - * - * @param string $location A previous openCPU session location - * @param string $return_format String like 'json' - * @param mixed $context If this paramter is set, $code will be evaluated with a context - * @param bool $return_session Should OpenCPU_Session object be returned - * @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; - } -} - -/** - * Execute a piece of code against OpenCPU - * - * @param string $code Each code line should be separated by a newline characted - * @param string|array $variables An array or string (separated by newline) of variables to be used in OpenCPU request - * @param string $return_format String like 'json' - * @param mixed $context If this paramter is set, $code will be evaluated with a context - * @param bool $return_session Should OpenCPU_Session object be returned - * @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' => '{ -(function() { - library(formr) - ' . $variables . ' - ' . $code . ' -})() }'); - - $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; - } -} - - -/** - * In one common, well-defined case, we just skip calling openCPU - * - * @param string code - * @param array data for openCPU - * @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]; - } - } - - return null; -} - - -/** - * Call knit() function from the knitr R package - * - * @param string $code - * @param string $return_format - * @param bool $return_session Should OpenCPU_Session object be returned - * @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; - } -} - - -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} -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.'") -' . $variables . ' -``` -' . -$source; - - return opencpu_knit($source, 'json', 0, $return_session); -} - -/** - * knit R markdown to html - * - * @param string $source - * @param string $return_format - * @param int $self_contained - * @param bool $return_session Should OpenCPU_Session object be returned - * @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; - } -} - - -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} -library(knitr); library(formr) -opts_chunk$set(warning='. $show_errors .',message='. $show_errors .',error=T,echo=F,fig.height=7,fig.width=10) -' . $variables . ' -``` - -'. -$description .' - - -' . -$source . -" - - - -#   - -" . $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; - } -} - -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} -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.'") -' . $variables . ' -``` -' . -$source; - - 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} -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.'") -' . $variables . ' -``` -' . -$source; - - 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} -library(knitr); library(formr) -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; - - return opencpu_knit2html($source, $return_format, 0, $return_session); -} - -function opencpu_string_key($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; -} - -/** - * Parse a bulk of strings in ocpu - * - * @param Survey $survey Current survey containing strings that are beigin parsed - * @param array $string_templates An array of strings to be parsed - * @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)); - } -} - -/** - * Substitute parsed strings in the collection of items that were sent for parsing - * This function does not return anything as the collection of items is passed by reference - * For objects having the property 'label_parsed', they are checked and substituted - * - * @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_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"; - - $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"; - - $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'] = ' - Download R Markdown file to debug.
- '; - } elseif(isset($params['x'])) { - $debug['R Code'] = ' - Download R code file to debug.
- '; - } - if ($session->hasError()) { - $debug['Response'] = pre_htmlescape($session->getError()); - } else { - if($session->getFiles("knit.html")) { - $iframesrc = $session->getFiles("knit.html")['knit.html']; - $debug['Response'] = ' -

- 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); -} - -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')); -} - -function pre_htmlescape($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; -} - -function shutdown_formr_org() { - $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"]; - - $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'); - } -} - -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; -} - -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']); - } -} - -/** - * Hackathon to dwnload an excel sheet from google - * - * @param string $survey_name - * @param string $google_link The URL of the Google Sheet - * @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; -} - -/** - * preg-match the Google sheet ID from the google sheet link - * - * @param string $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; -} - -/** - * Returns the google sheet link given ID - * - * @param string $id - * @return string - */ -function google_get_sheet_link($id) { - 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; -} - -function fill_array($array, $value = '') { - 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 (sha1_file($a) !== sha1_file($b)) - return false; - - return true; -} - -function create_zip_archive($files, $destination, $overwrite = false) { - $zip = new ZipArchive(); - - 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(); - - //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); -} - -function deletefiles($files) { - 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}"); - } -} - -function get_assets() { - return get_default_assets('assets'); -} - -function print_stylesheets($files, $id = null) { - 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"; - } -} - -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))); - } - } -} - -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; -} diff --git a/application/Library/LogParser.php b/application/Library/LogParser.php deleted file mode 100644 index 28c3477a2..000000000 --- a/application/Library/LogParser.php +++ /dev/null @@ -1,101 +0,0 @@ -'; - - const LOG_MARKER_ALERTS_END = ''; - - const LOG_MARKER_START_GM = 'Processing run >>>'; - - 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 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; - } - - 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); - } - } - - 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 deleted file mode 100644 index e2fd5f512..000000000 --- a/application/Library/OSF.php +++ /dev/null @@ -1,390 +0,0 @@ - $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; - } -} - -class OSF_Exception extends Exception {} - diff --git a/application/Library/OpenCPU.php b/application/Library/OpenCPU.php deleted file mode 100644 index 27b48f2f3..000000000 --- a/application/Library/OpenCPU.php +++ /dev/null @@ -1,523 +0,0 @@ -==========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); - } - - 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 - * @return array - */ - public function getFiles($match = '/files/') { - 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] = $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; - } - - 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 {} diff --git a/application/Library/Request.php b/application/Library/Request.php deleted file mode 100644 index a79d06781..000000000 --- a/application/Library/Request.php +++ /dev/null @@ -1,213 +0,0 @@ - $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 deleted file mode 100644 index dd1cf325b..000000000 --- a/application/Library/Response.php +++ /dev/null @@ -1,278 +0,0 @@ -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 deleted file mode 100644 index ffd7a688c..000000000 --- a/application/Library/Router.php +++ /dev/null @@ -1,206 +0,0 @@ -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'); - } - } - - /** - * @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; - } - - 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); - } - - /** - * @return Router - */ - public static function getInstance() { - if (self::$instance === null) { - global $site; - self::$instance = new self($site); - } - return self::$instance; - } - - public static function getWebRoot() { - return Config::get('web_dir'); - } - - public static function isWebRootDir($name) { - return is_dir(self::getWebRoot() . '/' . $name); - } -} - diff --git a/application/Library/Session.php b/application/Library/Session.php deleted file mode 100644 index a88d53395..000000000 --- a/application/Library/Session.php +++ /dev/null @@ -1,106 +0,0 @@ - $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(); - session_destroy(); - } - - 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; - } -} - - diff --git a/application/Library/Template.php b/application/Library/Template.php deleted file mode 100644 index 638501186..000000000 --- a/application/Library/Template.php +++ /dev/null @@ -1,67 +0,0 @@ -'; + const LOG_MARKER_ALERTS_END = ''; + const LOG_MARKER_START_GM = 'Processing run >>>'; + const LOG_COMMENT = '....'; + + 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 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 || strstr($line, self::LOG_COMMENT)) { + 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); + } + } + + 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/Model/Branch.php b/application/Model/Branch.php deleted file mode 100644 index c4254e8ce..000000000 --- a/application/Model/Branch.php +++ /dev/null @@ -1,163 +0,0 @@ -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 = '
-
- -
- else - - go on -
'; - $dialog .= ' -

- Save - Test -

'; - - $dialog = $prepend . $dialog; - - return parent::runDialog($dialog); - } - - public function removeFromRun($special = null) { - return $this->delete($special); - } - - public function test() { - $results = $this->getSampleSessions(); - - if (!$results) { - return false; - } - - $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) - foreach ($results as $row) { - $this->run_session_id = $row['id']; - $opencpu_vars = $this->getUserDataInRun($this->condition); - $eval = opencpu_evaluate($this->condition, $opencpu_vars); - - echo " - - - "; - } - - echo '
Code (Position)Test
" . $row['session'] . " ({$row['position']})" . stringBool($eval) . "
'; - $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! - } - - $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 deleted file mode 100644 index de9bb9a87..000000000 --- a/application/Model/Email.php +++ /dev/null @@ -1,505 +0,0 @@ -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 = $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 - 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 = '') { - $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 = $prepend . $dialog; - return parent::runDialog($dialog, 'fa-envelope'); - } - - 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 - LEFT JOIN survey_items_display ON survey_items_display.session_id = survey_unit_sessions.id - LEFT JOIN survey_items ON survey_items.id = survey_items_display.item_id - WHERE - survey_unit_sessions.run_session_id = :run_session_id AND - survey_run_units.run_id = :run_id AND - survey_items.type = 'email' - ORDER BY survey_items_display.answered DESC - 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) || - (Site::getCurrentUser()->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 - 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, - SUM(created > DATE_SUB(NOW(), INTERVAL 1 DAY)) AS in_last_1d, - 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; - } - global $user; - - $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(); - endif; - 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.

"; - endif; - - $results = $this->getSampleSessions(); - if ($results) { - - if ($this->recipient_field === null OR trim($this->recipient_field) == '') { - $this->recipient_field = 'survey_users$email'; - } - - $output = ' - - - - - - - - %s -
Code (Position)Test
'; - - $rows = ''; - 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; - - echo sprintf($output, $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 eb0280934..c14809963 100644 --- a/application/Model/EmailAccount.php +++ b/application/Model/EmailAccount.php @@ -1,130 +1,147 @@ 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 +use PHPMailer\PHPMailer\PHPMailer; + +class EmailAccount extends Model { + + public $id = null; + public $user_id = null; + public $valid = null; + public $account = array(); + + /** + * @var DB + */ + private $dbh; + + const AK_GLUE = ':fmr:'; + + public function __construct($id, $user_id) { + parent::__construct(); + + $this->id = (int) $id; + $this->user_id = (int) $user_id; + + if ($id) { + $this->load(); + } + } + + protected function load() { + $this->account = $this->db->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->db->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, `status` = 0 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'; - } - $mail->SMTPAuth = true; // turn on SMTP authentication - $mail->Username = $this->account['username']; // SMTP username - $mail->Password = $this->account['password']; // SMTP password - - $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->db->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_replace('email/test-account.ftpl', array('site_url' => site_url())); + + if (!$mail->Send()) { + $this->invalidate(); + alert('Account Test Failed: ' . $mail->ErrorInfo, 'alert-danger'); + return false; + } else { + $this->validate(); + 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 invalidate() { + return $this->db->update('survey_email_accounts', array('status' => -1), array('id' => $this->id)); + } + + public function validate() { + return $this->db->update('survey_email_accounts', array('status' => 1), array('id' => $this->id)); + } + + public function delete() { + return $this->db->update('survey_email_accounts', array('deleted' => 1), array('id' => $this->id)); + } } diff --git a/application/Model/External.php b/application/Model/External.php deleted file mode 100644 index 300220085..000000000 --- a/application/Model/External.php +++ /dev/null @@ -1,183 +0,0 @@ -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 = '

-

-

-

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; - - return parent::runDialog($dialog, 'fa-external-link-square'); - } - - 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 = ''.$this->address.""; - } - - $this->session = "TESTCODE"; - echo $this->makeAddress($output); - } - - private function hasExpired() { - $expire = $this->expire_after; - if ($expire === 0) { - return false; - } else { - $last = $this->run_session->unit_session->created; - if (!$last) { - 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 - if ($this->called_by_cron) { - if ($this->hasExpired()) { - $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 deleted file mode 100644 index bd8ae1e5f..000000000 --- a/application/Model/Item.php +++ /dev/null @@ -1,716 +0,0 @@ -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'; - } - -} - -/** - * HTML Item - * The default item is a text input, as many browser render any input type they don't understand as 'text'. - * 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 .= ' -
-
- %{input_group_open} - %{prepended} %{input} %{appended} - %{input_group_close} -
-
- '; - - $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 = ' - - - - - - - '; - 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. - * @deprecated This method will be moved to HtmlHelper in 3.0 - */ - 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. - * @deprecated This method will be moved to HtmlHelper in 3.0 - */ - 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..96eab81cd 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 f0904ccb5..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..52426f0ab 100644 --- a/application/Model/Item/Get.php +++ b/application/Model/Item/Get.php @@ -2,43 +2,58 @@ 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); + $value = $request->getParam($this->get_var); + + if ($value !== null && $value !== "") { + $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 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 = 'error (missing GET param): ' . $this->label_parsed; + $reply = null; + } elseif ($this->optional && $reply == '') { + $reply = null; + } + + return $reply; + } + + 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 6dce1db82..eb61140f0 100644 --- a/application/Model/Item/Ip.php +++ b/application/Model/Item/Ip.php @@ -2,22 +2,34 @@ 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'] = $this->getIp(); + } - public function getReply($reply) { - return isset($_SERVER["REMOTE_ADDR"]) ? $_SERVER["REMOTE_ADDR"] : null; - } + public function getReply($reply) { + return $this->getIp(); + } + + public function render() { + return $this->render_input(); + } + + protected function getIp() { + if (!empty($_SERVER['HTTP_CLIENT_IP'])) { + $ip = $_SERVER['HTTP_CLIENT_IP']; + } elseif (!empty($_SERVER['HTTP_X_FORWARDED_FOR'])) { + $ip = $_SERVER['HTTP_X_FORWARDED_FOR']; + } else { + $ip = env('REMOTE_ADDR'); + } - public function render() { - return $this->render_input(); + return $ip; } } diff --git a/application/Model/Item/Item.php b/application/Model/Item/Item.php new file mode 100644 index 000000000..6f2d7a8b4 --- /dev/null +++ b/application/Model/Item/Item.php @@ -0,0 +1,713 @@ +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'; + } + +} + +/** + * HTML Item + * The default item is a text input, as many browser render any input type they don't understand as 'text'. + * 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); + $this->js_showif = preg_replace("/\s*is.na\(([a-zA-Z0-9_'\"]+)\)/", "(typeof($1) === 'undefined')", $this->js_showif); + + if ($this->showif && 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_]+$/', (string)$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(" ", (string)$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} + %{prepended} %{input} %{appended} + %{input_group_close} +
+
+ '; + + $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 = ' + + + + + + + '; + 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($vars = [], $context = null) { + return $this->label_parsed === null; + } + + public function getShowIf() { + if ($this->showif && strstr($this->showif, "//js_only") !== false) { + return "NA"; + } + + if ($this->hidden !== null) { + return false; + } + if (trim((string)$this->showif) != "") { + return $this->showif; + } + return false; + } + + public function needsDynamicValue() { + $this->value = trim((string) $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(SurveyStudy $survey) { + + } + + public function getValue(SurveyStudy $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/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..596a6d158 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 8b51d7a80..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..e08fb5ce5 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($vars = [], $context = null) { + + $ocpu_session = opencpu_knit_iframe($this->label, $vars, true, $context); + 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..a6537bddf 100644 --- a/application/Model/Item/Number.php +++ b/application/Model/Item/Number.php @@ -5,121 +5,126 @@ * - 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; + } + + protected function chooseResultFieldBasedOnChoices() { + } + + 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) { + if ($reply === null) { + return null; + } + $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..4665bc2c8 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, 'min' => 0, 'max' => 10000000); + 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 793194a75..2f10ed2b8 100644 --- a/application/Model/Item/RangeTicks.php +++ b/application/Model/Item/RangeTicks.php @@ -5,26 +5,40 @@ */ class RangeTicks_Item extends Number_Item { - public $type = 'range_ticks'; - public $input_attributes = array('type' => 'range', 'step' => 1); + public $type = 'range_ticks'; + public $input_attributes = array('type' => 'range', 'step' => 1, 'min' => 0, 'max' => 100); + 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'); + } - 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')); - protected function render_input() { - $tpl = ' + // 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[(string)$i] = $i; + } + } + + protected function render_input() { + $tpl = ' %{left_label} @@ -35,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); - } - - return Template::replace($tpl, array( - 'left_label' => $this->render_pad_label(1, 'right'), - 'input_attributes' => self::_parseAttributes($this->input_attributes, array('required')), - 'id' => $this->id, - 'options' => $options, - '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 ''; - } + $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, + )); + } + + 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 fced1603d..c0ef44ffc 100644 --- a/application/Model/Item/RatingButton.php +++ b/application/Model/Item/RatingButton.php @@ -2,59 +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 - if ($this->step <= 0 || $this->step > $this->upper_limit) { - $this->step = $this->upper_limit; - } - - $choices = @range($this->lower_limit, $this->upper_limit, $this->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 = ' @@ -62,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 20f2fee54..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..0f54f3b71 100644 --- a/application/Model/Item/Timezone.php +++ b/application/Model/Item/Timezone.php @@ -2,53 +2,61 @@ 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; + } + + public function getReply($reply) { + if (isset($this->choices[$reply])) { + $reply = $this->choices[$reply]; + } + + return $reply; + } + + 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/Model.php b/application/Model/Model.php new file mode 100644 index 000000000..7252774fd --- /dev/null +++ b/application/Model/Model.php @@ -0,0 +1,103 @@ +boot(); + } + + public function boot() { + $this->db = DB::getInstance(); + $this->cron = Site::runningInConsole(); + } + + protected function assignProperties($props) { + if ($props && is_array($props)) { + foreach ($props as $prop => $value) { + if (property_exists($this, $prop)) { + if ($value === '') { + $value = null; + } + $this->{$prop} = $value; + } + } + } + + return $props; + } + + public function save() { + $data = $this->toArray(); + if ($data) { + if ($this->db->entry_exists($this->table, ['id' => $this->id])) { + return $this->db->update($this->table, $data, ['id' => $this->id]); + } else { + return $this->db->insert($this->table, $data); + } + } + } + + public function update($data) { + $this->assignProperties($data); + $this->save(); + } + + public function delete() { + $this->db->delete($this->table, ['id' => $this->id]); + } + + protected function toArray() { + return []; + } + + public function getDbConnection() { + return $this->db; + } + + public function isCron() { + return $this->cron; + } + + public function refresh($options) { + if (!$this->table) { + return null; + } + + $row = $this->db->findRow($this->table, $options); + if ($row) { + $this->assignProperties($row); + return $this; + } + + return null; + } +} diff --git a/application/Model/Page.php b/application/Model/Page.php deleted file mode 100644 index a5c0fdf7e..000000000 --- a/application/Model/Page.php +++ /dev/null @@ -1,124 +0,0 @@ -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->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 = - // '

'. - '

'; -# '

'; - $dialog .= '

Save - Test

'; - - - $dialog = $prepend . $dialog; - - return parent::runDialog($dialog, 'fa-stop fa-1-5x'); - } - - 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 - endif; - - $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( -// 'title' => $this->title, - 'body' => $body, - ); - } - -} diff --git a/application/Model/Pagination.php b/application/Model/Pagination.php deleted file mode 100644 index 16d84aad7..000000000 --- a/application/Model/Pagination.php +++ /dev/null @@ -1,108 +0,0 @@ -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, - )); - } - -} diff --git a/application/Model/Pause.php b/application/Model/Pause.php deleted file mode 100644 index 428682257..000000000 --- a/application/Model/Pause.php +++ /dev/null @@ -1,319 +0,0 @@ -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 = '

    - - and - -

    -

    - and - -

    -
    - - - - - - -
    - -
    -

    - '; - $dialog .= '

    Save - Test

    '; - - - $dialog = $prepend . $dialog; - - return parent::runDialog($dialog, 'fa-pause'); - } - - 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() { - // 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) { - 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'; - } elseif ($relative_to === false) { - $conditions['relative_to'] = '0=1'; - } elseif (!is_array($relative_to) && strtotime($relative_to)) { - $conditions['relative_to'] = ':relative_to <= NOW()'; - $bind_relative_to = true; - } 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'); - 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; - } else { - alert("Pause {$this->position}: Relative to yields neither a date, nor a time. " . print_r($relative_to, true), 'alert-warning'); - 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; - } - - 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)) { - // 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) { - return false; - } - - $wait_datetime = $wait_date . ' ' . $wait_time; - $conditions['datetime'] = ':wait_datetime <= NOW()'; - } - - 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; - } - - return $result; - } - - 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); - } - 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())) { - - echo ' - - '; - if ($this->has_relative_to) { - echo ''; - } - echo ' - - '; - - foreach ($results AS $row): - $this->run_session_id = $row['id']; - - $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) . "
    '; - } - } - - 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 af9595354..c571acc2f 100644 --- a/application/Model/Run.php +++ b/application/Model/Run.php @@ -25,347 +25,372 @@ * social network (later) * lab date selector (later) */ -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; - 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", - ); - public $renderedDescAndFooterAlready = false; - - /** - * @var DB - */ - private $dbh; - - 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"; - $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->valid = true; - } - } - - 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, - '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 +class Run extends Model { + + 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', + ); + protected $description_parsed = null; + protected $footer_text_parsed = null; + protected $public_blurb_parsed = null; + protected $api_secret_hash = null; + protected $owner = null; + protected $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 RunSession + */ + public $activeRunSession; + + const TEST_RUN = 'formr-test-run'; + + public function __construct($name = null, $id = null) { + parent::__construct(); + + 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(); + } + + if ($id !== null) { + $this->id = (int) $id; + $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"; + $where = $this->id ? array('id' => $this->id) : array('name' => $this->name); + $vars = $this->db->findRow('survey_runs', $where, $columns); + + if ($vars) { + $this->assignProperties($vars); + $this->setExpireCookieUnits(); + $this->valid = true; + } + } + + public function getCronDues() { + $sessions = $this->db->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->db->update('survey_runs', array('name' => $name), array('id' => $this->id)); + return true; + } + + public function delete() { + try { + $this->db->delete('survey_runs', array('id' => $this->id)); + alert("Success. Successfully deleted run '{$this->name}'.", 'alert-success'); + return true; + } catch (Exception $e) { + formr_log_exception($e, __METHOD__); + 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'); + return false; + } + } + + public function deleteUnits() { + $this->db->delete('survey_run_special_units', array('run_id' => $this->id)); + $this->db->delete('survey_run_units', array('run_id' => $this->id)); + } + + public function togglePublic($public) { + if (!in_array($public, range(0, 3))) { + return false; + } + + $updated = $this->db->update('survey_runs', array('public' => $public), array('id' => $this->id)); + return $updated !== false; + } + + public function toggleLocked($on) { + $on = (int) $on; + $updated = $this->db->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->db->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->db->pdo()->lastInsertId(); + $this->name = $name; + $this->load(); + + // create default run service message + $props = RunUnit::getDefaults('ServiceMessagePage'); + $unit = RunUnitFactory::make($this, $props)->create(); + + return $name; + } + + public function getUploadedFiles() { + return $this->db->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->db->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->db->findValue('survey_uploaded_files', $where, 'new_file_path'); + $deleted = $this->db->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->db->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->db->select(array('id' => 'run_unit_id', 'unit_id', 'position')) + ->from('survey_run_units') + ->where(array('run_id' => $this->id)) + ->order('position') + ->fetchAll(); + } + + public function getFirstPosition() { + if ($units = $this->getAllUnitIds()) { + return $units[0]['position']; + } + } + + public function getNextPosition($current) { + $row = $this->db->select('position') + ->from('survey_run_units') + ->where(['run_id' => $this->id, 'position >' => $current]) + ->order('position') + ->limit(1) + ->fetch(); + + if ($row) { + return $row['position']; + } + + return null; + } + + public function getAllUnitTypes() { + $select = $this->db->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->db->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->user_id); + } + return $this->owner; + } + + public function getUserCounts() { + $g_users = $this->db->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, @@ -373,279 +398,236 @@ 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'; - endif; - if (isset($posted['public_blurb'])): - $posted['public_blurb_parsed'] = $parsedown->text($posted['public_blurb']); - $this->run_settings[] = 'public_blurb_parsed'; - endif; - if (isset($posted['footer_text'])): - $posted['footer_text_parsed'] = $parsedown->text($posted['footer_text']); - $this->run_settings[] = 'footer_text_parsed'; - endif; - - $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; - } - endif; - - $updates[$name] = $value; - endforeach; - - 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, - `survey_run_units`.position, - `survey_run_units`.description, - `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(" - `survey_run_special_units`.`id` AS unit_id, - `survey_run_special_units`.`run_id`, - `survey_run_special_units`.`description`, - `survey_units`.id, - `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 + $g_users->bindParam(':run_id', $this->id); + $g_users->execute(); + return $g_users->fetch(PDO::FETCH_ASSOC); + } + + public function emptySelf() { + $surveys = $this->getAllSurveys(); + foreach ($surveys as $survey) { + $survey['type'] = 'Survey'; + /* @var $unit Survey */ + $unit = RunUnitFactory::make($this, $survey); + if (!$unit->surveyStudy->backupResults()) { + alert('Could not backup results of survey ' . $unit->surveyStudy->name, 'alert-danger'); + return false; + } + } + $rows = $this->db->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; + } + + return RunUnitFactory::make($this, [ + 'special' => $xtype, + 'type' => $units[0]['type'], + 'id' => $units[0]['unit_id'], + ]); + } + + 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->db->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 getReminderSession($reminder_id, $session, $run_session_id) { + // create a unit_session here and get a session_id and pass it when making the unit + $runUnit = RunUnitFactory::make($this, ['id' => $reminder_id]); + $runSession = new RunSession($session, $this, ['id' => $run_session_id]); + $runSession->createUnitSession($runUnit, false); + + return $runSession->currentUnitSession; + } + + 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((string)$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->db->update('survey_runs', $updates, array('id' => $this->id)); + } + + if (!in_array(false, $successes)) { + return true; + } + + return false; + } + + public function getAllSurveys() { + // first, generate a master list of the search set (all the surveys that are part of the run) + return $this->db->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->db->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')); + + $collect = $this->db->prepare("SELECT `survey_studies`.name AS survey_name, `survey_run_units`.position AS unit_position, `survey_unit_sessions`.id AS unit_session_id, @@ -673,21 +655,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->db->prepare("SELECT `survey_run_sessions`.session, `survey_unit_sessions`.id AS session_id, `survey_runs`.name AS run_name, @@ -706,276 +688,272 @@ 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(); - - if (!$output): - $output['title'] = 'Finish'; - $output['body'] = " + $g_users->bindParam(':run_id', $this->id); + $g_users->execute(); + return $g_users; + } + + public function isStudyTest() { + return $this->name === self::TEST_RUN; + } + + private function testStudy() { + if (!($data = Session::get('test_study_data'))) { + formr_error(404, 'Not Found', 'Nothing to Test-Drive'); + } + + if (isset($data['unit_id'])) { + $data['id'] = $data['unit_id']; + } + + $runUnit = (new Survey($this, $data))->load(); + $runSession = RunSession::getTestSession($this); + if (!isset($data['unit_session_id'])) { + $runSession->createUnitSession($runUnit); + $data['unit_session_id'] = $runSession->currentUnitSession->id; + Session::set('test_study_data', $data); + } else { + $unitSession = new UnitSession($runSession, $runUnit, ['id' => $data['unit_session_id'], 'load'=> true]); + $runSession->currentUnitSession = $unitSession; + } + $output = $runSession->execute(); + + if (!$output) { + $output = [ + 'title' => 'Finish', + '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) { - 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(); - } - - 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; - } + Back to the admin control panel." + ]; + + Session::delete('test_study_data'); + } + + return compact("output", "runSession"); + } + + 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->isStudyTest()) { + $test = $this->testStudy(); + extract($test); + } else { + + $runSession = new RunSession($user->user_code, $this, ['user' => $user]); + + if (($this->getOwner()->user_code == $user->user_code || // owner always has access + $runSession->isTesting()) || // testers always have access + ($this->public >= 1 && $runSession->id) || // already enrolled + ($this->public >= 2)) { // anyone with link can access + if ($runSession->id === null) { + $runSession->create($user->user_code, (int) $user->created($this)); + } + + Session::globalRefresh(); + $output = $runSession->execute(); + } else { + $runSession->createUnitSession($this->getServiceMessage(), null, false); + $output = $runSession->executeTest(); + alert("Sorry: You cannot currently access this run.", 'alert-warning'); + } + + $runSession->setLastAccess(); + $this->activeRunSession = $runSession; + } + + 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 && !empty($this->description_parsed)) { + $run_content .= $this->description_parsed; + } + + if (isset($output['body'])) { + $run_content .= $output['body']; + } + if (!$this->renderedDescAndFooterAlready && !empty($this->footer_text_parsed)) { + $run_content .= $this->footer_text_parsed; + } + + if ($runSession->isTesting()) { + $animal_end = strpos($user->user_code, "XXX"); + if ($animal_end === false) { + $animal_end = 10; + } + + $run_content .= Template::get('admin/run/monkey_bar', array( + 'user' => $user, + 'run' => $this, + 'run_session' => $runSession, + 'short_code' => substr($user->user_code, 0, $animal_end), + 'icon' => $user->created($this) ? "fa-user-md" : "fa-stethoscope", + 'disable_class' => $this->isStudyTest() ? " disabled " : "", + )); + } + + return array( + 'title' => $title, + 'css' => $css, + 'js' => $js, + 'run_session' => $runSession, + 'run_content' => $run_content, + 'redirect' => array_val($output, 'redirect'), + '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 = SurveyStudy::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(); + + foreach ($units as $unit) { + $options = []; + 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) { + $options = (array) $unit; + $options['importing'] = true; + $options['run'] = $this; + } + + if (strpos($unit->type, 'Skip') !== false) { + $unit->if_true = $unit->if_true + $start_position; + } + + if (strpos($unit->type, 'Email') !== false) { + $unit->account_id = null; + } + + if (strpos($unit->type, 'Wait') !== false) { + $unit->body = $unit->body + $start_position; + } + + $unit = (array) $unit; + $unitObj = RunUnitFactory::make($this, (array) $unit); + $unitObj->create($options); + + if ($unitObj->valid) { + $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 82376650c..9bde074b1 100644 --- a/application/Model/RunSession.php +++ b/application/Model/RunSession.php @@ -1,379 +1,679 @@ 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, - `survey_run_sessions`.run_id, - `survey_run_sessions`.created, - `survey_run_sessions`.ended, - `survey_run_sessions`.position, - `survey_run_sessions`.last_access, - `survey_run_sessions`.no_email, - `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->select('last_access') - ->from('survey_run_sessions') - ->where(array('id' => $this->id)); - } - - 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 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'); +class RunSession extends Model { + + public $id; + public $run_id; + public $user_id; + public $session = null; + public $created; + public $ended; + public $last_access; + public $position; + public $current_unit_session_id; + public $deactivated = 0; + public $no_email; + public $testing = 0; + /** + * + * @var Run + */ + protected $run; + /** + * + * @var User + */ + public $user; + protected $table = 'survey_run_sessions'; + /** + * Currently active unit session; + * + * @var UnitSession + */ + public $currentUnitSession; + /** + * Cache for unit ids in various positions + * + * @var array + */ + protected $positionedUnitIds = []; + /** Maximum number of recursions to happen during a run session execution; */ + const MAX_EXECUTION_COUNT = 10; + /** + * Current number of execution counts while recursive + * @var int + */ + protected $executionCount = 0; + + /** + * A RunSession should always be initiated with a Run and a User + * since a RunSession should belong to a User and needs a Run + * + * @param string $session The code of the user executing the run + * @param Run $run + * @param array $options Other options that could be used to initiate a RunSession + */ + public function __construct($session, Run $run, $options = []) { + parent::__construct(); + + $this->session = $session; + $this->run = $run; + $this->assignProperties($options); + + if (($this->id || $this->session) && $this->run) { + $this->load(); + } + + if ($run->isStudyTest()) { + // User is just testing the survey so we only need a dummy run session since data is not saved + $this->id = -1; + $this->testing = true; + Site::getInstance()->setRunSession($this); + } + + if (!$this->user) { + $this->user = new User(null, $this->session); + } + } + + private function load() { + $options = []; + if ($this->id) { + $options['id'] = (int) $this->id; + } elseif ($this->session) { + $options['session'] = $this->session; + $options['run_id'] = $this->run->id; + } + + if (!$options) { + return; + } + + $data = $this->db->findRow('survey_run_sessions', $options); + if ($data) { + $this->assignProperties($data); + $this->valid = true; + Site::getInstance()->setRunSession($this); + return true; + } + + return false; + } + + public function getRun() { + return $this->run; + } + + public function getLastAccess() { + return $this->db->findValue('survey_run_sessions', array('id' => $this->id), array('last_access')); + } + + public function setLastAccess() { + if (!$this->cron && (int) $this->id > 0) { + $this->db->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->db->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(); + } + + /** + * Create a new unit session for this run session + * + * @param RunUnit $unit + * @param boolean $setAsCurrent + * @param boolean $save Should unit session be saved on TV? + * @return \RunSession + */ + public function createUnitSession(RunUnit $unit, $setAsCurrent = true, $save = true) { + $this->debug('--', true); + $this->debug("CREATE {$unit->type}", true); + + $unitSession = new UnitSession($this, $unit); + if ($save === false) { + $this->currentUnitSession = $unitSession; + return $this; + } + + $this->currentUnitSession = $unitSession->create($setAsCurrent); + + $this->debug("Created"); + return $this; + } + + /** + * Loop over units in Run for a session until you get a unit with output + * + * @param UnitSession $referenceUnitSession + * @param boolean $executeReferenceUnit If TRUE, the first unit with session matching the $referenceUnitSession will be executed + * @return mixed + */ + public function execute(UnitSession $referenceUnitSession = null, $executeReferenceUnit = false) { + + if ($this->ended) { + // User tried to access an already ended run session, logout + if (formr_in_console()) { + $referenceUnitSession->end('ended_by_queue_rse'); + UnitSessionQueue::removeItem($referenceUnitSession->id); + } elseif ($this->current_unit_session_id) { + $this->currentUnitSession = new UnitSession($this, null, [ + 'id' => $this->current_unit_session_id, + 'load' => true + ]); + if (!$this->currentUnitSession->runUnit) { + formr_error(404, 'Run Unit Not Found', 'The Run Unit you are trying to access may have been deleted'); } - alert('Nesting too deep. Could there be an infinite loop or maybe no landing page?', 'alert-danger'); - return false; + return $this->executeUnitSession(); } - - $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; + + // logout if we are unable to get a current unit session + return redirect_to(run_url($this->run->name, 'logout', ['prev' => $this->session])); + } + + if ($this->executionCount > self::MAX_EXECUTION_COUNT) { + return $this->spam(); + } + + if ($this->run->isStudyTest()) { + return $this->executeTest(); + } + // Get the initial position if this run session hasn't executed before + if ($this->position === null && !($position = $this->run->getFirstPosition())) { + alert('This study has not been defined.', 'alert-danger'); + return false; + } + + if ($this->position === null) { + $this->position = $position; + $this->save(); + } + + $currentUnitSession = $this->getCurrentUnitSession(); + + // If there is a referenceUnitSession then it is sent by the queue + if ($referenceUnitSession && $currentUnitSession && $referenceUnitSession->id == $currentUnitSession->id && !$executeReferenceUnit) { + $this->debug("END-q"); + $this->endCurrentUnitSession(); + return $this->moveOn(); + } elseif ($referenceUnitSession && $currentUnitSession && $referenceUnitSession->id != $currentUnitSession->id) { + // if $currenUnitSession is not identical to the $referenceUnitSession sent by queue then something went terribly bad + UnitSessionQueue::removeItem($referenceUnitSession->id); + return $this->moveOn(); + } + + $this->debug('Current Unit Is ' . ($currentUnitSession ? $currentUnitSession->runUnit->type : ''), true); + if (!$currentUnitSession && $this->position === $this->run->getFirstPosition()) { + // We are in the first unit of the run + return $this->moveOn(true); + } elseif (!$currentUnitSession) { + // We maybe all previous unit sessions have ended so move on + return $this->moveOn(); + } else { + // Currently active unit session. Should most likey be a survey or pause + $this->currentUnitSession = $currentUnitSession; + } + + return $this->executeUnitSession(); + } + + /** + * Move on to the next unit of the Run + * + * @param boolean $starting TRUE if we are in the first run unit. FALSE otherwise. + * @param boolean $execute TRUE means we continue executing the next unit + * @return type + */ + public function moveOn($starting = false, $execute = true) { + if ($this->run->isStudyTest()) { + // nothing to move on to + return null; + } + + if (!$starting) { + $this->currentUnitSession = null; + $this->position = $this->run->getNextPosition($this->position); + if ($this->position !== null) { + $this->save(); + } + } + + if ($this->position && ($unit_id = $this->getUnitIdAtPosition($this->position))) { + $runUnit = RunUnitFactory::make($this->run, ['id' => $unit_id]); + $this->createUnitSession($runUnit); + return $execute ? $this->execute() : null; + } + + alert('Run ' . $this->run->name . ':
    Oops, this study\'s creator forgot to give it a proper ending (a Stop button), user ' . h($this->session) . ' is dangling at the end.', 'alert-danger'); + $this->end(); + return ['body' => '']; + } + + protected function executeUnitSession() { + $this->executionCount++; + $this->debug("Execute"); + + $result = $this->currentUnitSession->execute(); + $this->debug($result, true); + + if (!empty($result['expired'])) { + $this->debug("EXPIRE"); + $this->currentUnitSession->expire(); + } elseif (!empty($result['end_session'])) { + $this->debug("END"); + $this->currentUnitSession->end(); + } elseif (isset($result['queue'])) { + $this->debug('QUEUE'); + $this->currentUnitSession->queue(); + return [ + 'body' => array_val($result, 'content'), + 'redirect' => array_val($result, 'redirect') + ]; + } + + if (!empty($result['wait_opencpu']) || !empty($result['wait_user'])) { + return ['body' => '']; + } + + if (isset($result['redirect'])) { + // move on in the run before redirecting to external service (except for surveys) + if ($this->currentUnitSession->runUnit->type !== 'Survey') { + $this->moveOn(false, false); + } + return $result; + } + + if (isset($result['run_to'])) { + return $this->runTo($result['run_to'], null, true); + } + + if (isset($result['move_on'])) { + return $this->moveOn(); + } + + if (isset($result['end_run_session'])) { + $this->end(); + } + + if (isset($result['content'])) { + return ['body' => $result['content']]; + } + + return ['body' => 'FORMR_END']; + } + + public function getUnitIdAtPosition($position) { + if (empty($this->positionedUnitIds[$position])) { + $this->positionedUnitIds[$position] = $this->db->findValue('survey_run_units', ['run_id' => $this->run->id, 'position' => $position], 'unit_id'); + } + + return $this->positionedUnitIds[$position]; + } + + public function forceTo($position) { + // If there a unit for current position, then end the unit's session before moving + if (($unitSession = $this->getCurrentUnitSession())) { + $unitSession->end(); + $unitSession->result = 'manual_admin_push'; + $unitSession->logResult(); + } + return $this->runTo($position); + } + + public function runTo($position, $unit_id = null, $execute = false) { + if ($unit_id === null) { + $unit_id = $this->getUnitIdAtPosition($position); + } + + if ($unit_id) { + $this->position = $position; + $unit = RunUnitFactory::make($this->run, ['id' => $unit_id]); + if ($unit->valid) { + $this->createUnitSession($unit); + //$this->db->update('survey_run_sessions', ['position' => $position], ['id' => $this->id]); + $this->db->exec( + "UPDATE `survey_run_sessions` SET `ended` = NULL, `position` = :position WHERE `id` = :id", + [ + 'id' => $this->id, + 'position' => $position + ] + ); + + $this->ended = null; + $exec = ['body' => null]; + if (formr_in_console() || $execute) { + $exec = $this->execute(); } - $unit = $unit_factory->make($this->dbh, $this->session, $unit_info, $this, $this->run); - $this->current_unit_type = $unit->type; - $output = $unit->exec(); - - if (!$output && is_object($unit)) { - if (!isset($done[$unit->type])) { - $done[$unit->type] = 1; - } - $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(' + return $exec; + } else { + alert(__('Error. Could not create unit session for unit %s at pos. %s.', $unit_id, $position), 'alert-danger'); + } + } else { + alert('Error. You tried to jump to a non-existing run position or forgot to specify one entirely.', 'alert-danger'); + } + + return false; + } + + public function getCurrentUnitSession() { + if ($this->currentUnitSession) { + $this->debug("Using current unit session at {$this->position} [{$this->currentUnitSession->id}]", true); + return $this->currentUnitSession; + } + + $this->debug("Getting current unit session at {$this->position} [0]", true); + $query = $this->db->select(' `survey_unit_sessions`.unit_id, - `survey_unit_sessions`.id AS session_id, - `survey_unit_sessions`.created, + `survey_unit_sessions`.id, + `survey_unit_sessions`.run_session_id, + `survey_unit_sessions`.created, + `survey_unit_sessions`.expires, + `survey_unit_sessions`.ended, + `survey_unit_sessions`.expired, `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; - endif; - return false; - } - - public function getUnitSession() { - if($this->getCurrentUnit()) { - return $this->unit_session; - } - return false; - } - - 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` + ->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); + + $row = $query->fetch(); + + if ($row) { + $u = $row; + $u['id'] = $u['unit_id']; + $unit = RunUnitFactory::make($this->run, $u); + $this->currentUnitSession = new UnitSession($this, $unit, $row); + return $this->currentUnitSession; + } else { + return false; + } + } + + public function endCurrentUnitSession($reason = null) { + if ($this->getCurrentUnitSession()) { + $type = $this->currentUnitSession->runUnit->type; + if ($type == 'Survey' || $type == 'External') { + $this->currentUnitSession->expire(); + } else { + $this->currentUnitSession->end($reason); + } + + return true; + } + + return false; + } + + 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->db->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->db->exec($query, array('id' => $this->id)); + + if ($updated === 1) { + $this->ended = mysql_datetime(); + return true; + } + + return false; + } + + public function spam() { + $this->debug('SPAM'); + $this->endCurrentUnitSession('spam_' . $this->executionCount); + $this->end(); + + alert('This session is spamming us. Please fix your run definition', 'alert-danger'); + return ['body' => 'FORMR_SPAM']; + } + + public function setTestingStatus($status = 0) { + $this->db->update("survey_run_sessions", array('testing' => $status), array('id' => $this->id)); + } + + public function isTesting() { + return $this->testing; + } + + public function isCron() { + return $this->user->isCron(); + } + + /** + * 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->db->update('survey_run_sessions', $update, array('id' => $this->id)); + } + + $oldSettings = $this->getSettings(); + unset($oldSettings['code']); + if ($oldSettings) { + $settings = array_merge($oldSettings, $settings); + } + + $this->db->insert_update('survey_run_settings', array( + 'run_session_id' => $this->id, + 'settings' => json_encode($settings), + )); + } + + public function getSettings() { + $settings = array(); + $row = $this->db->findRow('survey_run_settings', array('run_session_id' => $this->id)); + if ($row) { + $settings = (array) json_decode($row['settings']); + } + $settings['code'] = $this->session; + 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(Run $run, $sessions, $position) { + if (is_string($sessions)) { + $sessions = array($sessions); + } + + $count = 0; + foreach ($sessions as $session) { + $runSession = new RunSession($session, $run); + if ($runSession->position != $position && $runSession->forceTo($position)) { + $runSession->execute(); + $count++; + } + } + return $count; + } + + 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); + } + + public function toArray() { + return [ + 'id' => $this->id, + 'run_id' => $this->run->id, + 'user_id' => $this->user->id, + 'session' => $this->session, + 'created' => $this->created, + 'ended' => $this->ended, + 'last_access' => $this->last_access, + 'position' => $this->position, + 'current_unit_session_id' => $this->current_unit_session_id, + 'deactivated' => $this->deactivated, + 'no_email' => $this->no_email, + 'testing' => $this->testing, + 'created' => $this->created, + ]; + } + + public function executeTest() { + return $this->executeUnitSession(); + } + + public function canReceiveMails() { + if ($this->no_email === null) { + return true; + } + + // If no mail is 0 then user has choose not to receive emails + if ((int) $this->no_email === 0) { + return false; + } + + // If no_mail is set && the timestamp is less that current time then the snooze period has expired + if ($this->no_email <= time()) { + // modify subscription settings + $this->saveSettings(array('no_email' => '1'), array('no_email' => null)); + return true; + } + + return false; + } + + public static function getTestSession(Run $run) { + $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($test_code, $run); + $run_session->create($test_code, true); + + return $run_session; + } + + public static function getNamedSession(Run $run, $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($new_code, $run); // does this user have a session? + $run_session->create($new_code, $testing); + + return $run_session; + } + + public function isTestingStudy() { + return $this->run->isStudyTest() || $this->id === -1; + } + + protected function debug($messsage = '', $only = false) { + if (!DEBUG) { + return; + } + if (is_array($messsage)) { + unset($messsage['content']); + } + $messsage = "(Count {$this->executionCount}) " . print_r($messsage, true); + + if ($this->currentUnitSession && $only === false) { + formr_log("{$messsage} {$this->currentUnitSession->runUnit->type} [{$this->currentUnitSession->id}]", $this->id); + } else { + formr_log($messsage, $this->id); + } + } } diff --git a/application/Model/RunUnit.php b/application/Model/RunUnit.php deleted file mode 100644 index 0a90d7e89..000000000 --- a/application/Model/RunUnit.php +++ /dev/null @@ -1,820 +0,0 @@ -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; - - 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; - return true; - } - - 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( - "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; - 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) { - return ' -
    -

    -
    -

    - ' . $this->howManyReachedIt() . '
    -
    -
    -
    - - - ' . $dialog . ' -
    -
    -
    - '; - } - - 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 - 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` - 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(); - -// $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; - } - } - -// 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) { - /* @var $session OpenCPU_Session */ - if (!$this->knittingNeeded($source)) { // knit if need be - if($email_embed) { - return array('body' => $this->body_parsed, 'images' => array()); - } else { - return $this->body_parsed; - } - } - - $session = NULL; - 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 - } - - $opencpu_url = rtrim($opencpu_url, "/") . $email_embed ? '' : '/R/.val/'; - $format = ""; - $session = opencpu_get($opencpu_url, $format , null, true); - } - } - - // 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()) { - if($has_session_data) { - $ocpu_vars = $this->getUserDataInRun($source); - } else { - $ocpu_vars = array(); - } - if($email_embed) { - $session = opencpu_knitemail($source, $ocpu_vars, '', true); - } else { - $session = opencpu_knit_iframe($source, $ocpu_vars, true, null, $this->run->description, $this->run->footer_text); - $this->run->renderedDescAndFooterAlready = 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; - } else { - - if($admin) { - return $session; - } - - 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' => $session->getFiles('/figure-html'), - ); - } else { - $iframesrc = $session->getFiles("knit.html")['knit.html']; - $report = '
    - -
    '; - } - - if($this->session_id) { - $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 -```{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()); - } - -} diff --git a/application/Model/RunUnit/Branch.php b/application/Model/RunUnit/Branch.php new file mode 100644 index 000000000..729fda68b --- /dev/null +++ b/application/Model/RunUnit/Branch.php @@ -0,0 +1,173 @@ +id) { + $vars = $this->db->findRow('survey_branches', ['id' => $this->id], 'id, condition, if_true, automatically_jump, automatically_go_on'); + if ($vars) { + array_walk($vars, "emptyNull"); + $vars['valid'] = true; + $this->assignProperties($vars); + } + } + } + + public function create($options = []) { + $this->db->beginTransaction(); + parent::create($options); + + if (isset($options['condition'])) { + array_walk($options, "emptyNull"); + $this->assignProperties($options); + } + + $this->condition = cr2nl($this->condition); + + $this->db->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->db->commit(); + $this->valid = true; + + return $this; + } + + public function displayForRun($prepend = '') { + $dialog = Template::get($this->getTemplatePath(), 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) { + $this->noTestSession(); + return null; + } + + $test_tpl = ' + + + + + + + %{rows} + +
    Code (Position)Test
    + '; + + $row_tpl = ' + + %{session} (%{position}) + %{result} + + '; + + // take the first sample session + $unitSession = current($results); + $opencpu_vars = $unitSession->getRunData($this->condition); + $ocpu_session = opencpu_evaluate($this->condition, $opencpu_vars, 'text', null, true); + $output = 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 $unitSession) { + $opencpu_vars = $unitSession->getRunData($this->condition); + $eval = opencpu_evaluate($this->condition, $opencpu_vars); + $rows .= Template::replace($row_tpl, array( + 'session' => $unitSession->runSession->session, + 'position' => $unitSession->runSession->position, + 'result' => stringBool($eval), + )); + } + + $output .= Template::replace($test_tpl, array('rows' => $rows)); + + return $output; + } + + public function getUnitSessionExpirationData(UnitSession $unitSession) { + $data = ['expire_relatively' => null, 'check_failed' => false]; + $opencpu_vars = $unitSession->getRunData($this->condition); + $eval = opencpu_evaluate($this->condition, $opencpu_vars); + + if ($eval === null) { + $data['log'] = $this->getLogMessage('error_opencpu_r', 'OpenCPU error. Fix R code'); + $data['wait_opencpu'] = true; + return $data; + } + if (is_array($eval)) { + $eval = array_shift($eval); + $data['log'] = $this->getLogMessage('opencpu_result_warn', "Your R code is returning more than one result. Please fix your code, so it returns only true/false"); + } + + if($eval === true || $eval === false) { + $result = $eval; + } else { + // If execution returned a timestamp in the future, then branching evaluates to FALSE + if ($eval && ($time = strtotime($eval)) && $time > time()) { + $result = false; + } elseif ($eval && ($time = strtotime($eval)) && $time <= time()) { + $result = true; + } else { + $result = (bool) $eval; + } + $data['log'] = $this->getLogMessage('opencpu_result_warn', "Your R code is not returning true/false. Please fix your code soon"); + } + + if ($result && ($this->automatically_jump || !$unitSession->isExecutedByCron())) { + // if condition is true and we're set to jump automatically, or if the user reacted + $data['log'] = $this->getLogMessage('skip_true'); + $data['end_session'] = true; + $data['run_to'] = $this->if_true; + } elseif (!$result && ($this->automatically_go_on || !$unitSession->isExecutedByCron())) { + // the condition is false and it goes on + $data['log'] = $this->getLogMessage('skip_false'); + $data['end_session'] = $data['move_on'] = true; + } else { + $data['log'] = $this->getLogMessage('waiting_deprecated', 'formr is phasing out support for delayed skipbackwards/forwards. Please switch to a different approach soon'); + $data['check_failed'] = true; + } + + // We already computed needed output data so save and use when getting session output + $this->outputData = $data; + return $data; + } + + public function getUnitSessionOutput(UnitSession $unitSession) { + unset($this->outputData['log']); + return $this->outputData; + } + + +} diff --git a/application/Model/RunUnit/Email.php b/application/Model/RunUnit/Email.php new file mode 100644 index 000000000..f312ef84b --- /dev/null +++ b/application/Model/RunUnit/Email.php @@ -0,0 +1,466 @@ +id) { + $vars = $this->db->findRow('survey_emails', array('id' => $this->id)); + if ($vars) { + $vars['html'] = 1; + $this->assignProperties($vars); + $this->valid = true; + } + } + } + + public function create($options = []) { + parent::create($options); + + $parsedown = new ParsedownExtra(); + if (isset($options['body'])) { + if (isset($options['account_id']) && is_numeric($options['account_id'])) { + $options['account_id'] = (int) $options['account_id']; + } + $options['cron_only'] = (int)isset($options['cron_only']); + $options['html'] = 1; + $this->assignProperties($options); + } + + if ($this->account_id === null) { + $email_accounts = Site::getCurrentUser()->getEmailAccounts(); + if (count($email_accounts) > 0) { + $this->account_id = current($email_accounts)['id']; + } + } + + if (!knitting_needed($this->body)) { + $this->body_parsed = $parsedown->text($this->body); + } + + $this->db->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 $this; + } + + public function displayForRun($prepend = '') { + $dialog = Template::get($this->getTemplatePath(), 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); + } + + protected function getPotentialRecipientFields() { + $stmt = $this->db->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. + $stmt->bindValue(':run_id', $this->run->id); + $stmt->execute(); + + $recips = [['id' => $this->mostrecent, 'text' => $this->mostrecent]]; + while ($res = $stmt->fetch(PDO::FETCH_ASSOC)) { + $email = $res['survey'] . "$" . $res['item']; + $recips[] = ["id" => $email, "text" => $email]; + } + + return $recips; + } + + public function getSubject(UnitSession $unitsession = null) { + if ($this->subject_parsed === null) { + if (knitting_needed($this->subject)) { + if ($unitsession !== null) { + $this->subject_parsed = $this->getParsedText($this->subject, $unitsession); + } else { + return false; + } + } else { + $this->subject_parsed = $this->subject; + } + } + + return $this->subject_parsed; + } + + protected function editParsedBody(UnitSession $unitsession = null) { + $sess = null; + $run_name = null; + if ($unitsession !== null) { + $run_name = $unitsession->runSession->getRun()->name; + $sess = $unitsession->runSession->session; + } + + return do_run_shortcodes($this->body_parsed, $run_name, $sess); + } + + protected function getBody(UnitSession $unitsession = null) { + $response = $this->getParsedBody($this->body, $unitsession, ['email_embed' => true]); + if (isset($response['body'])) { + $this->body_parsed = $response['body']; + } + + if (isset($response['images'])) { + $this->images = $response['images']; + } + + $this->body_parsed = $this->editParsedBody($unitsession); + return $this->body_parsed; + } + + public function getRecipientField(UnitSession $unitSession, $return_session = false) { + if (!$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 + LEFT JOIN survey_items_display ON survey_items_display.session_id = survey_unit_sessions.id + LEFT JOIN survey_items ON survey_items.id = survey_items_display.item_id + WHERE + survey_unit_sessions.run_session_id = :run_session_id AND + survey_run_units.run_id = :run_id AND + survey_items.type = 'email' + ORDER BY survey_items_display.answered DESC + LIMIT 1 + "; + + $get_recip = $this->db->prepare($recent_email_query); + $get_recip->bindValue(':run_id', $this->run->id); + $get_recip->bindValue(':run_session_id', $unitSession->runSession->id); + $get_recip->execute(); + + $res = $get_recip->fetch(PDO::FETCH_ASSOC); + $recipient = array_val($res, 'email', null); + } else { + $opencpu_vars = $unitSession->getRunData($this->recipient_field); + $recipient = opencpu_evaluate($this->recipient_field, $opencpu_vars, 'json', null, $return_session); + } + + return $recipient; + } + + public function sendMail(UnitSession $unitSession, $who = null) { + $this->mail_queued = $this->mail_sent = false; + $this->recipient = $who !== null ? $who : $this->getRecipientField($unitSession); + + 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: {$unitSession->runSession->session}", 'alert-danger'); + $this->errors['log'] = $this->getLogMessage('no_recipient', 'We could not find an email recipient'); + return false; + } + + 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'); + $this->errors['log'] = $this->getLogMessage('no_recipient', "The study administrator (you?) did not set up an email account."); + return false; + } + + $run_session = $unitSession->runSession; + + $testing = !$run_session || $run_session->isTesting(); + + $acc = new EmailAccount($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) { + $this->errors['log'] = $this->getLogMessage('error_send_eligible', $error); + $error = "Session: {$unitSession->runSession->session}:\n {$error}"; + alert(nl2br($error), 'alert-danger'); + return false; + } + + if ($warning !== null) { + $this->messages['log'] = $this->getLogMessage(null, $warning); + $warning = "Session: {$unitSession->runSession->session}:\n {$warning}"; + alert(nl2br($warning), 'alert-info'); + } + + $subject = $this->getSubject($unitSession); + if($subject === null || $subject === false || $subject === '') { + $this->errors['log'] = $this->getLogMessage('no_email_subject', 'No email subject set'); + alert('Email subject empty or could not be dynamically generated.', 'alert-danger'); + return false; + } + + $body = $this->getBody($unitSession); + if($body === null || $body === false || $body === '') { + $this->errors['log'] = $this->getLogMessage('no_email_body', 'No email body set'); + alert('Email body empty or could not be dynamically generated.', 'alert-danger'); + return false; + } + + if (!filter_var($this->recipient, FILTER_VALIDATE_EMAIL)) { + $this->errors['log'] = $this->getLogMessage('invalid_email', 'No valid email recipient set'); + alert('Intended recipient was not a valid email address: ' . $this->recipient, 'alert-danger'); + return false; + } + + // if formr is configured to use the email queue then add mail to queue and return + if (Config::get('email.use_queue', false) === true) { + $this->mail_queued = $this->db->insert('survey_email_log', array( + 'subject' => $subject, + 'status' => 0, + 'session_id' => $unitSession->id, + 'email_id' => $this->id, + 'message' => $body, + 'recipient' => $this->recipient, + 'created' => mysql_datetime(), + 'account_id' => (int) $this->account_id, + 'meta' => json_encode(array( + 'embedded_images' => $this->images, + 'attachments' => '' + )), + )); + + return $this->mail_queued; + } + + $mail = $acc->makeMailer(); + +// if($this->html) + $mail->IsHTML(true); + + $mail->AddAddress($this->recipient); + $mail->Subject = $subject; + $mail->Body = $body; + + 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("Could not embed image with id '{$image_id}'", 'alert-danger'); + } + } + + if ($mail->Send()) { + $this->mail_sent = true; + $this->logMail($$unitSession); + } else { + alert('Email with the subject "' . h($mail->Subject) . '" was not sent to ' . h($this->recipient) . ':
    ' . $mail->ErrorInfo, 'alert-danger'); + } + + return $this->mail_sent; + } + + protected function numberOfEmailsSent() { + $log = $this->db->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, + SUM(created > DATE_SUB(NOW(), INTERVAL 1 DAY)) AS in_last_1d, + SUM(1) AS in_last_1w + FROM `survey_email_log` + WHERE recipient = :recipient AND `status` = 1 AND created > DATE_SUB(NOW(), INTERVAL 7 DAY)"); + $log->bindParam(':recipient', $this->recipient); + $log->execute(); + + return $log->fetch(PDO::FETCH_ASSOC); + } + + protected function logMail(UnitSession $unitSession) { + $query = "INSERT INTO `survey_email_log` (session_id, email_id, created, recipient) VALUES (:session_id, :email_id, NOW(), :recipient)"; + $this->db->exec($query, array( + 'session_id' => $unitSession->id, + 'email_id' => $this->id, + 'recipient' => $this->recipient, + )); + } + + public function test() { + if (!($unitSession = $this->grabRandomSession())) { + $this->noTestSession(); + return null; + } + + $user = Site::getCurrentUser(); + $receiver = $user->getEmail(); + + $output = "

    Recipient

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

    Subject

    "; + if (knitting_needed($this->subject)) { + $output .= $this->getParsedText($this->subject, $unitSession, ['admin' => true]); + } else { + $output .= $this->getSubject($unitSession); + } + + $output .= "

    Body

    "; + $output .= $this->getParsedBody($this->body, $unitSession, ['admin' => true]); + + $output .= "

    Attempt to send email

    "; + if ($this->sendMail($unitSession, $receiver)) { + $output .= "

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

    "; + } else { + $output .= "

    No email sent.

    "; + } + + $results = $this->getSampleSessions(); + if ($results) { + if (empty($this->recipient_field)) { + $this->recipient_field = 'survey_users$email'; + } + + $test_tpl = ' + + + + + + + %{rows} + +
    Code (Position)Test
    + '; + + $row_tpl = ' + + %{session} (%{position}) + %{result} + + '; + + $rows = ''; + foreach ($results as $unitSession) { + $email = stringBool($this->getRecipientField($unitSession)); + $class = filter_var($email, FILTER_VALIDATE_EMAIL) ? '' : 'text-warning'; + $rows .= Template::replace($row_tpl, array( + 'session' => $unitSession->runSession->session, + 'position' => $unitSession->runSession->position, + 'result' => stringBool($email), + 'class' => $class, + )); + } + + $output .= Template::replace($test_tpl, ['rows' => $rows]); + } + + return $output; + } + + public function getUnitSessionOutput(UnitSession $unitSession) { + // If emails should be sent only when cron is active and unit is not called by cron, then end it and move on + $data = []; + if ($this->cron_only && !$unitSession->isExecutedByCron()) { + $data['log'] = $this->getLogMessage('email_skipped_user_active'); + $data['end_session'] = true; + return $data; + } + + // Check if user is enabled to receive emails + if (!$unitSession->runSession->canReceiveMails()) { + $data['log'] = $this->getLogMessage('email_skipped_user_disabled'); + $data['content'] = "

    User {$unitSession->runSession->session} disabled receiving emails at this time

    "; + return $data; + } + + // Try to send email + $err = $this->sendMail($unitSession); + if ($this->mail_sent || $this->mail_queued) { + $log = array_val($this->messages, 'log', ['result_log' => null]); + $data['log'] = $this->getLogMessage(($this->mail_queued ? 'email_queued' : 'email_sent'), $log['result_log']); + $data['end_session'] = $data['move_on'] = true; + } else { + $data['log'] = array_val($this->errors, 'log', $this->getLogMessage('error_email')); + $data['content'] = $err; + } + + return $data; + } + +} diff --git a/application/Model/RunUnit/External.php b/application/Model/RunUnit/External.php new file mode 100644 index 000000000..567b2c2e4 --- /dev/null +++ b/application/Model/RunUnit/External.php @@ -0,0 +1,183 @@ +address = $props['external_link']; + } + + if ($this->id) { + $vars = $this->db->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; + } + } + } + + public function create($options = []) { + $this->db->beginTransaction(); + parent::create($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->db->insert_update('survey_externals', array( + 'id' => $this->id, + 'address' => $this->address, + 'api_end' => $this->api_end, + 'expire_after' => $this->expire_after, + )); + $this->db->commit(); + $this->valid = true; + + return $this; + } + + public function displayForRun($prepend = '') { + $dialog = Template::get($this->getTemplatePath(), 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((string)$address, 0, 4) == "http") { + return false; + } + return true; + } + + private function isAddress($address) { + return !$this->isR($address); + } + + public function test() { + $unitSession = $this->grabRandomSession(); + if ($this->isR($this->address)) { + if (!$unitSession) { + $this->noTestSession(); + return; + } + + $opencpu_vars = $unitSession->getRunData($this->address); + $ocpu_session = opencpu_evaluate($this->address, $opencpu_vars, '', null, true); + $output = opencpu_debug($ocpu_session, null, 'text'); + } else { + $output = Template::replace('%{address}', ['address' => $this->address]); + } + + $run_name = $session = null; + if (!empty($unitSession)) { + $run_name = $unitSession->runSession->getRun()->name; + $session = $unitSession->runSession->session; + } + + return do_run_shortcodes($output, $run_name, $session); + } + + public function getUnitSessionExpirationData(UnitSession $unitSession) { + $data = []; + $expire = (int) $this->expire_after; + if ($expire) { + $last = $unitSession->created; + if (!$last || !strtotime($last)) { + return $data; + } + + $expire_ts = strtotime($last) + ($expire * 60); + if (($expired = $expire_ts < time())) { + $data['expired'] = true; + } else { + $data['expires'] = $expire_ts; + $data['queued'] = UnitSessionQueue::QUEUED_TO_END; + } + } + + return $data; + } + + public function getUnitSessionOutput(UnitSession $unitSession) { + $data = []; + $expired = $this->getUnitSessionExpirationData($unitSession); + if ($unitSession->isExecutedByCron()) { + if (!empty($expired['expired'])) { + $data['expired'] = true; + return $expired; + } + } + + // if it's the user, redirect them or do the call + if ($this->isR($this->address)) { + $opencpu_vars = $unitSession->getRunData($this->address); + $result = opencpu_evaluate($this->address, $opencpu_vars); + + if ($result === null) { + $data['log'] = $this->getLogMessage('error_opencpu'); + $data['wait_opencpu'] = true; // don't go anywhere, wait for the error to be fixed! + return $data; + } elseif ($result === false) { + $data['log'] = $this->getLogMessage('external_r_call_no_redirect'); + $data['end_session'] = true; + $data['move_on'] = true; // go on, no redirect + return $data; + } elseif ($this->isAddress($result)) { + $data['log'] = $this->getLogMessage('external_r_redirect'); + $data['redirect'] = $result; + } else { + $data['log'] = $this->getLogMessage('external_compute', $result); + $data['end_session'] = true; + $data['move_on'] = true; + return $data; + } + } else { // the simplest case, just an address + $data['log'] = $this->getLogMessage('external_redirect'); + $data['redirect'] = $this->address; + } + + $data['redirect'] = do_run_shortcodes($data['redirect'], $unitSession->runSession->getRun()->name, $unitSession->runSession->session); + + // never redirect if we're just in the cron job + if (!$unitSession->isExecutedByCron()) { + // sometimes we aren't able to control the other end + if (!$this->api_end) { + $data['end_session'] = $data['move_on'] = true; + } else { + $data['log'] = $this->getLogMessage('external_wait_for_api'); + } + } + + return $data; + } +} diff --git a/application/Model/RunUnit/Page.php b/application/Model/RunUnit/Page.php new file mode 100644 index 000000000..e8021e43e --- /dev/null +++ b/application/Model/RunUnit/Page.php @@ -0,0 +1,100 @@ +id) { + $vars = $this->db->findRow('survey_pages', array('id' => $this->id), 'title, body, body_parsed'); + if ($vars) { + $vars['valid'] = true; + $this->assignProperties($vars); + } + } + } + + public function create($options = []) { + parent::create($options); + + $parsedown = new ParsedownExtra(); + $this->body_parsed = $parsedown + ->setBreaksEnabled(true) + ->text($this->body); // transform upon insertion into db instead of at runtime + + $this->db->insert_update('survey_pages', array( + 'id' => $this->id, + 'body' => $this->body, + 'body_parsed' => $this->body_parsed, + 'title' => $this->title, + 'end' => 0, + )); + $this->valid = true; + + return $this; + } + + public function displayForRun($prepend = '') { + $dialog = Template::get($this->getTemplatePath(), array( + 'prepend' => $prepend, + 'body' => $this->body, + )); + + return $this->runDialog($dialog); + } + + public function removeFromRun($special = null) { + return $this->delete($special); + } + + public function test() { + if (($testSession = $this->getTestSession($this->body)) === false) { + // knitting needed but no test session to use data + return; + } + + return $this->getParsedBody($this->body, $testSession, ['admin' => true]); + } + + public function find($id, $special = false, $props = []) { + parent::find($id, $special, $props); + $this->type = 'Endpage'; + + return $this; + } + + public function getUnitSessionOutput(UnitSession $unitSession) { + $output = []; + if ($unitSession->isExecutedByCron()) { + $this->getParsedBody($this->body, $unitSession); + $output['log'] = array_val($this->errors, 'log', []); + $output['wait_user'] = true; + return $output; + } + + $this->body_parsed = $this->getParsedBody($this->body, $unitSession); + if ($this->body_parsed === false) { + $output['wait_opencpu'] = true; // wait for openCPU to be fixed! + $output['log'] = array_val($this->errors, 'log', []); + return $output; + } + + $output['content'] = do_run_shortcodes($this->body_parsed, $unitSession->runSession->getRun()->name, $unitSession->runSession->session); + $output['end_session'] = true; + $output['end_run_session'] = true; + $output['log'] = $this->getLogMessage('ended'); + + return $output; + } + +} diff --git a/application/Model/RunUnit/Pause.php b/application/Model/RunUnit/Pause.php new file mode 100644 index 000000000..5895d48ee --- /dev/null +++ b/application/Model/RunUnit/Pause.php @@ -0,0 +1,343 @@ +id) { + $cols = 'id, body, body_parsed, wait_until_time, wait_minutes, wait_until_date, relative_to'; + $vars = $this->db->findRow('survey_pauses', ['id' => $this->id], $cols); + if ($vars) { + array_walk($vars, "emptyNull"); + $vars['valid'] = true; + $this->assignProperties($vars); + } + } + } + + public function create($options = []) { + $this->db->beginTransaction(); + + parent::create($options); + + if (isset($options['body'])) { + array_walk($options, "emptyNull"); + $this->assignProperties($options); + } + + $parsedown = new ParsedownExtra(); + $parsedown->setBreaksEnabled(true); + + if (!knitting_needed($this->body)) { + $this->body_parsed = $parsedown->text($this->body); // transform upon insertion into db instead of at runtime + } + + $this->db->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->db->commit(); + $this->valid = true; + + return $this; + } + + public function displayForRun($prepend = '') { + $dialog = Template::get($this->getTemplatePath(), 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, + 'type' => $this->type, + )); + + return parent::runDialog($dialog); + } + + public function removeFromRun($special = null) { + return $this->delete($special); + } + + protected function parseRelativeTo() { + $this->relative_to = trim((string) $this->relative_to); + $this->wait_minutes = trim((string) $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 = $this->default_relative_to; + $this->has_relative_to = true; + } + + return $this->has_relative_to; + } + + public function getUnitSessionExpirationData(UnitSession $unitSession) { + $this->parseRelativeTo(); + $data = [ + 'check_failed' => false, + 'expire_relatively' => null, + 'expired' => false, + 'queued' => UnitSessionQueue::QUEUED_TO_END, + ]; + + if ($unitSession->expires && ($timestamp = strtotime($unitSession->expires)) > time()) { + // Pause has not expired, no need to fetch new data about it + $data['expires'] = $timestamp; + return $data; + } + + // if a relative_to has been defined by user or automatically, we need to retrieve its value + if ($this->has_relative_to) { + if($this->relative_to === 'tail(survey_unit_sessions$created,1)' && $unitSession->created) { + $result = $unitSession->created; + } else { + $opencpu_vars = $unitSession->getRunData($this->relative_to); + $result = opencpu_evaluate($this->relative_to, $opencpu_vars, 'json'); + if ($result === null) { + $data['check_failed'] = true; + $data['log'] = $this->getLogMessage('error_pause_relative_to', 'OpenCPU R error. Fix code.'); + $this->errors[] = 'Could not evaluate relative_to value on opencpu'; + return $data; + } + } + $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'; + $data['expire_relatively'] = true; + } elseif ($relative_to === false) { + $conditions['relative_to'] = '0=1'; + $data['expire_relatively'] = false; + } elseif (!is_array($relative_to) && strtotime($relative_to)) { + $conditions['relative_to'] = ':relative_to <= NOW()'; + $bind_relative_to = true; + $data['expires'] = strtotime($relative_to); + // If there was a wait_time, set the timestamp to have this time + if ($time = $this->parseWaitTime(true)) { + $ts = $data['expires']; + $data['expires'] = mktime((int)$time[0], (int)$time[1], 0, (int)date('m', $ts), (int)date('d', $ts), (int)date('Y', $ts)); + $relative_to = date('Y-m-d H:i:s', $data['expires']); + } + } else { + $this->errors[] = "Pause {$this->position}: Relative to yields neither true nor false, nor a date, nor a time. " . print_r($relative_to, true); + $data['check_failed'] = true; + $data['log'] = $this->getLogMessage('error_pause_relative_to', 'OpenCPU R error. Fix code.'); + return $data; + } + } elseif ($this->has_wait_minutes) { + if (!is_array($relative_to) && strtotime($relative_to)) { + $conditions['minute'] = "DATE_ADD(:relative_to, INTERVAL :wait_seconds SECOND) <= NOW()"; + $bind_relative_to = true; + $data['expires'] = strtotime($relative_to) + ($this->wait_minutes * 60); + } else { + $this->errors[] = "Pause {$this->position}: Relative to yields neither a date, nor a time. " . print_r($relative_to, true); + $data['check_failed'] = true; + $data['log'] = $this->getLogMessage('error_pause_wait_minutes', 'Relative to yields neither a date, nor a time'); + return $data; + } + } + + 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_defined = $this->wait_until_date && $this->wait_until_date != '0000-00-00'; + $wait_time_defined = $this->wait_until_time && $this->wait_until_time != '00:00:00'; + $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($data['expires'])) { + $wait_datetime = $wait_date . ' ' . $wait_time; + $data['expires'] = 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 = $data['expires']; + $created_ts = strtotime($unitSession->created); + $exp_hour_min = mktime(date('G', $exp_ts), date('i', $exp_ts), 0); + if ($created_ts > $exp_hour_min && !$wait_date_defined) { + $data['expires'] += 24 * 60 * 60; + return $data; + } + + $conditions['datetime'] = ':wait_datetime <= NOW()'; + } + + $now = time(); + $result = !empty($data['expires']) && ($data['expires'] <= $now); + + if ($conditions) { + $condition = implode(' AND ', $conditions); + $stmt = $this->db->prepare("SELECT {$condition} AS test LIMIT 1"); + if ($bind_relative_to) { + $stmt->bindValue(':relative_to', $relative_to); + } + if (isset($conditions['minute'])) { + $stmt->bindValue(':wait_seconds', floatval($this->wait_minutes) * 60); + } + 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; + } + + $data['end_session'] = $data['expired'] = $result; + + return $data; + } + + 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) { + $this->noTestSession(); + return null; + } + + // take the first sample session + $unitSession = current($results); + + $output = "

    Pause message

    "; + $output .= $this->getParsedBody($this->body, $unitSession, ['admin' => true]); + $this->setDefaultRelativeTo($unitSession); + + if ($this->parseRelativeTo()) { + $output .= "

    Pause relative to

    "; + $opencpu_vars = $unitSession->getRunData($this->relative_to); + $session = opencpu_evaluate($this->relative_to, $opencpu_vars, 'json', null, true); + $output .= opencpu_debug($session); + } + + if (!empty($results) && (empty($session) || !$session->hasError())) { + + $test_tpl = $output . ' + + + + + + + + %{rows} + +
    CodeRelative toOver
    + '; + + $row_tpl = ' + + %{session} (%{position}) + %{relative_to} + %{pause_over} + + '; + + $rows = ''; + foreach ($results as $unitSession) { + $expired = $this->getUnitSessionExpirationData($unitSession); + $pause_over = !empty($expired['check_failed']) ? 'check_failed' : !empty($expired['expired']); + $rows .= Template::replace($row_tpl, array( + 'session' => $unitSession->runSession->session, + 'position' => $unitSession->runSession->position, + 'pause_over' => stringBool($pause_over), + 'relative_to' => stringBool($this->relative_to_result), + )); + } + + return Template::replace($test_tpl, array('rows' => $rows)); + } + } + + public function getUnitSessionOutput(UnitSession $unitSession) { + $body = $this->getParsedBody($this->body, $unitSession); + if ($body === false) { + // opencpu error + $output['log'] = array_val($this->errors, 'log', []); + $output['wait_opencpu'] = true; // wait for openCPU to be fixed! + return $output; + } + + return [ + 'content' => $body, + 'log' => $this->getLogMessage('pause_waiting') + ]; + } + + protected function setDefaultRelativeTo(UnitSession $unitSession = null) { + + } + +} diff --git a/application/Model/RunUnit/RunUnit.php b/application/Model/RunUnit/RunUnit.php new file mode 100644 index 000000000..07bd8b9f2 --- /dev/null +++ b/application/Model/RunUnit/RunUnit.php @@ -0,0 +1,595 @@ +findValue('survey_units', ['id' => (int)$props['id']], 'type'); + } + + if (empty($props['type'])) { + throw new RuntimeException('Please specify the Unit Type in $props[]'); + } + + $type = $props['type']; + + if (!in_array($type, static::SupportedUnits)) { + throw new Exception("Unsupported unit type '$type'"); + } + + return new $type($run, $props); + } + + public static function getSupportedUnits() { + return static::SupportedUnits; + } + +} + +/** + * Base class for run units. A RunUnit "belongs to" a Run + * + */ + +class RunUnit extends Model { + + public $id = null; + + public $description = ""; + + public $position = 0; + + public $type = null; + + public $special = null; + + public $created = null; + + public $modified = null; + + public $run_unit_id = null; + + public $unit_id = null; + + public $icon = "fa-user"; + + protected $body = ''; + + protected $body_parsed = ''; + + /** + * + * @var Run $run + */ + public $run = null; + + /** + * An array of unit's exportable attributes + * + * @var array + */ + public $export_attribs = array('type', 'description', 'position', 'special'); + + /** + * + * @var boolean If set to True unit would try to generate output again for a session + */ + protected $retryOutput = true; + + /** + * + * @param Run $run + * @param array $props + */ + public function __construct(Run $run, array $props = []) { + parent::__construct(); + $this->run = $run; + $this->assignProperties($props); + + if ($this->id && empty($props['importing'])) { + $this->find($this->id, $this->special, $props); + } + } + + /** + * Create a RunUnit and assign it to a run + * + * @param array $props + * @return RunUnit + */ + public function create($props = []) { + $this->assignProperties($props); + + if ($this->id) { + $this->modify($props); + if (!empty($props['add_to_run'])) { + $this->addToRun(); + } + + return $this; + } else { + $id = $this->db->insert('survey_units', array( + 'type' => $this->type, + 'created' => mysql_now(), + 'modified' => mysql_now(), + )); + + $this->valid = true; + $this->id = $id; + + return $this->addToRun(); + } + } + + protected function addToRun() { + if (!is_numeric($this->position)) { + $this->position = 10; + } + + if ($this->special && $this->run->id) { + $run_unit_id = $this->db->insert('survey_run_special_units', array( + 'id' => $this->id, + 'run_id' => $this->run->id, + 'type' => $this->special, + 'description' => $this->description ?? '', + )); + } elseif ($this->run->id) { + $run_unit_id = $this->db->insert('survey_run_units', array( + 'unit_id' => $this->id, + 'run_id' => $this->run->id, + 'position' => $this->position, + 'description' => $this->description ?? '' + )); + } + + $this->run_unit_id = $run_unit_id ?? 0; + return $this; + } + + public function updateUnitId() { + return $this->db->update( + 'survey_run_units', + array('unit_id' => $this->id), + array('id' => $this->run_unit_id), + array('int'), + array('int') + ); + } + + public function modify($options = []) { + $change = array('modified' => mysql_now()); + $table = empty($options['special']) ? 'survey_run_units' : 'survey_run_special_units'; + if ($this->id && isset($options['description'])): + $this->db->update($table, array("description" => $options['description']), array('id' => $this->run_unit_id)); + $this->description = $options['description']; + endif; + + return $this->db->update('survey_units', $change, array('id' => $this->id)); + } + + + public function removeFromRun($special = null) { + // todo: set run modified + if ($special !== null) { + return $this->db->delete('survey_run_special_units', array('id' => $this->run_unit_id, 'type' => $special)); + } else { + return $this->db->delete('survey_run_units', array('id' => $this->run_unit_id)); + } + } + + 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 getTemplatePath($tpl = null) { + $tpl = $tpl ?? strtolower($this->type); + if ($tpl === 'page') { + $tpl = 'endpage'; + } + + return 'admin/run/units/' . $tpl; + } + + /** + * Get random unit sessions that have passed this unit + * + * @return \UnitSession[] + */ + protected function getSampleSessions() { + // Select a maximum of 20 random sessions that are on or have passed this unit + $results = []; + $rs = []; + $rows = $this->db->select('session, id, position') + ->from('survey_run_sessions') + ->order('position', 'desc')->order('RAND') + ->where(array('run_id' => $this->run->id, 'position >=' => $this->position)) + ->limit(20)->fetchAll(); + + foreach ($rows as $row) { + if (!isset($rs[$row['id']])) { + $rs[$row['id']] = new RunSession($row['session'], $this->run, ['id' => $row['id']]); + } + + $unitSession = (new UnitSession($rs[$row['id']], $this))->load(); + if ($unitSession->id) { + $results[] = $unitSession; + } + } + + return $results; + } + + /** + * Get a random unit session that has past this unit + * + * @return UnitSession|null + */ + protected function grabRandomSession() { + // Select a random run session that has past this unit's position + $row = $this->db->select('session, id, position') + ->from('survey_run_sessions') + ->order('position', 'desc')->order('RAND') + ->where(['run_id' => $this->run->id, 'position >=' => $this->position]) + ->limit(1) + ->fetch(); + if (!$row) { + return null; + } + + $runSession = new RunSession($row['session'], $this->run, ['id' => $row['id']]); + $unitSession = (new UnitSession($runSession, $this))->load(); + if (!$unitSession->id) { + return null; + } + + return $unitSession; + } + + public function getUnitSessionsCount() { + $reached = $this->db->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 displayUnitSessionsCount() { + $reached = $this->getUnitSessionsCount(); + if (!$reached['begun']) { + $reached['begun'] = ""; + } + if (!$reached['finished']) { + $reached['finished'] = ""; + } + if (!$reached['expired']) { + $reached['expired'] = ""; + } + return " + " . $reached['begun'] . " + " . $reached['expired'] . " + " . $reached['finished'] . " + "; + } + + public function runDialog($dialog) { + $tpl = $this->getTemplatePath('unit'); + return Template::get($tpl, array('dialog' => $dialog, 'unit' => $this)); + } + + public function displayForRun($prepend = '') { + return $this->runDialog($prepend); // FIXME: This class has no parent + } + + public function delete($special = null) { + // todo: set run modified + if ($special !== null) { + return $this->db->delete('survey_run_special_units', array('id' => $this->run_unit_id, 'type' => $special)); + } else { + $affected = $this->db->delete('survey_units', array('id' => $this->id)); + if ($affected) { // remove from all runs + $affected += $this->db->delete('survey_run_units', array('unit_id' => $this->id)); + } + } + return $affected; + } + + /** + * Find a unit using the id of the survey_unit + * + * @param int $id + * @param string $special + * @param array $props + * + * @return boolean|RunUnit + */ + public function find($id, $special = false, $props = []) { + $params = array('run_id' => $this->run->id, 'id' => $id); + + if (!$special) { + $select = $this->db->select(' + `survey_run_units`.id AS run_unit_id, + `survey_run_units`.run_id, + `survey_run_units`.unit_id, + `survey_run_units`.position, + `survey_run_units`.description, + `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_units.id = :id'); + + if (!empty($props['run_unit_id'])) { + $select->where('survey_run_units.id = :run_unit_id'); + $params['run_unit_id'] = $props['run_unit_id']; + } + + $unit = $select->bindParams($params)->limit(1)->fetch(); + } else { + $specials = array('ServiceMessagePage', 'OverviewScriptPage', 'ReminderEmail'); + if (!in_array($special, $specials)) { + die("Special unit not allowed"); + } + + $select = $this->db->select(" + `survey_run_special_units`.`id` AS run_unit_id, + `survey_run_special_units`.`run_id`, + `survey_run_special_units`.`description`, + `survey_units`.id, + `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` = :id"); + + if (!empty($props['run_unit_id'])) { + $select->where('survey_run_special_units.id = :run_unit_id'); + $params['run_unit_id'] = $props['run_unit_id']; + } + + $unit = $select->bindParams($params)->limit(1)->fetch(); + $unit["special"] = $special; + } + + if ($unit === false) { // or maybe we've got a problem + if ($this->run->isStudyTest() && $this->unit_id) { + return $this; + } + alert("Missing unit! $id", 'alert-danger'); + return false; + } + + + $unit['valid'] = true; + + $this->assignProperties($unit); + return $this; + } + + public function load() { + return $this->find($this->id, $this->special); + } + + + /** + * Get Run unit using run_unit_id + * + * @param int $id + * @param array $params + * @return RunUnit + */ + public static function findByRunUnitId($id, $params = []) { + $row = DB::getInstance()->findRow('survey_run_units', ['id' => $id]); + if ($row) { + $run = new Run(null, $row['run_id']); + $params = array_merge($params, ['id' => $row['unit_id']]); + return RunUnitFactory::make($run, $params); + } + } + + public function exec() { + return null; + } + + public function getUnitSessionExpirationData(UnitSession $unitSession) { + return []; + } + + public function getUnitSessionOutput(UnitSession $unitSession) { + return null; + } + + public function getParsedBody($source = null, UnitSession $unitSession = null, $options = []) { + $email_embed = array_val($options, 'email_embed'); + if (!knitting_needed($source)) { + return $email_embed ? ['body' => $this->body_parsed, 'images' => []] : $this->body_parsed; + } + + $admin = array_val($options, 'admin', false); + $isCron = $this->isCron(); + $sessionId = $unitSession->id; + + /* @var $ocpu OpenCPU_Session */ + $ocpu = null; + $cache_session = false; + $baseUrl = null; + + if (!$admin) { + $opencpu_url = $this->db->findValue('survey_reports', array( + 'unit_id' => $this->id, + 'session_id' => $sessionId, + '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 && ($ocpu = opencpu_get($opencpu_url, '', null, true))) { + if ($isCron) { + // don't regenerate once we once had a report for this feedback, if it's only the cronjob + return null; + } + + $filesMatch = 'files/'; + $baseUrl = $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($ocpu) || $ocpu->hasError()) { + $ocpu_vars = $unitSession->getRunData($source); + if ($email_embed) { + $ocpu = opencpu_knit_email($source, $ocpu_vars, '', true); + } else { + $ocpu = opencpu_knit_iframe($source, $ocpu_vars, true, null, $this->run->description, $this->run->footer_text); + } + + $filesMatch = 'knit.html'; + $cache_session = true; + } + + // At this stage we are sure to have an OpenCPU_Session in $ocpu. If there is an error in the session return FALSE + if (empty($ocpu)) { + $this->errors['log'] = $this->getLogMessage('error_opencpu_down', 'OpenCPU is probably down or inaccessible.'); + + alert('OpenCPU is probably down or inaccessible. Please retry in a few minutes.', 'alert-danger'); + return false; + } elseif ($ocpu->hasError()) { + $this->errors['log'] = $this->getLogMessage('error_opencpu_r', 'OpenCPU R error. Fix code.'); + + notify_user_error(opencpu_debug($ocpu), 'There was a computational error.'); + return false; + } elseif ($admin) { + return opencpu_debug($ocpu); + } else { + $this->messages['log'] = $this->getLogMessage('success_knitted'); + + print_hidden_opencpu_debug_message($ocpu, "OpenCPU debugger for run R code in {$this->type} at {$this->position}."); + $files = $ocpu->getFiles($filesMatch, $baseUrl); + $images = $ocpu->getFiles('/figure-html', $baseUrl); + $opencpu_url = $ocpu->getLocation(); + + if ($email_embed) { + $report = array( + 'body' => $ocpu->getObject(), + 'images' => $images, + ); + } else { + $this->run->renderedDescAndFooterAlready = true; + $iframesrc = $files['knit.html']; + $report = '' . + '
    + +
    '; + } + + if ($sessionId && $cache_session) { + $set_report = $this->db->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", $sessionId); + $set_report->execute(); + } + + return $report; + } + } + + public function getParsedText($source, UnitSession $unitSession = null, $options = []) { + $admin = array_val($options, 'admin', false); + if (!knitting_needed($source) || $unitSession === null) { + return $source; + } + + $ocpu_vars = $unitSession->getRunData($source); + if (!$admin) { + return opencpu_knit_plaintext($source, $ocpu_vars, false); + } else { + return opencpu_debug(opencpu_knit_plaintext($source, $ocpu_vars, true)); + } + } + + public function getLogMessage($result, $result_log = null) { + return compact('result', 'result_log'); + } + + protected function getTestSession($source) { + if (!knitting_needed($source)) { + return null; + } + + if (!($session = $this->grabRandomSession())) { + $this->noTestSession(); + return false; + } + + return $session; + } + + protected function noTestSession() { + return 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'); + } +} diff --git a/application/Model/RunUnit/Shuffle.php b/application/Model/RunUnit/Shuffle.php new file mode 100644 index 000000000..8af03d7fb --- /dev/null +++ b/application/Model/RunUnit/Shuffle.php @@ -0,0 +1,99 @@ +id) { + $groups = $this->db->findValue('survey_shuffles', array('id' => $this->id), array('groups')); + if ($groups) { + $this->groups = $groups; + $this->valid = true; + } + } + } + + public function create($options = []) { + parent::create($options); + + if (isset($options['groups'])) { + $this->groups = $options['groups']; + } + + $this->db->insert_update('survey_shuffles', array( + 'id' => $this->id, + 'groups' => $this->groups, + )); + + $this->valid = true; + + return $this; + } + + public function displayForRun($prepend = '') { + + $dialog = Template::get($this->getTemplatePath(), array( + 'prepend' => $prepend, + 'groups' => $this->groups + )); + + return parent::runDialog($dialog); + } + + public function removeFromRun($special = null) { + return $this->delete($special); + } + + public function selectRandomGroup() { + return mt_rand(1, $this->groups); + } + + 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->selectRandomGroup() . '  '; + } + + return Template::replace($test_tpl, array('groups' => $groups)); + } + + public function getUnitSessionOutput(UnitSession $unitSession) { + $group = $this->selectRandomGroup(); + $this->db->insert('shuffle', array( + 'session_id' => $unitSession->id, + 'unit_id' => $this->id, + 'group' => $group, + 'created' => mysql_now() + )); + + return [ + 'log' => $this->getLogMessage('group_' . $group), + 'end_session' => true, + 'move_on' => true, + ]; + } + +} diff --git a/application/Model/RunUnit/SkipBackward.php b/application/Model/RunUnit/SkipBackward.php new file mode 100644 index 000000000..56e6d1f35 --- /dev/null +++ b/application/Model/RunUnit/SkipBackward.php @@ -0,0 +1,26 @@ +getTemplatePath(), array( + 'prepend' => $prepend, + 'condition' => $this->condition, + 'position' => $this->position, + 'ifTrue' => $this->if_true, + )); + + return parent::runDialog($dialog); + } + +} diff --git a/application/Model/RunUnit/SkipForward.php b/application/Model/RunUnit/SkipForward.php new file mode 100644 index 000000000..07275de43 --- /dev/null +++ b/application/Model/RunUnit/SkipForward.php @@ -0,0 +1,14 @@ +db->entry_exists('survey_studies', ['id' => (int)$options['study_id']])) { + $this->unit_id = (int) $options['study_id']; + $this->surveyStudy = $this->getStudy(true); + } + + if (empty($options['description']) && $this->surveyStudy) { + $options['description'] = $this->surveyStudy->name ?? 'Survey Name'; + } + + if ($this->surveyStudy) { + $this->db->update( + 'survey_run_units', + ['description' => $options['description'], 'unit_id' => $this->surveyStudy->id], + ['id' => $this->run_unit_id] + ); + } + + $this->valid = true; + + return $this; + } + + public function displayForRun($prepend = '') { + $dialog = Template::get($this->getTemplatePath(), array( + 'survey' => $this->surveyStudy, + 'studies' => Site::getCurrentUser()->getStudies('id DESC', null, 'id, name'), + 'prepend' => $prepend, + 'resultCount' => $this->id ? $this->getUnitSessionsCount() : null, + 'time' => $this->surveyStudy ? $this->surveyStudy->getAverageTimeItTakes() : null, + )); + + return parent::runDialog($dialog); + } + + public function getStudy($force = true) { + if ($force || ($this->surveyStudy == null && $this->unit_id)) { + $this->surveyStudy = new SurveyStudy($this->unit_id); + } + + return $this->surveyStudy; + } + + public function load() { + parent::load(); + if ($this->unit_id && !$this->surveyStudy) { + $this->surveyStudy = new SurveyStudy(); + } + + return $this; + } + + /** + * @doc {inherit} + */ + public function find($id, $special = false, $props = []) { + parent::find($id, $special, $props); + $this->getStudy(); + } + + public function getUnitSessionExpirationData(UnitSession $unitSession) { + $data = []; + + $expire_invitation = (int) $this->surveyStudy->expire_invitation_after; + $grace_period = (int) $this->surveyStudy->expire_invitation_grace; + $expire_inactivity = (int) $this->surveyStudy->expire_after; + + if ($expire_inactivity === 0 && $expire_invitation === 0) { + return $data; + } else { + $now = time(); + + // Expire if the user started filling the survey and then stopped + // This uses "Inactivity Expiration" but can be overriden by "Start editing within" + $last_active = $this->getUnitSessionLastVisit($unitSession); + $expires = $expire_invitation_time = $expire_inactivity_time = 0; + if ($expire_inactivity !== 0 && $last_active != null && strtotime($last_active)) { + $expires = strtotime($last_active) + ($expire_inactivity * 60); + } + + // Get expiration time based on when invitation was sent or user arrived in unit but not reacted + // This uses "Start editing within" + $invitation_sent = $unitSession->created; + if ($expire_invitation !== 0 && $invitation_sent && strtotime($invitation_sent)) { + $expires = strtotime($invitation_sent) + ($expire_invitation * 60); + } + + // Get expiration time if user already started filling and there is a maximum time to spend on the survey + // This uses "finishing editing within" + $first_active = $this->getUnitSessionFirstVisit($unitSession); + $first_submit = $this->getUnitSessionFirstVisit($unitSession, 'survey_items_display.saved != "' . $invitation_sent . '"'); + if ($last_active && $last_active != $invitation_sent && + $grace_period !== 0 && + $first_active != null && strtotime($first_active) && + $first_submit != null && strtotime($first_submit) + ) { + $expires = strtotime($first_submit) + ($grace_period * 60); + } + + $data['expires'] = max(0, $expires); + $data['expired'] = ($data['expires'] > 0) && ($now > $data['expires']); + $data['queued'] = UnitSessionQueue::QUEUED_TO_END; + + return $data; + } + } + + public function getUnitSessionLastVisit(UnitSession $unitSession, $order = 'desc', $where = null) { + // use created (item render time) if viewed time is lacking + $arr = $this->db->select(array('COALESCE(`survey_items_display`.saved,`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') + ->where($where ? $where : '1=1') + ->order('survey_items_display.saved', $order) + ->order('survey_items_display.created', $order) + ->limit(1) + ->bindParams(array('session_id' => $unitSession->id, 'study_id' => $this->surveyStudy->id)) + ->fetch(); + + return isset($arr['last_viewed']) ? $arr['last_viewed'] : null; + } + + function getUnitSessionFirstVisit(UnitSession $unitSession, $where = null) { + return $this->getUnitSessionLastVisit($unitSession, 'asc', $where); + } + + public function getUnitSessionOutput(UnitSession $unitSession) { + try { + $request = new Request(array_merge($_POST, $_FILES)); + $run = $unitSession->runSession->getRun(); + $study = $this->surveyStudy; + + if (Request::isHTTPPostRequest() && !Session::canValidateRequestToken($request)) { + return ['redirect' => run_url($run->name)]; + } + + $unitSession->createSurveyStudyRecord(); + + if ($study->use_paging) { + return $this->processPagedStudy($request, $study, $unitSession); + } else { + return $this->processStudy($request, $study, $unitSession); + } + } catch (Exception $e) { + if ($this->db->retryTransaction($e) && $this->retryOutput) { + $this->retryOutput = false; + sleep(rand(1, 4)); + return $this->getUnitSessionOutput($unitSession); + } + + $data = [ + 'log' => $this->getLogMessage('error_survey', $e->getMessage()), + 'content' => '', + ]; + + formr_log_exception($e, __CLASS__ . '-' . $e->getCode()); + $this->db->logLastStatement($e); + + return $data; + } + } + + protected function processStudy($request, $study, $unitSession) { + if (Request::isHTTPPostRequest()) { + if ($unitSession->updateSurveyStudyRecord(array_merge($request->getParams(), $_FILES))) { + return ['redirect' => run_url($unitSession->runSession->getRun()->name), 'log' => $this->getLogMessage('survey_filling_out')]; + } + } + + $renderer = new SpreadsheetRenderer($study, $unitSession); + $renderer->processItems(); + if ($renderer->studyCompleted()) { + return ['end_session' => true, 'move_on' => true, 'log' => $this->getLogMessage('survey_completed')]; + } else { + return ['content' => $renderer->render()]; + } + } + + protected function processPagedStudy($request, $study, $unitSession) { + $renderer = new PagedSpreadsheetRenderer($study, $unitSession); + $renderer->setRequest($request); + + if (Request::isHTTPPostRequest()) { + $options = $renderer->getPostedItems(); + if ($unitSession->updateSurveyStudyRecord($options['posted'])) { + Session::set('is-survey-post', true); // FIX ME + return ['redirect' => $options['next_page'], 'log' => $this->getLogMessage('survey_filling_out')]; + } + } + + $renderer->processItems(); + if ($renderer->redirect) { + return ['redirect' => $renderer->redirect]; + } + + if ($renderer->studyCompleted()) { + return ['end_session' => true, 'move_on' => true, 'log' => $this->getLogMessage('survey_completed')]; + } else { + return ['content' => $renderer->render()]; + } + } + +} diff --git a/application/Model/RunUnit/Wait.php b/application/Model/RunUnit/Wait.php new file mode 100644 index 000000000..a35a6abbe --- /dev/null +++ b/application/Model/RunUnit/Wait.php @@ -0,0 +1,71 @@ +relative_to = trim((string)$this->relative_to); + $this->wait_minutes = trim((string)$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 == '' || !$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 previous unit + $this->relative_to = $this->default_relative_to; + $this->has_relative_to = true; + } + + return $this->has_relative_to; + } + + protected function setDefaultRelativeTo(UnitSession $unitSession = null) { + //formr_log(!$this->has_relative_to && !$this->has_relative_to, 'setDefaultRelativeTo'); + + if ($unitSession && $this->has_wait_minutes && !$this->has_relative_to) { + // Get previous unit session creation date + $q = "SELECT id, created FROM survey_unit_sessions WHERE id < {$unitSession->id} AND run_session_id = {$unitSession->runSession->id} ORDER BY id DESC LIMIT 1"; + $result = $this->db->query($q, true)->fetch(PDO::FETCH_ASSOC); + + if ($result) { + $this->default_relative_to = json_encode($result['created']); + return; + } + } + } + + public function getUnitSessionExpirationData(UnitSession $unitSession) { + $this->setDefaultRelativeTo($unitSession); + return parent::getUnitSessionExpirationData($unitSession); + } + + public function getUnitSessionOutput(UnitSession $unitSession) { + $output = []; + $expiration = $this->getUnitSessionExpirationData($unitSession); + $output['wait_opencpu'] = !empty($expiration['check_failed']); + + if (empty($expiration['expired']) && !$unitSession->isExecutedByCron() && empty($expiration['check_failed'])) { + $output['end_session'] = true; + $output['run_to'] = $this->body; + $output['log'] = $this->getLogMessage('wait_ended_by_user'); + } elseif ($expiration['expired'] === true) { + $output['end_session'] = true; + $output['move_on'] = true; + $output['log'] = $this->getLogMessage('wait_ended'); + } else { + // maybe errors + $output['wait_user'] = true; + } + + return $output; + } + +} diff --git a/application/Model/Shuffle.php b/application/Model/Shuffle.php deleted file mode 100644 index f563e9f07..000000000 --- a/application/Model/Shuffle.php +++ /dev/null @@ -1,102 +0,0 @@ -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 = '
    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; - - return parent::runDialog($dialog, 'fa-random fa-1-5x'); - } - - public function removeFromRun($special = null) { - return $this->delete($special); - } - - public function randomise_into_group() { - return mt_rand(1, $this->groups); - } - - public function test() { - - 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.

    '; - } - - 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 deleted file mode 100644 index d78737f42..000000000 --- a/application/Model/Site.php +++ /dev/null @@ -1,334 +0,0 @@ - 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'; - } - $mail->SMTPAuth = true; // turn on SMTP authentication - $mail->Username = $settings['email']['username']; // SMTP username - $mail->Password = $settings['email']['password']; // SMTP password - - $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 deleted file mode 100644 index a1e9fea5f..000000000 --- a/application/Model/SkipBackward.php +++ /dev/null @@ -1,35 +0,0 @@ - - -

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

    - Save. - Test

    '; - - - $dialog = $prepend . $dialog; - - return parent::runDialog($dialog); - } - -} diff --git a/application/Model/SkipForward.php b/application/Model/SkipForward.php deleted file mode 100644 index 7380cf31a..000000000 --- a/application/Model/SkipForward.php +++ /dev/null @@ -1,14 +0,0 @@ -objectFromArray($array); - - $objWriter = PHPExcel_IOFactory::createWriter($objPHPExcel, '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')); - - $objPHPExcel = new PHPExcel(); - $current = current($array); - if (!$current) { - return $objPHPExcel; - } - array_unshift($array, array_keys($current)); - $objPHPExcel->getSheet(0)->fromArray($array); - - return $objPHPExcel; - } - - /** - * - * @param PDOStatement $stmt - * @return PHPExcel; - */ - protected function objectFromPDOStatement(PDOStatement $stmt) { - $PHPExcel = new PHPExcel(); - $PHPExcelSheet = $PHPExcel->getSheet(0); - - list ($startColumn, $startRow) = PHPExcel_Cell::coordinateFromString('A1'); - $writeColumns = true; - while($row = $stmt->fetch(PDO::FETCH_ASSOC)) { - if ($writeColumns) { - $columns = array_keys($row); - $currentColumn = $startColumn; - foreach ($columns as $cellValue) { - $PHPExcelSheet->getCell($currentColumn . $startRow)->setValue($cellValue); - ++$currentColumn; - } - ++$startRow; - $writeColumns = false; - } - $currentColumn = $startColumn; - foreach ($row as $cellValue) { - $PHPExcelSheet->getCell($currentColumn . $startRow)->setValue($cellValue); - ++$currentColumn; - } - ++$startRow; - } - return $PHPExcel; - } - 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 { - $phpExel = $this->objectFromPDOStatement($stmt); - $phpExelWriter = PHPExcel_IOFactory::createWriter($phpExel, 'CSV'); - - header('Content-Disposition: attachment;filename="' . $filename . '.csv"'); - header('Cache-Control: max-age=0'); - header('Content-Type: text/csv'); - $phpExelWriter->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 { - $phpExel = $this->objectFromPDOStatement($stmt); - $phpExelWriter = PHPExcel_IOFactory::createWriter($phpExel, 'CSV'); - $phpExelWriter->setDelimiter("\t"); - $phpExelWriter->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? - $phpExelWriter->save('php://output'); - exit; - } else { - $phpExelWriter->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 { - $phpExel = $this->objectFromPDOStatement($stmt); - $phpExelWriter = PHPExcel_IOFactory::createWriter($phpExel, 'CSV'); - $phpExelWriter->setDelimiter(';'); - $phpExelWriter->setEnclosure('"'); - - if ($savefile === null) { - header('Content-Disposition: attachment;filename="' . $filename . '.csv"'); - header('Cache-Control: max-age=0'); - header('Content-Type: text/csv'); - $phpExelWriter->save('php://output'); - exit; - } else { - $phpExelWriter->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 { - $phpExel = $this->objectFromPDOStatement($stmt); - $phpExelWriter = PHPExcel_IOFactory::createWriter($phpExel, 'Excel5'); - header('Content-Disposition: attachment;filename="' . $filename . '.xls"'); - header('Cache-Control: max-age=0'); - header('Content-Type: application/vnd.ms-excel'); - $phpExelWriter->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 { - $phpExel = $this->objectFromPDOStatement($stmt); - $phpExelWriter = PHPExcel_IOFactory::createWriter($phpExel, 'Excel2007'); - header('Content-Disposition: attachment;filename="' . $filename . '.xlsx"'); - header('Cache-Control: max-age=0'); - header('Content-Type: application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'); - $phpExelWriter->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')); - - $objPHPExcel = new PHPExcel(); - $objPHPExcel->getDefaultStyle()->getFont()->setName('Helvetica'); - $objPHPExcel->getDefaultStyle()->getFont()->setSize(16); - $objPHPExcel->getDefaultStyle()->getAlignment()->setWrapText(true); - $sheet_index = $objPHPExcel->getSheetCount() - 1; - - if (is_array($choices) && count($choices) > 0): - $objPHPExcel->createSheet(); - $sheet_index++; - array_unshift($choices, array_keys(current($choices))); - $objPHPExcel->getSheet($sheet_index)->getDefaultColumnDimension()->setWidth(20); - $objPHPExcel->getSheet($sheet_index)->getColumnDimension('A')->setWidth(20); # list_name - $objPHPExcel->getSheet($sheet_index)->getColumnDimension('B')->setWidth(20); # name - $objPHPExcel->getSheet($sheet_index)->getColumnDimension('C')->setWidth(30); # label - - $objPHPExcel->getSheet($sheet_index)->fromArray($choices); - $objPHPExcel->getSheet($sheet_index)->setTitle('choices'); - $objPHPExcel->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); - } - - $objPHPExcel->createSheet(); - $sheet_index++; - $objPHPExcel->getSheet($sheet_index)->getDefaultColumnDimension()->setWidth(20); - $objPHPExcel->getSheet($sheet_index)->getColumnDimension('A')->setWidth(20); # item - $objPHPExcel->getSheet($sheet_index)->getColumnDimension('B')->setWidth(20); # value - - $objPHPExcel->getSheet($sheet_index)->fromArray($sttgs); - $objPHPExcel->getSheet($sheet_index)->setTitle('settings'); - $objPHPExcel->getSheet($sheet_index)->getStyle('A1:C1')->applyFromArray(array('font' => array('bold' => true))); - endif; - - array_unshift($items, array_keys(current($items))); - $objPHPExcel->getSheet(0)->getColumnDimension('A')->setWidth(20); # type - $objPHPExcel->getSheet(0)->getColumnDimension('B')->setWidth(20); # name - $objPHPExcel->getSheet(0)->getColumnDimension('C')->setWidth(30); # label - $objPHPExcel->getSheet(0)->getColumnDimension('D')->setWidth(3); # optional - $objPHPExcel->getSheet(0)->getStyle('D1')->getAlignment()->setWrapText(false); - - $objPHPExcel->getSheet(0)->fromArray($items); - $objPHPExcel->getSheet(0)->setTitle('survey'); - $objPHPExcel->getSheet(0)->getStyle('A1:H1')->applyFromArray(array('font' => array('bold' => true))); - - return $objPHPExcel; - } - - public function exportItemTableXLSX(Survey $study) { - $items = $study->getItemsForSheet(); - $choices = $study->getChoicesForSheet(); - $filename = $study->name; - - try { - $objPHPExcel = $this->getSheetsFromArrays($items, $choices, $study->settings); - $objWriter = PHPExcel_IOFactory::createWriter($objPHPExcel, 'Excel2007'); - - 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 { - $objPHPExcel = $this->getSheetsFromArrays($items, $choices, $study->settings); - - $objWriter = PHPExcel_IOFactory::createWriter($objPHPExcel, 'Excel5'); - - 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 PHPExcel object from a read-only reader - $filetype = PHPExcel_IOFactory::identify($filepath); - $phpExcelReader = PHPExcel_IOFactory::createReader($filetype); - $phpExcelReader->setReadDataOnly(true); - - /* @var $phpExcel PHPExcel */ - $phpExcel = $phpExcelReader->load($filepath); - - // Gather sheets to be read - if ($phpExcel->sheetNameExists('survey')) { - $surveySheet = $phpExcel->getSheetByName('survey'); - } else { - $surveySheet = $phpExcel->getSheet(0); - } - - if ($phpExcel->sheetNameExists('choices') && $phpExcel->getSheetCount() > 1) { - $choicesSheet = $phpExcel->getSheetByName('choices'); - } elseif ($phpExcel->getSheetCount() > 1) { - $choicesSheet = $phpExcel->getSheet(1); - } - - if (isset($choicesSheet)) { - $this->readChoicesSheet($choicesSheet); - } - - $this->readSurveySheet($surveySheet); - - } catch (PHPExcel_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(PHPExcel_Worksheet $worksheet) { - // Get worksheet dimensions - // non-allowed columns will be ignored, allows to specify auxiliary information if needed - $skippedColumns = $columns = array(); - $colCount = PHPExcel_Cell::columnIndexFromString($worksheet->getHighestDataColumn()); - $rowCount = $worksheet->getHighestDataRow(); - - for ($i = 0; $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 PHPExcel_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 PHPExcel_Cell */ - if (is_null($cell)) { - continue; - } - - $colNumber = PHPExcel_Cell::columnIndexFromString($cell->getColumn()) - 1; - 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 PHPExcel_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 PHPExcel_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(PHPExcel_Worksheet $worksheet) { - $callStartTime = microtime(true); - // non-allowed columns will be ignored, allows to specify auxiliary information if needed - - $skippedColumns = $columns = array(); - $colCount = PHPExcel_Cell::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; - for ($i = 0; $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 PHPExcel_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 PHPExcel_Cell */ - if (is_null($cell)) { - continue; - } - - $colNumber = PHPExcel_Cell::columnIndexFromString($cell->getColumn()) - 1; - 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 PHPExcel_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 deleted file mode 100644 index 1e56e2f05..000000000 --- a/application/Model/Survey.php +++ /dev/null @@ -1,2251 +0,0 @@ - 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 .= ' -
    -
    - '; - $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) - 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`.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 - created = COALESCE(created,NOW()), - answer = :answer, - saved = :saved, - shown = :shown, - shown_relative = :shown_relative, - answered = :answered, - answered_relative = :answered_relative, - 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(){ - {$showif} -})()"; - } - - // 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 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, - `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`.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 - (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'); - - $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; - - 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 ($oItem->type === 'submit') { - $inPage = false; - } - } - - return $pageItems; - } - - protected function renderNextItems() { - - $this->dbh->beginTransaction(); - - $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) { - $action = $action !== null ? $action : run_url($this->run_name); - $enctype = 'multipart/form-data'; # maybe make this conditional application/x-www-form-urlencoded - - $ret = '
    '; - - /* pass on hidden values */ - $ret .= ''; - $ret .= ''; - - 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); - - $ret .= ' -
    -
    -
    ' . $prog . '%
    -
    -
    '; - - if (!empty($this->validation_errors)) { - $ret .= '
    ' - . '' - . '' - . $this->render_errors($this->validation_errors) . - '
    '; - } - - return $ret; - } - - 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 "
    "; /* close form */ - } - - /** - * - * @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() { - $ended = $this->dbh->exec( - "UPDATE `{$this->results_table}` SET `ended` = NOW() WHERE `session_id` = :session_id AND `study_id` = :study_id AND `ended` IS null", array('session_id' => $this->session_id, 'study_id' => $this->id) - ); - return parent::end(); - } - - protected 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() { - $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); - 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 - if ($this->called_by_cron) { - if ($this->hasExpired()) { - $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); - //check if user session has a valid form token for POST requests - 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->createSurveyStmt(); - $new_items = $data['new_items']; - $result_columns = $data['result_columns']; - - //$unchanged = array_intersect_assoc($old_items, $new_items); - $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->doWeHaveRealData() && !$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 columns: " . $deleted_columns_string; - } - if($this->doWeHaveRealData() && $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->doWeHaveAnyDataAtAll()) { - 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 createSurveyStmt() { - // 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) - 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 = " - CREATE TABLE `{$this->results_table}` ( - `session_id` INT UNSIGNED NOT NULL , - `study_id` INT UNSIGNED NOT NULL , - `created` DATETIME NULL DEFAULT NULL , - `modified` DATETIME NULL DEFAULT NULL , - `ended` DATETIME NULL DEFAULT NULL , - - $columns_string - - PRIMARY KEY (`session_id`) , - INDEX `idx_survey_results_survey_studies` (`study_id` ASC) , - INDEX `idx_ending` (`session_id` DESC, `study_id` ASC, `ended` ASC) , - CONSTRAINT - FOREIGN KEY (`session_id` ) - REFERENCES `survey_unit_sessions` (`id` ) - ON DELETE CASCADE - ON UPDATE NO ACTION, - CONSTRAINT - FOREIGN KEY (`study_id` ) - REFERENCES `survey_studies` (`id` ) - 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("`survey_items`.item_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`, - `survey_items_display`.`answer`, - `survey_items_display`.`created`, - `survey_items_display`.`saved`, - `survey_items_display`.`shown`, - `survey_items_display`.`shown_relative`, - `survey_items_display`.`answered`, - `survey_items_display`.`answered_relative`, - `survey_items_display`.`displaycount`, - `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(" - `survey_run_sessions`.session, - `survey_items_display`.`session_id` as `unit_session_id`, - `survey_items`.`name` as `item_name`, - `survey_items_display`.`item_id`, - `survey_items_display`.`answer`, - `survey_items_display`.`created`, - `survey_items_display`.`saved`, - `survey_items_display`.`shown`, - `survey_items_display`.`shown_relative`, - `survey_items_display`.`answered`, - `survey_items_display`.`answered_relative`, - `survey_items_display`.`displaycount`, - `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(' - `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 doWeHaveAnyDataAtAll($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 doWeHaveRealData($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->doWeHaveRealData()) { - $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 - FROM `{$this->results_table}` AS x, (SELECT @row:=0) AS r - WHERE 1 - -- put some where clause here - ORDER BY took - ) AS t1, - ( - SELECT COUNT(*) as 'count' - FROM `{$this->results_table}` x - WHERE 1 - -- put same where clause here - ) AS t2 - -- 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 = '') { - - 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'); - } - - /** - * 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 = $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) { - $delQ = "ALTER TABLE `{$this->results_table}`" . implode(',', $deleteQuery); - $this->dbh->query($delQ); - $actions[] = "Deleted columns: $deleted_columns_string."; - } - - // we only get here if the deletion stuff was harmless, allowed or did not happen - if ($addQuery) { - $addQ = "ALTER TABLE `{$this->results_table}`" . implode(',', $addQuery); - $this->dbh->query($addQ); - $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/SurveyStudy.php b/application/Model/SurveyStudy.php new file mode 100644 index 000000000..b2bd26dbc --- /dev/null +++ b/application/Model/SurveyStudy.php @@ -0,0 +1,1230 @@ +assignProperties($options); + $this->load($id, $options); + } + + /** + * Get survey by id + * + * @param int $id + * @return SurveyStudy + */ + public static function loadById($id) { + return new SurveyStudy((int) $id); + } + + /** + * Get survey by name + * + * @param string $name + * @return SurveyStudy + */ + public static function loadByName($name) { + $options = ['name' => $name]; + return new SurveyStudy(null, $options); + } + + /** + * + * @param User $user + * @param string $name + * @return \SurveyStudy + */ + public static function loadByUserAndName(User $user, $name) { + $options = [ + 'name' => $name, + 'user_id' => $user->id, + ]; + return new SurveyStudy(null, $options); + } + + protected function load($id, $options) { + if (!$options || !is_array($options)) { + $options = []; + } + + if ($id) { + $options['id'] = (int) $id; + } + + if ($options && ($vars = $this->db->findRow('survey_studies', $options))) { + $this->assignProperties($vars); + if (!$this->results_table) { + $this->results_table = $this->name; + } + + $this->valid = true; + } + } + + /** + * + * @param array $file + * + * @return boolean + */ + + public function createFromFile($file, $options = []) { + // Create the corresponding entry in survey_units to get the ID + $id = RunUnitFactory::make(new Run(), ['type' => 'Survey'])->create($options)->id; + $this->assignProperties($file); + + $this->id = $id; + $this->name = preg_filter("/^([a-zA-Z][a-zA-Z0-9_]{2,64})(-[a-z0-9A-Z]+)?\.[a-z]{3,4}$/", "$1", basename($file['name'])); + $this->results_table = substr("s" . $this->id . '_' . $this->name, 0, 64); + $this->created = mysql_now(); + $this->modified = mysql_now(); + $this->user_id = Site::getCurrentUser()->id; + + $results_table = substr("s" . $this->id . '_' . $this->name, 0, 64); + + if ($this->db->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; + $this->save(); + + return true; + } + + public static function createFromData($data, $options = []) { + unset($options['survey_data']); + if (empty($data->name) || empty($data->items)) { + return false; + } + + $study = self::loadByUserAndName(Site::getCurrentUser(), $data->name); + + if ($study->valid) { + // Survey exists so use existing data + $unit = [ + 'id'=> $study->id, + 'study_id' => $study->id, + 'type' => 'Survey', + 'importing' => true, + ]; + $options['add_to_run'] = true; + + RunUnitFactory::make($options['run'], $unit)->create($options); + } else { + $study = new SurveyStudy(); + $file = ['name' => $data->name .'.xlsx']; + unset($options['is_import']); + if (!$study->createFromFile($file, $options)) { + return false; + } + + // save settings + $data->settings = isset($data->settings) ? (array) $data->settings : []; + $data->settings = array_merge([ + 'maximum_number_displayed' => 0, + 'displayed_percentage_maximum' => 100, + 'add_percentage_points' => 0, + + ], $data->settings); + $study->assignProperties($data->settings); + $study->is_new = true; + $study->save(); + } + + // Mock SpreadSheetReader to use existing mechanism of creating survey items + $reader = new SpreadsheetReader(); + foreach ($data->items as $item) { + if (!empty($item->choices) && !empty($item->choice_list)) { + foreach ($item->choices as $name => $label) { + $reader->choices[] = array( + 'list_name' => $item->choice_list, + 'name' => $name, + 'label' => $label, + ); + } + unset($item->choices); + } + $reader->addSurveyItem((array) $item); + } + + if ($study->saveUploadedItemsFromReader($reader)) { + if (!empty($study->warnings)) { + alert('
    • ' . implode("
    • ", $study->warnings) . '
    ', 'alert-warning'); + } + + if (!empty($study->messages)) { + alert('
    • ' . implode("
    • ", $study->messages) . '
    ', 'alert-info'); + } + + return true; + } else { + alert('
    • ' . implode("
    • ", $study->errors) . '
    ', 'alert-danger'); + return false; + } + + return $study; + } + + public function uploadItems($file, $can_delete = false, $is_new = false) { + umask(0002); + ini_set('memory_limit', Config::get('memory_limit.survey_upload_items')); + + $filepath = $file['tmp_name']; + $filename = $file['name']; + $this->can_delete = $can_delete; + $this->is_new = $is_new; + + $this->messages[] = "Items sheet ({$filename}) uploaded to survey {$this->name}."; + + $reader = new SpreadsheetReader(); + $reader->readItemTableFile($filepath); + + $this->errors = array_merge($this->errors, $reader->errors); + $this->warnings = array_unique(array_merge($this->warnings, $reader->warnings)); + $this->messages = array_unique(array_merge($this->messages, $reader->messages)); + + // if items are ok, make actual survey + if (empty($this->errors) && $this->saveUploadedItemsFromReader($reader)) { + 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($filepath) && (move_uploaded_file($filepath, $file) || rename($filepath, $file))) { + $this->original_file = $filename; + $this->modified = mysql_datetime(); + $this->save(); + } else { + alert('Unable to save original uploaded file', 'alert-warning'); + } + + return true; + } else { + alert('
    • ' . implode("
    • ", $this->errors) . '
    ', 'alert-danger'); + return false; + } + } + + protected function resultsTableExists() { + if (!$this->results_table) { + return false; + } + + return $this->db->table_exists($this->results_table); + } + + protected function toArray() { + return [ + 'id' => $this->id, + 'user_id' => $this->user_id, + 'name' => $this->name, + 'results_table' => $this->results_table, + 'valid' => $this->valid, + 'maximum_number_displayed' => $this->maximum_number_displayed, + 'displayed_percentage_maximum' => $this->displayed_percentage_maximum, + 'add_percentage_points' => $this->add_percentage_points, + 'expire_after' => $this->expire_after, + 'expire_invitation_after' => $this->expire_invitation_after, + 'expire_invitation_grace' => $this->expire_invitation_grace, + 'enable_instant_validation' => $this->enable_instant_validation, + 'original_file' => $this->original_file, + 'google_file_id' => $this->google_file_id, + 'unlinked' => $this->unlinked, + 'hide_results' => $this->hide_results, + 'created' => $this->created, + 'modified' => $this->modified, + ]; + } + + /** + * 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->db->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->db->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|JsonReader $reader + * @return boolean + */ + protected function saveUploadedItemsFromReader($reader) { + + // Get old choice lists for getting old items + $choiceLists = $this->getChoices(); + $itemFactory = new ItemFactory($choiceLists); + + // 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 = $itemFactory->make($item)) !== false) { + $old_items[$item['name']] = $object->getResultField(); + if ($object->isStoredInResultsTable()) { + $old_items_in_results[] = $item['name']; + } + } + } + + try { + + $this->db->beginTransaction(); + $data = $this->addItems($reader); + $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 = $itemFactory->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->hasRealData() && !$this->can_delete) { + $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->hasRealData() && $this->can_delete && !$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->db, 'quote'), $actually_deleted)); + $studyId = (int) $this->id; + $delQ = "DELETE FROM survey_items WHERE `name` IN ($toDelete) AND study_id = $studyId"; + $this->db->query($delQ); + } + + // we start fresh if it's a new creation, no results table exist or it is completely empty + if ($this->is_new || !$this->resultsTableExists() || !$this->hasData()) { + if ($this->is_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 '{$this->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->db->commit(); + return true; + } catch (Exception $e) { + $this->db->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 + * + * @param SpreadsheetReader|JsonReader $reader + * @return array array(new_items, result_columns) + */ + protected function addItems($reader) { + // Save new choices and re-build the item factory + $this->addChoices($reader); + $choice_lists = $this->getChoices(); + $itemFactory = new ItemFactory($choice_lists); + + $definedColumns = [ + '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 + ]; + + // Prepare SQL statement for adding items + $UPDATES = implode(', ', get_duplicate_update_string($definedColumns)); + $addStmt = $this->db->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 ($reader->survey as $row_number => $row) { + $item = $itemFactory->make($row); + if (!$item) { + $this->errors[] = __("Row %s: Type %s is invalid.", $row_number, array_val($reader->survey[$row_number], 'type')); + unset($reader->survey[$row_number]); + continue; + } + + $val_results = $item->validate(); + if (!empty($val_results['val_errors'])) { + $this->errors = $this->errors + $val_results['val_errors']; + unset($reader->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 (!knitting_needed($item->label) && !$item->label_parsed) { + try { + $markdown = $reader->parsedown->text($item->label); + } catch (Exception $e) { + formr_log_exception($e, 'PARSEDOWN.TEXT'); + $markdown = $item->label; + } + $item->label_parsed = $markdown; + if (mb_substr_count($markdown, "

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

    (.+)

    $@", trim($markdown), $matches)) { + $item->label_parsed = $matches[1]; + } + } + + foreach ($definedColumns 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(); + $itemFactory = new ItemFactory($choice_lists); + + $raw_items = $this->getItems($columns, $whereIn); + + $items = array(); + foreach ($raw_items as $row) { + $item = $itemFactory->make($row); + $items[$item->name] = $item; + } + return $items; + } else { + return array(); + } + } + + public function getItemsInResultsTable() { + if (($existingColumns = $this->db->getTableDefinition($this->results_table, 'Field'))) { + $existingColumns = array_keys($existingColumns); + } + + $items = $this->getItems(); + $names = array(); + $itemFactory = new ItemFactory(array()); + foreach ($items as $item) { + $item = $itemFactory->make($item); + if ($item->isStoredInResultsTable() && in_array($item->name, $existingColumns)) { + $names[] = $item->name; + } + } + return $names; + } + + private function addChoices($reader) { + // delete cascades to item display ?? FIXME so maybe not a good idea to delete then + $definedColumns = ['list_name', 'name', 'label', 'label_parsed']; + $deleted = $this->db->delete('survey_item_choices', array('study_id' => $this->id)); + $addChoiceStmt = $this->db->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 ($reader->choices as $choice) { + $choice['label_parsed'] = null; + + if (isset($choice['list_name']) && isset($choice['name']) && isset($choice['label'])) { + if (!knitting_needed($choice['label']) && empty($choice['label_parsed'])) { // if the parsed label is constant + $markdown = $reader->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 ($definedColumns 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 , + `created` DATETIME NULL DEFAULT NULL , + `modified` DATETIME NULL DEFAULT NULL , + `ended` DATETIME NULL DEFAULT NULL , + `expired` DATETIME NULL DEFAULT NULL , + + $columns_string + + PRIMARY KEY (`session_id`) , + INDEX `idx_survey_results_survey_studies` (`study_id` ASC) , + INDEX `idx_ending` (`session_id` DESC, `study_id` ASC, `ended` ASC) , + CONSTRAINT + FOREIGN KEY (`session_id` ) + REFERENCES `survey_unit_sessions` (`id` ) + ON DELETE CASCADE + ON UPDATE NO ACTION, + CONSTRAINT + FOREIGN KEY (`study_id` ) + REFERENCES `survey_studies` (`id` ) + ON DELETE NO ACTION + ON UPDATE NO ACTION) + ENGINE = InnoDB"; + return $create; + } + + private function createResultsTable($syntax) { + if ($this->deleteResults()) { + $drop = $this->db->query("DROP TABLE IF EXISTS `{$this->results_table}` ;"); + $drop->execute(); + } else { + return false; + } + + $create_table = $this->db->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->db->select($columns); + $select->from('survey_items'); + $select->where(array('study_id' => $this->id)); + if ($whereIn) { + $select->whereIn($whereIn['field'], $whereIn['values']); + } + $select->order("item_order"); + return $select->fetchAll(); + } + + public function getItemsForSheet() { + $get_items = $this->db->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; + } + + 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->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->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->db->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->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->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->db->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`, + `survey_items_display`.`answer`, + `survey_items_display`.`created`, + `survey_items_display`.`saved`, + `survey_items_display`.`shown`, + `survey_items_display`.`shown_relative`, + `survey_items_display`.`answered`, + `survey_items_display`.`answered_relative`, + `survey_items_display`.`displaycount`, + `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->unlinked) { + return array(); + } + ini_set('memory_limit', Config::get('memory_limit.survey_get_results')); + + $filter_select = $this->db->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->db->select(" + `survey_run_sessions`.session, + `survey_items_display`.`session_id` as `unit_session_id`, + `survey_items`.`name` as `item_name`, + `survey_items_display`.`item_id`, + `survey_items_display`.`answer`, + `survey_items_display`.`created`, + `survey_items_display`.`saved`, + `survey_items_display`.`shown`, + `survey_items_display`.`shown_relative`, + `survey_items_display`.`answered`, + `survey_items_display`.`answered_relative`, + `survey_items_display`.`displaycount`, + `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->db->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 hasData() { + $this->result_count = $this->getResultCount(); + if (($this->result_count["real_users"] + $this->result_count['testers']) > 0) { + return true; + } else { + return false; + } + } + + protected function hasRealData() { + $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->db->query("TRUNCATE TABLE `{$this->results_table}`"); + $delete_item_disp = $this->db->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->hasRealData()) { + $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->db->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 + FROM `{$this->results_table}` AS x, (SELECT @row:=0) AS r + WHERE 1 + -- put some where clause here + ORDER BY took + ) AS t1, + ( + SELECT COUNT(*) as 'count' + FROM `{$this->results_table}` x + WHERE 1 + -- put same where clause here + ) AS t2 + -- 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->db->query($get, true); + $time = $get->fetch(PDO::FETCH_NUM); + $time = round($time[0] / 60, 3); # seconds to minutes + + return $time; + } + return ''; + } + + public function delete() { + if ($this->deleteResults()) { + $this->db->query("DROP TABLE IF EXISTS `{$this->results_table}`"); + if (($filename = $this->getOriginalFileName())) { + @unlink(Config::get('survey_upload_dir') . '/' . $filename); + } + + $this->db->query('DELETE FROM survey_items WHERE study_id = ' . $this->id); + return $this->db->query('DELETE FROM survey_units WHERE id = ' . $this->id); + } + + return false; + } + + /** + * 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->db->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->db->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->db->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->original_file; + //return $this->db->findValue('survey_studies', array('id' => $this->id), 'original_file'); + } + + public function getGoogleFileId() { + return $this->google_file_id; + //return $this->db->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; + } + + public function getOrderedItemsIds() { + $get_items = $this->db->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); + } + + public function getSettings() { + $keys = [ + 'maximum_number_displayed', 'displayed_percentage_maximum', 'add_percentage_points', + 'enable_instant_validation', 'expire_after', 'google_file_id', 'unlinked', + 'expire_invitation_after', 'expire_invitation_grace', 'hide_results', 'use_paging', + ]; + $settings = []; + + foreach ($keys as $key) { + $settings[$key] = $this->{$key}; + } + + return $settings; + } +} diff --git a/application/Model/UnitSession.php b/application/Model/UnitSession.php index 1d4458337..a67a124e0 100644 --- a/application/Model/UnitSession.php +++ b/application/Model/UnitSession.php @@ -1,68 +1,755 @@ dbh = $fdb; - $this->unit_id = $unit_id; - $this->run_session_id = $run_session_id; - $this->id = $unit_session_id; - - $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 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(); + * A UnitSession needs a RunUnit to operate and belongs to a RunSession + * + * @param RunSession $runSession + * @param RunUnit $runUnit + * @param array $options An array of other options used to fetch a unit ID + */ + public function __construct(RunSession $runSession, RunUnit $runUnit = null, $options = []) { + parent::__construct(); + + $this->runSession = $runSession; + $this->runUnit = $runUnit; + $this->assignProperties($options); + if (isset($options['id'], $options['load'])) { + $this->load(); + } + } + + public function create($new_current_unit = true) { + // only one can be current unit session at all times + try { + $this->db->beginTransaction(); + $session = $this->assignProperties([ + 'unit_id' => $this->runUnit->id, + 'run_session_id' => $this->runSession->id > 0 ? $this->runSession->id : null, + 'created' => mysql_now(), + ]); + + $this->id = $this->db->insert('survey_unit_sessions', $session); + if ($this->runSession->id !== null && $new_current_unit) { + $this->runSession->currentUnitSession = $this; + $this->db->update('survey_run_sessions', ['current_unit_session_id' => $this->id], ['id' => $this->runSession->id]); + + $this->db->update('survey_unit_sessions', ['queued' => -9], [ + 'run_session_id' => $this->runSession->id, + 'id <>' => $this->id, + 'queued >' => 0, + ]); + } + + $this->db->commit(); + } catch (Exception $e) { + $this->db->rollBack(); + } + + return $this->load(); + } + + public function load() { + $columns = 'id, unit_id, run_session_id, created, expires, queued, result, result_log, ended, expired'; + if ($this->id !== null) { + $vars = $this->db->findRow('survey_unit_sessions', ['id' => (int)$this->id], $columns); + } else { + $run_session_id = $this->runSession ? $this->runSession->id : $this->run_session_id; + $unit_id = $this->runUnit ? $this->runUnit->id : $this->unit_id; + $vars = $this->db->findRow('survey_unit_sessions', ['run_session_id' => $run_session_id, 'unit_id' => $unit_id], $columns); + } + + if (!empty($vars['unit_id']) && !$this->runUnit) { + $this->runUnit = RunUnitFactory::make($this->runSession->getRun(), ['id' => $vars['unit_id']]); + } + + if ($vars) { + $this->assignProperties($vars); + $this->valid = true; } + + return $this; + } + + public function __sleep() { + return array('id', 'session', 'unit_id', 'created'); + } - if (!$vars) { - return; + public function execute() { + $this->execResults = []; + // Check if session has expired by getting relevant unit data + if ($this->isExpired()) { + $this->execResults['expired'] = true; + $this->execResults['move_on'] = true; + return $this->execResults; + } + + if (!empty($this->execResults['end_session'])) { + $this->execResults['move_on'] = true; + return $this->execResults; } - foreach ($vars as $property => $value) { - if (property_exists($this, $property)) { - $this->{$property} = $value; + if (($output = $this->runUnit->getUnitSessionOutput($this))) { + $this->logOutput($output); + unset($output['log']); + + foreach ($output as $key => $value) { + $this->execResults[$key] = $value; + } + } + + return $this->execResults; + } + + protected function isExpired() { + $expirationData = $this->runUnit->getUnitSessionExpirationData($this); + $this->logOutput($expirationData); + unset($expirationData['log']); + + $this->execResults = array_merge($this->execResults, $expirationData); + + if ($this->runUnit instanceof Pause || $this->runUnit instanceof Branch) { + $expiration_extension = Config::get('unit_session.queue_expiration_extension', '+10 minutes'); + if ($expirationData['check_failed'] === true || $expirationData['expire_relatively'] === false) { + // check again in x minutes something went wrong with ocpu evaluation + $expirationData['expires'] = mysql_datetime(strtotime($expiration_extension)); + $expirationData['queued'] = UnitSessionQueue::QUEUED_TO_EXECUTE; + } + } + + if (empty($expirationData['expires'])) { + return false; + } elseif(!empty($expirationData['end_session'])) { + $this->execResults['end_session'] = true; + return false; // ended NOT expired + } elseif ($expirationData['expires'] < time()) { + return true; + } elseif ($expirationData['queued']) { + $this->execResults['queue'] = [ + 'expires' => $expirationData['expires'], + 'queued' => $expirationData['queued'], + ]; + } + } + + protected function logOutput ($output) { + if (!empty($output['log'])) { + $this->assignProperties($output['log']); + $this->logResult(); + } + } + + /** + * Check if unit session should be queued + * ** ALWAYS CALL AFTER $this->isExpired() *** + * + * @return boolean + */ + protected function isQueuable() { + return !empty($this->execResults['queue']) && $this->runSession->getRun()->cron_active; + } + + public function expire() { + $unit = $this->runUnit; + + if ($unit->type === 'Survey') { + $query = "UPDATE `{$unit->surveyStudy->results_table}` SET `expired` = NOW() WHERE `session_id` = :session_id AND `study_id` = :study_id AND `ended` IS null"; + $params = ['session_id' => $this->id, 'study_id' => $unit->surveyStudy->id]; + try { + $this->db->exec($query, $params); + } catch (Exception $e) { + //formr_log_exception($e, 'RESULTS_TABLE: ' . $unit->surveyStudy->results_table); } + } + + $expired = $this->db->exec( + "UPDATE `survey_unit_sessions` SET + `expired` = NOW(), + `result` = 'expired', + `queued` = 0 + WHERE `id` = :id AND `unit_id` = :unit_id AND `ended` IS NULL LIMIT 1", + ['id' => $this->id, 'unit_id' => $unit->id] + ); + + return $expired === 1; + } + + public function end($reason = null) { + $unit = $this->runUnit; + + if ($unit->type == "Survey" || $unit->type == "External") { + if ($unit->type == "Survey") { + $query = "UPDATE `{$unit->surveyStudy->results_table}` SET `ended` = NOW() WHERE `session_id` = :session_id AND `study_id` = :study_id AND `ended` IS null"; + $params = array('session_id' => $this->id, 'study_id' => $unit->surveyStudy->id); + $this->db->exec($query, $params); + + $this->result = "survey_ended"; + } else if ($unit->type == "External") { + $this->result = "external_ended"; + } + } else { + if ($reason !== null) { + $this->result = $reason; + } else if ($unit->type == "Pause") { + $this->result = "pause_ended"; + } else if ($unit->type == "Wait") { + $this->result = "wait_ended"; + } else if ($unit->type == "Endpage") { + $this->result = $this->isExecutedByCron() ? 'ended_by_queue' : 'ended'; + } else { + //$this->result = "ended_other"; + } + } + + // @TODO import end from run unit + $ended = $this->db->exec( + "UPDATE `survey_unit_sessions` SET + `ended` = NOW(), + `result` = :result, + `result_log` = :result_log + WHERE `id` = :id AND `unit_id` = :unit_id AND `ended` IS NULL LIMIT 1", + [ + 'id' => $this->id, + 'unit_id' => $this->runUnit->id, + 'result' => $this->result, + 'result_log' => $this->result_log + ] + ); + + return $ended === 1; + } + + public function queue($output = null) { + if ($this->isQueuable()) { + UnitSessionQueue::addItem($this, $this->runUnit, $this->execResults['queue']); + } + } + + public function logResult() { + $log = $this->db->exec( + "UPDATE `survey_unit_sessions` SET + `result` = :result, + `result_log` = :result_log + WHERE `id` = :id AND `unit_id` = :unit_id AND `ended` IS NULL LIMIT 1", + [ + 'id' => $this->id, + 'unit_id' => $this->runUnit->id, + 'result' => $this->result, + 'result_log' => $this->result_log + ] + ); + + return $log; + } + + protected function hasOrderedStudyItems() { + /** @var SurveyStudy $study */ + $study = $this->runUnit->surveyStudy; + + $nr_items = $this->db->count('survey_items', array('study_id' => $study->id), 'id'); + $nr_display_items = $this->db->count('survey_items_display', array('session_id' => $this->id), 'id'); + + return $nr_display_items === $nr_items; + } + + /** + * Create a study record entry for this session. This is called only when + * operating on a Survey unit + * + * @return boolean + * @throws Exception + */ + public function createSurveyStudyRecord() { + /** @var SurveyStudy $study */ + $study = $this->runUnit->surveyStudy; + + if (!$this->db->entry_exists($this->table, ['id' => $this->id])) { + formr_error(404, 'Unit Session Not Found. Please contact study author'); } - } - public function __sleep() { - return array('id', 'session', 'unit_id', 'created'); - } + if (!$study->results_table || !$this->db->table_exists($study->results_table)) { + alert('A results table for this survey could not be found', 'alert-danger'); + throw new Exception("Results table '{$study->results_table}' not found!"); + } + + $entry = array( + 'session_id' => $this->id, + 'study_id' => $study->id, + ); + if (!$this->db->entry_exists($study->results_table, $entry)) { + $entry['created'] = mysql_now(); + $this->db->insert($study->results_table, $entry); + + $this->result = 'survey_started'; + $this->logResult(); + } else { + $this->db->update($study->results_table, array('modified' => mysql_now()), $entry); + } + + if (!$this->hasOrderedStudyItems()) { + // get the definition of the order + list($item_ids, $item_types) = $study->getOrderedItemsIds(); + + // define paramers to bind parameters + $display_order = null; + $item_id = null; + $page = 1; + $created = mysql_datetime(); + + $values = ''; + $valuesCount = 0; + $valuesMax = 60; + $sql_tpl = "INSERT INTO `survey_items_display` (`item_id`, `session_id`, `display_order`, `page`, `created`) VALUES %s ON DUPLICATE KEY UPDATE `display_order` = VALUES(`display_order`), `page` = VALUES(`page`)"; + $lastId = end($item_ids); + + foreach ($item_ids as $display_order => $item_id) { + $values .= '(' . $item_id . ',' . $this->id . ',' . $display_order . ',' . $page . ',' . $this->db->quote($created) . '),'; + $valuesCount++; + if (($valuesCount >= $valuesMax) || ($item_id == $lastId && $values)) { + $query = sprintf($sql_tpl, trim($values, ',')); + $this->db->query($query); + $values = ''; + $valuesCount = 0; + } + + //$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++; + } + } + } + } + + /** + * Save posted survey data to database + * + * @param array $posted An array of posted answers + * @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 updateSurveyStudyRecord($posted, $validate = true) { + /** @var SurveyStudy $study */ + $study = $this->runUnit->surveyStudy; + + // 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"]); + } + + /** + * 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 = $study->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->errors[$item_name] = $item->error; + } else { + $answer = $item->getReply($validInput); + if (is_array($answer)) { + $answer = json_encode($answer); + } + $update_data[$item_name] = $answer; + } + $item->value_validated = $item_value; + $items[$item_name] = $item; + } + } + + if (!empty($this->errors)) { + $this->validatedStudyItems = $items; + return false; + } + + $survey_items_display = $this->db->prepare( + "UPDATE `survey_items_display` SET + created = COALESCE(created,NOW()), + answer = :answer, + saved = :saved, + shown = :shown, + shown_relative = :shown_relative, + answered = :answered, + answered_relative = :answered_relative, + 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->id); + + try { + $this->db->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; + } + + $answer = $item->getReply($value); + if (is_array($answer)) { + $answer = json_encode($answer); + } + + $survey_items_display->bindValue(":item_id", $item->id); + $survey_items_display->bindValue(":answer", $answer); + $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 '{$study->results_table}'"); + } + + } //endforeach + // Update results table in one query + if ($update_data) { + $update_where = array( + 'study_id' => $study->id, + 'session_id' => $this->id, + ); + $this->db->update($study->results_table, $update_data, $update_where); + } + + $this->db->commit(); + return true; + } catch (Exception $e) { + $this->db->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; + } + + } + + public function isExecutedByCron() { + return $this->runSession->isCron(); + } + + /** + * Get data associated with this unit session based on query text + * + * @param string $q Query text to search for variables + * @param string $required + * @return array + */ + public function getRunData($q, $required = null) { + $runSession = $this->runSession; + $cache_key = Cache::makeKey(__METHOD__, $q, $required, $this->id, $runSession->id); + if (($data = Cache::get($cache_key))) { + return $data; + } + + $needed = $this->getRunDataNeeded($q, $required); + $surveys = $needed['matches']; + $results_tables = $needed['matches_results_tables']; + $matches_variable_names = $needed['matches_variable_names']; + $datasets = ['datasets' => []]; + + foreach ($surveys as $study_id => $survey_name) { + if (isset($datasets['datasets'][$survey_name])) { + continue; + } + + $results_table = $results_tables[$survey_name]; + $variables = []; + 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 (($runSession->id === null || $runSession->isTestingStudy()) && !in_array($results_table, get_db_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, get_db_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 + 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->db->prepare($q); + if (($runSession->id === null || $runSession->isTestingStudy()) && !in_array($results_table, get_db_non_session_tables())) { + $get_results->bindValue(':session_id', $this->id); + } else { + $get_results->bindValue(':run_session_id', $runSession->id); + } + if ($results_table == 'survey_unit_sessions') { + $get_results->bindValue(':run_id', $this->runSession->getRun()->id); + } + $get_results->execute(); + + $datasets['datasets'][$survey_name] = array(); + while ($res = $get_results->fetch(PDO::FETCH_ASSOC)) { + foreach ($res AS $var => $val) { + if (!isset($datasets['datasets'][$survey_name][$var])) { + $datasets['datasets'][$survey_name][$var] = array(); + } + $datasets['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'])) { + $datasets['.formr$last_action_date'] = "NA"; + $datasets['.formr$last_action_time'] = "NA"; + $last_action = $this->db->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' => $runSession->id, 'unit_id' => $this->runUnit->id), true + ); + if ($last_action !== false) { + $last_action_time = strtotime($last_action); + if (in_array('formr_last_action_date', $needed['variables'])) { + $datasets['.formr$last_action_date'] = "as.POSIXct('" . date("Y-m-d", $last_action_time) . "')"; + } + if (in_array('formr_last_action_time', $needed['variables'])) { + $datasets['.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'])) { + $datasets['.formr$login_link'] = "'" . run_url($runSession->getRun()->name, null, array('code' => $this->runSession->session)) . "'"; + } + if (in_array('formr_login_code', $needed['variables'])) { + $datasets['.formr$login_code'] = "'" . $this->runSession->session . "'"; + } + if (in_array('user_id', $needed['variables'])) { + $datasets['user_id'] = "'" . $this->runSession->session . "'"; + } + if (in_array('formr_nr_of_participants', $needed['variables'])) { + $count = (int) $this->db->count('survey_run_sessions', array('run_id' => $runSession->getRun()->id), 'id'); + $datasets['.formr$nr_of_participants'] = (int) $count; + } + if (in_array('formr_session_last_active', $needed['variables']) && $runSession->id) { + $last_access = $this->db->findValue('survey_run_sessions', array('id' => $runSession->id), 'last_access'); + if ($last_access) { + $datasets['.formr$session_last_active'] = "as.POSIXct('" . date("Y-m-d H:i:s T", strtotime($last_access)) . "')"; + } + } + } + + if ($needed['token_add'] !== null && !isset($datasets['datasets'][$needed['token_add']])) { + $datasets['datasets'][$needed['token_add']] = []; + } + + Cache::set($cache_key, $datasets); + return $datasets; + } + + protected function getRunDataNeeded($q, $token_add = null) { + $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 + $surveys = $this->runSession->getRun()->getAllSurveys(); + + // also add some "global" formr tables + $nu_tables = get_db_non_user_tables(); + $non_user_tables = array_keys($nu_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 + $study = $this->runUnit->surveyStudy; + $table_ids[] = $study->id; + $tables[] = $study->name; + $results_tables[$study->name] = $study->results_table; + } + + // 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 ($surveys 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']; + } + } + + 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 || preg_match("/\b$table_name\b/", (string)$q)) { + $matches[$study_id] = $table_name; + $matches_results_tables[$table_name] = $results_tables[$table_name]; + } + } + + // 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, $nu_tables)) { + $variable_names_in_table[$table_name] = $nu_tables[$table_name]; + } else { + $items = $this->db->select('name')->from('survey_items') + ->where(['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) || preg_match("/\b$variable_name_base\b/", $q)) { + $matches_variable_names[$table_name][] = $variable_name; + } + } + } + + $variables = opencpu_formr_variables($q); + + return compact("matches", "matches_results_tables", "matches_variable_names", "token_add", "variables"); + } } diff --git a/application/Model/User.php b/application/Model/User.php index 4b03cb86f..ee8f1e221 100644 --- a/application/Model/User.php +++ b/application/Model/User.php @@ -1,431 +1,411 @@ 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($email, $password, $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($email, $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($email, $token, $new_password, $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(); - } - - function getAvailableRuns() { - return $this->dbh->select('name,title, public_blurb_parsed') - ->from('survey_runs') - ->where('public > 2') - ->fetchAll(); - } +class User extends Model { + + 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 $cron = false; + public $logged_in = false; + public $admin = false; + public $referrer_code = null; + // todo: time zone, etc. + + protected $table = "survey_users"; + + public function __construct($id = null, $user_code = null, $options = []) { + parent::__construct(); + $this->assignProperties($options); + + 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 + } + } + + public function __sleep() { + return array('id', 'user_code'); + } + + private function load() { + $user = $this->db->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->assignProperties($user); + $this->valid = true; + $this->logged_in = true; + return true; + } + + return false; + } + + public function loggedIn() { + return Session::getAdminCookie() && $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->db->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->db->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"); + } + + $this->email = $email; + $this->id = $inserted; + $this->needToVerifyMail(); + return true; + + } else { + alert('Error! Hash error.', 'alert-danger'); + return false; + } + } + + public function needToVerifyMail() { + $token = crypto_token(48); + $token_hash = password_hash($token, PASSWORD_DEFAULT); + $this->db->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 + )); + + $mail = Site::getInstance()->makeAdminMailer(); + $mail->AddAddress($this->email); + $mail->Subject = 'formr: confirm your email address'; + $mail->Body = Template::get_replace('email/verify-email.ftpl', 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('An email has been sent to you. Please check your email and verify your account.', 'alert-info'); + } + $this->id = null; + } + + public function login($info) { + $email = array_val($info, 'email'); + $password = array_val($info, 'password'); + + $user = $this->db->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->db->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 setAdminLevel($level) { + if (!Site::getCurrentUser()->isSuperAdmin()) { + throw new Exception("You need more admin rights to effect this change"); + } + + $level = (int) $level; + $level = max(array(0, $level)); + $level = $level > 9 ? 1 : $level; + + return $this->db->update('survey_users', array('admin' => $level), array('id' => $this->id, 'admin <' => 10)); + } + + public function forgotPassword($email) { + $user_exists = $this->db->entry_exists('survey_users', array('email' => $email)); + + if ($user_exists) { + $token = crypto_token(48); + $hash = password_hash($token, PASSWORD_DEFAULT); + + $this->db->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 + )); + + $mail = Site::getInstance()->makeAdminMailer(); + $mail->AddAddress($email); + $mail->Subject = 'formr: forgot password'; + $mail->Body = Template::get_replace('email/forgot-password.ftpl', array( + 'site_url' => site_url(), + 'reset_link' => $reset_link, + )); + + if (!$mail->Send()) { + alert($mail->ErrorInfo, 'alert-danger'); + } 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("admin/account/forgot-password"); + } + } + } + + function logout() { + $this->logged_in = false; + $this->id = null; + Session::deleteAdminCookie(); + Session::destroy(); + } + + public function changePassword($info) { + if (!$this->login($info)) { + $this->errors = array('The old password you entered is not correct.'); + return false; + } + + $new_password = array_val($info, 'new_password'); + $hash = password_hash($new_password, PASSWORD_DEFAULT); + /* Store new hash in db */ + if ($hash) { + $this->db->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(['email' => $this->email, 'password' => $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->db->entry_exists('survey_users', array('email' => $update['email'])); + if ($exists) { + $this->errors[] = 'The provided email address is already in use!'; + return false; + } + } + + $this->db->update('survey_users', $update, array('id' => $this->id)); + $this->email = $update['email']; + if ($verificationRequired) { + $this->needToVerifyMail(); + } + $this->load(); + return true; + } + + public function resetPassword($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->db->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->db->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 verifyEmail($email, $token) { + $verify_data = $this->db->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, (string)$verify_data['email_verification_hash'])) { + $this->db->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->db->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->db->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, $cols = []) { + if ($this->isAdmin()) { + $select = $this->db->select($cols); + $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->db->find('survey_email_accounts', array('user_id' => $this->id, 'deleted' => 0), array('cols' => 'id, from, status')); + $results = array(); + foreach ($accs as $acc) { + if ($acc['from'] == null) { + $acc['from'] = 'New.'; + } + $results[] = $acc; + } + return $results; + } + + return false; + } + + public function getRuns($order = 'id DESC', $limit = null) { + if ($this->isAdmin()) { + $select = $this->db->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(); + } + } diff --git a/application/Pagination.php b/application/Pagination.php new file mode 100644 index 000000000..554ace5e0 --- /dev/null +++ b/application/Pagination.php @@ -0,0 +1,106 @@ +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, + )); + } + +} diff --git a/application/Queue/EmailQueue.php b/application/Queue/EmailQueue.php new file mode 100644 index 000000000..71de647b9 --- /dev/null +++ b/application/Queue/EmailQueue.php @@ -0,0 +1,344 @@ +itemTtl = array_val($this->config, 'queue_item_ttl', 20 * 60); + $this->itemTries = array_val($this->config, 'queue_item_tries', 4); + $this->skipAccounts = array_val($this->config, 'queue_skip_accounts', array()); + } + + /** + * + * @return PDOStatement + */ + protected function getEmailAccountsStatement($account_id) { + $WHERE = 'WHERE `survey_email_log`.`status` = 0 AND `survey_email_accounts`.status = 1'; + if ($account_id) { + $WHERE .= ' AND account_id = ' . (int) $account_id . ' '; + } + + $query = "SELECT account_id, `session_id`, `from`, from_name, host, port, tls, username, password, auth_key + FROM survey_email_log + LEFT JOIN survey_email_accounts ON survey_email_accounts.id = survey_email_log.account_id + {$WHERE} + GROUP BY account_id + ORDER BY RAND() + "; + return $this->db->rquery($query); + } + + /** + * + * @param int $account_id + * @return PDOStatement + */ + protected function getEmailsStatement($account_id) { + $query = 'SELECT id, `session_id`, subject, message, recipient, created, meta FROM survey_email_log WHERE `survey_email_log`.`status` = 0 AND account_id = ' . (int) $account_id; + return $this->db->rquery($query); + } + + /** + * Create an SMTP instance given an 'account' object + * If the instance is already created and connected then we return it. + * + * @param array $account + * @return PHPMailer + */ + protected function getSMTPConnection($account, $account_connected = null) { + $account_id = $account['account_id']; + if(!isset($this->connections[$account_id])) { + $account_connected = false; + } else if($account_connected === false) { + $this->closeSMTPConnection($account_id); + } else { + $account_connected = true; + } + + if (!$account_connected) { + $mailer = new PHPMailer(); + $mailer->SetLanguage("de", "/"); + + $mailer->isSMTP(); + $mailer->SMTPAuth = true; + $mailer->SMTPKeepAlive = true; + $mailer->Mailer = "smtp"; + $mailer->Host = $account['host']; + $mailer->Port = $account['port']; + if ($account['tls']) { + $mailer->SMTPSecure = 'tls'; + } else { + $mailer->SMTPSecure = 'ssl'; + } + if (isset($account['username'])) { + $mailer->Username = $account['username']; + $mailer->Password = $account['password']; + } else { + $mailer->SMTPAuth = false; + $mailer->SMTPSecure = false; + } + $mailer->setFrom($account['from'], $account['from_name']); + $mailer->AddReplyTo($account['from'], $account['from_name']); + $mailer->CharSet = "utf-8"; + $mailer->WordWrap = 65; + $mailer->AllowEmpty = true; + if (is_array(Config::get('email.smtp_options'))) { + $mailer->SMTPOptions = array_merge($mailer->SMTPOptions, Config::get('email.smtp_options')); + } + + $this->connections[$account_id] = $mailer; + } + + return $this->connections[$account_id]; + } + + protected function closeSMTPConnection($account_id) { + if (isset($this->connections[$account_id])) { + $this->connections[$account_id]->getSMTPInstance()->quit(true); + $this->connections[$account_id]->getSMTPInstance()->close(); + unset($this->connections[$account_id]); + } + } + + protected function logResult($session_id, $email_id, $status_code, $result, $result_log = null) { + $this->db->exec( + 'UPDATE `survey_email_log` SET `status` = :status_code, `sent` = NOW() WHERE `id` = :id', + ['id' => $email_id, 'status_code' => $status_code] + ); + + if($session_id) { + $this->db->exec( + 'UPDATE `survey_unit_sessions` SET `result` = :result, `result_log` = :resultlog WHERE `id` = :session_id', + ['session_id' => $session_id, 'result' => $result, 'resultlog' => $result_log] + ); + } + } + + protected function deactivateAccount($account_id) { + $this->db->exec('UPDATE `survey_email_accounts` SET `status` = -1 WHERE id = :id', array("id" => (int) $account_id)); + } + + /** + * + * @param \PHPMailer\PHPMailer\PHPMailer $mailer + * @param array $email [recipient, subject, message, session_id, meta] + * @param array $account [account_id, from, from_name] + * @param boolean $first_try Indicates if we are trying to send the email for the first time + * + * @return boolean Returns TRUE is email was sent and FALSE otherwise + */ + protected function sendEmail($mailer, $email, $account, $first_try = true) { + if (!filter_var($email['recipient'], FILTER_VALIDATE_EMAIL)) { + $this->logResult($email['session_id'], $email['id'], self::STATUS_INVALID_RECIPIENT, "error_email_invalid_recipient"); + return false; + } + + if (!$email['subject']) { + $this->logResult($email['session_id'], $email['id'], self::STATUS_INVALID_SUBJECT, "error_email_invalid_subject"); + return false; + } + + $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}"); + } + } + } + + // Send mail + try { + if (($sent = $mailer->send())) { + $this->logResult($email['session_id'], $email['id'], self::STATUS_SENT, "email_sent"); + $this->dbg("Send Success. \n {$debugInfo}"); + } else { + $this->dbg($mailer->ErrorInfo); + $this->logResult($email['session_id'], $email['id'], self::STATUS_FAILED_TO_SEND, "error_email_not_sent", $mailer->ErrorInfo); + throw new Exception($mailer->ErrorInfo); + } + } catch (Exception $e) { + //formr_log_exception($e, 'EmailQueue ' . $debugInfo); + $this->dbg("Send Failure: " . $mailer->ErrorInfo . ".\n {$debugInfo}"); + $this->dbg($mailer->ErrorInfo); + $this->logResult($email['session_id'], $email['id'], self::STATUS_FAILED_TO_SEND, "error_email_not_sent", $mailer->ErrorInfo); + + // Always try a new connection if we encounter an error and smtp client is not connected + $mailer = $this->getSMTPConnection($account, $mailer->getSMTPInstance()->connected()); + + // Try sending this particular email one more time otherwise give up + if($first_try) { + return $this->sendEmail($mailer, $email, $account, false); + } else { + $sent = false; + } + } + + $mailer->clearAddresses(); + $mailer->clearAttachments(); + $mailer->clearAllRecipients(); + $this->clearFiles($files); + + return $sent; + } + + 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 (in_array($account['account_id'], $this->skipAccounts)) { + $this->deactivateAccount($account['account_id']); + continue; + } + + list($username, $password) = explode(EmailAccount::AK_GLUE, Crypto::decrypt($account['auth_key']), 2); + $account['username'] = $username; + $account['password'] = $password; + + + $emailsStatement = $this->getEmailsStatement($account['account_id']); + while ($email = $emailsStatement->fetch(PDO::FETCH_ASSOC)) { + $mail_sent = $this->sendEmail($this->getSMTPConnection($account), $email, $account); + } + + // close sql emails cursor after processing batch + $emailsStatement->closeCursor(); + + // close connection after processing all emails for that account + $this->closeSMTPConnection($account['account_id']); + } + + $emailAccountsStatement->closeCursor(); + return true; + } + + 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); + + // 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("Unable to connect. waiting 5 seconds before reconnect."); + sleep(5); + } + } + } + +} diff --git a/application/Queue/Queue.php b/application/Queue/Queue.php new file mode 100644 index 000000000..a0ca2b1a2 --- /dev/null +++ b/application/Queue/Queue.php @@ -0,0 +1,159 @@ +db = $db; + $this->config = $config; + $this->loopInterval = array_val($this->config, 'queue_loop_interval', 5); + $this->debug = array_val($this->config, 'debug', false); + $this->num_processes = array_val($this->config, 'total_processes', 1); + $this->process_num = array_val($this->config, 'process_number', 1); + $this->items_per_process = array_val($this->config, 'items_per_process', 100); + + $this->setOffsetLimit(); + // 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; + } + + public function dbg($str) { + $args = func_get_args(); + if (count($args) > 1) { + $str = vsprintf(array_shift($args), $args); + } + + $proccess = "{$this->process_num}/{$this->num_processes} - {$this->list_type}"; + $str = date('Y-m-d H:i:s') . ' ' . $this->name . '['.$proccess.']: ' . $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((int) round(1000000 * ($this->loopInterval - $usleep))); + } + + $last_access = microtime(true); + return true; + } + + protected function hasMultipleProcesses() { + return $this->num_processes > 1; + } + + protected function setOffsetLimit() { + $this->offset = ($this->process_num - 1) * $this->items_per_process; + $this->limit = $this->offset + $this->items_per_process; + if ($this->process_num == $this->num_processes) { + //for the last process, set an everlasting limit + $this->limit = PHP_INT_MAX; + } + + $this->dbg( + "Setting queue process(%d of %d / %d items) with OFFSET %s and LIMIT %s", + $this->process_num, $this->num_processes, $this->items_per_process, $this->offset, $this->limit + ); + } + + /** + * 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/Queue/UnitSessionQueue.php b/application/Queue/UnitSessionQueue.php new file mode 100644 index 000000000..85764838e --- /dev/null +++ b/application/Queue/UnitSessionQueue.php @@ -0,0 +1,222 @@ +list_type = array_val($config, 'list_type', null); + parent::__construct($db, $config); + } + + public function run() { + if (empty($this->config['use_queue'])) { + throw new Exception('Explicitely configure $settings[unit_session][use_queue] 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() { + if ($this->list_type === 'fixed') { + $where = ' survey_unit_sessions.queued = :queued '; + $queued = self::QUEUED_TO_END; + } elseif ($this->list_type === 'execute') { + $where = ' survey_unit_sessions.queued = :queued '; + $queued = self::QUEUED_TO_EXECUTE; + } else { + $where = 'survey_unit_sessions.queued >= :queued '; + $queued = self::QUEUED_TO_EXECUTE; + } + + $query = "SELECT survey_unit_sessions.id, survey_unit_sessions.run_session_id, survey_unit_sessions.unit_id, + survey_unit_sessions.expires, survey_unit_sessions.queued, + survey_run_sessions.session, survey_run_sessions.run_id + FROM survey_unit_sessions + LEFT JOIN survey_run_sessions ON survey_unit_sessions.run_session_id = survey_run_sessions.id + LEFT JOIN survey_runs ON survey_run_sessions.run_id = survey_runs.id + WHERE {$where} AND survey_runs.cron_active = 1 AND survey_unit_sessions.expires <= NOW() + ORDER BY RAND();"; + //LIMIT {$this->limit} OFFSET {$this->offset}"; + + if ($this->debug) { + $this->dbg($query . ' queued: ' . $queued); + } + + return $this->db->rquery($query, array('queued' => $queued)); + } + + protected function processQueue() { + $sessionsStmt = $this->getSessionsStatement(); + + $this->dbg('Count: ' . $sessionsStmt->rowCount()); + 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['id']); + continue; + } + + $run = $this->getRun($session['run_id']); + if (!$run->valid || !$run->cron_active) { + continue; + } + $runSession = new RunSession($session['session'], $run); + if (!$runSession->id) { + $this->dbg('A run session could not be found for item in queue: ' . print_r($session, 1)); + self::removeItem($session['id']); + continue; + } + + // 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 + $unitSession = new UnitSession($runSession, null, ['id' => $session['id'], 'load' => true]); + $execution = $runSession->execute($unitSession, $session['queued'] == self::QUEUED_TO_EXECUTE); + + if ($this->debug) { + $this->dbg('Proccessed: ' . print_r($session, 1)); + } + } + } + + protected function setCache($type, $key, $value) { + $cache_key = "{$type}.{$key}"; + Cache::set($cache_key, $value); + } + + protected function getCache($type, $key) { + $cache_key = "{$type}.{$key}"; + return Cache::get($cache_key); + } + + /** + * Get Run Object + * + * @param string $runName + * @return \Run + */ + protected function getRun($runId) { + $run = $this->getCache('run', $runId); + if (!$run) { + $run = new Run(null, $runId); + $this->setCache('run', $runId, $run); + } + return $run; + } + + /** + * Remove item from session queue + * + * @param int $unitSessionId ID of the unit session + * @return booelan + */ + public static function removeItem($unitSessionId) { + $db = DB::getInstance(); + $removed = $db->update('survey_unit_sessions', array('queued' => 0), array('id' => $unitSessionId)); + + return (bool) $removed; + } + + /** + * Add item to session queue + * + * @param UnitSession $unitSession + * @param RunUnit $runUnit + * @param array $data Data array description expiration info + * @param mixed $execResults + */ + public static function addItem(UnitSession $unitSession, RunUnit $runUnit, $data, $execResults = null) { + if (!empty($data['expires'])) { + $db = DB::getInstance(); + $db->update('survey_unit_sessions', array( + 'expires' => mysql_datetime($data['expires']), + 'queued' => $data['queued'], + ), array('id' => $unitSession->id)); + } else { + UnitSessionQueue::removeItem($unitSession->id); + } + } + + /** + * Find a UnitSession in the queue + * + * @param UnitSession $unitSession + * @param array $where + * @return boolean|array Returns FALSE if an item was not found or an array with queue information + */ + public static function findItem(UnitSession $unitSession, $where = array()) { + if (!Config::get('unit_session.use_queue')) { + return false; + } + + $where['id'] = $unitSession->id; + return DB::getInstance()->findRow('survey_unit_sessions', $where, array('run_session_id', 'created', 'expires', 'queued')); + } + + public static function getRunItems(Run $run) { + $query = ' + SELECT survey_unit_sessions.run_session_id, survey_unit_sessions.id as unit_session_id, session, position, unit_id, survey_unit_sessions.created, expires, queued, survey_units.type as unit_type + 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_units.id = survey_unit_sessions.unit_id + WHERE survey_run_sessions.run_id = :run AND survey_unit_sessions.queued > :no_queued + ORDER BY unit_session_id DESC + '; + + return DB::getInstance()->rquery($query, array('run' => $run->id, 'no_queued' => self::QUEUED_NOT)); + } + +} diff --git a/application/Request.php b/application/Request.php new file mode 100644 index 000000000..d89c9c53d --- /dev/null +++ b/application/Request.php @@ -0,0 +1,240 @@ + $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::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 static 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() { + $method = array_val($_SERVER, 'REQUEST_METHOD'); + return strtolower($method) === 'post'; + } + + public static function isHTTPGetRequest() { + $method = array_val($_SERVER, 'REQUEST_METHOD'); + return strtolower($method) === 'get'; + } + + public static function isAjaxRequest() { + $env = env('HTTP_X_REQUESTED_WITH'); + if (!$env) { + return false; + } + + return strtolower($env) === '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; + } + + public static function stripslashes($value) { + // skip objects (object.toString() results in wrong output) + if (!is_object($value) && !is_array($value)) { + $value = stripslashes($value); + // object is array + } elseif (is_array($value)) { + foreach ($value as $k => $v) { + $value[$k] = self::stripslashes($v); + } + } + + return $value; + } + + public function redirect($uri = '', $params = array()) { + redirect_to($uri, $params); + } + +} diff --git a/application/Response.php b/application/Response.php new file mode 100644 index 000000000..a2b97aea2 --- /dev/null +++ b/application/Response.php @@ -0,0 +1,278 @@ +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/Router.php b/application/Router.php new file mode 100644 index 000000000..fb807e372 --- /dev/null +++ b/application/Router.php @@ -0,0 +1,199 @@ +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'); + } + } + + /** + * @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; + } + + 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); + } + + /** + * @return Router + */ + public static function getInstance() { + if (self::$instance === null) { + global $site; + self::$instance = new self($site); + } + return self::$instance; + } + + public static function getWebRoot() { + return Config::get('web_dir'); + } + + public static function isWebRootDir($name) { + return is_dir(self::getWebRoot() . '/' . $name); + } + +} diff --git a/application/Services/OSF.php b/application/Services/OSF.php new file mode 100644 index 000000000..e43ff2540 --- /dev/null +++ b/application/Services/OSF.php @@ -0,0 +1,382 @@ + $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, '', '&'); + // 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; + } + +} + +class OSF_Exception extends Exception { + +} diff --git a/application/Services/OpenCPU.php b/application/Services/OpenCPU.php new file mode 100644 index 000000000..42e7e777c --- /dev/null +++ b/application/Services/OpenCPU.php @@ -0,0 +1,528 @@ +==========formr=opencpu=string=delimiter==========

    "; + + /** + * @var OpenCPU[] + */ + protected static $instances = 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; + } + } + + $this->curl_opts = $this->curl_opts + array_val($config, 'curl_opts', array()); + } + + /** + * @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; + } + + /** + * Send HTTP request to opencpu + * + * @uses CURL + * @param string $uri + * @param array $params + * @param tystringpe $method + * @return \OpenCPU_Session + * @throws OpenCPU_Exception + */ + private function call($uri = '', $params = array(), $method = CURL::HTTP_METHOD_GET) { + 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"); + } + + return new OpenCPU_Session($headers['Location'], $headers['X-Ocpu-Session'], $results, $this); + } + + /** + * 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); + } + + 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 = is_null($json) ? 0 : 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; + } + + 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 { + +} diff --git a/application/Session.php b/application/Session.php new file mode 100644 index 000000000..298a314f2 --- /dev/null +++ b/application/Session.php @@ -0,0 +1,145 @@ + $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($with_admin = true) { + if ($with_admin === true) { + self::deleteAdminCookie(); + } + 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; + } + + 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; + } + + setcookie(self::REQUEST_TOKENS_COOKIE, $token, 0, self::$path, self::$domain, self::$secure, self::$httponly); + 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]) && array_val($_COOKIE, self::REQUEST_TOKENS_COOKIE) == $token) { + // a valid request token dies after it's validity is retrived :P + unset($tokens[$token]); + setcookie(self::REQUEST_TOKENS_COOKIE, '', -3600, self::$path, self::$domain, self::$secure, self::$httponly); + self::set(self::REQUEST_TOKENS, $tokens); + return true; + } + return false; + } + + public static function setCookie($name, $value, $expires = 0) { + return setcookie($name, $value, time() + $expires, self::$path, (string)self::$domain, self::$secure, self::$httponly); + } + + public static function deleteCookie($name) { + return setcookie($name, '', time() - 3600, self::$path, self::$domain, self::$secure, self::$httponly); + } + + public static function setAdminCookie(User $admin) { + $data = [$admin->id, $admin->user_code, time()]; + $cookie = self::setCookie(self::ADMIN_COOKIE, Crypto::encrypt($data, '-'), self::$lifetime); + if (!$cookie) { + formr_error(505, 'Invalid Token', 'Unable to set admin token'); + } + + Session::set('admin', $data); + } + + public static function getAdminCookie() { + $cookie = array_val($_COOKIE, self::ADMIN_COOKIE); + if ($cookie) { + $cookie_data = explode('-', Crypto::decrypt($cookie)); + $session_data = Session::get('admin', []); + if ($cookie_data && $session_data && $cookie_data[0] == $session_data[0]) { + return $session_data; + } + } + } + + public static function deleteAdminCookie() { + self::deleteCookie(self::ADMIN_COOKIE); + } + + + +} diff --git a/application/Site.php b/application/Site.php new file mode 100644 index 000000000..005295f10 --- /dev/null +++ b/application/Site.php @@ -0,0 +1,379 @@ + 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(); + + /** + * + * @var array + */ + protected static $settings = 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, 'admin/advanced') !== 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() { + $settings = Config::get('email'); + $mail = new PHPMailer(); + $mail->SetLanguage("de", "/"); + + $mail->IsSMTP(); // telling the class to use SMTP + $mail->Mailer = "smtp"; + $mail->Host = $settings['host']; + $mail->Port = $settings['port']; + if ($settings['tls']) { + $mail->SMTPSecure = 'tls'; + } else { + $mail->SMTPSecure = 'ssl'; + } + if (isset($settings['username'])) { + $mail->SMTPAuth = true; // turn on SMTP authentication + $mail->Username = $settings['username']; // SMTP username + $mail->Password = $settings['password']; // SMTP password + } else { + $mail->SMTPAuth = false; + $mail->SMTPSecure = false; + } + $mail->From = $settings['from']; + $mail->FromName = $settings['from_name']; + $mail->AddReplyTo($settings['from'], $settings['from_name']); + $mail->CharSet = "utf-8"; + $mail->WordWrap = 65; // set word wrap to 65 characters + if (is_array($settings['smtp_options'])) { + $mail->SMTPOptions = array_merge($mail->SMTPOptions, $settings['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(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 ($title && 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'); + } + + if (!$session) { + return null; + } + + $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($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(null, $user->user_code); + } + } + + if ($this->expire_session($expiry)) { + $user = null; + } + + if (empty($user->user_code)) { + $user = new User(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, '', ';'); + $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 static function getSettings($setting = null, $default = null) { + if (self::$settings) { + return $setting !== null ? array_val(self::$settings, $setting, $default) : self::$settings; + } + + $db = DB::getInstance(); + if ($setting !== null) { + $value = trim($db->findValue('survey_settings', array('setting' => $setting), 'value')); + return $value ? $value : $default; + } + + $settings = array(); + $rows = $db->select('setting, value') + ->from('survey_settings') + ->fetchAll(); + foreach ($rows as $row) { + $settings[$row['setting']] = $row['value']; + } + + self::$settings = $settings; + + return $settings; + } + + public static function runningInConsole() { + return php_sapi_name() === "cli"; + } + +} diff --git a/application/Spreadsheet/PagedSpreadsheetRenderer.php b/application/Spreadsheet/PagedSpreadsheetRenderer.php new file mode 100644 index 000000000..57821c024 --- /dev/null +++ b/application/Spreadsheet/PagedSpreadsheetRenderer.php @@ -0,0 +1,398 @@ +request = $request; + } + + /** + * Returns HTML page to be rendered for Survey or FALSE if survey ended + * + * @return string|boolean + */ + public function processItems() { + if (!Request::getGlobals('pageNo')) { + $pageNo = $this->getCurrentPage(); + return $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'); + return $this->redirectToPage($prev); + } + + if ($pageNo > $this->getMaxPage()) { + $this->completed = true; + return null; + } + + $formAction = ''; //run_url($this->run->name, $pageNo); + + $this->renderedItems = $this->getPageItems($pageNo); + $pageElement = $this->getPageElement($pageNo); + Session::delete('is-survey-post'); + $this->rendered = parent::render($formAction, $pageElement); + } + + public function studyCompleted() { + return $this->completed; + } + + public function render($form_action = null, $form_append = null) { + return $this->rendered; + } + + /** + * Save posted page item for specified Unit Session + * + */ + public function getPostedItems() { + if (!Request::isHTTPPostRequest() || $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']); + + return ['posted' => $this->postedValues, 'next_page' => $this->getPageUrl($currPage + 1)]; + } + + /** + * 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->study->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++; + return $this->redirectToPage($pageNo); + } + + $this->toRender = $pageItems; + return $this->getRenderedStudyItems(); + } + + 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->renderedItems), + 'answeredItems' => count($this->answeredItems), + ); + return $data; + } + + /** + * + * @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} + +
    +
    + %{buttons} +
    +
    + '; + $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; + } + + /** + * 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->unitSession->updateSurveyStudyRecord($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; + } + + public function redirectToPage($page) { + $this->redirect = $this->getPageUrl($page); + return []; + } + + 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/Spreadsheet/SpreadsheetReader.php b/application/Spreadsheet/SpreadsheetReader.php new file mode 100644 index 000000000..17e27be3c --- /dev/null +++ b/application/Spreadsheet/SpreadsheetReader.php @@ -0,0 +1,834 @@ +parsedown = new ParsedownExtra(); + $this->parsedown = $this->parsedown->setBreaksEnabled(true)->setUrlsLinked(true); + } + + 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(SurveyStudy $study) { + $items = $study->getItemsForSheet(); + $choices = $study->getChoicesForSheet(); + $filename = $study->name; + + try { + $objPhpSpreadsheet = $this->getSheetsFromArrays($items, $choices, $study->getSettings()); + $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(SurveyStudy $study) { + $items = $study->getItemsForSheet(); + $choices = $study->getChoicesForSheet(); + $filename = $study->name; + + try { + $objPhpSpreadsheet = $this->getSheetsFromArrays($items, $choices, $study->getSettings()); + + $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(SurveyStudy $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->getSettings(), + ); + + 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((string)$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((string)$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((string)$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((string)$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', 'id', 'study_id'))) { + $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/Spreadsheet/SpreadsheetRenderer.php b/application/Spreadsheet/SpreadsheetRenderer.php new file mode 100644 index 000000000..956594c95 --- /dev/null +++ b/application/Spreadsheet/SpreadsheetRenderer.php @@ -0,0 +1,633 @@ + 0, + '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 + ]; + + protected $errors = []; + + public function __construct(SurveyStudy $study, UnitSession $unitSession = null) { + $this->unitSession = $unitSession; + $this->run = $unitSession->runSession->getRun(); + $this->db = $unitSession->getDbConnection(); + $this->study = $study; + $this->validatedItems = $unitSession->validatedStudyItems; + } + + public function processItems() { + $loops = 0; + while (($items = $this->getNextStudyItems())) { + // 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->study->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->unitSession->id, + 'item_id' => $lastItem->id, + ); + $this->db->update('survey_items_display', array('hidden' => 1), $sess_item); + continue; + } else { + $this->toRender = $this->processDynamicLabelsAndChoices($items); + break; + } + } + + $this->renderedItems = $this->getRenderedStudyItems(); + } + + public function render($form_action = null, $form_append = null) { + $ret = ' +
    +
    + '; + $ret .= $this->renderHeader($form_action) . + $this->renderItems() . + $form_append . + $this->renderFooter(); + $ret .= ' +
    +
    + '; + //$this->dbh = null; + return $ret; + } + + protected function renderHeader($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 = ' +
    + + + + + +
    +
    +
    + %{progress} % +
    +
    +
    + + %{errors_tpl} + '; + + $errors_tpl = ' +
    + + + %{errors} +
    + '; + + if (!isset($this->study->displayed_percentage_maximum) OR $this->study->displayed_percentage_maximum == 0) { + $this->study->displayed_percentage_maximum = 100; + } + + $prog = $this->progressCounts['progress'] * // the fraction of this survey that was completed + ($this->study->displayed_percentage_maximum - // is multiplied with the stretch of percentage that it was accorded + $this->study->add_percentage_points); + + if (isset($this->study->add_percentage_points)) { + $prog += $this->study->add_percentage_points; + } + + if ($prog > $this->study->displayed_percentage_maximum) { + $prog = $this->study->displayed_percentage_maximum; + } + + $prog = round($prog); + $user = Site::getCurrentUser(); + + $tpl_vars = array( + 'action' => $action, + 'class' => 'form-horizontal main_formr_survey' . ($this->study->enable_instant_validation ? ' ws-validate' : ''), + 'enctype' => $enctype, + 'session_id' => $this->unitSession->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' => $user ? h($user->user_code) : '', //h($cookie->getData('code')), + 'cookie' => '', //$cookie->getFile(), + 'progress' => $prog, + 'add_percentage_points' => $this->study->add_percentage_points, + 'displayed_percentage_maximum' => $this->study->displayed_percentage_maximum, + 'already_answered' => $this->progressCounts['already_answered'], + 'not_answered_on_current_page' => $this->progressCounts['not_answered_on_current_page'], + 'items_on_page' => $this->progressCounts['not_answered'] - $this->progressCounts['not_answered_on_current_page'], + 'hidden_but_rendered' => $this->progressCounts['hidden_but_rendered_on_current_page'], + 'errors_tpl' => !empty($this->validationErrors) ? Template::replace($errors_tpl, array('errors' => $this->renderErrors())) : null, + ); + + return Template::replace($tpl, $tpl_vars); + } + + protected function renderItems() { + $ret = ''; + + foreach ($this->renderedItems as $item) { + if (!empty($this->validationErrors[$item->name])) { + $item->error = $this->validationErrors[$item->name]; + } + if (!empty($this->validatedItems[$item->name])) { + $item->value_validated = $this->validatedItems[$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 renderFooter() { + return '
    '; + } + + /** + * + * @param Item[] $items + * @return string + */ + protected function renderErrors() { + $labels = Session::get('labels', array()); + $tpl = ' +
  • + + Question/Code: %{question}
    + Error: %{error} +
  • + '; + $errors = ''; + + foreach ($this->validationErrors 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 . '
    '; + } + + /** + * 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 getNextStudyItems() { + $this->unanswered = []; + + $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`.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 + (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'); + + $get_items = $select->bindParams(array('session_id' => $this->unitSession->id, 'study_id' => $this->study->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; + + while ($item = $get_items->fetch(PDO::FETCH_ASSOC)) { + /* @var $oItem Item */ + $oItem = $itemFactory->make($item); + if (!$oItem) { + continue; + } + + $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 ($oItem->type === 'submit') { + $inPage = false; + } + } + + return $pageItems; + } + + /** + * 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) { + continue; + } + + if (!$item->requiresUserInput() && !$item->needsDynamicValue()) { + $hiddenItems[$name] = $item->getComputedValue(); + unset($items[$name]); + continue; + } + } + + // save these values + $updated = $this->unitSession->updateSurveyStudyRecord($hiddenItems, true); + if ($hiddenItems && $updated) { + $this->answered($hiddenItems); + } elseif ($hiddenItems && !$updated) { + $this->validationErrors = $this->unitSession->errors; + $items = array_merge($items, $this->unitSession->validatedStudyItems); + } + + // 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(); + $study = $this->study; + + /* @var $item Item */ + foreach ($items as $name => &$item) { + if (!$item) { + continue; + } + + // 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($study)); + $code[$name] = "{$name} = (function(){ +{$val} +})()"; + 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->unitSession, $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); + + $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($item->parent_attributes['data-show']); + unset($items[$item_name]); // we remove items that are definitely hidden from consideration + continue; // don't increment counter + } else { + if ($hidden === 0) { + $item->parent_attributes['data-show'] = "'true'"; + } + // 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 + } + + // @TODO remove items from unanswerd if this is successfull + if ($this->unitSession->updateSurveyStudyRecord($save, false)) { + $this->answered($save); + } + } + + return $items; + } + + protected function processDynamicLabelsAndChoices(&$items) { + $study = $this->study; + + // Gather choice lists + $lists_to_fetch = $strings_to_parse = array(); + $session_labels = array(); + + foreach ($items as $name => &$item) { + if (!$item) { + continue; + } + + if ($item->choice_list) { + $lists_to_fetch[] = $item->choice_list; + } + + $vars = ($item->type == 'note_iframe') ? $this->unitSession->getRunData($item->label, $study->name) : []; + if ($item->needsDynamicLabel($vars, $study->name)) { + $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 = $study->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->unitSession, $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; + } + + protected function getRenderedStudyItems() { + $study = $this->study; + + $this->db->beginTransaction(); + + $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->db->prepare($view_query); + $view_update->bindValue(":session_id", $this->unitSession->id); + + $itemsDisplayed = 0; + + $renderedItems = array(); + + try { + foreach ($this->toRender as &$item) { + if ($study->maximum_number_displayed && $study->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++; + } + + $renderedItems[] = $item; + } + } + + $this->db->commit(); + } catch (Exception $e) { + $this->db->rollBack(); + formr_log_exception($e, __CLASS__); + } + + return $renderedItems; + } + + protected function getStudyProgress() { + $study = $this->study; + + $answered = $this->db->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->unitSession->id, 'study_id' => $study->id)) + ->fetch(); + + $this->progressCounts['already_answered'] = $answered['count']; + + /** @var Item $item */ + foreach ($this->unanswered as $item) { + // count only rendered items, not skipped ones + if ($item && $item->isRendered()) { + $this->progressCounts['not_answered']++; + } + // count those items that were hidden but rendered (ie. those relying on missing data for their showif) + if ($item && $item->isHiddenButRendered()) { + $this->progressCounts['hidden_but_rendered']++; + } + } + /** @var Item $item */ + foreach ($this->toRender as $item) { + // On current page, count only rendered items, not skipped ones + if ($item && $item->isRendered()) { + $this->progressCounts['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 && $item->isHiddenButRendered()) { + $this->progressCounts['hidden_but_rendered_on_current_page']++; + } + } + + $this->progressCounts['not_answered_on_current_page'] = $this->progressCounts['not_answered'] - $this->progressCounts['visible_on_current_page']; + + $all_items = $this->progressCounts['already_answered'] + $this->progressCounts['not_answered']; + + if ($all_items !== 0) { + $this->progressCounts['progress'] = $this->progressCounts['already_answered'] / $all_items; + } else { + $this->errors[] = _('Something went wrong, there are no items in this survey!'); + $this->progressCounts['progress'] = 0; + } + + // if there only hidden items, that have no way of becoming visible (no other items) + if ($this->progressCounts['not_answered'] === $this->progressCounts['hidden_but_rendered']) { + $this->progressCounts['progress'] = 1; + } + + return $this->progressCounts['progress']; + } + + public function studyCompleted() { + return $this->getStudyProgress() === 1; + } + + protected function answered ($items) { + foreach ($items as $name => $item) { + unset($this->unanswered[$name]); + } + } + +} diff --git a/application/Template.php b/application/Template.php new file mode 100644 index 000000000..f5a372651 --- /dev/null +++ b/application/Template.php @@ -0,0 +1,79 @@ +setTemplate($template); + $this->setVariables($variables); + } + + public function setTemplate($template) { + if ($template && !is_string($template)) { + throw new Exception('Invalid template name'); + } + + $this->template = $template; + } + + public function setVariables(array $variables) { + $this->variables = $variables; + } + + public function render() { + if (!$this->template) { + return; + } + return Template::get($this->template, $this->variables); + } +} \ No newline at end of file diff --git a/application/View/admin/header.php b/application/View/admin/header.php deleted file mode 100755 index 7a370c7bf..000000000 --- a/application/View/admin/header.php +++ /dev/null @@ -1,101 +0,0 @@ - - - - - - formr admin - - - $files) { - print_stylesheets($files, $id); - } - foreach ($js as $id => $files) { - print_scripts($files, $id); - } - ?> - - getRuns('id DESC', 5); - $studies = $user->getStudies('id DESC', 5); - } - ?> - - - -
    - -
    - -
    -
    diff --git a/application/View/admin/home.php b/application/View/admin/home.php deleted file mode 100755 index 295f72144..000000000 --- a/application/View/admin/home.php +++ /dev/null @@ -1,172 +0,0 @@ - - -
    -
    -

    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
    # - - ... - -
    -
    - -
    - -
    - -
    -
    -
    - - -
    - - \ No newline at end of file diff --git a/application/View/admin/mail/edit.php b/application/View/admin/mail/edit.php deleted file mode 100644 index 7992c5281..000000000 --- a/application/View/admin/mail/edit.php +++ /dev/null @@ -1,81 +0,0 @@ - -
    -

    edit email account

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

    E-Mail Accounts

    -
    - - -
    -
    - -
    - -
    -
    -

    Current Accounts

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

    -
    -
    -
    - -
    - -
    - -
    -
    -
    - -
    - -
    -
    -
    - -
    - -
    -
    -
    - -
    - -
    -
    -
    - -
    -
    - -
    -
    -
    - - -
    - -
    - -
    -
    -
    - -
    - -
    -
    -
    -
    - -
    -
    - -
    -
    -
    -
    - -
    - -
    -
    - -
    - -
    - -
    -
    -
    - -
    - -
    - - diff --git a/application/View/admin/misc/info.php b/application/View/admin/misc/info.php deleted file mode 100644 index f35a8efd6..000000000 --- a/application/View/admin/misc/info.php +++ /dev/null @@ -1,3 +0,0 @@ - - - -
    - -
    -

    Open Science Framework «» FORMR actions

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

    - Create an formr project (run) -

    -
    -
    - -
    - -
    -
    -

    - Create an OSF project -

    -
    - - -
    -
    -
    -
    -
    -
    - -
    - - - \ No newline at end of file diff --git a/application/View/admin/misc/test_opencpu_speed.php b/application/View/admin/misc/test_opencpu_speed.php deleted file mode 100644 index 077a28608..000000000 --- a/application/View/admin/misc/test_opencpu_speed.php +++ /dev/null @@ -1,95 +0,0 @@ -OpenCPU test"; -echo '
    testing '.Config::get('alternative_opencpu_instance').'
    '; - -$max = 30; -for($i = 0; $i < $max; $i++): - $openCPU->clearUserData(); - $source = '{'. - mt_rand().' - '. - str_repeat(" ",$i). - ' - library(knitr) - knit2html(text = "' . addslashes("__Hello__ World `r 1` - ```{r} - library(ggplot2) - qplot(rnorm(100)) - qplot(rnorm(1000), rnorm(1000)) - library(formr) - 'blabla' %contains% 'bla' - ``` - ") . '", - fragment.only = T, options=c("base64_images","smartypants") - ) - '. - str_repeat(" ",$max-$i). - ' - }'; - - $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('1. HTTP status: ' . $openCPU->http_status, $alert_type); - - $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; - -endfor; - -$datasets = array('times' => $times); -$source = ' -# plot times -```{r} -library(ggplot2) -library(stringr) -library(reshape2) -just_times = times[,str_detect(names(times), "time")] -times_m = melt(just_times) -# qplot(value, data = times_m) + facet_wrap(~ variable) -# just_size = times[,str_detect(names(times),"_size")] -# size_m = melt(just_size) -# qplot(value, data = size_m) + facet_wrap(~ variable) -summary(times) -```'; -unset($times['certinfo']); - -$openCPU->addUserData(array('datasets' => $datasets)); -$accordion = $openCPU->knitForAdminDebug( $source); -$alert_type = 'alert-success'; - -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 '
    '; -endif; - -Template::load('footer'); diff --git a/application/View/admin/run/add_run.php b/application/View/admin/run/add_run.php deleted file mode 100644 index 2f68bc666..000000000 --- a/application/View/admin/run/add_run.php +++ /dev/null @@ -1,54 +0,0 @@ - - - -
    - -
    -

    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.
    • -
    -
    -
    - -
    -
    -
    - - - -
    -
    -

     

    - 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 deleted file mode 100644 index 6bfc48c16..000000000 --- a/application/View/admin/run/create_new_named_session.php +++ /dev/null @@ -1,53 +0,0 @@ - - -
    - -
    -

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

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

    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 deleted file mode 100755 index ed5ee71af..000000000 --- a/application/View/admin/run/cron_log.php +++ /dev/null @@ -1,63 +0,0 @@ - -

    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. -

    - - - - - - $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; - - echo "\n"; - endforeach; - ?> - -
    {$field}
    - render("admin/run/".$run->name."/cron_log"); - } else { - echo "No cron jobs yet. Maybe you disabled them in the settings."; - } - ?> -
    -
    - - - -
    - -
    -

    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); - } - ?> -
    -
    - - -
    -
    - -
    -
    - -
    -
    - -
    - - \ No newline at end of file diff --git a/application/View/admin/run/delete_run.php b/application/View/admin/run/delete_run.php deleted file mode 100644 index 953fe052e..000000000 --- a/application/View/admin/run/delete_run.php +++ /dev/null @@ -1,53 +0,0 @@ - - -
    - -
    -

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

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

    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

    -
    -
    -
    -
    - - -
    -
    -
    -
    - - - -
    -
    - -
    -
    - -
    -
    - -
    - - \ No newline at end of file diff --git a/application/View/admin/run/email_log.php b/application/View/admin/run/email_log.php deleted file mode 100755 index 9cfb6b303..000000000 --- a/application/View/admin/run/email_log.php +++ /dev/null @@ -1,63 +0,0 @@ - - -
    - -
    -

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

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

    Email Log

    -
    -
    - - - - - - $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 deleted file mode 100644 index 7d04b82b9..000000000 --- a/application/View/admin/run/empty_run.php +++ /dev/null @@ -1,54 +0,0 @@ - - -
    - -
    -

    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 deleted file mode 100644 index beac58472..000000000 --- a/application/View/admin/run/index.php +++ /dev/null @@ -1,123 +0,0 @@ - - -
    - -
    -

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

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

    - Edit Run - I am panicking :-( -

    -
    -
    - -
    - -
    - -
    Publicness:  
    - - - - - - - - - - - - - - - -
    - -
    -
    - -
    - -
    - -
    -
    - -
    click one of the symbols above to add a module
    -
    -
    -
    -
    -
    - -
    -
    -
    -
    - -
    -
    - -
    -
    - -
    -
    - -
    - - 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 deleted file mode 100644 index 745612389..000000000 --- a/application/View/admin/run/list.php +++ /dev/null @@ -1,81 +0,0 @@ - - -
    - -
    -

    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'; - } - ?>
    -
    - -
    -
    - -
    -
    -
    -
    - -
    -
    - -
    -
    - -
    -
    - -
    - - array())); -Template::load('admin/footer'); -?> \ No newline at end of file diff --git a/application/View/admin/run/menu.php b/application/View/admin/run/menu.php deleted file mode 100644 index fb795e439..000000000 --- a/application/View/admin/run/menu.php +++ /dev/null @@ -1,92 +0,0 @@ -
    -
    -

    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 deleted file mode 100644 index 67ab610df..000000000 --- a/application/View/admin/run/monkey_bar.php +++ /dev/null @@ -1,91 +0,0 @@ -
    -
    -
    - -
    - - - - - - - - - - "> - - - - - - - - - - - - - - - -
    -
    - - - - - -
    -
    diff --git a/application/View/admin/run/overview.php b/application/View/admin/run/overview.php deleted file mode 100644 index ae3190a13..000000000 --- a/application/View/admin/run/overview.php +++ /dev/null @@ -1,50 +0,0 @@ - - -
    - -
    -

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

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

    Run Overview

    -
    -
    - - - -
    -

    - 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 deleted file mode 100644 index c40ac4c5c..000000000 --- a/application/View/admin/run/random_groups.php +++ /dev/null @@ -1,118 +0,0 @@ - - -
    - -
    -

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

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

    Randomization Results

    -
    - Export -
    -
    -
    - - - - - - - $value): - echo ""; - endforeach; - ?> - - - - '; - foreach ($row as $cell) { - echo ""; - } - - echo "\n"; - } - ?> - - -
    {$field}
    $cell
    - - -
    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 deleted file mode 100644 index f16fd0010..000000000 --- a/application/View/admin/run/rename_run.php +++ /dev/null @@ -1,57 +0,0 @@ - - -
    - -
    -

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

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

    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.
    • -
    -
    -
    -
    -
    - - -
    -
    -
    -
    - - - -
    -
    - -
    -
    - -
    -
    - -
    - - \ 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 deleted file mode 100644 index d1ce81b23..000000000 --- a/application/View/admin/run/run_import_dialog.php +++ /dev/null @@ -1,25 +0,0 @@ -
    - -
    - -
    - - -
    -
    - -
    -
    - -
    - Select a json file - -
    -
    -
    -
    diff --git a/application/View/admin/run/settings.php b/application/View/admin/run/settings.php deleted file mode 100644 index e31401a9c..000000000 --- a/application/View/admin/run/settings.php +++ /dev/null @@ -1,291 +0,0 @@ - - -
    - -
    -

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

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

    Settings

    -
    - -
    - - -
    - - -
    - -
    -
    - -
    -
    - -
    - - array())); -Template::load('admin/footer'); -?> \ No newline at end of file diff --git a/application/View/admin/run/upload_files.php b/application/View/admin/run/upload_files.php deleted file mode 100644 index d26ada372..000000000 --- a/application/View/admin/run/upload_files.php +++ /dev/null @@ -1,83 +0,0 @@ - - -
    - -
    -

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

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

    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.
    • -
    -
    - -

    Files to upload:

    -
    -
    - -
    - - -
    - -
    -

    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 deleted file mode 100755 index a1a1793e4..000000000 --- a/application/View/admin/run/user_detail.php +++ /dev/null @@ -1,128 +0,0 @@ - - -
    - -
    -

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

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

    - 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
    () - -
    - - - -
    -
    - -
    -
    - -
    -
    - -
    - !empty($reminders) ? $reminders : array())); ?> - diff --git a/application/View/admin/run/user_overview.php b/application/View/admin/run/user_overview.php deleted file mode 100755 index d91887e91..000000000 --- a/application/View/admin/run/user_overview.php +++ /dev/null @@ -1,205 +0,0 @@ - - -
    - -
    -

    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 deleted file mode 100755 index 26ef3a298..000000000 --- a/application/View/admin/survey/add_survey.php +++ /dev/null @@ -1,93 +0,0 @@ - - - -
    - -
    -

    Surveys Add New

    -
    - - -
    - -
    -

    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

    -
    -
    -
    - -
    - - - 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 -
    -
    - - - -
    -
    -
    -
    -
    - -
    - - - - diff --git a/application/View/admin/survey/delete_results.php b/application/View/admin/survey/delete_results.php deleted file mode 100755 index bc9400f27..000000000 --- a/application/View/admin/survey/delete_results.php +++ /dev/null @@ -1,59 +0,0 @@ - - -
    - -
    -

    name ?> Survey ID: id ?>

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

    Delete Results complete, begun

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

    Warning!

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

    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 deleted file mode 100755 index ee6d2d37b..000000000 --- a/application/View/admin/survey/delete_study.php +++ /dev/null @@ -1,51 +0,0 @@ - - -
    - -
    -

    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 deleted file mode 100644 index 98a54c463..000000000 --- a/application/View/admin/survey/google_sheet_import.php +++ /dev/null @@ -1,33 +0,0 @@ - diff --git a/application/View/admin/survey/index.php b/application/View/admin/survey/index.php deleted file mode 100755 index 498ff6a97..000000000 --- a/application/View/admin/survey/index.php +++ /dev/null @@ -1,200 +0,0 @@ - - -
    - -
    -

    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. - -
    - -
    -
    - - -
    - -
    - - - -
    -
    -
    -
    -
    - -
    - -
    - - - - -
    - -
    -

    Surveys

    -
    - - -
    -
    -
    -
    -
    -

    Survey Listing

    - -
    -
    - -
    - - - - - - - - - - - - - - - - - - - - - -
    # IDNameCreatedModifiedGoogle Sheet
    # - - ... - -
    -
    - -
    -
    - -
    -
    - - -
    -
    - -
    -
    - -
    - - array())); -Template::load('admin/footer'); -?> \ No newline at end of file diff --git a/application/View/admin/survey/menu.php b/application/View/admin/survey/menu.php deleted file mode 100644 index 95efa9223..000000000 --- a/application/View/admin/survey/menu.php +++ /dev/null @@ -1,84 +0,0 @@ -
    -
    -

    Configuration

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

    Testing & Management

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

    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']); -?> -
    - - - - -
    - -
    - Add Survey diff --git a/application/View/admin/survey/rename_study.php b/application/View/admin/survey/rename_study.php deleted file mode 100755 index 5606fc11e..000000000 --- a/application/View/admin/survey/rename_study.php +++ /dev/null @@ -1,52 +0,0 @@ - - -
    - -
    -

    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 deleted file mode 100755 index 5411a7f8b..000000000 --- a/application/View/admin/survey/show_item_table.php +++ /dev/null @@ -1,132 +0,0 @@ - - -
    - -
    -

    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 deleted file mode 100644 index d48f9958a..000000000 --- a/application/View/admin/survey/show_item_table_table.php +++ /dev/null @@ -1,45 +0,0 @@ -
    - - - - $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 deleted file mode 100755 index 45ee86bca..000000000 --- a/application/View/admin/survey/show_itemdisplay.php +++ /dev/null @@ -1,173 +0,0 @@ - - -
    - -
    -

    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
    - - -
    -
    - -
    -
    -
    - -
    - -
    - - - - - - - - - - diff --git a/application/View/admin/survey/show_results.php b/application/View/admin/survey/show_results.php deleted file mode 100755 index e5717ee91..000000000 --- a/application/View/admin/survey/show_results.php +++ /dev/null @@ -1,161 +0,0 @@ - - -
    - -
    -

    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 . '
    - -
    -
    - -
    -
    -
    - -
    - -
    - - - - - - - - - - diff --git a/application/View/admin/survey/upload_items.php b/application/View/admin/survey/upload_items.php deleted file mode 100755 index d071a423f..000000000 --- a/application/View/admin/survey/upload_items.php +++ /dev/null @@ -1,127 +0,0 @@ -getResultCount(); -?> - -
    - -
    -

    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. -
      • -
      -
    • -
    -
    - -

    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? -
    - -

    - Or use - this - - a - Googlesheet -

    -
    - - - Make sure this sheet is accessible by anyone with the link -
    - - 0): ?> -
    -

    Delete Results Confirmation

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

     

    - more help on creating survey sheets - -
    -
    -
    - -
    - -
    - - - \ No newline at end of file diff --git a/application/View/email/test-account.txt b/application/View/email/test-account.txt deleted file mode 100644 index 9a77e27dd..000000000 --- a/application/View/email/test-account.txt +++ /dev/null @@ -1,6 +0,0 @@ -Dear User, - -Your account has been successfully set up on formr.org - -Best regards, -formr robots \ No newline at end of file diff --git a/application/View/public/about.php b/application/View/public/about.php deleted file mode 100644 index fed23618b..000000000 --- a/application/View/public/about.php +++ /dev/null @@ -1,178 +0,0 @@ - $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

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

    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. -

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

    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 deleted file mode 100644 index 7276a557f..000000000 --- a/application/View/public/account.php +++ /dev/null @@ -1,93 +0,0 @@ - - -
    -
    -
    - - - - diff --git a/application/View/public/alerts.php b/application/View/public/alerts.php deleted file mode 100644 index 8c26432c0..000000000 --- a/application/View/public/alerts.php +++ /dev/null @@ -1,9 +0,0 @@ -renderAlerts(); -} -if (!empty($alerts)): ?> -
    - -
    - diff --git a/application/View/public/disclaimer.php b/application/View/public/disclaimer.php deleted file mode 100644 index 58d435cb7..000000000 --- a/application/View/public/disclaimer.php +++ /dev/null @@ -1,18 +0,0 @@ -
    -
    -
    -
    -

    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 deleted file mode 100644 index ca74b09af..000000000 --- a/application/View/public/documentation.php +++ /dev/null @@ -1,75 +0,0 @@ - '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. -

    -
    - -
    -
    - -
    -
    - -
    -
    - -
    -
    - -
    -
    - -
    -
    - -
    -
    - -
    -
    - -
    -
    - -
    -
    - -
    -
    -
    -
    -
    - - - diff --git a/application/View/public/documentation/api.php b/application/View/public/documentation/api.php deleted file mode 100644 index fbed473d6..000000000 --- a/application/View/public/documentation/api.php +++ /dev/null @@ -1,155 +0,0 @@ -

    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) -

    -

    -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 -

    - - -

    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. -

    - -

    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. -

    - -

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

    - -
    -
    -POST /oauth/access_token?
    -     client_id={client-id}
    -    &client_secret={client-secret}
    -    &grant_type=client_credentials
    -
    -
    - -

    -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 -

    - -
    -
    -{
    -	"access_token":"XXXXXX3635f0dc13d563504b4d",
    -	"expires_in":3600,
    -	"token_type":"Bearer",
    -	"scope":null
    -}
    -
    -
    - -The attribute expires_in indicates the number of seconds for which is token is valid from its time of creation. If there is an error for example if the -client details are not correct, then an error object is returned containing an error_description and sometimes an error_uri where you can read more about the -generated error. An example of an error object is - -
    -
    -{
    -	"error":"invalid_client",
    -	"error_description":"The client credentials are invalid"
    -}
    -
    -
    - - -

    Making Resource Requests using generated access token

    - -With the generated access token, you are able to make requests to the resource endpoints of the formr API. For now only the results resource endpoint has -been implemented - -
    Getting study results over the API
    - -
    REQUEST
    -To obtain the results of a particular set of sessions in a particular run, send a GET HTTP request to the get endpoint along side the access_token obtained above -together with the necessary parameters as shown below: - -
    -
    -GET /get/results?
    -     access_token={access-token}
    -    &run[name]={name of the run as it appears on formr}
    -    &run[sessions]={comma separated list of session codes OR leave empty to get all sessions}
    -    &surveys[survey_name1]={comma separated list items to get from survey_name1 OR leave empty to get all items}
    -    &surveys[survey_name2]={comma separated list items to get from survey_name2 OR leave empty to get all items}
    -
    -
    - -

    - 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: -

    - -
    -
    -{
    -	"survey_1": [{
    -		"session": "sdaswew434df",
    -		"survey_1_item_1": "answer1",
    -		"survey_1_item_2": "answer2",
    -		"survey_1_item_2": "answer3",
    -	},
    -	{
    -		"session": "fdgdfg4323",
    -		"survey_1_item_1": "answer4",
    -		"survey_1_item_2": "answer5",
    -		"survey_1_item_2": "answer6",
    -	}],
    -	"survey_2": [........]
    -}
    -
    -
    - -

    Using the formr API in R

    - -
    # install formr library, you only need to do this once and for updates
    -devtools::install_github("rubenarslan/formr")
    -# load the library to the R work space, you need to do this each session
    -library(formr)
    -# connect to the API with your client_id and client_secret
    -# you only need to do this once per session if you don't need longer than one hour
    -formr_api_access_token(client_id = "your_id", client_secret = "your_secret" )
    -# To get the results row for a specific user, do the following
    -results = formr_api_results(list(
    -	run = list(
    -		name = "rotation",  # for which run do you want results
    -		sessions = c("joyousCoyoteXXXLk5ByctNPryS4k-5JqZJYE19HwFhPu4FFk8beIHoBtyWniv46") # and for which user
    -	),
    -	surveys = list(
    -		rotation_exercise = c("exercise_1", "exercise_2"),
    -		rotation_exercise2 = c("exercise2_1", "exercise2_2"),
    -	)
    -))
    -# Now you can e.g. do:
    -rotex = results$rotation_exercise
    -rotex[, c("exercise_1","exercise_2")]
    -# to read the documentation in the R package, do e.g.
    -?formr_api_results
    -
    -
    diff --git a/application/View/public/documentation/features.php b/application/View/public/documentation/features.php deleted file mode 100644 index cc32673bc..000000000 --- a/application/View/public/documentation/features.php +++ /dev/null @@ -1,153 +0,0 @@ -

    Features


    - -

    - 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
    • -
    -

    - 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. -
    • -
    -

    - 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. -
    • - -
    -

    - 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) -
    • - -
    • - 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 deleted file mode 100644 index fbaf04e86..000000000 --- a/application/View/public/documentation/get_help.php +++ /dev/null @@ -1,33 +0,0 @@ -

    Help


    - -

    Where to get help

    -

    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 deleted file mode 100644 index b05b02e27..000000000 --- a/application/View/public/documentation/getting_started.php +++ /dev/null @@ -1,42 +0,0 @@ -

    Getting Started


    - -

    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. -

    - -

    - 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. -

    -

    - 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. -

    -

    - 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 deleted file mode 100644 index 7b6a99365..000000000 --- a/application/View/public/documentation/item_types.php +++ /dev/null @@ -1,247 +0,0 @@ -

    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. -
    -
    - - - -

    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. Except this will only happen in interactive mode, in cron mode though the run will not process a non-interactive survey. 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. -
    - -
    diff --git a/application/View/public/documentation/knitr_markdown.php b/application/View/public/documentation/knitr_markdown.php deleted file mode 100644 index b532b8216..000000000 --- a/application/View/public/documentation/knitr_markdown.php +++ /dev/null @@ -1,80 +0,0 @@ -

    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. -

    -
    - 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. -

    -
    -* list item 1
    -* list item 2
    -
    -

    - will turn into a nice bulleted list. -

    -
      -
    • 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. -

    -

    - *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. -

    -

    - 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 -
    -

    - 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}
      -library(formr)
      -# build scales automatically
      -big5 = formr_aggregate(results = big5)
      -# standardise
      -big5$extraversion = scale(big5$extraversion, center = 3.2, scale = 2.1)
      -
      -# plot
      -qplot_on_normal(big$extraversion, xlab = "Extraversion")
      -```
      -
      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 deleted file mode 100644 index d61b23051..000000000 --- a/application/View/public/documentation/r_helpers.php +++ /dev/null @@ -1,23 +0,0 @@ -

    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. -

    -
    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); 
      -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); 
      -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.
    • -
    \ 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 deleted file mode 100644 index aff5e0df1..000000000 --- a/application/View/public/documentation/run_module_explanations.php +++ /dev/null @@ -1,412 +0,0 @@ -

    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). -

    -
    -
    - -
    -
    -

    - 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 -

    -
    -
    - -
    -
    -

    - 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). -

    -
    -
    -
    -
    diff --git a/application/View/public/documentation/sample_choices_sheet.php b/application/View/public/documentation/sample_choices_sheet.php deleted file mode 100644 index 1823139dd..000000000 --- a/application/View/public/documentation/sample_choices_sheet.php +++ /dev/null @@ -1,75 +0,0 @@ -

    Choices Spreadsheet


    - -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 diff --git a/application/View/public/documentation/sample_survey_sheet.php b/application/View/public/documentation/sample_survey_sheet.php deleted file mode 100644 index 475a89214..000000000 --- a/application/View/public/documentation/sample_survey_sheet.php +++ /dev/null @@ -1,293 +0,0 @@ -

    Survey Spreadsheet


    - -

    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*. -
    • -
    - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    - type - - name - - label - - optional - - showif -
    - text - - name - - Please enter your name - - * - -
    - number 1,130,1 - - age - - How old are you? - - -
    - 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. -
    -
    - -

    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). -
    - -
    - 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. -
    - -
    - 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 deleted file mode 100644 index 99bafe09a..000000000 --- a/application/View/public/error.php +++ /dev/null @@ -1,32 +0,0 @@ - - - - - <?php echo $title; ?> - formr.org - - - - -
    -

    -

    - -

    - -

    - -
    -
    - renderAlerts() : null; ?> -
    - - - diff --git a/application/View/public/forgot_password.php b/application/View/public/forgot_password.php deleted file mode 100755 index 2edf1ba46..000000000 --- a/application/View/public/forgot_password.php +++ /dev/null @@ -1,33 +0,0 @@ - - -
    -
    -
    -
    -
    -
    -
    - -
    -
    -
    -
    -

     

    -
    -
    -
    -
    -
    - - \ No newline at end of file diff --git a/application/View/public/header.php b/application/View/public/header.php deleted file mode 100755 index f9189ccff..000000000 --- a/application/View/public/header.php +++ /dev/null @@ -1,18 +0,0 @@ - - - - - - - - -
    - -
    -
    - -
    -
    - - - diff --git a/application/View/public/home.php b/application/View/public/home.php deleted file mode 100755 index 4e9521ab9..000000000 --- a/application/View/public/home.php +++ /dev/null @@ -1,175 +0,0 @@ - - - - -
    -
    -
    -
    -
    -
    -

    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)

    -
    -
    -
    - -
    -
    -
    - -
    -

    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)

    -
    -
    -
    - - -
    -
    - 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 deleted file mode 100755 index 7cf2878f7..000000000 --- a/application/View/public/login.php +++ /dev/null @@ -1,51 +0,0 @@ - - -
    -
    -
    -
    -
    -
    -
    -
    - -
    -
    -
    -
    -
    -
    -
    -

    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 deleted file mode 100644 index 4c8d57eef..000000000 --- a/application/View/public/navigation.php +++ /dev/null @@ -1,34 +0,0 @@ - diff --git a/application/View/public/publications.php b/application/View/public/publications.php deleted file mode 100644 index b4f825db1..000000000 --- a/application/View/public/publications.php +++ /dev/null @@ -1,57 +0,0 @@ - 'fmr-small-header', -)); -?> - - -
    -
    -
    -
    -

    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. -

    -
    -

     

    -
    -
    - -
    -

     

    - -
    -
    -
    - - diff --git a/application/View/public/register.php b/application/View/public/register.php deleted file mode 100755 index 1bca37d4c..000000000 --- a/application/View/public/register.php +++ /dev/null @@ -1,70 +0,0 @@ - - -
    -
    -
    -
    -
    - -
    -
    -
    - -
    -
    -
    -

     

    -
    -
    -
    -
    -

    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.

    -
    -
    -
    -
    -
    -
    -
    -
    - - diff --git a/application/View/public/reset_password.php b/application/View/public/reset_password.php deleted file mode 100755 index ab52a3c15..000000000 --- a/application/View/public/reset_password.php +++ /dev/null @@ -1,44 +0,0 @@ - - -
    -
    -
    -
    -
    -
    -
    -
    - -
    -
    -
    -
    -
    -

     

    -
    -
    -
    -
    -
    - - diff --git a/application/View/public/run/index.php b/application/View/public/run/index.php deleted file mode 100644 index 645a03151..000000000 --- a/application/View/public/run/index.php +++ /dev/null @@ -1,46 +0,0 @@ - - - - - - - - -
    -
    -
    -
    -
    - 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 deleted file mode 100755 index 6adf594a9..000000000 --- a/application/View/public/run/settings.php +++ /dev/null @@ -1,63 +0,0 @@ - 'fmr-small-header', -)); -?> - -
    -
    -
    -
    -

    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.
    -
    -
    - -
    -
    - - -
    -
    -
    -
    -
    - - diff --git a/application/View/public/social_share.php b/application/View/public/social_share.php deleted file mode 100644 index e050e90d2..000000000 --- a/application/View/public/social_share.php +++ /dev/null @@ -1,15 +0,0 @@ - - - $data): - $href = strt_replace($data['url'], array('title' => $title, 'url' => $url)); - ?> - - diff --git a/application/View/public/studies.php b/application/View/public/studies.php deleted file mode 100644 index 7849c0b78..000000000 --- a/application/View/public/studies.php +++ /dev/null @@ -1,43 +0,0 @@ - 'fmr-small-header', - )); -?> - -
    -
    -
    -
    -

    Studies

    -

    Some studies currently running on formr.

    -
    -
    -
    - - -
    -
    -

    -
    -  

    ' ?> -
    -
    -
    - -
    -
    - Participate - -
    -
    -
    - - -
    -
    -
    - - - diff --git a/application/View/superadmin/active_users.php b/application/View/superadmin/active_users.php deleted file mode 100644 index febbf7010..000000000 --- a/application/View/superadmin/active_users.php +++ /dev/null @@ -1,61 +0,0 @@ - - -
    - -
    -

    User Management Superadmin

    -
    - - -
    -
    -
    -
    -
    -

    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 deleted file mode 100755 index d03240971..000000000 --- a/application/View/superadmin/cron_log.php +++ /dev/null @@ -1,58 +0,0 @@ - -

    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. -

    - - - - - $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; - - echo "\n"; - endforeach; - } - ?> - -
    {$field}
    -
    - render("superadmin/cron_log"); ?> - - - - -
    - -
    -

    Cron Logs Superadmin

    -
    - - -
    -
    -
    -
    -
    -

    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 deleted file mode 100644 index 8de194256..000000000 --- a/application/View/superadmin/runs_management.php +++ /dev/null @@ -1,77 +0,0 @@ - - -
    - -
    -

    User Management Superadmin

    -
    - - -
    -
    -
    -
    -
    -

    Formr Runs ()

    -
    -
    - - - -
    - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    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 deleted file mode 100644 index 7aef37051..000000000 --- a/application/View/superadmin/user_management.php +++ /dev/null @@ -1,97 +0,0 @@ - - -
    - -
    -

    User Management Superadmin

    -
    - - -
    -
    -
    -
    -
    -

    Formr Users

    -
    -
    - - - - - $value): - echo ""; - endforeach; - ?> - - - - "; - foreach ($row as $cell): - echo ""; - endforeach; - echo ""; - endforeach; - ?> - -
    {$field}
    $cell
    - - - - -
    -
    - -
    -
    - -
    -
    - -
    - - - - - \ No newline at end of file diff --git a/bin/cron.php b/bin/cron.php index aa476215c..549a08db6 100755 --- a/bin/cron.php +++ b/bin/cron.php @@ -1,155 +1,12 @@ #!/usr/bin/php $max_exec_time) { - $msg = "[$interrupt] Cron exceeded or reached set maximum script execution time of $max_exec_time secs."; - cron_log($msg, $logfile); - } - } - - if (file_exists($lockfile)) { - unlink($lockfile); - cron_log("Cronfile cleanup complete", $logfile); - } -} - -function cron_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: - cron_cleanup('SIGINT|SIGTERM'); - break; - // @example: $ kill -s SIGUSR1 - case SIGUSR1: - cron_cleanup('SIGUSR1'); - break; - } -} - -function cron_parse_executed_types($types) { - $str = ''; - foreach ($types as $key => $value) { - $str .= " {$value} {$key}s,"; - } - return $str; -} - -function cron_lock_exists($lockfile, $start_date, $logfile = null) { - if (file_exists($lockfile)) { - $started = file_get_contents($lockfile); - cron_log("Cron overlapped. Started: $started, Overlapped: $start_date", $logfile); - - // hack to delete $lockfile if cron hangs for more that 30 mins - if ((strtotime($started) + ((int)Config::get('cron.ttl_lockfile') * 60)) < time()) { - cron_log("Forced delete of $lockfile", $logfile); - unlink($lockfile); - return false; - } - return true; - } - return false; -} - -function cron_run_cleanup() { - global $lockfile; - - if (file_exists($lockfile)) { - unlink($lockfile); - } -} - -function cron_process_run(Run $run, $run_lockfile) { - global $site; - $start_date = date('r'); - $start_time = microtime(true); - $logfile = get_log_file("cron/cron-run-{$run->name}.log"); - - if (cron_lock_exists($run_lockfile, $start_date, $logfile)) { - return false; - } - - file_put_contents($run_lockfile, $start_date); - cron_log('----------', $logfile); - cron_log("cron-run call start for {$run->name}", $logfile); - - // get all session codes that have Branch, Pause, or Email lined up (not ended) - $dues = $run->getCronDues(); - $done = array(); - $i = 0; - // Foreach session, execute all units - $run->getOwner(); - foreach ($dues as $session) { - $run_session = new RunSession(DB::getInstance(), $run->id, 'cron', $session, $run); - $types = $run_session->getUnit(); // start looping thru their units. - $i++; - - if ($types === false) { - alert("This session '$session' caused problems", 'alert-danger'); - continue; - } - - foreach ($types as $type => $nr) { - if (!isset($done[$type])) { - $done[$type] = 0; - } - $done[$type] += $nr; - } - } - $executed_types = cron_parse_executed_types($done); - - $msg = "$i sessions in the run " . $run->name . " were processed. {$executed_types}"; - cron_log($msg, $logfile); - if ($site->alerts) { - cron_log("\n\n" . $site->renderAlerts() . "\n", $logfile); - } - - // log execution time - $exec_time = microtime(true) - $start_time; - $lasted = $exec_time > 60 ? ceil($exec_time / 60) . ' minutes' : ceil($exec_time) . ' seconds'; - cron_log("Cron ran for {$lasted}", $logfile); - cron_log("cron-run call end for {$run->name}", $logfile); - if (file_exists($run_lockfile)) { - unlink($run_lockfile); - } - cron_run_cleanup(); - - return true; -} - -// Check if maintenance is going on -if (Config::get('in_maintenance')) { - formr_error(404, 'Not Found', 'This website is currently undergoing maintenance. Please try again later.', 'Maintenace Mode', false); +// If we are processing unit sessions via db queue then cron should not run +if (Config::get('unit_session.use_queue')) { + echo "\n Processing Sessions in DB Queue \n"; + exit(0); } // Global required variables @@ -157,103 +14,27 @@ function cron_process_run(Run $run, $run_lockfile) { $fdb = DB::getInstance(); $user = new User($fdb, null, null); $user->cron = true; +$cronConfig = Config::get('cron'); // IF cron.php is executed with a -n option, then run cron only for particular run whose name is specified in the -n option $opts = getopt('n:'); if (!empty($opts['n'])) { - $name = $opts['n']; - $run = new Run($fdb, $name); - if (!$run->valid) { - echo "Run not found"; - exit(1); - } - - $lockfile = APPLICATION_ROOT . "tmp/cron-{$name}.lock"; - register_shutdown_function('cron_run_cleanup'); - cron_process_run($run, $lockfile); - unset($site, $user, $run); - exit(0); -} - -// ELSE cron.php is called with no parameters - -// Define required variables and set maximum execution time for cron script -$start_date = date('r'); -$start_time = microtime(true); -$max_exec_time = (int)Config::get('cron.ttl_cron') * 60; -$intercept_if_expired = (int)Config::get('cron.intercept_if_expired'); -$lockfile = APPLICATION_ROOT . 'tmp/cron.lock'; - -set_time_limit($max_exec_time); - -// Check if lock file exists to prevent overlapping -if (cron_lock_exists($lockfile, $start_date, $logfile)) { - exit(0); -} - -// Lock cron -file_put_contents($lockfile, $start_date); -register_shutdown_function('cron_cleanup'); - -/** Do the Work */ -cron_log("Cron started .... {$start_date}", $logfile); - -// Wrap in a try catch just in case because we can't see shit -try { - // Get all runs - $runs = $fdb->select('name')->from('survey_runs')->where('cron_active = 1')->order('cron_fork', 'DESC')->fetchAll(); - - $r = 0; - foreach ($runs as $run_data) { - $i = 0; - $r++; - $done = array('Pause' => 0, 'Email' => 0, 'SkipForward' => 0, 'SkipBackward' => 0, 'Shuffle' => 0); - $created = date('Y-m-d H:i:s'); - - $run = new Run($fdb, $run_data['name']); - if (!$run->valid) { - alert("This run '{$run_data['name']}' caused problems", 'alert-danger'); - continue; - } - - // If run is locked, do not process it - $run_lockfile = APPLICATION_ROOT . "tmp/cron-{$run->name}.lock"; - if (cron_lock_exists($run_lockfile, $start_date, get_log_file("cron/cron-run-{$run->name}.log"))) { - continue; - } - - // If run should be forked, run in separate process. Else process in this loop - if ($run->cron_fork) { - $script = dirname(__FILE__) . '/cron.php'; - $stdout = get_log_file("cron/cron-run-{$run->name}.log"); - $command = "php $script -n {$run->name} >> {$stdout} 2>&1 &"; - cron_log("Execute Command Run: '{$command}'", $logfile); - exec($command, $output, $status); - if ($status != 0) { - cron_log("Command '{$command}' exited with status {$status}. Output: " . print_r($output, 1), $logfile); - } - continue; - } else { - cron_log("Execute Loop Run: '{$run->name}'", $logfile); - cron_process_run($run, $run_lockfile); - } - - if ($intercept_if_expired && microtime(true) - $start_time > $max_exec_time) { - throw new Exception("Cron Intercepted! Started at: $start_date, Intercepted at: " . date('r')); - } - } -} catch (Exception $e) { - error_log('Cron [Exception]: ' . $e->getMessage()); - error_log('Cron [Exception]: ' . $e->getTraceAsString()); -} - -$user->cron = false; - -$minutes = round((microtime(true) - $start_time) / 60, 3); -$end_date = date('r'); -cron_log("Cron ended .... {$end_date}. Took ~{$minutes} minutes", $logfile); -// Do cleanup just in case -cron_cleanup(); - -unset($site, $user, $run); -exit(0); + $name = $opts['n']; + $run = new Run($fdb, $name); + if (!$run->valid) { + exit('Run Not Found'); + } + $params['lockfile'] = APPLICATION_ROOT . "tmp/cron-{$run->name}.lock"; + $params['logfile'] = get_log_file("cron/cron-run-{$run->name}.log"); + $params['process_run'] = $run; +} else { + $params['lockfile'] = APPLICATION_ROOT . 'tmp/cron.lock'; + $params['logfile'] = get_log_file('cron.log'); + $params['process_run'] = false; +} + +$cron = new Cron($fdb, $site, $user, $cronConfig, $params); +$cron->execute(); + +unset($site, $fdb, $user, $params, $cronConfig); +exit(0); \ No newline at end of file diff --git a/bin/deamon.php b/bin/deamon.php deleted file mode 100644 index 05abab00b..000000000 --- a/bin/deamon.php +++ /dev/null @@ -1,53 +0,0 @@ -#!/usr/bin/php -cron = true; - $opts = getopt('w:a:t:c'); - $worker = isset($opts['w']) ? $opts['w'] : null; - $amount = isset($opts['a']) ? $opts['a'] : 5; - $timeout = isset($opts['t']) ? $opts['t'] : 40; - $cronize = isset($opts['c']); -} catch (Exception $e) { - _log("Deamon Start-up Error: " . $e->getMessage() . PHP_EOL . $e->getTraceAsString()); - exit(1); -} - -if ($worker) { - // Run the Workers - try { - _log("Starting {$worker} workers..."); - $workerClass = $worker . 'WorkerHelper'; - $worker = new $workerClass(); - $worker->doJobs($amount, $timeout); - } catch (Exception $e) { - _log("Worker Error [{$worker}]: " . $e->getMessage() . PHP_EOL . $e->getTraceAsString()); - exit(1); - } -} else { - // Run the Deamon - try { - _log('Starting deamon...'); - $deamon = new Deamon($fdb); - $ran = empty($cronize) ? $deamon->run() : $deamon->cronize(); - } catch (Exception $e) { - _log("Deamon Error: " . $e->getMessage() . PHP_EOL . $e->getTraceAsString()); - exit(1); - } -} diff --git a/bin/import-results.php b/bin/import-results.php index fcfb2cc58..50fbd5bd0 100644 --- a/bin/import-results.php +++ b/bin/import-results.php @@ -1,30 +1,28 @@ #!/usr/bin/php $col) { - $cols[$i] = DB::quoteCol($col); - } - return $cols; + foreach ($cols as $i => $col) { + $cols[$i] = DB::quoteCol($col); + } + return $cols; } function quoteVals($db, $vals) { - foreach ($vals as $i => $val) { - $vals[$i] = $db->quote($val); - } - return $vals; + foreach ($vals as $i => $val) { + $vals[$i] = $db->quote($val); + } + return $vals; } function help() { - echo " + echo " Import results of a study into a run. Results should referably be in CSV format. This script generates an SQL file that can then be ran against the respective formr database. Usage: @@ -40,77 +38,77 @@ function help() { --include-itemsdisplay: If this flag is present then queries for survey_items_display table will be included "; - exit(0); + exit(0); } function quit($msg = '', $code = 1) { - if ($code > 0) { - $msg = "Error($code): $msg"; - } - echo "\n$msg\n"; - exit($code); + if ($code > 0) { + $msg = "Error($code): $msg"; + } + echo "\n$msg\n"; + exit($code); } function collectVars() { - $opts = getopt ('h', array('survey-id:', 'run-id:', 'backup-file:', 'include-itemsdisplay', 'help')); - if (isset($opts['h']) || isset($opts['help'])) { - return help(); - } - - if (!isset($opts['survey-id'])) { - quit("Missing option 'survey-id'"); - } - - if (!isset($opts['run-id'])) { - quit("Missing option 'run-id'"); - } - - if (!isset($opts['position'])) { - quit("You need to specify the position of the Run unit to which the survey is attached"); - } - - if (!isset($opts['backup-file'])) { - quit("Missing option 'backup-file'"); - } - $backupFile = trim($opts['backup-file']); - if (!file_exists($backupFile)) { - quit("The backup-file provided does not exist"); - } - - return array( - 'runId' => (int) $opts['run-id'], - 'studyId' => (int) $opts['survey-id'], - 'position' => (int) $opts['position'], - 'backupFile' => $backupFile, - 'sqlBackupFile' => $backupFile . '.sql', - 'inlcudeItemsDisplay' => isset($opts['include-itemsdisplay']), - ); + $opts = getopt('h', array('survey-id:', 'run-id:', 'backup-file:', 'include-itemsdisplay', 'help')); + if (isset($opts['h']) || isset($opts['help'])) { + return help(); + } + + if (!isset($opts['survey-id'])) { + quit("Missing option 'survey-id'"); + } + + if (!isset($opts['run-id'])) { + quit("Missing option 'run-id'"); + } + + if (!isset($opts['position'])) { + quit("You need to specify the position of the Run unit to which the survey is attached"); + } + + if (!isset($opts['backup-file'])) { + quit("Missing option 'backup-file'"); + } + $backupFile = trim($opts['backup-file']); + if (!file_exists($backupFile)) { + quit("The backup-file provided does not exist"); + } + + return array( + 'runId' => (int) $opts['run-id'], + 'studyId' => (int) $opts['survey-id'], + 'position' => (int) $opts['position'], + 'backupFile' => $backupFile, + 'sqlBackupFile' => $backupFile . '.sql', + 'inlcudeItemsDisplay' => isset($opts['include-itemsdisplay']), + ); } function itemsDisplayCols($item_id, $session_id, $created, $answer) { - if (!$item_id || !$session_id || !$created) { - return null; - } - - // Simulate shown and answered times from created time - $shown = $created; - $answered = strtotime('+5 minutes', $created); - $saved = strtotime('+5 minutes', $answered); - - return array( - 'item_id' => $item_id, - 'session_id' => $session_id, - 'answer' => $answer, - 'created' => mysql_datetime($created), - 'saved' => mysql_datetime($saved), - 'shown' => mysql_datetime($shown), - 'shown_relative' => NULL, - 'answered' => mysql_datetime($answered), - 'answered_relative' => NULL, - 'displaycount' => 1, - 'display_order' => NULL, // FIX ME - 'hidden' => 0, // FIX-ME - ); + if (!$item_id || !$session_id || !$created) { + return null; + } + + // Simulate shown and answered times from created time + $shown = $created; + $answered = strtotime('+5 minutes', $created); + $saved = strtotime('+5 minutes', $answered); + + return array( + 'item_id' => $item_id, + 'session_id' => $session_id, + 'answer' => $answer, + 'created' => mysql_datetime($created), + 'saved' => mysql_datetime($saved), + 'shown' => mysql_datetime($shown), + 'shown_relative' => NULL, + 'answered' => mysql_datetime($answered), + 'answered_relative' => NULL, + 'displaycount' => 1, + 'display_order' => NULL, // FIX ME + 'hidden' => 0, // FIX-ME + ); } $vars = collectVars(); @@ -123,67 +121,67 @@ function itemsDisplayCols($item_id, $session_id, $created, $answer) { $runName = $db->findValue('survey_runs', array('id' => $runId), 'name'); $run = new Run($db, $runName); if (!$run->valid) { - quit('Invalid run. ID: ' . $runId); + quit('Invalid run. ID: ' . $runId); } $survey = Survey::loadById($studyId); if (!$survey->name || !$survey->id) { - quit('Invalid survey. ID: ' . $studyId); + quit('Invalid survey. ID: ' . $studyId); } $surveyRunUnit = $db->findRow('survey_run_units', array('unit_id' => $studyId, 'run_id' => $runId)); if (!$surveyRunUnit) { - quit("The specified survey is not referenced in the run. Please check your study if the run '{$run->name}' has a survey unit with value '{$survey->name}'"); + quit("The specified survey is not referenced in the run. Please check your study if the run '{$run->name}' has a survey unit with value '{$survey->name}'"); } $surveyItems = array(); -foreach($survey->getItems('id, name') as $item) { - $surveyItems[$item['name']] = $item; +foreach ($survey->getItems('id, name') as $item) { + $surveyItems[$item['name']] = $item; } // OPEN results sheet try { - echo "\nReading backup file '", $backupFile, "' ...\n"; - // Identify the type of $inputFileName - $fileType = PHPExcel_IOFactory::identify($backupFile); - // Create a new Reader of the type that has been identified - $objReader = PHPExcel_IOFactory::createReader($fileType); - // Load $inputFileName to a PHPExcel Object - /// Advise the Reader that we only want to load cell data - $objReader->setReadDataOnly(true); - - // Load $inputFileName to a PHPExcel Object - $objPHPExcel = PHPExcel_IOFactory::load($backupFile); - - // Get sheet - $resultsSheet = $objPHPExcel->getSheet(0); -} catch (PHPExcel_Exception $e) { - formr_log_exception($e, __CLASS__, $backupFile); - quit("Error occured while loading backup file: \n" . $e->getMessage()); + echo "\nReading backup file '", $backupFile, "' ...\n"; + // Identify the type of $inputFileName + $fileType = \PhpOffice\PhpSpreadsheet\IOFactory::identify($backupFile); + // Create a new Reader of the type that has been identified + $objReader = \PhpOffice\PhpSpreadsheet\IOFactory::createReader($fileType); + // Load $inputFileName to a \PhpOffice\PhpSpreadsheet\Spreadsheet Object + /// Advise the Reader that we only want to load cell data + $objReader->setReadDataOnly(true); + + // Load $inputFileName to a \PhpOffice\PhpSpreadsheet\Spreadsheet Object + $objPhpSpreadsheet = \PhpOffice\PhpSpreadsheet\IOFactory::load($backupFile); + + // Get sheet + $resultsSheet = $objPhpSpreadsheet->getSheet(0); +} catch (\PhpOffice\PhpSpreadsheet\Exception $e) { + formr_log_exception($e, __CLASS__, $backupFile); + quit("Error occured while loading backup file: \n" . $e->getMessage()); } // Read columns $columns = array(); -$nrColumns = PHPExcel_Cell::columnIndexFromString($resultsSheet->getHighestDataColumn()); +$nrColumns = \PhpOffice\PhpSpreadsheet\Cell\Coordinate::columnIndexFromString($resultsSheet->getHighestDataColumn()); $nrRows = $resultsSheet->getHighestDataRow(); echo "\nColumns: {$nrColumns}, Rows: {$nrRows} \n"; -for ($i = 0; $i < $nrColumns; $i++) { - $columns[] = trim($resultsSheet->getCellByColumnAndRow($i, 1)->getValue()); +for ($i = 1; $i <= $nrColumns; $i++) { + $columns[] = trim($resultsSheet->getCellByColumnAndRow($i, 1)->getValue()); } // Validate columns of backup sheet to ensure all are in survey items sheet $badCols = array(); foreach ($columns as $col) { - if (!isset($surveyItems[$col]) && !in_array($col, $sysColumns)) { - $badCols[] = $col; - } + if (!isset($surveyItems[$col]) && !in_array($col, $sysColumns)) { + $badCols[] = $col; + } } if ($badCols) { - quit('Invalid Column(s) in results sheet: ' . implode(', ', $badCols)); + quit('Invalid Column(s) in results sheet: ' . implode(', ', $badCols)); } // Empty sql backup file if (file_exists($sqlBackupFile)) { - rename($sqlBackupFile, $sqlBackupFile . '.formrbk'); + rename($sqlBackupFile, $sqlBackupFile . '.formrbk'); } file_put_contents($sqlBackupFile, ''); @@ -191,123 +189,123 @@ function itemsDisplayCols($item_id, $session_id, $created, $answer) { $processed = 0; $fp = fopen($sqlBackupFile, 'wb'); if (!$fp) { - quit('Unable to open sql file for writing'); + quit('Unable to open sql file for writing'); } foreach ($resultsSheet->getRowIterator() as $row) { - $rowNr = $row->getRowIndex(); - // Read only data rows - if ($rowNr > $nrRows) { - break; - } - - // Heading names - if ($rowNr == 1) { - continue; - } - - $cellIterator = $row->getCellIterator(); - $cellIterator->setIterateOnlyExistingCells(false); // Loop all cells, even if it is not set - $runSession = null; - $entry = array(); - foreach ($cellIterator as $cell) { - if (is_null($cell)) { - continue; - } - - $colIndex = $cell->columnIndexFromString($cell->getColumn()) - 1; - if (!array_key_exists($colIndex, $columns)) { - continue; - } - $column = $columns[$colIndex]; - $value = trim($cell->getValue()); - if (!is_formr_truthy($value)) { - $value = null; - } elseif (in_array($column, $dateColumns)) { - $value = mysql_datetime(PHPExcel_Shared_Date::ExcelToPHP($value)); - } - - if (!in_array($column, $exclColumns)) { - $entry[$column] = $value; - } - - if ($value && $runSession === null && $column == 'session') { - $value = ltrim($value, '='); - $runSession = new RunSession($db, $runId, null, $value, $run); - // Create a fake session and end it if it doesn't exist (maybe set a flag to enable this in command - if ($runSession->id <= 0) { - $runSession->create($value); - $runSession->runTo($position, $studyId); - $unitSession = $runSession->getCurrentUnit(); - $entry['session_id'] = $unitSession['session_id']; - $runSession->end(); - } else { - $unitSession = new UnitSession($db, $runSession->id, $studyId); - $entry['session_id'] = $unitSession->create(); - } - } - } - - if ($entry && $runSession && $runSession->id > 0) { - $entry['study_id'] = $studyId; - $newline = "\n\n/*NEW ROW: Inserting data for run-session: " . $runSession->session . "*/"; - echo $newline; - fwrite($fp, $newline); - - $unitSession = array( - 'id' => $entry['session_id'], - 'unit_id' => $studyId, - 'run_session_id' => $runSession->id, - 'created' => $entry['created'], - 'ended' => $entry['ended'], - ); - - // Insert unit session entry - $unitSessionCols = quoteCols($db, array_keys($unitSession)); - $unitSessionVals = quoteVals($db, array_values($unitSession)); - $sql = "\nINSERT INTO `survey_unit_sessions` (" . implode(', ', $unitSessionCols). ") VALUES (" . implode(', ', $unitSessionVals). ") ON DUPLICATE KEY UPDATE id=VALUES(id);"; - fwrite($fp, $sql); - - // Insert results table entry - $resultCols = quoteCols($db, array_keys($entry)); - $resultVals = quoteVals($db, array_values($entry)); - array_walk($resultCols, array('DB', 'quoteCol')); - array_walk($resultVals, array($db, 'quote')); - $sql = "\nINSERT INTO `{$survey->results_table}` (" . implode(', ', $resultCols). ") VALUES (" . implode(', ', $resultVals). ") ON DUPLICATE KEY UPDATE session_id=VALUES(session_id);"; - fwrite($fp, $sql); - - // Insert to items display table - foreach ($entry as $itemName => $itemValue) { - if (!isset($surveyItems[$itemName])) { - continue; - } - - $item_id = array_val($surveyItems[$itemName], 'id'); - $session_id = $entry['session_id']; - $created = strtotime($entry['created']); - $itemsDisplay = itemsDisplayCols($item_id, $session_id, $created, $itemValue); - - if (!empty($inlcudeItemsDisplay) && ($itemsDisplay = itemsDisplayCols($item_id, $session_id, $created, $itemValue))) { - $displayCols = quoteCols($db, array_keys($itemsDisplay)); - $displayVals = quoteVals($db, array_values($itemsDisplay)); - array_walk($displayCols, array('DB', 'quoteCol')); - array_walk($displayVals, array($db, 'quote')); - $sql = "\nINSERT INTO `survey_items_display` (" . implode(', ', $displayCols). ") VALUES (" . implode(', ', $displayVals). ") ON DUPLICATE KEY UPDATE session_id=VALUES(session_id);"; - fwrite($fp, $sql); - } - } - - $processed++; - if ($processed % 100 == 0) { - echo "\n sleeping for 2 seconds..."; - sleep(2); - } - } else { - $sess = !empty($runSession->session) ? $runSession->session : null; - $missing = "\n\n/* missing entry - unit session_id : {$entry['session_id']}; session: {$sess} */"; - echo $missing; - fwrite($fp, $missing); - } + $rowNr = $row->getRowIndex(); + // Read only data rows + if ($rowNr > $nrRows) { + break; + } + + // Heading names + if ($rowNr == 1) { + continue; + } + + $cellIterator = $row->getCellIterator(); + $cellIterator->setIterateOnlyExistingCells(false); // Loop all cells, even if it is not set + $runSession = null; + $entry = array(); + foreach ($cellIterator as $cell) { + if (is_null($cell)) { + continue; + } + + $colIndex = $cell->columnIndexFromString($cell->getColumn()); + if (!array_key_exists($colIndex, $columns)) { + continue; + } + $column = $columns[$colIndex]; + $value = trim($cell->getValue()); + if (!is_formr_truthy($value)) { + $value = null; + } elseif (in_array($column, $dateColumns)) { + $value = mysql_datetime(\PhpOffice\PhpSpreadsheet\Shared\Date::excelToTimestamp($value)); + } + + if (!in_array($column, $exclColumns)) { + $entry[$column] = $value; + } + + if ($value && $runSession === null && $column == 'session') { + $value = ltrim($value, '='); + $runSession = new RunSession($db, $runId, null, $value, $run); + // Create a fake session and end it if it doesn't exist (maybe set a flag to enable this in command + if ($runSession->id <= 0) { + $runSession->create($value); + $runSession->runTo($position, $studyId); + $unitSession = $runSession->getCurrentUnit(); + $entry['session_id'] = $unitSession['session_id']; + $runSession->end(); + } else { + $unitSession = new UnitSession($db, $runSession->id, $studyId); + $entry['session_id'] = $unitSession->create(); + } + } + } + + if ($entry && $runSession && $runSession->id > 0) { + $entry['study_id'] = $studyId; + $newline = "\n\n/*NEW ROW: Inserting data for run-session: " . $runSession->session . "*/"; + echo $newline; + fwrite($fp, $newline); + + $unitSession = array( + 'id' => $entry['session_id'], + 'unit_id' => $studyId, + 'run_session_id' => $runSession->id, + 'created' => $entry['created'], + 'ended' => $entry['ended'], + ); + + // Insert unit session entry + $unitSessionCols = quoteCols($db, array_keys($unitSession)); + $unitSessionVals = quoteVals($db, array_values($unitSession)); + $sql = "\nINSERT INTO `survey_unit_sessions` (" . implode(', ', $unitSessionCols) . ") VALUES (" . implode(', ', $unitSessionVals) . ") ON DUPLICATE KEY UPDATE id=VALUES(id);"; + fwrite($fp, $sql); + + // Insert results table entry + $resultCols = quoteCols($db, array_keys($entry)); + $resultVals = quoteVals($db, array_values($entry)); + array_walk($resultCols, array('DB', 'quoteCol')); + array_walk($resultVals, array($db, 'quote')); + $sql = "\nINSERT INTO `{$survey->results_table}` (" . implode(', ', $resultCols) . ") VALUES (" . implode(', ', $resultVals) . ") ON DUPLICATE KEY UPDATE session_id=VALUES(session_id);"; + fwrite($fp, $sql); + + // Insert to items display table + foreach ($entry as $itemName => $itemValue) { + if (!isset($surveyItems[$itemName])) { + continue; + } + + $item_id = array_val($surveyItems[$itemName], 'id'); + $session_id = $entry['session_id']; + $created = strtotime($entry['created']); + $itemsDisplay = itemsDisplayCols($item_id, $session_id, $created, $itemValue); + + if (!empty($inlcudeItemsDisplay) && ($itemsDisplay = itemsDisplayCols($item_id, $session_id, $created, $itemValue))) { + $displayCols = quoteCols($db, array_keys($itemsDisplay)); + $displayVals = quoteVals($db, array_values($itemsDisplay)); + array_walk($displayCols, array('DB', 'quoteCol')); + array_walk($displayVals, array($db, 'quote')); + $sql = "\nINSERT INTO `survey_items_display` (" . implode(', ', $displayCols) . ") VALUES (" . implode(', ', $displayVals) . ") ON DUPLICATE KEY UPDATE session_id=VALUES(session_id);"; + fwrite($fp, $sql); + } + } + + $processed++; + if ($processed % 100 == 0) { + echo "\n sleeping for 2 seconds..."; + sleep(2); + } + } else { + $sess = !empty($runSession->session) ? $runSession->session : null; + $missing = "\n\n/* missing entry - unit session_id : {$entry['session_id']}; session: {$sess} */"; + echo $missing; + fwrite($fp, $missing); + } } fclose($fp); diff --git a/bin/mailer.php b/bin/mailer.php deleted file mode 100644 index 7caa3a715..000000000 --- a/bin/mailer.php +++ /dev/null @@ -1,27 +0,0 @@ -#!/usr/bin/php -run($account_id); diff --git a/bin/queue-migration.php b/bin/queue-migration.php new file mode 100644 index 000000000..0592a1b90 --- /dev/null +++ b/bin/queue-migration.php @@ -0,0 +1,37 @@ +#!/usr/bin/php +query($query, true); + while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) { + // foreach item in the old sessions_queue table, + // update the expires timestamp in the unit_sessions table + $query = "UPDATE survey_unit_sessions SET expires = :expires, queued = :queued WHERE id = :id AND ended IS NULL"; + echo $db->exec($query, array( + 'id' => $row['unit_session_id'], + 'expires' => mysql_datetime($row['expires']), + 'queued' => $row['execute'] ? UnitSessionQueue::QUEUED_TO_EXECUTE : UnitSessionQueue::QUEUED_TO_END, + )); + } +} + + +function run_stuck_pauses(DB $db) { + $query = "UPDATE survey_unit_sessions + LEFT JOIN survey_units ON survey_units.id = survey_unit_sessions.unit_id + SET `survey_unit_sessions`.queued = 1, `survey_unit_sessions`.expires = :now + WHERE survey_units.type IN ('Pause', 'Wait') AND + survey_unit_sessions.ended IS NULL AND survey_unit_sessions.expires IS NULL"; + + echo $db->exec($query, array('now' => mysql_datetime())); +} + +$opts = getopt('m:'); +if ($opts['m'] === 'update_unit_sessions_table') { + update_unit_sessions_table(DB::getInstance()); +} elseif ($opts['m'] === 'run_stuck_pauses') { + run_stuck_pauses(DB::getInstance()); +} diff --git a/bin/queue.php b/bin/queue.php new file mode 100644 index 000000000..d47abc563 --- /dev/null +++ b/bin/queue.php @@ -0,0 +1,74 @@ +#!/usr/bin/php +run(); +} catch (Exception $e) { + formr_log_exception($e, 'Queue'); + sleep(15); +} diff --git a/bin/tasks.php b/bin/tasks.php index 4eb9758a3..2a3594024 100644 --- a/bin/tasks.php +++ b/bin/tasks.php @@ -1,10 +1,9 @@ #!/usr/bin/php prepare($sql); - $stmt->execute(); - while($row = $stmt->fetch(PDO::FETCH_ASSOC)) { - if (!empty($row['reminder_email'])) { - $data = array( - 'id' => $row['reminder_email'], - 'run_id' => $row['id'], - 'type' => 'ReminderEmail', - 'description' => $db->findValue('survey_run_units', array('unit_id' => $row['reminder_email']), 'description'), - ); - $db->insert_update('survey_run_special_units', $data); - } - - if (!empty($row['service_message'])) { - $data = array( - 'id' => $row['service_message'], - 'run_id' => $row['id'], - 'type' => 'ServiceMessagePage', - 'description' => $db->findValue('survey_run_units', array('unit_id' => $row['service_message']), 'description'), - ); - $db->insert_update('survey_run_special_units', $data); - } - - if (!empty($row['overview_script'])) { - $data = array( - 'id' => $row['overview_script'], - 'run_id' => $row['id'], - 'type' => 'OverviewScriptPage', - 'description' => $db->findValue('survey_run_units', array('unit_id' => $row['overview_script']), 'description'), - ); - $db->insert_update('survey_run_special_units', $data); - } - } + $sql = "SELECT survey_runs.id, survey_runs.reminder_email, survey_runs.service_message, survey_runs.overview_script FROM survey_runs"; + $db = DB::getInstance(); + $stmt = $db->prepare($sql); + $stmt->execute(); + while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) { + if (!empty($row['reminder_email'])) { + $data = array( + 'id' => $row['reminder_email'], + 'run_id' => $row['id'], + 'type' => 'ReminderEmail', + 'description' => $db->findValue('survey_run_units', array('unit_id' => $row['reminder_email']), 'description'), + ); + $db->insert_update('survey_run_special_units', $data); + } + + if (!empty($row['service_message'])) { + $data = array( + 'id' => $row['service_message'], + 'run_id' => $row['id'], + 'type' => 'ServiceMessagePage', + 'description' => $db->findValue('survey_run_units', array('unit_id' => $row['service_message']), 'description'), + ); + $db->insert_update('survey_run_special_units', $data); + } + + if (!empty($row['overview_script'])) { + $data = array( + 'id' => $row['overview_script'], + 'run_id' => $row['id'], + 'type' => 'OverviewScriptPage', + 'description' => $db->findValue('survey_run_units', array('unit_id' => $row['overview_script']), 'description'), + ); + $db->insert_update('survey_run_special_units', $data); + } + } } -/** +/** * Encrypt Existing email accounts * */ function encryptAccounts() { - $DB = DB::getInstance(); - Crypto::setup(); - $accounts = $DB->select('id, username, password, auth_key')->from('survey_email_accounts')->fetchAll(); - foreach ($accounts as $account) { - if (!$account['username'] || !$account['password']) { - continue; - } - if ($account['auth_key']) { - _e('ERROR: ' . $account['username'] . ' already has an auth key'); - continue; - } + $DB = DB::getInstance(); + Crypto::setup(); + $accounts = $DB->select('id, username, password, auth_key')->from('survey_email_accounts')->fetchAll(); + foreach ($accounts as $account) { + if (!$account['username'] || !$account['password']) { + continue; + } + if ($account['auth_key']) { + _e('ERROR: ' . $account['username'] . ' already has an auth key'); + continue; + } - $auth_key = Crypto::encrypt(array($account['username'], $account['password']), EmailAccount::AK_GLUE); - if ($auth_key && $DB->update('survey_email_accounts', array('auth_key' => $auth_key), array('id' => (int) $account['id']))) { - _e('SUCCESS: ' . $account['username'] . ' auth_key created'); - } else { - _e('ERROR: ' . $account['username'] . ' auth_key NOT created (check logs)'); - } - } + $auth_key = Crypto::encrypt(array($account['username'], $account['password']), EmailAccount::AK_GLUE); + if ($auth_key && $DB->update('survey_email_accounts', array('auth_key' => $auth_key), array('id' => (int) $account['id']))) { + _e('SUCCESS: ' . $account['username'] . ' auth_key created'); + } else { + _e('ERROR: ' . $account['username'] . ' auth_key NOT created (check logs)'); + } + } } /** @@ -80,29 +79,29 @@ function encryptAccounts() { * */ function renameRuns() { - $sql = "SELECT id, name FROM survey_runs"; - $db = DB::getInstance(); - $stmt = $db->prepare($sql); - $stmt->execute(); + $sql = "SELECT id, name FROM survey_runs"; + $db = DB::getInstance(); + $stmt = $db->prepare($sql); + $stmt->execute(); - while($row = $stmt->fetch(PDO::FETCH_ASSOC)) { - if (strstr($row['name'], '_') === false) { - continue; - } + while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) { + if (strstr($row['name'], '_') === false) { + continue; + } - $name = str_replace('_', '-', $row['name']); - echo sprintf("\n Rename %s -> %s.", $row['name'], $name); - $db->update('survey_runs', array('name' => $name), array('id' => (int)$row['id'])); - } + $name = str_replace('_', '-', $row['name']); + echo sprintf("\n Rename %s -> %s.", $row['name'], $name); + $db->update('survey_runs', array('name' => $name), array('id' => (int) $row['id'])); + } } -$opts = getopt ('t:'); +$opts = getopt('t:'); $task = !empty($opts['t']) ? $opts['t'] : null; if ($task && function_exists($task)) { - call_user_func($task); - echo "\n DONE \n"; - exit(0); + call_user_func($task); + echo "\n DONE \n"; + exit(0); } else { - exit("\nInvalid Task '{$task}' \n"); + exit("\nInvalid Task '{$task}' \n"); } \ No newline at end of file diff --git a/composer.json b/composer.json index 460b51a8a..736de6eed 100644 --- a/composer.json +++ b/composer.json @@ -11,19 +11,24 @@ "email": "rubenarslan@gmail.com", "homepage": "https://github.com/rubenarslan", "role": "Developer" + }, + { + "name": "Cyril Tata", + "email": "cyril.tata@gmail.com", + "homepage": "https://github.com/cyriltata", + "role": "Developer" } ], "require": { - "php": ">=5.4.0", + "php": ">=8.0", "erusev/parsedown": "1.*", "erusev/parsedown-extra": "0.*", "ircmaxell/password-compat": "1.*", - "phpoffice/phpexcel": "1.*", - "phpmailer/phpmailer": "5.*", "bshaffer/oauth2-server-php": "~1.7", - "atrox/haikunator": "^1.0", - "mhlavac/gearman": "dev-master", - "paragonie/halite": "^v1" + "atrox/haikunator": "1.*", + "paragonie/halite": "^4", + "phpoffice/phpspreadsheet": "^1.24", + "phpmailer/phpmailer": "6.*" }, "require-dev": { "phpunit/phpunit": "3.7.*" diff --git a/composer.lock b/composer.lock index de9473a11..d528cbddf 100644 --- a/composer.lock +++ b/composer.lock @@ -1,71 +1,86 @@ { "_readme": [ "This file locks the dependencies of your project to a known state", - "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file", + "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "hash": "b58aee79ae9525c549223f3d772da221", - "content-hash": "b0ec0e93db3d122650dbbed41e4e624f", + "content-hash": "4cc8da7b9dd04313d817a135d5dc8a0d", "packages": [ { "name": "atrox/haikunator", - "version": "v1.0.0", + "version": "v1.3.0", "source": { "type": "git", "url": "https://github.com/Atrox/haikunatorphp.git", - "reference": "cbf8f5056ada208b19a715001a9ce6ee8f78aad3" + "reference": "98a70f9da1bddae439189a459b5cd0e655cbca23" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Atrox/haikunatorphp/zipball/cbf8f5056ada208b19a715001a9ce6ee8f78aad3", - "reference": "cbf8f5056ada208b19a715001a9ce6ee8f78aad3", + "url": "https://api.github.com/repos/Atrox/haikunatorphp/zipball/98a70f9da1bddae439189a459b5cd0e655cbca23", + "reference": "98a70f9da1bddae439189a459b5cd0e655cbca23", "shasum": "" }, + "require": { + "php": ">=5.5" + }, "require-dev": { - "phpunit/php-code-coverage": "2.2.*", - "phpunit/phpunit": "4.5.*", - "satooshi/php-coveralls": "dev-master" + "phpunit/phpunit": ">=5.0" }, "type": "library", "autoload": { - "classmap": [ - "src/" - ] + "psr-4": { + "Atrox\\": "src" + } }, "notification-url": "https://packagist.org/downloads/", "license": [ - "GPLv3" + "BSD-3-Clause" ], "authors": [ { "name": "Atrox", - "email": "mail@atrox.me" + "email": "hello@atrox.dev" } ], "description": "Generate Heroku-like random names to use in your php applications.", - "time": "2015-10-05 21:17:21" + "support": { + "issues": "https://github.com/Atrox/haikunatorphp/issues", + "source": "https://github.com/Atrox/haikunatorphp/tree/v1.3.0" + }, + "time": "2020-05-08T10:24:01+00:00" }, { "name": "bshaffer/oauth2-server-php", - "version": "v1.7.1", + "version": "v1.12.1", "source": { "type": "git", "url": "https://github.com/bshaffer/oauth2-server-php.git", - "reference": "7fde34c417e0f6542dfbe6aedf8132eb5575acfb" + "reference": "2bfaf9d7bbebe2ba1c1deb48e756ba0b3af4e985" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/bshaffer/oauth2-server-php/zipball/7fde34c417e0f6542dfbe6aedf8132eb5575acfb", - "reference": "7fde34c417e0f6542dfbe6aedf8132eb5575acfb", + "url": "https://api.github.com/repos/bshaffer/oauth2-server-php/zipball/2bfaf9d7bbebe2ba1c1deb48e756ba0b3af4e985", + "reference": "2bfaf9d7bbebe2ba1c1deb48e756ba0b3af4e985", "shasum": "" }, "require": { - "php": ">=5.3.9" + "php": ">=7.1" + }, + "require-dev": { + "aws/aws-sdk-php": "^2.8", + "firebase/php-jwt": "^2.2", + "mongodb/mongodb": "^1.1", + "phpunit/phpunit": "^7.5||^8.0", + "predis/predis": "^1.1", + "thobbs/phpcassa": "dev-master", + "yoast/phpunit-polyfills": "^1.0" }, "suggest": { - "aws/aws-sdk-php": "Required to use the DynamoDB storage engine", - "predis/predis": "Required to use the Redis storage engine", - "thobbs/phpcassa": "Required to use the Cassandra storage engine" + "aws/aws-sdk-php": "~2.8 is required to use DynamoDB storage", + "firebase/php-jwt": "~2.2 is required to use JWT features", + "mongodb/mongodb": "^1.1 is required to use MongoDB storage", + "predis/predis": "Required to use Redis storage", + "thobbs/phpcassa": "Required to use Cassandra storage" }, "type": "library", "autoload": { @@ -91,22 +106,33 @@ "oauth", "oauth2" ], - "time": "2015-05-21 20:07:03" + "support": { + "issues": "https://github.com/bshaffer/oauth2-server-php/issues", + "source": "https://github.com/bshaffer/oauth2-server-php/tree/v1.12.1" + }, + "time": "2022-05-31T16:12:58+00:00" }, { "name": "erusev/parsedown", - "version": "1.5.4", + "version": "1.7.4", "source": { "type": "git", "url": "https://github.com/erusev/parsedown.git", - "reference": "0e89e3714bda18973184d30646306bb0a482bd96" + "reference": "cb17b6477dfff935958ba01325f2e8a2bfa6dab3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/erusev/parsedown/zipball/0e89e3714bda18973184d30646306bb0a482bd96", - "reference": "0e89e3714bda18973184d30646306bb0a482bd96", + "url": "https://api.github.com/repos/erusev/parsedown/zipball/cb17b6477dfff935958ba01325f2e8a2bfa6dab3", + "reference": "cb17b6477dfff935958ba01325f2e8a2bfa6dab3", "shasum": "" }, + "require": { + "ext-mbstring": "*", + "php": ">=5.3.0" + }, + "require-dev": { + "phpunit/phpunit": "^4.8.35" + }, "type": "library", "autoload": { "psr-0": { @@ -130,24 +156,31 @@ "markdown", "parser" ], - "time": "2015-08-03 09:24:05" + "support": { + "issues": "https://github.com/erusev/parsedown/issues", + "source": "https://github.com/erusev/parsedown/tree/1.7.x" + }, + "time": "2019-12-30T22:54:17+00:00" }, { "name": "erusev/parsedown-extra", - "version": "0.7.0", + "version": "0.8.1", "source": { "type": "git", "url": "https://github.com/erusev/parsedown-extra.git", - "reference": "11a44e076d02ffcc4021713398a60cd73f78b6f5" + "reference": "91ac3ff98f0cea243bdccc688df43810f044dcef" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/erusev/parsedown-extra/zipball/11a44e076d02ffcc4021713398a60cd73f78b6f5", - "reference": "11a44e076d02ffcc4021713398a60cd73f78b6f5", + "url": "https://api.github.com/repos/erusev/parsedown-extra/zipball/91ac3ff98f0cea243bdccc688df43810f044dcef", + "reference": "91ac3ff98f0cea243bdccc688df43810f044dcef", "shasum": "" }, "require": { - "erusev/parsedown": "~1.4" + "erusev/parsedown": "^1.7.4" + }, + "require-dev": { + "phpunit/phpunit": "^4.8.35" }, "type": "library", "autoload": { @@ -174,103 +207,62 @@ "parsedown", "parser" ], - "time": "2015-01-25 14:52:34" + "support": { + "issues": "https://github.com/erusev/parsedown-extra/issues", + "source": "https://github.com/erusev/parsedown-extra/tree/0.8.x" + }, + "time": "2019-12-30T23:20:37+00:00" }, { - "name": "guzzle/guzzle", - "version": "v3.9.3", + "name": "ezyang/htmlpurifier", + "version": "v4.14.0", "source": { "type": "git", - "url": "https://github.com/guzzle/guzzle3.git", - "reference": "0645b70d953bc1c067bbc8d5bc53194706b628d9" + "url": "https://github.com/ezyang/htmlpurifier.git", + "reference": "12ab42bd6e742c70c0a52f7b82477fcd44e64b75" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/guzzle/guzzle3/zipball/0645b70d953bc1c067bbc8d5bc53194706b628d9", - "reference": "0645b70d953bc1c067bbc8d5bc53194706b628d9", + "url": "https://api.github.com/repos/ezyang/htmlpurifier/zipball/12ab42bd6e742c70c0a52f7b82477fcd44e64b75", + "reference": "12ab42bd6e742c70c0a52f7b82477fcd44e64b75", "shasum": "" }, "require": { - "ext-curl": "*", - "php": ">=5.3.3", - "symfony/event-dispatcher": "~2.1" - }, - "replace": { - "guzzle/batch": "self.version", - "guzzle/cache": "self.version", - "guzzle/common": "self.version", - "guzzle/http": "self.version", - "guzzle/inflection": "self.version", - "guzzle/iterator": "self.version", - "guzzle/log": "self.version", - "guzzle/parser": "self.version", - "guzzle/plugin": "self.version", - "guzzle/plugin-async": "self.version", - "guzzle/plugin-backoff": "self.version", - "guzzle/plugin-cache": "self.version", - "guzzle/plugin-cookie": "self.version", - "guzzle/plugin-curlauth": "self.version", - "guzzle/plugin-error-response": "self.version", - "guzzle/plugin-history": "self.version", - "guzzle/plugin-log": "self.version", - "guzzle/plugin-md5": "self.version", - "guzzle/plugin-mock": "self.version", - "guzzle/plugin-oauth": "self.version", - "guzzle/service": "self.version", - "guzzle/stream": "self.version" - }, - "require-dev": { - "doctrine/cache": "~1.3", - "monolog/monolog": "~1.0", - "phpunit/phpunit": "3.7.*", - "psr/log": "~1.0", - "symfony/class-loader": "~2.1", - "zendframework/zend-cache": "2.*,<2.3", - "zendframework/zend-log": "2.*,<2.3" - }, - "suggest": { - "guzzlehttp/guzzle": "Guzzle 5 has moved to a new package name. The package you have installed, Guzzle 3, is deprecated." + "php": ">=5.2" }, "type": "library", - "extra": { - "branch-alias": { - "dev-master": "3.9-dev" - } - }, "autoload": { + "files": [ + "library/HTMLPurifier.composer.php" + ], "psr-0": { - "Guzzle": "src/", - "Guzzle\\Tests": "tests/" - } + "HTMLPurifier": "library/" + }, + "exclude-from-classmap": [ + "/library/HTMLPurifier/Language/" + ] }, "notification-url": "https://packagist.org/downloads/", "license": [ - "MIT" + "LGPL-2.1-or-later" ], "authors": [ { - "name": "Michael Dowling", - "email": "mtdowling@gmail.com", - "homepage": "https://github.com/mtdowling" - }, - { - "name": "Guzzle Community", - "homepage": "https://github.com/guzzle/guzzle/contributors" + "name": "Edward Z. Yang", + "email": "admin@htmlpurifier.org", + "homepage": "http://ezyang.com" } ], - "description": "PHP HTTP client. This library is deprecated in favor of https://packagist.org/packages/guzzlehttp/guzzle", - "homepage": "http://guzzlephp.org/", + "description": "Standards compliant HTML filter written in PHP", + "homepage": "http://htmlpurifier.org/", "keywords": [ - "client", - "curl", - "framework", - "http", - "http client", - "rest", - "web service" + "html" ], - "abandoned": "guzzlehttp/guzzle", - "time": "2015-03-18 18:23:50" + "support": { + "issues": "https://github.com/ezyang/htmlpurifier/issues", + "source": "https://github.com/ezyang/htmlpurifier/tree/v4.14.0" + }, + "time": "2021-12-25T01:21:49+00:00" }, { "name": "ircmaxell/password-compat", @@ -312,42 +304,112 @@ "hashing", "password" ], - "time": "2014-11-20 16:49:30" + "support": { + "issues": "https://github.com/ircmaxell/password_compat/issues", + "source": "https://github.com/ircmaxell/password_compat/tree/v1.0" + }, + "time": "2014-11-20T16:49:30+00:00" }, { - "name": "league/oauth2-client", - "version": "0.10.1", + "name": "maennchen/zipstream-php", + "version": "2.2.1", "source": { "type": "git", - "url": "https://github.com/thephpleague/oauth2-client.git", - "reference": "cd2e968b494641c1fd2b852691d000cd95e560de" + "url": "https://github.com/maennchen/ZipStream-PHP.git", + "reference": "211e9ba1530ea5260b45d90c9ea252f56ec52729" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/thephpleague/oauth2-client/zipball/cd2e968b494641c1fd2b852691d000cd95e560de", - "reference": "cd2e968b494641c1fd2b852691d000cd95e560de", + "url": "https://api.github.com/repos/maennchen/ZipStream-PHP/zipball/211e9ba1530ea5260b45d90c9ea252f56ec52729", + "reference": "211e9ba1530ea5260b45d90c9ea252f56ec52729", "shasum": "" }, "require": { - "guzzle/guzzle": "~3.7", - "php": ">=5.4.0" + "myclabs/php-enum": "^1.5", + "php": "^7.4 || ^8.0", + "psr/http-message": "^1.0", + "symfony/polyfill-mbstring": "^1.0" }, "require-dev": { - "jakub-onderka/php-parallel-lint": "0.8.*", - "mockery/mockery": "~0.9", - "phpunit/phpunit": "~4.0", - "satooshi/php-coveralls": "0.6.*", - "squizlabs/php_codesniffer": "~2.0" + "ext-zip": "*", + "guzzlehttp/guzzle": "^6.5.3 || ^7.2.0", + "mikey179/vfsstream": "^1.6", + "php-coveralls/php-coveralls": "^2.4", + "phpunit/phpunit": "^8.5.8 || ^9.4.2", + "vimeo/psalm": "^4.1" }, "type": "library", - "extra": { - "branch-alias": { - "dev-master": "0.10.x-dev" + "autoload": { + "psr-4": { + "ZipStream\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Paul Duncan", + "email": "pabs@pablotron.org" + }, + { + "name": "Jonatan Männchen", + "email": "jonatan@maennchen.ch" + }, + { + "name": "Jesse Donat", + "email": "donatj@gmail.com" + }, + { + "name": "András Kolesár", + "email": "kolesar@kolesar.hu" + } + ], + "description": "ZipStream is a library for dynamically streaming dynamic zip files from PHP without writing to the disk at all on the server.", + "keywords": [ + "stream", + "zip" + ], + "support": { + "issues": "https://github.com/maennchen/ZipStream-PHP/issues", + "source": "https://github.com/maennchen/ZipStream-PHP/tree/2.2.1" + }, + "funding": [ + { + "url": "https://opencollective.com/zipstream", + "type": "open_collective" } + ], + "time": "2022-05-18T15:52:06+00:00" + }, + { + "name": "markbaker/complex", + "version": "3.0.1", + "source": { + "type": "git", + "url": "https://github.com/MarkBaker/PHPComplex.git", + "reference": "ab8bc271e404909db09ff2d5ffa1e538085c0f22" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/MarkBaker/PHPComplex/zipball/ab8bc271e404909db09ff2d5ffa1e538085c0f22", + "reference": "ab8bc271e404909db09ff2d5ffa1e538085c0f22", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0" }, + "require-dev": { + "dealerdirect/phpcodesniffer-composer-installer": "^0.7.0", + "phpcompatibility/php-compatibility": "^9.0", + "phpunit/phpunit": "^7.0 || ^8.0 || ^9.3", + "squizlabs/php_codesniffer": "^3.4" + }, + "type": "library", "autoload": { "psr-4": { - "League\\OAuth2\\Client\\": "src/" + "Complex\\": "classes/src/" } }, "notification-url": "https://packagist.org/downloads/", @@ -356,135 +418,521 @@ ], "authors": [ { - "name": "Alex Bilbie", - "email": "hello@alexbilbie.com", - "homepage": "http://www.alexbilbie.com", - "role": "Developer" + "name": "Mark Baker", + "email": "mark@lange.demon.co.uk" } ], - "description": "OAuth 2.0 Client Library", + "description": "PHP Class for working with complex numbers", + "homepage": "https://github.com/MarkBaker/PHPComplex", "keywords": [ - "Authentication", - "SSO", - "authorization", - "identity", - "idp", - "oauth", - "oauth2", - "single sign on" + "complex", + "mathematics" ], - "time": "2015-04-02 15:09:47" + "support": { + "issues": "https://github.com/MarkBaker/PHPComplex/issues", + "source": "https://github.com/MarkBaker/PHPComplex/tree/3.0.1" + }, + "time": "2021-06-29T15:32:53+00:00" }, { - "name": "mhlavac/gearman", - "version": "dev-master", + "name": "markbaker/matrix", + "version": "3.0.0", "source": { "type": "git", - "url": "https://github.com/mhlavac/gearman.git", - "reference": "c174db79f5847921a497fe51931e83a35283a35e" + "url": "https://github.com/MarkBaker/PHPMatrix.git", + "reference": "c66aefcafb4f6c269510e9ac46b82619a904c576" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/mhlavac/gearman/zipball/c174db79f5847921a497fe51931e83a35283a35e", - "reference": "c174db79f5847921a497fe51931e83a35283a35e", + "url": "https://api.github.com/repos/MarkBaker/PHPMatrix/zipball/c66aefcafb4f6c269510e9ac46b82619a904c576", + "reference": "c66aefcafb4f6c269510e9ac46b82619a904c576", "shasum": "" }, "require": { - "php": ">=5.3.0" + "php": "^7.1 || ^8.0" }, "require-dev": { - "fabpot/php-cs-fixer": "~1.10", - "phpunit/phpunit": "~4.0", - "symfony/process": "~2.7" + "dealerdirect/phpcodesniffer-composer-installer": "^0.7.0", + "phpcompatibility/php-compatibility": "^9.0", + "phpdocumentor/phpdocumentor": "2.*", + "phploc/phploc": "^4.0", + "phpmd/phpmd": "2.*", + "phpunit/phpunit": "^7.0 || ^8.0 || ^9.3", + "sebastian/phpcpd": "^4.0", + "squizlabs/php_codesniffer": "^3.4" }, "type": "library", - "extra": { - "branch-alias": { - "dev-master": "0.1.x-dev" + "autoload": { + "psr-4": { + "Matrix\\": "classes/src/" } }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Mark Baker", + "email": "mark@demon-angel.eu" + } + ], + "description": "PHP Class for working with matrices", + "homepage": "https://github.com/MarkBaker/PHPMatrix", + "keywords": [ + "mathematics", + "matrix", + "vector" + ], + "support": { + "issues": "https://github.com/MarkBaker/PHPMatrix/issues", + "source": "https://github.com/MarkBaker/PHPMatrix/tree/3.0.0" + }, + "time": "2021-07-01T19:01:15+00:00" + }, + { + "name": "myclabs/php-enum", + "version": "1.8.3", + "source": { + "type": "git", + "url": "https://github.com/myclabs/php-enum.git", + "reference": "b942d263c641ddb5190929ff840c68f78713e937" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/myclabs/php-enum/zipball/b942d263c641ddb5190929ff840c68f78713e937", + "reference": "b942d263c641ddb5190929ff840c68f78713e937", + "shasum": "" + }, + "require": { + "ext-json": "*", + "php": "^7.3 || ^8.0" + }, + "require-dev": { + "phpunit/phpunit": "^9.5", + "squizlabs/php_codesniffer": "1.*", + "vimeo/psalm": "^4.6.2" + }, + "type": "library", "autoload": { "psr-4": { - "MHlavac\\Gearman\\": "src/" + "MyCLabs\\Enum\\": "src/" } }, "notification-url": "https://packagist.org/downloads/", "license": [ - "New BSD" + "MIT" ], "authors": [ { - "name": "Till Klampaeckel", - "homepage": "http://pear.php.net/user/till" - }, + "name": "PHP Enum contributors", + "homepage": "https://github.com/myclabs/php-enum/graphs/contributors" + } + ], + "description": "PHP Enum implementation", + "homepage": "http://github.com/myclabs/php-enum", + "keywords": [ + "enum" + ], + "support": { + "issues": "https://github.com/myclabs/php-enum/issues", + "source": "https://github.com/myclabs/php-enum/tree/1.8.3" + }, + "funding": [ { - "name": "Brian L. Moon", - "homepage": "http://pear.php.net/user/brianlmoon" + "url": "https://github.com/mnapoli", + "type": "github" }, { - "name": "Joe Stump", - "homepage": "http://pear.php.net/user/jstump" - }, + "url": "https://tidelift.com/funding/github/packagist/myclabs/php-enum", + "type": "tidelift" + } + ], + "time": "2021-07-05T08:18:36+00:00" + }, + { + "name": "paragonie/constant_time_encoding", + "version": "v2.6.3", + "source": { + "type": "git", + "url": "https://github.com/paragonie/constant_time_encoding.git", + "reference": "58c3f47f650c94ec05a151692652a868995d2938" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/paragonie/constant_time_encoding/zipball/58c3f47f650c94ec05a151692652a868995d2938", + "reference": "58c3f47f650c94ec05a151692652a868995d2938", + "shasum": "" + }, + "require": { + "php": "^7|^8" + }, + "require-dev": { + "phpunit/phpunit": "^6|^7|^8|^9", + "vimeo/psalm": "^1|^2|^3|^4" + }, + "type": "library", + "autoload": { + "psr-4": { + "ParagonIE\\ConstantTime\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ { - "name": "Chris Goffinet", - "homepage": "http://pear.php.net/user/lenn0x" + "name": "Paragon Initiative Enterprises", + "email": "security@paragonie.com", + "homepage": "https://paragonie.com", + "role": "Maintainer" }, { - "name": "Martin Hlavac", - "homepage": "http://www.mhlavac.net" + "name": "Steve 'Sc00bz' Thomas", + "email": "steve@tobtu.com", + "homepage": "https://www.tobtu.com", + "role": "Original Developer" } ], - "description": "Gearman (http://www.danga.com/gearman) is a system to farm out work to other machines. It can load balance function calls to lots of machines and allows you to call functions between languages. It can also run all function calls in parallel.", - "time": "2015-11-05 19:42:12" + "description": "Constant-time Implementations of RFC 4648 Encoding (Base-64, Base-32, Base-16)", + "keywords": [ + "base16", + "base32", + "base32_decode", + "base32_encode", + "base64", + "base64_decode", + "base64_encode", + "bin2hex", + "encoding", + "hex", + "hex2bin", + "rfc4648" + ], + "support": { + "email": "info@paragonie.com", + "issues": "https://github.com/paragonie/constant_time_encoding/issues", + "source": "https://github.com/paragonie/constant_time_encoding" + }, + "time": "2022-06-14T06:56:20+00:00" }, { - "name": "phpmailer/phpmailer", - "version": "v5.2.11", + "name": "paragonie/halite", + "version": "v4.8.0", "source": { "type": "git", - "url": "https://github.com/PHPMailer/PHPMailer.git", - "reference": "7830cb9a761d974e58e3173137eac93da1cd715a" + "url": "https://github.com/paragonie/halite.git", + "reference": "7596d5cb25154092b524c34cb9ce2201db612ffc" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/PHPMailer/PHPMailer/zipball/7830cb9a761d974e58e3173137eac93da1cd715a", - "reference": "7830cb9a761d974e58e3173137eac93da1cd715a", + "url": "https://api.github.com/repos/paragonie/halite/zipball/7596d5cb25154092b524c34cb9ce2201db612ffc", + "reference": "7596d5cb25154092b524c34cb9ce2201db612ffc", "shasum": "" }, "require": { - "guzzle/guzzle": "~3.7", - "league/oauth2-client": "0.10.*", - "php": ">=5.0.0" + "paragonie/constant_time_encoding": "^2", + "paragonie/hidden-string": "^1|^2", + "paragonie/sodium_compat": "^1.15", + "php": "^7.2|^8" }, "require-dev": { - "phpdocumentor/phpdocumentor": "*", - "phpunit/phpunit": "4.3.*" + "phpunit/phpunit": "^8|^9", + "vimeo/psalm": "^3|^4" }, "type": "library", "autoload": { - "classmap": [ - "class.phpmailer.php", - "class.phpmaileroauth.php", - "class.oauth.php", - "class.smtp.php", - "class.pop3.php", - "extras/EasyPeasyICS.php", - "extras/ntlm_sasl_client.php" + "psr-4": { + "ParagonIE\\Halite\\": "./src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MPL-2.0" + ], + "authors": [ + { + "name": "Paragon Initiative Enterprises", + "email": "info@paragonie.com", + "homepage": "https://paragonie.com" + } + ], + "description": "High-level cryptography interface powered by libsodium", + "homepage": "https://github.com/paragonie/halite", + "keywords": [ + "Argon2i", + "BLAKE", + "BLAKE2", + "BLAKE2b", + "Curve25519", + "Ed25519", + "X25519", + "Xsalsa20", + "argon2", + "cryptography", + "encryption", + "ext-sodium", + "hashing", + "libsodium", + "password", + "public-key", + "signatures", + "sodium" + ], + "support": { + "docs": "https://github.com/paragonie/halite/tree/master/doc", + "issues": "https://github.com/paragonie/halite/issues", + "source": "https://github.com/paragonie/halite/tree/v4.8.0" + }, + "time": "2021-04-18T19:24:56+00:00" + }, + { + "name": "paragonie/hidden-string", + "version": "v2.0.0", + "source": { + "type": "git", + "url": "https://github.com/paragonie/hidden-string.git", + "reference": "151e53d55bfc67dd58087cdf8762dd8177ea7575" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/paragonie/hidden-string/zipball/151e53d55bfc67dd58087cdf8762dd8177ea7575", + "reference": "151e53d55bfc67dd58087cdf8762dd8177ea7575", + "shasum": "" + }, + "require": { + "paragonie/constant_time_encoding": "^2", + "paragonie/sodium_compat": "^1.6", + "php": "^7.4|^8" + }, + "require-dev": { + "phpunit/phpunit": "^6|^7|^8|^9", + "vimeo/psalm": "^3|^4" + }, + "type": "library", + "autoload": { + "psr-4": { + "ParagonIE\\HiddenString\\": "./src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MPL-2.0" + ], + "authors": [ + { + "name": "Paragon Initiative Enterprises", + "email": "info@paragonie.com", + "homepage": "https://paragonie.com" + } + ], + "description": "Encapsulate strings in an object to hide them from stack traces", + "homepage": "https://github.com/paragonie/hidden-string", + "keywords": [ + "hidden", + "stack trace", + "string" + ], + "support": { + "issues": "https://github.com/paragonie/hidden-string/issues", + "source": "https://github.com/paragonie/hidden-string/tree/v2.0.0" + }, + "time": "2020-12-06T15:07:44+00:00" + }, + { + "name": "paragonie/random_compat", + "version": "v9.99.100", + "source": { + "type": "git", + "url": "https://github.com/paragonie/random_compat.git", + "reference": "996434e5492cb4c3edcb9168db6fbb1359ef965a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/paragonie/random_compat/zipball/996434e5492cb4c3edcb9168db6fbb1359ef965a", + "reference": "996434e5492cb4c3edcb9168db6fbb1359ef965a", + "shasum": "" + }, + "require": { + "php": ">= 7" + }, + "require-dev": { + "phpunit/phpunit": "4.*|5.*", + "vimeo/psalm": "^1" + }, + "suggest": { + "ext-libsodium": "Provides a modern crypto API that can be used to generate random bytes." + }, + "type": "library", + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Paragon Initiative Enterprises", + "email": "security@paragonie.com", + "homepage": "https://paragonie.com" + } + ], + "description": "PHP 5.x polyfill for random_bytes() and random_int() from PHP 7", + "keywords": [ + "csprng", + "polyfill", + "pseudorandom", + "random" + ], + "support": { + "email": "info@paragonie.com", + "issues": "https://github.com/paragonie/random_compat/issues", + "source": "https://github.com/paragonie/random_compat" + }, + "time": "2020-10-15T08:29:30+00:00" + }, + { + "name": "paragonie/sodium_compat", + "version": "v1.17.1", + "source": { + "type": "git", + "url": "https://github.com/paragonie/sodium_compat.git", + "reference": "ac994053faac18d386328c91c7900f930acadf1e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/paragonie/sodium_compat/zipball/ac994053faac18d386328c91c7900f930acadf1e", + "reference": "ac994053faac18d386328c91c7900f930acadf1e", + "shasum": "" + }, + "require": { + "paragonie/random_compat": ">=1", + "php": "^5.2.4|^5.3|^5.4|^5.5|^5.6|^7|^8" + }, + "require-dev": { + "phpunit/phpunit": "^3|^4|^5|^6|^7|^8|^9" + }, + "suggest": { + "ext-libsodium": "PHP < 7.0: Better performance, password hashing (Argon2i), secure memory management (memzero), and better security.", + "ext-sodium": "PHP >= 7.0: Better performance, password hashing (Argon2i), secure memory management (memzero), and better security." + }, + "type": "library", + "autoload": { + "files": [ + "autoload.php" ] }, "notification-url": "https://packagist.org/downloads/", "license": [ - "LGPL-2.1" + "ISC" ], "authors": [ { - "name": "Jim Jagielski", - "email": "jimjag@gmail.com" + "name": "Paragon Initiative Enterprises", + "email": "security@paragonie.com" }, + { + "name": "Frank Denis", + "email": "jedisct1@pureftpd.org" + } + ], + "description": "Pure PHP implementation of libsodium; uses the PHP extension if it exists", + "keywords": [ + "Authentication", + "BLAKE2b", + "ChaCha20", + "ChaCha20-Poly1305", + "Chapoly", + "Curve25519", + "Ed25519", + "EdDSA", + "Edwards-curve Digital Signature Algorithm", + "Elliptic Curve Diffie-Hellman", + "Poly1305", + "Pure-PHP cryptography", + "RFC 7748", + "RFC 8032", + "Salpoly", + "Salsa20", + "X25519", + "XChaCha20-Poly1305", + "XSalsa20-Poly1305", + "Xchacha20", + "Xsalsa20", + "aead", + "cryptography", + "ecdh", + "elliptic curve", + "elliptic curve cryptography", + "encryption", + "libsodium", + "php", + "public-key cryptography", + "secret-key cryptography", + "side-channel resistant" + ], + "support": { + "issues": "https://github.com/paragonie/sodium_compat/issues", + "source": "https://github.com/paragonie/sodium_compat/tree/v1.17.1" + }, + "time": "2022-03-23T19:32:04+00:00" + }, + { + "name": "phpmailer/phpmailer", + "version": "v6.6.2", + "source": { + "type": "git", + "url": "https://github.com/PHPMailer/PHPMailer.git", + "reference": "b52ed06864fdda81b82ec8bf564cf15d45ed4f95" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/PHPMailer/PHPMailer/zipball/b52ed06864fdda81b82ec8bf564cf15d45ed4f95", + "reference": "b52ed06864fdda81b82ec8bf564cf15d45ed4f95", + "shasum": "" + }, + "require": { + "ext-ctype": "*", + "ext-filter": "*", + "ext-hash": "*", + "php": ">=5.5.0" + }, + "require-dev": { + "dealerdirect/phpcodesniffer-composer-installer": "^0.7.0", + "doctrine/annotations": "^1.2", + "php-parallel-lint/php-console-highlighter": "^1.0.0", + "php-parallel-lint/php-parallel-lint": "^1.3.2", + "phpcompatibility/php-compatibility": "^9.3.5", + "roave/security-advisories": "dev-latest", + "squizlabs/php_codesniffer": "^3.6.2", + "yoast/phpunit-polyfills": "^1.0.0" + }, + "suggest": { + "ext-mbstring": "Needed to send email in multibyte encoding charset or decode encoded addresses", + "hayageek/oauth2-yahoo": "Needed for Yahoo XOAUTH2 authentication", + "league/oauth2-google": "Needed for Google XOAUTH2 authentication", + "psr/log": "For optional PSR-3 debug logging", + "stevenmaguire/oauth2-microsoft": "Needed for Microsoft XOAUTH2 authentication", + "symfony/polyfill-mbstring": "To support UTF-8 if the Mbstring PHP extension is not enabled (^1.2)" + }, + "type": "library", + "autoload": { + "psr-4": { + "PHPMailer\\PHPMailer\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "LGPL-2.1-only" + ], + "authors": [ { "name": "Marcus Bointon", "email": "phpmailer@synchromedia.co.uk" }, + { + "name": "Jim Jagielski", + "email": "jimjag@gmail.com" + }, { "name": "Andy Prevost", "email": "codeworxtech@users.sourceforge.net" @@ -494,103 +942,372 @@ } ], "description": "PHPMailer is a full-featured email creation and transfer class for PHP", - "time": "2015-08-31 10:37:13" + "support": { + "issues": "https://github.com/PHPMailer/PHPMailer/issues", + "source": "https://github.com/PHPMailer/PHPMailer/tree/v6.6.2" + }, + "funding": [ + { + "url": "https://github.com/Synchro", + "type": "github" + } + ], + "time": "2022-06-14T09:27:21+00:00" }, { - "name": "phpoffice/phpexcel", - "version": "1.8.1", + "name": "phpoffice/phpspreadsheet", + "version": "1.24.1", "source": { "type": "git", - "url": "https://github.com/PHPOffice/PHPExcel.git", - "reference": "372c7cbb695a6f6f1e62649381aeaa37e7e70b32" + "url": "https://github.com/PHPOffice/PhpSpreadsheet.git", + "reference": "69991111e05fca3ff7398e1e7fca9ebed33efec6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/PHPOffice/PHPExcel/zipball/372c7cbb695a6f6f1e62649381aeaa37e7e70b32", - "reference": "372c7cbb695a6f6f1e62649381aeaa37e7e70b32", + "url": "https://api.github.com/repos/PHPOffice/PhpSpreadsheet/zipball/69991111e05fca3ff7398e1e7fca9ebed33efec6", + "reference": "69991111e05fca3ff7398e1e7fca9ebed33efec6", "shasum": "" }, "require": { + "ext-ctype": "*", + "ext-dom": "*", + "ext-fileinfo": "*", + "ext-gd": "*", + "ext-iconv": "*", + "ext-libxml": "*", + "ext-mbstring": "*", + "ext-simplexml": "*", "ext-xml": "*", + "ext-xmlreader": "*", "ext-xmlwriter": "*", - "php": ">=5.2.0" + "ext-zip": "*", + "ext-zlib": "*", + "ezyang/htmlpurifier": "^4.13", + "maennchen/zipstream-php": "^2.1", + "markbaker/complex": "^3.0", + "markbaker/matrix": "^3.0", + "php": "^7.3 || ^8.0", + "psr/http-client": "^1.0", + "psr/http-factory": "^1.0", + "psr/simple-cache": "^1.0 || ^2.0" + }, + "require-dev": { + "dealerdirect/phpcodesniffer-composer-installer": "dev-master", + "dompdf/dompdf": "^1.0 || ^2.0", + "friendsofphp/php-cs-fixer": "^3.2", + "jpgraph/jpgraph": "^4.0", + "mpdf/mpdf": "8.1.1", + "phpcompatibility/php-compatibility": "^9.3", + "phpstan/phpstan": "^1.1", + "phpstan/phpstan-phpunit": "^1.0", + "phpunit/phpunit": "^8.5 || ^9.0", + "squizlabs/php_codesniffer": "^3.7", + "tecnickcom/tcpdf": "^6.4" + }, + "suggest": { + "dompdf/dompdf": "Option for rendering PDF with PDF Writer (doesn't yet support PHP8)", + "jpgraph/jpgraph": "Option for rendering charts, or including charts with PDF or HTML Writers", + "mpdf/mpdf": "Option for rendering PDF with PDF Writer", + "tecnickcom/tcpdf": "Option for rendering PDF with PDF Writer (doesn't yet support PHP8)" }, "type": "library", "autoload": { - "psr-0": { - "PHPExcel": "Classes/" + "psr-4": { + "PhpOffice\\PhpSpreadsheet\\": "src/PhpSpreadsheet" } }, "notification-url": "https://packagist.org/downloads/", "license": [ - "LGPL" + "MIT" ], "authors": [ { "name": "Maarten Balliauw", - "homepage": "http://blog.maartenballiauw.be" + "homepage": "https://blog.maartenballiauw.be" }, { - "name": "Mark Baker" + "name": "Mark Baker", + "homepage": "https://markbakeruk.net" }, { "name": "Franck Lefevre", - "homepage": "http://blog.rootslabs.net" + "homepage": "https://rootslabs.net" }, { "name": "Erik Tilt" + }, + { + "name": "Adrien Crivelli" } ], - "description": "PHPExcel - OpenXML - Read, Create and Write Spreadsheet documents in PHP - Spreadsheet engine", - "homepage": "http://phpexcel.codeplex.com", + "description": "PHPSpreadsheet - Read, Create and Write Spreadsheet documents in PHP - Spreadsheet engine", + "homepage": "https://github.com/PHPOffice/PhpSpreadsheet", "keywords": [ "OpenXML", "excel", + "gnumeric", + "ods", "php", "spreadsheet", "xls", "xlsx" ], - "time": "2015-05-01 07:00:55" + "support": { + "issues": "https://github.com/PHPOffice/PhpSpreadsheet/issues", + "source": "https://github.com/PHPOffice/PhpSpreadsheet/tree/1.24.1" + }, + "time": "2022-07-18T19:50:48+00:00" }, { - "name": "symfony/event-dispatcher", - "version": "v2.7.3", + "name": "psr/http-client", + "version": "1.0.1", "source": { "type": "git", - "url": "https://github.com/symfony/event-dispatcher.git", - "reference": "9310b5f9a87ec2ea75d20fec0b0017c77c66dac3" + "url": "https://github.com/php-fig/http-client.git", + "reference": "2dfb5f6c5eff0e91e20e913f8c5452ed95b86621" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/9310b5f9a87ec2ea75d20fec0b0017c77c66dac3", - "reference": "9310b5f9a87ec2ea75d20fec0b0017c77c66dac3", + "url": "https://api.github.com/repos/php-fig/http-client/zipball/2dfb5f6c5eff0e91e20e913f8c5452ed95b86621", + "reference": "2dfb5f6c5eff0e91e20e913f8c5452ed95b86621", "shasum": "" }, "require": { - "php": ">=5.3.9" + "php": "^7.0 || ^8.0", + "psr/http-message": "^1.0" }, - "require-dev": { - "psr/log": "~1.0", - "symfony/config": "~2.0,>=2.0.5", - "symfony/dependency-injection": "~2.6", - "symfony/expression-language": "~2.6", - "symfony/phpunit-bridge": "~2.7", - "symfony/stopwatch": "~2.3" + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Http\\Client\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "http://www.php-fig.org/" + } + ], + "description": "Common interface for HTTP clients", + "homepage": "https://github.com/php-fig/http-client", + "keywords": [ + "http", + "http-client", + "psr", + "psr-18" + ], + "support": { + "source": "https://github.com/php-fig/http-client/tree/master" + }, + "time": "2020-06-29T06:28:15+00:00" + }, + { + "name": "psr/http-factory", + "version": "1.0.1", + "source": { + "type": "git", + "url": "https://github.com/php-fig/http-factory.git", + "reference": "12ac7fcd07e5b077433f5f2bee95b3a771bf61be" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/http-factory/zipball/12ac7fcd07e5b077433f5f2bee95b3a771bf61be", + "reference": "12ac7fcd07e5b077433f5f2bee95b3a771bf61be", + "shasum": "" + }, + "require": { + "php": ">=7.0.0", + "psr/http-message": "^1.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Http\\Message\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "http://www.php-fig.org/" + } + ], + "description": "Common interfaces for PSR-7 HTTP message factories", + "keywords": [ + "factory", + "http", + "message", + "psr", + "psr-17", + "psr-7", + "request", + "response" + ], + "support": { + "source": "https://github.com/php-fig/http-factory/tree/master" + }, + "time": "2019-04-30T12:38:16+00:00" + }, + { + "name": "psr/http-message", + "version": "1.0.1", + "source": { + "type": "git", + "url": "https://github.com/php-fig/http-message.git", + "reference": "f6561bf28d520154e4b0ec72be95418abe6d9363" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/http-message/zipball/f6561bf28d520154e4b0ec72be95418abe6d9363", + "reference": "f6561bf28d520154e4b0ec72be95418abe6d9363", + "shasum": "" + }, + "require": { + "php": ">=5.3.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Http\\Message\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "http://www.php-fig.org/" + } + ], + "description": "Common interface for HTTP messages", + "homepage": "https://github.com/php-fig/http-message", + "keywords": [ + "http", + "http-message", + "psr", + "psr-7", + "request", + "response" + ], + "support": { + "source": "https://github.com/php-fig/http-message/tree/master" + }, + "time": "2016-08-06T14:39:51+00:00" + }, + { + "name": "psr/simple-cache", + "version": "2.0.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/simple-cache.git", + "reference": "8707bf3cea6f710bf6ef05491234e3ab06f6432a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/simple-cache/zipball/8707bf3cea6f710bf6ef05491234e3ab06f6432a", + "reference": "8707bf3cea6f710bf6ef05491234e3ab06f6432a", + "shasum": "" + }, + "require": { + "php": ">=8.0.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\SimpleCache\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interfaces for simple caching", + "keywords": [ + "cache", + "caching", + "psr", + "psr-16", + "simple-cache" + ], + "support": { + "source": "https://github.com/php-fig/simple-cache/tree/2.0.0" + }, + "time": "2021-10-29T13:22:09+00:00" + }, + { + "name": "symfony/polyfill-mbstring", + "version": "v1.26.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-mbstring.git", + "reference": "9344f9cb97f3b19424af1a21a3b0e75b0a7d8d7e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/9344f9cb97f3b19424af1a21a3b0e75b0a7d8d7e", + "reference": "9344f9cb97f3b19424af1a21a3b0e75b0a7d8d7e", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "provide": { + "ext-mbstring": "*" }, "suggest": { - "symfony/dependency-injection": "", - "symfony/http-kernel": "" + "ext-mbstring": "For best performance" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "2.7-dev" + "dev-main": "1.26-dev" + }, + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" } }, "autoload": { + "files": [ + "bootstrap.php" + ], "psr-4": { - "Symfony\\Component\\EventDispatcher\\": "" + "Symfony\\Polyfill\\Mbstring\\": "" } }, "notification-url": "https://packagist.org/downloads/", @@ -599,17 +1316,41 @@ ], "authors": [ { - "name": "Fabien Potencier", - "email": "fabien@symfony.com" + "name": "Nicolas Grekas", + "email": "p@tchwork.com" }, { "name": "Symfony Community", "homepage": "https://symfony.com/contributors" } ], - "description": "Symfony EventDispatcher Component", + "description": "Symfony polyfill for the Mbstring extension", "homepage": "https://symfony.com", - "time": "2015-06-18 19:21:56" + "keywords": [ + "compatibility", + "mbstring", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.26.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2022-05-24T11:49:31+00:00" } ], "packages-dev": [ @@ -672,20 +1413,25 @@ "testing", "xunit" ], - "time": "2014-09-02 10:13:14" + "support": { + "irc": "irc://irc.freenode.net/phpunit", + "issues": "https://github.com/sebastianbergmann/php-code-coverage/issues", + "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/1.2.18" + }, + "time": "2014-09-02T10:13:14+00:00" }, { "name": "phpunit/php-file-iterator", - "version": "1.4.1", + "version": "1.4.5", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-file-iterator.git", - "reference": "6150bf2c35d3fc379e50c7602b75caceaa39dbf0" + "reference": "730b01bc3e867237eaac355e06a36b85dd93a8b4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/6150bf2c35d3fc379e50c7602b75caceaa39dbf0", - "reference": "6150bf2c35d3fc379e50c7602b75caceaa39dbf0", + "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/730b01bc3e867237eaac355e06a36b85dd93a8b4", + "reference": "730b01bc3e867237eaac355e06a36b85dd93a8b4", "shasum": "" }, "require": { @@ -719,7 +1465,12 @@ "filesystem", "iterator" ], - "time": "2015-06-21 13:08:43" + "support": { + "irc": "irc://irc.freenode.net/phpunit", + "issues": "https://github.com/sebastianbergmann/php-file-iterator/issues", + "source": "https://github.com/sebastianbergmann/php-file-iterator/tree/1.4.5" + }, + "time": "2017-11-27T13:52:08+00:00" }, { "name": "phpunit/php-text-template", @@ -760,25 +1511,32 @@ "keywords": [ "template" ], - "time": "2015-06-21 13:50:34" + "support": { + "issues": "https://github.com/sebastianbergmann/php-text-template/issues", + "source": "https://github.com/sebastianbergmann/php-text-template/tree/1.2.1" + }, + "time": "2015-06-21T13:50:34+00:00" }, { "name": "phpunit/php-timer", - "version": "1.0.7", + "version": "1.0.8", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-timer.git", - "reference": "3e82f4e9fc92665fafd9157568e4dcb01d014e5b" + "reference": "38e9124049cf1a164f1e4537caf19c99bf1eb260" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/3e82f4e9fc92665fafd9157568e4dcb01d014e5b", - "reference": "3e82f4e9fc92665fafd9157568e4dcb01d014e5b", + "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/38e9124049cf1a164f1e4537caf19c99bf1eb260", + "reference": "38e9124049cf1a164f1e4537caf19c99bf1eb260", "shasum": "" }, "require": { "php": ">=5.3.3" }, + "require-dev": { + "phpunit/phpunit": "~4|~5" + }, "type": "library", "autoload": { "classmap": [ @@ -801,7 +1559,12 @@ "keywords": [ "timer" ], - "time": "2015-06-21 08:01:12" + "support": { + "irc": "irc://irc.freenode.net/phpunit", + "issues": "https://github.com/sebastianbergmann/php-timer/issues", + "source": "https://github.com/sebastianbergmann/php-timer/tree/master" + }, + "time": "2016-05-12T18:03:57+00:00" }, { "name": "phpunit/php-token-stream", @@ -851,7 +1614,13 @@ "keywords": [ "tokenizer" ], - "time": "2014-03-03 05:10:30" + "support": { + "irc": "irc://irc.freenode.net/phpunit", + "issues": "https://github.com/sebastianbergmann/php-token-stream/issues", + "source": "https://github.com/sebastianbergmann/php-token-stream/tree/1.2.2" + }, + "abandoned": true, + "time": "2014-03-03T05:10:30+00:00" }, { "name": "phpunit/phpunit", @@ -924,7 +1693,12 @@ "testing", "xunit" ], - "time": "2014-10-17 09:04:17" + "support": { + "irc": "irc://irc.freenode.net/phpunit", + "issues": "https://github.com/sebastianbergmann/phpunit/issues", + "source": "https://github.com/sebastianbergmann/phpunit/tree/3.7" + }, + "time": "2014-10-17T09:04:17+00:00" }, { "name": "phpunit/phpunit-mock-objects", @@ -973,38 +1747,127 @@ "mock", "xunit" ], - "time": "2013-01-13 10:24:48" + "support": { + "irc": "irc://irc.freenode.net/phpunit", + "issues": "https://github.com/sebastianbergmann/phpunit-mock-objects/issues", + "source": "https://github.com/sebastianbergmann/phpunit-mock-objects/tree/1.2.3" + }, + "abandoned": true, + "time": "2013-01-13T10:24:48+00:00" }, { - "name": "symfony/yaml", - "version": "v2.7.3", + "name": "symfony/polyfill-ctype", + "version": "v1.26.0", "source": { "type": "git", - "url": "https://github.com/symfony/Yaml.git", - "reference": "71340e996171474a53f3d29111d046be4ad8a0ff" + "url": "https://github.com/symfony/polyfill-ctype.git", + "reference": "6fd1b9a79f6e3cf65f9e679b23af304cd9e010d4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/Yaml/zipball/71340e996171474a53f3d29111d046be4ad8a0ff", - "reference": "71340e996171474a53f3d29111d046be4ad8a0ff", + "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/6fd1b9a79f6e3cf65f9e679b23af304cd9e010d4", + "reference": "6fd1b9a79f6e3cf65f9e679b23af304cd9e010d4", "shasum": "" }, "require": { - "php": ">=5.3.9" + "php": ">=7.1" }, - "require-dev": { - "symfony/phpunit-bridge": "~2.7" + "provide": { + "ext-ctype": "*" + }, + "suggest": { + "ext-ctype": "For best performance" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "2.7-dev" + "dev-main": "1.26-dev" + }, + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" } }, "autoload": { + "files": [ + "bootstrap.php" + ], "psr-4": { - "Symfony\\Component\\Yaml\\": "" + "Symfony\\Polyfill\\Ctype\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Gert de Pagter", + "email": "BackEndTea@gmail.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for ctype functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "ctype", + "polyfill", + "portable" + ], + "support": { + "source": "https://github.com/symfony/polyfill-ctype/tree/v1.26.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" } + ], + "time": "2022-05-24T11:49:31+00:00" + }, + { + "name": "symfony/yaml", + "version": "v2.8.52", + "source": { + "type": "git", + "url": "https://github.com/symfony/yaml.git", + "reference": "02c1859112aa779d9ab394ae4f3381911d84052b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/yaml/zipball/02c1859112aa779d9ab394ae4f3381911d84052b", + "reference": "02c1859112aa779d9ab394ae4f3381911d84052b", + "shasum": "" + }, + "require": { + "php": ">=5.3.9", + "symfony/polyfill-ctype": "~1.8" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.8-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Component\\Yaml\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] }, "notification-url": "https://packagist.org/downloads/", "license": [ @@ -1022,18 +1885,20 @@ ], "description": "Symfony Yaml Component", "homepage": "https://symfony.com", - "time": "2015-07-28 14:07:07" + "support": { + "source": "https://github.com/symfony/yaml/tree/v2.8.52" + }, + "time": "2018-11-11T11:18:13+00:00" } ], "aliases": [], "minimum-stability": "stable", - "stability-flags": { - "mhlavac/gearman": 20 - }, + "stability-flags": [], "prefer-stable": false, "prefer-lowest": false, "platform": { - "php": ">=5.4.0" + "php": ">=8.0" }, - "platform-dev": [] + "platform-dev": [], + "plugin-api-version": "2.3.0" } diff --git a/config-dist/assets.php b/config-dist/assets.php index 05c71f28f..7e4892c79 100644 --- a/config-dist/assets.php +++ b/config-dist/assets.php @@ -15,19 +15,19 @@ $assets = (array) json_decode(file_get_contents($assets_file), true); // Assets that are used in both admin and frontend, in the order in which they will be loaded -$assets_common = array('font-google', 'jquery', 'bootstrap', 'font-awesome', 'webshim', 'select2', 'hammer', 'highlight'); +$assets_common = $assets['config']['common']; $settings['default_assets']['dev'] = array( // site theme - 'site' => array_merge ($assets_common, array('main:js', 'run_users', 'run', 'survey', 'site', 'site:custom', 'cookie-consent')), - 'admin' => array_merge($assets_common, array('ace', 'main:js', 'run_users', 'run_settings', 'run', 'admin', 'cookie-consent')), + 'site' => array_merge ($assets_common, $assets['config']['site']), + 'admin' => array_merge($assets_common, array('ace'), $assets['config']['admin']), 'assets' => array_merge($assets, array( // use this array to override any asset defined above using its KEY )), ); $settings['default_assets']['prod'] = array( - 'site' => array('font-google', 'site', 'cookie-consent'), - 'admin' => array('admin', 'cookie-consent'), + 'site' => array('site'), + 'admin' => array('admin'), 'assets' => array_merge($assets, array( // use this array to override any asset defined above using its KEY // For example 'bootstrap-material-design' is overriden here when site goes to production diff --git a/config-dist/settings.php b/config-dist/settings.php index 75245a74a..b4bd1187d 100644 --- a/config-dist/settings.php +++ b/config-dist/settings.php @@ -13,8 +13,8 @@ 'password' => 'password', 'database' => 'database', 'prefix' => '', - 'encoding' => 'utf8', - 'unix_socket' => '/Applications/MAMP/tmp/mysql/mysql.sock', + 'encoding' => 'utf8mb', + 'unix_socket' => '', ); // OpenCPU instance settings @@ -45,6 +45,8 @@ 'queue_item_ttl' => 20*60, // Number of times to retry an item before deleting 'queue_item_tries' => 4, + // an array of account IDs to skip when processing mail queue + 'queue_skip_accounts' => array(), // SMTP options for phpmailer 'smtp_options' => array(), ); @@ -80,8 +82,9 @@ // Setup settings for application that can overwrite defaults in /define_root.php $settings['define_root'] = array( - //'protocol' => 'http://', + //'protocol' => 'https://', //'doc_root' => 'localhost/formr.org/', + //'study_domain' => 'localhost/formr.org/', //'server_root' => APPLICATION_ROOT . '/', //'online' => false, //'testing' => true @@ -139,14 +142,15 @@ // Limit to number of pages to skip in a survey $settings['allowed_empty_pages'] = 100; -// Deamon settings -$settings['deamon'] = array( - // List of gearman servers in format {host:port} - 'gearman_servers' => array('server.gearman.net:4730'), - // Number of seconds to expire before run is fetched from DB for processing - 'run_expire_time' => 10 * 60, - // Number of seconds for which deamon loop should rest before getting next batch - 'loop_interval' => 1, +// Run unit session settings for the sessions queue +$settings['unit_session'] = array( + // String representing howmany minutes to set as default expiration for unit sessions + // @see http://php.net/manual/en/function.strtotime.php + 'queue_expiration_extension' => '+10 minutes', + // use db queue for processing unit sessions + 'use_queue' => false, + // Log debug messages + 'debug' => false, ); // Configure memory limits to be set when performing certain actions @@ -181,10 +185,32 @@ // Restart all server daemon whenever this flag is changed $settings['in_maintenance'] = false; +// Configure IP addresses that can still access the application even in maintenance mode +// Example ['192.18.2.3', '192.18.3.4'] +$settings['maintenance_ips'] = ['134.76.2.248']; + // curl settings that override the default settings in the CURL class // Use exact PHP constants as defined in http://php.net/manual/en/function.curl-setopt.php $settings['curl'] = array( CURLOPT_SSL_VERIFYPEER => true, CURLOPT_SSL_VERIFYHOST => 2, ); - \ No newline at end of file + +// Disable features temporarily by entering the Controller action names in this array +$settings['disabled_features'] = array( + // RUN.controller_method_name + // SURVEY.controller_method_name +); + +// Brand +$settings['brand'] = 'form{`r}'; +$settings['brand_long'] = 'formr survey framework'; + +// Settings for PHP session +$settings['php_session'] = array( + 'path' => '/', + 'domain' => '.formr.org', // prefer env('SERVER_NAME') if using subdomains for run URLs + 'secure' => true, + 'httponly' => true, + //'lifetime' => 36000, +); diff --git a/config-dist/supervisord.conf b/config-dist/supervisord.conf index 8de6385a2..bdc8aaf87 100644 --- a/config-dist/supervisord.conf +++ b/config-dist/supervisord.conf @@ -6,8 +6,8 @@ ; - Add to supervisor config folder via symbolic link, Eg.: ln -s /var/www/formr/config/supervisord.conf /etc/supervisor/conf.d/formr.conf ; Program to process the email queue -;[program:formrmailer] -;command=timeout 60m php bin/mailer.php +;[program:formrmailqueue] +;command=timeout 60m php bin/queue.php -t Email ;process_name=%(program_name)s_%(process_num)02d ;numprocs=1 ;directory=/var/www/formr.org @@ -17,38 +17,16 @@ ;stderr_logfile=/var/www/formr.org/tmp/logs/email-queue.log ;autostart=true -; program to execute the run worker -;[program:formrrunworker] -;command=timeout 60m php bin/deamon.php -w Run -a10 -t40 -;process_name=%(program_name)s_%(process_num)02d -;numprocs=5 -;directory=/var/www/formr.org -;autorestart=unexpected -;exitcodes=90 -;stdout_logfile=/var/www/formr.org/tmp/logs/supervisord.log -;stderr_logfile=/var/www/formr.org/tmp/logs/supervisord.log -;autostart=true - -; program to execute the run session worker -;[program:formrrunsessionworker] -;command=timeout 60m php bin/deamon.php -w RunSession -a100 -t40 -;process_name=%(program_name)s_%(process_num)02d -;numprocs=15 -;directory=/var/www/formr.org -;autorestart=unexpected -;exitcodes=90 -;stdout_logfile=/var/www/formr.org/tmp/logs/supervisord.log -;stderr_logfile=/var/www/formr.org/tmp/logs/supervisord.log -;autostart=true - -; Program to execute the deamon -;[program:formrdeamon] -;command=timeout 60m php bin/deamon.php +; program to process the unit session queue +;[program:formrsessionqueue] +;Example with all parameters timeout 60m php bin/queue.php -t UnitSession -n 3 -b 300 -p %(process_num)02d +;command=timeout 60m php bin/queue.php -t UnitSession ;process_name=%(program_name)s_%(process_num)02d ;numprocs=1 ;directory=/var/www/formr.org ;autorestart=unexpected ;exitcodes=90 -;stdout_logfile=/var/www/formr.org/tmp/logs/supervisord.log -;stderr_logfile=/var/www/formr.org/tmp/logs/supervisord.log +;stdout_logfile=/var/www/formr.org/tmp/logs/session-queue.log +;stderr_logfile=/var/www/formr.org/tmp/logs/session-queue.log ;autostart=true + diff --git a/documentation/boombox_dials.psd b/documentation/boombox_dials.psd deleted file mode 100644 index 787e89fb9..000000000 Binary files a/documentation/boombox_dials.psd and /dev/null differ diff --git a/documentation/boombox_innards.psd b/documentation/boombox_innards.psd deleted file mode 100644 index 0ce405b5d..000000000 Binary files a/documentation/boombox_innards.psd and /dev/null differ diff --git a/documentation/run_components/Basic_Diary.json b/documentation/run_components/Basic_Diary.json index 67f5803e4..8888713a3 100644 --- a/documentation/run_components/Basic_Diary.json +++ b/documentation/run_components/Basic_Diary.json @@ -1,5 +1,5 @@ { - "name": "Basic_diary", + "name": "BasicDiary", "units": [ { "type": "Survey", @@ -7,21 +7,10 @@ "position": 10, "special": "" }, - { - "type": "Pause", - "description": "start the diary the next day", - "position": 20, - "special": "", - "wait_until_time": "", - "wait_until_date": "", - "wait_minutes": "", - "relative_to": "next_day()", - "body": "## Thank you for signing up for our study\r\n\r\nWe will invite you to the diary tomorrow at 5pm via email." - }, { "type": "Pause", "description": "diary beginning: a pause until the diary is accessible", - "position": 30, + "position": 20, "special": "", "wait_until_time": "17:00:00", "wait_until_date": "", @@ -32,9 +21,9 @@ { "type": "Email", "description": "diary invitation: sent after the pause above expires", - "position": 40, + "position": 30, "special": "", - "account_id": 1, + "account_id": 2, "subject": "Diary invitation", "recipient_field": "most recent reported address", "body": "Dear participant,\r\n\r\nplease fill out your diary now, you have until midnight to start.\r\n\r\n{{login_link}}\r\n\r\nBest wishes,\r\n\r\nthe study robot" @@ -42,13 +31,13 @@ { "type": "Survey", "description": "diary: main diary survey (this one is repeated)", - "position": 50, + "position": 40, "special": "" }, { "type": "SkipBackward", "description": "end of diary loop", - "position": 60, + "position": 50, "special": "", "condition": "nrow(diary) < 20 # diary has been filled out at least 20 times\r\n\r\n# alternatively: run the diary for 20 days, no matter how many times it was filled out (you can also combine these criteria using &&)\r\n# remove the comment signs: # to use this criterion instead\r\n# library(lubridate)\r\n# today() > ( as.Date(demographics$created) + days(20) )", "if_true": 20 @@ -56,16 +45,16 @@ { "type": "Endpage", "description": "end of study", - "position": 70, + "position": 60, "special": "", "body": "## It's over\r\n\r\nThanks for participating in our diary study.\r\n" }, { "type": "Endpage", "description": "Notes", - "position": 80, + "position": 70, "special": "", - "body": "# Notes\r\n\r\n1. Make sure you ask for an email address in the first survey\r\n2. Set an expiry time for the diary survey at pos 50, so that the survey is only accessible for a certain time window (otherwise the diary won't continue until the users responds to the diary).\r\n3. At step 60 we refer to the initial survey, assuming you named it \"demographics\". If you choose a different name, specify that instead.\r\n4. You can delete this unit (80), but users will never see it in this design." + "body": "# Notes\r\n\r\n1. Make sure you ask for an email address in the first survey\r\n2. Set an expiry time for the diary survey at pos 40, so that the survey is only accessible for a certain time window (otherwise the diary won't continue until the users responds to the diary).\r\n3. At step 50 we assume you named the diary \"diary\" and the first survey \"demographics\". If you pick different names, \r\n4. You can delete this unit (70), but users will never see it in this design." } ] -} \ No newline at end of file +} diff --git a/documentation/run_components/Reminder.json b/documentation/run_components/Reminder.json new file mode 100644 index 000000000..645d539ae --- /dev/null +++ b/documentation/run_components/Reminder.json @@ -0,0 +1,27 @@ +{ + "name": "Reminder", + "units": [ + { + "type": "Wait", + "description": "send a reminder if participant doesn't react within 60 minutes", + "position": 10, + "special": "", + "wait_until_time": "", + "wait_until_date": "", + "wait_minutes": 60, + "relative_to": "", + "body": 30 + }, + { + "type": "Email", + "description": "Reminder", + "position": 20, + "special": "", + "account_id": 2, + "subject": "", + "recipient_field": "", + "body": "Hey,\r\n\r\ndidn't you forget about your favourite study?\r\n{{login_link}}", + "cron_only": 1 + } + ] +} diff --git a/setup.php b/setup.php index 9de86b2bc..a6dac528f 100644 --- a/setup.php +++ b/setup.php @@ -1,5 +1,6 @@ 'PublicController', - 'admin' => 'AdminController', - 'admin/run' => 'AdminRunController', - 'admin/survey' => 'AdminSurveyController', - 'admin/mail' => 'AdminMailController', - 'superadmin' => 'SuperadminController', - 'api' => 'ApiController', - 'run' => 'RunController' + 'admin' => 'AdminController', + 'admin/run' => 'AdminRunController', + 'admin/survey' => 'AdminSurveyController', + 'admin/mail' => 'AdminMailController', + 'admin/advanced' => 'AdminAdvancedController', + 'admin/account' => 'AdminAccountController', + 'public' => 'PublicController', + 'api' => 'ApiController', + 'run' => 'RunController' ); +// Include helper functions +require_once APPLICATION_PATH . 'Functions.php'; + // Load application settings /* @var $settings array */ require_once APPLICATION_ROOT . 'config-dist/settings.php'; @@ -39,9 +44,8 @@ $settings['version'] = FORMR_VERSION; // Load application autoloader -$autoloader = require_once APPLICATION_PATH . 'Library/Autoloader.php'; -// Include helper functions -require_once APPLICATION_PATH . 'Library/Functions.php'; +$autoloader = require_once APPLICATION_PATH . 'Autoloader.php'; + // Initialize Config Config::initialize($settings); @@ -92,5 +96,9 @@ function __formr_setup($settings = array()) { register_shutdown_function('shutdown_formr_org'); } +// Bootstrap setup __formr_setup($settings); +// Check if maintenance is ongoing +formr_check_maintenance(); + diff --git a/sql/patches/029_run_expire_cookie_setting.sql b/sql/patches/029_run_expire_cookie_setting.sql new file mode 100644 index 000000000..92c8d7811 --- /dev/null +++ b/sql/patches/029_run_expire_cookie_setting.sql @@ -0,0 +1 @@ +ALTER TABLE `survey_runs` ADD `expire_cookie` INT UNSIGNED NOT NULL DEFAULT '0' ; diff --git a/sql/patches/030_sessions_queue.sql b/sql/patches/030_sessions_queue.sql new file mode 100644 index 000000000..edba61cf0 --- /dev/null +++ b/sql/patches/030_sessions_queue.sql @@ -0,0 +1,13 @@ +CREATE TABLE `survey_sessions_queue` ( + `unit_session_id` bigint(20) unsigned NOT NULL, + `run_session_id` int(10) unsigned NOT NULL, + `unit_id` int(10) unsigned NOT NULL, + `created` int(10) unsigned NOT NULL, + `expires` int(10) unsigned NOT NULL, + `run` varchar(45) COLLATE utf8_unicode_ci NOT NULL, + `counter` int(10) unsigned NOT NULL DEFAULT '0', + `execute` tinyint(1) unsigned NOT NULL DEFAULT '1' +) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci; + +ALTER TABLE `survey_sessions_queue` + ADD PRIMARY KEY (`unit_session_id`), ADD KEY `run_session_id` (`run_session_id`,`unit_id`), ADD KEY `expires` (`expires`); diff --git a/sql/patches/031_settings_table.sql b/sql/patches/031_settings_table.sql new file mode 100644 index 000000000..c34ae4432 --- /dev/null +++ b/sql/patches/031_settings_table.sql @@ -0,0 +1,7 @@ +CREATE TABLE `survey_settings` ( + `id` int(10) UNSIGNED NOT NULL AUTO_INCREMENT, + `setting` varchar(255) COLLATE utf8_unicode_ci NOT NULL, + `value` text COLLATE utf8_unicode_ci NOT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `setting` (`setting`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci; diff --git a/sql/patches/032_fraction_pause.sql b/sql/patches/032_fraction_pause.sql new file mode 100644 index 000000000..4eca99177 --- /dev/null +++ b/sql/patches/032_fraction_pause.sql @@ -0,0 +1,2 @@ +ALTER TABLE `survey_pauses` CHANGE `wait_minutes` `wait_minutes` decimal(13,2) DEFAULT NULL; +ALTER TABLE `survey_unit_sessions` ADD `expires` DATETIME NULL DEFAULT NULL AFTER `created`, ADD `queued` TINYINT NOT NULL DEFAULT '0' AFTER `expires`, ADD `result` VARCHAR(40) NULL AFTER `queued`, ADD `result_log` TEXT NULL AFTER `result`; diff --git a/sql/patches/033_add_index_survey_unit_sessions.sql b/sql/patches/033_add_index_survey_unit_sessions.sql new file mode 100644 index 000000000..8e35d3a66 --- /dev/null +++ b/sql/patches/033_add_index_survey_unit_sessions.sql @@ -0,0 +1 @@ +CREATE INDEX queued_expires ON survey_unit_sessions (queued, expires); diff --git a/sql/patches/034_add_session_id_to_email_queue.sql b/sql/patches/034_add_session_id_to_email_queue.sql new file mode 100644 index 000000000..869543a67 --- /dev/null +++ b/sql/patches/034_add_session_id_to_email_queue.sql @@ -0,0 +1,13 @@ +ALTER TABLE `survey_email_log` ADD COLUMN `account_id` INT(10) unsigned; +ALTER TABLE `survey_email_log` ADD COLUMN `subject` VARCHAR(355); +ALTER TABLE `survey_email_log` ADD COLUMN `message` TEXT; +ALTER TABLE `survey_email_log` ADD COLUMN `meta` TEXT; +ALTER TABLE `survey_email_log` ADD COLUMN `status` TINYINT(1); +ALTER TABLE `survey_email_log` DROP COLUMN `sent`; +ALTER TABLE `survey_email_log` ADD COLUMN `sent` DATETIME; +CREATE INDEX `account_status` ON survey_email_log (`account_id`, `status`); + +ALTER TABLE `survey_email_accounts` + ADD `status` TINYINT(1) DEFAULT 1; + +DROP TABLE IF EXISTS `survey_email_queue`; \ No newline at end of file diff --git a/sql/patches/035_current_unit.sql b/sql/patches/035_current_unit.sql new file mode 100644 index 000000000..a73d4dc09 --- /dev/null +++ b/sql/patches/035_current_unit.sql @@ -0,0 +1,28 @@ +ALTER TABLE `survey_run_sessions` RENAME COLUMN `current_unit_id` TO `current_unit_session_id`; +ALTER TABLE `survey_run_sessions` DROP FOREIGN KEY `fk_survey_run_sessions_survey_units1`; + +UPDATE `survey_run_sessions` + LEFT JOIN `survey_unit_sessions` m ON m.run_session_id = `survey_run_sessions`.id + LEFT JOIN `survey_unit_sessions` b -- "b" from "bigger" + ON m.run_session_id = b.run_session_id -- match "max" row with "bigger" row by `home` + AND m.id < b.id -- want "bigger" than "max" +SET `survey_run_sessions`.`current_unit_session_id` = m.id +WHERE m.run_session_id IS NOT NULL AND b.id IS NULL; + +DROP TABLE IF EXISTS `survey_sessions_queue`; + +-- SELECT run_id, `session`, position, m.id, m.queued, m.expires, m.ended, m.expired FROM `survey_run_sessions` +-- LEFT JOIN `survey_unit_sessions` m ON m.run_session_id = `survey_run_sessions`.id +-- LEFT JOIN `survey_unit_sessions` b -- "b" from "bigger" +-- ON m.run_session_id = b.run_session_id -- match "max" row with "bigger" row by `home` +-- AND m.id < b.id -- want "bigger" than "max" +-- WHERE m.run_session_id IS NOT NULL AND b.id IS NULL; + +-- SET GLOBAL join_buffer_size=524288; +-- SELECT run_id, `session`, position, m.id, m.queued, m.expires, m.ended, m.expired FROM `survey_run_sessions` +-- LEFT JOIN `survey_unit_sessions` m ON m.run_session_id = `survey_run_sessions`.id +-- LEFT JOIN `survey_unit_sessions` b -- "b" from "bigger" +-- ON m.run_session_id = b.run_session_id -- match "max" row with "bigger" row by `home` +-- AND m.id < b.id -- want "bigger" than "max" +-- WHERE m.run_session_id IS NOT NULL AND b.id IS NULL LIMIT 20000,10000; +-- SET GLOBAL join_buffer_size=262144; \ No newline at end of file diff --git a/sql/patches/036_unstick_branches.sql b/sql/patches/036_unstick_branches.sql new file mode 100644 index 000000000..e3bf4cb2c --- /dev/null +++ b/sql/patches/036_unstick_branches.sql @@ -0,0 +1,6 @@ +UPDATE `survey_unit_sessions` +INNER JOIN `survey_branches` ON `survey_branches`.`id` = `survey_unit_sessions`.`unit_id` +SET `survey_unit_sessions`.`queued` = 1 +WHERE `survey_branches`.`automatically_jump` = 1 AND `survey_branches`.`automatically_go_on` = 1 AND `survey_unit_sessions`.`ended` IS NULL AND `survey_unit_sessions`.`created` > '2021-08-01'; + +DROP TABLE IF EXISTS `survey_cron_log`; \ No newline at end of file diff --git a/sql/schema.sql b/sql/schema.sql index 622e51ddc..493aeeb81 100644 --- a/sql/schema.sql +++ b/sql/schema.sql @@ -1,34 +1,37 @@ -- -- Database: `formr` +-- Schema Updated: 10.05.2021 -- - +SET NAMES utf8mb4; +CREATE DATABASE IF NOT EXISTS formr CHARSET=utf8mb4 COLLATE utf8mb4_unicode_ci; +USE formr; -- -- Table structure for table `oauth_access_tokens` -- CREATE TABLE `oauth_access_tokens` ( - `access_token` varchar(40) COLLATE utf8_unicode_ci NOT NULL DEFAULT '', - `client_id` varchar(80) COLLATE utf8_unicode_ci DEFAULT NULL, - `user_id` varchar(255) COLLATE utf8_unicode_ci DEFAULT NULL, + `access_token` varchar(40) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '', + `client_id` varchar(80) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `user_id` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL, `expires` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, - `scope` varchar(2000) COLLATE utf8_unicode_ci DEFAULT NULL, + `scope` varchar(2000) COLLATE utf8mb4_unicode_ci DEFAULT NULL, PRIMARY KEY (`access_token`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci; +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; -- -- Table structure for table `oauth_authorization_codes` -- CREATE TABLE `oauth_authorization_codes` ( - `authorization_code` varchar(40) COLLATE utf8_unicode_ci NOT NULL DEFAULT '', - `client_id` varchar(80) COLLATE utf8_unicode_ci DEFAULT NULL, - `user_id` varchar(255) COLLATE utf8_unicode_ci DEFAULT NULL, - `redirect_uri` varchar(2000) COLLATE utf8_unicode_ci DEFAULT NULL, + `authorization_code` varchar(40) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '', + `client_id` varchar(80) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `user_id` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `redirect_uri` varchar(2000) COLLATE utf8mb4_unicode_ci DEFAULT NULL, `expires` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, - `scope` varchar(2000) COLLATE utf8_unicode_ci DEFAULT NULL, + `scope` varchar(2000) COLLATE utf8mb4_unicode_ci DEFAULT NULL, PRIMARY KEY (`authorization_code`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci; +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; -- @@ -36,25 +39,25 @@ CREATE TABLE `oauth_authorization_codes` ( -- CREATE TABLE `oauth_clients` ( - `client_id` varchar(80) COLLATE utf8_unicode_ci NOT NULL DEFAULT '', - `client_secret` varchar(80) COLLATE utf8_unicode_ci DEFAULT NULL, - `redirect_uri` varchar(2000) COLLATE utf8_unicode_ci DEFAULT NULL, - `grant_types` varchar(80) COLLATE utf8_unicode_ci DEFAULT NULL, - `scope` varchar(100) COLLATE utf8_unicode_ci DEFAULT NULL, - `user_id` varchar(80) COLLATE utf8_unicode_ci DEFAULT NULL, + `client_id` varchar(80) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '', + `client_secret` varchar(80) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `redirect_uri` varchar(2000) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `grant_types` varchar(80) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `scope` varchar(100) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `user_id` varchar(80) COLLATE utf8mb4_unicode_ci DEFAULT NULL, PRIMARY KEY (`client_id`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci; +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; -- -- Table structure for table `oauth_jwt` -- CREATE TABLE `oauth_jwt` ( - `client_id` varchar(80) COLLATE utf8_unicode_ci NOT NULL DEFAULT '', - `subject` varchar(80) COLLATE utf8_unicode_ci DEFAULT NULL, - `public_key` varchar(2000) COLLATE utf8_unicode_ci DEFAULT NULL, + `client_id` varchar(80) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '', + `subject` varchar(80) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `public_key` varchar(2000) COLLATE utf8mb4_unicode_ci DEFAULT NULL, PRIMARY KEY (`client_id`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci; +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; -- @@ -62,34 +65,34 @@ CREATE TABLE `oauth_jwt` ( -- CREATE TABLE `oauth_refresh_tokens` ( - `refresh_token` varchar(40) COLLATE utf8_unicode_ci NOT NULL DEFAULT '', - `client_id` varchar(80) COLLATE utf8_unicode_ci DEFAULT NULL, - `user_id` varchar(255) COLLATE utf8_unicode_ci DEFAULT NULL, + `refresh_token` varchar(40) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '', + `client_id` varchar(80) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `user_id` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL, `expires` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, - `scope` varchar(2000) COLLATE utf8_unicode_ci DEFAULT NULL, + `scope` varchar(2000) COLLATE utf8mb4_unicode_ci DEFAULT NULL, PRIMARY KEY (`refresh_token`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci; +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; -- -- Table structure for table `oauth_scopes` -- CREATE TABLE `oauth_scopes` ( - `scope` mediumtext COLLATE utf8_unicode_ci, + `scope` mediumtext COLLATE utf8mb4_unicode_ci, `is_default` tinyint(1) DEFAULT NULL -) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci; +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; -- -- Table structure for table `oauth_users` -- CREATE TABLE `oauth_users` ( - `username` varchar(255) COLLATE utf8_unicode_ci NOT NULL DEFAULT '', - `password` varchar(2000) COLLATE utf8_unicode_ci DEFAULT NULL, - `first_name` varchar(255) COLLATE utf8_unicode_ci DEFAULT NULL, - `last_name` varchar(255) COLLATE utf8_unicode_ci DEFAULT NULL, + `username` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '', + `password` varchar(2000) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `first_name` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `last_name` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL, PRIMARY KEY (`username`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci; +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; -- -- Table structure for table `osf` @@ -97,10 +100,10 @@ CREATE TABLE `oauth_users` ( CREATE TABLE `osf` ( `user_id` int(10) unsigned NOT NULL, - `access_token` varchar(150) COLLATE utf8_unicode_ci DEFAULT NULL, + `access_token` varchar(150) COLLATE utf8mb4_unicode_ci DEFAULT NULL, `access_token_expires` int(10) unsigned NOT NULL, PRIMARY KEY (`user_id`) -) ENGINE=MyISAM DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci; +) ENGINE=MyISAM DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; -- -- Table structure for table `survey_users` @@ -109,22 +112,25 @@ CREATE TABLE `osf` ( CREATE TABLE `survey_users` ( `id` int(10) unsigned NOT NULL AUTO_INCREMENT, `user_code` char(64) CHARACTER SET latin1 COLLATE latin1_bin NOT NULL, + `first_name` VARCHAR(50) NULL, + `last_name` VARCHAR(50) NULL, + `affiliation` VARCHAR(350) NULL, `created` datetime DEFAULT NULL, `modified` datetime DEFAULT NULL, `admin` tinyint(1) DEFAULT '0', - `email` varchar(255) COLLATE utf8_unicode_ci DEFAULT NULL, - `password` varchar(255) COLLATE utf8_unicode_ci DEFAULT NULL, + `email` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `password` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL, `email_verification_hash` varchar(255) CHARACTER SET latin1 COLLATE latin1_bin DEFAULT NULL, `email_verified` tinyint(1) DEFAULT '0', `reset_token_hash` varchar(255) CHARACTER SET latin1 COLLATE latin1_bin DEFAULT NULL, `reset_token_expiry` datetime DEFAULT NULL, - `mobile_number` varchar(30) COLLATE utf8_unicode_ci DEFAULT NULL, - `mobile_verification_hash` varchar(255) COLLATE utf8_unicode_ci DEFAULT NULL, + `mobile_number` varchar(30) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `mobile_verification_hash` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL, `mobile_verified` tinyint(1) DEFAULT '0', - `referrer_code` varchar(255) COLLATE utf8_unicode_ci DEFAULT NULL, + `referrer_code` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL, PRIMARY KEY (`id`), UNIQUE KEY `user_code_UNIQUE` (`user_code`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci; +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; -- -- Table structure for table `survey_units` @@ -134,10 +140,10 @@ CREATE TABLE `survey_units` ( `id` int(10) unsigned NOT NULL AUTO_INCREMENT, `created` datetime DEFAULT NULL, `modified` datetime DEFAULT NULL, - `type` varchar(20) COLLATE utf8_unicode_ci DEFAULT NULL, + `type` varchar(20) COLLATE utf8mb4_unicode_ci DEFAULT NULL, PRIMARY KEY (`id`), KEY `type` (`type`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci; +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; -- -- Table structure for table `survey_runs` @@ -148,7 +154,7 @@ CREATE TABLE `survey_runs` ( `user_id` int(10) unsigned NOT NULL, `created` datetime DEFAULT NULL, `modified` datetime DEFAULT NULL, - `name` varchar(45) COLLATE utf8_unicode_ci DEFAULT NULL, + `name` varchar(45) COLLATE utf8mb4_unicode_ci DEFAULT NULL, `api_secret_hash` varchar(255) CHARACTER SET latin1 COLLATE latin1_bin DEFAULT NULL, `cron_active` tinyint(1) DEFAULT '0', `public` tinyint(4) DEFAULT '0', @@ -157,20 +163,21 @@ CREATE TABLE `survey_runs` ( `service_message` int(10) unsigned DEFAULT NULL, `overview_script` int(10) unsigned DEFAULT NULL, `deactivated_page` int(10) unsigned DEFAULT NULL, - `title` varchar(255) COLLATE utf8_unicode_ci DEFAULT NULL, - `description` varchar(1000) COLLATE utf8_unicode_ci DEFAULT NULL, - `description_parsed` mediumtext COLLATE utf8_unicode_ci, - `public_blurb` mediumtext COLLATE utf8_unicode_ci, - `public_blurb_parsed` mediumtext COLLATE utf8_unicode_ci, - `header_image_path` varchar(255) COLLATE utf8_unicode_ci DEFAULT NULL, - `footer_text` mediumtext COLLATE utf8_unicode_ci, - `footer_text_parsed` mediumtext COLLATE utf8_unicode_ci, - `custom_css_path` varchar(255) COLLATE utf8_unicode_ci DEFAULT NULL, - `custom_js_path` varchar(255) COLLATE utf8_unicode_ci DEFAULT NULL, - `osf_project_id` varchar(20) COLLATE utf8_unicode_ci DEFAULT NULL, + `title` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `description` varchar(1000) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `description_parsed` mediumtext COLLATE utf8mb4_unicode_ci, + `public_blurb` mediumtext COLLATE utf8mb4_unicode_ci, + `public_blurb_parsed` mediumtext COLLATE utf8mb4_unicode_ci, + `header_image_path` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `footer_text` mediumtext COLLATE utf8mb4_unicode_ci, + `footer_text_parsed` mediumtext COLLATE utf8mb4_unicode_ci, + `custom_css_path` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `custom_js_path` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `osf_project_id` varchar(20) COLLATE utf8mb4_unicode_ci DEFAULT NULL, `last_deamon_access` int(10) unsigned DEFAULT '0', `cron_fork` tinyint(3) unsigned NOT NULL DEFAULT '1', `use_material_design` tinyint(1) NOT NULL DEFAULT '0', + `expire_cookie` INT UNSIGNED NOT NULL DEFAULT '0', PRIMARY KEY (`id`), KEY `fk_runs_survey_users1_idx` (`user_id`), KEY `fk_survey_runs_survey_units1_idx` (`reminder_email`), @@ -183,7 +190,7 @@ CREATE TABLE `survey_runs` ( CONSTRAINT `fk_survey_runs_survey_units2` FOREIGN KEY (`service_message`) REFERENCES `survey_units` (`id`) ON DELETE CASCADE ON UPDATE NO ACTION, CONSTRAINT `fk_survey_runs_survey_units3` FOREIGN KEY (`overview_script`) REFERENCES `survey_units` (`id`) ON DELETE NO ACTION ON UPDATE NO ACTION, CONSTRAINT `fk_survey_runs_survey_units4` FOREIGN KEY (`deactivated_page`) REFERENCES `survey_units` (`id`) ON DELETE NO ACTION ON UPDATE NO ACTION -) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci; +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; -- -- Table structure for table `survey_run_sessions` @@ -193,27 +200,25 @@ CREATE TABLE `survey_run_sessions` ( `id` int(11) NOT NULL AUTO_INCREMENT, `run_id` int(10) unsigned NOT NULL, `user_id` int(10) unsigned DEFAULT NULL, - `session` char(64) COLLATE utf8_unicode_ci NOT NULL, + `session` char(64) COLLATE utf8mb4_unicode_ci NOT NULL, `created` datetime DEFAULT NULL, `ended` datetime DEFAULT NULL, `last_access` datetime DEFAULT NULL, `position` smallint(6) DEFAULT NULL, - `current_unit_id` int(10) unsigned DEFAULT NULL, - `deactivated` tinyint(1) DEFAULT '0', + `current_unit_session_id` int(10) unsigned DEFAULT NULL, + `deactivated` tinyint(1) DEFAULT 0, `no_email` int(11) DEFAULT NULL, - `testing` tinyint(1) DEFAULT '0', + `testing` tinyint(1) DEFAULT 0, PRIMARY KEY (`id`), UNIQUE KEY `run_session` (`session`,`run_id`), UNIQUE KEY `run_user` (`user_id`,`run_id`), KEY `fk_survey_run_sessions_survey_runs1_idx` (`run_id`), KEY `fk_survey_run_sessions_survey_users1_idx` (`user_id`), - KEY `fk_survey_run_sessions_survey_units1_idx` (`current_unit_id`), + KEY `fk_survey_run_sessions_survey_units1_idx` (`current_unit_session_id`), KEY `position` (`position`), CONSTRAINT `fk_survey_run_sessions_survey_runs1` FOREIGN KEY (`run_id`) REFERENCES `survey_runs` (`id`) ON DELETE CASCADE ON UPDATE NO ACTION, - CONSTRAINT `fk_survey_run_sessions_survey_units1` FOREIGN KEY (`current_unit_id`) REFERENCES `survey_units` (`id`) ON DELETE SET NULL ON UPDATE NO ACTION, CONSTRAINT `fk_survey_run_sessions_survey_users1` FOREIGN KEY (`user_id`) REFERENCES `survey_users` (`id`) ON DELETE CASCADE ON UPDATE NO ACTION -) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci; - +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; -- -- Table structure for table `survey_unit_sessions` -- @@ -223,6 +228,10 @@ CREATE TABLE `survey_unit_sessions` ( `unit_id` int(10) unsigned NOT NULL, `run_session_id` int(11) DEFAULT NULL, `created` datetime NOT NULL, + `expires` datetime DEFAULT NULL, + `queued` tinyint(3) NOT NULL DEFAULT 0, + `result` varchar(40) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `result_log` text COLLATE utf8mb4_unicode_ci DEFAULT NULL, `ended` datetime DEFAULT NULL, `expired` datetime DEFAULT NULL, PRIMARY KEY (`id`), @@ -230,10 +239,11 @@ CREATE TABLE `survey_unit_sessions` ( KEY `fk_survey_sessions_survey_units1_idx` (`unit_id`), KEY `fk_survey_unit_sessions_survey_run_sessions1_idx` (`run_session_id`), KEY `ended` (`ended`), + KEY `queued_expires` (`queued`,`expires`), + KEY `results` (`created`,`result`,`run_session_id`), CONSTRAINT `fk_survey_sessions_survey_units1` FOREIGN KEY (`unit_id`) REFERENCES `survey_units` (`id`) ON DELETE CASCADE ON UPDATE NO ACTION, CONSTRAINT `fk_survey_unit_sessions_survey_run_sessions1` FOREIGN KEY (`run_session_id`) REFERENCES `survey_run_sessions` (`id`) ON DELETE CASCADE ON UPDATE NO ACTION -) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci; - +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; -- -- Table structure for table `shuffle` @@ -249,7 +259,7 @@ CREATE TABLE `shuffle` ( KEY `fk_survey_reports_survey_units1_idx` (`unit_id`), CONSTRAINT `fk_unit_sessions_shuffle` FOREIGN KEY (`session_id`) REFERENCES `survey_unit_sessions` (`id`) ON DELETE CASCADE ON UPDATE NO ACTION, CONSTRAINT `fk_unit_shuffle` FOREIGN KEY (`unit_id`) REFERENCES `survey_units` (`id`) ON DELETE CASCADE ON UPDATE NO ACTION -) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci; +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; -- -- Table structure for table `survey_branches` @@ -257,38 +267,14 @@ CREATE TABLE `shuffle` ( CREATE TABLE `survey_branches` ( `id` int(10) unsigned NOT NULL, - `condition` mediumtext COLLATE utf8_unicode_ci, + `condition` mediumtext COLLATE utf8mb4_unicode_ci, `if_true` smallint(6) DEFAULT NULL, `automatically_jump` tinyint(1) DEFAULT '1', `automatically_go_on` tinyint(1) DEFAULT '1', PRIMARY KEY (`id`), KEY `fk_survey_branch_survey_units1_idx` (`id`), CONSTRAINT `fk_branch_unit` FOREIGN KEY (`id`) REFERENCES `survey_units` (`id`) ON DELETE CASCADE ON UPDATE NO ACTION -) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci; - --- --- Table structure for table `survey_cron_log` --- - -CREATE TABLE `survey_cron_log` ( - `id` int(11) NOT NULL AUTO_INCREMENT, - `run_id` int(10) unsigned NOT NULL, - `created` datetime DEFAULT NULL, - `ended` datetime DEFAULT NULL, - `sessions` int(10) unsigned DEFAULT NULL, - `skipforwards` int(10) unsigned DEFAULT NULL, - `skipbackwards` int(10) unsigned DEFAULT NULL, - `pauses` int(10) unsigned DEFAULT NULL, - `emails` int(10) unsigned DEFAULT NULL, - `shuffles` int(10) unsigned DEFAULT NULL, - `errors` int(10) unsigned DEFAULT NULL, - `warnings` int(10) unsigned DEFAULT NULL, - `notices` int(10) unsigned DEFAULT NULL, - `message` mediumtext COLLATE utf8_unicode_ci, - PRIMARY KEY (`id`), - KEY `fk_survey_cron_log_survey_runs1_idx` (`run_id`), - CONSTRAINT `fk_survey_cron_log_survey_runs1` FOREIGN KEY (`run_id`) REFERENCES `survey_runs` (`id`) ON DELETE CASCADE ON UPDATE NO ACTION -) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci; +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; -- -- Table structure for table `survey_email_accounts` @@ -299,20 +285,20 @@ CREATE TABLE `survey_email_accounts` ( `user_id` int(10) unsigned NOT NULL, `created` datetime DEFAULT NULL, `modified` datetime DEFAULT NULL, - `from` varchar(255) COLLATE utf8_unicode_ci DEFAULT NULL, - `from_name` varchar(255) COLLATE utf8_unicode_ci DEFAULT NULL, - `host` varchar(255) COLLATE utf8_unicode_ci DEFAULT NULL, + `from` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `from_name` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `host` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL, `port` smallint(6) DEFAULT NULL, `tls` tinyint(4) DEFAULT NULL, - `username` varchar(255) COLLATE utf8_unicode_ci DEFAULT NULL, - `password` varchar(255) COLLATE utf8_unicode_ci DEFAULT NULL, - `auth_key` text COLLATE utf8_unicode_ci NOT NULL, - `deleted` int(1) NOT NULL DEFAULT '0', + `username` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `password` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `auth_key` text COLLATE utf8mb4_unicode_ci NOT NULL, + `deleted` int(1) NOT NULL DEFAULT 0, + `status` tinyint(1) DEFAULT NULL, PRIMARY KEY (`id`), KEY `fk_survey_emails_survey_users1_idx` (`user_id`), CONSTRAINT `fk_email_user` FOREIGN KEY (`user_id`) REFERENCES `survey_users` (`id`) ON DELETE CASCADE ON UPDATE NO ACTION -) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci; - +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; -- -- Table structure for table `survey_emails` -- @@ -320,10 +306,10 @@ CREATE TABLE `survey_email_accounts` ( CREATE TABLE `survey_emails` ( `id` int(10) unsigned NOT NULL, `account_id` int(10) unsigned DEFAULT NULL, - `subject` varchar(255) COLLATE utf8_unicode_ci DEFAULT NULL, - `recipient_field` varchar(255) COLLATE utf8_unicode_ci DEFAULT NULL, - `body` mediumtext COLLATE utf8_unicode_ci, - `body_parsed` mediumtext COLLATE utf8_unicode_ci, + `subject` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `recipient_field` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `body` mediumtext COLLATE utf8mb4_unicode_ci, + `body_parsed` mediumtext COLLATE utf8mb4_unicode_ci, `html` tinyint(1) DEFAULT NULL, `cron_only` tinyint(3) unsigned NOT NULL DEFAULT '0', PRIMARY KEY (`id`), @@ -331,7 +317,7 @@ CREATE TABLE `survey_emails` ( KEY `fk_survey_emails_survey_email_accounts1_idx` (`account_id`), CONSTRAINT `fk_email_acc` FOREIGN KEY (`account_id`) REFERENCES `survey_email_accounts` (`id`) ON DELETE NO ACTION ON UPDATE NO ACTION, CONSTRAINT `fk_email_unit` FOREIGN KEY (`id`) REFERENCES `survey_units` (`id`) ON DELETE CASCADE ON UPDATE NO ACTION -) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci; +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; -- @@ -343,32 +329,20 @@ CREATE TABLE `survey_email_log` ( `session_id` int(10) unsigned DEFAULT NULL, `email_id` int(10) unsigned DEFAULT NULL, `created` datetime NOT NULL, - `recipient` varchar(255) COLLATE utf8_unicode_ci DEFAULT NULL, - `sent` tinyint(1) NOT NULL DEFAULT '1', + `recipient` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `account_id` int(10) unsigned DEFAULT NULL, + `subject` varchar(355) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `message` text COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `meta` text COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `status` tinyint(1) DEFAULT NULL, + `sent` datetime DEFAULT NULL, PRIMARY KEY (`id`), KEY `fk_survey_email_log_survey_emails1_idx` (`email_id`), KEY `fk_survey_email_log_survey_unit_sessions1_idx` (`session_id`), + KEY `account_status` (`account_id`,`status`), CONSTRAINT `fk_survey_email_log_survey_emails1` FOREIGN KEY (`email_id`) REFERENCES `survey_emails` (`id`) ON DELETE CASCADE ON UPDATE NO ACTION, CONSTRAINT `fk_survey_email_log_survey_unit_sessions1` FOREIGN KEY (`session_id`) REFERENCES `survey_unit_sessions` (`id`) ON DELETE SET NULL ON UPDATE NO ACTION -) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci; - --- --- Table structure for table `survey_email_queue` --- - -CREATE TABLE `survey_email_queue` ( - `id` int(10) unsigned NOT NULL AUTO_INCREMENT, - `account_id` int(10) unsigned NOT NULL, - `subject` varchar(355) COLLATE utf8_unicode_ci NOT NULL, - `message` text COLLATE utf8_unicode_ci NOT NULL, - `recipient` varchar(100) COLLATE utf8_unicode_ci NOT NULL, - `created` datetime NOT NULL, - `meta` text COLLATE utf8_unicode_ci NOT NULL, - PRIMARY KEY (`id`), - KEY `account_id` (`account_id`), - CONSTRAINT `survey_email_queue_ibfk_1` FOREIGN KEY (`account_id`) REFERENCES `survey_email_accounts` (`id`) ON DELETE CASCADE ON UPDATE NO ACTION -) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci; - +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; -- -- Table structure for table `survey_externals` @@ -376,13 +350,13 @@ CREATE TABLE `survey_email_queue` ( CREATE TABLE `survey_externals` ( `id` int(10) unsigned NOT NULL, - `address` text COLLATE utf8_unicode_ci, + `address` text COLLATE utf8mb4_unicode_ci, `api_end` tinyint(1) DEFAULT '0', `expire_after` int(10) unsigned DEFAULT NULL, PRIMARY KEY (`id`), KEY `fk_survey_forks_survey_run_items1_idx` (`id`), CONSTRAINT `fk_external_unit` FOREIGN KEY (`id`) REFERENCES `survey_units` (`id`) ON DELETE CASCADE ON UPDATE NO ACTION -) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci; +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; -- @@ -394,8 +368,8 @@ CREATE TABLE `survey_studies` ( `user_id` int(10) unsigned NOT NULL, `created` datetime DEFAULT NULL, `modified` datetime DEFAULT NULL, - `name` varchar(255) COLLATE utf8_unicode_ci DEFAULT NULL, - `results_table` varchar(64) COLLATE utf8_unicode_ci DEFAULT NULL, + `name` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `results_table` varchar(64) COLLATE utf8mb4_unicode_ci DEFAULT NULL, `valid` tinyint(1) DEFAULT NULL, `maximum_number_displayed` smallint(5) unsigned DEFAULT NULL, `displayed_percentage_maximum` tinyint(3) unsigned DEFAULT NULL, @@ -404,8 +378,8 @@ CREATE TABLE `survey_studies` ( `expire_invitation_after` int(10) unsigned DEFAULT NULL, `expire_invitation_grace` int(10) unsigned DEFAULT NULL, `enable_instant_validation` tinyint(1) DEFAULT '1', - `original_file` varchar(225) COLLATE utf8_unicode_ci DEFAULT NULL, - `google_file_id` varchar(150) COLLATE utf8_unicode_ci DEFAULT NULL, + `original_file` varchar(225) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `google_file_id` varchar(150) COLLATE utf8mb4_unicode_ci DEFAULT NULL, `unlinked` tinyint(1) DEFAULT '0', `hide_results` tinyint(4) NOT NULL DEFAULT '0', `use_paging` tinyint(4) NOT NULL DEFAULT '0', @@ -415,7 +389,7 @@ CREATE TABLE `survey_studies` ( KEY `fk_survey_studies_run_items1_idx` (`id`), CONSTRAINT `fk_study_unit` FOREIGN KEY (`id`) REFERENCES `survey_units` (`id`) ON DELETE CASCADE ON UPDATE NO ACTION, CONSTRAINT `fk_survey_studies_survey_users` FOREIGN KEY (`user_id`) REFERENCES `survey_users` (`id`) ON DELETE NO ACTION ON UPDATE NO ACTION -) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci; +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; -- -- Table structure for table `survey_items` @@ -424,20 +398,20 @@ CREATE TABLE `survey_studies` ( CREATE TABLE `survey_items` ( `id` int(11) unsigned NOT NULL AUTO_INCREMENT, `study_id` int(10) unsigned NOT NULL, - `type` varchar(100) COLLATE utf8_unicode_ci DEFAULT NULL, - `choice_list` varchar(255) COLLATE utf8_unicode_ci DEFAULT NULL, - `type_options` varchar(255) COLLATE utf8_unicode_ci DEFAULT NULL, - `name` varchar(255) COLLATE utf8_unicode_ci DEFAULT NULL, - `label` text COLLATE utf8_unicode_ci, - `label_parsed` mediumtext COLLATE utf8_unicode_ci, + `type` varchar(100) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `choice_list` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `type_options` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `name` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `label` text COLLATE utf8mb4_unicode_ci, + `label_parsed` mediumtext COLLATE utf8mb4_unicode_ci, `optional` tinyint(4) DEFAULT NULL, - `class` varchar(255) COLLATE utf8_unicode_ci DEFAULT NULL, - `showif` mediumtext COLLATE utf8_unicode_ci, - `value` text COLLATE utf8_unicode_ci, - `block_order` varchar(4) COLLATE utf8_unicode_ci DEFAULT NULL, + `class` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `showif` mediumtext COLLATE utf8mb4_unicode_ci, + `value` text COLLATE utf8mb4_unicode_ci, + `block_order` varchar(4) COLLATE utf8mb4_unicode_ci DEFAULT NULL, `item_order` smallint(6) DEFAULT NULL, `order` int(10) DEFAULT NULL, - `post_process` mediumtext COLLATE utf8_unicode_ci, + `post_process` mediumtext COLLATE utf8mb4_unicode_ci, `page_no` smallint(5) unsigned DEFAULT NULL, PRIMARY KEY (`id`), UNIQUE KEY `study_item` (`study_id`,`name`), @@ -445,7 +419,7 @@ CREATE TABLE `survey_items` ( KEY `type` (`study_id`,`type`), KEY `page_no` (`page_no`), CONSTRAINT `fk_survey_items_survey_studies1` FOREIGN KEY (`study_id`) REFERENCES `survey_studies` (`id`) ON DELETE CASCADE ON UPDATE NO ACTION -) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci; +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; -- -- Table structure for table `survey_item_choices` @@ -454,17 +428,17 @@ CREATE TABLE `survey_items` ( CREATE TABLE `survey_item_choices` ( `id` int(10) unsigned NOT NULL AUTO_INCREMENT, `study_id` int(10) unsigned NOT NULL, - `list_name` varchar(255) COLLATE utf8_unicode_ci DEFAULT NULL, - `name` varchar(255) COLLATE utf8_unicode_ci DEFAULT NULL, - `label` mediumtext COLLATE utf8_unicode_ci, - `label_parsed` mediumtext COLLATE utf8_unicode_ci, + `list_name` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `name` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `label` mediumtext COLLATE utf8mb4_unicode_ci, + `label_parsed` mediumtext COLLATE utf8mb4_unicode_ci, PRIMARY KEY (`id`), KEY `fk_survey_item_choices_survey_studies1_idx` (`study_id`), KEY `listname` (`list_name`), KEY `list_name` (`list_name`), KEY `list_name_2` (`list_name`), CONSTRAINT `fk_survey_item_choices_survey_studies1` FOREIGN KEY (`study_id`) REFERENCES `survey_studies` (`id`) ON DELETE CASCADE ON UPDATE NO ACTION -) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci; +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; -- @@ -475,7 +449,7 @@ CREATE TABLE `survey_items_display` ( `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT, `item_id` int(10) unsigned NOT NULL, `session_id` int(10) unsigned NOT NULL, - `answer` mediumtext COLLATE utf8_unicode_ci, + `answer` mediumtext COLLATE utf8mb4_unicode_ci, `created` datetime DEFAULT NULL, `answered` datetime DEFAULT NULL, `answered_relative` double DEFAULT NULL, @@ -493,7 +467,7 @@ CREATE TABLE `survey_items_display` ( KEY `page` (`page`), CONSTRAINT `itemid` FOREIGN KEY (`item_id`) REFERENCES `survey_items` (`id`) ON DELETE CASCADE ON UPDATE NO ACTION, CONSTRAINT `sessionidx` FOREIGN KEY (`session_id`) REFERENCES `survey_unit_sessions` (`id`) ON DELETE CASCADE ON UPDATE NO ACTION -) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci; +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; -- -- Table structure for table `survey_newsletter` @@ -501,14 +475,14 @@ CREATE TABLE `survey_items_display` ( CREATE TABLE `survey_newsletter` ( `id` int(10) unsigned NOT NULL AUTO_INCREMENT, - `names` varchar(100) COLLATE utf8_unicode_ci DEFAULT NULL, - `email` varchar(100) COLLATE utf8_unicode_ci NOT NULL, + `names` varchar(100) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `email` varchar(100) COLLATE utf8mb4_unicode_ci NOT NULL, `email_verified` tinyint(1) NOT NULL DEFAULT '0', - `email_verification_hash` varchar(255) COLLATE utf8_unicode_ci DEFAULT NULL, + `email_verification_hash` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL, `date` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), UNIQUE KEY `email` (`email`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci; +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; -- -- Table structure for table `survey_pages` @@ -516,14 +490,14 @@ CREATE TABLE `survey_newsletter` ( CREATE TABLE `survey_pages` ( `id` int(10) unsigned NOT NULL, - `body` mediumtext COLLATE utf8_unicode_ci, - `body_parsed` mediumtext COLLATE utf8_unicode_ci, - `title` varchar(255) COLLATE utf8_unicode_ci DEFAULT NULL, + `body` mediumtext COLLATE utf8mb4_unicode_ci, + `body_parsed` mediumtext COLLATE utf8mb4_unicode_ci, + `title` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL, `end` tinyint(1) DEFAULT '1', PRIMARY KEY (`id`), KEY `fk_survey_feedback_survey_units1_idx` (`id`), CONSTRAINT `fk_page_unit` FOREIGN KEY (`id`) REFERENCES `survey_units` (`id`) ON DELETE CASCADE ON UPDATE NO ACTION -) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci; +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; -- -- Table structure for table `survey_pauses` @@ -533,14 +507,14 @@ CREATE TABLE `survey_pauses` ( `id` int(10) unsigned NOT NULL, `wait_until_time` time DEFAULT NULL, `wait_until_date` date DEFAULT NULL, - `wait_minutes` int(11) DEFAULT NULL, - `relative_to` mediumtext COLLATE utf8_unicode_ci, - `body` mediumtext COLLATE utf8_unicode_ci, - `body_parsed` mediumtext COLLATE utf8_unicode_ci, + `wait_minutes` decimal(13,2) DEFAULT NULL, + `relative_to` mediumtext COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `body` mediumtext COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `body_parsed` mediumtext COLLATE utf8mb4_unicode_ci DEFAULT NULL, PRIMARY KEY (`id`), KEY `fk_survey_breaks_survey_run_items1_idx` (`id`), CONSTRAINT `fk_survey_breaks_survey_run_items1` FOREIGN KEY (`id`) REFERENCES `survey_units` (`id`) ON DELETE CASCADE ON UPDATE NO ACTION -) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci; +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; -- -- Table structure for table `survey_reports` @@ -551,13 +525,13 @@ CREATE TABLE `survey_reports` ( `unit_id` int(10) unsigned NOT NULL, `created` datetime DEFAULT NULL, `last_viewed` datetime DEFAULT NULL, - `opencpu_url` varchar(400) COLLATE utf8_unicode_ci DEFAULT NULL, + `opencpu_url` varchar(400) COLLATE utf8mb4_unicode_ci DEFAULT NULL, PRIMARY KEY (`session_id`), KEY `fk_survey_results_survey_unit_sessions1_idx` (`session_id`), KEY `fk_survey_reports_survey_units1_idx` (`unit_id`), CONSTRAINT `fk_survey_reports_survey_units1` FOREIGN KEY (`unit_id`) REFERENCES `survey_units` (`id`) ON DELETE CASCADE ON UPDATE NO ACTION, CONSTRAINT `fk_survey_results_survey_unit_sessions10` FOREIGN KEY (`session_id`) REFERENCES `survey_unit_sessions` (`id`) ON DELETE CASCADE ON UPDATE NO ACTION -) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci; +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; -- -- Table structure for table `survey_results` @@ -575,7 +549,7 @@ CREATE TABLE `survey_results` ( KEY `ending` (`session_id`,`study_id`,`ended`), CONSTRAINT `fk_survey_results_survey_studies1` FOREIGN KEY (`study_id`) REFERENCES `survey_studies` (`id`) ON DELETE CASCADE ON UPDATE NO ACTION, CONSTRAINT `fk_survey_results_survey_unit_sessions1` FOREIGN KEY (`session_id`) REFERENCES `survey_unit_sessions` (`id`) ON DELETE CASCADE ON UPDATE NO ACTION -) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci; +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; -- @@ -584,9 +558,9 @@ CREATE TABLE `survey_results` ( CREATE TABLE `survey_run_settings` ( `run_session_id` int(10) unsigned NOT NULL, - `settings` mediumtext COLLATE utf8_unicode_ci, + `settings` mediumtext COLLATE utf8mb4_unicode_ci, PRIMARY KEY (`run_session_id`) -) ENGINE=MyISAM DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci; +) ENGINE=MyISAM DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; -- -- Table structure for table `survey_run_special_units` @@ -595,14 +569,14 @@ CREATE TABLE `survey_run_settings` ( CREATE TABLE `survey_run_special_units` ( `id` int(10) unsigned NOT NULL, `run_id` int(10) unsigned NOT NULL, - `type` varchar(25) COLLATE utf8_unicode_ci NOT NULL, - `description` varchar(225) COLLATE utf8_unicode_ci DEFAULT NULL, + `type` varchar(25) COLLATE utf8mb4_unicode_ci NOT NULL, + `description` varchar(225) COLLATE utf8mb4_unicode_ci DEFAULT NULL, PRIMARY KEY (`id`), KEY `run_id` (`run_id`), KEY `type` (`type`), CONSTRAINT `survey_run_special_units_ibfk_1` FOREIGN KEY (`id`) REFERENCES `survey_units` (`id`) ON DELETE CASCADE ON UPDATE NO ACTION, CONSTRAINT `survey_run_special_units_ibfk_2` FOREIGN KEY (`run_id`) REFERENCES `survey_runs` (`id`) ON DELETE CASCADE ON UPDATE NO ACTION -) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci; +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; -- -- Table structure for table `survey_run_units` @@ -613,14 +587,14 @@ CREATE TABLE `survey_run_units` ( `run_id` int(10) unsigned NOT NULL, `unit_id` int(10) unsigned DEFAULT NULL, `position` smallint(6) NOT NULL, - `description` varchar(500) COLLATE utf8_unicode_ci DEFAULT NULL, + `description` varchar(500) COLLATE utf8mb4_unicode_ci DEFAULT NULL, PRIMARY KEY (`id`,`run_id`), KEY `fk_survey_run_data_survey_runs1_idx` (`run_id`), KEY `fk_survey_run_data_survey_run_items1_idx` (`unit_id`), KEY `position_run` (`run_id`,`position`), CONSTRAINT `fk_suru` FOREIGN KEY (`run_id`) REFERENCES `survey_runs` (`id`) ON DELETE NO ACTION ON UPDATE NO ACTION, CONSTRAINT `fk_suru_it` FOREIGN KEY (`unit_id`) REFERENCES `survey_units` (`id`) ON DELETE CASCADE ON UPDATE NO ACTION -) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci; +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; -- @@ -633,7 +607,7 @@ CREATE TABLE `survey_shuffles` ( PRIMARY KEY (`id`), KEY `fk_survey_branch_survey_units1_idx` (`id`), CONSTRAINT `fk_shuffle_unit` FOREIGN KEY (`id`) REFERENCES `survey_units` (`id`) ON DELETE CASCADE ON UPDATE NO ACTION -) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci; +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; -- -- Table structure for table `survey_text_messages` @@ -642,14 +616,14 @@ CREATE TABLE `survey_shuffles` ( CREATE TABLE `survey_text_messages` ( `id` int(10) unsigned NOT NULL, `account_id` int(10) unsigned DEFAULT NULL, - `recipient_field` varchar(255) COLLATE utf8_unicode_ci DEFAULT NULL, - `body` mediumtext COLLATE utf8_unicode_ci, + `recipient_field` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `body` mediumtext COLLATE utf8mb4_unicode_ci, PRIMARY KEY (`id`), KEY `fk_survey_emails_survey_units1_idx` (`id`), KEY `fk_survey_emails_survey_email_accounts1_idx` (`account_id`), CONSTRAINT `fk_email_acc0` FOREIGN KEY (`account_id`) REFERENCES `survey_email_accounts` (`id`) ON DELETE NO ACTION ON UPDATE NO ACTION, CONSTRAINT `fk_email_unit0` FOREIGN KEY (`id`) REFERENCES `survey_units` (`id`) ON DELETE CASCADE ON UPDATE NO ACTION -) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci; +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; -- -- Table structure for table `survey_uploaded_files` @@ -660,12 +634,20 @@ CREATE TABLE `survey_uploaded_files` ( `run_id` int(10) unsigned NOT NULL, `created` datetime DEFAULT NULL, `modified` datetime DEFAULT NULL, - `original_file_name` varchar(255) COLLATE utf8_unicode_ci DEFAULT NULL, - `new_file_path` varchar(255) COLLATE utf8_unicode_ci DEFAULT NULL, + `original_file_name` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `new_file_path` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL, PRIMARY KEY (`id`,`run_id`), UNIQUE KEY `unique` (`run_id`,`original_file_name`), KEY `fk_survey_uploaded_files_survey_runs1_idx` (`run_id`), CONSTRAINT `fk_survey_uploaded_files_survey_runs1` FOREIGN KEY (`run_id`) REFERENCES `survey_runs` (`id`) ON DELETE CASCADE ON UPDATE NO ACTION -) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci; +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +CREATE TABLE `survey_settings` ( + `id` int(10) UNSIGNED NOT NULL AUTO_INCREMENT, + `setting` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL, + `value` text COLLATE utf8mb4_unicode_ci NOT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `setting` (`setting`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; diff --git a/templates/admin/account/forgot_password.php b/templates/admin/account/forgot_password.php new file mode 100755 index 000000000..58a475e5a --- /dev/null +++ b/templates/admin/account/forgot_password.php @@ -0,0 +1,80 @@ + + + + + + formr admin + + + $files) { + print_stylesheets($files, $id); + } + ?> + + + + $files) { + print_scripts($files, $id); + } + ?> + + + +
    +
    + +
    +
    +
    +
    +
    +
    +
    + +
    +
    +
    +
    +
    +
    +
    + +
    + +
    + + + + Copyright © formr +
    + +
    + + + + \ No newline at end of file diff --git a/templates/admin/account/index.php b/templates/admin/account/index.php new file mode 100755 index 000000000..af4953c20 --- /dev/null +++ b/templates/admin/account/index.php @@ -0,0 +1,144 @@ + + +
    +
    +

    User Profile

    +
    + +
    + +
    +
    + isAdmin()): ?> +
    +
    + +
    +
    +

    Your account is limited. You can request for full access as specified in the documentation

    + See Documentation +
    + +
    + + +
    +
    +
    + +
    + +

    + +

    + +
      +
    • + Surveys +
    • +
    • + Runs(Studies) +
    • +
    • + Email Accounts +
    • +
    + +
    + +
    +
    + +
    + + + + + +
    +
    + +
    + + +
    + + \ No newline at end of file diff --git a/templates/admin/account/login.php b/templates/admin/account/login.php new file mode 100755 index 000000000..fe1b4f246 --- /dev/null +++ b/templates/admin/account/login.php @@ -0,0 +1,85 @@ + + + + + + formr admin + + + $files) { + print_stylesheets($files, $id); + } + ?> + + + + $files) { + print_scripts($files, $id); + } + ?> + + + +
    +
    + +
    +
    +
    +
    +
    +
    + +
    +
    +
    +
    +
    +
    +
    + +
    + +
    + + + + Copyright © formr +
    + +
    + + + + \ No newline at end of file diff --git a/templates/admin/account/register.php b/templates/admin/account/register.php new file mode 100755 index 000000000..003f3e69a --- /dev/null +++ b/templates/admin/account/register.php @@ -0,0 +1,96 @@ + + + + + + formr admin + + + $files) { + print_stylesheets($files, $id); + } + ?> + + + + $files) { + print_scripts($files, $id); + } + ?> + + + +
    +
    + +
    +
    +
    +
    +
    +
    +
    + +
    +
    +
    +
    +
    +
    +
    + +
    + +
    + + + + Copyright © formr +
    + +
    + + + + \ No newline at end of file diff --git a/templates/admin/account/reset_password.php b/templates/admin/account/reset_password.php new file mode 100755 index 000000000..bcbcb0ca0 --- /dev/null +++ b/templates/admin/account/reset_password.php @@ -0,0 +1,86 @@ + + + + + + formr admin + + + $files) { + print_stylesheets($files, $id); + } + ?> + + + + $files) { + print_scripts($files, $id); + } + ?> + + + +
    +
    + +
    +
    +
    +
    +
    +
    +
    + +
    +
    +
    +
    +
    +
    +
    + +
    + +
    + + + + Copyright © formr +
    + +
    + + + + \ No newline at end of file diff --git a/templates/admin/advanced/active_users.php b/templates/admin/advanced/active_users.php new file mode 100644 index 000000000..0f28abe4c --- /dev/null +++ b/templates/admin/advanced/active_users.php @@ -0,0 +1,71 @@ + + +
    + +
    +

    User Management Superadmin

    +
    + + +
    +
    +
    +
    +
    +

    Formr active users

    +
    +
    + rowCount()): ?> + + + + + + + + + + + + + fetch(PDO::FETCH_ASSOC)): ?> + + + + + + + + + + +
    EmailCreatedModifiedRunUsersLast Active
    + + + ' : ' '; ?> + + + ' : ' '; + echo ''; + ?> +
    + + + + +
    +
    + +
    +
    + +
    +
    + +
    + + \ No newline at end of file diff --git a/templates/admin/advanced/cron_log.php b/templates/admin/advanced/cron_log.php new file mode 100755 index 000000000..9b35bdccf --- /dev/null +++ b/templates/admin/advanced/cron_log.php @@ -0,0 +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. +

    + + + + + $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; + + echo "\n"; + endforeach; + } + ?> + +
    {$field}
    + +render("admin/advanced/cron_log"); ?> + + + + +
    + +
    +

    Cron Logs Superadmin

    +
    + + +
    +
    +
    +
    +
    +

    Logs

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

    Cron Log

    +
    +
    +
    + printCronLogFile($parse, $expand_logs); + } + ?> +
    + + +
    +
    + +
    +
    + +
    +
    + +
    + + \ No newline at end of file diff --git a/templates/admin/advanced/info.php b/templates/admin/advanced/info.php new file mode 100644 index 000000000..83f154933 --- /dev/null +++ b/templates/admin/advanced/info.php @@ -0,0 +1,3 @@ + + +
    + +
    +

    Runs Management Superadmin

    +
    + + +
    +
    +
    +
    +
    +

    Formr Runs () Only runs with sessions are shown so might be different from total count

    +
    +
    + + rowCount()): ?> + +
    + + + + + + + + + + + + + + fetch(PDO::FETCH_ASSOC)): ?> + + + + + + + + + + + + + + +
    IDRun NameUserNo. SessionsCron ActiveLockedSessions Queue
    + + + /> + + + /> + See Queue
    + +
    +
    + + +
    +
    + +
    +
    + +
    +
    + +
    + + \ No newline at end of file diff --git a/templates/admin/advanced/runs_management_queue.php b/templates/admin/advanced/runs_management_queue.php new file mode 100644 index 000000000..ad9c95e03 --- /dev/null +++ b/templates/admin/advanced/runs_management_queue.php @@ -0,0 +1,69 @@ + + +
    + +
    +

    Runs Management Superadmin

    +
    + + +
    +
    +
    +
    +
    +

    name; ?> Sessions Queue

    +
    +
    + +

    This shows the list of sessions in run waiting to be processed.

    + +
    + + + + + + + + + + + + fetch(PDO::FETCH_ASSOC)): ?> + + + + + + + + + +
    SessionUnit (position)Added OnExpiresTo Execute
    + + + + () + YES' : 'NO'; ?> +
    +
    + +
    +
    + +
    +
    + +
    +
    + +
    + + \ No newline at end of file diff --git a/templates/admin/advanced/settings.php b/templates/admin/advanced/settings.php new file mode 100644 index 000000000..8f677bf94 --- /dev/null +++ b/templates/admin/advanced/settings.php @@ -0,0 +1,68 @@ + + +
    + +
    +

    Content Settings

    +
    + + +
    +
    +
    +
    + +
    + + +
    + + +
    + +
    +
    + +
    +
    + +
    + + array())); +Template::loadChild('admin/footer'); +?> \ No newline at end of file diff --git a/templates/admin/advanced/settings/about-page.php b/templates/admin/advanced/settings/about-page.php new file mode 100644 index 000000000..48f8b3b3a --- /dev/null +++ b/templates/admin/advanced/settings/about-page.php @@ -0,0 +1,22 @@ +
    +

    + +

    + + +
    +
    +
    + +
    +
    +
    +
    +
    \ No newline at end of file diff --git a/templates/admin/advanced/settings/docu-page.php b/templates/admin/advanced/settings/docu-page.php new file mode 100644 index 000000000..39ec7f201 --- /dev/null +++ b/templates/admin/advanced/settings/docu-page.php @@ -0,0 +1,27 @@ +
    +

    + +

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

    Users would send requests to this email for admin accounts.

    +
    +
    +
    +
    \ No newline at end of file diff --git a/templates/admin/advanced/settings/footer.php b/templates/admin/advanced/settings/footer.php new file mode 100644 index 000000000..b624810dd --- /dev/null +++ b/templates/admin/advanced/settings/footer.php @@ -0,0 +1,26 @@ +
    +

    + +

    + +
    + +
    + + +
    +
    + + +
    +
    + + +
    +
    + + +
    +
    +
    +
    \ No newline at end of file diff --git a/templates/admin/advanced/settings/js.php b/templates/admin/advanced/settings/js.php new file mode 100644 index 000000000..6171ffc48 --- /dev/null +++ b/templates/admin/advanced/settings/js.php @@ -0,0 +1,17 @@ +
    +

    + +

    + +
    + +

    Cookie Consent Configuration

    +

    + Define a JSON object that will be used to configure the cookie consent popup. Please see @doc for a sample configuration +

    +
    + +
    +
    +
    +
    \ No newline at end of file diff --git a/templates/admin/advanced/settings/publications-page.php b/templates/admin/advanced/settings/publications-page.php new file mode 100644 index 000000000..79908d25c --- /dev/null +++ b/templates/admin/advanced/settings/publications-page.php @@ -0,0 +1,26 @@ +
    +

    + +

    + +
    +
    +
    + +
    +
    +
    + + + +
    +
    +
    +
    \ No newline at end of file diff --git a/templates/admin/advanced/settings/signup.php b/templates/admin/advanced/settings/signup.php new file mode 100644 index 000000000..ca45e2e12 --- /dev/null +++ b/templates/admin/advanced/settings/signup.php @@ -0,0 +1,27 @@ +
    +

    + +

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

    Message to display to users in case sign-ups are disabled.

    +
    +
    +
    +
    \ No newline at end of file diff --git a/templates/admin/advanced/settings/studies-page.php b/templates/admin/advanced/settings/studies-page.php new file mode 100644 index 000000000..b9755e367 --- /dev/null +++ b/templates/admin/advanced/settings/studies-page.php @@ -0,0 +1,22 @@ +
    +

    + +

    + + +
    +
    +
    + + type="checkbox" value="true" name="content:studies:show" id="studies-page-show" /> + +
    +
    +
    +
    +
    \ No newline at end of file diff --git a/application/View/admin/misc/test_opencpu.php b/templates/admin/advanced/test_opencpu.php similarity index 97% rename from application/View/admin/misc/test_opencpu.php rename to templates/admin/advanced/test_opencpu.php index ce4157a90..60f75326a 100644 --- a/application/View/admin/misc/test_opencpu.php +++ b/templates/admin/advanced/test_opencpu.php @@ -1,20 +1,18 @@ '; -echo "

    OpenCPU test

    "; -echo '
    testing '.Config::get('alternative_opencpu_instance').'
    '; +echo '

    OpenCPU test

    '; +echo '

    testing ' . Config::get('opencpu_instance')['base_url'] . '

    '; +$nocache = ''; $source = '{ library(knitr) knit2html(text = "' . addslashes("__Hello__ World `r 1` ```{r} -# ".$nocache." +# " . $nocache . " library(ggplot2) qplot(rnorm(1000)) qplot(rnorm(10000), rnorm(10000)) @@ -27,36 +25,36 @@ }'; $before = microtime(true); -$results = $openCPU->identity(array('x' => $source),'', true); +//$ocpu = OpenCPU::getInstance('opencpu_instance'); +//$ocpuResponse = $ocpu->post('/base/R/identity/', array('x' => $source)); -$alert_type = 'alert-success'; -if($openCPU->http_status > 302 OR $openCPU->http_status === 0) { - $alert_type = 'alert-danger'; -} +$ocpuResponse = opencpu_knit2html($source, 'json', 1, true); -alert("1. HTTP status: ".$openCPU->http_status,$alert_type); +echo '

    Test 1

    '; +echo opencpu_debug($ocpuResponse); -$accordion = $openCPU->debugCall($results); +echo '

    Test 1 Output

    '; +echo nl2br($ocpuResponse->getObject('print')); -alert("1. Request took " . round((microtime(true) - $before) / 60, 4) . " minutes", 'alert-info'); -// =============================================== +echo "
    1. Request took " . round((microtime(true) - $before) / 60, 4) . " minutes
    "; -$openCPU = new OpenCPU(Config::get('alternative_opencpu_instance')); +// =============================================== +$openCPU = OpenCPU::getInstance(); $source = '{ library(formr) # ".$nocache." 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 +63,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 +88,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 +105,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 +113,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 +149,5 @@ echo $accordion4; -Template::load('footer'); \ No newline at end of file +echo ''; +Template::loadChild('admin/footer'); diff --git a/templates/admin/advanced/test_opencpu_speed.php b/templates/admin/advanced/test_opencpu_speed.php new file mode 100644 index 000000000..77135223b --- /dev/null +++ b/templates/admin/advanced/test_opencpu_speed.php @@ -0,0 +1,96 @@ +OpenCPU test"; +echo '
    testing ' . Config::get('alternative_opencpu_instance') . '
    '; + +$max = 30; +for ($i = 0; $i < $max; $i++): + $openCPU->clearUserData(); + $source = '{' . + mt_rand() . ' + ' . + str_repeat(" ", $i) . + ' + library(knitr) + knit2html(text = "' . addslashes("__Hello__ World `r 1` + ```{r} + library(ggplot2) + qplot(rnorm(100)) + qplot(rnorm(1000), rnorm(1000)) + library(formr) + 'blabla' %contains% 'bla' + ``` + ") . '", + fragment.only = T, options=c("base64_images","smartypants") + ) + ' . + str_repeat(" ", $max - $i) . + ' + }'; + + $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('1. HTTP status: ' . $openCPU->http_status, $alert_type); + + $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; + +endfor; + +$datasets = array('times' => $times); +$source = ' +# plot times +```{r} +library(ggplot2) +library(stringr) +library(reshape2) +just_times = times[,str_detect(names(times), "time")] +times_m = melt(just_times) +# qplot(value, data = times_m) + facet_wrap(~ variable) +# just_size = times[,str_detect(names(times),"_size")] +# size_m = melt(just_size) +# qplot(value, data = size_m) + facet_wrap(~ variable) +summary(times) +```'; +unset($times['certinfo']); + +$openCPU->addUserData(array('datasets' => $datasets)); +$accordion = $openCPU->knitForAdminDebug($source); +$alert_type = 'alert-success'; + +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 '
    '; +endif; + +Template::loadChild('footer'); diff --git a/templates/admin/advanced/timing.php b/templates/admin/advanced/timing.php new file mode 100644 index 000000000..befd5f1c5 --- /dev/null +++ b/templates/admin/advanced/timing.php @@ -0,0 +1,20 @@ + + + + + + + + + + + + + +
    + PHP +
    + DB +
    + OCPU +
    \ No newline at end of file diff --git a/templates/admin/advanced/user_detail.php b/templates/admin/advanced/user_detail.php new file mode 100755 index 000000000..f24cbcea7 --- /dev/null +++ b/templates/admin/advanced/user_detail.php @@ -0,0 +1,123 @@ + + +
    + +
    +
    +
    +
    +
    +

    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. +

    +
    +
    + + +
    +
    + +
    +
    + +
    + +
    +
    + +
    + + +
    +
    + +
    + + +
    + +
    + + + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    IDStudyUnit in RunModule DescriptionUser codeEnteredStayedLeftExpiresResultDelete
    () 0) echo ""; ?> + + 0) echo ""; ?> + + + + + + +
    + + +
    +
    + +
    +
    + +
    +
    + +
    diff --git a/templates/admin/advanced/user_management.php b/templates/admin/advanced/user_management.php new file mode 100644 index 000000000..566d9e31e --- /dev/null +++ b/templates/admin/advanced/user_management.php @@ -0,0 +1,147 @@ + + +
    + +
    +

    User Management Superadmin

    +
    + + +
    +
    +
    +
    +
    +

    Formr Users

    +
    + +
    +
    +
    SEARCH
    + +
    +
    + +
    +
    +
    + + rowCount()): ?> + + + + + + + + + + + + fetch(PDO::FETCH_ASSOC)): ?> + + + + + + + + + +
    EmailCreatedModifiedAdminActions
    + + ' : ' '; ?> + +
    + + + + + + + +
    +
    + + +
    + + + + +
    +
    + +
    +
    + +
    +
    + +
    + + + + + + \ No newline at end of file diff --git a/application/View/admin/footer.php b/templates/admin/footer.php similarity index 68% rename from application/View/admin/footer.php rename to templates/admin/footer.php index 0e94ad164..65a5e16d7 100644 --- a/application/View/admin/footer.php +++ b/templates/admin/footer.php @@ -1,9 +1,10 @@ diff --git a/templates/admin/header.php b/templates/admin/header.php new file mode 100755 index 000000000..7706d4784 --- /dev/null +++ b/templates/admin/header.php @@ -0,0 +1,110 @@ + + + + + + formr admin + + + $files) { + print_stylesheets($files, $id); + } + ?> + + $files) { + print_scripts($files, $id); + } + ?> + + getRuns('id DESC', 5); + $studies = $user->getStudies('id DESC', 5); + } + ?> + + + +
    + +
    + +
    +
    diff --git a/templates/admin/home.php b/templates/admin/home.php new file mode 100755 index 000000000..4f859bb89 --- /dev/null +++ b/templates/admin/home.php @@ -0,0 +1,172 @@ + + +
    +
    +

    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
    # + + ... + +
    +
    + +
    + +
    + +
    +
    +
    + + +
    + + \ No newline at end of file diff --git a/templates/admin/mail/edit.php b/templates/admin/mail/edit.php new file mode 100644 index 000000000..756178d28 --- /dev/null +++ b/templates/admin/mail/edit.php @@ -0,0 +1,81 @@ + +
    +

    edit email account

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

    E-Mail Accounts

    +
    + + +
    +
    + +
    + +
    +
    +

    Current Accounts

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

    +
    +
    +
    + +
    + +
    + +
    +
    +
    + +
    + +
    +
    +
    + +
    + +
    +
    +
    + +
    + +
    +
    +
    + +
    +
    + +
    +
    +
    + + +
    + +
    + +
    +
    +
    + +
    + +
    +
    +
    +
    + +
    +
    + +
    +
    +
    +
    + +
    + +
    +
    + +
    + +
    + +
    +
    +
    + +
    + +
    + + diff --git a/templates/admin/misc/osf.php b/templates/admin/misc/osf.php new file mode 100755 index 000000000..c509c770e --- /dev/null +++ b/templates/admin/misc/osf.php @@ -0,0 +1,82 @@ + + + +
    + +
    +

    Open Science Framework «» FORMR actions

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

    + Create an formr project (run) +

    +
    +
    + +
    + +
    +
    +

    + Create an OSF project +

    +
    + + +
    +
    +
    +
    +
    +
    + +
    + + + \ No newline at end of file diff --git a/templates/admin/run/add_run.php b/templates/admin/run/add_run.php new file mode 100644 index 000000000..564acce80 --- /dev/null +++ b/templates/admin/run/add_run.php @@ -0,0 +1,54 @@ + + + +
    + +
    +

    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.
    • +
    +
    +
    + +
    +
    +
    + + + +
    +
    +

     

    + more help on creating runs +
    + +
    +
    + +
    + + + + diff --git a/templates/admin/run/create_new_named_session.php b/templates/admin/run/create_new_named_session.php new file mode 100644 index 000000000..223f8d859 --- /dev/null +++ b/templates/admin/run/create_new_named_session.php @@ -0,0 +1,53 @@ + + +
    + +
    +

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

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

    Create Named Session

    +
    +
    +
    + + + +
    +
    +
    + + +
    +
    +
    +
    + + + +
    + +
    +
    +
    + +
    +
    + +
    + + \ No newline at end of file diff --git a/templates/admin/run/cron_log.php b/templates/admin/run/cron_log.php new file mode 100755 index 000000000..02f2de9ae --- /dev/null +++ b/templates/admin/run/cron_log.php @@ -0,0 +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. +

    + + + + + + $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; + + echo "\n"; + endforeach; + ?> + +
    {$field}
    + render("admin/run/" . $run->name . "/cron_log"); +} else { + echo "No cron jobs yet. Maybe you disabled them in the settings."; +} +?> +
    + + + + +
    + +
    +

    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); + } + ?> +
    +
    + + +
    +
    + +
    +
    + +
    +
    + +
    + + \ No newline at end of file diff --git a/templates/admin/run/delete_run.php b/templates/admin/run/delete_run.php new file mode 100644 index 000000000..b5c943dde --- /dev/null +++ b/templates/admin/run/delete_run.php @@ -0,0 +1,53 @@ + + +
    + +
    +

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

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

    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

    +
    +
    +
    +
    + + +
    +
    +
    +
    + + + +
    +
    + +
    +
    + +
    +
    + +
    + + \ No newline at end of file diff --git a/templates/admin/run/email_log.php b/templates/admin/run/email_log.php new file mode 100755 index 000000000..fea925d44 --- /dev/null +++ b/templates/admin/run/email_log.php @@ -0,0 +1,83 @@ + + +
    + +
    +

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

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

    Email Log

    +
    +
    + + + + + + + + + + + + + + + + + + + + + + + +
    FromToSubjectStatusDate and time (queued/attempted to send)


    at run position
    + $text"; + ?> + + q.
    + s. +
    + + +
    No E-mails yet
    + +
    +
    + +
    +
    + +
    +
    + +
    + + \ No newline at end of file diff --git a/templates/admin/run/empty_run.php b/templates/admin/run/empty_run.php new file mode 100644 index 000000000..318d7c26c --- /dev/null +++ b/templates/admin/run/empty_run.php @@ -0,0 +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.

    + +
    +
    +
    +
    + + +
    +
    +
    +
    + + + +
    +
    + +
    +
    + +
    +
    + +
    + + \ No newline at end of file diff --git a/templates/admin/run/index.php b/templates/admin/run/index.php new file mode 100644 index 000000000..0ccc31659 --- /dev/null +++ b/templates/admin/run/index.php @@ -0,0 +1,104 @@ + + +
    + +
    +

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

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

    + Edit Run + I am panicking :-( +

    +
    +
    + +
    + +
    + +
    Publicness:  
    + + + + + + + + + + + + + + + +
    + +
    +
    + +
    + +
    + +
    +
    +
    + $button): ?> + + + + +
    +
    click one of the symbols above to add a module
    +
    +
    +
    +
    +
    + +
    +
    +
    +
    + +
    +
    + +
    +
    + +
    +
    + +
    + + array())); +Template::loadChild('admin/footer'); +?> \ No newline at end of file diff --git a/templates/admin/run/list.php b/templates/admin/run/list.php new file mode 100644 index 000000000..b0ca3e88a --- /dev/null +++ b/templates/admin/run/list.php @@ -0,0 +1,81 @@ + + +
    + +
    +

    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'; + } + ?>
    +
    + +
    +
    + +
    +
    +
    +
    + +
    +
    + +
    +
    + +
    +
    + +
    + + array())); +Template::loadChild('admin/footer'); +?> \ No newline at end of file diff --git a/templates/admin/run/menu.php b/templates/admin/run/menu.php new file mode 100644 index 000000000..4ff6e2e2c --- /dev/null +++ b/templates/admin/run/menu.php @@ -0,0 +1,92 @@ +
    +
    +

    Configuration

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

    Testing & Management

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

    Danger Zone

    +
    + +
    +
    + + +
    + + Add Run diff --git a/templates/admin/run/monkey_bar.php b/templates/admin/run/monkey_bar.php new file mode 100644 index 000000000..d26f50add --- /dev/null +++ b/templates/admin/run/monkey_bar.php @@ -0,0 +1,96 @@ +
    +
    +
    + +
    + + + + + + + + + + "> + + + + + + + + + + + + + + + +
    +
    + + + + + +
    +
    diff --git a/templates/admin/run/overview.php b/templates/admin/run/overview.php new file mode 100644 index 000000000..75f9df95a --- /dev/null +++ b/templates/admin/run/overview.php @@ -0,0 +1,50 @@ + + +
    + +
    +

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

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

    Run Overview

    +
    +
    + + + +
    +

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

    + getParsedBody(); ?> +
    + +

    Add an overview script

    + + +
    + +
    +
    +
    + +
    +
    + +
    + + \ No newline at end of file diff --git a/templates/admin/run/random_groups.php b/templates/admin/run/random_groups.php new file mode 100644 index 000000000..aefae75f1 --- /dev/null +++ b/templates/admin/run/random_groups.php @@ -0,0 +1,116 @@ + + +
    + +
    +

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

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

    Randomization Results

    +
    + Export +
    +
    +
    + + + + + + + + + + + + + + + + + + + + + + +
    Unit in RunSessionGroupCreated
    '>()
    + + +
    No users to randomize
    + +
    +
    + +
    +
    + +
    +
    + +
    + + + + \ No newline at end of file diff --git a/templates/admin/run/rename_run.php b/templates/admin/run/rename_run.php new file mode 100644 index 000000000..9b1b236ef --- /dev/null +++ b/templates/admin/run/rename_run.php @@ -0,0 +1,57 @@ + + +
    + +
    +

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

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

    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.
    • +
    +
    +
    +
    +
    + + +
    +
    +
    +
    + + + +
    +
    + +
    +
    + +
    +
    + +
    + + \ No newline at end of file diff --git a/templates/admin/run/run_import_dialog.php b/templates/admin/run/run_import_dialog.php new file mode 100644 index 000000000..c85e172a2 --- /dev/null +++ b/templates/admin/run/run_import_dialog.php @@ -0,0 +1,25 @@ +
    + +
    + +
    + + +
    +
    + +
    +
    + +
    + Select a json file + +
    +
    +
    +
    diff --git a/application/View/admin/run/run_modals.php b/templates/admin/run/run_modals.php similarity index 100% rename from application/View/admin/run/run_modals.php rename to templates/admin/run/run_modals.php diff --git a/templates/admin/run/sessions_queue.php b/templates/admin/run/sessions_queue.php new file mode 100644 index 000000000..7e3783614 --- /dev/null +++ b/templates/admin/run/sessions_queue.php @@ -0,0 +1,72 @@ + + +
    + +
    +

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

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

    Sessions in Queue

    +
    +
    + +

    This shows the list of sessions in your run waiting to be processed.

    + +
    + + + + + + + + + + + + fetch(PDO::FETCH_ASSOC)): ?> + + + + + + + + + +
    SessionUnit (position)Added OnExpiresTo Execute
    + + + () + () + YES' : 'NO'; ?> +
    +
    + +
    +
    + +
    +
    + +
    +
    + +
    + + \ No newline at end of file diff --git a/templates/admin/run/settings.php b/templates/admin/run/settings.php new file mode 100644 index 000000000..42f69707f --- /dev/null +++ b/templates/admin/run/settings.php @@ -0,0 +1,315 @@ + + +
    + +
    +

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

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

    Settings

    +
    + +
    + + +
    + + +
    + +
    +
    + +
    +
    + +
    + + array())); +Template::loadChild('admin/footer'); +?> \ No newline at end of file diff --git a/templates/admin/run/units/email.php b/templates/admin/run/units/email.php new file mode 100644 index 000000000..aefd1821e --- /dev/null +++ b/templates/admin/run/units/email.php @@ -0,0 +1,50 @@ + + + +
    + + +
    +

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

    {{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/templates/admin/run/units/endpage.php b/templates/admin/run/units/endpage.php new file mode 100644 index 000000000..1ddcad83f --- /dev/null +++ b/templates/admin/run/units/endpage.php @@ -0,0 +1,12 @@ + + +

    + +

    + +

    + Save + Test +

    \ No newline at end of file diff --git a/templates/admin/run/units/external.php b/templates/admin/run/units/external.php new file mode 100644 index 000000000..d8f79914b --- /dev/null +++ b/templates/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/templates/admin/run/units/pause.php b/templates/admin/run/units/pause.php new file mode 100644 index 000000000..9a215d00a --- /dev/null +++ b/templates/admin/run/units/pause.php @@ -0,0 +1,51 @@ + + +

    + +   and + +

    +

    + +   and + +

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

    + +

    + + + +

    +

    +

    + + +
    +

    + Save + Test +

    diff --git a/templates/admin/run/units/shuffle.php b/templates/admin/run/units/shuffle.php new file mode 100644 index 000000000..310a9879a --- /dev/null +++ b/templates/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/templates/admin/run/units/skipbackward.php b/templates/admin/run/units/skipbackward.php new file mode 100644 index 000000000..65c19a314 --- /dev/null +++ b/templates/admin/run/units/skipbackward.php @@ -0,0 +1,18 @@ + + +

    + +

    +
    + + +
    + +

    + Save + Test +

    \ No newline at end of file diff --git a/templates/admin/run/units/skipforward.php b/templates/admin/run/units/skipforward.php new file mode 100644 index 000000000..dcc88da61 --- /dev/null +++ b/templates/admin/run/units/skipforward.php @@ -0,0 +1,36 @@ + + +
    +
    + + + +

    + + + How should participant skip? + Skip   + + +   else continue   + + + +
    +
    +
    +
    + Save + Test +
    +

     

    \ No newline at end of file diff --git a/templates/admin/run/units/survey.php b/templates/admin/run/units/survey.php new file mode 100644 index 000000000..f73efad8e --- /dev/null +++ b/templates/admin/run/units/survey.php @@ -0,0 +1,46 @@ + + + + +
    + + + id): ?> +

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

    +

    + View items + Upload items +

    +
    +

    + Save + Test +

    + +

    +

    + Save +
    +

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

    +
    +
    +

    + displayUnitSessionsCount() ?>
    +
    +
    +
    + + + + + +
    +
    +
    diff --git a/templates/admin/run/units/wait.php b/templates/admin/run/units/wait.php new file mode 100644 index 000000000..d998f1be6 --- /dev/null +++ b/templates/admin/run/units/wait.php @@ -0,0 +1,2 @@ + + +
    + +
    +

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

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

    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.
    • +
    +
    + +

    Files to upload:

    +
    +
    + +
    + + +
    + +
    +

    Files uploaded in this run

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

     

    +
    + + +
    + +
    +
    + +
    +
    + +
    + + \ No newline at end of file diff --git a/templates/admin/run/user_detail.php b/templates/admin/run/user_detail.php new file mode 100755 index 000000000..8ec15f43c --- /dev/null +++ b/templates/admin/run/user_detail.php @@ -0,0 +1,145 @@ + + +
    + +
    +

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

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

    + 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
    + +
    + + +
    +
    + +
    + + +
    + +
    + + + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    IDUnit in RunModule DescriptionUser codeEnteredStayedLeftExpiresResultDelete
    () 0) echo ""; ?> + + 0) echo ""; ?> + + + + + + +
    + + + +
    +
    + +
    +
    + +
    +
    + +
    + !empty($reminders) ? $reminders : array())); ?> + diff --git a/templates/admin/run/user_overview.php b/templates/admin/run/user_overview.php new file mode 100755 index 000000000..3ea837f2e --- /dev/null +++ b/templates/admin/run/user_overview.php @@ -0,0 +1,221 @@ + + +
    + +
    +

    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 positionDescriptionUser codeCreatedLast AccessLast ResultAction
    + + +
    + USI +
    + + + user_code == $user['session']): ?> + + + + + + + + + + + + + expires in + + +
    + + + + + ' + 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::loadChild('admin/footer'); +?> diff --git a/templates/admin/survey/add_survey.php b/templates/admin/survey/add_survey.php new file mode 100755 index 000000000..3f35ea2e4 --- /dev/null +++ b/templates/admin/survey/add_survey.php @@ -0,0 +1,93 @@ + + + +
    + +
    +

    Surveys Add New

    +
    + + +
    + +
    +

    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

    +
    +
    +
    + +
    + + + 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 +
    +
    + + + +
    +
    +
    +
    +
    + +
    + + + + diff --git a/templates/admin/survey/delete_results.php b/templates/admin/survey/delete_results.php new file mode 100755 index 000000000..5308bb984 --- /dev/null +++ b/templates/admin/survey/delete_results.php @@ -0,0 +1,59 @@ + + +
    + +
    +

    name ?> Survey ID: id ?>

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

    Delete Results complete, begun

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

    Warning!

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

    Type survey name to confirm it's deletion

    +
    +
    +
    + + +
    +
    +
    +
    + + + + +
    + +
    +
    +
    + + + + + diff --git a/templates/admin/survey/delete_study.php b/templates/admin/survey/delete_study.php new file mode 100755 index 000000000..4fc7c537c --- /dev/null +++ b/templates/admin/survey/delete_study.php @@ -0,0 +1,53 @@ + + +
    + +
    +

    name ?> Survey ID: id ?>

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

    Delete Survey with result rows

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

    Type survey name to confirm it's deletion

    +
    +
    +
    + + +
    +
    +
    +
    + + + + +
    + +
    +
    +
    + + + + + diff --git a/templates/admin/survey/index.php b/templates/admin/survey/index.php new file mode 100755 index 000000000..b0d70fe71 --- /dev/null +++ b/templates/admin/survey/index.php @@ -0,0 +1,197 @@ + + +
    + +
    +

    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. + +
    + +
    +
    + + +
    + +
    + + + +
    +
    +
    +
    +
    + +
    + +
    + + + + +
    + +
    +

    Surveys

    +
    + + +
    +
    +
    +
    +
    +

    Survey Listing

    + +
    +
    + +
    + + + + + + + + + + + + + + + + + + + + + +
    # IDNameCreatedModifiedGoogle Sheet
    # + + ... + +
    +
    + +
    +
    + +
    +
    + + +
    +
    + +
    +
    + +
    + + array())); +Template::loadChild('admin/footer'); +?> \ No newline at end of file diff --git a/templates/admin/survey/menu.php b/templates/admin/survey/menu.php new file mode 100644 index 000000000..c4729075f --- /dev/null +++ b/templates/admin/survey/menu.php @@ -0,0 +1,84 @@ +
    +
    +

    Configuration

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

    Testing & Management

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

    Danger Zone

    +
    + +
    +
    + + +
    + +getResultCount(); +} +if (trim((string)$study->google_file_id) && (int) $resultCount['real_users'] === 0): + $google_link = google_get_sheet_link($study->google_file_id); + ?> +
    + + + + +
    + +
    + Add Survey diff --git a/templates/admin/survey/rename_study.php b/templates/admin/survey/rename_study.php new file mode 100755 index 000000000..879cfa1ed --- /dev/null +++ b/templates/admin/survey/rename_study.php @@ -0,0 +1,52 @@ + + +
    + +
    +

    name ?> Survey ID: id ?>

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

    Rename Survey

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

    Choose a new name for your study

    +
    +
    +
    + + +
    +
    +
    +
    + + + +
    + +
    +
    +
    + + + + + diff --git a/templates/admin/survey/show_item_table.php b/templates/admin/survey/show_item_table.php new file mode 100755 index 000000000..bc90af078 --- /dev/null +++ b/templates/admin/survey/show_item_table.php @@ -0,0 +1,132 @@ + + +
    + +
    +

    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/templates/admin/survey/show_item_table_table.php b/templates/admin/survey/show_item_table_table.php new file mode 100644 index 000000000..61d3da367 --- /dev/null +++ b/templates/admin/survey/show_item_table_table.php @@ -0,0 +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' AND $cell === null) { + $cell = nl2br($row->label); + } elseif (($field == 'value' || $field == 'showif') && $cell != '') { + $cell = "
    $cell
    "; + } + echo $cell; + echo '
    +
    \ No newline at end of file diff --git a/templates/admin/survey/show_itemdisplay.php b/templates/admin/survey/show_itemdisplay.php new file mode 100755 index 000000000..54b294b42 --- /dev/null +++ b/templates/admin/survey/show_itemdisplay.php @@ -0,0 +1,173 @@ + + +
    + +
    +

    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((string)$row['created'])) . ''; + $row['shown'] = '' . timetostr(strtotime((string)$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((string)$row['saved'])) . ''; + $row['answered'] = '' . timetostr(strtotime((string)$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
    + + +
    +
    + +
    +
    +
    + +
    + +
    + + + + + + + + + + diff --git a/templates/admin/survey/show_results.php b/templates/admin/survey/show_results.php new file mode 100755 index 000000000..e6c44cedd --- /dev/null +++ b/templates/admin/survey/show_results.php @@ -0,0 +1,162 @@ + + +
    + +
    +

    name ?> Survey ID: id ?>

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

    Survey Results complete, begun, testers

    +
    + hide_results): ?> + Export + Detailed Results + +
    +
    + + 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((string)$row['created'])) . ''; + $row['ended'] = '' . timetostr(strtotime((string)$row['ended'])) . ''; + $row['modified'] = '' . timetostr(strtotime((string)$row['modified'])) . ''; + $row['expired'] = '' . timetostr(strtotime((string)$row['expired'])) . ''; + endif; + echo ''; + foreach ($row as $cell) { + echo ''; + } + echo ''; + } + echo ''; + ?> +
    ' . $field . '
    ' . $cell . '
    + +
    +
    + +
    +
    +
    + +
    + +
    + + + + + + + + + + diff --git a/templates/admin/survey/upload_items.php b/templates/admin/survey/upload_items.php new file mode 100755 index 000000000..81118fefd --- /dev/null +++ b/templates/admin/survey/upload_items.php @@ -0,0 +1,127 @@ +getResultCount(); +?> + +
    + +
    +

    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. +
      • +
      +
    • +
    +
    + +

    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? +
    + +

    + Or use + this + + a + Googlesheet +

    +
    + + + Make sure this sheet is accessible by anyone with the link +
    + + 0): ?> +
    +

    Delete Results Confirmation

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

     

    + more help on creating survey sheets + +
    +
    +
    + +
    + +
    + + + \ No newline at end of file diff --git a/templates/email/email-queue-problem.ftpl b/templates/email/email-queue-problem.ftpl new file mode 100644 index 000000000..63cf4fc3a --- /dev/null +++ b/templates/email/email-queue-problem.ftpl @@ -0,0 +1,16 @@ +Dear user,

    + +formr was unable to send the following email in your study +

    +----------------------------------------------------------
    +SUBJECT: %{subject}
    +TO: %{recipient}
    +----------------------------------------------------------
    +

    + +%{message} + + +

    +Best regards,
    +formr robots diff --git a/application/View/email/forgot-password.txt b/templates/email/forgot-password.ftpl similarity index 100% rename from application/View/email/forgot-password.txt rename to templates/email/forgot-password.ftpl diff --git a/templates/email/reg-account.ftpl b/templates/email/reg-account.ftpl new file mode 100644 index 000000000..e0799fb28 --- /dev/null +++ b/templates/email/reg-account.ftpl @@ -0,0 +1,15 @@ +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.Hi there! + +I'd like to create studies using formr. I'm totally not a dolphin. + +I confirm that I have already registered with the email address from which I'm sending this request. + +I'm affiliated with the University of Atlantis. + +This is what I want to use formr for: +[x] find out more about land mammals +[x] plan cetacean world domination +[ ] excessively use your server resources + +Squee'ek uh'k kk'kkkk squeek eee'eek! +Not a Dolphin diff --git a/templates/email/test-account.ftpl b/templates/email/test-account.ftpl new file mode 100644 index 000000000..abd4421d7 --- /dev/null +++ b/templates/email/test-account.ftpl @@ -0,0 +1,6 @@ +Dear User, + +Your account has been successfully set up on %{site_url} + +Best regards, +formr robots \ No newline at end of file diff --git a/application/View/email/verify-email.txt b/templates/email/verify-email.ftpl similarity index 100% rename from application/View/email/verify-email.txt rename to templates/email/verify-email.ftpl diff --git a/templates/public/about.php b/templates/public/about.php new file mode 100644 index 000000000..aa8ac2817 --- /dev/null +++ b/templates/public/about.php @@ -0,0 +1,168 @@ + + +
    +
    +
    +
    +

    about formr

    +

    + The open source software was created by Ruben C. Arslan and is being maintained and developed jointly with Cyril S. Tata. +

    +
    +
    +
    +
    +
    + Cyril Tata +
    +

    Cyril

    +
    +
    +
    +
    + Ruben Arslan +
    +

    Ruben

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

    Credit

    +

     

    +
    +
    +
    +
    +
    +

    Citation

    +
    +

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

    +
    + Arslan, R. C., Walther, M. P., & Tata, C. S. (2020). formr: A study framework allowing for automated feedback generation and complex longitudinal experience-sampling studies using R. Behavior Research Methods, 52, 376–387. https://doi.org/10.3758/s13428-019-01236-y +
    +
    +

    + 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. +

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

    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/templates/public/alerts.php b/templates/public/alerts.php new file mode 100644 index 000000000..ec1a68feb --- /dev/null +++ b/templates/public/alerts.php @@ -0,0 +1,10 @@ +renderAlerts(); +} +if (!empty($alerts)): + ?> +
    + +
    + diff --git a/templates/public/disclaimer.php b/templates/public/disclaimer.php new file mode 100644 index 000000000..37eefbb83 --- /dev/null +++ b/templates/public/disclaimer.php @@ -0,0 +1,21 @@ +
    +
    +
    +
    +

    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/templates/public/documentation.php b/templates/public/documentation.php new file mode 100644 index 000000000..c7daff994 --- /dev/null +++ b/templates/public/documentation.php @@ -0,0 +1,73 @@ + + +
    +
    +
    +
    +

    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/templates/public/documentation/api.php b/templates/public/documentation/api.php new file mode 100644 index 000000000..c14cef347 --- /dev/null +++ b/templates/public/documentation/api.php @@ -0,0 +1,166 @@ +

    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) +

    +

    + 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 +

    + + +

    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. +

    + +

    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. +

    + +

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

    + +
    +
    +POST /oauth/access_token?
    +     client_id={client-id}
    +    &client_secret={client-secret}
    +    &grant_type=client_credentials
    +
    +
    + +

    + 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 +

    + +
    +
    +{
    +	"access_token":"XXXXXX3635f0dc13d563504b4d",
    +	"expires_in":3600,
    +	"token_type":"Bearer",
    +	"scope":null
    +}
    +
    +
    + +The attribute expires_in indicates the number of seconds for which is token is valid from its time of creation. If there is an error for example if the +client details are not correct, then an error object is returned containing an error_description and sometimes an error_uri where you can read more about the +generated error. An example of an error object is + +
    +
    +{
    +	"error":"invalid_client",
    +	"error_description":"The client credentials are invalid"
    +}
    +
    +
    + + +

    Making Resource Requests using generated access token

    + +With the generated access token, you are able to make requests to the resource endpoints of the formr API. For now only the results resource endpoint has +been implemented + +
    Getting study results over the API
    + +
    REQUEST
    +To obtain the results of a particular set of sessions in a particular run, send a GET HTTP request to the get endpoint along side the access_token obtained above +together with the necessary parameters as shown below: + +
    +
    +GET /get/results?
    +     access_token={access-token}
    +    &run[name]={name of the run as it appears on formr}
    +    &run[sessions]={comma separated list of session codes OR leave empty to get all sessions}
    +    &surveys[survey_name1]={comma separated list items to get from survey_name1 OR leave empty to get all items}
    +    &surveys[survey_name2]={comma separated list items to get from survey_name2 OR leave empty to get all items}
    +
    +
    + +

    + 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: +

    + +
    +
    +{
    +	"survey_1": [{
    +		"session": "sdaswew434df",
    +		"survey_1_item_1": "answer1",
    +		"survey_1_item_2": "answer2",
    +		"survey_1_item_2": "answer3",
    +	},
    +	{
    +		"session": "fdgdfg4323",
    +		"survey_1_item_1": "answer4",
    +		"survey_1_item_2": "answer5",
    +		"survey_1_item_2": "answer6",
    +	}],
    +	"survey_2": [........]
    +}
    +
    +
    + +

    Using the formr API in R

    +
    
    +# load the httr package
    +library(httr)
    +
    +# Login using your client ID and client Secret to get an access token
    +login <- list( # define login credentials
    +  client_id = "bb472xxxxxxxxe1918",
    +  client_secret = "zEfgeyJ0eXXXXXEwYwNGZj",
    +  grant_type = "client_credentials"
    +)
    +request <- POST( # send POST request
    +  "https://api.formr.org/oauth/access_token",
    +  body = login, 
    +  encode = "form"
    +)
    +# parse response to get access token
    +# If there an error then the response object would contain the details of the error in response$error
    +response <- content(request)
    +access_token <- response$access_token
    +
    +#With a valid access token, call API to get the results of a particular study (run)
    +query <- list(
    +  access_token = access_token,
    +  "run[name]" = "/enter run name here/",
    +  "run[session]" = "/here you can specify a full session code or just a substring(but include the beginning /",
    +  "surveys[survey_1]" = "item_1, item_2, item_etc.", # comma separated list of items to get from the survey or leave empty string to get all items
    +  "surveys[survey_2]" = "item_3, item_4, item_etc."
    +)
    +request <- GET("https://api.formr.org/get/results", query=query)
    +results <- content(request)
    +
    +# With a valid response you can, for example, extract the results of a particular survey say 'survey_1':
    +survey_1 = results$survey_1
    +survey_1[, c("item_1","item_2")]
    +
    + + diff --git a/templates/public/documentation/empty_survey.xlsx b/templates/public/documentation/empty_survey.xlsx new file mode 100644 index 000000000..8675deabb Binary files /dev/null and b/templates/public/documentation/empty_survey.xlsx differ diff --git a/templates/public/documentation/features.php b/templates/public/documentation/features.php new file mode 100644 index 000000000..bde7e0f04 --- /dev/null +++ b/templates/public/documentation/features.php @@ -0,0 +1,153 @@ +

    Features


    + +

    + 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
    • +
    +

    + 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. +
    • +
    +

    + 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. +
    • + +
    +

    + 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) +
    • + +
    • + 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/templates/public/documentation/get_help.php b/templates/public/documentation/get_help.php new file mode 100644 index 000000000..d8b52194d --- /dev/null +++ b/templates/public/documentation/get_help.php @@ -0,0 +1,33 @@ +

    Help


    + +

    Where to get help

    +

    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/templates/public/documentation/getting_started.php b/templates/public/documentation/getting_started.php new file mode 100644 index 000000000..5cfc2b121 --- /dev/null +++ b/templates/public/documentation/getting_started.php @@ -0,0 +1,43 @@ +

    Getting Started


    + +

    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 . + 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 by enabling link sharing and using the form Import a Googlesheet. When importing a Googlesheet, + you will need to manually specify the name of your survey whereas if uploading a spreadsheet, the name of your survey is obtained from 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. +

    +

    + 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 created 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 +

    + + +

    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. +

    +

    + There is always help if you need assistance. +

    \ No newline at end of file diff --git a/templates/public/documentation/item_types.php b/templates/public/documentation/item_types.php new file mode 100644 index 000000000..020a6d67c --- /dev/null +++ b/templates/public/documentation/item_types.php @@ -0,0 +1,247 @@ +

    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. +
    +
    + + + +

    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. +
    + +
    diff --git a/templates/public/documentation/knitr_markdown.php b/templates/public/documentation/knitr_markdown.php new file mode 100644 index 000000000..5a945a6cf --- /dev/null +++ b/templates/public/documentation/knitr_markdown.php @@ -0,0 +1,80 @@ +

    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. +

    +
    + 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. +

    +
    +* list item 1
    +* list item 2
    +
    +

    + will turn into a nice bulleted list. +

    +
      +
    • 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. +

    +

    + *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. +

    +

    + 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 +
    +

    + 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}
      +library(formr)
      +# build scales automatically
      +big5 = formr_aggregate(results = big5)
      +# standardise
      +big5$extraversion = scale(big5$extraversion, center = 3.2, scale = 2.1)
      +
      +# plot
      +qplot_on_normal(big$extraversion, xlab = "Extraversion")
      +```
      +        
      yields
      + Graph of extraversion bell curve feedback +
    • +
    \ No newline at end of file diff --git a/templates/public/documentation/r_helpers.php b/templates/public/documentation/r_helpers.php new file mode 100644 index 000000000..0d3e14da3 --- /dev/null +++ b/templates/public/documentation/r_helpers.php @@ -0,0 +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. +

    +
    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); 
      +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); 
      +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.
    • +
    \ No newline at end of file diff --git a/templates/public/documentation/run_module_explanations.php b/templates/public/documentation/run_module_explanations.php new file mode 100644 index 000000000..97168714a --- /dev/null +++ b/templates/public/documentation/run_module_explanations.php @@ -0,0 +1,430 @@ +

    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). +

    +
    +
    + +
    +
    +

    + 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 +

    +
    +
    + +
    +
    +

    + 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. +

    +

    + This way, you can create filters, and parallel paths or branches in a study. Filters are useful to screen participants. You may need parallel paths in a study to randomise people to one experimental branch out of many, or to make sure a certain part of your study is only completed by those for whom it is relevant. +

    +

    + 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. +

    +
    +
    +
    +
    + +
    +
    +

    + Waiting Time are like Pauses, but instead of making the participant wait, we wait for the participant. +

    +

    + By waiting for the participant for a certain amount of time, we can make sure that people are reminded to participate in our diary study after one hour—but only if they need a reminder. We can also make sure that a part of a study is only accessible at certain times of day. +

    +

    + Example 1: reminder +

    +

    + Let's say your run contains +

    +
      +
    • Pos. 10. a pause (e.g. let's say we know when exchange students will arrive in their host country, and they cannot answer 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 Waiting Time for 7 days. If the user clicks the link to answer questions, the study jumps to position 50, the survey. If two weeks go by without a reaction, the study moves on to the next position, the reminder.
    • +
    • Pos. 40. This is our email reminder for the students who did not react after 7 days.
    • +
    • Pos. 50. the survey we want the exchange students to fill out. We set an access window of 7 weeks for this survey (in the survey settings), so we wait at most 7 weeks for students to fill the survey out.
    • +
    • Pos. 60. 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 (one week in the host country), we remind them.
    + How is this done? We set a waiting time for the participant of 7 days.
    + Now if he doesn't answer for one week, the run will automatically go on to 40, 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 seven weeks. This time, we set an expiry time in the survey settings to achieve this. Until seven weeks have passed he can do the survey. Once the seven weeks are over without him finishing the survey, the run moves on to the next position, 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/templates/public/documentation/sample_choices_sheet.php b/templates/public/documentation/sample_choices_sheet.php new file mode 100644 index 000000000..4c3eadd87 --- /dev/null +++ b/templates/public/documentation/sample_choices_sheet.php @@ -0,0 +1,75 @@ +

    Choices Spreadsheet


    + +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 diff --git a/templates/public/documentation/sample_survey_sheet.php b/templates/public/documentation/sample_survey_sheet.php new file mode 100644 index 000000000..f7340e540 --- /dev/null +++ b/templates/public/documentation/sample_survey_sheet.php @@ -0,0 +1,293 @@ +

    Survey Spreadsheet


    + +

    You can clone a Google spreadsheet to get started or start with an empty spread sheet.

    +

    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*. +
    • +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    + type + + name + + label + + optional + + showif +
    + text + + name + + Please enter your name + + * + +
    + number 1,130,1 + + age + + How old are you? + + +
    + 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. +
    +
    + +

    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). +
    + +
    + 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. +
    + +
    + 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. +
    +
    diff --git a/templates/public/error.php b/templates/public/error.php new file mode 100644 index 000000000..aff24d83d --- /dev/null +++ b/templates/public/error.php @@ -0,0 +1,32 @@ + + + + + <?php echo $title; ?> - formr.org + + + + +
    +

    +

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

    + +

    + +
    + + + + diff --git a/application/View/public/footer.php b/templates/public/footer.php similarity index 60% rename from application/View/public/footer.php rename to templates/public/footer.php index cb53efdba..a415d6df2 100755 --- a/application/View/public/footer.php +++ b/templates/public/footer.php @@ -1,3 +1,5 @@ + +