diff --git a/.claude/memory/MEMORY.md b/.claude/memory/MEMORY.md index 4f98e22..b902597 100644 --- a/.claude/memory/MEMORY.md +++ b/.claude/memory/MEMORY.md @@ -14,3 +14,4 @@ - [Roadmap: слой БД](roadmap.md) — MariaDB/PDO реализован и протестирован (реальное подключение); Mongo/Elasticsearch написаны, ждут внешней инфры от пользователя - [Работа с референс-кодом из других проектов](feedback_reference_code_handling.md) — пользователь кидает код из eoffice_v3 и т.п.: адаптировать критически, не копировать вслепую, но уважать явные повторные сигналы по неймингу - [Идея: CLI-точка входа](idea_cli_entrypoint.md) — отложенный cli.php для миграций/крона +- [Никакого inline JS в HTML](feedback_no_inline_js.md) — только jQuery `.on()` в отдельном .js-файле, даже для мелочи вроде onchange diff --git a/.claude/memory/feedback_no_inline_js.md b/.claude/memory/feedback_no_inline_js.md new file mode 100644 index 0000000..d0e2589 --- /dev/null +++ b/.claude/memory/feedback_no_inline_js.md @@ -0,0 +1,22 @@ +--- +name: feedback-no-inline-js +description: Никогда не писать inline JS-атрибуты (onchange/onclick/...) в HTML — только jQuery через .on() +metadata: + type: feedback +--- + +Пользователь прямо запретил inline JS-обработчики в HTML (`onchange="this.form.submit()"` и подобные) — +даже для мелочи вроде автосабмита ` diff --git a/CLAUDE.md b/CLAUDE.md index 529463d..8a0189a 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -495,6 +495,8 @@ Log::error('Сбой оплаты'); Log::debug(...); Log::warning(...); ``` +- Вкл/выкл целиком — `Log::$enabled` → `Config::get('log','enabled')` → `true` по умолчанию; + `false` — `write()` ничего не пишет ни в один файл, независимо от уровня/порога. - Уровни: `debug(100) < info(200) < warning(300) < error(400)`. Пишутся только уровни не ниже порога `Log::$threshold` (по умолчанию из `Config::get('log','threshold')` → `debug`). - Канал по уровню: `info`/`debug` → файл `action-…`, `warning`/`error` → файл `error-…`. @@ -620,7 +622,7 @@ PHPUnit 11 в Docker-контейнере `bicycle`. Bootstrap: `tests/bootstrap | `tests/Unit/CSRFTest.php` | `token()` стабильность/формат, `validate()`, `field()`, нет токена в сессии | | `tests/Unit/ControllerTest.php` | `executeAction()`, порядок `before/action/after`, no-op хуки | | `tests/Unit/ControllerAfterTest.php` | `after()` веб-`Controller`: заголовок `X-Profiler` только для admin, содержит SQL-сводку — через `xdebug_get_headers()`, иначе `markTestSkipped()` | -| `tests/Unit/LogTest.php` | уровни/порог, каналы (action/error), формат, append, `strtr`, маскировка, `requestInfo()` | +| `tests/Unit/LogTest.php` | уровни/порог, `enabled` (выкл — ничего не пишет), каналы (action/error), формат, append, `strtr`, маскировка, `requestInfo()` | | `tests/Unit/FileLogReaderTest.php` | парсинг, фильтры (level/q/channel), newest-first, missing file, `dates()` | | `tests/Unit/FileAuthDriverTest.php` | `login()` верно/неверно/неизвестный логин, `loggedIn()`, `getUser()` без пароля, `logout()`, `checkPassword()`, отсутствие файла | | `tests/Unit/AuthTest.php` | `instance()` драйвер по умолчанию/явный, кэширование по драйверу, неизвестный драйвер → исключение | @@ -636,7 +638,7 @@ PHPUnit 11 в Docker-контейнере `bicycle`. Bootstrap: `tests/bootstrap | `tests/Unit/MongoDriverTest.php` | `instance()`, `collection()`, `database()` — требует `ext-mongodb`, иначе `markTestSkipped()` | | `tests/Unit/ProfilerToolbarTest.php` | `render()` пусто для гостя/не-admin, панель с временем/памятью/SQL для admin, вкладки Vars/Files/Route, маскировка чувствительных ключей, вкладка Custom только при `addData()`, `EXPLAIN` для реального SELECT / пропуск для не-SELECT | -**247 тестов, 384 assertion — все проходят (8 skipped: Elasticsearch/Mongo без живой инфраструктуры — MariaDB подключена и все её тесты реально проходят, см. разделы DataBase/Elasticsearch/Mongo выше).** +**248 тестов, 386 assertion — все проходят (8 skipped: Elasticsearch/Mongo без живой инфраструктуры — MariaDB подключена и все её тесты реально проходят, см. разделы DataBase/Elasticsearch/Mongo выше).** ### Frontend dependencies (через Composer) diff --git a/System/Classes/Log.php b/System/Classes/Log.php index a37ba5f..9568666 100644 --- a/System/Classes/Log.php +++ b/System/Classes/Log.php @@ -40,6 +40,9 @@ class Log self::ERROR => 'error', ]; + /** @var bool|null Писать ли логи вообще; null → Config('log','enabled') → true */ + public static ?bool $enabled = null; + /** @var string|null Каталог логов; null → Config('log','path') → APPPATH/logs */ public static ?string $directory = null; @@ -59,6 +62,10 @@ class Log */ public static function write(string $level, string $message, array $variables = []): void { + if (!self::enabled()) { + return; + } + if (self::weight($level) < self::weight(self::threshold())) { return; } @@ -68,8 +75,8 @@ class Log } $dir = self::directory(); - if (!is_dir($dir)) { - mkdir($dir, 0775, true); + if (!is_dir($dir) && !mkdir($dir, 0775, true) && !is_dir($dir)) { + return; } $line = sprintf("[%s] %s: %s\n", date('Y-m-d H:i:s'), strtoupper($level), $message); @@ -143,6 +150,16 @@ class Log ); } + /** + * Писать ли логи вообще (из свойства, конфига или дефолт true). + * + * @return bool + */ + private static function enabled(): bool + { + return self::$enabled ?? Config::get('log', 'enabled') ?? true; + } + /** * Минимальный уровень записи (из свойства, конфига или дефолт debug). * diff --git a/tests/Unit/ControllerAfterTest.php b/tests/Unit/ControllerAfterTest.php index f4d0f61..c0dc91c 100644 --- a/tests/Unit/ControllerAfterTest.php +++ b/tests/Unit/ControllerAfterTest.php @@ -32,7 +32,7 @@ class ControllerAfterTest extends TestCase header_remove(); - $this->users_file = sys_get_temp_dir() . '/bicycle_after_users_' . uniqid() . '.php'; + $this->users_file = sys_get_temp_dir() . '/bicycle_after_users_' . uniqid('', true) . '.php'; file_put_contents($this->users_file, ' ['password' => password_hash('x', PASSWORD_DEFAULT), 'role' => 'admin'], 'bob' => ['password' => password_hash('x', PASSWORD_DEFAULT), 'role' => 'manager'], diff --git a/tests/Unit/FileAuthDriverTest.php b/tests/Unit/FileAuthDriverTest.php index c2ace15..d24b4f1 100644 --- a/tests/Unit/FileAuthDriverTest.php +++ b/tests/Unit/FileAuthDriverTest.php @@ -13,7 +13,7 @@ class FileAuthDriverTest extends TestCase protected function setUp(): void { - $this->file = sys_get_temp_dir() . '/bicycle_auth_users_' . uniqid() . '.php'; + $this->file = sys_get_temp_dir() . '/bicycle_auth_users_' . uniqid('', true) . '.php'; file_put_contents($this->file, ' [ @@ -108,7 +108,7 @@ class FileAuthDriverTest extends TestCase public function testMissingUsersFileBehavesAsEmptyList(): void { - $driver = new FileAuthDriver(sys_get_temp_dir() . '/does_not_exist_' . uniqid() . '.php'); + $driver = new FileAuthDriver(sys_get_temp_dir() . '/does_not_exist_' . uniqid('', true) . '.php'); $this->assertFalse($driver->login('bob', 'secret')); } } diff --git a/tests/Unit/FileLogReaderTest.php b/tests/Unit/FileLogReaderTest.php index 9f96d45..0e32eae 100644 --- a/tests/Unit/FileLogReaderTest.php +++ b/tests/Unit/FileLogReaderTest.php @@ -13,7 +13,7 @@ class FileLogReaderTest extends TestCase protected function setUp(): void { - $this->dir = sys_get_temp_dir() . '/bicycle_reader_' . uniqid(); + $this->dir = sys_get_temp_dir() . '/bicycle_reader_' . uniqid('', true); $this->today = date('Y-m-d'); mkdir($this->dir); Log::$directory = $this->dir; diff --git a/tests/Unit/LogTest.php b/tests/Unit/LogTest.php index ce92059..1975268 100644 --- a/tests/Unit/LogTest.php +++ b/tests/Unit/LogTest.php @@ -11,9 +11,10 @@ class LogTest extends TestCase protected function setUp(): void { - $this->dir = sys_get_temp_dir() . '/bicycle_log_' . uniqid(); + $this->dir = sys_get_temp_dir() . '/bicycle_log_' . uniqid('', true); Log::$directory = $this->dir; Log::$threshold = Log::DEBUG; + Log::$enabled = true; } protected function tearDown(): void @@ -26,6 +27,7 @@ class LogTest extends TestCase } Log::$directory = null; Log::$threshold = null; + Log::$enabled = null; } private function logFile(string $channel): string @@ -64,6 +66,16 @@ class LogTest extends TestCase $this->assertStringContainsString('WARNING: предупреждение', file_get_contents($this->logFile('error'))); } + public function testDisabledSkipsWritingEntirely(): void + { + Log::$enabled = false; + + Log::error('не должно записаться'); + + $this->assertFileDoesNotExist($this->logFile('action')); + $this->assertFileDoesNotExist($this->logFile('error')); + } + public function testThresholdFiltersLowerLevels(): void { Log::$threshold = Log::WARNING; diff --git a/tests/Unit/ProfilerToolbarTest.php b/tests/Unit/ProfilerToolbarTest.php index ae07ab5..f6781cc 100644 --- a/tests/Unit/ProfilerToolbarTest.php +++ b/tests/Unit/ProfilerToolbarTest.php @@ -18,7 +18,7 @@ class ProfilerToolbarTest extends TestCase protected function setUp(): void { - $this->users_file = sys_get_temp_dir() . '/bicycle_toolbar_users_' . uniqid() . '.php'; + $this->users_file = sys_get_temp_dir() . '/bicycle_toolbar_users_' . uniqid('', true) . '.php'; file_put_contents($this->users_file, ' ['password' => password_hash('x', PASSWORD_DEFAULT), 'role' => 'admin'], 'bob' => ['password' => password_hash('x', PASSWORD_DEFAULT), 'role' => 'manager'], diff --git a/tests/Unit/ViewTest.php b/tests/Unit/ViewTest.php index 8eb5f93..423e782 100644 --- a/tests/Unit/ViewTest.php +++ b/tests/Unit/ViewTest.php @@ -12,7 +12,7 @@ class ViewTest extends TestCase protected function setUp(): void { - $this->tmpFile = sys_get_temp_dir() . '/bicycle_view_' . uniqid() . '.html'; + $this->tmpFile = sys_get_temp_dir() . '/bicycle_view_' . uniqid('', true) . '.html'; } protected function tearDown(): void