Initial commit

master
eater 4 years ago
commit c6f6ec1bc1
Signed by: eater
GPG Key ID: AD2560A0F84F0759

@ -0,0 +1,11 @@
name: default
kind: pipeline
type: docker
steps:
- name: phpstan
image: d.xr.to/php
commands:
- composer install
- ./vendor/bin/php-cs-fixer fix --dry-run .
- ./vendor/bin/phpstan analyse src

4
.gitignore vendored

@ -0,0 +1,4 @@
/vendor/
/var
/certs
/.idea/

@ -0,0 +1,35 @@
{
"name": "cubistore/web",
"description": "The frontend of CubiStore website",
"type": "project",
"require": {
"php": "^7.3",
"slim/slim": "^4.3",
"php-di/php-di": "^6.0",
"doctrine/orm": "^2.7",
"php-di/slim-bridge": "^3.0",
"ext-json": "*",
"slim/psr7": "^0.6.0",
"twig/twig": "^3.0",
"doctrine/migrations": "^2.2",
"ext-zip": "*",
"ext-simplexml": "*",
"ext-openssl": "*"
},
"require-dev": {
"squizlabs/php_codesniffer": "3.*",
"phpstan/phpstan": "^0.11.19"
},
"license": "GPLv3",
"authors": [
{
"name": "eater",
"email": "=@eater.me"
}
],
"autoload": {
"psr-4": {
"CubiStore\\Web\\": "src/"
}
}
}

3497
composer.lock generated

File diff suppressed because it is too large Load Diff

@ -0,0 +1,10 @@
<?php
// Doctrine CLI config
use CubiStore\Web\Main;
use Doctrine\ORM\EntityManager;
use Doctrine\ORM\Tools\Console\ConsoleRunner;
require_once __DIR__ . '/../vendor/autoload.php';
$main = Main::make('dev');
$entityManager = $main->get(EntityManager::class);
return ConsoleRunner::createHelperSet($entityManager);

@ -0,0 +1,51 @@
<?php
use DI\Container;
use Doctrine\ORM\Configuration;
use Doctrine\ORM\EntityManager;
use Doctrine\ORM\Tools\Setup;
use Twig\Loader\FilesystemLoader;
use Twig\Loader\LoaderInterface;
use function DI\autowire;
use function DI\factory;
use function DI\get;
return [
'app.name' => 'CubiStore',
'app.description' => 'Hello world, ya we signing',
'app.domain' => 'localhost:8888',
'doctrine.connection' => [
'driver' => 'pdo_sqlite',
'path' => __DIR__ . '/../var/db.sqlite'
],
'twig.views' => __DIR__ . '/../views',
'env.is_dev' => factory(function () {
return $_ENV['ENV'] !== 'prod';
}),
'twig.config' => factory(function (Container $container) {
return [
'debug' => $container->get('env.is_dev'),
'cache' => __DIR__ . '/../var/cache/views'
];
}),
// Twig
Twig\Environment::class => autowire()->constructorParameter(1, get('twig.config')),
FilesystemLoader::class => autowire()->constructorParameter(0, get('twig.views')),
LoaderInterface::class => get(FilesystemLoader::class),
// Doctrine
Configuration::class => factory(function (Container $container) {
return Setup::createAnnotationMetadataConfiguration(
[__DIR__ . '/../src/Model'],
$container->get('env.is_dev'),
null,
null,
false
);
}),
EntityManager::class => factory([EntityManager::class, 'create'])
->parameter('connection', get('doctrine.connection'))
];

@ -0,0 +1,15 @@
<?php
use CubiStore\Web\Controller\AppManager;
use CubiStore\Web\Controller\Home;
use CubiStore\Web\Controller\Repo;
return [
'/' => Home::class . '::index',
'/app/create' => [
'GET' => AppManager::class . '::createShow',
'POST' => AppManager::class . '::createAction',
],
'/repo/index-v1.json' => Repo::class . '::json',
'/repo/index-v1.jar' => Repo::class . '::jarJson',
];

@ -0,0 +1,9 @@
<?php
use CubiStore\Web\Main;
include(__DIR__ . '/../vendor/autoload.php');
define('STDOUT', fopen('php://stdout', 'w'));
fprintf(STDOUT, $_SERVER['REQUEST_METHOD'] . ' ' . $_SERVER['REQUEST_URI'] . "\n");
Main::main('dev');

@ -0,0 +1,90 @@
<?php
namespace CubiStore\Web\Controller;
use CubiStore\Web\Service\ApkService;
use CubiStore\Web\Service\AppService;
use CubiStore\Web\Utils\Apk;
use Doctrine\ORM\EntityManager;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\UploadedFileInterface;
use Slim\Psr7\Request;
use Slim\Psr7\Response;
use Twig\Environment;
class AppManager
{
function createShow(Environment $twig, ResponseInterface $response)
{
$response->getBody()->write($twig->render('app/create.html.twig'));
return $response;
}
function createAction(Request $request, Response $response, ApkService $apkService, AppService $appService, EntityManager $em)
{
$files = $request->getUploadedFiles();
if (!isset($files['apk'])) {
return $response
->withStatus(302)
->withHeader('Location', '/app/create');
}
/** @var UploadedFileInterface $apkUpload */
$apkUpload = $files['apk'];
// Check if upload succeeded
if ($apkUpload->getError() !== UPLOAD_ERR_OK) {
return $response
->withStatus(302)
->withHeader('Location', '/app/create');
}
$tmp = tempnam(sys_get_temp_dir(), 'apk-');
$apkUpload->moveTo($tmp);
$apk = new Apk($tmp);
if (!$apk->isValid()) {
return $response
->withStatus(302)
->withHeader('Location', '/app/create');
}
if ($appService->hasApprovedApp($apk->getPackageName())) {
return $response
->withStatus(302)
->withHeader('Location', '/app/create');
}
$app = $appService->createApp($apk);
// Flush to get id, as it's used in getStorePath
$em->persist($app);
$em->flush();
$storePath = $appService->getStorePath($app, $apk);
if (!is_dir($storePath) && !mkdir($storePath, 0777, true)) {
return $response
->withStatus(302)
->withHeader('Location', '/app/create');
}
$newApkPath = $storePath . '/' . $apkUpload->getClientFilename();
if (!rename($tmp, $newApkPath)) {
return $response
->withStatus(302)
->withHeader('Location', '/app/create');
}
$release = $appService->createRelease($apk, $app, $newApkPath);
$em->persist($release);
$em->flush();
return $response
->withStatus(302)
->withHeader('Location', '/app/' . $app->getName() . '/' . $app->getId() . '/manage');
}
}

@ -0,0 +1,16 @@
<?php
namespace CubiStore\Web\Controller;
use Psr\Http\Message\ResponseInterface;
class Home
{
public function index(ResponseInterface $response): ResponseInterface
{
$response->getBody()->write("Hello world");
return $response;
}
}

@ -0,0 +1,35 @@
<?php
namespace CubiStore\Web\Controller;
use CubiStore\Web\Service\FDroidRepoService;
use Slim\Psr7\Response;
class Repo
{
function json(Response $response, FDroidRepoService $repoService)
{
$jsonArr = $repoService->createIndexV1Array();
$response
->getBody()->write(json_encode($jsonArr));
return $response
->withHeader('Content-Type', 'application/json');
}
function jarJson(Response $response, FDroidRepoService $repoService)
{
$jsonArr = $repoService->createIndexV1Array();
$jsonFile = json_encode($jsonArr);
$jar = $repoService->createSigned('index-v1.json', $jsonFile);
$response->getBody()->write($jar);
return $response
->withHeader('Content-Type', 'application/x-jar')
->withHeader('ETag', (string)time());
}
}

@ -0,0 +1,114 @@
<?php
namespace CubiStore\Web;
use CubiStore\Web\Cache\CompiledRoutes;
use CubiStore\Web\Utils\ICompiledRoutes;
use CubiStore\Web\Utils\RouteCompiler;
use DI\Bridge\Slim\Bridge;
use DI\Container;
use DI\ContainerBuilder;
use Slim\App;
use function DI\autowire;
class Main
{
/**
* @var string
*/
private $env;
/**
* @var Container
*/
private $container;
/**
* @var App
*/
private $app;
private function __construct(string $env = 'prod')
{
$this->env = $env;
}
static function main(string $env = 'prod')
{
static::make($env)->run();
}
static function make(string $env = 'prod')
{
$_ENV['ENV'] = $env;
$main = new Main($env);
$main->setup();
return $main;
}
function run()
{
$this->app->run();
}
function setup()
{
$routes = $this->setupRoutes();
$this->setupContainer($routes);
$this->setupSlim($routes);
}
function setupRoutes(): ICompiledRoutes
{
$compiledRoutesFile = __DIR__ . '/../var/cache/routes.php';
if ($this->env !== 'prod' || !file_exists($compiledRoutesFile)) {
$routes = include(__DIR__ . '/../config/routes.php');
$compiler = new RouteCompiler($compiledRoutesFile);
$compiler->addRoutes($routes);
$compiler->writeCache();
}
include(__DIR__ . '/../var/cache/routes.php');
return new CompiledRoutes();
}
function setupContainer(ICompiledRoutes $routes)
{
$containerBuilder = new ContainerBuilder();
if ($this->env === 'prod') {
$containerBuilder->enableCompilation(__DIR__ . '/../var/cache/container');
}
$containerBuilder->addDefinitions(include(__DIR__ . '/../config/container.php'));
if ($this->env !== 'prod') {
$def = [];
foreach ($routes->getKeys() as $key) {
if (class_exists($key)) {
$def[$key] = autowire();
}
}
$containerBuilder->addDefinitions($def);
}
$this->container = $containerBuilder->build();
}
function setupSlim(ICompiledRoutes $compiledRoutes)
{
$app = Bridge::create($this->container);
$compiledRoutes->configure($app);
$app->addRoutingMiddleware();
$app->addBodyParsingMiddleware();
$this->app = $app;
}
public function get(string $class)
{
return $this->container->get($class);
}
}

@ -0,0 +1,128 @@
<?php
namespace CubiStore\Web\Model;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\ORM\Mapping as ORM;
/**
* @ORM\Entity
* @ORM\Table(name="app")
*/
class App
{
const STATUS_CREATED = 'created';
const STATUS_REJECTED = 'rejected';
const STATUS_APPROVED = 'approved';
/**
* @ORM\Id
* @ORM\GeneratedValue(strategy="AUTO")
* @ORM\Column(type="bigint")
* @var integer
*/
private $id;
/**
* @ORM\Column(type="string")
* @var string
*/
private $name;
/**
* @ORM\Column(type="string")
* @var string
*/
private $status = self::STATUS_CREATED;
/**
* @ORM\Column(type="string")
* @var string
*/
private $label;
/**
* @ORM\OneToMany(targetEntity="Release", mappedBy="app")
* @var Release[]|ArrayCollection
*/
private $releases;
public function __construct()
{
$this->releases = new ArrayCollection();
}
/**
* @return int
*/
public function getId(): int
{
return $this->id;
}
/**
* @return string
*/
public function getName(): string
{
return $this->name;
}
/**
* @param string $name
*/
public function setName(string $name): void
{
$this->name = $name;
}
/**
* @return string
*/
public function getLabel(): string
{
return $this->label;
}
/**
* @param string $label
*/
public function setLabel(string $label): void
{
$this->label = $label;
}
/**
* @return Release[]|ArrayCollection
*/
public function getReleases()
{
return $this->releases;
}
/**
* @param Release[]|ArrayCollection $releases
*/
public function setReleases($releases): void
{
$this->releases = $releases;
}
/**
* @return string
*/
public function getStatus(): string
{
return $this->status;
}
/**
* @param string $status
*/
public function setStatus(string $status): void
{
$this->status = $status;
}
}

@ -0,0 +1,118 @@
<?php
namespace CubiStore\Web\Model;
use Doctrine\ORM\Mapping as ORM;
/**
* @ORM\Entity
* @ORM\Table(name="release")
*/
class Release
{
/**
* @var integer
* @ORM\Id
* @ORM\GeneratedValue(strategy="AUTO")
* @ORM\Column(type="bigint")
*/
private $id;
/**
* @var App
* @ORM\ManyToOne(targetEntity="App", inversedBy="releases")
*/
private $app;
/**
* @var string
* @ORM\Column(type="string")
*/
private $versionName;
/**
* @var integer
* @ORM\Column(type="integer")
*/
private $versionCode;
/**
* @var string
* @ORM\Column(type="string")
*/
private $apk;
/**
* @return int
*/
public function getId(): int
{
return $this->id;
}
/**
* @return App
*/
public function getApp(): App
{
return $this->app;
}
/**
* @param App $app
*/
public function setApp(App $app): void
{
$this->app = $app;
}
/**
* @return string
*/
public function getVersionName(): string
{
return $this->versionName;
}
/**
* @param string $versionName
*/
public function setVersionName(string $versionName): void
{
$this->versionName = $versionName;
}
/**
* @return int
*/
public function getVersionCode(): int
{
return $this->versionCode;
}
/**
* @param int $versionCode
*/
public function setVersionCode(int $versionCode): void
{
$this->versionCode = $versionCode;
}
/**
* @return string
*/
public function getApk(): string
{
return $this->apk;
}
/**
* @param string $apk
*/
public function setApk(string $apk): void
{
$this->apk = $apk;
}
}

@ -0,0 +1,10 @@
<?php
namespace CubiStore\Web\Service;
class ApkService
{
}

@ -0,0 +1,64 @@
<?php
namespace CubiStore\Web\Service;
use ApkParser\Manifest;
use ApkParser\Parser;
use CubiStore\Web\Model\App;
use CubiStore\Web\Model\Release;
use CubiStore\Web\Utils\Apk;
use Doctrine\ORM\EntityManager;
class AppService
{
/**
* @var EntityManager
*/
private $entityManager;
/**
* AppService constructor.
* @param EntityManager $entityManager
*/
public function __construct(EntityManager $entityManager)
{
$this->entityManager = $entityManager;
}
public function hasApprovedApp(string $name): bool
{
/** @var App|null $app */
$app = $this->entityManager
->getRepository(App::class)
->findOneBy(['name' => $name, 'status' => App::STATUS_APPROVED]);
return $app !== null;
}
public function createApp(Apk $apk): App
{
$app = new App();
$app->setName($apk->getPackageName());
$app->setLabel($apk->getLabel());
return $app;
}
public function createRelease(Apk $apk, App $app, string $file): Release
{
$release = new Release();
$release->setApp($app);
$release->setVersionCode($apk->getVersionCode());
$release->setVersionName($apk->getVersionName());
$release->setApk($file);
return $release;
}
public function getStorePath(App $app, Apk $apk): string
{
return __DIR__ . '/../../var/storage/apk/' . $app->getName() . '/' . $app->getId() . '/' . $apk->getVersionCode();
}
}

@ -0,0 +1,164 @@
<?php
namespace CubiStore\Web\Service;
use CubiStore\Web\Model\App;
use DI\Container;
use Doctrine\ORM\EntityManager;
use ZipArchive;
class FDroidRepoService
{
/**
* @var Container
*/
private $container;
/**
* FDroidRepoService constructor.
* @param Container $container
*/
public function __construct(Container $container)
{
$this->container = $container;
}
public function createIndexV1Array()
{
/** @var EntityManager $em */
$em = $this->container->get(EntityManager::class);
$apps = $em
->getRepository(App::class)
->findBy([
'status' => App::STATUS_APPROVED
]);
$appObjects = [];
$packageObjects = [];
foreach ($apps as $app) {
/** @var $app App */
$appObjects[] = [
"authorEmail" => "example@example.com",
"authorName" => "example",
"authorWebSite" => "https://example.org",
"categories" => [
'example'
],
"suggestedVersionCode" => $app->getReleases()->first()->getVersionCode(),
"name" => $app->getLabel(),
"added" => 0,
"packageName" => $app->getName(),
"lastUpdated" => 0,
"license" => "no"
];
$packages = [];
foreach ($app->getReleases() as $release) {
$packages[] = [
"added" => 0,
"apkName" => "/apk/" . $app->getName() . "/" . $release->getVersionCode() . ".apk",
"hash" => hash('sha256', $release->getApk()),
"hashType" => "sha256",
"minSdkVersion" => 4,
"packageName" => $app->getName(),
"sig" => "deadbeef",
"signer" => "deadbeef",
"size" => filesize($release->getApk()),
"targetSdkVersion" => 28,
"versionCode" => $release->getVersionCode(),
"versionName" => $release->getVersionName()
];
}
$packageObjects[$app->getName()] = $packages;
}
return [
'repo' => [
'timestamp' => time() * 1000,
'version' => 21,
'maxage' => 14,
'name' => $this->container->get('app.name'),
'description' => $this->container->get('app.description'),
'address' => "https://" . $this->container->get('app.domain') . '/repo'
],
'requests' => [
'install' => [],
'uninstall' => [],
],
"apps" => $appObjects,
"packages" => $packageObjects
];
}
public function createSigned($file, $contents)
{
$zip = new ZipArchive();
$zipPath = tempnam(sys_get_temp_dir(), 'zip');
$zip->open($zipPath, ZipArchive::CREATE);
$fileDigest = sha1($contents, true);
$fileHeader = 'Name: ' . $file . "\n";
$fileHeader .= 'SHA1-Digest: ' . base64_encode($fileDigest) . "\n\n";
$fileHeaderDigest = sha1($fileHeader, true);
$manifest = "Manifest-Version: 1.0\n";
$manifest .= "Created-By: CubiStore\n\n";
$manifestHeaderDigest = sha1($manifest, true);
$manifest .= $fileHeader;
$manifestDigest = sha1($manifest, true);
$fileManifest = "Signature-Version: 1.0\n";
$fileManifest .= "SHA1-Digest-Manifest-Main-Attributes: " . base64_encode($manifestHeaderDigest) . "\n";
$fileManifest .= "SHA1-Digest-Manifest: " . base64_encode($manifestDigest) . "\n";
$fileManifest .= "Created-By: CubiStore\n\n";
$fileManifest .= "Name: " . $file . "\n";
$fileManifest .= "SHA1-Digest: " . base64_encode($fileHeaderDigest) . "\n\n";
$zip->addFromString('META-INF/MANIFEST.MF', $manifest);
$zip->addFromString('META-INF/1.SF', $fileManifest);
$in = tempnam(sys_get_temp_dir(), 'repo');
file_put_contents($in, $fileManifest);
$out = tempnam(sys_get_temp_dir(), 'repo');
$success = openssl_pkcs7_sign(
$in,
$out,
'file://' . __DIR__ . '/../../certs/cert.pem',
'file://' . __DIR__ . '/../../certs/key.pem',
[],
PKCS7_BINARY | PKCS7_NOATTR
);
if (!$success) {
$error = '';
while ($line = openssl_error_string()) {
$error .= $line . "\n";
}
throw new \RuntimeException($error);
}
unlink($in);
$signed = file_get_contents($out);
unlink($out);
$contentOffset = strpos($signed, "\n\n");
$content = substr($signed, $contentOffset);
$base64 = str_replace("\n", "", $content);
$zip->addFromString('META-INF/1.RSA', base64_decode($base64));
$zip->addFromString($file, $contents);
$zip->close();
$zipContent = file_get_contents($zipPath);
unlink($zipPath);
return $zipContent;
}
}

@ -0,0 +1,101 @@
<?php
namespace CubiStore\Web\Utils;
use SimpleXMLElement;
class Apk
{
/**
* @var string
*/
private $file;
/**
* @var SimpleXMLElement|null
*/
private $manifest;
/**
* Apk constructor.
* @param string $file
*/
public function __construct(string $file)
{
$this->file = $file;
}
function isValid(): bool
{
// Get first 4 bytes
$fh = fopen($this->file, 'r');
$header = fread($fh, 4);
fclose($fh);
// Check if uploaded file is a valid ZIP file
if ($header !== "PK\x03\x04") {
return false;
}
$zip = new \ZipArchive();
$zip->open($this->file);
$valid = $zip->statName("AndroidManifest.xml") !== false && $zip->statName("resources.arsc");
$zip->close();
return $valid;
}
function getManifest(): SimpleXMLElement
{
if ($this->manifest === null) {
$xml = $this->cmd('androguard', 'axml', '-o', '/dev/stdout', $this->file);
$this->manifest = simplexml_load_string($xml);
}
return $this->manifest;
}
private function getAttribute(string $name, ?string $namespace = null): string
{
if ($namespace !== null) {
return (string)$this->getManifest()->attributes($namespace, true)[$name];
}
return (string)$this->getManifest()[$name];
}
public function getPackageName(): string
{
return $this->getAttribute('package');
}
public function getVersionCode(): int
{
return intval($this->getAttribute('versionCode', 'android'), 10);
}
public function getVersionName(): string
{
return $this->getAttribute('versionName', 'android');
}
public function getLabel(): string
{
$code = (string)$this->getManifest()->application->attributes('android', true)['label'];
$output = $this->cmd('androguard', 'arsc', '--id', $code, $this->file);
preg_match(':\<default\> = \'(.*)\'$:m', $output, $matches);
return stripslashes($matches[1]);
}
private function cmd(string $cmd, string... $args): string
{
$proc = popen($cmd . ' ' . implode(' ', array_map('escapeshellarg', $args)), 'r');
$data = '';
while ($text = fread($proc, 1024)) {
$data .= $text;
}
pclose($proc);
return $data;
}
}

@ -0,0 +1,13 @@
<?php
namespace CubiStore\Web\Utils;
use Slim\App;
interface ICompiledRoutes
{
public function configure(App $app);
public function getKeys(): array;
}

@ -0,0 +1,133 @@
<?php
namespace CubiStore\Web\Utils;
class Route
{
const TYPE_ROOT = 'root';
const TYPE_GROUP = 'group';
const TYPE_CALL = 'call';
/**
* @var string
*/
private $type;
/**
* @var string
*/
private $path;
/**
* @var string
*/
private $fullPath;
/**
* @var Route[]|null
*/
private $children;
/**
* @var string|null
*/
private $method;
/**
* @var string|null
*/
private $call;
private static function new(Route $parent, string $path, string $type): Route
{
if (strlen($path) > 0 && $path[0] !== '/') {
throw new \RuntimeException("path should start with /");
}
if ($parent->type !== self::TYPE_GROUP && $parent->type !== self::TYPE_ROOT) {
throw new \RuntimeException("Can only add child routes to a group or root route");
}
$route = new Route();
$parent->add($route);
$route->path = $path;
$route->fullPath = $parent->fullPath . $path;
$route->type = $type;
return $route;
}
static function group(Route $parent, string $path): Route
{
$route = static::new($parent, $path, self::TYPE_GROUP);
$route->children = [];
return $route;
}
/**
* @param Route $parent
* @param string $method
* @param string $path
* @param string $call
* @return Route
*/
static function call(Route $parent, string $method, string $path, string $call): Route
{
$route = static::new($parent, $path, self::TYPE_CALL);
$route->method = $method;
$route->call = $call;
return $route;
}
static function root(): Route
{
$route = new Route();
$route->type = self::TYPE_ROOT;
$route->fullPath = $route->path = '/';
$route->children = [];
return $route;
}
public function add(Route $child): void
{
if ($this->type !== self::TYPE_GROUP && $this->type !== self::TYPE_ROOT) {
throw new \RuntimeException("Only a group route can have child routes");
}
$this->children[] = $child;
}
private function json($value)
{
return json_encode($value, JSON_UNESCAPED_SLASHES);
}
public function compileRoute($var, $indent = 4): string
{
if ($this->type === self::TYPE_CALL) {
return str_repeat(' ', $indent) . "${var}->map([" . $this->json($this->method) . "], " . $this->json($this->path) . ", " . $this->json($this->call) . ");";
}
if ($this->type === self::TYPE_GROUP) {
$name = "\$group" . strlen($this->fullPath);
$code = str_repeat(' ', $indent) . "${var}->group(" . $this->json($this->path) . ", function (RouteCollectorProxy $name) {\n";
foreach ($this->children as $child) {
$code .= $child->compileRoute($name, $indent + 4) . "\n";
}
return $code . str_repeat(' ', $indent) . "});";
}
if ($this->type === self::TYPE_ROOT) {
$code = '';
foreach ($this->children as $child) {
$code .= $child->compileRoute($var, $indent) . "\n";
}
return $code;
}
throw new \RuntimeException("invalid type for route");
}
}

@ -0,0 +1,85 @@
<?php
namespace CubiStore\Web\Utils;
class RouteCompiler
{
private $target;
private $root;
private $keys;
public function __construct($target = __DIR__ . '/../../var/cache/routes.php')
{
$this->target = $target;
$this->root = Route::root();
$this->keys = [];
}
public function addRoutes(array $routes, ?Route $target = null)
{
if ($target === null) {
$target = $this->root;
}
foreach ($routes as $route => $details) {
if (is_array($details)) {
$route = Route::group($target, $route);
$this->addRoutes($details, $route);
}
if (!is_string($details)) {
continue;
}
$parts = explode(':', $details, 2);
$this->keys[$parts[0]] = true;
if ($route[0] !== '/') {
Route::call($target, $route, '', $details);
continue;
}
Route::call($target, 'GET', $route, $details);
}
}
public function writeCache()
{
file_put_contents($this->target, $this->compile());
}
public function compile()
{
$keys = array_map(function ($item) {
return json_encode($item, JSON_UNESCAPED_SLASHES);
}, array_keys($this->keys));
$head = "<?php
namespace CubiStore\\Web\\Cache;
use CubiStore\\Web\\Utils\\ICompiledRoutes;
use Slim\\App;
use Slim\\Routing\\RouteCollectorProxy;
class CompiledRoutes implements ICompiledRoutes {
function configure(App \$app) {\n";
$tail = "
}
function getKeys(): array {
return [
" . implode(",\n" . str_repeat(' ', 12), $keys) . "
];
}
}
";
$configure = $this->root->compileRoute("\$app", 8);
return $head . $configure . $tail;
}
}

@ -0,0 +1,6 @@
<form action="/app/create" enctype="multipart/form-data" method="post">
<label for="app_first_release_apk">APK</label>
<input type="file" id="app_first_release_apk" name="apk">
<button type="submit">Upload</button>
</form>

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8" ?>
<fdroid>
<repo maxage="14" name="CubiStore" >
</repo>
</fdroid>
Loading…
Cancel
Save