Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
68.90% covered (warning)
68.90%
226 / 328
40.00% covered (danger)
40.00%
4 / 10
CRAP
0.00% covered (danger)
0.00%
0 / 1
phpbb_test_case_helpers
68.90% covered (warning)
68.90%
226 / 328
40.00% covered (danger)
40.00%
4 / 10
327.21
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 copy_ext_fixtures
0.00% covered (danger)
0.00%
0 / 11
0.00% covered (danger)
0.00%
0 / 1
12
 restore_original_ext_dir
0.00% covered (danger)
0.00%
0 / 15
0.00% covered (danger)
0.00%
0 / 1
42
 setExpectedTriggerError
100.00% covered (success)
100.00%
13 / 13
100.00% covered (success)
100.00%
1 / 1
2
 makedirs
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 get_test_config
33.85% covered (danger)
33.85%
22 / 65
0.00% covered (danger)
0.00%
0 / 1
238.05
 copy_dir
0.00% covered (danger)
0.00%
0 / 21
0.00% covered (danger)
0.00%
0 / 1
132
 empty_dir
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
42
 set_s9e_services
98.42% covered (success)
98.42%
187 / 190
0.00% covered (danger)
0.00%
0 / 1
31
 format_date
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
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
14use Symfony\Component\DependencyInjection\ContainerInterface;
15
16class phpbb_test_case_helpers
17{
18    protected $expectedTriggerError = false;
19
20    protected $test_case;
21
22    public function __construct($test_case)
23    {
24        $this->test_case = $test_case;
25    }
26
27    /**
28    * This should only be called once before the tests are run.
29    * This is used to copy the fixtures to the phpBB install
30    */
31    public function copy_ext_fixtures($fixtures_dir, $fixtures)
32    {
33        global $phpbb_root_path;
34
35        if (file_exists($phpbb_root_path . 'ext/'))
36        {
37            // First, move any extensions setup on the board to a temp directory
38            $this->copy_dir($phpbb_root_path . 'ext/', $phpbb_root_path . 'store/temp_ext/');
39
40            // Back up vendor-ext directory
41            $this->copy_dir($phpbb_root_path . 'vendor-ext/', $phpbb_root_path . 'store/temp_vendor-ext/');
42
43            // Back up composer-ext.* files
44            copy($phpbb_root_path . 'composer-ext.json', $phpbb_root_path . 'store/temp_composer-ext.json');
45            copy($phpbb_root_path . 'composer-ext.lock', $phpbb_root_path . 'store/temp_composer-ext.lock');
46
47            // Then empty the ext/ directory on the board (for accurate test cases)
48            $this->empty_dir($phpbb_root_path . 'ext/');
49
50            // Then empty the vendor-ext/ directory and add back .git-keep
51            $this->empty_dir($phpbb_root_path . 'vendor-ext/');
52            file_put_contents($phpbb_root_path . 'vendor-ext/.git-keep', '');
53        }
54
55        // Copy our ext/ files from the test case to the board
56        foreach ($fixtures as $fixture)
57        {
58            $this->copy_dir($fixtures_dir . $fixture, $phpbb_root_path . 'ext/' . $fixture);
59        }
60    }
61
62    /**
63    * This should only be called once after the tests are run.
64    * This is used to remove the fixtures from the phpBB install
65    */
66    public function restore_original_ext_dir()
67    {
68        global $phpbb_root_path;
69
70        // Remove all of the files we copied from test ext -> board ext
71        $this->empty_dir($phpbb_root_path . 'ext/');
72
73        // Copy back the board installed extensions from the temp directory
74        if (file_exists($phpbb_root_path . 'store/temp_ext/'))
75        {
76            $this->copy_dir($phpbb_root_path . 'store/temp_ext/', $phpbb_root_path . 'ext/');
77
78            // Remove all of the files we copied from board ext -> temp_ext
79            $this->empty_dir($phpbb_root_path . 'store/temp_ext/');
80        }
81
82        if (file_exists($phpbb_root_path . 'store/temp_vendor-ext/'))
83        {
84            $this->empty_dir($phpbb_root_path . 'vendor-ext/');
85
86            $this->copy_dir($phpbb_root_path . 'store/temp_vendor-ext/', $phpbb_root_path . 'vendor-ext/');
87
88            $this->empty_dir($phpbb_root_path . 'store/temp_vendor-ext/');
89        }
90
91        if (file_exists($phpbb_root_path . 'store/temp_composer-ext.json'))
92        {
93            copy($phpbb_root_path . 'store/temp_composer-ext.json', $phpbb_root_path . 'composer-ext.json');
94        }
95
96        if (file_exists($phpbb_root_path . 'store/temp_composer-ext.lock'))
97        {
98            copy($phpbb_root_path . 'store/temp_composer-ext.lock', $phpbb_root_path . 'composer-ext.lock');
99        }
100
101        if (file_exists($phpbb_root_path . 'store/temp_ext/'))
102        {
103            $this->empty_dir($phpbb_root_path . 'store/temp_ext/');
104        }
105    }
106
107    public function setExpectedTriggerError($errno, $message = ''): void
108    {
109        set_error_handler(
110            static function ($errno, $errstr)
111            {
112                restore_error_handler();
113                throw new Exception($errstr, $errno);
114            },
115            E_ALL ^ E_DEPRECATED
116        );
117
118        $this->expectedTriggerError = true;
119        $this->test_case->expectException(Exception::class);
120        $this->test_case->expectExceptionCode($errno);
121        if ($message)
122        {
123            $this->test_case->expectExceptionMessage((string) $message);
124        }
125    }
126
127    public function makedirs($path)
128    {
129        // PHP bug #55124 (fixed in 5.4.0)
130        $path = str_replace('/./', '/', $path);
131
132        mkdir($path, 0777, true);
133    }
134
135    static public function get_test_config()
136    {
137        $config = array();
138
139        if (extension_loaded('sqlite3'))
140        {
141            $config = array_merge($config, array(
142                'dbms'        => 'phpbb\db\driver\sqlite3',
143                'dbhost'    => __DIR__ . '/../phpbb_unit_tests.sqlite3', // filename
144                'dbport'    => '',
145                'dbname'    => '',
146                'dbuser'    => '',
147                'dbpasswd'    => '',
148            ));
149        }
150
151        if (isset($_SERVER['PHPBB_TEST_CONFIG']))
152        {
153            // Could be an absolute path
154            $test_config = $_SERVER['PHPBB_TEST_CONFIG'];
155        }
156        else
157        {
158            $test_config = __DIR__ . '/../test_config.php';
159        }
160
161        $config_php_file = new \phpbb\config_php_file('', '');
162
163        if (file_exists($test_config))
164        {
165            $config_php_file->set_config_file($test_config);
166            extract($config_php_file->get_all());
167
168            $config = array_merge($config, array(
169                'dbms'        => \phpbb\config_php_file::convert_30_dbms_to_31($dbms),
170                'dbhost'    => $dbhost,
171                'dbport'    => $dbport,
172                'dbname'    => $dbname,
173                'dbuser'    => $dbuser,
174                'dbpasswd'    => $dbpasswd,
175                'custom_dsn'    => isset($custom_dsn) ? $custom_dsn : '',
176            ));
177
178            if (isset($phpbb_functional_url))
179            {
180                $config['phpbb_functional_url'] = $phpbb_functional_url;
181            }
182
183            if (isset($path_to_ssl_cert))
184            {
185                $config['path_to_ssl_cert'] = $path_to_ssl_cert;
186            }
187
188            if (isset($phpbb_redis_host))
189            {
190                $config['redis_host'] = $phpbb_redis_host;
191            }
192            if (isset($phpbb_redis_port))
193            {
194                $config['redis_port'] = $phpbb_redis_port;
195            }
196
197            if (isset($fulltext_sphinx_id))
198            {
199                $config['fulltext_sphinx_id'] = $fulltext_sphinx_id;
200            }
201
202            if (isset($phpbb_memcached_host))
203            {
204                $config['memcached_host'] = $phpbb_memcached_host;
205            }
206
207            if (isset($phpbb_memcached_port))
208            {
209                $config['memcached_port'] = $phpbb_memcached_port;
210            }
211        }
212
213        if (isset($_SERVER['PHPBB_TEST_DBMS']))
214        {
215            $config = array_merge($config, array(
216                'dbms'        => isset($_SERVER['PHPBB_TEST_DBMS']) ? \phpbb\config_php_file::convert_30_dbms_to_31($_SERVER['PHPBB_TEST_DBMS']) : '',
217                'dbhost'    => isset($_SERVER['PHPBB_TEST_DBHOST']) ? $_SERVER['PHPBB_TEST_DBHOST'] : '',
218                'dbport'    => isset($_SERVER['PHPBB_TEST_DBPORT']) ? $_SERVER['PHPBB_TEST_DBPORT'] : '',
219                'dbname'    => isset($_SERVER['PHPBB_TEST_DBNAME']) ? $_SERVER['PHPBB_TEST_DBNAME'] : '',
220                'dbuser'    => isset($_SERVER['PHPBB_TEST_DBUSER']) ? $_SERVER['PHPBB_TEST_DBUSER'] : '',
221                'dbpasswd'    => isset($_SERVER['PHPBB_TEST_DBPASSWD']) ? $_SERVER['PHPBB_TEST_DBPASSWD'] : '',
222                'custom_dsn'    => isset($_SERVER['PHPBB_TEST_CUSTOM_DSN']) ? $_SERVER['PHPBB_TEST_CUSTOM_DSN'] : '',
223            ));
224        }
225
226        if (isset($_SERVER['PHPBB_FUNCTIONAL_URL']))
227        {
228            $config = array_merge($config, array(
229                'phpbb_functional_url'    => isset($_SERVER['PHPBB_FUNCTIONAL_URL']) ? $_SERVER['PHPBB_FUNCTIONAL_URL'] : '',
230            ));
231        }
232
233        if (isset($_SERVER['PHPBB_TEST_REDIS_HOST']))
234        {
235            $config['redis_host'] = $_SERVER['PHPBB_TEST_REDIS_HOST'];
236        }
237
238        if (isset($_SERVER['PHPBB_TEST_REDIS_PORT']))
239        {
240            $config['redis_port'] = $_SERVER['PHPBB_TEST_REDIS_PORT'];
241        }
242
243        if (isset($_SERVER['PHPBB_TEST_MEMCACHED_HOST']))
244        {
245            $config['memcached_host'] = $_SERVER['PHPBB_TEST_MEMCACHED_HOST'];
246        }
247
248        if (isset($_SERVER['PHPBB_TEST_MEMCACHED_PORT']))
249        {
250            $config['memcached_port'] = $_SERVER['PHPBB_TEST_MEMCACHED_PORT'];
251        }
252
253        if (isset($_SERVER['PHPBB_TEST_SSL_CERT_PATH']))
254        {
255            $config['path_to_ssl_cert'] = $_SERVER['PHPBB_TEST_SSL_CERT_PATH'];
256        }
257
258        return $config;
259    }
260
261    /**
262    * Recursive directory copying function
263    *
264    * @param string $source
265    * @param string $dest
266    * @return array list of files copied
267    */
268    public function copy_dir($source, $dest)
269    {
270        $source = (substr($source, -1) == '/') ? $source : $source . '/';
271        $dest = (substr($dest, -1) == '/') ? $dest : $dest . '/';
272
273        $copied_files = array();
274
275        if (!is_dir($dest))
276        {
277            $this->makedirs($dest);
278        }
279
280        $files = scandir($source);
281        foreach ($files as $file)
282        {
283            if ($file == '.' || $file == '..')
284            {
285                continue;
286            }
287
288            if (is_dir($source . $file))
289            {
290                $created_dir = false;
291                if (!is_dir($dest . $file))
292                {
293                    $created_dir = true;
294                    $this->makedirs($dest . $file);
295                }
296
297                $copied_files = array_merge($copied_files, self::copy_dir($source . $file, $dest . $file));
298
299                if ($created_dir)
300                {
301                    $copied_files[] = $dest . $file;
302                }
303            }
304            else
305            {
306                if (!file_exists($dest . $file))
307                {
308                    copy($source . $file, $dest . $file);
309
310                    $copied_files[] = $dest . $file;
311                }
312            }
313        }
314
315        return $copied_files;
316    }
317
318    /**
319    * Empty directory (remove any subdirectories/files below)
320    *
321    * @param array $file_list
322    */
323    public function empty_dir($path)
324    {
325        $path = (substr($path, -1) == '/') ? $path : $path . '/';
326
327        $files = scandir($path);
328        foreach ($files as $file)
329        {
330            if ($file == '.' || $file == '..')
331            {
332                continue;
333            }
334
335            if (is_dir($path . $file))
336            {
337                $this->empty_dir($path . $file);
338
339                rmdir($path . $file);
340            }
341            else
342            {
343                unlink($path . $file);
344            }
345        }
346    }
347
348    /**
349    * Set working instances of the text_formatter.* services
350    *
351    * If no container is passed, the global $phpbb_container will be used and/or
352    * created if applicable
353    *
354    * @param  ContainerInterface $container   Service container
355    * @param  string             $fixture     Path to the XML fixture
356    * @param  string             $styles_path Path to the styles dir
357    * @return ContainerInterface
358    */
359    public function set_s9e_services(ContainerInterface|null $container = null, $fixture = null, $styles_path = null)
360    {
361        static $first_run;
362        global $config, $phpbb_container, $phpbb_dispatcher, $phpbb_root_path, $phpEx, $request, $user;
363
364        $cache_dir = __DIR__ . '/../tmp/';
365
366        // Remove old cache files on first run
367        if (!isset($first_run))
368        {
369            $first_run = 1;
370
371            array_map('unlink', array_merge(
372                glob($cache_dir . 'data_s9e_*'),
373                glob($cache_dir . 's9e_*')
374            ));
375        }
376
377        if (!isset($container))
378        {
379            if (!isset($phpbb_container))
380            {
381                $phpbb_container = new phpbb_mock_container_builder;
382            }
383
384            $container = $phpbb_container;
385        }
386
387        if (!isset($fixture))
388        {
389            $fixture = __DIR__ . '/../text_formatter/s9e/fixtures/default_formatting.xml';
390        }
391
392        if (!isset($styles_path))
393        {
394            $styles_path = $phpbb_root_path . 'styles/';
395        }
396
397        $dataset = new DOMDocument;
398        $dataset->load($fixture);
399
400        $tables = array(
401            'phpbb_bbcodes' => array(),
402            'phpbb_smilies' => array(),
403            'phpbb_styles'  => array(),
404            'phpbb_words'   => array()
405        );
406        foreach ($dataset->getElementsByTagName('table') as $table)
407        {
408            $name = $table->getAttribute('name');
409            $columns = array();
410
411            foreach ($table->getElementsByTagName('column') as $column)
412            {
413                $columns[] = $column->textContent;
414            }
415
416            foreach ($table->getElementsByTagName('row') as $row)
417            {
418                $values = array();
419
420                foreach ($row->getElementsByTagName('value') as $value)
421                {
422                    $values[] = $value->textContent;
423                }
424
425                $tables[$name][] = array_combine($columns, $values);
426            }
427        }
428
429        // Set up a default style if there's none set
430        if (empty($tables['phpbb_styles']))
431        {
432            $tables['phpbb_styles'][] = array(
433                'style_id' => 1,
434                'style_path' => 'prosilver',
435                'bbcode_bitfield' => '//g='
436            );
437        }
438
439        // Mock the DAL, make it return data from the fixture
440        $db_driver = $this->test_case->getMockBuilder('phpbb\\db\\driver\\driver')
441            ->disableOriginalConstructor()
442            ->disableOriginalClone()
443            ->disableArgumentCloning()
444            ->disallowMockingUnknownTypes()
445            ->getMock();
446        $mb = $this->test_case->getMockBuilder('phpbb\\textformatter\\data_access');
447        $mb->onlyMethods(array('get_bbcodes', 'get_censored_words', 'get_smilies', 'get_styles'));
448        $mb->setConstructorArgs(array(
449            $db_driver,
450            'phpbb_bbcodes',
451            'phpbb_smilies',
452            'phpbb_styles',
453            'phpbb_words',
454            $styles_path
455        ));
456
457        $dal = $mb->getMock();
458        $container->set('text_formatter.data_access', $dal);
459
460        $dal->expects($this->test_case->any())
461            ->method('get_bbcodes')
462            ->will($this->test_case->returnValue($tables['phpbb_bbcodes']));
463        $dal->expects($this->test_case->any())
464            ->method('get_smilies')
465            ->will($this->test_case->returnValue($tables['phpbb_smilies']));
466        $dal->expects($this->test_case->any())
467            ->method('get_styles')
468            ->will($this->test_case->returnValue($tables['phpbb_styles']));
469        $dal->expects($this->test_case->any())
470            ->method('get_censored_words')
471            ->will($this->test_case->returnValue($tables['phpbb_words']));
472
473        // Cache the parser and renderer with a key based on this method's arguments
474        $cache = new \phpbb\cache\driver\file($cache_dir);
475
476        // Don't serialize unserializable resource/object arguments
477        // See https://www.php.net/manual/en/function.serialize.php#refsect1-function.serialize-notes
478        $args = func_get_args();
479        foreach ($args as $key => $arg)
480        {
481            if (is_resource($arg) || (is_object($arg) && (!is_a($arg, 'Serializable') && !method_exists($arg, '__serialize'))))
482            {
483                unset($args[$key]);
484            }
485        }
486        $prefix = '_s9e_' . md5(serialize($args));
487        $cache_key_parser = $prefix . '_parser';
488        $cache_key_renderer = $prefix . '_renderer';
489        $container->set('cache.driver', $cache);
490
491        if (!$container->isCompiled())
492        {
493            $container->setParameter('cache.dir', $cache_dir);
494        }
495
496        // Create a path_helper
497        if (!$container->has('path_helper') || $container->getDefinition('path_helper')->isSynthetic())
498        {
499            $path_helper = $this->test_case->getMockBuilder('phpbb\\path_helper')
500                ->disableOriginalConstructor()
501                ->onlyMethods(array('get_web_root_path'))
502                ->getMock();
503            $path_helper->expects($this->test_case->any())
504                ->method('get_web_root_path')
505                ->will($this->test_case->returnValue('phpBB/'));
506
507            $container->set('path_helper', $path_helper);
508        }
509        else
510        {
511            $path_helper = $container->get('path_helper');
512        }
513
514        // Create an event dispatcher
515        if ($container->has('event_dispatcher'))
516        {
517            $dispatcher = $container->get('event_dispatcher');
518        }
519        else if (isset($phpbb_dispatcher))
520        {
521            $dispatcher = $phpbb_dispatcher;
522        }
523        else
524        {
525            $dispatcher = new phpbb_mock_event_dispatcher;
526        }
527        if (!isset($phpbb_dispatcher))
528        {
529            $phpbb_dispatcher = $dispatcher;
530        }
531
532        // Set up the a minimum config
533        if ($container->has('config'))
534        {
535            $config = $container->get('config');
536        }
537        elseif (!isset($config))
538        {
539            $config = new \phpbb\config\config(array());
540        }
541        $default_config = array(
542            'allow_nocensors'       => false,
543            'allowed_schemes_links' => 'http,https,ftp',
544            'script_path'           => '/phpbb',
545            'server_name'           => 'localhost',
546            'server_port'           => 80,
547            'server_protocol'       => 'http://',
548            'smilies_path'          => 'images/smilies',
549        );
550        foreach ($default_config as $config_name => $config_value)
551        {
552            if (!isset($config[$config_name]))
553            {
554                $config[$config_name] = $config_value;
555            }
556        }
557
558        // Create a fake request
559        if (!isset($request))
560        {
561            $request = new phpbb_mock_request;
562        }
563
564        // Get a log interface
565        $log = ($container->has('log')) ? $container->get('log') : $this->test_case->getMockBuilder('phpbb\\log\\log_interface')->getMock();
566
567        // Create and register the text_formatter.s9e.factory service
568        $factory = new \phpbb\textformatter\s9e\factory($dal, $cache, $dispatcher, $config, new \phpbb\textformatter\s9e\link_helper, $log, $cache_dir, $cache_key_parser, $cache_key_renderer);
569        $container->set('text_formatter.s9e.factory', $factory);
570
571        // Create a user if none was provided, and add the common lang strings
572        if ($container->has('user'))
573        {
574            $user = $container->get('user');
575
576            // Set default required user data if not set
577            $user->data['is_bot'] = $user->data['is_bot'] ?? false;
578            $user->data['is_registered'] = $user->data['is_registered'] ?? false;
579            $user->data['style_id'] = $user->data['style_id'] ?? 1;
580            $user->data['user_id'] = $user->data['user_id'] ?? ANONYMOUS;
581            $user->data['user_options'] = $user->data['user_options'] ?? 230271;
582            $user->style['style_id'] = $user->style['style_id'] ?? 1;
583        }
584        else
585        {
586            $lang_loader = new \phpbb\language\language_file_loader($phpbb_root_path, $phpEx);
587            $lang = new \phpbb\language\language($lang_loader);
588
589            $user = $this->test_case->getMockBuilder('\phpbb\user')
590                    ->setConstructorArgs(array($lang, '\phpbb\datetime'))
591                    ->onlyMethods(array('format_date'))
592                    ->getMock();
593            $user->expects($this->test_case->any())
594                 ->method('format_date')
595                 ->will($this->test_case->returnCallback(__CLASS__ . '::format_date'));
596
597            // Set default required user data
598            $user->data['is_bot'] = false;
599            $user->data['is_registered'] = false;
600            $user->data['style_id'] = 1;
601            $user->data['user_id'] = ANONYMOUS;
602            $user->data['user_options'] = 230271;
603            $user->style['style_id'] = 1;
604
605            $user->date_format = 'Y-m-d H:i:s';
606            $user->optionset('viewcensors', true);
607            $user->optionset('viewimg', true);
608            $user->optionset('viewsmilies', true);
609            $user->timezone = new \DateTimeZone('UTC');
610            $container->set('user', $user);
611        }
612        $user->add_lang('common');
613
614        // Get an auth interface
615        $auth = ($container->has('auth')) ? $container->get('auth') : new \phpbb\auth\auth;
616
617        // Create and register a quote_helper
618        $quote_helper = new \phpbb\textformatter\s9e\quote_helper(
619            $container->get('user'),
620            $phpbb_root_path,
621            $phpEx
622        );
623        $container->set('text_formatter.s9e.quote_helper', $quote_helper);
624
625        // Create and register a mention_helper
626        $mention_helper = new \phpbb\textformatter\s9e\mention_helper(
627            ($container->has('dbal.conn')) ? $container->get('dbal.conn') : $db_driver,
628            $auth,
629            $container->get('user'),
630            $phpbb_root_path,
631            $phpEx
632        );
633        $container->set('text_formatter.s9e.mention_helper', $mention_helper);
634
635        // Create and register the text_formatter.s9e.parser service and its alias
636        $parser = new \phpbb\textformatter\s9e\parser(
637            $cache,
638            $cache_key_parser,
639            $factory,
640            $dispatcher
641        );
642        $container->set('text_formatter.parser', $parser);
643        $container->set('text_formatter.s9e.parser', $parser);
644
645        // Create and register the text_formatter.s9e.renderer service and its alias
646        $renderer = new \phpbb\textformatter\s9e\renderer(
647            $cache,
648            $cache_dir,
649            $cache_key_renderer,
650            $factory,
651            $dispatcher
652        );
653
654        // Calls configured in services.yml
655        $renderer->configure_quote_helper($quote_helper);
656        $renderer->configure_mention_helper($mention_helper);
657        $renderer->configure_smilies_path($config, $path_helper);
658        $renderer->configure_user($user, $config, $auth);
659
660        $container->set('text_formatter.renderer', $renderer);
661        $container->set('text_formatter.s9e.renderer', $renderer);
662
663        // Create and register the text_formatter.s9e.utils service and its alias
664        $utils = new \phpbb\textformatter\s9e\utils;
665        $container->set('text_formatter.utils', $utils);
666        $container->set('text_formatter.s9e.utils', $utils);
667
668        return $container;
669    }
670
671    /**
672    * Mocked replacement for \phpbb\user::format_date()
673    *
674    * @param  integer $gmepoch unix timestamp
675    * @return string
676    */
677    static public function format_date($gmepoch)
678    {
679        return gmdate('Y-m-d H:i:s', $gmepoch);
680    }
681}