80
80
* @property-read array $legacythemeinuse True if the legacy browser theme is in use.
81
81
* @property-read navbar $navbar The navbar object used to display the navbar
82
82
* @property-read global_navigation $navigation The navigation structure for this page.
83
* @property-read xml_container_stack $opencontainers Tracks XHTML tags on this page that have been opened but not closed.
83
* @property-read xhtml_container_stack $opencontainers Tracks XHTML tags on this page that have been opened but not closed.
84
84
* mainly for internal use by the rendering code.
85
85
* @property-read string $pagelayout The general type of page this is. For example 'normal', 'popup', 'home'.
86
86
* Allows the theme to display things differently, if it wishes to.
219
219
protected $_alternateversions = array();
222
* @var block_manager The blocks manager for this page. It is reponsible for
222
* @var block_manager The blocks manager for this page. It is responsible for
223
223
* the blocks and there content on this page.
225
225
protected $_blocks = null;
228
* @var page_requirements_manager Page requirements manager. It is reponsible
228
* @var page_requirements_manager Page requirements manager. It is responsible
229
229
* for all JavaScript and CSS resources required by this page.
231
231
protected $_requires = null;
443
443
protected function magic_get_context() {
444
444
if (is_null($this->_context)) {
445
445
if (CLI_SCRIPT or NO_MOODLE_COOKIES) {
446
// cli scripts work in system context, do not annoy devs with debug info
447
// very few scripts do not use cookies, we can safely use system as default context there
446
// Cli scripts work in system context, do not annoy devs with debug info.
447
// Very few scripts do not use cookies, we can safely use system as default context there.
449
449
debugging('Coding problem: $PAGE->context was not set. You may have forgotten '
450
450
.'to call require_login() or $PAGE->set_context(). The page may not display '
575
575
* Please do not call this method directly, use the ->blocks syntax. {@link moodle_page::__get()}.
576
* @return blocks_manager the blocks manager object for this page.
576
* @return block_manager the blocks manager object for this page.
578
578
protected function magic_get_blocks() {
596
596
* @return page_requirements_manager tracks the JavaScript, CSS files, etc. required by this page.
598
598
protected function magic_get_requires() {
600
599
if (is_null($this->_requires)) {
601
600
$this->_requires = new page_requirements_manager();
661
660
protected function magic_get_devicetypeinuse() {
662
661
if (empty($this->_devicetypeinuse)) {
663
$this->_devicetypeinuse = get_user_device_type();
662
$this->_devicetypeinuse = core_useragent::get_user_device_type();
665
664
return $this->_devicetypeinuse;
748
748
* @param string $name property name
749
749
* @param mixed $value Value
750
750
* @return void Throws exception if field not defined in page class
751
* @throws coding_exception
752
753
public function __set($name, $value) {
753
754
if (method_exists($this, 'set_' . $name)) {
768
769
* @return renderer_base
770
771
public function get_renderer($component, $subtype = null, $target = null) {
772
if ($this->pagelayout === 'maintenance') {
773
// If the page is using the maintenance layout then we're going to force target to maintenance.
774
// This leads to a special core renderer that is designed to block access to API's that are likely unavailable for this
776
$target = RENDERER_TARGET_MAINTENANCE;
771
778
return $this->magic_get_theme()->get_renderer($this, $component, $subtype, $target);
814
* Get a description of this page. Normally displayed in the footer in
815
* developer debug mode.
821
* Get a description of this page. Normally displayed in the footer in developer debug mode.
818
824
public function debug_summary() {
820
826
$summary .= 'General type: ' . $this->pagelayout . '. ';
821
827
if (!during_initial_install()) {
822
$summary .= 'Context ' . print_context_name($this->_context) . ' (context id ' . $this->_context->id . '). ';
828
$summary .= 'Context ' . $this->context->get_context_name() . ' (context id ' . $this->_context->id . '). ';
824
830
$summary .= 'Page type ' . $this->pagetype . '. ';
825
831
if ($this->subpage) {
826
'Sub-page ' . $this->subpage . '. ';
832
$summary .= 'Sub-page ' . $this->subpage . '. ';
831
// Setter methods =============================================================
837
// Setter methods =============================================================.
834
* Set the state. The state must be one of that STATE_... constants, and
835
* the state is only allowed to advance one step at a time.
837
* @param integer $state The new state.
842
* The state must be one of that STATE_... constants, and the state is only allowed to advance one step at a time.
844
* @param int $state The new state.
845
* @throws coding_exception
839
847
public function set_state($state) {
840
848
if ($state != $this->_state + 1 || $state > self::STATE_DONE) {
860
868
* Sets $PAGE->context to the course context, if it is not already set.
862
870
* @param stdClass $course the course to set as the global course.
871
* @throws coding_exception
864
873
public function set_course($course) {
865
874
global $COURSE, $PAGE, $CFG, $SITE;
885
894
$this->set_context(context_course::instance($this->_course->id));
888
// notify course format that this page is set for the course
897
// Notify course format that this page is set for the course.
889
898
if ($this->_course->id != $SITE->id) {
890
899
require_once($CFG->dirroot.'/course/lib.php');
891
900
$courseformat = course_get_format($this->_course);
900
909
* Set the main context to which this page belongs.
902
* @param context $context a context object, normally obtained with get_context_instance.
911
* @param context $context a context object. You normally get this with context_xxxx::instance().
904
913
public function set_context($context) {
905
914
if ($context === null) {
906
// extremely ugly hack which sets context to some value in order to prevent warnings,
915
// Extremely ugly hack which sets context to some value in order to prevent warnings,
907
916
// use only for core error handling!!!!
908
917
if (!$this->_context) {
909
918
$this->_context = context_system::instance();
914
// ideally we should set context only once
915
if (isset($this->_context)) {
916
if ($context->id == $this->_context->id) {
917
// fine - no change needed
918
} else if ($this->_context->contextlevel == CONTEXT_SYSTEM or $this->_context->contextlevel == CONTEXT_COURSE) {
919
// hmm - not ideal, but it might produce too many warnings due to the design of require_login
920
} else if ($this->_context->contextlevel == CONTEXT_MODULE and $this->_context->id == get_parent_contextid($context)) {
921
// hmm - most probably somebody did require_login() and after that set the block context
923
// Ideally we should set context only once.
924
if (isset($this->_context) && $context->id !== $this->_context->id) {
925
$current = $this->_context->contextlevel;
926
if ($current == CONTEXT_SYSTEM or $current == CONTEXT_COURSE) {
927
// Hmm - not ideal, but it might produce too many warnings due to the design of require_login.
928
} else if ($current == CONTEXT_MODULE and ($parentcontext = $context->get_parent_context()) and
929
$this->_context->id == $parentcontext->id) {
930
// Hmm - most probably somebody did require_login() and after that set the block context.
923
// we do not want devs to do weird switching of context levels on the fly,
924
// because we might have used the context already such as in text filter in page title
925
debugging('Coding problem: unsupported modification of PAGE->context from '.$this->_context->contextlevel.' to '.$context->contextlevel);
932
// We do not want devs to do weird switching of context levels on the fly because we might have used
933
// the context already such as in text filter in page title.
934
debugging("Coding problem: unsupported modification of PAGE->context from {$current} to {$context->contextlevel}");
936
945
* @param stdClass $course
937
946
* @param stdClass $module
948
* @throws coding_exception
940
950
public function set_cm($cm, $course = null, $module = null) {
941
951
global $DB, $CFG, $SITE;
943
953
if (!isset($cm->id) || !isset($cm->course)) {
944
throw new coding_exception('Invalid $cm parameter for $PAGE object, it has to be instance of cm_info or record from the course_modules table.');
954
throw new coding_exception('Invalid $cm. It has to be instance of cm_info or record from the course_modules table.');
947
957
if (!$this->_course || $this->_course->id != $cm->course) {
954
964
$this->set_course($course);
957
// make sure we have a $cm from get_fast_modinfo as this contains activity access details
967
// Make sure we have a $cm from get_fast_modinfo as this contains activity access details.
958
968
if (!($cm instanceof cm_info)) {
959
969
$modinfo = get_fast_modinfo($this->_course);
960
970
$cm = $modinfo->get_cm($cm->id);
962
972
$this->_cm = $cm;
964
// unfortunately the context setting is a mess, let's try to work around some common block problems and show some debug messages
974
// Unfortunately the context setting is a mess.
975
// Let's try to work around some common block problems and show some debug messages.
965
976
if (empty($this->_context) or $this->_context->contextlevel != CONTEXT_BLOCK) {
966
977
$context = context_module::instance($cm->id);
967
978
$this->set_context($context);
971
982
$this->set_activity_record($module);
974
// notify course format that this page is set for the course module
985
// Notify course format that this page is set for the course module.
975
986
if ($this->_course->id != $SITE->id) {
976
987
require_once($CFG->dirroot.'/course/lib.php');
977
988
course_get_format($this->_course)->page_set_cm($this);
983
994
* module. For instance if the current module (cm) is a forum this should be a row
984
995
* from the forum table.
986
* @param stdClass $module A row from the main database table for the module that this
997
* @param stdClass $module A row from the main database table for the module that this page belongs to.
998
* @throws coding_exception
990
1000
public function set_activity_record($module) {
991
1001
if (is_null($this->_cm)) {
992
1002
throw new coding_exception('You cannot call $PAGE->set_activity_record until after $PAGE->cm has been set.');
994
1004
if ($module->id != $this->_cm->instance || $module->course != $this->_course->id) {
995
throw new coding_exception('The activity record your are trying to set does not seem to correspond to the cm that has been set.');
1005
throw new coding_exception('The activity record does not seem to correspond to the cm that has been set.');
997
1007
$this->_module = $module;
1032
1042
* @param string $pagelayout the page layout this is. For example 'popup', 'home'.
1034
1044
public function set_pagelayout($pagelayout) {
1036
* Uncomment this to debug theme pagelayout issues like missing blocks.
1038
* if (!empty($this->_wherethemewasinitialised) && $pagelayout != $this->_pagelayout) {
1039
* debugging('Page layout has already been set and cannot be changed.', DEBUG_DEVELOPER);
1045
// Uncomment this to debug theme pagelayout issues like missing blocks.
1046
// if (!empty($this->_wherethemewasinitialised) && $pagelayout != $this->_pagelayout)
1047
// debugging('Page layout has already been set and cannot be changed.', DEBUG_DEVELOPER);
1042
1048
$this->_pagelayout = $pagelayout;
1061
1067
* Adds a CSS class to the body tag of the page.
1063
1069
* @param string $class add this class name ot the class attribute on the body tag.
1070
* @throws coding_exception
1065
1072
public function add_body_class($class) {
1066
1073
if ($this->_state > self::STATE_BEFORE_HEADER) {
1120
1127
* the category must be the one that the course belongs to. This also
1121
1128
* automatically sets the page context to the category context.
1123
* @param integer $categoryid The id of the category to set.
1130
* @param int $categoryid The id of the category to set.
1131
* @throws coding_exception
1125
1133
public function set_category_by_id($categoryid) {
1127
1135
if (!is_null($this->_course)) {
1128
throw new coding_exception('Attempt to manually set the course category when the course has been set. This is not allowed.');
1136
throw new coding_exception('Course has already been set. You cannot change the category now.');
1130
1138
if (is_array($this->_categories)) {
1131
throw new coding_exception('Course category has already been set. You are not allowed to change it.');
1139
throw new coding_exception('Course category has already been set. You cannot to change it now.');
1133
1141
$this->ensure_theme_not_set();
1134
1142
$this->set_course($SITE);
1153
* You should call this method from every page to set the cleaned-up URL
1154
* that should be used to return to this page.
1161
* You should call this method from every page to set the URL that should be used to return to this page.
1156
1163
* Used, for example, by the blocks editing UI to know where to return the
1157
1164
* user after an action.
1162
1169
* @param moodle_url|string $url URL relative to $CFG->wwwroot or {@link moodle_url} instance
1163
1170
* @param array $params parameters to add to the URL
1171
* @throws coding_exception
1165
1173
public function set_url($url, array $params = null) {
1168
if (is_string($url)) {
1169
if (strpos($url, 'http') === 0) {
1171
} else if (strpos($url, '/') === 0) {
1172
// we have to use httpswwwroot here, because of loginhttps pages
1176
if (is_string($url) && strpos($url, 'http') !== 0) {
1177
if (strpos($url, '/') === 0) {
1178
// We have to use httpswwwroot here, because of loginhttps pages.
1173
1179
$url = $CFG->httpswwwroot . $url;
1175
1181
throw new coding_exception('Invalid parameter $url, has to be full url or in shortened form starting with /.');
1204
1210
public function ensure_param_not_in_url($param) {
1205
$discard = $this->url; // Make sure $this->url is lazy-loaded;
1206
1211
$this->_url->remove_params($param);
1215
* Sets an alternative version of this page.
1210
1217
* There can be alternate versions of some pages (for example an RSS feed version).
1211
* If such other version exist, call this method, and a link to the alternate
1212
* version will be included in the <head> of the page.
1218
* Call this method for each alternative version available.
1219
* For each alternative version a link will be included in the <head> tag.
1214
1221
* @param string $title The title to give the alternate version.
1215
1222
* @param string|moodle_url $url The URL of the alternate version.
1216
1223
* @param string $mimetype The mime-type of the alternate version.
1224
* @throws coding_exception
1218
1226
public function add_alternate_version($title, $url, $mimetype) {
1219
1227
if ($this->_state > self::STATE_BEFORE_HEADER) {
1275
1283
* Sets whether the browser should cache this page or not.
1277
* @return bool $cacheable can this page be cached by the user's browser.
1285
* @param bool $cacheable can this page be cached by the user's browser.
1279
1287
public function set_cacheable($cacheable) {
1280
1288
$this->_cacheable = $cacheable;
1286
1294
* This function must be called before $OUTPUT->header has been called or
1287
1295
* a coding exception will be thrown.
1289
* @param int $delay Sets the delay before refreshing the page, if set to null
1290
* refresh is cancelled
1297
* @param int $delay Sets the delay before refreshing the page, if set to null refresh is cancelled.
1298
* @throws coding_exception
1292
public function set_periodic_refresh_delay($delay=null) {
1300
public function set_periodic_refresh_delay($delay = null) {
1293
1301
if ($this->_state > self::STATE_BEFORE_HEADER) {
1294
1302
throw new coding_exception('You cannot set a periodic refresh delay after the header has been printed');
1296
if ($delay===null) {
1304
if ($delay === null) {
1297
1305
$this->_periodicrefreshdelay = null;
1298
1306
} else if (is_int($delay)) {
1299
1307
$this->_periodicrefreshdelay = $delay;
1329
* This function indicates that current page requires the https
1330
* when $CFG->loginhttps enabled.
1337
* This function indicates that current page requires the https when $CFG->loginhttps enabled.
1332
1339
* By using this function properly, we can ensure 100% https-ized pages
1333
1340
* at our entire discretion (login, forgot_password, change_password)
1343
* @throws coding_exception
1336
1345
public function https_required() {
1355
* Makes sure that page previously marked with https_required()
1356
* is really using https://, if not it redirects to https://
1364
* Makes sure that page previously marked with https_required() is really using https://, if not it redirects to https://
1358
1366
* @return void (may redirect to https://self)
1367
* @throws coding_exception
1360
1369
public function verify_https_required() {
1361
1370
global $CFG, $FULLME;
1371
1380
if (empty($CFG->loginhttps)) {
1372
// https not required, so stop checking
1381
// Https not required, so stop checking.
1376
1385
if (strpos($this->_url, 'https://')) {
1377
// detect if incorrect PAGE->set_url() used, it is recommended to use root-relative paths there
1378
throw new coding_exception('Invalid page url specified, it must start with https:// for pages that set https_required()!');
1386
// Detect if incorrect PAGE->set_url() used, it is recommended to use root-relative paths there.
1387
throw new coding_exception('Invalid page url. It must start with https:// for pages that set https_required()!');
1381
1390
if (!empty($CFG->sslproxy)) {
1382
// it does not make much sense to use sslproxy and loginhttps at the same time
1391
// It does not make much sense to use sslproxy and loginhttps at the same time.
1386
// now the real test and redirect!
1395
// Now the real test and redirect!
1387
1396
// NOTE: do NOT use this test for detection of https on current page because this code is not compatible with SSL proxies,
1388
// instead use strpos($CFG->httpswwwroot, 'https:') === 0
1397
// instead use (strpos($CFG->httpswwwroot, 'https:') === 0).
1389
1398
if (strpos($FULLME, 'https:') !== 0) {
1390
// this may lead to infinite redirect on misconfigured sites, in that case use $CFG->loginhttps=0; in /config.php
1399
// This may lead to infinite redirect on an incorrectly configured site.
1400
// In that case set $CFG->loginhttps=0; within /config.php.
1391
1401
redirect($this->_url);
1445
1455
public function initialise_theme_and_output() {
1446
global $OUTPUT, $PAGE, $SITE;
1456
global $OUTPUT, $PAGE, $SITE, $CFG;
1448
1458
if (!empty($this->_wherethemewasinitialised)) {
1452
1462
if (!during_initial_install()) {
1453
// detect PAGE->context mess
1463
// Detect PAGE->context mess.
1454
1464
$this->magic_get_context();
1466
1476
$this->_theme->setup_blocks($this->pagelayout, $this->blocks);
1477
if ($this->_theme->enable_dock && !empty($CFG->allowblockstodock)) {
1478
$this->requires->strings_for_js(array('addtodock', 'undockitem', 'dockblock', 'undockblock', 'undockall', 'hidedockpanel', 'hidepanel'), 'block');
1479
$this->requires->string_for_js('thisdirectionvertical', 'langconfig');
1480
$this->requires->yui_module('moodle-core-dock-loader', 'M.core.dock.loader.initLoader');
1468
1483
if ($this === $PAGE) {
1469
$OUTPUT = $this->get_renderer('core');
1485
if ($this->pagelayout === 'maintenance') {
1486
// If the page is using the maintenance layout then we're going to force target to maintenance.
1487
// This leads to a special core renderer that is designed to block access to API's that are likely unavailable for this
1489
$target = RENDERER_TARGET_MAINTENANCE;
1491
$OUTPUT = $this->get_renderer('core', null, $target);
1472
1494
$this->_wherethemewasinitialised = debug_backtrace();
1540
1562
return $mnetpeertheme;
1542
1564
// First try for the device the user is using.
1543
$devicetheme = get_selected_theme_for_device_type($this->devicetypeinuse);
1565
$devicetheme = core_useragent::get_device_type_theme($this->devicetypeinuse);
1544
1566
if (!empty($devicetheme)) {
1545
1567
return $devicetheme;
1547
// Next try for the default device (as a fallback)
1548
$devicetheme = get_selected_theme_for_device_type('default');
1569
// Next try for the default device (as a fallback).
1570
$devicetheme = core_useragent::get_device_type_theme('default');
1549
1571
if (!empty($devicetheme)) {
1550
1572
return $devicetheme;
1553
1575
return theme_config::DEFAULT_THEME;
1579
// We should most certainly have resolved a theme by now. Something has gone wrong.
1580
debugging('Error resolving the theme to use for this page.', DEBUG_DEVELOPER);
1581
return theme_config::DEFAULT_THEME;
1619
1645
$this->add_body_class($this->_legacyclass);
1621
1647
$pathbits = explode('-', trim($pagetype));
1622
for ($i=1;$i<count($pathbits);$i++) {
1623
$this->add_body_class('path-'.join('-',array_slice($pathbits, 0, $i)));
1648
for ($i = 1; $i < count($pathbits); $i++) {
1649
$this->add_body_class('path-' . join('-', array_slice($pathbits, 0, $i)));
1626
$this->add_body_classes(get_browser_version_classes());
1652
$this->add_body_classes(core_useragent::get_browser_version_classes());
1627
1653
$this->add_body_class('dir-' . get_string('thisdirection', 'langconfig'));
1628
1654
$this->add_body_class('lang-' . current_language());
1629
1655
$this->add_body_class('yui-skin-sam'); // Make YUI happy, if it is used.
1630
1656
$this->add_body_class('yui3-skin-sam'); // Make YUI3 happy, if it is used.
1631
1657
$this->add_body_class($this->url_to_class_name($CFG->wwwroot));
1633
$this->add_body_class('pagelayout-' . $this->_pagelayout); // extra class describing current page layout
1659
// Extra class describing current page layout.
1660
$this->add_body_class('pagelayout-' . $this->_pagelayout);
1635
1662
if (!during_initial_install()) {
1636
1663
$this->add_body_class('course-' . $this->_course->id);
1666
1693
if (!empty($USER->editing)) {
1667
1694
$this->add_body_class('editing');
1668
1695
if (optional_param('bui_moveid', false, PARAM_INT)) {
1669
$this->add_body_class('blocks-moving');
1696
$this->add_body_class('blocks-moving');
1673
1700
if (!empty($CFG->blocksdrag)) {