Proyectos de Subversion Moodle

Rev

Rev 11 | | Comparar con el anterior | Ultima modificación | Ver Log |

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