From f5d5166b4bda4097b00d37eb50ac0bda0191dead Mon Sep 17 00:00:00 2001 From: overflowerror Date: Wed, 29 Nov 2023 22:17:08 +0100 Subject: [PATCH] feat: Basic project setup --- .github/workflows/upload.yml | 49 +++++++++ .gitignore | 5 + context.php | 4 + controllers/GET.php | 5 + controllers/routes.php | 18 +++ controllers/slug/GET.php | 5 + core.php | 25 +++++ credentials.templ.php | 7 ++ html/.htaccess | 13 +++ html/index.php | 3 + maintenance.php | 3 + persistence/Repositories.php | 33 ++++++ persistence/connection.php | 7 ++ persistence/migrate.php | 116 ++++++++++++++++++++ persistence/migrations/.gitkeep | 0 persistence/migrations/0001_addUrlTable.sql | 12 ++ persistence/models/URL.php | 21 ++++ persistence/repositories/URLs.php | 40 +++++++ router/Router.php | 78 +++++++++++++ templates/404.php | 1 + templates/maintenance.php | 1 + utils/arrays.php | 10 ++ utils/error.php | 13 +++ utils/http.php | 45 ++++++++ 24 files changed, 514 insertions(+) create mode 100644 .github/workflows/upload.yml create mode 100644 .gitignore create mode 100644 context.php create mode 100644 controllers/GET.php create mode 100644 controllers/routes.php create mode 100644 controllers/slug/GET.php create mode 100644 core.php create mode 100644 credentials.templ.php create mode 100644 html/.htaccess create mode 100644 html/index.php create mode 100644 maintenance.php create mode 100644 persistence/Repositories.php create mode 100644 persistence/connection.php create mode 100644 persistence/migrate.php create mode 100644 persistence/migrations/.gitkeep create mode 100644 persistence/migrations/0001_addUrlTable.sql create mode 100644 persistence/models/URL.php create mode 100644 persistence/repositories/URLs.php create mode 100644 router/Router.php create mode 100644 templates/404.php create mode 100644 templates/maintenance.php create mode 100644 utils/arrays.php create mode 100644 utils/error.php create mode 100644 utils/http.php diff --git a/.github/workflows/upload.yml b/.github/workflows/upload.yml new file mode 100644 index 0000000..53b7012 --- /dev/null +++ b/.github/workflows/upload.yml @@ -0,0 +1,49 @@ +name: 'Publish to prod' + +on: + push: + branches: + - "main" + +permissions: + id-token: write + contents: read + + +jobs: + upload-prod: + runs-on: ubuntu-latest + permissions: + contents: 'read' + id-token: 'write' + + steps: + - uses: 'actions/checkout@v3' + - name: Upload + env: + FTP_SERVER: ${{ secrets.FTP_SERVER }} + FTP_USERNAME: ${{ secrets.FTP_USERNAME }} + FTP_PASSWORD: ${{ secrets.FTP_PASSWORD }} + DB_DSN: ${{ secrets.DB_DSN }} + DB_SCHEMA: ${{ secrets.DB_SCHEMA }} + DB_USERNAME: ${{ secrets.DB_USERNAME }} + DB_PASSWORD: ${{ secrets.DB_PASSWORD }} + run: | + sudo apt install lftp + rm -rf .git .github + cp credentials.templ.php credentials.php + sed -i -e "s/{DB_DSN}/${DB_DSN}/g" credentials.php + sed -i -e "s/{DB_SCHEMA}/${DB_SCHEMA}/g" credentials.php + sed -i -e "s/{DB_USERNAME}/${DB_USERNAME}/g" credentials.php + sed -i -e "s/{DB_PASSWORD}/${DB_PASSWORD}/g" credentials.php + mv maintenance.php maintenance-real.php + sed -e 's/false/true/g' maintenance-real.php > maintenance.php + lftp -e " + set sftp:auto-confirm yes; + set ssl:verify-certificate no; + open -u ${FTP_USERNAME},${FTP_PASSWORD} sftp://${FTP_SERVER}; + put maintenance.php -o maintenance.php; + mirror --exclude=maintenance-real.php -e -R ./ ./; + put maintenance-real.php -o maintenance.php; + quit; + " diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..615d029 --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +.idea + +credentials.php + +notes.txt \ No newline at end of file diff --git a/context.php b/context.php new file mode 100644 index 0000000..397539e --- /dev/null +++ b/context.php @@ -0,0 +1,4 @@ +addRoute(GET, "/", fromController("/GET")); + + $router->addRoute(GET, "/.*", fromController("/slug/GET")); +}; diff --git a/controllers/slug/GET.php b/controllers/slug/GET.php new file mode 100644 index 0000000..ec210b7 --- /dev/null +++ b/controllers/slug/GET.php @@ -0,0 +1,5 @@ + $connection, + REPOSITORIES => $repositories, + ]; + + $router->execute($context); +} diff --git a/credentials.templ.php b/credentials.templ.php new file mode 100644 index 0000000..562d6e7 --- /dev/null +++ b/credentials.templ.php @@ -0,0 +1,7 @@ + +RewriteEngine On +RewriteBase / + +RewriteRule ^index\.php$ - [L] +RewriteCond %{REQUEST_URI} !^/static/ +RewriteCond %{REQUEST_FILENAME} !-f +RewriteCond %{REQUEST_FILENAME} !-d +RewriteRule . /index.php [L] + + +php_flag log_errors on +php_value error_log /usr/home/usefula/php_dm.log \ No newline at end of file diff --git a/html/index.php b/html/index.php new file mode 100644 index 0000000..bee614d --- /dev/null +++ b/html/index.php @@ -0,0 +1,3 @@ + URLs::class, + ]; + + private array $repositoryCache = []; + + public function __construct(PDO $connection) { + $this->connection = $connection; + } + + public function __get(string $name) { + $repository = $this->repositoryCache[$name] ?? null; + + if (!$repository) { + $repository = new ($this->repositoryClasses[$name])($this->connection); + $this->repositoryCache[$name] = $repository; + } + + return $repository; + } + +} + +return function(PDO $connection): Repositories { + return new Repositories($connection); +}; \ No newline at end of file diff --git a/persistence/connection.php b/persistence/connection.php new file mode 100644 index 0000000..001dc60 --- /dev/null +++ b/persistence/connection.php @@ -0,0 +1,7 @@ +query(<<rowCount() == 0 + ) { + if ($connection->exec(<<errorCode()); + } + } +} + +function getAllMigrations() { + $files = scandir(ROOT . "/persistence/migrations/"); + $files = array_values(array_filter($files, fn($f) => $f[0] != ".")); + sort($files); + + $migrations = []; + + foreach ($files as $file) { + $delimiterPos = strpos($file, "_"); + $migrations[intval(substr($file, 0, $delimiterPos))] = $file; + } + + return $migrations; +} + +function getAppliedMigrations(PDO $connection) { + global $MIGRATION_TABLE; + + $result = $connection->query(<<fetchAll() as $row) { + $migrations[$row["id"]] = $row["file"]; + } + + return $migrations; +} + +function getMigrationsToApply(PDO $connection) { + $all = getAllMigrations(); + $applied = getAppliedMigrations($connection); + + foreach ($applied as $id => $file) { + unset($all[$id]); + } + + return $all; +} + +function executeSqlScript(PDO $connection, string $sql, string $file) { + if ($connection->exec($sql) === false) { + die("failed to apply migration " . $file . ": " . $connection->errorCode()); + } +} + +function applyMigration(PDO $connection, int $id, string $file) { + global $MIGRATION_TABLE; + + $connection->beginTransaction(); + + $sql = file_get_contents(ROOT . "/persistence/migrations/" . $file); + if (!$sql) { + die("Unable to read migration file: " . $file); + } + + executeSqlScript($connection, $sql, $file); + + $statement = $connection->prepare(<<execute([$id, $file]); + + try { + $connection->commit(); + } catch (PDOException $e) { + // this might happen if the migration script contains a DDL statement + // -> ignore + } +} + +return function(PDO $connection) { + ensureDbStructureForMigrations($connection); + + $migrations = getMigrationsToApply($connection); + foreach ($migrations as $id => $file) { + applyMigration($connection, $id, $file); + } +}; diff --git a/persistence/migrations/.gitkeep b/persistence/migrations/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/persistence/migrations/0001_addUrlTable.sql b/persistence/migrations/0001_addUrlTable.sql new file mode 100644 index 0000000..da7cc29 --- /dev/null +++ b/persistence/migrations/0001_addUrlTable.sql @@ -0,0 +1,12 @@ + +CREATE TABLE `dm_urls` ( + `id` BIGINT AUTO_INCREMENT PRIMARY KEY, + `created` DATETIME DEFAULT CURRENT_TIMESTAMP, + `updated` DATETIME DEFAULT CURRENT_TIMESTAMP, + `deleted` DATETIME, + `slug` VARCHAR(100) NOT NULL, + `url` VARCHAR(255) NOT NULL, + `access_key` VARCHAR(255), + + INDEX (`slug`) +); \ No newline at end of file diff --git a/persistence/models/URL.php b/persistence/models/URL.php new file mode 100644 index 0000000..2dafa4f --- /dev/null +++ b/persistence/models/URL.php @@ -0,0 +1,21 @@ +slug = $slug; + $this->url = $url; + $this->accessKey = $accessKey; + } +} \ No newline at end of file diff --git a/persistence/repositories/URLs.php b/persistence/repositories/URLs.php new file mode 100644 index 0000000..65c9bbe --- /dev/null +++ b/persistence/repositories/URLs.php @@ -0,0 +1,40 @@ +connection = $connection; + } + + public function add(URL $entry) { + $statement = $this->connection->prepare(<<table` + (`slug`, `url`, `access_key`) VALUES + (?, ?, ?) + EOF); + $statement->execute([ + $entry->slug, + $entry->url, + $entry->accessKey, + ]); + } + + public function getBySlug(string $slug) { + $statement = $this->connection->prepare(<<table` + WHERE `slug` = ? + EOF); + $statement->execute([$slug]); + + if ($statement->rowCount() == 0) { + return null; + } else { + return $statement->fetch(); + } + } +} diff --git a/router/Router.php b/router/Router.php new file mode 100644 index 0000000..9c73126 --- /dev/null +++ b/router/Router.php @@ -0,0 +1,78 @@ +notFoundHandler = function(array &$context) { + setStatusCode(404); + require(ROOT . "/templates/404.php"); + }; + $this->prefix = $prefix; + } + + private function findRoute(string $method, string $url) { + if (!key_exists($method, $this->routes)) { + return null; + } + + $paths = $this->routes[$method]; + + foreach ($paths as $path => $handler) { + if (preg_match("/^" . str_replace("/", "\/", $path, ) . "$/", $url)) { + return $handler; + } + } + + return null; + } + + private function getPath($uri) { + if (($i = strpos($uri, "?")) !== false) { + return substr($uri, 0, $i); + } else { + return $uri; + } + } + + public function addRoute(string $method, string $path, $handler) { + array_get_or_add($method, $this->routes, [])[$path] = $handler; + } + + public function execute(array &$context = []) { + $path = $this->getPath($_SERVER["REQUEST_URI"]); + $path = substr($path, strlen($this->prefix)); + + $route = $this->findRoute($_SERVER["REQUEST_METHOD"], $path); + + if (!$route) { + $route = $this->notFoundHandler; + } + + $context["REQUEST_PATH"] = $path; + + return $route($context); + } + + // calling magic to make the router a handler and thus cascade-able + public function __invoke(array &$context = []) { + return $this->execute($context); + } +} + +return new Router(); \ No newline at end of file diff --git a/templates/404.php b/templates/404.php new file mode 100644 index 0000000..1bcdf15 --- /dev/null +++ b/templates/404.php @@ -0,0 +1 @@ +404 Not found \ No newline at end of file diff --git a/templates/maintenance.php b/templates/maintenance.php new file mode 100644 index 0000000..bc7e662 --- /dev/null +++ b/templates/maintenance.php @@ -0,0 +1 @@ +We are currently in maintenance mode. \ No newline at end of file diff --git a/utils/arrays.php b/utils/arrays.php new file mode 100644 index 0000000..0361edd --- /dev/null +++ b/utils/arrays.php @@ -0,0 +1,10 @@ + $error, + "description" => $description, + "timestamp" => (new DateTime())->format("c") + ]; +} \ No newline at end of file diff --git a/utils/http.php b/utils/http.php new file mode 100644 index 0000000..4e822f7 --- /dev/null +++ b/utils/http.php @@ -0,0 +1,45 @@ + $k . ": " . $v, array_keys($headers), array_values($headers)); + + + curl_setopt($curlResource, CURLOPT_HTTPHEADER, $headers); + } + + $body = curl_exec($curlResource); + $result = [ + "body" => $body, + "isError" => curl_errno($curlResource), + "error" => curl_error($curlResource), + "status" => curl_getinfo($curlResource, CURLINFO_RESPONSE_CODE), + ]; + + curl_close($curlResource); + + return $result; +} + +function getRequest(string $url, array $headers = []) { + return request("GET", $url, null, $headers); +} + +function postRequest(string $url, string $body = null, $headers = []) { + return request("POST", $url, $body, $headers); +} \ No newline at end of file