<repo_name>gerkirill/book-box<file_sep>/src/app.php
<?php

use Silex\Application;
use Silex\Provider\SessionServiceProvider;
use Silex\Provider\TwigServiceProvider;
use Silex\Provider\UrlGeneratorServiceProvider;
use Silex\Provider\ValidatorServiceProvider;
use Silex\Provider\ServiceControllerServiceProvider;
use Dropbox as dbx;

$app = new Application();
$app->register(new UrlGeneratorServiceProvider());
$app->register(new ValidatorServiceProvider());
$app->register(new ServiceControllerServiceProvider());
$app->register(new TwigServiceProvider());
$app->register(new SessionServiceProvider());

$app['twig'] = $app->share($app->extend('twig', function($twig, $app) {
    // add custom globals, filters, tags, ...
    $twig->addGlobal('currentUser', $app['persi']->getCurrentUser());
    $twig->addFilter(new Twig_SimpleFilter('md5', 'md5'));
    return $twig;
}));

$app['dbox.auth'] = $app->share(function() {
    $appInfo = dbx\AppInfo::loadFromJsonFile(__DIR__ . '/../config/dropbox.json');
    $webAuth = new dbx\WebAuthNoRedirect($appInfo, "PHP-Example/1.0");
    return $webAuth;
});
$app['persi'] = $app->share(function() use ($app) {
    $persi = new Personalizer();
    $persi->setCurrentUserId($app['session']->get('user'));
    return $persi;
});
$app['user.dboxClient'] = $app->share(function($app) {
    $user = $app['persi']->getCurrentUser();
    if (!$user) {
        throw new \Exception('Can not create user.dboxClient service instance until user is logged in');
    }
    if (!$user->dbAccessToken) {
        throw new \Exception('User has not linked dropbox account yet');
    }
    return new dbx\Client($user->dbAccessToken, "PHP-Example/1.0");
});

$app['injectBasename'] = $app->protect(function($entries) {
    return array_map(function($entry) {
        $entry['basename'] = preg_replace('%^.*/%', '', $entry['path']);
        return $entry;
    }, $entries);
});

$app['tmpFileManager'] = $app->share(function() {
    return new TmpFileManager(__DIR__ . '/../var/cache');
});

return $app;
<file_sep>/config/dev.php
<?php

use Silex\Provider\MonologServiceProvider;
use Silex\Provider\WebProfilerServiceProvider;

// include the prod configuration
require __DIR__.'/prod.php';

require_once(__DIR__ . '/../lib/redbean/rb.php');
R::setup(
    'mysql:host=localhost;dbname=bookbox',
    'root', 'vagrant'
);

// enable the debug mode
$app['debug'] = true;

$app->register(new MonologServiceProvider(), array(
    'monolog.logfile' => __DIR__.'/../var/logs/silex_dev.log',
));

$app->register($p = new WebProfilerServiceProvider(), array(
    'profiler.cache_dir' => __DIR__.'/../var/cache/profiler',
));
$app->mount('/_profiler', $p);<file_sep>/Vagrantfile
$script = <<SCRIPT
    echo Adjusting webroot symlink...
    rm -rf /var/www
    ln -s /vagrant/web /var/www
    service apache2 restart
SCRIPT

Vagrant.configure("2") do |config|
    config.vm.box = "ubuntu-lamp-2014-07-07"
    config.vm.box_url = "https://www.dropbox.com/s/r7awn51cpfef8gz/package-2014-07-07-php55.box?dl=1"
    config.vm.network :private_network, ip: "192.168.72.10"
    config.vm.hostname = "bookbox.local"
    config.vm.provision "shell", inline: $script

    config.vm.provider "virtualbox" do |v|
        v.name = "book-box"
        v.customize ["modifyvm", :id, "--memory", "650"]
        v.customize ["modifyvm", :id, "--cpus", "1"]
        v.customize ["modifyvm", :id, "--ioapic", "on"]
    end
end<file_sep>/src/controllers.php
<?php
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Dropbox as dbx;

// link dropbox account
$app->get('/', function () use ($app) {
    $user = $app['persi']->getCurrentUser();
    if (!$user->dbAccessToken) {
        return new RedirectResponse('/link');
    } elseif (!$user->booksFolder) {
        return new RedirectResponse('/choose');
    } else {
        return new RedirectResponse('/done');
    }
})
->bind('homepage');

$app->get('/link', function() use ($app) {
    $authorizeUrl = $app['dbox.auth']->start();
    return $app['twig']->render('index.html', array('authUrl' => $authorizeUrl));
});

// select OPDS-published folder
$app->get('/choose/{path}', function ($path) use ($app) {
    try {
        $folderMetadata = $app['user.dboxClient']->getMetadataWithChildren("/" . $path);
    } catch (\Exception $e) {
        return new RedirectResponse('/');
    }
    return $app['twig']->render( 'account.html', [
        'entries' => $app['injectBasename']($folderMetadata['contents']),
        'path' => $path
    ]);
})
->value('path', '')
->assert('path', '.*');

$app->get('/done', function() use ($app) {
    $user = $app['persi']->getCurrentUser();
    if (!$user->booksFolder) {
        return new RedirectResponse('/');
    }
    return $app['twig']->render('done.html', []);
});

// finish dropbox auth process
$app->post('/code', function (Request $request) use ($app) {
    $authCode = trim($request->get('code'));
    list($accessToken, $dropboxUserId) = $app['dbox.auth']->finish($authCode);

    $user = $app['persi']->getCurrentUser();
    $user->dbAccessToken = $accessToken;
    $user->dbUserId = $dropboxUserId;
    R::store($user);

    return new RedirectResponse('/');
});

// list books in a subpath of OPDS-published folder
$app->get('/opds/{path}', function($path, Request $request) use ($app) {
    $user = $app['persi']->getCurrentUser();
    // connect to dropbox and list contents
    $folderMetadata = $app['user.dboxClient']->getMetadataWithChildren($user->booksFolder . ($path ? "/$path" : ''));
    return $app['twig']->render('opds-file-list.xml', ['entries' => $app['injectBasename']($folderMetadata['contents'])]);
})
->value('path', '')
->assert('path', '.*')
->bind('opds');

$app->get('/opds-download/{path}', function($path, Request $request) use ($app) {
    $user  = $app['persi']->getCurrentUser();
    $path = '/' . $path;
    if (0 !== strpos($path, $user->booksFolder)) {
        throw new \Exception('This file is out of the OPDS-shared folder');
    }
    list($tmpDownloadUrl, $expires) = $app['user.dboxClient']->createTemporaryDirectLink($path);
    return new RedirectResponse($tmpDownloadUrl);
//    list($cachedFilePath, $cachedFileHandle) = $app['tmpFileManager']->createFile('downloads/');
//    $fileMetadata = $app['user.dboxClient']->getFile($path, $cachedFileHandle);
//    fclose($cachedFileHandle);
//    return $app->sendFile($cachedFilePath);
})
->assert('path', '.+')
->bind('opds-download');

// display context menu at the folder chooser
$app->get('/folder-menu/{path}', function($path) use ($app) {
    return $app['twig']->render( 'folder-menu.html', [
        'path' => $path,
        'parentPath' => preg_replace('%/?[^/]*$%', '', $path)
    ] );
})
->value('path', '')
->assert('path', '.*');

// currently selected folder to share with opds
$app->get('/selected-folder', function() use ($app) {
    $user = $app['persi']->getCurrentUser();
    return $app['twig']->render( 'selected-folder.html', [
        'folder' => $user->booksFolder
    ] );
});

// update OPDS-shared folder
$app->get('/select-folder/{path}', function($path ) use ($app) {
    $user = $app['persi']->getCurrentUser();
    $user->booksFolder = '/' . $path;
    R::store($user);
    return new RedirectResponse( '/' );
})
->value('path', '')
->assert('path', '.*');

$app->match('/login', function(Request $request) use ($app) {
    $data = [];
    if ('POST' == $request->getMethod()) {
        $user = $app['persi']->getUserByCredentials($request->get('login'), $request->get('password'));
        if ($user) {
            $app['session']->set('user', $user->id);
            return new RedirectResponse('/');
        }
        $data['login'] = $request->get('login');
        $data['errors']['credentials'] = true;
    }
    return $app['twig']->render('login.html', $data);
})
->bind('login');

$app->match('/register', function(Request $request) use ($app) {
    $login = trim($request->get('login'));
    $data = ['login' => $login, 'password' => $request->get('<PASSWORD>')];
    if ('POST' == $request->getMethod()) {
        if ('' == $login) {
            $data['errors']['login']['empty'] = true;
        } elseif (R::findOne( 'user', ' email = ? ', [$login] )) {
            $data['errors']['login']['unique'] = true;
        }
        if('' == $request->get('password')) {
            $data['errors']['password']['empty'] = true;
        }
        if (empty($data['errors'])) {
            $newUser = R::dispense('user');
            $newUser->email = $request->get('login');
            $newUser->password = $request->get('password');
            $app['session']->set('user', R::store($newUser));
            return new RedirectResponse('/');
        }
    }
    return $app['twig']->render('register.html', $data);
})
->bind('register');

$app->get('/logout', function() use ($app) {
    $app['session']->set('user', null);
    return new RedirectResponse('/');
})
->bind('logout');

$app->get('/navigation', function() use($app) {
    return $app['twig']->render('navigation.html');
});

// handling errors
$app->error(function (\Exception $e, $code) use ($app) {
    if ($app['debug']) {
        return;
    }

    // 404.html, or 40x.html, or 4xx.html, or error.html
    $templates = array(
        'errors/'.$code.'.html',
        'errors/'.substr($code, 0, 2).'x.html',
        'errors/'.substr($code, 0, 1).'xx.html',
        'errors/default.html',
    );

    return new Response($app['twig']->resolveTemplate($templates)->render(array('code' => $code)), $code);
});

$app->finish(function (Request $request, Response $response) use ($app) {
    $app['tmpFileManager']->cleanup();
});

$app->before(function(Request $request) use ($app) {
    $route = $app['request']->attributes->get('_route');
    $publicRoutes = ['login', 'register', 'logout'];
    $basicHttpAuthRoutes = ['opds', 'opds-download'];
    if (in_array($route, $basicHttpAuthRoutes)) {
        $user = $app['persi']->authBasicHttp($request);
        if (!$user) {
            return new Response('Not Authorized', 401, ['WWW-Authenticate' => 'Basic realm="Bookbox OPDS"']);
        }
        $app['persi']->setCurrentUserId($user->id);
    }
    else {
        if (!in_array($route, $publicRoutes) && !$app['session']->get('user')) {
            return new RedirectResponse('/login');
        }
    }
});<file_sep>/src/Personalizer.php
<?php

use Symfony\Component\HttpFoundation\Request;

class Personalizer {

    private $userId;

    public function setCurrentUserId($userId) {
        $this->userId = $userId;
    }

    public function getCurrentUser() {
        if (!$this->userId) {
            return null;
        }
        return R::load( 'user', $this->userId );
    }

    public function getUserByCredentials($login, $password) {
        if (empty($login) || empty($password)) {
            return NULL;
        }
        // check against database
        $user = R::findOne( 'user', ' email = ? ', [$login] );
        if (!$user || $user->password != $password) {
            return NULL;
        }
        return $user;
    }

    public function authBasicHttp(Request $request) {
        // get username and pass from headers
        $credentials = preg_replace('/^Basic /i', '', $request->headers->get('Authorization'));
        $credentials = base64_decode($credentials);
        // Notice: Undefined offset: 1
        @list($userName, $pass) = explode(':', $credentials, 2);
        return $this->getUserByCredentials($userName, $pass);
    }
} <file_sep>/src/TmpFileManager.php
<?php

class TmpFileManager {
    private $tmpDir;
    private $createdFiles = array();

    public function __construct($tmpDir) {
        $this->tmpDir = rtrim($tmpDir, '/') . '/';
    }

    public function createFile($subpath, $prefix='tmp_') {
        $path = tempnam($this->tmpDir . $subpath, $prefix);
        $handle = fopen($path, 'w+b');
        $this->createdFiles[$path] = $handle;
        return [$path, $handle];
    }

    public function cleanup() {
        foreach($this->createdFiles as $path => $handle) {
            @fclose($handle);
            @unlink($path);
        }
    }
} <file_sep>/README.md
book-box
========

Silex-powered service which allows you to access selected dropbox folder over OPDS (password-protected)
