Browse Source

i can't believe it's getting updates

Julian van de Groep 4 months ago
parent
commit
2fbc29f79a
8 changed files with 409 additions and 6 deletions
  1. 2
    0
      .gitignore
  2. 6
    1
      README.md
  3. 8
    0
      composer.json
  4. 46
    3
      index.php
  5. 2
    2
      public/index.php
  6. 237
    0
      src/DatabaseMigrationManager.php
  7. 53
    0
      src/Router.php
  8. 55
    0
      src/Templates.php

+ 2
- 0
.gitignore View File

@@ -4,3 +4,5 @@ $RECYCLE.BIN/
4 4
 vendor/
5 5
 img/
6 6
 thumb/
7
+config.ini.php
8
+.debug

+ 6
- 1
README.md View File

@@ -1,3 +1,8 @@
1 1
 # Satoko
2 2
 
3
-it's not dead!
3
+cool image board software that exists because i am bored
4
+
5
+## Requirements
6
+
7
+ - PHP 7.3
8
+ - MariaDB 10.3+ / MySQL 5.7+

+ 8
- 0
composer.json View File

@@ -1,4 +1,12 @@
1 1
 {
2
+    "autoload": {
3
+        "psr-4": {
4
+            "Satoko\\": "src/"
5
+        },
6
+        "classmap": [
7
+            "database"
8
+        ]
9
+    },
2 10
     "require": {
3 11
         "twig/twig": "^2.10",
4 12
         "phroute/phroute": "^2.1"

+ 46
- 3
index.php View File

@@ -1,5 +1,48 @@
1 1
 <?php
2
-if(!defined('SATOKO_ROOT'))
3
-    define('SATOKO_ROOT', __DIR__ . '/');
2
+namespace Satoko;
4 3
 
5
-require_once SATOKO_ROOT . 'vendor/autoload.php';
4
+if(!defined('STK_ROOT'))
5
+    define('STK_ROOT', __DIR__ . '/');
6
+
7
+if(!defined('STK_DEBUG'))
8
+    define('STK_DEBUG', is_file(STK_ROOT . '/.debug'));
9
+
10
+define('STK_PHP_MIN_VER', '7.3.0');
11
+
12
+if (version_compare(PHP_VERSION, STK_PHP_MIN_VER, '<')) {
13
+    die('Satoko requires PHP <b>' . STK_PHP_MIN_VER . '</b> or newer to run.');
14
+}
15
+
16
+error_reporting(STK_DEBUG ? -1 : 0);
17
+ini_set('display_errors', STK_DEBUG ? 'On' : 'Off');
18
+
19
+define('STK_CONFIG', STK_ROOT . '/config.ini.php');
20
+
21
+if(!is_file(STK_CONFIG))
22
+    die('Missing configuration.');
23
+
24
+$config = parse_ini_file(STK_ROOT . '/config.ini.php', true, INI_SCANNER_TYPED);
25
+
26
+if(!defined('STK_DB_PFX'))
27
+    define('STK_DB_PFX', $config['Database']['table_prefix'] ?? 'stk_');
28
+
29
+require_once STK_ROOT . 'vendor/autoload.php';
30
+
31
+$templates = new Templates([
32
+    'debug' => STK_DEBUG,
33
+    'auto_reload' => STK_DEBUG,
34
+    'cache' => false,
35
+]);
36
+
37
+$router = new Router(
38
+    $config['Satoko']['base_path'] ?? '/',
39
+    !empty($config['Satoko']['use_path_info'])
40
+);
41
+$router->get(['/', 'index'], function() {
42
+    return 'whoa ! images !<br>';
43
+});
44
+$router->get(['/{board}', 'board-index'], function($board) {
45
+    return "Viewing {$board}.<br>";
46
+});
47
+
48
+$router->dispatch();

+ 2
- 2
public/index.php View File

@@ -1,4 +1,4 @@
1 1
 <?php
2
-define('SATOKO_ROOT', __DIR__ . '/../');
2
+define('STK_ROOT', __DIR__ . '/../');
3 3
 
4
-require_once SATOKO_ROOT . 'index.php';
4
+require_once STK_ROOT . 'index.php';

+ 237
- 0
src/DatabaseMigrationManager.php View File

@@ -0,0 +1,237 @@
1
+<?php
2
+namespace Satoko;
3
+
4
+use Exception;
5
+use PDO;
6
+use PDOException;
7
+
8
+final class DatabaseMigrationManager
9
+{
10
+    private $targetConnection;
11
+    private $migrationStorage;
12
+
13
+    private const MIGRATION_NAMESPACE = '\\Satoko\\DatabaseMigrations\\%s\\%s';
14
+
15
+    private $errors = [];
16
+
17
+    private $logFunction;
18
+
19
+    public function __construct(PDO $conn, string $path)
20
+    {
21
+        $this->targetConnection = $conn;
22
+        $this->migrationStorage = realpath($path);
23
+    }
24
+
25
+    private function addError(Exception $exception): void
26
+    {
27
+        $this->errors[] = $exception;
28
+        $this->writeLog($exception->getMessage());
29
+    }
30
+
31
+    public function setLogger(callable $logger): void
32
+    {
33
+        $this->logFunction = $logger;
34
+    }
35
+
36
+    private function writeLog(string $log): void
37
+    {
38
+        if (!is_callable($this->logFunction)) {
39
+            return;
40
+        }
41
+
42
+        call_user_func($this->logFunction, $log);
43
+    }
44
+
45
+    public function getErrors(): array
46
+    {
47
+        return $this->errors;
48
+    }
49
+
50
+    private function getMigrationScripts(): array
51
+    {
52
+        if (!file_exists($this->migrationStorage) || !is_dir($this->migrationStorage)) {
53
+            $this->addError(new Exception('Migrations script directory does not exist.'));
54
+            return [];
55
+        }
56
+
57
+        $files = glob(rtrim($this->migrationStorage, '/\\') . '/*.php');
58
+        return $files;
59
+    }
60
+
61
+    private function createMigrationRepository(): bool
62
+    {
63
+        try {
64
+            $this->targetConnection->exec('
65
+                CREATE TABLE IF NOT EXISTS `' . STK_DB_PFX . 'migrations` (
66
+                    `migration_id`      INT(10) UNSIGNED    NOT NULL AUTO_INCREMENT,
67
+                    `migration_name`    VARCHAR(255)        NOT NULL,
68
+                    `migration_batch`   INT(11) UNSIGNED    NOT NULL,
69
+                    PRIMARY KEY (`migration_id`),
70
+                    UNIQUE INDEX (`migration_id`)
71
+                )
72
+            ');
73
+        } catch (PDOException $ex) {
74
+            $this->addError($ex);
75
+            return false;
76
+        }
77
+
78
+        return true;
79
+    }
80
+
81
+    public function migrate(): bool
82
+    {
83
+        $this->writeLog('Running migrations...');
84
+
85
+        if (!$this->createMigrationRepository()) {
86
+            return false;
87
+        }
88
+
89
+        $migrationScripts = $this->getMigrationScripts();
90
+
91
+        if (count($migrationScripts) < 1) {
92
+            if (count($this->errors) > 0) {
93
+                return false;
94
+            }
95
+
96
+            $this->writeLog('Nothing to migrate!');
97
+            return true;
98
+        }
99
+
100
+        try {
101
+            $this->writeLog('Fetching completed migration...');
102
+            $fetchStatus = $this->targetConnection->prepare("
103
+                SELECT *, CONCAT(:basepath, '/', `migration_name`, '.php') as `migration_path`
104
+                FROM `" . STK_DB_PFX . "migrations`
105
+            ");
106
+            $fetchStatus->bindValue('basepath', $this->migrationStorage);
107
+            $migrationStatus = $fetchStatus->execute() ? $fetchStatus->fetchAll() : [];
108
+        } catch (PDOException $ex) {
109
+            $this->addError($ex);
110
+            return false;
111
+        }
112
+
113
+        if (count($migrationStatus) < 1 && count($this->errors) > 0) {
114
+            return false;
115
+        }
116
+
117
+        $remainingMigrations = array_diff($migrationScripts, array_column($migrationStatus, 'migration_path'));
118
+
119
+        if (count($remainingMigrations) < 1) {
120
+            $this->writeLog('Nothing to migrate!');
121
+            return true;
122
+        }
123
+
124
+        $batchNumber = $this->targetConnection->query('
125
+            SELECT COALESCE(MAX(`migration_batch`), 0) + 1
126
+            FROM `' . STK_DB_PFX . 'migrations`
127
+        ')->fetchColumn();
128
+
129
+        $recordMigration = $this->targetConnection->prepare('
130
+            INSERT INTO `' . STK_DB_PFX . 'migrations`
131
+                (`migration_name`, `migration_batch`)
132
+            VALUES
133
+                (:name, :batch)
134
+        ');
135
+        $recordMigration->bindValue('batch', $batchNumber);
136
+
137
+        foreach ($remainingMigrations as $migration) {
138
+            $filename = pathinfo($migration, PATHINFO_FILENAME);
139
+            $filenameSplit = explode('_', $filename);
140
+            $recordMigration->bindValue('name', $filename);
141
+            $migrationName = '';
142
+
143
+            if (count($filenameSplit) < 5) {
144
+                $this->addError(new Exception("Invalid migration name: '{$filename}'"));
145
+                return false;
146
+            }
147
+
148
+            for ($i = 4; $i < count($filenameSplit); $i++) {
149
+                $migrationName .= ucfirst(mb_strtolower($filenameSplit[$i]));
150
+            }
151
+
152
+            include_once $migration;
153
+
154
+            $this->writeLog("Running migration '{$filename}'...");
155
+            $migrationFunction = sprintf(self::MIGRATION_NAMESPACE, $migrationName, 'migrate_up');
156
+            $migrationFunction($this->targetConnection);
157
+            $recordMigration->execute();
158
+        }
159
+
160
+        $this->writeLog('Successfully completed all migrations!');
161
+
162
+        return true;
163
+    }
164
+
165
+    public function rollback(): bool
166
+    {
167
+        $this->writeLog('Rolling back last migration batch...');
168
+
169
+        if (!$this->createMigrationRepository()) {
170
+            return false;
171
+        }
172
+
173
+        try {
174
+            $fetchStatus = $this->targetConnection->prepare("
175
+                SELECT *, CONCAT(:basepath, '/', `migration_name`, '.php') as `migration_path`
176
+                FROM `" . STK_DB_PFX . "migrations`
177
+                WHERE `migration_batch` = (
178
+                    SELECT MAX(`migration_batch`)
179
+                    FROM `" . STK_DB_PFX . "migrations`
180
+                )
181
+            ");
182
+            $fetchStatus->bindValue('basepath', $this->migrationStorage);
183
+            $migrations = $fetchStatus->execute() ? $fetchStatus->fetchAll() : [];
184
+        } catch (PDOException $ex) {
185
+            $this->addError($ex);
186
+            return false;
187
+        }
188
+
189
+        if (count($migrations) < 1) {
190
+            if (count($this->errors) > 0) {
191
+                return false;
192
+            }
193
+
194
+            $this->writeLog('Nothing to roll back!');
195
+            return true;
196
+        }
197
+
198
+        $migrationScripts = $this->getMigrationScripts();
199
+
200
+        if (count($migrationScripts) < count($migrations)) {
201
+            $this->addError(new Exception('There are missing migration scripts!'));
202
+            return false;
203
+        }
204
+
205
+        $removeRecord = $this->targetConnection->prepare('
206
+            DELETE FROM `' . STK_DB_PFX . 'migrations`
207
+            WHERE `migration_id` = :id
208
+        ');
209
+
210
+        foreach ($migrations as $migration) {
211
+            if (!file_exists($migration['migration_path'])) {
212
+                $this->addError(new Exception("Migration '{$migration['migration_name']}' does not exist."));
213
+                return false;
214
+            }
215
+
216
+            $nameSplit = explode('_', $migration['migration_name']);
217
+            $migrationName = '';
218
+
219
+            for ($i = 4; $i < count($nameSplit); $i++) {
220
+                $migrationName .= ucfirst(mb_strtolower($nameSplit[$i]));
221
+            }
222
+
223
+            include_once $migration['migration_path'];
224
+
225
+            $this->writeLog("Rolling '{$migration['migration_name']}' back...");
226
+            $migrationFunction = sprintf(self::MIGRATION_NAMESPACE, $migrationName, 'migrate_down');
227
+            $migrationFunction($this->targetConnection);
228
+
229
+            $removeRecord->bindValue('id', $migration['migration_id']);
230
+            $removeRecord->execute();
231
+        }
232
+
233
+        $this->writeLog('Successfully completed all rollbacks');
234
+
235
+        return true;
236
+    }
237
+}

+ 53
- 0
src/Router.php View File

@@ -0,0 +1,53 @@
1
+<?php
2
+namespace Satoko;
3
+
4
+use Phroute\Phroute\RouteCollector;
5
+use Phroute\Phroute\Dispatcher;
6
+
7
+final class Router extends RouteCollector {
8
+    private static $instance = null;
9
+    private $usePathInfo;
10
+    private $basePath = '/';
11
+
12
+    public function __construct(string $basePath = '/', bool $usePathInfo = false) {
13
+        if(is_null(self::$instance))
14
+            self::$instance = $this;
15
+
16
+        $this->basePath = $basePath;
17
+        $this->usePathInfo = $usePathInfo;
18
+        parent::__construct();
19
+    }
20
+
21
+    public static function instance(): Router {
22
+        return self::$instance ?? new static;
23
+    }
24
+
25
+    public function url(string $name, ...$params): string {
26
+        $path = $this->basePath;
27
+
28
+        if($this->hasRoute($name)) {
29
+            if($this->usePathInfo)
30
+                $path .= 'index.php/';
31
+
32
+            $path .= $this->route($name, $params);
33
+        } else
34
+            $path .= $name;
35
+
36
+        return $path;
37
+    }
38
+
39
+    public function dispatch(): void
40
+    {
41
+        $dispatcher = new Dispatcher($this->getData());
42
+        $response = $dispatcher->dispatch(
43
+            $_SERVER['REQUEST_METHOD'],
44
+            $this->usePathInfo
45
+                ? $_SERVER['PATH_INFO']
46
+                : parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH)
47
+        );
48
+
49
+        // just always assume html for now
50
+        header('Content-Type: text/html; charset=utf-8');
51
+        echo $response;
52
+    }
53
+}

+ 55
- 0
src/Templates.php View File

@@ -0,0 +1,55 @@
1
+<?php
2
+namespace Satoko;
3
+
4
+use Twig_Environment;
5
+use Twig_Loader_Filesystem;
6
+use UnexpectedValueException;
7
+
8
+final class Templates extends Twig_Environment
9
+{
10
+    private static $instance = null;
11
+    private $vars = [];
12
+
13
+    public static function instance(): Twig_Environment {
14
+        return self::$instance;
15
+    }
16
+
17
+    public function __construct(array $options = []) {
18
+        if (self::$instance !== null) {
19
+            throw new UnexpectedValueException('Instance of Twig already present, use the static instance() function.');
20
+        }
21
+
22
+        $options = array_merge([
23
+            'cache' => false,
24
+            'strict_variables' => true,
25
+            'auto_reload' => false,
26
+            'debug' => false,
27
+        ], $options);
28
+
29
+        parent::__construct(new Twig_Loader_Filesystem, $options);
30
+        self::$instance = $this;
31
+    }
32
+
33
+    public function var(string $key, $value): void {
34
+        $this->vars([$key => $value]);
35
+    }
36
+
37
+    public function vars(array $vars): void {
38
+        $this->vars = array_merge_recursive($this->vars, $vars);
39
+    }
40
+
41
+    public function addPath(string $path): void {
42
+        $this->getLoader()->addPath($path);
43
+    }
44
+
45
+    public function exists(string $path): void {
46
+        $this->getLoader()->exists($path);
47
+    }
48
+
49
+    public function render($path, $vars = []): string {
50
+        if(!empty($vars))
51
+            $this->vars($vars);
52
+
53
+        return parent::render($path, $this->vars);
54
+    }
55
+}