69 changes: 69 additions & 0 deletions .github/workflows/php.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
name: PHP Check

on:
push:
branches:
- release-2.1
pull_request:

jobs:
build:
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 #4.2.2

- name: Cache Composer packages
id: composer-cache
uses: actions/cache@d4323d4df104b026a6aa633fdb11d772146be0bf #4.2.2
with:
path: vendor
key: ${{ runner.os }}-php-${{ hashFiles('**/composer.lock') }}
restore-keys: ${{ runner.os }}-php-

- name: Install dependencies
if: steps.composer-cache.outputs.cache-hit != 'true'
run: composer install --prefer-dist --no-progress --ansi

- run: php -v

- name: Checking for sign off (GPG also accepted)
run: php ./vendor/simplemachines/build-tools/check-signed-off.php

- name: Checking file integrity
run: |
php ./vendor/simplemachines/build-tools/check-eof.php
php ./vendor/simplemachines/build-tools/check-smf-license.php
php ./vendor/simplemachines/build-tools/check-smf-languages.php
php ./vendor/simplemachines/build-tools/check-version.php
lint:
runs-on: ubuntu-latest
strategy:
matrix:
php: [ 7.0, 7.1, 7.2, 7.3, 7.4, 8.0, 8.1 ]

name: PHP ${{ matrix.php }} Syntax Check
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 #4.2.2

- name: Setup PHP ${{ matrix.php }}
uses: shivammathur/setup-php@9e72090525849c5e82e596468b86eb55e9cc5401 #2.32.0
with:
php-version: ${{ matrix.php }}
coverage: none

- name: Cache Composer packages
id: composer-cache
uses: actions/cache@d4323d4df104b026a6aa633fdb11d772146be0bf #4.2.2
with:
path: vendor
key: ${{ runner.os }}-php-${{ hashFiles('**/composer.lock') }}
restore-keys: ${{ runner.os }}-php-

- name: Install dependencies
if: steps.composer-cache.outputs.cache-hit != 'true'
run: composer install --prefer-dist --no-progress --ansi

- name: Lint PHP files
run: vendor/bin/phplint -w --exclude .git --exclude vendor --ansi .
31 changes: 31 additions & 0 deletions .github/workflows/update-year.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
name: Update Year

on:
workflow_dispatch:

jobs:
update-year:
runs-on: ubuntu-latest

steps:
- name: Checkout repository
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 #4.2.2

- name: Set current year environment variable
run: echo "CURRENT_YEAR=$(date +'%Y')" >> $GITHUB_ENV

- name: Update year in PHP files
run: |
for file in SSI.php proxy.php cron.php index.php other/*.php; do
sed -i "s/@copyright [0-9]\{4\}/@copyright ${{ env.CURRENT_YEAR }}/" $file
sed -i "s/define('SMF_SOFTWARE_YEAR', '[0-9]\{4\}');/define('SMF_SOFTWARE_YEAR', '${{ env.CURRENT_YEAR }}');/" $file
done
- name: Create Pull Request
uses: peter-evans/create-pull-request@271a8d0340265f705b14b6d32b9829c1cb33d45e #7.0.8
with:
token: ${{ secrets.GITHUB_TOKEN }}
commit-message: Update year to ${{ env.CURRENT_YEAR }}
branch: 2.1/update-year-${{ env.CURRENT_YEAR }}
title: "[2.1] Update year to ${{ env.CURRENT_YEAR }}"
body: This PR updates the year to ${{ env.CURRENT_YEAR }}. This action was performed by a bot.
17 changes: 14 additions & 3 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
# SMF Generated Files #
#######################
agreement.txt
Settings.php
Settings_bak.php
Settings_org.php
db_last_error.php
cache/data*.php
cache/data*.cache
cache/*.db3
Packages/backups/*
Packages/temp
*.*~
Expand All @@ -13,9 +17,12 @@ Packages/*.zip
Packages/*/
attachments/
/upgrade.php
Themes/default/css/minified.css
Themes/default/scripts/minified.js
Themes/default/scripts/minified_deferred.js
Themes/default/css/minified*.css
Themes/default/scripts/minified*.js
Themes/default/scripts/minified_deferred*.js
custom_avatar/
!custom_avatar/index.php
!custom_avatar/blank.png

# Compiled source #
###################
Expand Down Expand Up @@ -64,3 +71,7 @@ Thumbs.db
/test.php
Themes/default/scripts/minified.js
Themes/default/css/minified.css
upgrade-helper.php
.vscode/

vendor/
3 changes: 0 additions & 3 deletions .gitmodules

This file was deleted.

168 changes: 121 additions & 47 deletions .scrutinizer.yml
Original file line number Diff line number Diff line change
@@ -1,20 +1,17 @@
tools:
php_sim: true
php_pdepend: true
php_analyzer: true

filter:
excluded_paths:
- '*.min.js'
- 'Sources/minify/*'
- 'Sources/ReCaptcha/*'

- 'Sources/minify/'
- 'Sources/random_compat/'
- 'Sources/ReCaptcha/'
dependency_paths:
- 'Sources/minify/'
- 'Sources/random_compat/'
- 'Sources/ReCaptcha/'
checks:
php:
variable_existence: true
use_statement_alias_conflict: true
use_self_instead_of_fqcn: true
uppercase_constants: true
unused_variables: true
unused_properties: true
unused_parameters: true
Expand All @@ -24,37 +21,26 @@ checks:
simplify_boolean_return: true
return_doc_comments: true
return_doc_comment_if_not_inferrable: true
require_scope_for_properties: true
require_scope_for_methods: true
require_php_tag_first: true
remove_extra_empty_lines: true
property_assignments: true
prefer_while_loop_over_for_loop: true
precedence_mistakes: true
precedence_in_conditions: true
php5_style_constructor: true
parse_doc_comments: true
parameter_non_unique: true
parameter_doc_comments: true
param_doc_comment_if_not_inferrable: true
overriding_private_members: true
optional_parameters_at_the_end: true
no_unnecessary_if: true
no_underscore_prefix_in_properties: true
no_underscore_prefix_in_methods: true
no_trailing_whitespace: true
no_short_open_tag: true
no_property_on_interface: true
no_non_implemented_abstract_methods: true
no_short_method_names:
minimum: '3'
no_goto: true
no_global_keyword: true
no_error_suppression: true
no_empty_statements: true
no_duplicate_arguments: true
no_error_suppression: false
no_debug_code: true
no_commented_out_code: true
more_specific_types_in_doc_comments: true
missing_arguments: true
method_calls_on_non_object: true
Expand All @@ -67,51 +53,139 @@ checks:
order_alphabetically: false
fix_line_ending: true
fix_doc_comments: true
encourage_single_quotes: true
encourage_shallow_comparison: true
encourage_postdec_operator: true
duplication: true
deprecated_code_usage: true
deadlock_detection_in_loops: true
code_rating: true
closure_use_not_conflicting: true
closure_use_modifiable: true
catch_class_exists: true
avoid_useless_overridden_methods: true
avoid_usage_of_logical_operators: true
avoid_perl_style_comments: true
avoid_multiple_statements_on_same_line: true
avoid_duplicate_types: true
avoid_corrupting_byteorder_marks: true
avoid_conflicting_incrementers: true
avoid_closing_tag: false
avoid_aliased_php_functions: true
assignment_of_null_return: true
argument_type_checks: true
avoid_todo_comments: true
avoid_fixme_comments: true
no_long_variable_names:
maximum: '40'
no_short_variable_names:
minimum: '3'
line_length:
max_length: '150'
phpunit_assertions: true
remove_php_closing_tag: false
one_class_per_file: false
side_effects_or_types: false
no_mixed_inline_html: false
require_braces_around_control_structures: false
php5_style_constructor: false
no_global_keyword: false
avoid_usage_of_logical_operators: false
psr2_class_declaration: false
no_underscore_prefix_in_properties: false
no_underscore_prefix_in_methods: false
blank_line_after_namespace_declaration: false
single_namespace_per_use: false
psr2_switch_declaration: false
psr2_control_structure_declaration: false
avoid_superglobals: false
security_vulnerabilities: false
no_exit: false
no_exit: false
coding_style:
php:
indentation:
general:
use_tabs: true
size: 4
switch:
indent_case: true
spaces:
general:
linefeed_character: newline
before_parentheses:
function_declaration: false
closure_definition: false
function_call: false
if: true
for: true
while: true
switch: true
catch: true
array_initializer: false
around_operators:
assignment: true
logical: true
equality: true
relational: true
bitwise: true
additive: true
multiplicative: true
shift: true
unary_additive: false
concatenation: true
negation: false
before_left_brace:
class: true
function: true
if: true
else: true
for: true
while: true
do: true
switch: true
try: true
catch: true
finally: true
before_keywords:
else: true
while: true
catch: true
finally: true
within:
brackets: false
array_initializer: false
grouping: false
function_call: false
function_declaration: false
if: false
for: false
while: false
switch: false
catch: false
type_cast: false
ternary_operator:
before_condition: true
after_condition: true
before_alternative: true
after_alternative: true
in_short_version: false
other:
before_comma: false
after_comma: true
before_semicolon: false
after_semicolon: true
after_type_cast: true
braces:
classes_functions:
class: new-line
function: new-line
closure: new-line
if:
opening: new-line
always: false
else_on_new_line: true
for:
opening: new-line
always: false
while:
opening: new-line
always: false
do_while:
opening: undefined
always: true
while_on_new_line: true
switch:
opening: new-line
try:
opening: new-line
catch_on_new_line: true
finally_on_new_line: true
upper_lower_casing:
keywords:
general: lower
constants:
true_false_null: lower


build:
nodes:
analysis:
tests:
override:
- php-scrutinizer-run
27 changes: 0 additions & 27 deletions .travis.yml

This file was deleted.

4 changes: 2 additions & 2 deletions LICENSE
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
Copyright © 2011 Simple Machines. All rights reserved.
Copyright © 2024 Simple Machines. All rights reserved.

Developed by: Simple Machines Forum Project
Simple Machines
http://www.simplemachines.org
https://www.simplemachines.org

All rights reserved.

Expand Down
2 changes: 1 addition & 1 deletion Packages/index.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
{
// Found it!
require(dirname(dirname(__FILE__)) . '/Settings.php');
header('Location: ' . $boardurl);
header('location: ' . $boardurl);
}
// Can't find it... just forget it.
else
Expand Down
26 changes: 10 additions & 16 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
# [SMF](www.simplemachines.org)
[![Build Status](https://travis-ci.org/SimpleMachines/SMF2.1.svg?branch=release-2.1)](https://travis-ci.org/SimpleMachines/SMF2.1)
# [SMF](https://www.simplemachines.org)
![Build Status](https://github.com/SimpleMachines/SMF/actions/workflows/php.yml/badge.svg)
![CrowdIn Status](https://github.com/SimpleMachines/SMF/actions/workflows/crowdin_wf.yml/badge.svg)

This is a SMF 2.1 development repository.
This the development repository for Simple Machines Forum.
The software is licensed under [BSD 3-clause license](https://opensource.org/licenses/BSD-3-Clause).

Contributions to documentation are licensed under [CC-by-SA 3](https://creativecommons.org/licenses/by-sa/3.0). Third party libraries or sets of images, are under their own licenses.
Contributions to documentation are licensed under [CC-by-SA 3](https://creativecommons.org/licenses/by-sa/3.0). Third party libraries or sets of images are under their own licenses.

## Notes:

Expand All @@ -14,29 +15,22 @@ Please see the [Developer's Certificate of Origin](https://github.com/SimpleMach
by signing off your contributions, you acknowledge that you can and do license your submissions under the license of the project.

## Branches organization:
* ***master*** - is the main branch, only used to merge in a "final release"
* ***development*** - is the branch where the development of the "next" version/s happens
* ***release-2.1*** - is the branch where bug fixes for the version 2.1 are applied
* ***release-2.1*** - is the branch where updates for version 2.1 are applied

## How to contribute:
* fork the repository. If you are not used to Github, please check out [fork a repository](https://help.github.com/fork-a-repo).
* branch your repository, to commit the desired changes.
* sign-off your commits, to acknowledge your submission under the license of the project.
* It is enough to include in your commit comment "Signed-off by: " followed by your name and email address (for example: `Signed-off-by: Angelina Belle <angelinabelle1@hotmail.com>`)
* an easy way to do so, is to define an alias for the git commit command, which includes -s switch (reference: [How to create Git aliases](https://git.wiki.kernel.org/index.php/Aliases))
* It is enough to include in your commit comment "Signed-off by: " followed by your name and email address (for example: `Signed-off-by: Your Name <youremail@example.com>`)
* an easy way to do so is to define an alias for the git commit command, which includes -s switch (reference: [How to create Git aliases](https://git.wiki.kernel.org/index.php/Aliases))
* send a pull request to us.

## How to submit a pull request:
* If you want to send a bug fix for the version 2.1, send it to the branch ***release-2.1***
* If you want to send a new feature, use the branch ***development***
* You should never send any pull request against the master branch
For more informations, the ideal branching we would like to follow is the one described in [this article](http://nvie.com/posts/a-successful-git-branching-model/)

Please, feel free to play around. That's what we're doing. ;)
* If you want to send a bug fix for version 2.1, send it to the branch ***release-2.1***

## Security matters:

Lastly, if you have a security issue you would like to notify us about regarding SMF - not just for 2.1, but for any version -
please file a [security report](https://www.simplemachines.org/about/smf/security.php) on our website: https://www.simplemachines.org/about/smf/security.php

This will enable the team to review it and prepare patches as appropriate before exploits are widely known, which helps keep everyone safe.
This will enable the team to review it and prepare patches as appropriate before exploits are widely known, which helps keep everyone safe.
325 changes: 249 additions & 76 deletions SSI.php

Large diffs are not rendered by default.

Binary file removed Smileys/aaron/afro.gif
Binary file not shown.
Binary file removed Smileys/aaron/angel.gif
Binary file not shown.
Binary file removed Smileys/aaron/angry.gif
Binary file not shown.
Binary file removed Smileys/aaron/azn.gif
Binary file not shown.
Binary file removed Smileys/aaron/blank.gif
Binary file not shown.
Binary file removed Smileys/aaron/cheesy.gif
Binary file not shown.
Binary file removed Smileys/aaron/cool.gif
Binary file not shown.
Binary file removed Smileys/aaron/cry.gif
Binary file not shown.
Binary file removed Smileys/aaron/embarrassed.gif
Binary file not shown.
Binary file removed Smileys/aaron/evil.gif
Binary file not shown.
Binary file removed Smileys/aaron/grin.gif
Binary file not shown.
Binary file removed Smileys/aaron/huh.gif
Binary file not shown.
Binary file removed Smileys/aaron/kiss.gif
Binary file not shown.
Binary file removed Smileys/aaron/laugh.gif
Binary file not shown.
Binary file removed Smileys/aaron/lipsrsealed.gif
Binary file not shown.
Binary file removed Smileys/aaron/police.gif
Binary file not shown.
Binary file removed Smileys/aaron/rolleyes.gif
Binary file not shown.
Binary file removed Smileys/aaron/sad.gif
Binary file not shown.
Binary file removed Smileys/aaron/shocked.gif
Binary file not shown.
Binary file removed Smileys/aaron/smiley.gif
Binary file not shown.
Binary file removed Smileys/aaron/tongue.gif
Binary file not shown.
Binary file removed Smileys/aaron/undecided.gif
Binary file not shown.
Binary file removed Smileys/aaron/wink.gif
Binary file not shown.
Binary file removed Smileys/akyhne/afro.gif
Binary file not shown.
Binary file removed Smileys/akyhne/angel.gif
Binary file not shown.
Binary file removed Smileys/akyhne/angry.gif
Diff not rendered.
Binary file removed Smileys/akyhne/azn.gif
Diff not rendered.
Binary file removed Smileys/akyhne/blank.gif
Diff not rendered.
Binary file removed Smileys/akyhne/cheesy.gif
Diff not rendered.
Binary file removed Smileys/akyhne/cool.gif
Diff not rendered.
Binary file removed Smileys/akyhne/cry.gif
Diff not rendered.
Binary file removed Smileys/akyhne/embarrassed.gif
Diff not rendered.
Binary file removed Smileys/akyhne/evil.gif
Diff not rendered.
Binary file removed Smileys/akyhne/grin.gif
Diff not rendered.
Binary file removed Smileys/akyhne/huh.gif
Diff not rendered.
Binary file removed Smileys/akyhne/kiss.gif
Diff not rendered.
Binary file removed Smileys/akyhne/laugh.gif
Diff not rendered.
Binary file removed Smileys/akyhne/lipsrsealed.gif
Diff not rendered.
Binary file removed Smileys/akyhne/police.gif
Diff not rendered.
Binary file removed Smileys/akyhne/rolleyes.gif
Diff not rendered.
Binary file removed Smileys/akyhne/sad.gif
Diff not rendered.
Binary file removed Smileys/akyhne/shocked.gif
Diff not rendered.
Binary file removed Smileys/akyhne/smiley.gif
Diff not rendered.
Binary file removed Smileys/akyhne/tongue.gif
Diff not rendered.
Binary file removed Smileys/akyhne/undecided.gif
Diff not rendered.
Binary file removed Smileys/akyhne/wink.gif
Diff not rendered.
Binary file added Smileys/alienine/afro.png
Binary file added Smileys/alienine/angel.png
Binary file added Smileys/alienine/angry.png
Binary file added Smileys/alienine/azn.png
Binary file added Smileys/alienine/blank.png
Binary file added Smileys/alienine/cheesy.png
Binary file added Smileys/alienine/cool.png
Binary file added Smileys/alienine/cry.png
Binary file added Smileys/alienine/embarrassed.png
Binary file added Smileys/alienine/evil.png
Binary file added Smileys/alienine/grin.png
Binary file added Smileys/alienine/huh.png
File renamed without changes.
Binary file added Smileys/alienine/kiss.png
Binary file added Smileys/alienine/laugh.png
Binary file added Smileys/alienine/lipsrsealed.png
Binary file added Smileys/alienine/police.png
Binary file added Smileys/alienine/rolleyes.png
Binary file added Smileys/alienine/sad.png
Binary file added Smileys/alienine/shocked.png
Binary file added Smileys/alienine/smiley.png
Binary file added Smileys/alienine/tongue.png
Binary file added Smileys/alienine/undecided.png
Binary file added Smileys/alienine/wink.png
Binary file removed Smileys/default/afro.gif
Diff not rendered.
Binary file removed Smileys/default/angel.gif
Diff not rendered.
Binary file removed Smileys/default/angry.gif
Diff not rendered.
Binary file removed Smileys/default/azn.gif
Diff not rendered.
Binary file removed Smileys/default/blank.gif
Diff not rendered.
Binary file removed Smileys/default/cheesy.gif
Diff not rendered.
Binary file removed Smileys/default/cool.gif
Diff not rendered.
Binary file removed Smileys/default/cry.gif
Diff not rendered.
Binary file removed Smileys/default/embarrassed.gif
Diff not rendered.
Binary file removed Smileys/default/evil.gif
Diff not rendered.
Binary file removed Smileys/default/grin.gif
Diff not rendered.
Binary file removed Smileys/default/huh.gif
Diff not rendered.
Binary file removed Smileys/default/kiss.gif
Diff not rendered.
Binary file removed Smileys/default/laugh.gif
Diff not rendered.
Binary file removed Smileys/default/lipsrsealed.gif
Diff not rendered.
Binary file removed Smileys/default/police.gif
Diff not rendered.
Binary file removed Smileys/default/rolleyes.gif
Diff not rendered.
Binary file removed Smileys/default/sad.gif
Diff not rendered.
Binary file removed Smileys/default/shocked.gif
Diff not rendered.
Binary file removed Smileys/default/smiley.gif
Diff not rendered.
Binary file removed Smileys/default/tongue.gif
Diff not rendered.
Binary file removed Smileys/default/undecided.gif
Diff not rendered.
Binary file removed Smileys/default/wink.gif
Diff not rendered.
Binary file removed Smileys/fugue/afro.gif
Diff not rendered.
Binary file added Smileys/fugue/afro.png
Binary file removed Smileys/fugue/angel.gif
Diff not rendered.
Binary file added Smileys/fugue/angel.png
Binary file removed Smileys/fugue/angry.gif
Diff not rendered.
Binary file added Smileys/fugue/angry.png
Binary file removed Smileys/fugue/azn.gif
Diff not rendered.
Binary file added Smileys/fugue/azn.png
Binary file removed Smileys/fugue/blank.gif
Diff not rendered.
Binary file added Smileys/fugue/blank.png
Binary file removed Smileys/fugue/cheesy.gif
Diff not rendered.
Binary file added Smileys/fugue/cheesy.png
Binary file removed Smileys/fugue/cool.gif
Diff not rendered.
Binary file added Smileys/fugue/cool.png
Binary file removed Smileys/fugue/cry.gif
Diff not rendered.
Binary file added Smileys/fugue/cry.png
Binary file removed Smileys/fugue/embarrassed.gif
Diff not rendered.
Binary file added Smileys/fugue/embarrassed.png
Binary file removed Smileys/fugue/evil.gif
Diff not rendered.
Binary file added Smileys/fugue/evil.png
Binary file removed Smileys/fugue/grin.gif
Diff not rendered.
Binary file added Smileys/fugue/grin.png
Binary file removed Smileys/fugue/huh.gif
Diff not rendered.
Binary file added Smileys/fugue/huh.png
Binary file removed Smileys/fugue/kiss.gif
Diff not rendered.
Binary file added Smileys/fugue/kiss.png
Binary file removed Smileys/fugue/laugh.gif
Diff not rendered.
Binary file added Smileys/fugue/laugh.png
Binary file removed Smileys/fugue/lipsrsealed.gif
Diff not rendered.
Binary file added Smileys/fugue/lipsrsealed.png
Binary file removed Smileys/fugue/police.gif
Diff not rendered.
Binary file added Smileys/fugue/police.png
Binary file removed Smileys/fugue/rolleyes.gif
Diff not rendered.
Binary file added Smileys/fugue/rolleyes.png
Binary file removed Smileys/fugue/sad.gif
Diff not rendered.
Binary file added Smileys/fugue/sad.png
Binary file removed Smileys/fugue/shocked.gif
Diff not rendered.
Binary file added Smileys/fugue/shocked.png
Binary file removed Smileys/fugue/smiley.gif
Diff not rendered.
Binary file added Smileys/fugue/smiley.png
Binary file removed Smileys/fugue/tongue.gif
Diff not rendered.
Binary file added Smileys/fugue/tongue.png
Binary file removed Smileys/fugue/undecided.gif
Diff not rendered.
Binary file added Smileys/fugue/undecided.png
Binary file removed Smileys/fugue/wink.gif
Diff not rendered.
Binary file added Smileys/fugue/wink.png
2 changes: 1 addition & 1 deletion Smileys/index.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
{
// Found it!
require(dirname(dirname(__FILE__)) . '/Settings.php');
header('Location: ' . $boardurl);
header('location: ' . $boardurl);
}
// Can't find it... just forget it.
else
Expand Down
50 changes: 28 additions & 22 deletions Sources/Admin.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,11 @@
* Simple Machines Forum (SMF)
*
* @package SMF
* @author Simple Machines http://www.simplemachines.org
* @copyright 2017 Simple Machines and individual contributors
* @license http://www.simplemachines.org/about/smf/license.php BSD
* @author Simple Machines https://www.simplemachines.org
* @copyright 2022 Simple Machines and individual contributors
* @license https://www.simplemachines.org/about/smf/license.php BSD
*
* @version 2.1 Beta 4
* @version 2.1.0
*/

if (!defined('SMF'))
Expand All @@ -30,7 +30,7 @@ function AdminMain()
// Load the language and templates....
loadLanguage('Admin');
loadTemplate('Admin');
loadJavaScriptFile('admin.js', array(), 'smf_admin');
loadJavaScriptFile('admin.js', array('minimize' => true), 'smf_admin');
loadCSSFile('admin.css', array(), 'smf_admin');

// No indexing evil stuff.
Expand Down Expand Up @@ -178,7 +178,7 @@ function AdminMain()
'icon' => 'boards',
'permission' => array('manage_boards'),
'subsections' => array(
'main' => array($txt['boardsEdit']),
'main' => array($txt['boards_edit']),
'newcat' => array($txt['mboards_new_cat']),
'settings' => array($txt['settings'], 'admin_forum'),
),
Expand Down Expand Up @@ -315,6 +315,7 @@ function AdminMain()
'subsections' => array(
'register' => array($txt['admin_browse_register_new'], 'moderate_forum'),
'agreement' => array($txt['registration_agreement'], 'admin_forum'),
'policy' => array($txt['privacy_policy'], 'admin_forum'),
'reservednames' => array($txt['admin_reserved_set'], 'admin_forum'),
'settings' => array($txt['settings'], 'admin_forum'),
),
Expand Down Expand Up @@ -369,6 +370,7 @@ function AdminMain()
'cookie' => array($txt['cookies_sessions_settings']),
'security' => array($txt['security_settings']),
'cache' => array($txt['caching_settings']),
'export' => array($txt['export_settings']),
'loads' => array($txt['load_balancing_settings']),
'phpinfo' => array($txt['phpinfo_settings']),
),
Expand All @@ -394,6 +396,7 @@ function AdminMain()
'subsections' => array(
'tasks' => array($txt['maintain_tasks'], 'admin_forum'),
'tasklog' => array($txt['scheduled_log'], 'admin_forum'),
'settings' => array($txt['scheduled_tasks_settings'], 'admin_forum'),
),
),
'mailqueue' => array(
Expand All @@ -404,6 +407,7 @@ function AdminMain()
'subsections' => array(
'browse' => array($txt['mailqueue_browse'], 'admin_forum'),
'settings' => array($txt['mailqueue_settings'], 'admin_forum'),
'test' => array($txt['mailqueue_test'], 'admin_forum'),
),
),
'reports' => array(
Expand All @@ -417,7 +421,7 @@ function AdminMain()
'function' => 'AdminLogs',
'icon' => 'logs',
'subsections' => array(
'errorlog' => array($txt['errlog'], 'admin_forum', 'enabled' => !empty($modSettings['enableErrorLogging']), 'url' => $scripturl . '?action=admin;area=logs;sa=errorlog;desc'),
'errorlog' => array($txt['errorlog'], 'admin_forum', 'enabled' => !empty($modSettings['enableErrorLogging']), 'url' => $scripturl . '?action=admin;area=logs;sa=errorlog;desc'),
'adminlog' => array($txt['admin_log'], 'admin_forum', 'enabled' => !empty($modSettings['adminlog_enabled'])),
'modlog' => array($txt['moderation_log'], 'admin_forum', 'enabled' => !empty($modSettings['modlog_enabled'])),
'banlog' => array($txt['ban_log'], 'manage_bans'),
Expand Down Expand Up @@ -504,10 +508,10 @@ function AdminMain()
* manage_boards, edit_news, or send_mail permission.
* It uses the index administrative area.
* It can be found by going to ?action=admin.
*/
*/
function AdminHome()
{
global $sourcedir, $forum_version, $txt, $scripturl, $context, $user_info;
global $sourcedir, $txt, $scripturl, $context, $user_info;

// You have to be able to do at least one of the below to see this page.
isAllowedTo(array('admin_forum', 'manage_permissions', 'moderate_forum', 'manage_membergroups', 'manage_bans', 'send_mail', 'edit_news', 'manage_boards', 'manage_smileys', 'manage_attachments'));
Expand All @@ -526,18 +530,21 @@ function AdminHome()

// This makes it easier to get the latest news with your time format.
$context['time_format'] = urlencode($user_info['time_format']);
$context['forum_version'] = $forum_version;
$context['forum_version'] = SMF_FULL_VERSION;

// Get a list of current server versions.
require_once($sourcedir . '/Subs-Admin.php');
$checkFor = array(
'gd',
'imagemagick',
'db_server',
'phpa',
'apc',
'memcache',
'xcache',
'apcu',
'memcacheimplementation',
'memcachedimplementation',
'postgres',
'sqlite',
'zend',
'filebased',
'php',
'server',
);
Expand All @@ -552,7 +559,7 @@ function AdminHome()
'title' => $txt['admin_center'],
'help' => '',
'description' => '<strong>' . $txt['hello_guest'] . ' ' . $context['user']['name'] . '!</strong>
' . sprintf($txt['admin_main_welcome'], $txt['admin_center'], $txt['help'], $txt['help']),
' . sprintf($txt['admin_main_welcome'], $txt['admin_center'], $txt['help'], $txt['help']),
);

// Lastly, fill in the blanks in the support resources paragraphs.
Expand All @@ -572,7 +579,7 @@ function AdminHome()
);

if ($context['admin_area'] == 'admin')
loadJavaScriptFile('admin.js', array('defer' => false), 'smf_admin');
loadJavaScriptFile('admin.js', array('defer' => false, 'minimize' => true), 'smf_admin');
}

/**
Expand All @@ -588,7 +595,7 @@ function DisplayAdminFile()
fatal_lang_error('no_access', false);

// Strip off the forum cache part or we won't find it...
$_REQUEST['filename'] = str_replace($modSettings['browser_cache'], '', $_REQUEST['filename']);
$_REQUEST['filename'] = str_replace($context['browser_cache'], '', $_REQUEST['filename']);

$request = $smcFunc['db_query']('', '
SELECT data, filetype
Expand Down Expand Up @@ -623,7 +630,7 @@ function DisplayAdminFile()
@ob_start();

// Make sure they know what type of file we are.
header('Content-Type: ' . $filetype);
header('content-type: ' . $filetype);
echo $file_data;
obExit(false);
}
Expand Down Expand Up @@ -765,7 +772,7 @@ function AdminSearchInternal()
foreach ($settings_search as $setting_area)
{
// Get a list of their variables.
$config_vars = $setting_area[0](true);
$config_vars = call_user_func($setting_area[0], true);

foreach ($config_vars as $var)
if (!empty($var[1]) && !in_array($var[0], array('permissions', 'switch', 'desc')))
Expand Down Expand Up @@ -848,13 +855,12 @@ function AdminSearchOM()
$postVars = implode('+', $postVars);

// Get the results from the doc site.
require_once($sourcedir . '/Subs-Package.php');
// Demo URL:
// https://wiki.simplemachines.org/api.php?action=query&list=search&srprop=timestamp|snippet&format=xml&srwhat=text&srsearch=template+eval
$search_results = fetch_web_data($context['doc_apiurl'] . '?action=query&list=search&srprop=timestamp|snippet&format=xml&srwhat=text&srsearch=' . $postVars);

// If we didn't get any xml back we are in trouble - perhaps the doc site is overloaded?
if (!$search_results || preg_match('~<' . '\?xml\sversion="\d+\.\d+"\?' . '>\s*(<api>.+?</api>)~is', $search_results, $matches) != true)
if (!$search_results || preg_match('~<' . '\?xml\sversion="\d+\.\d+"\?' . '>\s*(<api\b[^>]*>.+?</api>)~is', $search_results, $matches) != true)
fatal_lang_error('cannot_connect_doc_site');

$search_results = $matches[1];
Expand Down Expand Up @@ -915,7 +921,7 @@ function AdminLogs()
'tabs' => array(
'errorlog' => array(
'url' => $scripturl . '?action=admin;area=logs;sa=errorlog;desc',
'description' => sprintf($txt['errlog_desc'], $txt['remove']),
'description' => sprintf($txt['errorlog_desc'], $txt['remove']),
),
'adminlog' => array(
'description' => $txt['admin_log_desc'],
Expand Down
184 changes: 184 additions & 0 deletions Sources/Agreement.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
<?php

/**
* This file handles the user and privacy policy agreements.
*
* Simple Machines Forum (SMF)
*
* @package SMF
* @author Simple Machines https://www.simplemachines.org
* @copyright 2022 Simple Machines and individual contributors
* @license https://www.simplemachines.org/about/smf/license.php BSD
*
* @version 2.1.0
*/

if (!defined('SMF'))
die('No direct access...');

/* The purpose of this file is to show the user an updated registration
agreement, and get them to agree to it.
bool prepareAgreementContext()
// !!!
bool canRequireAgreement()
// !!!
bool canRequirePrivacyPolicy()
// !!!
void Agreement()
- Show the new registration agreement
void AcceptAgreement()
- Called when they actually accept the agreement
- Save the date of the current agreement to the members database table
- Redirect back to wherever they came from
*/

function prepareAgreementContext()
{
global $boarddir, $context, $language, $modSettings, $user_info;

// What, if anything, do they need to accept?
$context['can_accept_agreement'] = !empty($modSettings['requireAgreement']) && canRequireAgreement();
$context['can_accept_privacy_policy'] = !empty($modSettings['requirePolicyAgreement']) && canRequirePrivacyPolicy();
$context['accept_doc'] = $context['can_accept_agreement'] || $context['can_accept_privacy_policy'];

if (!$context['accept_doc'] || $context['can_accept_agreement'])
{
// Grab the agreement.
// Have we got a localized one?
if (file_exists($boarddir . '/agreement.' . $user_info['language'] . '.txt'))
$context['agreement_file'] = $boarddir . '/agreement.' . $user_info['language'] . '.txt';
elseif (file_exists($boarddir . '/agreement.txt'))
$context['agreement_file'] = $boarddir . '/agreement.txt';

if (!empty($context['agreement_file']))
{
$cache_id = strtr($context['agreement_file'], array($boarddir => '', '.txt' => '', '.' => '_'));
$context['agreement'] = parse_bbc(file_get_contents($context['agreement_file']), true, $cache_id);
}
elseif ($context['can_accept_agreement'])
fatal_lang_error('error_no_agreement', false);
}

if (!$context['accept_doc'] || $context['can_accept_privacy_policy'])
{
// Have we got a localized policy?
if (!empty($modSettings['policy_' . $user_info['language']]))
$context['privacy_policy'] = parse_bbc($modSettings['policy_' . $user_info['language']]);
elseif (!empty($modSettings['policy_' . $language]))
$context['privacy_policy'] = parse_bbc($modSettings['policy_' . $language]);
// Then I guess we've got nothing
elseif ($context['can_accept_privacy_policy'])
fatal_lang_error('error_no_privacy_policy', false);
}
}

function canRequireAgreement()
{
global $boarddir, $context, $modSettings, $options, $user_info;

// Guests can't agree
if (!empty($user_info['is_guest']) || empty($modSettings['requireAgreement']))
return false;

$agreement_lang = file_exists($boarddir . '/agreement.' . $user_info['language'] . '.txt') ? $user_info['language'] : 'default';

if (empty($modSettings['agreement_updated_' . $agreement_lang]))
return false;

$context['agreement_accepted_date'] = empty($options['agreement_accepted']) ? 0 : $options['agreement_accepted'];

// A new timestamp means that there are new changes to the registration agreement and must therefore be shown.
return empty($options['agreement_accepted']) || $modSettings['agreement_updated_' . $agreement_lang] > $options['agreement_accepted'];
}

function canRequirePrivacyPolicy()
{
global $modSettings, $options, $user_info, $language, $context;

if (!empty($user_info['is_guest']) || empty($modSettings['requirePolicyAgreement']))
return false;

$policy_lang = !empty($modSettings['policy_' . $user_info['language']]) ? $user_info['language'] : $language;

if (empty($modSettings['policy_updated_' . $policy_lang]))
return false;

$context['privacy_policy_accepted_date'] = empty($options['policy_accepted']) ? 0 : $options['policy_accepted'];

return empty($options['policy_accepted']) || $modSettings['policy_updated_' . $policy_lang] > $options['policy_accepted'];
}

// Let's tell them there's a new agreement
function Agreement()
{
global $context, $modSettings, $scripturl, $smcFunc, $txt;

prepareAgreementContext();

loadLanguage('Agreement');
loadTemplate('Agreement');

$page_title = '';
if (!empty($context['agreement']) && !empty($context['privacy_policy']))
$page_title = $txt['agreement_and_privacy_policy'];
elseif (!empty($context['agreement']))
$page_title = $txt['agreement'];
elseif (!empty($context['privacy_policy']))
$page_title = $txt['privacy_policy'];

$context['page_title'] = $page_title;
$context['linktree'][] = array(
'url' => $scripturl . '?action=agreement',
'name' => $context['page_title'],
);

if (isset($_SESSION['old_url']))
$_SESSION['redirect_url'] = $_SESSION['old_url'];

}

// I solemly swear to no longer chase squirrels.
function AcceptAgreement()
{
global $context, $modSettings, $smcFunc, $user_info;

$can_accept_agreement = !empty($modSettings['requireAgreement']) && canRequireAgreement();
$can_accept_privacy_policy = !empty($modSettings['requirePolicyAgreement']) && canRequirePrivacyPolicy();

if ($can_accept_agreement || $can_accept_privacy_policy)
{
checkSession();

if ($can_accept_agreement)
{
$smcFunc['db_insert']('replace',
'{db_prefix}themes',
array('id_member' => 'int', 'id_theme' => 'int', 'variable' => 'string', 'value' => 'string'),
array($user_info['id'], 1, 'agreement_accepted', time()),
array('id_member', 'id_theme', 'variable')
);
logAction('agreement_accepted', array('applicator' => $user_info['id']), 'user');
}

if ($can_accept_privacy_policy)
{
$smcFunc['db_insert']('replace',
'{db_prefix}themes',
array('id_member' => 'int', 'id_theme' => 'int', 'variable' => 'string', 'value' => 'string'),
array($user_info['id'], 1, 'policy_accepted', time()),
array('id_member', 'id_theme', 'variable')
);
logAction('policy_accepted', array('applicator' => $user_info['id']), 'user');
}
}

// Redirect back to chasing those squirrels, er, viewing those memes.
redirectexit(!empty($_SESSION['redirect_url']) ? $_SESSION['redirect_url'] : '');
}

?>
132 changes: 110 additions & 22 deletions Sources/Attachments.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,40 +6,105 @@
* Simple Machines Forum (SMF)
*
* @package SMF
* @author Simple Machines http://www.simplemachines.org
* @copyright 2017 Simple Machines and individual contributors
* @license http://www.simplemachines.org/about/smf/license.php BSD
* @author Simple Machines https://www.simplemachines.org
* @copyright 2022 Simple Machines and individual contributors
* @license https://www.simplemachines.org/about/smf/license.php BSD
*
* @version 2.1 Beta 4
* @version 2.1.2
*/

if (!defined('SMF'))
die('No direct access...');

/**
* Class Attachments
*
* This class handles adding/deleting attachments
*/
class Attachments
{
/**
* @var int $_msg The ID of the message this attachment is associated with
*/
protected $_msg = 0;

/**
* @var int|null $_board The ID of the board this attachment's post is in or null if it's not set
*/
protected $_board = null;

/**
* @var string|bool $_attachmentUploadDir An array of info about attachment upload directories or false
*/
protected $_attachmentUploadDir = false;

/**
* @var string $_attchDir The path to the current attachment directory
*/
protected $_attchDir = '';

/**
* @var int $_currentAttachmentUploadDir ID of the current attachment directory
*/
protected $_currentAttachmentUploadDir;

/**
* @var bool $_canPostAttachment Whether or not an attachment can be posted
*/
protected $_canPostAttachment;

/**
* @var array $_generalErrors An array of information about any errors that occurred
*/
protected $_generalErrors = array();

/**
* @var mixed $_initialError Not used?
*/
protected $_initialError;

/**
* @var array $_attachments Not used?
*/
protected $_attachments = array();

/**
* @var array $_attachResults An array of information about the results of each file
*/
protected $_attachResults = array();

/**
* @var array $_attachSuccess An array of information about successful attachments
*/
protected $_attachSuccess = array();

/**
* @var array $_response An array of response information. @used-by \sendResponse() when adding attachments
*/
protected $_response = array(
'error' => true,
'data' => array(),
'extra' => '',
);

/**
* @var array $_subActions An array of all valid sub-actions
*/
protected $_subActions = array(
'add',
'delete',
);

/**
* @var string|bool $_sa The current sub-action, or false if there isn't one
*/
protected $_sa = false;

/**
* Attachments constructor.
*
* Sets up some initial information - the message ID, board, current attachment upload dir, etc.
*/
public function __construct()
{
global $modSettings, $context;
Expand All @@ -56,15 +121,15 @@ public function __construct()
$this->_canPostAttachment = $context['can_post_attachment'] = !empty($modSettings['attachmentEnable']) && $modSettings['attachmentEnable'] == 1 && (allowedTo('post_attachment', $this->_board) || ($modSettings['postmod_active'] && allowedTo('post_unapproved_attachments', $this->_board)));
}

/**
* Handles calling the appropriate function based on the sub-action
*/
public function call()
{
global $smcFunc, $sourcedir;

require_once($sourcedir . '/Subs-Attachments.php');

// Guest aren't welcome, sorry.
is_not_guest();

// Need this. For reasons...
loadLanguage('Post');

Expand All @@ -76,7 +141,7 @@ public function call()
// Just send a generic message.
else
$this->setResponse(array(
'text' => $this->_sa == 'add' ? 'attach_error_title' : 'attached_file_deleted_error',
'text' => $this->_sa == 'add' ? 'attach_error_title' : 'attached_file_deleted_error',
'type' => 'error',
'data' => false,
));
Expand All @@ -85,6 +150,9 @@ public function call()
$this->sendResponse();
}

/**
* Handles deleting the attachment
*/
public function delete()
{
global $sourcedir;
Expand Down Expand Up @@ -116,6 +184,9 @@ public function delete()
));
}

/**
* Handles adding an attachment
*/
public function add()
{
// You gotta be able to post attachments.
Expand All @@ -131,7 +202,7 @@ public function add()

// The attachments was created and moved the the right folder, time to update the DB.
if (!empty($_SESSION['temp_attachments']))
$this->createAtttach();
$this->createAttach();

// Set the response.
$this->setResponse();
Expand Down Expand Up @@ -255,9 +326,9 @@ protected function processAttachments()
if (empty($errors))
{
// The reported MIME type of the attachment might not be reliable.
// Fortunately, PHP 5.3+ lets us easily verify the real MIME type.
if (function_exists('mime_content_type'))
$_FILES['attachment']['type'][$n] = mime_content_type($_FILES['attachment']['tmp_name'][$n]);
$detected_mime_type = get_mime_type($_FILES['attachment']['tmp_name'][$n], true);
if ($detected_mime_type !== false)
$_FILES['attachment']['type'][$n] = $detected_mime_type;

$_SESSION['temp_attachments'][$attachID] = array(
'name' => $smcFunc['htmlspecialchars'](basename($_FILES['attachment']['name'][$n])),
Expand Down Expand Up @@ -313,14 +384,18 @@ protected function processAttachments()
call_integration_hook('integrate_attachment_upload', array());
}

protected function createAtttach()
/**
* Actually attaches the file
*/
protected function createAttach()
{
global $txt, $user_info, $modSettings;

// Create an empty session var to keep track of all the files we attached.
$SESSION['already_attached'] = array();
if (!isset($_SESSION['already_attached']))
$_SESSION['already_attached'] = array();

foreach ($_SESSION['temp_attachments'] as $attachID => $attachment)
foreach ($_SESSION['temp_attachments'] as $attachID => $attachment)
{
$attachmentOptions = array(
'post' => $this->_msg,
Expand Down Expand Up @@ -358,7 +433,7 @@ protected function createAtttach()

foreach ($attachment['errors'] as $error)
{
$attachmentOptions['errors'][] = vsprintf($txt['attach_warning'], $attachment['name']);
$attachmentOptions['errors'][] = sprintf($txt['attach_warning'], $attachment['name']);

if (!is_array($error))
{
Expand All @@ -367,7 +442,7 @@ protected function createAtttach()
log_error($attachment['name'] . ': ' . $txt[$error], 'critical');
}
else
$attachmentOptions['errors'][] = vsprintf($txt[$error[0]], $error[1]);
$attachmentOptions['errors'][] = vsprintf($txt[$error[0]], (array) $error[1]);
}
if (file_exists($attachment['tmp_name']))
unlink($attachment['tmp_name']);
Expand All @@ -386,8 +461,19 @@ protected function createAtttach()
$this->_attachSuccess = $_SESSION['already_attached'];

unset($_SESSION['temp_attachments']);

// Allow user to see previews for all of this post's attachments, even if the post hasn't been submitted yet.
if (!isset($_SESSION['attachments_can_preview']))
$_SESSION['attachments_can_preview'] = array();
if (!empty($_SESSION['already_attached']))
$_SESSION['attachments_can_preview'] += array_fill_keys(array_keys($_SESSION['already_attached']), true);
}

/**
* Sets up the response information
*
* @param array $data Data for the response if we're not adding an attachment
*/
protected function setResponse($data = array())
{
global $txt;
Expand All @@ -405,12 +491,12 @@ protected function setResponse($data = array())
// Is there any generic errors? made some sense out of them!
if ($this->_generalErrors)
foreach ($this->_generalErrors as $k => $v)
$this->_generalErrors[$k] = (is_array($v) ? vsprintf($txt[$v[0]], $v[1]) : $txt[$v]);
$this->_generalErrors[$k] = (is_array($v) ? vsprintf($txt[$v[0]], (array) $v[1]) : $txt[$v]);

// Gotta urlencode the filename.
if ($this->_attachResults)
foreach ($this->_attachResults as $k => $v)
$this->_attachResults[$k]['name'] = urlencode($this->_attachResults[$k]['name']);
$this->_attachResults[$k]['name'] = urlencode($this->_attachResults[$k]['name']);

$this->_response = array(
'files' => $this->_attachResults ? $this->_attachResults : false,
Expand All @@ -424,20 +510,22 @@ protected function setResponse($data = array())
$this->_response['text'] = $txt[$data['text']];
}

/**
* Sends the response data
*/
protected function sendResponse()
{
global $smcFunc, $modSettings, $context;

ob_end_clean();

if (!empty($modSettings['CompressedOutput']))
if (!empty($modSettings['enableCompressedOutput']))
@ob_start('ob_gzhandler');

else
ob_start();

// Set the header.
header('Content-Type: application/json; charset='. $context['character_set'] .'');
header('content-type: application/json; charset=' . $context['character_set'] . '');

echo $smcFunc['json_encode']($this->_response ? $this->_response : array());

Expand Down
30 changes: 17 additions & 13 deletions Sources/BoardIndex.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,11 @@
* Simple Machines Forum (SMF)
*
* @package SMF
* @author Simple Machines http://www.simplemachines.org
* @copyright 2017 Simple Machines and individual contributors
* @license http://www.simplemachines.org/about/smf/license.php BSD
* @author Simple Machines https://www.simplemachines.org
* @copyright 2023 Simple Machines and individual contributors
* @license https://www.simplemachines.org/about/smf/license.php BSD
*
* @version 2.1 Beta 4
* @version 2.1.4
*/

if (!defined('SMF'))
Expand Down Expand Up @@ -79,15 +79,15 @@ function BoardIndex()
'include_events' => $modSettings['cal_showevents'] > 1,
'num_days_shown' => empty($modSettings['cal_days_for_index']) || $modSettings['cal_days_for_index'] < 1 ? 1 : $modSettings['cal_days_for_index'],
);
$context += cache_quick_get('calendar_index_offset_' . ($user_info['time_offset'] + $modSettings['time_offset']), 'Subs-Calendar.php', 'cache_getRecentEvents', array($eventOptions));
$context += cache_quick_get('calendar_index_offset_' . $user_info['time_offset'], 'Subs-Calendar.php', 'cache_getRecentEvents', array($eventOptions));

// Whether one or multiple days are shown on the board index.
$context['calendar_only_today'] = $modSettings['cal_days_for_index'] == 1;

// This is used to show the "how-do-I-edit" help.
$context['calendar_can_edit'] = allowedTo('calendar_edit_any');

if ($context['show_calendar'])
if (!empty($context['show_calendar']))
$context['info_center'][] = array(
'tpl' => 'calendar',
'txt' => $context['calendar_only_today'] ? 'calendar_today' : 'calendar_upcoming',
Expand All @@ -98,9 +98,9 @@ function BoardIndex()
$context['show_stats'] = allowedTo('view_stats') && !empty($modSettings['trackStats']);
if ($settings['show_stats_index'])
$context['info_center'][] = array(
'tpl' => 'stats',
'txt' => 'forum_stats',
);
'tpl' => 'stats',
'txt' => 'forum_stats',
);

// Now the online stuff
require_once($sourcedir . '/Subs-MembersOnline.php');
Expand All @@ -113,13 +113,13 @@ function BoardIndex()
$context['show_buddies'] = !empty($user_info['buddies']);
$context['show_who'] = allowedTo('who_view') && !empty($modSettings['who_enabled']);
$context['info_center'][] = array(
'tpl' => 'online',
'txt' => 'online_users',
);
'tpl' => 'online',
'txt' => 'online_users',
);

// Track most online statistics? (Subs-MembersOnline.php)
if (!empty($modSettings['trackStats']))
trackStatsUsersOnline($context['num_guests'] + $context['num_spiders'] + $context['num_users_online']);
trackStatsUsersOnline($context['num_guests'] + $context['num_users_online']);

// Are we showing all membergroups on the board index?
if (!empty($settings['show_group_key']))
Expand All @@ -133,6 +133,10 @@ function BoardIndex()
'markread' => array('text' => 'mark_as_read', 'image' => 'markread.png', 'custom' => 'data-confirm="' . $txt['are_sure_mark_read'] . '"', 'class' => 'you_sure', 'url' => $scripturl . '?action=markasread;sa=all;' . $context['session_var'] . '=' . $context['session_id']),
);

// Replace the collapse and expand default alts.
addJavaScriptVar('smf_expandAlt', $txt['show_category'], true);
addJavaScriptVar('smf_collapseAlt', $txt['hide_category'], true);

// Allow mods to add additional buttons here
call_integration_hook('integrate_mark_read_button');

Expand Down
37 changes: 28 additions & 9 deletions Sources/CacheAPI-apcu.php → Sources/Cache/APIs/Apcu.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,21 +4,27 @@
* Simple Machines Forum (SMF)
*
* @package SMF
* @author Simple Machines http://www.simplemachines.org
* @copyright 2017 Simple Machines and individual contributors
* @license http://www.simplemachines.org/about/smf/license.php BSD
* @author Simple Machines https://www.simplemachines.org
* @copyright 2022 Simple Machines and individual contributors
* @license https://www.simplemachines.org/about/smf/license.php BSD
*
* @version 2.1 Beta 4
* @version 2.1.0
*/

namespace SMF\Cache\APIs;

use SMF\Cache\CacheApi;
use SMF\Cache\CacheApiInterface;

if (!defined('SMF'))
die('Hacking attempt...');
die('No direct access...');

/**
* Our Cache API class
* @package cacheAPI
*
* @package CacheAPI
*/
class apcu_cache extends cache_api
class Apcu extends CacheApi implements CacheApiInterface
{
/**
* {@inheritDoc}
Expand All @@ -29,17 +35,28 @@ public function isSupported($test = false)

if ($test)
return $supported;

return parent::isSupported() && $supported;
}

/**
* {@inheritDoc}
*/
public function connect()
{
return true;
}

/**
* {@inheritDoc}
*/
public function getData($key, $ttl = null)
{
$key = $this->prefix . strtr($key, ':/', '-_');

return apcu_fetch($key . 'smf');
$value = apcu_fetch($key . 'smf');

return !empty($value) ? $value : null;
}

/**
Expand All @@ -52,8 +69,9 @@ public function putData($key, $value, $ttl = null)
// An extended key is needed to counteract a bug in APC.
if ($value === null)
return apcu_delete($key . 'smf');

else
return apcu_store($key . 'smf', $value, $ttl);
return apcu_store($key . 'smf', $value, $ttl !== null ? $ttl : $this->ttl);
}

/**
Expand All @@ -62,6 +80,7 @@ public function putData($key, $value, $ttl = null)
public function cleanCache($type = '')
{
$this->invalidateCache();

return apcu_clear_cache();
}

Expand Down
172 changes: 114 additions & 58 deletions Sources/CacheAPI-smf.php → Sources/Cache/APIs/FileBased.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,21 +4,29 @@
* Simple Machines Forum (SMF)
*
* @package SMF
* @author Simple Machines http://www.simplemachines.org
* @copyright 2017 Simple Machines and individual contributors
* @license http://www.simplemachines.org/about/smf/license.php BSD
* @author Simple Machines https://www.simplemachines.org
* @copyright 2023 Simple Machines and individual contributors
* @license https://www.simplemachines.org/about/smf/license.php BSD
*
* @version 2.1 Beta 4
* @version 2.1.4
*/

namespace SMF\Cache\APIs;

use GlobIterator;
use FilesystemIterator;
use SMF\Cache\CacheApi;
use SMF\Cache\CacheApiInterface;

if (!defined('SMF'))
die('Hacking attempt...');
die('No direct access...');

/**
* Our Cache API class
* @package cacheAPI
*
* @package CacheAPI
*/
class smf_cache extends cache_api
class FileBased extends CacheApi implements CacheApiInterface
{
/**
* @var string The path to the current $cachedir directory.
Expand All @@ -45,69 +53,119 @@ public function isSupported($test = false)

if ($test)
return $supported;

return parent::isSupported() && $supported;
}

private function readFile($file)
{
if (($fp = @fopen($file, 'rb')) !== false)
{
if (!flock($fp, LOCK_SH))
{
fclose($fp);
return false;
}
$string = '';
while (!feof($fp))
$string .= fread($fp, 8192);

flock($fp, LOCK_UN);
fclose($fp);

return $string;
}

return false;
}

private function writeFile($file, $string)
{
if (($fp = fopen($file, 'cb')) !== false)
{
if (!flock($fp, LOCK_EX))
{
fclose($fp);
return false;
}
ftruncate($fp, 0);
$bytes = 0;
$pieces = str_split($string, 8192);
foreach ($pieces as $piece)
{
if (($val = fwrite($fp, $piece, 8192)) !== false)
$bytes += $val;
else
return false;
}
fflush($fp);
flock($fp, LOCK_UN);
fclose($fp);

return $bytes;
}

return false;
}

/**
* {@inheritDoc}
*/
public function connect()
{
return true;
}

/**
* {@inheritDoc}
*/
public function getData($key, $ttl = null)
{
$key = $this->prefix . strtr($key, ':/', '-_');
$cachedir = $this->cachedir;
$file = sprintf('%s/data_%s.cache',
$this->cachedir,
$this->prefix . strtr($key, ':/', '-_')
);

// SMF Data returns $value and $expired. $expired has a unix timestamp of when this expires.
if (file_exists($cachedir . '/data_' . $key . '.php') && filesize($cachedir . '/data_' . $key . '.php') > 10)
if (file_exists($file) && ($raw = $this->readFile($file)) !== false)
{
// Work around Zend's opcode caching (PHP 5.5+), they would cache older files for a couple of seconds
// causing newer files to take effect a while later.
if (function_exists('opcache_invalidate'))
opcache_invalidate($cachedir . '/data_' . $key . '.php', true);

if (function_exists('apc_delete_file'))
@apc_delete_file($cachedir . '/data_' . $key . '.php');

// php will cache file_exists et all, we can't 100% depend on its results so proceed with caution
@include($cachedir . '/data_' . $key . '.php');
if (!empty($expired) && isset($value))
{
@unlink($cachedir . '/data_' . $key . '.php');
unset($value);
}
if (($value = smf_json_decode($raw, true, false)) !== array() && isset($value['expiration']) && $value['expiration'] >= time())
return $value['value'];
else
@unlink($file);
}

return !empty($value) ? $value : null;
return null;
}

/**
* {@inheritDoc}
*/
public function putData($key, $value, $ttl = null)
{
$key = $this->prefix . strtr($key, ':/', '-_');
$cachedir = $this->cachedir;
$file = sprintf('%s/data_%s.cache',
$this->cachedir,
$this->prefix . strtr($key, ':/', '-_')
);
$ttl = $ttl !== null ? $ttl : $this->ttl;

// Work around Zend's opcode caching (PHP 5.5+), they would cache older files for a couple of seconds
// causing newer files to take effect a while later.
if (function_exists('opcache_invalidate'))
opcache_invalidate($cachedir . '/data_' . $key . '.php', true);

if (function_exists('apc_delete_file'))
@apc_delete_file($cachedir . '/data_' . $key . '.php');

// Otherwise custom cache?
if ($value === null)
@unlink($cachedir . '/data_' . $key . '.php');
@unlink($file);
else
{
$cache_data = '<' . '?' . 'php if (!defined(\'SMF\')) die; if (' . (time() + $ttl) . ' < time()) $expired = true; else{$expired = false; $value = \'' . addcslashes($value, '\\\'') . '\';}' . '?' . '>';
$cache_data = json_encode(
array(
'expiration' => time() + $ttl,
'value' => $value
),
JSON_NUMERIC_CHECK
);

// Write out the cache file, check that the cache write was successful; all the data must be written
// If it fails due to low diskspace, or other, remove the cache file
$fileSize = file_put_contents($cachedir . '/data_' . $key . '.php', $cache_data, LOCK_EX);
if ($fileSize !== strlen($cache_data))
if ($this->writeFile($file, $cache_data) !== strlen($cache_data))
{
@unlink($cachedir . '/data_' . $key . '.php');
@unlink($file);
return false;
}
else
Expand All @@ -120,20 +178,15 @@ public function putData($key, $value, $ttl = null)
*/
public function cleanCache($type = '')
{
$cachedir = $this->cachedir;

// No directory = no game.
if (!is_dir($cachedir))
if (!is_dir($this->cachedir))
return;

// Remove the files in SMF's own disk cache, if any
$dh = opendir($cachedir);
while ($file = readdir($dh))
{
if ($file != '.' && $file != '..' && $file != 'index.php' && $file != '.htaccess' && (!$type || substr($file, 0, strlen($type)) == $type))
@unlink($cachedir . '/' . $file);
}
closedir($dh);
$files = new GlobIterator($this->cachedir . '/' . $type . '*.cache', FilesystemIterator::NEW_CURRENT_AND_KEY);

foreach ($files as $file => $info)
unlink($this->cachedir . '/' . $file);

// Make this invalid.
$this->invalidateCache();
Expand Down Expand Up @@ -162,16 +215,20 @@ public function cacheSettings(array &$config_vars)
{
global $context, $txt;

$config_vars[] = $txt['cache_smf_settings'];
$class_name = $this->getImplementationClassKeyName();
$class_name_txt_key = strtolower($class_name);

$config_vars[] = $txt['cache_'. $class_name_txt_key .'_settings'];
$config_vars[] = array('cachedir', $txt['cachedir'], 'file', 'text', 36, 'cache_cachedir');

if (!isset($context['settings_post_javascript']))
$context['settings_post_javascript'] = '';

$context['settings_post_javascript'] .= '
if (empty($context['settings_not_writable']))
$context['settings_post_javascript'] .= '
$("#cache_accelerator").change(function (e) {
var cache_type = e.currentTarget.value;
$("#cachedir").prop("disabled", cache_type != "smf");
$("#cachedir").prop("disabled", cache_type != "'. $class_name .'");
});';
}

Expand All @@ -189,6 +246,7 @@ public function setCachedir($dir = null)
// If its invalid, use SMF's.
if (is_null($dir) || !is_writable($dir))
$this->cachedir = $cachedir;

else
$this->cachedir = $dir;
}
Expand All @@ -209,9 +267,7 @@ public function getCachedir()
*/
public function getVersion()
{
global $forum_version;

return isset($forum_version) ? $forum_version : '2.1';
return SMF_VERSION;
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,24 +4,33 @@
* Simple Machines Forum (SMF)
*
* @package SMF
* @author Simple Machines http://www.simplemachines.org
* @copyright 2017 Simple Machines and individual contributors
* @license http://www.simplemachines.org/about/smf/license.php BSD
* @author Simple Machines https://www.simplemachines.org
* @copyright 2022 Simple Machines and individual contributors
* @license https://www.simplemachines.org/about/smf/license.php BSD
*
* @version 2.1 Beta 4
* @version 2.1.2
*/

namespace SMF\Cache\APIs;

use Memcache;
use SMF\Cache\CacheApi;
use SMF\Cache\CacheApiInterface;

if (!defined('SMF'))
die('Hacking attempt...');
die('No direct access...');

/**
* Our Cache API class
* @package cacheAPI
*
* @package CacheAPI
*/
class memcache_cache extends cache_api
class MemcacheImplementation extends CacheApi implements CacheApiInterface
{
const CLASS_KEY = 'cache_memcached';

/**
* @var \Memcache The memcache instance.
* @var Memcache The memcache instance.
*/
private $memcache = null;

Expand All @@ -32,10 +41,11 @@ public function isSupported($test = false)
{
global $cache_memcached;

$supported = class_exists('memcache');
$supported = class_exists('Memcache');

if ($test)
return $supported;

return parent::isSupported() && $supported && !empty($cache_memcached);
}

Expand All @@ -46,6 +56,8 @@ public function connect()
{
global $db_persist, $cache_memcached;

$this->memcache = new Memcache();

$servers = explode(',', $cache_memcached);
$port = 0;

Expand All @@ -57,12 +69,17 @@ public function connect()
while (!$connected && $level < count($servers))
{
++$level;
$this->memcache = new Memcache();

$server = trim($servers[array_rand($servers)]);

// No server, can't connect to this.
if (empty($server))
continue;

// Normal host names do not contain slashes, while e.g. unix sockets do. Assume alternative transport pipe with port 0.
if (strpos($server,'/') !== false)
if (strpos($server, '/') !== false)
$host = $server;

else
{
$server = explode(':', $server);
Expand All @@ -73,6 +90,7 @@ public function connect()
// Don't wait too long: yes, we want the server, but we might be able to run the query faster!
if (empty($db_persist))
$connected = $this->memcache->connect($host, $port);

else
$connected = $this->memcache->pconnect($host, $port);
}
Expand All @@ -92,6 +110,7 @@ public function getData($key, $ttl = null)
// $value should return either data or false (from failure, key not found or empty array).
if ($value === false)
return null;

return $value;
}

Expand All @@ -102,7 +121,7 @@ public function putData($key, $value, $ttl = null)
{
$key = $this->prefix . strtr($key, ':/', '-_');

return $this->memcache->set($key, $value, 0, $ttl);
return $this->memcache->set($key, $value, 0, $ttl !== null ? $ttl : $this->ttl);
}

/**
Expand All @@ -119,6 +138,7 @@ public function quit()
public function cleanCache($type = '')
{
$this->invalidateCache();

return $this->memcache->flush();
}

Expand All @@ -129,16 +149,26 @@ public function cacheSettings(array &$config_vars)
{
global $context, $txt;

$config_vars[] = $txt['cache_memcache_settings'];
$config_vars[] = array('cache_memcached', $txt['cache_memcache_servers'], 'file', 'text', 0, 'cache_memcached', 'postinput' => '<br><div class="smalltext"><em>' . $txt['cache_memcache_servers_subtext'] . '</em></div>');
if (!in_array($txt[self::CLASS_KEY .'_settings'], $config_vars))
{
$config_vars[] = $txt[self::CLASS_KEY .'_settings'];
$config_vars[] = array(
self::CLASS_KEY,
$txt[self::CLASS_KEY .'_servers'],
'file',
'text',
0,
'subtext' => $txt[self::CLASS_KEY .'_servers_subtext']);
}

if (!isset($context['settings_post_javascript']))
$context['settings_post_javascript'] = '';

$context['settings_post_javascript'] .= '
if (empty($context['settings_not_writable']))
$context['settings_post_javascript'] .= '
$("#cache_accelerator").change(function (e) {
var cache_type = e.currentTarget.value;
$("#cache_memcached").prop("disabled", cache_type != "memcache");
$("#'. self::CLASS_KEY .'").prop("disabled", cache_type != "MemcacheImplementation" && cache_type != "MemcachedImplementation");
});';
}

Expand All @@ -147,7 +177,16 @@ public function cacheSettings(array &$config_vars)
*/
public function getVersion()
{
return $this->memcache->getVersion();
if (!is_object($this->memcache))
return false;

// This gets called in Subs-Admin getServerVersions when loading up support information. If we can't get a connection, return nothing.
$result = $this->memcache->getVersion();

if (!empty($result))
return $result;

return false;
}
}

Expand Down
213 changes: 213 additions & 0 deletions Sources/Cache/APIs/MemcachedImplementation.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,213 @@
<?php

/**
* Simple Machines Forum (SMF)
*
* @package SMF
* @author Simple Machines https://www.simplemachines.org
* @copyright 2022 Simple Machines and individual contributors
* @license https://www.simplemachines.org/about/smf/license.php BSD
*
* @version 2.1.2
*/

namespace SMF\Cache\APIs;

use Memcached;
use SMF\Cache\CacheApi;
use SMF\Cache\CacheApiInterface;

if (!defined('SMF'))
die('No direct access...');

/**
* Our Cache API class
*
* @package CacheAPI
*/
class MemcachedImplementation extends CacheApi implements CacheApiInterface
{
const CLASS_KEY = 'cache_memcached';

/** @var Memcached The memcache instance. */
private $memcached = null;

/** @var string[] */
private $servers;

/**
* {@inheritDoc}
*/
public function __construct()
{
global $cache_memcached;

$this->servers = array_map(
function($server)
{
if (strpos($server, '/') !== false)
return array($server, 0);

else
{
$server = explode(':', $server);
return array($server[0], isset($server[1]) ? (int) $server[1] : 11211);
}
},
explode(',', $cache_memcached)
);

parent::__construct();
}

/**
* {@inheritDoc}
*/
public function isSupported($test = false)
{
global $cache_memcached;

$supported = class_exists('Memcached');

if ($test)
return $supported;

return parent::isSupported() && $supported && !empty($cache_memcached);
}

/**
* {@inheritDoc}
*/
public function connect()
{
$this->memcached = new Memcached;

return $this->addServers();
}

/**
* Add memcached servers.
*
* Don't add servers if they already exist. Ideal for persistent connections.
*
* @return bool True if there are servers in the daemon, false if not.
*/
protected function addServers()
{
$currentServers = $this->memcached->getServerList();
$retVal = !empty($currentServers);
foreach ($this->servers as $server)
{
// Figure out if we have this server or not
$foundServer = false;
foreach ($currentServers as $currentServer)
{
if ($server[0] == $currentServer['host'] && $server[1] == $currentServer['port'])
{
$foundServer = true;
break;
}
}

// Found it?
if (empty($foundServer))
$retVal |= $this->memcached->addServer($server[0], $server[1]);
}

return $retVal;
}

/**
* {@inheritDoc}
*/
public function getData($key, $ttl = null)
{
$key = $this->prefix . strtr($key, ':/', '-_');

$value = $this->memcached->get($key);

// $value should return either data or false (from failure, key not found or empty array).
if ($value === false)
return null;

return $value;
}

/**
* {@inheritDoc}
*/
public function putData($key, $value, $ttl = null)
{
$key = $this->prefix . strtr($key, ':/', '-_');

return $this->memcached->set($key, $value, $ttl !== null ? $ttl : $this->ttl);
}

/**
* {@inheritDoc}
*/
public function cleanCache($type = '')
{
$this->invalidateCache();

// Memcached accepts a delay parameter, always use 0 (instant).
return $this->memcached->flush(0);
}

/**
* {@inheritDoc}
*/
public function quit()
{
return $this->memcached->quit();
}

/**
* {@inheritDoc}
*/
public function cacheSettings(array &$config_vars)
{
global $context, $txt;

if (!in_array($txt[self::CLASS_KEY .'_settings'], $config_vars))
{
$config_vars[] = $txt[self::CLASS_KEY .'_settings'];
$config_vars[] = array(
self::CLASS_KEY,
$txt[self::CLASS_KEY .'_servers'],
'file',
'text',
0,
'subtext' => $txt[self::CLASS_KEY .'_servers_subtext']);
}

if (!isset($context['settings_post_javascript']))
$context['settings_post_javascript'] = '';

if (empty($context['settings_not_writable']))
$context['settings_post_javascript'] .= '
$("#cache_accelerator").change(function (e) {
var cache_type = e.currentTarget.value;
$("#'. self::CLASS_KEY .'").prop("disabled", cache_type != "MemcacheImplementation" && cache_type != "MemcachedImplementation");
});';
}

/**
* {@inheritDoc}
*/
public function getVersion()
{
if (!is_object($this->memcached))
return false;

// This gets called in Subs-Admin getServerVersions when loading up support information. If we can't get a connection, return nothing.
$result = $this->memcached->getVersion();

if (!empty($result))
return current($result);

return false;
}
}

?>
220 changes: 220 additions & 0 deletions Sources/Cache/APIs/Postgres.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,220 @@
<?php

/**
* Simple Machines Forum (SMF)
*
* @package SMF
* @author Simple Machines https://www.simplemachines.org
* @copyright 2022 Simple Machines and individual contributors
* @license https://www.simplemachines.org/about/smf/license.php BSD
*
* @version 2.1.0
*/

namespace SMF\Cache\APIs;

use SMF\Cache\CacheApi;
use SMF\Cache\CacheApiInterface;

if (!defined('SMF'))
die('No direct access...');

/**
* PostgreSQL Cache API class
*
* @package CacheAPI
*/
class Postgres extends CacheApi implements CacheApiInterface
{
/** @var string */
private $db_prefix;

/** @var resource result of pg_connect. */
private $db_connection;

public function __construct()
{
global $db_prefix, $db_connection;

$this->db_prefix = $db_prefix;
$this->db_connection = $db_connection;

parent::__construct();
}

/**
* {@inheritDoc}
*/
public function connect()
{
$result = pg_query_params($this->db_connection, 'SELECT 1
FROM pg_tables
WHERE schemaname = $1
AND tablename = $2',
array(
'public',
$this->db_prefix . 'cache',
)
);

if (pg_affected_rows($result) === 0)
pg_query($this->db_connection, 'CREATE UNLOGGED TABLE ' . $this->db_prefix . 'cache (key text, value text, ttl bigint, PRIMARY KEY (key))');

$this->prepareQueries(
array(
'smf_cache_get_data',
'smf_cache_put_data',
'smf_cache_delete_data',
),
array(
'SELECT value FROM ' . $this->db_prefix . 'cache WHERE key = $1 AND ttl >= $2 LIMIT 1',
'INSERT INTO ' . $this->db_prefix . 'cache(key,value,ttl) VALUES($1,$2,$3)
ON CONFLICT(key) DO UPDATE SET value = $2, ttl = $3',
'DELETE FROM ' . $this->db_prefix . 'cache WHERE key = $1',
)
);

return true;
}

/**
* Stores a prepared SQL statement, ensuring that it's not done twice.
*
* @param array $stmtnames
* @param array $queries
*/
private function prepareQueries(array $stmtnames, array $queries)
{
$result = pg_query_params(
$this->db_connection,
'SELECT name FROM pg_prepared_statements WHERE name = ANY ($1)',
array('{' . implode(', ', $stmtnames) . '}')
);

$arr = pg_num_rows($result) == 0 ? array() : array_map(
function($el)
{
return $el['name'];
},
pg_fetch_all($result)
);
foreach ($stmtnames as $idx => $stmtname)
if (!in_array($stmtname, $arr))
pg_prepare($this->db_connection, $stmtname, $queries[$idx]);
}

/**
* {@inheritDoc}
*/
public function isSupported($test = false)
{
global $smcFunc;

if ($smcFunc['db_title'] !== POSTGRE_TITLE)
return false;

$result = pg_query($this->db_connection, 'SHOW server_version_num');
$res = pg_fetch_assoc($result);

if ($res['server_version_num'] < 90500)
return false;

return $test ? true : parent::isSupported();
}

/**
* {@inheritDoc}
*/
public function getData($key, $ttl = null)
{
$result = pg_execute($this->db_connection, 'smf_cache_get_data', array($key, time()));

if (pg_affected_rows($result) === 0)
return null;

$res = pg_fetch_assoc($result);

return $res['value'];
}

/**
* {@inheritDoc}
*/
public function putData($key, $value, $ttl = null)
{
$ttl = time() + (int) ($ttl !== null ? $ttl : $this->ttl);

if ($value === null)
$result = pg_execute($this->db_connection, 'smf_cache_delete_data', array($key));
else
$result = pg_execute($this->db_connection, 'smf_cache_put_data', array($key, $value, $ttl));

return pg_affected_rows($result) > 0;
}

/**
* {@inheritDoc}
*/
public function cleanCache($type = '')
{
if ($type == 'expired')
pg_query($this->db_connection, 'DELETE FROM ' . $this->db_prefix . 'cache WHERE ttl < ' . time() . ';');
else
pg_query($this->db_connection, 'TRUNCATE ' . $this->db_prefix . 'cache');

$this->invalidateCache();

return true;
}

/**
* {@inheritDoc}
*/
public function getVersion()
{
return pg_version($this->db_connection)['server'];
}

/**
* {@inheritDoc}
*/
public function housekeeping()
{
$this->createTempTable();
$this->cleanCache();
$this->retrieveData();
$this->deleteTempTable();
}

/**
* Create the temp table of valid data.
*
* @return void
*/
private function createTempTable()
{
pg_query($this->db_connection, 'CREATE LOCAL TEMP TABLE IF NOT EXISTS ' . $this->db_prefix . 'cache_tmp AS SELECT * FROM ' . $this->db_prefix . 'cache WHERE ttl >= ' . time());
}

/**
* Delete the temp table.
*
* @return void
*/
private function deleteTempTable()
{
pg_query($this->db_connection, 'DROP TABLE IF EXISTS ' . $this->db_prefix . 'cache_tmp');
}

/**
* Retrieve the valid data from temp table.
*
* @return void
*/
private function retrieveData()
{
pg_query($this->db_connection, 'INSERT INTO ' . $this->db_prefix . 'cache SELECT * FROM ' . $this->db_prefix . 'cache_tmp ON CONFLICT DO NOTHING');
}
}

?>
97 changes: 68 additions & 29 deletions Sources/CacheAPI-sqlite.php → Sources/Cache/APIs/Sqlite.php
100644 → 100755
Original file line number Diff line number Diff line change
Expand Up @@ -4,21 +4,28 @@
* Simple Machines Forum (SMF)
*
* @package SMF
* @author Simple Machines http://www.simplemachines.org
* @copyright 2017 Simple Machines and individual contributors
* @license http://www.simplemachines.org/about/smf/license.php BSD
* @author Simple Machines https://www.simplemachines.org
* @copyright 2022 Simple Machines and individual contributors
* @license https://www.simplemachines.org/about/smf/license.php BSD
*
* @version 2.1 Beta 4
* @version 2.1.2
*/

namespace SMF\Cache\APIs;

use SMF\Cache\CacheApi;
use SMF\Cache\CacheApiInterface;
use SQLite3;

if (!defined('SMF'))
die('Hacking attempt...');
die('No direct access...');

/**
* SQLite Cache API class
* @package cacheAPI
*
* @package CacheAPI
*/
class sqlite_cache extends cache_api
class Sqlite extends CacheApi implements CacheApiInterface
{
/**
* @var string The path to the current $cachedir directory.
Expand All @@ -30,11 +37,6 @@ class sqlite_cache extends cache_api
*/
private $cacheDB = null;

/**
* @var int
*/
private $cacheTime = 0;

public function __construct()
{
parent::__construct();
Expand All @@ -48,7 +50,6 @@ public function __construct()
*/
public function connect()
{

$database = $this->cachedir . '/' . 'SQLite3Cache.db3';
$this->cacheDB = new SQLite3($database);
$this->cacheDB->busyTimeout(1000);
Expand All @@ -57,8 +58,6 @@ public function connect()
$this->cacheDB->exec('CREATE TABLE cache (key text unique, value blob, ttl int);');
$this->cacheDB->exec('CREATE INDEX ttls ON cache(ttl);');
}
$this->cacheTime = time();

}

/**
Expand All @@ -79,8 +78,7 @@ public function isSupported($test = false)
*/
public function getData($key, $ttl = null)
{
$ttl = time() - $ttl;
$query = 'SELECT value FROM cache WHERE key = \'' . $this->cacheDB->escapeString($key) . '\' AND ttl >= ' . $ttl . ' LIMIT 1';
$query = 'SELECT value FROM cache WHERE key = \'' . $this->cacheDB->escapeString($key) . '\' AND ttl >= ' . time() . ' LIMIT 1';
$result = $this->cacheDB->query($query);

$value = null;
Expand All @@ -95,9 +93,11 @@ public function getData($key, $ttl = null)
*/
public function putData($key, $value, $ttl = null)
{

$ttl = $this->cacheTime + $ttl;
$query = 'REPLACE INTO cache VALUES (\'' . $this->cacheDB->escapeString($key) . '\', \'' . $this->cacheDB->escapeString($value) . '\', ' . $this->cacheDB->escapeString($ttl) . ');';
$ttl = time() + (int) ($ttl !== null ? $ttl : $this->ttl);
if ($value === null)
$query = 'DELETE FROM cache WHERE key = \'' . $this->cacheDB->escapeString($key) . '\';';
else
$query = 'REPLACE INTO cache VALUES (\'' . $this->cacheDB->escapeString($key) . '\', \'' . $this->cacheDB->escapeString($value) . '\', ' . $ttl . ');';
$result = $this->cacheDB->exec($query);

return $result;
Expand All @@ -108,10 +108,18 @@ public function putData($key, $value, $ttl = null)
*/
public function cleanCache($type = '')
{
if ($type == 'expired')
$query = 'DELETE FROM cache WHERE ttl < ' . time() . ';';
else
$query = 'DELETE FROM cache;';

$query = 'DELETE FROM cache;';
$result = $this->cacheDB->exec($query);

$query = 'VACUUM;';
$this->cacheDB->exec($query);

$this->invalidateCache();

return $result;
}

Expand All @@ -122,16 +130,27 @@ public function cacheSettings(array &$config_vars)
{
global $context, $txt;

$config_vars[] = $txt['cache_sqlite_settings'];
$config_vars[] = array('cachedir_sqlite', $txt['cachedir_sqlite'], 'file', 'text', 36, 'cache_sqlite_cachedir');
$class_name = $this->getImplementationClassKeyName();
$class_name_txt_key = strtolower($class_name);

$config_vars[] = $txt['cache_'. $class_name_txt_key .'_settings'];
$config_vars[] = array(
'cachedir_'. $class_name_txt_key,
$txt['cachedir_'. $class_name_txt_key],
'file',
'text',
36,
'cache_'. $class_name_txt_key .'_cachedir',
);

if (!isset($context['settings_post_javascript']))
$context['settings_post_javascript'] = '';

$context['settings_post_javascript'] .= '
if (empty($context['settings_not_writable']))
$context['settings_post_javascript'] .= '
$("#cache_accelerator").change(function (e) {
var cache_type = e.currentTarget.value;
$("#cachedir_sqlite").prop("disabled", cache_type != "sqlite");
$("#cachedir_'. $class_name_txt_key .'").prop("disabled", cache_type != "'. $class_name .'");
});';
}

Expand All @@ -146,11 +165,21 @@ public function cacheSettings(array &$config_vars)
*/
public function setCachedir($dir = null)
{
global $cachedir_sqlite;
global $cachedir, $cachedir_sqlite, $sourcedir;

// If its invalid, use SMF's.
if (is_null($dir) || !is_writable($dir))
if (!isset($dir) || !is_writable($dir))
{
if (!isset($cachedir_sqlite) || !is_writable($cachedir_sqlite))
{
$cachedir_sqlite = $cachedir;

require_once($sourcedir . '/Subs-Admin.php');
updateSettingsFile(array('cachedir_sqlite' => $cachedir_sqlite));
}

$this->cachedir = $cachedir_sqlite;
}
else
$this->cachedir = $dir;
}
Expand All @@ -160,8 +189,18 @@ public function setCachedir($dir = null)
*/
public function getVersion()
{
$temp = $this->cacheDB->version();
return $temp['versionString'];
if (null == $this->cacheDB)
$this->connect();

return $this->cacheDB->version()['versionString'];
}

/**
* {@inheritDoc}
*/
public function housekeeping()
{
$this->cleanCache('expired');
}
}

Expand Down
28 changes: 21 additions & 7 deletions Sources/CacheAPI-zend.php → Sources/Cache/APIs/Zend.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,21 +4,27 @@
* Simple Machines Forum (SMF)
*
* @package SMF
* @author Simple Machines http://www.simplemachines.org
* @copyright 2017 Simple Machines and individual contributors
* @license http://www.simplemachines.org/about/smf/license.php BSD
* @author Simple Machines https://www.simplemachines.org
* @copyright 2022 Simple Machines and individual contributors
* @license https://www.simplemachines.org/about/smf/license.php BSD
*
* @version 2.1 Beta 4
* @version 2.1.0
*/

namespace SMF\Cache\APIs;

use SMF\Cache\CacheApi;
use SMF\Cache\CacheApiInterface;

if (!defined('SMF'))
die('Hacking attempt...');
die('No direct access...');

/**
* Our Cache API class
* @package cacheAPI
*
* @package CacheAPI
*/
class zend_cache extends cache_api
class Zend extends CacheApi implements CacheApiInterface
{
/**
* {@inheritDoc}
Expand All @@ -29,9 +35,15 @@ public function isSupported($test = false)

if ($test)
return $supported;

return parent::isSupported() && $supported;
}

public function connect()
{
return true;
}

/**
* {@inheritDoc}
*/
Expand All @@ -42,6 +54,7 @@ public function getData($key, $ttl = null)
// Zend's pricey stuff.
if (function_exists('zend_shm_cache_fetch'))
return zend_shm_cache_fetch('SMF::' . $key);

elseif (function_exists('output_cache_get'))
return output_cache_get($key, $ttl);
}
Expand All @@ -55,6 +68,7 @@ public function putData($key, $value, $ttl = null)

if (function_exists('zend_shm_cache_store'))
return zend_shm_cache_store('SMF::' . $key, $value, $ttl);

elseif (function_exists('output_cache_put'))
return output_cache_put($key, $value);
}
Expand Down
252 changes: 252 additions & 0 deletions Sources/Cache/CacheApi.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,252 @@
<?php

/**
* Simple Machines Forum (SMF)
*
* @package SMF
* @author Simple Machines https://www.simplemachines.org
* @copyright 2022 Simple Machines and individual contributors
* @license https://www.simplemachines.org/about/smf/license.php BSD
*
* @version 2.1.0
*/

namespace SMF\Cache;

if (!defined('SMF'))
die('No direct access...');

abstract class CacheApi
{
const APIS_FOLDER = 'APIs';
const APIS_NAMESPACE = 'SMF\Cache\APIs\\';
const APIS_DEFAULT = 'FileBased';

/**
* @var string The maximum SMF version that this will work with.
*/
protected $version_compatible = '2.1.999';

/**
* @var string The minimum SMF version that this will work with.
*/
protected $min_smf_version = '2.1 RC1';

/**
* @var string The prefix for all keys.
*/
protected $prefix = '';

/**
* @var int The default TTL.
*/
protected $ttl = 120;

/**
* Does basic setup of a cache method when we create the object but before we call connect.
*
* @access public
*/
public function __construct()
{
$this->setPrefix();
}

/**
* Checks whether we can use the cache method performed by this API.
*
* @access public
* @param bool $test Test if this is supported or enabled.
* @return bool Whether or not the cache is supported
*/
public function isSupported($test = false)
{
global $cache_enable;

if ($test)
return true;

return !empty($cache_enable);
}

/**
* Sets the cache prefix.
*
* @access public
* @param string $prefix The prefix to use.
* If empty, the prefix will be generated automatically.
* @return bool If this was successful or not.
*/
public function setPrefix($prefix = '')
{
global $boardurl, $cachedir, $boarddir;

if (!is_string($prefix))
$prefix = '';

// Use the supplied prefix, if there is one.
if (!empty($prefix))
{
$this->prefix = $prefix;

return true;
}

// Ideally the prefix should reflect the last time the cache was reset.
if (!empty($cachedir) && file_exists($cachedir . '/index.php'))
{
$mtime = filemtime($cachedir . '/index.php');
}
// Fall back to the last time that Settings.php was updated.
elseif (!empty($boarddir) && file_exists($boarddir . '/Settings.php'))
{
$mtime = filemtime($boarddir . '/Settings.php');
}
// This should never happen, but just in case...
else
{
$mtime = filemtime(realpath($_SERVER['SCRIPT_FILENAME']));
}

$this->prefix = md5($boardurl . $mtime) . '-SMF-';

return true;
}

/**
* Gets the prefix as defined from set or the default.
*
* @access public
* @return string the value of $key.
*/
public function getPrefix()
{
return $this->prefix;
}

/**
* Sets a default Time To Live, if this isn't specified we let the class define it.
*
* @access public
* @param int $ttl The default TTL
* @return bool If this was successful or not.
*/
public function setDefaultTTL($ttl = 120)
{
$this->ttl = $ttl;

return true;
}

/**
* Gets the TTL as defined from set or the default.
*
* @access public
* @return int the value of $ttl.
*/
public function getDefaultTTL()
{
return $this->ttl;
}

/**
* Invalidate all cached data.
*
* @return bool Whether or not we could invalidate the cache.
*/
public function invalidateCache()
{
global $cachedir;

// Invalidate cache, to be sure!
// ... as long as index.php can be modified, anyway.
if (is_writable($cachedir . '/' . 'index.php'))
@touch($cachedir . '/' . 'index.php');

return true;
}

/**
* Closes connections to the cache method.
*
* @access public
* @return bool Whether the connections were closed.
*/
public function quit()
{
return true;
}

/**
* Specify custom settings that the cache API supports.
*
* @access public
* @param array $config_vars Additional config_vars, see ManageSettings.php for usage.
*/
public function cacheSettings(array &$config_vars)
{
}

/**
* Gets the latest version of SMF this is compatible with.
*
* @access public
* @return string the value of $key.
*/
public function getCompatibleVersion()
{
return $this->version_compatible;
}

/**
* Gets the min version that we support.
*
* @access public
* @return string the value of $key.
*/
public function getMinimumVersion()
{
return $this->min_smf_version;
}

/**
* Gets the Version of the Caching API.
*
* @access public
* @return string the value of $key.
*/
public function getVersion()
{
return $this->min_smf_version;
}

/**
* Run housekeeping of this cache
* exp. clean up old data or do optimization
*
* @access public
* @return void
*/
public function housekeeping()
{
}

/**
* Gets the class identifier of the current caching API implementation.
*
* @access public
* @return string the unique identifier for the current class implementation.
*/
public function getImplementationClassKeyName()
{
$class_name = get_class($this);

if ($position = strrpos($class_name, '\\'))
return substr($class_name, $position + 1);

else
return get_class($this);
}
}

?>
82 changes: 82 additions & 0 deletions Sources/Cache/CacheApiInterface.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
<?php

/**
* Simple Machines Forum (SMF)
*
* @package SMF
* @author Simple Machines https://www.simplemachines.org
* @copyright 2022 Simple Machines and individual contributors
* @license https://www.simplemachines.org/about/smf/license.php BSD
*
* @version 2.1.0
*/

namespace SMF\Cache;

if (!defined('SMF'))
die('No direct access...');


interface CacheApiInterface
{
/**
* Checks whether we can use the cache method performed by this API.
*
* @access public
* @param bool $test Test if this is supported or enabled.
* @return bool Whether or not the cache is supported
*/
public function isSupported($test = false);

/**
* Connects to the cache method. This defines our $key. If this fails, we return false, otherwise we return true.
*
* @access public
* @return bool Whether or not the cache method was connected to.
*/
public function connect();

/**
* Retrieves an item from the cache.
*
* @access public
* @param string $key The key to use, the prefix is applied to the key name.
* @param int $ttl Overrides the default TTL. Not really used anymore,
* but is kept for backwards compatibility.
* @return mixed The result from the cache, if there is no data or it is invalid, we return null.
* @todo Seperate existence checking into its own method
*/
public function getData($key, $ttl = null);

/**
* Stores a value, regardless of whether or not the key already exists (in
* which case it will overwrite the existing value for that key).
*
* @access public
* @param string $key The key to use, the prefix is applied to the key name.
* @param mixed $value The data we wish to save. Use null to delete.
* @param int $ttl How long (in seconds) the data should be cached for.
* The default TTL will be used if this is null.
* @return bool Whether or not we could save this to the cache.
* @todo Seperate deletion into its own method
*/
public function putData($key, $value, $ttl = null);

/**
* Clean out the cache.
*
* @param string $type If supported, the type of cache to clear, blank/data or user.
* @return bool Whether or not we could clean the cache.
*/
public function cleanCache($type = '');

/**
* Gets the class identifier of the current caching API implementation.
*
* @access public
* @return string the unique identifier for the current class implementation.
*/
public function getImplementationClassKeyName();
}

?>
87 changes: 0 additions & 87 deletions Sources/CacheAPI-apc.php

This file was deleted.

Loading