diff --git a/DATABASE_STRUCTURE.md b/DATABASE_STRUCTURE.md index cd858d0..44c989e 100644 --- a/DATABASE_STRUCTURE.md +++ b/DATABASE_STRUCTURE.md @@ -134,10 +134,14 @@ Pliki artykułów. | Kolumna | Opis | |---------|------| +| id | PK | | article_id | FK do pp_articles | | 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 Jednostki/slowniki (np. jednostki produktu). diff --git a/PROJECT_STRUCTURE.md b/PROJECT_STRUCTURE.md index 738c505..f8817ca 100644 --- a/PROJECT_STRUCTURE.md +++ b/PROJECT_STRUCTURE.md @@ -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`. - 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. + +### 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. diff --git a/REFACTORING_PLAN.md b/REFACTORING_PLAN.md index 8e48b33..a62c7f8 100644 --- a/REFACTORING_PLAN.md +++ b/REFACTORING_PLAN.md @@ -654,3 +654,31 @@ Gdy `persist = true`: - 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` - 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**. diff --git a/TESTING.md b/TESTING.md index a5c83dd..ad519b2 100644 --- a/TESTING.md +++ b/TESTING.md @@ -246,3 +246,24 @@ OK (165 tests, 424 assertions) Nowe testy dodane 2026-02-12: - `tests/Unit/Domain/Article/ArticleRepositoryTest.php` (rozszerzenie o testy `restore`, `deletePermanently`, `listArchivedForAdmin`) - `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`) diff --git a/admin/ajax.php b/admin/ajax.php index e951dcd..eeef67c 100644 --- a/admin/ajax.php +++ b/admin/ajax.php @@ -38,8 +38,7 @@ $mdb = new medoo( [ ] ); require_once 'ajax/pages.php'; -require_once 'ajax/articles.php'; require_once 'ajax/shop-category.php'; require_once 'ajax/users.php'; require_once 'ajax/shop.php'; -?> \ No newline at end of file +?> diff --git a/admin/ajax/articles.php b/admin/ajax/articles.php deleted file mode 100644 index 66d52d6..0000000 --- a/admin/ajax/articles.php +++ /dev/null @@ -1,46 +0,0 @@ - '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; -} \ No newline at end of file diff --git a/admin/templates/articles/article-edit-custom-script.php b/admin/templates/articles/article-edit-custom-script.php new file mode 100644 index 0000000..51a0b71 --- /dev/null +++ b/admin/templates/articles/article-edit-custom-script.php @@ -0,0 +1,583 @@ +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; + } +} +?> + + + + + + + + + + + + + + + + + + diff --git a/admin/templates/articles/article-edit.php b/admin/templates/articles/article-edit.php index 8cd9888..7d3e596 100644 --- a/admin/templates/articles/article-edit.php +++ b/admin/templates/articles/article-edit.php @@ -1,804 +1,6 @@ - - - - $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(); -?> -
- -
-
-
-
    - languages ) ): foreach ( $this -> languages as $lg ):?> - -
  • ';?>
  • - - -
-
- languages ) ): foreach ( $this -> languages as $lg ):?> - languages ) ) foreach ( $this -> languages as $lg_tmp ) - { - if ( $lg_tmp['id'] != $lg['id'] ) - $languages[ $lg_tmp['id'] ] = $lg_tmp['name']; - } - ?> - -
- 'Wyświetlaj treść z wersji', - 'name' => 'copy_from[' . $lg['id'] . ']', - 'values' => $languages, - 'value' => $this -> article['languages'][ $lg['id'] ]['copy_from'], - ) - );?> - 'Tytuł', - 'name' => 'title[' . $lg['id'] . ']', - 'id' => 'title_' . $lg['id'], - 'value' => $this -> article['languages'][ $lg['id'] ]['title'], - 'inline' => true - ) - );?> - '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');" - ] ); - ?> - 'Wstęp', - 'name' => 'entry[' . $lg['id'] . ']', - 'id' => 'entry_' . $lg['id'], - 'value' => $this -> article['languages'][ $lg['id'] ]['entry'], - 'inline' => true - ) - );?> - 'Treść', - 'name' => 'text[' . $lg['id'] . ']', - 'id' => 'text_' . $lg['id'], - 'value' => $this -> article['languages'][ $lg['id'] ]['text'], - 'inline' => true - ) - );?> - '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 - ] );?> - -
- - -
-
-
-
-
- 'Opublikowany', - 'name' => 'status', - 'checked' => $this -> article['status'] == 1 or !$this -> article['id'] ? true : false - ) - );?> - 'Pokaż tytuł', - 'name' => 'show_title', - 'checked' => $this -> article['show_title'] == 1 ? true : false - ) - );?> - 'Pokaż spis treści', - 'name' => 'show_table_of_contents', - 'checked' => $this -> article['show_table_of_contents'] == 1 ? true : false - ] );?> - 'Pokaż datę dodania', - 'name' => 'show_date_add', - 'checked' => $this -> article['show_date_add'] == 1 ? true : false - ) - );?> - 'Pokaż datę modyfikacji', - 'name' => 'show_date_modify', - 'checked' => $this -> article['show_date_modify'] == 1 ? true : false - ) - );?> - 'Powtórz wprowadzenie', - 'name' => 'repeat_entry', - 'checked' => $this -> article['repeat_entry'] == 1 ? true : false - ) - );?> - 'Linki do portali społecznościowych', - 'name' => 'social_icons', - 'checked' => $this -> article['social_icons'] == 1 ? true : false - ) - );?> -
-
-
-
    - languages ) ): foreach ( $this -> languages as $lg ):?> - -
  • ';?>
  • - - -
-
- languages ) ): foreach ( $this -> languages as $lg ):?> - -
- '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'] . ' );' - ) - );?> - 'Meta title', - 'name' => 'meta_title[' . $lg['id'] . ']', - 'id' => 'meta_title_' . $lg['id'], - 'value' => $this -> article['languages'][ $lg['id'] ]['meta_title'] - ] - );?> - 'Meta description', - 'name' => 'meta_description[' . $lg['id'] . ']', - 'id' => 'meta_description_' . $lg['id'], - 'value' => $this -> article['languages'][ $lg['id'] ]['meta_description'] - ) - );?> - 'Meta keywords', - 'name' => 'meta_keywords[' . $lg['id'] . ']', - 'id' => 'meta_keywords_' . $lg['id'], - 'value' => $this -> article['languages'][ $lg['id'] ]['meta_keywords'] - ) - );?> - 'Blokuj indeksację', - 'name' => 'noindex[' . $lg['id'] . ']', - 'id' => 'noindex_' . $lg['id'], - 'checked' => $this -> article['languages'][ $lg['id'] ]['noindex'] == 1 ? 1 : 0 - ] - );?> - '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 - ] );?> -
- - -
-
-
-
-
- layouts ) ): foreach ( $this -> layouts as $layout ): - $layouts[ $layout['id'] ] = $layout['name']; - endforeach; endif; - ?> - 'Szablon', - 'name' => 'layout_id', - 'id' => 'layout_id', - 'values' => $layouts, - 'value' => $this -> article['layout_id'] - ) - );?> -
- -
- menus ) ) foreach ( $this -> menus as $menu ) - { - ?> - - -
-
-
-
-
-
    - article['images'] ) ): foreach ( $this -> article['images'] as $img ): - ?> -
  • - - - - - -
  • - -
-
You browser doesn't have Flash installed.
-
-
-
    - article['files'] ) ): foreach ( $this -> article['files'] as $file ): - - if ( $file['name'] ) - $name = $file['name']; - else - { - $name = explode( '/', $file['src'] ); - $name = $name[ count( $name ) - 1 ]; - } - ?> -
  • -
    - - - - -
    -
  • - -
-
You browser doesn't have Flash installed.
-
-
-
- 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(); -?> - - - - - - - - - - - - - - + $this->form]); ?> + $this->article, + 'user' => $this->user, + 'languages' => $this->form->languages ?? [] +]); ?> diff --git a/admin/templates/articles/subpages-list.php b/admin/templates/articles/subpages-list.php index 2ab0071..6cb10e6 100644 --- a/admin/templates/articles/subpages-list.php +++ b/admin/templates/articles/subpages-list.php @@ -6,11 +6,13 @@ article_pages ) and in_array( $page['id'], $this -> article_pages ) ):?>checked="checked" /> - article_pages, $page['id'], $this -> step + 1 ); - ?> + $page['subpages'], + 'article_pages' => $this->article_pages, + 'parent_id' => $page['id'], + 'step' => $this->step + 1, + ] ); ?> - \ No newline at end of file + diff --git a/admin/templates/shop-product/product-edit.php b/admin/templates/shop-product/product-edit.php index 9289ad8..e4a216a 100644 --- a/admin/templates/shop-product/product-edit.php +++ b/admin/templates/shop-product/product-edit.php @@ -949,6 +949,7 @@ echo $grid->draw(); }, url: '/libraries/plupload/upload-product-images.php', chunk_size: '1mb', + max_file_size: '20mb', unique_names: false, resize: { width: settings['image_px'] ? $this->settings['image_px'] : '1920'; ?>, @@ -991,6 +992,7 @@ echo $grid->draw(); }, url: '/libraries/plupload/upload-product-files.php', chunk_size: '1mb', + max_file_size: '50mb', unique_names: false, filters: [{ title: "Wszystkie pliki", diff --git a/autoload/Domain/Article/ArticleRepository.php b/autoload/Domain/Article/ArticleRepository.php index 429578c..41f7360 100644 --- a/autoload/Domain/Article/ArticleRepository.php +++ b/autoload/Domain/Article/ArticleRepository.php @@ -37,7 +37,15 @@ class ArticleRepository 'article_id' => $articleId, '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]); return $article; @@ -70,6 +78,8 @@ class ArticleRepository $this->savePages($id, $data['pages'] ?? null, true); $this->assignTempFiles($id); $this->assignTempImages($id); + $this->applyGalleryOrderIfProvided($id, $data); + $this->applyFilesOrderIfProvided($id, $data); \S::htacces(); \S::delete_dir('../temp/'); @@ -87,6 +97,8 @@ class ArticleRepository $this->savePages($articleId, $data['pages'] ?? null, false); $this->assignTempFiles($articleId); $this->assignTempImages($articleId); + $this->applyGalleryOrderIfProvided($articleId, $data); + $this->applyFilesOrderIfProvided($articleId, $data); $this->deleteMarkedImages($articleId); $this->deleteMarkedFiles($articleId); @@ -99,16 +111,16 @@ class ArticleRepository private function buildArticleRow(array $data, int $userId, bool $isNew): array { $row = [ - 'show_title' => ($data['show_title'] ?? '') == 'on' ? 1 : 0, - 'show_date_add' => ($data['show_date_add'] ?? '') == 'on' ? 1 : 0, - 'show_date_modify' => ($data['show_date_modify'] ?? '') == 'on' ? 1 : 0, + 'show_title' => $this->isCheckedValue($data['show_title'] ?? null) ? 1 : 0, + 'show_date_add' => $this->isCheckedValue($data['show_date_add'] ?? null) ? 1 : 0, + 'show_date_modify' => $this->isCheckedValue($data['show_date_modify'] ?? null) ? 1 : 0, 'date_modify' => date('Y-m-d H:i:s'), 'modify_by' => $userId, 'layout_id' => !empty($data['layout_id']) ? (int)$data['layout_id'] : null, - 'status' => ($data['status'] ?? '') == 'on' ? 1 : 0, - 'repeat_entry' => ($data['repeat_entry'] ?? '') == 'on' ? 1 : 0, - 'social_icons' => ($data['social_icons'] ?? '') == 'on' ? 1 : 0, - 'show_table_of_contents' => ($data['show_table_of_contents'] ?? '') == 'on' ? 1 : 0, + 'status' => $this->isCheckedValue($data['status'] ?? null) ? 1 : 0, + 'repeat_entry' => $this->isCheckedValue($data['repeat_entry'] ?? null) ? 1 : 0, + 'social_icons' => $this->isCheckedValue($data['social_icons'] ?? null) ? 1 : 0, + 'show_table_of_contents' => $this->isCheckedValue($data['show_table_of_contents'] ?? null) ? 1 : 0, ]; if ($isNew) { @@ -131,12 +143,32 @@ class ArticleRepository 'meta_description' => ($data['meta_description'][$langId] ?? '') != '' ? $data['meta_description'][$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, - '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, - '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 { $titles = $data['title'] ?? []; @@ -573,6 +605,182 @@ class ArticleRepository 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 $articleIds + * @return array + */ + 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( array &$where, array &$params, diff --git a/autoload/admin/Controllers/ArticlesController.php b/autoload/admin/Controllers/ArticlesController.php index 9766a01..d8ecf6e 100644 --- a/autoload/admin/Controllers/ArticlesController.php +++ b/autoload/admin/Controllers/ArticlesController.php @@ -4,6 +4,10 @@ namespace admin\Controllers; use Domain\Article\ArticleRepository; use Domain\Languages\LanguagesRepository; 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 { @@ -61,12 +65,21 @@ class ArticlesController $listRequest['perPage'] ); + $articleIds = []; + foreach ($result['items'] as $item) { + $id = (int)($item['id'] ?? 0); + if ($id > 0) { + $articleIds[] = $id; + } + } + $pagesSummary = $this->repository->pagesSummaryForArticles($articleIds); + $rows = []; $lp = ($listRequest['page'] - 1) * $listRequest['perPage'] + 1; foreach ($result['items'] as $item) { $id = (int)$item['id']; $title = (string)($item['title'] ?? ''); - $pages = (string)\admin\factory\Articles::article_pages($id); + $pages = (string)($pagesSummary[$id] ?? ''); $rows[] = [ 'lp' => $lp++ . '.', @@ -146,6 +159,18 @@ class ArticlesController 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) */ @@ -153,11 +178,72 @@ class ArticlesController { global $user; - $values = json_decode(\S::get('values'), true); - $response = ['status' => 'error', 'msg' => 'Podczas zapisywania artykulu wystapil blad. Prosze sprobowac ponownie.']; + $values = $this->resolveSavePayload(); + $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'])) { - $response = ['status' => 'ok', 'msg' => 'Artykul zostal zapisany.', 'id' => $id]; + if ($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); @@ -192,12 +278,226 @@ class ArticlesController $this->repository->deleteNonassignedImages(); $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', [ - 'article' => $this->repository->find((int)\S::get('id')), - 'menus' => \admin\factory\Pages::menus_list(), - 'languages' => $this->languagesRepository->languagesList(), - 'layouts' => $this->layoutsRepository->listAll(), - 'user' => $user + 'form' => $viewModel, + 'article' => $article, + '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: ' . $this->escapeHtml((string)($article['languages'][$defaultLanguageId]['title'] ?? '')) . '' + : '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 = '
'; + $html .= ''; + $html .= '
'; + + 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 .= ''; + } + + $html .= '
'; + + return $html; + } + + private function renderImagesBox(array $article): string + { + $html = '
You browser doesn\'t have Flash installed.
'; + + return $html; + } + + private function renderFilesBox(array $article): string + { + $html = '
You browser doesn\'t have Flash installed.
'; + + return $html; + } + + private function escapeHtml(string $value): string + { + return htmlspecialchars($value, ENT_QUOTES, 'UTF-8'); + } } diff --git a/autoload/admin/Support/Forms/FormFieldRenderer.php b/autoload/admin/Support/Forms/FormFieldRenderer.php index 5a35c10..9d256bf 100644 --- a/autoload/admin/Support/Forms/FormFieldRenderer.php +++ b/autoload/admin/Support/Forms/FormFieldRenderer.php @@ -201,7 +201,7 @@ class FormFieldRenderer 'name' => $field->name, 'id' => $field->id, 'value' => $value ?? '', - 'options' => $field->options, + 'values' => $field->options, 'class' => ($field->required ? 'require ' : '') . ($field->attributes['class'] ?? ''), ]; @@ -308,6 +308,11 @@ class FormFieldRenderer 'value="' . htmlspecialchars($value ?? '') . '">'; } + public function renderCustom(FormField $field): string + { + return (string)($field->customHtml ?? ''); + } + /** * Renderuje sekcję językową */ @@ -388,6 +393,16 @@ class FormFieldRenderer 'id' => $id, '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. return $this->wrapWithError(\Html::input([ diff --git a/autoload/admin/ViewModels/Forms/FormField.php b/autoload/admin/ViewModels/Forms/FormField.php index faaf46d..cc07788 100644 --- a/autoload/admin/ViewModels/Forms/FormField.php +++ b/autoload/admin/ViewModels/Forms/FormField.php @@ -29,6 +29,7 @@ class FormField // Specyficzne dla lang_section public ?array $langFields; public ?string $langSectionParentTab; + public ?string $customHtml; /** * @param string $name Nazwa pola (name) @@ -64,7 +65,8 @@ class FormField string $editorToolbar = 'MyTool', int $editorHeight = 300, ?array $langFields = null, - ?string $langSectionParentTab = null + ?string $langSectionParentTab = null, + ?string $customHtml = null ) { $this->name = $name; $this->type = $type; @@ -82,6 +84,7 @@ class FormField $this->editorHeight = $editorHeight; $this->langFields = $langFields; $this->langSectionParentTab = $langSectionParentTab; + $this->customHtml = $customHtml; $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 * diff --git a/autoload/admin/ViewModels/Forms/FormFieldType.php b/autoload/admin/ViewModels/Forms/FormFieldType.php index 66e68dc..579c15d 100644 --- a/autoload/admin/ViewModels/Forms/FormFieldType.php +++ b/autoload/admin/ViewModels/Forms/FormFieldType.php @@ -20,4 +20,5 @@ class FormFieldType public const FILE = 'file'; public const HIDDEN = 'hidden'; public const LANG_SECTION = 'lang_section'; + public const CUSTOM = 'custom'; } diff --git a/autoload/admin/class.Site.php b/autoload/admin/class.Site.php index f519909..8777cd9 100644 --- a/autoload/admin/class.Site.php +++ b/autoload/admin/class.Site.php @@ -330,11 +330,16 @@ class Site */ private static $actionMap = [ 'gallery_order_save' => 'galleryOrderSave', + 'files_order_save' => 'filesOrderSave', 'view_list' => 'list', 'article_edit' => 'edit', 'article_save' => 'save', 'article_delete' => 'delete', '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_save' => 'save', 'banner_delete' => 'delete', diff --git a/autoload/admin/view/class.Articles.php b/autoload/admin/view/class.Articles.php deleted file mode 100644 index fb2879b..0000000 --- a/autoload/admin/view/class.Articles.php +++ /dev/null @@ -1,22 +0,0 @@ - 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' ); - } -} -?> diff --git a/autoload/class.Image.php b/autoload/class.Image.php index 0065afa..bd509ee 100644 --- a/autoload/class.Image.php +++ b/autoload/class.Image.php @@ -3,7 +3,8 @@ class ImageManipulator { protected int $width; protected int $height; - protected \GdImage $image; + /** @var resource|\GdImage */ + protected $image; protected ?string $file = null; /** @@ -37,7 +38,7 @@ class ImageManipulator 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); } @@ -66,7 +67,7 @@ class ImageManipulator 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"); } @@ -91,12 +92,12 @@ class ImageManipulator */ 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); } $image = imagecreatefromstring($data); - if (!$image instanceof \GdImage) { + if (!$this->isValidImageResource($image)) { 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 { - if (!isset($this->image) || !$this->image instanceof \GdImage) { + if (!isset($this->image) || !$this->isValidImageResource($this->image)) { 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 { - if (!isset($this->image) || !$this->image instanceof \GdImage) { + if (!isset($this->image) || !$this->isValidImageResource($this->image)) { throw new RuntimeException('No image set'); } @@ -177,7 +178,7 @@ class ImageManipulator $height = max($height, $this->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'); } @@ -233,7 +234,7 @@ class ImageManipulator */ 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'); } @@ -257,7 +258,7 @@ class ImageManipulator } $temp = imagecreatetruecolor($cropWidth, $cropHeight); - if (!$temp instanceof \GdImage) { + if (!$this->isValidImageResource($temp)) { 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 * - * @param \GdImage $res New image resource + * @param resource|\GdImage $res New image resource * @return self * @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'); } - if (isset($this->image) && $this->image instanceof \GdImage) { + if (isset($this->image) && $this->isValidImageResource($this->image)) { imagedestroy($this->image); } @@ -386,9 +387,9 @@ class ImageManipulator /** * Returns the GD image resource * - * @return \GdImage + * @return resource|\GdImage */ - public function getResource(): \GdImage + public function getResource() { return $this->image; } @@ -418,8 +419,22 @@ class ImageManipulator */ public function __destruct() { - if (isset($this->image) && $this->image instanceof \GdImage) { + if (isset($this->image) && $this->isValidImageResource($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; + } } diff --git a/libraries/grid/config.php b/libraries/grid/config.php index b6d615b..a04d21a 100644 --- a/libraries/grid/config.php +++ b/libraries/grid/config.php @@ -12,10 +12,21 @@ session_start(); /* połączenie z bazą ustawić wg własnych preferencji */ require_once dirname( __FILE__ ) . '/../../config.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'; -require_once dirname( __FILE__ ) . '/../../autoload/admin/factory/class.ShopCategory.php'; -require_once dirname( __FILE__ ) . '/../../autoload/admin/factory/class.ShopProduct.php'; + +$legacyFactoryFiles = [ + '/../../autoload/admin/factory/class.Articles.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__ ) . '/../../libraries/medoo/medoo.php'; @@ -35,4 +46,4 @@ $mdb = new medoo( [ 'username' => $database['user'], 'password' => $database['password'], 'charset' => 'utf8' - ] ); \ No newline at end of file + ] ); diff --git a/libraries/plupload/upload-articles-files.php b/libraries/plupload/upload-articles-files.php index de93762..10b547c 100644 --- a/libraries/plupload/upload-articles-files.php +++ b/libraries/plupload/upload-articles-files.php @@ -1,154 +1,53 @@ 'Brak tokenu uploadu'] ); - exit; +plupload_bootstrap(); +plupload_require_post(); +$userId = plupload_require_admin_user(); +plupload_validate_token($userId); + +$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]; -if ( $tokenData['expires'] < time() ) { - unset( $_SESSION['upload_tokens'][$upload_token] ); - http_response_code(403); - echo json_encode( ['error' => 'Token wygasł'] ); - exit; -} +plupload_send_success([ + 'file_name' => $responseFileName, + 'file_id' => $fileId, +]); -$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 . '"}' ); -?> \ No newline at end of file diff --git a/libraries/plupload/upload-articles-images.php b/libraries/plupload/upload-articles-images.php index b2c6dad..d0d90c7 100644 --- a/libraries/plupload/upload-articles-images.php +++ b/libraries/plupload/upload-articles-images.php @@ -1,153 +1,61 @@ 'Brak tokenu uploadu'] ); - exit; +plupload_bootstrap(); +plupload_require_post(); +$userId = plupload_require_admin_user(); +plupload_validate_token($userId); + +$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]; -if ( $tokenData['expires'] < time() ) { - unset( $_SESSION['upload_tokens'][$upload_token] ); - http_response_code(403); - echo json_encode( ['error' => 'Token wygasł'] ); - exit; -} +plupload_send_success([ + 'data_link' => str_replace('../../', '', $filePath), + 'image_id' => $imageId, +]); -$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 . '"}' ); -?> \ No newline at end of file diff --git a/libraries/plupload/upload-common.php b/libraries/plupload/upload-common.php new file mode 100644 index 0000000..d8fa95d --- /dev/null +++ b/libraries/plupload/upload-common.php @@ -0,0 +1,307 @@ + '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; + } +} + diff --git a/libraries/plupload/upload-product-files.php b/libraries/plupload/upload-product-files.php index 735b9bf..49bdaab 100644 --- a/libraries/plupload/upload-product-files.php +++ b/libraries/plupload/upload-product-files.php @@ -1,154 +1,53 @@ 'Brak tokenu uploadu'] ); - exit; +plupload_bootstrap(); +plupload_require_post(); +$userId = plupload_require_admin_user(); +plupload_validate_token($userId); + +$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]; -if ( $tokenData['expires'] < time() ) { - unset( $_SESSION['upload_tokens'][$upload_token] ); - http_response_code(403); - echo json_encode( ['error' => 'Token wygasł'] ); - exit; -} +plupload_send_success([ + 'file_name' => $responseFileName, + 'file_id' => $fileId, +]); -$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 . '"}' ); -?> \ No newline at end of file diff --git a/libraries/plupload/upload-product-images.php b/libraries/plupload/upload-product-images.php index 88894b9..04be8cb 100644 --- a/libraries/plupload/upload-product-images.php +++ b/libraries/plupload/upload-product-images.php @@ -1,154 +1,61 @@ 'Brak tokenu uploadu'] ); - exit; +plupload_bootstrap(); +plupload_require_post(); +$userId = plupload_require_admin_user(); +plupload_validate_token($userId); + +$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]; -if ( $tokenData['expires'] < time() ) { - unset( $_SESSION['upload_tokens'][$upload_token] ); - http_response_code(403); - echo json_encode( ['error' => 'Token wygasł'] ); - exit; -} +plupload_send_success([ + 'data_link' => str_replace('../../', '', $filePath), + 'image_id' => $imageId, +]); -$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 . '"}' ); -?> \ No newline at end of file diff --git a/tests/Unit/Domain/Article/ArticleRepositoryTest.php b/tests/Unit/Domain/Article/ArticleRepositoryTest.php index 06aea6a..333f818 100644 --- a/tests/Unit/Domain/Article/ArticleRepositoryTest.php +++ b/tests/Unit/Domain/Article/ArticleRepositoryTest.php @@ -413,6 +413,56 @@ class ArticleRepositoryTest extends TestCase $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 { $mockDb = $this->createMock(\medoo::class); @@ -482,6 +532,56 @@ class ArticleRepositoryTest extends TestCase $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 { $mockDb = $this->createMock(\medoo::class); diff --git a/tests/Unit/admin/Controllers/ArticlesControllerTest.php b/tests/Unit/admin/Controllers/ArticlesControllerTest.php index 15b92e2..4051585 100644 --- a/tests/Unit/admin/Controllers/ArticlesControllerTest.php +++ b/tests/Unit/admin/Controllers/ArticlesControllerTest.php @@ -56,6 +56,26 @@ class ArticlesControllerTest extends TestCase $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 { $reflection = new \ReflectionClass($this->controller); @@ -74,6 +94,30 @@ class ArticlesControllerTest extends TestCase $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 { $reflection = new \ReflectionClass(ArticlesController::class); diff --git a/tests/bootstrap.php b/tests/bootstrap.php index 6cf9a68..ada8d9d 100644 --- a/tests/bootstrap.php +++ b/tests/bootstrap.php @@ -44,6 +44,7 @@ if (!class_exists('S')) { public static function delete_dir($path) {} public static function alert($msg) {} public static function htacces() {} + public static function delete_cache() {} public static function get($key) { return null; } public static function set_message($msg) {} public static function clear_redis_cache() {} diff --git a/updates/0.20/ver_0.262.zip b/updates/0.20/ver_0.262.zip new file mode 100644 index 0000000..e3fd16d Binary files /dev/null and b/updates/0.20/ver_0.262.zip differ diff --git a/updates/0.20/ver_0.262_files.txt b/updates/0.20/ver_0.262_files.txt new file mode 100644 index 0000000..d54b32a --- /dev/null +++ b/updates/0.20/ver_0.262_files.txt @@ -0,0 +1,2 @@ +F: ../admin/ajax/articles.php +F: ../autoload/admin/view/class.Articles.php diff --git a/updates/0.20/ver_0.262_sql.txt b/updates/0.20/ver_0.262_sql.txt new file mode 100644 index 0000000..fe1dc8c --- /dev/null +++ b/updates/0.20/ver_0.262_sql.txt @@ -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; diff --git a/updates/versions.php b/updates/versions.php index 59bbd79..a863dfa 100644 --- a/updates/versions.php +++ b/updates/versions.php @@ -1,5 +1,5 @@