Featured
Drupal 8/9 Social Auth Buttons
Finally an easy way to render Social Auth links/buttons. Simply download and enable Social Auth Buttons and the buttons will be rendered in Drupal's default login form. You can even embed and render it to your custom forms like this: $form['social_auto_buttons'] = [ '#type' => 'social_auth_buttons', '#title' => t('Social Auth Buttons'), ]; Theming The form elements that came with this module are fully themeable. Below is a sample to add icons to the buttons using Font Awesome icons Make sure you have Font Awesome libraries loaded in to your theme. [theme].libraries.yml ... fontawesome: version: VERSION js: node_modules/@fortawesome/fontawesome-free/js/all.min.js: {} css: component: node_modules/@fortawesome/fontawesome-free/css/all.min.css: {} ... Theme the buttons using hook_preprocess_social_auth_buttons_link() [theme].theme ... /** * Implements hook_preprocess_HOOK(). */ function [theme]_preprocess_social_auth_buttons_link(&$variables) { /* Your code here */ $icons = [ 'facebook' => 'fab fa-facebook-f', 'google' => 'fab fa-google', 'instagram' => 'fab fa-instagram', ]; $id = $variables['name']; if (isset($icons[$id])) { $variables['icon'] = [ '#markup' => '<i class="' . $icons[$id] . '"></i>', ]; } $variables['attributes']['class'][] = 'btn-' . $id; } ...0Read moreDrupal 8 module: Bootstrap 4 Modal
Are you using Drupal 8 Bootstrap 4 Theme (Barrio) and you want to use Drupal Ajax Dialog? Chances are you might have to do some extra coding just to make that Drupal core modal dialog make it look and feel like a bootstrap modal. Bootstrap 4 Modal module adds new type of dialog that you can use. Example of Drupal core ajax modal dialog: <a class="use-ajax" data-dialog-options="{"width":400}" data-dialog-type="modal" href="/node/1"> First node displayed in core modal dialog. </a> This link will load drupal node id 1 by ajax on drupal core modal dialog with 400px width Example of Bootstrap 4 Modal dialog: <a class="use-ajax" data-dialog-options="{"dialogClasses":"modal-dialog-centered","dialogShowHeader":false}" data-dialog-type="bootstrap4_modal" href="/node/1"> First node displayed in bootstrap 4 modal dialog. </a> This link will load drupal node id 1 by ajax on drupal core modal dialog with modal dialog vertically centered and no modal header.Generate Free Wildcard SSL certificate using Let's Encrypt/Certbot
1. Generate the wildcard SSL certificate /opt/certbot/certbot-auto certonly --manual --preferred-challenges=dns --email my@email.com --server https://acme-v02.api.letsencrypt.org/directory --agree-tos -d *.mydomain.com -d mydomain.com Note: You need to replace my@email.com, *.mydomain.com and mydomain.com with your actual information. 2. Verify domain's ownership Let’s Encrypt Wildcard certificates only accepts DNS challenge method, which we can invoke by using the preferred-challenges=dns flag. After executing the command on step 1, the Certbot will return a text record that you should add on your DNS. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Please deploy a DNS TXT record under the name _acme-challenge.vhinandrich.com with the following value: vE7k91-8K9XPyMcNYFXP19Ijv7T4o0GAkJnRlwW7af0 Before continuing, verify the record is deployed. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Host: _acme-challenge Value: vE7k91-8K9XPyMcNYFXP19Ijv7T4o0GAkJnRlwW7af0 Create TXT record via DNS console and setup key and value 3. Get your Certificate After adding the dns challange, you can proceed with the generate certificate. It will return you the path of ssl certicates and chain. IMPORTANT NOTES: - Congratulations! Your certificate and chain have been saved at: /etc/letsencrypt/live/mydomain.com/fullchain.pem Your key file has been saved at: /etc/letsencrypt/live/mydomain.com/privkey.pem Your cert will expire on 2020-09-07. To obtain a new or tweaked version of this certificate in the future, simply run certbot-auto again. To non-interactively renew *all* of your certificates, run "certbot-auto renew" - If you like Certbot, please consider supporting our work by: Donating to ISRG / Let's Encrypt: https://letsencrypt.org/donate Donating to EFF: https://eff.org/donate-le 4. Verify the Certificate To check the validity of all your certificates, you can run this in your command line /opt/certbot/certbot-auto certificates Return: - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Found the following certs: Certificate Name: mydomain.com Serial Number: 3e53264dd24388560fb3dd95e2aa5970bbd Domains: *.mydomain.com mydomain.com Expiry Date: 2020-09-07 02:43:05+00:00 (VALID: 89 days) Certificate Path: /etc/letsencrypt/live/mydomain.com/fullchain.pem Private Key Path: /etc/letsencrypt/live/mydomain.com/privkey.pem - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Note: Renewal will always have to be done manually for wildcard certificates because of the dns challenge requirment. Thus you won't be able to create cronjobs to renew wildcard certificates.0Read moreSaving foreign key ID with Django REST framework serializer
If you are using Django REST framework on serving your APIs, you probably did the below code in returning the related object in your serializer. class MyTableSerializer(serializers.ModelSerializer): user = UserSerializer(many=False, read_only=True) class Meta: fields = '__all__' model = MyTable But doing this will not allow your API to pass the foreign key id. Instead, you need to include the field name in the serializer like the code below: class MyTableSerializer(serializers.ModelSerializer): user = UserSerializer(many=False, read_only=True) user_id = serializers.IntegerField(write_only=True) class Meta: fields = '__all__' model = MyTableJenkins (standalone) SSL + Let's Encrypt
In this tutorial, I will show how to use Let's Encrypt free SSL with a standalone Jenkins in Ubuntu 16.04. Installation of certbot and jenkins are not included in this tutorial. Generate Certificates Run the command to generate the certificate and key files. sudo certbot certonly --standalone --preferred-challenges http -d example.com You should get this response: Saving debug log to /var/log/letsencrypt/letsencrypt.log Plugins selected: Authenticator standalone, Installer None Obtaining a new certificate Performing the following challenges: http-01 challenge for example.com Waiting for verification... Cleaning up challenges IMPORTANT NOTES: - Congratulations! Your certificate and chain have been saved at: /etc/letsencrypt/live/example.com/fullchain.pem Your key file has been saved at: /etc/letsencrypt/live/example.com/privkey.pem Your cert will expire on 2019-02-07. To obtain a new or tweaked version of this certificate in the future, simply run certbot again. To non-interactively renew *all* of your certificates, run "certbot renew" - If you like Certbot, please consider supporting our work by: Donating to ISRG / Let's Encrypt: https://letsencrypt.org/donate Donating to EFF: https://eff.org/donate-le Just in case you got this response: Saving debug log to /var/log/letsencrypt/letsencrypt.log Plugins selected: Authenticator standalone, Installer None Obtaining a new certificate Performing the following challenges: http-01 challenge for example.com Cleaning up challenges Problem binding to port 80: Could not bind to IPv4 or IPv6. You need to stop your web server and try again. Convert the certificate to JKS keystore Go to your certificate folder cd /etc/letsencrypt/live/example.com And execute this command to convert the certificate to PKCS12 file first openssl pkcs12 -inkey privkey.pem -in fullchain.pem -export -out keys.pkcs12 If you are renewing the certificates, make sure to delete the existing /var/lib/jenkins/jenkins.jks file first. Then convert to JKS file keytool -importkeystore -srckeystore keys.pkcs12 -srcstoretype pkcs12 -destkeystore /var/lib/jenkins/jenkins.jks Enter export and import passwords and answer "yes" if asked to overwrite an existing alias Enter Export Password: Verifying - Enter Export Password: root@example:/etc/letsencrypt/live/example.com# keytool -importkeystore -srckeystore keys.pkcs12 -srcstoretype pkcs12 -destkeystore /var/lib/jenkins/jenkins.jks Importing keystore keys.pkcs12 to /var/lib/jenkins/jenkins.jks... Enter destination keystore password: Enter source keystore password: Existing entry alias 1 exists, overwrite? [no]: yes Entry for alias 1 successfully imported. Import command completed: 1 entries successfully imported, 0 entries failed or cancelled Set Jenkins configuration to use the SSL Edit the Jenkins config file vim /etc/default/jenkins Look for JENKINS_ARGS and update the value to this: JENKINS_ARGS="--webroot=/var/cache/$NAME/war --httpPort=-1 --httpsPort=8443 --httpsKeyStore=/var/lib/jenkins/jenkins.jks --httpsKeyStorePassword=PASSWORD_SET_ON_CONVERT_TO_JKS" Restart jenkins sudo service jenkins restartDrupal 8: Show Drupal throbber on button that manually calls Views AJAX RefreshView
In this tutorial, I will guide you how to show Drupal's progress/throbber icon on your button that call the Views AJAX RefreshView. Views Configuration Make sure your view is using AJAX. To do that, set the "Advanced > Use Ajax" on your view. HTML Button <a href="#" class="btn btn-default reload-view-button">Refresh View</a> Javascript Drupal.behaviors.refreshLink = { attach: function(context, settings) { if ($('.reload-view-button', context).length > 0) { $('.reload-view-button', context).once().on('click', function(e) { e.preventDefault(); var self = this; // Change [YOUR VIEW ID] to your actual view id // This loop will get the view instance from the view class var viewInstance = Object.values(Drupal.views.instances).find(function(item) { if (item.$view.length > 0 && $('.view-id-[YOUR VIEW ID]').length > 0) { return item.$view[0] === $('.view-id-[YOUR VIEW ID]')[0]; } }); if (viewInstance) { // Get the ajax throbber from theme var progressElement = $(Drupal.theme('ajaxProgressThrobber')); // Append the throbber to the button $(self).append(progressElement); // Refresh the view by ajax $('.view-id-[YOUR VIEW ID]').trigger('RefreshView'); // Override the success callback method viewInstance.refreshViewAjax.success = function(response, status) { // Call the original callback Drupal.Ajax.prototype.success.call(this, response, status); // ADD ALL THE THINGS WE WANT TO DO AFTER REFRESH VIEW AJAX SUCCESS! // Remove the throbber element that we added above $(progressElement).remove(); }; } }); } } }
Latest
Automated Let's encrypt + Jenkins ssl renewal script with cron
Create the script Create a file named renew-ssl-jenkins.sh anywhere accessible and enter the code below: #!/bin/bash # change password value to your password SSLPASS=MyPassword123 sudo service apache2 stop || true certbot renew || true cd /etc/letsencrypt/live/mysite.com rm /var/lib/jenkins/jenkins.jks openssl pkcs12 -inkey privkey.pem -in fullchain.pem -export -out keys.pkcs12 -password pass:$SSLPASS || true keytool -importkeystore -srcstorepass $SSLPASS -deststorepass $SSLPASS -noprompt -v -srckeystore keys.pkcs12 -srcstoretype pkcs12 -destkeystore /var/lib/jenkins/jenkins.jks || true sudo service jenkins restart || true sudo service apache2 start || true Execute the script every month using cron Crontab entry: ... 0 1 1 * * ~/renew-ssl-jenkins.sh > /var/log/renew-ssl-jenkins.log ...0Read moreDrupal 8: Change Commerce Product Variation URL to SKU
To pull this off, we need to write a custom module. Let's name our module commerce_variation_sku. In our commerce_variation_sku.module file, we need implement the hook hook_entity_type_build to change the entity class and entity storage class to our custom classes. /** * Implements hook_entity_type_build(). */ function commerce_variation_sku_entity_type_build(array &$entity_types) { if (isset($entity_types['commerce_product_variation'])) { $entity_types['commerce_product_variation']->setClass('Drupal\commerce_variation_sku\Entity\ProductVariation'); $entity_types['commerce_product_variation']->setStorageClass('Drupal\commerce_variation_sku\ProductVariationStorage'); } } Next, we create our custom entity class that will extend the original entity class. <?php namespace Drupal\commerce_variation_sku\Entity; use Drupal\Core\Url; use Drupal\commerce_product\Entity\ProductVariation as ProductVariationBase; class ProductVariation extends ProductVariationBase { /** * {@inheritdoc} */ public function toUrl($rel = 'canonical', array $options = []) { // Product variation URLs depend on the parent product. if (!$this->getProductId()) { // RouteNotFoundException tells EntityBase::uriRelationships() // to skip this product variation's link relationships. throw new RouteNotFoundException(); } // StringFormatter assumes 'revision' is always a valid link template. if (in_array($rel, ['canonical', 'revision'])) { $route_name = 'entity.commerce_product.canonical'; $route_parameters = [ 'commerce_product' => $this->getProductId(), ]; $options += [ 'query' => [ 'sku' => $this->getSku(), ], 'entity_type' => 'commerce_product', 'entity' => $this->getProduct(), // Display links by default based on the current language. 'language' => $this->language(), ]; return new Url($route_name, $route_parameters, $options); } else { return parent::toUrl($rel, $options); } } } Then we create our custom entity storage class that again extends the original entity storage class. <?php namespace Drupal\commerce_variation_sku; use Drupal\commerce_product\Entity\ProductInterface; use Drupal\commerce_product\ProductVariationStorage as ProductVariationStorageBase; /** * Defines the product variation storage. */ class ProductVariationStorage extends ProductVariationStorageBase { /** * {@inheritdoc} */ public function loadFromContext(ProductInterface $product) { $current_request = $this->requestStack->getCurrentRequest(); if ($sku = $current_request->query->get('sku')) { $variation = $this->loadBySku($sku); if (in_array($variation->id(), $product->getVariationIds())) { /** @var \Drupal\commerce_product\Entity\ProductVariationInterface $variation */ if ($variation->isPublished() && $variation->access('view')) { return $variation; } } } return $product->getDefaultVariation(); } } Last step is to clear your cache and you're good to go 😀0Read moreManually start Solr server in docker (wodby)
If you have problem issue with your Solr server like it was not connected to your site, you might need to manually start your Solr server. First, you need to login to your Solr docker container: make shell solr Check the status first. Inside the Solr container, run: solr status If you see something like this: neither jattach nor jstack in /opt/java/openjdk could be found, so no thread dumps are possible. Continuing. No Solr nodes are running. It means your Solr is not running. You can then proceed to start the Solr server by running: solr start If you see this message: neither jattach nor jstack in /opt/java/openjdk could be found, so no thread dumps are possible. Continuing. Waiting up to 180 seconds to see Solr running on port 8983 [\] Started Solr server on port 8983 (pid=131). Happy searching! It means your Solr server is up and running. Next step is to create a Solr core (Optional if your Solr core is not yet created). The command below will create a core with specific configuration. solr create_core -c [core-name] -d /opt/solr/server/solr/configsets/search_api_solr_4.2.0/conf/ And that's it!Automated testing with Drupal 8 + Selenium + Behat + Gherkins + Jenkins + Cucumber
Here's a simple guide on how to setup automated testing with Behat, Behat Gherkins, Selenium, Jenkins and Cucumber with Drupal 8/9. In this guide, we will be using a Drupal 8 instance with composer. Setup Behat for Drupal 8/9 First thing we need to do is to install all the composer libraries that we need by installing drupal/drupal-extension. composer require drupal/drupal-extension --dev Creating a file called behat.yml inside the root directory of your project default: suites: default: contexts: - FeatureContext - Drupal\DrupalExtension\Context\DrupalContext - Drupal\DrupalExtension\Context\MinkContext extensions: Behat\MinkExtension: goutte: ~ base_url: https://www.mysite.com javascript_session: selenium2 browser_name: 'chrome' selenium2: wd_host: http://localhost:4444/wd/hub capabilities: { "browser": "chrome", "version": "*", 'chrome': {'switches':['--start-maximized']}} Drupal\DrupalExtension: blackbox: ~ api_driver: drupal drupal: drupal_root: web/ region_map: navigation: ".navbar-header" navigation_collapsible: "#navbar-collapse" header: ".region-header" highlighted: ".highlighted" help: ".region-help" content: ".main-content" sidebar_first: ".region-sidebar-first" sidebar_second: ".region-sidebar-second" footer: ".footer" local: extensions: Drupal\MinkExtension: base_url: http://mysite.localhost:8000 Initialize behat. This creates the features folder where we will put all our features files and some of our custom context if we needed. Custom context will be inside the features/bootstrap directory. behat --init +d features/bootstrap - place your context classes here +f features/bootstrap/FeatureContext.php - place your definitions, transformations and hooks here To check if everything is working as expected, you can try listing definitions by: behat -dl You should see something like this: default | Given I am an anonymous user default | Given I am not logged in default | Then I log out default | Given I am logged in as a user with the :role role(s) default | Given I am logged in as a/an :role default | Given I am logged in as a user with the :role role(s) and I have the following fields: default | Given I am logged in as :name default | Given I am logged in as a user with the :permissions permission(s) default | Then I should see (the text ):text in the :rowText row default | Then I should not see (the text ):text in the :rowText row default | Given I click :link in the :rowText row default | Then I (should )see the :link in the :rowText row default | Given the cache has been cleared default | Given I run cron ... Setup Selenium server with Chrome driver On this part, we will be using a standalone Selenium server. Create a directory that you can easily access. E.g.: mkdir ~/selenium-server Download the standalone Selenium server and move it in to the folder you just created. Download your desired browser to use for testing. In this guide, we will be using Chrome driver. Head to https://chromedriver.chromium.org/ to download the right Chrome driver version that fits your Chrome browser version. Extract the zip file and move the chromedriver executable file in to the same folder we created. Run the server cd ~/selenium-server java -Dwebdriver.chrome.driver=./chromedriver -jar selenium-server-standalone-3.141.59.jar Create feature and run the test Create a new file inside features folder and name it like homepage.feature @api @javascript Feature: Homepage check Scenario: Anonymous user access Given I am an anonymous user When I visit "/" Then I should see the text "Featured" in the "content" region This example feature file will access the homepage as an anonymous user and will look for the text "Featured" in the "content" region (which the css selector were specified in the behat.yml) Now let's run the test by executing: behat --profile local I'm running the local profile (which we also specified the url from our behat.yml) since I'm running the test from my local.How to fix broken API responses with Apache proxy
I've been stuck on this for a while and I have difficulties looking for a solution. I have an application that calls an api and it was hosted in a docker and a reverse proxy in apache. I noticed that every time I request to any of my apis, I always get weird behaviour like the actual error message (which is in json format) are not being displayed. After a long investigation, I noticed that the api that returns an error (e.g.: 400, 401, etc..) always returns html error message/page. If you are like me, hope you find this 😀 Just add this line: <If "%{HTTP_ACCEPT} =~ /json/"> ProxyErrorOverride Off </If> In your apache vhost config.Sublime Text 3 Install Package not working in Big Sur
Here's how to fix Sublime Text 3 Install Package not working or not showing on package list (command + shift + P) Open the Sublime app Press cmd + shift + p Enter Browse Packages and press Enter This command will open the Finder app Go to the Installed Packages folder and delete the Package Control plugin file (if it exists) Select to Packages folder (in the same level of the Installed Packages folder) and hit cmd + c to copy the folder (path) Opeb Terminal app Enter cd and paste the folder path (cmd + v). You will see the command like this: cd /Users/[youre-username]/Library/Application Support/Sublime Text 3/Packages Press Enter to go into the folder Clone the @TheSecEng 's fork git clone git@github.com:TheSecEng/package_control.git "Package Control" Restart Sublime1Read moreApache service randomly stopping
If you're apache server somehow stops working randomly, first thing you can do is to check Apache error logs. [Mon May 24 15:39:26.328479 2021] [php7:error] [pid 3203] [client 52.65.15.196:9267] script '/var/www/html/wp-login.php' not found or unable to stat [Mon May 24 15:39:26.852814 2021] [php7:error] [pid 7809] [client 52.65.15.196:31930] script '/var/www/html/wp-login.php' not found or unable to stat [Mon May 24 15:42:15.348017 2021] [php7:error] [pid 6091] [client 52.192.73.251:59992] script '/var/www/html/wp-login.php' not found or unable to stat [Mon May 24 15:42:18.382698 2021] [php7:error] [pid 6263] [client 52.192.73.251:20394] script '/var/www/html/wp-login.php' not found or unable to stat [Mon May 24 15:51:41.982574 2021] [php7:error] [pid 4420] [client 3.8.12.221:32028] script '/var/www/html/wp-login.php' not found or unable to stat [Mon May 24 15:51:42.243716 2021] [php7:error] [pid 15790] [client 3.8.12.221:39625] script '/var/www/html/wp-login.php' not found or unable to stat For my case, the logs above are so suspicious because I don't even run a Wordpress site. So it feels like something or someone is trying to brute force attack my server. If you have notice something similar, one thing you can do is to install Fail2ban. Installing in Ubuntu 16.04 apt-get install fail2ban then copy jail.conf to jail.local cd /etc/fail2ban cp jail.conf jail.local edit jail.local and search for "apache-noscript" [apache-noscript] enabled=true then restart the fail2ban service to reload the configurations service fail2ban restart to check the status like the ip addresses that were banned and some statistics, run fail2ban-client status apache-noscript0Read moreRemove git submodule created by mistake
Make sure that the sub-directory/sub-module doesn't have .git directory. If it does, delete the .git directory inside the sub-directory/sub-module rm -rf [sub-module]/.git Clear the git cache git rm --cached [sub-module]Django OAuth Toolkit: Allow access token expiration date per user
In this tutorial, I will demonstrate how to implement a per-user access token expiration for Django OAuth Toolkit. Setup OAuth Toolkit Override OAUTH2_VALIDATOR_CLASS in settings.py OAUTH2_PROVIDER = { 'ACCESS_TOKEN_EXPIRE_SECONDS': 1800, # 30 minutes 'REFRESH_TOKEN_EXPIRE_SECONDS': 3600, # 1 hour 'OAUTH2_VALIDATOR_CLASS': 'py_app.validator.MyOAuth2Validator', } Custom Validator Create the custom validator Create MyOAuth2Validator.py inside <app-root>/py_app/validator *Create validator folder if it doesn't exist yet Override save_bearer_token method to check for our custom expiration field and use it if there's any. from oauth2_provider.oauth2_validators import OAuth2Validator from oauth2_provider.models import AccessToken class MyOAuth2Validator(OAuth2Validator): """ Primarily extend the functionality of token generation """ def save_bearer_token(self, token, request, *args, **kwargs): from datetime import datetime, timedelta super(MyOAuth2Validator, self).save_bearer_token(token, request, *args, **kwargs) ip = self.get_client_ip(request) accessToken = AccessToken.objects.get(token=token.get('access_token')) if accessToken.user.detail.session_expire_in is not None: accessToken.expires = datetime.now() + timedelta(seconds=accessToken.user.detail.session_expire_in) accessToken.save() Custom field We first need to create a model that we can link to our user model. We can call it UserDetail. Create this class on a dedicated django app or on any existing app model. class UserDetail(models.Model): user = models.OneToOneField(USER_MODEL, related_name='detail', on_delete=models.CASCADE, null=True, blank=True) session_expire_in = models.IntegerField(default=None, null=True, blank=True) And that's it.. Run the makemigrations, migrate and runserver. Have fun!0Read more