web.php 178 KB
Newer Older
1
2
3
4
5
<?php
/**
 *
 * @package    mahara
 * @subpackage core
6
 * @author     Catalyst IT Ltd
7
8
 * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL version 3 or later
 * @copyright  For copyright information on Mahara, please see the README file distributed with this software.
9
10
11
 * @copyright  (C) portions from Moodle, (C) Martin Dougiamas http://dougiamas.com
 */

12
use Mahara\Dwoo_Mahara as Dwoo_Mahara;
13
14
15
defined('INTERNAL') || die();


16
function smarty_core() {
17
18
    require_once(__DIR__ . '/dwoo/vendor/autoload.php');
    require_once(__DIR__ . '/dwoo/mahara/Dwoo_Mahara.php');
19

20
    return new Dwoo_Mahara();
21
22
23
}


24
25
26
27
28
29
30
31
32

/**
 * Function to set an optional page icon. Mahara uses fontawesome for icons by default,
 * (http://fortawesome.github.io/Font-Awesome/icons/) but this can be overridden at the theme
 * level by supplying a different icon font + css.
 *
 * @param Smarty | an initialized smarty object
 * @param String | the name of the icon to include (eg "icon-university")
 */
Pat Kira's avatar
Pat Kira committed
33
function setpageicon($smarty, $icon) {
34
35
36
37
38
39
    $smarty->assign('pageicon', 'icon ' . $icon);
}



/**
40
41
 * Helper function (called by smarty()) to determine what stylesheets to include
 * on the page (based on constants, global variables, and $extraconfig)
42
 *
43
44
 * @param $stylesheets Stylesheets we already know we're going to need
 * @param $extraconfig Extra configuration passed to smarty()
45
46
47
 * @return array
 */

Pat Kira's avatar
Pat Kira committed
48
function get_stylesheets_for_current_page($stylesheets, $extraconfig) {
49
50
51
52
53
54
55

    global $USER, $SESSION, $THEME, $HEADDATA, $langselectform;

    // stylesheet set up - if we're in a plugin also get its stylesheet
    $allstylesheets = $THEME->get_url('style/style.css', true);

    // determine if we want to include the parent css
Pat Kira's avatar
Pat Kira committed
56
    if (isset($THEME->overrideparentcss) && $THEME->overrideparentcss && $THEME->parent) {
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
        unset($allstylesheets[$THEME->parent]);
    }

    $stylesheets = array_merge($stylesheets, array_reverse(array_values($allstylesheets)));

    if (defined('SECTION_PLUGINTYPE') && defined('SECTION_PLUGINNAME') && SECTION_PLUGINTYPE != 'core') {
        if ($pluginsheets = $THEME->get_url('style/style.css', true, SECTION_PLUGINTYPE . '/' . SECTION_PLUGINNAME)) {
            $stylesheets = array_merge($stylesheets, array_reverse($pluginsheets));
        }
    }

    if ($adminsection = in_admin_section()) {
        if ($adminsheets = $THEME->get_url('style/admin.css', true)) {
            $stylesheets = array_merge($stylesheets, array_reverse($adminsheets));
        }
    }

    if (get_config('developermode') & DEVMODE_DEBUGCSS) {
        $stylesheets[] = get_config('wwwroot') . 'theme/debug.css';
    }

    // look for extra stylesheets
    if (isset($extraconfig['stylesheets']) && is_array($extraconfig['stylesheets'])) {
        foreach ($extraconfig['stylesheets'] as $extrasheet) {
            if ($sheets = $THEME->get_url($extrasheet, true)) {
                $stylesheets = array_merge($stylesheets, array_reverse(array_values($sheets)));
            }
        }
    }
86
87
88
89

    // Only add additional stylesheets when configurable theme is set.
    if ($THEME->basename == 'custom') {
        $sheets = $THEME->additional_stylesheets();
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
        $stylesheets = array_merge($stylesheets, $sheets);
    }

    // Give the skin a chance to affect the page
    if (!empty($extraconfig['skin'])) {
        require_once(get_config('docroot').'/lib/skin.php');
        $skinobj = new Skin($extraconfig['skin']['skinid']);
        $viewid = isset($extraconfig['skin']['viewid']) ? $extraconfig['skin']['viewid'] : null;
        $stylesheets = array_merge($stylesheets, $skinobj->get_stylesheets($viewid));
    }

    $langdirection = get_string('thisdirection', 'langconfig');

    // Include rtl.css for right-to-left langs
    if ($langdirection == 'rtl') {
        $smarty->assign('LANGDIRECTION', 'rtl');
        if ($rtlsheets = $THEME->get_url('style/rtl.css', true)) {
            $stylesheets = array_merge($stylesheets, array_reverse($rtlsheets));
        }
    }

    $stylesheets = append_version_number($stylesheets);

    return $stylesheets;
}

116
117
118
119
120
121
122
123
124
/**
* True if we are not in admin, institution or admin section
*/
function user_personal_section() {
    $usersection = !defined('ADMIN') && !defined('STAFF') && !defined('INSTITUTIONALADMIN') &&
        !defined('INSTITUTIONALSTAFF') && !defined('GROUP') && !defined('CREATEGROUP');

    return $usersection ? 1 : 0;
}
Pat Kira's avatar
Pat Kira committed
125

126
127
128
129
/**
 * This function creates a Smarty object and sets it up for use within our
 * podclass app, setting up some variables.
 *
130
131
 * WARNING: If you are using pieforms, set them up BEFORE calling this function.
 *
132
133
 * The variables that it sets up are:
 *
134
 * - WWWROOT: The base url for the Mahara system
135
136
137
138
139
 * - USER: The user object
 * - JAVASCRIPT: A list of javascript files to include in the header.  This
 *   list is passed into this function (see below).
 * - HEADERS: An array of any further headers to set.  Each header is just
 *   straight HTML (see below).
140
141
 * - PUBLIC: Set true if this page is a public page
 * - MAINNAV: Array defining the main navigation
142
 *
143
 * @param $javascript A list of javascript includes.  Each include should be just
144
 *                    the name of a file, and reside in js/{filename}
145
146
147
 * @param $headers    A list of additional headers.  These are to be specified as
 *                    actual HTML.
 * @param $strings    A list of language strings required by the javascript code.
148
 * @return Dwoo_Mahara
149
 */
150

151
152


153
function smarty($javascript = array(), $headers = array(), $pagestrings = array(), $extraconfig = array()) {
154
    global $USER, $SESSION, $THEME, $HEADDATA, $langselectform, $CFG, $viewid;
155
156
157
158
159
160
161
162
163
164
165

    if (!is_array($headers)) {
        $headers = array();
    }
    if (!is_array($pagestrings)) {
        $pagestrings = array();
    }
    if (!is_array($extraconfig)) {
        $extraconfig = array();
    }

166
    $sideblocks = array();
167
168
169
170
171
    // Some things like die_info() will try and create a smarty() call when we are already in one, which causes
    // language_select_form() to throw headdata error as it is called twice.
    if (!isset($langselectform)) {
        $langselectform = language_select_form();
    }
172
    $sideblock_menu = array();
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
    if (get_config('installed')) {
        // Fetch all the core side blocks now to avoid any 'set headdata' before smarty_core() problems
        $authgenerateloginform = auth_generate_login_form();
        $isloginblockvisible = !$USER->is_logged_in() && !get_config('siteclosedforupgrade') && get_config('showloginsideblock');
        $loginsideblock = array(
            'name'   => 'login',
            'weight' => -10,
            'id'     => 'sb-loginbox',
            'data'   => array('loginform' => $authgenerateloginform),
            'visible' => $isloginblockvisible,
            'template' => 'sideblocks/login.tpl',
            'smarty' => array('SHOWLOGINBLOCK' => $isloginblockvisible),
        );
        sideblock_template($loginsideblock, $sideblock_menu);
        sideblock_template(site_menu(), $sideblock_menu);
        sideblock_template(tags_sideblock(), $sideblock_menu);
        sideblock_template(selfsearch_sideblock(), $sideblock_menu);
        sideblock_template(profile_sideblock(), $sideblock_menu);
        sideblock_template(onlineusers_sideblock(), $sideblock_menu);
        sideblock_template(progressbar_sideblock(), $sideblock_menu);
        sideblock_template(ssopeer_sideblock(), $sideblock_menu);
        sideblock_template(quota_sideblock(), $sideblock_menu);
        if (isset($extraconfig['sideblocks']) && is_array($extraconfig['sideblocks'])) {
            foreach ($extraconfig['sideblocks'] as $sideblock) {
                sideblock_template($sideblock, $sideblock_menu);
            }
199
        }
200
201
202
203
204
205
206
207
208
        // local_sideblocks_update allows sites to customise the sideblocks by munging the $sideblock_menu array.
        if (function_exists('local_sideblocks_update')) {
            local_sideblocks_update($sideblock_menu);
        }
        // Remove those that are not visible before displaying
        foreach ($sideblock_menu as $sbk => $sbv) {
            if (empty($sbv['visible'])) {
                unset($sideblock_menu[$sbk]);
            }
209
        }
210
        usort($sideblock_menu, "sort_menu_by_weight");
Gregor Anzelj's avatar
Gregor Anzelj committed
211
    }
212

213
214
    $smarty = smarty_core();

215
    $wwwroot = get_config('wwwroot');
Aaron Wells's avatar
Aaron Wells committed
216
    // NOTE: not using jswwwroot - it seems to wreck image paths if you
217
    // drag them around the wysiwyg editor
218
    $jswwwroot = json_encode($wwwroot);
Martyn Smith's avatar
Martyn Smith committed
219

220
221
222
223
224
225
226
227
228
229
    // Workaround for $cfg->cleanurlusersubdomains.
    // When cleanurlusersubdomains is on, ajax requests might come from somewhere other than
    // the wwwroot.  To avoid cross-domain requests, set a js variable when this page is on a
    // different subdomain, and let the ajax wrapper function sendjsonrequest rewrite its url
    // if necessary.
    if (get_config('cleanurls') && get_config('cleanurlusersubdomains')) {
        if ($requesthost = get_requested_host_name()) {
            $wwwrootparts = parse_url($wwwroot);
            if ($wwwrootparts['host'] != $requesthost) {
                $fakewwwroot = $wwwrootparts['scheme'] . '://' . $requesthost . '/';
230
                $headers[] = '<script>var fakewwwroot = ' . json_encode($fakewwwroot) . ';</script>';
231
232
233
234
            }
        }
    }

Martyn Smith's avatar
Martyn Smith committed
235
    $theme_list = array();
236
    $adminsection = in_admin_section();
Aaron Wells's avatar
Aaron Wells committed
237

238
239
    if (function_exists('pieform_get_headdata')) {
        $headers = array_merge($headers, pieform_get_headdata());
240
241
242
        if (!defined('PIEFORM_GOT_HEADDATA')) {
          define('PIEFORM_GOT_HEADDATA', 1);
        }
243
    }
244

245
246
247
    // Define the stylesheets array early so that javascript modules can add extras
    $stylesheets = array();

Aaron Wells's avatar
Aaron Wells committed
248
    // Insert the appropriate javascript tags
249
    $javascript_array = array();
250
    $jsroot = $wwwroot . 'js/';
251

252
253
    $langdirection = get_string('thisdirection', 'langconfig');

254
    // Make jQuery accessible with $j
255
    $javascript_array[] = $jsroot . 'jquery/jquery.js';
256
    $headers[] = '<script>$j=jQuery;</script>';
257

258
259
    // If necessary, load MathJax configuration
    if (get_config('mathjax')) {
260
        $headers[] = '<script>'.get_config('mathjaxconfig').'</script>';
261
262
    }

Richard Mansfield's avatar
Richard Mansfield committed
263
    // TinyMCE must be included first for some reason we're not sure about
264
265
266
267
    //
    // Note: we do not display tinyMCE for mobile devices
    // as it doesn't work on some of them and can
    // disable the editing of a textarea field
268
    if (is_html_editor_enabled()) {
269
270
        $checkarray = array(&$javascript, &$headers);
        $found_tinymce = false;
271
272
273
274
275
276
277
278
279
        $tinymceviewid = 'null';
        if ($inpersonalarea = user_personal_section()) {
            if (defined('SECTION_PAGE') && (SECTION_PAGE == 'view' || SECTION_PAGE == 'blocks' || SECTION_PAGE == 'editlayout')) {
                if (isset($viewid) && $viewid > 0) {
                    $tinymceviewid = $viewid;
                }
            }
        }

280
281
282
283
        foreach ($checkarray as &$check) {
            if (($key = array_search('tinymce', $check)) !== false || ($key = array_search('tinytinymce', $check)) !== false) {
                if (!$found_tinymce) {
                    $found_tinymce = $check[$key];
284
                    $javascript_array[] = $wwwroot . 'artefact/file/js/filebrowser.js';
285
                    $javascript_array[] = $jsroot . 'switchbox.js';
286
287
                    $javascript_array[] = $jsroot . 'tinymce/tinymce.js';
                    $stylesheets = array_merge($stylesheets, array_reverse(array_values($THEME->get_url('style/tinymceskin.css', true))));
288
                    $content_css = json_encode($THEME->get_url('style/tinymce.css'));
289
290
                    $language = current_language();
                    $language = substr($language, 0, ((substr_count($language, '_') > 0) ? 5 : 2));
291
                    if ($language != 'en' && !file_exists(get_config('docroot') . 'js/tinymce/langs/' . $language . '.js')) {
292
293
294
295
296
297
298
299
                        // In case the language file exists as a string with both lower and upper case, eg fr_FR we test for this
                        $language = substr($language, 0, 2) . '_' . strtoupper(substr($language, 0, 2));
                        if (!file_exists(get_config('docroot') . 'js/tinymce/langs/' . $language . '.js')) {
                            // In case we fail to find a language of 5 chars, eg pt_BR (Portugese, Brazil) we try the 'parent' pt (Portugese)
                            $language = substr($language, 0, 2);
                            if ($language != 'en' && !file_exists(get_config('docroot') . 'js/tinymce/langs/' . $language . '.js')) {
                                $language = 'en';
                            }
300
                        }
301
302
                    }
                    $extrasetup = isset($extraconfig['tinymcesetup']) ? $extraconfig['tinymcesetup'] : '';
303
                    $extramceconfig = isset($extraconfig['tinymceconfig']) ? $extraconfig['tinymceconfig'] : '';
304

305
                    // Check whether to make the spellchecker available
306
                    if (get_config('tinymcespellcheckerengine')) {
307
                        $spellchecker = ',spellchecker';
308
                        $spellchecker_toolbar = '| spellchecker';
309
                        $spellchecker_config = "gecko_spellcheck : false, spellchecker_rpc_url : \"{$jsroot}tinymce/plugins/spellchecker/spellchecker.php\",";
310
311
                    }
                    else {
312
                        $spellchecker = $spellchecker_toolbar = '';
313
314
                        $spellchecker_config = 'gecko_spellcheck : true,';
                    }
315
316
                    $mathslate = (get_config('mathjax')) ? 'mathslate' : '';
                    $mathslateplugin = !empty($mathslate) ? ',' . $mathslate : '';
317
                    $toolbar = array(
318
                        null,
319
                        '"toolbar_toggle | formatselect | bold italic | bullist numlist | link unlink | imagebrowser | undo redo"',
320
                        '"underline strikethrough subscript superscript | alignleft aligncenter alignright alignjustify | outdent indent | forecolor backcolor | ltr rtl | fullscreen"',
321
                        '"fontselect | fontsizeselect | emoticons nonbreaking charmap ' . $mathslate . ' ' . $spellchecker_toolbar . ' | table | removeformat pastetext | anchor | code"',
322
323
324
325
326
327
                    );

                    // For right-to-left langs, reverse button order & align controls right.
                    $tinymce_langdir = $langdirection == 'rtl' ? 'rtl' : 'ltr';
                    $toolbar_align = 'left';

328
329
330
331
                    // Language strings required for TinyMCE
                    $pagestrings['mahara'] = isset($pagestrings['mahara']) ? $pagestrings['mahara'] : array();
                    $pagestrings['mahara'][] = 'attachedimage';

332
333
334
335
336
337
338
339
340
341
342
                    $tinymceinitbehatsetup = '';
                    $tinymcebehatsetup = '';
                    if (defined('BEHAT_TEST')) {
                        $tinymceinitbehatsetup = 'window.isEditorInitializing = false;';
                        $tinymcebehatsetup = <<<EOF
        ed.on('PreInit', function(ed) {
            window.isEditorInitializing = true;
        });
EOF;
                    }

343
                    if ($check[$key] == 'tinymce') {
344
345
                        $tinymceconfig = <<<EOF
    theme: "modern",
346
    plugins: "tooltoggle,textcolor,visualblocks,wordcount,link,lists,imagebrowser,table,emoticons{$spellchecker},paste,code,fullscreen,directionality,searchreplace,nonbreaking,charmap{$mathslateplugin},anchor",
347
    skin: 'light',
348
349
    toolbar1: {$toolbar[1]},
    toolbar2: {$toolbar[2]},
350
    toolbar3: {$toolbar[3]},
351
    menubar: false,
352
    fix_list_elements: true,
353
    image_advtab: true,
354
    table_style_by_css: true,
355
    {$spellchecker_config}
356
EOF;
357
358
                    }
                    else {
359
360
361
                        $tinymceconfig = <<<EOF
    selector: "textarea.tinywysiwyg",
    theme: "modern",
362
    skin: 'light',
363
364
    plugins: "fullscreen,autoresize",
    toolbar: {$toolbar[0]},
365
EOF;
366
                    }
367
$samepage = get_string('samepage', 'mahara');
368
                    $headers[] = <<<EOF
369
<script>
370
tinyMCE.init({
371
372
    {$tinymceconfig}
    schema: 'html4',
373
374
375
376
377
378
379
    extended_valid_elements:
        "object[width|height|classid|codebase]"
        + ",param[name|value]"
        + ",embed[src|type|width|height|flashvars|wmode]"
        + ",script[src,type,language]"
        + ",ul[id|type|compact]"
        + ",iframe[src|width|height|name|scrolling|frameborder|allowfullscreen|webkitallowfullscreen|mozallowfullscreen|longdesc|marginheight|marginwidth|align|title|class|type]"
380
        + ",a[id|class|title|href|name|target]"
381
        + ",button[id|class|title]"
382
    ,urlconverter_callback : "custom_urlconvert",
383
    language: '{$language}',
384
    directionality: "{$tinymce_langdir}",
385
    content_css : {$content_css},
386
    font_formats: 'Andale Mono=andale mono,times;Arial=arial,helvetica,sans-serif;Arial Black=arial black,avant garde;Book Antiqua=book antiqua,palatino;Comic Sans MS=comic sans ms,sans-serif;Courier New=courier new,courier;Georgia=georgia,palatino;Helvetica=helvetica;Impact=impact,chicago;Open Sans=Open Sans;Symbol=symbol;Tahoma=tahoma,arial,helvetica,sans-serif;Terminal=terminal,monaco;Times New Roman=times new roman,times;Trebuchet MS=trebuchet ms,geneva;Verdana=verdana,geneva;Webdings=webdings;Wingdings=wingdings,zapf dingbats;',
387
    remove_script_host: false,
388
    relative_urls: false,
389
390
391
392
393
    target_list: [
        {title: 'None', value: ''},
        {title: "{$samepage}", value: '_self'}, // This one is not translated in tinymce lang files
        {title: 'New window', value: '_blank'}
    ],
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
    link_list: function(success) {
        // Only show the list of links in the normal user section
        if ({$inpersonalarea}) {
            var params = {
                'viewid': {$tinymceviewid}
            }
            sendjsonrequest(config['wwwroot'] + 'json/tinymceviewlist.json.php',  params, 'POST', function(data) {
                if (data.count > 0) {
                    success(JSON.parse(data.data));
                }
                else {
                    success(''); // stop showing list with only option being 'none'
                }
            });
        }
        else {
            success(''); // stop showing list with only option being 'none'
        }
    },
413
    'branding': false,
Aaron Wells's avatar
Aaron Wells committed
414
    cache_suffix: '?v={$CFG->cacheversion}',
415
    {$extramceconfig}
416
    setup: function(ed) {
417
        {$tinymcebehatsetup}
418
        ed.on('init', function(ed) {
419
        {$tinymceinitbehatsetup}
420
            if (typeof(editor_to_focus) == 'string' && ed.editorId == editor_to_focus) {
421
                ed.trigger("focus");
422
423
            }
        });
424
425
426
        ed.on('keyup change', function (e) {
            checkTextareaMaxLength(ed.settings.id);
        });
427
428
        ed.on('LoadContent', function(e) {
            // Hide all the 2nd/3rd row menu buttons
429
            jQuery('.mce-toolbar.mce-first').siblings().addClass('d-none');
430
431
432
433
434
435
436
437
438
439
440
            // The tinymce fullscreen mode does not work properly in a transformed container div
            // such as div.vertcentre
            // and IE doesn't like a preset z-index
            // This work-around will remove/add classes: .vertcenter .configure .blockinstane
            // of the configure block div
            // when toggling fullscreen
            jQuery('div[aria-label="Fullscreen"]').on('click', function(e) {
                jQuery('div#configureblock').toggleClass('vertcentre');
                jQuery('div#configureblock').toggleClass('blockinstance');
                jQuery('div#configureblock').toggleClass('configure');
            });
441
        });
442
        {$extrasetup}
443
    }
444
});
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461

function imageBrowserConfigSuccess(form, data) {
    // handle updates to file browser
    // final form submission handled by tinymce plugin
    if (data.formelementsuccess) {
        eval(data.formelementsuccess + '(form, data)');
        return;
    }
}

function imageBrowserConfigError(form, data) {
    if (data.formelementerror) {
        eval(data.formelementerror + '(form, data)');
        return;
    }
}

462
function custom_urlconvert (u, n, e) {
463
464
    // Don't convert the url on the skype status buttons.
    if (u.indexOf('skype:') == 0) {
465
      return u;
466
467
    }
    var t = tinyMCE.activeEditor, s = t.settings;
468

469
470
    // Don't convert link href since thats the CSS files that gets loaded into the editor also skip local file URLs
    if (!s.convert_urls || (e && e.nodeName == 'LINK') || u.indexOf('file:') === 0)
471
472
      return u;

473
474
    // Convert to relative
    if (s.relative_urls)
475
476
      return t.documentBaseURI.toRelative(u);

477
478
    // Convert to absolute
    u = t.documentBaseURI.toAbsolute(u, s.remove_script_host);
479

480
    return u;
481
}
482
483
484
</script>

EOF;
485
486
487
488
489
490
491
                    unset($check[$key]);
                }
                else {
                    if ($check[$key] != $found_tinymce) {
                        log_warn('Two differently configured tinyMCE instances have been asked for on this page! This is not possible');
                    }
                    unset($check[$key]);
492
                }
493
            }
494

495
496
497
498
            // If any page adds jquery explicitly, remove it from the list
            if (($key = array_search('jquery', $check)) !== false) {
                unset($check[$key]);
            }
499
        }
500
    }
501
502
503
504
505
506
507
508
    else {
        if (($key = array_search('tinymce', $javascript)) !== false || ($key = array_search('tinytinymce', $javascript)) !== false) {
            unset($javascript[$key]);
        }
        if (($key = array_search('tinymce', $headers)) !== false || ($key = array_search('tinytinymce', $headers)) !== false) {
            unset($headers[$key]);
        }
    }
Martyn Smith's avatar
Martyn Smith committed
509
    $javascript_array[] = $jsroot . 'keyboardNavigation.js';
510

511
512
513
514
515
    //If necessary, load MathJax path
    if (get_config('mathjax')) {
        $javascript_array[] = get_config('mathjaxpath');
    }

516
    $strings = array();
517
518
519
520
521
522
523
524
525
    foreach ($pagestrings as $k => $v) {
        if (is_array($v)) {
            foreach ($v as $tag) {
                $strings[$tag] = get_raw_string($tag, $k);
            }
        }
        else {
            $strings[$k] = get_raw_string($k, $v);
        }
526
527
    }

528
    $jsstrings = jsstrings();
Martyn Smith's avatar
Martyn Smith committed
529
    $themepaths = themepaths();
530

Richard Mansfield's avatar
Richard Mansfield committed
531
    foreach ($javascript as $jsfile) {
532
533
534
535
        // For now, if there's no path in the js file, assume it's in
        // $jsroot and append '.js' to the name.  Later we may want to
        // ensure all smarty() calls include the full path to the js
        // file, with the proper extension.
536
        if (strpos($jsfile, '/') === false) {
537
            $javascript_array[] = $jsroot . $jsfile . '.js';
538
            if (isset($jsstrings[$jsfile])) {
539
540
541
                foreach ($jsstrings[$jsfile] as $section => $tags) {
                    foreach ($tags as $tag) {
                        $strings[$tag] = get_raw_string($tag, $section);
542
543
544
                    }
                }
            }
Martyn Smith's avatar
Martyn Smith committed
545
546
            if (isset($themepaths[$jsfile])) {
                foreach ($themepaths[$jsfile] as $themepath) {
Nigel McNie's avatar
Nigel McNie committed
547
                    $theme_list[$themepath] = $THEME->get_url($themepath);
Martyn Smith's avatar
Martyn Smith committed
548
549
                }
            }
550
        }
551
        else if (stripos($jsfile, 'http://') === false && stripos($jsfile, 'https://') === false) {
552
            // A local .js file with a fully specified path
553
            $javascript_array[] = $wwwroot . $jsfile;
554
555
556
557
558
559
            // If $jsfile is from a plugin or plugin's block, i.e.:
            // - plugintype/pluginname/js/foo.js
            // - plugintype/pluginname/blocktype/pluginname/js/foo.js
            // Then get js strings from static function jsstrings in:
            // - plugintype/pluginname/lib.php, or
            // - plugintype/pluginname/blocktype/pluginname/lib.php
560
            $bits = explode('/', $jsfile);
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
            $pluginname = false;
            $plugintype = false;
            $jsfilename = false;
            if (count($bits) == 4 && $bits[2] == 'js' && in_array($bits[0], plugin_types())) {
                $plugintype = $bits[0];
                $pluginname = $bits[1];
                $jsfilename = $bits[3];
            }
            if (count($bits) == 6 && $bits[0] == 'artefact' && $bits[2] == 'blocktype' && $bits[4] == 'js') {
                $plugintype = 'blocktype';
                $pluginname = $bits[3];
                $jsfilename = $bits[5];
            }
            if ($pluginname) {
                safe_require($plugintype, $pluginname);
                $pluginclass = generate_class_name($plugintype, $pluginname);
                $name = substr($jsfilename, 0, strpos($jsfilename, '.js'));
578
579
                if (is_callable(array($pluginclass, 'jsstrings'))) {
                    $tempstrings = call_static_method($pluginclass, 'jsstrings', $name);
580
581
582
                    foreach ($tempstrings as $section => $tags) {
                        foreach ($tags as $tag) {
                            $strings[$tag] = get_raw_string($tag, $section);
583
584
                        }
                    }
Richard Mansfield's avatar
Richard Mansfield committed
585
                }
586
587
588
589
                if (is_callable(array($pluginclass, 'jshelp'))) {
                    $tempstrings = call_static_method($pluginclass, 'jshelp', $name);
                    foreach ($tempstrings as $section => $tags) {
                        foreach ($tags as $tag) {
590
                            $strings[$tag . '.help'] = get_help_icon($plugintype, $pluginname, null, null,
591
592
593
594
                                                                     null, $tag);
                        }
                    }
                }
Martyn Smith's avatar
Martyn Smith committed
595
596
597
                if (is_callable(array($pluginclass, 'themepaths'))) {
                    $tmpthemepaths = call_static_method($pluginclass, 'themepaths', $name);
                    foreach ($tmpthemepaths as $themepath) {
Nigel McNie's avatar
Nigel McNie committed
598
                        $theme_list[$themepath] = $THEME->get_url($themepath);
Martyn Smith's avatar
Martyn Smith committed
599
600
                    }
                }
Martyn Smith's avatar
Martyn Smith committed
601
            }
Martyn Smith's avatar
Martyn Smith committed
602
        }
603
604
605
606
        else {
            // A remote .js file
            $javascript_array[] = $jsfile;
        }
607
    }
608
609

    $javascript_array[] = $jsroot . 'mahara.js';
610
    $javascript_array[] = $jsroot . 'formchangechecker.js';
611
    $javascript_array[] = $jsroot . 'textareamaxlengthchecker.js';
612

613
614
615
616
617
618
    // Load some event handler functions for checking if all AJAX requests have completed
    // when running behat tests
    if (defined('BEHAT_TEST')) {
        $javascript_array[] = get_config('wwwroot') . 'testing/frameworks/behat/page_status.js';
    }

619
620
621
    foreach ($jsstrings['mahara'] as $section => $tags) {
        foreach ($tags as $tag) {
            $strings[$tag] = get_raw_string($tag, $section);
622
623
        }
    }
624
625
    if (isset($extraconfig['themepaths']) && is_array($extraconfig['themepaths'])) {
        foreach ($extraconfig['themepaths'] as $themepath) {
Nigel McNie's avatar
Nigel McNie committed
626
            $theme_list[$themepath] = $THEME->get_url($themepath);
627
628
        }
    }
629

630
    $stringjs = '<script>';
631
    $stringjs .= 'var strings = ' . json_encode($strings) . ';';
632
    $stringjs .= "\nfunction plural(n) { return " . get_raw_string('pluralrule', 'langconfig') . "; }\n";
633
634
    $stringjs .= '</script>';

635

636
637
638
    // Allow us to set the HTML lang attribute
    $smarty->assign('LANGUAGE', substr(current_language(), 0, 2));

639
    $smarty->assign('STRINGJS', $stringjs);
640

641
    $stylesheets = get_stylesheets_for_current_page($stylesheets, $extraconfig);
642

Son Nguyen's avatar
Son Nguyen committed
643
644
645
646
    // Disable CSS transforms, transitions, and animations when running behat tests
    if (defined('BEHAT_TEST')) {
        $stylesheets[] = get_config('wwwroot') . 'testing/frameworks/behat/no_transitions.css';
    }
647
    $smarty->assign('STYLESHEETLIST', $stylesheets);
648
649
    if (!empty($theme_list)) {
        // this gets assigned in smarty_core, but do it again here if it's changed locally
Aaron Wells's avatar
Aaron Wells committed
650
        $smarty->assign('THEMELIST', json_encode(array_merge((array)json_decode($smarty->get_template_vars('THEMELIST')),  $theme_list)));
651
    }
652

653
654
655
656
657
    $dropdownmenu = get_config('dropdownmenu');
    // disable drop-downs if overridden at institution level
    $sitethemeprefs = get_config('sitethemeprefs');
    $institutions = $USER->institutions;
    if (!empty($institutions)) {
658
659
660
661
        if (count($institutions) == 1) {
            $i = reset($institutions);
            if ($i->theme == $THEME->basename && $USER->institutiontheme->institutionname == $i->institution) {
                $dropdownmenu = $i->dropdownmenu;
662
            }
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
        }
        else {
            foreach ($institutions as $i) {
                if (!empty($sitethemeprefs)) {
                    if (!empty($USER->accountprefs['theme']) && $USER->accountprefs['theme'] == $THEME->basename . '/' . $i->institution) {
                        $dropdownmenu = $i->dropdownmenu;
                        break;
                    }
                }
                else {
                    if ((!empty($USER->accountprefs['theme']) && $USER->accountprefs['theme'] == $THEME->basename . '/' . $i->institution)
                        || (empty($USER->accountprefs) && $i->theme == $THEME->basename && $USER->institutiontheme->institutionname == $i->institution)) {
                        $dropdownmenu = $i->dropdownmenu;
                        break;
                    }
678
679
680
681
682
683
                }
            }
        }
    }

    // and/or disable drop-downs if a handheld device detected
684
    $dropdownmenu = $SESSION->get('handheld_device') ? false : $dropdownmenu && get_config('dropdownmenuenabled');
685

686
687
    if ($dropdownmenu) {
        $smarty->assign('DROPDOWNMENU', $dropdownmenu);
688
        $javascript_array[] = $jsroot . 'dropdown-nav.js';
689
    }
690

691
    $smarty->assign('MOBILE', $SESSION->get('mobile'));
692
    $smarty->assign('HANDHELD_DEVICE', $SESSION->get('handheld_device'));
693
694
695
696
697
698
699
700
701
    if (defined('FILEBROWSERS') ||
        (defined('SECTION_PAGE') && SECTION_PAGE == 'blocks')) {
        // Need to add the headers for select2 here so filebrowser has correct language
        require_once(get_config('libroot') . 'form/elements/autocomplete.php');
        $select2lang = pieform_element_autocomplete_language();
        $select2headdata = pieform_element_autocomplete_get_headdata();
        $headers = array_merge($headers, $select2headdata);
        $smarty->assign('select2_language', $select2lang);
    }
702
703
704
    $maxuploadsize = get_max_upload_size(false);
    $smarty->assign('maxuploadsize', $maxuploadsize);
    $smarty->assign('maxuploadsizepretty', display_size($maxuploadsize));
705

706
707
708
709
    $sitename = get_config('sitename');
    if (!$sitename) {
       $sitename = 'Mahara';
    }
710
    $smarty->assign('sitename', $sitename);
711

712
713
714
715
    $sitelogocustom = false;
    if (get_config('installed')) {
        $sitelogocustom = (int) (get_field('institution', 'logo', 'name', 'mahara') || $THEME->headerlogo);
    }
716
    $smarty->assign('sitelogocustom', $sitelogocustom);
717
    $sitelogo = $THEME->header_logo();
718
    $sitelogo = append_version_number($sitelogo);
719
720
    $sitelogocustomsmall = $THEME->header_logo_small_custom();
    $sitelogocustomsmall = ($sitelogocustomsmall ? append_version_number($sitelogocustomsmall) : null);
721
    $smarty->assign('sitelogo', $sitelogo);
722
723
    $smarty->assign('sitelogosmall', $THEME->header_logo_small());
    $smarty->assign('sitelogocustomsmall', $sitelogocustomsmall);
724
725
    $smarty->assign('sitelogo4facebook', $THEME->facebook_logo());
    $smarty->assign('sitedescription4facebook', get_string('facebookdescription', 'mahara'));
726

Martyn Smith's avatar
Martyn Smith committed
727
    if (defined('TITLE')) {
728
        $smarty->assign('PAGETITLE', TITLE . ' - ' . $sitename);
Martyn Smith's avatar
Martyn Smith committed
729
730
    }
    else {
731
        $smarty->assign('PAGETITLE', $sitename);
Martyn Smith's avatar
Martyn Smith committed
732
    }
733
734
735
736
737
738
739
740
741
    if (defined('PAGEHEADING')) {
        $smarty->assign('PAGEHEADING', PAGEHEADING);
    }
    else {
        if (defined('TITLE')) {
            $smarty->assign('PAGEHEADING', TITLE);
        }
    }

742
743
744
    if (defined('SUBSECTIONHEADING')) {
        $smarty->assign('SUBSECTIONHEADING', SUBSECTIONHEADING);
    }
Martyn Smith's avatar
Martyn Smith committed
745

746
    $smarty->assign('PRODUCTIONMODE', get_config('productionmode'));
747
748
749
    if (defined('SITEOUTOFSYNC')) {
        $smarty->assign('SITEOUTOFSYNC', SITEOUTOFSYNC);
    }
750
    if (function_exists('local_header_top_content')) {
751
        $sitetop = (isset($sitetop) ? $sitetop : '') . local_header_top_content();
752
753
754
    }
    if (isset($sitetop)) {
        $smarty->assign('SITETOP', $sitetop);
755
    }
756
757
758
    if (defined('PUBLIC')) {
        $smarty->assign('PUBLIC', true);
    }
759
760
761
    if (defined('ADMIN')) {
        $smarty->assign('ADMIN', true);
    }
762
763
764
    if (defined('INSTITUTIONALADMIN')) {
        $smarty->assign('INSTITUTIONALADMIN', true);
    }
765
766
767
768
769
770
    if (defined('STAFF')) {
        $smarty->assign('STAFF', true);
    }
    if (defined('INSTITUTIONALSTAFF')) {
        $smarty->assign('INSTITUTIONALSTAFF', true);
    }
771

772
    $smarty->assign('LOGGEDIN', $USER->is_logged_in());
773
    $smarty->assign('loggedout', !$USER->is_logged_in());
774
775
776
777
778
779
780
    $publicsearchallowed = false;
    $searchplugin = get_config('searchplugin');
    if ($searchplugin) {
        safe_require('search', $searchplugin);
        $publicsearchallowed = (call_static_method(generate_class_name('search', $searchplugin), 'publicform_allowed') && get_config('publicsearchallowed'));
    }
    $smarty->assign('publicsearchallowed', $publicsearchallowed);
781
    if ($USER->is_logged_in()) {
782
        global $SELECTEDSUBNAV; // It's evil, but rightnav & mainnav stuff are now in different templates.
783
784
785
786
        $smarty->assign('MAINNAV', main_nav());
        $is_admin = $USER->get('admin') || $USER->is_institutional_admin() || $USER->get('staff') || $USER->is_institutional_staff();
        if ($is_admin) {
            $smarty->assign('MAINNAVADMIN', main_nav('adminnav'));
787
        }
788
        $mainnavsubnav = $SELECTEDSUBNAV;
789
        $smarty->assign('RIGHTNAV', right_nav());
790
        $smarty->assign('MESSAGEBOX', message_nav());
791
792
793
794
795
796
797
798
799
        if (!$mainnavsubnav && $dropdownmenu) {
            // In drop-down navigation, the submenu is only usable if its parent is one of the top-level menu
            // items.  But if the submenu comes from something in right_nav (settings), it's unreachable.
            // Turning the submenu into SUBPAGENAV group-style tabs makes it usable.
            $smarty->assign('SUBPAGENAV', $SELECTEDSUBNAV);
        }
        else {
            $smarty->assign('SELECTEDSUBNAV', $SELECTEDSUBNAV);
        }
800
    }
801
    else {
802
        $smarty->assign('languageform', $langselectform);
803
    }
804
    $smarty->assign('FOOTERMENU', footer_menu());
805

806
    $smarty->assign('USER', $USER);
807
    $smarty->assign('SESSKEY', $USER->get('sesskey'));
808
    $smarty->assign('CC_ENABLED', get_config('cookieconsent_enabled'));
809
    $javascript_array = append_version_number($javascript_array);
810
    $smarty->assign('JAVASCRIPT', $javascript_array);
811
    $smarty->assign('RELEASE', get_config('release'));
812
    $smarty->assign('SERIES', get_config('series'));
813
    $smarty->assign('CACHEVERSION', get_config('cacheversion', 0));
814
    if (get_config('siteclosedforupgrade')) {
815
        $smarty->assign('SITECLOSED', 'logindisabled');
816
    }
817
    else if (get_config('siteclosedbyadmin')) {
818
        $smarty->assign('SITECLOSED', 'loginallowed');
819
    }
820

821
822
    if ((!isset($extraconfig['pagehelp']) || $extraconfig['pagehelp'] !== false)
        and $help = has_page_help()) {
823
824
825
        $smarty->assign('PAGEHELPNAME', $help[0]);
        $smarty->assign('PAGEHELPICON', $help[1]);
    }
826
    if (defined('GROUP')) {
827
        require_once('group.php');
828
829
830
831
832
833
        if ($group = group_current_group()) {
            $smarty->assign('GROUP', $group);
            if (!defined('NOGROUPMENU')) {
                $smarty->assign('SUBPAGENAV', group_get_menu_tabs());
                $smarty->assign('PAGEHEADING', $group->name);
            }
834
        }
835
    }
836

837
838
839
840
841
842
    if (defined('APPS')) {
       if (!defined('NOAPPSMENU')) {
            $smarty->assign('SUBPAGENAV', apps_get_menu_tabs());
        }
    }

843
    // ---------- sideblock smarty stuff ----------
844
    $sidebars = !isset($extraconfig['sidebars']) || $extraconfig['sidebars'] !== false;
845
846
847
848
849
    if ($sidebars && !defined('INSTALLER')) {
        foreach ($sideblock_menu as $sideblock) {
            if (!empty($sideblock['visible']) && !empty($sideblock['smarty'])) {
                foreach ($sideblock['smarty'] as $ks => $vs) {
                    $smarty->assign($ks, $vs);
850
851
                }
            }
852
        }
Martyn Smith's avatar
Martyn Smith committed
853

Aaron Wells's avatar
Aaron Wells committed
854
855
        // Place all sideblocks on the right. If this structure is munged
        // appropriately, you can put blocks on the left. In future versions of
856
        // Mahara, we'll make it easy to do this.
857
858
        $sidebars = $sidebars && !empty($sideblock_menu);
        $sideblocks = array('left' => array(), 'right' => $sideblock_menu);
859

860
        $smarty->assign('userauthinstance', $SESSION->get('authinstance'));
861
        $smarty->assign('MNETUSER', $SESSION->get('mnetuser'));
862
        $smarty->assign('SIDEBLOCKS', $sideblocks);
863
        $smarty->assign('SIDEBARS', $sidebars);
864

865
866
    }

867
868
869
    if (is_array($HEADDATA) && !empty($HEADDATA)) {
        $headers = array_merge($HEADDATA, $headers);
    }
870
    $smarty->assign('HEADERS', $headers);
871

872
873
    if ($USER->get('parentuser')) {
        $smarty->assign('USERMASQUERADING', true);
874
        $smarty->assign('masqueradedetails', get_string('youaremasqueradingas', 'mahara', display_name($USER)));
875
        $smarty->assign('becomeyoulink', hsc($wwwroot) . 'admin/users/changeuser.php?restore=1');
876
        $smarty->assign('becomeyouagain', get_string('becomeadminagain', 'admin', $USER->get('parentuser')->name));
877
    }
Martyn Smith's avatar
Martyn Smith committed
878

879
880
    // Define additional html content
    if (get_config('installed')) {
881
882
883
884
        $additionalhtmlitems = array(
            'ADDITIONALHTMLHEAD'      => get_config('additionalhtmlhead'),
            'ADDITIONALHTMLTOPOFBODY' => get_config('additionalhtmltopofbody'),
            'ADDITIONALHTMLFOOTER'    => get_config('additionalhtmlfooter')
885
886
        );
        if ($additionalhtmlitems) {
887
888
            foreach ($additionalhtmlitems as $name=>$content) {
                $smarty->assign($name, $content);
889
890
891
            }
        }
    }
892
893
894
895
896
897

    // If Cookie Consent is enabled, than define conent
    if (get_config('cookieconsent_enabled')) {
        require_once('cookieconsent.php');
        $smarty->assign('COOKIECONSENTCODE', get_cookieconsent_code());
    }
898
    // Render the session messages
899
900
901
902
    $messages = array();
    $messages = array_merge($messages, insert_messages('loginbox'));
    $messages = array_merge($messages, insert_messages('messages'));
    $smarty->assign('messages', $messages);
903
904
905
    return $smarty;
}

906
907
908
909

/**
 * Manages theme configuration.
 *
Aaron Wells's avatar
Aaron Wells committed
910
 * Does its best to give the user _a_ theme, even if it's not the theme they
911
912
913
914
915
916
917
918
919
 * want to use (e.g. the theme they want has been uninstalled)
 */
class Theme {

    /**
     * The base name of the theme (the name of the directory in which it lives)
     */
    public $basename = '';

920
921
922
923
924
    /**
     * A user may have had the header logo overridden by an institution
     */
    public $headerlogo;

925
926
927
928
929
    /**
     * A user may have had the small header logo added by an institution
     */
    public $headerlogosmall;

930
931
932
933
934
    /**
     * Additional stylesheets to display after the basename theme's stylesheets
     */
    public $addedstylesheets;

935
936
937
938
939
940
941
942
943
944
    /**
     * A human-readable version of the theme name
     */
    public $displayname = '';

    /**
     * Which pieform renderer to use by default for all forms
     */
    public $formrenderer = '';

945
946
947
948
949
950
951
952
953
954
    /**
     * Directories where to look for templates by default
     */
    public $templatedirs = array();

    /**
     * Theme inheritance path from this theme to 'raw'
     */
    public $inheritance = array();

955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
    /**
     * What unit the left/center/right column widths are in. 'pixels' or 'percent'
     */
    public $columnwidthunits    = '';

    /**
     * Width of the left column. Integer - see $columnwidthunits
     */
    public $leftcolumnwidth     = 256;

    /**
     * Background colour for the left column
     */
    public $leftcolumnbgcolor   = '#fff';

    /**
     * Background colour for the center column
     */
    public $centercolumnbgcolor = '#fff';

    /**
     * Width of the right column. Integer - see $columnwidthunits
     */
    public $rightcolumnwidth    = 256;

    /**
     * Background colour for the right column
     */
    public $rightcolumnbgcolor  = '#fff';

    /**
     * Initialises a theme object based on the theme 'hint' passed.
     *
Aaron Wells's avatar
Aaron Wells committed
988
989
     * If arg is a string, it's taken to be a theme name. If it's a user
     * object, we ask it for a theme name. If it's an integer, we pretend
990
991
     * that's a user ID and ask for the theme for that user.
     *
Aaron Wells's avatar
Aaron Wells committed
992
     * If the theme they want doesn't exist, the object is initialised for the
993
994
995
996
997
998
999
1000
     * default theme. This means you can initialise one of these for a user
     * and then use it without worrying if the theme exists.
     *
     * @param mixed $arg Theme name, user object or user ID
     */
    public function __construct($arg) {
        if (is_string($arg)) {
            $themename = $arg;
1001
            $themedata = null;
1002
1003
        }
        else if ($arg instanceof User) {
1004
            $themedata = $arg->get_themedata();
1005
        }
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
        else if ($arg instanceof View) {
            $themename = $arg->get('theme');
            $themedata = null;
            $userid = $arg->get('owner');
            if ($userid) {
                $user = new User();
                $user->find_by_id($userid);
                $themedata = $user->get_themedata();
                $themedata->viewbasename = $themedata->basename;
                unset($themedata->basename);
            }
        }
1018
1019
1020
        else if (is_int($arg)) {
            $user = new User();
            $user->find_by_id($arg);
1021
            $themedata = $user->get_themedata();
1022
1023
1024
1025
1026
        }
        else {
            throw new SystemException("Argument to Theme::__construct was not a theme name, user object or user ID");
        }

1027
        if (isset($themedata) && isset($themedata->basename)) {
1028
1029
1030
            $themename = $themedata->basename;
        }

1031
        if (empty($themename)) {
1032
            // Theme to show to when no theme has been suggested
1033
1034
1035
            if (!$themename = get_config('theme')) {
                $themename = 'raw';
            }
1036
        }
1037
1038

        // check the validity of the name
1039
        if (!$this->name_is_valid($themename)) {
1040
1041
            throw new SystemException("Theme name is in invalid form: '$themename'");
        }
1042
1043

        $this->init_theme($themename, $themedata);
1044
1045
1046
1047
1048
1049
1050
1051
    }

    /**
     * Given a theme name, check that it is valid
     */
    public static function name_is_valid($themename) {
        // preg_match returns 0 if invalid characters were found, 1 if not
        return (preg_match('/^[a-zA-Z0-9_-]+$/', $themename) == 1);
1052
1053
    }

1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
    /**
     * Given a theme name, retrieves the $theme variable in the themeconfig.php file
     */
    public static function get_theme_config($themename) {
        $themeconfigfile = get_config('docroot') . 'theme/' . $themename . '/themeconfig.php';
        if (is_readable($themeconfigfile)) {
            require( get_config('docroot') . 'theme/' . $themename . '/themeconfig.php' );
            return $theme;
        }
        else {
            return false;
        }
    }

1068
1069
1070
    /**
     * Given a theme name, reads in all config and sets fields on this object
     */
1071
    private function init_theme($themename, $themedata) {
1072

1073
        $themeconfig = self::get_theme_config($themename);