• 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 ...    
  • Drupal 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 ūüėÄ 
  • Manually 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 Sublime    
  • Apache 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-noscript  
  • Remove 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]  
    0Read more
  • 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!
  • 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; } ...