Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 603
0.00% covered (danger)
0.00%
0 / 28
CRAP
0.00% covered (danger)
0.00%
0 / 1
acp_styles
0.00% covered (danger)
0.00%
0 / 601
0.00% covered (danger)
0.00%
0 / 28
38220
0.00% covered (danger)
0.00%
0 / 1
 main
0.00% covered (danger)
0.00%
0 / 56
0.00% covered (danger)
0.00%
0 / 1
210
 frontend
0.00% covered (danger)
0.00%
0 / 11
0.00% covered (danger)
0.00%
0 / 1
12
 action_install
0.00% covered (danger)
0.00%
0 / 29
0.00% covered (danger)
0.00%
0 / 1
132
 action_uninstall
0.00% covered (danger)
0.00%
0 / 19
0.00% covered (danger)
0.00%
0 / 1
20
 action_uninstall_confirmed
0.00% covered (danger)
0.00%
0 / 33
0.00% covered (danger)
0.00%
0 / 1
110
 action_activate
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
2
 action_deactivate
0.00% covered (danger)
0.00%
0 / 13
0.00% covered (danger)
0.00%
0 / 1
12
 action_details
0.00% covered (danger)
0.00%
0 / 88
0.00% covered (danger)
0.00%
0 / 1
702
 show_installed
0.00% covered (danger)
0.00%
0 / 30
0.00% covered (danger)
0.00%
0 / 1
72
 show_available
0.00% covered (danger)
0.00%
0 / 31
0.00% covered (danger)
0.00%
0 / 1
210
 find_available
0.00% covered (danger)
0.00%
0 / 49
0.00% covered (danger)
0.00%
0 / 1
110
 show_styles_list
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
20
 show_available_child_styles
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
20
 update_styles_tree
0.00% covered (danger)
0.00%
0 / 17
0.00% covered (danger)
0.00%
0 / 1
72
 find_possible_parents
0.00% covered (danger)
0.00%
0 / 14
0.00% covered (danger)
0.00%
0 / 1
20
 list_style
0.00% covered (danger)
0.00%
0 / 64
0.00% covered (danger)
0.00%
0 / 1
210
 list_invalid
0.00% covered (danger)
0.00%
0 / 19
0.00% covered (danger)
0.00%
0 / 1
6
 welcome_message
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
 find_style_dirs
0.00% covered (danger)
0.00%
0 / 11
0.00% covered (danger)
0.00%
0 / 1
42
 sort_styles
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
72
 read_style_composer_file
0.00% covered (danger)
0.00%
0 / 13
0.00% covered (danger)
0.00%
0 / 1
110
 install_style
0.00% covered (danger)
0.00%
0 / 13
0.00% covered (danger)
0.00%
0 / 1
20
 get_styles
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
2
 get_users
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
6
 uninstall_style
0.00% covered (danger)
0.00%
0 / 18
0.00% covered (danger)
0.00%
0 / 1
6
 delete_style_files
0.00% covered (danger)
0.00%
0 / 17
0.00% covered (danger)
0.00%
0 / 1
90
 request_vars
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
56
 default_bitfield
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
20
1<?php
2/**
3*
4* This file is part of the phpBB Forum Software package.
5*
6* @copyright (c) phpBB Limited <https://www.phpbb.com>
7* @license GNU General Public License, version 2 (GPL-2.0)
8*
9* For full copyright and license information, please see
10* the docs/CREDITS.txt file.
11*
12*/
13
14/**
15* @ignore
16*/
17if (!defined('IN_PHPBB'))
18{
19    exit;
20}
21
22class acp_styles
23{
24    public $u_action;
25
26    protected $u_base_action;
27    protected $s_hidden_fields;
28    protected $mode;
29    protected $styles_path;
30    protected $styles_path_absolute = 'styles';
31    protected $default_style = 0;
32    protected $styles_list_cols = 0;
33    protected $reserved_style_names = array('adm', 'admin', 'all');
34
35    /** @var \phpbb\config\config */
36    protected $config;
37
38    /** @var \phpbb\db\driver\driver_interface */
39    protected $db;
40
41    /** @var \phpbb\language\language */
42    protected $language;
43
44    /** @var \phpbb\template\template */
45    protected $template;
46
47    /** @var \phpbb\request\request_interface */
48    protected $request;
49
50    /** @var \phpbb\cache\driver\driver_interface */
51    protected $cache;
52
53    /** @var \phpbb\auth\auth */
54    protected $auth;
55
56    /** @var \phpbb\textformatter\cache_interface */
57    protected $text_formatter_cache;
58
59    /** @var string */
60    protected $phpbb_root_path;
61
62    /** @var string */
63    protected $php_ext;
64
65    /** @var \phpbb\event\dispatcher_interface */
66    protected $dispatcher;
67
68    public function main($id, $mode)
69    {
70        global $db, $phpbb_admin_path, $phpbb_root_path, $phpEx, $template, $request, $cache, $auth, $config, $phpbb_dispatcher, $phpbb_container;
71
72        $this->db = $db;
73        $this->language = $phpbb_container->get('language');
74        $this->template = $template;
75        $this->request = $request;
76        $this->cache = $cache;
77        $this->auth = $auth;
78        $this->text_formatter_cache = $phpbb_container->get('text_formatter.cache');
79        $this->config = $config;
80        $this->phpbb_root_path = $phpbb_root_path;
81        $this->php_ext = $phpEx;
82        $this->dispatcher = $phpbb_dispatcher;
83
84        $this->default_style = $config['default_style'];
85        $this->styles_path = $this->phpbb_root_path . $this->styles_path_absolute . '/';
86
87        $this->u_base_action = append_sid("{$phpbb_admin_path}index.{$this->php_ext}", "i={$id}");
88        $this->s_hidden_fields = array(
89            'mode'        => $mode,
90        );
91
92        $this->language->add_lang('acp/styles');
93
94        $this->tpl_name = 'acp_styles';
95        $this->page_title = 'ACP_CAT_STYLES';
96        $this->mode = $mode;
97
98        $action = $this->request->variable('action', '');
99        $post_actions = array('install', 'activate', 'deactivate', 'uninstall');
100
101        foreach ($post_actions as $key)
102        {
103            if ($this->request->is_set_post($key))
104            {
105                $action = $key;
106            }
107        }
108
109        // The uninstall action uses confirm_box() to verify the validity of the request,
110        // so there is no need to check for a valid token here.
111        if (in_array($action, $post_actions) && $action != 'uninstall')
112        {
113            $is_valid_request = check_link_hash($request->variable('hash', ''), $action) || check_form_key('styles_management');
114
115            if (!$is_valid_request)
116            {
117                trigger_error($this->language->lang('FORM_INVALID') . adm_back_link($this->u_action), E_USER_WARNING);
118            }
119        }
120
121        if ($action != '')
122        {
123            $this->s_hidden_fields['action'] = $action;
124        }
125
126        $this->template->assign_vars(array(
127            'U_ACTION'            => $this->u_base_action,
128            'S_HIDDEN_FIELDS'    => build_hidden_fields($this->s_hidden_fields)
129            )
130        );
131
132        /**
133         * Run code before ACP styles action execution
134         *
135         * @event core.acp_styles_action_before
136         * @var    int     id          Module ID
137         * @var    string  mode        Active module
138         * @var    string  action      Module that should be run
139         * @since 3.1.7-RC1
140         */
141        $vars = array('id', 'mode', 'action');
142        extract($this->dispatcher->trigger_event('core.acp_styles_action_before', compact($vars)));
143
144        // Execute actions
145        switch ($action)
146        {
147            case 'install':
148                $this->action_install();
149                return;
150            case 'uninstall':
151                $this->action_uninstall();
152                return;
153            case 'activate':
154                $this->action_activate();
155                return;
156            case 'deactivate':
157                $this->action_deactivate();
158                return;
159            case 'details':
160                $this->action_details();
161                return;
162            default:
163                $this->frontend();
164        }
165    }
166
167    /**
168    * Main page
169    */
170    protected function frontend()
171    {
172        add_form_key('styles_management');
173
174        // Check mode
175        switch ($this->mode)
176        {
177            case 'style':
178                $this->welcome_message('ACP_STYLES', 'ACP_STYLES_EXPLAIN');
179                $this->show_installed();
180                return;
181            case 'install':
182                $this->welcome_message('INSTALL_STYLES', 'INSTALL_STYLES_EXPLAIN');
183                $this->show_available();
184                return;
185        }
186        trigger_error($this->language->lang('NO_MODE') . adm_back_link($this->u_action), E_USER_WARNING);
187    }
188
189    /**
190    * Install style(s)
191    */
192    protected function action_install()
193    {
194        // Get list of styles to install
195        $dirs = $this->request_vars('dir', '', true);
196
197        // Get list of styles that can be installed
198        $styles = $this->find_available(false);
199
200        // Install each style
201        $messages = array();
202        $installed_names = array();
203        $installed_dirs = array();
204        foreach ($dirs as $dir)
205        {
206            if (in_array($dir, $this->reserved_style_names))
207            {
208                $messages[] = $this->language->lang('STYLE_NAME_RESERVED', htmlspecialchars($dir, ENT_COMPAT));
209                continue;
210            }
211
212            $found = false;
213            foreach ($styles as &$style)
214            {
215                // Check if:
216                // 1. Directory matches directory we are looking for
217                // 2. Style is not installed yet
218                // 3. Style with same name or directory hasn't been installed already within this function
219                if ($style['style_path'] == $dir && empty($style['_installed']) && !in_array($style['style_path'], $installed_dirs) && !in_array($style['style_name'], $installed_names))
220                {
221                    // Install style
222                    $style['style_active'] = 1;
223                    $style['style_id'] = $this->install_style($style);
224                    $style['_installed'] = true;
225                    $found = true;
226                    $installed_names[] = $style['style_name'];
227                    $installed_dirs[] = $style['style_path'];
228                    $messages[] = $this->language->lang('STYLE_INSTALLED', htmlspecialchars($style['style_name'], ENT_COMPAT));
229                }
230            }
231            if (!$found)
232            {
233                $messages[] = $this->language->lang('STYLE_NOT_INSTALLED', htmlspecialchars($dir, ENT_COMPAT));
234            }
235        }
236
237        // Invalidate the text formatter's cache for the new styles to take effect
238        if (!empty($installed_names))
239        {
240            $this->text_formatter_cache->invalidate();
241        }
242
243        // Show message
244        if (!count($messages))
245        {
246            trigger_error($this->language->lang('NO_MATCHING_STYLES_FOUND') . adm_back_link($this->u_action), E_USER_WARNING);
247        }
248        $message = implode('<br />', $messages);
249        $message .= '<br /><br /><a href="' . $this->u_base_action . '&amp;mode=style' . '">&laquo; ' . $this->language->lang('STYLE_INSTALLED_RETURN_INSTALLED_STYLES') . '</a>';
250        $message .= '<br /><br /><a href="' . $this->u_base_action . '&amp;mode=install' . '">&raquo; ' . $this->language->lang('STYLE_INSTALLED_RETURN_UNINSTALLED_STYLES') . '</a>';
251        trigger_error($message, E_USER_NOTICE);
252    }
253
254    /**
255    * Confirm styles removal
256    */
257    protected function action_uninstall()
258    {
259        // Get list of styles to uninstall
260        $ids = $this->request_vars('id', 0, true);
261
262        // Don't remove prosilver, you can still deactivate it.
263        $sql = 'SELECT style_id
264            FROM ' . STYLES_TABLE . "
265            WHERE style_name = '" . $this->db->sql_escape('prosilver') . "'";
266        $result = $this->db->sql_query($sql);
267        $prosilver_id = (int) $this->db->sql_fetchfield('style_id');
268        $this->db->sql_freeresult($result);
269
270        if ($prosilver_id && in_array($prosilver_id, $ids))
271        {
272            trigger_error($this->language->lang('UNINSTALL_PROSILVER') . adm_back_link($this->u_action), E_USER_WARNING);
273        }
274
275        // Check if confirmation box was submitted
276        if (confirm_box(true))
277        {
278            // Uninstall
279            $this->action_uninstall_confirmed($ids, $this->request->variable('confirm_delete_files', false));
280            return;
281        }
282
283        // Confirm box
284        $s_hidden = build_hidden_fields(array(
285            'action'    => 'uninstall',
286            'ids'        => $ids
287        ));
288        $this->template->assign_var('S_CONFIRM_DELETE', true);
289        confirm_box(false, $this->language->lang('CONFIRM_UNINSTALL_STYLES'), $s_hidden, 'acp_styles.html');
290
291        // Canceled - show styles list
292        $this->frontend();
293    }
294
295    /**
296    * Uninstall styles(s)
297    *
298    * @param array $ids List of style IDs
299    * @param bool $delete_files If true, script will attempt to remove files for selected styles
300    */
301    protected function action_uninstall_confirmed($ids, $delete_files)
302    {
303        global $user, $phpbb_log;
304
305        $default = $this->default_style;
306        $uninstalled = array();
307        $messages = array();
308
309        // Check styles list
310        foreach ($ids as $id)
311        {
312            if (!$id)
313            {
314                trigger_error($this->language->lang('INVALID_STYLE_ID') . adm_back_link($this->u_action), E_USER_WARNING);
315            }
316            if ($id == $default)
317            {
318                trigger_error($this->language->lang('UNINSTALL_DEFAULT') . adm_back_link($this->u_action), E_USER_WARNING);
319            }
320            $uninstalled[$id] = false;
321        }
322
323        // Order by reversed style_id, so parent styles would be removed after child styles
324        // This way parent and child styles can be removed in same function call
325        $sql = 'SELECT *
326            FROM ' . STYLES_TABLE . '
327            WHERE style_id IN (' . implode(', ', $ids) . ')
328            ORDER BY style_id DESC';
329        $result = $this->db->sql_query($sql);
330
331        $rows = $this->db->sql_fetchrowset($result);
332        $this->db->sql_freeresult($result);
333
334        // Uninstall each style
335        $uninstalled = array();
336        foreach ($rows as $style)
337        {
338            $result = $this->uninstall_style($style);
339
340            if (is_string($result))
341            {
342                $messages[] = $result;
343                continue;
344            }
345            $messages[] = $this->language->lang('STYLE_UNINSTALLED', $style['style_name']);
346            $uninstalled[] = $style['style_name'];
347
348            // Attempt to delete files
349            if ($delete_files)
350            {
351                $messages[] = $this->language->lang($this->delete_style_files($style['style_path']) ? 'DELETE_STYLE_FILES_SUCCESS' : 'DELETE_STYLE_FILES_FAILED', $style['style_name']);
352            }
353        }
354
355        if (empty($messages))
356        {
357            // Nothing to uninstall?
358            trigger_error($this->language->lang('NO_MATCHING_STYLES_FOUND') . adm_back_link($this->u_action), E_USER_WARNING);
359        }
360
361        // Log action
362        if (count($uninstalled))
363        {
364            $phpbb_log->add('admin', $user->data['user_id'], $user->ip, 'LOG_STYLE_DELETE', false, array(implode(', ', $uninstalled)));
365        }
366
367        // Clear cache
368        $this->cache->purge();
369
370        // Show message
371        trigger_error(implode('<br />', $messages) . adm_back_link($this->u_action), E_USER_NOTICE);
372    }
373
374    /**
375    * Activate styles
376    */
377    protected function action_activate()
378    {
379        // Get list of styles to activate
380        $ids = $this->request_vars('id', 0, true);
381
382        // Activate styles
383        $sql = 'UPDATE ' . STYLES_TABLE . '
384            SET style_active = 1
385            WHERE style_id IN (' . implode(', ', $ids) . ')';
386        $this->db->sql_query($sql);
387
388        // Purge cache
389        $this->cache->destroy('sql', STYLES_TABLE);
390
391        // Show styles list
392        $this->frontend();
393    }
394
395    /**
396    * Deactivate styles
397    */
398    protected function action_deactivate()
399    {
400        // Get list of styles to deactivate
401        $ids = $this->request_vars('id', 0, true);
402
403        // Check for default style
404        foreach ($ids as $id)
405        {
406            if ($id == $this->default_style)
407            {
408                trigger_error($this->language->lang('DEACTIVATE_DEFAULT') . adm_back_link($this->u_action), E_USER_WARNING);
409            }
410        }
411
412        // Reset default style for users who use selected styles
413        $sql = 'UPDATE ' . USERS_TABLE . '
414            SET user_style = ' . (int) $this->default_style . '
415            WHERE user_style IN (' . implode(', ', $ids) . ')';
416        $this->db->sql_query($sql);
417
418        // Deactivate styles
419        $sql = 'UPDATE ' . STYLES_TABLE . '
420            SET style_active = 0
421            WHERE style_id IN (' . implode(', ', $ids) . ')';
422        $this->db->sql_query($sql);
423
424        // Purge cache
425        $this->cache->destroy('sql', STYLES_TABLE);
426
427        // Show styles list
428        $this->frontend();
429    }
430
431    /**
432    * Show style details
433    */
434    protected function action_details()
435    {
436        global $user, $phpbb_log;
437
438        $id = $this->request->variable('id', 0);
439        if (!$id)
440        {
441            trigger_error($this->language->lang('NO_MATCHING_STYLES_FOUND') . adm_back_link($this->u_action), E_USER_WARNING);
442        }
443
444        // Get all styles
445        $styles = $this->get_styles();
446        usort($styles, array($this, 'sort_styles'));
447
448        // Find current style
449        $style = false;
450        foreach ($styles as $row)
451        {
452            if ($row['style_id'] == $id)
453            {
454                $style = $row;
455                break;
456            }
457        }
458
459        if ($style === false)
460        {
461            trigger_error($this->language->lang('NO_MATCHING_STYLES_FOUND') . adm_back_link($this->u_action), E_USER_WARNING);
462        }
463
464        // Read style configuration file
465        $style_cfg = $this->read_style_composer_file($style['style_path']);
466
467        // Find all available parent styles
468        $list = $this->find_possible_parents($styles, $id);
469
470        // Add form key
471        $form_key = 'acp_styles';
472        add_form_key($form_key);
473
474        // Change data
475        if ($this->request->variable('update', false))
476        {
477            if (!check_form_key($form_key))
478            {
479                trigger_error($this->language->lang('FORM_INVALID') . adm_back_link($this->u_action), E_USER_WARNING);
480            }
481
482            $update = array(
483                'style_name'        => trim($this->request->variable('style_name', $style['style_name'])),
484                'style_parent_id'    => $this->request->variable('style_parent', (int) $style['style_parent_id']),
485                'style_active'        => $this->request->variable('style_active', (int) $style['style_active']),
486            );
487            $update_action = $this->u_action . '&amp;action=details&amp;id=' . $id;
488
489            // Check style name
490            if ($update['style_name'] != $style['style_name'])
491            {
492                if (!strlen($update['style_name']))
493                {
494                    trigger_error($this->language->lang('STYLE_ERR_STYLE_NAME') . adm_back_link($update_action), E_USER_WARNING);
495                }
496                foreach ($styles as $row)
497                {
498                    if ($row['style_name'] == $update['style_name'])
499                    {
500                        trigger_error($this->language->lang('STYLE_ERR_NAME_EXIST') . adm_back_link($update_action), E_USER_WARNING);
501                    }
502                }
503            }
504            else
505            {
506                unset($update['style_name']);
507            }
508
509            // Check parent style id
510            if ($update['style_parent_id'] != $style['style_parent_id'])
511            {
512                if ($update['style_parent_id'] != 0)
513                {
514                    $found = false;
515                    foreach ($list as $row)
516                    {
517                        if ($row['style_id'] == $update['style_parent_id'])
518                        {
519                            $found = true;
520                            $update['style_parent_tree'] = ($row['style_parent_tree'] != '' ? $row['style_parent_tree'] . '/' : '') . $row['style_path'];
521                            break;
522                        }
523                    }
524                    if (!$found)
525                    {
526                        trigger_error($this->language->lang('STYLE_ERR_INVALID_PARENT') . adm_back_link($update_action), E_USER_WARNING);
527                    }
528                }
529                else
530                {
531                    $update['style_parent_tree'] = '';
532                }
533            }
534            else
535            {
536                unset($update['style_parent_id']);
537            }
538
539            // Check style_active
540            if ($update['style_active'] != $style['style_active'])
541            {
542                if (!$update['style_active'] && $this->default_style == $style['style_id'])
543                {
544                    trigger_error($this->language->lang('DEACTIVATE_DEFAULT') . adm_back_link($update_action), E_USER_WARNING);
545                }
546            }
547            else
548            {
549                unset($update['style_active']);
550            }
551
552            // Update data
553            if (count($update))
554            {
555                $sql = 'UPDATE ' . STYLES_TABLE . '
556                    SET ' . $this->db->sql_build_array('UPDATE', $update) . "
557                    WHERE style_id = $id";
558                $this->db->sql_query($sql);
559
560                $style = array_merge($style, $update);
561
562                if (isset($update['style_parent_id']))
563                {
564                    // Update styles tree
565                    $styles = $this->get_styles();
566                    if ($this->update_styles_tree($styles, $style))
567                    {
568                        // Something was changed in styles tree, purge all cache
569                        $this->cache->purge();
570                    }
571                }
572
573                $phpbb_log->add('admin', $user->data['user_id'], $user->ip, 'LOG_STYLE_EDIT_DETAILS', false, array($style['style_name']));
574            }
575
576            // Update default style
577            $default = $this->request->variable('style_default', 0);
578            if ($default)
579            {
580                if (!$style['style_active'])
581                {
582                    trigger_error($this->language->lang('STYLE_DEFAULT_CHANGE_INACTIVE') . adm_back_link($update_action), E_USER_WARNING);
583                }
584                $this->config->set('default_style', $id);
585                $this->cache->purge();
586            }
587
588            // Show styles list
589            $this->frontend();
590            return;
591        }
592
593        // Show page title
594        $this->welcome_message('ACP_STYLES', null);
595
596        // Show parent styles
597        foreach ($list as $row)
598        {
599            $this->template->assign_block_vars('parent_styles', array(
600                'STYLE_ID'        => $row['style_id'],
601                'STYLE_NAME'    => htmlspecialchars($row['style_name'], ENT_COMPAT),
602                'LEVEL'            => $row['level'],
603                'SPACER'        => str_repeat('&nbsp; ', $row['level']),
604                )
605            );
606        }
607
608        // Show style details
609        $this->template->assign_vars(array(
610            'S_STYLE_DETAILS'    => true,
611            'STYLE_ID'            => $style['style_id'],
612            'STYLE_NAME'        => htmlspecialchars($style['style_name'], ENT_COMPAT),
613            'STYLE_PATH'        => htmlspecialchars($style['style_path'], ENT_COMPAT),
614            'STYLE_VERSION'        => htmlspecialchars($style_cfg['version'], ENT_COMPAT),
615            'STYLE_COPYRIGHT'    => strip_tags($style['style_copyright']),
616            'STYLE_PARENT'        => $style['style_parent_id'],
617            'S_STYLE_ACTIVE'    => $style['style_active'],
618            'S_STYLE_DEFAULT'    => ($style['style_id'] == $this->default_style)
619        ));
620    }
621
622    /**
623    * List installed styles
624    */
625    protected function show_installed()
626    {
627        // Get all installed styles
628        $styles = $this->get_styles();
629
630        if (!count($styles))
631        {
632            trigger_error($this->language->lang('NO_MATCHING_STYLES_FOUND') . adm_back_link($this->u_action), E_USER_WARNING);
633        }
634
635        usort($styles, array($this, 'sort_styles'));
636
637        // Get users
638        $users = $this->get_users();
639
640        // Add users counter to rows
641        foreach ($styles as &$style)
642        {
643            $style['_users'] = isset($users[$style['style_id']]) ? $users[$style['style_id']] : 0;
644        }
645
646        // Set up styles list variables
647        // Addons should increase this number and update template variable
648        $this->styles_list_cols = 5;
649        $this->template->assign_var('STYLES_LIST_COLS', $this->styles_list_cols);
650
651        // Show styles list
652        $this->show_styles_list($styles, 0, 0);
653
654        // Show styles with invalid inherits_id
655        foreach ($styles as $style)
656        {
657            if (empty($style['_shown']))
658            {
659                $style['_note'] = $this->language->lang('REQUIRES_STYLE', htmlspecialchars($style['style_parent_tree'], ENT_COMPAT));
660                $this->list_style($style, 0);
661            }
662        }
663
664        // Add buttons
665        $this->template->assign_block_vars('extra_actions', array(
666                'ACTION_NAME'    => 'activate',
667                'L_ACTION'        => $this->language->lang('STYLE_ACTIVATE'),
668            )
669        );
670
671        $this->template->assign_block_vars('extra_actions', array(
672                'ACTION_NAME'    => 'deactivate',
673                'L_ACTION'        => $this->language->lang('STYLE_DEACTIVATE'),
674            )
675        );
676
677        if (isset($this->style_counters) && $this->style_counters['total'] > 1)
678        {
679            $this->template->assign_block_vars('extra_actions', array(
680                    'ACTION_NAME'    => 'uninstall',
681                    'L_ACTION'        => $this->language->lang('STYLE_UNINSTALL'),
682                )
683            );
684        }
685    }
686
687    /**
688    * Show list of styles that can be installed
689    */
690    protected function show_available()
691    {
692        // Get list of styles
693        $styles = $this->find_available(true);
694
695        // Show styles
696        if (empty($styles))
697        {
698            trigger_error($this->language->lang('NO_UNINSTALLED_STYLE') . adm_back_link($this->u_base_action), E_USER_NOTICE);
699        }
700
701        usort($styles, array($this, 'sort_styles'));
702
703        $this->styles_list_cols = 4;
704        $this->template->assign_vars(array(
705            'STYLES_LIST_COLS'    => $this->styles_list_cols,
706            'STYLES_LIST_HIDE_COUNT'    => true
707            )
708        );
709
710        // Show styles
711        foreach ($styles as &$style)
712        {
713            if (!$style['_available'] && !empty($style['_invalid']))
714            {
715                $this->list_invalid($style);
716                continue;
717            }
718
719            // Check if style has a parent style in styles list
720            $has_parent = false;
721            if ($style['_inherit_name'] != '')
722            {
723                foreach ($styles as $parent_style)
724                {
725                    if ($parent_style['style_name'] == $style['_inherit_name'] && empty($parent_style['_shown']))
726                    {
727                        // Show parent style first
728                        $has_parent = true;
729                    }
730                }
731            }
732            if (!$has_parent)
733            {
734                $this->list_style($style, 0);
735                $this->show_available_child_styles($styles, $style['style_name'], 1);
736            }
737        }
738
739        // Show styles that do not have parent style in styles list
740        foreach ($styles as $style)
741        {
742            if (empty($style['_shown']))
743            {
744                $this->list_style($style, 0);
745            }
746        }
747
748        // Add button
749        if (isset($this->style_counters) && $this->style_counters['caninstall'] > 0)
750        {
751            $this->template->assign_block_vars('extra_actions', array(
752                    'ACTION_NAME'    => 'install',
753                    'L_ACTION'        => $this->language->lang('INSTALL_STYLES'),
754                )
755            );
756        }
757    }
758
759    /**
760    * Find styles available for installation
761    *
762    * @param bool $all if true, function will return all installable styles. if false, function will return only styles that can be installed
763    * @return array List of styles
764    */
765    protected function find_available($all)
766    {
767        // Get list of installed styles
768        $installed = $this->get_styles();
769
770        $installed_dirs = array();
771        $installed_names = array();
772        foreach ($installed as $style)
773        {
774            $installed_dirs[] = $style['style_path'];
775            $installed_names[$style['style_name']] = array(
776                'path'        => $style['style_path'],
777                'id'        => $style['style_id'],
778                'parent'    => $style['style_parent_id'],
779                'tree'        => (strlen($style['style_parent_tree']) ? $style['style_parent_tree'] . '/' : '') . $style['style_path'],
780            );
781        }
782
783        // Get list of directories
784        $dirs = $this->find_style_dirs();
785
786        // Find styles that can be installed
787        $styles = array();
788        foreach ($dirs as $dir)
789        {
790            if (in_array($dir, $installed_dirs))
791            {
792                // Style is already installed
793                continue;
794            }
795
796            try
797            {
798                $style_data = $this->read_style_composer_file($dir);
799            }
800            catch (\DomainException $e)
801            {
802                // Invalid composer.json
803                $style = array(
804                    '_available'    => false,
805                    '_invalid'        => true,
806                    'style_path'    => $dir,
807                );
808                $styles[] = $style;
809
810                continue;
811            }
812
813            // Style should be available for installation
814            $parent = $style_data['extra']['parent-style'];
815            $style = array(
816                'style_id'            => 0,
817                'style_name'        => $style_data['extra']['display-name'],
818                'style_copyright'    => $style_data['license'],
819                'style_active'        => 0,
820                'style_path'        => $dir,
821                'bbcode_bitfield'    => $style_data['extra']['template-bitfield'],
822                'style_parent_id'    => 0,
823                'style_parent_tree'    => '',
824                // Extra values for styles list
825                // All extra variable start with _ so they won't be confused with data that can be added to styles table
826                '_inherit_name'            => $parent,
827                '_available'            => true,
828                '_note'                    => '',
829            );
830
831            // Check style inheritance
832            if ($parent != '')
833            {
834                if (isset($installed_names[$parent]))
835                {
836                    // Parent style is installed
837                    $row = $installed_names[$parent];
838                    $style['style_parent_id'] = $row['id'];
839                    $style['style_parent_tree'] = $row['tree'];
840                }
841                else
842                {
843                    // Parent style is not installed yet
844                    $style['_available'] = false;
845                    $style['_note'] = $this->language->lang('REQUIRES_STYLE', htmlspecialchars($parent, ENT_COMPAT));
846                }
847            }
848
849            if ($all || $style['_available'])
850            {
851                $styles[] = $style;
852            }
853        }
854
855        return $styles;
856    }
857
858    /**
859    * Show styles list
860    *
861    * @param array $styles styles list
862    * @param int $parent parent style id
863    * @param int $level style inheritance level
864    */
865    protected function show_styles_list(&$styles, $parent, $level)
866    {
867        foreach ($styles as &$style)
868        {
869            if (empty($style['_shown']) && $style['style_parent_id'] == $parent)
870            {
871                $this->list_style($style, $level);
872                $this->show_styles_list($styles, $style['style_id'], $level + 1);
873            }
874        }
875    }
876
877    /**
878    * Show available styles tree
879    *
880    * @param array $styles Styles list, passed as reference
881    * @param string $name Name of parent style
882    * @param int $level Styles tree level
883    */
884    protected function show_available_child_styles(&$styles, $name, $level)
885    {
886        foreach ($styles as &$style)
887        {
888            if (empty($style['_shown']) && $style['_inherit_name'] == $name)
889            {
890                $this->list_style($style, $level);
891                $this->show_available_child_styles($styles, $style['style_name'], $level + 1);
892            }
893        }
894    }
895
896    /**
897    * Update styles tree
898    *
899    * @param array $styles Styles list, passed as reference
900    * @param array|false $style Current style, false if root
901    * @return bool True if something was updated, false if not
902    */
903    protected function update_styles_tree(&$styles, $style = false)
904    {
905        $parent_id = ($style === false) ? 0 : $style['style_id'];
906        $parent_tree = ($style === false) ? '' : ($style['style_parent_tree'] == '' ? '' : $style['style_parent_tree']) . $style['style_path'];
907        $update = false;
908        $updated = false;
909        foreach ($styles as &$row)
910        {
911            if ($row['style_parent_id'] == $parent_id)
912            {
913                if ($row['style_parent_tree'] != $parent_tree)
914                {
915                    $row['style_parent_tree'] = $parent_tree;
916                    $update = true;
917                }
918                $updated |= $this->update_styles_tree($styles, $row);
919            }
920        }
921        if ($update)
922        {
923            $sql = 'UPDATE ' . STYLES_TABLE . "
924                SET style_parent_tree = '" . $this->db->sql_escape($parent_tree) . "'
925                WHERE style_parent_id = {$parent_id}";
926            $this->db->sql_query($sql);
927            $updated = true;
928        }
929        return $updated;
930    }
931
932    /**
933    * Find all possible parent styles for style
934    *
935    * @param array $styles list of styles
936    * @param int $id id of style
937    * @param int $parent current parent style id
938    * @param int $level current tree level
939    * @return array Style ids, names and levels
940    */
941    protected function find_possible_parents($styles, $id = -1, $parent = 0, $level = 0)
942    {
943        $results = array();
944        foreach ($styles as $style)
945        {
946            if ($style['style_id'] != $id && $style['style_parent_id'] == $parent)
947            {
948                $results[] = array(
949                    'style_id'        => $style['style_id'],
950                    'style_name'    => $style['style_name'],
951                    'style_path'    => $style['style_path'],
952                    'style_parent_id'    => $style['style_parent_id'],
953                    'style_parent_tree'    => $style['style_parent_tree'],
954                    'level'            => $level
955                );
956                $results = array_merge($results, $this->find_possible_parents($styles, $id, $style['style_id'], $level + 1));
957            }
958        }
959        return $results;
960    }
961
962    /**
963    * Show item in styles list
964    *
965    * @param array $style style row
966    * @param int $level style inheritance level
967    */
968    protected function list_style(array &$style, int $level) : void
969    {
970        // Mark row as shown
971        if (!empty($style['_shown']))
972        {
973            return;
974        }
975
976        $style['_shown'] = true;
977
978        $style_cfg = $this->read_style_composer_file($style['style_path']);
979
980        // Generate template variables
981        $actions = [];
982        $row = [
983            // Style data
984            'STYLE_ID'                => $style['style_id'],
985            'STYLE_NAME'            => htmlspecialchars($style['style_name'], ENT_COMPAT),
986            'STYLE_VERSION'            => $style_cfg['version'] ?? '-',
987            'STYLE_PHPBB_VERSION'    => $style_cfg['extra']['phpbb-version'] ?? '',
988            'STYLE_PATH'            => htmlspecialchars($style['style_path'], ENT_COMPAT),
989            'STYLE_COPYRIGHT'        => strip_tags($style['style_copyright']),
990            'STYLE_ACTIVE'            => $style['style_active'],
991
992            // Additional data
993            'DEFAULT'            => ($style['style_id'] && $style['style_id'] == $this->default_style),
994            'USERS'                => $style['_users'] ?? '',
995            'LEVEL'                => $level,
996            'PADDING'            => (4 + 16 * $level),
997            'SHOW_COPYRIGHT'    => ($style['style_id']) ? false : true,
998            'STYLE_PATH_FULL'    => htmlspecialchars($this->styles_path_absolute . '/' . $style['style_path'], ENT_COMPAT) . '/',
999
1000            // Comment to show below style
1001            'COMMENT'        => $style['_note'] ?? '',
1002
1003            // The following variables should be used by hooks to add custom HTML code
1004            'EXTRA'            => '',
1005            'EXTRA_OPTIONS'    => ''
1006        ];
1007
1008        // Status specific data
1009        if ($style['style_id'])
1010        {
1011            // Style is installed
1012
1013            // Details
1014            $actions[] = [
1015                'U_ACTION'    => $this->u_action . '&amp;action=details&amp;id=' . $style['style_id'],
1016                'L_ACTION'    => $this->language->lang('DETAILS')
1017            ];
1018
1019            // Activate/Deactivate
1020            $action_name = ($style['style_active'] ? 'de' : '') . 'activate';
1021
1022            $actions[] = [
1023                'U_ACTION'    => $this->u_action . '&amp;action=' . $action_name . '&amp;hash=' . generate_link_hash($action_name) . '&amp;id=' . $style['style_id'],
1024                'L_ACTION'    => $this->language->lang('STYLE_' . ($style['style_active'] ? 'DE' : '') . 'ACTIVATE')
1025            ];
1026
1027            if ($style['style_name'] !== 'prosilver')
1028            {
1029                // Uninstall
1030                $actions[] = [
1031                    'U_ACTION'    => $this->u_action . '&amp;action=uninstall&amp;hash=' . generate_link_hash('uninstall') . '&amp;id=' . $style['style_id'],
1032                    'L_ACTION'    => $this->language->lang('STYLE_UNINSTALL')
1033                ];
1034            }
1035
1036            // Preview
1037            $actions[] = [
1038                'U_ACTION'    => append_sid($this->phpbb_root_path . 'index.' . $this->php_ext, 'style=' . $style['style_id']),
1039                'L_ACTION'    => $this->language->lang('PREVIEW')
1040            ];
1041        }
1042        else
1043        {
1044            // Style is not installed
1045            if (empty($style['_available']))
1046            {
1047                $actions[] = [
1048                    'HTML'        => $this->language->lang('CANNOT_BE_INSTALLED')
1049                ];
1050            }
1051            else
1052            {
1053                $actions[] = [
1054                    'U_ACTION'    => $this->u_action . '&amp;action=install&amp;hash=' . generate_link_hash('install') . '&amp;dir=' . urlencode($style['style_path']),
1055                    'L_ACTION'    => $this->language->lang('INSTALL_STYLE')
1056                ];
1057            }
1058        }
1059
1060        // Assign template variables
1061        $this->template->assign_block_vars('styles_list', $row);
1062        foreach ($actions as $action)
1063        {
1064            $this->template->assign_block_vars('styles_list.actions', $action);
1065        }
1066
1067        // Increase counters
1068        $counter = ($style['style_id']) ? ($style['style_active'] ? 'active' : 'inactive') : (empty($style['_available']) ? 'cannotinstall' : 'caninstall');
1069        if (!isset($this->style_counters))
1070        {
1071            $this->style_counters = [
1072                'total'        => 0,
1073                'active'    => 0,
1074                'inactive'    => 0,
1075                'caninstall'    => 0,
1076                'cannotinstall'    => 0
1077            ];
1078        }
1079        $this->style_counters[$counter]++;
1080        $this->style_counters['total']++;
1081    }
1082
1083    /**
1084     * List invalid style
1085     *
1086     * @param array $style Array with info about style to display as invalid
1087     */
1088    protected function list_invalid(&$style)
1089    {
1090        $style['_shown'] = true;
1091
1092        $row = [
1093            // Style data
1094            'STYLE_INVALID'    => true,
1095            'STYLE_NAME'    => $this->language->lang('INVALID_STYLE_MESSAGE', $style['style_path']),
1096        ];
1097
1098        $this->template->assign_block_vars('styles_list', $row);
1099
1100        $this->template->assign_block_vars('styles_list.actions', [
1101            'HTML'        => $this->language->lang('CANNOT_BE_INSTALLED')
1102        ]);
1103
1104        // Increase counters
1105        if (!isset($this->style_counters))
1106        {
1107            $this->style_counters = [
1108                'total'        => 0,
1109                'active'    => 0,
1110                'inactive'    => 0,
1111                'caninstall'    => 0,
1112                'cannotinstall'    => 0
1113            ];
1114        }
1115        $this->style_counters['cannotinstall']++;
1116        $this->style_counters['total']++;
1117    }
1118
1119    /**
1120    * Show welcome message
1121    *
1122    * @param string $title main title
1123    * @param string $description page description
1124    */
1125    protected function welcome_message($title, $description)
1126    {
1127        $this->template->assign_vars([
1128            'L_TITLE'    => $this->language->lang($title),
1129            'L_EXPLAIN'    => $this->language->is_set($description) ? $this->language->lang($description) : ''
1130        ]);
1131    }
1132
1133    /**
1134    * Find all directories that have styles
1135    *
1136    * @return array Directory names
1137    */
1138    protected function find_style_dirs()
1139    {
1140        $styles = array();
1141
1142        $dp = @opendir($this->styles_path);
1143        if ($dp)
1144        {
1145            while (($file = readdir($dp)) !== false)
1146            {
1147                $dir = $this->styles_path . $file;
1148                if ($file[0] == '.' || !is_dir($dir))
1149                {
1150                    continue;
1151                }
1152
1153                if (file_exists("{$dir}/composer.json"))
1154                {
1155                    $styles[] = $file;
1156                }
1157            }
1158            closedir($dp);
1159        }
1160
1161        return $styles;
1162    }
1163
1164    /**
1165    * Sort styles
1166    */
1167    public function sort_styles($style1, $style2)
1168    {
1169        if ($style1['style_active'] != $style2['style_active'])
1170        {
1171            return ($style1['style_active']) ? -1 : 1;
1172        }
1173        if (isset($style1['_available']) && $style1['_available'] != $style2['_available'])
1174        {
1175            return ($style1['_available']) ? -1 : 1;
1176        }
1177        return strcasecmp(isset($style1['style_name']) ? $style1['style_name'] : $style1['name'], isset($style2['style_name']) ? $style2['style_name'] : $style2['name']);
1178    }
1179
1180    /**
1181     * Read style composer.json file
1182     *
1183     * @param string $dir style directory
1184     *
1185     * @return array Style data
1186     * @throws \DomainException in case of error
1187     */
1188    protected function read_style_composer_file($dir)
1189    {
1190        // This should never happen, we give them a red warning because of its relevance.
1191        if (!file_exists($this->styles_path . $dir . '/composer.json'))
1192        {
1193            trigger_error($this->language->lang('NO_STYLE_CFG', $dir), E_USER_WARNING);
1194        }
1195
1196        $json = file_get_contents($this->styles_path . $dir . '/composer.json');
1197        $style_data = \phpbb\json\sanitizer::decode($json);
1198
1199        if (!is_array($style_data) || !isset($style_data['type']) || $style_data['type'] !== 'phpbb-style')
1200        {
1201            throw new \DomainException('NO_VALID_STYLE');
1202        }
1203
1204        if (!isset($style_data['extra']))
1205        {
1206            $style_data['extra'] = array();
1207        }
1208
1209        // Check data
1210        if (!isset($style_data['extra']['parent-style']) || !is_string($style_data['extra']['parent-style']) || $style_data['extra']['parent-style'] === $style_data['name'])
1211        {
1212            $style_data['extra']['parent-style'] = '';
1213        }
1214        if (!isset($style_data['extra']['template-bitfield']))
1215        {
1216            $style_data['extra']['template-bitfield'] = $this->default_bitfield();
1217        }
1218
1219        return $style_data;
1220    }
1221
1222    /**
1223    * Install style
1224    *
1225    * @param array $style style data
1226    * @return int Style id
1227    */
1228    protected function install_style($style)
1229    {
1230        global $user, $phpbb_log;
1231
1232        // Generate row
1233        $sql_ary = array();
1234        foreach ($style as $key => $value)
1235        {
1236            if ($key != 'style_id' && substr($key, 0, 1) != '_')
1237            {
1238                $sql_ary[$key] = $value;
1239            }
1240        }
1241
1242        // Add to database
1243        $this->db->sql_transaction('begin');
1244
1245        $sql = 'INSERT INTO ' . STYLES_TABLE . '
1246            ' . $this->db->sql_build_array('INSERT', $sql_ary);
1247        $this->db->sql_query($sql);
1248
1249        $id = $this->db->sql_nextid();
1250
1251        $this->db->sql_transaction('commit');
1252
1253        $phpbb_log->add('admin', $user->data['user_id'], $user->ip, 'LOG_STYLE_ADD', false, array($sql_ary['style_name']));
1254
1255        return $id;
1256    }
1257
1258    /**
1259    * Lists all styles
1260    *
1261    * @return array Rows with styles data
1262    */
1263    protected function get_styles()
1264    {
1265        $sql = 'SELECT *
1266            FROM ' . STYLES_TABLE;
1267        $result = $this->db->sql_query($sql);
1268
1269        $rows = $this->db->sql_fetchrowset($result);
1270        $this->db->sql_freeresult($result);
1271
1272        return $rows;
1273    }
1274
1275    /**
1276    * Count users for each style
1277    *
1278    * @return array Styles in following format: [style_id] = number of users
1279    */
1280    protected function get_users()
1281    {
1282        $sql = 'SELECT user_style, COUNT(user_style) AS style_count
1283            FROM ' . USERS_TABLE . '
1284            GROUP BY user_style';
1285        $result = $this->db->sql_query($sql);
1286
1287        $style_count = array();
1288        while ($row = $this->db->sql_fetchrow($result))
1289        {
1290            $style_count[$row['user_style']] = $row['style_count'];
1291        }
1292        $this->db->sql_freeresult($result);
1293
1294        return $style_count;
1295    }
1296
1297    /**
1298    * Uninstall style
1299    *
1300    * @param array $style Style data
1301    * @return bool|string True on success, error message on error
1302    */
1303    protected function uninstall_style($style)
1304    {
1305        $id = $style['style_id'];
1306        $path = $style['style_path'];
1307
1308        // Check if style has child styles
1309        $sql = 'SELECT style_id
1310            FROM ' . STYLES_TABLE . '
1311            WHERE style_parent_id = ' . (int) $id . " OR style_parent_tree = '" . $this->db->sql_escape($path) . "'";
1312        $result = $this->db->sql_query($sql);
1313
1314        $conflict = $this->db->sql_fetchrow($result);
1315        $this->db->sql_freeresult($result);
1316
1317        if ($conflict !== false)
1318        {
1319            return $this->language->lang('STYLE_UNINSTALL_DEPENDENT', $style['style_name']);
1320        }
1321
1322        // Change default style for users
1323        $sql = 'UPDATE ' . USERS_TABLE . '
1324            SET user_style = ' . (int) $this->default_style . '
1325            WHERE user_style = ' . $id;
1326        $this->db->sql_query($sql);
1327
1328        // Uninstall style
1329        $sql = 'DELETE FROM ' . STYLES_TABLE . '
1330            WHERE style_id = ' . $id;
1331        $this->db->sql_query($sql);
1332        return true;
1333    }
1334
1335    /**
1336    * Delete all files in style directory
1337    *
1338    * @param string $path Style directory
1339    * @param string $dir Directory to remove inside style's directory
1340    * @return bool True on success, false on error
1341    */
1342    protected function delete_style_files($path, $dir = '')
1343    {
1344        $dirname = $this->styles_path . $path . $dir;
1345        $result = true;
1346
1347        $dp = @opendir($dirname);
1348
1349        if ($dp)
1350        {
1351            while (($file = readdir($dp)) !== false)
1352            {
1353                if ($file == '.' || $file == '..')
1354                {
1355                    continue;
1356                }
1357                $filename = $dirname . '/' . $file;
1358                if (is_dir($filename))
1359                {
1360                    if (!$this->delete_style_files($path, $dir . '/' . $file))
1361                    {
1362                        $result = false;
1363                    }
1364                }
1365                else
1366                {
1367                    if (!@unlink($filename))
1368                    {
1369                        $result = false;
1370                    }
1371                }
1372            }
1373            closedir($dp);
1374        }
1375        if (!@rmdir($dirname))
1376        {
1377            return false;
1378        }
1379
1380        return $result;
1381    }
1382
1383    /**
1384    * Get list of items from posted data
1385    *
1386    * @param string $name Variable name
1387    * @param string|int $default Default value for array
1388    * @param bool $error If true, error will be triggered if list is empty
1389    * @return array Items
1390    */
1391    protected function request_vars($name, $default, $error = false)
1392    {
1393        $item = $this->request->variable($name, $default);
1394        $items = $this->request->variable($name . 's', array($default));
1395
1396        if (count($items) == 1 && $items[0] == $default)
1397        {
1398            $items = array();
1399        }
1400
1401        if ($item != $default && !count($items))
1402        {
1403            $items[] = $item;
1404        }
1405
1406        if ($error && !count($items))
1407        {
1408            trigger_error($this->language->lang('NO_MATCHING_STYLES_FOUND') . adm_back_link($this->u_action), E_USER_WARNING);
1409        }
1410
1411        return $items;
1412    }
1413
1414    /**
1415    * Generates default bitfield
1416    *
1417    * This bitfield decides which bbcodes are defined in a template.
1418    *
1419    * @return string Bitfield
1420    */
1421    protected function default_bitfield()
1422    {
1423        static $value;
1424        if (isset($value))
1425        {
1426            return $value;
1427        }
1428
1429        // Hardcoded template bitfield to add for new templates
1430        $default_bitfield = '1111111111111';
1431
1432        $bitfield = new bitfield();
1433        for ($i = 0; $i < strlen($default_bitfield); $i++)
1434        {
1435            if ($default_bitfield[$i] == '1')
1436            {
1437                $bitfield->set($i);
1438            }
1439        }
1440
1441        return $bitfield->get_base64();
1442    }
1443
1444}