Completed
Push — master ( 9cbf24...1a8d81 )
by Carsten
215:15 queued 181:37
created

QuestionTheme::convertLS3toLS4()   F

Complexity

Conditions 14
Paths 804

Size

Total Lines 91
Code Lines 54

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 14
eloc 54
nc 804
nop 1
dl 0
loc 91
rs 2.3722
c 1
b 0
f 0

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
use LimeSurvey\Helpers\questionHelper;
4
5
/**
6
 * This is the model class for table "{{question_themes}}".
7
 *
8
 * The followings are the available columns in table '{{question_themes}}':
9
 *
10
 * @property integer $id
11
 * @property string  $name
12
 * @property string  $visible
13
 * @property string  $xml_path
14
 * @property string  $image_path
15
 * @property string  $title
16
 * @property string  $creation_date
17
 * @property string  $author
18
 * @property string  $author_email
19
 * @property string  $author_url
20
 * @property string  $copyright
21
 * @property string  $license
22
 * @property string  $version
23
 * @property string  $api_version
24
 * @property string  $description
25
 * @property string  $last_update
26
 * @property integer $owner_id
27
 * @property string  $theme_type
28
 * @property string  $question_type
29
 * @property integer $core_theme
30
 * @property string  $extends
31
 * @property string  $group
32
 * @property string  $settings
33
 */
34
class QuestionTheme extends LSActiveRecord
35
{
36
    /**
37
     * @return string the associated database table name
38
     */
39
    public function tableName()
40
    {
41
        return '{{question_themes}}';
42
    }
43
44
    /**
45
     * @return array validation rules for model attributes.
46
     */
47
    public function rules()
48
    {
49
        // NOTE: you should only define rules for those attributes that
50
        // will receive user inputs.
51
        return array(
52
            [
53
                'name',
54
                'unique',
55
                'caseSensitive' => false,
56
                'criteria'      => [
57
                    'condition' => 'extends=:extends',
58
                    'params'    => [
59
                        ':extends' => $this->extends
60
                    ]
61
                ],
62
            ],
63
            array('name, title, api_version, question_type', 'required'),
64
            array('owner_id, core_theme', 'numerical', 'integerOnly' => true),
65
            array('name, author, theme_type, question_type, extends, group', 'length', 'max' => 150),
66
            array('visible', 'length', 'max' => 1),
67
            array('xml_path, image_path, author_email, author_url', 'length', 'max' => 255),
68
            array('title', 'length', 'max' => 100),
69
            array('version, api_version', 'length', 'max' => 45),
70
            array('creation_date, copyright, license, description, last_update, settings', 'safe'),
71
            // The following rule is used by search().
72
            array('id, name, visible, xml_path, image_path, title, creation_date, author, author_email, author_url, copyright, license, version, api_version, description, last_update, owner_id, theme_type, question_type, core_theme, extends, group, settings', 'safe', 'on' => 'search'),
73
        );
74
    }
75
76
    /**
77
     * @return array relational rules.
78
     */
79
    public function relations()
80
    {
81
        return array();
82
    }
83
84
    /**
85
     * @return array customized attribute labels (name=>label)
86
     */
87
    public function attributeLabels()
88
    {
89
        return array(
90
            'id'            => 'ID',
91
            'name'          => 'Name',
92
            'visible'       => 'Visible',
93
            'xml_path'      => 'Xml Path',
94
            'image_path'    => 'Image Path',
95
            'title'         => 'Title',
96
            'creation_date' => 'Creation Date',
97
            'author'        => 'Author',
98
            'author_email'  => 'Author Email',
99
            'author_url'    => 'Author Url',
100
            'copyright'     => 'Copyright',
101
            'license'       => 'License',
102
            'version'       => 'Version',
103
            'api_version'   => 'Api Version',
104
            'description'   => 'Description',
105
            'last_update'   => 'Last Update',
106
            'owner_id'      => 'Owner',
107
            'theme_type'    => 'Theme Type',
108
            'question_type' => 'Question Type',
109
            'core_theme'    => 'Core Theme',
110
            'extends'       => 'Extends',
111
            'group'         => 'Group',
112
            'settings'      => 'Settings',
113
        );
114
    }
115
116
    /**
117
     * Retrieves a list of models based on the current search/filter conditions.
118
     *
119
     * Typical usecase:
120
     * - Initialize the model fields with values from filter form.
121
     * - Execute this method to get CActiveDataProvider instance which will filter
122
     * models according to data in model fields.
123
     * - Pass data provider to CGridView, CListView or any similar widget.
124
     *
125
     * @return CActiveDataProvider the data provider that can return the models
126
     * based on the search/filter conditions.
127
     */
128
    public function search()
129
    {
130
        $pageSizeTemplateView = App()->user->getState('pageSizeTemplateView', App()->params['defaultPageSize']);
131
132
        $criteria = new CDbCriteria();
133
        $criteria->compare('id', $this->id);
134
        $criteria->compare('name', $this->name, true);
135
        $criteria->compare('visible', $this->visible, true);
136
        $criteria->compare('xml_path', $this->xml_path, true);
137
        $criteria->compare('image_path', $this->image_path, true);
138
        $criteria->compare('title', $this->title, true);
139
        $criteria->compare('creation_date', $this->creation_date, true);
140
        $criteria->compare('author', $this->author, true);
141
        $criteria->compare('author_email', $this->author_email, true);
142
        $criteria->compare('author_url', $this->author_url, true);
143
        $criteria->compare('copyright', $this->copyright, true);
144
        $criteria->compare('license', $this->license, true);
145
        $criteria->compare('version', $this->version, true);
146
        $criteria->compare('api_version', $this->api_version, true);
147
        $criteria->compare('description', $this->description, true);
148
        $criteria->compare('last_update', $this->last_update, true);
149
        $criteria->compare('owner_id', $this->owner_id);
150
        $criteria->compare('theme_type', $this->theme_type, true);
151
        $criteria->compare('question_type', $this->question_type, true);
152
        $criteria->compare('core_theme', $this->core_theme);
153
        $criteria->compare('extends', $this->extends, true);
154
        $criteria->compare('group', $this->group, true);
155
        $criteria->compare('settings', $this->settings, true);
156
        return new CActiveDataProvider($this, array(
157
            'criteria'   => $criteria,
158
            'pagination' => array(
159
                'pageSize' => $pageSizeTemplateView,
160
            ),
161
        ));
162
    }
163
164
    /**
165
     * Returns the static model of the specified AR class.
166
     * Please note that you should have this exact method in all your CActiveRecord descendants!
167
     *
168
     * @param string $className active record class name.
169
     *
170
     * @return Template the static model class
171
     */
172
    public static function model($className = __CLASS__)
173
    {
174
        return parent::model($className);
175
    }
176
177
    /**
178
     * Returns this table's primary key
179
     *
180
     * @access public
181
     * @return string
182
     */
183
    public function primaryKey()
184
    {
185
        return 'id';
186
    }
187
188
    /**
189
     * Import all Questiontypes and Themes to the {{questions_themes}} table
190
     *
191
     * @param bool $bUseTransaction
192
     *
193
     * @throws CException
194
     */
195
    public function loadAllQuestionXMLConfigurationsIntoDatabase($bUseTransaction = true)
196
    {
197
        $missingQuestionThemeAttributes = [];
198
        $questionThemeDirectories = $this->getQuestionThemeDirectories();
199
200
        // @see: http://phpsecurity.readthedocs.io/en/latest/Injection-Attacks.html#xml-external-entity-injection
201
        if (\PHP_VERSION_ID < 80000) {
202
            $bOldEntityLoaderState = libxml_disable_entity_loader(true);
203
        }            
204
    // process XML Question Files
205
        if (isset($questionThemeDirectories)) {
206
            try {
207
                if ($bUseTransaction) {
208
                    $transaction = App()->db->beginTransaction();
209
                }
210
                $questionsMetaData = self::getAllQuestionMetaData()['available_themes'];
0 ignored issues
show
Bug Best Practice introduced by Patrick Teichmann
The method QuestionTheme::getAllQuestionMetaData() is not static, but was called statically. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

210
                $questionsMetaData = self::/** @scrutinizer ignore-call */ getAllQuestionMetaData()['available_themes'];
Loading history...
211
                foreach ($questionsMetaData as $questionMetaData) {
212
                    // test xml for required metaData
213
                    $requiredMetaDataArray = ['name', 'title', 'creationDate', 'author', 'authorEmail', 'authorUrl', 'copyright', 'copyright', 'license', 'version', 'apiVersion', 'description', 'question_type', 'group', 'subquestions', 'answerscales', 'hasdefaultvalues', 'assessable', 'class'];
214
                    foreach ($requiredMetaDataArray as $requiredMetaData) {
215
                        if (!array_key_exists($requiredMetaData, $questionMetaData)) {
216
                            $missingQuestionThemeAttributes[$questionMetaData['xml_path']][] = $requiredMetaData;
217
                        }
218
                    }
219
                    $questionTheme = QuestionTheme::model()->find('name=:name AND extends=:extends', [':name' => $questionMetaData['name'], ':extends' => $questionMetaData['extends']]);
220
                    if ($questionTheme == null) {
221
                        $questionTheme = new QuestionTheme();
222
                    }
223
                    $metaDataArray = $this->getMetaDataArray($questionMetaData);
224
                    $questionTheme->setAttributes($metaDataArray, false);
225
                    $questionTheme->save();
226
                }
227
                if ($bUseTransaction) {
228
                    $transaction->commit();
0 ignored issues
show
Comprehensibility Best Practice introduced by lacrioque
The variable $transaction does not seem to be defined for all execution paths leading up to this point.
Loading history...
229
                }
230
            } catch (Exception $e) {
231
                //TODO: flashmessage for users
232
                echo $e->getMessage();
233
                // var_dump($e->getTrace());
234
                // var_dump($missingQuestionThemeAttributes);
235
                if ($bUseTransaction) {
236
                    $transaction->rollback();
237
                }
238
            }
239
        }
240
241
        // Put back entity loader to its original state, to avoid contagion to other applications on the server
242
        if (\PHP_VERSION_ID < 80000) {
243
            libxml_disable_entity_loader($bOldEntityLoaderState);
0 ignored issues
show
Comprehensibility Best Practice introduced by Carsten Schmitz
The variable $bOldEntityLoaderState does not seem to be defined for all execution paths leading up to this point.
Loading history...
244
        }            
245
    }
246
247
    /**
248
     * Returns visibility button.
249
     *
250
     * @return string|array
251
     */
252
    public function getVisibilityButton()
253
    {
254
        // don't show any buttons if user doesn't have update permission
255
        if (!Permission::model()->hasGlobalPermission('templates', 'update')) {
256
            return '';
257
        }
258
        $bVisible = $this->visible == 'Y' ? true : false;
259
        $aButtons = [
260
            'visibility_button' => [
261
                'url'     => $sToggleVisibilityUrl = App()->getController()->createUrl('admin/questionthemes/sa/togglevisibility', ['id' => $this->id]),
0 ignored issues
show
Unused Code introduced by Patrick Teichmann
The assignment to $sToggleVisibilityUrl is dead and can be removed.
Loading history...
262
                'visible' => $bVisible
263
            ]
264
        ];
265
        $sButtons = App()->getController()->renderPartial('./theme_buttons', ['id' => $this->id, 'buttons' => $aButtons], true);
266
        return $sButtons;
267
    }
268
269
    /**
270
     * Install Button for the available questions
271
     */
272
    public function getManifestButtons()
273
    {
274
        $sLoadLink = CHtml::form(array("themeOptions/importManifest/"), 'post', array('id' => 'forminstallquestiontheme', 'name' => 'forminstallquestiontheme')) .
275
            "<input type='hidden' name='templatefolder' value='" . $this->xml_path . "'>
276
            <input type='hidden' name='theme' value='questiontheme'>
277
            <button id='template_options_link_" . $this->name . "'class='btn btn-default btn-block'>
278
            <span class='fa fa-download text-warning'></span>
279
            " . gT('Install') . "
280
            </button>
281
            </form>";
282
283
        return $sLoadLink;
284
    }
285
286
    /**
287
     * Import config manifest to database.
288
     *
289
     * @param string $sXMLDirectoryPath the relative path to the Question Theme XML directory
290
     * @param bool   $bSkipConversion   If converting should be skipped
291
     *
292
     * @return bool|string
293
     * @throws Exception
294
     */
295
    public function importManifest($sXMLDirectoryPath, $bSkipConversion = false)
296
    {
297
        if (empty($sXMLDirectoryPath)) {
298
            throw new InvalidArgumentException('$templateFolder cannot be empty');
299
        }
300
301
        // convert Question Theme
302
        if ($bSkipConversion === false) {
303
            $aConvertSuccess = self::convertLS3toLS4($sXMLDirectoryPath);
304
            if (!$aConvertSuccess['success']) {
305
                App()->setFlashMessage($aConvertSuccess['message'], 'error');
306
                App()->getController()->redirect(array("themeOptions/index#questionthemes"));
307
            }
308
        }
309
310
        /** @var array */
311
        $aQuestionMetaData = $this->getQuestionMetaData($sXMLDirectoryPath);
312
313
        if (empty($aQuestionMetaData)) {
314
            // todo detailed error handling
315
            return null;
316
        }
317
        /** @var array<string, mixed> */
318
        // todo proper error handling should be done before in getQuestionMetaData via validate() remove @ afterwards
319
        $aMetaDataArray = @$this->getMetaDataArray($aQuestionMetaData);
320
321
        $this->setAttributes($aMetaDataArray, false);
322
        if ($this->save()) {
323
            return $aQuestionMetaData['title'];
324
        } else {
325
            // todo detailed error handling
326
            return null;
327
        }
328
    }
329
330
    /**
331
     * Returns all Questions that can be installed
332
     *
333
     * @return QuestionTheme[]
334
     * @throws Exception
335
     */
336
    public function getAvailableQuestions()
337
    {
338
        $aAvailableThemes = [];
339
        $aThemes = $this->getAllQuestionMetaData();
340
        $questionsInDB = $this->findAll();
341
342
        if (!empty($aThemes['available_themes'])) {
343
            if (!empty($questionsInDB)) {
344
                foreach ($questionsInDB as $questionInDB) {
345
                    if (array_key_exists($questionKey = $questionInDB->name . '_' . $questionInDB->question_type, $aThemes['available_themes'])) {
0 ignored issues
show
Bug introduced by Patrick Teichmann
Are you sure $questionInDB->question_type of type array|mixed|null can be used in concatenation? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

345
                    if (array_key_exists($questionKey = $questionInDB->name . '_' . /** @scrutinizer ignore-type */ $questionInDB->question_type, $aThemes['available_themes'])) {
Loading history...
Bug introduced by Patrick Teichmann
Are you sure $questionInDB->name of type array|mixed|null can be used in concatenation? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

345
                    if (array_key_exists($questionKey = /** @scrutinizer ignore-type */ $questionInDB->name . '_' . $questionInDB->question_type, $aThemes['available_themes'])) {
Loading history...
346
                        unset($aThemes['available_themes'][$questionKey]);
347
                    }
348
                }
349
            }
350
            array_values($aThemes['available_themes']);
351
            foreach ($aThemes['available_themes'] as $questionMetaData) {
352
                // TODO: replace by manifest
353
                $questionTheme = new QuestionTheme();
354
355
                $metaDataArray = $this->getMetaDataArray($questionMetaData);
356
                $questionTheme->setAttributes($metaDataArray, false);
357
                $aAvailableThemes[] = $questionTheme;
358
            }
359
        }
360
361
        return [
0 ignored issues
show
Bug Best Practice introduced by Patrick Teichmann
The expression return array('available_...hemes['broken_themes']) returns an array which contains values of type QuestionTheme[]|array which are incompatible with the documented value type QuestionTheme.
Loading history...
362
            'available_themes' => $aAvailableThemes,
363
            'broken_themes' => $aThemes['broken_themes']
364
        ];
365
    }
366
367
    /**
368
     * Returns an Array of all questionthemes and their metadata
369
     *
370
     * @param bool $core
371
     * @param bool $custom
372
     * @param bool $user
373
     * @return array
374
     */
375
    public function getAllQuestionMetaData($core = true, $custom = true, $user = true)
376
    {
377
        $questionsMetaData = $aBrokenQuestionThemes = [];
378
        $questionDirectoriesAndPaths = $this->getAllQuestionXMLPaths($core, $custom, $user);
379
        if (isset($questionDirectoriesAndPaths) && !empty($questionDirectoriesAndPaths)) {
380
            foreach ($questionDirectoriesAndPaths as $directory => $questionConfigFilePaths) {
381
                foreach ($questionConfigFilePaths as $questionConfigFilePath) {
382
                    try {
383
                        $questionMetaData = self::getQuestionMetaData($questionConfigFilePath);
384
                        $questionsMetaData[$questionMetaData['name'] . '_' . $questionMetaData['questionType']] = $questionMetaData;
385
                    } catch (Exception $e) {
386
                        array_push($aBrokenQuestionThemes, [
387
                            'path'    => $questionConfigFilePath,
388
                            'exception' => $e
389
                        ]);
390
                    }
391
                }
392
            }
393
        }
394
        return $aQuestionThemes = [
0 ignored issues
show
Unused Code introduced by Patrick Teichmann
The assignment to $aQuestionThemes is dead and can be removed.
Loading history...
395
            'available_themes' => $questionsMetaData,
396
            'broken_themes'    => $aBrokenQuestionThemes
397
        ];
398
    }
399
400
    /**
401
     * Read all the MetaData for given Question XML definition
402
     *
403
     * @param $pathToXML
404
     *
405
     * @return array Question Meta Data
406
     * @throws Exception
407
     */
408
    public static function getQuestionMetaData($pathToXML)
409
    {
410
        $questionDirectories = self::getQuestionThemeDirectories();
411
        foreach ($questionDirectories as $key => $questionDirectory) {
412
            $questionDirectories[$key] = str_replace('\\', '/', $questionDirectory);
413
        }
414
        $publicurl = App()->getConfig('publicurl');
415
416
        $pathToXML = str_replace('\\', '/', $pathToXML);
417
        if (\PHP_VERSION_ID < 80000) {
418
            $bOldEntityLoaderState = libxml_disable_entity_loader(true);
419
        }            
420
        $sQuestionConfigFilePath = App()->getConfig('rootdir') . DIRECTORY_SEPARATOR . $pathToXML . DIRECTORY_SEPARATOR . 'config.xml';
421
        $sQuestionConfigFile = file_get_contents($sQuestionConfigFilePath);  // @see: Now that entity loader is disabled, we can't use simplexml_load_file; so we must read the file with file_get_contents and convert it as a string
422
        $oQuestionConfig = simplexml_load_string($sQuestionConfigFile);
423
424
        if (!$sQuestionConfigFile) {
425
            throw new Exception(gT('Extension configuration file is not valid or missing.'));
426
        }
427
428
        // TODO: Copied from PluginManager - remake to extension manager.
429
        $extensionConfig = new ExtensionConfig($oQuestionConfig);
430
        if (!$extensionConfig->validate()) {
431
            throw new Exception(gT('Extension configuration file is not valid.'));
432
        }
433
        if (!$extensionConfig->isCompatible()) {
434
            throw new Exception(
435
                sprintf(
436
                    gT('Extension "%s" is not compatible with your LimeSurvey version.'),
437
                    $extensionConfig->getName()
438
                )
439
            );
440
        }
441
442
        // read all metadata from the provided $pathToXML
443
        $questionMetaData = json_decode(json_encode($oQuestionConfig->metadata), true);
444
445
        $aQuestionThemes = QuestionTheme::model()->findAll(
446
            '(question_type = :question_type AND extends = :extends)',
447
            [
448
                ':question_type' => $questionMetaData['questionType'],
449
                ':extends'       => '',
450
            ]
451
        );
452
        //set extends if there is allready an existing Question with this type
453
        if (empty($aQuestionThemes)) {
454
            $questionMetaData['extends'] = '';
455
        } else {
456
            $questionMetaData['extends'] = $questionMetaData['questionType'];
457
        }
458
459
        // get custom previewimage if defined
460
        if (!empty($oQuestionConfig->files->preview->filename)) {
461
            $previewFileName = json_decode(json_encode($oQuestionConfig->files->preview->filename), true)[0];
462
            $questionMetaData['image_path'] = $publicurl . $pathToXML . '/assets/' . $previewFileName;
463
        }
464
465
        $questionMetaData['xml_path'] = $pathToXML;
466
467
        // set settings as json
468
        $questionMetaData['settings'] = json_encode([
469
            'subquestions'     => $questionMetaData['subquestions'] ?? 0,
470
            'answerscales'     => $questionMetaData['answerscales'] ?? 0,
471
            'hasdefaultvalues' => $questionMetaData['hasdefaultvalues'] ?? 0,
472
            'assessable'       => $questionMetaData['assessable'] ?? 0,
473
            'class'            => $questionMetaData['class'] ?? '',
474
        ]);
475
476
        // override MetaData depending on directory
477
        if (substr($pathToXML, 0, strlen($questionDirectories['coreQuestion'])) === $questionDirectories['coreQuestion']) {
478
            $questionMetaData['coreTheme'] = 1;
479
            $questionMetaData['image_path'] = App()->getConfig("imageurl") . '/screenshots/' . self::getQuestionThemeImageName($questionMetaData['questionType']);
480
        }
481
        if (substr($pathToXML, 0, strlen($questionDirectories['customCoreTheme'])) === $questionDirectories['customCoreTheme']) {
482
            $questionMetaData['coreTheme'] = 1;
483
        }
484
        if (substr($pathToXML, 0, strlen($questionDirectories['customUserTheme'])) === $questionDirectories['customUserTheme']) {
485
            $questionMetaData['coreTheme'] = 0;
486
        }
487
488
        // get Default Image if undefined
489
        if (empty($questionMetaData['image_path']) || !file_exists(App()->getConfig('rootdir') . $questionMetaData['image_path'])) {
490
            $questionMetaData['image_path'] = App()->getConfig("imageurl") . '/screenshots/' . self::getQuestionThemeImageName($questionMetaData['questionType']);
491
        }
492
493
        if (\PHP_VERSION_ID < 80000) {
494
            libxml_disable_entity_loader($bOldEntityLoaderState);
0 ignored issues
show
Comprehensibility Best Practice introduced by Carsten Schmitz
The variable $bOldEntityLoaderState does not seem to be defined for all execution paths leading up to this point.
Loading history...
495
        }            
496
497
        return $questionMetaData;
498
    }
499
500
    /**
501
     * Find all XML paths for specified Question Root folders
502
     *
503
     * @param bool $core
504
     * @param bool $custom
505
     * @param bool $user
506
     *
507
     * @return array
508
     */
509
    public static function getAllQuestionXMLPaths($core = true, $custom = true, $user = true)
510
    {
511
        $questionDirectories = self::getQuestionThemeDirectories();
512
        $questionDirectoriesAndPaths = [];
513
        if ($core) {
514
            $coreQuestionsPath = $questionDirectories['coreQuestion'];
515
            $selectedQuestionDirectories[] = $coreQuestionsPath;
0 ignored issues
show
Comprehensibility Best Practice introduced by Patrick Teichmann
$selectedQuestionDirectories was never initialized. Although not strictly required by PHP, it is generally a good practice to add $selectedQuestionDirectories = array(); before regardless.
Loading history...
516
        }
517
        if ($custom) {
518
            $customQuestionThemesPath = $questionDirectories['customCoreTheme'];
519
            $selectedQuestionDirectories[] = $customQuestionThemesPath;
520
        }
521
        if ($user) {
522
            $userQuestionThemesPath = $questionDirectories['customUserTheme'];
523
            if (!is_dir($userQuestionThemesPath)) {
524
                mkdir($userQuestionThemesPath);
525
            }
526
            $selectedQuestionDirectories[] = $userQuestionThemesPath;
527
        }
528
529
        if (isset($selectedQuestionDirectories)) {
530
            foreach ($selectedQuestionDirectories as $questionThemeDirectory) {
531
                $directory = new RecursiveDirectoryIterator($questionThemeDirectory);
532
                $iterator = new RecursiveIteratorIterator($directory);
533
                foreach ($iterator as $info) {
534
                    $ext = pathinfo($info->getPathname(), PATHINFO_EXTENSION);
535
                    if ($ext == 'xml') {
536
                        $questionDirectoriesAndPaths[$questionThemeDirectory][] = dirname($info->getPathname());
537
                    }
538
                }
539
            }
540
        }
541
        return $questionDirectoriesAndPaths;
542
    }
543
544
545
    /**
546
     * @param QuestionTheme $oQuestionTheme
547
     *
548
     * @return array
549
     * todo move actions to its controller and split between controller and model, related search for: 1573123789741
550
     */
551
    public static function uninstall($oQuestionTheme)
552
    {
553
        if (!Permission::model()->hasGlobalPermission('templates', 'delete')) {
554
            return false;
0 ignored issues
show
Bug Best Practice introduced by Patrick Teichmann
The expression return false returns the type false which is incompatible with the documented return type array.
Loading history...
555
        }
556
557
        // if this questiontype is extended, it cannot be deleted
558
        if (empty($oQuestionTheme->extends)) {
559
            $aQuestionThemes = self::model()->findAll(
560
                'extends = :extends AND NOT id = :id',
561
                [
562
                    ':extends' => $oQuestionTheme->question_type,
563
                    ':id'      => $oQuestionTheme->id
564
                ]
565
            );
566
            if (!empty($aQuestionThemes)) {
567
                return [
568
                    'error'  => gT('Question type is being extended and cannot be uninstalled'),
569
                    'result' => false
570
                ];
571
            };
572
        }
573
574
        // transform theme name compatible with question attributes for core/default theme_template
575
        $sThemeName = empty($oQuestionTheme->extends) ? 'core' : $oQuestionTheme->name;
576
577
        // todo optimize function for very big surveys, eventually in yii 2 or 3 with batch processing / if this is breaking in Yii 1 use CDbDataReader $query = new CDbDataReader($command), $query->read()
578
        $aQuestions = Question::model()->with('questionattributes')->findAll(
579
            'type = :type AND parent_qid = :parent_qid',
580
            [
581
                ':type'       => $oQuestionTheme->question_type,
582
                ':parent_qid' => 0
583
            ]
584
        );
585
        foreach ($aQuestions as $oQuestion) {
586
            if (isset($oQuestion['questionattributes']['question_template'])) {
587
                if ($sThemeName == $oQuestion['questionattributes']['question_template']) {
588
                    $bDeleteTheme = false;
589
                    break;
590
                };
591
            } else {
592
                if ($sThemeName == 'core') {
593
                    $bDeleteTheme = false;
594
                    break;
595
                };
596
            }
597
        }
598
        // if this questiontheme is used, it cannot be deleted
599
        if (isset($bDeleteTheme) && !$bDeleteTheme) {
600
            return [
601
                'error'  => gT('Question type is used in a Survey and cannot be uninstalled'),
602
                'result' => false
603
            ];
604
        }
605
606
        // delete questiontheme if it is not used
607
        try {
608
            return [
609
                'result' => $oQuestionTheme->delete()
610
            ];
611
        } catch (CDbException $e) {
612
            return [
613
                'error'  => $e->getMessage(),
614
                'result' => false
615
            ];
616
        }
617
    }
618
619
    /**
620
     * Returns all question types with metadata as an array indexed by type.
621
     * (all entries in table question_themes extends='')
622
     *
623
     * @return array
624
     */
625
    public static function findQuestionMetaDataForAllTypes()
626
    {
627
        //getting all question_types which are NOT extended
628
        $baseQuestions = self::model()->findAllByAttributes(['extends' => '']);
629
        $aQuestionsIndexedByType = [];
630
631
        foreach ($baseQuestions as $baseQuestion) {
632
            /**@var QuestionTheme $baseQuestion */
633
            $baseQuestion['settings'] = json_decode($baseQuestion['settings']);
634
            $aQuestionsIndexedByType[$baseQuestion->question_type] = $baseQuestion;
635
        }
636
637
        return $aQuestionsIndexedByType;
638
    }
639
640
    /**
641
     * Returns All QuestionTheme settings
642
     *
643
     * @param string $question_type
644
     * @param string $language
645
     *
646
     * @return mixed $baseQuestions Questions as Array or Object
647
     */
648
    public static function findQuestionMetaData($question_type, $question_template = 'core', $language = '')
649
    {
650
        $criteria = new CDbCriteria();
651
652
        if ($question_template === 'core') {
653
            $criteria->condition = 'extends = :extends';
654
            $criteria->addCondition('question_type = :question_type', 'AND');
655
            $criteria->params = [':extends' => '', ':question_type' => $question_type];
656
        } else {
657
            $criteria->addCondition('question_type = :question_type AND name = :name');
658
            $criteria->params = [':question_type' => $question_type, ':name' => $question_template];
659
        }
660
661
        $baseQuestion = self::model()->query($criteria, false, false);
662
663
        // language settings
664
        $baseQuestion['title'] = gT($baseQuestion['title'], "html", $language);
665
        $baseQuestion['group'] = gT($baseQuestion['group'], "html", $language);
666
667
        // decode settings json
668
        $baseQuestion['settings'] = json_decode($baseQuestion['settings']);
669
670
        return $baseQuestion;
671
    }
672
673
    /**
674
     * Returns all Question Meta Data for the question type selector
675
     *
676
     * @return mixed $baseQuestions Questions as Array or Object
677
     */
678
    public static function findAllQuestionMetaDataForSelector()
679
    {
680
        $criteria = new CDbCriteria();
681
        //            $criteria->condition = 'extends = :extends';
682
        $criteria->addCondition('visible = :visible', 'AND');
683
        $criteria->params = [':visible' => 'Y'];
684
685
        $baseQuestions = self::model()->query($criteria, true, false);
686
687
        if (\PHP_VERSION_ID < 80000) {
688
            $bOldEntityLoaderState = libxml_disable_entity_loader(true);
689
        }            
690
691
        $baseQuestionsModified = [];
692
        foreach ($baseQuestions as $baseQuestion) {
693
            //TODO: should be moved into DB column (question_theme_settings table)
694
            $sQuestionConfigFile = file_get_contents(App()->getConfig('rootdir') . DIRECTORY_SEPARATOR . $baseQuestion['xml_path'] . DIRECTORY_SEPARATOR . 'config.xml');  // @see: Now that entity loader is disabled, we can't use simplexml_load_file; so we must read the file with file_get_contents and convert it as a string
695
            $oQuestionConfig = simplexml_load_string($sQuestionConfigFile);
696
            $questionEngineData = json_decode(json_encode($oQuestionConfig->engine), true);
697
            $showAsQuestionType = $questionEngineData['show_as_question_type'];
698
699
            // if an extended Question should not be shown as a selectable questiontype skip it
700
            if (!empty($baseQuestion['extends'] && !$showAsQuestionType)) {
701
                continue;
702
            }
703
704
            // language settings
705
            $baseQuestion['title'] = gT($baseQuestion['title'], "html");
706
            $baseQuestion['group'] = gT($baseQuestion['group'], "html");
707
708
            // decode settings json
709
            $baseQuestion['settings'] = json_decode($baseQuestion['settings']);
710
711
            // if its a core question change name to core for rendering Default rendering in the selector
712
            if (empty($baseQuestion['extends'])) {
713
                $baseQuestion['name'] = 'core';
714
            }
715
            $baseQuestion['image_path'] = str_replace(
716
                '//',
717
                '/',
718
                App()->getConfig('publicurl') . $baseQuestion['image_path']
719
            );
720
            $baseQuestionsModified[] = $baseQuestion;
721
        }
722
        if (\PHP_VERSION_ID < 80000) {
723
            libxml_disable_entity_loader($bOldEntityLoaderState);
0 ignored issues
show
Comprehensibility Best Practice introduced by Carsten Schmitz
The variable $bOldEntityLoaderState does not seem to be defined for all execution paths leading up to this point.
Loading history...
724
        }            
725
726
        $baseQuestions = $baseQuestionsModified;
727
728
        return $baseQuestions;
729
    }
730
731
    public static function getQuestionThemeDirectories()
732
    {
733
        $questionThemeDirectories['coreQuestion'] = App()->getConfig('corequestiontypedir') . '/survey/questions/answer';
0 ignored issues
show
Comprehensibility Best Practice introduced by Patrick Teichmann
$questionThemeDirectories was never initialized. Although not strictly required by PHP, it is generally a good practice to add $questionThemeDirectories = array(); before regardless.
Loading history...
734
        $questionThemeDirectories['customCoreTheme'] = App()->getConfig('userquestionthemedir');
735
        $questionThemeDirectories['customUserTheme'] = App()->getConfig('userquestionthemerootdir');
736
737
        return $questionThemeDirectories;
738
    }
739
740
    /**
741
     * Returns QuestionMetaData Array for use in ->save operations
742
     *
743
     * @param array $questionMetaData
744
     *
745
     * @return array $questionMetaData
746
     */
747
    private function getMetaDataArray($questionMetaData)
748
    {
749
        $questionMetaData = [
750
            'name'          => $questionMetaData['name'],
751
            'visible'       => 'Y',
752
            'xml_path'      => $questionMetaData['xml_path'],
753
            'image_path'    => $questionMetaData['image_path'] ?? '',
754
            'title'         => $questionMetaData['title'],
755
            'creation_date' => date('Y-m-d H:i:s', strtotime($questionMetaData['creationDate'])),
756
            'author'        => $questionMetaData['author'] ?? '',
757
            'author_email'  => $questionMetaData['authorEmail'] ?? '',
758
            'author_url'    => $questionMetaData['authorUrl'] ?? '',
759
            'copyright'     => $questionMetaData['copyright'] ?? '',
760
            'license'       => $questionMetaData['license'] ?? '',
761
            'version'       => $questionMetaData['version'],
762
            'api_version'   => $questionMetaData['apiVersion'],
763
            'description'   => $questionMetaData['description'],
764
            'last_update'   => date('Y-m-d H:i:s'), //todo
765
            'owner_id'      => 1, //todo
766
            'theme_type'    => $questionMetaData['type'],
767
            'question_type' => $questionMetaData['questionType'],
768
            'core_theme'    => $questionMetaData['coreTheme'],
769
            'extends'       => $questionMetaData['extends'],
770
            'group'         => $questionMetaData['group'] ?? '',
771
            'settings'      => $questionMetaData['settings'] ?? ''
772
        ];
773
        return $questionMetaData;
774
    }
775
776
    /**
777
     * Return the question Theme preview URL
778
     *
779
     * @param $sType : type of question
0 ignored issues
show
Documentation Bug introduced by Olle Haerstedt
The doc comment : type at position 0 could not be parsed: Unknown type name ':' at position 0 in : type.
Loading history...
780
     *
781
     * @return string : question theme preview URL
782
     */
783
    public static function getQuestionThemeImageName($sType = null)
784
    {
785
        if ($sType == '*') {
786
            $preview_filename = 'EQUATION.png';
787
        } elseif ($sType == ':') {
788
            $preview_filename = 'COLON.png';
789
        } elseif ($sType == '|') {
790
            $preview_filename = 'PIPE.png';
791
        } elseif (!empty($sType)) {
792
            $preview_filename = $sType . '.png';
793
        } else {
794
            $preview_filename = '.png';
795
        }
796
797
        return $preview_filename;
798
    }
799
800
    /**
801
     * Returns the table definition for the current Question
802
     *
803
     * @param string $name
804
     * @param string $type
805
     *
806
     * @return string mixed
807
     */
808
    public static function getAnswerColumnDefinition($name, $type)
809
    {
810
        // cache the value between function calls
811
        static $cacheMemo = [];
812
        $cacheKey = $name . '_' . $type;
813
        if (isset($cacheMemo[$cacheKey])) {
814
            return $cacheMemo[$cacheKey];
815
        }
816
817
        if ($name == 'core') {
818
            $questionTheme = self::model()->findByAttributes([], 'question_type=:question_type AND extends=:extends', ['question_type' => $type, 'extends' => '']);
819
        } else {
820
            $questionTheme = self::model()->findByAttributes([], 'name=:name AND question_type=:question_type', ['name' => $name, 'question_type' => $type]);
821
        }
822
823
        $answerColumnDefinition = '';
824
        if (isset($questionTheme['xml_path'])) {
825
            if (\PHP_VERSION_ID < 80000) {
826
                $bOldEntityLoaderState = libxml_disable_entity_loader(true);
827
            }            
828
    
829
830
            $sQuestionConfigFile = file_get_contents(App()->getConfig('rootdir') . DIRECTORY_SEPARATOR . $questionTheme['xml_path'] . DIRECTORY_SEPARATOR . 'config.xml');  // @see: Now that entity loader is disabled, we can't use simplexml_load_file; so we must read the file with file_get_contents and convert it as a string
831
            $oQuestionConfig = simplexml_load_string($sQuestionConfigFile);
832
            if (isset($oQuestionConfig->metadata->answercolumndefinition)) {
833
                // TODO: Check json_last_error.
834
                $answerColumnDefinition = json_decode(json_encode($oQuestionConfig->metadata->answercolumndefinition), true)[0];
835
            }
836
837
            if (\PHP_VERSION_ID < 80000) {
838
                libxml_disable_entity_loader($bOldEntityLoaderState);
0 ignored issues
show
Comprehensibility Best Practice introduced by Patrick Teichmann
The variable $bOldEntityLoaderState does not seem to be defined for all execution paths leading up to this point.
Loading history...
839
            }            
840
        }
841
842
        $cacheMemo[$cacheKey] = $answerColumnDefinition;
843
        return $answerColumnDefinition;
844
    }
845
846
    /**
847
     * Returns the Config Path for the selected Question Type base definition
848
     *
849
     * @param string $type
850
     *
851
     * @return string Path to config XML
852
     * @throws CException
853
     */
854
    public static function getQuestionXMLPathForBaseType($type)
855
    {
856
        $aQuestionTheme = QuestionTheme::model()->findByAttributes([], 'question_type = :question_type AND extends = :extends', ['question_type' => $type, 'extends' => '']);
857
        if (empty($aQuestionTheme)) {
858
            throw new \CException("The Database definition for Questiontype: " . $type . " is missing");
859
        }
860
        $configXMLPath = App()->getConfig('rootdir') . '/' . $aQuestionTheme['xml_path'] . '/config.xml';
861
862
        return $configXMLPath;
863
    }
864
865
    /**
866
     * Converts LS3 Question Theme to LS4
867
     *
868
     * @param string $sXMLDirectoryPath
869
     *
870
     * @return array $success Returns an array with the conversion status
871
     */
872
    public static function convertLS3toLS4($sXMLDirectoryPath)
873
    {
874
875
        $sXMLDirectoryPath = str_replace('\\', '/', $sXMLDirectoryPath);
876
        $sConfigPath = $sXMLDirectoryPath . DIRECTORY_SEPARATOR . 'config.xml';
877
        if (\PHP_VERSION_ID < 80000) {
878
            $bOldEntityLoaderState = libxml_disable_entity_loader(true);
879
        }            
880
881
        $sQuestionConfigFilePath = App()->getConfig('rootdir') . DIRECTORY_SEPARATOR . $sConfigPath;
882
        $sQuestionConfigFile = file_get_contents($sQuestionConfigFilePath);  // @see: Now that entity loader is disabled, we can't use simplexml_load_file; so we must read the file with file_get_contents and convert it as a string
883
884
        if (!$sQuestionConfigFile) {
885
            if (\PHP_VERSION_ID < 80000) {
886
                libxml_disable_entity_loader($bOldEntityLoaderState);
0 ignored issues
show
Comprehensibility Best Practice introduced by Carsten Schmitz
The variable $bOldEntityLoaderState does not seem to be defined for all execution paths leading up to this point.
Loading history...
887
            }            
888
            return $aSuccess = [
0 ignored issues
show
Unused Code introduced by Patrick Teichmann
The assignment to $aSuccess is dead and can be removed.
Loading history...
889
                'message' => sprintf(
890
                    gT('Configuration file %s could not be found or read.'),
891
                    $sConfigPath
892
                ),
893
                'success' => false
894
            ];
895
        }
896
897
        // replace custom_attributes with attributes
898
        if (preg_match('/<custom_attributes>/', $sQuestionConfigFile)) {
899
            $sQuestionConfigFile = preg_replace('/<custom_attributes>/', '<attributes>', $sQuestionConfigFile);
900
            $sQuestionConfigFile = preg_replace('/<\/custom_attributes>/', '</attributes>', $sQuestionConfigFile);
901
        };
902
        $oThemeConfig = simplexml_load_string($sQuestionConfigFile);
903
        if (\PHP_VERSION_ID < 80000) {
904
            libxml_disable_entity_loader($bOldEntityLoaderState);
905
        }            
906
907
        // get type from core theme
908
        if (isset($oThemeConfig->metadata->type)) {
909
            $oThemeConfig->metadata->type = 'question_theme';
910
        } else {
911
            $oThemeConfig->metadata->addChild('type', 'question_theme');
912
        };
913
914
        // set compatibility version
915
        if (isset($oThemeConfig->compatibility->version)) {
916
            $oThemeConfig->compatibility->version = '4.0';
917
        } else {
918
            $compatibility = $oThemeConfig->addChild('compatibility');
919
            $compatibility->addChild('version');
920
            $oThemeConfig->compatibility->version = '4.0';
0 ignored issues
show
Bug introduced by Patrick Teichmann
The property version does not seem to exist on SimpleXMLElement.
Loading history...
921
        }
922
923
        $sThemeDirectoryName = basename(dirname($sQuestionConfigFilePath, 1)); //todo: this does not work for all themes in array/... like arrays/10point
924
        $sPathToCoreConfigFile = str_replace('\\', '/', App()->getConfig('rootdir') . '/application/views/survey/questions/answer/' . $sThemeDirectoryName . '/config.xml');
925
        // check if core question theme can be found to fill in missing information
926
        if (!is_file($sPathToCoreConfigFile)) {
927
            return $aSuccess = [
928
                'message' => sprintf(
929
                    gT("Question theme could not be converted to LimeSurvey 4 standard. Reason: No matching core theme with the name %s could be found"),
930
                    $sThemeDirectoryName
931
                ),
932
                'success' => false
933
            ];
934
        }
935
        if (\PHP_VERSION_ID < 80000) {
936
            $bOldEntityLoaderState = libxml_disable_entity_loader(true);
937
        }            
938
        $sThemeCoreConfigFile = file_get_contents($sPathToCoreConfigFile);  // @see: Now that entity loader is disabled, we can't use simplexml_load_file; so we must read the file with file_get_contents and convert it as a string
939
        $oThemeCoreConfig = simplexml_load_string($sThemeCoreConfigFile);
940
        if (\PHP_VERSION_ID < 80000) {
941
            libxml_disable_entity_loader($bOldEntityLoaderState);
942
        }            
943
944
        // get questiontype from core if it is missing
945
        if (!isset($oThemeConfig->metadata->questionType)) {
946
            $oThemeConfig->metadata->addChild('questionType', $oThemeCoreConfig->metadata->questionType);
947
        };
948
949
        // search missing new tags and copy theme from the core theme
950
        $aNewMetadataTagsToRecoverFromCoreType = ['group', 'subquestions', 'answerscales', 'hasdefaultvalues', 'assessable', 'class'];
951
        foreach ($aNewMetadataTagsToRecoverFromCoreType as $sMetaTag) {
952
            if (!isset($oThemeConfig->metadata->$sMetaTag)) {
953
                $oThemeConfig->metadata->addChild($sMetaTag, $oThemeCoreConfig->metadata->$sMetaTag);
954
            }
955
        }
956
957
        // write everything back to to xml file
958
        $oThemeConfig->saveXML($sQuestionConfigFilePath);
959
960
        return $aSuccess = [
961
            'message' => gT('Question Theme has been sucessfully converted to LimeSurvey 4'),
962
            'success' => true
963
        ];
964
    }
965
966
    /**
967
     * Return the question theme custom attributes values
968
     * -- gets coreAttributes from xml-file
969
     * -- gets additional attributes from extended theme (if theme is extended)
970
     * -- gets "own" attributes via plugin
971
     *
972
     * @param string  $type question type (this is the attribute 'question_type' in table question_theme)
973
     * @param string  $sQuestionThemeName : question theme name
974
     *
975
     * @return array : the attribute settings for this question type
976
     * @throws Exception when question type attributes are not available
977
     */
978
    public static function getQuestionThemeAttributeValues($type, $sQuestionThemeName = null)
979
    {
980
        $aQuestionAttributes = array();
981
        $xmlConfigPath = self::getQuestionXMLPathForBaseType($type);
982
983
        if (\PHP_VERSION_ID < 80000) {
984
            libxml_disable_entity_loader(false);
985
        }            
986
        $oCoreConfig = simplexml_load_file($xmlConfigPath);
987
        $aCoreAttributes = json_decode(json_encode((array)$oCoreConfig), true);
988
        if (\PHP_VERSION_ID < 80000) {
989
            libxml_disable_entity_loader(true);
990
        }            
991
        if (!isset($aCoreAttributes['attributes']['attribute'])) {
992
            throw new Exception("Question type attributes not available!");
993
        }
994
        foreach ($aCoreAttributes['attributes']['attribute'] as $aCoreAttribute) {
995
            $aQuestionAttributes[$aCoreAttribute['name']] = $aCoreAttribute;
996
        }
997
998
        $additionalAttributes = array();
999
        if ($sQuestionThemeName !== null) {
1000
            $additionalAttributes = self::getAdditionalAttrFromExtendedTheme($sQuestionThemeName, $type);
1001
        }
1002
1003
        return array_merge(
1004
            $aQuestionAttributes,
1005
            $additionalAttributes,
1006
            QuestionAttribute::getOwnQuestionAttributesViaPlugin()
1007
        );
1008
    }
1009
1010
    /**
1011
     * Gets the additional attributes for an extended theme from xml file.
1012
     * If there are no attributes, an empty array is returned
1013
     *
1014
     * @param string $sQuestionThemeName the question theme name (see table question theme "name")
1015
     * @param string $type   the extended typ (see table question_themes "extends")
1016
     * @return array additional attributes for an extended theme or empty array
1017
     */
1018
    public static function getAdditionalAttrFromExtendedTheme($sQuestionThemeName, $type)
1019
    {
1020
        $additionalAttributes = array();
1021
        if (\PHP_VERSION_ID < 80000) {
1022
            libxml_disable_entity_loader(false);
1023
        }            
1024
            $questionTheme = QuestionTheme::model()->findByAttributes([], 'name = :name AND extends = :extends', ['name' => $sQuestionThemeName, 'extends' => $type]);
1025
        if ($questionTheme !== null) {
1026
            $xml_config = simplexml_load_file(App()->getConfig('rootdir') . '/' . $questionTheme['xml_path'] . '/config.xml');
1027
            $attributes = json_decode(json_encode((array)$xml_config->attributes), true);
1028
        }
1029
        if (\PHP_VERSION_ID < 80000) {
1030
            libxml_disable_entity_loader(true);
1031
        }            
1032
1033
        if (!empty($attributes)) {
1034
            if (!empty($attributes['attribute']['name'])) {
1035
                // Only one attribute set in config: need an array of attributes
1036
                $attributes['attribute'] = array($attributes['attribute']);
1037
            }
1038
            // Create array of attribute with name as key
1039
            $defaultQuestionAttributeValues = QuestionAttribute::getDefaultSettings();
1040
            foreach ($attributes['attribute'] as $attribute) {
1041
                if (!empty($attribute['name'])) {
1042
                    // inputtype is text by default
1043
                    $additionalAttributes[$attribute['name']] = array_merge($defaultQuestionAttributeValues, $attribute);
1044
                }
1045
            }
1046
        }
1047
        return $additionalAttributes;
1048
    }
1049
}
1050