Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -243,4 +243,4 @@ Scriptlog is Open Source and Free PHP Blog Software licensed under the [MIT Lice

---

*Thank you for creating with Scriptlog.*
*Thank you for creating with Scriptlog.*
2 changes: 1 addition & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@
"vlucas/phpdotenv": "^5.6",
"voku/anti-xss": "^4.1"
},
"require-dev": {
"require-dev": {
"friendsofphp/php-cs-fixer": "^3.0",
"phpmetrics/phpmetrics": "^2.9",
"phpstan/phpstan": "^1.10",
Expand Down
53 changes: 52 additions & 1 deletion src/admin/admin-layout.php
Original file line number Diff line number Diff line change
Expand Up @@ -187,7 +187,54 @@ function admin_footer($stylePath, $ubench = null)
$('#summernote').summernote({
height: 300,
minHeight: null,
maxHeight: null,
maxHeight: null,
toolbar: [
['style', ['style']],
['font', ['bold', 'italic', 'underline', 'clear']],
['fontname', ['fontname']],
['color', ['color']],
['para', ['ul', 'ol', 'paragraph']],
['height', ['height']],
['insert', ['link', 'picture', 'video']],
['view', ['fullscreen', 'codeview']],
['help', ['help']]
],
callbacks: {
onImageUpload: function(files) {
// Upload image to server
var file = files[0];
var formData = new FormData();
formData.append('image', file);
formData.append('csrfToken', $('#csrf-token').val());

// Get post_id from hidden input if available
var postId = $('#post_id').val() || null;
if (postId) {
formData.append('post_id', postId);
}

$.ajax({
url: '/admin/media-upload.php',
method: 'POST',
data: formData,
processData: false,
contentType: false,
xhrFields: {
withCredentials: true
},
success: function(response) {
if (response.success && response.data && response.data.url) {
$('#summernote').summernote('insertImage', response.data.url);
} else {
alert('Failed to upload image: ' + (response.error?.message || 'Unknown error'));
}
},
error: function(xhr, status, error) {
alert('Failed to upload image: ' + error);
}
});
}
}
});
});
</script>
Expand Down Expand Up @@ -225,6 +272,10 @@ function admin_footer($stylePath, $ubench = null)
});
</script>

<!-- Hidden inputs for Summernote AJAX upload -->
<input type="hidden" id="csrf-token" value="<?= (isset($csrfToken)) ? $csrfToken : ""; ?>">
<input type="hidden" id="post_id" value="<?= (isset($post_id)) ? $post_id : ""; ?>">

</body>
</html>
<?php } ?>
3 changes: 2 additions & 1 deletion src/admin/sidebar-nav.php
Original file line number Diff line number Diff line change
Expand Up @@ -211,7 +211,7 @@ function sidebar_navigation($module, $url, $user_id = null, $user_session = null
<li><a href="<?= $url . '/' . generate_request('index.php', 'get', ['option-memberships', ActionConst::MEMBERSHIP_CONFIG, 0])['link']; ?>"><?= admin_translate('nav.membership'); ?></a></li>
<li><a href="<?= $url . '/' . generate_request('index.php', 'get', ['option-mail', ActionConst::MAIL_CONFIG, 0])['link']; ?>"><?= admin_translate('nav.mail_settings'); ?></a></li>
<li><a href="<?= $url . '/' . generate_request('index.php', 'get', ['option-downloads', ActionConst::DOWNLOAD_CONFIG, 0])['link']; ?>"><?= admin_translate('nav.download_settings'); ?></a></li>
<li><a href="<?= $url . '/' . generate_request('index.php', 'get', ['option-api', ActionConst::API_CONFIG, 0])['link']; ?>">RESTful API</a></li>
<li><a href="<?= $url . '/' . generate_request('index.php', 'get', ['option-api', ActionConst::API_CONFIG, 0])['link']; ?>"><?= admin_translate('nav.api'); ?></a></li>
</ul>
</li>
<?php endif; ?>
Expand All @@ -238,6 +238,7 @@ function sidebar_navigation($module, $url, $user_id = null, $user_session = null
<li><a href="<?= $url . '/' . generate_request('index.php', 'get', ['privacy'], false)['link']; ?>"><?= admin_translate('nav.privacy_settings'); ?></a></li>
<li><a href="<?= $url; ?>/index.php?load=privacy&p=data-requests"><?= admin_translate('nav.data_requests'); ?></a></li>
<li><a href="<?= $url; ?>/index.php?load=privacy&p=audit-logs"><?= admin_translate('nav.audit_logs'); ?></a></li>
<li><a href="<?= $url . '/' . generate_request('index.php', 'get', ['privacy-policy'], false)['link']; ?>"><?= admin_translate('nav.privacy_policy'); ?></a></li>
</ul>
</li>
<?php endif; ?>
Expand Down
2 changes: 1 addition & 1 deletion src/admin/ui/pages/edit-page.php
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@

<div class="form-group">
<label for="summernote">Content <span class="text-red" title="required">*</span></label>
<textarea class="form-control" id="summernote" name="post_content" rows="10" cols="80" maxlength="50000" required aria-required="true"><?=(isset($pageData['post_content']) ? safe_html($pageData['post_content']) : ""); ?><?=(isset($formData['post_content']) ? safe_html($formData['post_content']) : "") ; ?></textarea>
<textarea class="form-control" id="summernote" name="post_content" rows="10" cols="80" maxlength="500000" required aria-required="true"><?=(isset($pageData['post_content']) ? safe_html($pageData['post_content']) : ""); ?><?=(isset($formData['post_content']) ? safe_html($formData['post_content']) : "") ; ?></textarea>
</div>

<div class="form-group">
Expand Down
2 changes: 1 addition & 1 deletion src/admin/ui/posts/edit-post.php
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@

<div class="form-group">
<label for="summernote">Content <span class="text-red" title="required">*</span></label>
<textarea class="form-control" id="summernote" name="post_content" rows="10" cols="80" maxlength="50000" required aria-required="true"><?= (isset($postContent) ? $postContent : ""); ?><?= (isset($formData['post_content']) ? safe_html($formData['post_content']) : ""); ?></textarea>
<textarea class="form-control" id="summernote" name="post_content" rows="10" cols="80" maxlength="500000" required aria-required="true"><?= (isset($postContent) ? $postContent : ""); ?><?= (isset($formData['post_content']) ? safe_html($formData['post_content']) : ""); ?></textarea>
</div>

<div class="form-group">
Expand Down
178 changes: 102 additions & 76 deletions src/admin/ui/setting/api-setting.php
Original file line number Diff line number Diff line change
@@ -1,93 +1,119 @@
<?php if (!defined('SCRIPTLOG')) {
exit();
} ?>
<!-- Content Wrapper. Contains page content -->
<div class="content-wrapper">
<!-- Content Header (Page header) -->
<section class="content-header">
<h1><?= (isset($pageTitle) ? $pageTitle : admin_translate('nav.api_settings')); ?>
<small>Control Panel</small>
</h1>
<ol class="breadcrumb">
<li><a href="index.php?load=dashboard"><i class="fa fa-dashboard" aria-hidden="true"></i> Home </a></li>
<li class="active"><a href="index.php?load=option-api"><?= admin_translate('nav.api_settings'); ?></a></li>
</ol>
</section>

<!-- Main content -->
<section class="content">
<div class="row">
<div class="col-md-8">
<div class="box box-primary">
<div class="box-header with-border"></div>
<!-- /.box-header -->
<?php
defined('SCRIPTLOG') || die("Direct access not permitted");
if (isset($errors) && !empty($errors)) :
?>
<div class="alert alert-danger alert-dismissible">
<button type="button" class="close" data-dismiss="alert" aria-hidden="true">&times;</button>
<h2><i class="icon fa fa-warning" aria-hidden="true"></i> Invalid Form Data!</h2>
<?php
foreach ($errors as $e) :
echo '<p>' . safe_html($e) . '</p>';
endforeach;
?>
</div>

$api = $this->api ?? [];
$errors = $this->errors ?? [];
$status = isset($_GET['status']) ? $_GET['status'] : "";
$csrfToken = $this->csrfToken ?? csrf_generate_token('csrfToken');
?>
<?php
endif;

<?php if ($status === 'apiConfigUpdated'): ?>
<div class="alert alert-success alert-dismissible">
<button type="button" class="close" data-dismiss="alert" aria-hidden="true">&times;</button>
<h4><i class="icon fa fa-check"></i> Success!</h4>
API settings have been updated successfully.
</div>
<?php endif; ?>
if (isset($_GET['status']) && $_GET['status'] === 'apiConfigUpdated') :
?>

<?php if (!empty($errors)): ?>
<div class="alert alert-danger alert-dismissible">
<div class="alert alert-success alert-dismissible">
<button type="button" class="close" data-dismiss="alert" aria-hidden="true">&times;</button>
<h4><i class="icon fa fa-ban"></i> Error!</h4>
<?php foreach ($errors as $error): ?>
<p><?= safe_html($error) ?></p>
<?php endforeach; ?>
<h2><i class="icon fa fa-check" aria-hidden="true"></i> Success!</h2>
<p>API settings have been updated successfully.</p>
</div>
<?php endif; ?>

<div class="row">
<div class="col-md-12">
<div class="box box-primary">
<div class="box-header with-border">
<h3 class="box-title">RESTful API Settings</h3>
</div>
<?php
endif;

<form method="post" action="<?= generate_request('index.php', 'get', ['option-api', 'apiConfig', 0])['link']; ?>">
<input type="hidden" name="csrfToken" value="<?= $csrfToken ?>">

<div class="box-body">
<!-- Rate Limiting Toggle -->
<div class="form-group">
<label>
<input type="checkbox" name="api_rate_limit_enabled" value="1" <?= ($api['api_rate_limit_enabled'] ?? '1') === '1' ? 'checked' : '' ?>>
Enable Rate Limiting
</label>
<p class="help-block">Protect your API from abuse by limiting the number of requests per client.</p>
</div>
$action = (isset($formAction)) ? $formAction : null;
?>

<div class="row">
<div class="col-md-6">
<!-- Read Rate Limit -->
<div class="form-group">
<label for="api_rate_limit_read">Read Rate Limit (requests per minute)</label>
<input type="number" id="api_rate_limit_read" name="api_rate_limit_read"
class="form-control" value="<?= safe_html($api['api_rate_limit_read'] ?? '60') ?>"
min="1" max="1000">
<p class="help-block">Maximum GET requests per client per minute. Default: 60</p>
</div>
</div>
<div class="box-body">
<form method="post" action="<?= generate_request('index.php', 'get', ['option-api', $action, 0])['link']; ?>">
<input type="hidden" name="csrfToken" value="<?= $csrfToken ?>">

<div class="col-md-6">
<!-- Write Rate Limit -->
<div class="form-group">
<label for="api_rate_limit_write">Write Rate Limit (requests per minute)</label>
<input type="number" id="api_rate_limit_write" name="api_rate_limit_write"
class="form-control" value="<?= safe_html($api['api_rate_limit_write'] ?? '20') ?>"
min="1" max="500">
<p class="help-block">Maximum POST/PUT/DELETE/PATCH requests per client per minute. Default: 20</p>
</div>
</div>
</div>
<!-- Rate Limiting Toggle -->
<div class="form-group">
<label>
<input type="checkbox" name="api_rate_limit_enabled" value="1" <?= ($api['api_rate_limit_enabled'] ?? '1') === '1' ? 'checked' : '' ?>>
Enable Rate Limiting
</label>
<p class="help-block">Protect your API from abuse by limiting the number of requests per client.</p>
</div>

<!-- Info Box -->
<div class="callout callout-info">
<h4>How Rate Limiting Works</h4>
<p>Rate limits are tracked per client using the following priority:</p>
<ol>
<li><strong>API Key</strong> (X-API-Key header)</li>
<li><strong>Bearer Token</strong> (Authorization header)</li>
<li><strong>IP Address</strong> (fallback)</li>
</ol>
<p>When a client exceeds the rate limit, they receive a <code>429 Too Many Requests</code> response with a <code>Retry-After</code> header.</p>
</div>
<div class="row">
<div class="col-md-6">
<!-- Read Rate Limit -->
<div class="form-group">
<label for="api_rate_limit_read">Read Rate Limit (requests per minute)</label>
<input type="number" id="api_rate_limit_read" name="api_rate_limit_read"
class="form-control" value="<?= safe_html($api['api_rate_limit_read'] ?? '60') ?>"
min="1" max="1000">
<p class="help-block">Maximum GET requests per client per minute. Default: 60</p>
</div>
</div>

<div class="box-footer">
<button type="submit" name="apiConfigSubmit" class="btn btn-primary">
<i class="fa fa-save"></i> Update API Settings
</button>
<div class="col-md-6">
<!-- Write Rate Limit -->
<div class="form-group">
<label for="api_rate_limit_write">Write Rate Limit (requests per minute)</label>
<input type="number" id="api_rate_limit_write" name="api_rate_limit_write"
class="form-control" value="<?= safe_html($api['api_rate_limit_write'] ?? '20') ?>"
min="1" max="500">
<p class="help-block">Maximum POST/PUT/DELETE/PATCH requests per client per minute. Default: 20</p>
</div>
</form>
</div>
</div>

<!-- Info Box -->
<div class="callout callout-info">
<h4>How Rate Limiting Works</h4>
<p>Rate limits are tracked per client using the following priority:</p>
<ol>
<li><strong>API Key</strong> (X-API-Key header)</li>
<li><strong>Bearer Token</strong> (Authorization header)</li>
<li><strong>IP Address</strong> (fallback)</li>
</ol>
<p>When a client exceeds the rate limit, they receive a <code>429 Too Many Requests</code> response with a <code>Retry-After</code> header.</p>
</div>
</div>

<div class="box-footer">
<button type="submit" name="apiConfigSubmit" class="btn btn-primary">
<i class="fa fa-save"></i> Update API Settings
</button>
</div>
</form>
</div>
<!-- /.box-primary -->
</div>
<!-- /.col-md-8 -->
</div>
</section>
<!-- /.content -->
</div>
<!-- /.content-wrapper -->
15 changes: 15 additions & 0 deletions src/api/index.php
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,17 @@

// Load required core files
require_once __DIR__ . '/../lib/main.php';

// Ensure sessions are properly started for API requests that need authentication
// This is needed for endpoints like media upload which require admin session
if (isset($app->sessionMaker)) {
session_save_path(sys_get_temp_dir());
session_set_save_handler($app->sessionMaker, true);
register_shutdown_function('session_write_close');
if (function_exists('start_session_on_site')) {
start_session_on_site($app->sessionMaker);
}
}
require_once __DIR__ . '/../lib/core/ApiAuth.php';
require_once __DIR__ . '/../lib/core/ApiResponse.php';
require_once __DIR__ . '/../lib/core/ApiRouter.php';
Expand All @@ -77,6 +88,7 @@
require_once __DIR__ . '/../lib/controller/api/TranslationsApiController.php';
require_once __DIR__ . '/../lib/controller/api/SearchApiController.php';
require_once __DIR__ . '/../lib/controller/api/ProtectedPostApiController.php';
require_once __DIR__ . '/../lib/controller/api/MediaApiController.php';
require_once __DIR__ . '/../lib/utility/rate-limiter.php';
require_once __DIR__ . '/../lib/core/ApiHateoas.php';

Expand Down Expand Up @@ -164,6 +176,9 @@
$router->post('posts/(?P<id>[0-9]+)/unlock', 'ProtectedPostApiController@unlock');
$router->post('posts/(?P<id>[0-9]+)/verify', 'ProtectedPostApiController@verify');

// Media API (for SummerNote image upload)
$router->post('media/upload', 'MediaApiController@upload');

// Categories/Topics API
$router->get('categories', 'CategoriesApiController@index');
$router->get('categories/(?P<id>[0-9]+)', 'CategoriesApiController@show');
Expand Down
Loading