Initial commit
commit
c6f6ec1bc1
@ -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
|
@ -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/"
|
||||
}
|
||||
}
|
||||
}
|
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…
Reference in New Issue