Articles: finish admin refactor, uploads hardening, and attachment sorting (0.262)

This commit is contained in:
2026-02-13 09:00:24 +01:00
parent 42e4396064
commit 5354f97baa
31 changed files with 1951 additions and 1512 deletions

View File

@@ -134,10 +134,14 @@ Pliki artykułów.
| Kolumna | Opis | | Kolumna | Opis |
|---------|------| |---------|------|
| id | PK |
| article_id | FK do pp_articles | | article_id | FK do pp_articles |
| src | Ścieżka do pliku | | src | Ścieżka do pliku |
| name | Nazwa wyświetlana załącznika (opcjonalna) |
| to_delete | Flaga miękkiego usuwania (0/1) |
| o | Kolejność załączników (używana przez sortowanie drag&drop w adminie) |
**Używane w:** `Domain\Article\ArticleRepository::find()` **Używane w:** `Domain\Article\ArticleRepository::find()`, `Domain\Article\ArticleRepository::saveFilesOrder()`
## pp_units ## pp_units
Jednostki/slowniki (np. jednostki produktu). Jednostki/slowniki (np. jednostki produktu).

View File

@@ -455,3 +455,19 @@ Aktualnie w suite są też testy modułów `Dictionaries`, `Articles` i `Users`
- UPDATE: `/admin/articles_archive/view_list/` przepiete z legacy `grid` na `components/table-list`. - UPDATE: `/admin/articles_archive/view_list/` przepiete z legacy `grid` na `components/table-list`.
- CLEANUP: usuniete legacy klasy `autoload/admin/controls/class.ArticlesArchive.php`, `autoload/admin/factory/class.ArticlesArchive.php`, `autoload/admin/view/class.ArticlesArchive.php`. - CLEANUP: usuniete legacy klasy `autoload/admin/controls/class.ArticlesArchive.php`, `autoload/admin/factory/class.ArticlesArchive.php`, `autoload/admin/view/class.ArticlesArchive.php`.
- Testy: 165 tests, 424 assertions. - Testy: 165 tests, 424 assertions.
### 2026-02-13: Refaktoryzacja /admin/articles (ver. 0.261)
- **UPDATE:** routing DI dla `ArticlesController` obsluguje akcje AJAX: `article_image_alt_change`, `article_file_name_change`, `article_image_delete`, `article_file_delete`.
- **UPDATE:** widok `admin/templates/articles/article-edit.php` korzysta z endpointow `/admin/articles/*` zamiast `admin/ajax.php?a=article_*`.
- **UPDATE:** lista artykulow nie korzysta juz z `admin\factory\Articles::article_pages` (etykiety stron z `Domain\Article\ArticleRepository`).
- **CLEANUP:** usuniete legacy pliki `autoload/admin/view/class.Articles.php` i `admin/ajax/articles.php`; odpiecie include w `admin/ajax.php`.
- Testy: 176 tests, 439 assertions.
### 2026-02-13: Articles edit UX i sortowanie zalacznikow (ver. 0.262)
- **UPDATE:** `Domain\Article\ArticleRepository` - dodane `saveFilesOrder()` oraz obsluga `files_order` podczas `save()` (pierwszy zapis zachowuje kolejnosc).
- **UPDATE:** routing DI (`admin\Site::$actionMap`) rozszerzony o `files_order_save -> filesOrderSave`.
- **UPDATE:** `admin\Controllers\ArticlesController` - nowa akcja AJAX `filesOrderSave()`.
- **UPDATE:** `admin/templates/articles/article-edit-custom-script.php` - drag&drop sortowania listy zalacznikow + synchronizacja hidden input `files_order`.
- **UPDATE:** potwierdzenia usuwania zdjec i zalacznikow w edycji artykulu ujednolicone wizualnie z dialogiem usuwania z listy (jquery-confirm, `table-list-confirm-dialog`).
- **FIX:** dodane ladowanie biblioteki `jquery-impromptu` w widoku edycji artykulu (kompatybilnosc dla `$.prompt`).
- Testy: 178 tests, 443 assertions.

View File

@@ -654,3 +654,31 @@ Gdy `persist = true`:
- UPDATE: routing DI (`admin\Site`) rozszerzony o modul `ArticlesArchive` + mapowanie `article_restore -> restore` - UPDATE: routing DI (`admin\Site`) rozszerzony o modul `ArticlesArchive` + mapowanie `article_restore -> restore`
- CLEANUP: usuniete `autoload/admin/controls/class.ArticlesArchive.php`, `autoload/admin/factory/class.ArticlesArchive.php`, `autoload/admin/view/class.ArticlesArchive.php` - CLEANUP: usuniete `autoload/admin/controls/class.ArticlesArchive.php`, `autoload/admin/factory/class.ArticlesArchive.php`, `autoload/admin/view/class.ArticlesArchive.php`
- Testy po zmianie: **165 tests, 424 assertions** - Testy po zmianie: **165 tests, 424 assertions**
## Plan 2026-02-13 - Refaktoryzacja `/admin/articles/`
- [ ] Przeniesc zaleznosci listy artykulow z `admin\factory\Articles` do `Domain\Article\ArticleRepository` (etykiety stron, operacje pomocnicze).
- [ ] Dodac akcje routowane przez `admin\Controllers\ArticlesController` dla operacji AJAX (`article_image_alt_change`, `article_file_name_change`, `article_image_delete`, `article_file_delete`).
- [ ] Przepiac widok `admin/templates/articles/article-edit.php` z `/admin/ajax.php` na endpointy `/admin/articles/*`.
- [ ] Usunac legacy `admin\view\Articles` i zastapic rekurencje podstron przez `Tpl::view('articles/subpages-list', ...)`.
- [ ] Usunac `admin/ajax/articles.php` oraz odpiac include z `admin/ajax.php`.
- [ ] Przeszukac projekt pod pozostale zaleznosci i uruchomic testy modulu Articles.
## Aktualizacja 2026-02-13 (ver. 0.261)
- **Articles** - dalsza refaktoryzacja `/admin/articles/`
- UPDATE: `Domain\Article\ArticleRepository` rozszerzone o metody UI/admin: `pagesSummaryForArticles()`, `updateImageAlt()`, `updateFileName()`, `markImageToDelete()`, `markFileToDelete()`.
- UPDATE: `admin\Controllers\ArticlesController` obsluguje nowe akcje routingu: `article_image_alt_change`, `article_file_name_change`, `article_image_delete`, `article_file_delete`.
- UPDATE: lista artykulow (`list`) nie korzysta juz z `admin\factory\Articles::article_pages()`.
- UPDATE: `admin/templates/articles/article-edit.php` przepiete z `/admin/ajax.php?a=article_*` na endpointy `/admin/articles/article_*/`.
- UPDATE: rekurencja podstron w widoku oparta o `Tpl::view('articles/subpages-list', ...)` (bez `admin\view\Articles`).
- CLEANUP: usuniete legacy pliki `autoload/admin/view/class.Articles.php` oraz `admin/ajax/articles.php`; `admin/ajax.php` nie includuje juz `ajax/articles.php`.
- Testy po zmianie: **176 tests, 439 assertions**.
## Aktualizacja 2026-02-13 (ver. 0.262)
- **Articles (/admin/articles)**
- UPDATE: `Domain\Article\ArticleRepository` rozszerzone o `saveFilesOrder()` oraz zapis `files_order` przy `save()` (eliminuje koniecznosc drugiego zapisu po sortowaniu).
- UPDATE: routing DI (`admin\Site`) rozszerzony o mapowanie `files_order_save -> filesOrderSave`.
- UPDATE: `admin\Controllers\ArticlesController` - nowa akcja AJAX `filesOrderSave`.
- UPDATE: widok `admin/templates/articles/article-edit-custom-script.php` - drag&drop dla listy zalacznikow + hidden input `files_order`.
- UPDATE: potwierdzenia usuwania zdjec i zalacznikow przepiete na `jquery-confirm` ze stylem `table-list-confirm-dialog` (jak na liscie artykulow).
- FIX: dolaczona biblioteka `jquery-impromptu` w widoku edycji artykulu dla kompatybilnosci.
- Testy po zmianie: **178 tests, 443 assertions**.

View File

@@ -246,3 +246,24 @@ OK (165 tests, 424 assertions)
Nowe testy dodane 2026-02-12: Nowe testy dodane 2026-02-12:
- `tests/Unit/Domain/Article/ArticleRepositoryTest.php` (rozszerzenie o testy `restore`, `deletePermanently`, `listArchivedForAdmin`) - `tests/Unit/Domain/Article/ArticleRepositoryTest.php` (rozszerzenie o testy `restore`, `deletePermanently`, `listArchivedForAdmin`)
- `tests/Unit/admin/Controllers/ArticlesArchiveControllerTest.php` - `tests/Unit/admin/Controllers/ArticlesArchiveControllerTest.php`
## Aktualizacja suite (release 0.261)
Ostatnio zweryfikowano: 2026-02-13
```text
OK (176 tests, 439 assertions)
```
Nowe testy/rozszerzenia 2026-02-13:
- `tests/Unit/Domain/Article/ArticleRepositoryTest.php` (nowe przypadki dla `pagesSummaryForArticles`, `updateImageAlt`, `markFileToDelete`)
- `tests/Unit/admin/Controllers/ArticlesControllerTest.php` (nowe kontrakty dla akcji `imageAltChange`, `fileNameChange`, `imageDelete`, `fileDelete`)
## Aktualizacja suite (release 0.262)
Ostatnio zweryfikowano: 2026-02-13
```text
OK (178 tests, 443 assertions)
```
Nowe testy/rozszerzenia 2026-02-13:
- `tests/Unit/Domain/Article/ArticleRepositoryTest.php` (nowe przypadki dla `saveFilesOrder`)

View File

@@ -38,7 +38,6 @@ $mdb = new medoo( [
] ); ] );
require_once 'ajax/pages.php'; require_once 'ajax/pages.php';
require_once 'ajax/articles.php';
require_once 'ajax/shop-category.php'; require_once 'ajax/shop-category.php';
require_once 'ajax/users.php'; require_once 'ajax/users.php';
require_once 'ajax/shop.php'; require_once 'ajax/shop.php';

View File

@@ -1,46 +0,0 @@
<?php
$a = \S::get( 'a' );
if ( $a == 'article_image_delete' )
{
$response = [ 'status' => 'error', 'msg' => 'Podczas usuwania zdjecia wystąpił błąd. Proszę spróbować ponownie.' ];
if ( \admin\factory\Articles::delete_img( \S::get( 'image_id' ) ) )
$response = [ 'status' => 'ok' ];
echo json_encode( $response );
exit;
}
if ( $a == 'article_file_delete' )
{
$response = [ 'status' => 'error', 'msg' => 'Podczas usuwania załącznika wystąpił błąd. Proszę spróbować ponownie.' ];
if ( \admin\factory\Articles::delete_file( \S::get( 'file_id' ) ) )
$response = [ 'status' => 'ok' ];
echo json_encode( $response );
exit;
}
if ( $a == 'article_image_alt_change' )
{
$response = [ 'status' => 'error', 'msg' => 'Podczas zmiany atrybutu alt zdjęcia wystąpił błąd. Proszę spróbować ponownie.' ];
if ( \admin\factory\Articles::image_alt_change( \S::get( 'image_id' ), \S::get( 'image_alt' ) ) )
$response = [ 'status' => 'ok' ];
echo json_encode( $response );
exit;
}
if ( $a == 'article_file_name_change' )
{
$response = [ 'status' => 'error', 'msg' => 'Podczas zmiany nazwy załącznika wystąpił błąd. Proszę spróbować ponownie.' ];
if ( \admin\factory\Articles::file_name_change( \S::get( 'file_id' ), \S::get( 'file_name' ) ) )
$response = [ 'status' => 'ok' ];
echo json_encode( $response );
exit;
}

View File

@@ -0,0 +1,583 @@
<?php
$article = is_array($this->article ?? null) ? $this->article : [];
$articleId = (int)($article['id'] ?? 0);
$userId = (int)($this->user['id'] ?? 0);
$imagesCount = is_array($article['images'] ?? null) ? count($article['images']) : 0;
$filesCount = is_array($article['files'] ?? null) ? count($article['files']) : 0;
$imageMaxPx = 1920;
if (isset($this->settings['image_px']) && (int)$this->settings['image_px'] > 0) {
$imageMaxPx = (int)$this->settings['image_px'];
} elseif (isset($GLOBALS['settings']['image_px']) && (int)$GLOBALS['settings']['image_px'] > 0) {
$imageMaxPx = (int)$GLOBALS['settings']['image_px'];
}
$uploadToken = bin2hex(random_bytes(24));
if (!isset($_SESSION['upload_tokens']) || !is_array($_SESSION['upload_tokens'])) {
$_SESSION['upload_tokens'] = [];
}
$_SESSION['upload_tokens'][$uploadToken] = [
'user_id' => $userId,
'expires' => time() + 60 * 20,
];
$cookiePages = [];
$cookieMenus = [];
if (!empty($_COOKIE['cookie_pages'])) {
$decoded = @unserialize($_COOKIE['cookie_pages']);
if (is_array($decoded)) {
$cookiePages = $decoded;
}
}
if (!empty($_COOKIE['cookie_menus'])) {
$decoded = @unserialize($_COOKIE['cookie_menus']);
if (is_array($decoded)) {
$cookieMenus = $decoded;
}
}
?>
<link type="text/css" rel="stylesheet" href="/libraries/plupload/jquery.plupload.queue/css/jquery.plupload.queue.css" />
<link type="text/css" rel="stylesheet" href="/libraries/grid/plugins/impromptu/jquery-impromptu.css" />
<script type="text/javascript" src="/libraries/grid/plugins/impromptu/jquery-impromptu.js"></script>
<script type="text/javascript" src="/libraries/jquery/sortable/sortable.js"></script>
<script type="text/javascript" src="/libraries/plupload/plupload.js"></script>
<script type="text/javascript" src="/libraries/plupload/plupload.flash.js"></script>
<script type="text/javascript" src="/libraries/plupload/plupload.html5.js"></script>
<script type="text/javascript" src="/libraries/plupload/plupload.html4.js"></script>
<script type="text/javascript" src="/libraries/plupload/jquery.plupload.queue/jquery.plupload.queue.js"></script>
<script type="text/javascript" src="/libraries/plupload/i18n/pl.js"></script>
<script type="text/javascript" src="/libraries/jquery-nested-sortable/jquery.mjs.nestedSortable.js"></script>
<script type="text/javascript" src="/libraries/jquery/lozad.js"></script>
<style type="text/css">
.jconfirm.table-list-confirm-dialog .jconfirm-row {
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
}
.jconfirm.table-list-confirm-dialog .jconfirm-box-container {
width: 100%;
max-width: 560px;
padding-left: 12px;
padding-right: 12px;
margin: 0 auto;
}
.jconfirm.table-list-confirm-dialog .jconfirm-box {
width: 100% !important;
}
</style>
<script type="text/javascript">
var images_count = <?= (int)$imagesCount ?>;
var files_count = <?= (int)$filesCount ?>;
var article_id = <?= (int)$articleId ?>;
$(function() {
function confirmDialog(message, onConfirm, okLabel) {
if (typeof $.confirm === 'function') {
$.confirm({
title: 'Potwierdzenie',
content: String(message),
type: 'red',
boxWidth: '560px',
useBootstrap: false,
animation: 'scale',
closeAnimation: 'scale',
backgroundDismissAnimation: 'shake',
container: 'body',
theme: 'modern',
columnClass: '',
typeAnimated: true,
lazyOpen: false,
draggable: false,
closeIcon: true,
containerFluid: true,
escapeKey: true,
backgroundDismiss: true,
animationBounce: 1.1,
watchInterval: 100,
offsetTop: 0,
offsetBottom: 0,
customClass: 'table-list-confirm-dialog',
buttons: {
cancel: {
text: 'Anuluj',
btnClass: 'btn-default'
},
confirm: {
text: okLabel || 'Usun',
btnClass: 'btn-danger',
action: function() {
if (typeof onConfirm === 'function') {
onConfirm();
}
}
}
}
});
return;
}
if (window.confirm(message) && typeof onConfirm === 'function') {
onConfirm();
}
}
function ensureGalleryOrderInput() {
var $form = $('#fg-article-edit');
if (!$form.length) {
return null;
}
var $input = $form.find('input[name="gallery_order"]');
if (!$input.length) {
$input = $('<input>', {
type: 'hidden',
name: 'gallery_order',
id: 'gallery_order'
});
$form.append($input);
}
return $input;
}
function ensureFilesOrderInput() {
var $form = $('#fg-article-edit');
if (!$form.length) {
return null;
}
var $input = $form.find('input[name="files_order"]');
if (!$input.length) {
$input = $('<input>', {
type: 'hidden',
name: 'files_order',
id: 'files_order'
});
$form.append($input);
}
return $input;
}
function buildGalleryOrder() {
var order = [];
$('#images-list li').each(function() {
var imageId = $(this).find('a.article_image_delete').attr('image-id');
if (imageId) {
order.push(imageId);
}
});
return order.join(';');
}
function refreshGalleryOrderInput() {
var $input = ensureGalleryOrderInput();
if ($input) {
$input.val(buildGalleryOrder());
}
}
function buildFilesOrder() {
var order = [];
$('#files-list li').each(function() {
var fileId = $(this).find('.article_file_edit').attr('file_id');
if (fileId) {
order.push(fileId);
}
});
return order.join(';');
}
function refreshFilesOrderInput() {
var $input = ensureFilesOrderInput();
if ($input) {
$input.val(buildFilesOrder());
}
}
ensureGalleryOrderInput();
refreshGalleryOrderInput();
ensureFilesOrderInput();
refreshFilesOrderInput();
var observer = lozad();
observer.observe();
var imageList = document.getElementById('images-list');
if (imageList) {
Sortable.create(imageList, {
onEnd: function() {
var order = '';
$('#images-list li').each(function() {
if (order !== '') {
order += ';';
}
order += $(this).children('a').attr('image-id');
});
$.ajax({
type: 'POST',
cache: false,
url: '/admin/articles/gallery_order_save/',
data: {
article_id: article_id,
order: order
},
beforeSend: function() {
$('#overlay').show();
},
success: function(data) {
$('#overlay').hide();
var response = jQuery.parseJSON(data);
if (response.status !== 'ok') {
create_error(response.msg);
}
}
});
refreshGalleryOrderInput();
}
});
}
var filesList = document.getElementById('files-list');
if (filesList) {
Sortable.create(filesList, {
onEnd: function() {
var order = buildFilesOrder();
$.ajax({
type: 'POST',
cache: false,
url: '/admin/articles/files_order_save/',
data: {
article_id: article_id,
order: order
},
beforeSend: function() {
$('#overlay').show();
},
success: function(data) {
$('#overlay').hide();
var response = jQuery.parseJSON(data);
if (response.status !== 'ok') {
create_error(response.msg);
}
}
});
refreshFilesOrderInput();
}
});
}
$('#images-uploader').pluploadQueue({
multipart_params: {
upload_token: '<?= htmlspecialchars($uploadToken, ENT_QUOTES, 'UTF-8') ?>',
article_id: article_id
},
runtimes: 'html5,flash,html4',
init: {
Refresh: function() {
$('.plupload_buttons').css('display', 'inline');
$('.plupload_upload_status').css('display', 'inline');
$('.plupload_start').addClass('plupload_disabled');
$('.plupload_start').removeClass('plupload_disabled');
},
UploadComplete: function() {
$('.plupload_buttons').css('display', 'inline');
$('.plupload_upload_status').css('display', 'inline');
$('.plupload_start').addClass('plupload_disabled');
$('.plupload_start').removeClass('plupload_disabled');
},
FileUploaded: function(up, file, response) {
var data = jQuery.parseJSON(response.response);
$('#images-list').append(
'<li id="image-' + data.image_id + '">' +
'<img class="article-image lozad" data-src="/libraries/thumb.php?img=' + data.data_link + '&w=300&h=300">' +
'<a href="#" class="input-group-addon btn btn-danger article_image_delete" image-id="' + data.image_id + '">' +
'<i class="fa fa-trash"></i>' +
'</a>' +
'<input type="text" class="form-control image-alt" value="" image-id="' + data.image_id + '" placeholder="atrybut alt...">' +
'</li>'
);
images_count++;
observer.observe();
refreshGalleryOrderInput();
$('html, body').animate({
scrollTop: $('#images-uploader').offset().top
}, 1);
}
},
url: '/libraries/plupload/upload-articles-images.php',
chunk_size: '1mb',
max_file_size: '20mb',
unique_names: false,
resize: {
width: <?= (int)$imageMaxPx ?>,
height: <?= (int)$imageMaxPx ?>,
quality: 95
},
filters: [
{ title: 'Obrazki', extensions: 'jpg,gif,png,bmp,jpeg' }
],
flash_swf_url: '/../libraries/plupload/plupload.flash.swf'
});
$('#files-uploader').pluploadQueue({
multipart_params: {
upload_token: '<?= htmlspecialchars($uploadToken, ENT_QUOTES, 'UTF-8') ?>'
},
runtimes: 'html5,flash,html4',
init: {
Refresh: function() {
$('.plupload_buttons').css('display', 'inline');
$('.plupload_upload_status').css('display', 'inline');
$('.plupload_start').addClass('plupload_disabled');
$('.plupload_start').removeClass('plupload_disabled');
},
FileUploaded: function(up, file, response) {
var data = jQuery.parseJSON(response.response);
$('#files-list').append(
'<li id="file-' + data.file_id + '">' +
'<div class="input-group">' +
'<input type="text" class="form-control article_file_edit" file_id="' + data.file_id + '" value="' + data.file_name + '" />' +
'<a href="#" class="input-group-addon btn btn-info article_file_delete" file_id="' + data.file_id + '">' +
'<i class="fa fa-trash"></i>' +
'</a>' +
'</div>' +
'</li>'
);
files_count++;
refreshFilesOrderInput();
}
},
url: '/libraries/plupload/upload-articles-files.php',
chunk_size: '1mb',
max_file_size: '50mb',
unique_names: false,
filters: [
{ title: 'Wszystkie pliki', extensions: '*' }
],
flash_swf_url: '/../libraries/plupload/plupload.flash.swf'
});
$('ol.sortable').nestedSortable({
forcePlaceholderSize: true,
handle: 'div',
helper: 'clone',
items: 'li',
opacity: .6,
placeholder: 'placeholder',
revert: 250,
tabSize: 25,
tolerance: 'pointer',
toleranceElement: '> div',
maxLevels: 3,
isTree: true,
expandOnHover: 700,
isAllowed: function() {
return false;
}
});
$('.disclose').on('click', function() {
$(this).closest('li').toggleClass('sort-collapsed').toggleClass('sort-expanded');
});
$('.disclose').mousedown(function(e) {
if (e.which !== 1) {
return;
}
if ($(this).parent('div').hasClass('content_menu')) {
var menu_id = $(this).parent('div').parent('li').attr('menu');
$.ajax({
type: 'POST',
cache: false,
url: '/admin/ajax.php',
data: {
a: 'cookie_menus',
menu_id: menu_id
}
});
} else {
var page_id = $(this).parent('div').parent('li').attr('id');
$.ajax({
type: 'POST',
cache: false,
url: '/admin/ajax.php',
data: {
a: 'cookie_pages',
page_id: page_id
}
});
}
});
<?php foreach ($cookiePages as $key => $val): ?>
<?php if ($val): ?>$('.<?= htmlspecialchars((string)$key, ENT_QUOTES, 'UTF-8') ?>').children('div').children('span.disclose').click();<?php endif; ?>
<?php endforeach; ?>
<?php foreach ($cookieMenus as $key => $val): ?>
<?php if ($val): ?>$('.menu_<?= (int)$key ?>').children('div').children('span.disclose').click();<?php endif; ?>
<?php endforeach; ?>
$('body').on('change', '.image-alt', function() {
var image_alt = $(this).val();
var image_id = $(this).attr('image-id');
$.ajax({
type: 'POST',
cache: false,
url: '/admin/articles/article_image_alt_change/',
data: {
image_id: image_id,
image_alt: image_alt
},
beforeSend: function() {
$('#overlay').show();
},
success: function(data) {
$('#overlay').hide();
var response = jQuery.parseJSON(data);
if (response.status !== 'ok') {
create_error(response.msg);
}
}
});
});
$('body').on('change', '.article_file_edit', function() {
var file_name = $(this).val();
var file_id = $(this).attr('file_id');
$.ajax({
type: 'POST',
cache: false,
url: '/admin/articles/article_file_name_change/',
data: {
file_id: file_id,
file_name: file_name
},
beforeSend: function() {
$('#overlay').show();
},
success: function(data) {
$('#overlay').hide();
var response = jQuery.parseJSON(data);
if (response.status !== 'ok') {
create_error(response.msg);
}
}
});
});
$('body').on('click', '.article_file_delete', function() {
$(this).blur();
var file_id = $(this).attr('file_id');
confirmDialog('Na pewno chcesz usunac wybrany plik?', function() {
$.ajax({
type: 'POST',
cache: false,
url: '/admin/articles/article_file_delete/',
data: {
file_id: file_id
},
beforeSend: function() {
$('#file-' + file_id).children('input').addClass('disabled');
$('#file-' + file_id).children('a').addClass('disabled');
},
success: function(data) {
var response = jQuery.parseJSON(data);
if (response.status === 'ok') {
$('#file-' + file_id).remove();
refreshFilesOrderInput();
} else {
create_error(response.msg);
}
}
});
}, 'Usun');
return false;
});
$('body').on('click', '.article_image_delete', function() {
$(this).blur();
var image_id = $(this).attr('image-id');
confirmDialog('Na pewno chcesz usunac wybrane zdjecie?', function() {
$.ajax({
type: 'POST',
cache: false,
url: '/admin/articles/article_image_delete/',
data: {
image_id: image_id
},
beforeSend: function() {
$('#overlay').show();
},
success: function(data) {
$('#overlay').hide();
var response = jQuery.parseJSON(data);
if (response.status === 'ok') {
$('#image-' + image_id).remove();
refreshGalleryOrderInput();
} else {
create_error(response.msg);
}
}
});
}, 'Usun');
return false;
});
$('body').on('click', '#g-edit-save, #g-edit-save-close', function() {
refreshGalleryOrderInput();
refreshFilesOrderInput();
});
});
function generate_seo_links(lang, title, article_id) {
if (title === '') {
return false;
}
$.ajax({
type: 'POST',
cache: false,
url: '/admin/ajax.php',
data: {
a: 'generate_seo_link',
title: title,
article_id: article_id
},
beforeSend: function() {
$('#seo_link_' + lang).parents('.g-form-data').find('input, a').each(function() {
$(this).prop('disabled', true).addClass('disabled');
});
},
success: function(data) {
$('#seo_link_' + lang).parents('.g-form-data').find('input, a').each(function() {
$(this).prop('disabled', false).removeClass('disabled');
});
var response = jQuery.parseJSON(data);
if (response.status === 'ok') {
$('#seo_link_' + lang).val(response.seo_link);
} else {
create_error(response.msg);
}
}
});
}
</script>

View File

@@ -1,804 +1,6 @@
<script type="text/javascript" src="/libraries/ckeditor/ckeditor.js"></script> <?= \Tpl::view('components/form-edit', ['form' => $this->form]); ?>
<script type="text/javascript" src="/libraries/ckeditor/adapters/jquery.js"></script> <?= \Tpl::view('articles/article-edit-custom-script', [
<script type="text/javascript" src="/libraries/jquery/sortable/sortable.js"></script> 'article' => $this->article,
<? 'user' => $this->user,
global $db; 'languages' => $this->form->languages ?? []
]); ?>
$upload_token = bin2hex( random_bytes(24) );
$_SESSION['upload_tokens'][$upload_token] = [
'user_id' => $this -> user['id'],
'expires' => time() + 60*20
];
$_SESSION['rfm_akey'] = bin2hex(random_bytes(16));
$_SESSION['rfm_akey_expires'] = time() + 20*60;
$_SESSION['can_use_rfm'] = true;
$rfmAkeyJS = $_SESSION['rfm_akey'];
ob_start();
?>
<div id="settings-tabs">
<ul class="resp-tabs-list settings-tabs">
<li><i class="fa fa-file"></i>Treść</li>
<li><i class="fa fa-wrench"></i>Ustawienia</li>
<li><i class="fa fa-globe"></i>SEO</li>
<li><i class="fa fa-share-alt"></i>Wyświetlanie</li>
<li><i class="fa fa-file-image-o"></i>Galeria</li>
<li><i class="fa fa-file-archive-o"></i>Załączniki</li>
</ul>
<div class="resp-tabs-container settings-tabs">
<div>
<div id="languages-main">
<ul class="resp-tabs-list languages-main htabs">
<? if ( is_array( $this -> languages ) ): foreach ( $this -> languages as $lg ):?>
<? if ( $lg['status'] ):?>
<li><? if ( $lg['id'] == \front\factory\Languages::default_language() ) echo '<i class="fa fa-star fa-lg text-system" title="Język domyślny"></i> ';?><?= $lg['name'];?></a></li>
<? endif;?>
<? endforeach; endif;?>
</ul>
<div class="resp-tabs-container languages-main">
<? if ( is_array( $this -> languages ) ): foreach ( $this -> languages as $lg ):?>
<?
$languages = array();
$languages[''] = '---- wersja językowa ----';
if ( is_array( $this -> languages ) ) foreach ( $this -> languages as $lg_tmp )
{
if ( $lg_tmp['id'] != $lg['id'] )
$languages[ $lg_tmp['id'] ] = $lg_tmp['name'];
}
?>
<? if ( $lg['status'] ):?>
<div>
<?= \Html::select(
array(
'label' => 'Wyświetlaj treść z wersji',
'name' => 'copy_from[' . $lg['id'] . ']',
'values' => $languages,
'value' => $this -> article['languages'][ $lg['id'] ]['copy_from'],
)
);?>
<?= \Html::input(
array(
'label' => 'Tytuł',
'name' => 'title[' . $lg['id'] . ']',
'id' => 'title_' . $lg['id'],
'value' => $this -> article['languages'][ $lg['id'] ]['title'],
'inline' => true
)
);?>
<?= \Html::input_icon( [
'label' => 'Zdjęcie tytułowe',
'name' => 'main_image[' . $lg['id'] . ']',
'id' => 'main_image_' . $lg['id'],
'value' => htmlspecialchars( $this -> article['languages'][ $lg['id'] ]['main_image'] ),
'icon_content' => 'przeglądaj',
'inline' => true,
'icon_js' => "window.open ( '/libraries/filemanager-9.14.2/dialog.php?type=1&popup=1&field_id=main_image_" . $lg['id'] . "&akey=" . $rfmAkeyJS . "', 'mywindow', 'location=1,status=1,scrollbars=1, width=1100,height=700');"
] );
?>
<?= \Html::textarea(
array(
'label' => 'Wstęp',
'name' => 'entry[' . $lg['id'] . ']',
'id' => 'entry_' . $lg['id'],
'value' => $this -> article['languages'][ $lg['id'] ]['entry'],
'inline' => true
)
);?>
<?= \Html::textarea(
array(
'label' => 'Treść',
'name' => 'text[' . $lg['id'] . ']',
'id' => 'text_' . $lg['id'],
'value' => $this -> article['languages'][ $lg['id'] ]['text'],
'inline' => true
)
);?>
<?= \Html::textarea( [
'label' => 'Spis treści',
'name' => 'table_of_contents[' . $lg['id'] . ']',
'id' => 'table_of_contents_' . $lg['id'],
'value' => $this -> article['languages'][ $lg['id'] ]['table_of_contents'],
'inline' => true
] );?>
<script type="text/javascript">
$( function() {
$( '#text_<?= $lg['id'];?>, #entry_<?= $lg['id'];?>, #table_of_contents_<?= $lg['id'];?>' ).ckeditor( {
toolbar : 'MyToolbar',
height:'250',
filebrowserBrowseUrl: '/libraries/filemanager-9.14.2/dialog.php?type=2&editor=ckeditor&akey=<?= $rfmAkeyJS;?>',
filebrowserImageBrowseUrl: '/libraries/filemanager-9.14.2/dialog.php?type=1&editor=ckeditor&akey=<?= $rfmAkeyJS;?>',
filebrowserUploadUrl: '/libraries/filemanager-9.14.2/dialog.php?type=2&editor=ckeditor&upload=1&akey=<?= $rfmAkeyJS;?>',
filebrowserImageUploadUrl: '/libraries/filemanager-9.14.2/dialog.php?type=1&editor=ckeditor&upload=1&akey=<?= $rfmAkeyJS;?>'
});
});
</script>
</div>
<? endif;?>
<? endforeach; endif;?>
</div>
<div class="clear"></div>
</div>
</div>
<div>
<?= \Html::input_switch(
array(
'label' => 'Opublikowany',
'name' => 'status',
'checked' => $this -> article['status'] == 1 or !$this -> article['id'] ? true : false
)
);?>
<?= \Html::input_switch(
array(
'label' => 'Pokaż tytuł',
'name' => 'show_title',
'checked' => $this -> article['show_title'] == 1 ? true : false
)
);?>
<?= \Html::input_switch( [
'label' => 'Pokaż spis treści',
'name' => 'show_table_of_contents',
'checked' => $this -> article['show_table_of_contents'] == 1 ? true : false
] );?>
<?= \Html::input_switch(
array(
'label' => 'Pokaż datę dodania',
'name' => 'show_date_add',
'checked' => $this -> article['show_date_add'] == 1 ? true : false
)
);?>
<?= \Html::input_switch(
array(
'label' => 'Pokaż datę modyfikacji',
'name' => 'show_date_modify',
'checked' => $this -> article['show_date_modify'] == 1 ? true : false
)
);?>
<?= \Html::input_switch(
array(
'label' => 'Powtórz wprowadzenie',
'name' => 'repeat_entry',
'checked' => $this -> article['repeat_entry'] == 1 ? true : false
)
);?>
<?= \Html::input_switch(
array(
'label' => 'Linki do portali społecznościowych',
'name' => 'social_icons',
'checked' => $this -> article['social_icons'] == 1 ? true : false
)
);?>
</div>
<div>
<div id="languages-seo">
<ul class="resp-tabs-list languages-seo htabs">
<? if ( is_array( $this -> languages ) ): foreach ( $this -> languages as $lg ):?>
<? if ( $lg['status'] ):?>
<li><? if ( $lg['id'] == \front\factory\Languages::default_language() ) echo '<i class="fa fa-star fa-lg text-system" title="Język domyślny"></i> ';?><?= $lg['name'];?></a></li>
<? endif;?>
<? endforeach; endif;?>
</ul>
<div class="resp-tabs-container languages-seo">
<? if ( is_array( $this -> languages ) ): foreach ( $this -> languages as $lg ):?>
<? if ( $lg['status'] ):?>
<div>
<?= \Html::input_icon(
array(
'label' => 'Link SEO',
'name' => 'seo_link[' . $lg['id'] . ']',
'id' => 'seo_link_' . $lg['id'],
'value' => $this -> article['languages' ][ $lg['id'] ]['seo_link'],
'icon_content' => 'generuj',
'icon_js' => 'generate_seo_links( "' . $lg['id'] . '", $( "#title_' . $lg['id'] . '" ).val(), ' . (int)$this -> article['id'] . ' );'
)
);?>
<?= \Html::input( [
'label' => 'Meta title',
'name' => 'meta_title[' . $lg['id'] . ']',
'id' => 'meta_title_' . $lg['id'],
'value' => $this -> article['languages'][ $lg['id'] ]['meta_title']
]
);?>
<?= \Html::textarea(
array(
'label' => 'Meta description',
'name' => 'meta_description[' . $lg['id'] . ']',
'id' => 'meta_description_' . $lg['id'],
'value' => $this -> article['languages'][ $lg['id'] ]['meta_description']
)
);?>
<?= \Html::textarea(
array(
'label' => 'Meta keywords',
'name' => 'meta_keywords[' . $lg['id'] . ']',
'id' => 'meta_keywords_' . $lg['id'],
'value' => $this -> article['languages'][ $lg['id'] ]['meta_keywords']
)
);?>
<?= \Html::input_switch( [
'label' => 'Blokuj indeksację',
'name' => 'noindex[' . $lg['id'] . ']',
'id' => 'noindex_' . $lg['id'],
'checked' => $this -> article['languages'][ $lg['id'] ]['noindex'] == 1 ? 1 : 0
]
);?>
<?= \Html::input_switch( [
'label' => 'Blokuj bezpośredni dostęp',
'name' => 'block_direct_access[' . $lg['id'] . ']',
'id' => 'block_direct_access_' . $lg['id'],
'checked' => $this -> article['languages'][ $lg['id'] ]['block_direct_access'] == 1 ? 1 : 0
] );?>
</div>
<? endif;?>
<? endforeach; endif;?>
</div>
<div class="clear"></div>
</div>
</div>
<div>
<?
$layouts[''] = '---- szablon domyślny ----';
if ( is_array( $this -> layouts ) ): foreach ( $this -> layouts as $layout ):
$layouts[ $layout['id'] ] = $layout['name'];
endforeach; endif;
?>
<?= \Html::select(
array(
'label' => 'Szablon',
'name' => 'layout_id',
'id' => 'layout_id',
'values' => $layouts,
'value' => $this -> article['layout_id']
)
);?>
<div class="form-group">
<label class="col-lg-4 control-label">Wyświetlaj na:</label>
<div class="col-lg-8">
<?
if ( is_array( $this -> menus ) ) foreach ( $this -> menus as $menu )
{
?>
<div class="menu_sortable">
<ol class="sortable" id="sortable_<?= $menu['id'];?>">
<li id="list_<?= $menu['id'];?>" class="menu_<?= $menu['id'];?>" menu="<?= $menu['id'];?>">
<div class="context_0 content content_menu" <? if ( !$menu['status'] ) echo 'style="color: #cc0000;"';?>>
<span class="disclose"><span></span></span>
Menu: <b><?= $menu['name'];?></b>
</div>
<?= \admin\view\Articles::subpages_list( \admin\factory\Pages::menu_pages( $menu['id'] ), $this -> article['pages'] );?>
</li>
</ol>
</div>
<?
}
?>
</div>
<div class="clear"></div>
</div>
</div>
<div>
<ul id="images-list">
<?
$images_count = 0;
if ( is_array( $this -> article['images'] ) ): foreach ( $this -> article['images'] as $img ):
?>
<li id="image-<?= $img['id'];?>">
<img class="article-image lozad" data-src="/libraries/thumb.php?img=<?= $img['src'];?>&w=300&h=300">
<a href="#" class="input-group-addon btn btn-danger article_image_delete" image-id="<?= $img['id'];?>">
<i class="fa fa-trash"></i>
</a>
<input type="text" class="form-control image-alt" value="<?= $img['alt'];?>" image-id="<?= $img['id'];?>" placeholder="atrybut alt...">
</li>
<?
$images_count++;
endforeach; endif;
?>
</ul>
<div id="images-uploader">You browser doesn't have Flash installed.</div>
</div>
<div>
<ul id="files-list">
<?
$files_count = 0;
if ( is_array( $this -> article['files'] ) ): foreach ( $this -> article['files'] as $file ):
if ( $file['name'] )
$name = $file['name'];
else
{
$name = explode( '/', $file['src'] );
$name = $name[ count( $name ) - 1 ];
}
?>
<li id="file-<?= $file['id'];?>">
<div class="input-group">
<input type="text" class="article_file_edit form-control" file_id="<?= $file['id'];?>" value="<?= $name;?>" />
<a href="#" class="input-group-addon btn btn-info article_file_delete" file_id="<?= $file['id'];?>">
<i class="fa fa-trash"></i>
</a>
</div>
</li>
<?
$files_count++;
endforeach; endif;
?>
</ul>
<div id="files-uploader">You browser doesn't have Flash installed.</div>
</div>
</div>
</div>
<?
$out = ob_get_clean();
$grid = new \gridEdit;
$grid -> id = 'article-edit';
$grid -> gdb_opt = $gdb;
$grid -> include_plugins = true;
$grid -> title = 'Edycja artykułu';
$grid -> fields = [
[
'db' => 'id',
'type' => 'hidden',
'value' => $this -> article['id']
]
];
$grid -> actions = [
'save' => [ 'url' => '/admin/articles/article_save/', 'back_url' => '/admin/articles/view_list/' ],
'cancel' => [ 'url' => '/admin/articles/view_list/' ]
];
$grid -> external_code = $out;
$grid -> persist_edit = true;
$grid -> id_param = 'id';
echo $grid -> draw();
?>
<link type="text/css" rel="stylesheet" href="/libraries/plupload/jquery.plupload.queue/css/jquery.plupload.queue.css" />
<script type="text/javascript" src="/libraries/plupload/plupload.js"></script>
<script type="text/javascript" src="/libraries/plupload/plupload.flash.js"></script>
<script type="text/javascript" src="/libraries/plupload/plupload.html5.js"></script>
<script type="text/javascript" src="/libraries/plupload/plupload.html4.js"></script>
<script type="text/javascript" src="/libraries/plupload/jquery.plupload.queue/jquery.plupload.queue.js"></script>
<script type="text/javascript" src="/libraries/plupload/i18n/pl.js"></script>
<script type="text/javascript" src="/libraries/jquery-nested-sortable/jquery.mjs.nestedSortable.js"></script>
<script type="text/javascript" src="/libraries/jquery/lozad.js"></script>
<script type="text/javascript">
var images_count = <?= $images_count;?>;
var files_count = <?= $files_count;?>;
$( function()
{
const observer = lozad();
observer.observe();
disable_menu();
var el = document.getElementById( 'images-list' );
var sortable = Sortable.create(el, {
onEnd: function ( evt ) {
var order = '';
$( '#images-list li' ).each( function()
{
if ( order !== '' )
order += ';';
order += $( this ).children( 'a' ).attr( 'image-id' );
});
$.ajax(
{
type: 'POST',
cache: false,
url: '/admin/articles/gallery_order_save/',
data:
{
article_id: '<?= $this -> article['id'];?>',
order: order
},
beforeSend: function()
{
$( '#overlay' ).show();
},
success: function( data )
{
$( '#overlay' ).hide();
response = jQuery.parseJSON( data );
if ( response.status !== 'ok' )
create_error( response.msg );
}
});
}
});
$( "#images-uploader" ).pluploadQueue(
{
multipart_params: {
upload_token: '<?= $upload_token ?>',
},
runtimes: 'html5,flash,html4',
init:
{
Refresh: function(up)
{
$( ".plupload_buttons" ).css( "display", "inline" );
$( ".plupload_upload_status" ).css( "display", "inline" );
$( ".plupload_start" ).addClass( "plupload_disabled" );
$( ".plupload_start" ).removeClass( "plupload_disabled" );
},
UploadComplete: function()
{
$( ".plupload_buttons" ).css( "display", "inline" );
$( ".plupload_upload_status" ).css( "display", "inline" );
$( ".plupload_start" ).addClass( "plupload_disabled" );
$( ".plupload_start" ).removeClass( "plupload_disabled" );
},
FileUploaded: function( up, file, response )
{
data = jQuery.parseJSON( response.response );
$( '#images-list' ).append(
'<li id="image-' + data.image_id + '">' +
'<img class="article-image lozad" data-src="/libraries/thumb.php?img=' + data.data_link + '&w=300&h=300">' +
'<a href="#" class="input-group-addon btn btn-danger article_image_delete" image-id="' + data.image_id + '">' +
'<i class="fa fa-trash"></i>' +
'</a>' +
'<input type="text" class="form-control image-alt" value="" image-id="' + data.image_id + '" placeholder="atrybut alt...">' +
'</li>'
);
images_count++;
observer.observe();
$( 'html, body' ).animate({
scrollTop: $( "#images-uploader" ).offset().top
}, 1 );
}
},
url: '/libraries/plupload/upload-articles-images.php',
chunk_size: '1mb',
unique_names: false,
resize : {
width : <?= $this -> settings['image_px'] ? $this -> settings['image_px'] : '1920';?>,
height : <?= $this -> settings['image_px'] ? $this -> settings['image_px'] : '1920';?>,
quality : 95
},
filters: [
{ title: "Obrazki", extensions : "jpg, gif, png, bmp, jpeg" }
],
flash_swf_url: '/../libraries/plupload/plupload.flash.swf'
});
$( "#files-uploader" ).pluploadQueue(
{
multipart_params: {
upload_token: '<?= $upload_token ?>',
},
runtimes: 'html5,flash,html4',
init:
{
Refresh: function( up )
{
$( ".plupload_buttons" ).css( "display", "inline" );
$( ".plupload_upload_status" ).css( "display", "inline" );
$( ".plupload_start" ).addClass( "plupload_disabled" );
$( ".plupload_start" ).removeClass( "plupload_disabled" );
},
FileUploaded: function( up, file, response )
{
data = jQuery.parseJSON( response.response );
$( '#files-list' ).append(
'<li id="file-' + data.file_id + '">' +
'<div class="input-group">' +
'<input type="text" class="form-control article_file_edit" file_id="' + data.file_id + '" value="' + data.file_name + '" />' +
'<a href="#" class="input-group-addon btn btn-info article_file_delete" file_id="' + data.file_id + '">' +
'<i class="fa fa-trash"></i>' +
'</a>' +
'</div>' +
'</li>'
);
files_count++;
}
},
url: '/libraries/plupload/upload-articles-files.php',
chunk_size: '1mb',
unique_names: false,
filters: [
{ title: "Wszystkie pliki", extensions: "*" }
],
flash_swf_url: '/../libraries/plupload/plupload.flash.swf'
});
$( '#settings-tabs' ).easyResponsiveTabs({
width: 'auto',
fit: true,
tabidentify: 'settings-tabs',
type: 'vertical'
});
$( '#languages-main' ).easyResponsiveTabs({
width: 'auto',
fit: true,
tabidentify: 'languages-main'
});
$( '#languages-seo' ).easyResponsiveTabs({
width: 'auto',
fit: true,
tabidentify: 'languages-seo'
});
$('ol.sortable').nestedSortable({
forcePlaceholderSize: true,
handle: 'div',
helper: 'clone',
items: 'li',
opacity: .6,
placeholder: 'placeholder',
revert: 250,
tabSize: 25,
tolerance: 'pointer',
toleranceElement: '> div',
maxLevels: 3,
isTree: true,
expandOnHover: 700,
isAllowed: function()
{
return false;
}
});
$( '.disclose' ).on( 'click', function() {
$( this ).closest( 'li' ).toggleClass( 'sort-collapsed' ).toggleClass( 'sort-expanded' );
});
$( '.disclose' ).mousedown( function(e) {
if ( e.which === 1 ) {
if ( $( this ).parent( 'div' ).hasClass( 'content_menu' ) )
{
var menu_id = $( this ).parent( 'div' ).parent( 'li' ).attr( 'menu' );
$.ajax(
{
type: 'POST',
cache: false,
url: '/admin/ajax.php',
data: {
a: 'cookie_menus',
menu_id: menu_id
}
});
}
else
{
var page_id = $( this ).parent( 'div' ).parent( 'li' ).attr( 'id' );
$.ajax(
{
type: 'POST',
cache: false,
url: '/admin/ajax.php',
data: {
a: 'cookie_pages',
page_id: page_id
}
});
}
}
});
<?php
$array = unserialize( $_COOKIE[ 'cookie_pages' ] );
if ( is_array( $array ) ): foreach ( $array as $key => $val ):
if ( $val ):
?>$( '.<?= $key;?>' ).children( 'div' ).children( 'span.disclose' ).click();<?
endif;
endforeach; endif;
$array = unserialize( $_COOKIE[ 'cookie_menus' ] );
if ( is_array( $array ) ): foreach ( $array as $key => $val ):
if ( $val ):
?>$( '.menu_<?= $key;?>' ).children( 'div' ).children( 'span.disclose' ).click();<?
endif;
endforeach; endif;
?>
$( 'body' ).on( 'change', '.image-alt', function()
{
var image_alt = $( this ).val();
var image_id = $( this ).attr( 'image-id' );
$.ajax(
{
type: 'POST',
cache: false,
url: '/admin/ajax.php',
data:
{
a: 'article_image_alt_change',
image_id: image_id,
image_alt: image_alt
},
beforeSend: function()
{
$( '#overlay' ).show();
},
success: function( data )
{
$( '#overlay' ).hide();
response = jQuery.parseJSON( data );
if ( response.status !== 'ok' )
create_error( response.msg );
}
});
});
$( 'body' ).on( 'change', '.article_file_edit', function()
{
var file_name = $( this ).val();
var file_id = $( this ).attr( 'file_id' );
$.ajax({
type: 'POST',
cache: false,
url: '/admin/ajax.php',
data:
{
a: 'article_file_name_change',
file_id: file_id,
file_name: file_name
},
beforeSend: function()
{
$( '#overlay' ).show();
},
success: function( data )
{
$( '#overlay' ).hide();
response = jQuery.parseJSON( data );
if ( response.status !== 'ok' )
create_error( response.msg );
}
});
});
$( 'body' ).on( 'click', '.article_file_delete', function()
{
$( this ).blur();
var file_id = $( this ).attr( 'file_id' );
$.prompt( 'Na pewno chcesz usunąć wybrany plik?', {
title: 'Potwierdź?',
submit: function(e,v,m,f)
{
if ( v === true )
{
$.ajax({
type: 'POST',
cache: false,
url: '/admin/ajax.php',
data:
{
a: 'article_file_delete',
file_id: file_id
},
beforeSend: function()
{
$( '#file-' + file_id ).children( 'input' ).addClass( 'disabled' );
$( '#file-' + file_id ).children( 'a' ).addClass( 'disabled' );
},
success: function( data )
{
response = jQuery.parseJSON( data );
if ( response.status === 'ok' )
$( '#file-' + file_id ).remove();
else
create_error( msg );
}
});
}
},
buttons: {
'tak': true,
'nie': false
},
focus: 0
});
});
$( 'body' ).on( 'click', '.article_image_delete', function()
{
$( this ).blur();
var image_id = $( this ).attr( 'image-id' );
$.prompt( 'Na pewno chcesz usunąć wybrane zdjęcie?',
{
title: 'Potwierdź?',
submit: function(e,v,m,f)
{
if ( v === true )
{
$.ajax(
{
type: 'POST',
cache: false,
url: '/admin/ajax.php',
data:
{
a: 'article_image_delete',
image_id: image_id
},
beforeSend: function()
{
$( '#overlay' ).show();
},
success: function( data )
{
$( '#overlay' ).hide();
response = jQuery.parseJSON( data );
if ( response.status === 'ok' )
$( '#image-' + image_id ).remove();
else
create_error( msg );
}
});
}
},
buttons: {
'tak': true,
'nie': false
},
focus: 0
});
return false;
});
});
function generate_seo_links( lang, title, article_id )
{
if ( title === '' )
return false;
$.ajax(
{
type: 'POST',
cache: false,
url: '/admin/ajax.php',
data:
{
a: 'generate_seo_link',
title: title,
article_id: article_id
},
beforeSend: function()
{
$( '#seo_link_' + lang ).parents( '.g-form-data' ).find( 'input, a' ).each( function()
{
$( this ).prop( 'disabled', true ).addClass( 'disabled' );
});
},
success: function( data ) {
$( '#seo_link_' + lang ).parents( '.g-form-data' ).find( 'input, a' ).each( function() {
$( this ).prop( 'disabled', false ).removeClass( 'disabled' );
});
response = jQuery.parseJSON( data );
if ( response.status === 'ok' )
$( '#seo_link_' + lang ).val( response.seo_link );
else
create_error( response.msg );
}
});
}
</script>
<script>CKEDITOR.dtd.$removeEmpty['span'] = false;</script>

View File

@@ -6,10 +6,12 @@
<span class="disclose"><span></span></span> <span class="disclose"><span></span></span>
<input type="checkbox" class="g-checkbox" name="pages[]" value="<?= $page['id'];?>" <? if ( is_array( $this -> article_pages ) and in_array( $page['id'], $this -> article_pages ) ):?>checked="checked"<? endif;?> /><?= $page['title'];?> <input type="checkbox" class="g-checkbox" name="pages[]" value="<?= $page['id'];?>" <? if ( is_array( $this -> article_pages ) and in_array( $page['id'], $this -> article_pages ) ):?>checked="checked"<? endif;?> /><?= $page['title'];?>
</div> </div>
<? <?= \Tpl::view( 'articles/subpages-list', [
if ( is_array( $page['subpages'] ) ) 'pages' => $page['subpages'],
echo \admin\view\Articles::subpages_list( $page['subpages'], $this -> article_pages, $page['id'], $this -> step + 1 ); 'article_pages' => $this->article_pages,
?> 'parent_id' => $page['id'],
'step' => $this->step + 1,
] ); ?>
</li> </li>
<? endforeach;?> <? endforeach;?>
</ol> </ol>

View File

@@ -949,6 +949,7 @@ echo $grid->draw();
}, },
url: '/libraries/plupload/upload-product-images.php', url: '/libraries/plupload/upload-product-images.php',
chunk_size: '1mb', chunk_size: '1mb',
max_file_size: '20mb',
unique_names: false, unique_names: false,
resize: { resize: {
width: <?= $this->settings['image_px'] ? $this->settings['image_px'] : '1920'; ?>, width: <?= $this->settings['image_px'] ? $this->settings['image_px'] : '1920'; ?>,
@@ -991,6 +992,7 @@ echo $grid->draw();
}, },
url: '/libraries/plupload/upload-product-files.php', url: '/libraries/plupload/upload-product-files.php',
chunk_size: '1mb', chunk_size: '1mb',
max_file_size: '50mb',
unique_names: false, unique_names: false,
filters: [{ filters: [{
title: "Wszystkie pliki", title: "Wszystkie pliki",

View File

@@ -37,7 +37,15 @@ class ArticleRepository
'article_id' => $articleId, 'article_id' => $articleId,
'ORDER' => ['o' => 'ASC', 'id' => 'DESC'] 'ORDER' => ['o' => 'ASC', 'id' => 'DESC']
]); ]);
$article['files'] = $this->db->select('pp_articles_files', '*', ['article_id' => $articleId]); try {
$article['files'] = $this->db->select('pp_articles_files', '*', [
'article_id' => $articleId,
'ORDER' => ['o' => 'ASC', 'id' => 'DESC']
]);
} catch (\Throwable $e) {
// Fallback for instances where pp_articles_files does not yet have "o" column.
$article['files'] = $this->db->select('pp_articles_files', '*', ['article_id' => $articleId]);
}
$article['pages'] = $this->db->select('pp_articles_pages', 'page_id', ['article_id' => $articleId]); $article['pages'] = $this->db->select('pp_articles_pages', 'page_id', ['article_id' => $articleId]);
return $article; return $article;
@@ -70,6 +78,8 @@ class ArticleRepository
$this->savePages($id, $data['pages'] ?? null, true); $this->savePages($id, $data['pages'] ?? null, true);
$this->assignTempFiles($id); $this->assignTempFiles($id);
$this->assignTempImages($id); $this->assignTempImages($id);
$this->applyGalleryOrderIfProvided($id, $data);
$this->applyFilesOrderIfProvided($id, $data);
\S::htacces(); \S::htacces();
\S::delete_dir('../temp/'); \S::delete_dir('../temp/');
@@ -87,6 +97,8 @@ class ArticleRepository
$this->savePages($articleId, $data['pages'] ?? null, false); $this->savePages($articleId, $data['pages'] ?? null, false);
$this->assignTempFiles($articleId); $this->assignTempFiles($articleId);
$this->assignTempImages($articleId); $this->assignTempImages($articleId);
$this->applyGalleryOrderIfProvided($articleId, $data);
$this->applyFilesOrderIfProvided($articleId, $data);
$this->deleteMarkedImages($articleId); $this->deleteMarkedImages($articleId);
$this->deleteMarkedFiles($articleId); $this->deleteMarkedFiles($articleId);
@@ -99,16 +111,16 @@ class ArticleRepository
private function buildArticleRow(array $data, int $userId, bool $isNew): array private function buildArticleRow(array $data, int $userId, bool $isNew): array
{ {
$row = [ $row = [
'show_title' => ($data['show_title'] ?? '') == 'on' ? 1 : 0, 'show_title' => $this->isCheckedValue($data['show_title'] ?? null) ? 1 : 0,
'show_date_add' => ($data['show_date_add'] ?? '') == 'on' ? 1 : 0, 'show_date_add' => $this->isCheckedValue($data['show_date_add'] ?? null) ? 1 : 0,
'show_date_modify' => ($data['show_date_modify'] ?? '') == 'on' ? 1 : 0, 'show_date_modify' => $this->isCheckedValue($data['show_date_modify'] ?? null) ? 1 : 0,
'date_modify' => date('Y-m-d H:i:s'), 'date_modify' => date('Y-m-d H:i:s'),
'modify_by' => $userId, 'modify_by' => $userId,
'layout_id' => !empty($data['layout_id']) ? (int)$data['layout_id'] : null, 'layout_id' => !empty($data['layout_id']) ? (int)$data['layout_id'] : null,
'status' => ($data['status'] ?? '') == 'on' ? 1 : 0, 'status' => $this->isCheckedValue($data['status'] ?? null) ? 1 : 0,
'repeat_entry' => ($data['repeat_entry'] ?? '') == 'on' ? 1 : 0, 'repeat_entry' => $this->isCheckedValue($data['repeat_entry'] ?? null) ? 1 : 0,
'social_icons' => ($data['social_icons'] ?? '') == 'on' ? 1 : 0, 'social_icons' => $this->isCheckedValue($data['social_icons'] ?? null) ? 1 : 0,
'show_table_of_contents' => ($data['show_table_of_contents'] ?? '') == 'on' ? 1 : 0, 'show_table_of_contents' => $this->isCheckedValue($data['show_table_of_contents'] ?? null) ? 1 : 0,
]; ];
if ($isNew) { if ($isNew) {
@@ -131,12 +143,32 @@ class ArticleRepository
'meta_description' => ($data['meta_description'][$langId] ?? '') != '' ? $data['meta_description'][$langId] : null, 'meta_description' => ($data['meta_description'][$langId] ?? '') != '' ? $data['meta_description'][$langId] : null,
'meta_keywords' => ($data['meta_keywords'][$langId] ?? '') != '' ? $data['meta_keywords'][$langId] : null, 'meta_keywords' => ($data['meta_keywords'][$langId] ?? '') != '' ? $data['meta_keywords'][$langId] : null,
'seo_link' => \S::seo($data['seo_link'][$langId] ?? '') != '' ? \S::seo($data['seo_link'][$langId]) : null, 'seo_link' => \S::seo($data['seo_link'][$langId] ?? '') != '' ? \S::seo($data['seo_link'][$langId]) : null,
'noindex' => ($data['noindex'][$langId] ?? '') == 'on' ? 1 : 0, 'noindex' => $this->isCheckedValue($data['noindex'][$langId] ?? null) ? 1 : 0,
'copy_from' => ($data['copy_from'][$langId] ?? '') != '' ? $data['copy_from'][$langId] : null, 'copy_from' => ($data['copy_from'][$langId] ?? '') != '' ? $data['copy_from'][$langId] : null,
'block_direct_access' => ($data['block_direct_access'][$langId] ?? '') == 'on' ? 1 : 0, 'block_direct_access' => $this->isCheckedValue($data['block_direct_access'][$langId] ?? null) ? 1 : 0,
]; ];
} }
private function applyGalleryOrderIfProvided(int $articleId, array $data): void
{
$order = trim((string)($data['gallery_order'] ?? ''));
if ($order === '') {
return;
}
$this->saveGalleryOrder($articleId, $order);
}
private function applyFilesOrderIfProvided(int $articleId, array $data): void
{
$order = trim((string)($data['files_order'] ?? ''));
if ($order === '') {
return;
}
$this->saveFilesOrder($articleId, $order);
}
private function saveTranslations(int $articleId, array $data, bool $isNew): void private function saveTranslations(int $articleId, array $data, bool $isNew): void
{ {
$titles = $data['title'] ?? []; $titles = $data['title'] ?? [];
@@ -573,6 +605,182 @@ class ArticleRepository
return true; return true;
} }
/**
* Zapisuje kolejnosc zalacznikow artykulu.
*/
public function saveFilesOrder(int $articleId, string $order): bool
{
$fileIds = explode(';', $order);
if (!is_array($fileIds) || empty($fileIds)) {
return true;
}
$position = 0;
foreach ($fileIds as $fileId) {
if ($fileId === '' || $fileId === null) {
continue;
}
try {
$this->db->update('pp_articles_files', [
'o' => $position++,
], [
'AND' => [
'article_id' => $articleId,
'id' => (int)$fileId,
],
]);
} catch (\Throwable $e) {
// Fallback for instances where pp_articles_files does not yet have "o" column.
return true;
}
}
return true;
}
/**
* Zwraca mape: article_id => etykieta stron (np. " - Strona A / Strona B").
*
* @param array<int, int> $articleIds
* @return array<int, string>
*/
public function pagesSummaryForArticles(array $articleIds): array
{
$normalizedIds = [];
foreach ($articleIds as $articleId) {
$id = (int)$articleId;
if ($id > 0) {
$normalizedIds[$id] = $id;
}
}
if (empty($normalizedIds)) {
return [];
}
$placeholders = [];
$params = [];
foreach (array_values($normalizedIds) as $index => $id) {
$key = ':article_id_' . $index;
$placeholders[] = $key;
$params[$key] = $id;
}
$sql = "
SELECT
ap.article_id,
ap.page_id,
(
SELECT title
FROM pp_pages_langs AS ppl, pp_langs AS pl
WHERE ppl.lang_id = pl.id AND ppl.page_id = ap.page_id AND ppl.title != ''
ORDER BY pl.o ASC
LIMIT 1
) AS title
FROM pp_articles_pages AS ap
WHERE ap.article_id IN (" . implode(', ', $placeholders) . ")
ORDER BY ap.article_id ASC, ap.o ASC, ap.page_id ASC
";
$stmt = $this->db->query($sql, $params);
$rows = $stmt ? $stmt->fetchAll() : [];
if (!is_array($rows)) {
return [];
}
$titlesByArticle = [];
foreach ($rows as $row) {
$articleId = (int)($row['article_id'] ?? 0);
if ($articleId <= 0) {
continue;
}
$title = trim((string)($row['title'] ?? ''));
if ($title === '') {
continue;
}
$titlesByArticle[$articleId][] = $title;
}
$summary = [];
foreach (array_values($normalizedIds) as $articleId) {
if (empty($titlesByArticle[$articleId])) {
$summary[$articleId] = '';
continue;
}
$summary[$articleId] = ' - ' . implode(' / ', $titlesByArticle[$articleId]);
}
return $summary;
}
public function updateImageAlt(int $imageId, string $imageAlt): bool
{
$result = $this->db->update('pp_articles_images', [
'alt' => $imageAlt,
], [
'id' => $imageId,
]);
\S::delete_cache();
return (bool)$result;
}
public function updateFileName(int $fileId, string $fileName): bool
{
$result = $this->db->update('pp_articles_files', [
'name' => $fileName,
], [
'id' => $fileId,
]);
return (bool)$result;
}
public function markFileToDelete(int $fileId): bool
{
$result = $this->db->update('pp_articles_files', [
'to_delete' => 1,
], [
'id' => $fileId,
]);
return (bool)$result;
}
public function markImageToDelete(int $imageId): bool
{
$result = $this->db->update('pp_articles_images', [
'to_delete' => 1,
], [
'id' => $imageId,
]);
return (bool)$result;
}
private function isCheckedValue($value): bool
{
if (is_bool($value)) {
return $value;
}
if (is_numeric($value)) {
return ((int)$value) === 1;
}
if (is_string($value)) {
$normalized = strtolower(trim($value));
return in_array($normalized, ['1', 'on', 'true', 'yes'], true);
}
return false;
}
private function appendDateRangeFilter( private function appendDateRangeFilter(
array &$where, array &$where,
array &$params, array &$params,

View File

@@ -4,6 +4,10 @@ namespace admin\Controllers;
use Domain\Article\ArticleRepository; use Domain\Article\ArticleRepository;
use Domain\Languages\LanguagesRepository; use Domain\Languages\LanguagesRepository;
use Domain\Layouts\LayoutsRepository; use Domain\Layouts\LayoutsRepository;
use admin\ViewModels\Forms\FormAction;
use admin\ViewModels\Forms\FormEditViewModel;
use admin\ViewModels\Forms\FormField;
use admin\ViewModels\Forms\FormTab;
class ArticlesController class ArticlesController
{ {
@@ -61,12 +65,21 @@ class ArticlesController
$listRequest['perPage'] $listRequest['perPage']
); );
$articleIds = [];
foreach ($result['items'] as $item) {
$id = (int)($item['id'] ?? 0);
if ($id > 0) {
$articleIds[] = $id;
}
}
$pagesSummary = $this->repository->pagesSummaryForArticles($articleIds);
$rows = []; $rows = [];
$lp = ($listRequest['page'] - 1) * $listRequest['perPage'] + 1; $lp = ($listRequest['page'] - 1) * $listRequest['perPage'] + 1;
foreach ($result['items'] as $item) { foreach ($result['items'] as $item) {
$id = (int)$item['id']; $id = (int)$item['id'];
$title = (string)($item['title'] ?? ''); $title = (string)($item['title'] ?? '');
$pages = (string)\admin\factory\Articles::article_pages($id); $pages = (string)($pagesSummary[$id] ?? '');
$rows[] = [ $rows[] = [
'lp' => $lp++ . '.', 'lp' => $lp++ . '.',
@@ -146,6 +159,18 @@ class ArticlesController
exit; exit;
} }
/**
* Zapis kolejnosci zalacznikow (AJAX)
*/
public function filesOrderSave(): void
{
if ($this->repository->saveFilesOrder((int)\S::get('article_id'), (string)\S::get('order'))) {
echo json_encode(['status' => 'ok', 'msg' => 'Artykul zostal zapisany.']);
}
exit;
}
/** /**
* Zapis artykulu (AJAX) * Zapis artykulu (AJAX)
*/ */
@@ -153,11 +178,72 @@ class ArticlesController
{ {
global $user; global $user;
$values = json_decode(\S::get('values'), true); $values = $this->resolveSavePayload();
$response = ['status' => 'error', 'msg' => 'Podczas zapisywania artykulu wystapil blad. Prosze sprobowac ponownie.']; $articleId = (int)($values['id'] ?? \S::get('id') ?? 0);
$id = $this->repository->save($articleId, $values, (int)$user['id']);
if ($id = $this->repository->save((int)($values['id'] ?? 0), $values, (int)$user['id'])) { if ($id) {
$response = ['status' => 'ok', 'msg' => 'Artykul zostal zapisany.', 'id' => $id]; echo json_encode([
'success' => true,
'status' => 'ok',
'message' => 'Artykul zostal zapisany.',
'msg' => 'Artykul zostal zapisany.',
'id' => (int)$id,
]);
exit;
}
echo json_encode([
'success' => false,
'status' => 'error',
'message' => 'Podczas zapisywania artykulu wystapil blad. Prosze sprobowac ponownie.',
'msg' => 'Podczas zapisywania artykulu wystapil blad. Prosze sprobowac ponownie.',
]);
exit;
}
public function imageAltChange(): void
{
$response = ['status' => 'error', 'msg' => 'Podczas zmiany atrybutu alt zdjecia wystapil blad. Prosze sprobowac ponownie.'];
if ($this->repository->updateImageAlt((int)\S::get('image_id'), (string)\S::get('image_alt'))) {
$response = ['status' => 'ok'];
}
echo json_encode($response);
exit;
}
public function fileNameChange(): void
{
$response = ['status' => 'error', 'msg' => 'Podczas zmiany nazwy zalacznika wystapil blad. Prosze sprobowac ponownie.'];
if ($this->repository->updateFileName((int)\S::get('file_id'), (string)\S::get('file_name'))) {
$response = ['status' => 'ok'];
}
echo json_encode($response);
exit;
}
public function imageDelete(): void
{
$response = ['status' => 'error', 'msg' => 'Podczas usuwania zdjecia wystapil blad. Prosze sprobowac ponownie.'];
if ($this->repository->markImageToDelete((int)\S::get('image_id'))) {
$response = ['status' => 'ok'];
}
echo json_encode($response);
exit;
}
public function fileDelete(): void
{
$response = ['status' => 'error', 'msg' => 'Podczas usuwania zalacznika wystapil blad. Prosze sprobowac ponownie.'];
if ($this->repository->markFileToDelete((int)\S::get('file_id'))) {
$response = ['status' => 'ok'];
} }
echo json_encode($response); echo json_encode($response);
@@ -192,12 +278,226 @@ class ArticlesController
$this->repository->deleteNonassignedImages(); $this->repository->deleteNonassignedImages();
$this->repository->deleteNonassignedFiles(); $this->repository->deleteNonassignedFiles();
$article = $this->repository->find((int)\S::get('id')) ?: ['id' => 0, 'languages' => [], 'images' => [], 'files' => [], 'pages' => []];
$languages = $this->languagesRepository->languagesList();
$menus = \admin\factory\Pages::menus_list();
$layouts = $this->layoutsRepository->listAll();
$viewModel = $this->buildFormViewModel($article, $languages, $menus, $layouts);
return \Tpl::view('articles/article-edit', [ return \Tpl::view('articles/article-edit', [
'article' => $this->repository->find((int)\S::get('id')), 'form' => $viewModel,
'menus' => \admin\factory\Pages::menus_list(), 'article' => $article,
'languages' => $this->languagesRepository->languagesList(), 'user' => $user,
'layouts' => $this->layoutsRepository->listAll(),
'user' => $user
]); ]);
} }
private function resolveSavePayload(): array
{
$legacyValuesRaw = \S::get('values');
if ($legacyValuesRaw !== null && $legacyValuesRaw !== '') {
$legacyValues = json_decode((string)$legacyValuesRaw, true);
if (is_array($legacyValues)) {
return $legacyValues;
}
}
$payload = $_POST;
unset($payload['_form_id']);
return is_array($payload) ? $payload : [];
}
private function buildFormViewModel(array $article, array $languages, array $menus, array $layouts): FormEditViewModel
{
$articleId = (int)($article['id'] ?? 0);
$defaultLanguageId = (string)($this->languagesRepository->defaultLanguageId() ?? 'pl');
$title = $articleId > 0
? 'Edycja artykulu: <u>' . $this->escapeHtml((string)($article['languages'][$defaultLanguageId]['title'] ?? '')) . '</u>'
: 'Edycja artykulu';
$layoutOptions = ['' => '---- szablon domyslny ----'];
foreach ($layouts as $layout) {
$layoutOptions[(string)$layout['id']] = (string)$layout['name'];
}
$copyFromOptions = ['' => '---- wersja jezykowa ----'];
foreach ($languages as $language) {
if (!empty($language['id'])) {
$copyFromOptions[(string)$language['id']] = (string)$language['name'];
}
}
$tabs = [
new FormTab('content', 'Tresc', 'fa-file'),
new FormTab('settings', 'Ustawienia', 'fa-wrench'),
new FormTab('seo', 'SEO', 'fa-globe'),
new FormTab('display', 'Wyswietlanie', 'fa-share-alt'),
new FormTab('gallery', 'Galeria', 'fa-file-image-o'),
new FormTab('files', 'Zalaczniki', 'fa-file-archive-o'),
];
$fields = [
FormField::hidden('id', $articleId),
FormField::langSection('article_content', 'content', [
FormField::select('copy_from', [
'label' => 'Wyswietlaj tresc z wersji',
'options' => $copyFromOptions,
]),
FormField::text('title', [
'label' => 'Tytul',
'required' => true,
'attributes' => ['id' => 'title'],
]),
FormField::image('main_image', [
'label' => 'Zdjecie tytulowe',
'filemanager' => true,
'attributes' => ['id' => 'main_image'],
]),
FormField::editor('entry', [
'label' => 'Wstep',
'toolbar' => 'MyToolbar',
'height' => 250,
'attributes' => ['id' => 'entry'],
]),
FormField::editor('text', [
'label' => 'Tresc',
'toolbar' => 'MyToolbar',
'height' => 250,
'attributes' => ['id' => 'text'],
]),
FormField::editor('table_of_contents', [
'label' => 'Spis tresci',
'toolbar' => 'MyToolbar',
'height' => 250,
'attributes' => ['id' => 'table_of_contents'],
]),
]),
FormField::switch('status', ['label' => 'Opublikowany', 'tab' => 'settings', 'value' => ((int)($article['status'] ?? 0) === 1) || $articleId === 0]),
FormField::switch('show_title', ['label' => 'Pokaz tytul', 'tab' => 'settings', 'value' => (int)($article['show_title'] ?? 0) === 1]),
FormField::switch('show_table_of_contents', ['label' => 'Pokaz spis tresci', 'tab' => 'settings', 'value' => (int)($article['show_table_of_contents'] ?? 0) === 1]),
FormField::switch('show_date_add', ['label' => 'Pokaz date dodania', 'tab' => 'settings', 'value' => (int)($article['show_date_add'] ?? 0) === 1]),
FormField::switch('show_date_modify', ['label' => 'Pokaz date modyfikacji', 'tab' => 'settings', 'value' => (int)($article['show_date_modify'] ?? 0) === 1]),
FormField::switch('repeat_entry', ['label' => 'Powtorz wprowadzenie', 'tab' => 'settings', 'value' => (int)($article['repeat_entry'] ?? 0) === 1]),
FormField::switch('social_icons', ['label' => 'Linki do portali spolecznosciowych', 'tab' => 'settings', 'value' => (int)($article['social_icons'] ?? 0) === 1]),
FormField::langSection('article_seo', 'seo', [
FormField::text('seo_link', ['label' => 'Link SEO', 'attributes' => ['id' => 'seo_link']]),
FormField::text('meta_title', ['label' => 'Meta title']),
FormField::textarea('meta_description', ['label' => 'Meta description', 'rows' => 4]),
FormField::textarea('meta_keywords', ['label' => 'Meta keywords', 'rows' => 4]),
FormField::switch('noindex', ['label' => 'Blokuj indeksacje']),
FormField::switch('block_direct_access', ['label' => 'Blokuj bezposredni dostep']),
]),
FormField::select('layout_id', [
'label' => 'Szablon',
'tab' => 'display',
'options' => $layoutOptions,
'value' => $article['layout_id'] ?? '',
]),
FormField::custom('pages_tree', $this->renderPagesTree($menus, $article), ['tab' => 'display']),
FormField::custom('images_box', $this->renderImagesBox($article), ['tab' => 'gallery']),
FormField::custom('files_box', $this->renderFilesBox($article), ['tab' => 'files']),
];
$actions = [
FormAction::save('/admin/articles/article_save/' . ($articleId > 0 ? 'id=' . $articleId : ''), '/admin/articles/view_list/'),
FormAction::cancel('/admin/articles/view_list/'),
];
return new FormEditViewModel(
'article-edit',
$title,
$article,
$fields,
$tabs,
$actions,
'POST',
'/admin/articles/article_save/' . ($articleId > 0 ? 'id=' . $articleId : ''),
'/admin/articles/view_list/',
true,
['id' => $articleId],
$languages
);
}
private function renderPagesTree(array $menus, array $article): string
{
$html = '<div class="form-group row">';
$html .= '<label class="col-lg-4 control-label">Wyswietlaj na:</label>';
$html .= '<div class="col-lg-8">';
foreach ($menus as $menu) {
$menuId = (int)($menu['id'] ?? 0);
$menuName = $this->escapeHtml((string)($menu['name'] ?? ''));
$menuStatus = (int)($menu['status'] ?? 0);
$menuPages = \admin\factory\Pages::menu_pages($menuId);
$html .= '<div class="menu_sortable">';
$html .= '<ol class="sortable" id="sortable_' . $menuId . '">';
$html .= '<li id="list_' . $menuId . '" class="menu_' . $menuId . '" menu="' . $menuId . '">';
$html .= '<div class="context_0 content content_menu"' . ($menuStatus ? '' : ' style="color: #cc0000;"') . '>';
$html .= '<span class="disclose"><span></span></span>Menu: <b>' . $menuName . '</b>';
$html .= '</div>';
$html .= \Tpl::view('articles/subpages-list', [
'pages' => $menuPages,
'article_pages' => $article['pages'] ?? [],
'parent_id' => $menuId,
'step' => 1,
]);
$html .= '</li></ol></div>';
}
$html .= '</div><div class="clear"></div></div>';
return $html;
}
private function renderImagesBox(array $article): string
{
$html = '<ul id="images-list">';
$images = is_array($article['images'] ?? null) ? $article['images'] : [];
foreach ($images as $img) {
$id = (int)($img['id'] ?? 0);
$src = $this->escapeHtml((string)($img['src'] ?? ''));
$alt = $this->escapeHtml((string)($img['alt'] ?? ''));
$html .= '<li id="image-' . $id . '">';
$html .= '<img class="article-image lozad" data-src="/libraries/thumb.php?img=' . $src . '&w=300&h=300">';
$html .= '<a href="#" class="input-group-addon btn btn-danger article_image_delete" image-id="' . $id . '"><i class="fa fa-trash"></i></a>';
$html .= '<input type="text" class="form-control image-alt" value="' . $alt . '" image-id="' . $id . '" placeholder="atrybut alt...">';
$html .= '</li>';
}
$html .= '</ul><div id="images-uploader">You browser doesn\'t have Flash installed.</div>';
return $html;
}
private function renderFilesBox(array $article): string
{
$html = '<ul id="files-list">';
$files = is_array($article['files'] ?? null) ? $article['files'] : [];
foreach ($files as $file) {
$id = (int)($file['id'] ?? 0);
$src = (string)($file['src'] ?? '');
$name = trim((string)($file['name'] ?? ''));
if ($name === '') {
$parts = explode('/', $src);
$name = (string)end($parts);
}
$name = $this->escapeHtml($name);
$html .= '<li id="file-' . $id . '"><div class="input-group">';
$html .= '<input type="text" class="article_file_edit form-control" file_id="' . $id . '" value="' . $name . '" />';
$html .= '<a href="#" class="input-group-addon btn btn-info article_file_delete" file_id="' . $id . '"><i class="fa fa-trash"></i></a>';
$html .= '</div></li>';
}
$html .= '</ul><div id="files-uploader">You browser doesn\'t have Flash installed.</div>';
return $html;
}
private function escapeHtml(string $value): string
{
return htmlspecialchars($value, ENT_QUOTES, 'UTF-8');
}
} }

View File

@@ -201,7 +201,7 @@ class FormFieldRenderer
'name' => $field->name, 'name' => $field->name,
'id' => $field->id, 'id' => $field->id,
'value' => $value ?? '', 'value' => $value ?? '',
'options' => $field->options, 'values' => $field->options,
'class' => ($field->required ? 'require ' : '') . ($field->attributes['class'] ?? ''), 'class' => ($field->required ? 'require ' : '') . ($field->attributes['class'] ?? ''),
]; ];
@@ -308,6 +308,11 @@ class FormFieldRenderer
'value="' . htmlspecialchars($value ?? '') . '">'; 'value="' . htmlspecialchars($value ?? '') . '">';
} }
public function renderCustom(FormField $field): string
{
return (string)($field->customHtml ?? '');
}
/** /**
* Renderuje sekcję językową * Renderuje sekcję językową
*/ */
@@ -389,6 +394,16 @@ class FormFieldRenderer
'checked' => (bool) $value, 'checked' => (bool) $value,
]); ]);
case FormFieldType::SELECT:
return $this->wrapWithError(\Html::select([
'label' => $field->label,
'name' => $name,
'id' => $id,
'value' => $value ?? '',
'values' => $field->options,
'class' => ($field->required ? 'require ' : '') . ($field->attributes['class'] ?? ''),
]), $error);
default: // TEXT, URL, etc. default: // TEXT, URL, etc.
return $this->wrapWithError(\Html::input([ return $this->wrapWithError(\Html::input([
'label' => $field->label, 'label' => $field->label,

View File

@@ -29,6 +29,7 @@ class FormField
// Specyficzne dla lang_section // Specyficzne dla lang_section
public ?array $langFields; public ?array $langFields;
public ?string $langSectionParentTab; public ?string $langSectionParentTab;
public ?string $customHtml;
/** /**
* @param string $name Nazwa pola (name) * @param string $name Nazwa pola (name)
@@ -64,7 +65,8 @@ class FormField
string $editorToolbar = 'MyTool', string $editorToolbar = 'MyTool',
int $editorHeight = 300, int $editorHeight = 300,
?array $langFields = null, ?array $langFields = null,
?string $langSectionParentTab = null ?string $langSectionParentTab = null,
?string $customHtml = null
) { ) {
$this->name = $name; $this->name = $name;
$this->type = $type; $this->type = $type;
@@ -82,6 +84,7 @@ class FormField
$this->editorHeight = $editorHeight; $this->editorHeight = $editorHeight;
$this->langFields = $langFields; $this->langFields = $langFields;
$this->langSectionParentTab = $langSectionParentTab; $this->langSectionParentTab = $langSectionParentTab;
$this->customHtml = $customHtml;
$this->id = $attributes['id'] ?? $name; $this->id = $attributes['id'] ?? $name;
} }
@@ -276,6 +279,29 @@ class FormField
); );
} }
public static function custom(string $name, string $html, array $config = []): self
{
return new self(
$name,
FormFieldType::CUSTOM,
$config['label'] ?? '',
null,
$config['tab'] ?? 'default',
false,
$config['attributes'] ?? [],
[],
null,
null,
false,
null,
'MyTool',
300,
null,
null,
$html
);
}
/** /**
* Sekcja językowa - grupa pól powtarzana dla każdego języka * Sekcja językowa - grupa pól powtarzana dla każdego języka
* *

View File

@@ -20,4 +20,5 @@ class FormFieldType
public const FILE = 'file'; public const FILE = 'file';
public const HIDDEN = 'hidden'; public const HIDDEN = 'hidden';
public const LANG_SECTION = 'lang_section'; public const LANG_SECTION = 'lang_section';
public const CUSTOM = 'custom';
} }

View File

@@ -330,11 +330,16 @@ class Site
*/ */
private static $actionMap = [ private static $actionMap = [
'gallery_order_save' => 'galleryOrderSave', 'gallery_order_save' => 'galleryOrderSave',
'files_order_save' => 'filesOrderSave',
'view_list' => 'list', 'view_list' => 'list',
'article_edit' => 'edit', 'article_edit' => 'edit',
'article_save' => 'save', 'article_save' => 'save',
'article_delete' => 'delete', 'article_delete' => 'delete',
'article_restore' => 'restore', 'article_restore' => 'restore',
'article_image_alt_change' => 'imageAltChange',
'article_file_name_change' => 'fileNameChange',
'article_image_delete' => 'imageDelete',
'article_file_delete' => 'fileDelete',
'banner_edit' => 'edit', 'banner_edit' => 'edit',
'banner_save' => 'save', 'banner_save' => 'save',
'banner_delete' => 'delete', 'banner_delete' => 'delete',

View File

@@ -1,22 +0,0 @@
<?php
namespace admin\view;
class Articles
{
public static function subpages_list( $pages, $article_pages, $parent_id = 0, $step = 1 )
{
$tpl = new \Tpl();
$tpl -> pages = $pages;
$tpl -> parent_id = $parent_id;
$tpl -> step = $step;
$tpl -> article_pages = $article_pages;
return $tpl -> render( 'articles/subpages-list' );
}
public static function articles_list()
{
$tpl = new \Tpl;
return $tpl -> render( 'articles/articles-list' );
}
}
?>

View File

@@ -3,7 +3,8 @@ class ImageManipulator
{ {
protected int $width; protected int $width;
protected int $height; protected int $height;
protected \GdImage $image; /** @var resource|\GdImage */
protected $image;
protected ?string $file = null; protected ?string $file = null;
/** /**
@@ -37,7 +38,7 @@ class ImageManipulator
throw new InvalidArgumentException("Image file $file is not readable"); throw new InvalidArgumentException("Image file $file is not readable");
} }
if (isset($this->image) && $this->image instanceof \GdImage) { if (isset($this->image) && $this->isValidImageResource($this->image)) {
imagedestroy($this->image); imagedestroy($this->image);
} }
@@ -66,7 +67,7 @@ class ImageManipulator
throw new InvalidArgumentException("Image type $type not supported"); throw new InvalidArgumentException("Image type $type not supported");
} }
if (!$this->image instanceof \GdImage) { if (!$this->isValidImageResource($this->image)) {
throw new InvalidArgumentException("Failed to create image from $file"); throw new InvalidArgumentException("Failed to create image from $file");
} }
@@ -91,12 +92,12 @@ class ImageManipulator
*/ */
public function setImageString(string $data): self public function setImageString(string $data): self
{ {
if (isset($this->image) && $this->image instanceof \GdImage) { if (isset($this->image) && $this->isValidImageResource($this->image)) {
imagedestroy($this->image); imagedestroy($this->image);
} }
$image = imagecreatefromstring($data); $image = imagecreatefromstring($data);
if (!$image instanceof \GdImage) { if (!$this->isValidImageResource($image)) {
throw new RuntimeException('Cannot create image from data string'); throw new RuntimeException('Cannot create image from data string');
} }
@@ -124,7 +125,7 @@ class ImageManipulator
*/ */
public function resample(int $width, int $height, bool $constrainProportions = true): self public function resample(int $width, int $height, bool $constrainProportions = true): self
{ {
if (!isset($this->image) || !$this->image instanceof \GdImage) { if (!isset($this->image) || !$this->isValidImageResource($this->image)) {
throw new RuntimeException('No image set'); throw new RuntimeException('No image set');
} }
@@ -169,7 +170,7 @@ class ImageManipulator
*/ */
public function enlargeCanvas(int $width, int $height, array $rgb = [], ?int $xpos = null, ?int $ypos = null): self public function enlargeCanvas(int $width, int $height, array $rgb = [], ?int $xpos = null, ?int $ypos = null): self
{ {
if (!isset($this->image) || !$this->image instanceof \GdImage) { if (!isset($this->image) || !$this->isValidImageResource($this->image)) {
throw new RuntimeException('No image set'); throw new RuntimeException('No image set');
} }
@@ -177,7 +178,7 @@ class ImageManipulator
$height = max($height, $this->height); $height = max($height, $this->height);
$temp = imagecreatetruecolor($width, $height); $temp = imagecreatetruecolor($width, $height);
if (!$temp instanceof \GdImage) { if (!$this->isValidImageResource($temp)) {
throw new RuntimeException('Failed to create a new image for enlarging canvas'); throw new RuntimeException('Failed to create a new image for enlarging canvas');
} }
@@ -233,7 +234,7 @@ class ImageManipulator
*/ */
public function crop($x1, int $y1 = 0, int $x2 = 0, int $y2 = 0): self public function crop($x1, int $y1 = 0, int $x2 = 0, int $y2 = 0): self
{ {
if (!isset($this->image) || !$this->image instanceof \GdImage) { if (!isset($this->image) || !$this->isValidImageResource($this->image)) {
throw new RuntimeException('No image set'); throw new RuntimeException('No image set');
} }
@@ -257,7 +258,7 @@ class ImageManipulator
} }
$temp = imagecreatetruecolor($cropWidth, $cropHeight); $temp = imagecreatetruecolor($cropWidth, $cropHeight);
if (!$temp instanceof \GdImage) { if (!$this->isValidImageResource($temp)) {
throw new RuntimeException('Failed to create a new image for cropping'); throw new RuntimeException('Failed to create a new image for cropping');
} }
@@ -286,17 +287,17 @@ class ImageManipulator
/** /**
* Replace current image resource with a new one * Replace current image resource with a new one
* *
* @param \GdImage $res New image resource * @param resource|\GdImage $res New image resource
* @return self * @return self
* @throws UnexpectedValueException * @throws UnexpectedValueException
*/ */
protected function _replace(\GdImage $res): self protected function _replace($res): self
{ {
if (!$res instanceof \GdImage) { if (!$this->isValidImageResource($res)) {
throw new UnexpectedValueException('Invalid image resource'); throw new UnexpectedValueException('Invalid image resource');
} }
if (isset($this->image) && $this->image instanceof \GdImage) { if (isset($this->image) && $this->isValidImageResource($this->image)) {
imagedestroy($this->image); imagedestroy($this->image);
} }
@@ -386,9 +387,9 @@ class ImageManipulator
/** /**
* Returns the GD image resource * Returns the GD image resource
* *
* @return \GdImage * @return resource|\GdImage
*/ */
public function getResource(): \GdImage public function getResource()
{ {
return $this->image; return $this->image;
} }
@@ -418,8 +419,22 @@ class ImageManipulator
*/ */
public function __destruct() public function __destruct()
{ {
if (isset($this->image) && $this->image instanceof \GdImage) { if (isset($this->image) && $this->isValidImageResource($this->image)) {
imagedestroy($this->image); imagedestroy($this->image);
} }
} }
/**
* Compatibility helper for PHP 7.4 (resource) and PHP 8+ (GdImage).
*
* @param mixed $image
*/
private function isValidImageResource($image): bool
{
if (is_resource($image)) {
return true;
}
return class_exists('GdImage', false) && $image instanceof \GdImage;
}
} }

View File

@@ -12,10 +12,21 @@ session_start();
/* połączenie z bazą ustawić wg własnych preferencji */ /* połączenie z bazą ustawić wg własnych preferencji */
require_once dirname( __FILE__ ) . '/../../config.php'; require_once dirname( __FILE__ ) . '/../../config.php';
require_once dirname( __FILE__ ) . '/../../autoload/class.S.php'; require_once dirname( __FILE__ ) . '/../../autoload/class.S.php';
require_once dirname( __FILE__ ) . '/../../autoload/admin/factory/class.Articles.php';
require_once dirname( __FILE__ ) . '/../../autoload/admin/factory/class.Pages.php'; $legacyFactoryFiles = [
require_once dirname( __FILE__ ) . '/../../autoload/admin/factory/class.ShopCategory.php'; '/../../autoload/admin/factory/class.Articles.php',
require_once dirname( __FILE__ ) . '/../../autoload/admin/factory/class.ShopProduct.php'; '/../../autoload/admin/factory/class.Pages.php',
'/../../autoload/admin/factory/class.ShopCategory.php',
'/../../autoload/admin/factory/class.ShopProduct.php',
];
foreach ( $legacyFactoryFiles as $legacyFactoryFile )
{
$legacyFactoryPath = dirname( __FILE__ ) . $legacyFactoryFile;
if ( file_exists( $legacyFactoryPath ) )
require_once $legacyFactoryPath;
}
require_once dirname( __FILE__ ) . '/../../autoload/shop/class.Product.php'; require_once dirname( __FILE__ ) . '/../../autoload/shop/class.Product.php';
require_once dirname( __FILE__ ) . '/../../libraries/medoo/medoo.php'; require_once dirname( __FILE__ ) . '/../../libraries/medoo/medoo.php';

View File

@@ -1,154 +1,53 @@
<?php <?php
require_once '../../config.php'; require_once '../../config.php';
require_once '../medoo/medoo.php'; require_once '../medoo/medoo.php';
date_default_timezone_set( 'Europe/Warsaw' ); require_once 'upload-common.php';
session_start();
$upload_token = $_REQUEST['upload_token'] ?? ''; plupload_bootstrap();
if ( !isset( $_SESSION['upload_tokens'][$upload_token] ) ) { plupload_require_post();
http_response_code(403); $userId = plupload_require_admin_user();
echo json_encode( ['error' => 'Brak tokenu uploadu'] ); plupload_validate_token($userId);
exit;
$fileDir = '/upload/article_files/tmp';
$targetDir = '../..' . $fileDir;
plupload_ensure_target_dir($targetDir);
list($chunk, $chunks) = plupload_get_chunks();
list($fileName, $extension, $filePath, $partPath) = plupload_build_target_paths(
$targetDir,
$_REQUEST['name'] ?? '',
null,
[
'php', 'php3', 'php4', 'php5', 'php7', 'php8', 'phtml', 'phar',
'cgi', 'pl', 'py', 'rb',
'asp', 'aspx', 'jsp',
'js', 'mjs', 'vbs', 'wsf', 'hta',
'sh', 'bash', 'zsh', 'ps1', 'bat', 'cmd', 'com',
'exe', 'msi', 'scr', 'dll', 'jar',
]
);
plupload_cleanup_stale_parts($targetDir, $partPath, 5 * 3600);
plupload_write_chunk_to_part($partPath, $chunk);
plupload_assert_size_limit($partPath, 50 * 1024 * 1024, 'Plik przekracza dozwolony rozmiar (50 MB).');
$fileId = null;
$responseFileName = $fileName;
if (plupload_is_last_chunk($chunk, $chunks)) {
plupload_finalize_part($partPath, $filePath);
$mdb = plupload_create_medoo($database);
$mdb->insert('pp_articles_files', [
'article_id' => null,
'src' => substr($filePath, 5),
]);
$fileId = (int)$mdb->id();
$responseFileName = basename($filePath);
} }
$tokenData = $_SESSION['upload_tokens'][$upload_token]; plupload_send_success([
if ( $tokenData['expires'] < time() ) { 'file_name' => $responseFileName,
unset( $_SESSION['upload_tokens'][$upload_token] ); 'file_id' => $fileId,
http_response_code(403); ]);
echo json_encode( ['error' => 'Token wygasł'] );
exit;
}
$mdb = new medoo( [
'database_type' => 'mysql',
'database_name' => $database['name'],
'server' => $database['host'],
'username' => $database['user'],
'password' => $database['password'],
'charset' => 'utf8'
] );
header( "Expires: Mon, 26 Jul 1997 05:00:00 GMT" );
header( "Last-Modified: " . gmdate( "D, d M Y H:i:s" ) . " GMT" );
header( "Cache-Control: no-store, no-cache, must-revalidate" );
header( "Cache-Control: post-check=0, pre-check=0", false );
header( "Pragma: no-cache" );
$fileDir = '/upload/article_files/tmp';
$targetDir = '../..' . $fileDir;
if ( !is_dir( $targetDir ) )
mkdir( $targetDir, 0755, true );
$cleanupTargetDir = true;
$maxFileAge = 5 * 3600;
$chunk = isset( $_REQUEST["chunk"] ) ? intval( $_REQUEST["chunk"] ) : 0;
$chunks = isset( $_REQUEST["chunks"] ) ? intval( $_REQUEST["chunks"] ) : 0;
$fileName = isset( $_REQUEST["name"] ) ? $_REQUEST["name"] : '';
$fileName = preg_replace( '/[^\w\._]+/', '-', $fileName );
if ( file_exists( $targetDir . DIRECTORY_SEPARATOR . $fileName ) )
{
$ext = strrpos( $fileName, '.' );
$fileName_a = substr( $fileName, 0, $ext );
$fileName_b = substr( $fileName, $ext );
$count = 1;
while ( file_exists( $targetDir . DIRECTORY_SEPARATOR . $fileName_a . '_' . $count . $fileName_b ) )
$count++;
$fileName = $fileName_a . '_' . $count . $fileName_b;
}
$filePath = $targetDir . DIRECTORY_SEPARATOR . $fileName;
if ( $cleanupTargetDir && is_dir( $targetDir ) && ( $dir = opendir( $targetDir ) ) )
{
while ( ( $file = readdir( $dir ) ) !== false )
{
$tmpfilePath = $targetDir . DIRECTORY_SEPARATOR . $file;
if ( preg_match( '/\.part$/', $file ) && ( filemtime( $tmpfilePath ) < time() - $maxFileAge ) && ( $tmpfilePath != "{$filePath}.part" ) ) {
@unlink( $tmpfilePath );
}
}
closedir($dir);
}
else
die( '{"jsonrpc" : "2.0", "error" : {"code": 100, "message": "Failed to open temp directory."}, "id" : "id"}' );
if ( isset( $_SERVER["HTTP_CONTENT_TYPE"] ) )
$contentType = $_SERVER["HTTP_CONTENT_TYPE"];
if ( isset( $_SERVER["CONTENT_TYPE"] ) )
$contentType = $_SERVER["CONTENT_TYPE"];
if ( strpos( $contentType, "multipart" ) !== false )
{
if ( isset( $_FILES['file']['tmp_name'] ) && is_uploaded_file( $_FILES['file']['tmp_name'] ) )
{
$out = fopen( "{$filePath}.part", $chunk == 0 ? "wb" : "ab" );
if ( $out )
{
$in = fopen( $_FILES['file']['tmp_name'], "rb" );
if ( $in )
{
while ( $buff = fread( $in, 4096 ) )
fwrite($out, $buff);
}
else
die( '{"jsonrpc" : "2.0", "error" : {"code": 101, "message": "Failed to open input stream."}, "id" : "id"}' );
fclose( $in );
fclose( $out );
@unlink( $_FILES['file']['tmp_name'] );
}
else
die( '{"jsonrpc" : "2.0", "error" : {"code": 102, "message": "Failed to open output stream."}, "id" : "id"}' );
}
else
die( '{"jsonrpc" : "2.0", "error" : {"code": 103, "message": "Failed to move uploaded file."}, "id" : "id"}' );
}
else
{
$out = fopen( "{$filePath}.part", $chunk == 0 ? "wb" : "ab" );
if ( $out )
{
$in = fopen( "php://input", "rb" );
if ( $in )
{
while ( $buff = fread( $in, 4096 ) )
fwrite( $out, $buff );
}
else
die( '{"jsonrpc" : "2.0", "error" : {"code": 101, "message": "Failed to open input stream."}, "id" : "id"}' );
fclose( $in );
fclose( $out );
}
else
die( '{"jsonrpc" : "2.0", "error" : {"code": 102, "message": "Failed to open output stream."}, "id" : "id"}' );
}
if ( !$chunks || $chunk == $chunks - 1 )
{
rename( "{$filePath}.part", $filePath );
$mdb -> insert( 'pp_articles_files', [
'article_id' => null,
'src' => substr( $filePath, 5, strlen( $filePath ) )
] );
$file_id = $mdb -> id();
$file_name = explode( '/', $filePath );
$file_name = $file_name[ count( $file_name ) - 1 ];
}
die( '{"jsonrpc" : "2.0", "result" : null, "id" : "id", "file_name" : "' . $file_name . '", "file_id" : "' . $file_id . '"}' );
?>

View File

@@ -1,153 +1,61 @@
<?php <?php
require_once '../../config.php'; require_once '../../config.php';
require_once '../medoo/medoo.php'; require_once '../medoo/medoo.php';
date_default_timezone_set( 'Europe/Warsaw' ); require_once 'upload-common.php';
session_start();
$upload_token = $_REQUEST['upload_token'] ?? ''; plupload_bootstrap();
if ( !isset( $_SESSION['upload_tokens'][$upload_token] ) ) { plupload_require_post();
http_response_code(403); $userId = plupload_require_admin_user();
echo json_encode( ['error' => 'Brak tokenu uploadu'] ); plupload_validate_token($userId);
exit;
$fileDir = '/upload/article_images/tmp';
$targetDir = '../..' . $fileDir;
plupload_ensure_target_dir($targetDir);
list($chunk, $chunks) = plupload_get_chunks();
list($fileName, $extension, $filePath, $partPath) = plupload_build_target_paths(
$targetDir,
$_REQUEST['name'] ?? '',
['jpg', 'jpeg', 'png', 'gif', 'webp'],
null
);
plupload_cleanup_stale_parts($targetDir, $partPath, 5 * 3600);
plupload_write_chunk_to_part($partPath, $chunk);
plupload_assert_size_limit($partPath, 20 * 1024 * 1024, 'Plik przekracza dozwolony rozmiar (20 MB).');
$imageId = null;
if (plupload_is_last_chunk($chunk, $chunks)) {
plupload_finalize_part($partPath, $filePath);
$mime = mime_content_type($filePath) ?: '';
$imageMeta = @getimagesize($filePath);
$allowedMimeTypes = ['image/jpeg', 'image/png', 'image/gif', 'image/webp'];
$isValidImage = in_array($mime, $allowedMimeTypes, true)
&& is_array($imageMeta)
&& (int)($imageMeta[0] ?? 0) > 0
&& (int)($imageMeta[1] ?? 0) > 0;
if (!$isValidImage) {
@unlink($filePath);
plupload_send_error(400, 601, 'Plik nie jest prawidlowym obrazem.');
}
$mdb = plupload_create_medoo($database);
$order = (int)$mdb->max('pp_articles_images', 'o');
$articleId = (int)($_POST['article_id'] ?? 0);
$mdb->insert('pp_articles_images', [
'article_id' => $articleId > 0 ? $articleId : null,
'src' => substr($filePath, 5),
'o' => $order + 1,
]);
$imageId = (int)$mdb->id();
} }
$tokenData = $_SESSION['upload_tokens'][$upload_token]; plupload_send_success([
if ( $tokenData['expires'] < time() ) { 'data_link' => str_replace('../../', '', $filePath),
unset( $_SESSION['upload_tokens'][$upload_token] ); 'image_id' => $imageId,
http_response_code(403); ]);
echo json_encode( ['error' => 'Token wygasł'] );
exit;
}
$mdb = new medoo( [
'database_type' => 'mysql',
'database_name' => $database['name'],
'server' => $database['host'],
'username' => $database['user'],
'password' => $database['password'],
'charset' => 'utf8'
] );
header( "Expires: Mon, 26 Jul 1997 05:00:00 GMT" );
header( "Last-Modified: " . gmdate( "D, d M Y H:i:s" ) . " GMT" );
header( "Cache-Control: no-store, no-cache, must-revalidate" );
header( "Cache-Control: post-check=0, pre-check=0", false );
header( "Pragma: no-cache" );
$fileDir = '/upload/article_images/tmp';
$targetDir = '../..' . $fileDir;
if ( !is_dir( $targetDir ) )
mkdir( $targetDir, 0755, true );
$cleanupTargetDir = true;
$maxFileAge = 5 * 3600;
$chunk = isset( $_REQUEST["chunk"] ) ? intval( $_REQUEST["chunk"] ) : 0;
$chunks = isset( $_REQUEST["chunks"] ) ? intval( $_REQUEST["chunks"] ) : 0;
$fileName = isset( $_REQUEST["name"] ) ? $_REQUEST["name"] : '';
$fileName = preg_replace( '/[^\w\._]+/', '-', $fileName );
if ( file_exists( $targetDir . DIRECTORY_SEPARATOR . $fileName ) )
{
$ext = strrpos( $fileName, '.' );
$fileName_a = substr( $fileName, 0, $ext );
$fileName_b = substr( $fileName, $ext );
$count = 1;
while ( file_exists( $targetDir . DIRECTORY_SEPARATOR . $fileName_a . '_' . $count . $fileName_b ) )
$count++;
$fileName = $fileName_a . '_' . $count . $fileName_b;
}
$filePath = $targetDir . DIRECTORY_SEPARATOR . $fileName;
if ( $cleanupTargetDir && is_dir( $targetDir ) && ( $dir = opendir( $targetDir ) ) )
{
while ( ( $file = readdir( $dir ) ) !== false )
{
$tmpfilePath = $targetDir . DIRECTORY_SEPARATOR . $file;
if ( preg_match( '/\.part$/', $file ) && ( filemtime( $tmpfilePath ) < time() - $maxFileAge ) && ( $tmpfilePath != "{$filePath}.part" ) ) {
@unlink( $tmpfilePath );
}
}
closedir($dir);
}
else
die( '{"jsonrpc" : "2.0", "error" : {"code": 100, "message": "Failed to open temp directory."}, "id" : "id"}' );
if ( isset( $_SERVER["HTTP_CONTENT_TYPE"] ) )
$contentType = $_SERVER["HTTP_CONTENT_TYPE"];
if ( isset( $_SERVER["CONTENT_TYPE"] ) )
$contentType = $_SERVER["CONTENT_TYPE"];
if ( strpos( $contentType, "multipart" ) !== false )
{
if ( isset( $_FILES['file']['tmp_name'] ) && is_uploaded_file( $_FILES['file']['tmp_name'] ) )
{
$out = fopen( "{$filePath}.part", $chunk == 0 ? "wb" : "ab" );
if ( $out )
{
$in = fopen( $_FILES['file']['tmp_name'], "rb" );
if ( $in )
{
while ( $buff = fread( $in, 4096 ) )
fwrite($out, $buff);
}
else
die( '{"jsonrpc" : "2.0", "error" : {"code": 101, "message": "Failed to open input stream."}, "id" : "id"}' );
fclose( $in );
fclose( $out );
@unlink( $_FILES['file']['tmp_name'] );
}
else
die( '{"jsonrpc" : "2.0", "error" : {"code": 102, "message": "Failed to open output stream."}, "id" : "id"}' );
}
else
die( '{"jsonrpc" : "2.0", "error" : {"code": 103, "message": "Failed to move uploaded file."}, "id" : "id"}' );
}
else
{
$out = fopen( "{$filePath}.part", $chunk == 0 ? "wb" : "ab" );
if ( $out )
{
$in = fopen( "php://input", "rb" );
if ( $in )
{
while ( $buff = fread( $in, 4096 ) )
fwrite( $out, $buff );
}
else
die( '{"jsonrpc" : "2.0", "error" : {"code": 101, "message": "Failed to open input stream."}, "id" : "id"}' );
fclose( $in );
fclose( $out );
}
else
die( '{"jsonrpc" : "2.0", "error" : {"code": 102, "message": "Failed to open output stream."}, "id" : "id"}' );
}
if ( !$chunks || $chunk == $chunks - 1 )
{
rename( "{$filePath}.part", $filePath );
$o = $mdb -> max( 'pp_articles_images', 'o' );
$mdb -> insert( 'pp_articles_images', [
'article_id' => $_POST['article_id'] ? $_POST['article_id'] : null,
'src' => substr( $filePath, 5, strlen( $filePath ) ),
'o' => ++$o
] );
$image_id = $mdb -> id();
}
die( '{"jsonrpc" : "2.0", "result" : null, "id" : "id", "data_link" : "' . str_replace( '../../', '', $filePath ) . '", "image_id" : "' . $image_id . '"}' );
?>

View File

@@ -0,0 +1,307 @@
<?php
if (!function_exists('plupload_send_error')) {
function plupload_send_error($httpCode, $code, $message)
{
http_response_code((int)$httpCode);
echo json_encode([
'jsonrpc' => '2.0',
'error' => [
'code' => (int)$code,
'message' => (string)$message,
],
'id' => 'id',
]);
exit;
}
}
if (!function_exists('plupload_bootstrap')) {
function plupload_bootstrap()
{
date_default_timezone_set('Europe/Warsaw');
if (session_status() !== PHP_SESSION_ACTIVE) {
session_start();
}
header('Content-Type: application/json; charset=utf-8');
header('Expires: Mon, 26 Jul 1997 05:00:00 GMT');
header('Last-Modified: ' . gmdate('D, d M Y H:i:s') . ' GMT');
header('Cache-Control: no-store, no-cache, must-revalidate');
header('Cache-Control: post-check=0, pre-check=0', false);
header('Pragma: no-cache');
}
}
if (!function_exists('plupload_require_post')) {
function plupload_require_post()
{
if (($_SERVER['REQUEST_METHOD'] ?? '') !== 'POST') {
plupload_send_error(405, 405, 'Method not allowed.');
}
}
}
if (!function_exists('plupload_require_admin_user')) {
function plupload_require_admin_user()
{
$adminSession = isset($_SESSION['admin']) && $_SESSION['admin'] === true;
$userId = (int)($_SESSION['user']['id'] ?? 0);
if (!$adminSession || $userId <= 0) {
plupload_send_error(403, 403, 'Brak autoryzacji.');
}
$sessionOk = isset($_SESSION['check'], $_SESSION['ip'])
&& $_SESSION['check'] === true
&& $_SESSION['ip'] === ($_SERVER['REMOTE_ADDR'] ?? '');
if (!$sessionOk) {
plupload_send_error(403, 403, 'Nieprawidlowa sesja.');
}
return $userId;
}
}
if (!function_exists('plupload_validate_token')) {
function plupload_validate_token($userId)
{
$uploadToken = (string)($_REQUEST['upload_token'] ?? '');
if ($uploadToken === '' || !isset($_SESSION['upload_tokens'][$uploadToken])) {
plupload_send_error(403, 403, 'Brak tokenu uploadu.');
}
$tokenData = $_SESSION['upload_tokens'][$uploadToken];
$tokenUserId = (int)($tokenData['user_id'] ?? 0);
$tokenExpires = (int)($tokenData['expires'] ?? 0);
if ($tokenUserId <= 0 || $tokenUserId !== (int)$userId) {
plupload_send_error(403, 403, 'Token nie nalezy do aktualnego uzytkownika.');
}
if ($tokenExpires < time()) {
unset($_SESSION['upload_tokens'][$uploadToken]);
plupload_send_error(403, 403, 'Token wygasl.');
}
return [$uploadToken, $tokenData];
}
}
if (!function_exists('plupload_normalize_filename')) {
function plupload_normalize_filename($fileName)
{
$fileName = basename((string)$fileName);
$fileName = preg_replace('/[^\w\.-]+/', '-', $fileName);
$fileName = trim((string)$fileName, '.-');
if ($fileName === '') {
$fileName = 'file-' . bin2hex(random_bytes(8));
}
return strtolower($fileName);
}
}
if (!function_exists('plupload_ensure_target_dir')) {
function plupload_ensure_target_dir($targetDir)
{
if (!is_dir($targetDir) && !mkdir($targetDir, 0755, true)) {
plupload_send_error(500, 100, 'Failed to open temp directory.');
}
}
}
if (!function_exists('plupload_build_target_paths')) {
function plupload_build_target_paths($targetDir, $requestName, $allowedExtensions = null, $blockedExtensions = null, $maxNameLength = 180)
{
$fileName = plupload_normalize_filename((string)$requestName);
$extension = strtolower((string)pathinfo($fileName, PATHINFO_EXTENSION));
if (is_array($allowedExtensions)) {
if ($extension === '' || !in_array($extension, $allowedExtensions, true)) {
plupload_send_error(400, 601, 'Nieobslugiwane rozszerzenie pliku.');
}
}
if (is_array($blockedExtensions)) {
if ($extension !== '' && in_array($extension, $blockedExtensions, true)) {
plupload_send_error(400, 601, 'Rozszerzenie pliku jest zablokowane.');
}
}
if (strlen($fileName) > (int)$maxNameLength) {
$base = substr((string)pathinfo($fileName, PATHINFO_FILENAME), 0, 140);
$suffix = '-' . bin2hex(random_bytes(4));
$fileName = $base . $suffix . ($extension !== '' ? '.' . $extension : '');
}
if (file_exists($targetDir . DIRECTORY_SEPARATOR . $fileName)) {
$nameWithoutExt = (string)pathinfo($fileName, PATHINFO_FILENAME);
$extWithDot = $extension !== '' ? '.' . $extension : '';
$count = 1;
while (file_exists($targetDir . DIRECTORY_SEPARATOR . $nameWithoutExt . '_' . $count . $extWithDot)) {
$count++;
}
$fileName = $nameWithoutExt . '_' . $count . $extWithDot;
}
$filePath = $targetDir . DIRECTORY_SEPARATOR . $fileName;
$partPath = $filePath . '.part';
return [$fileName, $extension, $filePath, $partPath];
}
}
if (!function_exists('plupload_get_chunks')) {
function plupload_get_chunks()
{
$chunk = max(0, (int)($_REQUEST['chunk'] ?? 0));
$chunks = max(0, (int)($_REQUEST['chunks'] ?? 0));
return [$chunk, $chunks];
}
}
if (!function_exists('plupload_cleanup_stale_parts')) {
function plupload_cleanup_stale_parts($targetDir, $currentPartPath, $maxFileAge = 18000)
{
$dir = @opendir($targetDir);
if (!$dir) {
return;
}
while (($file = readdir($dir)) !== false) {
$tmpFilePath = $targetDir . DIRECTORY_SEPARATOR . $file;
if (!preg_match('/\.part$/', $file)) {
continue;
}
if ($tmpFilePath === $currentPartPath) {
continue;
}
if (@filemtime($tmpFilePath) < (time() - (int)$maxFileAge)) {
@unlink($tmpFilePath);
}
}
closedir($dir);
}
}
if (!function_exists('plupload_write_chunk_to_part')) {
function plupload_write_chunk_to_part($partPath, $chunk)
{
$contentType = (string)($_SERVER['HTTP_CONTENT_TYPE'] ?? $_SERVER['CONTENT_TYPE'] ?? '');
$isMultipart = strpos($contentType, 'multipart') !== false;
if ($isMultipart) {
$fileInfo = $_FILES['file'] ?? null;
if (!is_array($fileInfo) || !isset($fileInfo['tmp_name']) || !is_uploaded_file($fileInfo['tmp_name'])) {
plupload_send_error(400, 103, 'Failed to move uploaded file.');
}
if ((int)($fileInfo['error'] ?? UPLOAD_ERR_OK) !== UPLOAD_ERR_OK) {
plupload_send_error(400, 104, 'Upload error.');
}
$in = fopen($fileInfo['tmp_name'], 'rb');
$out = fopen($partPath, ((int)$chunk === 0) ? 'wb' : 'ab');
if (!$in) {
plupload_send_error(500, 101, 'Failed to open input stream.');
}
if (!$out) {
fclose($in);
plupload_send_error(500, 102, 'Failed to open output stream.');
}
while ($buff = fread($in, 4096)) {
fwrite($out, $buff);
}
fclose($in);
fclose($out);
@unlink($fileInfo['tmp_name']);
return;
}
$in = fopen('php://input', 'rb');
$out = fopen($partPath, ((int)$chunk === 0) ? 'wb' : 'ab');
if (!$in) {
plupload_send_error(500, 101, 'Failed to open input stream.');
}
if (!$out) {
fclose($in);
plupload_send_error(500, 102, 'Failed to open output stream.');
}
while ($buff = fread($in, 4096)) {
fwrite($out, $buff);
}
fclose($in);
fclose($out);
}
}
if (!function_exists('plupload_assert_size_limit')) {
function plupload_assert_size_limit($partPath, $maxBytes, $message)
{
if (@filesize($partPath) > (int)$maxBytes) {
@unlink($partPath);
plupload_send_error(413, 413, (string)$message);
}
}
}
if (!function_exists('plupload_is_last_chunk')) {
function plupload_is_last_chunk($chunk, $chunks)
{
return ((int)$chunks === 0) || ((int)$chunk === ((int)$chunks - 1));
}
}
if (!function_exists('plupload_finalize_part')) {
function plupload_finalize_part($partPath, $filePath)
{
if (!@rename($partPath, $filePath)) {
@unlink($partPath);
plupload_send_error(500, 105, 'Failed to finalize uploaded file.');
}
}
}
if (!function_exists('plupload_create_medoo')) {
function plupload_create_medoo($database)
{
return new medoo([
'database_type' => 'mysql',
'database_name' => $database['name'],
'server' => $database['host'],
'username' => $database['user'],
'password' => $database['password'],
'charset' => 'utf8',
]);
}
}
if (!function_exists('plupload_send_success')) {
function plupload_send_success(array $payload)
{
echo json_encode(array_merge([
'jsonrpc' => '2.0',
'result' => null,
'id' => 'id',
], $payload));
exit;
}
}

View File

@@ -1,154 +1,53 @@
<?php <?php
require_once '../../config.php'; require_once '../../config.php';
require_once '../medoo/medoo.php'; require_once '../medoo/medoo.php';
date_default_timezone_set( 'Europe/Warsaw' ); require_once 'upload-common.php';
session_start();
$upload_token = $_REQUEST['upload_token'] ?? ''; plupload_bootstrap();
if ( !isset( $_SESSION['upload_tokens'][$upload_token] ) ) { plupload_require_post();
http_response_code(403); $userId = plupload_require_admin_user();
echo json_encode( ['error' => 'Brak tokenu uploadu'] ); plupload_validate_token($userId);
exit;
$fileDir = '/upload/product_files/tmp';
$targetDir = '../..' . $fileDir;
plupload_ensure_target_dir($targetDir);
list($chunk, $chunks) = plupload_get_chunks();
list($fileName, $extension, $filePath, $partPath) = plupload_build_target_paths(
$targetDir,
$_REQUEST['name'] ?? '',
null,
[
'php', 'php3', 'php4', 'php5', 'php7', 'php8', 'phtml', 'phar',
'cgi', 'pl', 'py', 'rb',
'asp', 'aspx', 'jsp',
'js', 'mjs', 'vbs', 'wsf', 'hta',
'sh', 'bash', 'zsh', 'ps1', 'bat', 'cmd', 'com',
'exe', 'msi', 'scr', 'dll', 'jar',
]
);
plupload_cleanup_stale_parts($targetDir, $partPath, 5 * 3600);
plupload_write_chunk_to_part($partPath, $chunk);
plupload_assert_size_limit($partPath, 50 * 1024 * 1024, 'Plik przekracza dozwolony rozmiar (50 MB).');
$fileId = null;
$responseFileName = $fileName;
if (plupload_is_last_chunk($chunk, $chunks)) {
plupload_finalize_part($partPath, $filePath);
$mdb = plupload_create_medoo($database);
$mdb->insert('pp_shop_products_files', [
'product_id' => null,
'src' => substr($filePath, 5),
]);
$fileId = (int)$mdb->id();
$responseFileName = basename($filePath);
} }
$tokenData = $_SESSION['upload_tokens'][$upload_token]; plupload_send_success([
if ( $tokenData['expires'] < time() ) { 'file_name' => $responseFileName,
unset( $_SESSION['upload_tokens'][$upload_token] ); 'file_id' => $fileId,
http_response_code(403); ]);
echo json_encode( ['error' => 'Token wygasł'] );
exit;
}
$mdb = new medoo( [
'database_type' => 'mysql',
'database_name' => $database['name'],
'server' => $database['host'],
'username' => $database['user'],
'password' => $database['password'],
'charset' => 'utf8'
] );
header( "Expires: Mon, 26 Jul 1997 05:00:00 GMT" );
header( "Last-Modified: " . gmdate( "D, d M Y H:i:s" ) . " GMT" );
header( "Cache-Control: no-store, no-cache, must-revalidate" );
header( "Cache-Control: post-check=0, pre-check=0", false );
header( "Pragma: no-cache" );
$fileDir = '/upload/product_files/tmp';
$targetDir = '../..' . $fileDir;
if ( !is_dir( $targetDir ) )
mkdir( $targetDir, 0755, true );
$cleanupTargetDir = true;
$maxFileAge = 5 * 3600;
$chunk = isset( $_REQUEST["chunk"] ) ? intval( $_REQUEST["chunk"] ) : 0;
$chunks = isset( $_REQUEST["chunks"] ) ? intval( $_REQUEST["chunks"] ) : 0;
$fileName = isset( $_REQUEST["name"] ) ? $_REQUEST["name"] : '';
$fileName = preg_replace( '/[^\w\._]+/', '-', $fileName );
if ( file_exists( $targetDir . DIRECTORY_SEPARATOR . $fileName ) )
{
$ext = strrpos( $fileName, '.' );
$fileName_a = substr( $fileName, 0, $ext );
$fileName_b = substr( $fileName, $ext );
$count = 1;
while ( file_exists( $targetDir . DIRECTORY_SEPARATOR . $fileName_a . '_' . $count . $fileName_b ) )
$count++;
$fileName = $fileName_a . '_' . $count . $fileName_b;
}
$filePath = $targetDir . DIRECTORY_SEPARATOR . $fileName;
if ( $cleanupTargetDir && is_dir( $targetDir ) && ( $dir = opendir( $targetDir ) ) )
{
while ( ( $file = readdir( $dir ) ) !== false )
{
$tmpfilePath = $targetDir . DIRECTORY_SEPARATOR . $file;
if ( preg_match( '/\.part$/', $file ) && ( filemtime( $tmpfilePath ) < time() - $maxFileAge ) && ( $tmpfilePath != "{$filePath}.part" ) ) {
@unlink( $tmpfilePath );
}
}
closedir($dir);
}
else
die( '{"jsonrpc" : "2.0", "error" : {"code": 100, "message": "Failed to open temp directory."}, "id" : "id"}' );
if ( isset( $_SERVER["HTTP_CONTENT_TYPE"] ) )
$contentType = $_SERVER["HTTP_CONTENT_TYPE"];
if ( isset( $_SERVER["CONTENT_TYPE"] ) )
$contentType = $_SERVER["CONTENT_TYPE"];
if ( strpos( $contentType, "multipart" ) !== false )
{
if ( isset( $_FILES['file']['tmp_name'] ) && is_uploaded_file( $_FILES['file']['tmp_name'] ) )
{
$out = fopen( "{$filePath}.part", $chunk == 0 ? "wb" : "ab" );
if ( $out )
{
$in = fopen( $_FILES['file']['tmp_name'], "rb" );
if ( $in )
{
while ( $buff = fread( $in, 4096 ) )
fwrite($out, $buff);
}
else
die( '{"jsonrpc" : "2.0", "error" : {"code": 101, "message": "Failed to open input stream."}, "id" : "id"}' );
fclose( $in );
fclose( $out );
@unlink( $_FILES['file']['tmp_name'] );
}
else
die( '{"jsonrpc" : "2.0", "error" : {"code": 102, "message": "Failed to open output stream."}, "id" : "id"}' );
}
else
die( '{"jsonrpc" : "2.0", "error" : {"code": 103, "message": "Failed to move uploaded file."}, "id" : "id"}' );
}
else
{
$out = fopen( "{$filePath}.part", $chunk == 0 ? "wb" : "ab" );
if ( $out )
{
$in = fopen( "php://input", "rb" );
if ( $in )
{
while ( $buff = fread( $in, 4096 ) )
fwrite( $out, $buff );
}
else
die( '{"jsonrpc" : "2.0", "error" : {"code": 101, "message": "Failed to open input stream."}, "id" : "id"}' );
fclose( $in );
fclose( $out );
}
else
die( '{"jsonrpc" : "2.0", "error" : {"code": 102, "message": "Failed to open output stream."}, "id" : "id"}' );
}
if ( !$chunks || $chunk == $chunks - 1 )
{
rename( "{$filePath}.part", $filePath );
$mdb -> insert( 'pp_shop_products_files', [
'product_id' => null,
'src' => substr( $filePath, 5, strlen( $filePath ) )
] );
$file_id = $mdb -> id();
$file_name = explode( '/', $filePath );
$file_name = $file_name[ count( $file_name ) - 1 ];
}
die( '{"jsonrpc" : "2.0", "result" : null, "id" : "id", "file_name" : "' . $file_name . '", "file_id" : "' . $file_id . '"}' );
?>

View File

@@ -1,154 +1,61 @@
<?php <?php
require_once '../../config.php'; require_once '../../config.php';
require_once '../medoo/medoo.php'; require_once '../medoo/medoo.php';
date_default_timezone_set( 'Europe/Warsaw' ); require_once 'upload-common.php';
session_start();
$upload_token = $_REQUEST['upload_token'] ?? ''; plupload_bootstrap();
if ( !isset( $_SESSION['upload_tokens'][$upload_token] ) ) { plupload_require_post();
http_response_code(403); $userId = plupload_require_admin_user();
echo json_encode( ['error' => 'Brak tokenu uploadu'] ); plupload_validate_token($userId);
exit;
$fileDir = '/upload/product_images/tmp';
$targetDir = '../..' . $fileDir;
plupload_ensure_target_dir($targetDir);
list($chunk, $chunks) = plupload_get_chunks();
list($fileName, $extension, $filePath, $partPath) = plupload_build_target_paths(
$targetDir,
$_REQUEST['name'] ?? '',
['jpg', 'jpeg', 'png', 'gif', 'webp'],
null
);
plupload_cleanup_stale_parts($targetDir, $partPath, 5 * 3600);
plupload_write_chunk_to_part($partPath, $chunk);
plupload_assert_size_limit($partPath, 20 * 1024 * 1024, 'Plik przekracza dozwolony rozmiar (20 MB).');
$imageId = null;
if (plupload_is_last_chunk($chunk, $chunks)) {
plupload_finalize_part($partPath, $filePath);
$mime = mime_content_type($filePath) ?: '';
$imageMeta = @getimagesize($filePath);
$allowedMimeTypes = ['image/jpeg', 'image/png', 'image/gif', 'image/webp'];
$isValidImage = in_array($mime, $allowedMimeTypes, true)
&& is_array($imageMeta)
&& (int)($imageMeta[0] ?? 0) > 0
&& (int)($imageMeta[1] ?? 0) > 0;
if (!$isValidImage) {
@unlink($filePath);
plupload_send_error(400, 601, 'Plik nie jest prawidlowym obrazem.');
}
$mdb = plupload_create_medoo($database);
$order = (int)$mdb->max('pp_shop_products_images', 'o');
$productId = (int)($_POST['product_id'] ?? 0);
$mdb->insert('pp_shop_products_images', [
'product_id' => $productId > 0 ? $productId : null,
'src' => substr($filePath, 5),
'o' => $order + 1,
]);
$imageId = (int)$mdb->id();
} }
$tokenData = $_SESSION['upload_tokens'][$upload_token]; plupload_send_success([
if ( $tokenData['expires'] < time() ) { 'data_link' => str_replace('../../', '', $filePath),
unset( $_SESSION['upload_tokens'][$upload_token] ); 'image_id' => $imageId,
http_response_code(403); ]);
echo json_encode( ['error' => 'Token wygasł'] );
exit;
}
$mdb = new medoo( [
'database_type' => 'mysql',
'database_name' => $database['name'],
'server' => $database['host'],
'username' => $database['user'],
'password' => $database['password'],
'charset' => 'utf8'
] );
header( "Expires: Mon, 26 Jul 1997 05:00:00 GMT" );
header( "Last-Modified: " . gmdate( "D, d M Y H:i:s" ) . " GMT" );
header( "Cache-Control: no-store, no-cache, must-revalidate" );
header( "Cache-Control: post-check=0, pre-check=0", false );
header( "Pragma: no-cache" );
$fileDir = '/upload/product_images/tmp';
$targetDir = '../..' . $fileDir;
if ( !is_dir( $targetDir ) )
mkdir( $targetDir, 0755, true );
$cleanupTargetDir = true;
$maxFileAge = 5 * 3600;
$chunk = isset( $_REQUEST["chunk"] ) ? intval( $_REQUEST["chunk"] ) : 0;
$chunks = isset( $_REQUEST["chunks"] ) ? intval( $_REQUEST["chunks"] ) : 0;
$fileName = isset( $_REQUEST["name"] ) ? $_REQUEST["name"] : '';
$fileName = preg_replace( '/[^\w\._]+/', '-', $fileName );
if ( file_exists( $targetDir . DIRECTORY_SEPARATOR . $fileName ) )
{
$ext = strrpos( $fileName, '.' );
$fileName_a = substr( $fileName, 0, $ext );
$fileName_b = substr( $fileName, $ext );
$count = 1;
while ( file_exists( $targetDir . DIRECTORY_SEPARATOR . $fileName_a . '_' . $count . $fileName_b ) )
$count++;
$fileName = $fileName_a . '_' . $count . $fileName_b;
}
$filePath = $targetDir . DIRECTORY_SEPARATOR . $fileName;
if ( $cleanupTargetDir && is_dir( $targetDir ) && ( $dir = opendir( $targetDir ) ) )
{
while ( ( $file = readdir( $dir ) ) !== false )
{
$tmpfilePath = $targetDir . DIRECTORY_SEPARATOR . $file;
if ( preg_match( '/\.part$/', $file ) && ( filemtime( $tmpfilePath ) < time() - $maxFileAge ) && ( $tmpfilePath != "{$filePath}.part" ) ) {
@unlink( $tmpfilePath );
}
}
closedir($dir);
}
else
die( '{"jsonrpc" : "2.0", "error" : {"code": 100, "message": "Failed to open temp directory."}, "id" : "id"}' );
if ( isset( $_SERVER["HTTP_CONTENT_TYPE"] ) )
$contentType = $_SERVER["HTTP_CONTENT_TYPE"];
if ( isset( $_SERVER["CONTENT_TYPE"] ) )
$contentType = $_SERVER["CONTENT_TYPE"];
if ( strpos( $contentType, "multipart" ) !== false )
{
if ( isset( $_FILES['file']['tmp_name'] ) && is_uploaded_file( $_FILES['file']['tmp_name'] ) )
{
$out = fopen( "{$filePath}.part", $chunk == 0 ? "wb" : "ab" );
if ( $out )
{
$in = fopen( $_FILES['file']['tmp_name'], "rb" );
if ( $in )
{
while ( $buff = fread( $in, 4096 ) )
fwrite($out, $buff);
}
else
die( '{"jsonrpc" : "2.0", "error" : {"code": 101, "message": "Failed to open input stream."}, "id" : "id"}' );
fclose( $in );
fclose( $out );
@unlink( $_FILES['file']['tmp_name'] );
}
else
die( '{"jsonrpc" : "2.0", "error" : {"code": 102, "message": "Failed to open output stream."}, "id" : "id"}' );
}
else
die( '{"jsonrpc" : "2.0", "error" : {"code": 103, "message": "Failed to move uploaded file."}, "id" : "id"}' );
}
else
{
$out = fopen( "{$filePath}.part", $chunk == 0 ? "wb" : "ab" );
if ( $out )
{
$in = fopen( "php://input", "rb" );
if ( $in )
{
while ( $buff = fread( $in, 4096 ) )
fwrite( $out, $buff );
}
else
die( '{"jsonrpc" : "2.0", "error" : {"code": 101, "message": "Failed to open input stream."}, "id" : "id"}' );
fclose( $in );
fclose( $out );
}
else
die( '{"jsonrpc" : "2.0", "error" : {"code": 102, "message": "Failed to open output stream."}, "id" : "id"}' );
}
if ( !$chunks || $chunk == $chunks - 1 )
{
rename( "{$filePath}.part", $filePath );
$o = $mdb -> max( 'pp_shop_products_images', 'o' );
$mdb -> insert( 'pp_shop_products_images', [
'product_id' => isset( $_POST['product_id'] ) ? $_POST['product_id'] : null,
'src' => substr( $filePath, 5, strlen( $filePath ) ),
'o' => ++$o
] );
$image_id = $mdb -> id();
}
die( '{"jsonrpc" : "2.0", "result" : null, "id" : "id", "data_link" : "' . str_replace( '../../', '', $filePath ) . '", "image_id" : "' . $image_id . '"}' );
?>

View File

@@ -413,6 +413,56 @@ class ArticleRepositoryTest extends TestCase
$this->assertTrue($result); $this->assertTrue($result);
} }
public function testSaveFilesOrderUpdatesFilesOrder(): void
{
$mockDb = $this->createMock(\medoo::class);
$mockDb->expects($this->exactly(3))
->method('update')
->withConsecutive(
[
'pp_articles_files',
['o' => 0],
['AND' => ['article_id' => 12, 'id' => 70]]
],
[
'pp_articles_files',
['o' => 1],
['AND' => ['article_id' => 12, 'id' => 71]]
],
[
'pp_articles_files',
['o' => 2],
['AND' => ['article_id' => 12, 'id' => 72]]
]
)
->willReturn(true);
$repository = new ArticleRepository($mockDb);
$result = $repository->saveFilesOrder(12, '70;71;72');
$this->assertTrue($result);
}
public function testSaveFilesOrderSkipsEmptyValues(): void
{
$mockDb = $this->createMock(\medoo::class);
$mockDb->expects($this->once())
->method('update')
->with(
'pp_articles_files',
['o' => 0],
['AND' => ['article_id' => 7, 'id' => 101]]
)
->willReturn(true);
$repository = new ArticleRepository($mockDb);
$result = $repository->saveFilesOrder(7, ';101;');
$this->assertTrue($result);
}
public function testArchiveSetsStatusToMinusOne(): void public function testArchiveSetsStatusToMinusOne(): void
{ {
$mockDb = $this->createMock(\medoo::class); $mockDb = $this->createMock(\medoo::class);
@@ -482,6 +532,56 @@ class ArticleRepositoryTest extends TestCase
$this->assertSame('pp_articles', $deleteCalls[4]['table']); $this->assertSame('pp_articles', $deleteCalls[4]['table']);
} }
public function testPagesSummaryForArticlesBuildsLabels(): void
{
$mockDb = $this->createMock(\medoo::class);
$mockDb->expects($this->once())
->method('query')
->willReturnCallback(function ($sql, $params = []) {
return new class {
public function fetchAll()
{
return [
['article_id' => 5, 'page_id' => 10, 'title' => 'Blog'],
['article_id' => 5, 'page_id' => 11, 'title' => 'Poradniki'],
['article_id' => 8, 'page_id' => 12, 'title' => 'Aktualnosci'],
];
}
};
});
$repository = new ArticleRepository($mockDb);
$result = $repository->pagesSummaryForArticles([5, 8]);
$this->assertSame(' - Blog / Poradniki', $result[5]);
$this->assertSame(' - Aktualnosci', $result[8]);
}
public function testUpdateImageAltDelegatesToDatabase(): void
{
$mockDb = $this->createMock(\medoo::class);
$mockDb->expects($this->once())
->method('update')
->with('pp_articles_images', ['alt' => 'Nowy alt'], ['id' => 33])
->willReturn(true);
$repository = new ArticleRepository($mockDb);
$this->assertTrue($repository->updateImageAlt(33, 'Nowy alt'));
}
public function testMarkFileToDeleteDelegatesToDatabase(): void
{
$mockDb = $this->createMock(\medoo::class);
$mockDb->expects($this->once())
->method('update')
->with('pp_articles_files', ['to_delete' => 1], ['id' => 17])
->willReturn(true);
$repository = new ArticleRepository($mockDb);
$this->assertTrue($repository->markFileToDelete(17));
}
public function testListArchivedForAdminWhitelistsSortAndDirection(): void public function testListArchivedForAdminWhitelistsSortAndDirection(): void
{ {
$mockDb = $this->createMock(\medoo::class); $mockDb = $this->createMock(\medoo::class);

View File

@@ -56,6 +56,26 @@ class ArticlesControllerTest extends TestCase
$this->assertTrue(method_exists($this->controller, 'galleryOrderSave')); $this->assertTrue(method_exists($this->controller, 'galleryOrderSave'));
} }
public function testHasImageAltChangeMethod(): void
{
$this->assertTrue(method_exists($this->controller, 'imageAltChange'));
}
public function testHasFileNameChangeMethod(): void
{
$this->assertTrue(method_exists($this->controller, 'fileNameChange'));
}
public function testHasImageDeleteMethod(): void
{
$this->assertTrue(method_exists($this->controller, 'imageDelete'));
}
public function testHasFileDeleteMethod(): void
{
$this->assertTrue(method_exists($this->controller, 'fileDelete'));
}
public function testListMethodReturnType(): void public function testListMethodReturnType(): void
{ {
$reflection = new \ReflectionClass($this->controller); $reflection = new \ReflectionClass($this->controller);
@@ -74,6 +94,30 @@ class ArticlesControllerTest extends TestCase
$this->assertEquals('void', (string)$reflection->getMethod('galleryOrderSave')->getReturnType()); $this->assertEquals('void', (string)$reflection->getMethod('galleryOrderSave')->getReturnType());
} }
public function testImageAltChangeMethodReturnType(): void
{
$reflection = new \ReflectionClass($this->controller);
$this->assertEquals('void', (string)$reflection->getMethod('imageAltChange')->getReturnType());
}
public function testFileNameChangeMethodReturnType(): void
{
$reflection = new \ReflectionClass($this->controller);
$this->assertEquals('void', (string)$reflection->getMethod('fileNameChange')->getReturnType());
}
public function testImageDeleteMethodReturnType(): void
{
$reflection = new \ReflectionClass($this->controller);
$this->assertEquals('void', (string)$reflection->getMethod('imageDelete')->getReturnType());
}
public function testFileDeleteMethodReturnType(): void
{
$reflection = new \ReflectionClass($this->controller);
$this->assertEquals('void', (string)$reflection->getMethod('fileDelete')->getReturnType());
}
public function testConstructorRequiresArticleRepository(): void public function testConstructorRequiresArticleRepository(): void
{ {
$reflection = new \ReflectionClass(ArticlesController::class); $reflection = new \ReflectionClass(ArticlesController::class);

View File

@@ -44,6 +44,7 @@ if (!class_exists('S')) {
public static function delete_dir($path) {} public static function delete_dir($path) {}
public static function alert($msg) {} public static function alert($msg) {}
public static function htacces() {} public static function htacces() {}
public static function delete_cache() {}
public static function get($key) { return null; } public static function get($key) { return null; }
public static function set_message($msg) {} public static function set_message($msg) {}
public static function clear_redis_cache() {} public static function clear_redis_cache() {}

BIN
updates/0.20/ver_0.262.zip Normal file

Binary file not shown.

View File

@@ -0,0 +1,2 @@
F: ../admin/ajax/articles.php
F: ../autoload/admin/view/class.Articles.php

View File

@@ -0,0 +1,2 @@
ALTER TABLE pp_articles_files ADD COLUMN o int(11) NOT NULL DEFAULT 0;
UPDATE pp_articles_files SET o = id WHERE o = 0;

View File

@@ -1,5 +1,5 @@
<? <?
$current_ver = 258; $current_ver = 262;
for ($i = 1; $i <= $current_ver; $i++) for ($i = 1; $i <= $current_ver; $i++)
{ {