diff --git a/.gitignore b/.gitignore index f5eab43..2f75ca1 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,17 @@ -vendor/* -resources/cryptopaste.db -config.ini +/.web-server-pid +/app/config/parameters.yml +/build/ +/phpunit.xml +/var/* +!/var/cache +/var/cache/* +!var/cache/.gitkeep +!/var/logs +/var/logs/* +!var/logs/.gitkeep +!/var/sessions +/var/sessions/* +!var/sessions/.gitkeep +!var/SymfonyRequirements.php +/vendor/ +/web/bundles/ diff --git a/.travis.yml b/.travis.yml index a24f319..ddaff9d 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,6 +3,7 @@ language: php php: - '7.0' - '7.1' + - '7.2' before_script: - composer self-update diff --git a/CHANGELOG.md b/CHANGELOG.md index ae0e9dd..ae3ec3a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,12 @@ All notable changes to this project will be documented in this file. Versions fo ### Added - Nothing yet, submit Pull Requests! +## [1.0.0] - 2018-09-xx - First release "[Rijmen](https://en.wikipedia.org/wiki/Vincent_Rijmen)" +### Changed +- Completely recoded everything in the Symfony 3.4 framework +- Fixed opened bugs +- Added functionality that confirms loading a burn-after-reading paste (prevents social media or messenger app pre-loads from deleting them) + ## [0.1.0] - 2017-06-11 - Initial Beta "[Daemen](https://en.wikipedia.org/wiki/Joan_Daemen)" ### Added - Initial Silex structure, controllers, and Doctrine DB interfaces diff --git a/README.md b/README.md index e57e437..62cdd84 100644 --- a/README.md +++ b/README.md @@ -1,35 +1,30 @@ - +

CryptoPaste

A secure, browser-side encrypted pastebin. -[![Build Status](https://travis-ci.org/HackThisCode/CryptoPaste.svg?branch=master)](https://travis-ci.org/HackThisCode/CryptoPaste) -[![Dependency Status](https://www.versioneye.com/user/projects/59f514dd2de28c1e03d950b2/badge.svg?style=flat-square)](https://www.versioneye.com/user/projects/59f514dd2de28c1e03d950b2) +[![Build Status](https://travis-ci.org/HackThisSite/CryptoPaste.svg?branch=symfony)](https://travis-ci.org/HackThisSite/CryptoPaste) # About -CryptoPaste is a secure pastebin service inspired by [CryptoBin](https://cryptobin.org). Like CryptoBin, CryptoPaste strives to be a secure, stable and clean pastebin service, especially now that CryptoBin has seemingly shut its doors indefinitely. The goal is to perform all encryption, decryption, and data handling in the user's browser so that the CryptoPaste host has both plausible deniability and the inability to comply with court orders or takedown requests. +CryptoPaste is a secure pastebin service inspired by [CryptoBin](https://cryptobin.org). Like CryptoBin, CryptoPaste strives to be a secure, stable and clean pastebin service, especially now that CryptoBin has seemingly shut its doors indefinitely. The goal is to perform all encryption, decryption, and data handling in the user's browser so that the CryptoPaste host has both plausible deniability and the inability to comply with court orders or takedown requests. CryptoPaste is a [HackThisSite](https://www.hackthissite.org) project. # Features -- Pastes are encrypted before being sent to the server +- Pastes are encrypted in-browser before being sent to the server - No passwords stored - All identifying information is anonymized - Expired content is deleted forever -- CRON for enforced expiration +- Web cron available +- Template override supported # Demonstration An active demonstration of CryptoPaste can be found at https://cryptopaste.org -# TODO -- (**Need Help!**) Write legitimate testing -- (**Need Help!**) Tidy up all code -- (**Need Help!**) Fix the UI to have better responsive scaling and other improvements - # Prerequisites -- PHP 7 +- PHP >= 7.0 - Composer -- MySQL or SQLite +- MySQL / MariaDB # Install @@ -37,69 +32,200 @@ An active demonstration of CryptoPaste can be found at https://cryptopaste.org `$ composer install` -2. Install `resources/cryptopaste.mysql.sql` into your MySQL database and create a user with SELECT, INSERT, UPDATE, and DELETE grants - - For SQLite, use the `resources/cryptopaste.sqlite.sql` file +Because CryptoPaste uses the Symfony framework, Composer will automatically prompt you for configuration settings. Unfortunately this does not include definitions in the prompts, so please read the comments in `app/config/parameters.yml.dist` for an explanation of what the configuration settings are. -3. Copy `config.ini.example` to `config.ini` and edit the values +If you want to change the configuration settings later (i.e. changing to a DB user will limited permissions), they are saved in the `app/config/parameters.yml` file. -4. Edit your `nginx.conf` and add this to your `http` block: +2. Install the database using the following command: + +`$ php bin/console doctrine:migrations:migrate` + +This will install the database schema using the username and password you supplied in step 1. The database user you specified will need enough permissions to create the database and so forth. If you want to specify a different user to do this, edit the `app/config/parameters.yml` file. + +3. Edit your `nginx.conf` and make it look something like the following:
-    map $remote_addr $ip_anonym1 {
-     default 0.0.0;
-     "~(?P<ip>(\d+)\.(\d+)\.(\d+))\.\d+" $ip;
-     "~(?P<ip>[^:]+:[^:]+):" $ip;
-    }
-
-    map $remote_addr $ip_anonym2 {
-     default .0;
-     "~(?P<ip>(\d+)\.(\d+)\.(\d+))\.\d+" .0;
-     "~(?P<ip>[^:]+:[^:]+):" ::;
-    }
-
-    map $ip_anonym1$ip_anonym2 $ip_anonymized {
-     default 0.0.0.0;
-     "~(?P<ip>.*)" $ip;
-    }
-
-    log_format anonymized '$ip_anonymized - $remote_user [$time_local] ' 
-       '"$request" $status $body_bytes_sent ' 
-       '"$http_referer" "$http_user_agent"';
-
-    access_log /var/log/nginx/access.log anonymized;
+http {
+
+  # These maps anonymize IP addresses in nginx logs
+
+  map $remote_addr $ip_anonym1 {
+   default 0.0.0;
+   "~(?P<ip>(\d+)\.(\d+)\.(\d+))\.\d+" $ip;
+   "~(?P<ip>[^:]+:[^:]+):" $ip;
+  }
+
+  map $remote_addr $ip_anonym2 {
+   default .0;
+   "~(?P<ip>(\d+)\.(\d+)\.(\d+))\.\d+" .0;
+   "~(?P<ip>[^:]+:[^:]+):" ::;
+  }
+
+  map $ip_anonym1$ip_anonym2 $ip_anonymized {
+   default 0.0.0.0;
+   "~(?P<ip>.*)" $ip;
+  }
+
+  log_format anonymized '$ip_anonymized - $remote_user [$time_local] '
+     '"$request" $status $body_bytes_sent '
+     '"$http_referer" "$http_user_agent"';
+
+  access_log /var/log/nginx/access.log anonymized;
+}
+
+server {
+  listen 80;
+  server_name _;
+  access_log /var/log/nginx/access.log anonymized;
+
+  root /path/to/cryptopaste/web;
+
+  index app.php;
+  location ~ /\.ht {
+    deny  all;
+  }
+  location / {
+    try_files $uri /app.php$is_args$args;
+  }
+
+  location ~ ^/app\.php(/|$) {
+    fastcgi_pass unix:/var/run/php-fpm.sock;
+    fastcgi_split_path_info ^(.+\.php)(/.*)$;
+    include fastcgi_params;
+    fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
+    fastcgi_param PATH_INFO $fastcgi_script_name;
+    fastcgi_param SERVER_NAME $host;
+  }
+}
 
-5. In your `nginx.conf`, in the `server` block, this is all you need to run the CryptoPaste app: +4. If you chose not to enable the web cron, add an entry to your crontab to ensure deletion of expired pastes and sessions. Here is an example crontab entry that is run every hour as the `www-data` user:
-      location ~ /securimage/(images/.*|securimage(_play\.swf|\.js|\.css))$ {
-        try_files $uri $uri/ =404;
-        alias /var/www/cryptopaste/vendor/dapphp;
-      }
-
-      location / {
-        try_files $uri /index.php$is_args$args;
-      }
-
-      location ~ ^/index\.php(/|$) {
-        fastcgi_pass unix:/var/run/php-fpm.sock; # Change this to reflect how your PHP-FPM is running
-        fastcgi_split_path_info ^(.+\.php)(/.*)$;
-        include fastcgi_params;
-        fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
-        fastcgi_param PATH_INFO $fastcgi_script_name;
-        fastcgi_param SERVER_NAME $host;
-      }
+0	*	*	*	*	www-data	/usr/bin/php /path/to/cryptopaste/bin/console cron:run
 
-6. Add a CRON entry to force deletion of expired pastes. Here is an example crontab entry that is run every 5 minutes as the `www-data` user: +# Upgrade + +Because Silex is now end-of-life, CryptoPaste v1.0 is a complete recode in the Symfony 3.4 framework. This has resulted in some major changes, but your data should be portable from a v0.x version of CryptoPaste. + +## Warning about SQLite + +CryptoPaste v1.x no longer supports SQLite. If you were using SQLite, please convert it to a MySQL database, table name of `cryptopaste`, and follow the upgrade procedure below. + +## Upgrade from v0.1.x to v1.x + +1. Make sure to backup your database and your `config.ini` file! You will need the data from your `config.ini` in the next steps. + +2. Delete your old v0.x CryptoPaste installation, but do not delete your database! + +3. Follow **all** of the instructions for Installation above, including for cron jobs and nginx settings. When prompted for configuration settings during the `composer install` phase, use the values from your `config.ini`. You will need to provide MySQL credentials for a user that can create and alter tables. + +4. When you run the `bin/console doctrine:migrations:migrate` command from the Installation instructions, this will automatically convert your database for CryptoPaste v1.x. + +## Upgrade v1.x + +1. Make sure you backup your database and `app/config/parameters.yml` file! + +2. If you cloned this repository, `cd` into your CryptoPaste installation directory and run `git pull`. If not, you will need to clone or download this repository to a new directory, then move your old `app/config/parameters.yml` file and any custom templates into your that new directory. + +3. Next, run + +`$ composer install` + +Because CryptoPaste uses the Symfony framework, Composer will automatically prompt you for any new configuration settings. Unfortunately this does not include definitions in the prompts, so please read the comments in `app/config/parameters.yml.dist` for an explanation of what the configuration settings are. + +If you want to change the configuration settings later (i.e. changing to a DB user will limited permissions), they are saved in the `app/config/parameters.yml` file. + +4. Update the database using the following command: + +`$ php bin/console doctrine:migrations:migrate` + +This will update the database schema using the username and password specified in your `app/config/parameters.yml` file. The database user you specified will need enough permissions to modify the database and so forth. If you want to specify a different user to do this, edit the `app/config/parameters.yml` file. + +5. Clear the CryptoPaste application cache by running the following command: + +`$ php bin/console cache:clear` + +# Modify the template + +Note that all template files are written in [Twig 2.0](https://twig.symfony.com). You can find the documentation for it [here](https://twig.symfony.com/doc/2.x/). + +## Override default template files + +All default template files reside in `app/Resources/views/default/`. If you want to override any file here, simply copy the file to the `custom/` directory one level up and modify as needed. For example, if you want to override the FAQ page, you would copy `app/Resources/views/default/pages/faq.html.twig` to `app/Resources/views/custom/pages/faq.html.twig`. + +## Add custom static pages + +If you want to add custom static pages, i.e. a privacy policy or terms of service, you can do that quite easily as described below. + +### Custom page file + +First, create a file in `app/Resources/views/custom/pages/` that must end in `.html.twig`. The part before `.html.twig` will become the page *slug*, or identifier, used in the URL and menu. For example, if you create `app/Resources/views/custom/pages/privacy.html.twig`, then "privacy" becomes the slug. + +Two Twig settings must exist within a custom page: + +* **extends** - Must be set exactly as shown below in the example +* **content** block - The HTML to display + +There are also some optional settings: + +* **title** - Page title that shows up in the `` HTML tag +* **breadcrumb** - Breadcrumb that appears on the left side of the top menu bar. Hash-map of two values: + * **icon** - Fontawesome icon ([reference](https://fontawesome.com/icons)) + * **text** - Text to show in the breadcrumb +* **head** block - HTML content to put just before the `</head>` closing tag +* **footerjs** block - HTML content to put just before the `</body>` closing tag + +Shown below is an example of a privacy policy custom page. <pre> -*/5 * * * * www-data /usr/bin/php /var/www/cryptopaste/src/cron.php >> /var/log/cryptopaste-cron.log +{% extends ["custom/_template.html.twig", "default/_template.html.twig"] %} +{% set title = "Privacy Policy" %} +{% set breadcrumb = {'icon': 'lock', 'text': 'Privacy Policy'} %} + +{% block content %} + +... privacy policy HTML ... + +{% endblock %} </pre> -# Upgrade +All page URL slugs are prefixed with `/p/`, such as `/p/privacy` for the above privacy policy page (if your CryptoPaste is accessible at `http://cryptopaste/` then this would be `http://cryptopaste/p/privacy`). + +If your page filename starts with an underscore (`_`), then the page will not be visible and will return a 404 error. + +### Modify the menu + +If you want your custom page or an outside URL to be visible in the top menu bar, you will need to create a `app/Resources/views/custom/_menu.yaml.twig` file. You can copy the default `app/Resources/views/default/_menu.yaml.twig` to start and modify as needed. You can also use this to hide the default FAQ page if you don't want to show it. + +The `_menu.yaml.html` file contents must start with a `menu` key, and each menu item is a list of two keys. You must have the **name** key set, and either **slug** or **url**: +* **name** - The name to show in the menu +* **slug** - The slug of your custom page (the part of the filename just before the `.html.twig` extension) +* **url** - A URL to point to + +Shown below is an example of a custom menu with the default FAQ page, a privacy policy custom page, and a link to an outside website. Note that this list is ordered top-to-bottom == left-to-right. So the menu example below would render the menu as: 'HackThisSite | Privacy Policy | FAQ | New Paste' ('New Paste' is always shown). + +<pre> +menu: + - + name: 'HackThisSite' + url: 'https://www.hackthissite.org' + - + name: 'Privacy Policy' + slug: privacy + - + name: 'FAQ' + slug: faq +</pre> + +If a menu slug starts with an underscore (`_`), that menu item will be hidden. + +If you don't want to show anything in the menu except the 'New Paste' button, simply create an empty `_menu.yaml.twig` file in the `custom/` folder. -Any time you upgrade, you must make sure to flush the `cache/twig/` folder of all content (minus the `.gitignore` file, of course). +# TODO +- (**Need Help!**) Write legitimate testing +- (**Need Help!**) Fix the UI to have better responsive scaling and other improvements # License @@ -108,7 +234,7 @@ CryptoPaste is licensed under the **GNU General Public License v3.0**. More deta # Acknowledgements CryptoPaste uses the following technologies: -* Sensio Labs frameworks ([Silex](https://silex.sensiolabs.org), [Symfony components](http://symfony.com/components), [Twig](https://twig.sensiolabs.org)) +* [Symfony framework](http://symfony.com) * [Composer](https://getcomposer.org), [phpUnit](https://phpunit.de) * Client-side JavaScript libraries and frameworks: * [Bootstrap](http://getbootstrap.com) and [jQuery](https://jquery.com) diff --git a/app/.htaccess b/app/.htaccess new file mode 100644 index 0000000..fb1de45 --- /dev/null +++ b/app/.htaccess @@ -0,0 +1,7 @@ +<IfModule mod_authz_core.c> + Require all denied +</IfModule> +<IfModule !mod_authz_core.c> + Order deny,allow + Deny from all +</IfModule> diff --git a/app/AppCache.php b/app/AppCache.php new file mode 100644 index 0000000..639ec2c --- /dev/null +++ b/app/AppCache.php @@ -0,0 +1,7 @@ +<?php + +use Symfony\Bundle\FrameworkBundle\HttpCache\HttpCache; + +class AppCache extends HttpCache +{ +} diff --git a/app/AppKernel.php b/app/AppKernel.php new file mode 100644 index 0000000..26af6a3 --- /dev/null +++ b/app/AppKernel.php @@ -0,0 +1,62 @@ +<?php + +use Symfony\Component\DependencyInjection\ContainerBuilder; +use Symfony\Component\HttpKernel\Kernel; +use Symfony\Component\Config\Loader\LoaderInterface; + +class AppKernel extends Kernel +{ + public function registerBundles() + { + $bundles = [ + new Symfony\Bundle\FrameworkBundle\FrameworkBundle(), + new Symfony\Bundle\SecurityBundle\SecurityBundle(), + new Symfony\Bundle\TwigBundle\TwigBundle(), + new Symfony\Bundle\MonologBundle\MonologBundle(), + new Symfony\Bundle\SwiftmailerBundle\SwiftmailerBundle(), + new Doctrine\Bundle\DoctrineBundle\DoctrineBundle(), + new Doctrine\Bundle\MigrationsBundle\DoctrineMigrationsBundle(), + new Sensio\Bundle\FrameworkExtraBundle\SensioFrameworkExtraBundle(), + new AppBundle\AppBundle(), + ]; + + if (in_array($this->getEnvironment(), ['dev', 'test'], true)) { + $bundles[] = new Symfony\Bundle\DebugBundle\DebugBundle(); + $bundles[] = new Symfony\Bundle\WebProfilerBundle\WebProfilerBundle(); + $bundles[] = new Sensio\Bundle\DistributionBundle\SensioDistributionBundle(); + + if ('dev' === $this->getEnvironment()) { + $bundles[] = new Sensio\Bundle\GeneratorBundle\SensioGeneratorBundle(); + $bundles[] = new Symfony\Bundle\WebServerBundle\WebServerBundle(); + } + } + + return $bundles; + } + + public function getRootDir() + { + return __DIR__; + } + + public function getCacheDir() + { + return dirname(__DIR__).'/var/cache/'.$this->getEnvironment(); + } + + public function getLogDir() + { + return dirname(__DIR__).'/var/logs'; + } + + public function registerContainerConfiguration(LoaderInterface $loader) + { + $loader->load(function (ContainerBuilder $container) { + $container->setParameter('container.autowiring.strict_mode', true); + $container->setParameter('container.dumper.inline_class_loader', true); + + $container->addObjectResource($this); + }); + $loader->load($this->getRootDir().'/config/config_'.$this->getEnvironment().'.yml'); + } +} diff --git a/app/DoctrineMigrations/Version20180901000000.php b/app/DoctrineMigrations/Version20180901000000.php new file mode 100644 index 0000000..db255e9 --- /dev/null +++ b/app/DoctrineMigrations/Version20180901000000.php @@ -0,0 +1,43 @@ +<?php + +namespace Application\Migrations; + +use Symfony\Component\DependencyInjection\ContainerAwareInterface; +use Symfony\Component\DependencyInjection\ContainerInterface; +use Symfony\Component\DependencyInjection\ContainerAwareTrait; +use Doctrine\DBAL\Migrations\AbstractMigration; +use Doctrine\DBAL\Schema\Schema; + +class Version20180901000000 extends AbstractMigration implements ContainerAwareInterface { + + use ContainerAwareTrait; + + /** + * @param Schema $schema + */ + public function up(Schema $schema) { + $this->abortIf($this->connection->getDatabasePlatform()->getName() !== 'mysql', 'Migration can only be executed safely on \'mysql\'.'); + $prefix = $this->container->getParameter('database_table_prefix'); + $this->addSql('CREATE TABLE '.$prefix.'sessions (sess_id VARBINARY(128) NOT NULL, sess_time BIGINT NOT NULL, sess_lifetime INT NOT NULL, sess_data LONGBLOB NOT NULL, PRIMARY KEY(sess_id)) DEFAULT CHARACTER SET utf8 COLLATE utf8_unicode_ci ENGINE = InnoDB'); + // Create new table if fresh install + $this->addSql('CREATE TABLE IF NOT EXISTS '.$prefix.'cryptopaste (id BIGINT AUTO_INCREMENT NOT NULL, timestamp BIGINT NOT NULL, expiry BIGINT NOT NULL, views INT DEFAULT 0 NOT NULL, data LONGBLOB NOT NULL, PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8 COLLATE utf8_unicode_ci ENGINE = InnoDB'); + // Backwards compatibility for v0.x.x upgrade + $this->addSql('RENAME TABLE '.$prefix.'cryptopaste TO '.$prefix.'pastes'); + $this->addSql('ALTER TABLE '.$prefix.'pastes CHANGE id id BIGINT AUTO_INCREMENT NOT NULL, CHANGE timestamp timestamp BIGINT NOT NULL, CHANGE expiry expiry BIGINT NOT NULL, CHANGE data data LONGBLOB NOT NULL'); + } + + /** + * @param Schema $schema + */ + public function down(Schema $schema) { + $this->abortIf($this->connection->getDatabasePlatform()->getName() !== 'mysql', 'Migration can only be executed safely on \'mysql\'.'); + $prefix = $this->container->getParameter('database_table_prefix'); + // Backwards compatibility for v0.x.x downgrade + $this->addSql('RENAME TABLE '.$prefix.'pastes TO '.$prefix.'cryptopaste'); + $this->addSql('ALTER TABLE '.$prefix.'cryptopaste CHANGE id id INT AUTO_INCREMENT NOT NULL, CHANGE timestamp timestamp INT NOT NULL, CHANGE expiry expiry INT NOT NULL, CHANGE data data MEDIUMTEXT NOT NULL COLLATE utf8mb4_general_ci'); + // Note: This does not drop the old 'cryptopaste' table in case you are downgrading back to v0.x.x + $this->addSql('DROP TABLE '.$prefix.'sessions'); + } +} + +// EOF diff --git a/app/Resources/TwigBundle/views/Exception/error.html.twig b/app/Resources/TwigBundle/views/Exception/error.html.twig new file mode 100644 index 0000000..6803448 --- /dev/null +++ b/app/Resources/TwigBundle/views/Exception/error.html.twig @@ -0,0 +1,3 @@ +{% extends ["custom/message.html.twig", "default/message.html.twig"] %} +{% set heading = 'Error' %} +{% set message = 'There was an error processing your request' %} diff --git a/app/Resources/TwigBundle/views/Exception/error403.html.twig b/app/Resources/TwigBundle/views/Exception/error403.html.twig new file mode 100644 index 0000000..b69f259 --- /dev/null +++ b/app/Resources/TwigBundle/views/Exception/error403.html.twig @@ -0,0 +1,3 @@ +{% extends ["custom/message.html.twig", "default/message.html.twig"] %} +{% set heading = 'Not Authorized' %} +{% set message = 'You are not authorized to view that page' %} diff --git a/app/Resources/TwigBundle/views/Exception/error404.html.twig b/app/Resources/TwigBundle/views/Exception/error404.html.twig new file mode 100644 index 0000000..990e80b --- /dev/null +++ b/app/Resources/TwigBundle/views/Exception/error404.html.twig @@ -0,0 +1,3 @@ +{% extends ["custom/message.html.twig", "default/message.html.twig"] %} +{% set heading = 'Not Found' %} +{% set message = 'Page not found' %} diff --git a/cache/log/.gitignore b/app/Resources/views/custom/.gitignore similarity index 100% rename from cache/log/.gitignore rename to app/Resources/views/custom/.gitignore diff --git a/app/Resources/views/default/_menu.yaml.twig b/app/Resources/views/default/_menu.yaml.twig new file mode 100644 index 0000000..0dfdb0b --- /dev/null +++ b/app/Resources/views/default/_menu.yaml.twig @@ -0,0 +1,4 @@ +menu: + - + name: 'FAQ' + slug: faq diff --git a/src/views/_template.twig b/app/Resources/views/default/_template.html.twig similarity index 77% rename from src/views/_template.twig rename to app/Resources/views/default/_template.html.twig index e95c56b..277b1b9 100644 --- a/src/views/_template.twig +++ b/app/Resources/views/default/_template.html.twig @@ -8,9 +8,9 @@ <meta name="author" content="HackThisSite"> <link rel="icon" href="{{ asset('favicon.ico') }}"> <title>CryptoPaste{% if title is defined %}{{ ' :: %s'|format(title) }}{% endif %} - - - + + +
- +{{ form_widget(form.paste, {'id': 'paste', 'attr': {'class': 'form-control npinput', 'rows': 3}}) }}
@@ -75,16 +76,7 @@ Expiration - +{{ form_widget(form.expiration, {'id': 'expiration', 'attr': {'class': 'form-control npinput'}}) }} Set paste expiration. "Burn After Reading" deletes the paste immediately upon being viewed. @@ -100,7 +92,7 @@
- +{{ form_widget(form.password, {'id': 'password', 'attr': {'class': 'form-control npinput'}}) }} @@ -133,24 +125,24 @@