diff --git a/app/bootstrap.php.d/60-api.php b/app/bootstrap.php.d/60-api.php index 4892f39..d630058 100644 --- a/app/bootstrap.php.d/60-api.php +++ b/app/bootstrap.php.d/60-api.php @@ -3,5 +3,11 @@ use Gist\Api\Client; $app['api_client'] = $app->share(function ($app) { - return new Client(['base_uri' => rtrim($app['settings']['api']['base_url'], '/')]); + $client = new Client(['base_uri' => rtrim($app['settings']['api']['base_url'], '/')]); + + if (!empty($app['settings']['api']['client']['api_key'])) { + $client->setApiKey($app['settings']['api']['client']['api_key']); + } + + return $client; }); diff --git a/app/config/config.yml.dist b/app/config/config.yml.dist index b8af280..c9845ec 100644 --- a/app/config/config.yml.dist +++ b/app/config/config.yml.dist @@ -6,7 +6,11 @@ security: login_required_to_view_gist: false login_required_to_view_embeded_gist: false api: + enabled: true + api_key_required: false base_url: 'https://gist.deblan.org/' + client: + api_key: data: path: data/git git: diff --git a/app/config/routing.yml b/app/config/routing.yml index 666ca81..49ad3f4 100644 --- a/app/config/routing.yml +++ b/app/config/routing.yml @@ -57,10 +57,18 @@ revisions: path: /revs/{gist} defaults: {_controller: Gist\Controller\ViewController::revisionsAction, _locale: en} +api_list: + path: /api/list/{apiKey} + defaults: {_controller: Gist\Controller\ApiController::listAction, _locale: en, apiKey: null} + api_create: - path: /api/create - defaults: {_controller: Gist\Controller\ApiController::createAction, _locale: en} + path: /api/create/{apiKey} + defaults: {_controller: Gist\Controller\ApiController::createAction, _locale: en, apiKey: null} api_update: - path: /api/update/{gist} - defaults: {_controller: Gist\Controller\ApiController::updateAction, _locale: en} + path: /api/update/{gist}/{apiKey} + defaults: {_controller: Gist\Controller\ApiController::updateAction, _locale: en, apiKey: null} + +api_delete: + path: /api/delete/{gist}/{apiKey} + defaults: {_controller: Gist\Controller\ApiController::deleteAction, _locale: en, apiKey: null} diff --git a/app/console b/app/console index eef6670..b1e12fb 100755 --- a/app/console +++ b/app/console @@ -2,15 +2,19 @@ add(new CreateCommand()); +$app['console']->add(new ListCommand()); $app['console']->add(new UpdateCommand()); +$app['console']->add(new DeleteCommand()); $app['console']->add(new StatsCommand()); $app['console']->add(new UserCreateCommand()); $app['console']->add(new UpgradeTo1p4p1Command()); diff --git a/app/locales/cn.yml b/app/locales/cn.yml index 30c5aa7..adfe2d8 100644 --- a/app/locales/cn.yml +++ b/app/locales/cn.yml @@ -20,6 +20,11 @@ app: my: title: '我的 Gist' nothing: '这家伙很懒,暂时没有内容' + api: + title: 'API' + warning: 'Keep it secret!' + form: + generate: 'Regenerate' gist: untitled: '未命名' diff --git a/app/locales/de.yml b/app/locales/de.yml index 42cc012..c91934d 100644 --- a/app/locales/de.yml +++ b/app/locales/de.yml @@ -20,6 +20,11 @@ app: my: title: 'Meine Gists' nothing: 'Nichts zu finden (momentan)!' + api: + title: 'API' + warning: 'Keep it secret!' + form: + generate: 'Regenerate' gist: untitled: 'Ohne Titel' diff --git a/app/locales/en.yml b/app/locales/en.yml index 51630d3..ff80822 100644 --- a/app/locales/en.yml +++ b/app/locales/en.yml @@ -20,6 +20,12 @@ app: my: title: 'My gists' nothing: 'Nothing yet!' + api: + title: 'API' + warning: 'Keep it secret!' + form: + generate: 'Regenerate' + gist: untitled: 'Untitled' diff --git a/app/locales/es.yml b/app/locales/es.yml index 75cfe90..2d8176b 100644 --- a/app/locales/es.yml +++ b/app/locales/es.yml @@ -20,6 +20,11 @@ app: my: title: 'Mis Gists' nothing: 'Nada por ahora.' + api: + title: 'API' + warning: 'Keep it secret!' + form: + generate: 'Regenerate' gist: untitled: 'Sin título' diff --git a/app/locales/fr.yml b/app/locales/fr.yml index 7674d1b..8741cf5 100644 --- a/app/locales/fr.yml +++ b/app/locales/fr.yml @@ -20,6 +20,11 @@ app: my: title: 'Mes Gists' nothing: 'Rien pour le moment !' + api: + title: 'API' + warning: 'Gardez-la secrète !' + form: + generate: 'Regénérer' gist: untitled: 'Sans titre' diff --git a/src/Gist/Api/Client.php b/src/Gist/Api/Client.php index 29c26a0..82ed6cd 100644 --- a/src/Gist/Api/Client.php +++ b/src/Gist/Api/Client.php @@ -12,31 +12,53 @@ use GuzzleHttp\Client as BaseClient; class Client extends BaseClient { /** - * URI of creation - * + * URI of creation. + * * @const string */ const CREATE = '/en/api/create'; /** - * URI of updating + * URI of update. * * @const string */ const UPDATE = '/en/api/update/{gist}'; /** - * Creates a gist + * URI of delete. * - * @param string $title The title - * @param string $type The type + * @const string + */ + const DELETE = '/en/api/delete/{gist}'; + + /** + * URI of list. + * + * @const string + */ + const LIST = '/en/api/list'; + + /** + * The API key. + * + * @var string|null + */ + protected $apiKey; + + /** + * Creates a gist. + * + * @param string $title The title + * @param string $type The type * @param string $content The content + * * @return array */ public function create($title, $type, $content) { $response = $this->post( - self::CREATE, + $this->mergeApiKey(self::CREATE), array( 'form_params' => array( 'form' => array( @@ -56,9 +78,9 @@ class Client extends BaseClient } /** - * Clones and update a gist + * Clones and update a gist. * - * @param string $gist Gist's ID + * @param string $gist Gist's ID * @param string $content The content * * @return array @@ -66,7 +88,7 @@ class Client extends BaseClient public function update($gist, $content) { $response = $this->post( - str_replace('{gist}', $gist, self::UPDATE), + str_replace('{gist}', $gist, $this->mergeApiKey(self::LIST)), array( 'form_params' => array( 'form' => array( @@ -82,4 +104,81 @@ class Client extends BaseClient return []; } + + /** + * Deletes a gist. + * + * @param string $gist Gist's ID + * + * @return array + */ + public function delete($gist) + { + $response = $this->post(str_replace('{gist}', $gist, $this->mergeApiKey(self::DELETE))); + + if ($response->getStatusCode() === 200) { + return json_decode($response->getBody()->getContents(), true); + } + + return []; + } + + /** + * Lists the user's gists. + * + * @param string $gist Gist's ID + * @param string $content The content + * + * @return array + */ + public function list() + { + $response = $this->get($this->mergeApiKey(self::LIST)); + + if ($response->getStatusCode() === 200) { + return json_decode($response->getBody()->getContents(), true); + } + + return []; + } + + /* + * Merges the API key with the given url. + * + * @param string $url + * + * @return string + */ + public function mergeApiKey($url) + { + if (empty($this->apiKey)) { + return $url; + } + + return rtrim($url, '/').'/'.$this->apiKey; + } + + /* + * Set the value of "apiKey". + * + * @param string|null $apiKey + * + * @return Client + */ + public function setApiKey($apiKey) + { + $this->apiKey = $apiKey; + + return $this; + } + + /* + * Get the value of "apiKey". + * + * @return string|null + */ + public function getApiKey() + { + return $this->apiKey; + } } diff --git a/src/Gist/Command/CreateCommand.php b/src/Gist/Command/CreateCommand.php index 7aa7027..14fdd46 100644 --- a/src/Gist/Command/CreateCommand.php +++ b/src/Gist/Command/CreateCommand.php @@ -7,6 +7,8 @@ use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputOption; +use Propel\Runtime\Parser\YamlParser; +use Symfony\Component\Yaml\Yaml; /** * class CreateCommand. @@ -27,8 +29,9 @@ class CreateCommand extends Command ->addArgument('input', InputArgument::REQUIRED, 'Input') ->addArgument('type', InputArgument::OPTIONAL, 'Type', 'text') ->addOption('title', 't', InputOption::VALUE_REQUIRED, 'Title of the gist') - ->addOption('show-url', 'u', InputOption::VALUE_NONE, 'Display only the gist url') - ->addOption('show-id', 'i', InputOption::VALUE_NONE, 'Display only the gist Id') + ->addOption('all', 'a', InputOption::VALUE_NONE, 'Display all the response') + ->addOption('id', 'i', InputOption::VALUE_NONE, 'Display only the id of the gist') + ->addOption('json', 'j', InputOption::VALUE_NONE, 'Format the response to json') ->setHelp(<<--title, -t Defines a title - - --show-id, -i - Display only the Id of the gist - --show-url, -u - Display only the url of the gist + --id, -i + Display only the id of the gist + + --all, -a + Display all the response + + --json, -j + Format the response to json EOF ); } @@ -58,11 +64,10 @@ EOF */ protected function execute(InputInterface $input, OutputInterface $output) { - //$output->writeln(sprintf('%s bar.', 'test')); - $file = $input->getArgument('input'); $type = $input->getArgument('type'); $title = $input->getOption('title'); + $json = $input->getOption('json'); if ($file === '-') { $content = file_get_contents('php://stdin'); @@ -94,19 +99,17 @@ EOF $gist = $this->getSilexApplication()['api_client']->create($title, $type, $content); - if ($input->getOption('show-url')) { - $output->writeln($gist['url']); - - return true; + if ($input->getOption('id')) { + $id = isset($gist['gist']['id']) ? $gist['gist']['id'] : $gist['gist']['Id']; + $result = $json ? json_encode(array('id' => $id)) : $id; + } elseif ($input->getOption('all')) { + $result = $json ? json_encode($gist) : Yaml::dump($gist); + } else { + $url = $gist['url']; + $result = $json ? json_encode(array('url' => $url)) : $url; } - if ($input->getOption('show-id')) { - $output->writeln($gist['gist']['Id']); - - return true; - } - - $output->writeln(json_encode($gist)); + $output->writeln($result); } /** diff --git a/src/Gist/Command/DeleteCommand.php b/src/Gist/Command/DeleteCommand.php new file mode 100644 index 0000000..ec0ca31 --- /dev/null +++ b/src/Gist/Command/DeleteCommand.php @@ -0,0 +1,48 @@ + + */ +class DeleteCommand extends Command +{ + /** + * {@inheritdoc} + */ + protected function configure() + { + $this + ->setName('delete') + ->setDescription('Delete a gist using the API') + ->addOption('gist', null, InputOption::VALUE_REQUIRED, 'Id or File of the gist') + ->setHelp(<<<'EOF' +Provides a client to delete a gist using the API. + +Arguments: + none. + +Options: + --gist + Defines the Gist to delete by using its Id or its File +EOF + ); + } + + /** + * {@inheritdoc} + */ + protected function execute(InputInterface $input, OutputInterface $output) + { + $result = $this->getSilexApplication()['api_client']->delete($input->getOption('gist')); + + $output->writeln(empty($result['error']) ? 'OK' : 'An error occured.'); + } +} diff --git a/src/Gist/Command/ListCommand.php b/src/Gist/Command/ListCommand.php new file mode 100644 index 0000000..878b443 --- /dev/null +++ b/src/Gist/Command/ListCommand.php @@ -0,0 +1,59 @@ + + */ +class ListCommand extends Command +{ + /** + * {@inheritdoc} + */ + protected function configure() + { + $this + ->setName('gists') + ->setDescription('List gists using the API'); + } + + /** + * {@inheritdoc} + */ + protected function execute(InputInterface $input, OutputInterface $output) + { + $gists = $this->getSilexApplication()['api_client']->list(); + $rows = []; + + foreach ($gists as $gist) { + $rows[] = array( + $gist['id'], + $gist['title'], + $gist['cipher'] ? 'y' : 'n', + $gist['type'], + (new DateTime($gist['createdAt']))->format('Y-m-d H:i:s'), + (new DateTime($gist['updatedAt']))->format('Y-m-d H:i:s'), + $gist['url'], + ); + } + + $table = new Table($output); + $table + ->setHeaders(array('ID', 'Title', 'Cipher', 'Type', 'Created At', 'Updated At', 'url')) + ->setRows($rows); + + $table->render(); + } +} diff --git a/src/Gist/Command/UpdateCommand.php b/src/Gist/Command/UpdateCommand.php index ef09a00..a48ec14 100644 --- a/src/Gist/Command/UpdateCommand.php +++ b/src/Gist/Command/UpdateCommand.php @@ -7,6 +7,7 @@ use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputOption; +use Symfony\Component\Yaml\Yaml; /** * class UpdateCommand. @@ -26,8 +27,9 @@ class UpdateCommand extends Command ->setDescription('Update a gist using the API') ->addArgument('input', InputArgument::REQUIRED, 'Input') ->addOption('gist', null, InputOption::VALUE_REQUIRED, 'Id or File of the gist') - ->addOption('show-url', 'u', InputOption::VALUE_NONE, 'Display only the gist url') - ->addOption('show-id', 'i', InputOption::VALUE_NONE, 'Display only the gist Id') + ->addOption('all', 'a', InputOption::VALUE_NONE, 'Display all the response') + ->addOption('id', 'i', InputOption::VALUE_NONE, 'Display only the id of the gist') + ->addOption('json', 'j', InputOption::VALUE_NONE, 'Format the response to json') ->setHelp(<<--gist Defines the Gist to update by using its Id or its File - --show-id, -i - Display only the Id of the gist + --id, -i + Display only the id of the gist - --show-url, -u - Display only the url of the gist + --all, -a + Display all the response + + --json, -j + Format the response to json EOF ); } @@ -57,10 +62,9 @@ EOF */ protected function execute(InputInterface $input, OutputInterface $output) { - //$output->writeln(sprintf('%s bar.', 'test')); - $file = $input->getArgument('input'); $gist = $input->getOption('gist'); + $json = $input->getOption('json'); if ($file === '-') { $content = file_get_contents('php://stdin'); @@ -86,19 +90,17 @@ EOF $gist = $this->getSilexApplication()['api_client']->update($gist, $content); - if ($input->getOption('show-url')) { - $output->writeln($gist['url']); - - return true; + if ($input->getOption('id')) { + $id = isset($gist['gist']['id']) ? $gist['gist']['id'] : $gist['gist']['Id']; + $result = $json ? json_encode(array('id' => $id)) : $id; + } elseif ($input->getOption('all')) { + $result = $json ? json_encode($gist) : Yaml::dump($gist); + } else { + $url = $gist['url']; + $result = $json ? json_encode(array('url' => $url)) : $url; } - if ($input->getOption('show-id')) { - $output->writeln($gist['gist']['Id']); - - return true; - } - - $output->writeln(json_encode($gist)); + $output->writeln($result); } /** diff --git a/src/Gist/Controller/ApiController.php b/src/Gist/Controller/ApiController.php index 5b86bbe..842bf99 100644 --- a/src/Gist/Controller/ApiController.php +++ b/src/Gist/Controller/ApiController.php @@ -8,6 +8,8 @@ use Symfony\Component\HttpFoundation\JsonResponse; use Gist\Form\ApiCreateGistForm; use Gist\Model\GistQuery; use Gist\Form\ApiUpdateGistForm; +use GitWrapper\GitException; +use Gist\Model\UserQuery; /** * Class ApiController. @@ -16,10 +18,75 @@ use Gist\Form\ApiUpdateGistForm; */ class ApiController extends Controller { - public function createAction(Request $request) + /** + * Lists gists. + * + * @param Request $request + * @param string $apiKey + * + * @return JsonResponse + */ + public function listAction(Request $request, $apiKey) { $app = $this->getApp(); + if (false === $app['settings']['api']['enabled']) { + return new Response('', 403); + } + + if (false === $this->isValidApiKey($apiKey, true)) { + return $this->invalidApiKeyResponse(); + } + + if (false === $request->isMethod('get')) { + return $this->invalidMethodResponse('GET method is required.'); + } + + $user = $app['user.provider']->loadUserByApiKey($apiKey); + $gists = $user->getGists(); + $data = array(); + + foreach ($gists as $gist) { + try { + $history = $app['gist']->getHistory($gist); + + $value = $gist->toArray(); + $value['url'] = $request->getSchemeAndHttpHost().$app['url_generator']->generate( + 'view', + array( + 'gist' => $gist->getFile(), + 'commit' => array_pop($history)['commit'], + ) + ); + + $data[] = $value; + } catch (GitException $e) { + } + } + + return new JsonResponse($data); + } + + /** + * Creates a gist. + * + * @param Request $request + * @param string $apiKey + * + * @return JsonResponse + */ + public function createAction(Request $request, $apiKey) + { + $app = $this->getApp(); + + if (false === $app['settings']['api']['enabled']) { + return new Response('', 403); + } + + if (false === $this->isValidApiKey($apiKey, (bool) $app['settings']['api']['api_key_required'])) { + return $this->invalidApiKeyResponse(); + } + if (false === $request->isMethod('post')) { return $this->invalidMethodResponse('POST method is required.'); } @@ -36,30 +103,51 @@ class ApiController extends Controller $form->submit($request); if ($form->isValid()) { + $user = !empty($apiKey) ? $app['user.provider']->loadUserByApiKey($apiKey) : null; $gist = $app['gist']->create(new Gist(), $form->getData()); - $gist->setCipher(false)->save(); + $gist + ->setCipher(false) + ->setUser($user) + ->save(); $history = $app['gist']->getHistory($gist); - return new JsonResponse(array( - 'url' => $request->getSchemeAndHttpHost().$app['url_generator']->generate( - 'view', - array( - 'gist' => $gist->getFile(), - 'commit' => array_pop($history)['commit'], - ) - ), - 'gist' => $gist->toArray(), - )); + $data = $gist->toArray(); + $data['url'] = $request->getSchemeAndHttpHost().$app['url_generator']->generate( + 'view', + array( + 'gist' => $gist->getFile(), + 'commit' => array_pop($history)['commit'], + ) + ); + + return new JsonResponse($data); } return $this->invalidRequestResponse('Invalid field(s)'); } - public function updateAction(Request $request, $gist) + /** + * Updates a gist. + * + * @param Request $request + * @param string $gist + * @param string $apiKey + * + * @return JsonResponse + */ + public function updateAction(Request $request, $gist, $apiKey) { $app = $this->getApp(); + if (false === $app['settings']['api']['enabled']) { + return new Response('', 403); + } + + if (false === $this->isValidApiKey($apiKey, (bool) $app['settings']['api']['api_key_required'])) { + return $this->invalidApiKeyResponse(); + } + if (false === $request->isMethod('post')) { return $this->invalidMethodResponse('POST method is required.'); } @@ -91,21 +179,88 @@ class ApiController extends Controller $history = $app['gist']->getHistory($gist); - return new JsonResponse(array( - 'url' => $request->getSchemeAndHttpHost().$app['url_generator']->generate( - 'view', - array( - 'gist' => $gist->getFile(), - 'commit' => array_pop($history)['commit'], - ) - ), - 'gist' => $gist->toArray(), - )); + $data = $gist->toArray(); + $data['url'] = $request->getSchemeAndHttpHost().$app['url_generator']->generate( + 'view', + array( + 'gist' => $gist->getFile(), + 'commit' => array_pop($history)['commit'], + ) + ); + + return new JsonResponse($data); } return $this->invalidRequestResponse('Invalid field(s)'); } + /** + * Deletes a gist. + * + * @param Request $request + * @param string $gist + * @param string $apiKey + * + * @return JsonResponse + */ + public function deleteAction(Request $request, $gist, $apiKey) + { + $app = $this->getApp(); + + if (false === $app['settings']['api']['enabled']) { + return new Response('', 403); + } + + if (false === $this->isValidApiKey($apiKey, true)) { + return $this->invalidApiKeyResponse(); + } + + if (false === $request->isMethod('post')) { + return $this->invalidMethodResponse('POST method is required.'); + } + + $user = $app['user.provider']->loadUserByApiKey($apiKey); + + $gist = GistQuery::create() + ->filterById((int) $gist) + ->_or() + ->filterByFile($gist) + ->filterByUser($user) + ->findOne(); + + if (!$gist) { + return $this->invalidRequestResponse('Invalid Gist'); + } + + $gist->delete(); + + return new JsonResponse(['error' => false]); + } + + /** + * Builds an invalid api key response. + * + * @param mixed $message + * + * @return JsonResponse + */ + protected function invalidApiKeyResponse() + { + $data = [ + 'error' => ' Unauthorized', + 'message' => 'Invalid API KEY', + ]; + + return new JsonResponse($data, 401); + } + + /** + * Builds an invalid method response. + * + * @param mixed $message + * + * @return JsonResponse + */ protected function invalidMethodResponse($message = null) { $data = [ @@ -116,6 +271,13 @@ class ApiController extends Controller return new JsonResponse($data, 405); } + /** + * Builds an invalid request response. + * + * @param mixed $message + * + * @return JsonResponse + */ protected function invalidRequestResponse($message = null) { $data = [ @@ -125,4 +287,24 @@ class ApiController extends Controller return new JsonResponse($data, 400); } + + /** + * Checks if the given api key is valid + * depending of the requirement. + * + * @param mixed $apiKey + * @param mixed $required + * + * @return bool + */ + protected function isValidApiKey($apiKey, $required = false) + { + if (empty($apiKey)) { + return !$required; + } + + return UserQuery::create() + ->filterByApiKey($apiKey) + ->count() === 1; + } } diff --git a/src/Gist/Controller/Controller.php b/src/Gist/Controller/Controller.php index c96d8d0..e166f39 100644 --- a/src/Gist/Controller/Controller.php +++ b/src/Gist/Controller/Controller.php @@ -13,7 +13,7 @@ use Symfony\Component\HttpFoundation\Response; * * @author Simon Vieille */ -class Controller +abstract class Controller { /** * @var Application @@ -128,12 +128,18 @@ class Controller /** * Returns the connected user. * + * @param Request $request An API request + * * @return mixed */ - public function getUser() + public function getUser(Request $request = null) { $app = $this->getApp(); + if (!empty($request)) { + + } + $securityContext = $app['security.token_storage']; $securityToken = $securityContext->getToken(); diff --git a/src/Gist/Controller/MyController.php b/src/Gist/Controller/MyController.php index aa113c8..d59f315 100644 --- a/src/Gist/Controller/MyController.php +++ b/src/Gist/Controller/MyController.php @@ -58,6 +58,26 @@ class MyController extends Controller $gists = $this->getUser()->getGistsPager($page, $options); + $apiKey = $this->getUser()->getApiKey(); + + if (empty($apiKey)) { + $regenerateApiKey = true; + } + // FIXME: CSRF issue! + elseif ($request->request->get('apiKey') === $apiKey && $request->request->has('generateApiKey')) { + $regenerateApiKey = true; + } else { + $regenerateApiKey = false; + } + + if ($regenerateApiKey) { + $apiKey = $app['salt_generator']->generate(32, true); + + $this->getUser() + ->setApiKey($apiKey) + ->save(); + } + if ($request->isMethod('post')) { $deleteForm->handleRequest($request); $passwordForm->handleRequest($request); @@ -104,6 +124,7 @@ class MyController extends Controller array( 'gists' => $gists, 'page' => $page, + 'apiKey' => $apiKey, 'deleteForm' => $deleteForm->createView(), 'filterForm' => $filterForm->createView(), 'passwordForm' => $passwordForm->createView(), diff --git a/src/Gist/Form/ApiCreateGistForm.php b/src/Gist/Form/ApiCreateGistForm.php index 27f3e4b..436f78a 100644 --- a/src/Gist/Form/ApiCreateGistForm.php +++ b/src/Gist/Form/ApiCreateGistForm.php @@ -16,7 +16,9 @@ class ApiCreateGistForm extends CreateGistForm { parent::build($options); - $this->builder->remove('cipher'); + $this->builder + ->remove('cipher') + ->remove('file'); return $this->builder; } diff --git a/src/Gist/Form/ApiUpdateGistForm.php b/src/Gist/Form/ApiUpdateGistForm.php index 2594d59..bc058e3 100644 --- a/src/Gist/Form/ApiUpdateGistForm.php +++ b/src/Gist/Form/ApiUpdateGistForm.php @@ -18,6 +18,7 @@ class ApiUpdateGistForm extends ApiCreateGistForm $this->builder ->remove('title') + ->remove('file') ->remove('type'); return $this->builder; diff --git a/src/Gist/Model/Gist.php b/src/Gist/Model/Gist.php index 23a4809..86efe42 100644 --- a/src/Gist/Model/Gist.php +++ b/src/Gist/Model/Gist.php @@ -3,6 +3,7 @@ namespace Gist\Model; use Gist\Model\Base\Gist as BaseGist; +use Propel\Runtime\Map\TableMap; /** * Class Gist. @@ -86,4 +87,25 @@ class Gist extends BaseGist return $this; } + + /** + * {@inheritdoc} + */ + public function toArray($keyType = TableMap::TYPE_PHPNAME, $includeLazyLoadColumns = true, $alreadyDumpedObjects = array(), $includeForeignObjects = false) + { + $data = parent::toArray( + $keyType, + $includeLazyLoadColumns, + $alreadyDumpedObjects, + $includeForeignObjects + ); + + foreach ($data as $key => $value) { + $newKey = lcfirst($key); + unset($data[$key]); + $data[$newKey] = $value; + } + + return $data; + } } diff --git a/src/Gist/Model/User.php b/src/Gist/Model/User.php index e5d483b..b57d03e 100644 --- a/src/Gist/Model/User.php +++ b/src/Gist/Model/User.php @@ -54,7 +54,7 @@ class User extends BaseUser implements UserInterface * * @return Propel\Runtime\Util\PropelModelPager */ - public function getGistsPager($page, $options = array(), $maxPerPage = 10) + public function getGistsPager($page, $options = array(), $maxPerPage = 10) { $query = GistQuery::create() ->filterByUser($this) @@ -63,11 +63,11 @@ class User extends BaseUser implements UserInterface if (!empty($options['type']) && $options['type'] !== 'all') { $query->filterByType($options['type']); } - + if (!empty($options['title'])) { $query->filterByTitle('%'.$options['title'].'%', Criteria::LIKE); } - + if (!empty($options['cipher']) && $options['cipher'] !== 'anyway') { $bools = array( 'yes' => true, diff --git a/src/Gist/Resources/config/propel/schema.xml b/src/Gist/Resources/config/propel/schema.xml index ce79ebb..8caac4e 100644 --- a/src/Gist/Resources/config/propel/schema.xml +++ b/src/Gist/Resources/config/propel/schema.xml @@ -22,6 +22,7 @@ + diff --git a/src/Gist/Resources/views/My/my.html.twig b/src/Gist/Resources/views/My/my.html.twig index 2e65a3b..9ccbbc7 100644 --- a/src/Gist/Resources/views/My/my.html.twig +++ b/src/Gist/Resources/views/My/my.html.twig @@ -205,30 +205,61 @@ -
-
- {{ 'login.login.form.password.placeholder'|trans }} -
-
-
-
-

- {{ form_errors(passwordForm.currentPassword) }} - {{ form_widget(passwordForm.currentPassword) }} -

+ {% set apiEnabled = app.settings.api.enabled %} -

- {{ form_errors(passwordForm.newPassword) }} - {{ form_widget(passwordForm.newPassword) }} -

+
+
+
+
+ {{ 'login.login.form.password.placeholder'|trans }} +
+
+
+ +

+ {{ form_errors(passwordForm.currentPassword) }} + {{ form_widget(passwordForm.currentPassword) }} +

-

- {{ form_rest(passwordForm) }} - -

- +

+ {{ form_errors(passwordForm.newPassword) }} + {{ form_widget(passwordForm.newPassword) }} +

+ +

+ {{ form_rest(passwordForm) }} + +

+ +
+
+ {% if apiEnabled %} +
+
+
+ {{ 'my.api.title'|trans }} +
+
+
+

{{ 'my.api.warning'|trans|raw }}

+ +
+
+

+ +

+

+ +

+
+
+
+
+
+
+ {% endif %}
diff --git a/src/Gist/Service/SaltGenerator.php b/src/Gist/Service/SaltGenerator.php index f86a0e0..a3143c2 100644 --- a/src/Gist/Service/SaltGenerator.php +++ b/src/Gist/Service/SaltGenerator.php @@ -18,18 +18,30 @@ class SaltGenerator * * @return string */ - public function generate($length = 32) + public function generate($length = 32, $isApiKey = false) { if (!is_numeric($length)) { throw new InvalidArgumentException('Paramter length must be a valid integer.'); } if (function_exists('openssl_random_pseudo_bytes')) { - return substr(base64_encode(openssl_random_pseudo_bytes($length)), 0, $length); + $string = base64_encode(openssl_random_pseudo_bytes(256)); } if (function_exists('mcrypt_create_iv')) { - return substr(base64_encode(mcrypt_create_iv($length, MCRYPT_DEV_URANDOM)), 0, $length); + $string = base64_encode(mcrypt_create_iv(256, MCRYPT_DEV_URANDOM)); + } + + if (!empty($string)) { + if (true === $isApiKey) { + $string = str_replace( + array('+', '%', '/', '#', '&'), + '', + $string + ); + } + + return substr($string, 0, $length); } throw new RuntimeException('You must enable openssl or mcrypt modules.'); diff --git a/src/Gist/Service/UserProvider.php b/src/Gist/Service/UserProvider.php index beab8e1..e887b50 100644 --- a/src/Gist/Service/UserProvider.php +++ b/src/Gist/Service/UserProvider.php @@ -126,6 +126,7 @@ class UserProvider implements UserProviderInterface $user ->setRoles('ROLE_USER') ->setPassword($this->encoder->encodePassword($password, $user->getSalt())) + ->setApiKey($this->saltGenerator->generate(32, true)) ->save(); return $user; @@ -166,6 +167,20 @@ class UserProvider implements UserProviderInterface return $user; } + /** + * Loads a user by his api key. + * + * @param string $apiKey + * + * @return User + */ + public function loadUserByApiKey($apiKey) + { + $user = UserQuery::create()->findOneByApiKey($apiKey); + + return $user; + } + /* * Checks if the given password is the current user password. * diff --git a/web/app/js/app.js b/web/app/js/app.js index a4d64d4..671d102 100644 --- a/web/app/js/app.js +++ b/web/app/js/app.js @@ -98,6 +98,10 @@ var myEvents = function() { $('#form-deletion form').submit(); } }); + + $(document).on('change keyup keydown', '#form-api-key', function() { + $(this).val($(this).data('key')); + }); } var mainEditorEvents = function() {