Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
58 commits
Select commit Hold shift + click to select a range
9fcdf12
Исправление отображения списка в режиме для чтения
n2ref Oct 7, 2025
c7d6bb1
Исправление поля для импорта в edit классе
n2ref Oct 8, 2025
45059fc
Исправление фатала
Oct 12, 2025
ccfe649
Исправление sql ошибки при входе через ldap
n2ref Oct 14, 2025
b8a97f8
Merge remote-tracking branch 'origin/2.9.1' into 2.9.1
n2ref Oct 14, 2025
e1d40e0
Merge remote-tracking branch 'easterism/2.9.2' into 2.9.2
n2ref Oct 15, 2025
9eda6d8
listTable deprecated
Oct 27, 2025
fee6b3b
Исправление варнинга
n2ref Nov 14, 2025
64e2ade
Улучшение checkbox2
n2ref Nov 14, 2025
5829278
Добавлено описание доступных для загрузки форматов файлов
n2ref Nov 18, 2025
8850315
api работает с двумя подходами
Nov 22, 2025
e603c4e
Исправление ошибок при отображении схемы api
Nov 23, 2025
2381ce6
Исправление создания лишних серверов при наличии папки
Nov 23, 2025
a93ed9b
Добавлено описание ошибки при отсутствии методы api
n2ref Dec 12, 2025
1f82473
Добавлена возможсность кастомно обрабатывать сортировку в таблицах
Dec 18, 2025
0eccbbd
Исправление переноса меню на новую строку
n2ref Dec 22, 2025
2d751c3
Merge remote-tracking branch 'easterism/2.9.2' into 2.9.2
n2ref Dec 23, 2025
36410a7
Добавлен метод getFilesNormalize
Dec 23, 2025
c845ed2
Merge remote-tracking branch 'origin/2.9.2' into 2.9.2
Dec 23, 2025
d4b1adf
Merge remote-tracking branch 'easterism/2.9.2' into 2.9.2
n2ref Jan 22, 2026
82c517f
Исправление генерации документации апи
n2ref Jan 22, 2026
53a3b97
Добавлено ограничение в api модулей по доступу
n2ref Jan 22, 2026
ed40284
В регулярках путей для api по умолчанию только цифры
n2ref Jan 23, 2026
eeef2fc
Merge remote-tracking branch 'easterism/2.9.2' into 2.9.2
n2ref Jan 29, 2026
71ffcc5
Исправление получения схемы api
n2ref Jan 29, 2026
e414019
Версия
Feb 3, 2026
db068f7
Возможность указывать лимит на одновременную загрузку
n2ref Feb 5, 2026
766a98d
Merge remote-tracking branch 'origin/2.9.2' into 2.9.2
BeHuK Feb 17, 2026
c9534fe
Добавлен метод setOverflow
n2ref Feb 17, 2026
5481579
Merge remote-tracking branch 'origin/2.9.2'
n2ref Feb 17, 2026
64b80b4
Merge remote-tracking branch 'easterism/2.9.2'
n2ref Feb 20, 2026
16b6f57
тамбнэйлы файлов
easterism Mar 20, 2026
bda25b0
Merge remote-tracking branch 'origin/2.9.2' into 2.9.2
easterism Mar 21, 2026
31d2ae3
new: redis pub/sub
easterism Mar 22, 2026
2450208
Merge remote-tracking branch 'easterism/2.9.2' into 2.9.2
n2ref Mar 25, 2026
084c6fb
Добавлен метод для получения справочника по id
n2ref Mar 25, 2026
87c1e29
Merge remote-tracking branch 'origin/2.9.2' into 2.9.2
easterism Mar 26, 2026
0779e1c
попытка повторного входа
easterism Mar 26, 2026
fbc31b0
Исправление некорректного определения типа переменной в IDE
n2ref Mar 26, 2026
661a702
мердж с веткой Паши
n2ref Mar 26, 2026
bf3e003
Исправление некорректного определения типа переменной в IDE
n2ref Mar 26, 2026
708727f
Паша добавил метод deleteRow. я его перенес сюда
n2ref Mar 27, 2026
0e245d7
Исправления ошибок
n2ref Mar 27, 2026
b12dde4
Merge remote-tracking branch 'origin/2.9.2' into 2.9.2
n2ref Mar 27, 2026
113ede4
refactor(js): replace $.ajax calls with fetch across core modules
Mar 30, 2026
6bdd8d6
Merge remote-tracking branch 'rdo-belhard/chore/ajax-to-fetch-2.9.2' …
easterism Mar 30, 2026
b90218c
исправление фатала
n2ref Mar 30, 2026
a3d7fd8
Merge remote-tracking branch 'origin/2.9.2' into 2.9.2
BeHuK Apr 9, 2026
4460b98
Merge remote-tracking branch 'ihor/2.9.2' into 2.9.2
BeHuK Apr 9, 2026
224b701
employees#114 проверка на устаревание пароля.
BeHuK Apr 10, 2026
ad5a3ac
герман выключен по умолчанию
Apr 20, 2026
80c34c2
Адрес запроса может быть с разными символами по высоте
n2ref Apr 22, 2026
9afb8c6
сортировка пунктов навигации
easterism Apr 28, 2026
9fb12a9
фильтр диапазона дат 23:59:59 по умолчанию
BeHuK Apr 28, 2026
06df904
Добавлена возможность сворачивания групп в таблице
n2ref May 13, 2026
7142c6e
Merge remote-tracking branch 'easterism/2.9.2' into 2.9.2
n2ref May 14, 2026
6f94bb1
Merge remote-tracking branch 'BeHuK/2.9.2_filter_date_period' into 2.9.2
n2ref May 14, 2026
c653b8a
Исправление нотиса
n2ref May 14, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .phpactor.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"$schema": "/phpactor.schema.json",
"language_server_phpstan.enabled": true
}
75 changes: 75 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
# Core2 — Agent Instructions

## What this is
PHP fullstack framework for business apps. Modular MVC with CLI, Gearman workers, REST/SOAP APIs.
Version: 2.9.1 | PHP >= 8.2 | MySQL/PostgreSQL

## Developer Commands

```bash
composer install # Install deps (classmap-authoritative)
php cli.php --module cron --action run # Run CLI task
php cli.php --module cron --action runJob --param 123
php worker.php -d -c conf.ini -s site.com # Start Gearman worker daemon
php worker.php -H # Worker help

# Static analysis
vendor/bin/phpstan analyse # Level 6, scans inc/ and mod/admin/

# Tests
vendor/bin/phpunit # PHPUnit 9.5 (minimal coverage)
```

## Project Layout

| Path | What |
|------|------|
| `inc/classes/` | 58 core classes — Db, Cache, Acl, Config, Router, Init, Api, SSE |
| `inc/CoreController.php` | Admin panel controller (action_* methods) |
| `mod/<name>/` | Modules (admin, auth, billing, cron, webservice, oauth, etc.) |
| `mod/<name>/vX.Y.Z/` | Module version dirs (e.g. `mod/cron/v3.6.0/`) |
| `workers/` | Gearman workers (Eventer, Logger, Workhorse) |
| `html/{default,light,material}/` | UI themes |

## Architecture Facts

- **Entry point**: `Init::dispatch()` routes `module/action/params` → controller method `action_<action>()`
- **Routing**: `Router::routeParse()` parses URI. `/api/...` → API dispatch, else → module controller
- **Module naming**: Class `Mod<Name>Controller` for module `<name>`. Submodules use `module_sm_key` as resource ID
- **Magic getters**: `$this->db`, `$this->cache`, `$this->log`, `$this->translate`, `$this->modAdmin`, `$this->dataUsers`, `$this->apiProfile` — all resolved via `__get()` in `Common`/`Db` base classes
- **ACL**: Laminas Permissions ACL, role-based. Resources = modules + submodules. Types: `access`, `list_all`, `read_all`, `edit_all`, `delete_all`, `*_owner`, `*_default`. Cached per role
- **XAJAX**: POST requests from UI go through `post()` function in `Init.php` → `ModAjax.php` methods prefixed with `ax` (e.g. `axSave`, `axDelete`)
- **SSE**: `/sse` route handled by `Core2\SSE` class
- **Systemd**: `core2_worker.service` template for Gearman daemon (user=www-data, php8.2)

## Configuration

- **App config**: `conf.ini` next to `index.php`, section = `$_SERVER['SERVER_NAME']` or `production`
- **Core config**: `core2/conf.ini` (gearman, cache, auth, SSE defaults)
- **Extended config**: `conf.ext.ini` (optional, merged if present)
- **Module config**: `mod/<name>/conf.ini` — read via `$this->moduleConfig`

## Key Conventions

- Controller action methods: `public function action_<name>()`
- Translations: `$this->_("string")` or `$this->translate->tr("string", $module)`
- Models: `$this->data<Name>` auto-resolves to model classes (e.g. `$this->dataUsers`)
- API classes: `$this->api<Name>` auto-resolves (e.g. `$this->apiProfile`)
- Cross-module controllers: `$this->mod<Name>` (e.g. `$this->modAdmin`)
- File handler contexts: `fileid`, `thumbid`, `tfile`, `field_<name>`

## Gotchas

- `.gitignore` excludes `mod/*` and `html/*` — only `mod/admin` is tracked. Other modules/themes are external
- `vendor/` is gitignored — always run `composer install`
- `phpstan-baseline.neon` and `gen/` are gitignored
- Tests use `$GLOBALS['DB_NAME']`, `$GLOBALS['DB_USER']`, etc. for DB config — set before running
- Test bootstrap expects `core2/` subdirectory structure (DOC_ROOT points 2 levels up from `tests/`)
- Multiple DEPRECATED auth paths still present (`HTTP_CORE2M` header, `apikey` param)
- FIXME/TODO comments throughout — do not treat as implemented

## Security Notes

- Default admin password is MD5("admin") = `ad7123ebca969de21e49c12a7d69ce25` — change immediately
- `Tool::password_verify_secure()` used for password verification
- POST requests check `HTTP_REFERER` for same-host validation
11 changes: 5 additions & 6 deletions cli.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,6 @@
<?php
namespace Core2;

require_once 'inc/classes/Error.php';
require_once 'inc/classes/Cli.php';
require_once 'inc/classes/Registry.php';
require_once 'inc/classes/Config.php';
require_once 'inc/classes/I18n.php';

try {
if (PHP_SAPI !== 'cli') {
throw new \Exception("Allowed for CLI only.");
Expand All @@ -17,6 +11,11 @@
throw new \Exception("Composer autoload is missing.");
}
require_once($autoload);
require_once 'inc/classes/Error.php';
require_once 'inc/classes/Cli.php';
require_once 'inc/classes/Registry.php';
require_once 'inc/classes/Config.php';
require_once 'inc/classes/I18n.php';

$_SERVER['SERVER_NAME'] = '_';
$options = getopt('c:m:a:p:s:h', array(
Expand Down
2 changes: 1 addition & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@
"phpseclib/phpseclib": "~2.0",
"zircote/swagger-php": "6.*",
"phpmailer/phpmailer": "v6.9.3",
"predis/predis": "^2.0",
"predis/predis": "v3.4.2",
"ext-json": "*",
"ext-zip": "*",
"ext-mbstring": "*",
Expand Down
4 changes: 2 additions & 2 deletions conf.ini
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@

[production]
; ## Текущая версия ядра
version = 2.9.1
version = 2.9.2

; ## Используется в модуле webservice
; ## Время в секундах на которое выписывается вебтокен пользователя
Expand Down Expand Up @@ -132,7 +132,7 @@ auth.digest.userhash = false
; should be absolute or relative to running
; gearman.worker_dir = ./workers

gearman.host = "127.0.0.1:4730"
; gearman.host = "127.0.0.1:4730"

; All workers in worker_dir will be loaded
gearman.include=*
Expand Down
70 changes: 67 additions & 3 deletions inc/CoreController.php
Original file line number Diff line number Diff line change
Expand Up @@ -392,10 +392,23 @@ public function action_seq(): string {

if ($rows) {
$rows_seq = array_values($rows);
$records = [];

foreach ($_POST['data'] as $k => $row_id) {
$where = $this->db->quoteInto("{$id_name} = ?", $row_id);
$this->db->update($table_name, ['seq' => $rows_seq[$k]], $where);
foreach ($_POST['data'] as $k => $row_id) {
$records[$row_id] = $rows_seq[$k];
}


$is_custom = $records
? $this->customSequence($resource, $records)
: false;


if ( ! $is_custom) {
foreach ($records as $row_id => $sequence) {
$where = $this->db->quoteInto("{$id_name} = ?", $row_id);
$this->db->update($table_name, ['seq' => $sequence], $where);
}
}
}

Expand Down Expand Up @@ -1106,4 +1119,55 @@ public function action_workhorse() {
echo Alert::danger($e->getMessage());
}
}


/**
* Проверка модуля на реализацию собственного удаления
* @param string $resource
* @param array $records
* @return bool
* @throws Exception
*/
private function customSequence(string $resource, array $records): bool {

$mod = explode('xxx', $resource);
$mod = explode("_", $mod[0]);
$controller_name = "Mod" . ucfirst(strtolower($mod[0])) . "Controller";

$this->requireController($mod[0], $controller_name);
$controller = new $controller_name();

return $controller instanceof Sequence
? $controller->sequence($resource, $records)
: false;
}


/**
* @param string $module_name
* @param string $mod_controller
* @return void
* @throws Exception
*/
private function requireController(string $module_name, string $mod_controller): void {

$location = $this->getModuleLocation($module_name); //определяем местоположение модуля
$controller_path = $location . "/" . $mod_controller . ".php";

if ( ! file_exists($controller_path)) {
throw new Exception(sprintf($this->translate->tr("Модуль не найден: %s"), $mod_controller), 400);
}

$autoload = $location . "/vendor/autoload.php";

if (file_exists($autoload)) { //подключаем автозагрузку если есть
require_once $autoload;
}

require_once $controller_path; // подлючаем контроллер

if ( ! class_exists($mod_controller)) {
throw new RuntimeException(sprintf($this->translate->tr("Модуль сломан: %s"), $location));
}
}
}
16 changes: 16 additions & 0 deletions inc/Interfaces/Queue.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<?php
declare(strict_types=1);
namespace Core2;
/**
* Очереди Redis с гарантированной доставкой
*/
interface Queue
{

public function push(array $payload, string $queue = 'default'): bool;
public function pop(string $queue = 'default', int $timeout = 0): ?array;
public function acknowledge(string $queue, string $messageId): bool;
public function reject(string $queue, string $messageId, bool $requeue = false): bool;
public function getPendingCount(string $queue): int;
public function getQueueSize(string $queue): int;
}
16 changes: 16 additions & 0 deletions inc/Interfaces/Sequence.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<?php


/**
*
*/
interface Sequence {

/**
* @param string $resource_name
* @param array $records
* Если возвращено true, то будет считаться, что вы самостоятельно сортировали объекты
* Если возвращено false будет считаться, что нужно применить стандартную процедуру сортировки
*/
public function sequence(string $resource_name, array $records): bool;
}
2 changes: 2 additions & 0 deletions inc/Traits/Import.php
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,8 @@ public function getFieldImport(array $options, array $data): string {

if ($options['rows_select'] == 'checked') {
$tpl->row->checked_row->assign('[ROW_NUMBER]', $num);
} else {
$tpl->row->start_row->assign('[ROW_NUMBER]', $num);
}

$col = 0;
Expand Down
17 changes: 13 additions & 4 deletions inc/WorkerManager.php
Original file line number Diff line number Diff line change
Expand Up @@ -731,16 +731,24 @@ protected function load_workers() {
}
if (isset($this->functions['Workhorse'])) {
$db = new Db();
$mods = $db->dataModules->getModuleList();
$select = $db->dataModules->select()->where("visible='Y'");
$mods = $db->dataModules->fetchAll($select);

foreach ($mods as $k => $data) {
$location = $this->config['doc_root'] . "/mod/{$data['module_id']}/v{$data['version']}";
if ($data['is_system'] === "Y") {
$location = __DIR__ . "/../mod/{$data['module_id']}/v{$data['version']}";
} else {
$location = $this->config['doc_root'] . "/mod/{$data['module_id']}/v{$data['version']}";
}
// $location = $this->config['doc_root'] . "/mod/{$data['module_id']}/v{$data['version']}";
$name = "Mod" . ucfirst(strtolower($data['module_id'])) . "Worker";
if (!isset($this->functions[$name])) {
$worker = $location . "/{$name}.php";
$worker = realpath($location) . "/{$name}.php";
if (file_exists($worker)) {
$co = !empty($this->functions[$name]["count"]) ? $this->functions[$name]["count"] : 2;
$this->functions[$name] = [
'name' => 'Workhorse',
'count' => 2,
'count' => $co,
'path' => $worker,
'mod' => $data['module_id'],
'path_workhorse' => $this->functions['Workhorse']['path'],
Expand All @@ -751,6 +759,7 @@ protected function load_workers() {
}
unset($this->functions['Workhorse']);
}

// echo "<PRE>";print_r($this->config);echo "</PRE>";//die;
// echo "<PRE>";print_r($this->functions);echo "</PRE>";die;
}
Expand Down
15 changes: 6 additions & 9 deletions inc/classes/Emitter.php
Original file line number Diff line number Diff line change
Expand Up @@ -64,15 +64,12 @@ public function sync($module, $event_name, $data): array {

public function async($module, $event_name, $data): void {

foreach ($this->subscribers as $mod => $controller) {
$w = $this->workerAdmin->doBackground('Eventer', [
'mod' => $mod,
'location' => $this->getModuleLocation($mod),
'context' => $module,
'event' => $event_name,
'data' => $data
]);
}
$w = $this->workerAdmin->doBackground('Eventer', [
'context' => $module,
'event' => $event_name,
'data' => $data
]);

// $this->log->info(is_array($data) ? json_encode($data) : $data, ['module' => $module, 'event' => $event_name]);
}

Expand Down
4 changes: 3 additions & 1 deletion inc/classes/Error.php
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,9 @@ public static function catchException(\Exception $exception): void {
self::Exception('Нет такой страницы', 404);

} elseif ($message == 'expired') {
setcookie($cnf->session->name, false);
if ($cnf && $cnf?->session?->name) {
setcookie($cnf->session->name, false);
}
header("{$_SERVER['SERVER_PROTOCOL']} 403 Forbidden");
die();
}
Expand Down
17 changes: 13 additions & 4 deletions inc/classes/File.php
Original file line number Diff line number Diff line change
Expand Up @@ -159,13 +159,12 @@ public function getContent($table, $id)
* @throws \Exception
*/
public function handleThumb($table, $id) {
$this->getFileData($table, $id);
$this->getThumb($table, $id);
$res2 = $this->data;

header("Content-type: {$res2['type']}");
header("Content-Disposition: filename=\"{$res2['filename']}\"");


if ( ! empty($res2['hash'])) {
$etagHeader = (isset($_SERVER['HTTP_IF_NONE_MATCH']) ? trim($_SERVER['HTTP_IF_NONE_MATCH']) : false);

Expand All @@ -175,23 +174,33 @@ public function handleThumb($table, $id) {
//check if page has changed. If not, send 304 and exit
if ($etagHeader == $res2['hash']) {
header("HTTP/1.1 304 Not Modified");
return '';
}
}

}

/**
* @param string $table
* @param int $id
* @return string
*/
public function getThumb(string $table, int $id): string
{
$this->getFileData($table, $id);
$quote_table_files = $this->db->quoteIdentifier($table . '_files');
$thumb = $this->db->fetchOne("SELECT `thumb` FROM {$quote_table_files} WHERE id = ?", $id);
//Если задан размер тамбнейла или если тамбнейла нет в базе
if (!empty($_GET['size']) || !$thumb) {
$content = $this->getContent($table, $id);
ob_start();
$image = new Image();
$image->outStringResized($content, $res2['type'], $this->imgWidth, $this->imgHeight);
$image->outStringResized($content, $this->data['type'], $this->imgWidth, $this->imgHeight);
$this->content = ob_get_clean();

} else {
$this->content = $thumb;
}
return $this->content;
}


Expand Down
Loading