1<?php
2
3// This file is part of Moodle - http://moodle.org/
4//
5// Moodle is free software: you can redistribute it and/or modify
6// it under the terms of the GNU General Public License as published by
7// the Free Software Foundation, either version 3 of the License, or
8// (at your option) any later version.
9//
10// Moodle is distributed in the hope that it will be useful,
11// but WITHOUT ANY WARRANTY; without even the implied warranty of
12// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13// GNU General Public License for more details.
14//
15// You should have received a copy of the GNU General Public License
16// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
17
18/**
19 * Block Class and Functions
20 *
21 * This file defines the {@link block_manager} class,
22 *
23 * @package    core
24 * @subpackage block
25 * @copyright  1999 onwards Martin Dougiamas  http://dougiamas.com
26 * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
27 */
28
29defined('MOODLE_INTERNAL') || die();
30
31/**#@+
32 * Default names for the block regions in the standard theme.
33 */
34define('BLOCK_POS_LEFT',  'side-pre');
35define('BLOCK_POS_RIGHT', 'side-post');
36/**#@-*/
37
38define('BUI_CONTEXTS_FRONTPAGE_ONLY', 0);
39define('BUI_CONTEXTS_FRONTPAGE_SUBS', 1);
40define('BUI_CONTEXTS_ENTIRE_SITE', 2);
41
42define('BUI_CONTEXTS_CURRENT', 0);
43define('BUI_CONTEXTS_CURRENT_SUBS', 1);
44
45// Position of "Add block" control, to be used in theme config as a value for $THEME->addblockposition:
46// - default: as a fake block that is displayed in editing mode
47// - flatnav: "Add block" item in the flat navigation drawer in editing mode
48// - custom: none of the above, theme will take care of displaying the control.
49define('BLOCK_ADDBLOCK_POSITION_DEFAULT', 0);
50define('BLOCK_ADDBLOCK_POSITION_FLATNAV', 1);
51define('BLOCK_ADDBLOCK_POSITION_CUSTOM', -1);
52
53/**
54 * Exception thrown when someone tried to do something with a block that does
55 * not exist on a page.
56 *
57 * @copyright 2009 Tim Hunt
58 * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
59 * @since     Moodle 2.0
60 */
61class block_not_on_page_exception extends moodle_exception {
62    /**
63     * Constructor
64     * @param int $instanceid the block instance id of the block that was looked for.
65     * @param object $page the current page.
66     */
67    public function __construct($instanceid, $page) {
68        $a = new stdClass;
69        $a->instanceid = $instanceid;
70        $a->url = $page->url->out();
71        parent::__construct('blockdoesnotexistonpage', '', $page->url->out(), $a);
72    }
73}
74
75/**
76 * This class keeps track of the block that should appear on a moodle_page.
77 *
78 * The page to work with as passed to the constructor.
79 *
80 * @copyright 2009 Tim Hunt
81 * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
82 * @since     Moodle 2.0
83 */
84class block_manager {
85    /**
86     * The UI normally only shows block weights between -MAX_WEIGHT and MAX_WEIGHT,
87     * although other weights are valid.
88     */
89    const MAX_WEIGHT = 10;
90
91/// Field declarations =========================================================
92
93    /**
94     * the moodle_page we are managing blocks for.
95     * @var moodle_page
96     */
97    protected $page;
98
99    /** @var array region name => 1.*/
100    protected $regions = array();
101
102    /** @var string the region where new blocks are added.*/
103    protected $defaultregion = null;
104
105    /** @var array will be $DB->get_records('blocks') */
106    protected $allblocks = null;
107
108    /**
109     * @var array blocks that this user can add to this page. Will be a subset
110     * of $allblocks, but with array keys block->name. Access this via the
111     * {@link get_addable_blocks()} method to ensure it is lazy-loaded.
112     */
113    protected $addableblocks = null;
114
115    /**
116     * Will be an array region-name => array(db rows loaded in load_blocks);
117     * @var array
118     */
119    protected $birecordsbyregion = null;
120
121    /**
122     * array region-name => array(block objects); populated as necessary by
123     * the ensure_instances_exist method.
124     * @var array
125     */
126    protected $blockinstances = array();
127
128    /**
129     * array region-name => array(block_contents objects) what actually needs to
130     * be displayed in each region.
131     * @var array
132     */
133    protected $visibleblockcontent = array();
134
135    /**
136     * array region-name => array(block_contents objects) extra block-like things
137     * to be displayed in each region, before the real blocks.
138     * @var array
139     */
140    protected $extracontent = array();
141
142    /**
143     * Used by the block move id, to track whether a block is currently being moved.
144     *
145     * When you click on the move icon of a block, first the page needs to reload with
146     * extra UI for choosing a new position for a particular block. In that situation
147     * this field holds the id of the block being moved.
148     *
149     * @var integer|null
150     */
151    protected $movingblock = null;
152
153    /**
154     * Show only fake blocks
155     */
156    protected $fakeblocksonly = false;
157
158/// Constructor ================================================================
159
160    /**
161     * Constructor.
162     * @param object $page the moodle_page object object we are managing the blocks for,
163     * or a reasonable faxilimily. (See the comment at the top of this class
164     * and {@link http://en.wikipedia.org/wiki/Duck_typing})
165     */
166    public function __construct($page) {
167        $this->page = $page;
168    }
169
170/// Getter methods =============================================================
171
172    /**
173     * Get an array of all region names on this page where a block may appear
174     *
175     * @return array the internal names of the regions on this page where block may appear.
176     */
177    public function get_regions() {
178        if (is_null($this->defaultregion)) {
179            $this->page->initialise_theme_and_output();
180        }
181        return array_keys($this->regions);
182    }
183
184    /**
185     * Get the region name of the region blocks are added to by default
186     *
187     * @return string the internal names of the region where new blocks are added
188     * by default, and where any blocks from an unrecognised region are shown.
189     * (Imagine that blocks were added with one theme selected, then you switched
190     * to a theme with different block positions.)
191     */
192    public function get_default_region() {
193        $this->page->initialise_theme_and_output();
194        return $this->defaultregion;
195    }
196
197    /**
198     * The list of block types that may be added to this page.
199     *
200     * @return array block name => record from block table.
201     */
202    public function get_addable_blocks() {
203        $this->check_is_loaded();
204
205        if (!is_null($this->addableblocks)) {
206            return $this->addableblocks;
207        }
208
209        // Lazy load.
210        $this->addableblocks = array();
211
212        $allblocks = blocks_get_record();
213        if (empty($allblocks)) {
214            return $this->addableblocks;
215        }
216
217        $unaddableblocks = self::get_undeletable_block_types();
218        $requiredbythemeblocks = $this->get_required_by_theme_block_types();
219        $pageformat = $this->page->pagetype;
220        foreach($allblocks as $block) {
221            if (!$bi = block_instance($block->name)) {
222                continue;
223            }
224            if ($block->visible && !in_array($block->name, $unaddableblocks) &&
225                    !in_array($block->name, $requiredbythemeblocks) &&
226                    ($bi->instance_allow_multiple() || !$this->is_block_present($block->name)) &&
227                    blocks_name_allowed_in_format($block->name, $pageformat) &&
228                    $bi->user_can_addto($this->page)) {
229                $block->title = $bi->get_title();
230                $this->addableblocks[$block->name] = $block;
231            }
232        }
233
234        core_collator::asort_objects_by_property($this->addableblocks, 'title');
235        return $this->addableblocks;
236    }
237
238    /**
239     * Given a block name, find out of any of them are currently present in the page
240
241     * @param string $blockname - the basic name of a block (eg "navigation")
242     * @return boolean - is there one of these blocks in the current page?
243     */
244    public function is_block_present($blockname) {
245        if (empty($this->blockinstances)) {
246            return false;
247        }
248
249        $requiredbythemeblocks = $this->get_required_by_theme_block_types();
250        foreach ($this->blockinstances as $region) {
251            foreach ($region as $instance) {
252                if (empty($instance->instance->blockname)) {
253                    continue;
254                }
255                if ($instance->instance->blockname == $blockname) {
256                    if ($instance->instance->requiredbytheme) {
257                        if (!in_array($blockname, $requiredbythemeblocks)) {
258                            continue;
259                        }
260                    }
261                    return true;
262                }
263            }
264        }
265        return false;
266    }
267
268    /**
269     * Find out if a block type is known by the system
270     *
271     * @param string $blockname the name of the type of block.
272     * @param boolean $includeinvisible if false (default) only check 'visible' blocks, that is, blocks enabled by the admin.
273     * @return boolean true if this block in installed.
274     */
275    public function is_known_block_type($blockname, $includeinvisible = false) {
276        $blocks = $this->get_installed_blocks();
277        foreach ($blocks as $block) {
278            if ($block->name == $blockname && ($includeinvisible || $block->visible)) {
279                return true;
280            }
281        }
282        return false;
283    }
284
285    /**
286     * Find out if a region exists on a page
287     *
288     * @param string $region a region name
289     * @return boolean true if this region exists on this page.
290     */
291    public function is_known_region($region) {
292        if (empty($region)) {
293            return false;
294        }
295        return array_key_exists($region, $this->regions);
296    }
297
298    /**
299     * Get an array of all blocks within a given region
300     *
301     * @param string $region a block region that exists on this page.
302     * @return array of block instances.
303     */
304    public function get_blocks_for_region($region) {
305        $this->check_is_loaded();
306        $this->ensure_instances_exist($region);
307        return $this->blockinstances[$region];
308    }
309
310    /**
311     * Returns an array of block content objects that exist in a region
312     *
313     * @param string $region a block region that exists on this page.
314     * @return array of block block_contents objects for all the blocks in a region.
315     */
316    public function get_content_for_region($region, $output) {
317        $this->check_is_loaded();
318        $this->ensure_content_created($region, $output);
319        return $this->visibleblockcontent[$region];
320    }
321
322    /**
323     * Returns an array of block content objects for all the existings regions
324     *
325     * @param renderer_base $output the rendered to use
326     * @return array of block block_contents objects for all the blocks in all regions.
327     * @since  Moodle 3.3
328     */
329    public function get_content_for_all_regions($output) {
330        $contents = array();
331        $this->check_is_loaded();
332
333        foreach ($this->regions as $region => $val) {
334            $this->ensure_content_created($region, $output);
335            $contents[$region] = $this->visibleblockcontent[$region];
336        }
337        return $contents;
338    }
339
340    /**
341     * Helper method used by get_content_for_region.
342     * @param string $region region name
343     * @param float $weight weight. May be fractional, since you may want to move a block
344     * between ones with weight 2 and 3, say ($weight would be 2.5).
345     * @return string URL for moving block $this->movingblock to this position.
346     */
347    protected function get_move_target_url($region, $weight) {
348        return new moodle_url($this->page->url, array('bui_moveid' => $this->movingblock,
349                'bui_newregion' => $region, 'bui_newweight' => $weight, 'sesskey' => sesskey()));
350    }
351
352    /**
353     * Determine whether a region contains anything. (Either any real blocks, or
354     * the add new block UI.)
355     *
356     * (You may wonder why the $output parameter is required. Unfortunately,
357     * because of the way that blocks work, the only reliable way to find out
358     * if a block will be visible is to get the content for output, and to
359     * get the content, you need a renderer. Fortunately, this is not a
360     * performance problem, because we cache the output that is generated, and
361     * in almost every case where we call region_has_content, we are about to
362     * output the blocks anyway, so we are not doing wasted effort.)
363     *
364     * @param string $region a block region that exists on this page.
365     * @param core_renderer $output a core_renderer. normally the global $OUTPUT.
366     * @return boolean Whether there is anything in this region.
367     */
368    public function region_has_content($region, $output) {
369
370        if (!$this->is_known_region($region)) {
371            return false;
372        }
373        $this->check_is_loaded();
374        $this->ensure_content_created($region, $output);
375        // if ($this->page->user_is_editing() && $this->page->user_can_edit_blocks()) {
376        // Mark Nielsen's patch - part 1
377        if ($this->page->user_is_editing() && $this->page->user_can_edit_blocks() && $this->movingblock) {
378            // If editing is on, we need all the block regions visible, for the
379            // move blocks UI.
380            return true;
381        }
382        return !empty($this->visibleblockcontent[$region]) || !empty($this->extracontent[$region]);
383    }
384
385    /**
386     * Get an array of all of the installed blocks.
387     *
388     * @return array contents of the block table.
389     */
390    public function get_installed_blocks() {
391        global $DB;
392        if (is_null($this->allblocks)) {
393            $this->allblocks = $DB->get_records('block');
394        }
395        return $this->allblocks;
396    }
397
398    /**
399     * @return array names of block types that must exist on every page with this theme.
400     */
401    public function get_required_by_theme_block_types() {
402        $requiredbythemeblocks = false;
403        if (isset($this->page->theme->requiredblocks)) {
404            $requiredbythemeblocks = $this->page->theme->requiredblocks;
405        }
406
407        if ($requiredbythemeblocks === false) {
408            return array('navigation', 'settings');
409        } else if ($requiredbythemeblocks === '') {
410            return array();
411        } else if (is_string($requiredbythemeblocks)) {
412            return explode(',', $requiredbythemeblocks);
413        } else {
414            return $requiredbythemeblocks;
415        }
416    }
417
418    /**
419     * Make this block type undeletable and unaddable.
420     *
421     * @param mixed $blockidorname string or int
422     */
423    public static function protect_block($blockidorname) {
424        global $DB;
425
426        $syscontext = context_system::instance();
427
428        require_capability('moodle/site:config', $syscontext);
429
430        $block = false;
431        if (is_int($blockidorname)) {
432            $block = $DB->get_record('block', array('id' => $blockidorname), 'id, name', MUST_EXIST);
433        } else {
434            $block = $DB->get_record('block', array('name' => $blockidorname), 'id, name', MUST_EXIST);
435        }
436        $undeletableblocktypes = self::get_undeletable_block_types();
437        if (!in_array($block->name, $undeletableblocktypes)) {
438            $undeletableblocktypes[] = $block->name;
439            set_config('undeletableblocktypes', implode(',', $undeletableblocktypes));
440            add_to_config_log('block_protect', "0", "1", $block->name);
441        }
442    }
443
444    /**
445     * Make this block type deletable and addable.
446     *
447     * @param mixed $blockidorname string or int
448     */
449    public static function unprotect_block($blockidorname) {
450        global $DB;
451
452        $syscontext = context_system::instance();
453
454        require_capability('moodle/site:config', $syscontext);
455
456        $block = false;
457        if (is_int($blockidorname)) {
458            $block = $DB->get_record('block', array('id' => $blockidorname), 'id, name', MUST_EXIST);
459        } else {
460            $block = $DB->get_record('block', array('name' => $blockidorname), 'id, name', MUST_EXIST);
461        }
462        $undeletableblocktypes = self::get_undeletable_block_types();
463        if (in_array($block->name, $undeletableblocktypes)) {
464            $undeletableblocktypes = array_diff($undeletableblocktypes, array($block->name));
465            set_config('undeletableblocktypes', implode(',', $undeletableblocktypes));
466            add_to_config_log('block_protect', "1", "0", $block->name);
467        }
468
469    }
470
471    /**
472     * Get the list of "protected" blocks via admin block manager ui.
473     *
474     * @return array names of block types that cannot be added or deleted. E.g. array('navigation','settings').
475     */
476    public static function get_undeletable_block_types() {
477        global $CFG;
478        $undeletableblocks = false;
479        if (isset($CFG->undeletableblocktypes)) {
480            $undeletableblocks = $CFG->undeletableblocktypes;
481        }
482
483        if (empty($undeletableblocks)) {
484            return array();
485        } else if (is_string($undeletableblocks)) {
486            return explode(',', $undeletableblocks);
487        } else {
488            return $undeletableblocks;
489        }
490    }
491
492/// Setter methods =============================================================
493
494    /**
495     * Add a region to a page
496     *
497     * @param string $region add a named region where blocks may appear on the current page.
498     *      This is an internal name, like 'side-pre', not a string to display in the UI.
499     * @param bool $custom True if this is a custom block region, being added by the page rather than the theme layout.
500     */
501    public function add_region($region, $custom = true) {
502        global $SESSION;
503        $this->check_not_yet_loaded();
504        if ($custom) {
505            if (array_key_exists($region, $this->regions)) {
506                // This here is EXACTLY why we should not be adding block regions into a page. It should
507                // ALWAYS be done in a theme layout.
508                debugging('A custom region conflicts with a block region in the theme.', DEBUG_DEVELOPER);
509            }
510            // We need to register this custom region against the page type being used.
511            // This allows us to check, when performing block actions, that unrecognised regions can be worked with.
512            $type = $this->page->pagetype;
513            if (!isset($SESSION->custom_block_regions)) {
514                $SESSION->custom_block_regions = array($type => array($region));
515            } else if (!isset($SESSION->custom_block_regions[$type])) {
516                $SESSION->custom_block_regions[$type] = array($region);
517            } else if (!in_array($region, $SESSION->custom_block_regions[$type])) {
518                $SESSION->custom_block_regions[$type][] = $region;
519            }
520        }
521        $this->regions[$region] = 1;
522
523        // Checking the actual property instead of calling get_default_region as it ends up in a recursive call.
524        if (empty($this->defaultregion)) {
525            $this->set_default_region($region);
526        }
527    }
528
529    /**
530     * Add an array of regions
531     * @see add_region()
532     *
533     * @param array $regions this utility method calls add_region for each array element.
534     */
535    public function add_regions($regions, $custom = true) {
536        foreach ($regions as $region) {
537            $this->add_region($region, $custom);
538        }
539    }
540
541    /**
542     * Finds custom block regions associated with a page type and registers them with this block manager.
543     *
544     * @param string $pagetype
545     */
546    public function add_custom_regions_for_pagetype($pagetype) {
547        global $SESSION;
548        if (isset($SESSION->custom_block_regions[$pagetype])) {
549            foreach ($SESSION->custom_block_regions[$pagetype] as $customregion) {
550                $this->add_region($customregion, false);
551            }
552        }
553    }
554
555    /**
556     * Set the default region for new blocks on the page
557     *
558     * @param string $defaultregion the internal names of the region where new
559     * blocks should be added by default, and where any blocks from an
560     * unrecognised region are shown.
561     */
562    public function set_default_region($defaultregion) {
563        $this->check_not_yet_loaded();
564        if ($defaultregion) {
565            $this->check_region_is_known($defaultregion);
566        }
567        $this->defaultregion = $defaultregion;
568    }
569
570    /**
571     * Add something that looks like a block, but which isn't an actual block_instance,
572     * to this page.
573     *
574     * @param block_contents $bc the content of the block-like thing.
575     * @param string $region a block region that exists on this page.
576     */
577    public function add_fake_block($bc, $region) {
578        $this->page->initialise_theme_and_output();
579        if (!$this->is_known_region($region)) {
580            $region = $this->get_default_region();
581        }
582        if (array_key_exists($region, $this->visibleblockcontent)) {
583            throw new coding_exception('block_manager has already prepared the blocks in region ' .
584                    $region . 'for output. It is too late to add a fake block.');
585        }
586        if (!isset($bc->attributes['data-block'])) {
587            $bc->attributes['data-block'] = '_fake';
588        }
589        $bc->attributes['class'] .= ' block_fake';
590        $this->extracontent[$region][] = $bc;
591    }
592
593    /**
594     * Checks to see whether all of the blocks within the given region are docked
595     *
596     * @see region_uses_dock
597     * @param string $region
598     * @return bool True if all of the blocks within that region are docked
599     *
600     * Return false as from MDL-64506
601     */
602    public function region_completely_docked($region, $output) {
603        return false;
604    }
605
606    /**
607     * Checks to see whether any of the blocks within the given regions are docked
608     *
609     * @see region_completely_docked
610     * @param array|string $regions array of regions (or single region)
611     * @return bool True if any of the blocks within that region are docked
612     *
613     * Return false as from MDL-64506
614     */
615    public function region_uses_dock($regions, $output) {
616        return false;
617    }
618
619/// Actions ====================================================================
620
621    /**
622     * This method actually loads the blocks for our page from the database.
623     *
624     * @param boolean|null $includeinvisible
625     *      null (default) - load hidden blocks if $this->page->user_is_editing();
626     *      true - load hidden blocks.
627     *      false - don't load hidden blocks.
628     */
629    public function load_blocks($includeinvisible = null) {
630        global $DB, $CFG;
631
632        if (!is_null($this->birecordsbyregion)) {
633            // Already done.
634            return;
635        }
636
637        if ($CFG->version < 2009050619) {
638            // Upgrade/install not complete. Don't try too show any blocks.
639            $this->birecordsbyregion = array();
640            return;
641        }
642
643        // Ensure we have been initialised.
644        if (is_null($this->defaultregion)) {
645            $this->page->initialise_theme_and_output();
646            // If there are still no block regions, then there are no blocks on this page.
647            if (empty($this->regions)) {
648                $this->birecordsbyregion = array();
649                return;
650            }
651        }
652
653        // Check if we need to load normal blocks
654        if ($this->fakeblocksonly) {
655            $this->birecordsbyregion = $this->prepare_per_region_arrays();
656            return;
657        }
658
659        // Exclude auto created blocks if they are not undeletable in this theme.
660        $requiredbytheme = $this->get_required_by_theme_block_types();
661        $requiredbythemecheck = '';
662        $requiredbythemeparams = array();
663        $requiredbythemenotparams = array();
664        if (!empty($requiredbytheme)) {
665            list($testsql, $requiredbythemeparams) = $DB->get_in_or_equal($requiredbytheme, SQL_PARAMS_NAMED, 'requiredbytheme');
666            list($testnotsql, $requiredbythemenotparams) = $DB->get_in_or_equal($requiredbytheme, SQL_PARAMS_NAMED,
667                                                                                'notrequiredbytheme', false);
668            $requiredbythemecheck = 'AND ((bi.blockname ' . $testsql . ' AND bi.requiredbytheme = 1) OR ' .
669                                ' (bi.blockname ' . $testnotsql . ' AND bi.requiredbytheme = 0))';
670        } else {
671            $requiredbythemecheck = 'AND (bi.requiredbytheme = 0)';
672        }
673
674        if (is_null($includeinvisible)) {
675            $includeinvisible = $this->page->user_is_editing();
676        }
677        if ($includeinvisible) {
678            $visiblecheck = '';
679        } else {
680            $visiblecheck = 'AND (bp.visible = 1 OR bp.visible IS NULL) AND (bs.visible = 1 OR bs.visible IS NULL)';
681        }
682
683        $context = $this->page->context;
684        $contexttest = 'bi.parentcontextid IN (:contextid2, :contextid3)';
685        $parentcontextparams = array();
686        $parentcontextids = $context->get_parent_context_ids();
687        if ($parentcontextids) {
688            list($parentcontexttest, $parentcontextparams) =
689                    $DB->get_in_or_equal($parentcontextids, SQL_PARAMS_NAMED, 'parentcontext');
690            $contexttest = "($contexttest OR (bi.showinsubcontexts = 1 AND bi.parentcontextid $parentcontexttest))";
691        }
692
693        $pagetypepatterns = matching_page_type_patterns($this->page->pagetype);
694        list($pagetypepatterntest, $pagetypepatternparams) =
695                $DB->get_in_or_equal($pagetypepatterns, SQL_PARAMS_NAMED, 'pagetypepatterntest');
696
697        $ccselect = ', ' . context_helper::get_preload_record_columns_sql('ctx');
698        $ccjoin = "LEFT JOIN {context} ctx ON (ctx.instanceid = bi.id AND ctx.contextlevel = :contextlevel)";
699
700        $systemcontext = context_system::instance();
701        $params = array(
702            'contextlevel' => CONTEXT_BLOCK,
703            'subpage1' => $this->page->subpage,
704            'subpage2' => $this->page->subpage,
705            'subpage3' => $this->page->subpage,
706            'contextid1' => $context->id,
707            'contextid2' => $context->id,
708            'contextid3' => $systemcontext->id,
709            'contextid4' => $systemcontext->id,
710            'pagetype' => $this->page->pagetype,
711            'pagetype2' => $this->page->pagetype,
712        );
713        if ($this->page->subpage === '') {
714            $params['subpage1'] = '';
715            $params['subpage2'] = '';
716            $params['subpage3'] = '';
717        }
718        $sql = "SELECT
719                    bi.id,
720                    COALESCE(bp.id, bs.id) AS blockpositionid,
721                    bi.blockname,
722                    bi.parentcontextid,
723                    bi.showinsubcontexts,
724                    bi.pagetypepattern,
725                    bi.requiredbytheme,
726                    bi.subpagepattern,
727                    bi.defaultregion,
728                    bi.defaultweight,
729                    COALESCE(bp.visible, bs.visible, 1) AS visible,
730                    COALESCE(bp.region, bs.region, bi.defaultregion) AS region,
731                    COALESCE(bp.weight, bs.weight, bi.defaultweight) AS weight,
732                    bi.configdata
733                    $ccselect
734
735                FROM {block_instances} bi
736                JOIN {block} b ON bi.blockname = b.name
737                LEFT JOIN {block_positions} bp ON bp.blockinstanceid = bi.id
738                                                  AND bp.contextid = :contextid1
739                                                  AND bp.pagetype = :pagetype
740                                                  AND bp.subpage = :subpage1
741                LEFT JOIN {block_positions} bs ON bs.blockinstanceid = bi.id
742                                                  AND bs.contextid = :contextid4
743                                                  AND bs.pagetype = :pagetype2
744                                                  AND bs.subpage = :subpage3
745                $ccjoin
746
747                WHERE
748                $contexttest
749                AND bi.pagetypepattern $pagetypepatterntest
750                AND (bi.subpagepattern IS NULL OR bi.subpagepattern = :subpage2)
751                $visiblecheck
752                AND b.visible = 1
753                $requiredbythemecheck
754
755                ORDER BY
756                    COALESCE(bp.region, bs.region, bi.defaultregion),
757                    COALESCE(bp.weight, bs.weight, bi.defaultweight),
758                    bi.id";
759
760        $allparams = $params + $parentcontextparams + $pagetypepatternparams + $requiredbythemeparams + $requiredbythemenotparams;
761        $blockinstances = $DB->get_recordset_sql($sql, $allparams);
762
763        $this->birecordsbyregion = $this->prepare_per_region_arrays();
764        $unknown = array();
765        foreach ($blockinstances as $bi) {
766            context_helper::preload_from_record($bi);
767            if ($this->is_known_region($bi->region)) {
768                $this->birecordsbyregion[$bi->region][] = $bi;
769            } else {
770                $unknown[] = $bi;
771            }
772        }
773        $blockinstances->close();
774
775        // Pages don't necessarily have a defaultregion. The  one time this can
776        // happen is when there are no theme block regions, but the script itself
777        // has a block region in the main content area.
778        if (!empty($this->defaultregion)) {
779            $this->birecordsbyregion[$this->defaultregion] =
780                    array_merge($this->birecordsbyregion[$this->defaultregion], $unknown);
781        }
782    }
783
784    /**
785     * Add a block to the current page, or related pages. The block is added to
786     * context $this->page->contextid. If $pagetypepattern $subpagepattern
787     *
788     * @param string $blockname The type of block to add.
789     * @param string $region the block region on this page to add the block to.
790     * @param integer $weight determines the order where this block appears in the region.
791     * @param boolean $showinsubcontexts whether this block appears in subcontexts, or just the current context.
792     * @param string|null $pagetypepattern which page types this block should appear on. Defaults to just the current page type.
793     * @param string|null $subpagepattern which subpage this block should appear on. NULL = any (the default), otherwise only the specified subpage.
794     */
795    public function add_block($blockname, $region, $weight, $showinsubcontexts, $pagetypepattern = NULL, $subpagepattern = NULL) {
796        global $DB;
797        // Allow invisible blocks because this is used when adding default page blocks, which
798        // might include invisible ones if the user makes some default blocks invisible
799        $this->check_known_block_type($blockname, true);
800        $this->check_region_is_known($region);
801
802        if (empty($pagetypepattern)) {
803            $pagetypepattern = $this->page->pagetype;
804        }
805
806        $blockinstance = new stdClass;
807        $blockinstance->blockname = $blockname;
808        $blockinstance->parentcontextid = $this->page->context->id;
809        $blockinstance->showinsubcontexts = !empty($showinsubcontexts);
810        $blockinstance->pagetypepattern = $pagetypepattern;
811        $blockinstance->subpagepattern = $subpagepattern;
812        $blockinstance->defaultregion = $region;
813        $blockinstance->defaultweight = $weight;
814        $blockinstance->configdata = '';
815        $blockinstance->timecreated = time();
816        $blockinstance->timemodified = $blockinstance->timecreated;
817        $blockinstance->id = $DB->insert_record('block_instances', $blockinstance);
818
819        // Ensure the block context is created.
820        context_block::instance($blockinstance->id);
821
822        // If the new instance was created, allow it to do additional setup
823        if ($block = block_instance($blockname, $blockinstance)) {
824            $block->instance_create();
825        }
826    }
827
828    public function add_block_at_end_of_default_region($blockname) {
829        if (empty($this->birecordsbyregion)) {
830            // No blocks or block regions exist yet.
831            return;
832        }
833        $defaulregion = $this->get_default_region();
834
835        $lastcurrentblock = end($this->birecordsbyregion[$defaulregion]);
836        if ($lastcurrentblock) {
837            $weight = $lastcurrentblock->weight + 1;
838        } else {
839            $weight = 0;
840        }
841
842        if ($this->page->subpage) {
843            $subpage = $this->page->subpage;
844        } else {
845            $subpage = null;
846        }
847
848        // Special case. Course view page type include the course format, but we
849        // want to add the block non-format-specifically.
850        $pagetypepattern = $this->page->pagetype;
851        if (strpos($pagetypepattern, 'course-view') === 0) {
852            $pagetypepattern = 'course-view-*';
853        }
854
855        // We should end using this for ALL the blocks, making always the 1st option
856        // the default one to be used. Until then, this is one hack to avoid the
857        // 'pagetypewarning' message on blocks initial edition (MDL-27829) caused by
858        // non-existing $pagetypepattern set. This way at least we guarantee one "valid"
859        // (the FIRST $pagetypepattern will be set)
860
861        // We are applying it to all blocks created in mod pages for now and only if the
862        // default pagetype is not one of the available options
863        if (preg_match('/^mod-.*-/', $pagetypepattern)) {
864            $pagetypelist = generate_page_type_patterns($this->page->pagetype, null, $this->page->context);
865            // Only go for the first if the pagetype is not a valid option
866            if (is_array($pagetypelist) && !array_key_exists($pagetypepattern, $pagetypelist)) {
867                $pagetypepattern = key($pagetypelist);
868            }
869        }
870        // Surely other pages like course-report will need this too, they just are not important
871        // enough now. This will be decided in the coming days. (MDL-27829, MDL-28150)
872
873        $this->add_block($blockname, $defaulregion, $weight, false, $pagetypepattern, $subpage);
874    }
875
876    /**
877     * Convenience method, calls add_block repeatedly for all the blocks in $blocks. Optionally, a starting weight
878     * can be used to decide the starting point that blocks are added in the region, the weight is passed to {@link add_block}
879     * and incremented by the position of the block in the $blocks array
880     *
881     * @param array $blocks array with array keys the region names, and values an array of block names.
882     * @param string $pagetypepattern optional. Passed to {@link add_block()}
883     * @param string $subpagepattern optional. Passed to {@link add_block()}
884     * @param boolean $showinsubcontexts optional. Passed to {@link add_block()}
885     * @param integer $weight optional. Determines the starting point that the blocks are added in the region.
886     */
887    public function add_blocks($blocks, $pagetypepattern = NULL, $subpagepattern = NULL, $showinsubcontexts=false, $weight=0) {
888        $initialweight = $weight;
889        $this->add_regions(array_keys($blocks), false);
890        foreach ($blocks as $region => $regionblocks) {
891            foreach ($regionblocks as $offset => $blockname) {
892                $weight = $initialweight + $offset;
893                $this->add_block($blockname, $region, $weight, $showinsubcontexts, $pagetypepattern, $subpagepattern);
894            }
895        }
896    }
897
898    /**
899     * Move a block to a new position on this page.
900     *
901     * If this block cannot appear on any other pages, then we change defaultposition/weight
902     * in the block_instances table. Otherwise we just set the position on this page.
903     *
904     * @param $blockinstanceid the block instance id.
905     * @param $newregion the new region name.
906     * @param $newweight the new weight.
907     */
908    public function reposition_block($blockinstanceid, $newregion, $newweight) {
909        global $DB;
910
911        $this->check_region_is_known($newregion);
912        $inst = $this->find_instance($blockinstanceid);
913
914        $bi = $inst->instance;
915        if ($bi->weight == $bi->defaultweight && $bi->region == $bi->defaultregion &&
916                !$bi->showinsubcontexts && strpos($bi->pagetypepattern, '*') === false &&
917                (!$this->page->subpage || $bi->subpagepattern)) {
918
919            // Set default position
920            $newbi = new stdClass;
921            $newbi->id = $bi->id;
922            $newbi->defaultregion = $newregion;
923            $newbi->defaultweight = $newweight;
924            $newbi->timemodified = time();
925            $DB->update_record('block_instances', $newbi);
926
927            if ($bi->blockpositionid) {
928                $bp = new stdClass;
929                $bp->id = $bi->blockpositionid;
930                $bp->region = $newregion;
931                $bp->weight = $newweight;
932                $DB->update_record('block_positions', $bp);
933            }
934
935        } else {
936            // Just set position on this page.
937            $bp = new stdClass;
938            $bp->region = $newregion;
939            $bp->weight = $newweight;
940
941            if ($bi->blockpositionid) {
942                $bp->id = $bi->blockpositionid;
943                $DB->update_record('block_positions', $bp);
944
945            } else {
946                $bp->blockinstanceid = $bi->id;
947                $bp->contextid = $this->page->context->id;
948                $bp->pagetype = $this->page->pagetype;
949                if ($this->page->subpage) {
950                    $bp->subpage = $this->page->subpage;
951                } else {
952                    $bp->subpage = '';
953                }
954                $bp->visible = $bi->visible;
955                $DB->insert_record('block_positions', $bp);
956            }
957        }
958    }
959
960    /**
961     * Find a given block by its instance id
962     *
963     * @param integer $instanceid
964     * @return block_base
965     */
966    public function find_instance($instanceid) {
967        foreach ($this->regions as $region => $notused) {
968            $this->ensure_instances_exist($region);
969            foreach($this->blockinstances[$region] as $instance) {
970                if ($instance->instance->id == $instanceid) {
971                    return $instance;
972                }
973            }
974        }
975        throw new block_not_on_page_exception($instanceid, $this->page);
976    }
977
978/// Inner workings =============================================================
979
980    /**
981     * Check whether the page blocks have been loaded yet
982     *
983     * @return void Throws coding exception if already loaded
984     */
985    protected function check_not_yet_loaded() {
986        if (!is_null($this->birecordsbyregion)) {
987            throw new coding_exception('block_manager has already loaded the blocks, to it is too late to change things that might affect which blocks are visible.');
988        }
989    }
990
991    /**
992     * Check whether the page blocks have been loaded yet
993     *
994     * Nearly identical to the above function {@link check_not_yet_loaded()} except different message
995     *
996     * @return void Throws coding exception if already loaded
997     */
998    protected function check_is_loaded() {
999        if (is_null($this->birecordsbyregion)) {
1000            throw new coding_exception('block_manager has not yet loaded the blocks, to it is too soon to request the information you asked for.');
1001        }
1002    }
1003
1004    /**
1005     * Check if a block type is known and usable
1006     *
1007     * @param string $blockname The block type name to search for
1008     * @param bool $includeinvisible Include disabled block types in the initial pass
1009     * @return void Coding Exception thrown if unknown or not enabled
1010     */
1011    protected function check_known_block_type($blockname, $includeinvisible = false) {
1012        if (!$this->is_known_block_type($blockname, $includeinvisible)) {
1013            if ($this->is_known_block_type($blockname, true)) {
1014                throw new coding_exception('Unknown block type ' . $blockname);
1015            } else {
1016                throw new coding_exception('Block type ' . $blockname . ' has been disabled by the administrator.');
1017            }
1018        }
1019    }
1020
1021    /**
1022     * Check if a region is known by its name
1023     *
1024     * @param string $region
1025     * @return void Coding Exception thrown if the region is not known
1026     */
1027    protected function check_region_is_known($region) {
1028        if (!$this->is_known_region($region)) {
1029            throw new coding_exception('Trying to reference an unknown block region ' . $region);
1030        }
1031    }
1032
1033    /**
1034     * Returns an array of region names as keys and nested arrays for values
1035     *
1036     * @return array an array where the array keys are the region names, and the array
1037     * values are empty arrays.
1038     */
1039    protected function prepare_per_region_arrays() {
1040        $result = array();
1041        foreach ($this->regions as $region => $notused) {
1042            $result[$region] = array();
1043        }
1044        return $result;
1045    }
1046
1047    /**
1048     * Create a set of new block instance from a record array
1049     *
1050     * @param array $birecords An array of block instance records
1051     * @return array An array of instantiated block_instance objects
1052     */
1053    protected function create_block_instances($birecords) {
1054        $results = array();
1055        foreach ($birecords as $record) {
1056            if ($blockobject = block_instance($record->blockname, $record, $this->page)) {
1057                $results[] = $blockobject;
1058            }
1059        }
1060        return $results;
1061    }
1062
1063    /**
1064     * Create all the block instances for all the blocks that were loaded by
1065     * load_blocks. This is used, for example, to ensure that all blocks get a
1066     * chance to initialise themselves via the {@link block_base::specialize()}
1067     * method, before any output is done.
1068     *
1069     * It is also used to create any blocks that are "requiredbytheme" by the current theme.
1070     * These blocks that are auto-created have requiredbytheme set on the block instance
1071     * so they are only visible on themes that require them.
1072     */
1073    public function create_all_block_instances() {
1074        $missing = false;
1075
1076        // If there are any un-removable blocks that were not created - force them.
1077        $requiredbytheme = $this->get_required_by_theme_block_types();
1078        if (!$this->fakeblocksonly) {
1079            foreach ($requiredbytheme as $forced) {
1080                if (empty($forced)) {
1081                    continue;
1082                }
1083                $found = false;
1084                foreach ($this->get_regions() as $region) {
1085                    foreach($this->birecordsbyregion[$region] as $instance) {
1086                        if ($instance->blockname == $forced) {
1087                            $found = true;
1088                        }
1089                    }
1090                }
1091                if (!$found) {
1092                    $this->add_block_required_by_theme($forced);
1093                    $missing = true;
1094                }
1095            }
1096        }
1097
1098        if ($missing) {
1099            // Some blocks were missing. Lets do it again.
1100            $this->birecordsbyregion = null;
1101            $this->load_blocks();
1102        }
1103        foreach ($this->get_regions() as $region) {
1104            $this->ensure_instances_exist($region);
1105        }
1106
1107    }
1108
1109    /**
1110     * Add a block that is required by the current theme but has not been
1111     * created yet. This is a special type of block that only shows in themes that
1112     * require it (by listing it in undeletable_block_types).
1113     *
1114     * @param string $blockname the name of the block type.
1115     */
1116    protected function add_block_required_by_theme($blockname) {
1117        global $DB;
1118
1119        if (empty($this->birecordsbyregion)) {
1120            // No blocks or block regions exist yet.
1121            return;
1122        }
1123
1124        // Never auto create blocks when we are showing fake blocks only.
1125        if ($this->fakeblocksonly) {
1126            return;
1127        }
1128
1129        // Never add a duplicate block required by theme.
1130        if ($DB->record_exists('block_instances', array('blockname' => $blockname, 'requiredbytheme' => 1))) {
1131            return;
1132        }
1133
1134        $systemcontext = context_system::instance();
1135        $defaultregion = $this->get_default_region();
1136        // Add a special system wide block instance only for themes that require it.
1137        $blockinstance = new stdClass;
1138        $blockinstance->blockname = $blockname;
1139        $blockinstance->parentcontextid = $systemcontext->id;
1140        $blockinstance->showinsubcontexts = true;
1141        $blockinstance->requiredbytheme = true;
1142        $blockinstance->pagetypepattern = '*';
1143        $blockinstance->subpagepattern = null;
1144        $blockinstance->defaultregion = $defaultregion;
1145        $blockinstance->defaultweight = 0;
1146        $blockinstance->configdata = '';
1147        $blockinstance->timecreated = time();
1148        $blockinstance->timemodified = $blockinstance->timecreated;
1149        $blockinstance->id = $DB->insert_record('block_instances', $blockinstance);
1150
1151        // Ensure the block context is created.
1152        context_block::instance($blockinstance->id);
1153
1154        // If the new instance was created, allow it to do additional setup.
1155        if ($block = block_instance($blockname, $blockinstance)) {
1156            $block->instance_create();
1157        }
1158    }
1159
1160    /**
1161     * Return an array of content objects from a set of block instances
1162     *
1163     * @param array $instances An array of block instances
1164     * @param renderer_base The renderer to use.
1165     * @param string $region the region name.
1166     * @return array An array of block_content (and possibly block_move_target) objects.
1167     */
1168    protected function create_block_contents($instances, $output, $region) {
1169        $results = array();
1170
1171        $lastweight = 0;
1172        $lastblock = 0;
1173        if ($this->movingblock) {
1174            $first = reset($instances);
1175            if ($first) {
1176                $lastweight = $first->instance->weight - 2;
1177            }
1178        }
1179
1180        foreach ($instances as $instance) {
1181            $content = $instance->get_content_for_output($output);
1182            if (empty($content)) {
1183                continue;
1184            }
1185
1186            if ($this->movingblock && $lastweight != $instance->instance->weight &&
1187                    $content->blockinstanceid != $this->movingblock && $lastblock != $this->movingblock) {
1188                $results[] = new block_move_target($this->get_move_target_url($region, ($lastweight + $instance->instance->weight)/2));
1189            }
1190
1191            if ($content->blockinstanceid == $this->movingblock) {
1192                $content->add_class('beingmoved');
1193                $content->annotation .= get_string('movingthisblockcancel', 'block',
1194                        html_writer::link($this->page->url, get_string('cancel')));
1195            }
1196
1197            $results[] = $content;
1198            $lastweight = $instance->instance->weight;
1199            $lastblock = $instance->instance->id;
1200        }
1201
1202        if ($this->movingblock && $lastblock != $this->movingblock) {
1203            $results[] = new block_move_target($this->get_move_target_url($region, $lastweight + 1));
1204        }
1205        return $results;
1206    }
1207
1208    /**
1209     * Ensure block instances exist for a given region
1210     *
1211     * @param string $region Check for bi's with the instance with this name
1212     */
1213    protected function ensure_instances_exist($region) {
1214        $this->check_region_is_known($region);
1215        if (!array_key_exists($region, $this->blockinstances)) {
1216            $this->blockinstances[$region] =
1217                    $this->create_block_instances($this->birecordsbyregion[$region]);
1218        }
1219    }
1220
1221    /**
1222     * Ensure that there is some content within the given region
1223     *
1224     * @param string $region The name of the region to check
1225     */
1226    public function ensure_content_created($region, $output) {
1227        $this->ensure_instances_exist($region);
1228
1229        if (!has_capability('moodle/block:view', $this->page->context) ) {
1230            $this->visibleblockcontent[$region] = [];
1231            return;
1232        }
1233
1234        if (!array_key_exists($region, $this->visibleblockcontent)) {
1235            $contents = array();
1236            if (array_key_exists($region, $this->extracontent)) {
1237                $contents = $this->extracontent[$region];
1238            }
1239            $contents = array_merge($contents, $this->create_block_contents($this->blockinstances[$region], $output, $region));
1240            if (($region == $this->defaultregion) && (!isset($this->page->theme->addblockposition) ||
1241                    $this->page->theme->addblockposition == BLOCK_ADDBLOCK_POSITION_DEFAULT)) {
1242                $addblockui = block_add_block_ui($this->page, $output);
1243                if ($addblockui) {
1244                    $contents[] = $addblockui;
1245                }
1246            }
1247            $this->visibleblockcontent[$region] = $contents;
1248        }
1249    }
1250
1251/// Process actions from the URL ===============================================
1252
1253    /**
1254     * Get the appropriate list of editing icons for a block. This is used
1255     * to set {@link block_contents::$controls} in {@link block_base::get_contents_for_output()}.
1256     *
1257     * @param $output The core_renderer to use when generating the output. (Need to get icon paths.)
1258     * @return an array in the format for {@link block_contents::$controls}
1259     */
1260    public function edit_controls($block) {
1261        global $CFG;
1262
1263        $controls = array();
1264        $actionurl = $this->page->url->out(false, array('sesskey'=> sesskey()));
1265        $blocktitle = $block->title;
1266        if (empty($blocktitle)) {
1267            $blocktitle = $block->arialabel;
1268        }
1269
1270        if ($this->page->user_can_edit_blocks()) {
1271            // Move icon.
1272            $str = new lang_string('moveblock', 'block', $blocktitle);
1273            $controls[] = new action_menu_link_primary(
1274                new moodle_url($actionurl, array('bui_moveid' => $block->instance->id)),
1275                new pix_icon('t/move', $str, 'moodle', array('class' => 'iconsmall', 'title' => '')),
1276                $str,
1277                array('class' => 'editing_move')
1278            );
1279
1280        }
1281
1282        if ($this->page->user_can_edit_blocks() || $block->user_can_edit()) {
1283            // Edit config icon - always show - needed for positioning UI.
1284            $str = new lang_string('configureblock', 'block', $blocktitle);
1285            $editactionurl = new moodle_url($actionurl, ['bui_editid' => $block->instance->id]);
1286            $editactionurl->remove_params(['sesskey']);
1287
1288            // Handle editing block on admin index page, prevent the page redirecting before block action can begin.
1289            if ($editactionurl->compare(new moodle_url('/admin/index.php'), URL_MATCH_BASE)) {
1290                $editactionurl->param('cache', 1);
1291            }
1292
1293            $controls[] = new action_menu_link_secondary(
1294                $editactionurl,
1295                new pix_icon('t/edit', $str, 'moodle', array('class' => 'iconsmall', 'title' => '')),
1296                $str,
1297                array('class' => 'editing_edit')
1298            );
1299
1300        }
1301
1302        if ($this->page->user_can_edit_blocks() && $block->instance_can_be_hidden()) {
1303            // Show/hide icon.
1304            if ($block->instance->visible) {
1305                $str = new lang_string('hideblock', 'block', $blocktitle);
1306                $url = new moodle_url($actionurl, array('bui_hideid' => $block->instance->id));
1307                $icon = new pix_icon('t/hide', $str, 'moodle', array('class' => 'iconsmall', 'title' => ''));
1308                $attributes = array('class' => 'editing_hide');
1309            } else {
1310                $str = new lang_string('showblock', 'block', $blocktitle);
1311                $url = new moodle_url($actionurl, array('bui_showid' => $block->instance->id));
1312                $icon = new pix_icon('t/show', $str, 'moodle', array('class' => 'iconsmall', 'title' => ''));
1313                $attributes = array('class' => 'editing_show');
1314            }
1315            $controls[] = new action_menu_link_secondary($url, $icon, $str, $attributes);
1316        }
1317
1318        // Assign roles.
1319        if (get_assignable_roles($block->context, ROLENAME_SHORT)) {
1320            $rolesurl = new moodle_url('/admin/roles/assign.php', array('contextid' => $block->context->id,
1321                'returnurl' => $this->page->url->out_as_local_url()));
1322            $str = new lang_string('assignrolesinblock', 'block', $blocktitle);
1323            $controls[] = new action_menu_link_secondary(
1324                $rolesurl,
1325                new pix_icon('i/assignroles', $str, 'moodle', array('class' => 'iconsmall', 'title' => '')),
1326                $str, array('class' => 'editing_assignroles')
1327            );
1328        }
1329
1330        // Permissions.
1331        if (has_capability('moodle/role:review', $block->context) or get_overridable_roles($block->context)) {
1332            $rolesurl = new moodle_url('/admin/roles/permissions.php', array('contextid' => $block->context->id,
1333                'returnurl' => $this->page->url->out_as_local_url()));
1334            $str = get_string('permissions', 'role');
1335            $controls[] = new action_menu_link_secondary(
1336                $rolesurl,
1337                new pix_icon('i/permissions', $str, 'moodle', array('class' => 'iconsmall', 'title' => '')),
1338                $str, array('class' => 'editing_permissions')
1339            );
1340        }
1341
1342        // Change permissions.
1343        if (has_any_capability(array('moodle/role:safeoverride', 'moodle/role:override', 'moodle/role:assign'), $block->context)) {
1344            $rolesurl = new moodle_url('/admin/roles/check.php', array('contextid' => $block->context->id,
1345                'returnurl' => $this->page->url->out_as_local_url()));
1346            $str = get_string('checkpermissions', 'role');
1347            $controls[] = new action_menu_link_secondary(
1348                $rolesurl,
1349                new pix_icon('i/checkpermissions', $str, 'moodle', array('class' => 'iconsmall', 'title' => '')),
1350                $str, array('class' => 'editing_checkroles')
1351            );
1352        }
1353
1354        if ($this->user_can_delete_block($block)) {
1355            // Delete icon.
1356            $str = new lang_string('deleteblock', 'block', $blocktitle);
1357            $deleteactionurl = new moodle_url($actionurl, ['bui_deleteid' => $block->instance->id]);
1358            $deleteactionurl->remove_params(['sesskey']);
1359
1360            // Handle deleting block on admin index page, prevent the page redirecting before block action can begin.
1361            if ($deleteactionurl->compare(new moodle_url('/admin/index.php'), URL_MATCH_BASE)) {
1362                $deleteactionurl->param('cache', 1);
1363            }
1364
1365            $controls[] = new action_menu_link_secondary(
1366                $deleteactionurl,
1367                new pix_icon('t/delete', $str, 'moodle', array('class' => 'iconsmall', 'title' => '')),
1368                $str,
1369                array('class' => 'editing_delete')
1370            );
1371        }
1372
1373        if (!empty($CFG->contextlocking) && has_capability('moodle/site:managecontextlocks', $block->context)) {
1374            $parentcontext = $block->context->get_parent_context();
1375            if (empty($parentcontext) || empty($parentcontext->locked)) {
1376                if ($block->context->locked) {
1377                    $lockicon = 'i/unlock';
1378                    $lockstring = get_string('managecontextunlock', 'admin');
1379                } else {
1380                    $lockicon = 'i/lock';
1381                    $lockstring = get_string('managecontextlock', 'admin');
1382                }
1383                $controls[] = new action_menu_link_secondary(
1384                    new moodle_url(
1385                        '/admin/lock.php',
1386                        [
1387                            'id' => $block->context->id,
1388                        ]
1389                    ),
1390                    new pix_icon($lockicon, $lockstring, 'moodle', array('class' => 'iconsmall', 'title' => '')),
1391                    $lockstring,
1392                    ['class' => 'editing_lock']
1393                );
1394            }
1395        }
1396
1397        return $controls;
1398    }
1399
1400    /**
1401     * @param block_base $block a block that appears on this page.
1402     * @return boolean boolean whether the currently logged in user is allowed to delete this block.
1403     */
1404    protected function user_can_delete_block($block) {
1405        return $this->page->user_can_edit_blocks() && $block->user_can_edit() &&
1406                $block->user_can_addto($this->page) &&
1407                !in_array($block->instance->blockname, self::get_undeletable_block_types()) &&
1408                !in_array($block->instance->blockname, $this->get_required_by_theme_block_types());
1409    }
1410
1411    /**
1412     * Process any block actions that were specified in the URL.
1413     *
1414     * @return boolean true if anything was done. False if not.
1415     */
1416    public function process_url_actions() {
1417        if (!$this->page->user_is_editing()) {
1418            return false;
1419        }
1420        return $this->process_url_add() || $this->process_url_delete() ||
1421            $this->process_url_show_hide() || $this->process_url_edit() ||
1422            $this->process_url_move();
1423    }
1424
1425    /**
1426     * Handle adding a block.
1427     * @return boolean true if anything was done. False if not.
1428     */
1429    public function process_url_add() {
1430        global $CFG, $PAGE, $OUTPUT;
1431
1432        $blocktype = optional_param('bui_addblock', null, PARAM_PLUGIN);
1433        if ($blocktype === null) {
1434            return false;
1435        }
1436
1437        require_sesskey();
1438
1439        if (!$this->page->user_can_edit_blocks()) {
1440            throw new moodle_exception('nopermissions', '', $this->page->url->out(), get_string('addblock'));
1441        }
1442
1443        $addableblocks = $this->get_addable_blocks();
1444
1445        if ($blocktype === '') {
1446            // Display add block selection.
1447            $addpage = new moodle_page();
1448            $addpage->set_pagelayout('admin');
1449            $addpage->blocks->show_only_fake_blocks(true);
1450            $addpage->set_course($this->page->course);
1451            $addpage->set_context($this->page->context);
1452            if ($this->page->cm) {
1453                $addpage->set_cm($this->page->cm);
1454            }
1455
1456            $addpagebase = str_replace($CFG->wwwroot . '/', '/', $this->page->url->out_omit_querystring());
1457            $addpageparams = $this->page->url->params();
1458            $addpage->set_url($addpagebase, $addpageparams);
1459            $addpage->set_block_actions_done();
1460            // At this point we are going to display the block selector, overwrite global $PAGE ready for this.
1461            $PAGE = $addpage;
1462            // Some functions use $OUTPUT so we need to replace that too.
1463            $OUTPUT = $addpage->get_renderer('core');
1464
1465            $site = get_site();
1466            $straddblock = get_string('addblock');
1467
1468            $PAGE->navbar->add($straddblock);
1469            $PAGE->set_title($straddblock);
1470            $PAGE->set_heading($site->fullname);
1471            echo $OUTPUT->header();
1472            echo $OUTPUT->heading($straddblock);
1473
1474            if (!$addableblocks) {
1475                echo $OUTPUT->box(get_string('noblockstoaddhere'));
1476                echo $OUTPUT->container($OUTPUT->action_link($addpage->url, get_string('back')), 'mx-3 mb-1');
1477            } else {
1478                $url = new moodle_url($addpage->url, array('sesskey' => sesskey()));
1479                echo $OUTPUT->render_from_template('core/add_block_body',
1480                    ['blocks' => array_values($addableblocks),
1481                     'url' => '?' . $url->get_query_string(false)]);
1482                echo $OUTPUT->container($OUTPUT->action_link($addpage->url, get_string('cancel')), 'mx-3 mb-1');
1483            }
1484
1485            echo $OUTPUT->footer();
1486            // Make sure that nothing else happens after we have displayed this form.
1487            exit;
1488        }
1489
1490        if (!array_key_exists($blocktype, $addableblocks)) {
1491            throw new moodle_exception('cannotaddthisblocktype', '', $this->page->url->out(), $blocktype);
1492        }
1493
1494        $this->add_block_at_end_of_default_region($blocktype);
1495
1496        // If the page URL was a guess, it will contain the bui_... param, so we must make sure it is not there.
1497        $this->page->ensure_param_not_in_url('bui_addblock');
1498
1499        return true;
1500    }
1501
1502    /**
1503     * Handle deleting a block.
1504     * @return boolean true if anything was done. False if not.
1505     */
1506    public function process_url_delete() {
1507        global $CFG, $PAGE, $OUTPUT;
1508
1509        $blockid = optional_param('bui_deleteid', null, PARAM_INT);
1510        $confirmdelete = optional_param('bui_confirm', null, PARAM_INT);
1511
1512        if (!$blockid) {
1513            return false;
1514        }
1515
1516        $block = $this->page->blocks->find_instance($blockid);
1517        if (!$this->user_can_delete_block($block)) {
1518            throw new moodle_exception('nopermissions', '', $this->page->url->out(), get_string('deleteablock'));
1519        }
1520
1521        if (!$confirmdelete) {
1522            $deletepage = new moodle_page();
1523            $deletepage->set_pagelayout('admin');
1524            $deletepage->blocks->show_only_fake_blocks(true);
1525            $deletepage->set_course($this->page->course);
1526            $deletepage->set_context($this->page->context);
1527            if ($this->page->cm) {
1528                $deletepage->set_cm($this->page->cm);
1529            }
1530
1531            $deleteurlbase = str_replace($CFG->wwwroot . '/', '/', $this->page->url->out_omit_querystring());
1532            $deleteurlparams = $this->page->url->params();
1533            $deletepage->set_url($deleteurlbase, $deleteurlparams);
1534            $deletepage->set_block_actions_done();
1535            // At this point we are either going to redirect, or display the form, so
1536            // overwrite global $PAGE ready for this. (Formslib refers to it.)
1537            $PAGE = $deletepage;
1538            //some functions like MoodleQuickForm::addHelpButton use $OUTPUT so we need to replace that too
1539            $output = $deletepage->get_renderer('core');
1540            $OUTPUT = $output;
1541
1542            $site = get_site();
1543            $blocktitle = $block->get_title();
1544            $strdeletecheck = get_string('deletecheck', 'block', $blocktitle);
1545            $message = get_string('deleteblockcheck', 'block', $blocktitle);
1546
1547            // If the block is being shown in sub contexts display a warning.
1548            if ($block->instance->showinsubcontexts == 1) {
1549                $parentcontext = context::instance_by_id($block->instance->parentcontextid);
1550                $systemcontext = context_system::instance();
1551                $messagestring = new stdClass();
1552                $messagestring->location = $parentcontext->get_context_name();
1553
1554                // Checking for blocks that may have visibility on the front page and pages added on that.
1555                if ($parentcontext->id != $systemcontext->id && is_inside_frontpage($parentcontext)) {
1556                    $messagestring->pagetype = get_string('showonfrontpageandsubs', 'block');
1557                } else {
1558                    $pagetypes = generate_page_type_patterns($this->page->pagetype, $parentcontext);
1559                    $messagestring->pagetype = $block->instance->pagetypepattern;
1560                    if (isset($pagetypes[$block->instance->pagetypepattern])) {
1561                        $messagestring->pagetype = $pagetypes[$block->instance->pagetypepattern];
1562                    }
1563                }
1564
1565                $message = get_string('deleteblockwarning', 'block', $messagestring);
1566            }
1567
1568            $PAGE->navbar->add($strdeletecheck);
1569            $PAGE->set_title($blocktitle . ': ' . $strdeletecheck);
1570            $PAGE->set_heading($site->fullname);
1571            echo $OUTPUT->header();
1572            $confirmurl = new moodle_url($deletepage->url, array('sesskey' => sesskey(), 'bui_deleteid' => $block->instance->id, 'bui_confirm' => 1));
1573            $cancelurl = new moodle_url($deletepage->url);
1574            $yesbutton = new single_button($confirmurl, get_string('yes'));
1575            $nobutton = new single_button($cancelurl, get_string('no'));
1576            echo $OUTPUT->confirm($message, $yesbutton, $nobutton);
1577            echo $OUTPUT->footer();
1578            // Make sure that nothing else happens after we have displayed this form.
1579            exit;
1580        } else {
1581            require_sesskey();
1582
1583            blocks_delete_instance($block->instance);
1584            // bui_deleteid and bui_confirm should not be in the PAGE url.
1585            $this->page->ensure_param_not_in_url('bui_deleteid');
1586            $this->page->ensure_param_not_in_url('bui_confirm');
1587            return true;
1588        }
1589    }
1590
1591    /**
1592     * Handle showing or hiding a block.
1593     * @return boolean true if anything was done. False if not.
1594     */
1595    public function process_url_show_hide() {
1596        if ($blockid = optional_param('bui_hideid', null, PARAM_INT)) {
1597            $newvisibility = 0;
1598        } else if ($blockid = optional_param('bui_showid', null, PARAM_INT)) {
1599            $newvisibility = 1;
1600        } else {
1601            return false;
1602        }
1603
1604        require_sesskey();
1605
1606        $block = $this->page->blocks->find_instance($blockid);
1607
1608        if (!$this->page->user_can_edit_blocks()) {
1609            throw new moodle_exception('nopermissions', '', $this->page->url->out(), get_string('hideshowblocks'));
1610        } else if (!$block->instance_can_be_hidden()) {
1611            return false;
1612        }
1613
1614        blocks_set_visibility($block->instance, $this->page, $newvisibility);
1615
1616        // If the page URL was a guses, it will contain the bui_... param, so we must make sure it is not there.
1617        $this->page->ensure_param_not_in_url('bui_hideid');
1618        $this->page->ensure_param_not_in_url('bui_showid');
1619
1620        return true;
1621    }
1622
1623    /**
1624     * Handle showing/processing the submission from the block editing form.
1625     * @return boolean true if the form was submitted and the new config saved. Does not
1626     *      return if the editing form was displayed. False otherwise.
1627     */
1628    public function process_url_edit() {
1629        global $CFG, $DB, $PAGE, $OUTPUT;
1630
1631        $blockid = optional_param('bui_editid', null, PARAM_INT);
1632        if (!$blockid) {
1633            return false;
1634        }
1635
1636        require_once($CFG->dirroot . '/blocks/edit_form.php');
1637
1638        $block = $this->find_instance($blockid);
1639
1640        if (!$block->user_can_edit() && !$this->page->user_can_edit_blocks()) {
1641            throw new moodle_exception('nopermissions', '', $this->page->url->out(), get_string('editblock'));
1642        }
1643
1644        $editpage = new moodle_page();
1645        $editpage->set_pagelayout('admin');
1646        $editpage->blocks->show_only_fake_blocks(true);
1647        $editpage->set_course($this->page->course);
1648        //$editpage->set_context($block->context);
1649        $editpage->set_context($this->page->context);
1650        if ($this->page->cm) {
1651            $editpage->set_cm($this->page->cm);
1652        }
1653        $editurlbase = str_replace($CFG->wwwroot . '/', '/', $this->page->url->out_omit_querystring());
1654        $editurlparams = $this->page->url->params();
1655        $editurlparams['bui_editid'] = $blockid;
1656        $editpage->set_url($editurlbase, $editurlparams);
1657        $editpage->set_block_actions_done();
1658        // At this point we are either going to redirect, or display the form, so
1659        // overwrite global $PAGE ready for this. (Formslib refers to it.)
1660        $PAGE = $editpage;
1661        //some functions like MoodleQuickForm::addHelpButton use $OUTPUT so we need to replace that to
1662        $output = $editpage->get_renderer('core');
1663        $OUTPUT = $output;
1664
1665        $formfile = $CFG->dirroot . '/blocks/' . $block->name() . '/edit_form.php';
1666        if (is_readable($formfile)) {
1667            require_once($formfile);
1668            $classname = 'block_' . $block->name() . '_edit_form';
1669            if (!class_exists($classname)) {
1670                $classname = 'block_edit_form';
1671            }
1672        } else {
1673            $classname = 'block_edit_form';
1674        }
1675
1676        $mform = new $classname($editpage->url, $block, $this->page);
1677        $mform->set_data($block->instance);
1678
1679        if ($mform->is_cancelled()) {
1680            redirect($this->page->url);
1681
1682        } else if ($data = $mform->get_data()) {
1683            $bi = new stdClass;
1684            $bi->id = $block->instance->id;
1685
1686            // This may get overwritten by the special case handling below.
1687            $bi->pagetypepattern = $data->bui_pagetypepattern;
1688            $bi->showinsubcontexts = (bool) $data->bui_contexts;
1689            if (empty($data->bui_subpagepattern) || $data->bui_subpagepattern == '%@NULL@%') {
1690                $bi->subpagepattern = null;
1691            } else {
1692                $bi->subpagepattern = $data->bui_subpagepattern;
1693            }
1694
1695            $systemcontext = context_system::instance();
1696            $frontpagecontext = context_course::instance(SITEID);
1697            $parentcontext = context::instance_by_id($data->bui_parentcontextid);
1698
1699            // Updating stickiness and contexts.  See MDL-21375 for details.
1700            if (has_capability('moodle/site:manageblocks', $parentcontext)) { // Check permissions in destination
1701
1702                // Explicitly set the default context
1703                $bi->parentcontextid = $parentcontext->id;
1704
1705                if ($data->bui_editingatfrontpage) {   // The block is being edited on the front page
1706
1707                    // The interface here is a special case because the pagetype pattern is
1708                    // totally derived from the context menu.  Here are the excpetions.   MDL-30340
1709
1710                    switch ($data->bui_contexts) {
1711                        case BUI_CONTEXTS_ENTIRE_SITE:
1712                            // The user wants to show the block across the entire site
1713                            $bi->parentcontextid = $systemcontext->id;
1714                            $bi->showinsubcontexts = true;
1715                            $bi->pagetypepattern  = '*';
1716                            break;
1717                        case BUI_CONTEXTS_FRONTPAGE_SUBS:
1718                            // The user wants the block shown on the front page and all subcontexts
1719                            $bi->parentcontextid = $frontpagecontext->id;
1720                            $bi->showinsubcontexts = true;
1721                            $bi->pagetypepattern  = '*';
1722                            break;
1723                        case BUI_CONTEXTS_FRONTPAGE_ONLY:
1724                            // The user want to show the front page on the frontpage only
1725                            $bi->parentcontextid = $frontpagecontext->id;
1726                            $bi->showinsubcontexts = false;
1727                            $bi->pagetypepattern  = 'site-index';
1728                            // This is the only relevant page type anyway but we'll set it explicitly just
1729                            // in case the front page grows site-index-* subpages of its own later
1730                            break;
1731                    }
1732                }
1733            }
1734
1735            $bits = explode('-', $bi->pagetypepattern);
1736            // hacks for some contexts
1737            if (($parentcontext->contextlevel == CONTEXT_COURSE) && ($parentcontext->instanceid != SITEID)) {
1738                // For course context
1739                // is page type pattern is mod-*, change showinsubcontext to 1
1740                if ($bits[0] == 'mod' || $bi->pagetypepattern == '*') {
1741                    $bi->showinsubcontexts = 1;
1742                } else {
1743                    $bi->showinsubcontexts = 0;
1744                }
1745            } else  if ($parentcontext->contextlevel == CONTEXT_USER) {
1746                // for user context
1747                // subpagepattern should be null
1748                if ($bits[0] == 'user' or $bits[0] == 'my') {
1749                    // we don't need subpagepattern in usercontext
1750                    $bi->subpagepattern = null;
1751                }
1752            }
1753
1754            $bi->defaultregion = $data->bui_defaultregion;
1755            $bi->defaultweight = $data->bui_defaultweight;
1756            $bi->timemodified = time();
1757            $DB->update_record('block_instances', $bi);
1758
1759            if (!empty($block->config)) {
1760                $config = clone($block->config);
1761            } else {
1762                $config = new stdClass;
1763            }
1764            foreach ($data as $configfield => $value) {
1765                if (strpos($configfield, 'config_') !== 0) {
1766                    continue;
1767                }
1768                $field = substr($configfield, 7);
1769                $config->$field = $value;
1770            }
1771            $block->instance_config_save($config);
1772
1773            $bp = new stdClass;
1774            $bp->visible = $data->bui_visible;
1775            $bp->region = $data->bui_region;
1776            $bp->weight = $data->bui_weight;
1777            $needbprecord = !$data->bui_visible || $data->bui_region != $data->bui_defaultregion ||
1778                    $data->bui_weight != $data->bui_defaultweight;
1779
1780            if ($block->instance->blockpositionid && !$needbprecord) {
1781                $DB->delete_records('block_positions', array('id' => $block->instance->blockpositionid));
1782
1783            } else if ($block->instance->blockpositionid && $needbprecord) {
1784                $bp->id = $block->instance->blockpositionid;
1785                $DB->update_record('block_positions', $bp);
1786
1787            } else if ($needbprecord) {
1788                $bp->blockinstanceid = $block->instance->id;
1789                $bp->contextid = $this->page->context->id;
1790                $bp->pagetype = $this->page->pagetype;
1791                if ($this->page->subpage) {
1792                    $bp->subpage = $this->page->subpage;
1793                } else {
1794                    $bp->subpage = '';
1795                }
1796                $DB->insert_record('block_positions', $bp);
1797            }
1798
1799            redirect($this->page->url);
1800
1801        } else {
1802            $strheading = get_string('blockconfiga', 'moodle', $block->get_title());
1803            $editpage->set_title($strheading);
1804            $editpage->set_heading($strheading);
1805            $bits = explode('-', $this->page->pagetype);
1806            if ($bits[0] == 'tag' && !empty($this->page->subpage)) {
1807                // better navbar for tag pages
1808                $editpage->navbar->add(get_string('tags'), new moodle_url('/tag/'));
1809                $tag = core_tag_tag::get($this->page->subpage);
1810                // tag search page doesn't have subpageid
1811                if ($tag) {
1812                    $editpage->navbar->add($tag->get_display_name(), $tag->get_view_url());
1813                }
1814            }
1815            $editpage->navbar->add($block->get_title());
1816            $editpage->navbar->add(get_string('configuration'));
1817            echo $output->header();
1818            echo $output->heading($strheading, 2);
1819            $mform->display();
1820            echo $output->footer();
1821            exit;
1822        }
1823    }
1824
1825    /**
1826     * Handle showing/processing the submission from the block editing form.
1827     * @return boolean true if the form was submitted and the new config saved. Does not
1828     *      return if the editing form was displayed. False otherwise.
1829     */
1830    public function process_url_move() {
1831        global $CFG, $DB, $PAGE;
1832
1833        $blockid = optional_param('bui_moveid', null, PARAM_INT);
1834        if (!$blockid) {
1835            return false;
1836        }
1837
1838        require_sesskey();
1839
1840        $block = $this->find_instance($blockid);
1841
1842        if (!$this->page->user_can_edit_blocks()) {
1843            throw new moodle_exception('nopermissions', '', $this->page->url->out(), get_string('editblock'));
1844        }
1845
1846        $newregion = optional_param('bui_newregion', '', PARAM_ALPHANUMEXT);
1847        $newweight = optional_param('bui_newweight', null, PARAM_FLOAT);
1848        if (!$newregion || is_null($newweight)) {
1849            // Don't have a valid target position yet, must be just starting the move.
1850            $this->movingblock = $blockid;
1851            $this->page->ensure_param_not_in_url('bui_moveid');
1852            return false;
1853        }
1854
1855        if (!$this->is_known_region($newregion)) {
1856            throw new moodle_exception('unknownblockregion', '', $this->page->url, $newregion);
1857        }
1858
1859        // Move this block. This may involve moving other nearby blocks.
1860        $blocks = $this->birecordsbyregion[$newregion];
1861
1862        $maxweight = self::MAX_WEIGHT;
1863        $minweight = -self::MAX_WEIGHT;
1864
1865        // Initialise the used weights and spareweights array with the default values
1866        $spareweights = array();
1867        $usedweights = array();
1868        for ($i = $minweight; $i <= $maxweight; $i++) {
1869            $spareweights[$i] = $i;
1870            $usedweights[$i] = array();
1871        }
1872
1873        // Check each block and sort out where we have used weights
1874        foreach ($blocks as $bi) {
1875            if ($bi->weight > $maxweight) {
1876                // If this statement is true then the blocks weight is more than the
1877                // current maximum. To ensure that we can get the best block position
1878                // we will initialise elements within the usedweights and spareweights
1879                // arrays between the blocks weight (which will then be the new max) and
1880                // the current max
1881                $parseweight = $bi->weight;
1882                while (!array_key_exists($parseweight, $usedweights)) {
1883                    $usedweights[$parseweight] = array();
1884                    $spareweights[$parseweight] = $parseweight;
1885                    $parseweight--;
1886                }
1887                $maxweight = $bi->weight;
1888            } else if ($bi->weight < $minweight) {
1889                // As above except this time the blocks weight is LESS than the
1890                // the current minimum, so we will initialise the array from the
1891                // blocks weight (new minimum) to the current minimum
1892                $parseweight = $bi->weight;
1893                while (!array_key_exists($parseweight, $usedweights)) {
1894                    $usedweights[$parseweight] = array();
1895                    $spareweights[$parseweight] = $parseweight;
1896                    $parseweight++;
1897                }
1898                $minweight = $bi->weight;
1899            }
1900            if ($bi->id != $block->instance->id) {
1901                unset($spareweights[$bi->weight]);
1902                $usedweights[$bi->weight][] = $bi->id;
1903            }
1904        }
1905
1906        // First we find the nearest gap in the list of weights.
1907        $bestdistance = max(abs($newweight - self::MAX_WEIGHT), abs($newweight + self::MAX_WEIGHT)) + 1;
1908        $bestgap = null;
1909        foreach ($spareweights as $spareweight) {
1910            if (abs($newweight - $spareweight) < $bestdistance) {
1911                $bestdistance = abs($newweight - $spareweight);
1912                $bestgap = $spareweight;
1913            }
1914        }
1915
1916        // If there is no gap, we have to go outside -self::MAX_WEIGHT .. self::MAX_WEIGHT.
1917        if (is_null($bestgap)) {
1918            $bestgap = self::MAX_WEIGHT + 1;
1919            while (!empty($usedweights[$bestgap])) {
1920                $bestgap++;
1921            }
1922        }
1923
1924        // Now we know the gap we are aiming for, so move all the blocks along.
1925        if ($bestgap < $newweight) {
1926            $newweight = floor($newweight);
1927            for ($weight = $bestgap + 1; $weight <= $newweight; $weight++) {
1928                if (array_key_exists($weight, $usedweights)) {
1929                    foreach ($usedweights[$weight] as $biid) {
1930                        $this->reposition_block($biid, $newregion, $weight - 1);
1931                    }
1932                }
1933            }
1934            $this->reposition_block($block->instance->id, $newregion, $newweight);
1935        } else {
1936            $newweight = ceil($newweight);
1937            for ($weight = $bestgap - 1; $weight >= $newweight; $weight--) {
1938                if (array_key_exists($weight, $usedweights)) {
1939                    foreach ($usedweights[$weight] as $biid) {
1940                        $this->reposition_block($biid, $newregion, $weight + 1);
1941                    }
1942                }
1943            }
1944            $this->reposition_block($block->instance->id, $newregion, $newweight);
1945        }
1946
1947        $this->page->ensure_param_not_in_url('bui_moveid');
1948        $this->page->ensure_param_not_in_url('bui_newregion');
1949        $this->page->ensure_param_not_in_url('bui_newweight');
1950        return true;
1951    }
1952
1953    /**
1954     * Turns the display of normal blocks either on or off.
1955     *
1956     * @param bool $setting
1957     */
1958    public function show_only_fake_blocks($setting = true) {
1959        $this->fakeblocksonly = $setting;
1960    }
1961}
1962
1963/// Helper functions for working with block classes ============================
1964
1965/**
1966 * Call a class method (one that does not require a block instance) on a block class.
1967 *
1968 * @param string $blockname the name of the block.
1969 * @param string $method the method name.
1970 * @param array $param parameters to pass to the method.
1971 * @return mixed whatever the method returns.
1972 */
1973function block_method_result($blockname, $method, $param = NULL) {
1974    if(!block_load_class($blockname)) {
1975        return NULL;
1976    }
1977    return call_user_func(array('block_'.$blockname, $method), $param);
1978}
1979
1980/**
1981 * Returns a new instance of the specified block instance id.
1982 *
1983 * @param int $blockinstanceid
1984 * @return block_base the requested block instance.
1985 */
1986function block_instance_by_id($blockinstanceid) {
1987    global $DB;
1988
1989    $blockinstance = $DB->get_record('block_instances', ['id' => $blockinstanceid]);
1990    $instance = block_instance($blockinstance->blockname, $blockinstance);
1991    return $instance;
1992}
1993
1994/**
1995 * Creates a new instance of the specified block class.
1996 *
1997 * @param string $blockname the name of the block.
1998 * @param $instance block_instances DB table row (optional).
1999 * @param moodle_page $page the page this block is appearing on.
2000 * @return block_base the requested block instance.
2001 */
2002function block_instance($blockname, $instance = NULL, $page = NULL) {
2003    if(!block_load_class($blockname)) {
2004        return false;
2005    }
2006    $classname = 'block_'.$blockname;
2007    $retval = new $classname;
2008    if($instance !== NULL) {
2009        if (is_null($page)) {
2010            global $PAGE;
2011            $page = $PAGE;
2012        }
2013        $retval->_load_instance($instance, $page);
2014    }
2015    return $retval;
2016}
2017
2018/**
2019 * Load the block class for a particular type of block.
2020 *
2021 * @param string $blockname the name of the block.
2022 * @return boolean success or failure.
2023 */
2024function block_load_class($blockname) {
2025    global $CFG;
2026
2027    if(empty($blockname)) {
2028        return false;
2029    }
2030
2031    $classname = 'block_'.$blockname;
2032
2033    if(class_exists($classname)) {
2034        return true;
2035    }
2036
2037    $blockpath = $CFG->dirroot.'/blocks/'.$blockname.'/block_'.$blockname.'.php';
2038
2039    if (file_exists($blockpath)) {
2040        require_once($CFG->dirroot.'/blocks/moodleblock.class.php');
2041        include_once($blockpath);
2042    }else{
2043        //debugging("$blockname code does not exist in $blockpath", DEBUG_DEVELOPER);
2044        return false;
2045    }
2046
2047    return class_exists($classname);
2048}
2049
2050/**
2051 * Given a specific page type, return all the page type patterns that might
2052 * match it.
2053 *
2054 * @param string $pagetype for example 'course-view-weeks' or 'mod-quiz-view'.
2055 * @return array an array of all the page type patterns that might match this page type.
2056 */
2057function matching_page_type_patterns($pagetype) {
2058    $patterns = array($pagetype);
2059    $bits = explode('-', $pagetype);
2060    if (count($bits) == 3 && $bits[0] == 'mod') {
2061        if ($bits[2] == 'view') {
2062            $patterns[] = 'mod-*-view';
2063        } else if ($bits[2] == 'index') {
2064            $patterns[] = 'mod-*-index';
2065        }
2066    }
2067    while (count($bits) > 0) {
2068        $patterns[] = implode('-', $bits) . '-*';
2069        array_pop($bits);
2070    }
2071    $patterns[] = '*';
2072    return $patterns;
2073}
2074
2075/**
2076 * Give an specific pattern, return all the page type patterns that would also match it.
2077 *
2078 * @param  string $pattern the pattern, e.g. 'mod-forum-*' or 'mod-quiz-view'.
2079 * @return array of all the page type patterns matching.
2080 */
2081function matching_page_type_patterns_from_pattern($pattern) {
2082    $patterns = array($pattern);
2083    if ($pattern === '*') {
2084        return $patterns;
2085    }
2086
2087    // Only keep the part before the star because we will append -* to all the bits.
2088    $star = strpos($pattern, '-*');
2089    if ($star !== false) {
2090        $pattern = substr($pattern, 0, $star);
2091    }
2092
2093    $patterns = array_merge($patterns, matching_page_type_patterns($pattern));
2094    $patterns = array_unique($patterns);
2095
2096    return $patterns;
2097}
2098
2099/**
2100 * Given a specific page type, parent context and currect context, return all the page type patterns
2101 * that might be used by this block.
2102 *
2103 * @param string $pagetype for example 'course-view-weeks' or 'mod-quiz-view'.
2104 * @param stdClass $parentcontext Block's parent context
2105 * @param stdClass $currentcontext Current context of block
2106 * @return array an array of all the page type patterns that might match this page type.
2107 */
2108function generate_page_type_patterns($pagetype, $parentcontext = null, $currentcontext = null) {
2109    global $CFG; // Required for includes bellow.
2110
2111    $bits = explode('-', $pagetype);
2112
2113    $core = core_component::get_core_subsystems();
2114    $plugins = core_component::get_plugin_types();
2115
2116    //progressively strip pieces off the page type looking for a match
2117    $componentarray = null;
2118    for ($i = count($bits); $i > 0; $i--) {
2119        $possiblecomponentarray = array_slice($bits, 0, $i);
2120        $possiblecomponent = implode('', $possiblecomponentarray);
2121
2122        // Check to see if the component is a core component
2123        if (array_key_exists($possiblecomponent, $core) && !empty($core[$possiblecomponent])) {
2124            $libfile = $core[$possiblecomponent].'/lib.php';
2125            if (file_exists($libfile)) {
2126                require_once($libfile);
2127                $function = $possiblecomponent.'_page_type_list';
2128                if (function_exists($function)) {
2129                    if ($patterns = $function($pagetype, $parentcontext, $currentcontext)) {
2130                        break;
2131                    }
2132                }
2133            }
2134        }
2135
2136        //check the plugin directory and look for a callback
2137        if (array_key_exists($possiblecomponent, $plugins) && !empty($plugins[$possiblecomponent])) {
2138
2139            //We've found a plugin type. Look for a plugin name by getting the next section of page type
2140            if (count($bits) > $i) {
2141                $pluginname = $bits[$i];
2142                $directory = core_component::get_plugin_directory($possiblecomponent, $pluginname);
2143                if (!empty($directory)){
2144                    $libfile = $directory.'/lib.php';
2145                    if (file_exists($libfile)) {
2146                        require_once($libfile);
2147                        $function = $possiblecomponent.'_'.$pluginname.'_page_type_list';
2148                        if (!function_exists($function)) {
2149                            $function = $pluginname.'_page_type_list';
2150                        }
2151                        if (function_exists($function)) {
2152                            if ($patterns = $function($pagetype, $parentcontext, $currentcontext)) {
2153                                break;
2154                            }
2155                        }
2156                    }
2157                }
2158            }
2159
2160            //we'll only get to here if we still don't have any patterns
2161            //the plugin type may have a callback
2162            $directory = $plugins[$possiblecomponent];
2163            $libfile = $directory.'/lib.php';
2164            if (file_exists($libfile)) {
2165                require_once($libfile);
2166                $function = $possiblecomponent.'_page_type_list';
2167                if (function_exists($function)) {
2168                    if ($patterns = $function($pagetype, $parentcontext, $currentcontext)) {
2169                        break;
2170                    }
2171                }
2172            }
2173        }
2174    }
2175
2176    if (empty($patterns)) {
2177        $patterns = default_page_type_list($pagetype, $parentcontext, $currentcontext);
2178    }
2179
2180    // Ensure that the * pattern is always available if editing block 'at distance', so
2181    // we always can 'bring back' it to the original context. MDL-30340
2182    if ((!isset($currentcontext) or !isset($parentcontext) or $currentcontext->id != $parentcontext->id) && !isset($patterns['*'])) {
2183        // TODO: We could change the string here, showing its 'bring back' meaning
2184        $patterns['*'] = get_string('page-x', 'pagetype');
2185    }
2186
2187    return $patterns;
2188}
2189
2190/**
2191 * Generates a default page type list when a more appropriate callback cannot be decided upon.
2192 *
2193 * @param string $pagetype
2194 * @param stdClass $parentcontext
2195 * @param stdClass $currentcontext
2196 * @return array
2197 */
2198function default_page_type_list($pagetype, $parentcontext = null, $currentcontext = null) {
2199    // Generate page type patterns based on current page type if
2200    // callbacks haven't been defined
2201    $patterns = array($pagetype => $pagetype);
2202    $bits = explode('-', $pagetype);
2203    while (count($bits) > 0) {
2204        $pattern = implode('-', $bits) . '-*';
2205        $pagetypestringname = 'page-'.str_replace('*', 'x', $pattern);
2206        // guessing page type description
2207        if (get_string_manager()->string_exists($pagetypestringname, 'pagetype')) {
2208            $patterns[$pattern] = get_string($pagetypestringname, 'pagetype');
2209        } else {
2210            $patterns[$pattern] = $pattern;
2211        }
2212        array_pop($bits);
2213    }
2214    $patterns['*'] = get_string('page-x', 'pagetype');
2215    return $patterns;
2216}
2217
2218/**
2219 * Generates the page type list for the my moodle page
2220 *
2221 * @param string $pagetype
2222 * @param stdClass $parentcontext
2223 * @param stdClass $currentcontext
2224 * @return array
2225 */
2226function my_page_type_list($pagetype, $parentcontext = null, $currentcontext = null) {
2227    return array('my-index' => get_string('page-my-index', 'pagetype'));
2228}
2229
2230/**
2231 * Generates the page type list for a module by either locating and using the modules callback
2232 * or by generating a default list.
2233 *
2234 * @param string $pagetype
2235 * @param stdClass $parentcontext
2236 * @param stdClass $currentcontext
2237 * @return array
2238 */
2239function mod_page_type_list($pagetype, $parentcontext = null, $currentcontext = null) {
2240    $patterns = plugin_page_type_list($pagetype, $parentcontext, $currentcontext);
2241    if (empty($patterns)) {
2242        // if modules don't have callbacks
2243        // generate two default page type patterns for modules only
2244        $bits = explode('-', $pagetype);
2245        $patterns = array($pagetype => $pagetype);
2246        if ($bits[2] == 'view') {
2247            $patterns['mod-*-view'] = get_string('page-mod-x-view', 'pagetype');
2248        } else if ($bits[2] == 'index') {
2249            $patterns['mod-*-index'] = get_string('page-mod-x-index', 'pagetype');
2250        }
2251    }
2252    return $patterns;
2253}
2254/// Functions update the blocks if required by the request parameters ==========
2255
2256/**
2257 * Return a {@link block_contents} representing the add a new block UI, if
2258 * this user is allowed to see it.
2259 *
2260 * @return block_contents an appropriate block_contents, or null if the user
2261 * cannot add any blocks here.
2262 */
2263function block_add_block_ui($page, $output) {
2264    global $CFG, $OUTPUT;
2265    if (!$page->user_is_editing() || !$page->user_can_edit_blocks()) {
2266        return null;
2267    }
2268
2269    $bc = new block_contents();
2270    $bc->title = get_string('addblock');
2271    $bc->add_class('block_adminblock');
2272    $bc->attributes['data-block'] = 'adminblock';
2273
2274    $missingblocks = $page->blocks->get_addable_blocks();
2275    if (empty($missingblocks)) {
2276        $bc->content = get_string('noblockstoaddhere');
2277        return $bc;
2278    }
2279
2280    $menu = array();
2281    foreach ($missingblocks as $block) {
2282        $menu[$block->name] = $block->title;
2283    }
2284
2285    $actionurl = new moodle_url($page->url, array('sesskey'=>sesskey()));
2286    $select = new single_select($actionurl, 'bui_addblock', $menu, null, array(''=>get_string('adddots')), 'add_block');
2287    $select->set_label(get_string('addblock'), array('class'=>'accesshide'));
2288    $bc->content = $OUTPUT->render($select);
2289    return $bc;
2290}
2291
2292/**
2293 * Actually delete from the database any blocks that are currently on this page,
2294 * but which should not be there according to blocks_name_allowed_in_format.
2295 *
2296 * @todo Write/Fix this function. Currently returns immediately
2297 * @param $course
2298 */
2299function blocks_remove_inappropriate($course) {
2300    // TODO
2301    return;
2302    /*
2303    $blockmanager = blocks_get_by_page($page);
2304
2305    if (empty($blockmanager)) {
2306        return;
2307    }
2308
2309    if (($pageformat = $page->pagetype) == NULL) {
2310        return;
2311    }
2312
2313    foreach($blockmanager as $region) {
2314        foreach($region as $instance) {
2315            $block = blocks_get_record($instance->blockid);
2316            if(!blocks_name_allowed_in_format($block->name, $pageformat)) {
2317               blocks_delete_instance($instance->instance);
2318            }
2319        }
2320    }*/
2321}
2322
2323/**
2324 * Check that a given name is in a permittable format
2325 *
2326 * @param string $name
2327 * @param string $pageformat
2328 * @return bool
2329 */
2330function blocks_name_allowed_in_format($name, $pageformat) {
2331    $accept = NULL;
2332    $maxdepth = -1;
2333    if (!$bi = block_instance($name)) {
2334        return false;
2335    }
2336
2337    $formats = $bi->applicable_formats();
2338    if (!$formats) {
2339        $formats = array();
2340    }
2341    foreach ($formats as $format => $allowed) {
2342        $formatregex = '/^'.str_replace('*', '[^-]*', $format).'.*$/';
2343        $depth = substr_count($format, '-');
2344        if (preg_match($formatregex, $pageformat) && $depth > $maxdepth) {
2345            $maxdepth = $depth;
2346            $accept = $allowed;
2347        }
2348    }
2349    if ($accept === NULL) {
2350        $accept = !empty($formats['all']);
2351    }
2352    return $accept;
2353}
2354
2355/**
2356 * Delete a block, and associated data.
2357 *
2358 * @param object $instance a row from the block_instances table
2359 * @param bool $nolongerused legacy parameter. Not used, but kept for backwards compatibility.
2360 * @param bool $skipblockstables for internal use only. Makes @see blocks_delete_all_for_context() more efficient.
2361 */
2362function blocks_delete_instance($instance, $nolongerused = false, $skipblockstables = false) {
2363    global $DB;
2364
2365    // Allow plugins to use this block before we completely delete it.
2366    if ($pluginsfunction = get_plugins_with_function('pre_block_delete')) {
2367        foreach ($pluginsfunction as $plugintype => $plugins) {
2368            foreach ($plugins as $pluginfunction) {
2369                $pluginfunction($instance);
2370            }
2371        }
2372    }
2373
2374    if ($block = block_instance($instance->blockname, $instance)) {
2375        $block->instance_delete();
2376    }
2377    context_helper::delete_instance(CONTEXT_BLOCK, $instance->id);
2378
2379    if (!$skipblockstables) {
2380        $DB->delete_records('block_positions', array('blockinstanceid' => $instance->id));
2381        $DB->delete_records('block_instances', array('id' => $instance->id));
2382        $DB->delete_records_list('user_preferences', 'name', array('block'.$instance->id.'hidden','docked_block_instance_'.$instance->id));
2383    }
2384}
2385
2386/**
2387 * Delete multiple blocks at once.
2388 *
2389 * @param array $instanceids A list of block instance ID.
2390 */
2391function blocks_delete_instances($instanceids) {
2392    global $DB;
2393
2394    $limit = 1000;
2395    $count = count($instanceids);
2396    $chunks = [$instanceids];
2397    if ($count > $limit) {
2398        $chunks = array_chunk($instanceids, $limit);
2399    }
2400
2401    // Perform deletion for each chunk.
2402    foreach ($chunks as $chunk) {
2403        $instances = $DB->get_recordset_list('block_instances', 'id', $chunk);
2404        foreach ($instances as $instance) {
2405            blocks_delete_instance($instance, false, true);
2406        }
2407        $instances->close();
2408
2409        $DB->delete_records_list('block_positions', 'blockinstanceid', $chunk);
2410        $DB->delete_records_list('block_instances', 'id', $chunk);
2411
2412        $preferences = array();
2413        foreach ($chunk as $instanceid) {
2414            $preferences[] = 'block' . $instanceid . 'hidden';
2415            $preferences[] = 'docked_block_instance_' . $instanceid;
2416        }
2417        $DB->delete_records_list('user_preferences', 'name', $preferences);
2418    }
2419}
2420
2421/**
2422 * Delete all the blocks that belong to a particular context.
2423 *
2424 * @param int $contextid the context id.
2425 */
2426function blocks_delete_all_for_context($contextid) {
2427    global $DB;
2428    $instances = $DB->get_recordset('block_instances', array('parentcontextid' => $contextid));
2429    foreach ($instances as $instance) {
2430        blocks_delete_instance($instance, true);
2431    }
2432    $instances->close();
2433    $DB->delete_records('block_instances', array('parentcontextid' => $contextid));
2434    $DB->delete_records('block_positions', array('contextid' => $contextid));
2435}
2436
2437/**
2438 * Set a block to be visible or hidden on a particular page.
2439 *
2440 * @param object $instance a row from the block_instances, preferably LEFT JOINed with the
2441 *      block_positions table as return by block_manager.
2442 * @param moodle_page $page the back to set the visibility with respect to.
2443 * @param integer $newvisibility 1 for visible, 0 for hidden.
2444 */
2445function blocks_set_visibility($instance, $page, $newvisibility) {
2446    global $DB;
2447    if (!empty($instance->blockpositionid)) {
2448        // Already have local information on this page.
2449        $DB->set_field('block_positions', 'visible', $newvisibility, array('id' => $instance->blockpositionid));
2450        return;
2451    }
2452
2453    // Create a new block_positions record.
2454    $bp = new stdClass;
2455    $bp->blockinstanceid = $instance->id;
2456    $bp->contextid = $page->context->id;
2457    $bp->pagetype = $page->pagetype;
2458    if ($page->subpage) {
2459        $bp->subpage = $page->subpage;
2460    }
2461    $bp->visible = $newvisibility;
2462    $bp->region = $instance->defaultregion;
2463    $bp->weight = $instance->defaultweight;
2464    $DB->insert_record('block_positions', $bp);
2465}
2466
2467/**
2468 * Get the block record for a particular blockid - that is, a particular type os block.
2469 *
2470 * @param $int blockid block type id. If null, an array of all block types is returned.
2471 * @param bool $notusedanymore No longer used.
2472 * @return array|object row from block table, or all rows.
2473 */
2474function blocks_get_record($blockid = NULL, $notusedanymore = false) {
2475    global $PAGE;
2476    $blocks = $PAGE->blocks->get_installed_blocks();
2477    if ($blockid === NULL) {
2478        return $blocks;
2479    } else if (isset($blocks[$blockid])) {
2480        return $blocks[$blockid];
2481    } else {
2482        return false;
2483    }
2484}
2485
2486/**
2487 * Find a given block by its blockid within a provide array
2488 *
2489 * @param int $blockid
2490 * @param array $blocksarray
2491 * @return bool|object Instance if found else false
2492 */
2493function blocks_find_block($blockid, $blocksarray) {
2494    if (empty($blocksarray)) {
2495        return false;
2496    }
2497    foreach($blocksarray as $blockgroup) {
2498        if (empty($blockgroup)) {
2499            continue;
2500        }
2501        foreach($blockgroup as $instance) {
2502            if($instance->blockid == $blockid) {
2503                return $instance;
2504            }
2505        }
2506    }
2507    return false;
2508}
2509
2510// Functions for programatically adding default blocks to pages ================
2511
2512 /**
2513  * Parse a list of default blocks. See config-dist for a description of the format.
2514  *
2515  * @param string $blocksstr Determines the starting point that the blocks are added in the region.
2516  * @return array the parsed list of default blocks
2517  */
2518function blocks_parse_default_blocks_list($blocksstr) {
2519    $blocks = array();
2520    $bits = explode(':', $blocksstr);
2521    if (!empty($bits)) {
2522        $leftbits = trim(array_shift($bits));
2523        if ($leftbits != '') {
2524            $blocks[BLOCK_POS_LEFT] = explode(',', $leftbits);
2525        }
2526    }
2527    if (!empty($bits)) {
2528        $rightbits = trim(array_shift($bits));
2529        if ($rightbits != '') {
2530            $blocks[BLOCK_POS_RIGHT] = explode(',', $rightbits);
2531        }
2532    }
2533    return $blocks;
2534}
2535
2536/**
2537 * @return array the blocks that should be added to the site course by default.
2538 */
2539function blocks_get_default_site_course_blocks() {
2540    global $CFG;
2541
2542    if (isset($CFG->defaultblocks_site)) {
2543        return blocks_parse_default_blocks_list($CFG->defaultblocks_site);
2544    } else {
2545        return array(
2546            BLOCK_POS_LEFT => array(),
2547            BLOCK_POS_RIGHT => array()
2548        );
2549    }
2550}
2551
2552/**
2553 * Add the default blocks to a course.
2554 *
2555 * @param object $course a course object.
2556 */
2557function blocks_add_default_course_blocks($course) {
2558    global $CFG;
2559
2560    if (isset($CFG->defaultblocks_override)) {
2561        $blocknames = blocks_parse_default_blocks_list($CFG->defaultblocks_override);
2562
2563    } else if ($course->id == SITEID) {
2564        $blocknames = blocks_get_default_site_course_blocks();
2565
2566    } else if (isset($CFG->{'defaultblocks_' . $course->format})) {
2567        $blocknames = blocks_parse_default_blocks_list($CFG->{'defaultblocks_' . $course->format});
2568
2569    } else {
2570        require_once($CFG->dirroot. '/course/lib.php');
2571        $blocknames = course_get_format($course)->get_default_blocks();
2572
2573    }
2574
2575    if ($course->id == SITEID) {
2576        $pagetypepattern = 'site-index';
2577    } else {
2578        $pagetypepattern = 'course-view-*';
2579    }
2580    $page = new moodle_page();
2581    $page->set_course($course);
2582    $page->blocks->add_blocks($blocknames, $pagetypepattern);
2583}
2584
2585/**
2586 * Add the default system-context blocks. E.g. the admin tree.
2587 */
2588function blocks_add_default_system_blocks() {
2589    global $DB;
2590
2591    $page = new moodle_page();
2592    $page->set_context(context_system::instance());
2593    // We don't add blocks required by the theme, they will be auto-created.
2594    $page->blocks->add_blocks(array(BLOCK_POS_LEFT => array('admin_bookmarks')), 'admin-*', null, null, 2);
2595
2596    if ($defaultmypage = $DB->get_record('my_pages', array('userid' => null, 'name' => '__default', 'private' => 1))) {
2597        $subpagepattern = $defaultmypage->id;
2598    } else {
2599        $subpagepattern = null;
2600    }
2601
2602    $newblocks = array('timeline', 'private_files', 'online_users', 'badges', 'calendar_month', 'calendar_upcoming');
2603    $newcontent = array('lp', 'recentlyaccessedcourses', 'myoverview');
2604    $page->blocks->add_blocks(array(BLOCK_POS_RIGHT => $newblocks, 'content' => $newcontent), 'my-index', $subpagepattern);
2605}
2606