diff --git a/.gitignore b/.gitignore index 2b2d8d0ac6..ac29bb403c 100644 --- a/.gitignore +++ b/.gitignore @@ -4,7 +4,7 @@ !/phpBB/cache/.htaccess !/phpBB/cache/index.html /phpBB/composer.phar -/phpBB/config*.php +/phpBB/config*.php* /phpBB/ext/* /phpBB/files/* /phpBB/images/avatars/gallery/* @@ -16,9 +16,11 @@ /phpBB/store/* /phpBB/styles/* !/phpBB/styles/prosilver -!/phpBB/styles/subsilver2 +!/phpBB/styles/all /phpBB/vendor /tests/phpbb_unit_tests.sqlite* /tests/test_config*.php /tests/tmp/* /tests/vendor +/vagrant/phpbb-install-config.yml +.vagrant diff --git a/.travis.yml b/.travis.yml index c70f70ee2d..fa2a86c0db 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,12 +4,10 @@ dist: precise matrix: include: - - php: 5.3.3 - env: DB=mysqli - - php: 5.3 - env: DB=mysqli # MyISAM - php: 5.4 - env: DB=mysqli + env: DB=none;NOTESTS=1 + - php: 5.4 + env: DB=mysqli # MyISAM - php: 5.4 env: DB=mysql - php: 5.4 @@ -24,28 +22,35 @@ matrix: env: DB=mysqli - php: 5.6 env: DB=mysqli + - php: 7.0 + env: DB=mysqli + - php: 7.1 + env: DB=mysqli + - php: nightly + env: DB=mysqli - php: hhvm env: DB=mysqli allow_failures: - php: hhvm + - php: nightly fast_finish: true services: - redis-server install: - - travis/setup-phpbb.sh $DB $TRAVIS_PHP_VERSION + - travis/setup-phpbb.sh $DB $TRAVIS_PHP_VERSION $NOTESTS before_script: - - travis/setup-database.sh $DB $TRAVIS_PHP_VERSION + - travis/setup-database.sh $DB $TRAVIS_PHP_VERSION $NOTESTS - phantomjs --webdriver=8910 > /dev/null & script: - - travis/phing-sniff.sh $DB $TRAVIS_PHP_VERSION - - travis/check-sami-parse-errors.sh $DB $TRAVIS_PHP_VERSION - - travis/check-image-icc-profiles.sh $DB $TRAVIS_PHP_VERSION - - travis/check-executable-files.sh $DB $TRAVIS_PHP_VERSION ./ + - travis/phing-sniff.sh $DB $TRAVIS_PHP_VERSION $NOTESTS + - travis/check-sami-parse-errors.sh $DB $TRAVIS_PHP_VERSION $NOTESTS + - travis/check-image-icc-profiles.sh $DB $TRAVIS_PHP_VERSION $NOTESTS + - travis/check-executable-files.sh $DB $TRAVIS_PHP_VERSION $NOTESTS ./ - sh -c "if [ '$SLOWTESTS' != '1' -a '$DB' = 'mysqli' ]; then phpBB/vendor/bin/phpunit tests/lint_test.php; fi" - - sh -c "if [ '$SLOWTESTS' != '1' ]; then phpBB/vendor/bin/phpunit --configuration travis/phpunit-$DB-travis.xml; fi" + - sh -c "if [ '$NOTESTS' != '1' -a '$SLOWTESTS' != '1' ]; then phpBB/vendor/bin/phpunit --configuration travis/phpunit-$DB-travis.xml --verbose --stop-on-error; fi" - sh -c "if [ '$SLOWTESTS' = '1' ]; then phpBB/vendor/bin/phpunit --configuration travis/phpunit-$DB-travis.xml --group slow; fi" - - sh -c "if [ '$TRAVIS_PHP_VERSION' = '5.3.3' -a '$DB' = 'mysqli' -a '$TRAVIS_PULL_REQUEST' != 'false' ]; then git-tools/commit-msg-hook-range.sh origin/$TRAVIS_BRANCH..FETCH_HEAD; fi" + - sh -c "set -x;if [ '$NOTESTS' = '1' -a '$TRAVIS_PULL_REQUEST' != 'false' ]; then git-tools/commit-msg-hook-range.sh origin/$TRAVIS_BRANCH..FETCH_HEAD; fi" diff --git a/README.md b/README.md index f465d7496e..63ccfedf1b 100644 --- a/README.md +++ b/README.md @@ -23,6 +23,10 @@ To be able to run an installation from the repo (and not from a pre-built packag 3. Read our [Coding guidelines](https://wiki.phpbb.com/Coding_guidelines) and [Git Contribution Guidelines](http://wiki.phpbb.com/Git) 4. Send us a pull request +## VAGRANT + +Read our [Vagrant documentation](phpBB/docs/vagrant.md) to find out how to use Vagrant to develop and contribute to phpBB. + ## AUTOMATED TESTING We have unit and functional tests in order to prevent regressions. You can view the bamboo continuous integration [here](http://bamboo.phpbb.com) or check our travis builds below: diff --git a/Vagrantfile b/Vagrantfile new file mode 100644 index 0000000000..ab225c9ad9 --- /dev/null +++ b/Vagrantfile @@ -0,0 +1,25 @@ +require 'json' +require 'yaml' + +VAGRANTFILE_API_VERSION ||= "2" +confDir = $confDir ||= File.expand_path("phpBB/vendor/laravel/homestead", File.dirname(__FILE__)) + +homesteadYamlPath = "vagrant/bootstrap.yaml" +afterScriptPath = "vagrant/after.sh" +aliasesPath = "vagrant/aliases" + +require File.expand_path(confDir + '/scripts/homestead.rb') + +Vagrant.configure(VAGRANTFILE_API_VERSION) do |config| + if File.exists? aliasesPath then + config.vm.provision "file", source: aliasesPath, destination: "~/.bash_aliases" + end + + if File.exists? homesteadYamlPath then + Homestead.configure(config, YAML::load(File.read(homesteadYamlPath))) + end + + if File.exists? afterScriptPath then + config.vm.provision "shell", path: afterScriptPath + end +end diff --git a/build/build.xml b/build/build.xml index 462983b83d..42f242b59d 100644 --- a/build/build.xml +++ b/build/build.xml @@ -2,9 +2,9 @@ - - - + + + @@ -142,6 +142,7 @@ + @@ -183,9 +184,6 @@ - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -310,6 +358,50 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -318,69 +410,121 @@ + + + + + + + + + + + + - - - - - + + + + + + - - - - - + - - - - - + + + + + + - - - - - + + + + + + - - - - - + + + + + + - - - - - + + + + + + - - - - - + + + + + + - - - - - + + + + + + - - - - - + + + + + + - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -391,6 +535,15 @@ + + + + + + + + + diff --git a/build/code_sniffer/phpbb/Sniffs/Namespaces/UnusedUseSniff.php b/build/code_sniffer/phpbb/Sniffs/Namespaces/UnusedUseSniff.php index 3125e4f05f..b3cdbf7496 100644 --- a/build/code_sniffer/phpbb/Sniffs/Namespaces/UnusedUseSniff.php +++ b/build/code_sniffer/phpbb/Sniffs/Namespaces/UnusedUseSniff.php @@ -87,6 +87,11 @@ class phpbb_Sniffs_Namespaces_UnusedUseSniff implements PHP_CodeSniffer_Sniff $old_simple_statement = $simple_statement; $simple_class_name_start = $phpcsFile->findNext(array(T_NS_SEPARATOR, T_STRING), ($simple_statement + 1)); + + if ($simple_class_name_start === false) { + continue; + } + $simple_class_name_end = $phpcsFile->findNext($find, ($simple_statement + 1), null, true); $simple_class_name = trim($phpcsFile->getTokensAsString($simple_class_name_start, ($simple_class_name_end - $simple_class_name_start))); diff --git a/build/package.php b/build/package.php index d168957ca5..178a27faad 100755 --- a/build/package.php +++ b/build/package.php @@ -196,9 +196,10 @@ if (sizeof($package->old_packages)) */ $copy_relative_directories = array( 'config/' => array( + 'recursive' => true, 'copied' => false, 'copy' => array( - 'config/*.yml' => 'config', + 'config/*' => 'config', ), ), ); @@ -256,7 +257,15 @@ if (sizeof($package->old_packages)) } $source_dir_files = $package->locations['old_versions'] . $package->get('simple_name') . '/' . $source_dir_files; $destination_dir = $dest_filename_dir . '/install/update/new/' . $destination_dir; - $package->run_command('cp ' . $source_dir_files . ' ' . $destination_dir); + + if (isset($data['recursive']) && $data['recursive']) + { + $package->run_command('cp -Rp ' . $source_dir_files . ' ' . $destination_dir); + } + else + { + $package->run_command('cp ' . $source_dir_files . ' ' . $destination_dir); + } } $copy_relative_directories[$reference]['copied'] = true; } @@ -272,7 +281,7 @@ if (sizeof($package->old_packages)) 'adm/style/admin.css' => 'adm/style', 'adm/style/admin.js' => 'adm/style', 'adm/style/ajax.js' => 'adm/style', - 'adm/style/install_*' => 'adm/style', + 'adm/style/installer_*' => 'adm/style', 'assets/javascript/*' => 'assets/javascript', ); @@ -470,22 +479,14 @@ chdir($package->get('dest_dir') . '/install'); // $package->run_command('rm -v database_update.php'); $package->run_command('rm -v install_update.php'); -chdir($package->get('dest_dir')); -$package->run_command('mv -v styles/subsilver2 ../subsilver2'); -$package->run_command('cp -p docs/COPYING ../subsilver2/license.txt'); - chdir($package->locations['package_dir']); foreach ($compress_programs as $extension => $compress_command) { $package->begin_status('Packaging phpBB for ' . $extension); $package->run_command('rm -v ./release_files/' . $package->get('release_filename') . ".{$extension}"); - $package->run_command('rm -v ./release_files/subsilver2_' . $package->get('new_version_number') . ".{$extension}"); // Build Package $package->run_command("$compress_command ./release_files/" . $package->get('release_filename') . '.' . $extension . ' ' . $package->get('package_name')); - - // Build subSilver2 Package - $package->run_command("$compress_command ./release_files/subsilver2_" . $package->get('new_version_number') . '.' . $extension . ' subsilver2'); } // Microsoft Web PI packaging diff --git a/phpBB/adm/index.php b/phpBB/adm/index.php index 519f6c8310..d27f56f28b 100644 --- a/phpBB/adm/index.php +++ b/phpBB/adm/index.php @@ -41,6 +41,7 @@ if (!isset($user->data['session_admin']) || !$user->data['session_admin']) // check specific permissions but this is a catchall if (!$auth->acl_get('a_')) { + send_status_line(403, 'Forbidden'); trigger_error('NO_ADMIN'); } @@ -50,8 +51,8 @@ define('IN_ADMIN', true); // Some oft used variables $safe_mode = (@ini_get('safe_mode') == '1' || strtolower(@ini_get('safe_mode')) === 'on') ? true : false; $file_uploads = (@ini_get('file_uploads') == '1' || strtolower(@ini_get('file_uploads')) === 'on') ? true : false; -$module_id = request_var('i', ''); -$mode = request_var('mode', ''); +$module_id = $request->variable('i', ''); +$mode = $request->variable('mode', ''); // Set custom style for admin area $template->set_custom_style(array( diff --git a/phpBB/adm/style/acp_board.html b/phpBB/adm/style/acp_board.html index 64592a5de2..fe3e250099 100644 --- a/phpBB/adm/style/acp_board.html +++ b/phpBB/adm/style/acp_board.html @@ -18,10 +18,11 @@ - + +
- {options.LEGEND} + {options.LEGEND}
diff --git a/phpBB/adm/style/acp_ext_details.html b/phpBB/adm/style/acp_ext_details.html index bd9eca623a..bbddf2e6b6 100644 --- a/phpBB/adm/style/acp_ext_details.html +++ b/phpBB/adm/style/acp_ext_details.html @@ -7,19 +7,17 @@

{L_EXTENSIONS_ADMIN}

-
-

{UP_TO_DATE_MSG} - {L_VERSIONCHECK_FORCE_UPDATE}

-
- -
-

{L_VERSIONCHECK_FAIL}

-

{VERSIONCHECK_FAIL_REASON}

-

{L_VERSIONCHECK_FORCE_UPDATE}

-
- -
-

{VERSIONCHECK_FAIL_REASON}

-
+ +
+

{L_VERSIONCHECK_FAIL}

+

{VERSIONCHECK_FAIL_REASON}

+

{L_VERSIONCHECK_FORCE_UPDATE}

+
+ +
+

{UP_TO_DATE_MSG} - {L_VERSIONCHECK_FORCE_UPDATE}

+
+ diff --git a/phpBB/adm/style/acp_groups.html b/phpBB/adm/style/acp_groups.html index 1412744cc9..26d7030531 100644 --- a/phpBB/adm/style/acp_groups.html +++ b/phpBB/adm/style/acp_groups.html @@ -267,11 +267,12 @@ - + + @@ -281,7 +282,7 @@ - + @@ -302,11 +303,12 @@

{L_SPECIAL_GROUPS_EXPLAIN}

{L_GROUP} {L_TOTAL_MEMBERS}{L_PENDING_MEMBERS} {L_OPTIONS} {L_ACTION}
{L_NO_GROUPS_CREATED}{L_NO_GROUPS_CREATED}
- + + @@ -316,6 +318,7 @@ + diff --git a/phpBB/adm/style/acp_help_phpbb.html b/phpBB/adm/style/acp_help_phpbb.html new file mode 100644 index 0000000000..478ecc162a --- /dev/null +++ b/phpBB/adm/style/acp_help_phpbb.html @@ -0,0 +1,61 @@ + + + + +

{L_ACP_HELP_PHPBB}

+ + +
+ +
+

{L_SEND_STATISTICS}

+

{L_EXPLAIN_SEND_STATISTICS}

+
+ +
+
+ +
+ {providers.NAME} + +
+
{providers.values.KEY}
+
{providers.values.VALUE}
+
+ +
+ +
+
+
+
+
+ checked="checked" /> + +
+
{L_SEND_STATISTICS_LONG}
+
+
+ +
+

+ + + +

+ {S_FORM_TOKEN} +
+
+ + +
+

+ + +

+
+ + + diff --git a/phpBB/adm/style/acp_icons.html b/phpBB/adm/style/acp_icons.html index e0d2840bb5..5493cbde0a 100644 --- a/phpBB/adm/style/acp_icons.html +++ b/phpBB/adm/style/acp_icons.html @@ -89,6 +89,9 @@ + + + @@ -102,7 +105,7 @@ - + @@ -110,6 +113,9 @@ + + + - - @@ -150,22 +148,25 @@ - + + + + + + + + + + + - - - - - - - diff --git a/phpBB/adm/style/acp_permissions.html b/phpBB/adm/style/acp_permissions.html index a4d33ed78b..7766052c59 100644 --- a/phpBB/adm/style/acp_permissions.html +++ b/phpBB/adm/style/acp_permissions.html @@ -329,14 +329,9 @@

- - + - + {S_HIDDEN_FIELDS} diff --git a/phpBB/adm/style/acp_send_statistics.html b/phpBB/adm/style/acp_send_statistics.html deleted file mode 100644 index 480e438e1f..0000000000 --- a/phpBB/adm/style/acp_send_statistics.html +++ /dev/null @@ -1,64 +0,0 @@ - - - - -

{L_SEND_STATISTICS}

- -

{L_EXPLAIN_SEND_STATISTICS}

- - - - - - - -

{L_DONT_SEND_STATISTICS}

- -

{L_EXPLAIN_SHOW_STATISTICS}

- -

- -
- -

- -

- - -
- {providers.NAME} - -
-
{providers.values.KEY}
-
{providers.values.VALUE}
-
- -
- -
-

- - -

- - -
-

{L_THANKS_SEND_STATISTICS}

« {L_GO_ACP_MAIN}

-
- - diff --git a/phpBB/adm/style/acp_users.html b/phpBB/adm/style/acp_users.html index 18c3d84f96..50b6ec9bc9 100644 --- a/phpBB/adm/style/acp_users.html +++ b/phpBB/adm/style/acp_users.html @@ -231,6 +231,10 @@ + + + + diff --git a/phpBB/adm/style/admin.css b/phpBB/adm/style/admin.css index 0c00e5339e..2322b3da88 100644 --- a/phpBB/adm/style/admin.css +++ b/phpBB/adm/style/admin.css @@ -1,4 +1,4 @@ -/* phpBB 3.1 Admin Style Sheet +/* phpBB 3.2 Admin Style Sheet ------------------------------------------------------------------------ Original author: subBlue ( http://www.subblue.com/ ) Copyright (c) phpBB Limited @@ -1782,7 +1782,7 @@ li.pagination ul { /* Action Highlighting ---------------------------------------- */ -.successbox, .errorbox { +.successbox, .errorbox, .warningbox { padding: 8px; margin: 10px 0; color: #FFFFFF; @@ -1806,6 +1806,10 @@ li.pagination ul { background-color: #BC2A4D; } +.warningbox { + background-color: #fca600; +} + .successbox h3, .errorbox h3 { color: #FFFFFF; margin: 0 0 0.5em; @@ -1832,10 +1836,33 @@ li.pagination ul { font-weight: bold; } +#log-container { + display: none; + max-height: 300px; + padding: 8px; + margin: 10px 0; + clear: both; + overflow-y: auto; + background-color: #FFFFFF; +} + +#log-container.show_log_container { + display: block; + border: 1px solid #DBD7D1; +} + +.log { + font-size: 0.8em; +} + .notice { background-color: #62A5CC; } +.download-box { + margin: 10px 0 10px 0; +} + /* Special cases for the error page */ #errorpage #page-header a { font-weight: bold; @@ -1864,6 +1891,7 @@ li.pagination ul { color: #000; text-align: center; border: 1px solid #AAA; + opacity: .95; } .tooltip span.top { @@ -2441,6 +2469,39 @@ fieldset.permissions .padding { display: none !important; } +.roles-options > .dropdown { + left: auto; + top: 3.2em; + width: 250px; +} + +.roles-options { + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + -o-user-select: none; + user-select: none; + width: 250px; +} + +.roles-options > span { + border: 1px solid #DEDEDE; + border-radius: 3px; + padding: 4px; + width: 250px; + display: none; + background: url('../images/arrow_down.gif') no-repeat 245px .7em; +} + +.roles-options li { + list-style: none; +} + +.roles-highlight { + background-color: #1e90ff; + color: #fff; +} + /* Classes for additional tasks ---------------------------------------- */ @@ -2489,3 +2550,153 @@ fieldset.permissions .padding { clear: both; display: block; } + +#progress-bar { + position: relative; + width: 90%; + text-align: center; + height: 25px; + margin: 20px auto; + border: 1px solid #cecece; +} + +#progress-bar #progress-bar-text { + position: absolute; + top: 0; + width: 100%; + color: #000; +} + +#progress-bar #progress-bar-filler { + display: block; + position: relative; + top: 0; + left: 0; + background-color: #3c84ad; + width: 0; + height: 25px; + overflow: hidden; + color: #fff; +} + +#progress-bar p { + line-height: 25px; + font-weight: bold; +} + +.send-stats-row { + margin: 15px 0; +} + +.send-stats-row:before { + display: table; + content: " "; +} + +.send-stats-tile { + position: relative; + padding: 14px; + margin-bottom: 20px; + background-color: #eff0f2; + border-radius: 6px; + box-shadow: rgba(0,0,0,0.3) 1px 1px 5px; +} + +.send-stats-tile h2 { + margin-top: 0; + text-align: center; + padding-bottom: 1em; +} + +.send-stats-tile i { + padding-right: 0.3em; +} + +.icon { + font-family: FontAwesome; + font-style: normal; +} + +.send-stats-data-row { + background: #f9f9f9; + border-radius: 6px; + border: #DEDEDE 1px solid; + padding: 10px; + border-top-width: 0; + border-top-right-radius: 0; + border-top-left-radius: 0; +} + +.send-stats-data-hidden .configlist { + display: none; +} + +.send-stats-data-only-row { + border-radius: 6px !important; + border-bottom-width: 1px !important; +} + +.send-stats-data-hidden { + padding: 0; + border: none; +} + +.send-stats-row > .send-stats-data-row:first-child { + background-color: #d9edf7; + border-bottom-width: 0; + border-top-right-radius: 6px; + border-top-left-radius: 6px; + border-top-width: 1px; + border-bottom-left-radius: 0; + border-bottom-right-radius: 0; +} + +.send-stats-settings dt, .send-stats-settings dd { + min-width: 25px; +} + +.send-stats-settings dd { + line-height: 1.5em; +} + +.send-stats-settings input { + display: none; +} + +.send-stats-settings input[type=checkbox] + label:before { + content: "\f096"; + font-family: FontAwesome; + font-size: 1.5em; +} + +.send-stats-settings input[type=checkbox]:checked + label:before { + content: "\f14a"; + color: #3c763d; +} + +.send-stats-data-row a:hover span { + text-decoration: underline; +} + +.send-stats-data-row a { + text-decoration: none; + cursor: default; +} + +.send-stats-data-row i { + padding-left: 6px; +} + +.configlist { + word-wrap: break-word; + word-break: break-all; +} + +/* stylelint-disable declaration-property-unit-whitelist */ +.emoji { + min-height: 18px; + min-width: 18px; + height: 1em; + width: 1em; +} +/* stylelint-enable declaration-property-unit-whitelist */ diff --git a/phpBB/adm/style/admin.js b/phpBB/adm/style/admin.js index 253fd46a62..551c78a4a3 100644 --- a/phpBB/adm/style/admin.js +++ b/phpBB/adm/style/admin.js @@ -243,8 +243,16 @@ function parse_document(container) parse_document($('body')); - // Hide configlist and success message in send statistics page - phpbb.toggleDisplay('configlist', -1); - phpbb.toggleDisplay('questionnaire-thanks', -1); + $('#questionnaire-form').css('display', 'none'); + var $triggerConfiglist = $('#trigger-configlist'); + + $triggerConfiglist.on('click', function () { + var $configlist = $('#configlist'); + $configlist.closest('.send-stats-data-row').toggleClass('send-stats-data-hidden'); + $configlist.closest('.send-stats-row').find('.send-stats-data-row:first-child').toggleClass('send-stats-data-only-row'); + $(this).find('i').toggleClass('fa-angle-down fa-angle-up'); + }); + + $('#configlist').closest('.send-stats-data-row').addClass('send-stats-data-hidden'); }); })(jQuery); diff --git a/phpBB/adm/style/ajax.js b/phpBB/adm/style/ajax.js index 77fd28fbe6..895bb056e5 100644 --- a/phpBB/adm/style/ajax.js +++ b/phpBB/adm/style/ajax.js @@ -4,6 +4,101 @@ 'use strict'; + +phpbb.prepareSendStats = function () { + var $form = $('#acp_help_phpbb'); + var $dark = $('#darkenwrapper'); + var $loadingIndicator; + + $form.on('submit', function (event) { + var $this = $(this), + currentTime = Math.floor(new Date().getTime() / 1000), + statsTime = parseInt($this.find('input[name=help_send_statistics_time]').val(), 10); + + event.preventDefault(); + $this.unbind('submit'); + + // Skip ajax request if form is submitted too early or send stats + // checkbox is not checked + if (!$this.find('input[name=help_send_statistics]').is(':checked') || + statsTime > currentTime) { + $form.find('input[type=submit]').click(); + setTimeout(function () { + $form.find('input[type=submit]').click(); + }, 300); + return; + } + + /** + * Handler for AJAX errors + */ + function errorHandler(jqXHR, textStatus, errorThrown) { + if (typeof console !== 'undefined' && console.log) { + console.log('AJAX error. status: ' + textStatus + ', message: ' + errorThrown); + } + phpbb.clearLoadingTimeout(); + var errorText = ''; + + if (typeof errorThrown === 'string' && errorThrown.length > 0) { + errorText = errorThrown; + } else { + errorText = $dark.attr('data-ajax-error-text-' + textStatus); + if (typeof errorText !== 'string' || !errorText.length) { + errorText = $dark.attr('data-ajax-error-text'); + } + } + phpbb.alert($dark.attr('data-ajax-error-title'), errorText); + } + + /** + * This is a private function used to handle the callbacks, refreshes + * and alert. It calls the callback, refreshes the page if necessary, and + * displays an alert to the user and removes it after an amount of time. + * + * It cannot be called from outside this function, and is purely here to + * avoid repetition of code. + * + * @param {object} res The object sent back by the server. + */ + function returnHandler(res) { + phpbb.clearLoadingTimeout(); + + // If a confirmation is not required, display an alert and call the + // callbacks. + $dark.fadeOut(phpbb.alertTime); + + if ($loadingIndicator) { + $loadingIndicator.fadeOut(phpbb.alertTime); + } + + var $sendStatisticsSuccess = $('', { + type: 'hidden', + name: 'send_statistics_response', + value: res + }); + $sendStatisticsSuccess.appendTo('p.submit-buttons'); + + // Finish actual form submission + $form.find('input[type=submit]').click(); + } + + $loadingIndicator = phpbb.loadingIndicator(); + + $.ajax({ + url: $this.attr('data-ajax-action').replace('&', '&'), + type: 'POST', + data: 'systemdata=' + encodeURIComponent($this.find('input[name=systemdata]').val()), + success: returnHandler, + error: errorHandler, + cache: false + }).always(function() { + if ($loadingIndicator && $loadingIndicator.is(':visible')) { + $loadingIndicator.fadeOut(phpbb.alertTime); + } + }); + }); +}; + /** * The following callbacks are for reording items. row_down * is triggered when an item is moved down, and row_up is triggered when @@ -70,6 +165,7 @@ function submitPermissions() { var $form = $('form#set-permissions'), fieldsetList = $form.find('fieldset[id^=perm]'), formDataSets = [], + dataSetIndex = 0, $submitAllButton = $form.find('input[type=submit][name^=action]')[0], $submitButton = $form.find('input[type=submit][data-clicked=true]')[0]; @@ -84,10 +180,21 @@ function submitPermissions() { } $.each(fieldsetList, function (key, value) { + dataSetIndex = Math.floor(key / 5); + var $fieldset = $('fieldset#' + value.id); if (key % 5 === 0) { - formDataSets[Math.floor(key / 5)] = $form.find('fieldset#' + value.id).serialize(); + formDataSets[dataSetIndex] = $fieldset.find('select:visible, input:not([data-name])').serialize(); } else { - formDataSets[Math.floor(key / 5)] += '&' + $form.find('fieldset#' + value.id).serialize(); + formDataSets[dataSetIndex] += '&' + $fieldset.find('select:visible, input:not([data-name])').serialize(); + } + + // Find proper role value + var roleInput = $fieldset.find('input[name^=role][data-name]'); + if (roleInput.val()) { + formDataSets[dataSetIndex] += '&' + roleInput.attr('name') + '=' + roleInput.val(); + } else { + formDataSets[dataSetIndex] += '&' + roleInput.attr('name') + '=' + + $fieldset.find('select[name="' + roleInput.attr('name') + '"]').val(); } }); @@ -215,6 +322,10 @@ $(function() { $(this).attr('data-clicked', true); }); } + + if ($('#acp_help_phpbb')) { + phpbb.prepareSendStats(); + } }); diff --git a/phpBB/adm/style/captcha_recaptcha.html b/phpBB/adm/style/captcha_recaptcha.html index d3038fd714..3f61c76cb1 100644 --- a/phpBB/adm/style/captcha_recaptcha.html +++ b/phpBB/adm/style/captcha_recaptcha.html @@ -1,32 +1,12 @@
- - - - - + +
diff --git a/phpBB/adm/style/install_convert.html b/phpBB/adm/style/install_convert.html deleted file mode 100644 index 7e22404f56..0000000000 --- a/phpBB/adm/style/install_convert.html +++ /dev/null @@ -1,134 +0,0 @@ - - - - -

{TITLE}

- -

{BODY}

- - - -
- -

{TITLE}

- -

{BODY}

- - -
-

{ERROR_TITLE}

-

{ERROR_MSG}

-
- - - -
{L_GROUP} {L_TOTAL_MEMBERS}{L_PENDING_MEMBERS} {L_OPTIONS} {L_ACTION}
{groups.GROUP_NAME} {groups.TOTAL_MEMBERS}{groups.PENDING_MEMBERS} {L_SETTINGS} {L_MEMBERS} {L_DELETE}{L_DELETE}
{L_WIDTH} {L_HEIGHT}{L_ALT_TEXT} {L_DISPLAY_ON_POSTING} {L_ORDER}
{items.TEXT_ALT} [{items.IMG}] diff --git a/phpBB/adm/style/acp_main.html b/phpBB/adm/style/acp_main.html index 1bdb7b8d2a..9522baa9c2 100644 --- a/phpBB/adm/style/acp_main.html +++ b/phpBB/adm/style/acp_main.html @@ -130,8 +130,6 @@ {L_FILES_PER_DAY}{L_COLON} {FILES_PER_DAY}
{L_BOARD_STARTED}{L_COLON} {START_DATE}{L_GZIP_COMPRESSION}{L_COLON} {GZIP_COMPRESSION}
{L_PHP_VERSION}{L_COLON} {PHP_VERSION_INFO}{L_NUMBER_ORPHAN}{L_COLON} {TOTAL_ORPHAN}  
{L_BOARD_VERSION}{L_COLON} style="color: #228822;" style="color: #BC2A4D;" title="{L_MORE_INFORMATION}">{BOARD_VERSION}{L_VERSIONCHECK_FORCE_UPDATE} ] {L_NUMBER_ORPHAN}{L_COLON} {TOTAL_ORPHAN}    
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
{L_AVAILABLE_CONVERTORS}
{L_SOFTWARE}{L_VERSION}{L_AUTHOR}{L_OPTIONS}
{convertors.SOFTWARE}{convertors.VERSION}{convertors.AUTHOR}{L_CONVERT}
{L_NO_CONVERTORS}---
- - - - - -
-
- -
-
-
- -
-
- -
- - - -
- - - - -
- -
- - {checks.LEGEND} -

{checks.LEGEND_EXPLAIN}

- - -
-

{checks.TITLE_EXPLAIN}
-
{checks.RESULT}
-
- - - -
- - - -
- - - - -
- -
- - {options.LEGEND} - - -
-

{options.TITLE_EXPLAIN}
-
{options.CONTENT}
-
- - - - -
- - - -

{L_MESSAGE}

- -
- {S_HIDDEN} - disabled="disabled" onclick="this.className = 'button1 disabled';" onsubmit="this.disabled = 'disabled';" name="submit" value="{L_SUBMIT}" /> -
- - -
- - - diff --git a/phpBB/adm/style/install_error.html b/phpBB/adm/style/install_error.html deleted file mode 100644 index 3f7c8b9ed4..0000000000 --- a/phpBB/adm/style/install_error.html +++ /dev/null @@ -1,8 +0,0 @@ - - -
-

{MESSAGE_TITLE}

-

{MESSAGE_TEXT}

-
- - diff --git a/phpBB/adm/style/install_header.html b/phpBB/adm/style/install_header.html deleted file mode 100644 index cfafe7917f..0000000000 --- a/phpBB/adm/style/install_header.html +++ /dev/null @@ -1,54 +0,0 @@ - - - - - - -{META} -{PAGE_TITLE} - - - - - -
- - -
-
- -
- -
-
- - -
-
diff --git a/phpBB/adm/style/install_install.html b/phpBB/adm/style/install_install.html deleted file mode 100644 index 1a809a3588..0000000000 --- a/phpBB/adm/style/install_install.html +++ /dev/null @@ -1,77 +0,0 @@ - - -
- -

{TITLE}

-

{BODY}

- - -
- - - - -
- -
- - {checks.LEGEND} -

{checks.LEGEND_EXPLAIN}

- - -
-
{checks.TITLE}{L_COLON}
{checks.TITLE_EXPLAIN}
-
{checks.RESULT}
-
- - - -
- - - -
- - - - -
- -
- - {options.LEGEND} - - -
-

{options.TITLE_EXPLAIN}
-
{options.CONTENT}
-
- - - - -
- - - -

{L_DL_CONFIG}

-

{L_DL_CONFIG_EXPLAIN}

- -
- {L_DL_CONFIG} - {S_HIDDEN} -   -
- - - -
- {L_SUBMIT} - {S_HIDDEN} - -
- - -
- - diff --git a/phpBB/adm/style/install_main.html b/phpBB/adm/style/install_main.html deleted file mode 100644 index 73e73ad578..0000000000 --- a/phpBB/adm/style/install_main.html +++ /dev/null @@ -1,6 +0,0 @@ - - -

{TITLE}

-

{BODY}

- - diff --git a/phpBB/adm/style/install_update.html b/phpBB/adm/style/install_update.html deleted file mode 100644 index 898233f72d..0000000000 --- a/phpBB/adm/style/install_update.html +++ /dev/null @@ -1,491 +0,0 @@ - - - - - -
-

{L_NOTICE}

-

{ERROR_MSG}

-
- - - - -
-

{L_IN_PROGRESS}

-

{L_IN_PROGRESS_EXPLAIN}

-
- - - - -
-

{L_NOTICE}

-

{WARNING_MSG}

-
- - -
-

{L_NOTICE}

-

{L_BACKUP_NOTICE}

-
- -
- -

{L_UPDATE_INSTALLATION}

-

{L_UPDATE_INSTALLATION_EXPLAIN}

- -
- -
- -
- - - -
- -

{L_UPDATE_SUCCESS}

-

{L_UPDATE_SUCCESS_EXPLAIN}

- -
- -
- -
- - - - - -

{L_VERSION_CHECK}

- -

{L_VERSION_CHECK_EXPLAIN}

- - -
-

{L_VERSION_UP_TO_DATE}

-
- -
-

{L_VERSION_NOT_UP_TO_DATE}

-
- - -
- -
-
-
{CURRENT_VERSION}
-
-
-
-
{LATEST_VERSION}
-
- -
-
-
{PACKAGE_VERSION}
-
- -
- -
- -
-

{L_CHECK_FILES_EXPLAIN}

- -
- -
- - - - - -

{L_PERFORM_DATABASE_UPDATE}

- -

- {L_PERFORM_DATABASE_UPDATE_EXPLAIN}
-

- -

- -
- -
- {L_RUN_DATABASE_SCRIPT} - - -
- -
- - - -
-

{L_UPDATE_SUCCESS}

-

{L_EVERYTHING_UP_TO_DATE}

-
- - - - - - - -

{L_UPDATE_FILE_SUCCESS}

-

{L_ALL_FILES_UP_TO_DATE}

- -

{L_UPDATE_DATABASE_EXPLAIN}

- -
- -
- -
- -
- - -

{L_COLLECTED_INFORMATION}

- -

{L_COLLECTED_INFORMATION_EXPLAIN}

- - -
-

{L_NO_UPDATE_FILES}

- -

{L_NO_UPDATE_FILES_EXPLAIN}


- - {NO_UPDATE_FILES} - -
- - -
- - -

{L_FILES_DELETED}

- -

{L_FILES_DELETED_EXPLAIN}

- -
- {L_STATUS_DELETED} - -
-
{deleted.DIR_PART}
{deleted.FILE_PART}
-
- [{deleted.L_SHOW_DIFF}]{L_BINARY_FILE} -
-
- -
- - - - -

{L_FILES_CONFLICT}

- -

{L_FILES_CONFLICT_EXPLAIN}

- - -
- {L_STATUS_CONFLICT} -
-
{conflict.DIR_PART}
{conflict.FILE_PART}
-
{L_FILE_USED}{L_COLON} {conflict.CUSTOM_ORIGINAL} -
{L_NUM_CONFLICTS}{L_COLON} {conflict.NUM_CONFLICTS} -
-
- [{L_DOWNLOAD_CONFLICTS}]
{L_DOWNLOAD_CONFLICTS_EXPLAIN} - {L_BINARY_FILE} -
- -
- -
- -
-
-
 
-
- -
-
-
[{L_SHOW_DIFF_MODIFIED}]
-
-
-
-
[{L_SHOW_DIFF_MODIFIED}]
-
-
-
-
[{L_SHOW_DIFF_FINAL}]
-
-
-
-
[{L_SHOW_DIFF_FINAL}]
-
- -
- - - - - -

{L_FILES_NEW_CONFLICT}

- -

{L_FILES_NEW_CONFLICT_EXPLAIN}

- -
- {L_STATUS_NEW_CONFLICT} - -
-
{new_conflict.DIR_PART}
{new_conflict.FILE_PART}
-
{L_FILE_USED}{L_COLON} {new_conflict.CUSTOM_ORIGINAL} -
-
- [{new_conflict.L_SHOW_DIFF}]{L_BINARY_FILE} -
- -
- -
- -
- - - - -

{L_FILES_MODIFIED}

- -

{L_FILES_MODIFIED_EXPLAIN}

- - -
- {L_STATUS_MODIFIED} -
-
{modified.DIR_PART}
{modified.FILE_PART}
-
{L_FILE_USED}{L_COLON} {modified.CUSTOM_ORIGINAL} -
-
 
- -
- -
-
-
-
[{modified.L_SHOW_DIFF}]{L_BINARY_FILE}
-
-
-
-
[{L_SHOW_DIFF_FINAL}] 
-
-
-
-
[{L_SHOW_DIFF_FINAL}] 
-
-
- - - - - -

{L_FILES_NEW}

- -

{L_FILES_NEW_EXPLAIN}

- - - - - - -

{L_FILES_NOT_MODIFIED}

- -

{L_FILES_NOT_MODIFIED_EXPLAIN}

- - - - - - -

{L_FILES_UP_TO_DATE}

- -

{L_FILES_UP_TO_DATE_EXPLAIN}

- - - - - -
- -
- -
- -
- -

{L_UPDATE_METHOD}

- -

{L_UPDATE_METHOD_EXPLAIN}

- -
-     -
- -
- - - - - -

{L_DOWNLOAD_UPDATE_METHOD}

- -

{L_DOWNLOAD_UPDATE_METHOD_EXPLAIN}

- -
- -
- {L_SELECT_DOWNLOAD_FORMAT} -
-
-
{RADIO_BUTTONS}
-
-
- -
- {S_HIDDEN_FIELDS} -     -
- -
- -

- -

{L_MAPPING_FILE_STRUCTURE}

- - - - - - - - - - - - - - - - - - - -
{L_ARCHIVE_FILE} {L_DESTINATION}
{location.SOURCE}»{location.DESTINATION}
- - - -

{L_SELECT_FTP_SETTINGS}

- -
- - -
-

{L_CONNECTION_SUCCESS}

-
- -
-

{L_TRY_DOWNLOAD_METHOD}

- -
- -
-
- -
-

{L_CONNECTION_FAILED}
{ERROR_MSG}

-
- - -
- {L_FTP_SETTINGS} -
-
-
{UPLOAD_METHOD}
-
- -
-

{data.EXPLAIN}
-
-
- -
- -
- {S_HIDDEN_FIELDS} - - - -
- -
- - - - diff --git a/phpBB/adm/style/install_update_diff.html b/phpBB/adm/style/install_update_diff.html deleted file mode 100644 index 324dc50d05..0000000000 --- a/phpBB/adm/style/install_update_diff.html +++ /dev/null @@ -1,255 +0,0 @@ - - - - - - -{META} -{PAGE_TITLE} - - - - - - - - - - - - - - - -
- - -
-
-
-
-
- {DIFF_CONTENT} -
-
-
-
-
- - - diff --git a/phpBB/adm/style/installer_convert.html b/phpBB/adm/style/installer_convert.html new file mode 100644 index 0000000000..aa16542b6b --- /dev/null +++ b/phpBB/adm/style/installer_convert.html @@ -0,0 +1,87 @@ + +

{TITLE}

+ +
+

{ERROR_TITLE}

+

{ERROR_MSG}

+
+ + +
+ + {errors.TITLE} +

{errors.DESCRIPTION}

+ +
+ +

{BODY}

+{CONTENT} + +

onclick="return false;">{L_SUBMIT}

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
{L_AVAILABLE_CONVERTORS}
{L_SOFTWARE}{L_VERSION}{L_AUTHOR}{L_CONVERT_OPTIONS}
{convertors.SOFTWARE}{convertors.VERSION}{convertors.AUTHOR}{L_CONVERT}
{L_NO_CONVERTORS}---
+ + +
+ + + + +
+ +
+ + {checks.LEGEND} +

{checks.LEGEND_EXPLAIN}

+ + +
+

{checks.TITLE_EXPLAIN}
+
{checks.RESULT}
+
+ + + +
+ + + diff --git a/phpBB/adm/style/install_footer.html b/phpBB/adm/style/installer_footer.html similarity index 75% rename from phpBB/adm/style/install_footer.html rename to phpBB/adm/style/installer_footer.html index 8e7599dc3f..fefa8f6d3f 100644 --- a/phpBB/adm/style/install_footer.html +++ b/phpBB/adm/style/installer_footer.html @@ -1,6 +1,6 @@ -
-
-
+
+
+
@@ -11,6 +11,15 @@ + + diff --git a/phpBB/adm/style/installer_form.html b/phpBB/adm/style/installer_form.html new file mode 100644 index 0000000000..a38f33c7c2 --- /dev/null +++ b/phpBB/adm/style/installer_form.html @@ -0,0 +1,56 @@ +
+ + +
+ + + + + +
+ +
+ + {options.LEGEND} + +
+

{options.TITLE_EXPLAIN}
+
+ + + + + + + + + + + + + + + checked /> {options.OPTIONS.label} + + +
+
+ + + +
+ + + +
+ {L_SUBMIT} + + disabled="disabled" /> + +
+ +
diff --git a/phpBB/adm/style/installer_header.html b/phpBB/adm/style/installer_header.html new file mode 100644 index 0000000000..704db9e174 --- /dev/null +++ b/phpBB/adm/style/installer_header.html @@ -0,0 +1,58 @@ + + + + + + + {META} + {PAGE_TITLE} + + + + + +
+ + +
+
+ +
+ +
+
+ + +
+
diff --git a/phpBB/adm/style/installer_install.html b/phpBB/adm/style/installer_install.html new file mode 100644 index 0000000000..53a91f2700 --- /dev/null +++ b/phpBB/adm/style/installer_install.html @@ -0,0 +1,13 @@ + +

{TITLE}

+

{CONTENT}

+ +
+
+ {L_SUBMIT} + +
+
+ + + diff --git a/phpBB/adm/style/installer_main.html b/phpBB/adm/style/installer_main.html new file mode 100644 index 0000000000..f14fe4da70 --- /dev/null +++ b/phpBB/adm/style/installer_main.html @@ -0,0 +1,6 @@ + + +

{TITLE}

+

{BODY}

+ + diff --git a/phpBB/adm/style/installer_update.html b/phpBB/adm/style/installer_update.html new file mode 100644 index 0000000000..48cc07f5d6 --- /dev/null +++ b/phpBB/adm/style/installer_update.html @@ -0,0 +1,13 @@ + +

{TITLE}

+

{CONTENT}

+ +
+
+ {L_SUBMIT} + +
+
+ + + diff --git a/phpBB/adm/style/installer_update_file_status.html b/phpBB/adm/style/installer_update_file_status.html new file mode 100644 index 0000000000..a27bfa6a44 --- /dev/null +++ b/phpBB/adm/style/installer_update_file_status.html @@ -0,0 +1,80 @@ + +

{L_FILES_DELETED}

+ +

{L_FILES_DELETED_EXPLAIN}

+ +
+ {L_STATUS_DELETED} + +
+
{deleted.DIR_PART}{deleted.FILE_PART}
+
+ +
+ + + + +

{L_FILES_CONFLICT}

+ +

{L_FILES_CONFLICT_EXPLAIN}

+ +
+ {L_STATUS_CONFLICT} + +
+
{conflict.DIR_PART}{conflict.FILE_PART}
+
+ +
+ + + + +

{L_FILES_MODIFIED}

+ +

{L_FILES_MODIFIED_EXPLAIN}

+ +
+ {L_STATUS_MODIFIED} + +
+
{modified.DIR_PART}{modified.FILE_PART}
+
+ +
+ + + + +

{L_FILES_NEW}

+ +

{L_FILES_NEW_EXPLAIN}

+ + + + + + +

{L_FILES_NOT_MODIFIED}

+ +

{L_FILES_NOT_MODIFIED_EXPLAIN}

+ + + + diff --git a/phpBB/adm/style/overall_header.html b/phpBB/adm/style/overall_header.html index bd8caf1443..8279ac34dc 100644 --- a/phpBB/adm/style/overall_header.html +++ b/phpBB/adm/style/overall_header.html @@ -7,6 +7,7 @@ {META} {PAGE_TITLE} + \n"; + } + + return $output; + } +} diff --git a/phpBB/phpbb/template/base.php b/phpBB/phpbb/template/base.php index 41c0a01ba8..d502aceab8 100644 --- a/phpBB/phpbb/template/base.php +++ b/phpBB/phpbb/template/base.php @@ -104,6 +104,27 @@ abstract class base implements template return $this; } + /** + * {@inheritdoc} + */ + public function retrieve_vars(array $vararray) + { + $result = array(); + foreach ($vararray as $varname) + { + $result[$varname] = $this->retrieve_var($varname); + } + return $result; + } + + /** + * {@inheritdoc} + */ + public function retrieve_var($varname) + { + return $this->context->retrieve_var($varname); + } + /** * {@inheritdoc} */ @@ -124,6 +145,14 @@ abstract class base implements template return $this; } + /** + * {@inheritdoc} + */ + public function retrieve_block_vars($blockname, array $vararray) + { + return $this->context->retrieve_block_vars($blockname, $vararray); + } + /** * {@inheritdoc} */ diff --git a/phpBB/phpbb/template/context.php b/phpBB/phpbb/template/context.php index 5d04a09865..392efd5933 100644 --- a/phpBB/phpbb/template/context.php +++ b/phpBB/phpbb/template/context.php @@ -86,6 +86,17 @@ class context return true; } + /** + * Retreive a single scalar value from a single key. + * + * @param string $varname Variable name + * @return mixed Variable value, or null if not set + */ + public function retrieve_var($varname) + { + return isset($this->rootref[$varname]) ? $this->rootref[$varname] : null; + } + /** * Returns a reference to template data array. * @@ -263,6 +274,68 @@ class context return true; } + /** + * Retrieve key variable pairs from the specified block + * + * @param string $blockname Name of block to retrieve $vararray from + * @param array $vararray An array of variable names, empty array retrieves all vars + * @return array of hashes with variable name as key and retrieved value or null as value + */ + public function retrieve_block_vars($blockname, array $vararray) + { + // For nested block, $blockcount > 0, for top-level block, $blockcount == 0 + $blocks = explode('.', $blockname); + $blockcount = sizeof($blocks) - 1; + + $block = $this->tpldata; + for ($i = 0; $i <= $blockcount; $i++) + { + if (($pos = strpos($blocks[$i], '[')) !== false) + { + $name = substr($blocks[$i], 0, $pos); + + if (strpos($blocks[$i], '[]') === $pos) + { + $index = sizeof($block[$name]) - 1; + } + else + { + $index = min((int) substr($blocks[$i], $pos + 1, -1), sizeof($block[$name]) - 1); + } + } + else + { + $name = $blocks[$i]; + $index = sizeof($block[$name]) - 1; + } + $block = $block[$name]; + $block = $block[$index]; + } + + $result = array(); + if ($vararray === array()) + { + // The calculated vars that depend on the block position are excluded from the complete block returned results + $excluded_vars = array('S_FIRST_ROW', 'S_LAST_ROW', 'S_BLOCK_NAME', 'S_NUM_ROWS', 'S_ROW_COUNT', 'S_ROW_NUM'); + + foreach ($block as $varname => $varvalue) + { + if ($varname === strtoupper($varname) && !is_array($varvalue) && !in_array($varname, $excluded_vars)) + { + $result[$varname] = $varvalue; + } + } + } + else + { + foreach ($vararray as $varname) + { + $result[$varname] = isset($block[$varname]) ? $block[$varname] : null; + } + } + return $result; + } + /** * Find the index for a specified key in the innermost specified block * @@ -363,10 +436,11 @@ class context * If key is false the position is set to 0 * If key is true the position is set to the last entry * - * @param string $mode Mode to execute (valid modes are 'insert' and 'change') + * @param string $mode Mode to execute (valid modes are 'insert', 'change' and 'delete') * * If insert, the vararray is inserted at the given position (position counting from zero). * If change, the current block gets merged with the vararray (resulting in new key/value pairs be added and existing keys be replaced by the new \value). + * If delete, the vararray is ignored, and the block at the given position (counting from zero) is removed. * * Since counting begins by zero, inserting at the last position will result in this array: array(vararray, last positioned array) * and inserting at position 1 will result in this array: array(first positioned array, vararray, following vars) @@ -502,6 +576,45 @@ class context return true; } + // Delete Block + if ($mode == 'delete') + { + // If we are exceeding last iteration, do not delete anything + if ($key > sizeof($block) || $key < 0) + { + return false; + } + + // If we are positioned at the end, we remove the last element + if ($key == sizeof($block)) + { + $key--; + } + + // We are deleting the last element in the block, so remove the block + if (sizeof($block) === 1) + { + $block = null; // unset($block); does not work on references + return true; + } + + // Re-position template blocks + for ($i = $key; $i < sizeof($block)-1; $i++) + { + $block[$i] = $block[$i+1]; + $block[$i]['S_ROW_COUNT'] = $block[$i]['S_ROW_NUM'] = $i; + } + + // Remove the last element + unset($block[$i]); + + // Set first and last elements again, in case they were removed + $block[0]['S_FIRST_ROW'] = true; + $block[sizeof($block)-1]['S_LAST_ROW'] = true; + + return true; + } + return false; } diff --git a/phpBB/phpbb/template/exception/user_object_not_available.php b/phpBB/phpbb/template/exception/user_object_not_available.php new file mode 100644 index 0000000000..62fd2743c1 --- /dev/null +++ b/phpBB/phpbb/template/exception/user_object_not_available.php @@ -0,0 +1,22 @@ + + * @license GNU General Public License, version 2 (GPL-2.0) + * + * For full copyright and license information, please see + * the docs/CREDITS.txt file. + * + */ + +namespace phpbb\template\exception; + +/** + * This exception is thrown when the user object was not set but it is required by the called method + */ +class user_object_not_available extends \phpbb\exception\runtime_exception +{ + +} diff --git a/phpBB/phpbb/template/template.php b/phpBB/phpbb/template/template.php index 9e3d658ca8..df83d5bc43 100644 --- a/phpBB/phpbb/template/template.php +++ b/phpBB/phpbb/template/template.php @@ -127,6 +127,22 @@ interface template */ public function append_var($varname, $varval); + /** + * Retrieve multiple template values + * + * @param array $vararray An array with variable names + * @return array A hash of variable name => value pairs (value is null if not set) + */ + public function retrieve_vars(array $vararray); + + /** + * Retreive a single scalar value from a single key. + * + * @param string $varname Variable name + * @return mixed Variable value, or null if not set + */ + public function retrieve_var($varname); + /** * Assign key variable pairs from an array to a specified block * @param string $blockname Name of block to assign $vararray to @@ -143,6 +159,14 @@ interface template */ public function assign_block_vars_array($blockname, array $block_vars_array); + /** + * Retrieve variable values from an specified block + * @param string $blockname Name of block to retrieve $vararray from + * @param array $vararray An array with variable names, empty array gets all vars + * @return array A hash of variable name => value pairs (value is null if not set) + */ + public function retrieve_block_vars($blockname, array $vararray); + /** * Change already assigned key variable pair (one-dimensional - single loop entry) * @@ -160,10 +184,11 @@ interface template * If key is false the position is set to 0 * If key is true the position is set to the last entry * - * @param string $mode Mode to execute (valid modes are 'insert' and 'change') + * @param string $mode Mode to execute (valid modes are 'insert', 'change' and 'delete') * * If insert, the vararray is inserted at the given position (position counting from zero). * If change, the current block gets merged with the vararray (resulting in new \key/value pairs be added and existing keys be replaced by the new \value). + * If delete, the vararray is ignored, and the block at the given position (counting from zero) is removed. * * Since counting begins by zero, inserting at the last position will result in this array: array(vararray, last positioned array) * and inserting at position 1 will result in this array: array(first positioned array, vararray, following vars) diff --git a/phpBB/phpbb/template/twig/environment.php b/phpBB/phpbb/template/twig/environment.php index 476ffd935e..ac4b16e457 100644 --- a/phpBB/phpbb/template/twig/environment.php +++ b/phpBB/phpbb/template/twig/environment.php @@ -13,17 +13,28 @@ namespace phpbb\template\twig; +use phpbb\template\assets_bag; + class environment extends \Twig_Environment { /** @var \phpbb\config\config */ protected $phpbb_config; + /** @var \phpbb\filesystem\filesystem */ + protected $filesystem; + /** @var \phpbb\path_helper */ protected $phpbb_path_helper; + /** @var \Symfony\Component\DependencyInjection\ContainerInterface */ + protected $container; + /** @var \phpbb\extension\manager */ protected $extension_manager; + /** @var \phpbb\event\dispatcher_interface */ + protected $phpbb_dispatcher; + /** @var string */ protected $phpbb_root_path; @@ -33,26 +44,43 @@ class environment extends \Twig_Environment /** @var array **/ protected $namespace_look_up_order = array('__main__'); + /** @var assets_bag */ + protected $assets_bag; + /** * Constructor * * @param \phpbb\config\config $phpbb_config The phpBB configuration + * @param \phpbb\filesystem\filesystem $filesystem * @param \phpbb\path_helper $path_helper phpBB path helper + * @param string $cache_path The path to the cache directory * @param \phpbb\extension\manager $extension_manager phpBB extension manager * @param \Twig_LoaderInterface $loader Twig loader interface + * @param \phpbb\event\dispatcher_interface $phpbb_dispatcher Event dispatcher object * @param array $options Array of options to pass to Twig */ - public function __construct($phpbb_config, \phpbb\path_helper $path_helper, \phpbb\extension\manager $extension_manager = null, \Twig_LoaderInterface $loader = null, $options = array()) + public function __construct(\phpbb\config\config $phpbb_config, \phpbb\filesystem\filesystem $filesystem, \phpbb\path_helper $path_helper, $cache_path, \phpbb\extension\manager $extension_manager = null, \Twig_LoaderInterface $loader = null, \phpbb\event\dispatcher_interface $phpbb_dispatcher = null, $options = array()) { $this->phpbb_config = $phpbb_config; + $this->filesystem = $filesystem; $this->phpbb_path_helper = $path_helper; $this->extension_manager = $extension_manager; + $this->phpbb_dispatcher = $phpbb_dispatcher; $this->phpbb_root_path = $this->phpbb_path_helper->get_phpbb_root_path(); $this->web_root_path = $this->phpbb_path_helper->get_web_root_path(); - return parent::__construct($loader, $options); + $this->assets_bag = new assets_bag(); + + $options = array_merge(array( + 'cache' => (defined('IN_INSTALL')) ? false : $cache_path, + 'debug' => false, + 'auto_reload' => (bool) $this->phpbb_config['load_tplcompile'], + 'autoescape' => false, + ), $options); + + parent::__construct($loader, $options); } /** @@ -78,15 +106,25 @@ class environment extends \Twig_Environment } /** - * Get the phpBB root path - * - * @return string - */ + * Get the phpBB root path + * + * @return string + */ public function get_phpbb_root_path() { return $this->phpbb_root_path; } + /** + * Get the filesystem object + * + * @return \phpbb\filesystem\filesystem + */ + public function get_filesystem() + { + return $this->filesystem; + } + /** * Get the web root path * @@ -107,6 +145,16 @@ class environment extends \Twig_Environment return $this->phpbb_path_helper; } + /** + * Gets the assets bag + * + * @return assets_bag + */ + public function get_assets_bag() + { + return $this->assets_bag; + } + /** * Get the namespace look up order * @@ -130,6 +178,84 @@ class environment extends \Twig_Environment return $this; } + /** + * {@inheritdoc} + */ + public function render($name, array $context = []) + { + return $this->display_with_assets($name, $context); + } + + /** + * {@inheritdoc} + */ + public function display($name, array $context = []) + { + echo $this->display_with_assets($name, $context); + } + + /** + * {@inheritdoc} + */ + private function display_with_assets($name, array $context = []) + { + $placeholder_salt = unique_id(); + + if (array_key_exists('definition', $context)) + { + $context['definition']->set('SCRIPTS', '__SCRIPTS_' . $placeholder_salt . '__'); + $context['definition']->set('STYLESHEETS', '__STYLESHEETS_' . $placeholder_salt . '__'); + } + + /** + * Allow changing the template output stream before rendering + * + * @event core.twig_environment_render_template_before + * @var array context Array with template variables + * @var string name The template name + * @since 3.2.1-RC1 + */ + if ($this->phpbb_dispatcher) + { + $vars = array('context', 'name'); + extract($this->phpbb_dispatcher->trigger_event('core.twig_environment_render_template_before', compact($vars))); + } + + $output = parent::render($name, $context); + + /** + * Allow changing the template output stream after rendering + * + * @event core.twig_environment_render_template_after + * @var array context Array with template variables + * @var string name The template name + * @var string output Rendered template output stream + * @since 3.2.1-RC1 + */ + if ($this->phpbb_dispatcher) + { + $vars = array('context', 'name', 'output'); + extract($this->phpbb_dispatcher->trigger_event('core.twig_environment_render_template_after', compact($vars))); + } + + return $this->inject_assets($output, $placeholder_salt); + } + + /** + * Injects the assets (from INCLUDECSS/JS) in the output. + * + * @param string $output + * + * @return string + */ + private function inject_assets($output, $placeholder_salt) + { + $output = str_replace('__STYLESHEETS_' . $placeholder_salt . '__', $this->assets_bag->get_stylesheets_content(), $output); + $output = str_replace('__SCRIPTS_' . $placeholder_salt . '__', $this->assets_bag->get_scripts_content(), $output); + + return $output; + } + /** * Loads a template by name. * diff --git a/phpBB/phpbb/template/twig/extension.php b/phpBB/phpbb/template/twig/extension.php index d5b14129b5..f0e716d697 100644 --- a/phpBB/phpbb/template/twig/extension.php +++ b/phpBB/phpbb/template/twig/extension.php @@ -18,20 +18,20 @@ class extension extends \Twig_Extension /** @var \phpbb\template\context */ protected $context; - /** @var \phpbb\user */ - protected $user; + /** @var \phpbb\language\language */ + protected $language; /** * Constructor * * @param \phpbb\template\context $context - * @param \phpbb\user $user + * @param \phpbb\language\language $language * @return \phpbb\template\twig\extension */ - public function __construct(\phpbb\template\context $context, $user) + public function __construct(\phpbb\template\context $context, $language) { $this->context = $context; - $this->user = $user; + $this->language = $language; } /** @@ -71,6 +71,7 @@ class extension extends \Twig_Extension { return array( new \Twig_SimpleFilter('subset', array($this, 'loop_subset'), array('needs_environment' => true)), + // @deprecated 3.2.0 Uses twig's JS escape method instead of addslashes new \Twig_SimpleFilter('addslashes', 'addslashes'), ); } @@ -176,9 +177,9 @@ class extension extends \Twig_Extension return $context_vars['L_' . $key]; } - // LA_ is transformed into lang(\'$1\')|addslashes, so we should not + // LA_ is transformed into lang(\'$1\')|escape('js'), so we should not // need to check for it - return call_user_func_array(array($this->user, 'lang'), $args); + return call_user_func_array(array($this->language, 'lang'), $args); } } diff --git a/phpBB/phpbb/template/twig/extension/routing.php b/phpBB/phpbb/template/twig/extension/routing.php new file mode 100644 index 0000000000..829ce738eb --- /dev/null +++ b/phpBB/phpbb/template/twig/extension/routing.php @@ -0,0 +1,43 @@ + +* @license GNU General Public License, version 2 (GPL-2.0) +* +* For full copyright and license information, please see +* the docs/CREDITS.txt file. +* +*/ + +namespace phpbb\template\twig\extension; + +use Symfony\Bridge\Twig\Extension\RoutingExtension; +use Symfony\Component\Routing\Generator\UrlGeneratorInterface; + +class routing extends RoutingExtension +{ + /** @var \phpbb\controller\helper */ + protected $helper; + + /** + * Constructor + * + * @param \phpbb\routing\helper $helper + */ + public function __construct(\phpbb\routing\helper $helper) + { + $this->helper = $helper; + } + + public function getPath($name, $parameters = array(), $relative = false) + { + return $this->helper->route($name, $parameters, true, false, $relative ? UrlGeneratorInterface::RELATIVE_PATH : UrlGeneratorInterface::ABSOLUTE_PATH); + } + + public function getUrl($name, $parameters = array(), $schemeRelative = false) + { + return $this->helper->route($name, $parameters, true, false, $schemeRelative ? UrlGeneratorInterface::NETWORK_PATH : UrlGeneratorInterface::ABSOLUTE_URL); + } +} diff --git a/phpBB/phpbb/template/twig/lexer.php b/phpBB/phpbb/template/twig/lexer.php index c5dc7273ba..d0bcfa615e 100644 --- a/phpBB/phpbb/template/twig/lexer.php +++ b/phpBB/phpbb/template/twig/lexer.php @@ -15,8 +15,21 @@ namespace phpbb\template\twig; class lexer extends \Twig_Lexer { + public function set_environment(\Twig_Environment $env) + { + $this->env = $env; + } + public function tokenize($code, $filename = null) { + // Handle \Twig_Source format input + if ($code instanceof \Twig_Source) + { + $source = $code; + $code = $source->getCode(); + $filename = $source->getName(); + } + // Our phpBB tags // Commented out tokens are handled separately from the main replace $phpbb_tags = array( @@ -112,15 +125,16 @@ class lexer extends \Twig_Lexer // Appends any filters after lang() $code = preg_replace('#{L_([a-zA-Z0-9_\.]+)(\|[^}]+?)?}#', '{{ lang(\'$1\')$2 }}', $code); - // Replace all of our escaped language variables, {LA_VARNAME}, with Twig style, {{ lang('NAME')|addslashes }} - // Appends any filters after lang(), but before addslashes - $code = preg_replace('#{LA_([a-zA-Z0-9_\.]+)(\|[^}]+?)?}#', '{{ lang(\'$1\')$2|addslashes }}', $code); + // Replace all of our escaped language variables, {LA_VARNAME}, with Twig style, {{ lang('NAME')|escape('js') }} + // Appends any filters after lang(), but before escape('js') + $code = preg_replace('#{LA_([a-zA-Z0-9_\.]+)(\|[^}]+?)?}#', '{{ lang(\'$1\')$2|escape(\'js\') }}', $code); // Replace all of our variables, {VARNAME}, with Twig style, {{ VARNAME }} // Appends any filters $code = preg_replace('#{([a-zA-Z0-9_\.]+)(\|[^}]+?)?}#', '{{ $1$2 }}', $code); - return parent::tokenize($code, $filename); + // Tokenize \Twig_Source instance + return parent::tokenize(new \Twig_Source($code, $filename)); } /** diff --git a/phpBB/phpbb/template/twig/loader.php b/phpBB/phpbb/template/twig/loader.php index 139a413b70..c13e3ee298 100644 --- a/phpBB/phpbb/template/twig/loader.php +++ b/phpBB/phpbb/template/twig/loader.php @@ -20,6 +20,24 @@ class loader extends \Twig_Loader_Filesystem { protected $safe_directories = array(); + /** + * @var \phpbb\filesystem\filesystem_interface + */ + protected $filesystem; + + /** + * Constructor + * + * @param \phpbb\filesystem\filesystem_interface $filesystem + * @param string|array $paths + */ + public function __construct(\phpbb\filesystem\filesystem_interface $filesystem, $paths = array()) + { + $this->filesystem = $filesystem; + + parent::__construct($paths, $this->filesystem->realpath(dirname(__FILE__))); + } + /** * Set safe directories * @@ -49,7 +67,7 @@ class loader extends \Twig_Loader_Filesystem */ public function addSafeDirectory($directory) { - $directory = phpbb_realpath($directory); + $directory = $this->filesystem->realpath($directory); if ($directory !== false) { @@ -82,6 +100,16 @@ class loader extends \Twig_Loader_Filesystem return; } + /** + * Adds a realpath call to fix a BC break in Twig 1.26 (https://github.com/twigphp/Twig/issues/2145) + * + * {@inheritdoc} + */ + public function addPath($path, $namespace = self::MAIN_NAMESPACE) + { + return parent::addPath($this->filesystem->realpath($path), $namespace); + } + /** * Find the template * @@ -119,7 +147,7 @@ class loader extends \Twig_Loader_Filesystem // can now check if we're within a "safe" directory // Find the real path of the directory the file is in - $directory = phpbb_realpath(dirname($file)); + $directory = $this->filesystem->realpath(dirname($file)); if ($directory === false) { diff --git a/phpBB/phpbb/template/twig/node/event.php b/phpBB/phpbb/template/twig/node/event.php index b765bde98d..11fdb75247 100644 --- a/phpBB/phpbb/template/twig/node/event.php +++ b/phpBB/phpbb/template/twig/node/event.php @@ -46,7 +46,7 @@ class event extends \Twig_Node { $ext_namespace = str_replace('/', '_', $ext_namespace); - if (defined('DEBUG')) + if ($this->environment->isDebug()) { // If debug mode is enabled, lets check for new/removed EVENT // templates on page load rather than at compile. This is @@ -58,7 +58,7 @@ class event extends \Twig_Node ; } - if (defined('DEBUG') || $this->environment->getLoader()->exists('@' . $ext_namespace . '/' . $location . '.html')) + if ($this->environment->isDebug() || $this->environment->getLoader()->exists('@' . $ext_namespace . '/' . $location . '.html')) { $compiler ->write("\$previous_look_up_order = \$this->env->getNamespaceLookUpOrder();\n") @@ -70,7 +70,7 @@ class event extends \Twig_Node ; } - if (defined('DEBUG')) + if ($this->environment->isDebug()) { $compiler ->outdent() diff --git a/phpBB/phpbb/template/twig/node/includeasset.php b/phpBB/phpbb/template/twig/node/includeasset.php index 15195a226b..6d50eafc9d 100644 --- a/phpBB/phpbb/template/twig/node/includeasset.php +++ b/phpBB/phpbb/template/twig/node/includeasset.php @@ -39,7 +39,7 @@ abstract class includeasset extends \Twig_Node ->write("\$asset_file = ") ->subcompile($this->getNode('expr')) ->raw(";\n") - ->write("\$asset = new \phpbb\\template\\asset(\$asset_file, \$this->getEnvironment()->get_path_helper());\n") + ->write("\$asset = new \phpbb\\template\\asset(\$asset_file, \$this->getEnvironment()->get_path_helper(), \$this->getEnvironment()->get_filesystem());\n") ->write("if (substr(\$asset_file, 0, 2) !== './' && \$asset->is_relative()) {\n") ->indent() ->write("\$asset_path = \$asset->get_path();") @@ -49,33 +49,18 @@ abstract class includeasset extends \Twig_Node ->write("\$local_file = \$this->getEnvironment()->findTemplate(\$asset_path);\n") ->write("\$asset->set_path(\$local_file, true);\n") ->outdent() - ->write("\$asset->add_assets_version('{$config['assets_version']}');\n") - ->write("\$asset_file = \$asset->get_url();\n") ->write("}\n") + ->write("\$asset->add_assets_version('{$config['assets_version']}');\n") ->outdent() ->write("}\n") - ->write("\$context['definition']->append('{$this->get_definition_name()}', '") - ; - - $this->append_asset($compiler); - - $compiler - ->raw("\n');\n") + ->write("\$this->getEnvironment()->get_assets_bag()->add_{$this->get_setters_name()}(\$asset);") ; } /** - * Get the definition name + * Get the name of the assets bag setter * - * @return string (e.g. 'SCRIPTS') + * @return string (e.g. 'script') */ - abstract public function get_definition_name(); - - /** - * Append the output code for the asset - * - * @param \Twig_Compiler A Twig_Compiler instance - * @return null - */ - abstract protected function append_asset(\Twig_Compiler $compiler); + abstract public function get_setters_name(); } diff --git a/phpBB/phpbb/template/twig/node/includecss.php b/phpBB/phpbb/template/twig/node/includecss.php index 2dac154036..2e97d4972d 100644 --- a/phpBB/phpbb/template/twig/node/includecss.php +++ b/phpBB/phpbb/template/twig/node/includecss.php @@ -18,20 +18,8 @@ class includecss extends \phpbb\template\twig\node\includeasset /** * {@inheritdoc} */ - public function get_definition_name() + public function get_setters_name() { - return 'STYLESHEETS'; - } - - /** - * {@inheritdoc} - */ - public function append_asset(\Twig_Compiler $compiler) - { - $compiler - ->raw("raw("\$asset_file . '\"") - ->raw(' rel="stylesheet" type="text/css" media="screen" />') - ; + return 'stylesheet'; } } diff --git a/phpBB/phpbb/template/twig/node/includejs.php b/phpBB/phpbb/template/twig/node/includejs.php index 0f67f9ff60..505b49757b 100644 --- a/phpBB/phpbb/template/twig/node/includejs.php +++ b/phpBB/phpbb/template/twig/node/includejs.php @@ -18,22 +18,8 @@ class includejs extends \phpbb\template\twig\node\includeasset /** * {@inheritdoc} */ - public function get_definition_name() + public function get_setters_name() { - return 'SCRIPTS'; - } - - /** - * {@inheritdoc} - */ - protected function append_asset(\Twig_Compiler $compiler) - { - $config = $this->environment->get_phpbb_config(); - - $compiler - ->raw("\n") - ; + return 'script'; } } diff --git a/phpBB/phpbb/template/twig/twig.php b/phpBB/phpbb/template/twig/twig.php index d1bbb2b55a..f322778eda 100644 --- a/phpBB/phpbb/template/twig/twig.php +++ b/phpBB/phpbb/template/twig/twig.php @@ -13,6 +13,8 @@ namespace phpbb\template\twig; +use phpbb\template\exception\user_object_not_available; + /** * Twig Template class. */ @@ -76,11 +78,14 @@ class twig extends \phpbb\template\base * * @param \phpbb\path_helper $path_helper * @param \phpbb\config\config $config - * @param \phpbb\user $user * @param \phpbb\template\context $context template context + * @param \phpbb\template\twig\environment $twig_environment + * @param string $cache_path + * @param \phpbb\user|null $user + * @param array|\ArrayAccess $extensions * @param \phpbb\extension\manager $extension_manager extension manager, if null then template events will not be invoked */ - public function __construct(\phpbb\path_helper $path_helper, $config, $user, \phpbb\template\context $context, \phpbb\extension\manager $extension_manager = null) + public function __construct(\phpbb\path_helper $path_helper, $config, \phpbb\template\context $context, \phpbb\template\twig\environment $twig_environment, $cache_path, \phpbb\user $user = null, $extensions = array(), \phpbb\extension\manager $extension_manager = null) { $this->path_helper = $path_helper; $this->phpbb_root_path = $path_helper->get_phpbb_root_path(); @@ -89,41 +94,14 @@ class twig extends \phpbb\template\base $this->user = $user; $this->context = $context; $this->extension_manager = $extension_manager; + $this->cachepath = $cache_path; + $this->twig = $twig_environment; - $this->cachepath = $this->phpbb_root_path . 'cache/twig/'; - - // Initiate the loader, __main__ namespace paths will be setup later in set_style_names() - $loader = new \phpbb\template\twig\loader(''); - - $this->twig = new \phpbb\template\twig\environment( - $this->config, - $this->path_helper, - $this->extension_manager, - $loader, - array( - 'cache' => (defined('IN_INSTALL')) ? false : $this->cachepath, - 'debug' => defined('DEBUG'), - 'auto_reload' => (bool) $this->config['load_tplcompile'], - 'autoescape' => false, - ) - ); - - $this->twig->addExtension( - new \phpbb\template\twig\extension( - $this->context, - $this->user - ) - ); - - if (defined('DEBUG')) + foreach ($extensions as $extension) { - $this->twig->addExtension(new \Twig_Extension_Debug()); + $this->twig->addExtension($extension); } - $lexer = new \phpbb\template\twig\lexer($this->twig); - - $this->twig->setLexer($lexer); - // Add admin namespace if ($this->path_helper->get_adm_relative_path() !== null && is_dir($this->phpbb_root_path . $this->path_helper->get_adm_relative_path() . 'style/')) { @@ -150,9 +128,16 @@ class twig extends \phpbb\template\base * Get the style tree of the style preferred by the current user * * @return array Style tree, most specific first + * + * @throws \phpbb\template\exception\user_object_not_available When user service was not set */ public function get_user_style() { + if ($this->user === null) + { + throw new user_object_not_available(); + } + $style_list = array( $this->user->style['style_path'], ); @@ -368,14 +353,24 @@ class twig extends \phpbb\template\base $context_vars['.'][0], // To get normal vars array( 'definition' => new \phpbb\template\twig\definition(), - 'user' => $this->user, 'loops' => $context_vars, // To get loops ) ); + if ($this->user instanceof \phpbb\user) + { + $vars['user'] = $this->user; + } + // cleanup unset($vars['loops']['.']); + // Inject in the main context the value added by assign_block_vars() to be able to use directly the Twig loops. + foreach ($vars['loops'] as $key => &$value) + { + $vars[$key] = $value; + } + return $vars; } diff --git a/phpBB/phpbb/textformatter/cache_interface.php b/phpBB/phpbb/textformatter/cache_interface.php new file mode 100644 index 0000000000..f6b5f195c7 --- /dev/null +++ b/phpBB/phpbb/textformatter/cache_interface.php @@ -0,0 +1,31 @@ + +* @license GNU General Public License, version 2 (GPL-2.0) +* +* For full copyright and license information, please see +* the docs/CREDITS.txt file. +* +*/ + +namespace phpbb\textformatter; + +/** +* Currently only used to signal that something that could effect the rendering has changed. +* BBCodes, smilies, censored words, templates, etc... +*/ +interface cache_interface +{ + /** + * Invalidate and/or regenerate this text formatter's cache(s) + */ + public function invalidate(); + + /** + * Tidy/prune this text formatter's cache(s) + */ + public function tidy(); +} diff --git a/phpBB/phpbb/textformatter/data_access.php b/phpBB/phpbb/textformatter/data_access.php new file mode 100644 index 0000000000..0d37e62c87 --- /dev/null +++ b/phpBB/phpbb/textformatter/data_access.php @@ -0,0 +1,252 @@ + +* @license GNU General Public License, version 2 (GPL-2.0) +* +* For full copyright and license information, please see +* the docs/CREDITS.txt file. +* +*/ + +namespace phpbb\textformatter; + +/** +* Data access layer that fetchs BBCodes, smilies and censored words from the database. +* To be extended to include insert/update/delete operations. +* +* Also used to get templates. +*/ +class data_access +{ + /** + * @var string Name of the BBCodes table + */ + protected $bbcodes_table; + + /** + * @var \phpbb\db\driver\driver_interface + */ + protected $db; + + /** + * @var string Name of the smilies table + */ + protected $smilies_table; + + /** + * @var string Name of the styles table + */ + protected $styles_table; + + /** + * @var string Path to the styles dir + */ + protected $styles_path; + + /** + * @var string Name of the words table + */ + protected $words_table; + + /** + * Constructor + * + * @param \phpbb\db\driver\driver_interface $db Database connection + * @param string $bbcodes_table Name of the BBCodes table + * @param string $smilies_table Name of the smilies table + * @param string $styles_table Name of the styles table + * @param string $words_table Name of the words table + * @param string $styles_path Path to the styles dir + */ + public function __construct(\phpbb\db\driver\driver_interface $db, $bbcodes_table, $smilies_table, $styles_table, $words_table, $styles_path) + { + $this->db = $db; + + $this->bbcodes_table = $bbcodes_table; + $this->smilies_table = $smilies_table; + $this->styles_table = $styles_table; + $this->words_table = $words_table; + + $this->styles_path = $styles_path; + } + + /** + * Return the list of custom BBCodes + * + * @return array + */ + public function get_bbcodes() + { + $sql = 'SELECT bbcode_match, bbcode_tpl FROM ' . $this->bbcodes_table; + + return $this->fetch_decoded_rowset($sql, ['bbcode_match']); + } + + /** + * Return the list of smilies + * + * @return array + */ + public function get_smilies() + { + // NOTE: smilies that are displayed on the posting page are processed first because they're + // typically the most used smilies and it ends up producing a slightly more efficient + // renderer + $sql = 'SELECT code, emotion, smiley_url, smiley_width, smiley_height + FROM ' . $this->smilies_table . ' + ORDER BY display_on_posting DESC'; + + return $this->fetch_decoded_rowset($sql, ['code', 'emotion', 'smiley_url']); + } + + /** + * Return the list of installed styles + * + * @return array + */ + protected function get_styles() + { + $sql = 'SELECT style_id, style_path, style_parent_id, bbcode_bitfield FROM ' . $this->styles_table; + + return $this->fetch_decoded_rowset($sql); + } + + /** + * Return the bbcode.html template for every installed style + * + * @return array 2D array. style_id as keys, each element is an array with a "template" element that contains the style's bbcode.html and a "bbcodes" element that contains the name of each BBCode that is to be stylised + */ + public function get_styles_templates() + { + $templates = array(); + + $bbcode_ids = array( + 'quote' => 0, + 'b' => 1, + 'i' => 2, + 'url' => 3, + 'img' => 4, + 'size' => 5, + 'color' => 6, + 'u' => 7, + 'code' => 8, + 'list' => 9, + '*' => 9, + 'email' => 10, + 'flash' => 11, + 'attachment' => 12, + ); + + $styles = array(); + foreach ($this->get_styles() as $row) + { + $styles[$row['style_id']] = $row; + } + + foreach ($styles as $style_id => $style) + { + $bbcodes = array(); + + // Collect the name of the BBCodes whose bit is set in the style's bbcode_bitfield + $template_bitfield = new \bitfield($style['bbcode_bitfield']); + foreach ($bbcode_ids as $bbcode_name => $bit) + { + if ($template_bitfield->get($bit)) + { + $bbcodes[] = $bbcode_name; + } + } + + $filename = $this->resolve_style_filename($styles, $style); + if ($filename === false) + { + // Ignore this style, it will use the default templates + continue; + } + + $templates[$style_id] = array( + 'bbcodes' => $bbcodes, + 'template' => file_get_contents($filename), + ); + } + + return $templates; + } + + /** + * Resolve inheritance for given style and return the path to their bbcode.html file + * + * @param array $styles Associative array of [style_id => style] containing all styles + * @param array $style Style for which we resolve + * @return string|bool Path to this style's bbcode.html, or FALSE + */ + protected function resolve_style_filename(array $styles, array $style) + { + // Look for a bbcode.html in this style's dir + $filename = $this->styles_path . $style['style_path'] . '/template/bbcode.html'; + if (file_exists($filename)) + { + return $filename; + } + + // Resolve using this style's parent + $parent_id = $style['style_parent_id']; + if ($parent_id && !empty($styles[$parent_id])) + { + return $this->resolve_style_filename($styles, $styles[$parent_id]); + } + + return false; + } + + /** + * Return the list of censored words + * + * @return array + */ + public function get_censored_words() + { + $sql = 'SELECT word, replacement FROM ' . $this->words_table; + + return $this->fetch_decoded_rowset($sql, ['word', 'replacement']); + } + + /** + * Decode HTML special chars in given rowset + * + * @param array $rows Original rowset + * @param array $columns List of columns to decode + * @return array Decoded rowset + */ + protected function decode_rowset(array $rows, array $columns) + { + foreach ($rows as &$row) + { + foreach ($columns as $column) + { + $row[$column] = htmlspecialchars_decode($row[$column]); + } + } + + return $rows; + } + + /** + * Fetch all rows for given query and decode plain text columns + * + * @param string $sql SELECT query + * @param array $columns List of columns to decode + * @return array + */ + protected function fetch_decoded_rowset($sql, array $columns = []) + { + $result = $this->db->sql_query($sql); + $rows = $this->db->sql_fetchrowset($result); + $this->db->sql_freeresult($result); + + return $this->decode_rowset($rows, $columns); + } +} diff --git a/phpBB/phpbb/textformatter/parser_interface.php b/phpBB/phpbb/textformatter/parser_interface.php new file mode 100644 index 0000000000..ad611fb5b4 --- /dev/null +++ b/phpBB/phpbb/textformatter/parser_interface.php @@ -0,0 +1,112 @@ + +* @license GNU General Public License, version 2 (GPL-2.0) +* +* For full copyright and license information, please see +* the docs/CREDITS.txt file. +* +*/ + +namespace phpbb\textformatter; + +interface parser_interface +{ + /** + * Parse given text + * + * @param string $text + * @return string + */ + public function parse($text); + + /** + * Disable a specific BBCode + * + * @param string $name BBCode name + * @return null + */ + public function disable_bbcode($name); + + /** + * Disable BBCodes in general + */ + public function disable_bbcodes(); + + /** + * Disable the censor + */ + public function disable_censor(); + + /** + * Disable magic URLs + */ + public function disable_magic_url(); + + /** + * Disable smilies + */ + public function disable_smilies(); + + /** + * Enable a specific BBCode + * + * @param string $name BBCode name + * @return null + */ + public function enable_bbcode($name); + + /** + * Enable BBCodes in general + */ + public function enable_bbcodes(); + + /** + * Enable the censor + */ + public function enable_censor(); + + /** + * Enable magic URLs + */ + public function enable_magic_url(); + + /** + * Enable smilies + */ + public function enable_smilies(); + + /** + * Get the list of errors that were generated during last parsing + * + * @return array[] Array of arrays. Each array contains a lang string at index 0 plus any number + * of optional parameters + */ + public function get_errors(); + + /** + * Set a variable to be used by the parser + * + * - max_font_size + * - max_img_height + * - max_img_width + * - max_smilies + * - max_urls + * + * @param string $name + * @param mixed $value + * @return null + */ + public function set_var($name, $value); + + /** + * Set multiple variables to be used by the parser + * + * @param array $vars Associative array of [name => value] + * @return null + */ + public function set_vars(array $vars); +} diff --git a/phpBB/phpbb/textformatter/renderer_interface.php b/phpBB/phpbb/textformatter/renderer_interface.php new file mode 100644 index 0000000000..609b0bb642 --- /dev/null +++ b/phpBB/phpbb/textformatter/renderer_interface.php @@ -0,0 +1,92 @@ + +* @license GNU General Public License, version 2 (GPL-2.0) +* +* For full copyright and license information, please see +* the docs/CREDITS.txt file. +* +*/ + +namespace phpbb\textformatter; + +interface renderer_interface +{ + /** + * Render given text + * + * @param string $text Text, as parsed by something that implements \phpbb\textformatter\parser + * @return string + */ + public function render($text); + + /** + * Set the smilies' path + * + * @return null + */ + public function set_smilies_path($path); + + /** + * Return the value of the "viewcensors" option + * + * @return bool Option's value + */ + public function get_viewcensors(); + + /** + * Return the value of the "viewflash" option + * + * @return bool Option's value + */ + public function get_viewflash(); + + /** + * Return the value of the "viewimg" option + * + * @return bool Option's value + */ + public function get_viewimg(); + + /** + * Return the value of the "viewsmilies" option + * + * @return bool Option's value + */ + public function get_viewsmilies(); + + /** + * Set the "viewcensors" option + * + * @param bool $value Option's value + * @return null + */ + public function set_viewcensors($value); + + /** + * Set the "viewflash" option + * + * @param bool $value Option's value + * @return null + */ + public function set_viewflash($value); + + /** + * Set the "viewimg" option + * + * @param bool $value Option's value + * @return null + */ + public function set_viewimg($value); + + /** + * Set the "viewsmilies" option + * + * @param bool $value Option's value + * @return null + */ + public function set_viewsmilies($value); +} diff --git a/phpBB/phpbb/textformatter/s9e/factory.php b/phpBB/phpbb/textformatter/s9e/factory.php new file mode 100644 index 0000000000..d5ad8283d9 --- /dev/null +++ b/phpBB/phpbb/textformatter/s9e/factory.php @@ -0,0 +1,652 @@ + +* @license GNU General Public License, version 2 (GPL-2.0) +* +* For full copyright and license information, please see +* the docs/CREDITS.txt file. +* +*/ + +namespace phpbb\textformatter\s9e; + +use s9e\TextFormatter\Configurator; +use s9e\TextFormatter\Configurator\Items\AttributeFilters\RegexpFilter; +use s9e\TextFormatter\Configurator\Items\UnsafeTemplate; + +/** +* Creates s9e\TextFormatter objects +*/ +class factory implements \phpbb\textformatter\cache_interface +{ + /** + * @var \phpbb\textformatter\s9e\link_helper + */ + protected $link_helper; + + /** + * @var \phpbb\cache\driver\driver_interface + */ + protected $cache; + + /** + * @var string Path to the cache dir + */ + protected $cache_dir; + + /** + * @var string Cache key used for the parser + */ + protected $cache_key_parser; + + /** + * @var string Cache key used for the renderer + */ + protected $cache_key_renderer; + + /** + * @var \phpbb\config\config + */ + protected $config; + + /** + * @var array Custom tokens used in bbcode.html and their corresponding token from the definition + */ + protected $custom_tokens = array( + 'email' => array('{DESCRIPTION}' => '{TEXT}'), + 'flash' => array('{WIDTH}' => '{NUMBER1}', '{HEIGHT}' => '{NUMBER2}'), + 'img' => array('{URL}' => '{IMAGEURL}'), + 'list' => array('{LIST_TYPE}' => '{HASHMAP}'), + 'quote' => array('{USERNAME}' => '{TEXT1}'), + 'size' => array('{SIZE}' => '{FONTSIZE}'), + 'url' => array('{DESCRIPTION}' => '{TEXT}'), + ); + + /** + * @var \phpbb\textformatter\data_access + */ + protected $data_access; + + /** + * @var array Default BBCode definitions + */ + protected $default_definitions = array( + 'attachment' => '[ATTACHMENT index={NUMBER} filename={TEXT;useContent}]', + 'b' => '[B]{TEXT}[/B]', + 'code' => '[CODE lang={IDENTIFIER;optional}]{TEXT}[/CODE]', + 'color' => '[COLOR={COLOR}]{TEXT}[/COLOR]', + 'email' => '[EMAIL={EMAIL;useContent} subject={TEXT;optional;postFilter=rawurlencode} body={TEXT;optional;postFilter=rawurlencode}]{TEXT}[/EMAIL]', + 'flash' => '[FLASH={NUMBER1},{NUMBER2} width={NUMBER1;postFilter=#flashwidth} height={NUMBER2;postFilter=#flashheight} url={URL;useContent} /]', + 'i' => '[I]{TEXT}[/I]', + 'img' => '[IMG src={IMAGEURL;useContent}]', + 'list' => '[LIST type={HASHMAP=1:decimal,a:lower-alpha,A:upper-alpha,i:lower-roman,I:upper-roman;optional;postFilter=#simpletext} #createChild=LI]{TEXT}[/LIST]', + 'li' => '[* $tagName=LI]{TEXT}[/*]', + 'quote' => + "[QUOTE + author={TEXT1;optional} + post_id={UINT;optional} + post_url={URL;optional;postFilter=#false} + profile_url={URL;optional;postFilter=#false} + time={UINT;optional} + url={URL;optional} + user_id={UINT;optional} + author={PARSE=/^\\[url=(?'url'.*?)](?'author'.*)\\[\\/url]$/i} + author={PARSE=/^\\[url](?'author'(?'url'.*?))\\[\\/url]$/i} + author={PARSE=/(?'url'https?:\\/\\/[^[\\]]+)/i} + ]{TEXT2}[/QUOTE]", + 'size' => '[SIZE={FONTSIZE}]{TEXT}[/SIZE]', + 'u' => '[U]{TEXT}[/U]', + 'url' => '[URL={URL;useContent} $forceLookahead=true]{TEXT}[/URL]', + ); + + /** + * @var array Default templates, taken from bbcode::bbcode_tpl() + */ + protected $default_templates = array( + 'b' => '', + 'i' => '', + 'u' => '', + 'img' => '{L_IMAGE}', + 'size' => '', + 'color' => '', + 'email' => ' + + mailto: + + + ? + subject= + &body= + + + + ', + ); + + /** + * @var \phpbb\event\dispatcher_interface + */ + protected $dispatcher; + + /** + * Constructor + * + * @param \phpbb\textformatter\data_access $data_access + * @param \phpbb\cache\driver\driver_interface $cache + * @param \phpbb\event\dispatcher_interface $dispatcher + * @param \phpbb\config\config $config + * @param \phpbb\textformatter\s9e\link_helper $link_helper + * @param string $cache_dir Path to the cache dir + * @param string $cache_key_parser Cache key used for the parser + * @param string $cache_key_renderer Cache key used for the renderer + */ + public function __construct(\phpbb\textformatter\data_access $data_access, \phpbb\cache\driver\driver_interface $cache, \phpbb\event\dispatcher_interface $dispatcher, \phpbb\config\config $config, \phpbb\textformatter\s9e\link_helper $link_helper, $cache_dir, $cache_key_parser, $cache_key_renderer) + { + $this->link_helper = $link_helper; + $this->cache = $cache; + $this->cache_dir = $cache_dir; + $this->cache_key_parser = $cache_key_parser; + $this->cache_key_renderer = $cache_key_renderer; + $this->config = $config; + $this->data_access = $data_access; + $this->dispatcher = $dispatcher; + } + + /** + * {@inheritdoc} + */ + public function invalidate() + { + $this->regenerate(); + } + + /** + * {@inheritdoc} + * + * Will remove old renderers from the cache dir but won't touch the current renderer + */ + public function tidy() + { + // Get the name of current renderer + $renderer_data = $this->cache->get($this->cache_key_renderer); + $renderer_file = ($renderer_data) ? $renderer_data['class'] . '.php' : null; + + foreach (glob($this->cache_dir . 's9e_*') as $filename) + { + // Only remove the file if it's not the current renderer + if (!$renderer_file || substr($filename, -strlen($renderer_file)) !== $renderer_file) + { + unlink($filename); + } + } + } + + /** + * Generate and return a new configured instance of s9e\TextFormatter\Configurator + * + * @return Configurator + */ + public function get_configurator() + { + // Create a new Configurator + $configurator = new Configurator; + + /** + * Modify the s9e\TextFormatter configurator before the default settings are set + * + * @event core.text_formatter_s9e_configure_before + * @var \s9e\TextFormatter\Configurator configurator Configurator instance + * @since 3.2.0-a1 + */ + $vars = array('configurator'); + extract($this->dispatcher->trigger_event('core.text_formatter_s9e_configure_before', compact($vars))); + + // Reset the list of allowed schemes + foreach ($configurator->urlConfig->getAllowedSchemes() as $scheme) + { + $configurator->urlConfig->disallowScheme($scheme); + } + foreach (explode(',', $this->config['allowed_schemes_links']) as $scheme) + { + $configurator->urlConfig->allowScheme(trim($scheme)); + } + + // Convert newlines to br elements by default + $configurator->rootRules->enableAutoLineBreaks(); + + // Don't automatically ignore text in places where text is not allowed + $configurator->rulesGenerator->remove('IgnoreTextIfDisallowed'); + + // Don't remove comments and instead convert them to xsl:comment elements + $configurator->templateNormalizer->remove('RemoveComments'); + $configurator->templateNormalizer->add('TransposeComments'); + + // Set the rendering engine and configure it to save to the cache dir + $configurator->rendering->engine = 'PHP'; + $configurator->rendering->engine->cacheDir = $this->cache_dir; + $configurator->rendering->engine->defaultClassPrefix = 's9e_renderer_'; + $configurator->rendering->engine->enableQuickRenderer = true; + + // Create custom filters for BBCode tokens that are supported in phpBB but not in + // s9e\TextFormatter + $filter = new RegexpFilter('#^' . get_preg_expression('relative_url') . '$#Du'); + $configurator->attributeFilters->add('#local_url', $filter); + $configurator->attributeFilters->add('#relative_url', $filter); + + // INTTEXT regexp from acp_bbcodes + $filter = new RegexpFilter('!^([\p{L}\p{N}\-+,_. ]+)$!Du'); + $configurator->attributeFilters->add('#inttext', $filter); + + // Create custom filters for Flash restrictions, which use the same values as the image + // restrictions but have their own error message + $configurator->attributeFilters + ->add('#flashheight', __NAMESPACE__ . '\\parser::filter_flash_height') + ->addParameterByName('max_img_height') + ->addParameterByName('logger'); + + $configurator->attributeFilters + ->add('#flashwidth', __NAMESPACE__ . '\\parser::filter_flash_width') + ->addParameterByName('max_img_width') + ->addParameterByName('logger'); + + // Create a custom filter for phpBB's per-mode font size limits + $configurator->attributeFilters + ->add('#fontsize', __NAMESPACE__ . '\\parser::filter_font_size') + ->addParameterByName('max_font_size') + ->addParameterByName('logger') + ->markAsSafeInCSS(); + + // Create a custom filter for image URLs + $configurator->attributeFilters + ->add('#imageurl', __NAMESPACE__ . '\\parser::filter_img_url') + ->addParameterByName('urlConfig') + ->addParameterByName('logger') + ->addParameterByName('max_img_height') + ->addParameterByName('max_img_width') + ->markAsSafeAsURL(); + + // Add default BBCodes + foreach ($this->get_default_bbcodes($configurator) as $bbcode) + { + $configurator->BBCodes->addCustom($bbcode['usage'], $bbcode['template']); + } + if (isset($configurator->tags['QUOTE'])) + { + // Remove the nesting limit and let other services remove quotes at parsing time + $configurator->tags['QUOTE']->nestingLimit = PHP_INT_MAX; + } + + // Modify the template to disable images/flash depending on user's settings + foreach (array('FLASH', 'IMG') as $name) + { + $tag = $configurator->tags[$name]; + $tag->template = '' . $tag->template . ''; + } + + // Load custom BBCodes + foreach ($this->data_access->get_bbcodes() as $row) + { + // Insert the board's URL before {LOCAL_URL} tokens + $tpl = preg_replace_callback( + '#\\{LOCAL_URL\\d*\\}#', + function ($m) + { + return generate_board_url() . '/' . $m[0]; + }, + $row['bbcode_tpl'] + ); + + try + { + $configurator->BBCodes->addCustom($row['bbcode_match'], new UnsafeTemplate($tpl)); + } + catch (\Exception $e) + { + /** + * @todo log an error? + */ + } + } + + // Load smilies + foreach ($this->data_access->get_smilies() as $row) + { + $configurator->Emoticons->set( + $row['code'], + '{.}' + ); + } + + if (isset($configurator->Emoticons)) + { + // Force emoticons to be rendered as text if $S_VIEWSMILIES is not set + $configurator->Emoticons->notIfCondition = 'not($S_VIEWSMILIES)'; + + // Only parse emoticons at the beginning of the text or if they're preceded by any + // one of: a new line, a space, a dot, or a right square bracket + $configurator->Emoticons->notAfter = '[^\\n .\\]]'; + + // Ignore emoticons that are immediately followed by a "word" character + $configurator->Emoticons->notBefore = '\\w'; + } + + // Load the censored words + $censor = $this->data_access->get_censored_words(); + if (!empty($censor)) + { + // Use a namespaced tag to avoid collisions + $configurator->plugins->load('Censor', array('tagName' => 'censor:tag')); + foreach ($censor as $row) + { + $configurator->Censor->add($row['word'], $row['replacement']); + } + } + + // Load the magic links plugins. We do that after BBCodes so that they use the same tags + $this->configure_autolink($configurator); + + // Register some vars with a default value. Those should be set at runtime by whatever calls + // the parser + $configurator->registeredVars['max_font_size'] = 0; + $configurator->registeredVars['max_img_height'] = 0; + $configurator->registeredVars['max_img_width'] = 0; + + // Load the Emoji plugin and modify its tag's template to obey viewsmilies + $configurator->Emoji->omitImageSize(); + $configurator->Emoji->useSVG(); + $tag = $configurator->Emoji->getTag(); + $tag->template = '' . str_replace('class="emoji"', 'class="emoji smilies"', $tag->template) . ''; + + /** + * Modify the s9e\TextFormatter configurator after the default settings are set + * + * @event core.text_formatter_s9e_configure_after + * @var \s9e\TextFormatter\Configurator configurator Configurator instance + * @since 3.2.0-a1 + */ + $vars = array('configurator'); + extract($this->dispatcher->trigger_event('core.text_formatter_s9e_configure_after', compact($vars))); + + return $configurator; + } + + /** + * Regenerate and cache a new parser and renderer + * + * @return array Associative array with at least two elements: "parser" and "renderer" + */ + public function regenerate() + { + $configurator = $this->get_configurator(); + + // Get the censor helper and remove the Censor plugin if applicable + if (isset($configurator->Censor)) + { + $censor = $configurator->Censor->getHelper(); + unset($configurator->Censor); + unset($configurator->tags['censor:tag']); + } + + $objects = $configurator->finalize(); + + /** + * Access the objects returned by finalize() before they are saved to cache + * + * @event core.text_formatter_s9e_configure_finalize + * @var array objects Array containing a "parser" object, a "renderer" object and optionally a "js" string + * @since 3.2.2-RC1 + */ + $vars = array('objects'); + extract($this->dispatcher->trigger_event('core.text_formatter_s9e_configure_finalize', compact($vars))); + + $parser = $objects['parser']; + $renderer = $objects['renderer']; + + // Cache the parser as-is + $this->cache->put($this->cache_key_parser, $parser); + + // We need to cache the name of the renderer's generated class + $renderer_data = array('class' => get_class($renderer)); + if (isset($censor)) + { + $renderer_data['censor'] = $censor; + } + $this->cache->put($this->cache_key_renderer, $renderer_data); + + return array('parser' => $parser, 'renderer' => $renderer); + } + + /** + * Configure the Autolink / Autoemail plugins used to linkify text + * + * @param \s9e\TextFormatter\Configurator $configurator + * @return void + */ + protected function configure_autolink(Configurator $configurator) + { + $configurator->plugins->load('Autoemail'); + $configurator->plugins->load('Autolink', array('matchWww' => true)); + + // Add a tag filter that creates a tag that stores and replace the + // content of a link created by the Autolink plugin + $configurator->Autolink->getTag()->filterChain + ->add(array($this->link_helper, 'generate_link_text_tag')) + ->resetParameters() + ->addParameterByName('tag') + ->addParameterByName('parser'); + + // Create a tag that will be used to display the truncated text by + // replacing the original content with the content of the @text attribute + $tag = $configurator->tags->add('LINK_TEXT'); + $tag->attributes->add('text'); + $tag->template = ''; + + $tag->filterChain + ->add(array($this->link_helper, 'truncate_local_url')) + ->resetParameters() + ->addParameterByName('tag') + ->addParameterByValue(generate_board_url() . '/'); + $tag->filterChain + ->add(array($this->link_helper, 'truncate_text')) + ->resetParameters() + ->addParameterByName('tag'); + $tag->filterChain + ->add(array($this->link_helper, 'cleanup_tag')) + ->resetParameters() + ->addParameterByName('tag') + ->addParameterByName('parser'); + } + + /** + * Escape a literal to be used in an HTML attribute in an XSL template + * + * Escapes "HTML special chars" for obvious reasons and curly braces to avoid them + * being interpreted as an attribute value template + * + * @param string $value Original string + * @return string Escaped string + */ + protected function escape_html_attribute($value) + { + return htmlspecialchars(strtr($value, ['{' => '{{', '}' => '}}']), ENT_COMPAT | ENT_XML1, 'UTF-8'); + } + + /** + * Return the default BBCodes configuration + * + * @return array 2D array. Each element has a 'usage' key, a 'template' key, and an optional 'options' key + */ + protected function get_default_bbcodes($configurator) + { + // For each BBCode, build an associative array matching style_ids to their template + $templates = array(); + foreach ($this->data_access->get_styles_templates() as $style_id => $data) + { + foreach ($this->extract_templates($data['template']) as $bbcode_name => $template) + { + $templates[$bbcode_name][$style_id] = $template; + } + + // Add default templates wherever missing, or for BBCodes that were not specified in + // this template's bitfield. For instance, prosilver has a custom template for b but its + // bitfield does not enable it so the default template is used instead + foreach ($this->default_templates as $bbcode_name => $template) + { + if (!isset($templates[$bbcode_name][$style_id]) || !in_array($bbcode_name, $data['bbcodes'], true)) + { + $templates[$bbcode_name][$style_id] = $template; + } + } + } + + // Replace custom tokens and normalize templates + foreach ($templates as $bbcode_name => $style_templates) + { + foreach ($style_templates as $i => $template) + { + if (isset($this->custom_tokens[$bbcode_name])) + { + $template = strtr($template, $this->custom_tokens[$bbcode_name]); + } + + $templates[$bbcode_name][$i] = $configurator->templateNormalizer->normalizeTemplate($template); + } + } + + $bbcodes = array(); + foreach ($this->default_definitions as $bbcode_name => $usage) + { + $bbcodes[$bbcode_name] = array( + 'usage' => $usage, + 'template' => $this->merge_templates($templates[$bbcode_name]), + ); + } + + return $bbcodes; + } + + /** + * Extract and recompose individual BBCode templates from a style's template file + * + * @param string $template Style template (bbcode.html) + * @return array Associative array matching BBCode names to their template + */ + protected function extract_templates($template) + { + // Capture the template fragments + // Allow either phpBB template or the Twig syntax + preg_match_all('#(.*?)#s', $template, $matches, PREG_SET_ORDER) ?: + preg_match_all('#{% for (.*?) in .*? %}(.*?){% endfor %}#s', $template, $matches, PREG_SET_ORDER); + + $fragments = array(); + foreach ($matches as $match) + { + // Normalize the whitespace + $fragment = preg_replace('#>\\n\\t*<#', '><', trim($match[2])); + + $fragments[$match[1]] = $fragment; + } + + // Automatically recompose templates split between *_open and *_close + foreach ($fragments as $fragment_name => $fragment) + { + if (preg_match('#^(\\w+)_close$#', $fragment_name, $match)) + { + $bbcode_name = $match[1]; + + if (isset($fragments[$bbcode_name . '_open'])) + { + $templates[$bbcode_name] = $fragments[$bbcode_name . '_open'] . '' . $fragment; + } + } + } + + // Manually recompose and overwrite irregular templates + $templates['list'] = + ' + + ' . $fragments['ulist_open_default'] . '' . $fragments['ulist_close'] . ' + + + ' . $fragments['olist_open'] . '' . $fragments['olist_close'] . ' + + + ' . $fragments['ulist_open'] . '' . $fragments['ulist_close'] . ' + + '; + + $templates['li'] = $fragments['listitem'] . '' . $fragments['listitem_close']; + + // Replace the regular quote template with the extended quote template if available + if (isset($fragments['quote_extended'])) + { + $templates['quote'] = $fragments['quote_extended']; + } + + // The [attachment] BBCode uses the inline_attachment template to output a comment that + // is post-processed by parse_attachments() + $templates['attachment'] = $fragments['inline_attachment_open'] . ' ia ia ' . $fragments['inline_attachment_close']; + + // Add fragments as templates + foreach ($fragments as $fragment_name => $fragment) + { + if (preg_match('#^\\w+$#', $fragment_name)) + { + $templates[$fragment_name] = $fragment; + } + } + + // Keep only templates that are named after an existing BBCode + $templates = array_intersect_key($templates, $this->default_definitions); + + return $templates; + } + + /** + * Merge the templates from any number of styles into one BBCode template + * + * When multiple templates are available for the same BBCode (because of multiple styles) we + * merge them into a single template that uses an xsl:choose construct that determines which + * style to use at rendering time. + * + * @param array $style_templates Associative array matching style_ids to their template + * @return string + */ + protected function merge_templates(array $style_templates) + { + // Return the template as-is if there's only one style or all styles share the same template + if (count(array_unique($style_templates)) === 1) + { + return end($style_templates); + } + + // Group identical templates together + $grouped_templates = array(); + foreach ($style_templates as $style_id => $style_template) + { + $grouped_templates[$style_template][] = '$STYLE_ID=' . $style_id; + } + + // Sort templates by frequency descending + $templates_cnt = array_map('sizeof', $grouped_templates); + array_multisort($grouped_templates, $templates_cnt); + + // Remove the most frequent template from the list; It becomes the default + reset($grouped_templates); + $default_template = key($grouped_templates); + unset($grouped_templates[$default_template]); + + // Build an xsl:choose switch + $template = ''; + foreach ($grouped_templates as $style_template => $exprs) + { + $template .= '' . $style_template . ''; + } + $template .= '' . $default_template . ''; + + return $template; + } +} diff --git a/phpBB/phpbb/textformatter/s9e/link_helper.php b/phpBB/phpbb/textformatter/s9e/link_helper.php new file mode 100644 index 0000000000..0f44603dec --- /dev/null +++ b/phpBB/phpbb/textformatter/s9e/link_helper.php @@ -0,0 +1,118 @@ + +* @license GNU General Public License, version 2 (GPL-2.0) +* +* For full copyright and license information, please see +* the docs/CREDITS.txt file. +* +*/ + +namespace phpbb\textformatter\s9e; + +class link_helper +{ + /** + * Clean up and invalidate a LINK_TEXT tag if applicable + * + * Will invalidate the tag if its replacement text is the same as the original + * text and would have no visible effect + * + * @param \s9e\TextFormatter\Parser\Tag $tag LINK_TEXT tag + * @param \s9e\TextFormatter\Parser $parser Parser + * @return bool Whether the tag is valid + */ + public function cleanup_tag(\s9e\TextFormatter\Parser\Tag $tag, \s9e\TextFormatter\Parser $parser) + { + // Invalidate if the content of the tag matches the text attribute + $text = substr($parser->getText(), $tag->getPos(), $tag->getLen()); + + return ($text !== $tag->getAttribute('text')); + } + + /** + * Create a LINK_TEXT tag inside of a link + * + * Meant to only apply to linkified URLs and [url] BBCodes without a parameter + * + * @param \s9e\TextFormatter\Parser\Tag $tag URL tag (start tag) + * @param \s9e\TextFormatter\Parser $parser Parser + * @return bool Always true to indicate that the tag is valid + */ + public function generate_link_text_tag(\s9e\TextFormatter\Parser\Tag $tag, \s9e\TextFormatter\Parser $parser) + { + // Only create a LINK_TEXT tag if the start tag is paired with an end + // tag, which is the case with tags from the Autolink plugins and with + // the [url] BBCode when its content is used for the URL + if (!$tag->getEndTag() || !$this->should_shorten($tag, $parser->getText())) + { + return true; + } + + // Capture the text between the start tag and its end tag + $start = $tag->getPos() + $tag->getLen(); + $end = $tag->getEndTag()->getPos(); + $length = $end - $start; + $text = substr($parser->getText(), $start, $length); + + // Create a tag that consumes the link's text + $parser->addSelfClosingTag('LINK_TEXT', $start, $length)->setAttribute('text', $text); + + return true; + } + + /** + * Test whether we should shorten this tag's text + * + * Will test whether the tag either does not use any markup or uses a single + * [url] BBCode + * + * @param \s9e\TextFormatter\Parser\Tag $tag URL tag + * @param string $text Original text + * @return bool + */ + protected function should_shorten(\s9e\TextFormatter\Parser\Tag $tag, $text) + { + return ($tag->getLen() === 0 || strtolower(substr($text, $tag->getPos(), $tag->getLen())) === '[url]'); + } + + /** + * Remove the board's root URL from a the start of a string + * + * @param \s9e\TextFormatter\Parser\Tag $tag LINK_TEXT tag + * @param string $board_url Forum's root URL (with trailing slash) + * @return bool Always true to indicate that the tag is valid + */ + public function truncate_local_url(\s9e\TextFormatter\Parser\Tag $tag, $board_url) + { + $text = $tag->getAttribute('text'); + if (stripos($text, $board_url) === 0 && strlen($text) > strlen($board_url)) + { + $tag->setAttribute('text', substr($text, strlen($board_url))); + } + + return true; + } + + /** + * Truncate the replacement text set in a LINK_TEXT tag + * + * @param \s9e\TextFormatter\Parser\Tag $tag LINK_TEXT tag + * @return bool Always true to indicate that the tag is valid + */ + public function truncate_text(\s9e\TextFormatter\Parser\Tag $tag) + { + $text = $tag->getAttribute('text'); + if (utf8_strlen($text) > 55) + { + $text = utf8_substr($text, 0, 39) . ' ... ' . utf8_substr($text, -10); + } + + $tag->setAttribute('text', $text); + + return true; + } +} diff --git a/phpBB/phpbb/textformatter/s9e/parser.php b/phpBB/phpbb/textformatter/s9e/parser.php new file mode 100644 index 0000000000..05ddfffa11 --- /dev/null +++ b/phpBB/phpbb/textformatter/s9e/parser.php @@ -0,0 +1,399 @@ + +* @license GNU General Public License, version 2 (GPL-2.0) +* +* For full copyright and license information, please see +* the docs/CREDITS.txt file. +* +*/ + +namespace phpbb\textformatter\s9e; + +use s9e\TextFormatter\Parser\BuiltInFilters; +use s9e\TextFormatter\Parser\Logger; + +/** +* s9e\TextFormatter\Parser adapter +*/ +class parser implements \phpbb\textformatter\parser_interface +{ + /** + * @var \phpbb\event\dispatcher_interface + */ + protected $dispatcher; + + /** + * @var \s9e\TextFormatter\Parser + */ + protected $parser; + + /** + * Constructor + * + * @param \phpbb\cache\driver_interface $cache + * @param string $key Cache key + * @param factory $factory + * @param \phpbb\event\dispatcher_interface $dispatcher + */ + public function __construct(\phpbb\cache\driver\driver_interface $cache, $key, factory $factory, \phpbb\event\dispatcher_interface $dispatcher) + { + $parser = $cache->get($key); + if (!$parser) + { + $objects = $factory->regenerate(); + $parser = $objects['parser']; + } + + $this->dispatcher = $dispatcher; + $this->parser = $parser; + + $parser = $this; + + /** + * Configure the parser service + * + * Can be used to: + * - toggle features or BBCodes + * - register variables or custom parsers in the s9e\TextFormatter parser + * - configure the s9e\TextFormatter parser's runtime settings + * + * @event core.text_formatter_s9e_parser_setup + * @var \phpbb\textformatter\s9e\parser parser This parser service + * @since 3.2.0-a1 + */ + $vars = array('parser'); + extract($dispatcher->trigger_event('core.text_formatter_s9e_parser_setup', compact($vars))); + } + + /** + * {@inheritdoc} + */ + public function parse($text) + { + $parser = $this; + + /** + * Modify a text before it is parsed + * + * @event core.text_formatter_s9e_parse_before + * @var \phpbb\textformatter\s9e\parser parser This parser service + * @var string text The original text + * @since 3.2.0-a1 + */ + $vars = array('parser', 'text'); + extract($this->dispatcher->trigger_event('core.text_formatter_s9e_parse_before', compact($vars))); + + $xml = $this->parser->parse($text); + + /** + * Modify a parsed text in its XML form + * + * @event core.text_formatter_s9e_parse_after + * @var \phpbb\textformatter\s9e\parser parser This parser service + * @var string xml The parsed text, in XML + * @since 3.2.0-a1 + */ + $vars = array('parser', 'xml'); + extract($this->dispatcher->trigger_event('core.text_formatter_s9e_parse_after', compact($vars))); + + return $xml; + } + + /** + * {@inheritdoc} + */ + public function disable_bbcode($name) + { + $this->parser->disableTag(strtoupper($name)); + } + + /** + * {@inheritdoc} + */ + public function disable_bbcodes() + { + $this->parser->disablePlugin('BBCodes'); + } + + /** + * {@inheritdoc} + */ + public function disable_censor() + { + $this->parser->disablePlugin('Censor'); + } + + /** + * {@inheritdoc} + */ + public function disable_magic_url() + { + $this->parser->disablePlugin('Autoemail'); + $this->parser->disablePlugin('Autolink'); + } + + /** + * {@inheritdoc} + */ + public function disable_smilies() + { + $this->parser->disablePlugin('Emoticons'); + $this->parser->disablePlugin('Emoji'); + } + + /** + * {@inheritdoc} + */ + public function enable_bbcode($name) + { + $this->parser->enableTag(strtoupper($name)); + } + + /** + * {@inheritdoc} + */ + public function enable_bbcodes() + { + $this->parser->enablePlugin('BBCodes'); + } + + /** + * {@inheritdoc} + */ + public function enable_censor() + { + $this->parser->enablePlugin('Censor'); + } + + /** + * {@inheritdoc} + */ + public function enable_magic_url() + { + $this->parser->enablePlugin('Autoemail'); + $this->parser->enablePlugin('Autolink'); + } + + /** + * {@inheritdoc} + */ + public function enable_smilies() + { + $this->parser->enablePlugin('Emoticons'); + $this->parser->enablePlugin('Emoji'); + } + + /** + * {@inheritdoc} + * + * This will convert the log entries found in s9e\TextFormatter's logger into phpBB error + * messages + */ + public function get_errors() + { + $errors = array(); + foreach ($this->parser->getLogger()->get() as $entry) + { + list(, $msg, $context) = $entry; + + if ($msg === 'Tag limit exceeded') + { + if ($context['tagName'] === 'E') + { + $errors[] = array('TOO_MANY_SMILIES', $context['tagLimit']); + } + else if ($context['tagName'] === 'URL') + { + $errors[] = array('TOO_MANY_URLS', $context['tagLimit']); + } + } + else if ($msg === 'MAX_FONT_SIZE_EXCEEDED') + { + $errors[] = array($msg, $context['max_size']); + } + else if (preg_match('/^MAX_(?:FLASH|IMG)_(HEIGHT|WIDTH)_EXCEEDED$/D', $msg, $m)) + { + $errors[] = array($msg, $context['max_' . strtolower($m[1])]); + } + else if ($msg === 'Tag is disabled') + { + $name = strtolower($context['tag']->getName()); + $errors[] = array('UNAUTHORISED_BBCODE', '[' . $name . ']'); + } + else if ($msg === 'UNABLE_GET_IMAGE_SIZE') + { + $errors[] = array($msg); + } + } + + // Deduplicate error messages. array_unique() only works on strings so we have to serialize + if (!empty($errors)) + { + $errors = array_map('unserialize', array_unique(array_map('serialize', $errors))); + } + + return $errors; + } + + /** + * Return the instance of s9e\TextFormatter\Parser used by this object + * + * @return \s9e\TextFormatter\Parser + */ + public function get_parser() + { + return $this->parser; + } + + /** + * {@inheritdoc} + */ + public function set_var($name, $value) + { + if ($name === 'max_smilies') + { + $this->parser->setTagLimit('E', $value ?: PHP_INT_MAX); + } + else if ($name === 'max_urls') + { + $this->parser->setTagLimit('URL', $value ?: PHP_INT_MAX); + } + else + { + $this->parser->registeredVars[$name] = $value; + } + } + + /** + * {@inheritdoc} + */ + public function set_vars(array $vars) + { + foreach ($vars as $name => $value) + { + $this->set_var($name, $value); + } + } + + /** + * Filter a flash object's height + * + * @see bbcode_firstpass::bbcode_flash() + * + * @param string $height + * @param integer $max_height + * @param Logger $logger + * @return mixed Original value if valid, FALSE otherwise + */ + static public function filter_flash_height($height, $max_height, Logger $logger) + { + if ($max_height && $height > $max_height) + { + $logger->err('MAX_FLASH_HEIGHT_EXCEEDED', array('max_height' => $max_height)); + + return false; + } + + return $height; + } + + /** + * Filter a flash object's width + * + * @see bbcode_firstpass::bbcode_flash() + * + * @param string $width + * @param integer $max_width + * @param Logger $logger + * @return mixed Original value if valid, FALSE otherwise + */ + static public function filter_flash_width($width, $max_width, Logger $logger) + { + if ($max_width && $width > $max_width) + { + $logger->err('MAX_FLASH_WIDTH_EXCEEDED', array('max_width' => $max_width)); + + return false; + } + + return $width; + } + + /** + * Filter the value used in a [size] BBCode + * + * @see bbcode_firstpass::bbcode_size() + * + * @param string $size Original size + * @param integer $max_size Maximum allowed size + * @param Logger $logger + * @return mixed Original value if valid, FALSE otherwise + */ + static public function filter_font_size($size, $max_size, Logger $logger) + { + if ($max_size && $size > $max_size) + { + $logger->err('MAX_FONT_SIZE_EXCEEDED', array('max_size' => $max_size)); + + return false; + } + + if ($size < 1) + { + return false; + } + + return $size; + } + + /** + * Filter an image's URL to enforce restrictions on its dimensions + * + * @see bbcode_firstpass::bbcode_img() + * + * @param string $url Original URL + * @param array $url_config Config used by the URL filter + * @param Logger $logger + * @param integer $max_height Maximum height allowed + * @param integer $max_width Maximum width allowed + * @return string|bool Original value if valid, FALSE otherwise + */ + static public function filter_img_url($url, array $url_config, Logger $logger, $max_height, $max_width) + { + // Validate the URL + $url = BuiltInFilters::filterUrl($url, $url_config, $logger); + if ($url === false) + { + return false; + } + + if ($max_height || $max_width) + { + $imagesize = new \FastImageSize\FastImageSize(); + $size_info = $imagesize->getImageSize($url); + if ($size_info === false) + { + $logger->err('UNABLE_GET_IMAGE_SIZE'); + return false; + } + + if ($max_height && $max_height < $size_info['height']) + { + $logger->err('MAX_IMG_HEIGHT_EXCEEDED', array('max_height' => $max_height)); + return false; + } + + if ($max_width && $max_width < $size_info['width']) + { + $logger->err('MAX_IMG_WIDTH_EXCEEDED', array('max_width' => $max_width)); + return false; + } + } + + return $url; + } +} diff --git a/phpBB/phpbb/textformatter/s9e/quote_helper.php b/phpBB/phpbb/textformatter/s9e/quote_helper.php new file mode 100644 index 0000000000..86c33c7591 --- /dev/null +++ b/phpBB/phpbb/textformatter/s9e/quote_helper.php @@ -0,0 +1,81 @@ + +* @license GNU General Public License, version 2 (GPL-2.0) +* +* For full copyright and license information, please see +* the docs/CREDITS.txt file. +* +*/ + +namespace phpbb\textformatter\s9e; + +class quote_helper +{ + /** + * @var string Base URL for a post link, uses {POST_ID} as placeholder + */ + protected $post_url; + + /** + * @var string Base URL for a profile link, uses {USER_ID} as placeholder + */ + protected $profile_url; + + /** + * @var \phpbb\user + */ + protected $user; + + /** + * Constructor + * + * @param \phpbb\user $user + * @param string $root_path + * @param string $php_ext + */ + public function __construct(\phpbb\user $user, $root_path, $php_ext) + { + $this->post_url = append_sid($root_path . 'viewtopic.' . $php_ext, 'p={POST_ID}#p{POST_ID}', false); + $this->profile_url = append_sid($root_path . 'memberlist.' . $php_ext, 'mode=viewprofile&u={USER_ID}', false); + $this->user = $user; + } + + /** + * Inject dynamic metadata into QUOTE tags in given XML + * + * @param string $xml Original XML + * @return string Modified XML + */ + public function inject_metadata($xml) + { + $post_url = $this->post_url; + $profile_url = $this->profile_url; + $user = $this->user; + + return \s9e\TextFormatter\Utils::replaceAttributes( + $xml, + 'QUOTE', + function ($attributes) use ($post_url, $profile_url, $user) + { + if (isset($attributes['post_id'])) + { + $attributes['post_url'] = str_replace('{POST_ID}', $attributes['post_id'], $post_url); + } + if (isset($attributes['time'])) + { + $attributes['date'] = $user->format_date($attributes['time']); + } + if (isset($attributes['user_id'])) + { + $attributes['profile_url'] = str_replace('{USER_ID}', $attributes['user_id'], $profile_url); + } + + return $attributes; + } + ); + } +} diff --git a/phpBB/phpbb/textformatter/s9e/renderer.php b/phpBB/phpbb/textformatter/s9e/renderer.php new file mode 100644 index 0000000000..6fcd2b0a98 --- /dev/null +++ b/phpBB/phpbb/textformatter/s9e/renderer.php @@ -0,0 +1,313 @@ + +* @license GNU General Public License, version 2 (GPL-2.0) +* +* For full copyright and license information, please see +* the docs/CREDITS.txt file. +* +*/ + +namespace phpbb\textformatter\s9e; + +/** +* s9e\TextFormatter\Renderer adapter +*/ +class renderer implements \phpbb\textformatter\renderer_interface +{ + /** + * @var \s9e\TextFormatter\Plugins\Censor\Helper + */ + protected $censor; + + /** + * @var \phpbb\event\dispatcher_interface + */ + protected $dispatcher; + + /** + * @var quote_helper + */ + protected $quote_helper; + + /** + * @var \s9e\TextFormatter\Renderer + */ + protected $renderer; + + /** + * @var bool Status of the viewcensors option + */ + protected $viewcensors = false; + + /** + * @var bool Status of the viewflash option + */ + protected $viewflash = false; + + /** + * @var bool Status of the viewimg option + */ + protected $viewimg = false; + + /** + * @var bool Status of the viewsmilies option + */ + protected $viewsmilies = false; + + /** + * Constructor + * + * @param \phpbb\cache\driver\driver_interface $cache + * @param string $cache_dir Path to the cache dir + * @param string $key Cache key + * @param factory $factory + * @param \phpbb\event\dispatcher_interface $dispatcher + */ + public function __construct(\phpbb\cache\driver\driver_interface $cache, $cache_dir, $key, factory $factory, \phpbb\event\dispatcher_interface $dispatcher) + { + $renderer_data = $cache->get($key); + if ($renderer_data) + { + $class = $renderer_data['class']; + if (!class_exists($class, false)) + { + // Try to load the renderer class from its cache file + $cache_file = $cache_dir . $class . '.php'; + + if (file_exists($cache_file)) + { + include($cache_file); + } + } + if (class_exists($class, false)) + { + $renderer = new $class; + } + if (isset($renderer_data['censor'])) + { + $censor = $renderer_data['censor']; + } + } + if (!isset($renderer)) + { + $objects = $factory->regenerate(); + $renderer = $objects['renderer']; + } + + if (isset($censor)) + { + $this->censor = $censor; + } + $this->dispatcher = $dispatcher; + $this->renderer = $renderer; + $renderer = $this; + + /** + * Configure the renderer service + * + * @event core.text_formatter_s9e_renderer_setup + * @var \phpbb\textformatter\s9e\renderer renderer This renderer service + * @since 3.2.0-a1 + */ + $vars = array('renderer'); + extract($dispatcher->trigger_event('core.text_formatter_s9e_renderer_setup', compact($vars))); + } + + /** + * Configure the quote_helper object used to display extended information in quotes + * + * @param quote_helper $quote_helper + */ + public function configure_quote_helper(quote_helper $quote_helper) + { + $this->quote_helper = $quote_helper; + } + + /** + * Automatically set the smilies path based on config + * + * @param \phpbb\config\config $config + * @param \phpbb\path_helper $path_helper + * @return null + */ + public function configure_smilies_path(\phpbb\config\config $config, \phpbb\path_helper $path_helper) + { + /** + * @see smiley_text() + */ + $root_path = (defined('PHPBB_USE_BOARD_URL_PATH') && PHPBB_USE_BOARD_URL_PATH) ? generate_board_url() . '/' : $path_helper->get_web_root_path(); + + $this->set_smilies_path($root_path . $config['smilies_path']); + } + + /** + * Configure this renderer as per the user's settings + * + * Should set the locale as well as the viewcensor/viewflash/viewimg/viewsmilies options. + * + * @param \phpbb\user $user + * @param \phpbb\config\config $config + * @param \phpbb\auth\auth $auth + * @return null + */ + public function configure_user(\phpbb\user $user, \phpbb\config\config $config, \phpbb\auth\auth $auth) + { + $censor = $user->optionget('viewcensors') || !$config['allow_nocensors'] || !$auth->acl_get('u_chgcensors'); + + $this->set_viewcensors($censor); + $this->set_viewflash($user->optionget('viewflash')); + $this->set_viewimg($user->optionget('viewimg')); + $this->set_viewsmilies($user->optionget('viewsmilies')); + + // Set the stylesheet parameters + foreach (array_keys($this->renderer->getParameters()) as $param_name) + { + if (strpos($param_name, 'L_') === 0) + { + // L_FOO is set to $user->lang('FOO') + $this->renderer->setParameter($param_name, $user->lang(substr($param_name, 2))); + } + } + + // Set this user's style id and other parameters + $this->renderer->setParameters(array( + 'S_IS_BOT' => $user->data['is_bot'], + 'S_REGISTERED_USER' => $user->data['is_registered'], + 'S_USER_LOGGED_IN' => ($user->data['user_id'] != ANONYMOUS), + 'STYLE_ID' => $user->style['style_id'], + )); + } + + /** + * Return the instance of s9e\TextFormatter\Renderer used by this object + * + * @return \s9e\TextFormatter\Renderer + */ + public function get_renderer() + { + return $this->renderer; + } + + /** + * {@inheritdoc} + */ + public function get_viewcensors() + { + return $this->viewcensors; + } + + /** + * {@inheritdoc} + */ + public function get_viewflash() + { + return $this->viewflash; + } + + /** + * {@inheritdoc} + */ + public function get_viewimg() + { + return $this->viewimg; + } + + /** + * {@inheritdoc} + */ + public function get_viewsmilies() + { + return $this->viewsmilies; + } + + /** + * {@inheritdoc} + */ + public function render($xml) + { + if (isset($this->quote_helper)) + { + $xml = $this->quote_helper->inject_metadata($xml); + } + + $renderer = $this; + + /** + * Modify a parsed text before it is rendered + * + * @event core.text_formatter_s9e_render_before + * @var \phpbb\textformatter\s9e\renderer renderer This renderer service + * @var string xml The parsed text, in its XML form + * @since 3.2.0-a1 + */ + $vars = array('renderer', 'xml'); + extract($this->dispatcher->trigger_event('core.text_formatter_s9e_render_before', compact($vars))); + + $html = $this->renderer->render($xml); + if (isset($this->censor) && $this->viewcensors) + { + $html = $this->censor->censorHtml($html, true); + } + + /** + * Modify a rendered text + * + * @event core.text_formatter_s9e_render_after + * @var string html The rendered text's HTML + * @var \phpbb\textformatter\s9e\renderer renderer This renderer service + * @since 3.2.0-a1 + */ + $vars = array('html', 'renderer'); + extract($this->dispatcher->trigger_event('core.text_formatter_s9e_render_after', compact($vars))); + + return $html; + } + + /** + * {@inheritdoc} + */ + public function set_smilies_path($path) + { + $this->renderer->setParameter('T_SMILIES_PATH', $path); + } + + /** + * {@inheritdoc} + */ + public function set_viewcensors($value) + { + $this->viewcensors = $value; + $this->renderer->setParameter('S_VIEWCENSORS', $value); + } + + /** + * {@inheritdoc} + */ + public function set_viewflash($value) + { + $this->viewflash = $value; + $this->renderer->setParameter('S_VIEWFLASH', $value); + } + + /** + * {@inheritdoc} + */ + public function set_viewimg($value) + { + $this->viewimg = $value; + $this->renderer->setParameter('S_VIEWIMG', $value); + } + + /** + * {@inheritdoc} + */ + public function set_viewsmilies($value) + { + $this->viewsmilies = $value; + $this->renderer->setParameter('S_VIEWSMILIES', $value); + } +} diff --git a/phpBB/phpbb/textformatter/s9e/utils.php b/phpBB/phpbb/textformatter/s9e/utils.php new file mode 100644 index 0000000000..a9a6d4b892 --- /dev/null +++ b/phpBB/phpbb/textformatter/s9e/utils.php @@ -0,0 +1,152 @@ + +* @license GNU General Public License, version 2 (GPL-2.0) +* +* For full copyright and license information, please see +* the docs/CREDITS.txt file. +* +*/ + +namespace phpbb\textformatter\s9e; + +/** +* Text manipulation utilities +*/ +class utils implements \phpbb\textformatter\utils_interface +{ + /** + * Replace BBCodes and other formatting elements with whitespace + * + * NOTE: preserves smilies as text + * + * @param string $xml Parsed text + * @return string Plain text + */ + public function clean_formatting($xml) + { + // Insert a space before and then remove formatting + $xml = preg_replace('#<[es]>#', ' $0', $xml); + + return \s9e\TextFormatter\Utils::removeFormatting($xml); + } + + /** + * Format given string to be used as an attribute value + * + * Will return the string as-is if it can be used in a BBCode without quotes. Otherwise, + * it will use either single- or double- quotes depending on whichever requires less escaping. + * Quotes and backslashes are escaped with backslashes where necessary + * + * @param string $str Original string + * @return string Same string if possible, escaped string within quotes otherwise + */ + protected function format_attribute_value($str) + { + if (!preg_match('/[ "\'\\\\\\]]/', $str)) + { + // Return as-is if it contains none of: space, ' " \ or ] + return $str; + } + $singleQuoted = "'" . addcslashes($str, "\\'") . "'"; + $doubleQuoted = '"' . addcslashes($str, '\\"') . '"'; + + return (strlen($singleQuoted) < strlen($doubleQuoted)) ? $singleQuoted : $doubleQuoted; + } + + /** + * {@inheritdoc} + */ + public function generate_quote($text, array $attributes = array()) + { + $text = trim($text); + $quote = '[quote'; + if (isset($attributes['author'])) + { + // Add the author as the BBCode's default attribute + $quote .= '=' . $this->format_attribute_value($attributes['author']); + unset($attributes['author']); + } + + if (isset($attributes['user_id']) && $attributes['user_id'] == ANONYMOUS) + { + unset($attributes['user_id']); + } + + ksort($attributes); + foreach ($attributes as $name => $value) + { + $quote .= ' ' . $name . '=' . $this->format_attribute_value($value); + } + $quote .= ']'; + $newline = (strlen($quote . $text . '[/quote]') > 80 || strpos($text, "\n") !== false) ? "\n" : ''; + $quote .= $newline . $text . $newline . '[/quote]'; + + return $quote; + } + + /** + * Get a list of quote authors, limited to the outermost quotes + * + * @param string $xml Parsed text + * @return string[] List of authors + */ + public function get_outermost_quote_authors($xml) + { + $authors = array(); + if (strpos($xml, 'loadXML($xml); + $xpath = new \DOMXPath($dom); + foreach ($xpath->query('//QUOTE[not(ancestor::QUOTE)]/@author') as $author) + { + $authors[] = $author->textContent; + } + + return $authors; + } + + /** + * Remove given BBCode and its content, at given nesting depth + * + * @param string $xml Parsed text + * @param string $bbcode_name BBCode's name + * @param integer $depth Minimum nesting depth (number of parents of the same name) + * @return string Parsed text + */ + public function remove_bbcode($xml, $bbcode_name, $depth = 0) + { + return \s9e\TextFormatter\Utils::removeTag($xml, strtoupper($bbcode_name), $depth); + } + + /** + * Return a parsed text to its original form + * + * @param string $xml Parsed text + * @return string Original plain text + */ + public function unparse($xml) + { + return \s9e\TextFormatter\Unparser::unparse($xml); + } + + /** + * {@inheritdoc} + */ + public function is_empty($text) + { + if ($text === null || $text === '') + { + return true; + } + + return trim($this->unparse($text)) === ''; + } +} diff --git a/phpBB/phpbb/textformatter/utils_interface.php b/phpBB/phpbb/textformatter/utils_interface.php new file mode 100644 index 0000000000..4b7392976a --- /dev/null +++ b/phpBB/phpbb/textformatter/utils_interface.php @@ -0,0 +1,79 @@ + +* @license GNU General Public License, version 2 (GPL-2.0) +* +* For full copyright and license information, please see +* the docs/CREDITS.txt file. +* +*/ + +namespace phpbb\textformatter; + +/** +* Used to manipulate a parsed text +*/ +interface utils_interface +{ + /** + * Replace BBCodes and other formatting elements with whitespace + * + * NOTE: preserves smilies as text + * + * @param string $text Parsed text + * @return string Plain text + */ + public function clean_formatting($text); + + /** + * Create a quote block for given text + * + * Possible attributes: + * - author: author's name (usually a username) + * - post_id: post_id of the post being quoted + * - user_id: user_id of the user being quoted + * - time: timestamp of the original message + * + * @param string $text Quote's text + * @param array $attributes Quote's attributes + * @return string Quote block to be used in a new post/text + */ + public function generate_quote($text, array $attributes = array()); + + /** + * Get a list of quote authors, limited to the outermost quotes + * + * @param string $text Parsed text + * @return string[] List of authors + */ + public function get_outermost_quote_authors($text); + + /** + * Remove given BBCode and its content, at given nesting depth + * + * @param string $text Parsed text + * @param string $bbcode_name BBCode's name + * @param integer $depth Minimum nesting depth (number of parents of the same name) + * @return string Parsed text + */ + public function remove_bbcode($text, $bbcode_name, $depth = 0); + + /** + * Return a parsed text to its original form + * + * @param string $text Parsed text + * @return string Original plain text + */ + public function unparse($text); + + /** + * Return whether or not a parsed text represent an empty text. + * + * @param string $text Parsed text + * @return bool Tue if the original text is empty + */ + public function is_empty($text); +} diff --git a/phpBB/phpbb/textreparser/base.php b/phpBB/phpbb/textreparser/base.php new file mode 100644 index 0000000000..27d7bc1f27 --- /dev/null +++ b/phpBB/phpbb/textreparser/base.php @@ -0,0 +1,269 @@ + +* @license GNU General Public License, version 2 (GPL-2.0) +* +* For full copyright and license information, please see +* the docs/CREDITS.txt file. +* +*/ + +namespace phpbb\textreparser; + +abstract class base implements reparser_interface +{ + /** + * @var string The reparser name + */ + protected $name; + + /** + * @var bool Whether to save changes to the database + */ + protected $save_changes = true; + + /** + * {@inheritdoc} + */ + abstract public function get_max_id(); + + /** + * Return all records in given range + * + * @param integer $min_id Lower bound + * @param integer $max_id Upper bound + * @return array Array of records + */ + abstract protected function get_records_by_range($min_id, $max_id); + + /** + * {@inheritdoc} + */ + abstract protected function save_record(array $record); + + /** + * Add fields to given record, if applicable + * + * The enable_* fields are not always saved to the database. Sometimes we need to guess their + * original value based on the text content or possibly other fields + * + * @param array $record Original record + * @return array Complete record + */ + protected function add_missing_fields(array $record) + { + if (!isset($record['enable_bbcode'], $record['enable_smilies'], $record['enable_magic_url'])) + { + if (isset($record['options'])) + { + $record += array( + 'enable_bbcode' => (bool) ($record['options'] & OPTION_FLAG_BBCODE), + 'enable_smilies' => (bool) ($record['options'] & OPTION_FLAG_SMILIES), + 'enable_magic_url' => (bool) ($record['options'] & OPTION_FLAG_LINKS), + ); + } + else + { + $record += array( + 'enable_bbcode' => $this->guess_bbcodes($record), + 'enable_smilies' => $this->guess_smilies($record), + 'enable_magic_url' => $this->guess_magic_url($record), + ); + } + } + + // Those BBCodes are disabled based on context and user permissions and that value is never + // stored in the database. Here we test whether they were used in the original text. + $bbcodes = array('flash', 'img', 'quote', 'url'); + foreach ($bbcodes as $bbcode) + { + $field_name = 'enable_' . $bbcode . '_bbcode'; + $record[$field_name] = $this->guess_bbcode($record, $bbcode); + } + + // Magic URLs are tied to the URL BBCode, that's why if magic URLs are enabled we make sure + // that the URL BBCode is also enabled + if ($record['enable_magic_url']) + { + $record['enable_url_bbcode'] = true; + } + + return $record; + } + + /** + * Returns the name of the reparser + * + * @return string Name of reparser + */ + public function get_name() + { + return $this->name; + } + + /** + * Sets the name of the reparser + * + * @param string $name The reparser name + */ + public function set_name($name) + { + $this->name = $name; + } + + /** + * Disable saving changes to the database + */ + public function disable_save() + { + $this->save_changes = false; + } + + /** + * Enable saving changes to the database + */ + public function enable_save() + { + $this->save_changes = true; + } + + /** + * Guess whether given BBCode is in use in given record + * + * @param array $record + * @param string $bbcode + * @return bool + */ + protected function guess_bbcode(array $record, $bbcode) + { + if (!empty($record['bbcode_uid'])) + { + // Look for the closing tag, e.g. [/url] + $match = '[/' . $bbcode . ':' . $record['bbcode_uid']; + if (strpos($record['text'], $match) !== false) + { + return true; + } + } + + if (substr($record['text'], 0, 2) === '[/url] + $match = '[/' . $bbcode . ']'; + if (strpos($record['text'], $match) !== false) + { + return true; + } + } + + return false; + } + + /** + * Guess whether any BBCode is in use in given record + * + * @param array $record + * @return bool + */ + protected function guess_bbcodes(array $record) + { + if (!empty($record['bbcode_uid'])) + { + // Test whether the bbcode_uid is in use + $match = ':' . $record['bbcode_uid']; + if (strpos($record['text'], $match) !== false) + { + return true; + } + } + + if (substr($record['text'], 0, 2) === '\\[/\\w+\\])', $match); + } + + return false; + } + + /** + * Guess whether magic URLs are in use in given record + * + * @param array $record + * @return bool + */ + protected function guess_magic_url(array $record) + { + // Look for or for a URL tag that's not immediately followed by + return (strpos($record['text'], '') !== false || preg_match('(]++>(?!))', $record['text'])); + } + + /** + * Guess whether smilies are in use in given record + * + * @param array $record + * @return bool + */ + protected function guess_smilies(array $record) + { + return (strpos($record['text'], ' - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + @@ -68,53 +39,7 @@ - - - - - - - - - - - - - - - - - -
- - - - - - - - - - -

- [ {L_PLAY_QUICKTIME_FILE} ] - {_file.DOWNLOAD_NAME} [ {_file.FILESIZE} {_file.SIZE_LANG} | {_file.L_DOWNLOAD_COUNT} ]

+

{_file.DOWNLOAD_NAME} [ {_file.FILESIZE} {_file.SIZE_LANG} | {_file.L_DOWNLOAD_COUNT} ]

diff --git a/phpBB/styles/prosilver/template/bbcode.html b/phpBB/styles/prosilver/template/bbcode.html index 49bcd56945..940c0ace29 100644 --- a/phpBB/styles/prosilver/template/bbcode.html +++ b/phpBB/styles/prosilver/template/bbcode.html @@ -11,9 +11,44 @@
{USERNAME} {L_WROTE}{L_COLON}
+ +
+ + uncited + +
+ + + + + + + + + + + + + + + + + + + + + +
+
+
+
+ +
+
+ -

{L_CODE}{L_COLON} {L_SELECT_ALL_CODE}

-
+

{L_CODE}{L_COLON} {L_SELECT_ALL_CODE}


+
diff --git a/phpBB/styles/prosilver/template/captcha_recaptcha.html b/phpBB/styles/prosilver/template/captcha_recaptcha.html index fee0f7423e..a123f543a8 100644 --- a/phpBB/styles/prosilver/template/captcha_recaptcha.html +++ b/phpBB/styles/prosilver/template/captcha_recaptcha.html @@ -12,24 +12,11 @@

{L_RECAPTCHA_EXPLAIN}
- - - - - + +
diff --git a/phpBB/styles/prosilver/template/confirm_delete_body.html b/phpBB/styles/prosilver/template/confirm_delete_body.html index fbd881a940..98974963c0 100644 --- a/phpBB/styles/prosilver/template/confirm_delete_body.html +++ b/phpBB/styles/prosilver/template/confirm_delete_body.html @@ -58,7 +58,7 @@
{S_HIDDEN_FIELDS} -   +  
diff --git a/phpBB/styles/prosilver/template/display_options.html b/phpBB/styles/prosilver/template/display_options.html new file mode 100644 index 0000000000..a426d08845 --- /dev/null +++ b/phpBB/styles/prosilver/template/display_options.html @@ -0,0 +1,27 @@ + diff --git a/phpBB/styles/prosilver/template/faq_body.html b/phpBB/styles/prosilver/template/faq_body.html index 53205d14e9..e55c12ac48 100644 --- a/phpBB/styles/prosilver/template/faq_body.html +++ b/phpBB/styles/prosilver/template/faq_body.html @@ -34,8 +34,10 @@
{faq_block.faq_row.FAQ_QUESTION}
{faq_block.faq_row.FAQ_ANSWER}
-
{L_BACK_TO_TOP}
+ + {L_BACK_TO_TOP} +
diff --git a/phpBB/styles/prosilver/template/forum_fn.js b/phpBB/styles/prosilver/template/forum_fn.js index d779008f80..3f59709ac6 100644 --- a/phpBB/styles/prosilver/template/forum_fn.js +++ b/phpBB/styles/prosilver/template/forum_fn.js @@ -194,37 +194,6 @@ function selectCode(a) { } } -/** -* Play quicktime file by determining it's width/height -* from the displayed rectangle area -*/ -function play_qt_file(obj) { - 'use strict'; - - var rectangle = obj.GetRectangle(); - var width, height; - - if (rectangle) { - rectangle = rectangle.split(','); - var x1 = parseInt(rectangle[0], 10); - var x2 = parseInt(rectangle[2], 10); - var y1 = parseInt(rectangle[1], 10); - var y2 = parseInt(rectangle[3], 10); - - width = (x1 < 0) ? (x1 * -1) + x2 : x2 - x1; - height = (y1 < 0) ? (y1 * -1) + y2 : y2 - y1; - } else { - width = 200; - height = 0; - } - - obj.width = width; - obj.height = height + 16; - - obj.SetControllerVisible(true); - obj.Play(); -} - var inAutocomplete = false; var lastKeyEntered = ''; @@ -365,13 +334,13 @@ function parseDocument($container) { /** * Adjust HTML code for IE8 and older versions */ - if (oldBrowser) { - // Fix .linklist.bulletin lists - $container - .find('ul.linklist.bulletin > li') - .filter(':first-child, .rightside:last-child') - .addClass('no-bulletin'); - } + // if (oldBrowser) { + // // Fix .linklist.bulletin lists + // $container + // .find('ul.linklist.bulletin > li') + // .filter(':first-child, .rightside:last-child') + // .addClass('no-bulletin'); + // } /** * Resize navigation (breadcrumbs) block to keep all links on same line @@ -492,7 +461,7 @@ function parseDocument($container) { $linksFirst = $linksNotSkip.not(filterLast), // The items that will be hidden first $linksLast = $linksNotSkip.filter(filterLast), // The items that will be hidden last persistent = $this.attr('id') === 'nav-main', // Does this list already have a menu (such as quick-links)? - html = '', + html = '', slack = 3; // Vertical slack space (in pixels). Determines how sensitive the script is in determining whether a line-break has occured. // Add a hidden drop-down menu to each links list (except those that already have one) @@ -581,8 +550,10 @@ function parseDocument($container) { $menuContents.prepend($clones1.addClass('clone clone-first').removeClass('leftside rightside')); if ($this.hasClass('post-buttons')) { - $('.button', $menuContents).removeClass('button icon-button'); - $('.responsive-menu-link', $menu).addClass('button icon-button').prepend(''); + $('.button', $menuContents).removeClass('button'); + $('.sr-only', $menuContents).removeClass('sr-only'); + $('.js-responsive-menu-link').addClass('button').addClass('button-icon-only'); + $('.js-responsive-menu-link .icon').removeClass('fa-bars').addClass('fa-ellipsis-h'); } copied1 = true; } @@ -636,7 +607,7 @@ function parseDocument($container) { } if (!persistent) { - phpbb.registerDropdown($menu.find('a.responsive-menu-link'), $menu.find('.dropdown'), false); + phpbb.registerDropdown($menu.find('a.js-responsive-menu-link'), $menu.find('.dropdown'), false); } // If there are any images in the links list, run the check again after they have loaded diff --git a/phpBB/styles/prosilver/template/forumlist_body.html b/phpBB/styles/prosilver/template/forumlist_body.html index a197545b90..621e226260 100644 --- a/phpBB/styles/prosilver/template/forumlist_body.html +++ b/phpBB/styles/prosilver/template/forumlist_body.html @@ -14,7 +14,7 @@
- {S_HIDDEN_FIELDS}  + {S_HIDDEN_FIELDS} 
diff --git a/phpBB/styles/prosilver/template/mcp_forum.html b/phpBB/styles/prosilver/template/mcp_forum.html index 4c037f56ae..f6c518e1df 100644 --- a/phpBB/styles/prosilver/template/mcp_forum.html +++ b/phpBB/styles/prosilver/template/mcp_forum.html @@ -10,7 +10,7 @@
-
+