Category: Crappy Tutorials

  • Rescuing Tampermonkey Scripts from a LevelDB Backup

    Rescuing Tampermonkey Scripts from a LevelDB Backup

    I have a confession to make. I didn’t back up my Tampermonkey scripts. I recently switched to a new computer, and thought I had copied all of my important information over from the old one.

    It turns out, I did not. Luckily I did have a full disk backup that I thought I could just pull the .user.js files off of. OH BOY WAS I WRONG.

    The Oops: No Script Backup

    I’d cobbled together a dozen userscripts or more over years, but never bothered to add them to a backup routine. When I opened Tampermonkey on the new machine, it greeted me with the emptiest dashboard imaginable.

    Digging Into Chrome’s LevelDB

    It turns out Chrome buries extension data in your profile directory. The Tampermonkey store lives in a LevelDB folder named after its extension ID:

    /Volumes/OldHDD/derrick/Library/Application Support/Google/Chrome/Profile 1/IndexedDB/\
    chrome-extension_dhdgffkkebhmkfjojejmpbldmpobfkfo_0.indexeddb.leveldb

    Dumping with leveldbutil

    I used leveldbutil, a C++ command-line tool for dumping LevelDB databases. I had to clone the repo and compile it from source before it would run on macOS.

    ./leveldbutil dump "/Volumes/OldHDD/derrick/Library/Application Support/Google/Chrome/Profile 1/IndexedDB/chrome-extension_dhdgffkkebhmkfjojejmpbldmpobfkfo_0.indexeddb.leveldb" > tampermonkey-dump.txtCode language: Bash (bash)

    This produced a massive text dump (tampermonkey-dump.txt) of JSON blobs—metadata, source code, state… you name it.

    Parsing the Dump: My One-Off Script

    Rather than manually eyeballing hundreds of lines, I whipped up a throwaway PHP script, let’s call it ScriptSalvager, to automate recovery. It:

    1. Loads the dump file.
    2. Scans for script entries via regex.
    3. json_decode()s each blob.
    4. Writes out individual script files.
    <?php
    /**
     * Exports Tampermonkey userscripts from a raw dump to per-script directories.
     *
     * This script reads the Tampermonkey IndexedDB dump, splits it into
     * individual userscript segments, parses each piece (meta, source,
     * rules, state, etc.), and writes each to its own file.
     */
    
    // Path to the raw Tampermonkey dump.
    $file = __DIR__ . '/tampermonkey-dump.txt';
    
    if ( ! is_readable( $file ) ) {
    	fwrite( STDERR, 'Cannot read file: ' . $file . "\n" );
    	exit( 1 );
    }
    
    // Load the file.
    $contents = file_get_contents( $file );
    
    // Split on lines like: "--- offset 123456; sequence 789".
    $segments = preg_split( '/^--- offset \d+; sequence \d+/m', $contents );
    
    // Remove any leading empty segment
    if ( isset( $segments[0] ) && trim( $segments[0] ) === '' ) {
    	array_shift( $segments );
    }
    
    // Collect only the userscript segments (those with both @meta and @source).
    $userscripts = [];
    foreach ( $segments as $segment ) {
    	if ( str_contains( $segment, "put '@meta#" ) && str_contains( $segment, "put '@source#" ) ) {
    		  // Normalize line endings and split into lines for readability.
    		  $normalized = str_replace( [ "\r\n", "\r" ], "\n", $segment );
    		  $lines = explode( "\n", $normalized );
    
    		  // Trim each line, and remove empty or external-resource lines.
    		  $filtered = [];
    		  foreach ( $lines as $line ) {
    			  $line = trim( $line );
    			  // Skip blank lines.
    			  if ( $line === '' ) {
    				  continue;
    			  }
    			  // Skip external-resource entries.
    			  if ( str_starts_with( $line, "put '@ext#" ) ) {
    				  continue;
    			  }
    			  $filtered[] = $line;
    		  }
    
    		  $userscripts[] = $filtered;
    	}
    }
    
    // Free memory we no longer need.
    unset( $contents, $segments );
    
    // Build an associative array of scripts by their UID values.
    $scripts = [];
    foreach ( $userscripts as $lines ) {
    	$items = [];
    	$script_uuid = null;
    	foreach ( $lines as $line ) {
    		  $trimmed = trim( $line );
    		  if ( ! str_starts_with( $trimmed, "put '" ) ) {
    			  echo "Error: Expected line to start with 'put '\n";
    			  die();
    		  }
    		  // Strip off leading "put '".
    		  $rest = substr( $trimmed, strlen( "put '" ) );
    		  // Split into type and remainder.
    		  [ $type, $after_hash ] = explode( '#', $rest, 2 );
    		  // Split remainder into UUID+closing-quote and JSON blob.
    		  [ $uid_quoted, $json_with_quote ] = explode( "' ", $after_hash, 2 );
    		  // Extract the UUID (no quotes).
    		  $uuid = trim( $uid_quoted, "'" );
    		  if ( $script_uuid === null ) {
    			  $script_uuid = $uuid;
    		  }
    
    		  // Trim single-quotes from JSON.
    		  $json_blob = trim( $json_with_quote, "'" );
    		  $value = $json_blob;
    		  $value = json_decode( $json_blob, true );
    		  if ( '@source' === $type ) {
    			  // Decode the source JSON.
    			  $value = json_decode( $json_blob, true );
    			  $value = $value['value'];
    		  } else {
    			$value = json_encode( $value['value'] ) ?? $json_blob;
    		}
    
    		  // Store, allowing duplicates to become arrays.
    		  if ( isset( $items[$type] ) ) {
    			  if ( ! is_array( $items[$type] ) ) {
    				  $items[$type] = [ $items[$type] ];
    			  }
    			  $items[$type][] = $value;
    		  } else {
    			  $items[$type] = $value;
    		  }
    	}
    	// Use the extracted UUID as the script key.
    	if ( $script_uuid === null ) {
    		echo "Error: Could not extract script UUID\n";
    		die();
    	}
    
    	if ( ! isset( $scripts[$script_uuid] ) ) {
    		$scripts[$script_uuid] = [];
    	}
    
    	$scripts[$script_uuid][] = $items;
    }
    
    // Add script names to the array.
    foreach ( $scripts as $uuid => $script ) {
    	foreach ( $script as $index => $version ) {
    		$name = $uuid;
    		// Check if the script has a name.
    		if ( isset( $version['@uid'] ) ) {
    			$uid = $version['@uid'];
    			$name = $uid;
    		} elseif ( isset( $version['@source'] ) ) {
    			$source = $version['@source'];
    			$normalized_source = str_replace( [ "\r\n", "\r" ], "\n", $source ?? '' );
    			$lines_source = explode( "\n", $normalized_source );
    			foreach( $lines_source as $line_source ) {
    				$line_source = trim( $line_source );
    				// Skip blank lines.
    				if ( $line_source === '' ) {
    					continue;
    				}
    
    				if ( str_contains( $line_source, "@name" ) ) {
    					$name_lines = explode( "@name", $line_source );
    					$name = trim( $name_lines[1] );
    					break;
    				}
    			}
    		}
    		if ( $name === $uuid ) {
    			echo "Error: Could not extract script name\n";
    			die();
    		}
    
    		// Remove leading and trailing quotes.
    		$name = trim( $name, '"' );
    
    		// Store the name in the script array.
    		$scripts[$uuid][$index]['@name'] = $name;
    	}
    }
    
    // If a script has multiple meta entries, split them into separate scripts.
    foreach ( $scripts as $uuid => $script ) {
    	foreach ( $script as $index => $version ) {
    		// Check if the script has multiple meta entries.
    		if ( isset( $version['@meta'] ) && is_array( $version['@meta'] ) ) {
    			foreach ( $version['@meta'] as $meta_index => $meta_value ) {
    				// Copy the script data to a new entry.
    				$new_script = $scripts[$uuid][$index];
    				// Remove the meta entry from the original script.
    				unset( $new_script['@meta'] );
    				// Copy the single meta entry to the new script.
    				$new_script['@meta'] = $meta_value;
    				// Unset the meta entry from the original script.
    				unset( $scripts[$uuid][$index]['@meta'] );
    				// Add the new script to the array.
    				$scripts[$uuid][] = $new_script;
    			}
    			// After splitting, remove the original script entry.
    			unset( $scripts[$uuid][$index] );
    		}
    	}
    }
    
    // Add lastModified to the array.
    foreach ( $scripts as $uuid => $script ) {
    	foreach ( $script as $index => $version ) {
    		// Check if the script has a lastModified.
    		if ( isset( $version['@meta'] ) ) {
    			$meta = $version['@meta'];
    			$meta = json_decode( $meta, true );
    			$meta = $meta['value'] ?? $meta;
    		   
    			if ( isset( $meta['lastModified'] ) ) {
    				$last_modified = $meta['lastModified'];
    				$scripts[$uuid][$index]['@lastModified'] = $last_modified;
    				$scripts[$uuid][$index]['@lastModifiedHuman'] = date( 'Y-m-d H:i:s', intval($last_modified/1000 ));
    			}
    
    			if ( isset( $meta['version'] ) ) {
    				$version_number = $meta['version'];
    				$scripts[$uuid][$index]['@version'] = $version_number;
    			}
    
    		}
    	}
    }
    
    // Export each script’s data into its own directory, with a unique name, timestamp, and version.
    foreach ( $scripts as $uuid => $script ) {
    	$latest_version = 0;
    	foreach ( $script as $index => $version ) {
    		// Determine directory name based on script name (fallback to UUID).
    		$name = $version['@name'];
    
    		$directory = $name . '/' . $version['@lastModifiedHuman'] . ' - v' . $version['@version'];
    
    		// Create or clear directory.
    		if ( ! is_dir( $directory ) ) {
    			mkdir( $directory, 0777, true );
    		}
    
    		// Write each part to its own JSON file.
    		foreach ( $version as $type => $value ) {
    			$base = trim( $type, '@' );
    
    			if ( 'source' === $base) {
    				// Export the userscript source unescaped so code remains intact.
    				$file_name = "{$directory}/{$base}.user.js";
    				file_put_contents( $file_name, $value );
    			} else {
    				// Single value, write normally.
    				if ( ! in_array( $base, [ 'name', 'lastModified', 'lastModifiedHuman', 'version' ] ) ) {
    					$value = json_decode( $value, true );
    				}
    				$file_name = "{$directory}/{$base}.json";
    				file_put_contents( $file_name, json_encode( $value, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES ) );
    			}
    		}
    
    		// Find the most recent lastModified date and set it as the "latest" version.
    		if ( isset( $version['@lastModified'] ) && $version['@lastModified'] > $latest_version ) {
    			$latest_version = $version['@lastModified'];
    
    			// Are we at the end of the array?
    			if ( $index === count( $script ) - 1 ) {
    				// Create the latest version directory.
    				$latest_directory = $name . '/latest';
    				if ( ! is_dir( $latest_directory ) ) {
    					mkdir( $latest_directory, 0777, true );
    				}
    				// Write each part to its own JSON file.
    				foreach ( $version as $type => $value ) {
    					if ( $latest_version !== $version['@lastModified'] ) {
    						continue;
    					}
    					$base = trim( $type, '@' );
    
    					if ( 'source' === $base) {
    						// Export the userscript source unescaped so code remains intact.
    						$file_name = "{$latest_directory}/{$base}.user.js";
    						file_put_contents( $file_name, $value );
    					} else {
    						// Single value, write normally.
    						if ( ! in_array( $base, [ 'name', 'lastModified', 'lastModifiedHuman', 'version' ] ) ) {
    							$value = json_decode( $value, true );
    						}
    						$file_name = "{$latest_directory}/{$base}.json";
    						file_put_contents( $file_name, json_encode( $value, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES ) );
    					}
    				}
    			}
    		}
    	}
    }Code language: PHP (php)

    Note: This was a one-and-done hack, don’t judge my variable names.

    The Recovery Steps

    1. Mount or locate your backup folder.
    2. Dump the LevelDB files: ./leveldbutil dump "/path/to/<profile>.leveldb" > tampermonkey-dump.txt
    3. Run ScriptSalvager: php scriptsalvager.php
    4. Cross your fingers and hope it dumps out all of your scripts.

    Lessons (That I Probably Won’t Learn)

    • Always export your userscripts from Tampermonkey–seriously.
    • Add them to your actual backup routine.
    • Or just never switch laptops again. 😬

    Happy salvaging!

  • Disabling Laptop Screen with Ubuntu Server

    Disabling Laptop Screen with Ubuntu Server

    As a part of my home network setup, I use an old Dell Studio XPS 15″ laptop as my “server.” It’s currently running Ubuntu 19.10. One issue I had during the last update was that whatever I set up a long time ago to manage the lid switch policy was lost, so I had to re-google it and re-do it, because I don’t want the screen to be on 24/7.

    Some Googling led me to a post that got me part of the way there:

    https://mensfeld.pl/2018/08/ubuntu-18-04-disable-screen-on-lid-close/

    I had to adjust a few things for my setup, and for posterity I’m going to log them here for the future.

    # As root, or sudo'd
    apt install vbetool -y
    echo 'event=button/lid.*' | tee --append /etc/acpi/events/lm_lid
    echo 'action=/etc/acpi/lid.sh' | tee --append /etc/acpi/events/lm_lid
    touch /etc/acpi/lid.sh
    chmod +x /etc/acpi/lid.shCode language: PHP (php)

    This will set everything up for the lid.sh script to run:

    #!/bin/bash
    # Close
    grep -q close /proc/acpi/button/lid/*/state
    if [ $? = 0 ]; then
    	sleep 1 &amp;&amp; vbetool dpms off
    fi
    # Open
    grep -q open /proc/acpi/button/lid/*/state
    if [ $? = 0 ]; then
    	sleep 1 &amp;&amp; vbetool dpms on
    fiCode language: HTML, XML (xml)

    Finally, reboot and enjoy not having to worry about the screen dying.

  • Disabling plugin deactivation in WordPress

    Disabling plugin deactivation in WordPress

    The problem came up recently about how to make sure plugins activated in the WordPress plugin UI don’t get deactivated if they are necessary for a site to function.  I thought that was an interesting thought puzzle worth spending 15 minutes on, so I came up with this function as a solution:

    function dt_force_plugin_active( $plugin ) {
    add_filter( 'pre_update_option_active_plugins', function ( $active_plugins ) use ( $plugin ) {
    // Match if properly named: wp-plugin (wp-plugin/wp-plugin.php).
    $proper_plugin_name = $plugin . '/' . $plugin . '.php';
    <pre><code>    if (
            file_exists( WP_PLUGIN_DIR . '/' . $proper_plugin_name )
            &amp;amp;&amp;amp; is_file( WP_PLUGIN_DIR . '/' . $proper_plugin_name )
            &amp;amp;&amp;amp; ! in_array( $proper_plugin_name, $active_plugins, true )
        ) {
            $active_plugins[] = $proper_plugin_name;
            return array_unique( $active_plugins );
        }
    
        // Match if improperly named: wp-plugin/cool-plugin.php.
        if (
            file_exists( WP_PLUGIN_DIR . '/' . $plugin )
            &amp;amp;&amp;amp; is_file( WP_PLUGIN_DIR . '/' . $plugin )
            &amp;amp;&amp;amp; ! in_array( $plugin, $active_plugins, true )
        ) {
            $active_plugins[] = $plugin;
            return array_unique( $active_plugins );
        }
    
        return array_unique( $active_plugins );
    }, 1000, 1 );Code language: PHP (php)

    Which can be activated in your theme’s functions.php like so:

    dt_force_plugin_active( 'akismet' ); or dt_force_plugin_active( 'wordpress-seo/wp-seo.php' );

    The only downside that I’ve seen so far is that you still get the Plugin deactivated. message in the admin notices.

  • Renewing Let’s Encrypt SSL on SABnzbd+

    Renewing Let’s Encrypt SSL on SABnzbd+

    Having a secure way to manage your usenet downloads of the hit movie Big Buck Bunny with SABnzbd+ is great, but one problem/feature of Let’s Encrypt is that the SSL certificates expire only after three months, requiring plenty of renewals.  Luckily this can be easily scripted and forgotten.

    The primary part of renewing the SSL certificates will be handled by a modified version of Erika Heidi‘s le-renew.sh script.  Erika’s script does a few things we don’t need, such as restarting Apache, so I forked it on GitHub and made a few changes.

    (more…)
  • Let’s Encrypt SSL on SABnzbd+

    Let’s Encrypt SSL on SABnzbd+

    Let’s Encrypt has been in public beta for some time now, so I thought it was time for me to test it out and see how it works.

    I’ve been working on some automation for Let’s Encrypt, WordPress Multisite, Domain Mapping, and Apache for a while, but I don’t have anything that I feel comfortable sharing yet.

    For now though, I was able to get Let’s Encrypt to work with SABnzbd+, which is a binary newsgroup downloader for things such as Linux ISOs.

    (more…)