forms/lib/Migration/Version010200Date20200323141300.php
John Molakvoæ (skjnldsv) fcae747c5f
Php cs fix
Signed-off-by: John Molakvoæ (skjnldsv) <skjnldsv@protonmail.com>
2020-04-29 11:50:03 +02:00

418 lines
15 KiB
PHP

<?php
/**
* @copyright Copyright (c) 2020 Jonas Rittershofer <jotoeri@users.noreply.github.com>
*
* @author John Molakvoæ (skjnldsv) <skjnldsv@protonmail.com>
* @author Jonas Rittershofer <jotoeri@users.noreply.github.com>
*
* @license GNU AGPL version 3 or any later version
*
* 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.
*
* 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.
*
* 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/>.
*
*/
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;
use \DateTime;
/**
* Installation class for the forms app.
* Initial db creation
*/
class Version010200Date20200323141300 extends SimpleMigrationStep {
/** @var IDBConnection */
protected $connection;
/** @var IConfig */
protected $config;
/** Map of questionTypes to change */
private $questionTypeMap = [
'radiogroup' => 'multiple_unique',
'checkbox' => 'multiple',
'text' => 'short',
'comment' => 'long',
'dropdown' => 'multiple_unique'
];
/**
* @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,
]);
$table->addColumn('access_json', Type::JSON, [
'notnull' => false,
]);
$table->addColumn('created', Type::INTEGER, [
'notnull' => false,
'comment' => 'unix-timestamp',
]);
$table->addColumn('expires', Type::INTEGER, [
'notnull' => false,
'default' => 0,
'comment' => 'unix-timestamp',
]);
$table->addColumn('is_anonymous', Type::BOOLEAN, [
'notnull' => true,
'default' => 0,
]);
$table->addColumn('submit_once', Type::BOOLEAN, [
'notnull' => true,
'default' => 0,
]);
$table->setPrimaryKey(['id']);
$table->addUniqueIndex(['hash'], 'uniqueHash');
}
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,
]);
$table->addColumn('order', Type::INTEGER, [
'notnull' => true,
'default' => 1,
]);
$table->addColumn('type', Type::STRING, [
'notnull' => true,
'length' => 256,
]);
$table->addColumn('mandatory', Type::BOOLEAN, [
'notnull' => true,
'default' => 0,
]);
$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,
]);
$table->addColumn('timestamp', Type::INTEGER, [
'notnull' => false,
'comment' => 'unix-timestamp',
]);
$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.
if ($schema->hasTable('forms_events')) {
$id_mapping = [];
$id_mapping['events'] = []; // Maps oldevent-id => ['newId' => newevent-id, 'nextQuestionOrder' => integer]
$id_mapping['questions'] = []; // Maps oldquestion-id => ['newId' => newquestion-id]
$id_mapping['currentSubmission'] = 0;
//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();
while ($event = $cursor->fetch()) {
$newAccessJSON = $this->convertAccessList($event['access']);
$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),
'access_json' => $qb_restore->createNamedParameter($newAccessJSON, IQueryBuilder::PARAM_STR),
'created' => $qb_restore->createNamedParameter($this->convertDateTime($event['created']), IQueryBuilder::PARAM_INT),
'expires' => $qb_restore->createNamedParameter($this->convertDateTime($event['expire']), IQueryBuilder::PARAM_INT),
'is_anonymous' => $qb_restore->createNamedParameter($event['is_anonymous'], IQueryBuilder::PARAM_BOOL),
'submit_once' => $qb_restore->createNamedParameter($event['unique'], IQueryBuilder::PARAM_BOOL)
]);
$qb_restore->execute();
$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
];
}
$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();
while ($question = $cursor->fetch()) {
//In case the old Question would have been longer than current possible length, create a warning and shorten text to avoid Error on upgrade.
if (strlen($question['form_question_text']) > 2048) {
$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([
'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),
'type' => $qb_restore->createNamedParameter($this->questionTypeMap[$question['form_question_type']], IQueryBuilder::PARAM_STR),
'text' => $qb_restore->createNamedParameter($question['form_question_text'], IQueryBuilder::PARAM_STR)
]);
$qb_restore->execute();
$id_mapping['questions'][$question['id']]['newId'] = $qb_restore->getLastInsertId(); //Store new question-id to connect options to new question.
}
$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();
while ($answer = $cursor->fetch()) {
//In case the old Answer would have been longer than current possible length, create a warning and shorten text to avoid Error on upgrade.
if (strlen($answer['text']) > 1024) {
$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([
'question_id' => $qb_restore->createNamedParameter($id_mapping['questions'][$answer['question_id']]['newId'], IQueryBuilder::PARAM_INT),
'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();
while ($tmp = $cursor->fetch()) {
$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();
while ($tmp = $cursor->fetch()) {
$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();
while ($vote = $cursor->fetch()) {
//If the form changed, if the user changed or if vote_option_id became smaller than last one, then a new submission is interpreted.
if (($vote['form_id'] != $last_vote['form_id']) || ($vote['user_id'] != $last_vote['user_id']) || ($vote['vote_option_id'] < $last_vote['vote_option_id'])) {
$qb_restore->insert('forms_v2_submissions')
->values([
'form_id' => $qb_restore->createNamedParameter($id_mapping['events'][$vote['form_id']]['newId'], IQueryBuilder::PARAM_INT),
'user_id' => $qb_restore->createNamedParameter($vote['user_id'], IQueryBuilder::PARAM_STR),
'timestamp' => $qb_restore->createNamedParameter(time(), IQueryBuilder::PARAM_STR) //Information not available. Just using Migration-Timestamp.
]);
$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.
if (strlen($vote['vote_answer']) > 2048) {
$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),
'question_id' => $qb_restore->createNamedParameter($id_mapping['questions'][$oldQuestionId]['newId'], IQueryBuilder::PARAM_STR),
'text' => $qb_restore->createNamedParameter($vote['vote_answer'], IQueryBuilder::PARAM_STR)
]);
$qb_restore->execute();
}
}
}
/**
* 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);
foreach ($stringExplode as $string) {
if (strpos($string, 'user_') === 0) {
$accessArray['users'][] = substr($string, 5);
} elseif (strpos($string, 'group_') === 0) {
$accessArray['groups'][] = substr($string, 6);
}
}
return json_encode($accessArray);
}
/** 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();
}
}