first commit
This commit is contained in:
11
.claude/settings.local.json
Normal file
11
.claude/settings.local.json
Normal file
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"permissions": {
|
||||
"allow": [
|
||||
"Bash(composer install:*)",
|
||||
"Bash(/c/xampp/php/php.exe -r \"copy\\(''https://getcomposer.org/installer'', ''composer-setup.php''\\);\")",
|
||||
"Bash(/c/xampp/php/php.exe composer-setup.php)",
|
||||
"Bash(/c/xampp/php/php.exe -r \"unlink\\(''composer-setup.php''\\);\")",
|
||||
"Bash(/c/xampp/php/php.exe composer.phar:*)"
|
||||
]
|
||||
}
|
||||
}
|
||||
14
.env
Normal file
14
.env
Normal file
@@ -0,0 +1,14 @@
|
||||
DB_HOST=localhost
|
||||
DB_NAME=host700513_backpro
|
||||
DB_USER=host700513_backpro
|
||||
DB_PASS=Mq9wH2B8KPeQh2wQ32Ya
|
||||
|
||||
OPENAI_API_KEY=sk-...
|
||||
OPENAI_MODEL=gpt-4o
|
||||
|
||||
FREEPIK_API_KEY=
|
||||
UNSPLASH_API_KEY=
|
||||
PEXELS_API_KEY=
|
||||
|
||||
APP_URL=https://backpro.projectpro.pl
|
||||
APP_SECRET=bP7x9kR3mW2vN5qT8sY1
|
||||
14
.env.example
Normal file
14
.env.example
Normal file
@@ -0,0 +1,14 @@
|
||||
DB_HOST=localhost
|
||||
DB_NAME=backpro
|
||||
DB_USER=root
|
||||
DB_PASS=
|
||||
|
||||
OPENAI_API_KEY=sk-...
|
||||
OPENAI_MODEL=gpt-4o
|
||||
|
||||
FREEPIK_API_KEY=
|
||||
UNSPLASH_API_KEY=
|
||||
PEXELS_API_KEY=
|
||||
|
||||
APP_URL=https://backpro.projectpro.pl
|
||||
APP_SECRET=change-this-to-random-string
|
||||
24
.htaccess
Normal file
24
.htaccess
Normal file
@@ -0,0 +1,24 @@
|
||||
RewriteEngine On
|
||||
|
||||
# Force HTTPS
|
||||
RewriteCond %{HTTPS} off
|
||||
RewriteRule ^(.*)$ https://%{HTTP_HOST}%{REQUEST_URI} [L,R=301]
|
||||
|
||||
# Block access to sensitive files and directories
|
||||
RewriteRule ^\.env$ - [F,L]
|
||||
RewriteRule ^composer\.(json|lock)$ - [F,L]
|
||||
RewriteRule ^src/ - [F,L]
|
||||
RewriteRule ^templates/ - [F,L]
|
||||
RewriteRule ^config/ - [F,L]
|
||||
RewriteRule ^cron/ - [F,L]
|
||||
RewriteRule ^storage/ - [F,L]
|
||||
RewriteRule ^migrations/ - [F,L]
|
||||
RewriteRule ^docs/ - [F,L]
|
||||
RewriteRule ^vendor/ - [F,L]
|
||||
|
||||
# Allow direct access to existing files and directories (assets, etc.)
|
||||
RewriteCond %{REQUEST_FILENAME} !-f
|
||||
RewriteCond %{REQUEST_FILENAME} !-d
|
||||
|
||||
# Route everything else through index.php
|
||||
RewriteRule ^(.*)$ index.php [QSA,L]
|
||||
17
.vscode/ftp-kr.json
vendored
Normal file
17
.vscode/ftp-kr.json
vendored
Normal file
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"host": "host700513.hostido.net.pl",
|
||||
"username": "www@backpro.projectpro.pl",
|
||||
"password": "WGnT4LEn6dLYKvDkXZdd",
|
||||
"remotePath": "/public_html",
|
||||
"protocol": "ftp",
|
||||
"port": 21,
|
||||
"fileNameEncoding": "utf8",
|
||||
"autoUpload": true,
|
||||
"autoDelete": false,
|
||||
"autoDownload": false,
|
||||
"ignoreRemoteModification": true,
|
||||
"ignore": [
|
||||
".git",
|
||||
"/.vscode"
|
||||
]
|
||||
}
|
||||
12
.vscode/sftp.json
vendored
Normal file
12
.vscode/sftp.json
vendored
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"name": "host700513.hostido.net.pl",
|
||||
"host": "host700513.hostido.net.pl",
|
||||
"protocol": "ftp",
|
||||
"port": 21,
|
||||
"username": "www@backpro.projectpro.pl",
|
||||
"password": "WGnT4LEn6dLYKvDkXZdd",
|
||||
"remotePath": "/public_html",
|
||||
"uploadOnSave": false,
|
||||
"useTempFile": false,
|
||||
"openSsh": false
|
||||
}
|
||||
60
assets/css/app.css
Normal file
60
assets/css/app.css
Normal file
@@ -0,0 +1,60 @@
|
||||
/* BackPRO - Custom Styles */
|
||||
|
||||
body {
|
||||
background-color: #f4f6f9;
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
||||
}
|
||||
|
||||
.sidebar {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
height: 100vh;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.sidebar .nav-link {
|
||||
padding: 0.6rem 1rem;
|
||||
border-radius: 0.375rem;
|
||||
margin: 0.1rem 0;
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
|
||||
.sidebar .nav-link:hover {
|
||||
background-color: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.sidebar .nav-link.active {
|
||||
background-color: rgba(255, 255, 255, 0.15);
|
||||
}
|
||||
|
||||
.card {
|
||||
border: none;
|
||||
box-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075);
|
||||
}
|
||||
|
||||
.table th {
|
||||
font-weight: 600;
|
||||
font-size: 0.85rem;
|
||||
text-transform: uppercase;
|
||||
color: #6c757d;
|
||||
border-bottom-width: 1px;
|
||||
}
|
||||
|
||||
.article-content h2 {
|
||||
font-size: 1.4rem;
|
||||
margin-top: 1.5rem;
|
||||
}
|
||||
|
||||
.article-content h3 {
|
||||
font-size: 1.2rem;
|
||||
margin-top: 1.2rem;
|
||||
}
|
||||
|
||||
.article-content p {
|
||||
line-height: 1.7;
|
||||
}
|
||||
|
||||
.btn-group .btn + form .btn {
|
||||
border-top-left-radius: 0;
|
||||
border-bottom-left-radius: 0;
|
||||
}
|
||||
76
assets/js/app.js
Normal file
76
assets/js/app.js
Normal file
@@ -0,0 +1,76 @@
|
||||
// BackPRO - Frontend Scripts
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
|
||||
// Test connection buttons
|
||||
document.querySelectorAll('.btn-test-connection').forEach(function (btn) {
|
||||
btn.addEventListener('click', function () {
|
||||
var siteId = this.dataset.siteId;
|
||||
var button = this;
|
||||
var originalHtml = button.innerHTML;
|
||||
|
||||
button.innerHTML = '<span class="spinner-border spinner-border-sm"></span>';
|
||||
button.disabled = true;
|
||||
|
||||
fetch('/sites/' + siteId + '/test', { method: 'POST' })
|
||||
.then(function (r) { return r.json(); })
|
||||
.then(function (data) {
|
||||
if (data.success) {
|
||||
button.innerHTML = '<i class="bi bi-check-lg"></i>';
|
||||
button.classList.remove('btn-outline-success');
|
||||
button.classList.add('btn-success');
|
||||
} else {
|
||||
button.innerHTML = '<i class="bi bi-x-lg"></i>';
|
||||
button.classList.remove('btn-outline-success');
|
||||
button.classList.add('btn-danger');
|
||||
alert('Błąd połączenia: ' + data.message);
|
||||
}
|
||||
})
|
||||
.catch(function () {
|
||||
button.innerHTML = '<i class="bi bi-x-lg"></i>';
|
||||
button.classList.add('btn-danger');
|
||||
alert('Błąd sieci');
|
||||
})
|
||||
.finally(function () {
|
||||
button.disabled = false;
|
||||
setTimeout(function () {
|
||||
button.innerHTML = originalHtml;
|
||||
button.className = button.className.replace('btn-success', 'btn-outline-success').replace('btn-danger', 'btn-outline-success');
|
||||
}, 3000);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// Topic edit buttons
|
||||
document.querySelectorAll('.btn-edit-topic').forEach(function (btn) {
|
||||
btn.addEventListener('click', function () {
|
||||
var id = this.dataset.id;
|
||||
var form = document.getElementById('topicForm');
|
||||
var title = document.getElementById('topicFormTitle');
|
||||
var submit = document.getElementById('topicFormSubmit');
|
||||
|
||||
form.action = '/topics/' + id + '/update';
|
||||
title.textContent = 'Edytuj temat';
|
||||
submit.textContent = 'Zapisz zmiany';
|
||||
|
||||
document.getElementById('topic_name').value = this.dataset.name;
|
||||
document.getElementById('topic_description').value = this.dataset.description;
|
||||
document.getElementById('topic_wp_category').value = this.dataset.wpCategory || '';
|
||||
document.getElementById('topic_is_active').checked = this.dataset.active === '1';
|
||||
|
||||
var globalSelect = document.getElementById('topic_global_id');
|
||||
if (globalSelect) {
|
||||
globalSelect.value = this.dataset.globalTopic || '';
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Highlight active sidebar link
|
||||
var currentPath = window.location.pathname;
|
||||
document.querySelectorAll('.sidebar .nav-link').forEach(function (link) {
|
||||
var href = link.getAttribute('href');
|
||||
if (currentPath === href || (href !== '/' && currentPath.startsWith(href))) {
|
||||
link.classList.add('active');
|
||||
}
|
||||
});
|
||||
});
|
||||
15
composer.json
Normal file
15
composer.json
Normal file
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"name": "backpro/seo-manager",
|
||||
"description": "BackPRO - System Zarządzania Zapleczem SEO",
|
||||
"type": "project",
|
||||
"require": {
|
||||
"php": ">=8.1",
|
||||
"guzzlehttp/guzzle": "^7.0",
|
||||
"vlucas/phpdotenv": "^5.6"
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"App\\": "src/"
|
||||
}
|
||||
}
|
||||
}
|
||||
1090
composer.lock
generated
Normal file
1090
composer.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
51
config/routes.php
Normal file
51
config/routes.php
Normal file
@@ -0,0 +1,51 @@
|
||||
<?php
|
||||
|
||||
/** @var \App\Core\Router $router */
|
||||
|
||||
// Auth
|
||||
$router->get('/login', 'AuthController', 'loginForm');
|
||||
$router->post('/login', 'AuthController', 'login');
|
||||
$router->get('/logout', 'AuthController', 'logout');
|
||||
$router->get('/change-password', 'AuthController', 'changePasswordForm');
|
||||
$router->post('/change-password', 'AuthController', 'changePassword');
|
||||
|
||||
// Dashboard
|
||||
$router->get('/', 'DashboardController', 'index');
|
||||
|
||||
// Global Topics (library)
|
||||
$router->get('/global-topics', 'GlobalTopicController', 'index');
|
||||
$router->post('/global-topics/categories', 'GlobalTopicController', 'storeCategory');
|
||||
$router->post('/global-topics/{id}/subtopics', 'GlobalTopicController', 'storeSubtopic');
|
||||
$router->post('/global-topics/{id}/update', 'GlobalTopicController', 'update');
|
||||
$router->post('/global-topics/{id}/delete', 'GlobalTopicController', 'destroy');
|
||||
|
||||
// Sites
|
||||
$router->get('/sites', 'SiteController', 'index');
|
||||
$router->get('/sites/create', 'SiteController', 'create');
|
||||
$router->post('/sites', 'SiteController', 'store');
|
||||
$router->get('/sites/{id}/edit', 'SiteController', 'edit');
|
||||
$router->post('/sites/{id}', 'SiteController', 'update');
|
||||
$router->post('/sites/{id}/delete', 'SiteController', 'destroy');
|
||||
$router->post('/sites/{id}/test', 'SiteController', 'testConnection');
|
||||
|
||||
// Topics
|
||||
$router->get('/sites/{id}/topics', 'TopicController', 'index');
|
||||
$router->post('/sites/{id}/topics', 'TopicController', 'store');
|
||||
$router->post('/topics/{id}/update', 'TopicController', 'update');
|
||||
$router->post('/topics/{id}/delete', 'TopicController', 'destroy');
|
||||
|
||||
// Categories (sync from WordPress)
|
||||
$router->get('/sites/{id}/categories', 'CategoryController', 'index');
|
||||
$router->post('/sites/{id}/categories/sync', 'CategoryController', 'sync');
|
||||
|
||||
// Articles
|
||||
$router->get('/articles', 'ArticleController', 'index');
|
||||
$router->get('/articles/{id}', 'ArticleController', 'show');
|
||||
|
||||
// Publish (manual trigger)
|
||||
$router->post('/publish/run', 'PublishController', 'run');
|
||||
$router->post('/publish/site/{id}', 'PublishController', 'runForSite');
|
||||
|
||||
// Settings
|
||||
$router->get('/settings', 'SettingsController', 'index');
|
||||
$router->post('/settings', 'SettingsController', 'update');
|
||||
42
cron/publish.php
Normal file
42
cron/publish.php
Normal file
@@ -0,0 +1,42 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
// Lock file to prevent concurrent execution
|
||||
$basePath = dirname(__DIR__);
|
||||
$lockFile = $basePath . '/storage/logs/publish.lock';
|
||||
|
||||
if (file_exists($lockFile)) {
|
||||
$lockTime = filemtime($lockFile);
|
||||
// If lock is older than 30 minutes, assume stale and remove
|
||||
if (time() - $lockTime < 1800) {
|
||||
echo "Another publish process is running. Exiting.\n";
|
||||
exit(0);
|
||||
}
|
||||
unlink($lockFile);
|
||||
}
|
||||
|
||||
file_put_contents($lockFile, date('Y-m-d H:i:s'));
|
||||
|
||||
try {
|
||||
require_once $basePath . '/vendor/autoload.php';
|
||||
|
||||
\App\Core\Config::load($basePath);
|
||||
\App\Helpers\Logger::setBasePath($basePath);
|
||||
|
||||
$publisher = new \App\Services\PublisherService();
|
||||
$result = $publisher->publishNext();
|
||||
|
||||
echo $result['message'] . "\n";
|
||||
} catch (\Throwable $e) {
|
||||
$message = "CRON Error: " . $e->getMessage();
|
||||
echo $message . "\n";
|
||||
|
||||
if (class_exists(\App\Helpers\Logger::class)) {
|
||||
\App\Helpers\Logger::error($message, 'publish');
|
||||
}
|
||||
} finally {
|
||||
if (file_exists($lockFile)) {
|
||||
unlink($lockFile);
|
||||
}
|
||||
}
|
||||
194
docs/API.md
Normal file
194
docs/API.md
Normal file
@@ -0,0 +1,194 @@
|
||||
# BackPRO - Integracje API
|
||||
|
||||
## 1. OpenAI API (Generowanie artykułów)
|
||||
|
||||
### Endpoint
|
||||
`POST https://api.openai.com/v1/chat/completions`
|
||||
|
||||
### Konfiguracja
|
||||
- **Model:** konfigurowalny w ustawieniach (domyślnie `gpt-4o`)
|
||||
- **Klucz API:** przechowywany w tabeli `settings`
|
||||
|
||||
### Struktura zapytania
|
||||
|
||||
```json
|
||||
{
|
||||
"model": "gpt-4o",
|
||||
"messages": [
|
||||
{
|
||||
"role": "system",
|
||||
"content": "Jesteś doświadczonym copywriterem SEO. Pisz artykuły w języku polskim, optymalizowane pod SEO. Artykuł powinien mieć {min_words}-{max_words} słów, zawierać nagłówki H2 i H3, być angażujący i merytoryczny. Formatuj treść w HTML (bez tagów <html>, <body>, <head>). Zwróć odpowiedź w formacie JSON: {\"title\": \"tytuł\", \"content\": \"treść HTML\"}"
|
||||
},
|
||||
{
|
||||
"role": "user",
|
||||
"content": "Napisz artykuł na temat: {topic_name}\nWytyczne: {topic_description}\n\nWAŻNE - NIE pisz o następujących tematach, bo artykuły o nich już istnieją na stronie:\n{lista_istniejących_tytułów}"
|
||||
}
|
||||
],
|
||||
"temperature": 0.8,
|
||||
"max_tokens": 4000,
|
||||
"response_format": { "type": "json_object" }
|
||||
}
|
||||
```
|
||||
|
||||
### Obsługa odpowiedzi
|
||||
1. Parsowanie JSON z odpowiedzi
|
||||
2. Walidacja - czy zawiera `title` i `content`
|
||||
3. Sanityzacja HTML treści
|
||||
4. Zapis do tabeli `articles`
|
||||
|
||||
### Obsługa błędów
|
||||
- Rate limiting (429) → retry z exponential backoff
|
||||
- Timeout → zapis status=failed z error_message
|
||||
- Nieprawidłowy JSON → próba ekstrakcji treści z raw response
|
||||
|
||||
### Klasa: `OpenAIService`
|
||||
```php
|
||||
class OpenAIService {
|
||||
public function generateArticle(
|
||||
string $topicName,
|
||||
string $topicDescription,
|
||||
array $existingTitles
|
||||
): array // ['title' => '', 'content' => '']
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. WordPress REST API (Publikacja)
|
||||
|
||||
### Wymagania po stronie WordPressa
|
||||
- WordPress 4.7+ (REST API wbudowane)
|
||||
- Utworzony użytkownik z rolą **Author** lub **Editor**
|
||||
- Wygenerowane **Application Password** (WP 5.6+: Użytkownicy → Profil → Application Passwords)
|
||||
|
||||
### Autentykacja
|
||||
HTTP Basic Auth z Application Password:
|
||||
```
|
||||
Authorization: Basic base64(username:application_password)
|
||||
```
|
||||
|
||||
### Używane endpointy
|
||||
|
||||
#### Test połączenia
|
||||
```
|
||||
GET {site_url}/wp-json/wp/v2/posts?per_page=1
|
||||
```
|
||||
|
||||
#### Pobieranie kategorii
|
||||
```
|
||||
GET {site_url}/wp-json/wp/v2/categories?per_page=100
|
||||
```
|
||||
|
||||
#### Upload obrazka (media)
|
||||
```
|
||||
POST {site_url}/wp-json/wp/v2/media
|
||||
Content-Type: image/jpeg
|
||||
Content-Disposition: attachment; filename="article-image.jpg"
|
||||
Body: [binary image data]
|
||||
```
|
||||
Zwraca `id` mediów do użycia jako `featured_media`.
|
||||
|
||||
#### Tworzenie posta
|
||||
```json
|
||||
POST {site_url}/wp-json/wp/v2/posts
|
||||
{
|
||||
"title": "Tytuł artykułu",
|
||||
"content": "<p>Treść HTML...</p>",
|
||||
"status": "publish",
|
||||
"categories": [15],
|
||||
"featured_media": 42
|
||||
}
|
||||
```
|
||||
|
||||
### Klasa: `WordPressService`
|
||||
```php
|
||||
class WordPressService {
|
||||
public function testConnection(Site $site): bool
|
||||
public function getCategories(Site $site): array
|
||||
public function uploadMedia(Site $site, string $imageData, string $filename): int
|
||||
public function createPost(
|
||||
Site $site,
|
||||
string $title,
|
||||
string $content,
|
||||
?int $categoryId = null,
|
||||
?int $mediaId = null
|
||||
): int // zwraca wp_post_id
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. Freepik API (Obrazki)
|
||||
|
||||
### AI Image Generation
|
||||
`POST https://api.freepik.com/v1/ai/text-to-image`
|
||||
|
||||
```json
|
||||
{
|
||||
"prompt": "Professional blog header image about {topic}: {article_title}",
|
||||
"negative_prompt": "text, watermark, logo",
|
||||
"image": {
|
||||
"size": "landscape_16_9"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Headers:**
|
||||
```
|
||||
x-freepik-api-key: {api_key}
|
||||
Content-Type: application/json
|
||||
```
|
||||
|
||||
### Alternatywy (fallback)
|
||||
|
||||
#### Unsplash API (darmowe stocki)
|
||||
```
|
||||
GET https://api.unsplash.com/search/photos?query={topic_keywords}&orientation=landscape
|
||||
Authorization: Client-ID {access_key}
|
||||
```
|
||||
|
||||
#### Pexels API (darmowe stocki)
|
||||
```
|
||||
GET https://api.pexels.com/v1/search?query={topic_keywords}&orientation=landscape&per_page=1
|
||||
Authorization: {api_key}
|
||||
```
|
||||
|
||||
### Klasa: `ImageService`
|
||||
```php
|
||||
class ImageService {
|
||||
public function generate(string $articleTitle, string $topicName): array
|
||||
// Zwraca ['data' => binary, 'filename' => 'image.jpg', 'mime' => 'image/jpeg']
|
||||
|
||||
// Wewnętrznie wybiera provider na podstawie ustawienia 'image_provider':
|
||||
private function generateFreepik(string $prompt): array
|
||||
private function searchUnsplash(string $query): array
|
||||
private function searchPexels(string $query): array
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. Konfiguracja API w panelu
|
||||
|
||||
Ustawienia przechowywane w tabeli `settings`:
|
||||
|
||||
| Klucz | Opis | Przykład |
|
||||
|-------|------|---------|
|
||||
| openai_api_key | Klucz API OpenAI | sk-proj-... |
|
||||
| openai_model | Model do artykułów | gpt-4o |
|
||||
| freepik_api_key | Klucz API Freepik | fpk-... |
|
||||
| unsplash_api_key | Klucz API Unsplash (opcjonalnie) | ... |
|
||||
| pexels_api_key | Klucz API Pexels (opcjonalnie) | ... |
|
||||
| image_provider | Aktywny provider obrazków | freepik / unsplash / pexels |
|
||||
| article_min_words | Min. słów w artykule | 800 |
|
||||
| article_max_words | Max. słów w artykule | 1200 |
|
||||
|
||||
## 5. Limity i koszty (orientacyjne)
|
||||
|
||||
| API | Limit | Koszt |
|
||||
|-----|-------|-------|
|
||||
| OpenAI GPT-4o | Brak limitu (pay-per-use) | ~$0.01-0.03 / artykuł |
|
||||
| OpenAI GPT-4o-mini | Brak limitu | ~$0.001-0.003 / artykuł |
|
||||
| Freepik AI | Zależy od planu | Od darmowego (ograniczony) |
|
||||
| Unsplash | 50 req/h (demo) | Darmowe |
|
||||
| Pexels | 200 req/h | Darmowe |
|
||||
118
docs/CRON.md
Normal file
118
docs/CRON.md
Normal file
@@ -0,0 +1,118 @@
|
||||
# BackPRO - Konfiguracja CRON
|
||||
|
||||
## Opis
|
||||
Skrypt `cron/publish.php` jest uruchamiany automatycznie co godzinę przez CRON serwera. Odpowiada za sprawdzenie, czy któraś ze stron wymaga nowej publikacji, i jeśli tak - generuje artykuł z obrazkiem i publikuje go na WordPressie.
|
||||
|
||||
## Algorytm działania
|
||||
|
||||
```
|
||||
1. Załaduj konfigurację (.env) i autoloader (vendor/autoload.php)
|
||||
2. Pobierz wszystkie aktywne strony (is_active = 1)
|
||||
3. Filtruj: zostaw strony, gdzie:
|
||||
- last_published_at IS NULL (nigdy nie publikowano)
|
||||
- LUB NOW() - last_published_at >= publish_interval_days dni
|
||||
4. Sortuj po last_published_at ASC (NULL first = najwyższy priorytet)
|
||||
5. Weź PIERWSZĄ stronę z listy (tylko jedna publikacja na uruchomienie)
|
||||
6. TopicBalancer → wybierz temat z najmniejszą liczbą artykułów
|
||||
7. Pobierz ostatnie 20 tytułów artykułów z tego tematu
|
||||
8. OpenAIService → wygeneruj artykuł (prompt zawiera istniejące tytuły)
|
||||
9. ImageService → wygeneruj/pobierz obrazek
|
||||
10. WordPressService → upload obrazka jako media
|
||||
11. WordPressService → utwórz post z featured image i kategorią
|
||||
12. Zapisz artykuł w bazie (tabela articles, status=published)
|
||||
13. Zaktualizuj topics.article_count (+1)
|
||||
14. Zaktualizuj sites.last_published_at = NOW()
|
||||
15. Zaloguj wynik do storage/logs/publish_YYYY-MM-DD.log
|
||||
```
|
||||
|
||||
**Ważne:** Skrypt publikuje MAKSYMALNIE 1 artykuł na uruchomienie. Dzięki temu:
|
||||
- Nie obciążamy API zbyt wieloma zapytaniami naraz
|
||||
- Mamy lepszą kontrolę nad tempem publikacji
|
||||
- W razie błędu tracimy tylko 1 artykuł, nie całą partię
|
||||
|
||||
## Konfiguracja CRON na Hostido
|
||||
|
||||
### Panel DirectAdmin / cPanel
|
||||
W panelu hostingu dodaj zadanie CRON:
|
||||
|
||||
```
|
||||
# Co godzinę (minuta 0 każdej godziny)
|
||||
0 * * * * /usr/bin/php /home/username/public_html/cron/publish.php >> /home/username/public_html/storage/logs/cron.log 2>&1
|
||||
```
|
||||
|
||||
Lub jeśli dostępny jest `php` w PATH:
|
||||
```
|
||||
0 * * * * php /home/username/public_html/cron/publish.php >> /home/username/public_html/storage/logs/cron.log 2>&1
|
||||
```
|
||||
|
||||
### Alternatywne interwały
|
||||
```
|
||||
# Co 2 godziny
|
||||
0 */2 * * * php /path/to/cron/publish.php
|
||||
|
||||
# Co 6 godzin
|
||||
0 */6 * * * php /path/to/cron/publish.php
|
||||
|
||||
# Raz dziennie o 8:00
|
||||
0 8 * * * php /path/to/cron/publish.php
|
||||
|
||||
# Dwa razy dziennie (8:00 i 20:00)
|
||||
0 8,20 * * * php /path/to/cron/publish.php
|
||||
```
|
||||
|
||||
## Zabezpieczenia
|
||||
|
||||
### Blokada dostępu HTTP
|
||||
Plik `cron/.htaccess`:
|
||||
```apache
|
||||
Deny from all
|
||||
```
|
||||
Skrypty CRON są uruchamiane z linii komend (CLI), nie przez HTTP.
|
||||
|
||||
### Blokada wielokrotnego uruchomienia
|
||||
Skrypt używa pliku lockfile (`storage/logs/publish.lock`):
|
||||
- Na początku sprawdza czy lockfile istnieje
|
||||
- Jeśli tak i nie jest starszy niż 30 minut → kończy działanie (inny proces w toku)
|
||||
- Jeśli nie → tworzy lockfile
|
||||
- Na końcu (lub w razie błędu) → usuwa lockfile
|
||||
|
||||
## Logowanie
|
||||
|
||||
Logi zapisywane w `storage/logs/publish_YYYY-MM-DD.log`:
|
||||
|
||||
```
|
||||
[2026-02-15 10:00:01] INFO: Rozpoczynam publikację
|
||||
[2026-02-15 10:00:01] INFO: Wybrano stronę: example.com (ID: 3)
|
||||
[2026-02-15 10:00:01] INFO: Wybrany temat: Ogrodnictwo (ID: 7)
|
||||
[2026-02-15 10:00:05] INFO: Wygenerowano artykuł: "10 roślin idealnych na balkon"
|
||||
[2026-02-15 10:00:08] INFO: Wygenerowano obrazek (Freepik)
|
||||
[2026-02-15 10:00:10] INFO: Upload mediów na WP: media_id=142
|
||||
[2026-02-15 10:00:12] INFO: Opublikowano post: wp_post_id=523
|
||||
[2026-02-15 10:00:12] INFO: Zakończono pomyślnie
|
||||
```
|
||||
|
||||
## Ręczne uruchomienie
|
||||
|
||||
### Z linii komend
|
||||
```bash
|
||||
php /path/to/cron/publish.php
|
||||
```
|
||||
|
||||
### Z panelu BackPRO
|
||||
Przycisk "Opublikuj teraz" na dashboardzie wywołuje `PublishController@run`, który uruchamia ten sam proces co CRON, ale synchronicznie z poziomu przeglądarki.
|
||||
|
||||
## Monitoring
|
||||
|
||||
### Sprawdzanie statusu
|
||||
- Dashboard BackPRO pokazuje datę ostatniej publikacji dla każdej strony
|
||||
- Lista artykułów z filtrem po statusie (published/failed)
|
||||
- Logi w `storage/logs/`
|
||||
|
||||
### Typowe problemy
|
||||
| Problem | Przyczyna | Rozwiązanie |
|
||||
|---------|-----------|-------------|
|
||||
| Brak publikacji | CRON nie uruchomiony | Sprawdź konfigurację CRON w panelu |
|
||||
| Status: failed | Błąd API | Sprawdź error_message w artykule i logi |
|
||||
| Lockfile blokuje | Poprzedni proces nie zakończył się | Usuń `storage/logs/publish.lock` |
|
||||
| 401 z WordPress | Złe dane API | Sprawdź api_user/api_token w konfiguracji strony |
|
||||
| 429 z OpenAI | Rate limit | Zwiększ interwał CRON lub poczekaj |
|
||||
198
docs/DATABASE.md
Normal file
198
docs/DATABASE.md
Normal file
@@ -0,0 +1,198 @@
|
||||
# BackPRO - Schemat bazy danych
|
||||
|
||||
## Diagram relacji
|
||||
|
||||
```
|
||||
users
|
||||
↓ (brak FK, niezależna tabela)
|
||||
|
||||
global_topics (parent_id → self) ← hierarchia 2-poziomowa
|
||||
↓
|
||||
└── topics.global_topic_id (opcjonalne powiązanie)
|
||||
|
||||
sites ←──── topics ←──── articles
|
||||
│ │
|
||||
│ ├── global_topic_id (FK → global_topics, opcjonalne)
|
||||
│ └── wp_category_id (mapowanie na kategorię WP)
|
||||
│
|
||||
└── last_published_at (tracking publikacji)
|
||||
|
||||
settings (klucz-wartość, konfiguracja globalna)
|
||||
```
|
||||
|
||||
## Tabele
|
||||
|
||||
### `users` - Użytkownicy systemu
|
||||
|
||||
```sql
|
||||
CREATE TABLE users (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
username VARCHAR(50) NOT NULL UNIQUE,
|
||||
password_hash VARCHAR(255) NOT NULL,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
```
|
||||
|
||||
| Kolumna | Typ | Opis |
|
||||
|---------|-----|------|
|
||||
| id | INT AUTO_INCREMENT PK | Identyfikator |
|
||||
| username | VARCHAR(50) UNIQUE NOT NULL | Login użytkownika |
|
||||
| password_hash | VARCHAR(255) NOT NULL | Hash hasła (bcrypt) |
|
||||
| created_at | DATETIME | Data utworzenia |
|
||||
|
||||
### `global_topics` - Biblioteka tematów (2-poziomowa hierarchia)
|
||||
|
||||
```sql
|
||||
CREATE TABLE global_topics (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
parent_id INT NULL,
|
||||
name VARCHAR(255) NOT NULL,
|
||||
description TEXT NULL,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (parent_id) REFERENCES global_topics(id) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
```
|
||||
|
||||
| Kolumna | Typ | Opis |
|
||||
|---------|-----|------|
|
||||
| id | INT AUTO_INCREMENT PK | Identyfikator |
|
||||
| parent_id | INT NULL FK → self | NULL = kategoria nadrzędna, wartość = subtemat |
|
||||
| name | VARCHAR(255) | Nazwa tematu/kategorii |
|
||||
| description | TEXT NULL | Opis / wytyczne dla AI |
|
||||
| created_at | DATETIME | Data dodania |
|
||||
|
||||
**Preinstalowane kategorie:** Polityka, Zdrowie, Sport, Technologia, Biznes i Finanse, Rozrywka, Nauka, Edukacja, Podróże, Motoryzacja, Dom i Ogród, Kuchnia, Moda i Uroda, Prawo (+ ~50 subtematów).
|
||||
|
||||
### `sites` - Strony WordPress
|
||||
|
||||
```sql
|
||||
CREATE TABLE sites (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
name VARCHAR(255) NOT NULL,
|
||||
url VARCHAR(255) NOT NULL,
|
||||
api_user VARCHAR(100) NOT NULL,
|
||||
api_token VARCHAR(255) NOT NULL,
|
||||
publish_interval_days INT NOT NULL DEFAULT 3,
|
||||
last_published_at DATETIME NULL,
|
||||
is_active TINYINT(1) NOT NULL DEFAULT 1,
|
||||
is_multisite TINYINT(1) NOT NULL DEFAULT 0,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
```
|
||||
|
||||
| Kolumna | Typ | Opis |
|
||||
|---------|-----|------|
|
||||
| id | INT AUTO_INCREMENT PK | Identyfikator |
|
||||
| name | VARCHAR(255) | Nazwa strony (wyświetlana w panelu) |
|
||||
| url | VARCHAR(255) | URL WordPressa (np. https://example.com) |
|
||||
| api_user | VARCHAR(100) | Login użytkownika WP do REST API |
|
||||
| api_token | VARCHAR(255) | Application Password WP |
|
||||
| publish_interval_days | INT DEFAULT 3 | Co ile dni publikować nowy artykuł |
|
||||
| last_published_at | DATETIME NULL | Data ostatniej publikacji |
|
||||
| is_active | TINYINT(1) DEFAULT 1 | Czy strona jest aktywna (0/1) |
|
||||
| is_multisite | TINYINT(1) DEFAULT 0 | Czy wielotematyczna (0/1) |
|
||||
| created_at | DATETIME | Data dodania |
|
||||
|
||||
### `topics` - Tematy
|
||||
|
||||
```sql
|
||||
CREATE TABLE topics (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
site_id INT NOT NULL,
|
||||
name VARCHAR(255) NOT NULL,
|
||||
description TEXT NULL,
|
||||
wp_category_id INT NULL,
|
||||
article_count INT NOT NULL DEFAULT 0,
|
||||
is_active TINYINT(1) NOT NULL DEFAULT 1,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (site_id) REFERENCES sites(id) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
```
|
||||
|
||||
| Kolumna | Typ | Opis |
|
||||
|---------|-----|------|
|
||||
| id | INT AUTO_INCREMENT PK | Identyfikator |
|
||||
| site_id | INT FK → sites.id | Strona, do której przypisany temat |
|
||||
| name | VARCHAR(255) | Nazwa tematu (np. "Ogrodnictwo", "DIY") |
|
||||
| description | TEXT NULL | Opis/wytyczne dla AI (jak pisać artykuły) |
|
||||
| wp_category_id | INT NULL | ID kategorii w WordPressie (mapowanie) |
|
||||
| article_count | INT DEFAULT 0 | Licznik opublikowanych artykułów z tego tematu |
|
||||
| is_active | TINYINT(1) DEFAULT 1 | Czy temat aktywny |
|
||||
| created_at | DATETIME | Data dodania |
|
||||
|
||||
**Uwaga:** `article_count` jest zinkrementalizowanym licznikiem, aktualizowanym po każdej publikacji. Służy do równomiernego rozkładu tematów (TopicBalancer).
|
||||
|
||||
### `articles` - Artykuły
|
||||
|
||||
```sql
|
||||
CREATE TABLE articles (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
site_id INT NOT NULL,
|
||||
topic_id INT NOT NULL,
|
||||
title VARCHAR(500) NOT NULL,
|
||||
content TEXT NOT NULL,
|
||||
wp_post_id INT NULL,
|
||||
image_url VARCHAR(500) NULL,
|
||||
status ENUM('generated', 'published', 'failed') NOT NULL DEFAULT 'generated',
|
||||
ai_model VARCHAR(50) NULL,
|
||||
prompt_used TEXT NULL,
|
||||
error_message TEXT NULL,
|
||||
published_at DATETIME NULL,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (site_id) REFERENCES sites(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (topic_id) REFERENCES topics(id) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
```
|
||||
|
||||
| Kolumna | Typ | Opis |
|
||||
|---------|-----|------|
|
||||
| id | INT AUTO_INCREMENT PK | Identyfikator |
|
||||
| site_id | INT FK → sites.id | Strona docelowa |
|
||||
| topic_id | INT FK → topics.id | Temat artykułu |
|
||||
| title | VARCHAR(500) | Tytuł artykułu |
|
||||
| content | TEXT | Treść artykułu (HTML) |
|
||||
| wp_post_id | INT NULL | ID posta w WordPressie (po publikacji) |
|
||||
| image_url | VARCHAR(500) NULL | URL wygenerowanego obrazka |
|
||||
| status | ENUM | Status: generated, published, failed |
|
||||
| ai_model | VARCHAR(50) | Model AI użyty do wygenerowania |
|
||||
| prompt_used | TEXT | Pełny prompt wysłany do AI |
|
||||
| error_message | TEXT NULL | Treść błędu (jeśli status = failed) |
|
||||
| published_at | DATETIME NULL | Data publikacji na WordPressie |
|
||||
| created_at | DATETIME | Data wygenerowania |
|
||||
|
||||
### `settings` - Ustawienia globalne
|
||||
|
||||
```sql
|
||||
CREATE TABLE settings (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
`key` VARCHAR(100) NOT NULL UNIQUE,
|
||||
value TEXT NULL
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
```
|
||||
|
||||
| Kolumna | Typ | Opis |
|
||||
|---------|-----|------|
|
||||
| id | INT AUTO_INCREMENT PK | Identyfikator |
|
||||
| key | VARCHAR(100) UNIQUE | Klucz ustawienia |
|
||||
| value | TEXT | Wartość ustawienia |
|
||||
|
||||
**Domyślne ustawienia:**
|
||||
| Klucz | Wartość | Opis |
|
||||
|-------|---------|------|
|
||||
| openai_api_key | sk-... | Klucz API OpenAI |
|
||||
| openai_model | gpt-4o | Model do generowania artykułów |
|
||||
| freepik_api_key | ... | Klucz API Freepik |
|
||||
| image_provider | freepik | Dostawca obrazków (freepik/unsplash/pexels) |
|
||||
| article_min_words | 800 | Min. długość artykułu |
|
||||
| article_max_words | 1200 | Max. długość artykułu |
|
||||
|
||||
## Indeksy
|
||||
|
||||
```sql
|
||||
CREATE INDEX idx_topics_site_id ON topics(site_id);
|
||||
CREATE INDEX idx_articles_site_id ON articles(site_id);
|
||||
CREATE INDEX idx_articles_topic_id ON articles(topic_id);
|
||||
CREATE INDEX idx_articles_status ON articles(status);
|
||||
CREATE INDEX idx_sites_is_active ON sites(is_active);
|
||||
CREATE INDEX idx_sites_last_published ON sites(last_published_at);
|
||||
```
|
||||
149
docs/PLAN.md
Normal file
149
docs/PLAN.md
Normal file
@@ -0,0 +1,149 @@
|
||||
# BackPRO - System Zarządzania Zapleczem SEO
|
||||
|
||||
## Opis projektu
|
||||
BackPRO to narzędzie webowe (PHP) do automatycznego zarządzania siecią stron WordPress stanowiących zaplecze SEO. System po dodaniu instancji WordPressa i skonfigurowaniu tematów automatycznie generuje i publikuje unikalne artykuły z obrazkami, równomiernie rozkładając tematy.
|
||||
|
||||
**URL:** https://backpro.projectpro.pl
|
||||
**Hosting:** Hostido (shared hosting)
|
||||
|
||||
## Stack technologiczny
|
||||
- **PHP 8.3** - czyste PHP z własną strukturą MVC (bez frameworka)
|
||||
- **MySQL/MariaDB** - baza danych
|
||||
- **WordPress REST API** - komunikacja z instancjami WordPress
|
||||
- **OpenAI API** - generowanie artykułów (konfigurowalny model: GPT-4o, GPT-4o-mini, itp.)
|
||||
- **Freepik API** - generowanie/pobieranie obrazków do artykułów (z fallbackiem na Unsplash/Pexels)
|
||||
- **CRON** - automatyczne uruchamianie publikacji co 1 godzinę
|
||||
- **Bootstrap 5** - frontend panelu administracyjnego
|
||||
- **Composer** - zarządzanie zależnościami (Guzzle HTTP, phpdotenv)
|
||||
|
||||
## Struktura katalogów
|
||||
|
||||
```
|
||||
public_html/
|
||||
├── index.php # Front controller
|
||||
├── .htaccess # Rewrite rules + zabezpieczenia katalogów
|
||||
├── .env # Konfiguracja (zablokowany .htaccess)
|
||||
├── .env.example # Przykładowa konfiguracja
|
||||
├── composer.json
|
||||
├── vendor/ # Composer autoload
|
||||
├── assets/
|
||||
│ ├── css/app.css
|
||||
│ └── js/app.js
|
||||
├── docs/ # Dokumentacja (deny from all)
|
||||
├── src/ # Kod PHP (deny from all)
|
||||
│ ├── Core/ # Rdzeń MVC
|
||||
│ │ ├── App.php # Bootstrap aplikacji
|
||||
│ │ ├── Router.php # Routing URL → Controller
|
||||
│ │ ├── Controller.php # Bazowy kontroler
|
||||
│ │ ├── Model.php # Bazowy model (PDO wrapper)
|
||||
│ │ ├── Database.php # Singleton połączenia PDO
|
||||
│ │ ├── Auth.php # System autoryzacji
|
||||
│ │ ├── View.php # Renderer szablonów
|
||||
│ │ └── Config.php # Ładowanie .env
|
||||
│ ├── Controllers/ # Kontrolery aplikacji
|
||||
│ │ ├── AuthController.php
|
||||
│ │ ├── DashboardController.php
|
||||
│ │ ├── SiteController.php
|
||||
│ │ ├── TopicController.php
|
||||
│ │ ├── CategoryController.php
|
||||
│ │ ├── ArticleController.php
|
||||
│ │ ├── PublishController.php
|
||||
│ │ └── SettingsController.php
|
||||
│ ├── Models/ # Modele danych
|
||||
│ │ ├── User.php
|
||||
│ │ ├── Site.php
|
||||
│ │ ├── Topic.php
|
||||
│ │ ├── Category.php
|
||||
│ │ ├── Article.php
|
||||
│ │ └── PublishQueue.php
|
||||
│ ├── Services/ # Logika biznesowa
|
||||
│ │ ├── WordPressService.php
|
||||
│ │ ├── OpenAIService.php
|
||||
│ │ ├── ImageService.php
|
||||
│ │ ├── PublisherService.php
|
||||
│ │ └── TopicBalancer.php
|
||||
│ └── Helpers/
|
||||
│ ├── Validator.php
|
||||
│ └── Logger.php
|
||||
├── templates/ # Szablony PHP (deny from all)
|
||||
│ ├── layout/
|
||||
│ ├── auth/
|
||||
│ ├── dashboard/
|
||||
│ ├── sites/
|
||||
│ ├── topics/
|
||||
│ ├── categories/
|
||||
│ ├── articles/
|
||||
│ └── settings/
|
||||
├── cron/ # Skrypty CRON (deny from all)
|
||||
│ └── publish.php
|
||||
├── storage/logs/ # Logi (deny from all)
|
||||
├── migrations/ # Migracje SQL (deny from all)
|
||||
└── config/ # Konfiguracja routingu (deny from all)
|
||||
└── routes.php
|
||||
```
|
||||
|
||||
## Główne funkcje
|
||||
|
||||
### 1. System logowania
|
||||
- Login/hasło z bcrypt
|
||||
- Sesje PHP
|
||||
- Middleware autoryzacji
|
||||
- Obsługa wielu użytkowników
|
||||
- Zmiana hasła przez zalogowanego użytkownika (`/change-password`)
|
||||
|
||||
### 2. Zarządzanie stronami WordPress
|
||||
- Dodawanie/edycja/usuwanie instancji WP
|
||||
- Konfiguracja: URL, dane API (Application Password), interwał publikacji
|
||||
- Oznaczanie jako jedno-/wielotematyczna
|
||||
- Test połączenia z WP REST API
|
||||
|
||||
### 3. Biblioteka tematów (`/global-topics`)
|
||||
- Globalna biblioteka tematów z hierarchią 2-poziomową (kategoria → temat)
|
||||
- 14 kategorii nadrzędnych + ~50 subtematów preinstalowanych
|
||||
- Dodawanie własnych kategorii i tematów
|
||||
- Edycja / usuwanie (CASCADE na subtematach)
|
||||
|
||||
### 4. Zarządzanie tematami stron (`/sites/{id}/topics`)
|
||||
- Przypisywanie tematów do stron z biblioteki globalnej lub tworzenie własnych
|
||||
- Opis/wytyczne dla AI (co i jak pisać)
|
||||
- Mapowanie na kategorię WordPress
|
||||
- Aktywacja/dezaktywacja
|
||||
|
||||
### 4. Automatyczna publikacja (CRON)
|
||||
Algorytm (co 1h):
|
||||
1. Znajdź stronę, której kolej na publikację (najdłużej bez nowego artykułu + minął interwał)
|
||||
2. TopicBalancer → wybierz temat z najmniejszą liczbą artykułów
|
||||
3. OpenAI → wygeneruj artykuł (z listą istniejących tytułów w prompcie)
|
||||
4. Freepik → wygeneruj obrazek
|
||||
5. WP REST API → upload obrazka + publikacja posta
|
||||
6. Zapisz w bazie, zaktualizuj liczniki
|
||||
|
||||
### 5. Historia artykułów
|
||||
- Lista wszystkich wygenerowanych artykułów
|
||||
- Podgląd treści
|
||||
- Status (generated/published/failed)
|
||||
- Logi błędów
|
||||
|
||||
### 6. Ustawienia
|
||||
- Klucze API (OpenAI, Freepik)
|
||||
- Wybór modelu AI
|
||||
- Konfiguracja globalna
|
||||
|
||||
## Plan implementacji
|
||||
|
||||
| Etap | Zakres | Pliki |
|
||||
|------|--------|-------|
|
||||
| 1. Fundament | Struktura, Core MVC, .htaccess, migracja SQL, logowanie | Core/*, AuthController, migrations/ |
|
||||
| 2. Strony | CRUD WordPress, tematy, kategorie, dashboard | SiteController, TopicController, CategoryController |
|
||||
| 3. API | Integracje OpenAI, Freepik, WordPress REST API | Services/* |
|
||||
| 4. Publikacja | PublisherService, TopicBalancer, CRON, historia | PublisherService, cron/publish.php, ArticleController |
|
||||
| 5. Finalizacja | Ustawienia, logi, dokumentacja, testy | SettingsController, Logger, docs/ |
|
||||
|
||||
## Bezpieczeństwo
|
||||
- Wszystkie katalogi poza assets/ zabezpieczone `.htaccess` (Deny from all)
|
||||
- Plik `.env` zablokowany przez .htaccess
|
||||
- Hasła hashowane bcrypt
|
||||
- Prepared statements (PDO) - ochrona przed SQL injection
|
||||
- htmlspecialchars() w szablonach - ochrona przed XSS
|
||||
- CSRF tokeny w formularzach
|
||||
- Application Passwords do WP REST API (nie zwykłe hasła)
|
||||
8
index.php
Normal file
8
index.php
Normal file
@@ -0,0 +1,8 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
require_once __DIR__ . '/vendor/autoload.php';
|
||||
|
||||
$app = new \App\Core\App(__DIR__);
|
||||
$app->run();
|
||||
79
install.php
Normal file
79
install.php
Normal file
@@ -0,0 +1,79 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* BackPRO - Installation script
|
||||
* Run this once after deploying to set up the database and create admin user.
|
||||
* DELETE THIS FILE after installation!
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
require_once __DIR__ . '/vendor/autoload.php';
|
||||
|
||||
\App\Core\Config::load(__DIR__);
|
||||
|
||||
echo "=== BackPRO Installer ===\n\n";
|
||||
|
||||
// Check if running from CLI or web
|
||||
$isCli = php_sapi_name() === 'cli';
|
||||
$nl = $isCli ? "\n" : "<br>";
|
||||
|
||||
try {
|
||||
$db = \App\Core\Database::getInstance();
|
||||
echo "Połączenie z bazą danych: OK{$nl}";
|
||||
} catch (\Exception $e) {
|
||||
echo "BŁĄD połączenia z bazą: " . $e->getMessage() . $nl;
|
||||
echo "Sprawdź konfigurację w pliku .env{$nl}";
|
||||
exit(1);
|
||||
}
|
||||
|
||||
// Run all migrations in order
|
||||
$migrationFiles = glob(__DIR__ . '/migrations/*.sql');
|
||||
sort($migrationFiles);
|
||||
|
||||
foreach ($migrationFiles as $migrationFile) {
|
||||
$migrationName = basename($migrationFile);
|
||||
$sql = file_get_contents($migrationFile);
|
||||
|
||||
$sqlClean = preg_replace('/--.*$/m', '', $sql);
|
||||
$statements = array_filter(
|
||||
array_map('trim', explode(';', $sqlClean)),
|
||||
fn($s) => !empty($s)
|
||||
);
|
||||
|
||||
foreach ($statements as $statement) {
|
||||
try {
|
||||
$db->exec($statement);
|
||||
} catch (\PDOException $e) {
|
||||
if (!str_contains($e->getMessage(), 'already exists') && !str_contains($e->getMessage(), 'Duplicate')) {
|
||||
echo "SQL Warning ({$migrationName}): " . $e->getMessage() . $nl;
|
||||
}
|
||||
}
|
||||
}
|
||||
echo "Migracja {$migrationName}: OK{$nl}";
|
||||
}
|
||||
|
||||
// Create admin user
|
||||
$username = 'admin';
|
||||
$password = 'admin123'; // Change this!
|
||||
|
||||
$hash = password_hash($password, PASSWORD_BCRYPT);
|
||||
|
||||
try {
|
||||
$stmt = $db->prepare("SELECT id FROM users WHERE username = :u");
|
||||
$stmt->execute(['u' => $username]);
|
||||
|
||||
if ($stmt->fetch()) {
|
||||
echo "Użytkownik '{$username}' już istnieje.{$nl}";
|
||||
} else {
|
||||
$stmt = $db->prepare("INSERT INTO users (username, password_hash) VALUES (:u, :p)");
|
||||
$stmt->execute(['u' => $username, 'p' => $hash]);
|
||||
echo "Utworzono użytkownika: {$username} / {$password}{$nl}";
|
||||
echo "WAŻNE: Zmień hasło po pierwszym logowaniu!{$nl}";
|
||||
}
|
||||
} catch (\PDOException $e) {
|
||||
echo "Błąd tworzenia użytkownika: " . $e->getMessage() . $nl;
|
||||
}
|
||||
|
||||
echo "{$nl}=== Instalacja zakończona ==={$nl}";
|
||||
echo "USUŃ PLIK install.php po zakończeniu instalacji!{$nl}";
|
||||
82
migrations/001_initial.sql
Normal file
82
migrations/001_initial.sql
Normal file
@@ -0,0 +1,82 @@
|
||||
-- BackPRO - Initial database migration
|
||||
-- Baza danych musi być już utworzona w panelu hostingu
|
||||
|
||||
-- Users table
|
||||
CREATE TABLE IF NOT EXISTS users (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
username VARCHAR(50) NOT NULL UNIQUE,
|
||||
password_hash VARCHAR(255) NOT NULL,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
-- Sites (WordPress instances)
|
||||
CREATE TABLE IF NOT EXISTS sites (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
name VARCHAR(255) NOT NULL,
|
||||
url VARCHAR(255) NOT NULL,
|
||||
api_user VARCHAR(100) NOT NULL,
|
||||
api_token VARCHAR(255) NOT NULL,
|
||||
publish_interval_days INT NOT NULL DEFAULT 3,
|
||||
last_published_at DATETIME NULL,
|
||||
is_active TINYINT(1) NOT NULL DEFAULT 1,
|
||||
is_multisite TINYINT(1) NOT NULL DEFAULT 0,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
-- Topics (assigned to sites)
|
||||
CREATE TABLE IF NOT EXISTS topics (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
site_id INT NOT NULL,
|
||||
name VARCHAR(255) NOT NULL,
|
||||
description TEXT NULL,
|
||||
wp_category_id INT NULL,
|
||||
article_count INT NOT NULL DEFAULT 0,
|
||||
is_active TINYINT(1) NOT NULL DEFAULT 1,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (site_id) REFERENCES sites(id) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
-- Articles (generated and published)
|
||||
CREATE TABLE IF NOT EXISTS articles (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
site_id INT NOT NULL,
|
||||
topic_id INT NOT NULL,
|
||||
title VARCHAR(500) NOT NULL,
|
||||
content TEXT NOT NULL,
|
||||
wp_post_id INT NULL,
|
||||
image_url VARCHAR(500) NULL,
|
||||
status ENUM('generated', 'published', 'failed') NOT NULL DEFAULT 'generated',
|
||||
ai_model VARCHAR(50) NULL,
|
||||
prompt_used TEXT NULL,
|
||||
error_message TEXT NULL,
|
||||
published_at DATETIME NULL,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (site_id) REFERENCES sites(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (topic_id) REFERENCES topics(id) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
-- Settings (key-value store)
|
||||
CREATE TABLE IF NOT EXISTS settings (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
`key` VARCHAR(100) NOT NULL UNIQUE,
|
||||
value TEXT NULL
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
-- Indexes
|
||||
CREATE INDEX idx_topics_site_id ON topics(site_id);
|
||||
CREATE INDEX idx_articles_site_id ON articles(site_id);
|
||||
CREATE INDEX idx_articles_topic_id ON articles(topic_id);
|
||||
CREATE INDEX idx_articles_status ON articles(status);
|
||||
CREATE INDEX idx_sites_is_active ON sites(is_active);
|
||||
CREATE INDEX idx_sites_last_published ON sites(last_published_at);
|
||||
|
||||
-- Default admin user - hasło ustawiasz przez skrypt install.php
|
||||
-- LUB ręcznie wstaw hash bcrypt:
|
||||
-- INSERT INTO users (username, password_hash) VALUES ('admin', '$2y$10$...');
|
||||
|
||||
-- Default settings
|
||||
INSERT INTO settings (`key`, value) VALUES
|
||||
('openai_model', 'gpt-4o'),
|
||||
('image_provider', 'freepik'),
|
||||
('article_min_words', '800'),
|
||||
('article_max_words', '1200');
|
||||
125
migrations/002_global_topics.sql
Normal file
125
migrations/002_global_topics.sql
Normal file
@@ -0,0 +1,125 @@
|
||||
-- Global topics library (2-level hierarchy: category -> subtopic)
|
||||
|
||||
CREATE TABLE IF NOT EXISTS global_topics (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
parent_id INT NULL,
|
||||
name VARCHAR(255) NOT NULL,
|
||||
description TEXT NULL,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (parent_id) REFERENCES global_topics(id) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
CREATE INDEX idx_global_topics_parent ON global_topics(parent_id);
|
||||
|
||||
-- Link topics to global_topics library
|
||||
ALTER TABLE topics ADD COLUMN global_topic_id INT NULL AFTER site_id;
|
||||
|
||||
-- Seed: top-level categories
|
||||
|
||||
INSERT INTO global_topics (id, parent_id, name, description) VALUES
|
||||
(1, NULL, 'Polityka', 'Tematy związane z polityką krajową i międzynarodową'),
|
||||
(2, NULL, 'Zdrowie', 'Zdrowie, medycyna, profilaktyka, wellness'),
|
||||
(3, NULL, 'Sport', 'Dyscypliny sportowe, wydarzenia, treningi'),
|
||||
(4, NULL, 'Technologia', 'IT, gadżety, sztuczna inteligencja, innowacje'),
|
||||
(5, NULL, 'Biznes i Finanse', 'Ekonomia, inwestycje, przedsiębiorczość'),
|
||||
(6, NULL, 'Rozrywka', 'Film, muzyka, gry, kultura popularna'),
|
||||
(7, NULL, 'Nauka', 'Odkrycia naukowe, kosmos, badania'),
|
||||
(8, NULL, 'Edukacja', 'Szkolnictwo, kursy, rozwój osobisty'),
|
||||
(9, NULL, 'Podróże', 'Turystyka, przewodniki, inspiracje podróżnicze'),
|
||||
(10, NULL, 'Motoryzacja', 'Samochody, motocykle, nowości motoryzacyjne'),
|
||||
(11, NULL, 'Dom i Ogród', 'Aranżacja wnętrz, ogrodnictwo, DIY'),
|
||||
(12, NULL, 'Kuchnia', 'Przepisy, diety, kuchnie świata'),
|
||||
(13, NULL, 'Moda i Uroda', 'Trendy, pielęgnacja, kosmetyki'),
|
||||
(14, NULL, 'Prawo', 'Porady prawne, zmiany w przepisach');
|
||||
|
||||
-- Seed: subtopics
|
||||
|
||||
-- Polityka
|
||||
INSERT INTO global_topics (parent_id, name, description) VALUES
|
||||
(1, 'Polityka krajowa', 'Sejm, rząd, partie polityczne, wybory w Polsce'),
|
||||
(1, 'Polityka międzynarodowa', 'Dyplomacja, konflikty, organizacje międzynarodowe'),
|
||||
(1, 'Unia Europejska', 'Polityka UE, fundusze europejskie, regulacje');
|
||||
|
||||
-- Zdrowie
|
||||
INSERT INTO global_topics (parent_id, name, description) VALUES
|
||||
(2, 'Medycyna', 'Choroby, leczenie, nowinki medyczne'),
|
||||
(2, 'Zdrowy styl życia', 'Dieta, aktywność fizyczna, profilaktyka'),
|
||||
(2, 'Zdrowie psychiczne', 'Psychologia, stres, mindfulness, terapia'),
|
||||
(2, 'Suplementy i żywienie', 'Witaminy, suplementy diety, superfoods');
|
||||
|
||||
-- Sport
|
||||
INSERT INTO global_topics (parent_id, name, description) VALUES
|
||||
(3, 'Piłka nożna', 'Liga, transfery, reprezentacja, piłka nożna na świecie'),
|
||||
(3, 'Koszykówka', 'NBA, polska liga, Euroliga'),
|
||||
(3, 'Siatkówka', 'PlusLiga, reprezentacja Polski, siatkówka plażowa'),
|
||||
(3, 'Sporty walki', 'MMA, boks, judo, karate'),
|
||||
(3, 'Fitness i siłownia', 'Treningi, plany treningowe, ćwiczenia'),
|
||||
(3, 'Bieganie', 'Maratony, technika biegu, sprzęt biegowy');
|
||||
|
||||
-- Technologia
|
||||
INSERT INTO global_topics (parent_id, name, description) VALUES
|
||||
(4, 'Sztuczna inteligencja', 'AI, machine learning, ChatGPT, automatyzacja'),
|
||||
(4, 'Smartfony i gadżety', 'Recenzje, nowości, porównania urządzeń'),
|
||||
(4, 'Programowanie', 'Języki programowania, frameworki, poradniki'),
|
||||
(4, 'Cyberbezpieczeństwo', 'Ochrona danych, hakerzy, prywatność w sieci'),
|
||||
(4, 'Gaming', 'Gry komputerowe, konsole, esport');
|
||||
|
||||
-- Biznes i Finanse
|
||||
INSERT INTO global_topics (parent_id, name, description) VALUES
|
||||
(5, 'Inwestycje', 'Giełda, kryptowaluty, nieruchomości, fundusze'),
|
||||
(5, 'Przedsiębiorczość', 'Zakładanie firmy, startupy, zarządzanie'),
|
||||
(5, 'Oszczędzanie', 'Finanse osobiste, budżet domowy, porady'),
|
||||
(5, 'Marketing', 'Marketing cyfrowy, SEO, social media, reklama');
|
||||
|
||||
-- Rozrywka
|
||||
INSERT INTO global_topics (parent_id, name, description) VALUES
|
||||
(6, 'Film i seriale', 'Recenzje, premiery, Netflix, kino'),
|
||||
(6, 'Muzyka', 'Artyści, festiwale, nowości muzyczne'),
|
||||
(6, 'Książki', 'Recenzje książek, bestsellery, literatura');
|
||||
|
||||
-- Nauka
|
||||
INSERT INTO global_topics (parent_id, name, description) VALUES
|
||||
(7, 'Kosmos', 'NASA, SpaceX, odkrycia astronomiczne'),
|
||||
(7, 'Ekologia', 'Zmiany klimatu, recykling, energia odnawialna'),
|
||||
(7, 'Historia', 'Ciekawostki historyczne, ważne wydarzenia');
|
||||
|
||||
-- Edukacja
|
||||
INSERT INTO global_topics (parent_id, name, description) VALUES
|
||||
(8, 'Rozwój osobisty', 'Produktywność, nawyki, motywacja'),
|
||||
(8, 'Języki obce', 'Nauka angielskiego, techniki zapamiętywania');
|
||||
|
||||
-- Podróże
|
||||
INSERT INTO global_topics (parent_id, name, description) VALUES
|
||||
(9, 'Podróże po Polsce', 'Atrakcje turystyczne, szlaki, regiony'),
|
||||
(9, 'Podróże zagraniczne', 'Egzotyczne kierunki, tanie loty, porady'),
|
||||
(9, 'Camping i outdoor', 'Turystyka górska, biwakowanie, sprzęt');
|
||||
|
||||
-- Motoryzacja
|
||||
INSERT INTO global_topics (parent_id, name, description) VALUES
|
||||
(10, 'Samochody elektryczne', 'Tesla, EV, ładowarki, przyszłość elektromobilności'),
|
||||
(10, 'Testy i recenzje aut', 'Porównania, testy drogowe, nowości'),
|
||||
(10, 'Porady kierowców', 'Ubezpieczenia, prawo jazdy, serwis');
|
||||
|
||||
-- Dom i Ogród
|
||||
INSERT INTO global_topics (parent_id, name, description) VALUES
|
||||
(11, 'Aranżacja wnętrz', 'Design, meble, dekoracje, trendy'),
|
||||
(11, 'Ogrodnictwo', 'Rośliny, uprawy, pielęgnacja ogrodu'),
|
||||
(11, 'Remonty i DIY', 'Majsterkowanie, poradniki remontowe');
|
||||
|
||||
-- Kuchnia
|
||||
INSERT INTO global_topics (parent_id, name, description) VALUES
|
||||
(12, 'Przepisy', 'Obiady, desery, dania na szybko'),
|
||||
(12, 'Diety', 'Keto, weganizm, intermittent fasting'),
|
||||
(12, 'Kuchnie świata', 'Włoska, azjatycka, meksykańska');
|
||||
|
||||
-- Moda i Uroda
|
||||
INSERT INTO global_topics (parent_id, name, description) VALUES
|
||||
(13, 'Moda damska', 'Trendy, stylizacje, inspiracje'),
|
||||
(13, 'Moda męska', 'Garnitury, casual, street style'),
|
||||
(13, 'Pielęgnacja', 'Kosmetyki, zabiegi, skincare');
|
||||
|
||||
-- Prawo
|
||||
INSERT INTO global_topics (parent_id, name, description) VALUES
|
||||
(14, 'Prawo pracy', 'Umowy, zwolnienia, prawa pracownika'),
|
||||
(14, 'Prawo cywilne', 'Umowy, spadki, nieruchomości'),
|
||||
(14, 'Prawo gospodarcze', 'Działalność gospodarcza, podatki, ZUS');
|
||||
44
src/Controllers/ArticleController.php
Normal file
44
src/Controllers/ArticleController.php
Normal file
@@ -0,0 +1,44 @@
|
||||
<?php
|
||||
|
||||
namespace App\Controllers;
|
||||
|
||||
use App\Core\Auth;
|
||||
use App\Core\Controller;
|
||||
use App\Models\Article;
|
||||
|
||||
class ArticleController extends Controller
|
||||
{
|
||||
public function index(): void
|
||||
{
|
||||
Auth::requireLogin();
|
||||
|
||||
$page = max(1, (int) ($this->input('page', 1)));
|
||||
$perPage = 20;
|
||||
$offset = ($page - 1) * $perPage;
|
||||
|
||||
$articles = Article::findAllWithRelations($perPage, $offset);
|
||||
$total = Article::count();
|
||||
$totalPages = (int) ceil($total / $perPage);
|
||||
|
||||
$this->view('articles/index', [
|
||||
'articles' => $articles,
|
||||
'page' => $page,
|
||||
'totalPages' => $totalPages,
|
||||
'total' => $total,
|
||||
]);
|
||||
}
|
||||
|
||||
public function show(string $id): void
|
||||
{
|
||||
Auth::requireLogin();
|
||||
|
||||
$article = Article::findWithRelations((int) $id);
|
||||
if (!$article) {
|
||||
$this->flash('danger', 'Artykuł nie znaleziony.');
|
||||
$this->redirect('/articles');
|
||||
return;
|
||||
}
|
||||
|
||||
$this->view('articles/show', ['article' => $article]);
|
||||
}
|
||||
}
|
||||
87
src/Controllers/AuthController.php
Normal file
87
src/Controllers/AuthController.php
Normal file
@@ -0,0 +1,87 @@
|
||||
<?php
|
||||
|
||||
namespace App\Controllers;
|
||||
|
||||
use App\Core\Auth;
|
||||
use App\Core\Controller;
|
||||
use App\Core\Database;
|
||||
use App\Helpers\Validator;
|
||||
|
||||
class AuthController extends Controller
|
||||
{
|
||||
public function loginForm(): void
|
||||
{
|
||||
if (Auth::check()) {
|
||||
$this->redirect('/');
|
||||
}
|
||||
$this->view('auth/login');
|
||||
}
|
||||
|
||||
public function login(): void
|
||||
{
|
||||
$username = $this->input('username', '');
|
||||
$password = $this->input('password', '');
|
||||
|
||||
if (Auth::attempt($username, $password)) {
|
||||
$this->redirect('/');
|
||||
}
|
||||
|
||||
$this->view('auth/login', ['error' => 'Nieprawidłowy login lub hasło.']);
|
||||
}
|
||||
|
||||
public function logout(): void
|
||||
{
|
||||
Auth::logout();
|
||||
$this->redirect('/login');
|
||||
}
|
||||
|
||||
public function changePasswordForm(): void
|
||||
{
|
||||
Auth::requireLogin();
|
||||
$this->view('auth/change-password');
|
||||
}
|
||||
|
||||
public function changePassword(): void
|
||||
{
|
||||
Auth::requireLogin();
|
||||
|
||||
$current = $this->input('current_password', '');
|
||||
$new = $this->input('new_password', '');
|
||||
$confirm = $this->input('confirm_password', '');
|
||||
|
||||
$validator = new Validator();
|
||||
$validator
|
||||
->required('current_password', $current, 'Aktualne hasło')
|
||||
->required('new_password', $new, 'Nowe hasło')
|
||||
->minLength('new_password', $new, 6, 'Nowe hasło');
|
||||
|
||||
if (!$validator->isValid()) {
|
||||
$this->view('auth/change-password', ['error' => $validator->getFirstError()]);
|
||||
return;
|
||||
}
|
||||
|
||||
if ($new !== $confirm) {
|
||||
$this->view('auth/change-password', ['error' => 'Nowe hasła nie są identyczne.']);
|
||||
return;
|
||||
}
|
||||
|
||||
$db = Database::getInstance();
|
||||
$stmt = $db->prepare("SELECT password_hash FROM users WHERE id = :id");
|
||||
$stmt->execute(['id' => Auth::user()['id']]);
|
||||
$user = $stmt->fetch();
|
||||
|
||||
if (!$user || !password_verify($current, $user['password_hash'])) {
|
||||
$this->view('auth/change-password', ['error' => 'Aktualne hasło jest nieprawidłowe.']);
|
||||
return;
|
||||
}
|
||||
|
||||
$stmt = $db->prepare("UPDATE users SET password_hash = :hash WHERE id = :id");
|
||||
$stmt->execute([
|
||||
'hash' => Auth::hashPassword($new),
|
||||
'id' => Auth::user()['id'],
|
||||
]);
|
||||
|
||||
$this->flash('success', 'Hasło zostało zmienione.');
|
||||
$this->redirect('/');
|
||||
}
|
||||
}
|
||||
54
src/Controllers/CategoryController.php
Normal file
54
src/Controllers/CategoryController.php
Normal file
@@ -0,0 +1,54 @@
|
||||
<?php
|
||||
|
||||
namespace App\Controllers;
|
||||
|
||||
use App\Core\Auth;
|
||||
use App\Core\Controller;
|
||||
use App\Models\Site;
|
||||
use App\Services\WordPressService;
|
||||
|
||||
class CategoryController extends Controller
|
||||
{
|
||||
public function index(string $id): void
|
||||
{
|
||||
Auth::requireLogin();
|
||||
|
||||
$site = Site::find((int) $id);
|
||||
if (!$site) {
|
||||
$this->flash('danger', 'Strona nie znaleziona.');
|
||||
$this->redirect('/sites');
|
||||
return;
|
||||
}
|
||||
|
||||
$wp = new WordPressService();
|
||||
$categories = $wp->getCategories($site);
|
||||
|
||||
$this->view('categories/index', [
|
||||
'site' => $site,
|
||||
'categories' => $categories,
|
||||
]);
|
||||
}
|
||||
|
||||
public function sync(string $id): void
|
||||
{
|
||||
Auth::requireLogin();
|
||||
|
||||
$site = Site::find((int) $id);
|
||||
if (!$site) {
|
||||
$this->flash('danger', 'Strona nie znaleziona.');
|
||||
$this->redirect('/sites');
|
||||
return;
|
||||
}
|
||||
|
||||
$wp = new WordPressService();
|
||||
$categories = $wp->getCategories($site);
|
||||
|
||||
if ($categories === false) {
|
||||
$this->flash('danger', 'Nie udało się pobrać kategorii z WordPress.');
|
||||
} else {
|
||||
$this->flash('success', 'Pobrano ' . count($categories) . ' kategorii z WordPress.');
|
||||
}
|
||||
|
||||
$this->redirect("/sites/{$id}/categories");
|
||||
}
|
||||
}
|
||||
33
src/Controllers/DashboardController.php
Normal file
33
src/Controllers/DashboardController.php
Normal file
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
namespace App\Controllers;
|
||||
|
||||
use App\Core\Auth;
|
||||
use App\Core\Controller;
|
||||
use App\Models\Site;
|
||||
use App\Models\Article;
|
||||
use App\Models\Topic;
|
||||
|
||||
class DashboardController extends Controller
|
||||
{
|
||||
public function index(): void
|
||||
{
|
||||
Auth::requireLogin();
|
||||
|
||||
$sites = Site::findAll('name ASC');
|
||||
$articleStats = Article::getStats();
|
||||
$totalSites = Site::count();
|
||||
$activeSites = Site::count('is_active = 1');
|
||||
$totalTopics = Topic::count();
|
||||
$recentArticles = Article::findAllWithRelations(10);
|
||||
|
||||
$this->view('dashboard/index', [
|
||||
'sites' => $sites,
|
||||
'articleStats' => $articleStats,
|
||||
'totalSites' => $totalSites,
|
||||
'activeSites' => $activeSites,
|
||||
'totalTopics' => $totalTopics,
|
||||
'recentArticles' => $recentArticles,
|
||||
]);
|
||||
}
|
||||
}
|
||||
103
src/Controllers/GlobalTopicController.php
Normal file
103
src/Controllers/GlobalTopicController.php
Normal file
@@ -0,0 +1,103 @@
|
||||
<?php
|
||||
|
||||
namespace App\Controllers;
|
||||
|
||||
use App\Core\Auth;
|
||||
use App\Core\Controller;
|
||||
use App\Helpers\Validator;
|
||||
use App\Models\GlobalTopic;
|
||||
|
||||
class GlobalTopicController extends Controller
|
||||
{
|
||||
public function index(): void
|
||||
{
|
||||
Auth::requireLogin();
|
||||
|
||||
$categories = GlobalTopic::findAllGrouped();
|
||||
|
||||
$this->view('global-topics/index', ['categories' => $categories]);
|
||||
}
|
||||
|
||||
public function storeCategory(): void
|
||||
{
|
||||
Auth::requireLogin();
|
||||
|
||||
$validator = new Validator();
|
||||
$validator->required('name', $this->input('name'), 'Nazwa kategorii');
|
||||
|
||||
if (!$validator->isValid()) {
|
||||
$this->flash('danger', $validator->getFirstError());
|
||||
$this->redirect('/global-topics');
|
||||
return;
|
||||
}
|
||||
|
||||
GlobalTopic::create([
|
||||
'parent_id' => null,
|
||||
'name' => $this->input('name'),
|
||||
'description' => $this->input('description', ''),
|
||||
]);
|
||||
|
||||
$this->flash('success', 'Kategoria została dodana.');
|
||||
$this->redirect('/global-topics');
|
||||
}
|
||||
|
||||
public function storeSubtopic(string $parentId): void
|
||||
{
|
||||
Auth::requireLogin();
|
||||
|
||||
$parent = GlobalTopic::find((int) $parentId);
|
||||
if (!$parent) {
|
||||
$this->flash('danger', 'Kategoria nadrzędna nie znaleziona.');
|
||||
$this->redirect('/global-topics');
|
||||
return;
|
||||
}
|
||||
|
||||
$validator = new Validator();
|
||||
$validator->required('name', $this->input('name'), 'Nazwa tematu');
|
||||
|
||||
if (!$validator->isValid()) {
|
||||
$this->flash('danger', $validator->getFirstError());
|
||||
$this->redirect('/global-topics');
|
||||
return;
|
||||
}
|
||||
|
||||
GlobalTopic::create([
|
||||
'parent_id' => (int) $parentId,
|
||||
'name' => $this->input('name'),
|
||||
'description' => $this->input('description', ''),
|
||||
]);
|
||||
|
||||
$this->flash('success', 'Temat został dodany.');
|
||||
$this->redirect('/global-topics');
|
||||
}
|
||||
|
||||
public function update(string $id): void
|
||||
{
|
||||
Auth::requireLogin();
|
||||
|
||||
$topic = GlobalTopic::find((int) $id);
|
||||
if (!$topic) {
|
||||
$this->flash('danger', 'Temat nie znaleziony.');
|
||||
$this->redirect('/global-topics');
|
||||
return;
|
||||
}
|
||||
|
||||
GlobalTopic::update((int) $id, [
|
||||
'name' => $this->input('name'),
|
||||
'description' => $this->input('description', ''),
|
||||
]);
|
||||
|
||||
$this->flash('success', 'Temat został zaktualizowany.');
|
||||
$this->redirect('/global-topics');
|
||||
}
|
||||
|
||||
public function destroy(string $id): void
|
||||
{
|
||||
Auth::requireLogin();
|
||||
|
||||
GlobalTopic::delete((int) $id);
|
||||
|
||||
$this->flash('success', 'Temat został usunięty.');
|
||||
$this->redirect('/global-topics');
|
||||
}
|
||||
}
|
||||
50
src/Controllers/PublishController.php
Normal file
50
src/Controllers/PublishController.php
Normal file
@@ -0,0 +1,50 @@
|
||||
<?php
|
||||
|
||||
namespace App\Controllers;
|
||||
|
||||
use App\Core\Auth;
|
||||
use App\Core\Controller;
|
||||
use App\Models\Site;
|
||||
use App\Services\PublisherService;
|
||||
|
||||
class PublishController extends Controller
|
||||
{
|
||||
public function run(): void
|
||||
{
|
||||
Auth::requireLogin();
|
||||
|
||||
$publisher = new PublisherService();
|
||||
$result = $publisher->publishNext();
|
||||
|
||||
if ($result['success']) {
|
||||
$this->flash('success', $result['message']);
|
||||
} else {
|
||||
$this->flash('danger', $result['message']);
|
||||
}
|
||||
|
||||
$this->redirect('/');
|
||||
}
|
||||
|
||||
public function runForSite(string $id): void
|
||||
{
|
||||
Auth::requireLogin();
|
||||
|
||||
$site = Site::find((int) $id);
|
||||
if (!$site) {
|
||||
$this->flash('danger', 'Strona nie znaleziona.');
|
||||
$this->redirect('/sites');
|
||||
return;
|
||||
}
|
||||
|
||||
$publisher = new PublisherService();
|
||||
$result = $publisher->publishForSite($site);
|
||||
|
||||
if ($result['success']) {
|
||||
$this->flash('success', $result['message']);
|
||||
} else {
|
||||
$this->flash('danger', $result['message']);
|
||||
}
|
||||
|
||||
$this->redirect("/sites/{$id}/edit");
|
||||
}
|
||||
}
|
||||
50
src/Controllers/SettingsController.php
Normal file
50
src/Controllers/SettingsController.php
Normal file
@@ -0,0 +1,50 @@
|
||||
<?php
|
||||
|
||||
namespace App\Controllers;
|
||||
|
||||
use App\Core\Auth;
|
||||
use App\Core\Config;
|
||||
use App\Core\Controller;
|
||||
|
||||
class SettingsController extends Controller
|
||||
{
|
||||
private array $settingKeys = [
|
||||
'openai_api_key',
|
||||
'openai_model',
|
||||
'freepik_api_key',
|
||||
'unsplash_api_key',
|
||||
'pexels_api_key',
|
||||
'image_provider',
|
||||
'article_min_words',
|
||||
'article_max_words',
|
||||
];
|
||||
|
||||
public function index(): void
|
||||
{
|
||||
Auth::requireLogin();
|
||||
|
||||
$settings = [];
|
||||
foreach ($this->settingKeys as $key) {
|
||||
$settings[$key] = Config::getDbSetting($key, '');
|
||||
}
|
||||
|
||||
$this->view('settings/index', ['settings' => $settings]);
|
||||
}
|
||||
|
||||
public function update(): void
|
||||
{
|
||||
Auth::requireLogin();
|
||||
|
||||
foreach ($this->settingKeys as $key) {
|
||||
$value = $this->input($key);
|
||||
if ($value !== null) {
|
||||
Config::setDbSetting($key, $value);
|
||||
}
|
||||
}
|
||||
|
||||
Config::clearCache();
|
||||
|
||||
$this->flash('success', 'Ustawienia zostały zapisane.');
|
||||
$this->redirect('/settings');
|
||||
}
|
||||
}
|
||||
165
src/Controllers/SiteController.php
Normal file
165
src/Controllers/SiteController.php
Normal file
@@ -0,0 +1,165 @@
|
||||
<?php
|
||||
|
||||
namespace App\Controllers;
|
||||
|
||||
use App\Core\Auth;
|
||||
use App\Core\Controller;
|
||||
use App\Helpers\Validator;
|
||||
use App\Models\Site;
|
||||
use App\Models\Topic;
|
||||
use App\Models\GlobalTopic;
|
||||
use App\Services\WordPressService;
|
||||
|
||||
class SiteController extends Controller
|
||||
{
|
||||
public function index(): void
|
||||
{
|
||||
Auth::requireLogin();
|
||||
|
||||
$sites = Site::findAll('name ASC');
|
||||
|
||||
// Attach topic count for each site
|
||||
foreach ($sites as &$site) {
|
||||
$site['topic_count'] = Topic::count('site_id = :sid', ['sid' => $site['id']]);
|
||||
}
|
||||
|
||||
$this->view('sites/index', ['sites' => $sites]);
|
||||
}
|
||||
|
||||
public function create(): void
|
||||
{
|
||||
Auth::requireLogin();
|
||||
$globalTopics = GlobalTopic::findAllGrouped();
|
||||
$this->view('sites/create', ['globalTopics' => $globalTopics]);
|
||||
}
|
||||
|
||||
public function store(): void
|
||||
{
|
||||
Auth::requireLogin();
|
||||
|
||||
$validator = new Validator();
|
||||
$validator
|
||||
->required('name', $this->input('name'), 'Nazwa')
|
||||
->required('url', $this->input('url'), 'URL')
|
||||
->url('url', $this->input('url'), 'URL')
|
||||
->required('api_user', $this->input('api_user'), 'Użytkownik API')
|
||||
->required('api_token', $this->input('api_token'), 'Token API');
|
||||
|
||||
if (!$validator->isValid()) {
|
||||
$this->flash('danger', $validator->getFirstError());
|
||||
$this->redirect('/sites/create');
|
||||
return;
|
||||
}
|
||||
|
||||
$siteId = Site::create([
|
||||
'name' => $this->input('name'),
|
||||
'url' => rtrim($this->input('url'), '/'),
|
||||
'api_user' => $this->input('api_user'),
|
||||
'api_token' => $this->input('api_token'),
|
||||
'publish_interval_days' => (int) ($this->input('publish_interval_days', 3)),
|
||||
'is_active' => $this->input('is_active') ? 1 : 0,
|
||||
'is_multisite' => $this->input('is_multisite') ? 1 : 0,
|
||||
]);
|
||||
|
||||
// Create topics from selected global topics
|
||||
$selectedTopics = $this->input('topics');
|
||||
if (is_array($selectedTopics)) {
|
||||
foreach ($selectedTopics as $globalTopicId) {
|
||||
$globalTopic = GlobalTopic::find((int) $globalTopicId);
|
||||
if ($globalTopic) {
|
||||
Topic::create([
|
||||
'site_id' => $siteId,
|
||||
'global_topic_id' => (int) $globalTopicId,
|
||||
'name' => $globalTopic['name'],
|
||||
'description' => $globalTopic['description'] ?? '',
|
||||
'is_active' => 1,
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$this->flash('success', 'Strona została dodana.');
|
||||
$this->redirect("/sites/{$siteId}/edit");
|
||||
}
|
||||
|
||||
public function edit(string $id): void
|
||||
{
|
||||
Auth::requireLogin();
|
||||
|
||||
$site = Site::find((int) $id);
|
||||
if (!$site) {
|
||||
$this->flash('danger', 'Strona nie znaleziona.');
|
||||
$this->redirect('/sites');
|
||||
return;
|
||||
}
|
||||
|
||||
$topics = Topic::findBySiteWithGlobal((int) $id);
|
||||
$globalTopics = GlobalTopic::findAllGrouped();
|
||||
$assignedGlobalIds = array_filter(array_column($topics, 'global_topic_id'));
|
||||
|
||||
$this->view('sites/edit', [
|
||||
'site' => $site,
|
||||
'topics' => $topics,
|
||||
'globalTopics' => $globalTopics,
|
||||
'assignedGlobalIds' => $assignedGlobalIds,
|
||||
]);
|
||||
}
|
||||
|
||||
public function update(string $id): void
|
||||
{
|
||||
Auth::requireLogin();
|
||||
|
||||
$validator = new Validator();
|
||||
$validator
|
||||
->required('name', $this->input('name'), 'Nazwa')
|
||||
->required('url', $this->input('url'), 'URL')
|
||||
->url('url', $this->input('url'), 'URL')
|
||||
->required('api_user', $this->input('api_user'), 'Użytkownik API')
|
||||
->required('api_token', $this->input('api_token'), 'Token API');
|
||||
|
||||
if (!$validator->isValid()) {
|
||||
$this->flash('danger', $validator->getFirstError());
|
||||
$this->redirect("/sites/{$id}/edit");
|
||||
return;
|
||||
}
|
||||
|
||||
Site::update((int) $id, [
|
||||
'name' => $this->input('name'),
|
||||
'url' => rtrim($this->input('url'), '/'),
|
||||
'api_user' => $this->input('api_user'),
|
||||
'api_token' => $this->input('api_token'),
|
||||
'publish_interval_days' => (int) ($this->input('publish_interval_days', 3)),
|
||||
'is_active' => $this->input('is_active') ? 1 : 0,
|
||||
'is_multisite' => $this->input('is_multisite') ? 1 : 0,
|
||||
]);
|
||||
|
||||
$this->flash('success', 'Strona została zaktualizowana.');
|
||||
$this->redirect('/sites');
|
||||
}
|
||||
|
||||
public function destroy(string $id): void
|
||||
{
|
||||
Auth::requireLogin();
|
||||
|
||||
Site::delete((int) $id);
|
||||
|
||||
$this->flash('success', 'Strona została usunięta.');
|
||||
$this->redirect('/sites');
|
||||
}
|
||||
|
||||
public function testConnection(string $id): void
|
||||
{
|
||||
Auth::requireLogin();
|
||||
|
||||
$site = Site::find((int) $id);
|
||||
if (!$site) {
|
||||
$this->json(['success' => false, 'message' => 'Strona nie znaleziona.']);
|
||||
return;
|
||||
}
|
||||
|
||||
$wp = new WordPressService();
|
||||
$result = $wp->testConnection($site);
|
||||
|
||||
$this->json($result);
|
||||
}
|
||||
}
|
||||
97
src/Controllers/TopicController.php
Normal file
97
src/Controllers/TopicController.php
Normal file
@@ -0,0 +1,97 @@
|
||||
<?php
|
||||
|
||||
namespace App\Controllers;
|
||||
|
||||
use App\Core\Auth;
|
||||
use App\Core\Controller;
|
||||
use App\Helpers\Validator;
|
||||
use App\Models\Site;
|
||||
use App\Models\Topic;
|
||||
use App\Models\GlobalTopic;
|
||||
|
||||
class TopicController extends Controller
|
||||
{
|
||||
public function index(string $id): void
|
||||
{
|
||||
Auth::requireLogin();
|
||||
|
||||
$site = Site::find((int) $id);
|
||||
if (!$site) {
|
||||
$this->flash('danger', 'Strona nie znaleziona.');
|
||||
$this->redirect('/sites');
|
||||
return;
|
||||
}
|
||||
|
||||
$topics = Topic::findBySiteWithGlobal((int) $id);
|
||||
$globalTopics = GlobalTopic::findAllGrouped();
|
||||
|
||||
$this->view('topics/index', ['site' => $site, 'topics' => $topics, 'globalTopics' => $globalTopics]);
|
||||
}
|
||||
|
||||
public function store(string $id): void
|
||||
{
|
||||
Auth::requireLogin();
|
||||
|
||||
$validator = new Validator();
|
||||
$validator->required('name', $this->input('name'), 'Nazwa tematu');
|
||||
|
||||
if (!$validator->isValid()) {
|
||||
$this->flash('danger', $validator->getFirstError());
|
||||
$this->redirect("/sites/{$id}/topics");
|
||||
return;
|
||||
}
|
||||
|
||||
Topic::create([
|
||||
'site_id' => (int) $id,
|
||||
'global_topic_id' => $this->input('global_topic_id') ? (int) $this->input('global_topic_id') : null,
|
||||
'name' => $this->input('name'),
|
||||
'description' => $this->input('description', ''),
|
||||
'wp_category_id' => $this->input('wp_category_id') ? (int) $this->input('wp_category_id') : null,
|
||||
'is_active' => $this->input('is_active') ? 1 : 0,
|
||||
]);
|
||||
|
||||
$this->flash('success', 'Temat został dodany.');
|
||||
$this->redirect("/sites/{$id}/topics");
|
||||
}
|
||||
|
||||
public function update(string $id): void
|
||||
{
|
||||
Auth::requireLogin();
|
||||
|
||||
$topic = Topic::find((int) $id);
|
||||
if (!$topic) {
|
||||
$this->flash('danger', 'Temat nie znaleziony.');
|
||||
$this->redirect('/sites');
|
||||
return;
|
||||
}
|
||||
|
||||
Topic::update((int) $id, [
|
||||
'global_topic_id' => $this->input('global_topic_id') ? (int) $this->input('global_topic_id') : null,
|
||||
'name' => $this->input('name'),
|
||||
'description' => $this->input('description', ''),
|
||||
'wp_category_id' => $this->input('wp_category_id') ? (int) $this->input('wp_category_id') : null,
|
||||
'is_active' => $this->input('is_active') ? 1 : 0,
|
||||
]);
|
||||
|
||||
$this->flash('success', 'Temat został zaktualizowany.');
|
||||
$this->redirect("/sites/{$topic['site_id']}/topics");
|
||||
}
|
||||
|
||||
public function destroy(string $id): void
|
||||
{
|
||||
Auth::requireLogin();
|
||||
|
||||
$topic = Topic::find((int) $id);
|
||||
if (!$topic) {
|
||||
$this->flash('danger', 'Temat nie znaleziony.');
|
||||
$this->redirect('/sites');
|
||||
return;
|
||||
}
|
||||
|
||||
$siteId = $topic['site_id'];
|
||||
Topic::delete((int) $id);
|
||||
|
||||
$this->flash('success', 'Temat został usunięty.');
|
||||
$this->redirect("/sites/{$siteId}/topics");
|
||||
}
|
||||
}
|
||||
40
src/Core/App.php
Normal file
40
src/Core/App.php
Normal file
@@ -0,0 +1,40 @@
|
||||
<?php
|
||||
|
||||
namespace App\Core;
|
||||
|
||||
use App\Helpers\Logger;
|
||||
|
||||
class App
|
||||
{
|
||||
private Router $router;
|
||||
private string $basePath;
|
||||
|
||||
public function __construct(string $basePath)
|
||||
{
|
||||
$this->basePath = $basePath;
|
||||
$this->router = new Router();
|
||||
}
|
||||
|
||||
public function run(): void
|
||||
{
|
||||
Config::load($this->basePath);
|
||||
|
||||
session_start();
|
||||
|
||||
View::setBasePath($this->basePath);
|
||||
Logger::setBasePath($this->basePath);
|
||||
|
||||
$this->loadRoutes();
|
||||
|
||||
$method = $_SERVER['REQUEST_METHOD'];
|
||||
$uri = $_SERVER['REQUEST_URI'];
|
||||
|
||||
$this->router->dispatch($method, $uri);
|
||||
}
|
||||
|
||||
private function loadRoutes(): void
|
||||
{
|
||||
$router = $this->router;
|
||||
require $this->basePath . '/config/routes.php';
|
||||
}
|
||||
}
|
||||
71
src/Core/Auth.php
Normal file
71
src/Core/Auth.php
Normal file
@@ -0,0 +1,71 @@
|
||||
<?php
|
||||
|
||||
namespace App\Core;
|
||||
|
||||
class Auth
|
||||
{
|
||||
public static function attempt(string $username, string $password): bool
|
||||
{
|
||||
$db = Database::getInstance();
|
||||
$stmt = $db->prepare("SELECT * FROM users WHERE username = :username");
|
||||
$stmt->execute(['username' => $username]);
|
||||
$user = $stmt->fetch();
|
||||
|
||||
if ($user && password_verify($password, $user['password_hash'])) {
|
||||
$_SESSION['user_id'] = $user['id'];
|
||||
$_SESSION['username'] = $user['username'];
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public static function check(): bool
|
||||
{
|
||||
return isset($_SESSION['user_id']);
|
||||
}
|
||||
|
||||
public static function user(): ?array
|
||||
{
|
||||
if (!self::check()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return [
|
||||
'id' => $_SESSION['user_id'],
|
||||
'username' => $_SESSION['username'],
|
||||
];
|
||||
}
|
||||
|
||||
public static function logout(): void
|
||||
{
|
||||
session_destroy();
|
||||
$_SESSION = [];
|
||||
}
|
||||
|
||||
public static function requireLogin(): void
|
||||
{
|
||||
if (!self::check()) {
|
||||
header('Location: /login');
|
||||
exit;
|
||||
}
|
||||
}
|
||||
|
||||
public static function hashPassword(string $password): string
|
||||
{
|
||||
return password_hash($password, PASSWORD_BCRYPT);
|
||||
}
|
||||
|
||||
public static function generateCsrfToken(): string
|
||||
{
|
||||
if (empty($_SESSION['csrf_token'])) {
|
||||
$_SESSION['csrf_token'] = bin2hex(random_bytes(32));
|
||||
}
|
||||
return $_SESSION['csrf_token'];
|
||||
}
|
||||
|
||||
public static function verifyCsrfToken(string $token): bool
|
||||
{
|
||||
return isset($_SESSION['csrf_token']) && hash_equals($_SESSION['csrf_token'], $token);
|
||||
}
|
||||
}
|
||||
53
src/Core/Config.php
Normal file
53
src/Core/Config.php
Normal file
@@ -0,0 +1,53 @@
|
||||
<?php
|
||||
|
||||
namespace App\Core;
|
||||
|
||||
class Config
|
||||
{
|
||||
private static array $settings = [];
|
||||
|
||||
public static function load(string $basePath): void
|
||||
{
|
||||
$dotenv = \Dotenv\Dotenv::createImmutable($basePath);
|
||||
$dotenv->load();
|
||||
}
|
||||
|
||||
public static function get(string $key, mixed $default = null): mixed
|
||||
{
|
||||
return $_ENV[$key] ?? $default;
|
||||
}
|
||||
|
||||
public static function getDbSetting(string $key, mixed $default = null): mixed
|
||||
{
|
||||
if (empty(self::$settings)) {
|
||||
self::loadDbSettings();
|
||||
}
|
||||
return self::$settings[$key] ?? $default;
|
||||
}
|
||||
|
||||
public static function setDbSetting(string $key, ?string $value): void
|
||||
{
|
||||
$db = Database::getInstance();
|
||||
$stmt = $db->prepare("INSERT INTO settings (`key`, value) VALUES (:key, :value) ON DUPLICATE KEY UPDATE value = :value2");
|
||||
$stmt->execute(['key' => $key, 'value' => $value, 'value2' => $value]);
|
||||
self::$settings[$key] = $value;
|
||||
}
|
||||
|
||||
private static function loadDbSettings(): void
|
||||
{
|
||||
try {
|
||||
$db = Database::getInstance();
|
||||
$stmt = $db->query("SELECT `key`, value FROM settings");
|
||||
while ($row = $stmt->fetch(\PDO::FETCH_ASSOC)) {
|
||||
self::$settings[$row['key']] = $row['value'];
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
self::$settings = [];
|
||||
}
|
||||
}
|
||||
|
||||
public static function clearCache(): void
|
||||
{
|
||||
self::$settings = [];
|
||||
}
|
||||
}
|
||||
35
src/Core/Controller.php
Normal file
35
src/Core/Controller.php
Normal file
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
namespace App\Core;
|
||||
|
||||
abstract class Controller
|
||||
{
|
||||
protected function view(string $template, array $data = []): void
|
||||
{
|
||||
View::render($template, $data);
|
||||
}
|
||||
|
||||
protected function redirect(string $url): void
|
||||
{
|
||||
header("Location: {$url}");
|
||||
exit;
|
||||
}
|
||||
|
||||
protected function json(array $data, int $statusCode = 200): void
|
||||
{
|
||||
http_response_code($statusCode);
|
||||
header('Content-Type: application/json');
|
||||
echo json_encode($data);
|
||||
exit;
|
||||
}
|
||||
|
||||
protected function input(string $key, mixed $default = null): mixed
|
||||
{
|
||||
return $_POST[$key] ?? $_GET[$key] ?? $default;
|
||||
}
|
||||
|
||||
protected function flash(string $type, string $message): void
|
||||
{
|
||||
$_SESSION['flash'][] = ['type' => $type, 'message' => $message];
|
||||
}
|
||||
}
|
||||
31
src/Core/Database.php
Normal file
31
src/Core/Database.php
Normal file
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
namespace App\Core;
|
||||
|
||||
class Database
|
||||
{
|
||||
private static ?\PDO $instance = null;
|
||||
|
||||
public static function getInstance(): \PDO
|
||||
{
|
||||
if (self::$instance === null) {
|
||||
$host = Config::get('DB_HOST', 'localhost');
|
||||
$name = Config::get('DB_NAME', 'backpro');
|
||||
$user = Config::get('DB_USER', 'root');
|
||||
$pass = Config::get('DB_PASS', '');
|
||||
|
||||
self::$instance = new \PDO(
|
||||
"mysql:host={$host};dbname={$name};charset=utf8mb4",
|
||||
$user,
|
||||
$pass,
|
||||
[
|
||||
\PDO::ATTR_ERRMODE => \PDO::ERRMODE_EXCEPTION,
|
||||
\PDO::ATTR_DEFAULT_FETCH_MODE => \PDO::FETCH_ASSOC,
|
||||
\PDO::ATTR_EMULATE_PREPARES => false,
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
return self::$instance;
|
||||
}
|
||||
}
|
||||
73
src/Core/Model.php
Normal file
73
src/Core/Model.php
Normal file
@@ -0,0 +1,73 @@
|
||||
<?php
|
||||
|
||||
namespace App\Core;
|
||||
|
||||
abstract class Model
|
||||
{
|
||||
protected static string $table = '';
|
||||
|
||||
protected static function db(): \PDO
|
||||
{
|
||||
return Database::getInstance();
|
||||
}
|
||||
|
||||
public static function findAll(string $orderBy = 'id DESC'): array
|
||||
{
|
||||
$table = static::$table;
|
||||
$stmt = static::db()->query("SELECT * FROM {$table} ORDER BY {$orderBy}");
|
||||
return $stmt->fetchAll();
|
||||
}
|
||||
|
||||
public static function find(int $id): ?array
|
||||
{
|
||||
$table = static::$table;
|
||||
$stmt = static::db()->prepare("SELECT * FROM {$table} WHERE id = :id");
|
||||
$stmt->execute(['id' => $id]);
|
||||
$result = $stmt->fetch();
|
||||
return $result ?: null;
|
||||
}
|
||||
|
||||
public static function create(array $data): int
|
||||
{
|
||||
$table = static::$table;
|
||||
$columns = implode(', ', array_keys($data));
|
||||
$placeholders = ':' . implode(', :', array_keys($data));
|
||||
|
||||
$stmt = static::db()->prepare("INSERT INTO {$table} ({$columns}) VALUES ({$placeholders})");
|
||||
$stmt->execute($data);
|
||||
return (int) static::db()->lastInsertId();
|
||||
}
|
||||
|
||||
public static function update(int $id, array $data): bool
|
||||
{
|
||||
$table = static::$table;
|
||||
$set = implode(', ', array_map(fn($k) => "{$k} = :{$k}", array_keys($data)));
|
||||
|
||||
$data['id'] = $id;
|
||||
$stmt = static::db()->prepare("UPDATE {$table} SET {$set} WHERE id = :id");
|
||||
return $stmt->execute($data);
|
||||
}
|
||||
|
||||
public static function delete(int $id): bool
|
||||
{
|
||||
$table = static::$table;
|
||||
$stmt = static::db()->prepare("DELETE FROM {$table} WHERE id = :id");
|
||||
return $stmt->execute(['id' => $id]);
|
||||
}
|
||||
|
||||
public static function count(string $where = '1=1', array $params = []): int
|
||||
{
|
||||
$table = static::$table;
|
||||
$stmt = static::db()->prepare("SELECT COUNT(*) FROM {$table} WHERE {$where}");
|
||||
$stmt->execute($params);
|
||||
return (int) $stmt->fetchColumn();
|
||||
}
|
||||
|
||||
public static function where(string $condition, array $params = [], string $orderBy = 'id DESC'): array
|
||||
{
|
||||
$table = static::$table;
|
||||
$stmt = static::db()->prepare("SELECT * FROM {$table} WHERE {$condition} ORDER BY {$orderBy}");
|
||||
$stmt->execute($params);
|
||||
return $stmt->fetchAll();
|
||||
}
|
||||
}
|
||||
70
src/Core/Router.php
Normal file
70
src/Core/Router.php
Normal file
@@ -0,0 +1,70 @@
|
||||
<?php
|
||||
|
||||
namespace App\Core;
|
||||
|
||||
class Router
|
||||
{
|
||||
private array $routes = [];
|
||||
|
||||
public function get(string $path, string $controller, string $method): void
|
||||
{
|
||||
$this->addRoute('GET', $path, $controller, $method);
|
||||
}
|
||||
|
||||
public function post(string $path, string $controller, string $method): void
|
||||
{
|
||||
$this->addRoute('POST', $path, $controller, $method);
|
||||
}
|
||||
|
||||
public function put(string $path, string $controller, string $method): void
|
||||
{
|
||||
$this->addRoute('PUT', $path, $controller, $method);
|
||||
}
|
||||
|
||||
public function delete(string $path, string $controller, string $method): void
|
||||
{
|
||||
$this->addRoute('DELETE', $path, $controller, $method);
|
||||
}
|
||||
|
||||
private function addRoute(string $httpMethod, string $path, string $controller, string $method): void
|
||||
{
|
||||
$pattern = preg_replace('/\{(\w+)\}/', '(?P<$1>[^/]+)', $path);
|
||||
$pattern = '#^' . $pattern . '$#';
|
||||
|
||||
$this->routes[] = [
|
||||
'httpMethod' => $httpMethod,
|
||||
'pattern' => $pattern,
|
||||
'controller' => $controller,
|
||||
'method' => $method,
|
||||
];
|
||||
}
|
||||
|
||||
public function dispatch(string $httpMethod, string $uri): void
|
||||
{
|
||||
// Support PUT/DELETE via POST with _method field
|
||||
if ($httpMethod === 'POST' && isset($_POST['_method'])) {
|
||||
$httpMethod = strtoupper($_POST['_method']);
|
||||
}
|
||||
|
||||
$uri = parse_url($uri, PHP_URL_PATH);
|
||||
$uri = rtrim($uri, '/') ?: '/';
|
||||
|
||||
foreach ($this->routes as $route) {
|
||||
if ($route['httpMethod'] !== $httpMethod) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (preg_match($route['pattern'], $uri, $matches)) {
|
||||
$params = array_filter($matches, 'is_string', ARRAY_FILTER_USE_KEY);
|
||||
|
||||
$controllerClass = "App\\Controllers\\{$route['controller']}";
|
||||
$controller = new $controllerClass();
|
||||
call_user_func_array([$controller, $route['method']], $params);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
http_response_code(404);
|
||||
echo '<h1>404 - Strona nie znaleziona</h1>';
|
||||
}
|
||||
}
|
||||
44
src/Core/View.php
Normal file
44
src/Core/View.php
Normal file
@@ -0,0 +1,44 @@
|
||||
<?php
|
||||
|
||||
namespace App\Core;
|
||||
|
||||
class View
|
||||
{
|
||||
private static string $basePath = '';
|
||||
|
||||
public static function setBasePath(string $path): void
|
||||
{
|
||||
self::$basePath = rtrim($path, '/');
|
||||
}
|
||||
|
||||
public static function render(string $template, array $data = []): void
|
||||
{
|
||||
extract($data);
|
||||
|
||||
$flashMessages = $_SESSION['flash'] ?? [];
|
||||
unset($_SESSION['flash']);
|
||||
|
||||
$contentFile = self::$basePath . "/templates/{$template}.php";
|
||||
|
||||
if (!file_exists($contentFile)) {
|
||||
throw new \RuntimeException("Template not found: {$template}");
|
||||
}
|
||||
|
||||
ob_start();
|
||||
require $contentFile;
|
||||
$content = ob_get_clean();
|
||||
|
||||
require self::$basePath . '/templates/layout/main.php';
|
||||
}
|
||||
|
||||
public static function renderPartial(string $template, array $data = []): void
|
||||
{
|
||||
extract($data);
|
||||
require self::$basePath . "/templates/{$template}.php";
|
||||
}
|
||||
|
||||
public static function escape(mixed $value): string
|
||||
{
|
||||
return htmlspecialchars((string) $value, ENT_QUOTES, 'UTF-8');
|
||||
}
|
||||
}
|
||||
44
src/Helpers/Logger.php
Normal file
44
src/Helpers/Logger.php
Normal file
@@ -0,0 +1,44 @@
|
||||
<?php
|
||||
|
||||
namespace App\Helpers;
|
||||
|
||||
class Logger
|
||||
{
|
||||
private static string $basePath = '';
|
||||
|
||||
public static function setBasePath(string $path): void
|
||||
{
|
||||
self::$basePath = rtrim($path, '/');
|
||||
}
|
||||
|
||||
public static function info(string $message, string $channel = 'app'): void
|
||||
{
|
||||
self::write('INFO', $message, $channel);
|
||||
}
|
||||
|
||||
public static function error(string $message, string $channel = 'app'): void
|
||||
{
|
||||
self::write('ERROR', $message, $channel);
|
||||
}
|
||||
|
||||
public static function warning(string $message, string $channel = 'app'): void
|
||||
{
|
||||
self::write('WARNING', $message, $channel);
|
||||
}
|
||||
|
||||
private static function write(string $level, string $message, string $channel): void
|
||||
{
|
||||
$date = date('Y-m-d');
|
||||
$time = date('Y-m-d H:i:s');
|
||||
$logDir = self::$basePath . '/storage/logs';
|
||||
|
||||
if (!is_dir($logDir)) {
|
||||
mkdir($logDir, 0755, true);
|
||||
}
|
||||
|
||||
$logFile = "{$logDir}/{$channel}_{$date}.log";
|
||||
$line = "[{$time}] {$level}: {$message}" . PHP_EOL;
|
||||
|
||||
file_put_contents($logFile, $line, FILE_APPEND | LOCK_EX);
|
||||
}
|
||||
}
|
||||
55
src/Helpers/Validator.php
Normal file
55
src/Helpers/Validator.php
Normal file
@@ -0,0 +1,55 @@
|
||||
<?php
|
||||
|
||||
namespace App\Helpers;
|
||||
|
||||
class Validator
|
||||
{
|
||||
private array $errors = [];
|
||||
|
||||
public function required(string $field, mixed $value, string $label = ''): self
|
||||
{
|
||||
if (empty(trim((string) $value))) {
|
||||
$this->errors[$field] = ($label ?: $field) . ' jest wymagane.';
|
||||
}
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function url(string $field, mixed $value, string $label = ''): self
|
||||
{
|
||||
if (!empty($value) && !filter_var($value, FILTER_VALIDATE_URL)) {
|
||||
$this->errors[$field] = ($label ?: $field) . ' musi być prawidłowym URL.';
|
||||
}
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function integer(string $field, mixed $value, string $label = ''): self
|
||||
{
|
||||
if (!empty($value) && !ctype_digit((string) $value)) {
|
||||
$this->errors[$field] = ($label ?: $field) . ' musi być liczbą całkowitą.';
|
||||
}
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function minLength(string $field, mixed $value, int $min, string $label = ''): self
|
||||
{
|
||||
if (!empty($value) && mb_strlen((string) $value) < $min) {
|
||||
$this->errors[$field] = ($label ?: $field) . " musi mieć co najmniej {$min} znaków.";
|
||||
}
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function isValid(): bool
|
||||
{
|
||||
return empty($this->errors);
|
||||
}
|
||||
|
||||
public function getErrors(): array
|
||||
{
|
||||
return $this->errors;
|
||||
}
|
||||
|
||||
public function getFirstError(): ?string
|
||||
{
|
||||
return $this->errors ? reset($this->errors) : null;
|
||||
}
|
||||
}
|
||||
82
src/Models/Article.php
Normal file
82
src/Models/Article.php
Normal file
@@ -0,0 +1,82 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use App\Core\Model;
|
||||
|
||||
class Article extends Model
|
||||
{
|
||||
protected static string $table = 'articles';
|
||||
|
||||
public static function findBySite(int $siteId, int $limit = 50): array
|
||||
{
|
||||
$stmt = self::db()->prepare(
|
||||
"SELECT a.*, t.name as topic_name, s.name as site_name
|
||||
FROM articles a
|
||||
JOIN topics t ON a.topic_id = t.id
|
||||
JOIN sites s ON a.site_id = s.id
|
||||
WHERE a.site_id = :site_id
|
||||
ORDER BY a.created_at DESC
|
||||
LIMIT {$limit}"
|
||||
);
|
||||
$stmt->execute(['site_id' => $siteId]);
|
||||
return $stmt->fetchAll();
|
||||
}
|
||||
|
||||
public static function findAllWithRelations(int $limit = 50, int $offset = 0): array
|
||||
{
|
||||
$stmt = self::db()->prepare(
|
||||
"SELECT a.*, t.name as topic_name, s.name as site_name
|
||||
FROM articles a
|
||||
JOIN topics t ON a.topic_id = t.id
|
||||
JOIN sites s ON a.site_id = s.id
|
||||
ORDER BY a.created_at DESC
|
||||
LIMIT :limit OFFSET :offset"
|
||||
);
|
||||
$stmt->bindValue('limit', $limit, \PDO::PARAM_INT);
|
||||
$stmt->bindValue('offset', $offset, \PDO::PARAM_INT);
|
||||
$stmt->execute();
|
||||
return $stmt->fetchAll();
|
||||
}
|
||||
|
||||
public static function findWithRelations(int $id): ?array
|
||||
{
|
||||
$stmt = self::db()->prepare(
|
||||
"SELECT a.*, t.name as topic_name, s.name as site_name, s.url as site_url
|
||||
FROM articles a
|
||||
JOIN topics t ON a.topic_id = t.id
|
||||
JOIN sites s ON a.site_id = s.id
|
||||
WHERE a.id = :id"
|
||||
);
|
||||
$stmt->execute(['id' => $id]);
|
||||
$result = $stmt->fetch();
|
||||
return $result ?: null;
|
||||
}
|
||||
|
||||
public static function getRecentTitlesByTopic(int $topicId, int $limit = 20): array
|
||||
{
|
||||
$stmt = self::db()->prepare(
|
||||
"SELECT title FROM articles
|
||||
WHERE topic_id = :topic_id
|
||||
ORDER BY created_at DESC
|
||||
LIMIT {$limit}"
|
||||
);
|
||||
$stmt->execute(['topic_id' => $topicId]);
|
||||
return $stmt->fetchAll(\PDO::FETCH_COLUMN);
|
||||
}
|
||||
|
||||
public static function getStats(): array
|
||||
{
|
||||
$db = self::db();
|
||||
|
||||
$total = $db->query("SELECT COUNT(*) FROM articles")->fetchColumn();
|
||||
$published = $db->query("SELECT COUNT(*) FROM articles WHERE status = 'published'")->fetchColumn();
|
||||
$failed = $db->query("SELECT COUNT(*) FROM articles WHERE status = 'failed'")->fetchColumn();
|
||||
|
||||
return [
|
||||
'total' => (int) $total,
|
||||
'published' => (int) $published,
|
||||
'failed' => (int) $failed,
|
||||
];
|
||||
}
|
||||
}
|
||||
41
src/Models/GlobalTopic.php
Normal file
41
src/Models/GlobalTopic.php
Normal file
@@ -0,0 +1,41 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use App\Core\Model;
|
||||
|
||||
class GlobalTopic extends Model
|
||||
{
|
||||
protected static string $table = 'global_topics';
|
||||
|
||||
public static function findCategories(): array
|
||||
{
|
||||
return self::where('parent_id IS NULL', [], 'name ASC');
|
||||
}
|
||||
|
||||
public static function findByParent(int $parentId): array
|
||||
{
|
||||
return self::where('parent_id = :pid', ['pid' => $parentId], 'name ASC');
|
||||
}
|
||||
|
||||
public static function findAllGrouped(): array
|
||||
{
|
||||
$categories = self::findCategories();
|
||||
foreach ($categories as &$cat) {
|
||||
$cat['children'] = self::findByParent($cat['id']);
|
||||
}
|
||||
return $categories;
|
||||
}
|
||||
|
||||
public static function findAllFlat(): array
|
||||
{
|
||||
$db = self::db();
|
||||
$stmt = $db->query(
|
||||
"SELECT g.*, p.name as parent_name
|
||||
FROM global_topics g
|
||||
LEFT JOIN global_topics p ON g.parent_id = p.id
|
||||
ORDER BY COALESCE(p.name, g.name), g.parent_id IS NULL DESC, g.name ASC"
|
||||
);
|
||||
return $stmt->fetchAll();
|
||||
}
|
||||
}
|
||||
33
src/Models/Site.php
Normal file
33
src/Models/Site.php
Normal file
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use App\Core\Model;
|
||||
|
||||
class Site extends Model
|
||||
{
|
||||
protected static string $table = 'sites';
|
||||
|
||||
public static function findActive(): array
|
||||
{
|
||||
return self::where('is_active = 1', [], 'last_published_at ASC');
|
||||
}
|
||||
|
||||
public static function findDueForPublishing(): array
|
||||
{
|
||||
$sql = "SELECT * FROM sites
|
||||
WHERE is_active = 1
|
||||
AND (
|
||||
last_published_at IS NULL
|
||||
OR DATE_ADD(last_published_at, INTERVAL publish_interval_days DAY) <= NOW()
|
||||
)
|
||||
ORDER BY last_published_at ASC";
|
||||
$stmt = self::db()->query($sql);
|
||||
return $stmt->fetchAll();
|
||||
}
|
||||
|
||||
public static function updateLastPublished(int $id): void
|
||||
{
|
||||
self::update($id, ['last_published_at' => date('Y-m-d H:i:s')]);
|
||||
}
|
||||
}
|
||||
42
src/Models/Topic.php
Normal file
42
src/Models/Topic.php
Normal file
@@ -0,0 +1,42 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use App\Core\Model;
|
||||
|
||||
class Topic extends Model
|
||||
{
|
||||
protected static string $table = 'topics';
|
||||
|
||||
public static function findBySite(int $siteId): array
|
||||
{
|
||||
return self::where('site_id = :site_id', ['site_id' => $siteId], 'name ASC');
|
||||
}
|
||||
|
||||
public static function findBySiteWithGlobal(int $siteId): array
|
||||
{
|
||||
$db = self::db();
|
||||
$stmt = $db->prepare(
|
||||
"SELECT t.*, g.name as global_topic_name, gp.name as global_category_name
|
||||
FROM topics t
|
||||
LEFT JOIN global_topics g ON t.global_topic_id = g.id
|
||||
LEFT JOIN global_topics gp ON g.parent_id = gp.id
|
||||
WHERE t.site_id = :site_id
|
||||
ORDER BY t.name ASC"
|
||||
);
|
||||
$stmt->execute(['site_id' => $siteId]);
|
||||
return $stmt->fetchAll();
|
||||
}
|
||||
|
||||
public static function findActiveBySite(int $siteId): array
|
||||
{
|
||||
return self::where('site_id = :site_id AND is_active = 1', ['site_id' => $siteId], 'article_count ASC, RAND()');
|
||||
}
|
||||
|
||||
public static function incrementArticleCount(int $id): void
|
||||
{
|
||||
$db = self::db();
|
||||
$stmt = $db->prepare("UPDATE topics SET article_count = article_count + 1 WHERE id = :id");
|
||||
$stmt->execute(['id' => $id]);
|
||||
}
|
||||
}
|
||||
10
src/Models/User.php
Normal file
10
src/Models/User.php
Normal file
@@ -0,0 +1,10 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use App\Core\Model;
|
||||
|
||||
class User extends Model
|
||||
{
|
||||
protected static string $table = 'users';
|
||||
}
|
||||
165
src/Services/ImageService.php
Normal file
165
src/Services/ImageService.php
Normal file
@@ -0,0 +1,165 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use GuzzleHttp\Client;
|
||||
use GuzzleHttp\Exception\GuzzleException;
|
||||
use App\Core\Config;
|
||||
use App\Helpers\Logger;
|
||||
|
||||
class ImageService
|
||||
{
|
||||
private Client $client;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->client = new Client(['timeout' => 60]);
|
||||
}
|
||||
|
||||
public function generate(string $articleTitle, string $topicName): ?array
|
||||
{
|
||||
$provider = Config::getDbSetting('image_provider', 'freepik');
|
||||
|
||||
return match ($provider) {
|
||||
'freepik' => $this->generateFreepik($articleTitle, $topicName),
|
||||
'unsplash' => $this->searchUnsplash($topicName),
|
||||
'pexels' => $this->searchPexels($topicName),
|
||||
default => $this->searchPexels($topicName),
|
||||
};
|
||||
}
|
||||
|
||||
private function generateFreepik(string $articleTitle, string $topicName): ?array
|
||||
{
|
||||
$apiKey = Config::getDbSetting('freepik_api_key', Config::get('FREEPIK_API_KEY'));
|
||||
|
||||
if (empty($apiKey)) {
|
||||
Logger::warning('Freepik API key not configured, falling back to Pexels', 'image');
|
||||
return $this->searchPexels($topicName);
|
||||
}
|
||||
|
||||
try {
|
||||
$prompt = "Professional blog header image about {$topicName}: {$articleTitle}, high quality, photorealistic";
|
||||
|
||||
$response = $this->client->post('https://api.freepik.com/v1/ai/text-to-image', [
|
||||
'headers' => [
|
||||
'x-freepik-api-key' => $apiKey,
|
||||
'Content-Type' => 'application/json',
|
||||
],
|
||||
'json' => [
|
||||
'prompt' => $prompt,
|
||||
'negative_prompt' => 'text, watermark, logo, blurry, low quality',
|
||||
'image' => ['size' => 'landscape_16_9'],
|
||||
],
|
||||
]);
|
||||
|
||||
$data = json_decode($response->getBody()->getContents(), true);
|
||||
|
||||
if (!empty($data['data'][0]['base64'])) {
|
||||
$imageData = base64_decode($data['data'][0]['base64']);
|
||||
return [
|
||||
'data' => $imageData,
|
||||
'filename' => 'article-' . time() . '.jpg',
|
||||
'mime' => 'image/jpeg',
|
||||
];
|
||||
}
|
||||
|
||||
Logger::error('Freepik returned empty image data', 'image');
|
||||
return null;
|
||||
} catch (GuzzleException $e) {
|
||||
Logger::error('Freepik API error: ' . $e->getMessage(), 'image');
|
||||
return $this->searchPexels($topicName);
|
||||
}
|
||||
}
|
||||
|
||||
private function searchUnsplash(string $query): ?array
|
||||
{
|
||||
$apiKey = Config::getDbSetting('unsplash_api_key', Config::get('UNSPLASH_API_KEY'));
|
||||
|
||||
if (empty($apiKey)) {
|
||||
Logger::warning('Unsplash API key not configured', 'image');
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
$response = $this->client->get('https://api.unsplash.com/search/photos', [
|
||||
'headers' => ['Authorization' => "Client-ID {$apiKey}"],
|
||||
'query' => [
|
||||
'query' => $query,
|
||||
'orientation' => 'landscape',
|
||||
'per_page' => 5,
|
||||
],
|
||||
]);
|
||||
|
||||
$data = json_decode($response->getBody()->getContents(), true);
|
||||
$results = $data['results'] ?? [];
|
||||
|
||||
if (empty($results)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$photo = $results[array_rand($results)];
|
||||
$imageUrl = $photo['urls']['regular'] ?? null;
|
||||
|
||||
if (!$imageUrl) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$imageData = $this->client->get($imageUrl)->getBody()->getContents();
|
||||
|
||||
return [
|
||||
'data' => $imageData,
|
||||
'filename' => 'article-' . time() . '.jpg',
|
||||
'mime' => 'image/jpeg',
|
||||
];
|
||||
} catch (GuzzleException $e) {
|
||||
Logger::error('Unsplash API error: ' . $e->getMessage(), 'image');
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private function searchPexels(string $query): ?array
|
||||
{
|
||||
$apiKey = Config::getDbSetting('pexels_api_key', Config::get('PEXELS_API_KEY'));
|
||||
|
||||
if (empty($apiKey)) {
|
||||
Logger::warning('Pexels API key not configured', 'image');
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
$response = $this->client->get('https://api.pexels.com/v1/search', [
|
||||
'headers' => ['Authorization' => $apiKey],
|
||||
'query' => [
|
||||
'query' => $query,
|
||||
'orientation' => 'landscape',
|
||||
'per_page' => 5,
|
||||
],
|
||||
]);
|
||||
|
||||
$data = json_decode($response->getBody()->getContents(), true);
|
||||
$photos = $data['photos'] ?? [];
|
||||
|
||||
if (empty($photos)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$photo = $photos[array_rand($photos)];
|
||||
$imageUrl = $photo['src']['large'] ?? $photo['src']['original'] ?? null;
|
||||
|
||||
if (!$imageUrl) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$imageData = $this->client->get($imageUrl)->getBody()->getContents();
|
||||
|
||||
return [
|
||||
'data' => $imageData,
|
||||
'filename' => 'article-' . time() . '.jpg',
|
||||
'mime' => 'image/jpeg',
|
||||
];
|
||||
} catch (GuzzleException $e) {
|
||||
Logger::error('Pexels API error: ' . $e->getMessage(), 'image');
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
98
src/Services/OpenAIService.php
Normal file
98
src/Services/OpenAIService.php
Normal file
@@ -0,0 +1,98 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use GuzzleHttp\Client;
|
||||
use GuzzleHttp\Exception\GuzzleException;
|
||||
use App\Core\Config;
|
||||
use App\Helpers\Logger;
|
||||
|
||||
class OpenAIService
|
||||
{
|
||||
private Client $client;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->client = new Client([
|
||||
'base_uri' => 'https://api.openai.com/v1/',
|
||||
'timeout' => 120,
|
||||
]);
|
||||
}
|
||||
|
||||
public function generateArticle(string $topicName, string $topicDescription, array $existingTitles): ?array
|
||||
{
|
||||
$apiKey = Config::getDbSetting('openai_api_key', Config::get('OPENAI_API_KEY'));
|
||||
$model = Config::getDbSetting('openai_model', Config::get('OPENAI_MODEL', 'gpt-4o'));
|
||||
$minWords = Config::getDbSetting('article_min_words', '800');
|
||||
$maxWords = Config::getDbSetting('article_max_words', '1200');
|
||||
|
||||
if (empty($apiKey)) {
|
||||
Logger::error('OpenAI API key not configured', 'openai');
|
||||
return null;
|
||||
}
|
||||
|
||||
$existingList = !empty($existingTitles)
|
||||
? implode("\n- ", $existingTitles)
|
||||
: '(brak - to pierwszy artykuł z tego tematu)';
|
||||
|
||||
$systemPrompt = "Jesteś doświadczonym copywriterem SEO. Pisz artykuły w języku polskim, "
|
||||
. "optymalizowane pod SEO. Artykuł powinien mieć {$minWords}-{$maxWords} słów, "
|
||||
. "zawierać nagłówki H2 i H3, być angażujący i merytoryczny. "
|
||||
. "Formatuj treść w HTML (bez tagów <html>, <body>, <head>). "
|
||||
. "Zwróć odpowiedź WYŁĄCZNIE w formacie JSON: {\"title\": \"tytuł artykułu\", \"content\": \"treść HTML artykułu\"}";
|
||||
|
||||
$userPrompt = "Napisz artykuł na temat: {$topicName}\n";
|
||||
if (!empty($topicDescription)) {
|
||||
$userPrompt .= "Wytyczne: {$topicDescription}\n";
|
||||
}
|
||||
$userPrompt .= "\nWAŻNE - NIE pisz o następujących tematach, bo artykuły o nich już istnieją na stronie:\n- {$existingList}";
|
||||
|
||||
$fullPrompt = $systemPrompt . "\n\n" . $userPrompt;
|
||||
|
||||
try {
|
||||
$response = $this->client->post('chat/completions', [
|
||||
'headers' => [
|
||||
'Authorization' => "Bearer {$apiKey}",
|
||||
'Content-Type' => 'application/json',
|
||||
],
|
||||
'json' => [
|
||||
'model' => $model,
|
||||
'messages' => [
|
||||
['role' => 'system', 'content' => $systemPrompt],
|
||||
['role' => 'user', 'content' => $userPrompt],
|
||||
],
|
||||
'temperature' => 0.8,
|
||||
'max_tokens' => 4000,
|
||||
'response_format' => ['type' => 'json_object'],
|
||||
],
|
||||
]);
|
||||
|
||||
$data = json_decode($response->getBody()->getContents(), true);
|
||||
$content = $data['choices'][0]['message']['content'] ?? null;
|
||||
|
||||
if (!$content) {
|
||||
Logger::error('Empty response from OpenAI', 'openai');
|
||||
return null;
|
||||
}
|
||||
|
||||
$article = json_decode($content, true);
|
||||
|
||||
if (!isset($article['title']) || !isset($article['content'])) {
|
||||
Logger::error('Invalid JSON structure from OpenAI: ' . $content, 'openai');
|
||||
return null;
|
||||
}
|
||||
|
||||
Logger::info("Generated article: {$article['title']}", 'openai');
|
||||
|
||||
return [
|
||||
'title' => $article['title'],
|
||||
'content' => $article['content'],
|
||||
'model' => $model,
|
||||
'prompt' => $fullPrompt,
|
||||
];
|
||||
} catch (GuzzleException $e) {
|
||||
Logger::error('OpenAI API error: ' . $e->getMessage(), 'openai');
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
139
src/Services/PublisherService.php
Normal file
139
src/Services/PublisherService.php
Normal file
@@ -0,0 +1,139 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use App\Models\Site;
|
||||
use App\Models\Topic;
|
||||
use App\Models\Article;
|
||||
use App\Helpers\Logger;
|
||||
|
||||
class PublisherService
|
||||
{
|
||||
private TopicBalancer $topicBalancer;
|
||||
private OpenAIService $openAI;
|
||||
private ImageService $imageService;
|
||||
private WordPressService $wordpress;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->topicBalancer = new TopicBalancer();
|
||||
$this->openAI = new OpenAIService();
|
||||
$this->imageService = new ImageService();
|
||||
$this->wordpress = new WordPressService();
|
||||
}
|
||||
|
||||
public function publishNext(): array
|
||||
{
|
||||
Logger::info('Rozpoczynam automatyczną publikację', 'publish');
|
||||
|
||||
$sites = Site::findDueForPublishing();
|
||||
|
||||
if (empty($sites)) {
|
||||
Logger::info('Brak stron do publikacji', 'publish');
|
||||
return ['success' => false, 'message' => 'Brak stron wymagających publikacji.'];
|
||||
}
|
||||
|
||||
$site = $sites[0];
|
||||
return $this->publishForSite($site);
|
||||
}
|
||||
|
||||
public function publishForSite(array $site): array
|
||||
{
|
||||
Logger::info("Publikacja dla strony: {$site['name']} (ID: {$site['id']})", 'publish');
|
||||
|
||||
// 1. Select topic
|
||||
$topic = $this->topicBalancer->getNextTopic($site['id']);
|
||||
if (!$topic) {
|
||||
Logger::error("Brak aktywnych tematów dla strony {$site['name']}", 'publish');
|
||||
return ['success' => false, 'message' => "Brak aktywnych tematów dla strony {$site['name']}."];
|
||||
}
|
||||
|
||||
Logger::info("Wybrany temat: {$topic['name']} (ID: {$topic['id']})", 'publish');
|
||||
|
||||
// 2. Get existing titles to avoid repetition
|
||||
$existingTitles = Article::getRecentTitlesByTopic($topic['id'], 20);
|
||||
|
||||
// 3. Generate article
|
||||
$article = $this->openAI->generateArticle(
|
||||
$topic['name'],
|
||||
$topic['description'] ?? '',
|
||||
$existingTitles
|
||||
);
|
||||
|
||||
if (!$article) {
|
||||
$this->saveFailedArticle($site, $topic, 'Nie udało się wygenerować artykułu przez OpenAI.');
|
||||
return ['success' => false, 'message' => 'Błąd generowania artykułu przez AI.'];
|
||||
}
|
||||
|
||||
Logger::info("Wygenerowano artykuł: {$article['title']}", 'publish');
|
||||
|
||||
// 4. Generate/fetch image
|
||||
$imageUrl = null;
|
||||
$mediaId = null;
|
||||
$image = $this->imageService->generate($article['title'], $topic['name']);
|
||||
|
||||
if ($image) {
|
||||
$mediaId = $this->wordpress->uploadMedia($site, $image['data'], $image['filename']);
|
||||
if ($mediaId) {
|
||||
Logger::info("Upload obrazka: media_id={$mediaId}", 'publish');
|
||||
}
|
||||
} else {
|
||||
Logger::warning('Nie udało się wygenerować obrazka, publikacja bez obrazka', 'publish');
|
||||
}
|
||||
|
||||
// 5. Publish to WordPress
|
||||
$wpPostId = $this->wordpress->createPost(
|
||||
$site,
|
||||
$article['title'],
|
||||
$article['content'],
|
||||
$topic['wp_category_id'],
|
||||
$mediaId
|
||||
);
|
||||
|
||||
if (!$wpPostId) {
|
||||
$this->saveFailedArticle($site, $topic, 'Nie udało się opublikować posta na WordPress.', $article);
|
||||
return ['success' => false, 'message' => 'Błąd publikacji na WordPress.'];
|
||||
}
|
||||
|
||||
Logger::info("Opublikowano post: wp_post_id={$wpPostId}", 'publish');
|
||||
|
||||
// 6. Save article in database
|
||||
Article::create([
|
||||
'site_id' => $site['id'],
|
||||
'topic_id' => $topic['id'],
|
||||
'title' => $article['title'],
|
||||
'content' => $article['content'],
|
||||
'wp_post_id' => $wpPostId,
|
||||
'image_url' => $imageUrl,
|
||||
'status' => 'published',
|
||||
'ai_model' => $article['model'],
|
||||
'prompt_used' => $article['prompt'],
|
||||
'published_at' => date('Y-m-d H:i:s'),
|
||||
]);
|
||||
|
||||
// 7. Update counters
|
||||
Topic::incrementArticleCount($topic['id']);
|
||||
Site::updateLastPublished($site['id']);
|
||||
|
||||
$message = "Opublikowano: \"{$article['title']}\" na {$site['name']}";
|
||||
Logger::info($message, 'publish');
|
||||
|
||||
return ['success' => true, 'message' => $message];
|
||||
}
|
||||
|
||||
private function saveFailedArticle(array $site, array $topic, string $error, ?array $article = null): void
|
||||
{
|
||||
Article::create([
|
||||
'site_id' => $site['id'],
|
||||
'topic_id' => $topic['id'],
|
||||
'title' => $article['title'] ?? 'FAILED - nie wygenerowano',
|
||||
'content' => $article['content'] ?? '',
|
||||
'status' => 'failed',
|
||||
'ai_model' => $article['model'] ?? null,
|
||||
'prompt_used' => $article['prompt'] ?? null,
|
||||
'error_message' => $error,
|
||||
]);
|
||||
|
||||
Logger::error("Publikacja nieudana: {$error}", 'publish');
|
||||
}
|
||||
}
|
||||
21
src/Services/TopicBalancer.php
Normal file
21
src/Services/TopicBalancer.php
Normal file
@@ -0,0 +1,21 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use App\Models\Topic;
|
||||
|
||||
class TopicBalancer
|
||||
{
|
||||
public function getNextTopic(int $siteId): ?array
|
||||
{
|
||||
$topics = Topic::findActiveBySite($siteId);
|
||||
|
||||
if (empty($topics)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Topics are already ordered by article_count ASC, RAND()
|
||||
// So the first one has the fewest articles (with random tiebreaker)
|
||||
return $topics[0];
|
||||
}
|
||||
}
|
||||
117
src/Services/WordPressService.php
Normal file
117
src/Services/WordPressService.php
Normal file
@@ -0,0 +1,117 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use GuzzleHttp\Client;
|
||||
use GuzzleHttp\Exception\GuzzleException;
|
||||
use App\Helpers\Logger;
|
||||
|
||||
class WordPressService
|
||||
{
|
||||
private Client $client;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->client = new Client(['timeout' => 30]);
|
||||
}
|
||||
|
||||
public function testConnection(array $site): array
|
||||
{
|
||||
try {
|
||||
$response = $this->client->get($site['url'] . '/wp-json/wp/v2/posts', [
|
||||
'auth' => [$site['api_user'], $site['api_token']],
|
||||
'query' => ['per_page' => 1],
|
||||
]);
|
||||
|
||||
if ($response->getStatusCode() === 200) {
|
||||
return ['success' => true, 'message' => 'Połączenie OK'];
|
||||
}
|
||||
|
||||
return ['success' => false, 'message' => 'Nieoczekiwany kod: ' . $response->getStatusCode()];
|
||||
} catch (GuzzleException $e) {
|
||||
Logger::error("WP test connection failed for {$site['url']}: " . $e->getMessage(), 'wordpress');
|
||||
return ['success' => false, 'message' => 'Błąd: ' . $e->getMessage()];
|
||||
}
|
||||
}
|
||||
|
||||
public function getCategories(array $site): array|false
|
||||
{
|
||||
try {
|
||||
$response = $this->client->get($site['url'] . '/wp-json/wp/v2/categories', [
|
||||
'auth' => [$site['api_user'], $site['api_token']],
|
||||
'query' => ['per_page' => 100],
|
||||
]);
|
||||
|
||||
return json_decode($response->getBody()->getContents(), true);
|
||||
} catch (GuzzleException $e) {
|
||||
Logger::error("WP getCategories failed for {$site['url']}: " . $e->getMessage(), 'wordpress');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public function uploadMedia(array $site, string $imageData, string $filename): ?int
|
||||
{
|
||||
try {
|
||||
$response = $this->client->post($site['url'] . '/wp-json/wp/v2/media', [
|
||||
'auth' => [$site['api_user'], $site['api_token']],
|
||||
'headers' => [
|
||||
'Content-Disposition' => "attachment; filename=\"{$filename}\"",
|
||||
'Content-Type' => $this->getMimeType($filename),
|
||||
],
|
||||
'body' => $imageData,
|
||||
]);
|
||||
|
||||
$data = json_decode($response->getBody()->getContents(), true);
|
||||
return $data['id'] ?? null;
|
||||
} catch (GuzzleException $e) {
|
||||
Logger::error("WP uploadMedia failed for {$site['url']}: " . $e->getMessage(), 'wordpress');
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public function createPost(
|
||||
array $site,
|
||||
string $title,
|
||||
string $content,
|
||||
?int $categoryId = null,
|
||||
?int $mediaId = null
|
||||
): ?int {
|
||||
try {
|
||||
$postData = [
|
||||
'title' => $title,
|
||||
'content' => $content,
|
||||
'status' => 'publish',
|
||||
];
|
||||
|
||||
if ($categoryId) {
|
||||
$postData['categories'] = [$categoryId];
|
||||
}
|
||||
|
||||
if ($mediaId) {
|
||||
$postData['featured_media'] = $mediaId;
|
||||
}
|
||||
|
||||
$response = $this->client->post($site['url'] . '/wp-json/wp/v2/posts', [
|
||||
'auth' => [$site['api_user'], $site['api_token']],
|
||||
'json' => $postData,
|
||||
]);
|
||||
|
||||
$data = json_decode($response->getBody()->getContents(), true);
|
||||
return $data['id'] ?? null;
|
||||
} catch (GuzzleException $e) {
|
||||
Logger::error("WP createPost failed for {$site['url']}: " . $e->getMessage(), 'wordpress');
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private function getMimeType(string $filename): string
|
||||
{
|
||||
$ext = strtolower(pathinfo($filename, PATHINFO_EXTENSION));
|
||||
return match ($ext) {
|
||||
'png' => 'image/png',
|
||||
'gif' => 'image/gif',
|
||||
'webp' => 'image/webp',
|
||||
default => 'image/jpeg',
|
||||
};
|
||||
}
|
||||
}
|
||||
66
templates/articles/index.php
Normal file
66
templates/articles/index.php
Normal file
@@ -0,0 +1,66 @@
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<h2>Artykuły <small class="text-muted">(<?= $total ?>)</small></h2>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-body p-0">
|
||||
<table class="table table-hover mb-0">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>#</th>
|
||||
<th>Tytuł</th>
|
||||
<th>Strona</th>
|
||||
<th>Temat</th>
|
||||
<th>Status</th>
|
||||
<th>Data</th>
|
||||
<th>Akcje</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php if (empty($articles)): ?>
|
||||
<tr><td colspan="7" class="text-center text-muted py-4">Brak artykułów</td></tr>
|
||||
<?php else: ?>
|
||||
<?php foreach ($articles as $article): ?>
|
||||
<tr>
|
||||
<td><?= $article['id'] ?></td>
|
||||
<td>
|
||||
<a href="/articles/<?= $article['id'] ?>">
|
||||
<?= htmlspecialchars(mb_strimwidth($article['title'], 0, 50, '...')) ?>
|
||||
</a>
|
||||
</td>
|
||||
<td><?= htmlspecialchars($article['site_name']) ?></td>
|
||||
<td><span class="badge bg-secondary"><?= htmlspecialchars($article['topic_name']) ?></span></td>
|
||||
<td>
|
||||
<?php if ($article['status'] === 'published'): ?>
|
||||
<span class="badge bg-success">Opublikowany</span>
|
||||
<?php elseif ($article['status'] === 'failed'): ?>
|
||||
<span class="badge bg-danger" title="<?= htmlspecialchars($article['error_message'] ?? '') ?>">Błąd</span>
|
||||
<?php else: ?>
|
||||
<span class="badge bg-warning">Wygenerowany</span>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
<td class="small"><?= $article['published_at'] ? date('d.m.Y H:i', strtotime($article['published_at'])) : date('d.m.Y H:i', strtotime($article['created_at'])) ?></td>
|
||||
<td>
|
||||
<a href="/articles/<?= $article['id'] ?>" class="btn btn-sm btn-outline-primary">
|
||||
<i class="bi bi-eye"></i>
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
<?php endif; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php if ($totalPages > 1): ?>
|
||||
<nav class="mt-3">
|
||||
<ul class="pagination justify-content-center">
|
||||
<?php for ($i = 1; $i <= $totalPages; $i++): ?>
|
||||
<li class="page-item <?= $i === $page ? 'active' : '' ?>">
|
||||
<a class="page-link" href="/articles?page=<?= $i ?>"><?= $i ?></a>
|
||||
</li>
|
||||
<?php endfor; ?>
|
||||
</ul>
|
||||
</nav>
|
||||
<?php endif; ?>
|
||||
51
templates/articles/show.php
Normal file
51
templates/articles/show.php
Normal file
@@ -0,0 +1,51 @@
|
||||
<div class="mb-4">
|
||||
<a href="/articles" class="text-muted small">← Powrót do listy</a>
|
||||
<h2 class="mt-2"><?= htmlspecialchars($article['title']) ?></h2>
|
||||
<div class="d-flex gap-3 text-muted small">
|
||||
<span><i class="bi bi-globe me-1"></i><?= htmlspecialchars($article['site_name']) ?></span>
|
||||
<span><i class="bi bi-tag me-1"></i><?= htmlspecialchars($article['topic_name']) ?></span>
|
||||
<span><i class="bi bi-calendar me-1"></i><?= date('d.m.Y H:i', strtotime($article['created_at'])) ?></span>
|
||||
<span><i class="bi bi-cpu me-1"></i><?= htmlspecialchars($article['ai_model'] ?? 'N/A') ?></span>
|
||||
<?php if ($article['status'] === 'published'): ?>
|
||||
<span class="badge bg-success">Opublikowany</span>
|
||||
<?php elseif ($article['status'] === 'failed'): ?>
|
||||
<span class="badge bg-danger">Błąd</span>
|
||||
<?php else: ?>
|
||||
<span class="badge bg-warning">Wygenerowany</span>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php if ($article['status'] === 'failed' && $article['error_message']): ?>
|
||||
<div class="alert alert-danger">
|
||||
<strong>Błąd:</strong> <?= htmlspecialchars($article['error_message']) ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if ($article['wp_post_id'] && !empty($article['site_url'])): ?>
|
||||
<div class="alert alert-info">
|
||||
<i class="bi bi-wordpress me-1"></i>
|
||||
Post WordPress ID: <strong><?= $article['wp_post_id'] ?></strong>
|
||||
| <a href="<?= htmlspecialchars($article['site_url']) ?>/?p=<?= $article['wp_post_id'] ?>" target="_blank">Zobacz na stronie</a>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<div class="card mb-4">
|
||||
<div class="card-header">
|
||||
<h5 class="mb-0">Treść artykułu</h5>
|
||||
</div>
|
||||
<div class="card-body article-content">
|
||||
<?= $article['content'] ?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php if (!empty($article['prompt_used'])): ?>
|
||||
<div class="card mb-4">
|
||||
<div class="card-header">
|
||||
<h5 class="mb-0">Prompt użyty do generowania</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<pre class="mb-0" style="white-space: pre-wrap;"><?= htmlspecialchars($article['prompt_used']) ?></pre>
|
||||
</div>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
32
templates/auth/change-password.php
Normal file
32
templates/auth/change-password.php
Normal file
@@ -0,0 +1,32 @@
|
||||
<h2 class="mb-4">Zmiana hasła</h2>
|
||||
|
||||
<div class="card" style="max-width: 500px;">
|
||||
<div class="card-body">
|
||||
<?php if (!empty($error)): ?>
|
||||
<div class="alert alert-danger"><?= htmlspecialchars($error) ?></div>
|
||||
<?php endif; ?>
|
||||
|
||||
<form method="post" action="/change-password">
|
||||
<div class="mb-3">
|
||||
<label for="current_password" class="form-label">Aktualne hasło</label>
|
||||
<input type="password" class="form-control" id="current_password" name="current_password" required>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="new_password" class="form-label">Nowe hasło</label>
|
||||
<input type="password" class="form-control" id="new_password" name="new_password" required minlength="6">
|
||||
<div class="form-text">Minimum 6 znaków</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="confirm_password" class="form-label">Powtórz nowe hasło</label>
|
||||
<input type="password" class="form-control" id="confirm_password" name="confirm_password" required>
|
||||
</div>
|
||||
|
||||
<div class="d-flex gap-2">
|
||||
<button type="submit" class="btn btn-primary">Zmień hasło</button>
|
||||
<a href="/" class="btn btn-outline-secondary">Anuluj</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
26
templates/auth/login.php
Normal file
26
templates/auth/login.php
Normal file
@@ -0,0 +1,26 @@
|
||||
<div class="d-flex align-items-center justify-content-center" style="min-height: 100vh; background: #f8f9fa;">
|
||||
<div class="card shadow" style="width: 400px;">
|
||||
<div class="card-body p-4">
|
||||
<div class="text-center mb-4">
|
||||
<h3><i class="bi bi-globe2"></i> BackPRO</h3>
|
||||
<p class="text-muted">Zarządzanie Zapleczem SEO</p>
|
||||
</div>
|
||||
|
||||
<?php if (!empty($error)): ?>
|
||||
<div class="alert alert-danger"><?= htmlspecialchars($error) ?></div>
|
||||
<?php endif; ?>
|
||||
|
||||
<form method="post" action="/login">
|
||||
<div class="mb-3">
|
||||
<label for="username" class="form-label">Login</label>
|
||||
<input type="text" class="form-control" id="username" name="username" required autofocus>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="password" class="form-label">Hasło</label>
|
||||
<input type="password" class="form-control" id="password" name="password" required>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary w-100">Zaloguj się</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
45
templates/categories/index.php
Normal file
45
templates/categories/index.php
Normal file
@@ -0,0 +1,45 @@
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<div>
|
||||
<h2>Kategorie WordPress: <?= htmlspecialchars($site['name']) ?></h2>
|
||||
<a href="/sites" class="text-muted small">← Powrót do stron</a>
|
||||
</div>
|
||||
<form method="post" action="/sites/<?= $site['id'] ?>/categories/sync">
|
||||
<button type="submit" class="btn btn-primary">
|
||||
<i class="bi bi-arrow-repeat me-1"></i>Synchronizuj z WordPress
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="card" style="max-width: 700px;">
|
||||
<div class="card-body p-0">
|
||||
<table class="table table-hover mb-0">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>Nazwa</th>
|
||||
<th>Slug</th>
|
||||
<th>Ilość postów</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php if (empty($categories) || $categories === false): ?>
|
||||
<tr><td colspan="4" class="text-center text-muted py-4">Kliknij "Synchronizuj" aby pobrać kategorie z WordPressa</td></tr>
|
||||
<?php else: ?>
|
||||
<?php foreach ($categories as $cat): ?>
|
||||
<tr>
|
||||
<td><code><?= $cat['id'] ?></code></td>
|
||||
<td><?= htmlspecialchars($cat['name']) ?></td>
|
||||
<td class="text-muted"><?= htmlspecialchars($cat['slug']) ?></td>
|
||||
<td><?= $cat['count'] ?? 0 ?></td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
<?php endif; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="alert alert-info mt-3" style="max-width: 700px;">
|
||||
<i class="bi bi-info-circle me-1"></i>
|
||||
Użyj <strong>ID kategorii</strong> przy konfiguracji tematów, aby artykuły trafiały do odpowiednich kategorii w WordPressie.
|
||||
</div>
|
||||
120
templates/dashboard/index.php
Normal file
120
templates/dashboard/index.php
Normal file
@@ -0,0 +1,120 @@
|
||||
<h2 class="mb-4">Dashboard</h2>
|
||||
|
||||
<div class="row g-4 mb-4">
|
||||
<div class="col-md-3">
|
||||
<div class="card border-primary">
|
||||
<div class="card-body text-center">
|
||||
<div class="fs-2 text-primary"><?= $totalSites ?></div>
|
||||
<div class="text-muted">Stron ogółem</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="card border-success">
|
||||
<div class="card-body text-center">
|
||||
<div class="fs-2 text-success"><?= $activeSites ?></div>
|
||||
<div class="text-muted">Stron aktywnych</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="card border-info">
|
||||
<div class="card-body text-center">
|
||||
<div class="fs-2 text-info"><?= $articleStats['published'] ?></div>
|
||||
<div class="text-muted">Opublikowanych</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="card border-danger">
|
||||
<div class="card-body text-center">
|
||||
<div class="fs-2 text-danger"><?= $articleStats['failed'] ?></div>
|
||||
<div class="text-muted">Błędnych</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row g-4">
|
||||
<div class="col-md-6">
|
||||
<div class="card">
|
||||
<div class="card-header d-flex justify-content-between align-items-center">
|
||||
<h5 class="mb-0">Strony WordPress</h5>
|
||||
<a href="/sites" class="btn btn-sm btn-outline-primary">Zarządzaj</a>
|
||||
</div>
|
||||
<div class="card-body p-0">
|
||||
<table class="table table-hover mb-0">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Nazwa</th>
|
||||
<th>Status</th>
|
||||
<th>Ostatnia publikacja</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php if (empty($sites)): ?>
|
||||
<tr><td colspan="3" class="text-muted text-center py-3">Brak stron. <a href="/sites/create">Dodaj pierwszą</a></td></tr>
|
||||
<?php else: ?>
|
||||
<?php foreach ($sites as $site): ?>
|
||||
<tr>
|
||||
<td><a href="/sites/<?= $site['id'] ?>/edit"><?= htmlspecialchars($site['name']) ?></a></td>
|
||||
<td>
|
||||
<?php if ($site['is_active']): ?>
|
||||
<span class="badge bg-success">Aktywna</span>
|
||||
<?php else: ?>
|
||||
<span class="badge bg-secondary">Nieaktywna</span>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
<td>
|
||||
<?= $site['last_published_at'] ? date('d.m.Y H:i', strtotime($site['last_published_at'])) : '<span class="text-muted">-</span>' ?>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
<?php endif; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6">
|
||||
<div class="card">
|
||||
<div class="card-header d-flex justify-content-between align-items-center">
|
||||
<h5 class="mb-0">Ostatnie artykuły</h5>
|
||||
<a href="/articles" class="btn btn-sm btn-outline-primary">Wszystkie</a>
|
||||
</div>
|
||||
<div class="card-body p-0">
|
||||
<table class="table table-hover mb-0">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Tytuł</th>
|
||||
<th>Strona</th>
|
||||
<th>Status</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php if (empty($recentArticles)): ?>
|
||||
<tr><td colspan="3" class="text-muted text-center py-3">Brak artykułów</td></tr>
|
||||
<?php else: ?>
|
||||
<?php foreach ($recentArticles as $article): ?>
|
||||
<tr>
|
||||
<td><a href="/articles/<?= $article['id'] ?>"><?= htmlspecialchars(mb_strimwidth($article['title'], 0, 40, '...')) ?></a></td>
|
||||
<td><?= htmlspecialchars($article['site_name']) ?></td>
|
||||
<td>
|
||||
<?php if ($article['status'] === 'published'): ?>
|
||||
<span class="badge bg-success">OK</span>
|
||||
<?php elseif ($article['status'] === 'failed'): ?>
|
||||
<span class="badge bg-danger">Błąd</span>
|
||||
<?php else: ?>
|
||||
<span class="badge bg-warning">Oczekuje</span>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
<?php endif; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
192
templates/global-topics/index.php
Normal file
192
templates/global-topics/index.php
Normal file
@@ -0,0 +1,192 @@
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<h2>Biblioteka tematów</h2>
|
||||
<button class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#addCategoryModal">
|
||||
<i class="bi bi-plus-lg me-1"></i>Dodaj kategorię
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<?php if (empty($categories)): ?>
|
||||
<div class="alert alert-info">Brak tematów. Dodaj pierwszą kategorię.</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<div class="accordion" id="topicsAccordion">
|
||||
<?php foreach ($categories as $cat): ?>
|
||||
<div class="accordion-item">
|
||||
<h2 class="accordion-header">
|
||||
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#cat-<?= $cat['id'] ?>">
|
||||
<strong><?= htmlspecialchars($cat['name']) ?></strong>
|
||||
<span class="badge bg-secondary ms-2"><?= count($cat['children']) ?></span>
|
||||
<?php if ($cat['description']): ?>
|
||||
<small class="text-muted ms-3"><?= htmlspecialchars(mb_strimwidth($cat['description'], 0, 60, '...')) ?></small>
|
||||
<?php endif; ?>
|
||||
</button>
|
||||
</h2>
|
||||
<div id="cat-<?= $cat['id'] ?>" class="accordion-collapse collapse" data-bs-parent="#topicsAccordion">
|
||||
<div class="accordion-body p-0">
|
||||
<div class="p-3 bg-light border-bottom d-flex justify-content-between align-items-center">
|
||||
<div class="d-flex gap-2">
|
||||
<button class="btn btn-sm btn-outline-primary btn-edit-global"
|
||||
data-id="<?= $cat['id'] ?>"
|
||||
data-name="<?= htmlspecialchars($cat['name']) ?>"
|
||||
data-description="<?= htmlspecialchars($cat['description'] ?? '') ?>">
|
||||
<i class="bi bi-pencil me-1"></i>Edytuj kategorię
|
||||
</button>
|
||||
<form method="post" action="/global-topics/<?= $cat['id'] ?>/delete" class="d-inline"
|
||||
onsubmit="return confirm('Usunąć kategorię i wszystkie jej tematy?')">
|
||||
<button type="submit" class="btn btn-sm btn-outline-danger">
|
||||
<i class="bi bi-trash me-1"></i>Usuń
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
<button class="btn btn-sm btn-success btn-add-subtopic" data-parent-id="<?= $cat['id'] ?>" data-parent-name="<?= htmlspecialchars($cat['name']) ?>">
|
||||
<i class="bi bi-plus-lg me-1"></i>Dodaj temat
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<?php if (!empty($cat['children'])): ?>
|
||||
<table class="table table-hover mb-0">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Temat</th>
|
||||
<th>Opis</th>
|
||||
<th style="width:120px">Akcje</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ($cat['children'] as $child): ?>
|
||||
<tr>
|
||||
<td><?= htmlspecialchars($child['name']) ?></td>
|
||||
<td class="text-muted small"><?= htmlspecialchars(mb_strimwidth($child['description'] ?? '', 0, 80, '...')) ?></td>
|
||||
<td>
|
||||
<div class="btn-group btn-group-sm">
|
||||
<button class="btn btn-outline-primary btn-edit-global"
|
||||
data-id="<?= $child['id'] ?>"
|
||||
data-name="<?= htmlspecialchars($child['name']) ?>"
|
||||
data-description="<?= htmlspecialchars($child['description'] ?? '') ?>">
|
||||
<i class="bi bi-pencil"></i>
|
||||
</button>
|
||||
<form method="post" action="/global-topics/<?= $child['id'] ?>/delete" class="d-inline"
|
||||
onsubmit="return confirm('Usunąć ten temat?')">
|
||||
<button type="submit" class="btn btn-outline-danger"><i class="bi bi-trash"></i></button>
|
||||
</form>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
<?php else: ?>
|
||||
<div class="p-3 text-muted text-center">Brak tematów w tej kategorii</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
|
||||
<!-- Modal: Dodaj kategorię -->
|
||||
<div class="modal fade" id="addCategoryModal" tabindex="-1">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<form method="post" action="/global-topics/categories">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">Dodaj kategorię</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Nazwa kategorii</label>
|
||||
<input type="text" class="form-control" name="name" required placeholder="np. Sport">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Opis</label>
|
||||
<textarea class="form-control" name="description" rows="2" placeholder="Krótki opis kategorii..."></textarea>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">Anuluj</button>
|
||||
<button type="submit" class="btn btn-primary">Dodaj</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal: Dodaj temat (subtopic) -->
|
||||
<div class="modal fade" id="addSubtopicModal" tabindex="-1">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<form method="post" id="subtopicForm" action="">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">Dodaj temat do: <span id="subtopicParentName"></span></h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Nazwa tematu</label>
|
||||
<input type="text" class="form-control" name="name" required placeholder="np. Piłka nożna">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Opis / wytyczne dla AI</label>
|
||||
<textarea class="form-control" name="description" rows="3" placeholder="Opisz zakres tematyczny, styl pisania..."></textarea>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">Anuluj</button>
|
||||
<button type="submit" class="btn btn-success">Dodaj temat</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal: Edytuj -->
|
||||
<div class="modal fade" id="editGlobalModal" tabindex="-1">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<form method="post" id="editGlobalForm" action="">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">Edytuj</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Nazwa</label>
|
||||
<input type="text" class="form-control" name="name" id="editGlobalName" required>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Opis</label>
|
||||
<textarea class="form-control" name="description" id="editGlobalDesc" rows="3"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">Anuluj</button>
|
||||
<button type="submit" class="btn btn-primary">Zapisz</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Add subtopic modal
|
||||
document.querySelectorAll('.btn-add-subtopic').forEach(function(btn) {
|
||||
btn.addEventListener('click', function() {
|
||||
document.getElementById('subtopicForm').action = '/global-topics/' + this.dataset.parentId + '/subtopics';
|
||||
document.getElementById('subtopicParentName').textContent = this.dataset.parentName;
|
||||
new bootstrap.Modal(document.getElementById('addSubtopicModal')).show();
|
||||
});
|
||||
});
|
||||
|
||||
// Edit modal
|
||||
document.querySelectorAll('.btn-edit-global').forEach(function(btn) {
|
||||
btn.addEventListener('click', function() {
|
||||
document.getElementById('editGlobalForm').action = '/global-topics/' + this.dataset.id + '/update';
|
||||
document.getElementById('editGlobalName').value = this.dataset.name;
|
||||
document.getElementById('editGlobalDesc').value = this.dataset.description;
|
||||
new bootstrap.Modal(document.getElementById('editGlobalModal')).show();
|
||||
});
|
||||
});
|
||||
});
|
||||
</script>
|
||||
13
templates/layout/header.php
Normal file
13
templates/layout/header.php
Normal file
@@ -0,0 +1,13 @@
|
||||
<header class="bg-white border-bottom p-3 d-flex justify-content-between align-items-center">
|
||||
<div></div>
|
||||
<div class="d-flex align-items-center gap-3">
|
||||
<form method="post" action="/publish/run" class="d-inline">
|
||||
<button type="submit" class="btn btn-sm btn-success" onclick="return confirm('Uruchomić publikację teraz?')">
|
||||
<i class="bi bi-play-circle me-1"></i>Publikuj teraz
|
||||
</button>
|
||||
</form>
|
||||
<a href="/logout" class="btn btn-sm btn-outline-secondary">
|
||||
<i class="bi bi-box-arrow-right me-1"></i>Wyloguj
|
||||
</a>
|
||||
</div>
|
||||
</header>
|
||||
44
templates/layout/main.php
Normal file
44
templates/layout/main.php
Normal file
@@ -0,0 +1,44 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="pl">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>BackPRO - Zarządzanie Zapleczem SEO</title>
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css" rel="stylesheet">
|
||||
<link href="/assets/css/app.css" rel="stylesheet">
|
||||
</head>
|
||||
<body>
|
||||
<?php if (\App\Core\Auth::check()): ?>
|
||||
<div class="d-flex">
|
||||
<?php require __DIR__ . '/sidebar.php'; ?>
|
||||
<div class="flex-grow-1">
|
||||
<?php require __DIR__ . '/header.php'; ?>
|
||||
<main class="p-4">
|
||||
<?php if (!empty($flashMessages)): ?>
|
||||
<?php foreach ($flashMessages as $flash): ?>
|
||||
<div class="alert alert-<?= htmlspecialchars($flash['type']) ?> alert-dismissible fade show">
|
||||
<?= htmlspecialchars($flash['message']) ?>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
<?php endif; ?>
|
||||
<?= $content ?>
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
<?php else: ?>
|
||||
<?php if (!empty($flashMessages)): ?>
|
||||
<?php foreach ($flashMessages as $flash): ?>
|
||||
<div class="alert alert-<?= htmlspecialchars($flash['type']) ?> m-3">
|
||||
<?= htmlspecialchars($flash['message']) ?>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
<?php endif; ?>
|
||||
<?= $content ?>
|
||||
<?php endif; ?>
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
|
||||
<script src="/assets/js/app.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
38
templates/layout/sidebar.php
Normal file
38
templates/layout/sidebar.php
Normal file
@@ -0,0 +1,38 @@
|
||||
<nav class="sidebar bg-dark text-white d-flex flex-column" style="width: 250px; min-height: 100vh;">
|
||||
<div class="p-3 border-bottom border-secondary">
|
||||
<h5 class="mb-0"><i class="bi bi-globe2"></i> BackPRO</h5>
|
||||
<small class="text-secondary">Zarządzanie Zapleczem SEO</small>
|
||||
</div>
|
||||
<ul class="nav flex-column p-2 flex-grow-1">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link text-white" href="/">
|
||||
<i class="bi bi-speedometer2 me-2"></i>Dashboard
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link text-white" href="/sites">
|
||||
<i class="bi bi-wordpress me-2"></i>Strony
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link text-white" href="/global-topics">
|
||||
<i class="bi bi-bookmarks me-2"></i>Tematy
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link text-white" href="/articles">
|
||||
<i class="bi bi-file-earmark-text me-2"></i>Artykuły
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link text-white" href="/settings">
|
||||
<i class="bi bi-gear me-2"></i>Ustawienia
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
<div class="p-3 border-top border-secondary">
|
||||
<small class="text-secondary">Zalogowano jako:</small><br>
|
||||
<span class="text-white"><?= htmlspecialchars(\App\Core\Auth::user()['username'] ?? '') ?></span>
|
||||
<a href="/change-password" class="d-block text-secondary small mt-1"><i class="bi bi-key me-1"></i>Zmień hasło</a>
|
||||
</div>
|
||||
</nav>
|
||||
79
templates/settings/index.php
Normal file
79
templates/settings/index.php
Normal file
@@ -0,0 +1,79 @@
|
||||
<h2 class="mb-4">Ustawienia</h2>
|
||||
|
||||
<div class="card" style="max-width: 700px;">
|
||||
<div class="card-body">
|
||||
<form method="post" action="/settings">
|
||||
|
||||
<h5 class="mb-3 border-bottom pb-2">OpenAI (Generowanie artykułów)</h5>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="openai_api_key" class="form-label">Klucz API OpenAI</label>
|
||||
<input type="password" class="form-control" id="openai_api_key" name="openai_api_key"
|
||||
value="<?= htmlspecialchars($settings['openai_api_key']) ?>"
|
||||
placeholder="sk-proj-...">
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="openai_model" class="form-label">Model AI</label>
|
||||
<select class="form-select" id="openai_model" name="openai_model">
|
||||
<optgroup label="GPT-5">
|
||||
<option value="gpt-5" <?= $settings['openai_model'] === 'gpt-5' ? 'selected' : '' ?>>GPT-5 (najnowszy)</option>
|
||||
<option value="gpt-5-mini" <?= $settings['openai_model'] === 'gpt-5-mini' ? 'selected' : '' ?>>GPT-5 Mini</option>
|
||||
</optgroup>
|
||||
<optgroup label="GPT-4">
|
||||
<option value="gpt-4o" <?= $settings['openai_model'] === 'gpt-4o' ? 'selected' : '' ?>>GPT-4o (rekomendowany)</option>
|
||||
<option value="gpt-4o-mini" <?= $settings['openai_model'] === 'gpt-4o-mini' ? 'selected' : '' ?>>GPT-4o-mini (tańszy)</option>
|
||||
<option value="gpt-4-turbo" <?= $settings['openai_model'] === 'gpt-4-turbo' ? 'selected' : '' ?>>GPT-4 Turbo</option>
|
||||
</optgroup>
|
||||
<optgroup label="GPT-3.5">
|
||||
<option value="gpt-3.5-turbo" <?= $settings['openai_model'] === 'gpt-3.5-turbo' ? 'selected' : '' ?>>GPT-3.5 Turbo (najtańszy)</option>
|
||||
</optgroup>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="row mb-3">
|
||||
<div class="col">
|
||||
<label for="article_min_words" class="form-label">Min. słów w artykule</label>
|
||||
<input type="number" class="form-control" id="article_min_words" name="article_min_words"
|
||||
value="<?= htmlspecialchars($settings['article_min_words']) ?>">
|
||||
</div>
|
||||
<div class="col">
|
||||
<label for="article_max_words" class="form-label">Max. słów w artykule</label>
|
||||
<input type="number" class="form-control" id="article_max_words" name="article_max_words"
|
||||
value="<?= htmlspecialchars($settings['article_max_words']) ?>">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h5 class="mb-3 mt-4 border-bottom pb-2">Obrazki</h5>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="image_provider" class="form-label">Dostawca obrazków</label>
|
||||
<select class="form-select" id="image_provider" name="image_provider">
|
||||
<option value="freepik" <?= $settings['image_provider'] === 'freepik' ? 'selected' : '' ?>>Freepik AI (generowanie)</option>
|
||||
<option value="unsplash" <?= $settings['image_provider'] === 'unsplash' ? 'selected' : '' ?>>Unsplash (darmowe stocki)</option>
|
||||
<option value="pexels" <?= $settings['image_provider'] === 'pexels' ? 'selected' : '' ?>>Pexels (darmowe stocki)</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="freepik_api_key" class="form-label">Klucz API Freepik</label>
|
||||
<input type="password" class="form-control" id="freepik_api_key" name="freepik_api_key"
|
||||
value="<?= htmlspecialchars($settings['freepik_api_key']) ?>">
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="unsplash_api_key" class="form-label">Klucz API Unsplash</label>
|
||||
<input type="password" class="form-control" id="unsplash_api_key" name="unsplash_api_key"
|
||||
value="<?= htmlspecialchars($settings['unsplash_api_key']) ?>">
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="pexels_api_key" class="form-label">Klucz API Pexels</label>
|
||||
<input type="password" class="form-control" id="pexels_api_key" name="pexels_api_key"
|
||||
value="<?= htmlspecialchars($settings['pexels_api_key']) ?>">
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn btn-primary">Zapisz ustawienia</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
86
templates/sites/create.php
Normal file
86
templates/sites/create.php
Normal file
@@ -0,0 +1,86 @@
|
||||
<h2 class="mb-4">Dodaj stronę WordPress</h2>
|
||||
|
||||
<div class="row g-4">
|
||||
<div class="col-lg-7">
|
||||
<div class="card">
|
||||
<div class="card-header"><h5 class="mb-0">Ustawienia strony</h5></div>
|
||||
<div class="card-body">
|
||||
<form method="post" action="/sites" id="siteForm">
|
||||
<div class="mb-3">
|
||||
<label for="name" class="form-label">Nazwa strony</label>
|
||||
<input type="text" class="form-control" id="name" name="name" required placeholder="np. Blog Ogrodniczy">
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="url" class="form-label">URL WordPressa</label>
|
||||
<input type="url" class="form-control" id="url" name="url" required placeholder="https://example.com">
|
||||
<div class="form-text">Podaj pełny URL strony WordPress (bez końcowego /)</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="api_user" class="form-label">Użytkownik API (WordPress)</label>
|
||||
<input type="text" class="form-control" id="api_user" name="api_user" required>
|
||||
<div class="form-text">Login użytkownika WordPress z uprawnieniami do publikacji</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="api_token" class="form-label">Application Password</label>
|
||||
<input type="text" class="form-control" id="api_token" name="api_token" required>
|
||||
<div class="form-text">Wygeneruj w WP: Użytkownicy → Profil → Application Passwords</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="publish_interval_days" class="form-label">Interwał publikacji (dni)</label>
|
||||
<input type="number" class="form-control" id="publish_interval_days" name="publish_interval_days" value="3" min="1" max="30">
|
||||
<div class="form-text">Co ile dni publikować nowy artykuł na tej stronie</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<div class="form-check">
|
||||
<input type="checkbox" class="form-check-input" id="is_multisite" name="is_multisite" value="1">
|
||||
<label class="form-check-label" for="is_multisite">Strona wielotematyczna</label>
|
||||
<div class="form-text">Zaznacz, jeśli strona ma wiele tematów - system będzie równomiernie rozkładał publikacje</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<div class="form-check">
|
||||
<input type="checkbox" class="form-check-input" id="is_active" name="is_active" value="1" checked>
|
||||
<label class="form-check-label" for="is_active">Aktywna</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="d-flex gap-2">
|
||||
<button type="submit" class="btn btn-primary">Zapisz</button>
|
||||
<a href="/sites" class="btn btn-outline-secondary">Anuluj</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-lg-5">
|
||||
<div class="card">
|
||||
<div class="card-header"><h5 class="mb-0">Tematy do publikacji</h5></div>
|
||||
<div class="card-body">
|
||||
<p class="text-muted small mb-3">Wybierz tematy, o których będą publikowane artykuły. Możesz je później zmienić w edycji strony.</p>
|
||||
|
||||
<?php foreach ($globalTopics as $cat): ?>
|
||||
<?php if (!empty($cat['children'])): ?>
|
||||
<div class="mb-3">
|
||||
<label class="form-label fw-bold mb-1"><?= htmlspecialchars($cat['name']) ?></label>
|
||||
<?php foreach ($cat['children'] as $child): ?>
|
||||
<div class="form-check">
|
||||
<input type="checkbox" class="form-check-input" id="topic_<?= $child['id'] ?>" form="siteForm" name="topics[]" value="<?= $child['id'] ?>">
|
||||
<label class="form-check-label" for="topic_<?= $child['id'] ?>">
|
||||
<?= htmlspecialchars($child['name']) ?>
|
||||
</label>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
157
templates/sites/edit.php
Normal file
157
templates/sites/edit.php
Normal file
@@ -0,0 +1,157 @@
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<h2>Edytuj stronę: <?= htmlspecialchars($site['name']) ?></h2>
|
||||
<div class="d-flex gap-2">
|
||||
<form method="post" action="/publish/site/<?= $site['id'] ?>" class="d-inline">
|
||||
<button type="submit" class="btn btn-success" onclick="return confirm('Opublikować artykuł na tej stronie teraz?')">
|
||||
<i class="bi bi-play-circle me-1"></i>Publikuj teraz
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row g-4">
|
||||
<div class="col-lg-7">
|
||||
<div class="card">
|
||||
<div class="card-header"><h5 class="mb-0">Ustawienia strony</h5></div>
|
||||
<div class="card-body">
|
||||
<form method="post" action="/sites/<?= $site['id'] ?>">
|
||||
<div class="mb-3">
|
||||
<label for="name" class="form-label">Nazwa strony</label>
|
||||
<input type="text" class="form-control" id="name" name="name" value="<?= htmlspecialchars($site['name']) ?>" required>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="url" class="form-label">URL WordPressa</label>
|
||||
<input type="url" class="form-control" id="url" name="url" value="<?= htmlspecialchars($site['url']) ?>" required>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="api_user" class="form-label">Użytkownik API</label>
|
||||
<input type="text" class="form-control" id="api_user" name="api_user" value="<?= htmlspecialchars($site['api_user']) ?>" required>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="api_token" class="form-label">Application Password</label>
|
||||
<input type="text" class="form-control" id="api_token" name="api_token" value="<?= htmlspecialchars($site['api_token']) ?>" required>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="publish_interval_days" class="form-label">Interwał publikacji (dni)</label>
|
||||
<input type="number" class="form-control" id="publish_interval_days" name="publish_interval_days" value="<?= $site['publish_interval_days'] ?>" min="1" max="30">
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<div class="form-check">
|
||||
<input type="checkbox" class="form-check-input" id="is_multisite" name="is_multisite" value="1" <?= $site['is_multisite'] ? 'checked' : '' ?>>
|
||||
<label class="form-check-label" for="is_multisite">Strona wielotematyczna</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<div class="form-check">
|
||||
<input type="checkbox" class="form-check-input" id="is_active" name="is_active" value="1" <?= $site['is_active'] ? 'checked' : '' ?>>
|
||||
<label class="form-check-label" for="is_active">Aktywna</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="d-flex gap-2">
|
||||
<button type="submit" class="btn btn-primary">Zapisz zmiany</button>
|
||||
<a href="/sites" class="btn btn-outline-secondary">Anuluj</a>
|
||||
<button class="btn btn-outline-success btn-test-connection ms-auto" data-site-id="<?= $site['id'] ?>" type="button">
|
||||
<i class="bi bi-wifi me-1"></i>Test
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-lg-5">
|
||||
<!-- Assigned topics -->
|
||||
<div class="card mb-4">
|
||||
<div class="card-header d-flex justify-content-between align-items-center">
|
||||
<h5 class="mb-0">Tematy (<?= count($topics) ?>)</h5>
|
||||
<a href="/sites/<?= $site['id'] ?>/topics" class="btn btn-sm btn-outline-info">
|
||||
<i class="bi bi-pencil me-1"></i>Zarządzaj
|
||||
</a>
|
||||
</div>
|
||||
<div class="card-body p-0">
|
||||
<?php if (empty($topics)): ?>
|
||||
<div class="p-3 text-muted text-center">Brak tematów. Dodaj z biblioteki poniżej.</div>
|
||||
<?php else: ?>
|
||||
<table class="table table-hover table-sm mb-0">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Temat</th>
|
||||
<th>Kategoria</th>
|
||||
<th>Art.</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ($topics as $topic): ?>
|
||||
<tr>
|
||||
<td>
|
||||
<?= htmlspecialchars($topic['name']) ?>
|
||||
<?php if (!$topic['is_active']): ?>
|
||||
<span class="badge bg-secondary">off</span>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
<td class="small text-muted"><?= htmlspecialchars($topic['global_category_name'] ?? '') ?></td>
|
||||
<td><span class="badge bg-primary"><?= $topic['article_count'] ?></span></td>
|
||||
<td>
|
||||
<form method="post" action="/topics/<?= $topic['id'] ?>/delete" class="d-inline"
|
||||
onsubmit="return confirm('Usunąć ten temat?')">
|
||||
<button type="submit" class="btn btn-sm btn-outline-danger py-0 px-1"><i class="bi bi-x"></i></button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Quick add from library -->
|
||||
<div class="card">
|
||||
<div class="card-header"><h5 class="mb-0">Dodaj temat z biblioteki</h5></div>
|
||||
<div class="card-body">
|
||||
<form method="post" action="/sites/<?= $site['id'] ?>/topics">
|
||||
<div class="mb-3">
|
||||
<select class="form-select" name="global_topic_id" id="quick_global_topic" onchange="quickFillTopic(this)" required>
|
||||
<option value="">Wybierz temat...</option>
|
||||
<?php foreach ($globalTopics as $cat): ?>
|
||||
<optgroup label="<?= htmlspecialchars($cat['name']) ?>">
|
||||
<?php foreach ($cat['children'] as $child): ?>
|
||||
<?php $alreadyAssigned = in_array($child['id'], $assignedGlobalIds); ?>
|
||||
<option value="<?= $child['id'] ?>"
|
||||
data-name="<?= htmlspecialchars($child['name']) ?>"
|
||||
data-desc="<?= htmlspecialchars($child['description'] ?? '') ?>"
|
||||
<?= $alreadyAssigned ? 'disabled' : '' ?>>
|
||||
<?= htmlspecialchars($child['name']) ?><?= $alreadyAssigned ? ' (dodany)' : '' ?>
|
||||
</option>
|
||||
<?php endforeach; ?>
|
||||
</optgroup>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
<input type="hidden" name="name" id="quick_topic_name">
|
||||
<input type="hidden" name="description" id="quick_topic_desc">
|
||||
<input type="hidden" name="is_active" value="1">
|
||||
<button type="submit" class="btn btn-success btn-sm w-100">
|
||||
<i class="bi bi-plus-lg me-1"></i>Dodaj
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function quickFillTopic(select) {
|
||||
var opt = select.options[select.selectedIndex];
|
||||
document.getElementById('quick_topic_name').value = opt.dataset.name || '';
|
||||
document.getElementById('quick_topic_desc').value = opt.dataset.desc || '';
|
||||
}
|
||||
</script>
|
||||
76
templates/sites/index.php
Normal file
76
templates/sites/index.php
Normal file
@@ -0,0 +1,76 @@
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<h2>Strony WordPress</h2>
|
||||
<a href="/sites/create" class="btn btn-primary">
|
||||
<i class="bi bi-plus-lg me-1"></i>Dodaj stronę
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-body p-0">
|
||||
<table class="table table-hover mb-0">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Nazwa</th>
|
||||
<th>URL</th>
|
||||
<th>Tematy</th>
|
||||
<th>Interwał</th>
|
||||
<th>Ostatnia publikacja</th>
|
||||
<th>Status</th>
|
||||
<th>Akcje</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php if (empty($sites)): ?>
|
||||
<tr><td colspan="7" class="text-center text-muted py-4">Brak stron. Dodaj pierwszą stronę WordPress.</td></tr>
|
||||
<?php else: ?>
|
||||
<?php foreach ($sites as $site): ?>
|
||||
<tr>
|
||||
<td>
|
||||
<a href="/sites/<?= $site['id'] ?>/edit"><?= htmlspecialchars($site['name']) ?></a>
|
||||
<?php if ($site['is_multisite']): ?>
|
||||
<span class="badge bg-info">Multi</span>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
<td><a href="<?= htmlspecialchars($site['url']) ?>" target="_blank" class="text-muted small"><?= htmlspecialchars($site['url']) ?></a></td>
|
||||
<td>
|
||||
<a href="/sites/<?= $site['id'] ?>/topics" class="badge bg-secondary text-decoration-none">
|
||||
<?= $site['topic_count'] ?> tematów
|
||||
</a>
|
||||
</td>
|
||||
<td>co <?= $site['publish_interval_days'] ?> dni</td>
|
||||
<td><?= $site['last_published_at'] ? date('d.m.Y H:i', strtotime($site['last_published_at'])) : '-' ?></td>
|
||||
<td>
|
||||
<?php if ($site['is_active']): ?>
|
||||
<span class="badge bg-success">Aktywna</span>
|
||||
<?php else: ?>
|
||||
<span class="badge bg-secondary">Nieaktywna</span>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
<td>
|
||||
<div class="btn-group btn-group-sm">
|
||||
<a href="/sites/<?= $site['id'] ?>/edit" class="btn btn-outline-primary" title="Edytuj">
|
||||
<i class="bi bi-pencil"></i>
|
||||
</a>
|
||||
<a href="/sites/<?= $site['id'] ?>/topics" class="btn btn-outline-info" title="Tematy">
|
||||
<i class="bi bi-tags"></i>
|
||||
</a>
|
||||
<a href="/sites/<?= $site['id'] ?>/categories" class="btn btn-outline-warning" title="Kategorie">
|
||||
<i class="bi bi-folder"></i>
|
||||
</a>
|
||||
<button class="btn btn-outline-success btn-test-connection" data-site-id="<?= $site['id'] ?>" title="Test połączenia">
|
||||
<i class="bi bi-wifi"></i>
|
||||
</button>
|
||||
<form method="post" action="/sites/<?= $site['id'] ?>/delete" class="d-inline" onsubmit="return confirm('Na pewno usunąć tę stronę?')">
|
||||
<button type="submit" class="btn btn-outline-danger" title="Usuń">
|
||||
<i class="bi bi-trash"></i>
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
<?php endif; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
141
templates/topics/index.php
Normal file
141
templates/topics/index.php
Normal file
@@ -0,0 +1,141 @@
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<div>
|
||||
<h2>Tematy: <?= htmlspecialchars($site['name']) ?></h2>
|
||||
<a href="/sites" class="text-muted small">← Powrót do stron</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row g-4">
|
||||
<div class="col-md-8">
|
||||
<div class="card">
|
||||
<div class="card-body p-0">
|
||||
<table class="table table-hover mb-0">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Temat</th>
|
||||
<th>Kategoria</th>
|
||||
<th>Kat. WP</th>
|
||||
<th>Artykuły</th>
|
||||
<th>Status</th>
|
||||
<th>Akcje</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php if (empty($topics)): ?>
|
||||
<tr><td colspan="6" class="text-center text-muted py-4">Brak tematów. Dodaj tematy z biblioteki lub utwórz własny.</td></tr>
|
||||
<?php else: ?>
|
||||
<?php foreach ($topics as $topic): ?>
|
||||
<tr>
|
||||
<td><strong><?= htmlspecialchars($topic['name']) ?></strong></td>
|
||||
<td class="small text-muted">
|
||||
<?php if ($topic['global_category_name']): ?>
|
||||
<?= htmlspecialchars($topic['global_category_name']) ?>
|
||||
<?php elseif ($topic['global_topic_name']): ?>
|
||||
<?= htmlspecialchars($topic['global_topic_name']) ?>
|
||||
<?php else: ?>
|
||||
<em>własny</em>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
<td><?= $topic['wp_category_id'] ?: '-' ?></td>
|
||||
<td><span class="badge bg-primary"><?= $topic['article_count'] ?></span></td>
|
||||
<td>
|
||||
<?php if ($topic['is_active']): ?>
|
||||
<span class="badge bg-success">Aktywny</span>
|
||||
<?php else: ?>
|
||||
<span class="badge bg-secondary">Nieaktywny</span>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
<td>
|
||||
<div class="btn-group btn-group-sm">
|
||||
<button class="btn btn-outline-primary btn-edit-topic"
|
||||
data-id="<?= $topic['id'] ?>"
|
||||
data-name="<?= htmlspecialchars($topic['name']) ?>"
|
||||
data-description="<?= htmlspecialchars($topic['description'] ?? '') ?>"
|
||||
data-wp-category="<?= $topic['wp_category_id'] ?>"
|
||||
data-global-topic="<?= $topic['global_topic_id'] ?>"
|
||||
data-active="<?= $topic['is_active'] ?>">
|
||||
<i class="bi bi-pencil"></i>
|
||||
</button>
|
||||
<form method="post" action="/topics/<?= $topic['id'] ?>/delete" class="d-inline" onsubmit="return confirm('Na pewno usunąć ten temat?')">
|
||||
<button type="submit" class="btn btn-outline-danger"><i class="bi bi-trash"></i></button>
|
||||
</form>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
<?php endif; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-4">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h5 class="mb-0" id="topicFormTitle">Dodaj temat</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form method="post" action="/sites/<?= $site['id'] ?>/topics" id="topicForm">
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Wybierz z biblioteki</label>
|
||||
<select class="form-select" id="topic_global_id" name="global_topic_id" onchange="fillFromLibrary(this)">
|
||||
<option value="">-- własny temat --</option>
|
||||
<?php foreach ($globalTopics as $cat): ?>
|
||||
<optgroup label="<?= htmlspecialchars($cat['name']) ?>">
|
||||
<?php foreach ($cat['children'] as $child): ?>
|
||||
<option value="<?= $child['id'] ?>"
|
||||
data-name="<?= htmlspecialchars($child['name']) ?>"
|
||||
data-desc="<?= htmlspecialchars($child['description'] ?? '') ?>">
|
||||
<?= htmlspecialchars($child['name']) ?>
|
||||
</option>
|
||||
<?php endforeach; ?>
|
||||
</optgroup>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
<div class="form-text">Wybierz temat z <a href="/global-topics">biblioteki</a> lub wpisz własny poniżej</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="topic_name" class="form-label">Nazwa tematu</label>
|
||||
<input type="text" class="form-control" id="topic_name" name="name" required placeholder="np. Ogrodnictwo">
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="topic_description" class="form-label">Opis / wytyczne dla AI</label>
|
||||
<textarea class="form-control" id="topic_description" name="description" rows="3" placeholder="Opisz czego powinny dotyczyć artykuły..."></textarea>
|
||||
<div class="form-text">Wskazówki dla AI, jak pisać artykuły z tego tematu</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="topic_wp_category" class="form-label">ID kategorii WordPress</label>
|
||||
<input type="number" class="form-control" id="topic_wp_category" name="wp_category_id" placeholder="np. 5">
|
||||
<div class="form-text">
|
||||
<a href="/sites/<?= $site['id'] ?>/categories">Sprawdź kategorie na stronie</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<div class="form-check">
|
||||
<input type="checkbox" class="form-check-input" id="topic_is_active" name="is_active" value="1" checked>
|
||||
<label class="form-check-label" for="topic_is_active">Aktywny</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn btn-primary w-100" id="topicFormSubmit">Dodaj temat</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function fillFromLibrary(select) {
|
||||
var opt = select.options[select.selectedIndex];
|
||||
if (opt.value) {
|
||||
document.getElementById('topic_name').value = opt.dataset.name || '';
|
||||
document.getElementById('topic_description').value = opt.dataset.desc || '';
|
||||
}
|
||||
}
|
||||
</script>
|
||||
22
vendor/autoload.php
vendored
Normal file
22
vendor/autoload.php
vendored
Normal file
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
// autoload.php @generated by Composer
|
||||
|
||||
if (PHP_VERSION_ID < 50600) {
|
||||
if (!headers_sent()) {
|
||||
header('HTTP/1.1 500 Internal Server Error');
|
||||
}
|
||||
$err = 'Composer 2.3.0 dropped support for autoloading on PHP <5.6 and you are running '.PHP_VERSION.', please upgrade PHP or use Composer 2.2 LTS via "composer self-update --2.2". Aborting.'.PHP_EOL;
|
||||
if (!ini_get('display_errors')) {
|
||||
if (PHP_SAPI === 'cli' || PHP_SAPI === 'phpdbg') {
|
||||
fwrite(STDERR, $err);
|
||||
} elseif (!headers_sent()) {
|
||||
echo $err;
|
||||
}
|
||||
}
|
||||
throw new RuntimeException($err);
|
||||
}
|
||||
|
||||
require_once __DIR__ . '/composer/autoload_real.php';
|
||||
|
||||
return ComposerAutoloaderInit6c68720c4b8879a1af2b630d6aeb512e::getLoader();
|
||||
579
vendor/composer/ClassLoader.php
vendored
Normal file
579
vendor/composer/ClassLoader.php
vendored
Normal file
@@ -0,0 +1,579 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Composer.
|
||||
*
|
||||
* (c) Nils Adermann <naderman@naderman.de>
|
||||
* Jordi Boggiano <j.boggiano@seld.be>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Composer\Autoload;
|
||||
|
||||
/**
|
||||
* ClassLoader implements a PSR-0, PSR-4 and classmap class loader.
|
||||
*
|
||||
* $loader = new \Composer\Autoload\ClassLoader();
|
||||
*
|
||||
* // register classes with namespaces
|
||||
* $loader->add('Symfony\Component', __DIR__.'/component');
|
||||
* $loader->add('Symfony', __DIR__.'/framework');
|
||||
*
|
||||
* // activate the autoloader
|
||||
* $loader->register();
|
||||
*
|
||||
* // to enable searching the include path (eg. for PEAR packages)
|
||||
* $loader->setUseIncludePath(true);
|
||||
*
|
||||
* In this example, if you try to use a class in the Symfony\Component
|
||||
* namespace or one of its children (Symfony\Component\Console for instance),
|
||||
* the autoloader will first look for the class under the component/
|
||||
* directory, and it will then fallback to the framework/ directory if not
|
||||
* found before giving up.
|
||||
*
|
||||
* This class is loosely based on the Symfony UniversalClassLoader.
|
||||
*
|
||||
* @author Fabien Potencier <fabien@symfony.com>
|
||||
* @author Jordi Boggiano <j.boggiano@seld.be>
|
||||
* @see https://www.php-fig.org/psr/psr-0/
|
||||
* @see https://www.php-fig.org/psr/psr-4/
|
||||
*/
|
||||
class ClassLoader
|
||||
{
|
||||
/** @var \Closure(string):void */
|
||||
private static $includeFile;
|
||||
|
||||
/** @var string|null */
|
||||
private $vendorDir;
|
||||
|
||||
// PSR-4
|
||||
/**
|
||||
* @var array<string, array<string, int>>
|
||||
*/
|
||||
private $prefixLengthsPsr4 = array();
|
||||
/**
|
||||
* @var array<string, list<string>>
|
||||
*/
|
||||
private $prefixDirsPsr4 = array();
|
||||
/**
|
||||
* @var list<string>
|
||||
*/
|
||||
private $fallbackDirsPsr4 = array();
|
||||
|
||||
// PSR-0
|
||||
/**
|
||||
* List of PSR-0 prefixes
|
||||
*
|
||||
* Structured as array('F (first letter)' => array('Foo\Bar (full prefix)' => array('path', 'path2')))
|
||||
*
|
||||
* @var array<string, array<string, list<string>>>
|
||||
*/
|
||||
private $prefixesPsr0 = array();
|
||||
/**
|
||||
* @var list<string>
|
||||
*/
|
||||
private $fallbackDirsPsr0 = array();
|
||||
|
||||
/** @var bool */
|
||||
private $useIncludePath = false;
|
||||
|
||||
/**
|
||||
* @var array<string, string>
|
||||
*/
|
||||
private $classMap = array();
|
||||
|
||||
/** @var bool */
|
||||
private $classMapAuthoritative = false;
|
||||
|
||||
/**
|
||||
* @var array<string, bool>
|
||||
*/
|
||||
private $missingClasses = array();
|
||||
|
||||
/** @var string|null */
|
||||
private $apcuPrefix;
|
||||
|
||||
/**
|
||||
* @var array<string, self>
|
||||
*/
|
||||
private static $registeredLoaders = array();
|
||||
|
||||
/**
|
||||
* @param string|null $vendorDir
|
||||
*/
|
||||
public function __construct($vendorDir = null)
|
||||
{
|
||||
$this->vendorDir = $vendorDir;
|
||||
self::initializeIncludeClosure();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, list<string>>
|
||||
*/
|
||||
public function getPrefixes()
|
||||
{
|
||||
if (!empty($this->prefixesPsr0)) {
|
||||
return call_user_func_array('array_merge', array_values($this->prefixesPsr0));
|
||||
}
|
||||
|
||||
return array();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, list<string>>
|
||||
*/
|
||||
public function getPrefixesPsr4()
|
||||
{
|
||||
return $this->prefixDirsPsr4;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return list<string>
|
||||
*/
|
||||
public function getFallbackDirs()
|
||||
{
|
||||
return $this->fallbackDirsPsr0;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return list<string>
|
||||
*/
|
||||
public function getFallbackDirsPsr4()
|
||||
{
|
||||
return $this->fallbackDirsPsr4;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, string> Array of classname => path
|
||||
*/
|
||||
public function getClassMap()
|
||||
{
|
||||
return $this->classMap;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, string> $classMap Class to filename map
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function addClassMap(array $classMap)
|
||||
{
|
||||
if ($this->classMap) {
|
||||
$this->classMap = array_merge($this->classMap, $classMap);
|
||||
} else {
|
||||
$this->classMap = $classMap;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers a set of PSR-0 directories for a given prefix, either
|
||||
* appending or prepending to the ones previously set for this prefix.
|
||||
*
|
||||
* @param string $prefix The prefix
|
||||
* @param list<string>|string $paths The PSR-0 root directories
|
||||
* @param bool $prepend Whether to prepend the directories
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function add($prefix, $paths, $prepend = false)
|
||||
{
|
||||
$paths = (array) $paths;
|
||||
if (!$prefix) {
|
||||
if ($prepend) {
|
||||
$this->fallbackDirsPsr0 = array_merge(
|
||||
$paths,
|
||||
$this->fallbackDirsPsr0
|
||||
);
|
||||
} else {
|
||||
$this->fallbackDirsPsr0 = array_merge(
|
||||
$this->fallbackDirsPsr0,
|
||||
$paths
|
||||
);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$first = $prefix[0];
|
||||
if (!isset($this->prefixesPsr0[$first][$prefix])) {
|
||||
$this->prefixesPsr0[$first][$prefix] = $paths;
|
||||
|
||||
return;
|
||||
}
|
||||
if ($prepend) {
|
||||
$this->prefixesPsr0[$first][$prefix] = array_merge(
|
||||
$paths,
|
||||
$this->prefixesPsr0[$first][$prefix]
|
||||
);
|
||||
} else {
|
||||
$this->prefixesPsr0[$first][$prefix] = array_merge(
|
||||
$this->prefixesPsr0[$first][$prefix],
|
||||
$paths
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers a set of PSR-4 directories for a given namespace, either
|
||||
* appending or prepending to the ones previously set for this namespace.
|
||||
*
|
||||
* @param string $prefix The prefix/namespace, with trailing '\\'
|
||||
* @param list<string>|string $paths The PSR-4 base directories
|
||||
* @param bool $prepend Whether to prepend the directories
|
||||
*
|
||||
* @throws \InvalidArgumentException
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function addPsr4($prefix, $paths, $prepend = false)
|
||||
{
|
||||
$paths = (array) $paths;
|
||||
if (!$prefix) {
|
||||
// Register directories for the root namespace.
|
||||
if ($prepend) {
|
||||
$this->fallbackDirsPsr4 = array_merge(
|
||||
$paths,
|
||||
$this->fallbackDirsPsr4
|
||||
);
|
||||
} else {
|
||||
$this->fallbackDirsPsr4 = array_merge(
|
||||
$this->fallbackDirsPsr4,
|
||||
$paths
|
||||
);
|
||||
}
|
||||
} elseif (!isset($this->prefixDirsPsr4[$prefix])) {
|
||||
// Register directories for a new namespace.
|
||||
$length = strlen($prefix);
|
||||
if ('\\' !== $prefix[$length - 1]) {
|
||||
throw new \InvalidArgumentException("A non-empty PSR-4 prefix must end with a namespace separator.");
|
||||
}
|
||||
$this->prefixLengthsPsr4[$prefix[0]][$prefix] = $length;
|
||||
$this->prefixDirsPsr4[$prefix] = $paths;
|
||||
} elseif ($prepend) {
|
||||
// Prepend directories for an already registered namespace.
|
||||
$this->prefixDirsPsr4[$prefix] = array_merge(
|
||||
$paths,
|
||||
$this->prefixDirsPsr4[$prefix]
|
||||
);
|
||||
} else {
|
||||
// Append directories for an already registered namespace.
|
||||
$this->prefixDirsPsr4[$prefix] = array_merge(
|
||||
$this->prefixDirsPsr4[$prefix],
|
||||
$paths
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers a set of PSR-0 directories for a given prefix,
|
||||
* replacing any others previously set for this prefix.
|
||||
*
|
||||
* @param string $prefix The prefix
|
||||
* @param list<string>|string $paths The PSR-0 base directories
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function set($prefix, $paths)
|
||||
{
|
||||
if (!$prefix) {
|
||||
$this->fallbackDirsPsr0 = (array) $paths;
|
||||
} else {
|
||||
$this->prefixesPsr0[$prefix[0]][$prefix] = (array) $paths;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers a set of PSR-4 directories for a given namespace,
|
||||
* replacing any others previously set for this namespace.
|
||||
*
|
||||
* @param string $prefix The prefix/namespace, with trailing '\\'
|
||||
* @param list<string>|string $paths The PSR-4 base directories
|
||||
*
|
||||
* @throws \InvalidArgumentException
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function setPsr4($prefix, $paths)
|
||||
{
|
||||
if (!$prefix) {
|
||||
$this->fallbackDirsPsr4 = (array) $paths;
|
||||
} else {
|
||||
$length = strlen($prefix);
|
||||
if ('\\' !== $prefix[$length - 1]) {
|
||||
throw new \InvalidArgumentException("A non-empty PSR-4 prefix must end with a namespace separator.");
|
||||
}
|
||||
$this->prefixLengthsPsr4[$prefix[0]][$prefix] = $length;
|
||||
$this->prefixDirsPsr4[$prefix] = (array) $paths;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Turns on searching the include path for class files.
|
||||
*
|
||||
* @param bool $useIncludePath
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function setUseIncludePath($useIncludePath)
|
||||
{
|
||||
$this->useIncludePath = $useIncludePath;
|
||||
}
|
||||
|
||||
/**
|
||||
* Can be used to check if the autoloader uses the include path to check
|
||||
* for classes.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function getUseIncludePath()
|
||||
{
|
||||
return $this->useIncludePath;
|
||||
}
|
||||
|
||||
/**
|
||||
* Turns off searching the prefix and fallback directories for classes
|
||||
* that have not been registered with the class map.
|
||||
*
|
||||
* @param bool $classMapAuthoritative
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function setClassMapAuthoritative($classMapAuthoritative)
|
||||
{
|
||||
$this->classMapAuthoritative = $classMapAuthoritative;
|
||||
}
|
||||
|
||||
/**
|
||||
* Should class lookup fail if not found in the current class map?
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function isClassMapAuthoritative()
|
||||
{
|
||||
return $this->classMapAuthoritative;
|
||||
}
|
||||
|
||||
/**
|
||||
* APCu prefix to use to cache found/not-found classes, if the extension is enabled.
|
||||
*
|
||||
* @param string|null $apcuPrefix
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function setApcuPrefix($apcuPrefix)
|
||||
{
|
||||
$this->apcuPrefix = function_exists('apcu_fetch') && filter_var(ini_get('apc.enabled'), FILTER_VALIDATE_BOOLEAN) ? $apcuPrefix : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* The APCu prefix in use, or null if APCu caching is not enabled.
|
||||
*
|
||||
* @return string|null
|
||||
*/
|
||||
public function getApcuPrefix()
|
||||
{
|
||||
return $this->apcuPrefix;
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers this instance as an autoloader.
|
||||
*
|
||||
* @param bool $prepend Whether to prepend the autoloader or not
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function register($prepend = false)
|
||||
{
|
||||
spl_autoload_register(array($this, 'loadClass'), true, $prepend);
|
||||
|
||||
if (null === $this->vendorDir) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ($prepend) {
|
||||
self::$registeredLoaders = array($this->vendorDir => $this) + self::$registeredLoaders;
|
||||
} else {
|
||||
unset(self::$registeredLoaders[$this->vendorDir]);
|
||||
self::$registeredLoaders[$this->vendorDir] = $this;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Unregisters this instance as an autoloader.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function unregister()
|
||||
{
|
||||
spl_autoload_unregister(array($this, 'loadClass'));
|
||||
|
||||
if (null !== $this->vendorDir) {
|
||||
unset(self::$registeredLoaders[$this->vendorDir]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads the given class or interface.
|
||||
*
|
||||
* @param string $class The name of the class
|
||||
* @return true|null True if loaded, null otherwise
|
||||
*/
|
||||
public function loadClass($class)
|
||||
{
|
||||
if ($file = $this->findFile($class)) {
|
||||
$includeFile = self::$includeFile;
|
||||
$includeFile($file);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds the path to the file where the class is defined.
|
||||
*
|
||||
* @param string $class The name of the class
|
||||
*
|
||||
* @return string|false The path if found, false otherwise
|
||||
*/
|
||||
public function findFile($class)
|
||||
{
|
||||
// class map lookup
|
||||
if (isset($this->classMap[$class])) {
|
||||
return $this->classMap[$class];
|
||||
}
|
||||
if ($this->classMapAuthoritative || isset($this->missingClasses[$class])) {
|
||||
return false;
|
||||
}
|
||||
if (null !== $this->apcuPrefix) {
|
||||
$file = apcu_fetch($this->apcuPrefix.$class, $hit);
|
||||
if ($hit) {
|
||||
return $file;
|
||||
}
|
||||
}
|
||||
|
||||
$file = $this->findFileWithExtension($class, '.php');
|
||||
|
||||
// Search for Hack files if we are running on HHVM
|
||||
if (false === $file && defined('HHVM_VERSION')) {
|
||||
$file = $this->findFileWithExtension($class, '.hh');
|
||||
}
|
||||
|
||||
if (null !== $this->apcuPrefix) {
|
||||
apcu_add($this->apcuPrefix.$class, $file);
|
||||
}
|
||||
|
||||
if (false === $file) {
|
||||
// Remember that this class does not exist.
|
||||
$this->missingClasses[$class] = true;
|
||||
}
|
||||
|
||||
return $file;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the currently registered loaders keyed by their corresponding vendor directories.
|
||||
*
|
||||
* @return array<string, self>
|
||||
*/
|
||||
public static function getRegisteredLoaders()
|
||||
{
|
||||
return self::$registeredLoaders;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $class
|
||||
* @param string $ext
|
||||
* @return string|false
|
||||
*/
|
||||
private function findFileWithExtension($class, $ext)
|
||||
{
|
||||
// PSR-4 lookup
|
||||
$logicalPathPsr4 = strtr($class, '\\', DIRECTORY_SEPARATOR) . $ext;
|
||||
|
||||
$first = $class[0];
|
||||
if (isset($this->prefixLengthsPsr4[$first])) {
|
||||
$subPath = $class;
|
||||
while (false !== $lastPos = strrpos($subPath, '\\')) {
|
||||
$subPath = substr($subPath, 0, $lastPos);
|
||||
$search = $subPath . '\\';
|
||||
if (isset($this->prefixDirsPsr4[$search])) {
|
||||
$pathEnd = DIRECTORY_SEPARATOR . substr($logicalPathPsr4, $lastPos + 1);
|
||||
foreach ($this->prefixDirsPsr4[$search] as $dir) {
|
||||
if (file_exists($file = $dir . $pathEnd)) {
|
||||
return $file;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// PSR-4 fallback dirs
|
||||
foreach ($this->fallbackDirsPsr4 as $dir) {
|
||||
if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr4)) {
|
||||
return $file;
|
||||
}
|
||||
}
|
||||
|
||||
// PSR-0 lookup
|
||||
if (false !== $pos = strrpos($class, '\\')) {
|
||||
// namespaced class name
|
||||
$logicalPathPsr0 = substr($logicalPathPsr4, 0, $pos + 1)
|
||||
. strtr(substr($logicalPathPsr4, $pos + 1), '_', DIRECTORY_SEPARATOR);
|
||||
} else {
|
||||
// PEAR-like class name
|
||||
$logicalPathPsr0 = strtr($class, '_', DIRECTORY_SEPARATOR) . $ext;
|
||||
}
|
||||
|
||||
if (isset($this->prefixesPsr0[$first])) {
|
||||
foreach ($this->prefixesPsr0[$first] as $prefix => $dirs) {
|
||||
if (0 === strpos($class, $prefix)) {
|
||||
foreach ($dirs as $dir) {
|
||||
if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) {
|
||||
return $file;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// PSR-0 fallback dirs
|
||||
foreach ($this->fallbackDirsPsr0 as $dir) {
|
||||
if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) {
|
||||
return $file;
|
||||
}
|
||||
}
|
||||
|
||||
// PSR-0 include paths.
|
||||
if ($this->useIncludePath && $file = stream_resolve_include_path($logicalPathPsr0)) {
|
||||
return $file;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return void
|
||||
*/
|
||||
private static function initializeIncludeClosure()
|
||||
{
|
||||
if (self::$includeFile !== null) {
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* Scope isolated include.
|
||||
*
|
||||
* Prevents access to $this/self from included files.
|
||||
*
|
||||
* @param string $file
|
||||
* @return void
|
||||
*/
|
||||
self::$includeFile = \Closure::bind(static function($file) {
|
||||
include $file;
|
||||
}, null, null);
|
||||
}
|
||||
}
|
||||
396
vendor/composer/InstalledVersions.php
vendored
Normal file
396
vendor/composer/InstalledVersions.php
vendored
Normal file
@@ -0,0 +1,396 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Composer.
|
||||
*
|
||||
* (c) Nils Adermann <naderman@naderman.de>
|
||||
* Jordi Boggiano <j.boggiano@seld.be>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Composer;
|
||||
|
||||
use Composer\Autoload\ClassLoader;
|
||||
use Composer\Semver\VersionParser;
|
||||
|
||||
/**
|
||||
* This class is copied in every Composer installed project and available to all
|
||||
*
|
||||
* See also https://getcomposer.org/doc/07-runtime.md#installed-versions
|
||||
*
|
||||
* To require its presence, you can require `composer-runtime-api ^2.0`
|
||||
*
|
||||
* @final
|
||||
*/
|
||||
class InstalledVersions
|
||||
{
|
||||
/**
|
||||
* @var string|null if set (by reflection by Composer), this should be set to the path where this class is being copied to
|
||||
* @internal
|
||||
*/
|
||||
private static $selfDir = null;
|
||||
|
||||
/**
|
||||
* @var mixed[]|null
|
||||
* @psalm-var array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>}|array{}|null
|
||||
*/
|
||||
private static $installed;
|
||||
|
||||
/**
|
||||
* @var bool
|
||||
*/
|
||||
private static $installedIsLocalDir;
|
||||
|
||||
/**
|
||||
* @var bool|null
|
||||
*/
|
||||
private static $canGetVendors;
|
||||
|
||||
/**
|
||||
* @var array[]
|
||||
* @psalm-var array<string, array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>}>
|
||||
*/
|
||||
private static $installedByVendor = array();
|
||||
|
||||
/**
|
||||
* Returns a list of all package names which are present, either by being installed, replaced or provided
|
||||
*
|
||||
* @return string[]
|
||||
* @psalm-return list<string>
|
||||
*/
|
||||
public static function getInstalledPackages()
|
||||
{
|
||||
$packages = array();
|
||||
foreach (self::getInstalled() as $installed) {
|
||||
$packages[] = array_keys($installed['versions']);
|
||||
}
|
||||
|
||||
if (1 === \count($packages)) {
|
||||
return $packages[0];
|
||||
}
|
||||
|
||||
return array_keys(array_flip(\call_user_func_array('array_merge', $packages)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a list of all package names with a specific type e.g. 'library'
|
||||
*
|
||||
* @param string $type
|
||||
* @return string[]
|
||||
* @psalm-return list<string>
|
||||
*/
|
||||
public static function getInstalledPackagesByType($type)
|
||||
{
|
||||
$packagesByType = array();
|
||||
|
||||
foreach (self::getInstalled() as $installed) {
|
||||
foreach ($installed['versions'] as $name => $package) {
|
||||
if (isset($package['type']) && $package['type'] === $type) {
|
||||
$packagesByType[] = $name;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $packagesByType;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether the given package is installed
|
||||
*
|
||||
* This also returns true if the package name is provided or replaced by another package
|
||||
*
|
||||
* @param string $packageName
|
||||
* @param bool $includeDevRequirements
|
||||
* @return bool
|
||||
*/
|
||||
public static function isInstalled($packageName, $includeDevRequirements = true)
|
||||
{
|
||||
foreach (self::getInstalled() as $installed) {
|
||||
if (isset($installed['versions'][$packageName])) {
|
||||
return $includeDevRequirements || !isset($installed['versions'][$packageName]['dev_requirement']) || $installed['versions'][$packageName]['dev_requirement'] === false;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether the given package satisfies a version constraint
|
||||
*
|
||||
* e.g. If you want to know whether version 2.3+ of package foo/bar is installed, you would call:
|
||||
*
|
||||
* Composer\InstalledVersions::satisfies(new VersionParser, 'foo/bar', '^2.3')
|
||||
*
|
||||
* @param VersionParser $parser Install composer/semver to have access to this class and functionality
|
||||
* @param string $packageName
|
||||
* @param string|null $constraint A version constraint to check for, if you pass one you have to make sure composer/semver is required by your package
|
||||
* @return bool
|
||||
*/
|
||||
public static function satisfies(VersionParser $parser, $packageName, $constraint)
|
||||
{
|
||||
$constraint = $parser->parseConstraints((string) $constraint);
|
||||
$provided = $parser->parseConstraints(self::getVersionRanges($packageName));
|
||||
|
||||
return $provided->matches($constraint);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a version constraint representing all the range(s) which are installed for a given package
|
||||
*
|
||||
* It is easier to use this via isInstalled() with the $constraint argument if you need to check
|
||||
* whether a given version of a package is installed, and not just whether it exists
|
||||
*
|
||||
* @param string $packageName
|
||||
* @return string Version constraint usable with composer/semver
|
||||
*/
|
||||
public static function getVersionRanges($packageName)
|
||||
{
|
||||
foreach (self::getInstalled() as $installed) {
|
||||
if (!isset($installed['versions'][$packageName])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$ranges = array();
|
||||
if (isset($installed['versions'][$packageName]['pretty_version'])) {
|
||||
$ranges[] = $installed['versions'][$packageName]['pretty_version'];
|
||||
}
|
||||
if (array_key_exists('aliases', $installed['versions'][$packageName])) {
|
||||
$ranges = array_merge($ranges, $installed['versions'][$packageName]['aliases']);
|
||||
}
|
||||
if (array_key_exists('replaced', $installed['versions'][$packageName])) {
|
||||
$ranges = array_merge($ranges, $installed['versions'][$packageName]['replaced']);
|
||||
}
|
||||
if (array_key_exists('provided', $installed['versions'][$packageName])) {
|
||||
$ranges = array_merge($ranges, $installed['versions'][$packageName]['provided']);
|
||||
}
|
||||
|
||||
return implode(' || ', $ranges);
|
||||
}
|
||||
|
||||
throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed');
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $packageName
|
||||
* @return string|null If the package is being replaced or provided but is not really installed, null will be returned as version, use satisfies or getVersionRanges if you need to know if a given version is present
|
||||
*/
|
||||
public static function getVersion($packageName)
|
||||
{
|
||||
foreach (self::getInstalled() as $installed) {
|
||||
if (!isset($installed['versions'][$packageName])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!isset($installed['versions'][$packageName]['version'])) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $installed['versions'][$packageName]['version'];
|
||||
}
|
||||
|
||||
throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed');
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $packageName
|
||||
* @return string|null If the package is being replaced or provided but is not really installed, null will be returned as version, use satisfies or getVersionRanges if you need to know if a given version is present
|
||||
*/
|
||||
public static function getPrettyVersion($packageName)
|
||||
{
|
||||
foreach (self::getInstalled() as $installed) {
|
||||
if (!isset($installed['versions'][$packageName])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!isset($installed['versions'][$packageName]['pretty_version'])) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $installed['versions'][$packageName]['pretty_version'];
|
||||
}
|
||||
|
||||
throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed');
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $packageName
|
||||
* @return string|null If the package is being replaced or provided but is not really installed, null will be returned as reference
|
||||
*/
|
||||
public static function getReference($packageName)
|
||||
{
|
||||
foreach (self::getInstalled() as $installed) {
|
||||
if (!isset($installed['versions'][$packageName])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!isset($installed['versions'][$packageName]['reference'])) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $installed['versions'][$packageName]['reference'];
|
||||
}
|
||||
|
||||
throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed');
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $packageName
|
||||
* @return string|null If the package is being replaced or provided but is not really installed, null will be returned as install path. Packages of type metapackages also have a null install path.
|
||||
*/
|
||||
public static function getInstallPath($packageName)
|
||||
{
|
||||
foreach (self::getInstalled() as $installed) {
|
||||
if (!isset($installed['versions'][$packageName])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
return isset($installed['versions'][$packageName]['install_path']) ? $installed['versions'][$packageName]['install_path'] : null;
|
||||
}
|
||||
|
||||
throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed');
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
* @psalm-return array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}
|
||||
*/
|
||||
public static function getRootPackage()
|
||||
{
|
||||
$installed = self::getInstalled();
|
||||
|
||||
return $installed[0]['root'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the raw installed.php data for custom implementations
|
||||
*
|
||||
* @deprecated Use getAllRawData() instead which returns all datasets for all autoloaders present in the process. getRawData only returns the first dataset loaded, which may not be what you expect.
|
||||
* @return array[]
|
||||
* @psalm-return array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>}
|
||||
*/
|
||||
public static function getRawData()
|
||||
{
|
||||
@trigger_error('getRawData only returns the first dataset loaded, which may not be what you expect. Use getAllRawData() instead which returns all datasets for all autoloaders present in the process.', E_USER_DEPRECATED);
|
||||
|
||||
if (null === self::$installed) {
|
||||
// only require the installed.php file if this file is loaded from its dumped location,
|
||||
// and not from its source location in the composer/composer package, see https://github.com/composer/composer/issues/9937
|
||||
if (substr(__DIR__, -8, 1) !== 'C') {
|
||||
self::$installed = include __DIR__ . '/installed.php';
|
||||
} else {
|
||||
self::$installed = array();
|
||||
}
|
||||
}
|
||||
|
||||
return self::$installed;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the raw data of all installed.php which are currently loaded for custom implementations
|
||||
*
|
||||
* @return array[]
|
||||
* @psalm-return list<array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>}>
|
||||
*/
|
||||
public static function getAllRawData()
|
||||
{
|
||||
return self::getInstalled();
|
||||
}
|
||||
|
||||
/**
|
||||
* Lets you reload the static array from another file
|
||||
*
|
||||
* This is only useful for complex integrations in which a project needs to use
|
||||
* this class but then also needs to execute another project's autoloader in process,
|
||||
* and wants to ensure both projects have access to their version of installed.php.
|
||||
*
|
||||
* A typical case would be PHPUnit, where it would need to make sure it reads all
|
||||
* the data it needs from this class, then call reload() with
|
||||
* `require $CWD/vendor/composer/installed.php` (or similar) as input to make sure
|
||||
* the project in which it runs can then also use this class safely, without
|
||||
* interference between PHPUnit's dependencies and the project's dependencies.
|
||||
*
|
||||
* @param array[] $data A vendor/composer/installed.php data set
|
||||
* @return void
|
||||
*
|
||||
* @psalm-param array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>} $data
|
||||
*/
|
||||
public static function reload($data)
|
||||
{
|
||||
self::$installed = $data;
|
||||
self::$installedByVendor = array();
|
||||
|
||||
// when using reload, we disable the duplicate protection to ensure that self::$installed data is
|
||||
// always returned, but we cannot know whether it comes from the installed.php in __DIR__ or not,
|
||||
// so we have to assume it does not, and that may result in duplicate data being returned when listing
|
||||
// all installed packages for example
|
||||
self::$installedIsLocalDir = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
private static function getSelfDir()
|
||||
{
|
||||
if (self::$selfDir === null) {
|
||||
self::$selfDir = strtr(__DIR__, '\\', '/');
|
||||
}
|
||||
|
||||
return self::$selfDir;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array[]
|
||||
* @psalm-return list<array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>}>
|
||||
*/
|
||||
private static function getInstalled()
|
||||
{
|
||||
if (null === self::$canGetVendors) {
|
||||
self::$canGetVendors = method_exists('Composer\Autoload\ClassLoader', 'getRegisteredLoaders');
|
||||
}
|
||||
|
||||
$installed = array();
|
||||
$copiedLocalDir = false;
|
||||
|
||||
if (self::$canGetVendors) {
|
||||
$selfDir = self::getSelfDir();
|
||||
foreach (ClassLoader::getRegisteredLoaders() as $vendorDir => $loader) {
|
||||
$vendorDir = strtr($vendorDir, '\\', '/');
|
||||
if (isset(self::$installedByVendor[$vendorDir])) {
|
||||
$installed[] = self::$installedByVendor[$vendorDir];
|
||||
} elseif (is_file($vendorDir.'/composer/installed.php')) {
|
||||
/** @var array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>} $required */
|
||||
$required = require $vendorDir.'/composer/installed.php';
|
||||
self::$installedByVendor[$vendorDir] = $required;
|
||||
$installed[] = $required;
|
||||
if (self::$installed === null && $vendorDir.'/composer' === $selfDir) {
|
||||
self::$installed = $required;
|
||||
self::$installedIsLocalDir = true;
|
||||
}
|
||||
}
|
||||
if (self::$installedIsLocalDir && $vendorDir.'/composer' === $selfDir) {
|
||||
$copiedLocalDir = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (null === self::$installed) {
|
||||
// only require the installed.php file if this file is loaded from its dumped location,
|
||||
// and not from its source location in the composer/composer package, see https://github.com/composer/composer/issues/9937
|
||||
if (substr(__DIR__, -8, 1) !== 'C') {
|
||||
/** @var array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>} $required */
|
||||
$required = require __DIR__ . '/installed.php';
|
||||
self::$installed = $required;
|
||||
} else {
|
||||
self::$installed = array();
|
||||
}
|
||||
}
|
||||
|
||||
if (self::$installed !== array() && !$copiedLocalDir) {
|
||||
$installed[] = self::$installed;
|
||||
}
|
||||
|
||||
return $installed;
|
||||
}
|
||||
}
|
||||
21
vendor/composer/LICENSE
vendored
Normal file
21
vendor/composer/LICENSE
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
|
||||
Copyright (c) Nils Adermann, Jordi Boggiano
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is furnished
|
||||
to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
|
||||
15
vendor/composer/autoload_classmap.php
vendored
Normal file
15
vendor/composer/autoload_classmap.php
vendored
Normal file
@@ -0,0 +1,15 @@
|
||||
<?php
|
||||
|
||||
// autoload_classmap.php @generated by Composer
|
||||
|
||||
$vendorDir = dirname(__DIR__);
|
||||
$baseDir = dirname($vendorDir);
|
||||
|
||||
return array(
|
||||
'Attribute' => $vendorDir . '/symfony/polyfill-php80/Resources/stubs/Attribute.php',
|
||||
'Composer\\InstalledVersions' => $vendorDir . '/composer/InstalledVersions.php',
|
||||
'PhpToken' => $vendorDir . '/symfony/polyfill-php80/Resources/stubs/PhpToken.php',
|
||||
'Stringable' => $vendorDir . '/symfony/polyfill-php80/Resources/stubs/Stringable.php',
|
||||
'UnhandledMatchError' => $vendorDir . '/symfony/polyfill-php80/Resources/stubs/UnhandledMatchError.php',
|
||||
'ValueError' => $vendorDir . '/symfony/polyfill-php80/Resources/stubs/ValueError.php',
|
||||
);
|
||||
15
vendor/composer/autoload_files.php
vendored
Normal file
15
vendor/composer/autoload_files.php
vendored
Normal file
@@ -0,0 +1,15 @@
|
||||
<?php
|
||||
|
||||
// autoload_files.php @generated by Composer
|
||||
|
||||
$vendorDir = dirname(__DIR__);
|
||||
$baseDir = dirname($vendorDir);
|
||||
|
||||
return array(
|
||||
'7b11c4dc42b3b3023073cb14e519683c' => $vendorDir . '/ralouphie/getallheaders/src/getallheaders.php',
|
||||
'6e3fae29631ef280660b3cdad06f25a8' => $vendorDir . '/symfony/deprecation-contracts/function.php',
|
||||
'320cde22f66dd4f5d3fd621d3e88b98f' => $vendorDir . '/symfony/polyfill-ctype/bootstrap.php',
|
||||
'0e6d7bf4a5811bfa5cf40c5ccd6fae6a' => $vendorDir . '/symfony/polyfill-mbstring/bootstrap.php',
|
||||
'a4a119a56e50fbb293281d9a48007e0e' => $vendorDir . '/symfony/polyfill-php80/bootstrap.php',
|
||||
'37a3dc5111fe8f707ab4c132ef1dbc62' => $vendorDir . '/guzzlehttp/guzzle/src/functions_include.php',
|
||||
);
|
||||
9
vendor/composer/autoload_namespaces.php
vendored
Normal file
9
vendor/composer/autoload_namespaces.php
vendored
Normal file
@@ -0,0 +1,9 @@
|
||||
<?php
|
||||
|
||||
// autoload_namespaces.php @generated by Composer
|
||||
|
||||
$vendorDir = dirname(__DIR__);
|
||||
$baseDir = dirname($vendorDir);
|
||||
|
||||
return array(
|
||||
);
|
||||
21
vendor/composer/autoload_psr4.php
vendored
Normal file
21
vendor/composer/autoload_psr4.php
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
<?php
|
||||
|
||||
// autoload_psr4.php @generated by Composer
|
||||
|
||||
$vendorDir = dirname(__DIR__);
|
||||
$baseDir = dirname($vendorDir);
|
||||
|
||||
return array(
|
||||
'Symfony\\Polyfill\\Php80\\' => array($vendorDir . '/symfony/polyfill-php80'),
|
||||
'Symfony\\Polyfill\\Mbstring\\' => array($vendorDir . '/symfony/polyfill-mbstring'),
|
||||
'Symfony\\Polyfill\\Ctype\\' => array($vendorDir . '/symfony/polyfill-ctype'),
|
||||
'Psr\\Http\\Message\\' => array($vendorDir . '/psr/http-factory/src', $vendorDir . '/psr/http-message/src'),
|
||||
'Psr\\Http\\Client\\' => array($vendorDir . '/psr/http-client/src'),
|
||||
'PhpOption\\' => array($vendorDir . '/phpoption/phpoption/src/PhpOption'),
|
||||
'GuzzleHttp\\Psr7\\' => array($vendorDir . '/guzzlehttp/psr7/src'),
|
||||
'GuzzleHttp\\Promise\\' => array($vendorDir . '/guzzlehttp/promises/src'),
|
||||
'GuzzleHttp\\' => array($vendorDir . '/guzzlehttp/guzzle/src'),
|
||||
'GrahamCampbell\\ResultType\\' => array($vendorDir . '/graham-campbell/result-type/src'),
|
||||
'Dotenv\\' => array($vendorDir . '/vlucas/phpdotenv/src'),
|
||||
'App\\' => array($baseDir . '/src'),
|
||||
);
|
||||
50
vendor/composer/autoload_real.php
vendored
Normal file
50
vendor/composer/autoload_real.php
vendored
Normal file
@@ -0,0 +1,50 @@
|
||||
<?php
|
||||
|
||||
// autoload_real.php @generated by Composer
|
||||
|
||||
class ComposerAutoloaderInit6c68720c4b8879a1af2b630d6aeb512e
|
||||
{
|
||||
private static $loader;
|
||||
|
||||
public static function loadClassLoader($class)
|
||||
{
|
||||
if ('Composer\Autoload\ClassLoader' === $class) {
|
||||
require __DIR__ . '/ClassLoader.php';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return \Composer\Autoload\ClassLoader
|
||||
*/
|
||||
public static function getLoader()
|
||||
{
|
||||
if (null !== self::$loader) {
|
||||
return self::$loader;
|
||||
}
|
||||
|
||||
require __DIR__ . '/platform_check.php';
|
||||
|
||||
spl_autoload_register(array('ComposerAutoloaderInit6c68720c4b8879a1af2b630d6aeb512e', 'loadClassLoader'), true, true);
|
||||
self::$loader = $loader = new \Composer\Autoload\ClassLoader(\dirname(__DIR__));
|
||||
spl_autoload_unregister(array('ComposerAutoloaderInit6c68720c4b8879a1af2b630d6aeb512e', 'loadClassLoader'));
|
||||
|
||||
require __DIR__ . '/autoload_static.php';
|
||||
call_user_func(\Composer\Autoload\ComposerStaticInit6c68720c4b8879a1af2b630d6aeb512e::getInitializer($loader));
|
||||
|
||||
$loader->register(true);
|
||||
|
||||
$filesToLoad = \Composer\Autoload\ComposerStaticInit6c68720c4b8879a1af2b630d6aeb512e::$files;
|
||||
$requireFile = \Closure::bind(static function ($fileIdentifier, $file) {
|
||||
if (empty($GLOBALS['__composer_autoload_files'][$fileIdentifier])) {
|
||||
$GLOBALS['__composer_autoload_files'][$fileIdentifier] = true;
|
||||
|
||||
require $file;
|
||||
}
|
||||
}, null, null);
|
||||
foreach ($filesToLoad as $fileIdentifier => $file) {
|
||||
$requireFile($fileIdentifier, $file);
|
||||
}
|
||||
|
||||
return $loader;
|
||||
}
|
||||
}
|
||||
118
vendor/composer/autoload_static.php
vendored
Normal file
118
vendor/composer/autoload_static.php
vendored
Normal file
@@ -0,0 +1,118 @@
|
||||
<?php
|
||||
|
||||
// autoload_static.php @generated by Composer
|
||||
|
||||
namespace Composer\Autoload;
|
||||
|
||||
class ComposerStaticInit6c68720c4b8879a1af2b630d6aeb512e
|
||||
{
|
||||
public static $files = array (
|
||||
'7b11c4dc42b3b3023073cb14e519683c' => __DIR__ . '/..' . '/ralouphie/getallheaders/src/getallheaders.php',
|
||||
'6e3fae29631ef280660b3cdad06f25a8' => __DIR__ . '/..' . '/symfony/deprecation-contracts/function.php',
|
||||
'320cde22f66dd4f5d3fd621d3e88b98f' => __DIR__ . '/..' . '/symfony/polyfill-ctype/bootstrap.php',
|
||||
'0e6d7bf4a5811bfa5cf40c5ccd6fae6a' => __DIR__ . '/..' . '/symfony/polyfill-mbstring/bootstrap.php',
|
||||
'a4a119a56e50fbb293281d9a48007e0e' => __DIR__ . '/..' . '/symfony/polyfill-php80/bootstrap.php',
|
||||
'37a3dc5111fe8f707ab4c132ef1dbc62' => __DIR__ . '/..' . '/guzzlehttp/guzzle/src/functions_include.php',
|
||||
);
|
||||
|
||||
public static $prefixLengthsPsr4 = array (
|
||||
'S' =>
|
||||
array (
|
||||
'Symfony\\Polyfill\\Php80\\' => 23,
|
||||
'Symfony\\Polyfill\\Mbstring\\' => 26,
|
||||
'Symfony\\Polyfill\\Ctype\\' => 23,
|
||||
),
|
||||
'P' =>
|
||||
array (
|
||||
'Psr\\Http\\Message\\' => 17,
|
||||
'Psr\\Http\\Client\\' => 16,
|
||||
'PhpOption\\' => 10,
|
||||
),
|
||||
'G' =>
|
||||
array (
|
||||
'GuzzleHttp\\Psr7\\' => 16,
|
||||
'GuzzleHttp\\Promise\\' => 19,
|
||||
'GuzzleHttp\\' => 11,
|
||||
'GrahamCampbell\\ResultType\\' => 26,
|
||||
),
|
||||
'D' =>
|
||||
array (
|
||||
'Dotenv\\' => 7,
|
||||
),
|
||||
'A' =>
|
||||
array (
|
||||
'App\\' => 4,
|
||||
),
|
||||
);
|
||||
|
||||
public static $prefixDirsPsr4 = array (
|
||||
'Symfony\\Polyfill\\Php80\\' =>
|
||||
array (
|
||||
0 => __DIR__ . '/..' . '/symfony/polyfill-php80',
|
||||
),
|
||||
'Symfony\\Polyfill\\Mbstring\\' =>
|
||||
array (
|
||||
0 => __DIR__ . '/..' . '/symfony/polyfill-mbstring',
|
||||
),
|
||||
'Symfony\\Polyfill\\Ctype\\' =>
|
||||
array (
|
||||
0 => __DIR__ . '/..' . '/symfony/polyfill-ctype',
|
||||
),
|
||||
'Psr\\Http\\Message\\' =>
|
||||
array (
|
||||
0 => __DIR__ . '/..' . '/psr/http-factory/src',
|
||||
1 => __DIR__ . '/..' . '/psr/http-message/src',
|
||||
),
|
||||
'Psr\\Http\\Client\\' =>
|
||||
array (
|
||||
0 => __DIR__ . '/..' . '/psr/http-client/src',
|
||||
),
|
||||
'PhpOption\\' =>
|
||||
array (
|
||||
0 => __DIR__ . '/..' . '/phpoption/phpoption/src/PhpOption',
|
||||
),
|
||||
'GuzzleHttp\\Psr7\\' =>
|
||||
array (
|
||||
0 => __DIR__ . '/..' . '/guzzlehttp/psr7/src',
|
||||
),
|
||||
'GuzzleHttp\\Promise\\' =>
|
||||
array (
|
||||
0 => __DIR__ . '/..' . '/guzzlehttp/promises/src',
|
||||
),
|
||||
'GuzzleHttp\\' =>
|
||||
array (
|
||||
0 => __DIR__ . '/..' . '/guzzlehttp/guzzle/src',
|
||||
),
|
||||
'GrahamCampbell\\ResultType\\' =>
|
||||
array (
|
||||
0 => __DIR__ . '/..' . '/graham-campbell/result-type/src',
|
||||
),
|
||||
'Dotenv\\' =>
|
||||
array (
|
||||
0 => __DIR__ . '/..' . '/vlucas/phpdotenv/src',
|
||||
),
|
||||
'App\\' =>
|
||||
array (
|
||||
0 => __DIR__ . '/../..' . '/src',
|
||||
),
|
||||
);
|
||||
|
||||
public static $classMap = array (
|
||||
'Attribute' => __DIR__ . '/..' . '/symfony/polyfill-php80/Resources/stubs/Attribute.php',
|
||||
'Composer\\InstalledVersions' => __DIR__ . '/..' . '/composer/InstalledVersions.php',
|
||||
'PhpToken' => __DIR__ . '/..' . '/symfony/polyfill-php80/Resources/stubs/PhpToken.php',
|
||||
'Stringable' => __DIR__ . '/..' . '/symfony/polyfill-php80/Resources/stubs/Stringable.php',
|
||||
'UnhandledMatchError' => __DIR__ . '/..' . '/symfony/polyfill-php80/Resources/stubs/UnhandledMatchError.php',
|
||||
'ValueError' => __DIR__ . '/..' . '/symfony/polyfill-php80/Resources/stubs/ValueError.php',
|
||||
);
|
||||
|
||||
public static function getInitializer(ClassLoader $loader)
|
||||
{
|
||||
return \Closure::bind(function () use ($loader) {
|
||||
$loader->prefixLengthsPsr4 = ComposerStaticInit6c68720c4b8879a1af2b630d6aeb512e::$prefixLengthsPsr4;
|
||||
$loader->prefixDirsPsr4 = ComposerStaticInit6c68720c4b8879a1af2b630d6aeb512e::$prefixDirsPsr4;
|
||||
$loader->classMap = ComposerStaticInit6c68720c4b8879a1af2b630d6aeb512e::$classMap;
|
||||
|
||||
}, null, ClassLoader::class);
|
||||
}
|
||||
}
|
||||
1117
vendor/composer/installed.json
vendored
Normal file
1117
vendor/composer/installed.json
vendored
Normal file
File diff suppressed because it is too large
Load Diff
167
vendor/composer/installed.php
vendored
Normal file
167
vendor/composer/installed.php
vendored
Normal file
@@ -0,0 +1,167 @@
|
||||
<?php return array(
|
||||
'root' => array(
|
||||
'name' => 'backpro/seo-manager',
|
||||
'pretty_version' => '1.0.0+no-version-set',
|
||||
'version' => '1.0.0.0',
|
||||
'reference' => null,
|
||||
'type' => 'project',
|
||||
'install_path' => __DIR__ . '/../../',
|
||||
'aliases' => array(),
|
||||
'dev' => false,
|
||||
),
|
||||
'versions' => array(
|
||||
'backpro/seo-manager' => array(
|
||||
'pretty_version' => '1.0.0+no-version-set',
|
||||
'version' => '1.0.0.0',
|
||||
'reference' => null,
|
||||
'type' => 'project',
|
||||
'install_path' => __DIR__ . '/../../',
|
||||
'aliases' => array(),
|
||||
'dev_requirement' => false,
|
||||
),
|
||||
'graham-campbell/result-type' => array(
|
||||
'pretty_version' => 'v1.1.4',
|
||||
'version' => '1.1.4.0',
|
||||
'reference' => 'e01f4a821471308ba86aa202fed6698b6b695e3b',
|
||||
'type' => 'library',
|
||||
'install_path' => __DIR__ . '/../graham-campbell/result-type',
|
||||
'aliases' => array(),
|
||||
'dev_requirement' => false,
|
||||
),
|
||||
'guzzlehttp/guzzle' => array(
|
||||
'pretty_version' => '7.10.0',
|
||||
'version' => '7.10.0.0',
|
||||
'reference' => 'b51ac707cfa420b7bfd4e4d5e510ba8008e822b4',
|
||||
'type' => 'library',
|
||||
'install_path' => __DIR__ . '/../guzzlehttp/guzzle',
|
||||
'aliases' => array(),
|
||||
'dev_requirement' => false,
|
||||
),
|
||||
'guzzlehttp/promises' => array(
|
||||
'pretty_version' => '2.3.0',
|
||||
'version' => '2.3.0.0',
|
||||
'reference' => '481557b130ef3790cf82b713667b43030dc9c957',
|
||||
'type' => 'library',
|
||||
'install_path' => __DIR__ . '/../guzzlehttp/promises',
|
||||
'aliases' => array(),
|
||||
'dev_requirement' => false,
|
||||
),
|
||||
'guzzlehttp/psr7' => array(
|
||||
'pretty_version' => '2.8.0',
|
||||
'version' => '2.8.0.0',
|
||||
'reference' => '21dc724a0583619cd1652f673303492272778051',
|
||||
'type' => 'library',
|
||||
'install_path' => __DIR__ . '/../guzzlehttp/psr7',
|
||||
'aliases' => array(),
|
||||
'dev_requirement' => false,
|
||||
),
|
||||
'phpoption/phpoption' => array(
|
||||
'pretty_version' => '1.9.5',
|
||||
'version' => '1.9.5.0',
|
||||
'reference' => '75365b91986c2405cf5e1e012c5595cd487a98be',
|
||||
'type' => 'library',
|
||||
'install_path' => __DIR__ . '/../phpoption/phpoption',
|
||||
'aliases' => array(),
|
||||
'dev_requirement' => false,
|
||||
),
|
||||
'psr/http-client' => array(
|
||||
'pretty_version' => '1.0.3',
|
||||
'version' => '1.0.3.0',
|
||||
'reference' => 'bb5906edc1c324c9a05aa0873d40117941e5fa90',
|
||||
'type' => 'library',
|
||||
'install_path' => __DIR__ . '/../psr/http-client',
|
||||
'aliases' => array(),
|
||||
'dev_requirement' => false,
|
||||
),
|
||||
'psr/http-client-implementation' => array(
|
||||
'dev_requirement' => false,
|
||||
'provided' => array(
|
||||
0 => '1.0',
|
||||
),
|
||||
),
|
||||
'psr/http-factory' => array(
|
||||
'pretty_version' => '1.1.0',
|
||||
'version' => '1.1.0.0',
|
||||
'reference' => '2b4765fddfe3b508ac62f829e852b1501d3f6e8a',
|
||||
'type' => 'library',
|
||||
'install_path' => __DIR__ . '/../psr/http-factory',
|
||||
'aliases' => array(),
|
||||
'dev_requirement' => false,
|
||||
),
|
||||
'psr/http-factory-implementation' => array(
|
||||
'dev_requirement' => false,
|
||||
'provided' => array(
|
||||
0 => '1.0',
|
||||
),
|
||||
),
|
||||
'psr/http-message' => array(
|
||||
'pretty_version' => '2.0',
|
||||
'version' => '2.0.0.0',
|
||||
'reference' => '402d35bcb92c70c026d1a6a9883f06b2ead23d71',
|
||||
'type' => 'library',
|
||||
'install_path' => __DIR__ . '/../psr/http-message',
|
||||
'aliases' => array(),
|
||||
'dev_requirement' => false,
|
||||
),
|
||||
'psr/http-message-implementation' => array(
|
||||
'dev_requirement' => false,
|
||||
'provided' => array(
|
||||
0 => '1.0',
|
||||
),
|
||||
),
|
||||
'ralouphie/getallheaders' => array(
|
||||
'pretty_version' => '3.0.3',
|
||||
'version' => '3.0.3.0',
|
||||
'reference' => '120b605dfeb996808c31b6477290a714d356e822',
|
||||
'type' => 'library',
|
||||
'install_path' => __DIR__ . '/../ralouphie/getallheaders',
|
||||
'aliases' => array(),
|
||||
'dev_requirement' => false,
|
||||
),
|
||||
'symfony/deprecation-contracts' => array(
|
||||
'pretty_version' => 'v3.6.0',
|
||||
'version' => '3.6.0.0',
|
||||
'reference' => '63afe740e99a13ba87ec199bb07bbdee937a5b62',
|
||||
'type' => 'library',
|
||||
'install_path' => __DIR__ . '/../symfony/deprecation-contracts',
|
||||
'aliases' => array(),
|
||||
'dev_requirement' => false,
|
||||
),
|
||||
'symfony/polyfill-ctype' => array(
|
||||
'pretty_version' => 'v1.33.0',
|
||||
'version' => '1.33.0.0',
|
||||
'reference' => 'a3cc8b044a6ea513310cbd48ef7333b384945638',
|
||||
'type' => 'library',
|
||||
'install_path' => __DIR__ . '/../symfony/polyfill-ctype',
|
||||
'aliases' => array(),
|
||||
'dev_requirement' => false,
|
||||
),
|
||||
'symfony/polyfill-mbstring' => array(
|
||||
'pretty_version' => 'v1.33.0',
|
||||
'version' => '1.33.0.0',
|
||||
'reference' => '6d857f4d76bd4b343eac26d6b539585d2bc56493',
|
||||
'type' => 'library',
|
||||
'install_path' => __DIR__ . '/../symfony/polyfill-mbstring',
|
||||
'aliases' => array(),
|
||||
'dev_requirement' => false,
|
||||
),
|
||||
'symfony/polyfill-php80' => array(
|
||||
'pretty_version' => 'v1.33.0',
|
||||
'version' => '1.33.0.0',
|
||||
'reference' => '0cc9dd0f17f61d8131e7df6b84bd344899fe2608',
|
||||
'type' => 'library',
|
||||
'install_path' => __DIR__ . '/../symfony/polyfill-php80',
|
||||
'aliases' => array(),
|
||||
'dev_requirement' => false,
|
||||
),
|
||||
'vlucas/phpdotenv' => array(
|
||||
'pretty_version' => 'v5.6.3',
|
||||
'version' => '5.6.3.0',
|
||||
'reference' => '955e7815d677a3eaa7075231212f2110983adecc',
|
||||
'type' => 'library',
|
||||
'install_path' => __DIR__ . '/../vlucas/phpdotenv',
|
||||
'aliases' => array(),
|
||||
'dev_requirement' => false,
|
||||
),
|
||||
),
|
||||
);
|
||||
25
vendor/composer/platform_check.php
vendored
Normal file
25
vendor/composer/platform_check.php
vendored
Normal file
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
// platform_check.php @generated by Composer
|
||||
|
||||
$issues = array();
|
||||
|
||||
if (!(PHP_VERSION_ID >= 80100)) {
|
||||
$issues[] = 'Your Composer dependencies require a PHP version ">= 8.1.0". You are running ' . PHP_VERSION . '.';
|
||||
}
|
||||
|
||||
if ($issues) {
|
||||
if (!headers_sent()) {
|
||||
header('HTTP/1.1 500 Internal Server Error');
|
||||
}
|
||||
if (!ini_get('display_errors')) {
|
||||
if (PHP_SAPI === 'cli' || PHP_SAPI === 'phpdbg') {
|
||||
fwrite(STDERR, 'Composer detected issues in your platform:' . PHP_EOL.PHP_EOL . implode(PHP_EOL, $issues) . PHP_EOL.PHP_EOL);
|
||||
} elseif (!headers_sent()) {
|
||||
echo 'Composer detected issues in your platform:' . PHP_EOL.PHP_EOL . str_replace('You are running '.PHP_VERSION.'.', '', implode(PHP_EOL, $issues)) . PHP_EOL.PHP_EOL;
|
||||
}
|
||||
}
|
||||
throw new \RuntimeException(
|
||||
'Composer detected issues in your platform: ' . implode(' ', $issues)
|
||||
);
|
||||
}
|
||||
21
vendor/graham-campbell/result-type/LICENSE
vendored
Normal file
21
vendor/graham-campbell/result-type/LICENSE
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2020-2024 Graham Campbell <hello@gjcampbell.co.uk>
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
33
vendor/graham-campbell/result-type/composer.json
vendored
Normal file
33
vendor/graham-campbell/result-type/composer.json
vendored
Normal file
@@ -0,0 +1,33 @@
|
||||
{
|
||||
"name": "graham-campbell/result-type",
|
||||
"description": "An Implementation Of The Result Type",
|
||||
"keywords": ["result", "result-type", "Result", "Result Type", "Result-Type", "Graham Campbell", "GrahamCampbell"],
|
||||
"license": "MIT",
|
||||
"authors": [
|
||||
{
|
||||
"name": "Graham Campbell",
|
||||
"email": "hello@gjcampbell.co.uk",
|
||||
"homepage": "https://github.com/GrahamCampbell"
|
||||
}
|
||||
],
|
||||
"require": {
|
||||
"php": "^7.2.5 || ^8.0",
|
||||
"phpoption/phpoption": "^1.9.5"
|
||||
},
|
||||
"require-dev": {
|
||||
"phpunit/phpunit": "^8.5.41 || ^9.6.22 || ^10.5.45 || ^11.5.7"
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"GrahamCampbell\\ResultType\\": "src/"
|
||||
}
|
||||
},
|
||||
"autoload-dev": {
|
||||
"psr-4": {
|
||||
"GrahamCampbell\\Tests\\ResultType\\": "tests/"
|
||||
}
|
||||
},
|
||||
"config": {
|
||||
"preferred-install": "dist"
|
||||
}
|
||||
}
|
||||
121
vendor/graham-campbell/result-type/src/Error.php
vendored
Normal file
121
vendor/graham-campbell/result-type/src/Error.php
vendored
Normal file
@@ -0,0 +1,121 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of Result Type.
|
||||
*
|
||||
* (c) Graham Campbell <hello@gjcampbell.co.uk>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace GrahamCampbell\ResultType;
|
||||
|
||||
use PhpOption\None;
|
||||
use PhpOption\Some;
|
||||
|
||||
/**
|
||||
* @template T
|
||||
* @template E
|
||||
*
|
||||
* @extends \GrahamCampbell\ResultType\Result<T,E>
|
||||
*/
|
||||
final class Error extends Result
|
||||
{
|
||||
/**
|
||||
* @var E
|
||||
*/
|
||||
private $value;
|
||||
|
||||
/**
|
||||
* Internal constructor for an error value.
|
||||
*
|
||||
* @param E $value
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private function __construct($value)
|
||||
{
|
||||
$this->value = $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new error value.
|
||||
*
|
||||
* @template F
|
||||
*
|
||||
* @param F $value
|
||||
*
|
||||
* @return \GrahamCampbell\ResultType\Result<T,F>
|
||||
*/
|
||||
public static function create($value)
|
||||
{
|
||||
return new self($value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the success option value.
|
||||
*
|
||||
* @return \PhpOption\Option<T>
|
||||
*/
|
||||
public function success()
|
||||
{
|
||||
return None::create();
|
||||
}
|
||||
|
||||
/**
|
||||
* Map over the success value.
|
||||
*
|
||||
* @template S
|
||||
*
|
||||
* @param callable(T):S $f
|
||||
*
|
||||
* @return \GrahamCampbell\ResultType\Result<S,E>
|
||||
*/
|
||||
public function map(callable $f)
|
||||
{
|
||||
return self::create($this->value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Flat map over the success value.
|
||||
*
|
||||
* @template S
|
||||
* @template F
|
||||
*
|
||||
* @param callable(T):\GrahamCampbell\ResultType\Result<S,F> $f
|
||||
*
|
||||
* @return \GrahamCampbell\ResultType\Result<S,F>
|
||||
*/
|
||||
public function flatMap(callable $f)
|
||||
{
|
||||
/** @var \GrahamCampbell\ResultType\Result<S,F> */
|
||||
return self::create($this->value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the error option value.
|
||||
*
|
||||
* @return \PhpOption\Option<E>
|
||||
*/
|
||||
public function error()
|
||||
{
|
||||
return Some::create($this->value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Map over the error value.
|
||||
*
|
||||
* @template F
|
||||
*
|
||||
* @param callable(E):F $f
|
||||
*
|
||||
* @return \GrahamCampbell\ResultType\Result<T,F>
|
||||
*/
|
||||
public function mapError(callable $f)
|
||||
{
|
||||
return self::create($f($this->value));
|
||||
}
|
||||
}
|
||||
69
vendor/graham-campbell/result-type/src/Result.php
vendored
Normal file
69
vendor/graham-campbell/result-type/src/Result.php
vendored
Normal file
@@ -0,0 +1,69 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of Result Type.
|
||||
*
|
||||
* (c) Graham Campbell <hello@gjcampbell.co.uk>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace GrahamCampbell\ResultType;
|
||||
|
||||
/**
|
||||
* @template T
|
||||
* @template E
|
||||
*/
|
||||
abstract class Result
|
||||
{
|
||||
/**
|
||||
* Get the success option value.
|
||||
*
|
||||
* @return \PhpOption\Option<T>
|
||||
*/
|
||||
abstract public function success();
|
||||
|
||||
/**
|
||||
* Map over the success value.
|
||||
*
|
||||
* @template S
|
||||
*
|
||||
* @param callable(T):S $f
|
||||
*
|
||||
* @return \GrahamCampbell\ResultType\Result<S,E>
|
||||
*/
|
||||
abstract public function map(callable $f);
|
||||
|
||||
/**
|
||||
* Flat map over the success value.
|
||||
*
|
||||
* @template S
|
||||
* @template F
|
||||
*
|
||||
* @param callable(T):\GrahamCampbell\ResultType\Result<S,F> $f
|
||||
*
|
||||
* @return \GrahamCampbell\ResultType\Result<S,F>
|
||||
*/
|
||||
abstract public function flatMap(callable $f);
|
||||
|
||||
/**
|
||||
* Get the error option value.
|
||||
*
|
||||
* @return \PhpOption\Option<E>
|
||||
*/
|
||||
abstract public function error();
|
||||
|
||||
/**
|
||||
* Map over the error value.
|
||||
*
|
||||
* @template F
|
||||
*
|
||||
* @param callable(E):F $f
|
||||
*
|
||||
* @return \GrahamCampbell\ResultType\Result<T,F>
|
||||
*/
|
||||
abstract public function mapError(callable $f);
|
||||
}
|
||||
120
vendor/graham-campbell/result-type/src/Success.php
vendored
Normal file
120
vendor/graham-campbell/result-type/src/Success.php
vendored
Normal file
@@ -0,0 +1,120 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of Result Type.
|
||||
*
|
||||
* (c) Graham Campbell <hello@gjcampbell.co.uk>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace GrahamCampbell\ResultType;
|
||||
|
||||
use PhpOption\None;
|
||||
use PhpOption\Some;
|
||||
|
||||
/**
|
||||
* @template T
|
||||
* @template E
|
||||
*
|
||||
* @extends \GrahamCampbell\ResultType\Result<T,E>
|
||||
*/
|
||||
final class Success extends Result
|
||||
{
|
||||
/**
|
||||
* @var T
|
||||
*/
|
||||
private $value;
|
||||
|
||||
/**
|
||||
* Internal constructor for a success value.
|
||||
*
|
||||
* @param T $value
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private function __construct($value)
|
||||
{
|
||||
$this->value = $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new error value.
|
||||
*
|
||||
* @template S
|
||||
*
|
||||
* @param S $value
|
||||
*
|
||||
* @return \GrahamCampbell\ResultType\Result<S,E>
|
||||
*/
|
||||
public static function create($value)
|
||||
{
|
||||
return new self($value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the success option value.
|
||||
*
|
||||
* @return \PhpOption\Option<T>
|
||||
*/
|
||||
public function success()
|
||||
{
|
||||
return Some::create($this->value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Map over the success value.
|
||||
*
|
||||
* @template S
|
||||
*
|
||||
* @param callable(T):S $f
|
||||
*
|
||||
* @return \GrahamCampbell\ResultType\Result<S,E>
|
||||
*/
|
||||
public function map(callable $f)
|
||||
{
|
||||
return self::create($f($this->value));
|
||||
}
|
||||
|
||||
/**
|
||||
* Flat map over the success value.
|
||||
*
|
||||
* @template S
|
||||
* @template F
|
||||
*
|
||||
* @param callable(T):\GrahamCampbell\ResultType\Result<S,F> $f
|
||||
*
|
||||
* @return \GrahamCampbell\ResultType\Result<S,F>
|
||||
*/
|
||||
public function flatMap(callable $f)
|
||||
{
|
||||
return $f($this->value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the error option value.
|
||||
*
|
||||
* @return \PhpOption\Option<E>
|
||||
*/
|
||||
public function error()
|
||||
{
|
||||
return None::create();
|
||||
}
|
||||
|
||||
/**
|
||||
* Map over the error value.
|
||||
*
|
||||
* @template F
|
||||
*
|
||||
* @param callable(E):F $f
|
||||
*
|
||||
* @return \GrahamCampbell\ResultType\Result<T,F>
|
||||
*/
|
||||
public function mapError(callable $f)
|
||||
{
|
||||
return self::create($this->value);
|
||||
}
|
||||
}
|
||||
1683
vendor/guzzlehttp/guzzle/CHANGELOG.md
vendored
Normal file
1683
vendor/guzzlehttp/guzzle/CHANGELOG.md
vendored
Normal file
File diff suppressed because it is too large
Load Diff
27
vendor/guzzlehttp/guzzle/LICENSE
vendored
Normal file
27
vendor/guzzlehttp/guzzle/LICENSE
vendored
Normal file
@@ -0,0 +1,27 @@
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2011 Michael Dowling <mtdowling@gmail.com>
|
||||
Copyright (c) 2012 Jeremy Lindblom <jeremeamia@gmail.com>
|
||||
Copyright (c) 2014 Graham Campbell <hello@gjcampbell.co.uk>
|
||||
Copyright (c) 2015 Márk Sági-Kazár <mark.sagikazar@gmail.com>
|
||||
Copyright (c) 2015 Tobias Schultze <webmaster@tubo-world.de>
|
||||
Copyright (c) 2016 Tobias Nyholm <tobias.nyholm@gmail.com>
|
||||
Copyright (c) 2016 George Mponos <gmponos@gmail.com>
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
94
vendor/guzzlehttp/guzzle/README.md
vendored
Normal file
94
vendor/guzzlehttp/guzzle/README.md
vendored
Normal file
@@ -0,0 +1,94 @@
|
||||

|
||||
|
||||
# Guzzle, PHP HTTP client
|
||||
|
||||
[](https://github.com/guzzle/guzzle/releases)
|
||||
[](https://github.com/guzzle/guzzle/actions?query=workflow%3ACI)
|
||||
[](https://packagist.org/packages/guzzlehttp/guzzle)
|
||||
|
||||
Guzzle is a PHP HTTP client that makes it easy to send HTTP requests and
|
||||
trivial to integrate with web services.
|
||||
|
||||
- Simple interface for building query strings, POST requests, streaming large
|
||||
uploads, streaming large downloads, using HTTP cookies, uploading JSON data,
|
||||
etc...
|
||||
- Can send both synchronous and asynchronous requests using the same interface.
|
||||
- Uses PSR-7 interfaces for requests, responses, and streams. This allows you
|
||||
to utilize other PSR-7 compatible libraries with Guzzle.
|
||||
- Supports PSR-18 allowing interoperability between other PSR-18 HTTP Clients.
|
||||
- Abstracts away the underlying HTTP transport, allowing you to write
|
||||
environment and transport agnostic code; i.e., no hard dependency on cURL,
|
||||
PHP streams, sockets, or non-blocking event loops.
|
||||
- Middleware system allows you to augment and compose client behavior.
|
||||
|
||||
```php
|
||||
$client = new \GuzzleHttp\Client();
|
||||
$response = $client->request('GET', 'https://api.github.com/repos/guzzle/guzzle');
|
||||
|
||||
echo $response->getStatusCode(); // 200
|
||||
echo $response->getHeaderLine('content-type'); // 'application/json; charset=utf8'
|
||||
echo $response->getBody(); // '{"id": 1420053, "name": "guzzle", ...}'
|
||||
|
||||
// Send an asynchronous request.
|
||||
$request = new \GuzzleHttp\Psr7\Request('GET', 'http://httpbin.org');
|
||||
$promise = $client->sendAsync($request)->then(function ($response) {
|
||||
echo 'I completed! ' . $response->getBody();
|
||||
});
|
||||
|
||||
$promise->wait();
|
||||
```
|
||||
|
||||
## Help and docs
|
||||
|
||||
We use GitHub issues only to discuss bugs and new features. For support please refer to:
|
||||
|
||||
- [Documentation](https://docs.guzzlephp.org)
|
||||
- [Stack Overflow](https://stackoverflow.com/questions/tagged/guzzle)
|
||||
- [#guzzle](https://app.slack.com/client/T0D2S9JCT/CE6UAAKL4) channel on [PHP-HTTP Slack](https://slack.httplug.io/)
|
||||
- [Gitter](https://gitter.im/guzzle/guzzle)
|
||||
|
||||
|
||||
## Installing Guzzle
|
||||
|
||||
The recommended way to install Guzzle is through
|
||||
[Composer](https://getcomposer.org/).
|
||||
|
||||
```bash
|
||||
composer require guzzlehttp/guzzle
|
||||
```
|
||||
|
||||
|
||||
## Version Guidance
|
||||
|
||||
| Version | Status | Packagist | Namespace | Repo | Docs | PSR-7 | PHP Version |
|
||||
|---------|---------------------|---------------------|--------------|---------------------|---------------------|-------|--------------|
|
||||
| 3.x | EOL (2016-10-31) | `guzzle/guzzle` | `Guzzle` | [v3][guzzle-3-repo] | [v3][guzzle-3-docs] | No | >=5.3.3,<7.0 |
|
||||
| 4.x | EOL (2016-10-31) | `guzzlehttp/guzzle` | `GuzzleHttp` | [v4][guzzle-4-repo] | N/A | No | >=5.4,<7.0 |
|
||||
| 5.x | EOL (2019-10-31) | `guzzlehttp/guzzle` | `GuzzleHttp` | [v5][guzzle-5-repo] | [v5][guzzle-5-docs] | No | >=5.4,<7.4 |
|
||||
| 6.x | EOL (2023-10-31) | `guzzlehttp/guzzle` | `GuzzleHttp` | [v6][guzzle-6-repo] | [v6][guzzle-6-docs] | Yes | >=5.5,<8.0 |
|
||||
| 7.x | Latest | `guzzlehttp/guzzle` | `GuzzleHttp` | [v7][guzzle-7-repo] | [v7][guzzle-7-docs] | Yes | >=7.2.5,<8.5 |
|
||||
|
||||
[guzzle-3-repo]: https://github.com/guzzle/guzzle3
|
||||
[guzzle-4-repo]: https://github.com/guzzle/guzzle/tree/4.x
|
||||
[guzzle-5-repo]: https://github.com/guzzle/guzzle/tree/5.3
|
||||
[guzzle-6-repo]: https://github.com/guzzle/guzzle/tree/6.5
|
||||
[guzzle-7-repo]: https://github.com/guzzle/guzzle
|
||||
[guzzle-3-docs]: https://guzzle3.readthedocs.io/
|
||||
[guzzle-5-docs]: https://docs.guzzlephp.org/en/5.3/
|
||||
[guzzle-6-docs]: https://docs.guzzlephp.org/en/6.5/
|
||||
[guzzle-7-docs]: https://docs.guzzlephp.org/en/latest/
|
||||
|
||||
|
||||
## Security
|
||||
|
||||
If you discover a security vulnerability within this package, please send an email to security@tidelift.com. All security vulnerabilities will be promptly addressed. Please do not disclose security-related issues publicly until a fix has been announced. Please see [Security Policy](https://github.com/guzzle/guzzle/security/policy) for more information.
|
||||
|
||||
## License
|
||||
|
||||
Guzzle is made available under the MIT License (MIT). Please see [License File](LICENSE) for more information.
|
||||
|
||||
## For Enterprise
|
||||
|
||||
Available as part of the Tidelift Subscription
|
||||
|
||||
The maintainers of Guzzle and thousands of other packages are working with Tidelift to deliver commercial support and maintenance for the open source dependencies you use to build your applications. Save time, reduce risk, and improve code health, while paying the maintainers of the exact dependencies you use. [Learn more.](https://tidelift.com/subscription/pkg/packagist-guzzlehttp-guzzle?utm_source=packagist-guzzlehttp-guzzle&utm_medium=referral&utm_campaign=enterprise&utm_term=repo)
|
||||
1253
vendor/guzzlehttp/guzzle/UPGRADING.md
vendored
Normal file
1253
vendor/guzzlehttp/guzzle/UPGRADING.md
vendored
Normal file
File diff suppressed because it is too large
Load Diff
131
vendor/guzzlehttp/guzzle/composer.json
vendored
Normal file
131
vendor/guzzlehttp/guzzle/composer.json
vendored
Normal file
@@ -0,0 +1,131 @@
|
||||
{
|
||||
"name": "guzzlehttp/guzzle",
|
||||
"description": "Guzzle is a PHP HTTP client library",
|
||||
"keywords": [
|
||||
"framework",
|
||||
"http",
|
||||
"rest",
|
||||
"web service",
|
||||
"curl",
|
||||
"client",
|
||||
"HTTP client",
|
||||
"PSR-7",
|
||||
"PSR-18"
|
||||
],
|
||||
"license": "MIT",
|
||||
"authors": [
|
||||
{
|
||||
"name": "Graham Campbell",
|
||||
"email": "hello@gjcampbell.co.uk",
|
||||
"homepage": "https://github.com/GrahamCampbell"
|
||||
},
|
||||
{
|
||||
"name": "Michael Dowling",
|
||||
"email": "mtdowling@gmail.com",
|
||||
"homepage": "https://github.com/mtdowling"
|
||||
},
|
||||
{
|
||||
"name": "Jeremy Lindblom",
|
||||
"email": "jeremeamia@gmail.com",
|
||||
"homepage": "https://github.com/jeremeamia"
|
||||
},
|
||||
{
|
||||
"name": "George Mponos",
|
||||
"email": "gmponos@gmail.com",
|
||||
"homepage": "https://github.com/gmponos"
|
||||
},
|
||||
{
|
||||
"name": "Tobias Nyholm",
|
||||
"email": "tobias.nyholm@gmail.com",
|
||||
"homepage": "https://github.com/Nyholm"
|
||||
},
|
||||
{
|
||||
"name": "Márk Sági-Kazár",
|
||||
"email": "mark.sagikazar@gmail.com",
|
||||
"homepage": "https://github.com/sagikazarmark"
|
||||
},
|
||||
{
|
||||
"name": "Tobias Schultze",
|
||||
"email": "webmaster@tubo-world.de",
|
||||
"homepage": "https://github.com/Tobion"
|
||||
}
|
||||
],
|
||||
"repositories": [
|
||||
{
|
||||
"type": "package",
|
||||
"package": {
|
||||
"name": "guzzle/client-integration-tests",
|
||||
"version": "v3.0.2",
|
||||
"dist": {
|
||||
"url": "https://codeload.github.com/guzzle/client-integration-tests/zip/2c025848417c1135031fdf9c728ee53d0a7ceaee",
|
||||
"type": "zip"
|
||||
},
|
||||
"require": {
|
||||
"php": "^7.2.5 || ^8.0",
|
||||
"phpunit/phpunit": "^7.5.20 || ^8.5.8 || ^9.3.11",
|
||||
"php-http/message": "^1.0 || ^2.0",
|
||||
"guzzlehttp/psr7": "^1.7 || ^2.0",
|
||||
"th3n3rd/cartesian-product": "^0.3"
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Http\\Client\\Tests\\": "src/"
|
||||
}
|
||||
},
|
||||
"bin": [
|
||||
"bin/http_test_server"
|
||||
]
|
||||
}
|
||||
}
|
||||
],
|
||||
"require": {
|
||||
"php": "^7.2.5 || ^8.0",
|
||||
"ext-json": "*",
|
||||
"guzzlehttp/promises": "^2.3",
|
||||
"guzzlehttp/psr7": "^2.8",
|
||||
"psr/http-client": "^1.0",
|
||||
"symfony/deprecation-contracts": "^2.2 || ^3.0"
|
||||
},
|
||||
"provide": {
|
||||
"psr/http-client-implementation": "1.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"ext-curl": "*",
|
||||
"bamarni/composer-bin-plugin": "^1.8.2",
|
||||
"guzzle/client-integration-tests": "3.0.2",
|
||||
"php-http/message-factory": "^1.1",
|
||||
"phpunit/phpunit": "^8.5.39 || ^9.6.20",
|
||||
"psr/log": "^1.1 || ^2.0 || ^3.0"
|
||||
},
|
||||
"suggest": {
|
||||
"ext-curl": "Required for CURL handler support",
|
||||
"ext-intl": "Required for Internationalized Domain Name (IDN) support",
|
||||
"psr/log": "Required for using the Log middleware"
|
||||
},
|
||||
"config": {
|
||||
"allow-plugins": {
|
||||
"bamarni/composer-bin-plugin": true
|
||||
},
|
||||
"preferred-install": "dist",
|
||||
"sort-packages": true
|
||||
},
|
||||
"extra": {
|
||||
"bamarni-bin": {
|
||||
"bin-links": true,
|
||||
"forward-command": false
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"GuzzleHttp\\": "src/"
|
||||
},
|
||||
"files": [
|
||||
"src/functions_include.php"
|
||||
]
|
||||
},
|
||||
"autoload-dev": {
|
||||
"psr-4": {
|
||||
"GuzzleHttp\\Tests\\": "tests/"
|
||||
}
|
||||
}
|
||||
}
|
||||
6
vendor/guzzlehttp/guzzle/package-lock.json
generated
vendored
Normal file
6
vendor/guzzlehttp/guzzle/package-lock.json
generated
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"name": "guzzle",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {}
|
||||
}
|
||||
28
vendor/guzzlehttp/guzzle/src/BodySummarizer.php
vendored
Normal file
28
vendor/guzzlehttp/guzzle/src/BodySummarizer.php
vendored
Normal file
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
namespace GuzzleHttp;
|
||||
|
||||
use Psr\Http\Message\MessageInterface;
|
||||
|
||||
final class BodySummarizer implements BodySummarizerInterface
|
||||
{
|
||||
/**
|
||||
* @var int|null
|
||||
*/
|
||||
private $truncateAt;
|
||||
|
||||
public function __construct(?int $truncateAt = null)
|
||||
{
|
||||
$this->truncateAt = $truncateAt;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a summarized message body.
|
||||
*/
|
||||
public function summarize(MessageInterface $message): ?string
|
||||
{
|
||||
return $this->truncateAt === null
|
||||
? Psr7\Message::bodySummary($message)
|
||||
: Psr7\Message::bodySummary($message, $this->truncateAt);
|
||||
}
|
||||
}
|
||||
13
vendor/guzzlehttp/guzzle/src/BodySummarizerInterface.php
vendored
Normal file
13
vendor/guzzlehttp/guzzle/src/BodySummarizerInterface.php
vendored
Normal file
@@ -0,0 +1,13 @@
|
||||
<?php
|
||||
|
||||
namespace GuzzleHttp;
|
||||
|
||||
use Psr\Http\Message\MessageInterface;
|
||||
|
||||
interface BodySummarizerInterface
|
||||
{
|
||||
/**
|
||||
* Returns a summarized message body.
|
||||
*/
|
||||
public function summarize(MessageInterface $message): ?string;
|
||||
}
|
||||
483
vendor/guzzlehttp/guzzle/src/Client.php
vendored
Normal file
483
vendor/guzzlehttp/guzzle/src/Client.php
vendored
Normal file
@@ -0,0 +1,483 @@
|
||||
<?php
|
||||
|
||||
namespace GuzzleHttp;
|
||||
|
||||
use GuzzleHttp\Cookie\CookieJar;
|
||||
use GuzzleHttp\Exception\GuzzleException;
|
||||
use GuzzleHttp\Exception\InvalidArgumentException;
|
||||
use GuzzleHttp\Promise as P;
|
||||
use GuzzleHttp\Promise\PromiseInterface;
|
||||
use Psr\Http\Message\RequestInterface;
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
use Psr\Http\Message\UriInterface;
|
||||
|
||||
/**
|
||||
* @final
|
||||
*/
|
||||
class Client implements ClientInterface, \Psr\Http\Client\ClientInterface
|
||||
{
|
||||
use ClientTrait;
|
||||
|
||||
/**
|
||||
* @var array Default request options
|
||||
*/
|
||||
private $config;
|
||||
|
||||
/**
|
||||
* Clients accept an array of constructor parameters.
|
||||
*
|
||||
* Here's an example of creating a client using a base_uri and an array of
|
||||
* default request options to apply to each request:
|
||||
*
|
||||
* $client = new Client([
|
||||
* 'base_uri' => 'http://www.foo.com/1.0/',
|
||||
* 'timeout' => 0,
|
||||
* 'allow_redirects' => false,
|
||||
* 'proxy' => '192.168.16.1:10'
|
||||
* ]);
|
||||
*
|
||||
* Client configuration settings include the following options:
|
||||
*
|
||||
* - handler: (callable) Function that transfers HTTP requests over the
|
||||
* wire. The function is called with a Psr7\Http\Message\RequestInterface
|
||||
* and array of transfer options, and must return a
|
||||
* GuzzleHttp\Promise\PromiseInterface that is fulfilled with a
|
||||
* Psr7\Http\Message\ResponseInterface on success.
|
||||
* If no handler is provided, a default handler will be created
|
||||
* that enables all of the request options below by attaching all of the
|
||||
* default middleware to the handler.
|
||||
* - base_uri: (string|UriInterface) Base URI of the client that is merged
|
||||
* into relative URIs. Can be a string or instance of UriInterface.
|
||||
* - **: any request option
|
||||
*
|
||||
* @param array $config Client configuration settings.
|
||||
*
|
||||
* @see RequestOptions for a list of available request options.
|
||||
*/
|
||||
public function __construct(array $config = [])
|
||||
{
|
||||
if (!isset($config['handler'])) {
|
||||
$config['handler'] = HandlerStack::create();
|
||||
} elseif (!\is_callable($config['handler'])) {
|
||||
throw new InvalidArgumentException('handler must be a callable');
|
||||
}
|
||||
|
||||
// Convert the base_uri to a UriInterface
|
||||
if (isset($config['base_uri'])) {
|
||||
$config['base_uri'] = Psr7\Utils::uriFor($config['base_uri']);
|
||||
}
|
||||
|
||||
$this->configureDefaults($config);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $method
|
||||
* @param array $args
|
||||
*
|
||||
* @return PromiseInterface|ResponseInterface
|
||||
*
|
||||
* @deprecated Client::__call will be removed in guzzlehttp/guzzle:8.0.
|
||||
*/
|
||||
public function __call($method, $args)
|
||||
{
|
||||
if (\count($args) < 1) {
|
||||
throw new InvalidArgumentException('Magic request methods require a URI and optional options array');
|
||||
}
|
||||
|
||||
$uri = $args[0];
|
||||
$opts = $args[1] ?? [];
|
||||
|
||||
return \substr($method, -5) === 'Async'
|
||||
? $this->requestAsync(\substr($method, 0, -5), $uri, $opts)
|
||||
: $this->request($method, $uri, $opts);
|
||||
}
|
||||
|
||||
/**
|
||||
* Asynchronously send an HTTP request.
|
||||
*
|
||||
* @param array $options Request options to apply to the given
|
||||
* request and to the transfer. See \GuzzleHttp\RequestOptions.
|
||||
*/
|
||||
public function sendAsync(RequestInterface $request, array $options = []): PromiseInterface
|
||||
{
|
||||
// Merge the base URI into the request URI if needed.
|
||||
$options = $this->prepareDefaults($options);
|
||||
|
||||
return $this->transfer(
|
||||
$request->withUri($this->buildUri($request->getUri(), $options), $request->hasHeader('Host')),
|
||||
$options
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Send an HTTP request.
|
||||
*
|
||||
* @param array $options Request options to apply to the given
|
||||
* request and to the transfer. See \GuzzleHttp\RequestOptions.
|
||||
*
|
||||
* @throws GuzzleException
|
||||
*/
|
||||
public function send(RequestInterface $request, array $options = []): ResponseInterface
|
||||
{
|
||||
$options[RequestOptions::SYNCHRONOUS] = true;
|
||||
|
||||
return $this->sendAsync($request, $options)->wait();
|
||||
}
|
||||
|
||||
/**
|
||||
* The HttpClient PSR (PSR-18) specify this method.
|
||||
*
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function sendRequest(RequestInterface $request): ResponseInterface
|
||||
{
|
||||
$options[RequestOptions::SYNCHRONOUS] = true;
|
||||
$options[RequestOptions::ALLOW_REDIRECTS] = false;
|
||||
$options[RequestOptions::HTTP_ERRORS] = false;
|
||||
|
||||
return $this->sendAsync($request, $options)->wait();
|
||||
}
|
||||
|
||||
/**
|
||||
* Create and send an asynchronous HTTP request.
|
||||
*
|
||||
* Use an absolute path to override the base path of the client, or a
|
||||
* relative path to append to the base path of the client. The URL can
|
||||
* contain the query string as well. Use an array to provide a URL
|
||||
* template and additional variables to use in the URL template expansion.
|
||||
*
|
||||
* @param string $method HTTP method
|
||||
* @param string|UriInterface $uri URI object or string.
|
||||
* @param array $options Request options to apply. See \GuzzleHttp\RequestOptions.
|
||||
*/
|
||||
public function requestAsync(string $method, $uri = '', array $options = []): PromiseInterface
|
||||
{
|
||||
$options = $this->prepareDefaults($options);
|
||||
// Remove request modifying parameter because it can be done up-front.
|
||||
$headers = $options['headers'] ?? [];
|
||||
$body = $options['body'] ?? null;
|
||||
$version = $options['version'] ?? '1.1';
|
||||
// Merge the URI into the base URI.
|
||||
$uri = $this->buildUri(Psr7\Utils::uriFor($uri), $options);
|
||||
if (\is_array($body)) {
|
||||
throw $this->invalidBody();
|
||||
}
|
||||
$request = new Psr7\Request($method, $uri, $headers, $body, $version);
|
||||
// Remove the option so that they are not doubly-applied.
|
||||
unset($options['headers'], $options['body'], $options['version']);
|
||||
|
||||
return $this->transfer($request, $options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create and send an HTTP request.
|
||||
*
|
||||
* Use an absolute path to override the base path of the client, or a
|
||||
* relative path to append to the base path of the client. The URL can
|
||||
* contain the query string as well.
|
||||
*
|
||||
* @param string $method HTTP method.
|
||||
* @param string|UriInterface $uri URI object or string.
|
||||
* @param array $options Request options to apply. See \GuzzleHttp\RequestOptions.
|
||||
*
|
||||
* @throws GuzzleException
|
||||
*/
|
||||
public function request(string $method, $uri = '', array $options = []): ResponseInterface
|
||||
{
|
||||
$options[RequestOptions::SYNCHRONOUS] = true;
|
||||
|
||||
return $this->requestAsync($method, $uri, $options)->wait();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a client configuration option.
|
||||
*
|
||||
* These options include default request options of the client, a "handler"
|
||||
* (if utilized by the concrete client), and a "base_uri" if utilized by
|
||||
* the concrete client.
|
||||
*
|
||||
* @param string|null $option The config option to retrieve.
|
||||
*
|
||||
* @return mixed
|
||||
*
|
||||
* @deprecated Client::getConfig will be removed in guzzlehttp/guzzle:8.0.
|
||||
*/
|
||||
public function getConfig(?string $option = null)
|
||||
{
|
||||
return $option === null
|
||||
? $this->config
|
||||
: ($this->config[$option] ?? null);
|
||||
}
|
||||
|
||||
private function buildUri(UriInterface $uri, array $config): UriInterface
|
||||
{
|
||||
if (isset($config['base_uri'])) {
|
||||
$uri = Psr7\UriResolver::resolve(Psr7\Utils::uriFor($config['base_uri']), $uri);
|
||||
}
|
||||
|
||||
if (isset($config['idn_conversion']) && ($config['idn_conversion'] !== false)) {
|
||||
$idnOptions = ($config['idn_conversion'] === true) ? \IDNA_DEFAULT : $config['idn_conversion'];
|
||||
$uri = Utils::idnUriConvert($uri, $idnOptions);
|
||||
}
|
||||
|
||||
return $uri->getScheme() === '' && $uri->getHost() !== '' ? $uri->withScheme('http') : $uri;
|
||||
}
|
||||
|
||||
/**
|
||||
* Configures the default options for a client.
|
||||
*/
|
||||
private function configureDefaults(array $config): void
|
||||
{
|
||||
$defaults = [
|
||||
'allow_redirects' => RedirectMiddleware::$defaultSettings,
|
||||
'http_errors' => true,
|
||||
'decode_content' => true,
|
||||
'verify' => true,
|
||||
'cookies' => false,
|
||||
'idn_conversion' => false,
|
||||
];
|
||||
|
||||
// Use the standard Linux HTTP_PROXY and HTTPS_PROXY if set.
|
||||
|
||||
// We can only trust the HTTP_PROXY environment variable in a CLI
|
||||
// process due to the fact that PHP has no reliable mechanism to
|
||||
// get environment variables that start with "HTTP_".
|
||||
if (\PHP_SAPI === 'cli' && ($proxy = Utils::getenv('HTTP_PROXY'))) {
|
||||
$defaults['proxy']['http'] = $proxy;
|
||||
}
|
||||
|
||||
if ($proxy = Utils::getenv('HTTPS_PROXY')) {
|
||||
$defaults['proxy']['https'] = $proxy;
|
||||
}
|
||||
|
||||
if ($noProxy = Utils::getenv('NO_PROXY')) {
|
||||
$cleanedNoProxy = \str_replace(' ', '', $noProxy);
|
||||
$defaults['proxy']['no'] = \explode(',', $cleanedNoProxy);
|
||||
}
|
||||
|
||||
$this->config = $config + $defaults;
|
||||
|
||||
if (!empty($config['cookies']) && $config['cookies'] === true) {
|
||||
$this->config['cookies'] = new CookieJar();
|
||||
}
|
||||
|
||||
// Add the default user-agent header.
|
||||
if (!isset($this->config['headers'])) {
|
||||
$this->config['headers'] = ['User-Agent' => Utils::defaultUserAgent()];
|
||||
} else {
|
||||
// Add the User-Agent header if one was not already set.
|
||||
foreach (\array_keys($this->config['headers']) as $name) {
|
||||
if (\strtolower($name) === 'user-agent') {
|
||||
return;
|
||||
}
|
||||
}
|
||||
$this->config['headers']['User-Agent'] = Utils::defaultUserAgent();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Merges default options into the array.
|
||||
*
|
||||
* @param array $options Options to modify by reference
|
||||
*/
|
||||
private function prepareDefaults(array $options): array
|
||||
{
|
||||
$defaults = $this->config;
|
||||
|
||||
if (!empty($defaults['headers'])) {
|
||||
// Default headers are only added if they are not present.
|
||||
$defaults['_conditional'] = $defaults['headers'];
|
||||
unset($defaults['headers']);
|
||||
}
|
||||
|
||||
// Special handling for headers is required as they are added as
|
||||
// conditional headers and as headers passed to a request ctor.
|
||||
if (\array_key_exists('headers', $options)) {
|
||||
// Allows default headers to be unset.
|
||||
if ($options['headers'] === null) {
|
||||
$defaults['_conditional'] = [];
|
||||
unset($options['headers']);
|
||||
} elseif (!\is_array($options['headers'])) {
|
||||
throw new InvalidArgumentException('headers must be an array');
|
||||
}
|
||||
}
|
||||
|
||||
// Shallow merge defaults underneath options.
|
||||
$result = $options + $defaults;
|
||||
|
||||
// Remove null values.
|
||||
foreach ($result as $k => $v) {
|
||||
if ($v === null) {
|
||||
unset($result[$k]);
|
||||
}
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Transfers the given request and applies request options.
|
||||
*
|
||||
* The URI of the request is not modified and the request options are used
|
||||
* as-is without merging in default options.
|
||||
*
|
||||
* @param array $options See \GuzzleHttp\RequestOptions.
|
||||
*/
|
||||
private function transfer(RequestInterface $request, array $options): PromiseInterface
|
||||
{
|
||||
$request = $this->applyOptions($request, $options);
|
||||
/** @var HandlerStack $handler */
|
||||
$handler = $options['handler'];
|
||||
|
||||
try {
|
||||
return P\Create::promiseFor($handler($request, $options));
|
||||
} catch (\Exception $e) {
|
||||
return P\Create::rejectionFor($e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies the array of request options to a request.
|
||||
*/
|
||||
private function applyOptions(RequestInterface $request, array &$options): RequestInterface
|
||||
{
|
||||
$modify = [
|
||||
'set_headers' => [],
|
||||
];
|
||||
|
||||
if (isset($options['headers'])) {
|
||||
if (array_keys($options['headers']) === range(0, count($options['headers']) - 1)) {
|
||||
throw new InvalidArgumentException('The headers array must have header name as keys.');
|
||||
}
|
||||
$modify['set_headers'] = $options['headers'];
|
||||
unset($options['headers']);
|
||||
}
|
||||
|
||||
if (isset($options['form_params'])) {
|
||||
if (isset($options['multipart'])) {
|
||||
throw new InvalidArgumentException('You cannot use '
|
||||
.'form_params and multipart at the same time. Use the '
|
||||
.'form_params option if you want to send application/'
|
||||
.'x-www-form-urlencoded requests, and the multipart '
|
||||
.'option to send multipart/form-data requests.');
|
||||
}
|
||||
$options['body'] = \http_build_query($options['form_params'], '', '&');
|
||||
unset($options['form_params']);
|
||||
// Ensure that we don't have the header in different case and set the new value.
|
||||
$options['_conditional'] = Psr7\Utils::caselessRemove(['Content-Type'], $options['_conditional']);
|
||||
$options['_conditional']['Content-Type'] = 'application/x-www-form-urlencoded';
|
||||
}
|
||||
|
||||
if (isset($options['multipart'])) {
|
||||
$options['body'] = new Psr7\MultipartStream($options['multipart']);
|
||||
unset($options['multipart']);
|
||||
}
|
||||
|
||||
if (isset($options['json'])) {
|
||||
$options['body'] = Utils::jsonEncode($options['json']);
|
||||
unset($options['json']);
|
||||
// Ensure that we don't have the header in different case and set the new value.
|
||||
$options['_conditional'] = Psr7\Utils::caselessRemove(['Content-Type'], $options['_conditional']);
|
||||
$options['_conditional']['Content-Type'] = 'application/json';
|
||||
}
|
||||
|
||||
if (!empty($options['decode_content'])
|
||||
&& $options['decode_content'] !== true
|
||||
) {
|
||||
// Ensure that we don't have the header in different case and set the new value.
|
||||
$options['_conditional'] = Psr7\Utils::caselessRemove(['Accept-Encoding'], $options['_conditional']);
|
||||
$modify['set_headers']['Accept-Encoding'] = $options['decode_content'];
|
||||
}
|
||||
|
||||
if (isset($options['body'])) {
|
||||
if (\is_array($options['body'])) {
|
||||
throw $this->invalidBody();
|
||||
}
|
||||
$modify['body'] = Psr7\Utils::streamFor($options['body']);
|
||||
unset($options['body']);
|
||||
}
|
||||
|
||||
if (!empty($options['auth']) && \is_array($options['auth'])) {
|
||||
$value = $options['auth'];
|
||||
$type = isset($value[2]) ? \strtolower($value[2]) : 'basic';
|
||||
switch ($type) {
|
||||
case 'basic':
|
||||
// Ensure that we don't have the header in different case and set the new value.
|
||||
$modify['set_headers'] = Psr7\Utils::caselessRemove(['Authorization'], $modify['set_headers']);
|
||||
$modify['set_headers']['Authorization'] = 'Basic '
|
||||
.\base64_encode("$value[0]:$value[1]");
|
||||
break;
|
||||
case 'digest':
|
||||
// @todo: Do not rely on curl
|
||||
$options['curl'][\CURLOPT_HTTPAUTH] = \CURLAUTH_DIGEST;
|
||||
$options['curl'][\CURLOPT_USERPWD] = "$value[0]:$value[1]";
|
||||
break;
|
||||
case 'ntlm':
|
||||
$options['curl'][\CURLOPT_HTTPAUTH] = \CURLAUTH_NTLM;
|
||||
$options['curl'][\CURLOPT_USERPWD] = "$value[0]:$value[1]";
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (isset($options['query'])) {
|
||||
$value = $options['query'];
|
||||
if (\is_array($value)) {
|
||||
$value = \http_build_query($value, '', '&', \PHP_QUERY_RFC3986);
|
||||
}
|
||||
if (!\is_string($value)) {
|
||||
throw new InvalidArgumentException('query must be a string or array');
|
||||
}
|
||||
$modify['query'] = $value;
|
||||
unset($options['query']);
|
||||
}
|
||||
|
||||
// Ensure that sink is not an invalid value.
|
||||
if (isset($options['sink'])) {
|
||||
// TODO: Add more sink validation?
|
||||
if (\is_bool($options['sink'])) {
|
||||
throw new InvalidArgumentException('sink must not be a boolean');
|
||||
}
|
||||
}
|
||||
|
||||
if (isset($options['version'])) {
|
||||
$modify['version'] = $options['version'];
|
||||
}
|
||||
|
||||
$request = Psr7\Utils::modifyRequest($request, $modify);
|
||||
if ($request->getBody() instanceof Psr7\MultipartStream) {
|
||||
// Use a multipart/form-data POST if a Content-Type is not set.
|
||||
// Ensure that we don't have the header in different case and set the new value.
|
||||
$options['_conditional'] = Psr7\Utils::caselessRemove(['Content-Type'], $options['_conditional']);
|
||||
$options['_conditional']['Content-Type'] = 'multipart/form-data; boundary='
|
||||
.$request->getBody()->getBoundary();
|
||||
}
|
||||
|
||||
// Merge in conditional headers if they are not present.
|
||||
if (isset($options['_conditional'])) {
|
||||
// Build up the changes so it's in a single clone of the message.
|
||||
$modify = [];
|
||||
foreach ($options['_conditional'] as $k => $v) {
|
||||
if (!$request->hasHeader($k)) {
|
||||
$modify['set_headers'][$k] = $v;
|
||||
}
|
||||
}
|
||||
$request = Psr7\Utils::modifyRequest($request, $modify);
|
||||
// Don't pass this internal value along to middleware/handlers.
|
||||
unset($options['_conditional']);
|
||||
}
|
||||
|
||||
return $request;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return an InvalidArgumentException with pre-set message.
|
||||
*/
|
||||
private function invalidBody(): InvalidArgumentException
|
||||
{
|
||||
return new InvalidArgumentException('Passing in the "body" request '
|
||||
.'option as an array to send a request is not supported. '
|
||||
.'Please use the "form_params" request option to send a '
|
||||
.'application/x-www-form-urlencoded request, or the "multipart" '
|
||||
.'request option to send a multipart/form-data request.');
|
||||
}
|
||||
}
|
||||
84
vendor/guzzlehttp/guzzle/src/ClientInterface.php
vendored
Normal file
84
vendor/guzzlehttp/guzzle/src/ClientInterface.php
vendored
Normal file
@@ -0,0 +1,84 @@
|
||||
<?php
|
||||
|
||||
namespace GuzzleHttp;
|
||||
|
||||
use GuzzleHttp\Exception\GuzzleException;
|
||||
use GuzzleHttp\Promise\PromiseInterface;
|
||||
use Psr\Http\Message\RequestInterface;
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
use Psr\Http\Message\UriInterface;
|
||||
|
||||
/**
|
||||
* Client interface for sending HTTP requests.
|
||||
*/
|
||||
interface ClientInterface
|
||||
{
|
||||
/**
|
||||
* The Guzzle major version.
|
||||
*/
|
||||
public const MAJOR_VERSION = 7;
|
||||
|
||||
/**
|
||||
* Send an HTTP request.
|
||||
*
|
||||
* @param RequestInterface $request Request to send
|
||||
* @param array $options Request options to apply to the given
|
||||
* request and to the transfer.
|
||||
*
|
||||
* @throws GuzzleException
|
||||
*/
|
||||
public function send(RequestInterface $request, array $options = []): ResponseInterface;
|
||||
|
||||
/**
|
||||
* Asynchronously send an HTTP request.
|
||||
*
|
||||
* @param RequestInterface $request Request to send
|
||||
* @param array $options Request options to apply to the given
|
||||
* request and to the transfer.
|
||||
*/
|
||||
public function sendAsync(RequestInterface $request, array $options = []): PromiseInterface;
|
||||
|
||||
/**
|
||||
* Create and send an HTTP request.
|
||||
*
|
||||
* Use an absolute path to override the base path of the client, or a
|
||||
* relative path to append to the base path of the client. The URL can
|
||||
* contain the query string as well.
|
||||
*
|
||||
* @param string $method HTTP method.
|
||||
* @param string|UriInterface $uri URI object or string.
|
||||
* @param array $options Request options to apply.
|
||||
*
|
||||
* @throws GuzzleException
|
||||
*/
|
||||
public function request(string $method, $uri, array $options = []): ResponseInterface;
|
||||
|
||||
/**
|
||||
* Create and send an asynchronous HTTP request.
|
||||
*
|
||||
* Use an absolute path to override the base path of the client, or a
|
||||
* relative path to append to the base path of the client. The URL can
|
||||
* contain the query string as well. Use an array to provide a URL
|
||||
* template and additional variables to use in the URL template expansion.
|
||||
*
|
||||
* @param string $method HTTP method
|
||||
* @param string|UriInterface $uri URI object or string.
|
||||
* @param array $options Request options to apply.
|
||||
*/
|
||||
public function requestAsync(string $method, $uri, array $options = []): PromiseInterface;
|
||||
|
||||
/**
|
||||
* Get a client configuration option.
|
||||
*
|
||||
* These options include default request options of the client, a "handler"
|
||||
* (if utilized by the concrete client), and a "base_uri" if utilized by
|
||||
* the concrete client.
|
||||
*
|
||||
* @param string|null $option The config option to retrieve.
|
||||
*
|
||||
* @return mixed
|
||||
*
|
||||
* @deprecated ClientInterface::getConfig will be removed in guzzlehttp/guzzle:8.0.
|
||||
*/
|
||||
public function getConfig(?string $option = null);
|
||||
}
|
||||
241
vendor/guzzlehttp/guzzle/src/ClientTrait.php
vendored
Normal file
241
vendor/guzzlehttp/guzzle/src/ClientTrait.php
vendored
Normal file
@@ -0,0 +1,241 @@
|
||||
<?php
|
||||
|
||||
namespace GuzzleHttp;
|
||||
|
||||
use GuzzleHttp\Exception\GuzzleException;
|
||||
use GuzzleHttp\Promise\PromiseInterface;
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
use Psr\Http\Message\UriInterface;
|
||||
|
||||
/**
|
||||
* Client interface for sending HTTP requests.
|
||||
*/
|
||||
trait ClientTrait
|
||||
{
|
||||
/**
|
||||
* Create and send an HTTP request.
|
||||
*
|
||||
* Use an absolute path to override the base path of the client, or a
|
||||
* relative path to append to the base path of the client. The URL can
|
||||
* contain the query string as well.
|
||||
*
|
||||
* @param string $method HTTP method.
|
||||
* @param string|UriInterface $uri URI object or string.
|
||||
* @param array $options Request options to apply.
|
||||
*
|
||||
* @throws GuzzleException
|
||||
*/
|
||||
abstract public function request(string $method, $uri, array $options = []): ResponseInterface;
|
||||
|
||||
/**
|
||||
* Create and send an HTTP GET request.
|
||||
*
|
||||
* Use an absolute path to override the base path of the client, or a
|
||||
* relative path to append to the base path of the client. The URL can
|
||||
* contain the query string as well.
|
||||
*
|
||||
* @param string|UriInterface $uri URI object or string.
|
||||
* @param array $options Request options to apply.
|
||||
*
|
||||
* @throws GuzzleException
|
||||
*/
|
||||
public function get($uri, array $options = []): ResponseInterface
|
||||
{
|
||||
return $this->request('GET', $uri, $options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create and send an HTTP HEAD request.
|
||||
*
|
||||
* Use an absolute path to override the base path of the client, or a
|
||||
* relative path to append to the base path of the client. The URL can
|
||||
* contain the query string as well.
|
||||
*
|
||||
* @param string|UriInterface $uri URI object or string.
|
||||
* @param array $options Request options to apply.
|
||||
*
|
||||
* @throws GuzzleException
|
||||
*/
|
||||
public function head($uri, array $options = []): ResponseInterface
|
||||
{
|
||||
return $this->request('HEAD', $uri, $options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create and send an HTTP PUT request.
|
||||
*
|
||||
* Use an absolute path to override the base path of the client, or a
|
||||
* relative path to append to the base path of the client. The URL can
|
||||
* contain the query string as well.
|
||||
*
|
||||
* @param string|UriInterface $uri URI object or string.
|
||||
* @param array $options Request options to apply.
|
||||
*
|
||||
* @throws GuzzleException
|
||||
*/
|
||||
public function put($uri, array $options = []): ResponseInterface
|
||||
{
|
||||
return $this->request('PUT', $uri, $options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create and send an HTTP POST request.
|
||||
*
|
||||
* Use an absolute path to override the base path of the client, or a
|
||||
* relative path to append to the base path of the client. The URL can
|
||||
* contain the query string as well.
|
||||
*
|
||||
* @param string|UriInterface $uri URI object or string.
|
||||
* @param array $options Request options to apply.
|
||||
*
|
||||
* @throws GuzzleException
|
||||
*/
|
||||
public function post($uri, array $options = []): ResponseInterface
|
||||
{
|
||||
return $this->request('POST', $uri, $options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create and send an HTTP PATCH request.
|
||||
*
|
||||
* Use an absolute path to override the base path of the client, or a
|
||||
* relative path to append to the base path of the client. The URL can
|
||||
* contain the query string as well.
|
||||
*
|
||||
* @param string|UriInterface $uri URI object or string.
|
||||
* @param array $options Request options to apply.
|
||||
*
|
||||
* @throws GuzzleException
|
||||
*/
|
||||
public function patch($uri, array $options = []): ResponseInterface
|
||||
{
|
||||
return $this->request('PATCH', $uri, $options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create and send an HTTP DELETE request.
|
||||
*
|
||||
* Use an absolute path to override the base path of the client, or a
|
||||
* relative path to append to the base path of the client. The URL can
|
||||
* contain the query string as well.
|
||||
*
|
||||
* @param string|UriInterface $uri URI object or string.
|
||||
* @param array $options Request options to apply.
|
||||
*
|
||||
* @throws GuzzleException
|
||||
*/
|
||||
public function delete($uri, array $options = []): ResponseInterface
|
||||
{
|
||||
return $this->request('DELETE', $uri, $options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create and send an asynchronous HTTP request.
|
||||
*
|
||||
* Use an absolute path to override the base path of the client, or a
|
||||
* relative path to append to the base path of the client. The URL can
|
||||
* contain the query string as well. Use an array to provide a URL
|
||||
* template and additional variables to use in the URL template expansion.
|
||||
*
|
||||
* @param string $method HTTP method
|
||||
* @param string|UriInterface $uri URI object or string.
|
||||
* @param array $options Request options to apply.
|
||||
*/
|
||||
abstract public function requestAsync(string $method, $uri, array $options = []): PromiseInterface;
|
||||
|
||||
/**
|
||||
* Create and send an asynchronous HTTP GET request.
|
||||
*
|
||||
* Use an absolute path to override the base path of the client, or a
|
||||
* relative path to append to the base path of the client. The URL can
|
||||
* contain the query string as well. Use an array to provide a URL
|
||||
* template and additional variables to use in the URL template expansion.
|
||||
*
|
||||
* @param string|UriInterface $uri URI object or string.
|
||||
* @param array $options Request options to apply.
|
||||
*/
|
||||
public function getAsync($uri, array $options = []): PromiseInterface
|
||||
{
|
||||
return $this->requestAsync('GET', $uri, $options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create and send an asynchronous HTTP HEAD request.
|
||||
*
|
||||
* Use an absolute path to override the base path of the client, or a
|
||||
* relative path to append to the base path of the client. The URL can
|
||||
* contain the query string as well. Use an array to provide a URL
|
||||
* template and additional variables to use in the URL template expansion.
|
||||
*
|
||||
* @param string|UriInterface $uri URI object or string.
|
||||
* @param array $options Request options to apply.
|
||||
*/
|
||||
public function headAsync($uri, array $options = []): PromiseInterface
|
||||
{
|
||||
return $this->requestAsync('HEAD', $uri, $options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create and send an asynchronous HTTP PUT request.
|
||||
*
|
||||
* Use an absolute path to override the base path of the client, or a
|
||||
* relative path to append to the base path of the client. The URL can
|
||||
* contain the query string as well. Use an array to provide a URL
|
||||
* template and additional variables to use in the URL template expansion.
|
||||
*
|
||||
* @param string|UriInterface $uri URI object or string.
|
||||
* @param array $options Request options to apply.
|
||||
*/
|
||||
public function putAsync($uri, array $options = []): PromiseInterface
|
||||
{
|
||||
return $this->requestAsync('PUT', $uri, $options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create and send an asynchronous HTTP POST request.
|
||||
*
|
||||
* Use an absolute path to override the base path of the client, or a
|
||||
* relative path to append to the base path of the client. The URL can
|
||||
* contain the query string as well. Use an array to provide a URL
|
||||
* template and additional variables to use in the URL template expansion.
|
||||
*
|
||||
* @param string|UriInterface $uri URI object or string.
|
||||
* @param array $options Request options to apply.
|
||||
*/
|
||||
public function postAsync($uri, array $options = []): PromiseInterface
|
||||
{
|
||||
return $this->requestAsync('POST', $uri, $options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create and send an asynchronous HTTP PATCH request.
|
||||
*
|
||||
* Use an absolute path to override the base path of the client, or a
|
||||
* relative path to append to the base path of the client. The URL can
|
||||
* contain the query string as well. Use an array to provide a URL
|
||||
* template and additional variables to use in the URL template expansion.
|
||||
*
|
||||
* @param string|UriInterface $uri URI object or string.
|
||||
* @param array $options Request options to apply.
|
||||
*/
|
||||
public function patchAsync($uri, array $options = []): PromiseInterface
|
||||
{
|
||||
return $this->requestAsync('PATCH', $uri, $options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create and send an asynchronous HTTP DELETE request.
|
||||
*
|
||||
* Use an absolute path to override the base path of the client, or a
|
||||
* relative path to append to the base path of the client. The URL can
|
||||
* contain the query string as well. Use an array to provide a URL
|
||||
* template and additional variables to use in the URL template expansion.
|
||||
*
|
||||
* @param string|UriInterface $uri URI object or string.
|
||||
* @param array $options Request options to apply.
|
||||
*/
|
||||
public function deleteAsync($uri, array $options = []): PromiseInterface
|
||||
{
|
||||
return $this->requestAsync('DELETE', $uri, $options);
|
||||
}
|
||||
}
|
||||
307
vendor/guzzlehttp/guzzle/src/Cookie/CookieJar.php
vendored
Normal file
307
vendor/guzzlehttp/guzzle/src/Cookie/CookieJar.php
vendored
Normal file
@@ -0,0 +1,307 @@
|
||||
<?php
|
||||
|
||||
namespace GuzzleHttp\Cookie;
|
||||
|
||||
use Psr\Http\Message\RequestInterface;
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
|
||||
/**
|
||||
* Cookie jar that stores cookies as an array
|
||||
*/
|
||||
class CookieJar implements CookieJarInterface
|
||||
{
|
||||
/**
|
||||
* @var SetCookie[] Loaded cookie data
|
||||
*/
|
||||
private $cookies = [];
|
||||
|
||||
/**
|
||||
* @var bool
|
||||
*/
|
||||
private $strictMode;
|
||||
|
||||
/**
|
||||
* @param bool $strictMode Set to true to throw exceptions when invalid
|
||||
* cookies are added to the cookie jar.
|
||||
* @param array $cookieArray Array of SetCookie objects or a hash of
|
||||
* arrays that can be used with the SetCookie
|
||||
* constructor
|
||||
*/
|
||||
public function __construct(bool $strictMode = false, array $cookieArray = [])
|
||||
{
|
||||
$this->strictMode = $strictMode;
|
||||
|
||||
foreach ($cookieArray as $cookie) {
|
||||
if (!($cookie instanceof SetCookie)) {
|
||||
$cookie = new SetCookie($cookie);
|
||||
}
|
||||
$this->setCookie($cookie);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new Cookie jar from an associative array and domain.
|
||||
*
|
||||
* @param array $cookies Cookies to create the jar from
|
||||
* @param string $domain Domain to set the cookies to
|
||||
*/
|
||||
public static function fromArray(array $cookies, string $domain): self
|
||||
{
|
||||
$cookieJar = new self();
|
||||
foreach ($cookies as $name => $value) {
|
||||
$cookieJar->setCookie(new SetCookie([
|
||||
'Domain' => $domain,
|
||||
'Name' => $name,
|
||||
'Value' => $value,
|
||||
'Discard' => true,
|
||||
]));
|
||||
}
|
||||
|
||||
return $cookieJar;
|
||||
}
|
||||
|
||||
/**
|
||||
* Evaluate if this cookie should be persisted to storage
|
||||
* that survives between requests.
|
||||
*
|
||||
* @param SetCookie $cookie Being evaluated.
|
||||
* @param bool $allowSessionCookies If we should persist session cookies
|
||||
*/
|
||||
public static function shouldPersist(SetCookie $cookie, bool $allowSessionCookies = false): bool
|
||||
{
|
||||
if ($cookie->getExpires() || $allowSessionCookies) {
|
||||
if (!$cookie->getDiscard()) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds and returns the cookie based on the name
|
||||
*
|
||||
* @param string $name cookie name to search for
|
||||
*
|
||||
* @return SetCookie|null cookie that was found or null if not found
|
||||
*/
|
||||
public function getCookieByName(string $name): ?SetCookie
|
||||
{
|
||||
foreach ($this->cookies as $cookie) {
|
||||
if ($cookie->getName() !== null && \strcasecmp($cookie->getName(), $name) === 0) {
|
||||
return $cookie;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public function toArray(): array
|
||||
{
|
||||
return \array_map(static function (SetCookie $cookie): array {
|
||||
return $cookie->toArray();
|
||||
}, $this->getIterator()->getArrayCopy());
|
||||
}
|
||||
|
||||
public function clear(?string $domain = null, ?string $path = null, ?string $name = null): void
|
||||
{
|
||||
if (!$domain) {
|
||||
$this->cookies = [];
|
||||
|
||||
return;
|
||||
} elseif (!$path) {
|
||||
$this->cookies = \array_filter(
|
||||
$this->cookies,
|
||||
static function (SetCookie $cookie) use ($domain): bool {
|
||||
return !$cookie->matchesDomain($domain);
|
||||
}
|
||||
);
|
||||
} elseif (!$name) {
|
||||
$this->cookies = \array_filter(
|
||||
$this->cookies,
|
||||
static function (SetCookie $cookie) use ($path, $domain): bool {
|
||||
return !($cookie->matchesPath($path)
|
||||
&& $cookie->matchesDomain($domain));
|
||||
}
|
||||
);
|
||||
} else {
|
||||
$this->cookies = \array_filter(
|
||||
$this->cookies,
|
||||
static function (SetCookie $cookie) use ($path, $domain, $name) {
|
||||
return !($cookie->getName() == $name
|
||||
&& $cookie->matchesPath($path)
|
||||
&& $cookie->matchesDomain($domain));
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public function clearSessionCookies(): void
|
||||
{
|
||||
$this->cookies = \array_filter(
|
||||
$this->cookies,
|
||||
static function (SetCookie $cookie): bool {
|
||||
return !$cookie->getDiscard() && $cookie->getExpires();
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
public function setCookie(SetCookie $cookie): bool
|
||||
{
|
||||
// If the name string is empty (but not 0), ignore the set-cookie
|
||||
// string entirely.
|
||||
$name = $cookie->getName();
|
||||
if (!$name && $name !== '0') {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Only allow cookies with set and valid domain, name, value
|
||||
$result = $cookie->validate();
|
||||
if ($result !== true) {
|
||||
if ($this->strictMode) {
|
||||
throw new \RuntimeException('Invalid cookie: '.$result);
|
||||
}
|
||||
$this->removeCookieIfEmpty($cookie);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// Resolve conflicts with previously set cookies
|
||||
foreach ($this->cookies as $i => $c) {
|
||||
// Two cookies are identical, when their path, and domain are
|
||||
// identical.
|
||||
if ($c->getPath() != $cookie->getPath()
|
||||
|| $c->getDomain() != $cookie->getDomain()
|
||||
|| $c->getName() != $cookie->getName()
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// The previously set cookie is a discard cookie and this one is
|
||||
// not so allow the new cookie to be set
|
||||
if (!$cookie->getDiscard() && $c->getDiscard()) {
|
||||
unset($this->cookies[$i]);
|
||||
continue;
|
||||
}
|
||||
|
||||
// If the new cookie's expiration is further into the future, then
|
||||
// replace the old cookie
|
||||
if ($cookie->getExpires() > $c->getExpires()) {
|
||||
unset($this->cookies[$i]);
|
||||
continue;
|
||||
}
|
||||
|
||||
// If the value has changed, we better change it
|
||||
if ($cookie->getValue() !== $c->getValue()) {
|
||||
unset($this->cookies[$i]);
|
||||
continue;
|
||||
}
|
||||
|
||||
// The cookie exists, so no need to continue
|
||||
return false;
|
||||
}
|
||||
|
||||
$this->cookies[] = $cookie;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public function count(): int
|
||||
{
|
||||
return \count($this->cookies);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return \ArrayIterator<int, SetCookie>
|
||||
*/
|
||||
public function getIterator(): \ArrayIterator
|
||||
{
|
||||
return new \ArrayIterator(\array_values($this->cookies));
|
||||
}
|
||||
|
||||
public function extractCookies(RequestInterface $request, ResponseInterface $response): void
|
||||
{
|
||||
if ($cookieHeader = $response->getHeader('Set-Cookie')) {
|
||||
foreach ($cookieHeader as $cookie) {
|
||||
$sc = SetCookie::fromString($cookie);
|
||||
if (!$sc->getDomain()) {
|
||||
$sc->setDomain($request->getUri()->getHost());
|
||||
}
|
||||
if (0 !== \strpos($sc->getPath(), '/')) {
|
||||
$sc->setPath($this->getCookiePathFromRequest($request));
|
||||
}
|
||||
if (!$sc->matchesDomain($request->getUri()->getHost())) {
|
||||
continue;
|
||||
}
|
||||
// Note: At this point `$sc->getDomain()` being a public suffix should
|
||||
// be rejected, but we don't want to pull in the full PSL dependency.
|
||||
$this->setCookie($sc);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Computes cookie path following RFC 6265 section 5.1.4
|
||||
*
|
||||
* @see https://datatracker.ietf.org/doc/html/rfc6265#section-5.1.4
|
||||
*/
|
||||
private function getCookiePathFromRequest(RequestInterface $request): string
|
||||
{
|
||||
$uriPath = $request->getUri()->getPath();
|
||||
if ('' === $uriPath) {
|
||||
return '/';
|
||||
}
|
||||
if (0 !== \strpos($uriPath, '/')) {
|
||||
return '/';
|
||||
}
|
||||
if ('/' === $uriPath) {
|
||||
return '/';
|
||||
}
|
||||
$lastSlashPos = \strrpos($uriPath, '/');
|
||||
if (0 === $lastSlashPos || false === $lastSlashPos) {
|
||||
return '/';
|
||||
}
|
||||
|
||||
return \substr($uriPath, 0, $lastSlashPos);
|
||||
}
|
||||
|
||||
public function withCookieHeader(RequestInterface $request): RequestInterface
|
||||
{
|
||||
$values = [];
|
||||
$uri = $request->getUri();
|
||||
$scheme = $uri->getScheme();
|
||||
$host = $uri->getHost();
|
||||
$path = $uri->getPath() ?: '/';
|
||||
|
||||
foreach ($this->cookies as $cookie) {
|
||||
if ($cookie->matchesPath($path)
|
||||
&& $cookie->matchesDomain($host)
|
||||
&& !$cookie->isExpired()
|
||||
&& (!$cookie->getSecure() || $scheme === 'https')
|
||||
) {
|
||||
$values[] = $cookie->getName().'='
|
||||
.$cookie->getValue();
|
||||
}
|
||||
}
|
||||
|
||||
return $values
|
||||
? $request->withHeader('Cookie', \implode('; ', $values))
|
||||
: $request;
|
||||
}
|
||||
|
||||
/**
|
||||
* If a cookie already exists and the server asks to set it again with a
|
||||
* null value, the cookie must be deleted.
|
||||
*/
|
||||
private function removeCookieIfEmpty(SetCookie $cookie): void
|
||||
{
|
||||
$cookieValue = $cookie->getValue();
|
||||
if ($cookieValue === null || $cookieValue === '') {
|
||||
$this->clear(
|
||||
$cookie->getDomain(),
|
||||
$cookie->getPath(),
|
||||
$cookie->getName()
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
80
vendor/guzzlehttp/guzzle/src/Cookie/CookieJarInterface.php
vendored
Normal file
80
vendor/guzzlehttp/guzzle/src/Cookie/CookieJarInterface.php
vendored
Normal file
@@ -0,0 +1,80 @@
|
||||
<?php
|
||||
|
||||
namespace GuzzleHttp\Cookie;
|
||||
|
||||
use Psr\Http\Message\RequestInterface;
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
|
||||
/**
|
||||
* Stores HTTP cookies.
|
||||
*
|
||||
* It extracts cookies from HTTP requests, and returns them in HTTP responses.
|
||||
* CookieJarInterface instances automatically expire contained cookies when
|
||||
* necessary. Subclasses are also responsible for storing and retrieving
|
||||
* cookies from a file, database, etc.
|
||||
*
|
||||
* @see https://docs.python.org/2/library/cookielib.html Inspiration
|
||||
*
|
||||
* @extends \IteratorAggregate<SetCookie>
|
||||
*/
|
||||
interface CookieJarInterface extends \Countable, \IteratorAggregate
|
||||
{
|
||||
/**
|
||||
* Create a request with added cookie headers.
|
||||
*
|
||||
* If no matching cookies are found in the cookie jar, then no Cookie
|
||||
* header is added to the request and the same request is returned.
|
||||
*
|
||||
* @param RequestInterface $request Request object to modify.
|
||||
*
|
||||
* @return RequestInterface returns the modified request.
|
||||
*/
|
||||
public function withCookieHeader(RequestInterface $request): RequestInterface;
|
||||
|
||||
/**
|
||||
* Extract cookies from an HTTP response and store them in the CookieJar.
|
||||
*
|
||||
* @param RequestInterface $request Request that was sent
|
||||
* @param ResponseInterface $response Response that was received
|
||||
*/
|
||||
public function extractCookies(RequestInterface $request, ResponseInterface $response): void;
|
||||
|
||||
/**
|
||||
* Sets a cookie in the cookie jar.
|
||||
*
|
||||
* @param SetCookie $cookie Cookie to set.
|
||||
*
|
||||
* @return bool Returns true on success or false on failure
|
||||
*/
|
||||
public function setCookie(SetCookie $cookie): bool;
|
||||
|
||||
/**
|
||||
* Remove cookies currently held in the cookie jar.
|
||||
*
|
||||
* Invoking this method without arguments will empty the whole cookie jar.
|
||||
* If given a $domain argument only cookies belonging to that domain will
|
||||
* be removed. If given a $domain and $path argument, cookies belonging to
|
||||
* the specified path within that domain are removed. If given all three
|
||||
* arguments, then the cookie with the specified name, path and domain is
|
||||
* removed.
|
||||
*
|
||||
* @param string|null $domain Clears cookies matching a domain
|
||||
* @param string|null $path Clears cookies matching a domain and path
|
||||
* @param string|null $name Clears cookies matching a domain, path, and name
|
||||
*/
|
||||
public function clear(?string $domain = null, ?string $path = null, ?string $name = null): void;
|
||||
|
||||
/**
|
||||
* Discard all sessions cookies.
|
||||
*
|
||||
* Removes cookies that don't have an expire field or a have a discard
|
||||
* field set to true. To be called when the user agent shuts down according
|
||||
* to RFC 2965.
|
||||
*/
|
||||
public function clearSessionCookies(): void;
|
||||
|
||||
/**
|
||||
* Converts the cookie jar to an array.
|
||||
*/
|
||||
public function toArray(): array;
|
||||
}
|
||||
101
vendor/guzzlehttp/guzzle/src/Cookie/FileCookieJar.php
vendored
Normal file
101
vendor/guzzlehttp/guzzle/src/Cookie/FileCookieJar.php
vendored
Normal file
@@ -0,0 +1,101 @@
|
||||
<?php
|
||||
|
||||
namespace GuzzleHttp\Cookie;
|
||||
|
||||
use GuzzleHttp\Utils;
|
||||
|
||||
/**
|
||||
* Persists non-session cookies using a JSON formatted file
|
||||
*/
|
||||
class FileCookieJar extends CookieJar
|
||||
{
|
||||
/**
|
||||
* @var string filename
|
||||
*/
|
||||
private $filename;
|
||||
|
||||
/**
|
||||
* @var bool Control whether to persist session cookies or not.
|
||||
*/
|
||||
private $storeSessionCookies;
|
||||
|
||||
/**
|
||||
* Create a new FileCookieJar object
|
||||
*
|
||||
* @param string $cookieFile File to store the cookie data
|
||||
* @param bool $storeSessionCookies Set to true to store session cookies
|
||||
* in the cookie jar.
|
||||
*
|
||||
* @throws \RuntimeException if the file cannot be found or created
|
||||
*/
|
||||
public function __construct(string $cookieFile, bool $storeSessionCookies = false)
|
||||
{
|
||||
parent::__construct();
|
||||
$this->filename = $cookieFile;
|
||||
$this->storeSessionCookies = $storeSessionCookies;
|
||||
|
||||
if (\file_exists($cookieFile)) {
|
||||
$this->load($cookieFile);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Saves the file when shutting down
|
||||
*/
|
||||
public function __destruct()
|
||||
{
|
||||
$this->save($this->filename);
|
||||
}
|
||||
|
||||
/**
|
||||
* Saves the cookies to a file.
|
||||
*
|
||||
* @param string $filename File to save
|
||||
*
|
||||
* @throws \RuntimeException if the file cannot be found or created
|
||||
*/
|
||||
public function save(string $filename): void
|
||||
{
|
||||
$json = [];
|
||||
/** @var SetCookie $cookie */
|
||||
foreach ($this as $cookie) {
|
||||
if (CookieJar::shouldPersist($cookie, $this->storeSessionCookies)) {
|
||||
$json[] = $cookie->toArray();
|
||||
}
|
||||
}
|
||||
|
||||
$jsonStr = Utils::jsonEncode($json);
|
||||
if (false === \file_put_contents($filename, $jsonStr, \LOCK_EX)) {
|
||||
throw new \RuntimeException("Unable to save file {$filename}");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Load cookies from a JSON formatted file.
|
||||
*
|
||||
* Old cookies are kept unless overwritten by newly loaded ones.
|
||||
*
|
||||
* @param string $filename Cookie file to load.
|
||||
*
|
||||
* @throws \RuntimeException if the file cannot be loaded.
|
||||
*/
|
||||
public function load(string $filename): void
|
||||
{
|
||||
$json = \file_get_contents($filename);
|
||||
if (false === $json) {
|
||||
throw new \RuntimeException("Unable to load file {$filename}");
|
||||
}
|
||||
if ($json === '') {
|
||||
return;
|
||||
}
|
||||
|
||||
$data = Utils::jsonDecode($json, true);
|
||||
if (\is_array($data)) {
|
||||
foreach ($data as $cookie) {
|
||||
$this->setCookie(new SetCookie($cookie));
|
||||
}
|
||||
} elseif (\is_scalar($data) && !empty($data)) {
|
||||
throw new \RuntimeException("Invalid cookie file: {$filename}");
|
||||
}
|
||||
}
|
||||
}
|
||||
77
vendor/guzzlehttp/guzzle/src/Cookie/SessionCookieJar.php
vendored
Normal file
77
vendor/guzzlehttp/guzzle/src/Cookie/SessionCookieJar.php
vendored
Normal file
@@ -0,0 +1,77 @@
|
||||
<?php
|
||||
|
||||
namespace GuzzleHttp\Cookie;
|
||||
|
||||
/**
|
||||
* Persists cookies in the client session
|
||||
*/
|
||||
class SessionCookieJar extends CookieJar
|
||||
{
|
||||
/**
|
||||
* @var string session key
|
||||
*/
|
||||
private $sessionKey;
|
||||
|
||||
/**
|
||||
* @var bool Control whether to persist session cookies or not.
|
||||
*/
|
||||
private $storeSessionCookies;
|
||||
|
||||
/**
|
||||
* Create a new SessionCookieJar object
|
||||
*
|
||||
* @param string $sessionKey Session key name to store the cookie
|
||||
* data in session
|
||||
* @param bool $storeSessionCookies Set to true to store session cookies
|
||||
* in the cookie jar.
|
||||
*/
|
||||
public function __construct(string $sessionKey, bool $storeSessionCookies = false)
|
||||
{
|
||||
parent::__construct();
|
||||
$this->sessionKey = $sessionKey;
|
||||
$this->storeSessionCookies = $storeSessionCookies;
|
||||
$this->load();
|
||||
}
|
||||
|
||||
/**
|
||||
* Saves cookies to session when shutting down
|
||||
*/
|
||||
public function __destruct()
|
||||
{
|
||||
$this->save();
|
||||
}
|
||||
|
||||
/**
|
||||
* Save cookies to the client session
|
||||
*/
|
||||
public function save(): void
|
||||
{
|
||||
$json = [];
|
||||
/** @var SetCookie $cookie */
|
||||
foreach ($this as $cookie) {
|
||||
if (CookieJar::shouldPersist($cookie, $this->storeSessionCookies)) {
|
||||
$json[] = $cookie->toArray();
|
||||
}
|
||||
}
|
||||
|
||||
$_SESSION[$this->sessionKey] = \json_encode($json);
|
||||
}
|
||||
|
||||
/**
|
||||
* Load the contents of the client session into the data array
|
||||
*/
|
||||
protected function load(): void
|
||||
{
|
||||
if (!isset($_SESSION[$this->sessionKey])) {
|
||||
return;
|
||||
}
|
||||
$data = \json_decode($_SESSION[$this->sessionKey], true);
|
||||
if (\is_array($data)) {
|
||||
foreach ($data as $cookie) {
|
||||
$this->setCookie(new SetCookie($cookie));
|
||||
}
|
||||
} elseif (\strlen($data)) {
|
||||
throw new \RuntimeException('Invalid cookie data');
|
||||
}
|
||||
}
|
||||
}
|
||||
492
vendor/guzzlehttp/guzzle/src/Cookie/SetCookie.php
vendored
Normal file
492
vendor/guzzlehttp/guzzle/src/Cookie/SetCookie.php
vendored
Normal file
@@ -0,0 +1,492 @@
|
||||
<?php
|
||||
|
||||
namespace GuzzleHttp\Cookie;
|
||||
|
||||
/**
|
||||
* Set-Cookie object
|
||||
*/
|
||||
class SetCookie
|
||||
{
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
private static $defaults = [
|
||||
'Name' => null,
|
||||
'Value' => null,
|
||||
'Domain' => null,
|
||||
'Path' => '/',
|
||||
'Max-Age' => null,
|
||||
'Expires' => null,
|
||||
'Secure' => false,
|
||||
'Discard' => false,
|
||||
'HttpOnly' => false,
|
||||
];
|
||||
|
||||
/**
|
||||
* @var array Cookie data
|
||||
*/
|
||||
private $data;
|
||||
|
||||
/**
|
||||
* Create a new SetCookie object from a string.
|
||||
*
|
||||
* @param string $cookie Set-Cookie header string
|
||||
*/
|
||||
public static function fromString(string $cookie): self
|
||||
{
|
||||
// Create the default return array
|
||||
$data = self::$defaults;
|
||||
// Explode the cookie string using a series of semicolons
|
||||
$pieces = \array_filter(\array_map('trim', \explode(';', $cookie)));
|
||||
// The name of the cookie (first kvp) must exist and include an equal sign.
|
||||
if (!isset($pieces[0]) || \strpos($pieces[0], '=') === false) {
|
||||
return new self($data);
|
||||
}
|
||||
|
||||
// Add the cookie pieces into the parsed data array
|
||||
foreach ($pieces as $part) {
|
||||
$cookieParts = \explode('=', $part, 2);
|
||||
$key = \trim($cookieParts[0]);
|
||||
$value = isset($cookieParts[1])
|
||||
? \trim($cookieParts[1], " \n\r\t\0\x0B")
|
||||
: true;
|
||||
|
||||
// Only check for non-cookies when cookies have been found
|
||||
if (!isset($data['Name'])) {
|
||||
$data['Name'] = $key;
|
||||
$data['Value'] = $value;
|
||||
} else {
|
||||
foreach (\array_keys(self::$defaults) as $search) {
|
||||
if (!\strcasecmp($search, $key)) {
|
||||
if ($search === 'Max-Age') {
|
||||
if (is_numeric($value)) {
|
||||
$data[$search] = (int) $value;
|
||||
}
|
||||
} elseif ($search === 'Secure' || $search === 'Discard' || $search === 'HttpOnly') {
|
||||
if ($value) {
|
||||
$data[$search] = true;
|
||||
}
|
||||
} else {
|
||||
$data[$search] = $value;
|
||||
}
|
||||
continue 2;
|
||||
}
|
||||
}
|
||||
$data[$key] = $value;
|
||||
}
|
||||
}
|
||||
|
||||
return new self($data);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $data Array of cookie data provided by a Cookie parser
|
||||
*/
|
||||
public function __construct(array $data = [])
|
||||
{
|
||||
$this->data = self::$defaults;
|
||||
|
||||
if (isset($data['Name'])) {
|
||||
$this->setName($data['Name']);
|
||||
}
|
||||
|
||||
if (isset($data['Value'])) {
|
||||
$this->setValue($data['Value']);
|
||||
}
|
||||
|
||||
if (isset($data['Domain'])) {
|
||||
$this->setDomain($data['Domain']);
|
||||
}
|
||||
|
||||
if (isset($data['Path'])) {
|
||||
$this->setPath($data['Path']);
|
||||
}
|
||||
|
||||
if (isset($data['Max-Age'])) {
|
||||
$this->setMaxAge($data['Max-Age']);
|
||||
}
|
||||
|
||||
if (isset($data['Expires'])) {
|
||||
$this->setExpires($data['Expires']);
|
||||
}
|
||||
|
||||
if (isset($data['Secure'])) {
|
||||
$this->setSecure($data['Secure']);
|
||||
}
|
||||
|
||||
if (isset($data['Discard'])) {
|
||||
$this->setDiscard($data['Discard']);
|
||||
}
|
||||
|
||||
if (isset($data['HttpOnly'])) {
|
||||
$this->setHttpOnly($data['HttpOnly']);
|
||||
}
|
||||
|
||||
// Set the remaining values that don't have extra validation logic
|
||||
foreach (array_diff(array_keys($data), array_keys(self::$defaults)) as $key) {
|
||||
$this->data[$key] = $data[$key];
|
||||
}
|
||||
|
||||
// Extract the Expires value and turn it into a UNIX timestamp if needed
|
||||
if (!$this->getExpires() && $this->getMaxAge()) {
|
||||
// Calculate the Expires date
|
||||
$this->setExpires(\time() + $this->getMaxAge());
|
||||
} elseif (null !== ($expires = $this->getExpires()) && !\is_numeric($expires)) {
|
||||
$this->setExpires($expires);
|
||||
}
|
||||
}
|
||||
|
||||
public function __toString()
|
||||
{
|
||||
$str = $this->data['Name'].'='.($this->data['Value'] ?? '').'; ';
|
||||
foreach ($this->data as $k => $v) {
|
||||
if ($k !== 'Name' && $k !== 'Value' && $v !== null && $v !== false) {
|
||||
if ($k === 'Expires') {
|
||||
$str .= 'Expires='.\gmdate('D, d M Y H:i:s \G\M\T', $v).'; ';
|
||||
} else {
|
||||
$str .= ($v === true ? $k : "{$k}={$v}").'; ';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return \rtrim($str, '; ');
|
||||
}
|
||||
|
||||
public function toArray(): array
|
||||
{
|
||||
return $this->data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the cookie name.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getName()
|
||||
{
|
||||
return $this->data['Name'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the cookie name.
|
||||
*
|
||||
* @param string $name Cookie name
|
||||
*/
|
||||
public function setName($name): void
|
||||
{
|
||||
if (!is_string($name)) {
|
||||
trigger_deprecation('guzzlehttp/guzzle', '7.4', 'Not passing a string to %s::%s() is deprecated and will cause an error in 8.0.', __CLASS__, __FUNCTION__);
|
||||
}
|
||||
|
||||
$this->data['Name'] = (string) $name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the cookie value.
|
||||
*
|
||||
* @return string|null
|
||||
*/
|
||||
public function getValue()
|
||||
{
|
||||
return $this->data['Value'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the cookie value.
|
||||
*
|
||||
* @param string $value Cookie value
|
||||
*/
|
||||
public function setValue($value): void
|
||||
{
|
||||
if (!is_string($value)) {
|
||||
trigger_deprecation('guzzlehttp/guzzle', '7.4', 'Not passing a string to %s::%s() is deprecated and will cause an error in 8.0.', __CLASS__, __FUNCTION__);
|
||||
}
|
||||
|
||||
$this->data['Value'] = (string) $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the domain.
|
||||
*
|
||||
* @return string|null
|
||||
*/
|
||||
public function getDomain()
|
||||
{
|
||||
return $this->data['Domain'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the domain of the cookie.
|
||||
*
|
||||
* @param string|null $domain
|
||||
*/
|
||||
public function setDomain($domain): void
|
||||
{
|
||||
if (!is_string($domain) && null !== $domain) {
|
||||
trigger_deprecation('guzzlehttp/guzzle', '7.4', 'Not passing a string or null to %s::%s() is deprecated and will cause an error in 8.0.', __CLASS__, __FUNCTION__);
|
||||
}
|
||||
|
||||
$this->data['Domain'] = null === $domain ? null : (string) $domain;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the path.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getPath()
|
||||
{
|
||||
return $this->data['Path'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the path of the cookie.
|
||||
*
|
||||
* @param string $path Path of the cookie
|
||||
*/
|
||||
public function setPath($path): void
|
||||
{
|
||||
if (!is_string($path)) {
|
||||
trigger_deprecation('guzzlehttp/guzzle', '7.4', 'Not passing a string to %s::%s() is deprecated and will cause an error in 8.0.', __CLASS__, __FUNCTION__);
|
||||
}
|
||||
|
||||
$this->data['Path'] = (string) $path;
|
||||
}
|
||||
|
||||
/**
|
||||
* Maximum lifetime of the cookie in seconds.
|
||||
*
|
||||
* @return int|null
|
||||
*/
|
||||
public function getMaxAge()
|
||||
{
|
||||
return null === $this->data['Max-Age'] ? null : (int) $this->data['Max-Age'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the max-age of the cookie.
|
||||
*
|
||||
* @param int|null $maxAge Max age of the cookie in seconds
|
||||
*/
|
||||
public function setMaxAge($maxAge): void
|
||||
{
|
||||
if (!is_int($maxAge) && null !== $maxAge) {
|
||||
trigger_deprecation('guzzlehttp/guzzle', '7.4', 'Not passing an int or null to %s::%s() is deprecated and will cause an error in 8.0.', __CLASS__, __FUNCTION__);
|
||||
}
|
||||
|
||||
$this->data['Max-Age'] = $maxAge === null ? null : (int) $maxAge;
|
||||
}
|
||||
|
||||
/**
|
||||
* The UNIX timestamp when the cookie Expires.
|
||||
*
|
||||
* @return string|int|null
|
||||
*/
|
||||
public function getExpires()
|
||||
{
|
||||
return $this->data['Expires'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the unix timestamp for which the cookie will expire.
|
||||
*
|
||||
* @param int|string|null $timestamp Unix timestamp or any English textual datetime description.
|
||||
*/
|
||||
public function setExpires($timestamp): void
|
||||
{
|
||||
if (!is_int($timestamp) && !is_string($timestamp) && null !== $timestamp) {
|
||||
trigger_deprecation('guzzlehttp/guzzle', '7.4', 'Not passing an int, string or null to %s::%s() is deprecated and will cause an error in 8.0.', __CLASS__, __FUNCTION__);
|
||||
}
|
||||
|
||||
$this->data['Expires'] = null === $timestamp ? null : (\is_numeric($timestamp) ? (int) $timestamp : \strtotime((string) $timestamp));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get whether or not this is a secure cookie.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function getSecure()
|
||||
{
|
||||
return $this->data['Secure'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Set whether or not the cookie is secure.
|
||||
*
|
||||
* @param bool $secure Set to true or false if secure
|
||||
*/
|
||||
public function setSecure($secure): void
|
||||
{
|
||||
if (!is_bool($secure)) {
|
||||
trigger_deprecation('guzzlehttp/guzzle', '7.4', 'Not passing a bool to %s::%s() is deprecated and will cause an error in 8.0.', __CLASS__, __FUNCTION__);
|
||||
}
|
||||
|
||||
$this->data['Secure'] = (bool) $secure;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get whether or not this is a session cookie.
|
||||
*
|
||||
* @return bool|null
|
||||
*/
|
||||
public function getDiscard()
|
||||
{
|
||||
return $this->data['Discard'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Set whether or not this is a session cookie.
|
||||
*
|
||||
* @param bool $discard Set to true or false if this is a session cookie
|
||||
*/
|
||||
public function setDiscard($discard): void
|
||||
{
|
||||
if (!is_bool($discard)) {
|
||||
trigger_deprecation('guzzlehttp/guzzle', '7.4', 'Not passing a bool to %s::%s() is deprecated and will cause an error in 8.0.', __CLASS__, __FUNCTION__);
|
||||
}
|
||||
|
||||
$this->data['Discard'] = (bool) $discard;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get whether or not this is an HTTP only cookie.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function getHttpOnly()
|
||||
{
|
||||
return $this->data['HttpOnly'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Set whether or not this is an HTTP only cookie.
|
||||
*
|
||||
* @param bool $httpOnly Set to true or false if this is HTTP only
|
||||
*/
|
||||
public function setHttpOnly($httpOnly): void
|
||||
{
|
||||
if (!is_bool($httpOnly)) {
|
||||
trigger_deprecation('guzzlehttp/guzzle', '7.4', 'Not passing a bool to %s::%s() is deprecated and will cause an error in 8.0.', __CLASS__, __FUNCTION__);
|
||||
}
|
||||
|
||||
$this->data['HttpOnly'] = (bool) $httpOnly;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the cookie matches a path value.
|
||||
*
|
||||
* A request-path path-matches a given cookie-path if at least one of
|
||||
* the following conditions holds:
|
||||
*
|
||||
* - The cookie-path and the request-path are identical.
|
||||
* - The cookie-path is a prefix of the request-path, and the last
|
||||
* character of the cookie-path is %x2F ("/").
|
||||
* - The cookie-path is a prefix of the request-path, and the first
|
||||
* character of the request-path that is not included in the cookie-
|
||||
* path is a %x2F ("/") character.
|
||||
*
|
||||
* @param string $requestPath Path to check against
|
||||
*/
|
||||
public function matchesPath(string $requestPath): bool
|
||||
{
|
||||
$cookiePath = $this->getPath();
|
||||
|
||||
// Match on exact matches or when path is the default empty "/"
|
||||
if ($cookiePath === '/' || $cookiePath == $requestPath) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Ensure that the cookie-path is a prefix of the request path.
|
||||
if (0 !== \strpos($requestPath, $cookiePath)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Match if the last character of the cookie-path is "/"
|
||||
if (\substr($cookiePath, -1, 1) === '/') {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Match if the first character not included in cookie path is "/"
|
||||
return \substr($requestPath, \strlen($cookiePath), 1) === '/';
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the cookie matches a domain value.
|
||||
*
|
||||
* @param string $domain Domain to check against
|
||||
*/
|
||||
public function matchesDomain(string $domain): bool
|
||||
{
|
||||
$cookieDomain = $this->getDomain();
|
||||
if (null === $cookieDomain) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Remove the leading '.' as per spec in RFC 6265.
|
||||
// https://datatracker.ietf.org/doc/html/rfc6265#section-5.2.3
|
||||
$cookieDomain = \ltrim(\strtolower($cookieDomain), '.');
|
||||
|
||||
$domain = \strtolower($domain);
|
||||
|
||||
// Domain not set or exact match.
|
||||
if ('' === $cookieDomain || $domain === $cookieDomain) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Matching the subdomain according to RFC 6265.
|
||||
// https://datatracker.ietf.org/doc/html/rfc6265#section-5.1.3
|
||||
if (\filter_var($domain, \FILTER_VALIDATE_IP)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return (bool) \preg_match('/\.'.\preg_quote($cookieDomain, '/').'$/', $domain);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the cookie is expired.
|
||||
*/
|
||||
public function isExpired(): bool
|
||||
{
|
||||
return $this->getExpires() !== null && \time() > $this->getExpires();
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the cookie is valid according to RFC 6265.
|
||||
*
|
||||
* @return bool|string Returns true if valid or an error message if invalid
|
||||
*/
|
||||
public function validate()
|
||||
{
|
||||
$name = $this->getName();
|
||||
if ($name === '') {
|
||||
return 'The cookie name must not be empty';
|
||||
}
|
||||
|
||||
// Check if any of the invalid characters are present in the cookie name
|
||||
if (\preg_match(
|
||||
'/[\x00-\x20\x22\x28-\x29\x2c\x2f\x3a-\x40\x5c\x7b\x7d\x7f]/',
|
||||
$name
|
||||
)) {
|
||||
return 'Cookie name must not contain invalid characters: ASCII '
|
||||
.'Control characters (0-31;127), space, tab and the '
|
||||
.'following characters: ()<>@,;:\"/?={}';
|
||||
}
|
||||
|
||||
// Value must not be null. 0 and empty string are valid. Empty strings
|
||||
// are technically against RFC 6265, but known to happen in the wild.
|
||||
$value = $this->getValue();
|
||||
if ($value === null) {
|
||||
return 'The cookie value must not be empty';
|
||||
}
|
||||
|
||||
// Domains must not be empty, but can be 0. "0" is not a valid internet
|
||||
// domain, but may be used as server name in a private network.
|
||||
$domain = $this->getDomain();
|
||||
if ($domain === null || $domain === '') {
|
||||
return 'The cookie domain must not be empty';
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
39
vendor/guzzlehttp/guzzle/src/Exception/BadResponseException.php
vendored
Normal file
39
vendor/guzzlehttp/guzzle/src/Exception/BadResponseException.php
vendored
Normal file
@@ -0,0 +1,39 @@
|
||||
<?php
|
||||
|
||||
namespace GuzzleHttp\Exception;
|
||||
|
||||
use Psr\Http\Message\RequestInterface;
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
|
||||
/**
|
||||
* Exception when an HTTP error occurs (4xx or 5xx error)
|
||||
*/
|
||||
class BadResponseException extends RequestException
|
||||
{
|
||||
public function __construct(
|
||||
string $message,
|
||||
RequestInterface $request,
|
||||
ResponseInterface $response,
|
||||
?\Throwable $previous = null,
|
||||
array $handlerContext = []
|
||||
) {
|
||||
parent::__construct($message, $request, $response, $previous, $handlerContext);
|
||||
}
|
||||
|
||||
/**
|
||||
* Current exception and the ones that extend it will always have a response.
|
||||
*/
|
||||
public function hasResponse(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* This function narrows the return type from the parent class and does not allow it to be nullable.
|
||||
*/
|
||||
public function getResponse(): ResponseInterface
|
||||
{
|
||||
/** @var ResponseInterface */
|
||||
return parent::getResponse();
|
||||
}
|
||||
}
|
||||
10
vendor/guzzlehttp/guzzle/src/Exception/ClientException.php
vendored
Normal file
10
vendor/guzzlehttp/guzzle/src/Exception/ClientException.php
vendored
Normal file
@@ -0,0 +1,10 @@
|
||||
<?php
|
||||
|
||||
namespace GuzzleHttp\Exception;
|
||||
|
||||
/**
|
||||
* Exception when a client error is encountered (4xx codes)
|
||||
*/
|
||||
class ClientException extends BadResponseException
|
||||
{
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user