2020-03-30 13:48:14 +02:00
< ? php
/**
* @ copyright Copyright ( c ) 2020 Jonas Rittershofer < jotoeri @ users . noreply . github . com >
*
2020-04-29 11:42:39 +02:00
* @ author John Molakvoæ ( skjnldsv ) < skjnldsv @ protonmail . com >
2020-03-30 13:48:14 +02:00
* @ author Jonas Rittershofer < jotoeri @ users . noreply . github . com >
*
* @ license GNU AGPL version 3 or any later version
*
2020-04-29 11:42:39 +02:00
* This program is free software : you can redistribute it and / or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation , either version 3 of the
* License , or ( at your option ) any later version .
2020-03-30 13:48:14 +02:00
*
2020-04-29 11:42:39 +02:00
* This program is distributed in the hope that it will be useful ,
* but WITHOUT ANY WARRANTY ; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE . See the
* GNU Affero General Public License for more details .
2020-03-30 13:48:14 +02:00
*
2020-04-29 11:42:39 +02:00
* You should have received a copy of the GNU Affero General Public License
* along with this program . If not , see < http :// www . gnu . org / licenses />.
2020-03-30 13:48:14 +02:00
*
*/
namespace OCA\Forms\Migration ;
use Doctrine\DBAL\Types\Type ;
use OCP\DB\ISchemaWrapper ;
use OCP\DB\QueryBuilder\IQueryBuilder ;
use OCP\IConfig ;
use OCP\IDBConnection ;
use OCP\Migration\SimpleMigrationStep ;
use OCP\Migration\IOutput ;
2020-04-11 14:18:24 +02:00
use \DateTime ;
2020-03-30 13:48:14 +02:00
/**
* Installation class for the forms app .
* Initial db creation
*/
2020-04-10 15:50:39 +02:00
class Version010200Date20200323141300 extends SimpleMigrationStep {
2020-03-30 13:48:14 +02:00
/** @var IDBConnection */
protected $connection ;
/** @var IConfig */
protected $config ;
2020-04-10 15:50:39 +02:00
/** Map of questionTypes to change */
private $questionTypeMap = [
'radiogroup' => 'multiple_unique' ,
'checkbox' => 'multiple' ,
'text' => 'short' ,
'comment' => 'long' ,
'dropdown' => 'multiple_unique'
];
2020-03-30 13:48:14 +02:00
/**
* @ param IDBConnection $connection
* @ param IConfig $config
*/
public function __construct ( IDBConnection $connection , IConfig $config ) {
$this -> connection = $connection ;
$this -> config = $config ;
}
/**
* @ param IOutput $output
* @ param \Closure $schemaClosure The `\Closure` returns a `ISchemaWrapper`
* @ param array $options
* @ return null | ISchemaWrapper
* @ since 13.0 . 0
*/
public function changeSchema ( IOutput $output , \Closure $schemaClosure , array $options ) {
/** @var ISchemaWrapper $schema */
$schema = $schemaClosure ();
if ( ! $schema -> hasTable ( 'forms_v2_forms' )) {
$table = $schema -> createTable ( 'forms_v2_forms' );
$table -> addColumn ( 'id' , Type :: INTEGER , [
'autoincrement' => true ,
'notnull' => true ,
]);
$table -> addColumn ( 'hash' , Type :: STRING , [
'notnull' => true ,
'length' => 64 ,
]);
$table -> addColumn ( 'title' , Type :: STRING , [
'notnull' => true ,
'length' => 256 ,
]);
$table -> addColumn ( 'description' , Type :: STRING , [
'notnull' => false ,
'length' => 2048 ,
]);
$table -> addColumn ( 'owner_id' , Type :: STRING , [
'notnull' => true ,
'length' => 64 ,
]);
2020-04-09 18:21:59 +02:00
$table -> addColumn ( 'access_json' , Type :: JSON , [
2020-03-30 13:48:14 +02:00
'notnull' => false ,
]);
2020-04-11 14:18:24 +02:00
$table -> addColumn ( 'created' , Type :: INTEGER , [
2020-03-30 13:48:14 +02:00
'notnull' => false ,
2020-04-11 14:18:24 +02:00
'comment' => 'unix-timestamp' ,
2020-03-30 13:48:14 +02:00
]);
2020-04-20 13:51:48 +02:00
$table -> addColumn ( 'expires' , Type :: INTEGER , [
2020-03-30 13:48:14 +02:00
'notnull' => false ,
2020-04-10 17:35:40 +02:00
'default' => 0 ,
2020-04-11 14:18:24 +02:00
'comment' => 'unix-timestamp' ,
2020-03-30 13:48:14 +02:00
]);
$table -> addColumn ( 'is_anonymous' , Type :: BOOLEAN , [
'notnull' => true ,
'default' => 0 ,
]);
$table -> addColumn ( 'submit_once' , Type :: BOOLEAN , [
'notnull' => true ,
'default' => 0 ,
]);
$table -> setPrimaryKey ([ 'id' ]);
2020-04-13 14:42:44 +02:00
$table -> addUniqueIndex ([ 'hash' ], 'uniqueHash' );
2020-03-30 13:48:14 +02:00
}
if ( ! $schema -> hasTable ( 'forms_v2_questions' )) {
$table = $schema -> createTable ( 'forms_v2_questions' );
$table -> addColumn ( 'id' , Type :: INTEGER , [
'autoincrement' => true ,
'notnull' => true ,
]);
$table -> addColumn ( 'form_id' , Type :: INTEGER , [
'notnull' => true ,
]);
2020-04-09 12:40:04 +02:00
$table -> addColumn ( 'order' , Type :: INTEGER , [
'notnull' => true ,
'default' => 1 ,
]);
2020-03-30 13:48:14 +02:00
$table -> addColumn ( 'type' , Type :: STRING , [
'notnull' => true ,
'length' => 256 ,
]);
$table -> addColumn ( 'mandatory' , Type :: BOOLEAN , [
'notnull' => true ,
2020-04-10 17:35:40 +02:00
'default' => 0 ,
2020-03-30 13:48:14 +02:00
]);
$table -> addColumn ( 'text' , Type :: STRING , [
'notnull' => true ,
'length' => 2048 ,
]);
$table -> setPrimaryKey ([ 'id' ]);
}
if ( ! $schema -> hasTable ( 'forms_v2_options' )) {
$table = $schema -> createTable ( 'forms_v2_options' );
$table -> addColumn ( 'id' , Type :: INTEGER , [
'autoincrement' => true ,
'notnull' => true ,
]);
$table -> addColumn ( 'question_id' , Type :: INTEGER , [
'notnull' => true ,
]);
$table -> addColumn ( 'text' , Type :: STRING , [
'notnull' => true ,
'length' => 1024 ,
]);
$table -> setPrimaryKey ([ 'id' ]);
}
if ( ! $schema -> hasTable ( 'forms_v2_submissions' )) {
$table = $schema -> createTable ( 'forms_v2_submissions' );
$table -> addColumn ( 'id' , Type :: INTEGER , [
'autoincrement' => true ,
'notnull' => true ,
]);
$table -> addColumn ( 'form_id' , Type :: INTEGER , [
'notnull' => true ,
]);
$table -> addColumn ( 'user_id' , Type :: STRING , [
'notnull' => true ,
'length' => 64 ,
]);
2020-04-11 14:18:24 +02:00
$table -> addColumn ( 'timestamp' , Type :: INTEGER , [
2020-03-30 13:48:14 +02:00
'notnull' => false ,
2020-04-11 14:18:24 +02:00
'comment' => 'unix-timestamp' ,
2020-03-30 13:48:14 +02:00
]);
$table -> setPrimaryKey ([ 'id' ]);
}
if ( ! $schema -> hasTable ( 'forms_v2_answers' )) {
$table = $schema -> createTable ( 'forms_v2_answers' );
$table -> addColumn ( 'id' , Type :: INTEGER , [
'autoincrement' => true ,
'notnull' => true ,
]);
$table -> addColumn ( 'submission_id' , Type :: INTEGER , [
'notnull' => true ,
]);
$table -> addColumn ( 'question_id' , Type :: INTEGER , [
'notnull' => true ,
]);
$table -> addColumn ( 'text' , Type :: STRING , [
'notnull' => true ,
'length' => 2048 ,
]);
$table -> setPrimaryKey ([ 'id' ]);
}
return $schema ;
}
public function postSchemaChange ( IOutput $output , \Closure $schemaClosure , array $options ) {
/** @var ISchemaWrapper $schema */
$schema = $schemaClosure ();
// if Database exists.
2020-04-29 11:50:03 +02:00
if ( $schema -> hasTable ( 'forms_events' )) {
2020-03-30 13:48:14 +02:00
$id_mapping = [];
2020-04-09 12:40:04 +02:00
$id_mapping [ 'events' ] = []; // Maps oldevent-id => ['newId' => newevent-id, 'nextQuestionOrder' => integer]
$id_mapping [ 'questions' ] = []; // Maps oldquestion-id => ['newId' => newquestion-id]
$id_mapping [ 'currentSubmission' ] = 0 ;
2020-03-30 13:48:14 +02:00
//Fetch & Restore Events
$qb_fetch = $this -> connection -> getQueryBuilder ();
$qb_restore = $this -> connection -> getQueryBuilder ();
$qb_fetch -> select ( 'id' , 'hash' , 'title' , 'description' , 'owner' , 'created' , 'access' , 'expire' , 'is_anonymous' , 'unique' )
-> from ( 'forms_events' );
$cursor = $qb_fetch -> execute ();
2020-04-29 11:50:03 +02:00
while ( $event = $cursor -> fetch ()) {
2020-04-09 18:21:59 +02:00
$newAccessJSON = $this -> convertAccessList ( $event [ 'access' ]);
2020-03-30 13:48:14 +02:00
$qb_restore -> insert ( 'forms_v2_forms' )
-> values ([
'hash' => $qb_restore -> createNamedParameter ( $event [ 'hash' ], IQueryBuilder :: PARAM_STR ),
'title' => $qb_restore -> createNamedParameter ( $event [ 'title' ], IQueryBuilder :: PARAM_STR ),
'description' => $qb_restore -> createNamedParameter ( $event [ 'description' ], IQueryBuilder :: PARAM_STR ),
'owner_id' => $qb_restore -> createNamedParameter ( $event [ 'owner' ], IQueryBuilder :: PARAM_STR ),
2020-04-09 18:21:59 +02:00
'access_json' => $qb_restore -> createNamedParameter ( $newAccessJSON , IQueryBuilder :: PARAM_STR ),
2020-04-11 14:18:24 +02:00
'created' => $qb_restore -> createNamedParameter ( $this -> convertDateTime ( $event [ 'created' ]), IQueryBuilder :: PARAM_INT ),
2020-04-20 13:51:48 +02:00
'expires' => $qb_restore -> createNamedParameter ( $this -> convertDateTime ( $event [ 'expire' ]), IQueryBuilder :: PARAM_INT ),
2020-03-30 13:48:14 +02:00
'is_anonymous' => $qb_restore -> createNamedParameter ( $event [ 'is_anonymous' ], IQueryBuilder :: PARAM_BOOL ),
'submit_once' => $qb_restore -> createNamedParameter ( $event [ 'unique' ], IQueryBuilder :: PARAM_BOOL )
]);
$qb_restore -> execute ();
2020-04-09 12:40:04 +02:00
$id_mapping [ 'events' ][ $event [ 'id' ]] = [
'newId' => $qb_restore -> getLastInsertId (), //Store new form-id to connect questions to new form.
'nextQuestionOrder' => 1 //Prepare for sorting questions
];
2020-03-30 13:48:14 +02:00
}
$cursor -> closeCursor ();
//Fetch & restore Questions
$qb_fetch = $this -> connection -> getQueryBuilder ();
$qb_restore = $this -> connection -> getQueryBuilder ();
$qb_fetch -> select ( 'id' , 'form_id' , 'form_question_type' , 'form_question_text' )
-> from ( 'forms_questions' );
$cursor = $qb_fetch -> execute ();
2020-04-29 11:50:03 +02:00
while ( $question = $cursor -> fetch ()) {
2020-03-30 13:48:14 +02:00
//In case the old Question would have been longer than current possible length, create a warning and shorten text to avoid Error on upgrade.
2020-04-29 11:50:03 +02:00
if ( strlen ( $question [ 'form_question_text' ]) > 2048 ) {
2020-03-30 13:48:14 +02:00
$output -> warning ( " Question-text is too long for new Database: ' " . $question [ 'form_question_text' ] . " ' " );
$question [ 'form_question_text' ] = substr ( $question [ 'form_question_text' ], 0 , 2048 );
}
$qb_restore -> insert ( 'forms_v2_questions' )
-> values ([
2020-04-09 12:40:04 +02:00
'form_id' => $qb_restore -> createNamedParameter ( $id_mapping [ 'events' ][ $question [ 'form_id' ]][ 'newId' ], IQueryBuilder :: PARAM_INT ),
'order' => $qb_restore -> createNamedParameter ( $id_mapping [ 'events' ][ $question [ 'form_id' ]][ 'nextQuestionOrder' ] ++ , IQueryBuilder :: PARAM_INT ),
2020-04-10 15:50:39 +02:00
'type' => $qb_restore -> createNamedParameter ( $this -> questionTypeMap [ $question [ 'form_question_type' ]], IQueryBuilder :: PARAM_STR ),
2020-03-30 13:48:14 +02:00
'text' => $qb_restore -> createNamedParameter ( $question [ 'form_question_text' ], IQueryBuilder :: PARAM_STR )
]);
$qb_restore -> execute ();
2020-04-09 12:40:04 +02:00
$id_mapping [ 'questions' ][ $question [ 'id' ]][ 'newId' ] = $qb_restore -> getLastInsertId (); //Store new question-id to connect options to new question.
2020-03-30 13:48:14 +02:00
}
$cursor -> closeCursor ();
//Fetch all Answers and restore to Options
$qb_fetch = $this -> connection -> getQueryBuilder ();
$qb_restore = $this -> connection -> getQueryBuilder ();
$qb_fetch -> select ( 'question_id' , 'text' )
-> from ( 'forms_answers' );
$cursor = $qb_fetch -> execute ();
2020-04-29 11:50:03 +02:00
while ( $answer = $cursor -> fetch ()) {
2020-03-30 13:48:14 +02:00
//In case the old Answer would have been longer than current possible length, create a warning and shorten text to avoid Error on upgrade.
2020-04-29 11:50:03 +02:00
if ( strlen ( $answer [ 'text' ]) > 1024 ) {
2020-03-30 13:48:14 +02:00
$output -> warning ( " Option-text is too long for new Database: ' " . $answer [ 'text' ] . " ' " );
$answer [ 'text' ] = substr ( $answer [ 'text' ], 0 , 1024 );
}
$qb_restore -> insert ( 'forms_v2_options' )
-> values ([
2020-04-09 12:40:04 +02:00
'question_id' => $qb_restore -> createNamedParameter ( $id_mapping [ 'questions' ][ $answer [ 'question_id' ]][ 'newId' ], IQueryBuilder :: PARAM_INT ),
2020-03-30 13:48:14 +02:00
'text' => $qb_restore -> createNamedParameter ( $answer [ 'text' ], IQueryBuilder :: PARAM_STR )
]);
$qb_restore -> execute ();
}
$cursor -> closeCursor ();
/* Fetch old id_structure of event - ids and question - ids
* This is necessary to restore the $oldQuestionId , as the vote_option_ids do not use the true question_ids
*/
$event_structure = [];
$qb_fetch = $this -> connection -> getQueryBuilder ();
$qb_fetch -> select ( 'id' )
-> from ( 'forms_events' );
$cursor = $qb_fetch -> execute ();
2020-04-29 11:50:03 +02:00
while ( $tmp = $cursor -> fetch ()) {
2020-03-30 13:48:14 +02:00
$event_structure [ $tmp [ 'id' ]] = $tmp ;
}
$cursor -> closeCursor ();
foreach ( $event_structure as $eventkey => $event ) {
$event_structure [ $eventkey ][ 'questions' ] = [];
$qb_fetch = $this -> connection -> getQueryBuilder ();
$qb_fetch -> select ( 'id' , 'form_question_text' )
-> from ( 'forms_questions' )
-> where ( $qb_fetch -> expr () -> eq ( 'form_id' , $qb_fetch -> createNamedParameter ( $event [ 'id' ], IQueryBuilder :: PARAM_INT )));
$cursor = $qb_fetch -> execute ();
2020-04-29 11:50:03 +02:00
while ( $tmp = $cursor -> fetch ()) {
2020-03-30 13:48:14 +02:00
$event_structure [ $event [ 'id' ]][ 'questions' ][] = $tmp ;
}
$cursor -> closeCursor ();
}
//Fetch Votes and restore to Submissions & Answers
$qb_fetch = $this -> connection -> getQueryBuilder ();
$qb_restore = $this -> connection -> getQueryBuilder ();
//initialize $last_vote
$last_vote = [];
$last_vote [ 'form_id' ] = 0 ;
$last_vote [ 'user_id' ] = '' ;
$last_vote [ 'vote_option_id' ] = 0 ;
$qb_fetch -> select ( 'id' , 'form_id' , 'user_id' , 'vote_option_id' , 'vote_option_text' , 'vote_answer' )
-> from ( 'forms_votes' );
$cursor = $qb_fetch -> execute ();
2020-04-29 11:50:03 +02:00
while ( $vote = $cursor -> fetch ()) {
2020-03-30 13:48:14 +02:00
//If the form changed, if the user changed or if vote_option_id became smaller than last one, then a new submission is interpreted.
2020-04-29 11:50:03 +02:00
if (( $vote [ 'form_id' ] != $last_vote [ 'form_id' ]) || ( $vote [ 'user_id' ] != $last_vote [ 'user_id' ]) || ( $vote [ 'vote_option_id' ] < $last_vote [ 'vote_option_id' ])) {
2020-03-30 13:48:14 +02:00
$qb_restore -> insert ( 'forms_v2_submissions' )
-> values ([
2020-04-09 12:40:04 +02:00
'form_id' => $qb_restore -> createNamedParameter ( $id_mapping [ 'events' ][ $vote [ 'form_id' ]][ 'newId' ], IQueryBuilder :: PARAM_INT ),
2020-03-30 13:48:14 +02:00
'user_id' => $qb_restore -> createNamedParameter ( $vote [ 'user_id' ], IQueryBuilder :: PARAM_STR ),
2020-04-11 14:18:24 +02:00
'timestamp' => $qb_restore -> createNamedParameter ( time (), IQueryBuilder :: PARAM_STR ) //Information not available. Just using Migration-Timestamp.
2020-03-30 13:48:14 +02:00
]);
$qb_restore -> execute ();
$id_mapping [ 'currentSubmission' ] = $qb_restore -> getLastInsertId (); //Store submission-id to connect answers to submission.
}
$last_vote = $vote ;
//In case the old Answer would have been longer than current possible length, create a warning and shorten text to avoid Error on upgrade.
2020-04-29 11:50:03 +02:00
if ( strlen ( $vote [ 'vote_answer' ]) > 2048 ) {
2020-03-30 13:48:14 +02:00
$output -> warning ( " Answer-text is too long for new Database: ' " . $vote [ 'vote_answer' ] . " ' " );
$vote [ 'vote_answer' ] = substr ( $vote [ 'vote_answer' ], 0 , 2048 );
}
/* Due to the unconventional storing fo vote_option_ids , the vote_option_id needs to get mapped onto old question - id and from there to new question - id .
* vote_option_ids count from 1 to x for the questions of a form . So the question at point [ $vote [ vote_option_id ] - 1 ] within the id - structure is the corresponding question .
*/
$oldQuestionId = $event_structure [ $vote [ 'form_id' ]][ 'questions' ][ $vote [ 'vote_option_id' ] - 1 ][ 'id' ];
//Just throw an Error, if aboves QuestionId-Mapping went wrong. Double-Checked by Question-Text.
if ( $event_structure [ $vote [ 'form_id' ]][ 'questions' ][ $vote [ 'vote_option_id' ] - 1 ][ 'form_question_text' ] != $vote [ 'vote_option_text' ]) {
$output -> warning ( " Some Question-Mapping went wrong within Submission-Mapping to new Database. On 'vote_id': " . $vote [ 'id' ] . " - 'vote_option_text': ' " . $vote [ 'vote_option_text' ] . " ' " );
}
$qb_restore -> insert ( 'forms_v2_answers' )
-> values ([
'submission_id' => $qb_restore -> createNamedParameter ( $id_mapping [ 'currentSubmission' ], IQueryBuilder :: PARAM_INT ),
2020-04-09 12:40:04 +02:00
'question_id' => $qb_restore -> createNamedParameter ( $id_mapping [ 'questions' ][ $oldQuestionId ][ 'newId' ], IQueryBuilder :: PARAM_STR ),
2020-03-30 13:48:14 +02:00
'text' => $qb_restore -> createNamedParameter ( $vote [ 'vote_answer' ], IQueryBuilder :: PARAM_STR )
]);
$qb_restore -> execute ();
}
}
}
2020-04-09 18:21:59 +02:00
/**
* Convert old Access - String into JSON of new Access - Structure .
* @ param $accessString Old access - String
*/
private function convertAccessList ( $accessString ) : string {
$accessArray = [];
if ( $accessString === 'public' || $accessString === 'registered' ) {
// Store type and return with empty users/groups.
$accessArray [ 'type' ] = $accessString ;
return json_encode ( $accessArray );
}
// Access 'selected'
$accessArray [ 'type' ] = 'selected' ;
$accessArray [ 'users' ] = [];
$accessArray [ 'groups' ] = [];
$stringExplode = explode ( ';' , $accessString );
2020-04-29 11:50:03 +02:00
foreach ( $stringExplode as $string ) {
2020-04-09 18:21:59 +02:00
if ( strpos ( $string , 'user_' ) === 0 ) {
$accessArray [ 'users' ][] = substr ( $string , 5 );
} elseif ( strpos ( $string , 'group_' ) === 0 ) {
$accessArray [ 'groups' ][] = substr ( $string , 6 );
}
}
return json_encode ( $accessArray );
}
2020-04-11 14:18:24 +02:00
/** Convert old Date-Format to unix-timestamps */
private function convertDateTime ( $oldDate ) : int {
// Expires can be NULL -> Converting to timestamp 0
if ( ! $oldDate ) {
return 0 ;
}
return DateTime :: createFromFormat ( 'Y-m-d H:i:s' , $oldDate ) -> getTimestamp ();
}
2020-03-30 13:48:14 +02:00
}