Passed
Push — master ( 346325...dc78e4 )
by
unknown
23:25 queued 14:41
created

chExerciseExtractAnswerIds()   D

Complexity

Conditions 21
Paths 92

Size

Total Lines 60
Code Lines 33

Duplication

Lines 0
Ratio 0 %

Importance

Changes 2
Bugs 1 Features 0
Metric Value
cc 21
eloc 33
nop 1
dl 0
loc 60
rs 4.1666
c 2
b 1
f 0
nc 92

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
declare(strict_types=1);
4
5
/* For licensing terms, see /license.txt */
6
7
use Chamilo\CoreBundle\Enums\ActionIcon;
8
use Chamilo\CoreBundle\Enums\ObjectIcon;
9
use Chamilo\CoreBundle\Enums\StateIcon;
10
use Chamilo\CoreBundle\Framework\Container;
11
use Chamilo\CourseBundle\Entity\CQuizQuestion;
12
use Chamilo\CourseBundle\Entity\CQuizRelQuestion;
13
use ChamiloSession as Session;
14
15
/**
16
 * Exercise feedback modal (self-evaluation / popup).
17
 *
18
 * This script is loaded inside the global modal to show immediate feedback
19
 * (score, expected answers and navigation links) for a single question.
20
 *
21
 * @author Julio Montoya <[email protected]>
22
 */
23
require_once __DIR__.'/../inc/global.inc.php';
24
$current_course_tool = TOOL_QUIZ;
25
26
api_protect_course_script();
27
28
require_once api_get_path(LIBRARY_PATH).'geometry.lib.php';
29
30
/** @var Exercise $objExercise */
31
$objExercise = Session::read('objExercise');
32
$exerciseResult = Session::read('exerciseResult');
33
34
if (empty($objExercise)) {
35
    api_not_allowed();
36
}
37
38
$feedbackType = $objExercise->getFeedbackType();
39
$exerciseType = $objExercise->type;
40
41
// Adaptive mode: direct feedback behaves as an adaptive flow.
42
$isAdaptative = (EXERCISE_FEEDBACK_TYPE_DIRECT === $feedbackType);
43
44
// Only direct or popup feedback are supported here.
45
if (!in_array($feedbackType, [EXERCISE_FEEDBACK_TYPE_DIRECT, EXERCISE_FEEDBACK_TYPE_POPUP], true)) {
46
    api_not_allowed();
47
}
48
49
$learnpath_id = (int) ($_REQUEST['learnpath_id'] ?? 0);
50
$learnpath_item_id = (int) ($_REQUEST['learnpath_item_id'] ?? 0);
51
$learnpath_item_view_id = (int) ($_REQUEST['learnpath_item_view_id'] ?? 0);
52
$exerciseId = (int) ($_GET['exerciseId'] ?? 0);
53
$exeId = (int) (Session::read('exe_id') ?? 0);
54
$preview = (int) ($_GET['preview'] ?? 0);
55
56
$cidreq = api_get_cidreq();
57
58
// Base URLs used for navigation from the modal.
59
$exerciseBaseUrl = api_get_path(WEB_CODE_PATH).'exercise/exercise_submit.php?'
60
    .$cidreq
61
    .'&exerciseId='.$exerciseId
62
    .'&learnpath_id='.$learnpath_id
63
    .'&learnpath_item_id='.$learnpath_item_id
64
    .'&learnpath_item_view_id='.$learnpath_item_view_id;
65
66
if ($preview) {
67
    $exerciseBaseUrl .= '&preview='.$preview;
68
}
69
70
$exerciseResultUrl = api_get_path(WEB_CODE_PATH).'exercise/exercise_result.php?'
71
    .$cidreq
72
    .'&exe_id='.$exeId
73
    .'&learnpath_id='.$learnpath_id
74
    .'&learnpath_item_id='.$learnpath_item_id
75
    .'&learnpath_item_view_id='.$learnpath_item_view_id;
76
77
// Question list and current question index.
78
$questionList = array_values(Session::read('questionList') ?? []);
79
$questionNum = max(0, ((int) ($_GET['num'] ?? 1)) - 1);
80
$questionId = $questionList[$questionNum] ?? null;
81
82
$logPrefix = '[exercise_submit_modal] ';
83
84
/**
85
 * Small helper: determine if an answer container really has data.
86
 *
87
 * - For arrays: at least one element.
88
 * - For scalars: '' and null are considered empty. "0" is a valid answer.
89
 *
90
 * @param mixed $value
91
 */
92
function chExerciseHasAnswer($value): bool
93
{
94
    if (is_array($value)) {
95
        return count($value) > 0;
96
    }
97
98
    return null !== $value && '' !== $value;
99
}
100
101
/**
102
 * Normalize a question answer container into a flat list of selected answer IDs.
103
 *
104
 * This tries to detect the most probable pattern:
105
 * - id => id  (e.g. [69432 => '69432'])
106
 * - 0..N-1 => id (e.g. [0 => '69432', 1 => '69433'])
107
 * - scalar '69432'
108
 *
109
 * @param mixed $value
110
 */
111
function chExerciseExtractAnswerIds($value): array
112
{
113
    if (!is_array($value)) {
114
        return is_numeric($value) ? [(int) $value] : [];
115
    }
116
117
    $keys = array_keys($value);
118
    $vals = array_values($value);
119
120
    $allNumericKeys = true;
121
    foreach ($keys as $k) {
122
        if (!is_numeric($k)) {
123
            $allNumericKeys = false;
124
125
            break;
126
        }
127
    }
128
129
    $allNumericVals = true;
130
    foreach ($vals as $v) {
131
        if (!is_numeric($v)) {
132
            $allNumericVals = false;
133
134
            break;
135
        }
136
    }
137
138
    $sequentialKeys = $allNumericKeys && ($keys === range(0, count($keys) - 1));
139
140
    $ids = [];
141
142
    if ($allNumericKeys && $allNumericVals && $keys === $vals) {
143
        // Pattern: id => id
144
        $ids = $keys;
145
    } elseif ($sequentialKeys && $allNumericVals) {
146
        // Pattern: 0..N-1 => id
147
        $ids = $vals;
148
    } elseif ($allNumericKeys && !$allNumericVals) {
149
        // Fallback: keys look like ids
150
        $ids = $keys;
151
    } elseif (!$allNumericKeys && $allNumericVals) {
152
        // Fallback: values look like ids
153
        $ids = $vals;
154
    } else {
155
        // Worst case: keep all numeric keys and values
156
        foreach ($keys as $k) {
157
            if (is_numeric($k)) {
158
                $ids[] = (int) $k;
159
            }
160
        }
161
        foreach ($vals as $v) {
162
            if (is_numeric($v)) {
163
                $ids[] = (int) $v;
164
            }
165
        }
166
    }
167
168
    $ids = array_map('intval', $ids);
169
170
    return array_values(array_unique($ids));
171
}
172
173
if (!$questionId) {
174
    exit;
175
}
176
177
// Try to read answer/hotspot from GET first.
178
$choiceValue = $_GET['choice'][$questionId] ?? ($_GET['choice'] ?? '');
179
$hotSpot = $_GET['hotspot'][$questionId] ?? ($_GET['hotspot'] ?? '');
180
$tryAgain = (1 === (int) ($_GET['tryagain'] ?? 0));
181
$loaded = (int) ($_GET['loaded'] ?? 0);
182
183
// Question entity.
184
$repo = Container::getQuestionRepository();
185
186
/** @var CQuizQuestion|null $question */
187
$question = $repo->find($questionId);
188
189
if (null === $question) {
190
    exit;
191
}
192
193
// Relationship to get adaptive destinations.
194
$entityManager = Database::getManager();
195
196
/** @var CQuizRelQuestion|null $rel */
197
$rel = $entityManager
198
    ->getRepository(CQuizRelQuestion::class)
199
    ->findOneBy([
200
        'quiz' => $exerciseId,
201
        'question' => $questionId,
202
    ])
203
;
204
205
$destinationArray = [];
206
if ($rel && $rel->getDestination()) {
207
    $decoded = json_decode($rel->getDestination(), true);
208
    if (is_array($decoded)) {
209
        $destinationArray = $decoded;
210
    }
211
}
212
213
// Normalize failure destinations.
214
$failure = [];
215
if (isset($destinationArray['failure'])) {
216
    $failure = is_array($destinationArray['failure'])
217
        ? $destinationArray['failure']
218
        : [$destinationArray['failure']];
219
}
220
221
$allowTryAgain = $tryAgain && !empty($failure) && in_array('repeat', $failure, true);
222
223
// If student clicked "Try again", clear previous answer for this question.
224
if ($allowTryAgain && is_array($exerciseResult)) {
225
    unset($exerciseResult[$questionId]);
226
}
227
228
/*
229
 * If we still have no choice/hotspot, we need to fetch the answer from the
230
 * main exercise form (frm_exercise) in the parent page.
231
 *
232
 * First pass: redirect to the same script with "loaded=1".
233
 * Second pass (loaded=1 and still no answer): inject JS that reads the
234
 * current choice from the form and performs a third GET with the proper
235
 * parameters. That third GET will finally compute the feedback.
236
 */
237
if (
238
    !chExerciseHasAnswer($choiceValue)
239
    && !chExerciseHasAnswer($hotSpot)
240
    && !isset($_GET['loaded'])
241
) {
242
    $params = $_REQUEST;
243
    $params['loaded'] = 1;
244
    $redirectUrl = $_SERVER['PHP_SELF'].'?'.http_build_query($params);
245
246
    header("Location: $redirectUrl");
247
248
    exit;
249
}
250
251
if (
252
    !chExerciseHasAnswer($choiceValue)
253
    && !chExerciseHasAnswer($hotSpot)
254
    && isset($_GET['loaded'])
255
) {
256
    $url = api_get_path(WEB_CODE_PATH).'exercise/exercise_submit_modal.php?'.api_get_cidreq()
257
        .'&loaded=1&exerciseId='.$exerciseId
258
        .'&num='.($questionNum + 1)
259
        .'&learnpath_id='.$learnpath_id
260
        .'&learnpath_item_id='.$learnpath_item_id
261
        .'&learnpath_item_view_id='.$learnpath_item_view_id
262
        .'&preview='.$preview;
263
264
    echo "<script>
265
    $(document).ready(function() {
266
        var f = document.frm_exercise;
267
        if (!f) {
268
            // No exercise form found; nothing to do.
269
            if (window.console && console.warn) {
270
                console.warn('[exercise_submit_modal] frm_exercise not found in document');
271
            }
272
            return;
273
        }
274
275
        var finalUrl = '".addslashes($url)."';
276
277
        // Collect all controls for this question using the \"choice[{$questionId}]\" prefix.
278
        // This should work for:
279
        // - Single choice (radio)
280
        // - Multiple choice (checkboxes)
281
        // - Matching (selects)
282
        // - Fill in the blanks (text/textarea using choice[...] naming)
283
        // - Other types reusing the same prefix
284
        var controls = f.querySelectorAll(
285
            'input[name^=\"choice[{$questionId}]\"],'
286
          + 'select[name^=\"choice[{$questionId}]\"],'
287
          + 'textarea[name^=\"choice[{$questionId}]\"]'
288
        );
289
290
        var hasChoice = false;
291
292
        controls.forEach(function(el) {
293
            var type = (el.type || '').toLowerCase();
294
295
            // For checkboxes/radios we only keep checked ones.
296
            if ((type === 'checkbox' || type === 'radio') && !el.checked) {
297
                return;
298
            }
299
300
            var val = el.value;
301
            if (val === undefined || val === null || val === '') {
302
                return;
303
            }
304
305
            hasChoice = true;
306
307
            // Preserve the original field name so PHP builds the same array structure.
308
            finalUrl += '&' + encodeURIComponent(el.name) + '=' + encodeURIComponent(val);
309
        });
310
311
        // Hotspot input (if any).
312
        var hotspotInput = f.querySelector('input[name^=\"hotspot[{$questionId}]\"]');
313
        var hotspotVal = hotspotInput ? hotspotInput.value : '';
314
        var hasHotspot = !!hotspotVal;
315
316
        // avoid infinite loop when no answer is selected
317
        if (!hasChoice && !hasHotspot) {
318
            var \$container = \$('#global-modal .modal-body');
319
            if (!\$container.length) {
320
                // Fallback for legacy templates that use #global-modal-body.
321
                \$container = \$('#global-modal-body');
322
            }
323
            if (\$container.length) {
324
                \$container.html('<p>".addslashes(get_lang('Please select an answer before checking the result.'))."</p>');
325
            }
326
            if (window.console && console.warn) {
327
                console.warn('[exercise_submit_modal] No answer/hotspot found in frm_exercise; aborting extra request.');
328
            }
329
            return;
330
        }
331
332
        if (hotspotVal) {
333
            finalUrl += '&hotspot[{$questionId}]=' + encodeURIComponent(hotspotVal);
334
        }
335
336
        $.get(finalUrl, function(data) {
337
            // Prefer the standard global modal body used by exercise_submit.php.
338
            var \$container = \$('#global-modal .modal-body');
339
            if (!\$container.length) {
340
                // Fallback for legacy templates that use #global-modal-body.
341
                \$container = \$('#global-modal-body');
342
            }
343
            if (\$container.length) {
344
                \$container.html(data);
345
            } else if (window.console && console.warn) {
346
                console.warn('[exercise_submit_modal] No modal container found (#global-modal or #global-modal-body)');
347
            }
348
        });
349
    });
350
    </script>";
351
352
    exit;
353
}
354
355
/*
356
 * Merge the current choice into exerciseResult, keeping all previous answers.
357
 * At this point $allowTryAgain might have removed the previous entry for this
358
 * question id, so we will store the fresh, normalized value.
359
 */
360
if (!is_array($exerciseResult)) {
361
    $exerciseResult = [];
362
}
363
364
$answerType = $question->getType();
365
$showResult = $isAdaptative;
366
367
$objAnswerTmp = new Answer($questionId, api_get_course_int_id());
368
369
// Normalize choice value depending on answer type.
370
if (MULTIPLE_ANSWER == $answerType && is_array($choiceValue)) {
371
    // For multiple answers we expect an associative array id => id.
372
    $choiceValue = array_combine(array_values($choiceValue), array_values($choiceValue));
373
}
374
375
if (UNIQUE_ANSWER == $answerType && is_array($choiceValue)) {
376
    // For unique answer we keep a single selected id; prefer "id => id" format.
377
    $ids = chExerciseExtractAnswerIds($choiceValue);
378
    if (!empty($ids)) {
379
        $choiceValue = $ids[0];
380
    }
381
}
382
383
if (HOT_SPOT_DELINEATION == $answerType && is_array($hotSpot)) {
384
    // For hotspot delineation we keep coordinates in a dedicated structure.
385
    $choiceValue = $hotSpot[1] ?? '';
386
    $_SESSION['exerciseResultCoordinates'][$questionId] = $choiceValue;
387
    $_SESSION['hotspot_coord'][$questionId][1] = $objAnswerTmp->selectHotspotCoordinates(1);
388
    $_SESSION['hotspot_dest'][$questionId][1] = $objAnswerTmp->selectDestination(1);
389
}
390
391
// Only persist if we actually have an answer for this question.
392
if (chExerciseHasAnswer($choiceValue) || chExerciseHasAnswer($hotSpot)) {
393
    $exerciseResult[$questionId] = $choiceValue;
394
    Session::write('exerciseResult', $exerciseResult);
395
}
396
397
// Capture HTML output from manage_answer; we only use it for some types.
398
ob_start();
399
$result = $objExercise->manage_answer(
400
    $exeId,
401
    $questionId,
402
    $choiceValue,
403
    'exercise_result',
404
    [],
405
    EXERCISE_FEEDBACK_TYPE_POPUP === $feedbackType,
406
    false,
407
    $showResult,
408
    null,
409
    [],
410
    true,
411
    false,
412
    true
413
);
414
$manageAnswerHtmlContent = ob_get_clean();
415
416
// -----------------------------------------------------------------------------
417
// Decide success / failure (adaptive routing and feedback flags)
418
// -----------------------------------------------------------------------------
419
// We fully trust manage_answer() for the scoring logic and use the ratio
420
// score/weight to decide if the question is correct or not, similar to the
421
// regular exercise flow (save_exercise_by_now).
422
$contents = '';
423
$answerCorrect = false;
424
$partialCorrect = false;
425
426
$score = isset($result['score']) ? (float) $result['score'] : 0.0;
427
$weight = isset($result['weight']) ? (float) $result['weight'] : 0.0;
428
429
if ($weight > 0.0) {
430
    // Full success only when the achieved score reaches the max weight.
431
    $answerCorrect = ($score >= $weight);
432
    // Partial success when there is some score but not the full weight.
433
    $partialCorrect = !$answerCorrect && $score > 0.0;
434
} else {
435
    // Zero or undefined weight: any positive score counts as correct.
436
    $answerCorrect = ($score > 0.0);
437
    $partialCorrect = false;
438
}
439
440
$routeKey = $answerCorrect ? 'success' : 'failure';
441
442
// Compute destination based on adaptive routing or default sequential order.
443
$destinationId = null;
444
if ($isAdaptative && !empty($destinationArray) && isset($destinationArray[$routeKey])) {
445
    $firstDest = $destinationArray[$routeKey];
446
447
    if (is_string($firstDest) && is_numeric($firstDest)) {
448
        $firstDest = (int) $firstDest;
449
    }
450
451
    if ('repeat' === $firstDest) {
452
        // Repeat the same question.
453
        $destinationId = $questionId;
454
    } elseif (-1 === $firstDest) {
455
        // End of activity.
456
        $destinationId = -1;
457
    } elseif (is_int($firstDest)) {
458
        // Go to question with id = $firstDest.
459
        $destinationId = $firstDest;
460
    } elseif (is_string($firstDest) && str_starts_with($firstDest, '/')) {
461
        // Go to an external resource (relative path to WEB_PATH).
462
        $destinationId = $firstDest;
463
    }
464
} else {
465
    // Default: next question in the list (or -1 if there is no next question).
466
    $nextQuestion = $questionNum + 1;
467
    $destinationId = $questionList[$nextQuestion] ?? -1;
468
}
469
470
if (is_string($destinationId) && is_numeric($destinationId)) {
471
    $destinationId = (int) $destinationId;
472
}
473
474
// Build feedback contents depending on feedback model.
475
if ($isAdaptative && isset($result['correct_answer_id'])) {
476
    // Adaptive mode: show specific comments for correct answers.
477
    foreach ($result['correct_answer_id'] as $answerId) {
478
        $contents .= $objAnswerTmp->selectComment($answerId);
479
    }
480
} elseif (EXERCISE_FEEDBACK_TYPE_POPUP === $feedbackType) {
481
    $message = get_lang(
482
        $answerCorrect ? 'Correct' : ($partialCorrect ? 'PartialCorrect' : 'Incorrect')
483
    );
484
    $comments = '';
485
486
    if (HOT_SPOT_DELINEATION !== $answerType && isset($result['correct_answer_id'])) {
487
        $table = new HTML_Table(['class' => 'table data_table']);
488
        $table->setCellContents(0, 0, get_lang('Your answer'));
489
        if (DRAGGABLE !== $answerType) {
490
            $table->setCellContents(0, 1, get_lang('Comment'));
491
        }
492
493
        $row = 1;
494
        foreach ($result['correct_answer_id'] as $answerId) {
495
            $a = $objAnswerTmp->getAnswerByAutoId($answerId);
496
            $table->setCellContents(
497
                $row,
498
                0,
499
                $a['answer'] ?? $objAnswerTmp->selectAnswer($answerId)
500
            );
501
            $table->setCellContents(
502
                $row,
503
                1,
504
                $a['comment'] ?? $objAnswerTmp->selectComment($answerId)
505
            );
506
            $row++;
507
        }
508
509
        $comments = $table->toHtml();
510
    }
511
512
    // If there is no specific comment, at least show a basic message.
513
    if ('' === trim($comments)) {
514
        $comments = '<p>'.get_lang('No detailed feedback is available for this question.').'</p>';
515
    }
516
517
    $contents .= $comments;
518
519
    echo '
520
        <div class="modal-header">
521
            <h4 class="modal-title" id="global-modal-title">'.$message.'</h4>
522
        </div>';
523
}
524
525
// For hotspot delineation we keep the HTML generated by manage_answer.
526
if (HOT_SPOT_DELINEATION === $answerType) {
527
    $contents = $manageAnswerHtmlContent;
528
}
529
530
// Build navigation links for the adaptive / popup flow.
531
$links = '';
532
$navBranch = 'none';
533
$indexForLog = null;
534
535
// Small JS helper that navigates explicitly, without relying on SendEx().
536
echo '<script>
537
var chExerciseBaseUrl = "'.addslashes($exerciseBaseUrl).'";
538
var chExerciseResultUrl = "'.addslashes($exerciseResultUrl).'";
539
540
/**
541
 * Navigate to the given question index (0-based) or to the result page.
542
 *
543
 * idx >= 0 → go to exercise_submit.php with num = idx + 1
544
 * idx  = -1 → go to exercise_result.php
545
 *
546
 * tryAgain = true → append tryagain=1, used when repeating the same question.
547
 */
548
function chExerciseSendEx(idx, tryAgain) {
549
    // Always navigate in the current window/frame.
550
    // When the exercise is launched from a learning path, exercise_submit.php
551
    // runs inside a frame; using window.parent here would navigate the LP
552
    // shell away and break the learning path context.
553
    var target = window;
554
555
    if (idx === -1) {
556
        // End of activity → show results.
557
        target.location.href = chExerciseResultUrl;
558
        return false;
559
    }
560
561
    var qIndex = parseInt(idx, 10);
562
    if (isNaN(qIndex) || qIndex < 0) {
563
        if (window.console && console.warn) {
564
            console.warn("[exercise_submit_modal] Invalid idx for chExerciseSendEx:", idx);
565
        }
566
        return false;
567
    }
568
569
    // Questions are 1-based in the URL.
570
    var num = qIndex + 1;
571
    var url = chExerciseBaseUrl + "&num=" + num;
572
573
    if (tryAgain) {
574
        url += "&tryagain=1";
575
    }
576
577
    target.location.href = url;
578
579
    return false;
580
}
581
</script>';
582
583
if ($destinationId === $questionId) {
584
    // Repeat same question.
585
    $index = array_search($questionId, $questionList, true);
586
    $indexForLog = $index;
587
    $navBranch = 'repeatQuestion';
588
589
    $links .= Display::getMdiIcon(
590
        ActionIcon::REFRESH,
591
        'ch-tool-icon',
592
        'padding-left:0px;padding-right:5px;',
593
        ICON_SIZE_SMALL
594
    )
595
        .'<a onclick="return chExerciseSendEx('.$index.', true);" href="#">'
596
        .get_lang('Try again').'</a><br /><br />';
597
} elseif (-1 === $destinationId) {
598
    // End of activity.
599
    $navBranch = 'endActivity';
600
601
    $links .= Display::getMdiIcon(
602
        StateIcon::COMPLETE,
603
        'ch-tool-icon',
604
        'padding-left:0px;padding-right:5px;',
605
        ICON_SIZE_SMALL
606
    )
607
        .'<a onclick="return chExerciseSendEx(-1, false);" href="#">'
608
        .get_lang('End of activity').'</a><br /><br />';
609
} elseif (is_int($destinationId) && in_array($destinationId, $questionList, true)) {
610
    // Go to another question by id.
611
    $index = array_search($destinationId, $questionList, true);
612
    $indexForLog = $index;
613
    $navBranch = 'nextQuestion';
614
615
    $icon = Display::getMdiIcon(
616
        ObjectIcon::TEST,
617
        'ch-tool-icon',
618
        'padding-left:0px;padding-right:5px;',
619
        ICON_SIZE_SMALL
620
    );
621
    $links .= '<a onclick="return chExerciseSendEx('.$index.', false);" href="#">'
622
        .get_lang('Question').' '.($index + 1).'</a>&nbsp;'.$icon;
623
} elseif (is_string($destinationId) && str_starts_with($destinationId, '/')) {
624
    // External resource.
625
    $navBranch = 'externalResource';
626
627
    $icon = Display::getMdiIcon(
628
        ObjectIcon::LINK,
629
        'ch-tool-icon',
630
        'padding-left:0px;padding-right:5px;',
631
        ICON_SIZE_SMALL
632
    );
633
    $fullUrl = api_get_path(WEB_PATH).ltrim($destinationId, '/');
634
    $links .= '<a href="'.$fullUrl.'">'.get_lang('Go to resource').'</a>&nbsp;'.$icon;
635
}
636
637
// Body + navigation block.
638
echo '<div>'.$contents.'</div>';
639
echo '<div style="padding-left: 450px"><h5>'.$links.'</h5></div>';
640