<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0"
	xmlns:content="http://purl.org/rss/1.0/modules/content/"
	xmlns:wfw="http://wellformedweb.org/CommentAPI/"
	xmlns:dc="http://purl.org/dc/elements/1.1/"
	xmlns:atom="http://www.w3.org/2005/Atom"
	xmlns:sy="http://purl.org/rss/1.0/modules/syndication/"
	xmlns:slash="http://purl.org/rss/1.0/modules/slash/"
	>

<channel>
	<title>my tech blog &#187; Drupal</title>
	<atom:link href="http://billauer.se/blog/category/drupal/feed/" rel="self" type="application/rss+xml" />
	<link>https://billauer.se/blog</link>
	<description>Anything I found worthy to write down.</description>
	<lastBuildDate>Thu, 12 Mar 2026 11:36:00 +0000</lastBuildDate>
	<language>en</language>
	<sy:updatePeriod>hourly</sy:updatePeriod>
	<sy:updateFrequency>1</sy:updateFrequency>
	<generator>http://wordpress.org/?v=3.1.2</generator>
		<item>
		<title>Linux Malware Detect for occasional non-root use</title>
		<link>https://billauer.se/blog/2015/12/non-root-lmd/</link>
		<comments>https://billauer.se/blog/2015/12/non-root-lmd/#comments</comments>
		<pubDate>Thu, 31 Dec 2015 08:20:48 +0000</pubDate>
		<dc:creator>eli</dc:creator>
				<category><![CDATA[Drupal]]></category>
		<category><![CDATA[Linux]]></category>
		<category><![CDATA[Server admin]]></category>
		<category><![CDATA[Software]]></category>

		<guid isPermaLink="false">https://billauer.se/blog/?p=4908</guid>
		<description><![CDATA[Intro This is a minimal HOWTO on installing Linux Malware Detect for occasional use as a regular non-root user. Not that I&#8217;m so sure it&#8217;s worth bothering, given that contemporary exploit code seems to be able to go under its radar. Background One not-so-bright afternoon, I got a sudden mail from my web hosting provider [...]]]></description>
			<content:encoded><![CDATA[<h3>Intro</h3>
<p>This is a minimal HOWTO on installing Linux Malware Detect for occasional use as a regular non-root user. Not that I&#8217;m so sure it&#8217;s worth bothering, given that contemporary exploit code seems to be able to go under its radar.</p>
<h3>Background</h3>
<p>One not-so-bright afternoon, I got a sudden mail from my web hosting provider saying that my account has been shut down immediately due to malware detected in my files (citation is slightly censored):</p>
<blockquote><p><span style="font-family: Verdana,Arial,Helvetica; font-size: x-small;">Hello,<br />
Our routine malware scanner has reported files on your account as malicious. Pasted below is the report for your confirmation. Your account hosts old, outdated and insecure scripts which needs to be updated asap. Please reply back to this email so that we can work this out.</span></p>
<p>====================================<br />
HOST: &#8212;&#8212;-<br />
SCAN ID: 151230-0408.31792<br />
STARTED: Dec 30 2015 04:08:40 -0500<br />
TOTAL HITS: 1<br />
TOTAL CLEANED: 0</p>
<p>FILE HIT LIST:<br />
{HEX}php.base64.v23au.185 : /home/&#8212;&#8212;/public_html/modules/toolbar/javascript21.php =&gt; /usr/local/maldetect/quarantine/javascript21.php.295615562<br />
===============================================</p></blockquote>
<p>I was lucky enough to have a backup of my entire hosted subdirectory, so I made a new backup, ran</p>
<pre>$ find . -type f | while read i ; do sha1sum "$i" ; done &gt; ../now-sha1.txt</pre>
<p>on the good and bad, and then compared the output files. This required some manual cleanup of several new PHP files which contained all kind of weird stuff.</p>
<p>In hindsight, it seems like the malware PHP files were created during an <a href="https://www.drupal.org/SA-CORE-2014-005" target="_blank">SQL injection attack on Drupal 7</a> back in October 2014 (read again: an SQL injection attack in 2014. It&#8217;s as if a malaria breakout would occur in Europe today). The web host did patch the relevant file for me (without me knowing about it, actually), but only a couple of days after the attack broke loose. Then the files remained undetected for about a year, after which only one of these was nailed down. The malware PHP code is clearly crafted to be random, so it works around pattern detection.</p>
<p>Now, when we&#8217;re convinced that Linux Malware Detect actually <strong>doesn&#8217;t</strong> find malware, let&#8217;s install it.</p>
<h3>Installing</h3>
<p>There are plenty of guides on the web. Here&#8217;s my own take.</p>
<pre>$ git clone https://github.com/rfxn/linux-malware-detect.git</pre>
<p>For those curious on which revision I&#8217;m using:</p>
<pre>$ git rev-parse HEAD
190f56e8704213fab233a5ac62820aea02a055b2</pre>
<p>Change directory to linux-malware-detect/, and as root:</p>
<pre># ./install.sh
Linux Malware Detect v1.5
            (C) 2002-2015, R-fx Networks &lt;proj@r-fx.org&gt;
            (C) 2015, Ryan MacDonald &lt;ryan@r-fx.org&gt;
This program may be freely redistributed under the terms of the GNU GPL

installation completed to /usr/local/maldetect
config file: /usr/local/maldetect/conf.maldet
exec file: /usr/local/maldetect/maldet
exec link: /usr/local/sbin/maldet
exec link: /usr/local/sbin/lmd
cron.daily: /etc/cron.daily/maldet
maldet(15488): {sigup} performing signature update check...
maldet(15488): {sigup} could not determine signature version
maldet(15488): {sigup} signature files missing or corrupted, forcing update...
maldet(15488): {sigup} new signature set (2015121610247) available
maldet(15488): {sigup} downloading http://cdn.rfxn.com/downloads/maldet-sigpack.tgz
maldet(15488): {sigup} downloading http://cdn.rfxn.com/downloads/maldet-cleanv2.tgz
maldet(15488): {sigup} verified md5sum of maldet-sigpack.tgz
maldet(15488): {sigup} unpacked and installed maldet-sigpack.tgz
maldet(15488): {sigup} verified md5sum of maldet-clean.tgz
maldet(15488): {sigup} unpacked and installed maldet-clean.tgz
maldet(15488): {sigup} signature set update completed
maldet(15488): {sigup} 10822 signatures (8908 MD5 / 1914 HEX / 0 USER)</pre>
<h3>Reduce installation</h3>
<p>Remove cronjobs: First /etc/cron.d/maldet_pub</p>
<pre>*/10 * * * * root /usr/local/maldetect/maldet --mkpubpaths &gt;&gt; /dev/null 2&gt;&amp;1</pre>
<p>and also /etc/cron.daily/maldet (scan through everything daily, I suppose):</p>
<pre>#!/usr/bin/env bash
export PATH=/bin:/sbin:/usr/bin:/usr/sbin:/usr/local/bin:/usr/local/sbin:$PATH
export LMDCRON=1
. /usr/local/maldetect/conf.maldet
if [ -f "/usr/local/maldetect/conf.maldet.cron" ]; then
	. /usr/local/maldetect/conf.maldet.cron
fi
find=`which find 2&gt; /dev/null`
if [ "$find" ]; then
	# prune any quarantine/session/tmp data older than 7 days
	tmpdirs="/usr/local/maldetect/tmp /usr/local/maldetect/sess /usr/local/maldetect/quarantine /usr/local/maldetect/pub"
	for dir in $tmpdirs; do
	 if [ -d "$dir" ]; then
	  $find $dir -type f -mtime +7 -print0 | xargs -0 rm -f &gt;&gt; /dev/null 2&gt;&amp;1
	 fi
	done
fi

if [ "$autoupdate_version" == "1" ] || [ "$autoupdate_signatures" == "1" ]; then
	# sleep for random 1-999s interval to better distribute upstream load
	sleep $(echo $RANDOM | cut -c1-3) &gt;&gt; /dev/null 2&gt;&amp;1
fi

if [ "$autoupdate_version" == "1" ]; then
	# check for new release version
	/usr/local/maldetect/maldet -d &gt;&gt; /dev/null 2&gt;&amp;1
fi

if [ "$autoupdate_signatures" == "1" ]; then
	# check for new definition set
	/usr/local/maldetect/maldet -u &gt;&gt; /dev/null 2&gt;&amp;1
fi

# if we're running inotify monitoring, send daily hit summary
if [ "$(ps -A --user root -o "cmd" | grep maldetect | grep inotifywait)" ]; then
        /usr/local/maldetect/maldet --monitor-report &gt;&gt; /dev/null 2&gt;&amp;1
else
	if [ -d "/home/virtual" ] &amp;&amp; [ -d "/usr/lib/opcenter" ]; then
		# ensim
	        /usr/local/maldetect/maldet -b -r /home/virtual/?/fst/var/www/html/,/home/virtual/?/fst/home/?/public_html/ 1 &gt;&gt; /dev/null 2&gt;&amp;1
	elif [ -d "/etc/psa" ] &amp;&amp; [ -d "/var/lib/psa" ]; then
		# psa
		/usr/local/maldetect/maldet -b -r /var/www/vhosts/?/ 1 &gt;&gt; /dev/null 2&gt;&amp;1
        elif [ -d "/usr/local/directadmin" ]; then
                # DirectAdmin
                /usr/local/maldetect/maldet -b -r /home?/?/domains/?/public_html/,/var/www/html/?/ 1 &gt;&gt; /dev/null 2&gt;&amp;1
	elif [ -d "/var/www/clients" ]; then
		# ISPConfig
                /usr/local/maldetect/maldet -b -r /var/www/clients/?/web?/web 1 &gt;&gt; /dev/null 2&gt;&amp;1
	elif [ -d "/etc/webmin/virtual-server" ]; then
		# Virtualmin
                /usr/local/maldetect/maldet -b -r /home/?/public_html/,/home/?/domains/?/public_html/ 1 &gt;&gt; /dev/null 2&gt;&amp;1
	elif [ -d "/usr/local/ispmgr" ]; then
		# ISPmanager
		/usr/local/maldetect/maldet -b -r /var/www/?/data/,/home/?/data/ 1 &gt;&gt; /dev/null 2&gt;&amp;1
	elif [ -d "/var/customers/webs" ]; then
		# froxlor
		/usr/local/maldetect/maldet -b -r /var/customers/webs/ 1 &gt;&gt; /dev/null 2&gt;&amp;1
	else
		# cpanel, interworx and other standard home/user/public_html setups
	        /usr/local/maldetect/maldet -b -r /home?/?/public_html/,/var/www/html/,/usr/local/apache/htdocs/ 1 &gt;&gt; /dev/null 2&gt;&amp;1
	fi
fi</pre>
<p>And then remove the bootup hooks (I could and should have done this with chkconfig, actually):</p>
<pre># rm `find /etc/rc.d/ -iname S\*maldet\*`
rm: remove symbolic link `/etc/rc.d/rc3.d/S70maldet'? y
rm: remove symbolic link `/etc/rc.d/rc4.d/S70maldet'? y
rm: remove symbolic link `/etc/rc.d/rc2.d/S70maldet'? y
rm: remove symbolic link `/etc/rc.d/rc5.d/S70maldet'? y</pre>
<h3>Configuration</h3>
<p>Edit /usr/local/maldetect/conf.maldet. The file is self-explained. The defaults are quite non-intrusive (no quarantine nor cleaning by default, no user suspension etc.). I turned off the automatic updates (I don&#8217;t run this as a cron job anyhow) and opted in scans by users:</p>
<pre>scan_user_access="1"</pre>
<p>Other than that, I kept it as is.</p>
<h3>Preparing for run as non-root user</h3>
<p>As a regular user (&#8220;eli&#8221;) I went</p>
<pre>$ maldet
touch: cannot touch `/usr/local/maldetect/pub/eli/event_log': No such file or directory
/usr/local/maldetect/internals/functions: line 31: cd: /usr/local/maldetect/pub/eli/tmp: No such file or directory
mkdir: cannot create directory `/usr/local/maldetect/pub/eli': Permission denied
chmod: cannot access `/usr/local/maldetect/pub/eli/tmp': No such file or directory
mkdir: cannot create directory `/usr/local/maldetect/pub/eli': Permission denied
chmod: cannot access `/usr/local/maldetect/pub/eli/sess': No such file or directory
mkdir: cannot create directory `/usr/local/maldetect/pub/eli': Permission denied
chmod: cannot access `/usr/local/maldetect/pub/eli/quar': No such file or directory
sed: couldn't open temporary file /usr/local/maldetect/sedIuE2ll: Permission denied

<span style="color: #888888;">[...]</span></pre>
<p>So it expects a directory accessible by non-root self. Let&#8217;s make one (as root)</p>
<pre># cd /usr/local/maldetect/pub/
# mkdir eli
# chown eli:eli eli</pre>
<h3>Giving it a try</h3>
<p>Try</p>
<pre>$ maldet -h</pre>
<p>And performing a scan (checking a specific sub-directory on my Desktop):</p>
<pre>$ maldet -a /home/eli/Desktop/hacked/
sed: couldn't open temporary file /usr/local/maldetect/sedcSyxa1: Permission denied
Linux Malware Detect v1.5
            (C) 2002-2015, R-fx Networks &lt;proj@rfxn.com&gt;
            (C) 2015, Ryan MacDonald &lt;ryan@rfxn.com&gt;
This program may be freely redistributed under the terms of the GNU GPL v2

ln: creating symbolic link `/usr/local/maldetect/sigs/lmd.user.ndb': Permission denied
ln: creating symbolic link `/usr/local/maldetect/sigs/lmd.user.hdb': Permission denied
/usr/local/maldetect/internals/functions: line 1647: /usr/local/maldetect/tmp/.runtime.hexsigs.18117: Permission denied
maldet(18117): {scan} signatures loaded: 10822 (8908 MD5 / 1914 HEX / 0 USER)
maldet(18117): {scan} building file list for /home/eli/Desktop/hacked/, this might take awhile...
maldet(18117): {scan} setting nice scheduler priorities for all operations: cpunice 19 , ionice 6
maldet(18117): {scan} file list completed in 0s, found 8843 files...
maldet(18117): {scan} scan of /home/eli/Desktop/hacked/ (8843 files) in progress...
maldet(18117): {scan} 8843/8843 files scanned: 0 hits 0 cleaned
maldet(18117): {scan} scan completed on /home/eli/Desktop/hacked/: files 8843, malware hits 0, cleaned hits 0, time 253s
maldet(18117): {scan} scan report saved, to view run: maldet --report 151231-0915.18117</pre>
<p>Uh, that was really bad. The directory contains several malware PHP files. Maybe the signature isn&#8217;t updated? The file my hosting provider detected was quarantined, and those that were left are probably sophisticated enough to go under the radar.</p>
<h3>Update the signature file</h3>
<p>Since I turned off the automatic update of signature files, I have to do this manually. As root,</p>
<pre># maldet -u
Linux Malware Detect v1.5
            (C) 2002-2015, R-fx Networks &lt;proj@rfxn.com&gt;
            (C) 2015, Ryan MacDonald &lt;ryan@rfxn.com&gt;
This program may be freely redistributed under the terms of the GNU GPL v2

maldet(15175): {sigup} performing signature update check...
maldet(15175): {sigup} local signature set is version 2015121610247
maldet(15175): {sigup} latest signature set already installed</pre>
<p>Well, no wonder, I just installed maldet.</p>
<p>So the bottom line, mentioned above, is that this tool isn&#8217;t all that effective against the specific malware I got.</p>
]]></content:encoded>
			<wfw:commentRss>https://billauer.se/blog/2015/12/non-root-lmd/feed/</wfw:commentRss>
		<slash:comments>3</slash:comments>
		</item>
		<item>
		<title>Drupal 7: Making a global regular expression replacement</title>
		<link>https://billauer.se/blog/2013/08/d7-token-parameters-javascript/</link>
		<comments>https://billauer.se/blog/2013/08/d7-token-parameters-javascript/#comments</comments>
		<pubDate>Sat, 17 Aug 2013 15:29:07 +0000</pubDate>
		<dc:creator>eli</dc:creator>
				<category><![CDATA[Drupal]]></category>
		<category><![CDATA[JavaScript]]></category>
		<category><![CDATA[Server admin]]></category>

		<guid isPermaLink="false">https://billauer.se/blog/?p=3712</guid>
		<description><![CDATA[The goal: Create spam-harvesting safe mailto links anywhere in the pages, generated on the fly with slightly obfuscated JavaScript, so that harvesting bots don&#8217;t get the email addresses. To make it slightly more complicateded, I want a replacement pattern with a parameter, telling the replacement script which email address to inject. In other words, if [...]]]></description>
			<content:encoded><![CDATA[<p>The goal: Create spam-harvesting safe mailto links anywhere in the pages, generated on the fly with slightly obfuscated JavaScript, so that harvesting bots don&#8217;t get the email addresses. To make it slightly more complicateded, I want a replacement pattern with a parameter, telling the replacement script which email address to inject.</p>
<p>In other words, if I wrote %mail{1}% somewhere in the page&#8217;s source, that is replaced by the second email address in a known list of addresses.</p>
<p>Of course there the Token module. But I wanted the replacement to depend on the token string. Globally.</p>
<p>I use arthemia, so this appears everywhere in the paths to files.</p>
<h3>Bad attempt #1</h3>
<p>I thought that dvessel&#8217;s suggestion on a <a href="https://drupal.org/node/262085" target="_blank">pretty old page</a> would do the job (and it looks like it was obvious to everyone which file to edit, sites/all/themes/arthemia/template.php). But that suggestion was good for Drupal 5.</p>
<p>The theme I used, arthemia, already has an arthemia_preprocess_page() function, but not an arthemia_render_template(). I followed the suggestion, and nothing happened, of course. Drupal 7 doesn&#8217;t look in that direction.</p>
<h3>Bad attempt #2</h3>
<p>There is this golden rule in Drupal, that you don&#8217;t edit the core functions. Well, well, it was time to break it again. This time with the core of the core: include/common.inc, modifying drupal_render. Yup, I couldn&#8217;t have broken that rule harder. But after a few hours of the regular Drupal ceremony of reading a lot of useless tips on the web, I thought it was best to just get the job done.</p>
<p>The main problem was that all hooks are related to a certain type of elements. There is no hook for &#8220;just before the HTML is handed over to the browser&#8221;. Or that&#8217;s the impression I got.</p>
<p>First, I added the script I wanted included in every page. It would prefer it to be in the &lt;head&gt; section (because that&#8217;s usually where global scripts are) and I would prefer it to be a piece of content. But again, hours of looking for the &#8220;Drupal&#8221; way to do it, I just added the following to the top of sites/all/themes/arthemia/page.tpl.php:</p>
<pre>&lt;script language="JavaScript"&gt;
&lt;!--
function myfunction(i) {
 [ ... ]
}
--&gt;
&lt;/script&gt;</pre>
<p>It&#8217;s an ugly solution, but it gets the job done. I could, in theory, plant it as the content of an unused part of the page, and then mangle the page&#8217;s structure as it was being processed. Too much work for the cute shortcut of altering the script from Drupal&#8217;s interface.</p>
<p>And now to finding the markups, and make them run the function above. Using the <a href="http://www.php.net/manual/en/function.preg-replace-callback.php" target="_blank">preg_replace_callback()</a> function, render_template was edited to this in the end of the function (just before &#8220;return $output&#8221;):</p>
<pre>// Mangle
 $output = preg_replace_callback("|%mail\{(\d+)\}%|",
   function ($matches) {
     return "&lt;script language=\"JavaScript\"&gt;\n&lt;!--\nmyfunction($matches[1]);\n--&gt;\n&lt;/script&gt;";
   },
   $output)</pre>
<p>That worked well. Actually, too well. The replacement took place also in editing boxes, so the original markup text couldn&#8217;t be edited.</p>
<h3>The correct way</h3>
<p>OK, so I took one step back. Let&#8217;s use the regular hooking mechanism, after all, and define the hooks for the content and the footer. Good enough. And as a side effect, the core is left intact, as I only edit the theme&#8217;s page template file.</p>
<p>So <strong>instead</strong> of the changes above I added this to the top of sites/all/themes/arthemia/page.tpl.php:</p>
<pre>&lt;script language="JavaScript"&gt;
&lt;!--
function myfunction(i) {

 [ ... ]
}
--&gt;
&lt;/script&gt;

&lt;?php
function mail_post_render($content, $element) {
  $content = preg_replace_callback("|%mail\{(\d+)\}%|",
    function ($matches) {
      return "&lt;script language=\"JavaScript\"&gt;\n&lt;!--\nmyfunction($matches[1]);\n--&gt;\n&lt;/script&gt;";
    },
    $content);

 return $content;
}

if ($page['content']) {
 $page['content']['#post_render'] = array('mail_post_render');
}
if ($page['footer']) {
 $page['footer']['#post_render'] = array('mail_post_render');
}

?&gt;</pre>
<p>And that finally worked in a sane manner: The footer and content were mangled, and I was still able to edit the original text. Hurray!</p>
<p>As usual, a simple task took a full day to complete. Welcome to the wonderful world of Drupal.</p>
]]></content:encoded>
			<wfw:commentRss>https://billauer.se/blog/2013/08/d7-token-parameters-javascript/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>Making a click-for-help window with jQuery&#8217;s tooltip widget</title>
		<link>https://billauer.se/blog/2013/02/jquery-tooltip-click-for-help/</link>
		<comments>https://billauer.se/blog/2013/02/jquery-tooltip-click-for-help/#comments</comments>
		<pubDate>Wed, 06 Feb 2013 21:18:19 +0000</pubDate>
		<dc:creator>eli</dc:creator>
				<category><![CDATA[Drupal]]></category>
		<category><![CDATA[Internet]]></category>
		<category><![CDATA[JavaScript]]></category>
		<category><![CDATA[Server admin]]></category>
		<category><![CDATA[Software]]></category>

		<guid isPermaLink="false">https://billauer.se/blog/?p=3427</guid>
		<description><![CDATA[Intro I needed some neat overlay to appear when the user clicks those question-mark icons on a specific, Drupal-based page. So I went for using jQuery&#8217;s tooltip widget, with due adaptations. It turned out to be a wise choice. The idea is simple: Click on the question mark, an overlay window appears right underneath it, [...]]]></description>
			<content:encoded><![CDATA[<h3>Intro</h3>
<p>I needed some neat overlay to appear when the user clicks those question-mark icons on a specific, Drupal-based page. So I went for using jQuery&#8217;s tooltip widget, with due adaptations. It turned out to be a wise choice.</p>
<p>The idea is simple: Click on the question mark, an overlay window appears right underneath it, and stays there. Click on the &#8220;X&#8221; at the window&#8217;s top right, the window closes. Simple and clean.</p>
<p>These are my notes as I set this up. Most of the work is around the fact that tooltips usually appear and disappear on mouse hovering, not clicks.</p>
<h3>Getting a bundle for a tooltip</h3>
<p>Build your own set and download on jQuery&#8217;s <a href="http://jqueryui.com/download/" target="_blank">official site</a>. For a plain tooltip activation, pick the following options: All UI Core, no Interactions. Widgets: Only Tooltip. Effects: Only Effects Core and Fade Effect.</p>
<p>From the downloaded bundle, the following inclusions (in the &lt;head&gt; section) suffice (version numbers may vary):</p>
<pre>&lt;style&gt;@import url("jquery-ui-1.10.0.custom.css");&lt;/style&gt;</pre>
<p>and</p>
<pre>&lt;script type="text/javascript" src="jquery-1.9.0.js"&gt;&lt;/script&gt;
&lt;script type="text/javascript" src="jquery-ui-1.10.0.custom.min.js"&gt;&lt;/script&gt;</pre>
<p>The &#8220;.min&#8221; version is just a condensed version of the non-min JavaScript file. The rest of the bundle can be ignored for a minimal tooltip implementation.</p>
<h3>In a Drupal environment</h3>
<p>Since Drupal uses jQuery as well, the initial idea was to rely on the script loaded anyhow, but I got the &#8220;$ is not a function&#8221; error in the error console. To solve this, the script inclusions shown above should appear after Drupal&#8217;s. In theory, the <a href="http://docs.jquery.com/Core/jQuery.noConflict" target="_blank">jQuery.noConflict()</a> call should be used to save the previous &#8220;$&#8221; function, but since Drupal doesn&#8217;t inject jQuery calls after its last declaration in &lt;head&gt;, there is no need for this. This is OK for me, since I work on one specific page, but it&#8217;s theoretically possible that other Drupal pages put JavaScript in the &lt;body&gt;. So keep this in mind.</p>
<p>Maybe the best way is to load your own jQuery script bundle before Drupal&#8217;s, run noConflict() and then load Drupal&#8217;s. Just to be safe.</p>
<h3>IE6 note</h3>
<p>Internet Explorer 6 is an ancient thing, but since I always check against antique browsers, I noted that if there&#8217;s a selection box under the tooltip, it will appear above the tooltip, despite the latter&#8217;s z-index set to 9999. This is a bug <a href="http://coding.smashingmagazine.com/2009/09/15/the-z-index-css-property-a-comprehensive-look/" target="_blank">specific to IE6</a>, and therefore nothing to worry about too much. Anyone with IE6 is used to pages appearing ugly anyhow.</p>
<h3>Declaring the jQuery handlers and hooks</h3>
<p>This is a general note on how to inject jQuery definitions into the HTML.</p>
<p>Following <a href="http://api.jquery.com/ready/" target="_blank">jQuery&#8217;s recommendation</a>, the application-specific definitions are made in the &lt;head&gt; section, in an anonymous function which is triggered by the &#8220;ready&#8221; event. This makes the jQuery hooks up and running as soon as the DOM hierarchy is set up, but doesn&#8217;t requires the onload event, which is triggered only when all assets (e.g. images) have been loaded.</p>
<p>So to run a tooltip on all elements marked &#8220;myclass&#8221; (they need to have a title= assignment),</p>
<pre>&lt;script&gt;
$(function() {
 $( ".myclass" ).tooltip();
});
&lt;/script&gt;</pre>
<p>The hooking to the &#8220;ready&#8221; event is implicit by the $(function() { &#8230; }) declaration.</p>
<h3>Appear on click, don&#8217;t disappear</h3>
<p>To make a &#8220;Hello world&#8221; tooltip appear on clicking an image of class &#8220;helpimg&#8221;, this goes into the &lt;head&gt; section (partly taken from <a href="https://forum.jquery.com/topic/tooltip-widget-open-on-click-instead-of-hover" target="_blank">here</a>):</p>
<pre>&lt;script&gt;
$(function() {
 $( ".helpimg" ).tooltip({
   content: "&lt;i&gt;Hello, world&lt;/i&gt;",
   items: 'img'
 })
 .off( "mouseover" )
 .on( "click", function(){
    var clicked = this;
    $( ".helpimg" ).each(function (i) {
      if (this != clicked) {
         $( this ).tooltip( "close" );
      }
    });

    $( this ).tooltip( "open" );
    <span style="color: #ff0000;">$( this ).unbind( "mouseleave" );</span>
    return false;
 });

 $( "body" ).on( "click", ".helpclose", function(){
   $( ".helpimg" ).each(function (i) { $( this ).tooltip( "close" ); });
   return false;
 });
});
&lt;/script&gt;</pre>
<p>This little script does two things: Assign a tooltip object for each DOM element of class &#8220;helpimg&#8221; (for opening the tooltip) and an event handler to each &#8220;helpclose&#8221; element (for closing it).</p>
<p>See the <a href="http://api.jqueryui.com/tooltip/" target="_blank">tooltip API reference</a> for the meanings of the attributes (content, item etc.).</p>
<h3>Open the tooltip (.helpimg assignment)</h3>
<p>The &#8220;content&#8221; attribute obviously sets the tooltip&#8217;s content. Here it&#8217;s given as a string itself, which is OK if the data is known when the tooltip is generated. Another possibility is to put a function here. Unlike what some documentation implies, <strong>there is no bodyHandler</strong> attribute (on jquery-ui-1.10.0). To provide a callback, a function is given instead of the string. For example, to put the ID of the element clicked on in the tooltip box, it should say (<strong>replacing</strong> the content: assignment above):</p>
<pre>content: function() {
  return $( this ).attr( "id" );
},</pre>
<p>The &#8220;item&#8221; attribute defines the item type(s) involved. If this is absent, the click operation works only once: After the tooltip fades away, another click on the same item do nothing. Frankly, I don&#8217;t understand why this is.</p>
<p>The anonymous function defined in the <a href="http://api.jquery.com/on/" target="_blank">.on() </a>call is called when the element is clicked. Its first part (the definition of &#8220;clicked&#8221; and the &#8220;<a href="http://api.jquery.com/each/" target="_blank">each</a>&#8221; call) scans all elements of class &#8220;helpimg&#8221; and sends them a &#8220;close&#8221; command. This part is optional, to maintain an exclusive tooltip open. The if-statement in the <a href="http://api.jquery.com/each/" target="_blank">each-function</a> prevents closing the tooltip that is just about to be opened, to prevent flickering. Maybe it&#8217;s better to let the flickering happen, so that the user gets some feedback on the click. A matter of taste, I suppose.</p>
<p>Note that the function defined in the &#8220;each&#8221; call has a variable, which is the index. It&#8217;s not used in this case.</p>
<p>The call to <a href="http://api.jquery.com/unbind/" target="_blank">unbind() </a>(marked in red) kills the &#8220;mouseleave&#8221; event handler(s) that are related to the tooltip, so it doesn&#8217;t listen to this event. Normally, the &#8220;open&#8221; method sets an event handler for either &#8220;mouseleave&#8221; or &#8220;focusout&#8221;, depending on whether the triggering event was &#8220;mouseover&#8221; or &#8220;focusin&#8221;, respectively.</p>
<p>There is a non-specific call to unbind(), which makes the tooltip deaf to any event, but that made the tooltip appear only on the first click and then not again. So obviously there are some events handlers that should be left (maybe timers?).</p>
<h3>Closing the tooltip</h3>
<p>Since the tooltip won&#8217;t close by itself, this must be done by clicking on some element. The dedicated elements (actually, top-right &#8220;X&#8221; boxes) are given the &#8220;helpclose&#8221; class. When one of those elements get the &#8220;onclick&#8221; event, all tooltips are closed, with a function identical to the one used before opening a tooltip.</p>
<p>The important difference is the selection of these elements. Since they are hidden when the script is run, selecting them with $( &#8220;.xillyclose&#8221;).on( &#8220;click&#8221;, &#8230;) will catch nothing. The solution is to delegate the event to the &#8220;body&#8221; object, which is of course visible, and push the selector to within the <a href="http://api.jquery.com/on/" target="_blank">.on() </a>call. This way, the to-be visible elements are selected as they appear.</p>
<h3>Closing on ESC keystroke</h3>
<p>I found myself hitting ESC to close a tooltip, so why not support it? The idea is to catch <a href="http://api.jquery.com/keyup/" target="_blank">keyup</a> events, check if the key code happens to be 27 (ESC), and act as necessary.</p>
<pre>$( document ).on( "keyup", function(event){
 if (event.keyCode == 27) {
 $( ".helpimg" ).each(function (i) { $( this ).tooltip( "close" ); });
 return false;     
}</pre>
<p>I found the key code for ESC by running the demo on the <a href="http://api.jquery.com/keyup/" target="_blank">.keyup()</a> API page. Note that the .keyup() command is just a shorthand for the .on() call used above.</p>
<h3>Position the tooltip</h3>
<p>To get the tooltip properly positioned, the <a href="http://api.jqueryui.com/position/" target="_blank">position property</a> is set up during initialization. The declaration above should therefore start as</p>
<pre>$(function() {
  $( ".helpimg" ).tooltip({
    content: "&lt;i&gt;Hello, world&lt;/i&gt;",
     items: 'img'
     position: {
       my: "center top",
       at: "center bottom+5",
       collision: "fit",
       within: "#content"
     }
[ ... ]</pre>
<p>To avoid confusion, let&#8217;s think about the tooltip as a piece of paper that we want to nail on a billboard.</p>
<p>As described in the <a href="http://api.jqueryui.com/position/" target="_blank">docs</a>, the &#8220;my&#8221; property says where the on the tooltip we should place that nail (on the horizontal center and top of the tooltip), and the &#8220;at&#8221; property says where we should stick that nail (the horizontal center and bottom of the object is relates to, just 5 pixels lower).</p>
<p>The &#8220;collision property&#8221; tells what to do when the positioning would make the tooltip exceed the given area (&#8220;fit&#8221; means push it away from the border as necessary, but don&#8217;t throw it to the opposite side). &#8220;within&#8221; defines the framework in which the tooltip should be placed (the id=&#8221;content&#8221;, which is the main context box, defined by Drupal).</p>
<h3>Conclusion</h3>
<p>It took some time to get the hang of it, but all in all, jQuery is worth it, in particular by making the application code compact and therefore less bug prone.</p>
<p>It also handles those corner cases gracefully and takes care of browser compatibility. After all, what&#8217;s good for Wikipedia is good for me.</p>
]]></content:encoded>
			<wfw:commentRss>https://billauer.se/blog/2013/02/jquery-tooltip-click-for-help/feed/</wfw:commentRss>
		<slash:comments>2</slash:comments>
		</item>
		<item>
		<title>Drupal 7 Views: Making a block of links to related pages</title>
		<link>https://billauer.se/blog/2011/07/d7-sql-views-taxonomy-tags/</link>
		<comments>https://billauer.se/blog/2011/07/d7-sql-views-taxonomy-tags/#comments</comments>
		<pubDate>Sat, 02 Jul 2011 10:06:03 +0000</pubDate>
		<dc:creator>eli</dc:creator>
				<category><![CDATA[Drupal]]></category>
		<category><![CDATA[Internet]]></category>
		<category><![CDATA[Server admin]]></category>

		<guid isPermaLink="false">https://billauer.se/blog/?p=1358</guid>
		<description><![CDATA[Views and SQL Using Drupal views basically is trying to figure out how to trick the machine into making the SQL query I would have written in five minutes. As a matter of fact, I don&#8217;t think I had a chance of getting this right, hadn&#8217;t I known SQL pretty well. Or, as one of [...]]]></description>
			<content:encoded><![CDATA[<h3>Views and SQL</h3>
<p>Using Drupal views basically is trying to figure out how to trick the machine into making the SQL query I would have written in five minutes. As a matter of fact, I don&#8217;t think I had a chance of getting this right, hadn&#8217;t I known SQL pretty well.</p>
<p>Or, as one of the help pages was kind enough to clarify (the left side is merely examples):</p>
<p>SELECT n.title, u.name &lt;–&gt; fields<br />
FROM {node} n base table &lt;–&gt; view type<br />
INNER JOIN {users} u ON n.uid = u.uid &lt;–&gt; relationship<br />
WHERE n.status = 1 &lt;–&gt; filter<br />
AND u.uid = arg(1) &lt;–&gt; argument<br />
ORDER BY n.changed DESC &lt;–&gt; sort</p>
<p>and I&#8217;ll add:</p>
<p>WHERE n.nid = p.nid &lt;&#8211;&gt; Contextual filter<br />
GROUP BY &lt;&#8211;&gt; Aggregation</p>
<p>A word about relationships: The relationship entries in the Views page define the &#8220;ON&#8221; part in the JOIN. To access the values of the fields in the joined tables (e.g. as a displayed field), just select the desired value in the first menu (e.g. &#8220;add field&#8221;), and then pick the administrative name in the &#8220;relationship&#8221; drop-down menu. For some reason, I had the initial expectation that the &#8220;ON&#8221; value would appear in itself in the field list, but it doesn&#8217;t, since it&#8217;s available just as any value, only picked from the respective table.</p>
<h3>Recommended modules</h3>
<p>The Development module (&#8220;devel&#8221;) allows a dump of all SQL queries made while producing the currently shown page. It&#8217;s recommended just to get an idea of what a database hog Drupal is. The second, very recommended module is PHP views, despite its development release status. The latter allows injecting small pieces of PHP exactly where they are needed. In particular, there&#8217;s the PHP:Global pseudofield for both display and sorting, so rather than banging the head on how to twist things around, just write a small and elegant snippet in PHP.</p>
<h3>Injecting a view with PHP</h3>
<p>Possibly using the PHP text format, just type in any node&#8217;s text input (useful for making a view within a book node):</p>
<pre>&lt;?php print views_embed_view('doc_outline', $display_id = 'default') ?&gt;</pre>
<p>where &#8216;doc_outline&#8217; is the machine name for the view (as it appears in URLs related to the view) and &#8216;default&#8217; could be exchanged with &#8216;page&#8217; or &#8216;block&#8217;, but why bother if the view has a single format? See the <a href="http://drupalcontrib.org/api/drupal/contributions--views--views.module/function/views_embed_view/6" target="_blank">API page</a>.</p>
<p>There has also been suggestions about <a href="http://drupal.org/node/26502" target="_blank">embedding a block</a> in PHP, but I never tried that.</p>
<h3>Strategy</h3>
<p>The purpose: Making a &#8220;relevant pages&#8221; block, based upon common tags of the pages. Don&#8217;t tell me Drupal has something doing that, because I know. It&#8217;s just that I&#8217;ve learned that these easy solutions always end up with more work. Besides, I have a special thing I want.</p>
<p>The setting is as follows: I have a special taxonomy named &#8220;relevance terms&#8221;. Pages with common terms in this taxonomy are considered to have similar content. I didn&#8217;t use the original tags, because I may want to use them for something representative.</p>
<p>Also, each content page has an extra field &#8220;LinkText&#8221;, which contains the text to appear on links to the page. For example, the download page&#8217;s title is simply &#8220;Download&#8221; but the link to this page should say something more imperative.</p>
<p>The immediate (and wrong) way to go is to make a view of content titles. Without any filtering, you get all content pages. So use the context filter to get only the current page, and use relations to list all taxonomy terms. Now another relation filter to expand this to pages for each taxonomy term? But the context filter kills any other page than the currently displayed. It&#8217;s a dead end.</p>
<p>So the right way is to make a view of the page&#8217;s taxonomy terms. For each term, list the pages using it, and then squash duplicates. And then make the output nice. Easier said than done.</p>
<h3>First steps</h3>
<p>Add a new View, showing <strong>Taxonomy Terms </strong> of type Relevance Taxonomy. Don&#8217;t create a page, but a block. Display as an HTML list of fields. Save &amp; Exit, and insert the block somewhere in the page, so it can be tested. Previews won&#8217;t work here, because it runs  on Taxonomy nodes, not pages. Set title and such.</p>
<p>The number of items should be limited, and I don&#8217;t use a pager.</p>
<h3>Contextual Filter &amp; Relationship</h3>
<p>This is done first, so we don&#8217;t mess with aggregation, which is going to be set up pretty soon. Under Advanced, add a contextual filter on &#8220;Taxonomy Term ID&#8221;. The following window will complain that there’s no source for contextual filter, so a default must be supplied. This is because we&#8217;re running in block context. The source is taken from the page.</p>
<p>We want the node ID to be compared with the current page, so pick “provide default value” and “Taxonomy term ID from URL”. Uncheck &#8220;Load default filter from term page&#8221; but check &#8220;Load default page from node page&#8221; and also &#8220;Limit terms by vocabulary&#8221; and pick the Relevance Taxonomy as the chosen vocabulary. Under &#8220;More&#8221; check &#8220;Allow multiple values&#8221; This is necessary, since we don&#8217;t want just the first term to be used. I&#8217;m not sure if this item appears without setting up relationships. So if it&#8217;s missing, set up a relationship and come back to add this.</p>
<p>That&#8217;s it. Save and check up. We should now have a simple list of relevance terms in the view.</p>
<p>Next we add a relationship with the pages having the terms: Check &#8220;Taxonomy term: Content using relevance&#8221; (note that &#8220;relevance&#8221; is the name of the vocabulary here), check &#8220;Require this relationship&#8221; on the next screen (I suppose this makes an INNER JOIN as opposed to a LEFT JOIN), and save this.</p>
<p>Checking where we stand, we have each taxonomy term appearing a number of times. This is the natural behaviour of an inner join: Each combinations of terms and pages using them creates a line. Since the pages aren&#8217;t listed, we just see each term repeated.</p>
<p>And since we&#8217;re at it, let&#8217;s eliminate the shown page&#8217;s entry in the related pages&#8217; list. We need negative contextual filtering here: So add a new contextual filter, check &#8220;Content: Nid&#8221; (it wasn&#8217;t there until we added the relationship). Provide default value as &#8220;Content ID of URL&#8221;, and under &#8220;More&#8221; check &#8220;Exclude&#8221;. So if the current page matches the shown page, it&#8217;s not shown.</p>
<p>Save and verify than one or a few items have disappeared from the list.</p>
<h3>Aggregation</h3>
<p>Aggregation is &#8220;GROUP BY&#8221; in SQL, meaning that several rows with the same value in one of the fields are turned into a single row. Instead of the field&#8217;s value we have the count of rows grouped together or the maximum, minimum, average value or whatever the database allows. Aggregation is needed to eliminate the duplicate rows created by the relationship (that is, the inner join).</p>
<p>True, there is a &#8220;Distinct&#8221; checkbox under &#8220;Query settings&#8221; but it&#8217;s ineffective, since each of these duplicate rows are indeed distinct when the database answers the query. What makes them duplicate is the fact that the taxonomy term is dropped in the display. &#8220;Distinct&#8221; just adds the DISTINCT word to the SQL query.</p>
<p>So at this point, change &#8220;Use aggregation&#8221; to Yes. Things will get slightly messier from this point on.</p>
<h3>Adding fields</h3>
<p>Rule #1 for fields: The order they appear <strong>matters</strong>. In particular when using rewrite rules: Inserting data from other fields in substitution patterns works only for fields <strong>above</strong> the displayed one (those declared before).</p>
<p>Remember that the goal is to show the Linktext field as a link to the page, and not just the title.</p>
<p>So the first field to add is the Node&#8217;s path (aliased link). We will use it later on. In the list, check &#8220;Content: Path&#8221;. Under &#8220;Aggregation type&#8221; pick &#8220;Group results together&#8221; which is what we pick all the time if not for any special reason. This choice wouldn&#8217;t appear without enabling aggregation, of course. On the third and last window, check &#8220;Exclude from display&#8221; unless you want to see it for debugging.</p>
<p>The second field to add is the link text. In the list, check &#8220;Content: LinkText&#8221;. Under &#8220;Aggregation type&#8221; pick &#8220;Group results together&#8221; and pick the &#8220;Entity ID&#8221; as group column, and no additional ones.</p>
<p>On the third page uncheck &#8220;Create a label&#8221; (no &#8220;LinkText:&#8221; shown to user). Under &#8220;Rewrite results&#8221; check &#8220;Output this field as a link&#8221;. Write [path] in the Link path text box. This string can be found in the Replacement Pattern list just below. The path was there because it was defined before the current field.</p>
<p>Check &#8220;Use absolute path&#8221; or the links start with a double-slash and render useless.</p>
<p>At this point I&#8217;ll mention that it&#8217;s possible to insert arbitrary HTML with replacement patterns. So it&#8217;s really useful.</p>
<h3>Squashing duplicates</h3>
<p>At this point it&#8217;s pretty evident that we have double entries, and these taxonomy terms should be removed.</p>
<p>So it&#8217;s time to edit the first field: The &#8220;Taxonomy Term: Name&#8221; and check &#8220;Exclude from Display&#8221;. But even more important, enter &#8220;Aggregation settings&#8221; and change &#8220;Aggregation type&#8221; to &#8220;Count&#8221;. The magic is that instead of a row for each taxonomy term, we get a single row with the number of them, ending up with a single row for each link.</p>
<h3>Filter out inaccessible items</h3>
<p>As is, users will see links to items the user can&#8217;t access. So let&#8217;s add a simple filter (&#8220;Filter Criteria&#8221;). Pick &#8220;Content: Published or admin&#8221; and &#8220;Group results together&#8221; in the two following menus. And then just apply on the next menu. Done.</p>
<p>Note that unpublished items will still appear for admins, since the criterion is access. Pick &#8220;Content: Published&#8221; and choose &#8220;Yes&#8221; to remove unpublished items for anyone.</p>
<h3>Sorting</h3>
<p>I have to admit that I failed on this one at first. My intention was to sort the output depending on the number of mutual tags. That would be easy in SQL, since the COUNT() can be applied a label with the AS keyword. I found nothing to assist this in the Views menus.</p>
<p>As it turned out, values of COUNT() <strong>are</strong> available, but not through the menu interface. With the Views PHP module, it&#8217;s a piece of cake.</p>
<p>Say that there&#8217;s already a field saying &#8220;COUNT(Taxonomy term: Name)&#8221;, then add a sort criteria of type global PHP and set the code to</p>
<pre>return ($row2-&gt;name - $row1-&gt;name);</pre>
<p>Since $row1-&gt;name is the <strong>count</strong> of the rows with the same name field, this simple chunk of code does the work.</p>
<h3>Using the view in a non-Drupal page</h3>
<p>Sometimes the whole framework is just too heavy for a site, and all that&#8217;s needed is just a view in a plain PHP file. So if a view block with the name &#8220;inject&#8221; exists in the system, the following code snippet displays it (and a lot of CSS mumbo-jumbo).</p>
<pre>&lt;?php

chdir('trydrupal');

define('DRUPAL_ROOT', getcwd());

require_once DRUPAL_ROOT . '/includes/bootstrap.inc';
drupal_bootstrap(DRUPAL_BOOTSTRAP_FULL);

print views_embed_view('inject', $display_id = 'default')
?&gt;</pre>
<p>Note the chdir(). It allows Drupal to be installed in a completely separate directory (&#8220;trydrupal&#8221; in this case).</p>
<p>&nbsp;</p>
]]></content:encoded>
			<wfw:commentRss>https://billauer.se/blog/2011/07/d7-sql-views-taxonomy-tags/feed/</wfw:commentRss>
		<slash:comments>3</slash:comments>
		</item>
		<item>
		<title>Setting up a Drupal site: Notes to self</title>
		<link>https://billauer.se/blog/2011/06/drupal-modules-notes/</link>
		<comments>https://billauer.se/blog/2011/06/drupal-modules-notes/#comments</comments>
		<pubDate>Sun, 19 Jun 2011 00:44:32 +0000</pubDate>
		<dc:creator>eli</dc:creator>
				<category><![CDATA[Drupal]]></category>
		<category><![CDATA[Internet]]></category>
		<category><![CDATA[JavaScript]]></category>
		<category><![CDATA[Server admin]]></category>

		<guid isPermaLink="false">https://billauer.se/blog/?p=1223</guid>
		<description><![CDATA[This is just a collection of jots I ( = a Drupal newbie) wrote down as I set up a site with Drupal 7.2. Don&#8217;t expect this to be more coherent than a typical shopping list. March 2019 update: Drupal was a huge mistake. Just in case someone out there still has a chance to [...]]]></description>
			<content:encoded><![CDATA[<p>This is just a collection of jots I ( = a Drupal newbie) wrote down as I set up a site with Drupal 7.2. Don&#8217;t expect this to be more coherent than a typical shopping list.</p>
<p><em>March 2019 update: Drupal was a huge mistake. Just in case someone out there still has a chance to escape.</em></p>
<p>And now I appreciate how easy it is to install and use WordPress. Drupal is definitely not good for my health: Hours of frustration trying to accomplish seemingly simple things can&#8217;t do me good. It&#8217;s tempting to conclude, that Drupal contributors have some job-security-by-obscurity kind of mind set. And I do understand why they end up with nasty hacks in PHP: The nice menus offer a million features, except what is really needed.</p>
<h3>General remarks</h3>
<ul>
<li>The development module is a blessing. In particular the ability to display the SQL queries related to a page: Under Modules, go to &#8220;configure&#8221; for the Development Module and check &#8220;Display query log&#8221;. And there&#8217;s an &#8220;A&#8221; next to each row of SQL, which means &#8220;display argument&#8221;. That is, show the query with what was inside the placeholders. Soooo nice.</li>
<li>The &#8220;devel&#8221; tab (part of the development module?) is great: The metadata, with internal names is all listed there. Everything there is to know about a node.</li>
<li>Enable the PHP text format. It allows running PHP code within any node, which is sometimes necessary.</li>
<li>The number of files necessary to display a simple page is absolutely insane: A simple textual home page reaches 46 files (many of which are Javascript and CSS files) with a total of 205 kB (!). No wonder the site is slow.</li>
<li>Take a look on the <a href="http://drupal.org/project/modules">module list </a>which is sorted by number of installations by default. The things you want are most likely in the beginning of this list.</li>
<li>Use URL aliases for every post. A node number says nothing and it&#8217;s a clear loss of Google juice.</li>
<li>CKEditor allows the insertion of a DIV container, for which, among others, the language direction can be chosen. This is a great thing for Hebrew, Arabic and other right-to-left languages.</li>
<li>Drupal&#8217;s taxonomy is hierarchical, so the site&#8217;s hierarchy can be implemented by setting up a hierarchy of terms within a certain vocabulary, and then tag nodes (pages) in order to place them in the menu hierarchy.</li>
<li>Flush the entire cache every now and then when changing things.</li>
<li>The administrative menu gets less quirky as time goes by. Go figure.</li>
<li>Structure &gt; Blocks is where blocks are added to or removed from the site globally.</li>
<li>Special pages are best done as Panels. Note that a panel page is just like a normal page with all the blocks around it.</li>
<li>Special note to self: I&#8217;ve hacked modules/system/html.tpl.php (see below). A Drupal upgrade will require rehacking.</li>
</ul>
<h3>Milestones in setting up the site:</h3>
<ul>
<li>Enable clean URLs</li>
<li>Enable book module</li>
<li>Installing a rich text editor (duh&#8230;). I went for the classic <a href="http://drupal.org/project/wysiwyg" target="_blank">WYSIWYG module</a> (see below)</li>
<li>Remove the &#8220;powered by Drupal&#8221; footer (with all due respect)</li>
<li>Themes go to tar -xzf in sites/all/themes. Switch themes through Appearance (not any submenu)</li>
<li>Downloaded and installed (tar -xzf in sites/all/modules) <a href="http://ftp.drupal.org/files/projects/pathauto-7.x-1.0-rc2.tar.gz">pathauto</a>,  <a href="http://ftp.drupal.org/files/projects/views-7.x-3.0-rc1.tar.gz" target="_blank">Views</a>,  <a href="http://ftp.drupal.org/files/projects/taxonomy_menu-7.x-1.1.tar.gz" target="_blank">Taxonomy Menu</a>, (<del>CCK</del> is part of Drupal 7),  <a href="http://ftp.drupal.org/files/projects/admin_menu-7.x-3.0-rc1.tar.gz" target="_blank">Administration Menu</a>, <del><a href="http://ftp.drupal.org/files/projects/imce-7.x-1.4.tar.gz" target="_blank">IMCE</a></del> (image uploader, I&#8217;ll do that manually), <a href="http://ftp.drupal.org/files/projects/backup_migrate-7.x-2.1.tar.gz" target="_blank">Backup and Migrate</a>, and <a href="http://ftp.drupal.org/files/projects/panels-7.x-3.0-alpha3.tar.gz" target="_blank">Panels</a>. <a href="http://ftp.drupal.org/files/projects/panels-7.x-3.0-alpha3.tar.gz" target="_blank">Ctools </a>was installed as well for Panels. Same goes with <a href="http://ftp.drupal.org/files/projects/token-7.x-1.0-beta2.tar.gz" target="_blank">Token </a>for pathauto. And then <a href="http://ftp.drupal.org/files/projects/devel-7.x-1.0.tar.gz" target="_blank">devel</a>. And <a href="http://ftp.drupal.org/files/projects/disable_messages-7.x-1.0.tar.gz" target="_blank">Disable messages</a>. (<a href="http://ftp.drupal.org/files/projects/page_title-7.x-2.5.tar.gz" target="_blank">Page Title</a> was downloaded but not used to, since it&#8217;s great for everything except what I needed: Set the title from a within a view. It&#8217;s maybe possible, but I couldn&#8217;t figure out how). <a href="http://ftp.drupal.org/files/projects/views_php-7.x-1.x-dev.tar.gz" target="_blank">Views PHP</a> (dev version!) for setting up page title (see below).</li>
<li>And then enabled the modules, of course.</li>
<li>Set up cron (run once an hour). I got a lot of &#8220;access denied&#8221; in the beginning, and then I realized that I should get the link with the cron key under Reports &gt; Status Reports. And then I found out that cron.php denies access when in maintenance mode. So start the cron thing only when the site goes online.<br />
Another thing about cron is that my hosting provider seems to block Curl from running cron jobs with a silly check on the user agent. My updated cron job command hence goes: curl &#8211;silent &#8211;compressed -A &#8216;My cronjob&#8217; &#8216;http://example.com/cron.php?cron_key=(the key supplied on the status page)&#8217;</li>
<li>Set the default page: Configuration &gt; System &gt; Site Information</li>
<li>Enable statistics: Modules &gt; List &gt; Statistics &gt; Configure. Set to never delete log info (I don&#8217;t expect massive traffic).</li>
<li>Disable Taxonomy term pages for users (see below). Same with node-by-number.</li>
<li>Ask web host to increase RAM limit (they gave me 128MB, which is OK)</li>
<li>Enable the &#8220;Disable messages&#8221; module and configure it to display error messages to admins only (why isn&#8217;t this in the core?). This is done by setting permissions (the existence of the module creates new relevant entries). There is no need to configure the module further.</li>
<li>Edit public_html/sites/all/modules/ctools/includes/cleanstring.inc and replace &#8220;\x{d800}&#8221; with &#8220;\x{e000}&#8221; in the expression for CTOOLS_PREG_CLASS_ALNUM. 0xd800 is <a href="https://www.drupal.org/node/1444006" target="_blank">apparently not a legal Unicode point</a>, and without this change there&#8217;s a warning saying exactly that: <em>&#8220;Warning: preg_match(): Compilation failed: disallowed Unicode code point (&gt;= 0xd800 &amp;&amp; &lt;= 0xdfff) at offset 1811 in ctools_cleanstring() (line 157 of /home/theuser/public_html/sites/all/modules/ctools/includes/cleanstring.inc).&#8221;</em></li>
<li>Edit sites/all/themes/arthemia/template.php and change arthemia_primary() at the beginning to read
<pre>function arthemia_primary() {
 $output = '&lt;div id="page-bar"&gt;';
 $the_menu_tree = menu_tree(variable_get('menu_main_links_source','main-menu'));
 $output .= drupal_render($the_menu_tree);
 $output .= '&lt;/div&gt;';
 return $output;
}</pre>
<p>Originally, drupal_render was called with the return value of menu_tree(), an a strict warning is issued about calling a function expecting a reference with a non-reference. Or something.</li>
<li><strong>April 2018 update:</strong> Patch Drupal against <a href="https://www.drupal.org/sa-core-2018-002" target="_blank">this arbitrary code execution exploit</a> (which, BTW, was relevant for current Drupal versions as well. Just being up to date wouldn&#8217;t have helped against this one).</li>
</ul>
<h3>Installing the WYSIWYG module</h3>
<p>Spoiler: The clear winners are CKEditor and NicEdit. CKEditor because it has the nicest features, but for some reason it has its own spell checked which contacts a dictionary over the web rather than using the browser&#8217;s spell checking. And hence it&#8217;s disabled by default. It doesn&#8217;t make sense to tell the world what you&#8217;re typing as a default setting.</p>
<ul>
<li>Downloaded from <a href="http://ftp.drupal.org/files/projects/wysiwyg-7.x-2.0.tar.gz" target="_blank">here</a>.</li>
<li> tar -xzf at sites/all/modules/.</li>
<li>Created a &#8220;libraries&#8221; directory under sites/all (why wasn&#8217;t it already there?)</li>
<li>Downloaded <a href="http://download.cksource.com/CKEditor/CKEditor/CKEditor%203.5.3/ckeditor_3.5.3.tar.gz" target="_blank">CKEditor </a>and did tar -xzf at the &#8220;libraries&#8221; just created. Be sure that CKEditor wasn&#8217;t released after Drupal, because it may not install, and the message &#8220;The version of CKEditor could not be detected&#8221; will appear at &#8220;Wysiwyg profiles&#8221;</li>
<li>Downloaded <a href="http://nicedit.com/download.php" target="_blank">NicEdit</a> (default configuration), created a &#8220;nicedit&#8221; directory under &#8220;libraries&#8221; and unzipped the file there.</li>
<li>Downloaded YUI, and unzipped it directly on &#8220;libraries&#8221;</li>
<li>On web interface menu: Configuration &gt; Text Formats &gt; Add Text format. Add a new text format, e.g. NicEdit and set it for use only for admins (for example).</li>
<li>Then on web interface menu: Configuration &gt; Wysiwyg profiles assign NicEdit text format to NicEdit editor</li>
<li>Then on web interface menu: Configuration &gt; Wysiwyg profiles &gt; edit NicEdit &gt; Buttons and plugins and check all checkboxes (TAB and space bar came handy). Or the rich edit box looks like just a plain text box. Why this extra step was necessary is pretty much beyond me.</li>
<li>Same procedure for other editors.</li>
</ul>
<h3>Setting up a taxonomy menu structure</h3>
<p>The sequence below is based upon the Administration Menu module (things may be found in other places using the core interface):</p>
<ul>
<li>Create a new menu:  Structure &gt; Menus &gt; Add Menu</li>
<li>Create a new vocabulary: Structure &gt; Taxonomy &gt; Add Vocabulary.</li>
<li>Generate a few terms in the new vocabulary</li>
<li>Edit the new vocabulary. Under &#8220;Taxonomy menu&#8221; pick the the new menu, and check the &#8220;Select to rebuild the menu on submit&#8221; checkbox. This copies the taxonomy items into the menu (not very sophisticated, is it).</li>
<li>Now on Structure &gt; Blocks &gt; { your chosen theme } put the menu somewhere. Or more precisely, find the new menu in the list, and assign it a point of appearance. Don&#8217;t forget to submit the form.</li>
<li>To use the menu items as top-page tabs, go to Structure &gt; Menus &gt; Settings and set the source as the new menu</li>
</ul>
<p>Note that new vocabulary items don&#8217;t appear automatically on the menu, and it&#8217;s not a matter of flushing the cache or running cron. The menus need to be updated by rebuilding them as mentioned above.</p>
<p>So the bottom line is that I tend to trash the Taxonomy menu and use the book structure.</p>
<h3>Tagging pages</h3>
<ul>
<li>Go to Structure &gt; Content Type &gt; { The content type to alter } &gt; Manage Fields</li>
<li>Add a new field of type &#8220;Term Reference&#8221;  and select any of the widgets offered.</li>
<li>Click Save settings</li>
<li>On the next page, you&#8217;ll be asked to choose the vocabulary to use. Save this as well.</li>
<li>On the next page set up the number of entries allowed (unlimited?) and default tag.</li>
<li>You may want to hide this field in the display (if it&#8217;s for internal uses): Structure &gt; Content Types &gt; { The content type to alter } &gt; Manage Display and set both label and format to hidden. Be sure to apply this to all view modes, full content in particular.</li>
</ul>
<h3>Custom styling for panes</h3>
<p>When the GUI tools don&#8217;t do the job, editing the theme&#8217;s main CSS file does (or a local.css file in the theme directory, but yet another CSS file?). Then, in the content editing page where the panes appear, pick the inner gear menu &gt; CSS Properties and enter a new class name, say mypane-class. In the CSS file setting the attributes for a header within the pane will look like</p>
<pre>.mypane-class h2 {
 font: Arial, Helvetica, sans-serif;
 font-weight: bold;
 font-size: 14px;
}</pre>
<h3>Allowing injected Javascript</h3>
<p>Full HTML actually allows Javascript, only it wraps it with CDATA. Which could have been forgiveable, had it done that correctly, and not killed the script on some browsers. The really annoying thing about this is that the purpose of this CDATA statement is merely to silence XHTML validators.</p>
<p>The trick is simple, and explained <a href="http://gotdrupal.com/videos/quick-tip-allowing-javascript-blocks" target="_blank">in a video</a> (I prefer things written down, but anyhow). The idea is to create a new input format. The way Drupal works, it filters the content of the database during page display (the input goes in as is), so the idea is to create a new filter which does nothing. So what is in the database is shown. Not the safest thing in the world for general practice, but sometimes there&#8217;s no choice.</p>
<p>Basically, go to the modules list, find &#8220;Filter&#8221; and click its &#8220;configure&#8221;. Pick &#8220;add text format&#8221;, name it &#8220;Unfiltered&#8221; (or something), make sure none of the filters is checked and that only the administrator has access to this format. That&#8217;s it. Now whatever goes in, goes out. Use this format, enter HTML with Javascript and whatever.</p>
<h3>Removing the navigation links at the bottom of a book page</h3>
<p>As suggested <a href="http://knaddison.com/drupal/remove-table-contents-drupal-book" target="_blank">here</a>, don&#8217;t edit the original PHP code generating the links, but rather copy /public_html/modules/book/book-navigation.tpl.php to the theme&#8217;s directory, edit the copy, and flush the caches.</p>
<p>To retain the links in the lower hierarchy, but remove the navigation links, simply change</p>
<pre>&lt;?php if ($has_links): ?&gt;</pre>
<p>to</p>
<pre>&lt;?php if (0): /* ($has_links): */ ?&gt;</pre>
<p>(deleting code is for barbarians)</p>
<h3>Disallow access to taxonomy pages</h3>
<p>There may be an elegant way to do this with Drupal&#8217;s own interface, but I&#8217;m yet to discover it. So the trick is to create a custom page, and make it empty. Silly, but it works.</p>
<p>Under Views, clone the view which emulates the core&#8217;s taxonomy display (it&#8217;s name is simply &#8220;Taxonomy&#8221;). Add a header, saying whatever you want, which is active even when no items match (who cares). This is merely so you know it&#8217;s not the default page you&#8217;re watching.</p>
<p>Then set an access rule, saying only admins are allowed: Access &gt; Role &gt; Check administrator.</p>
<p>To get the same effect with nodes, do something similar for node/%. Don&#8217;t clone the taxonomy view, since they display taxonomy, and sometimes a page needs to be displayed by node (for admin purposes).</p>
<h3>Setting the page title hack</h3>
<p>Goal: Set up the page title (the one within &lt;title&gt; tags) based upon the title of a content entry within a view.</p>
<p>I should mention, that what I present here is considered very wrong, in particular because it involves hacking a Drupal core PHP file. But after trying to reach an elegant solution and <a href="http://drupal.org/node/1221222" target="_blank">asking for help</a>, I gave up for something that is ugly but works.</p>
<p>This is the ugly part: modify modules/system/html.tpl.php so that the part saying</p>
<pre><code>&lt;title&gt;&lt;?php print $head_title; ?&gt;&lt;/title&gt;</code></pre>
<p>becomes</p>
<pre>&lt;title&gt;&lt;?php if (isset($GLOBALS['head_title_override'])) { print $GLOBALS['head_title_override'] ; } else { print $head_title; } ?&gt;&lt;/title&gt;</pre>
<p>While I <strong>am</strong> ashamed of hacking something under &#8220;modules/system&#8221;, the usage of PHP global variables may be adequate here, even though I&#8217;m sure there are more Drupalish ways to accomplish the same. The advantage of a global variable is that it&#8217;s easy to set from any execution context.</p>
<p>Having installed and enabled the &#8220;Views PHP&#8221; module, there&#8217;s now a Global: PHP field. It allows access to the data of fields previously fetched, so having the &#8220;title&#8221; field already in the list, it&#8217;s just</p>
<pre>$GLOBALS['head_title_override'] = $row-&gt;title;</pre>
<p>in the &#8220;Value code&#8221; window. Note that the available variables are listed just below the window. And this should be excluded from display, of course.</p>
<p>Careful with the PHP: If it&#8217;s syntactically wrong, the views edit page may be impossible to reach, so the only way to get out is deleting the view. Disabling auto preview may reduce the risk for  this.</p>
<h3>Breadcrumbs on Arthemia</h3>
<p>For a reason which was beyond me, no breadcrumbs appeared in on my pages. And I ran Arthemia. Until I found the solution <a href="http://drupal.org/node/1267724" target="_blank">here</a>. There&#8217;s a bug in Arthemia&#8217;s template.php, so</p>
<pre><code>function arthemia_breadcrumb($breadcrumb) {
 if (count($breadcrumb) &gt; 1) {
   return '&lt;div&gt;'. implode(' &amp;rsaquo; ', $breadcrumb) .'&lt;/div&gt;';
   }
}</code></pre>
<p>should be replaced with</p>
<pre><code>function arthemia_breadcrumb($breadcrumb) {
 if (count($breadcrumb<span style="color: #ff0000;">["breadcrumb"]</span>) &gt; 1) {
  return '&lt;div&gt;'. implode(' &amp;rsaquo; ', $breadcrumb<span style="color: #ff0000;">["breadcrumb"]</span>) .'&lt;/div&gt;';
  }
}</code></pre>
<p>Looks like the calling convention changed on Drupal 7, but nobody paid attention&#8230; <a href="http://www.amandarodriguez.com/blog/drupal-7-theming-basics" target="_blank">This page</a> says a few words about changes in Drupal 7.</p>
<h3>Making a local mirror of a site</h3>
<p>First, make a copy of the files, of course. Then make a local copy by using a mysqldump backup. Basically, generate the data base with</p>
<pre>CREATE DATABASE delme_mirror</pre>
<p>from mysql prompt, and then from shell:</p>
<pre>mysql -D delme_mirror &lt; databasedump</pre>
<p>This can take a few minutes&#8230;</p>
<p>Now, allow write-enable on sites/default, make a copy of settings.php within this directory, and make it writable.</p>
<p>Then change the database setting (maybe copy the remote site&#8217;s?) so it says something like</p>
<pre>$databases = array (
 'default' =&gt;
 array (
 'default' =&gt;
 array (
 'database' =&gt; 'delme_xillybus',
 'username' =&gt; '<em>username</em>',
 'password' =&gt; '<em>password</em>',
 'host' =&gt; 'localhost',
 'port' =&gt; '',
 'driver' =&gt; 'mysql',
 'prefix' =&gt; '',
 ),
 ),
);</pre>
<p>Well, I think this is the thing to do. I didn&#8217;t manage to run this, because Drupal went &#8220;<strong>Fatal error</strong>:  Undefined class constant &#8216;MYSQL_ATTR_USE_BUFFERED_QUERY&#8217;&#8221; which appears to be a PHP version issue (only cutting edge PHP for Drupal, it seems).</p>
<h3>Themes</h3>
<p>These are my very shallow impressions of a few selected themes I checked up for a hi-tec site. I checked with Firefox 3.6 and IE6 to get a wide range of compatibility issues.</p>
<ul>
<li>0-point: Beautiful and sleek on Firefox, complete disaster on IE6.</li>
<li>Aqua Marina: Not bad, and survived IE6 test fairly OK, but still pretty bad. Doesn&#8217;t have a hi-tec look, though.</li>
<li>Danblog: Simple &amp; to-the-face design pretty suitable for a hi-tec site, but looks a bit too simple maybe. Survived the IE6 test very well (no differences).</li>
<li>Danland: Same impression as Danblog. The differences are possibly deeper in.</li>
<li>Fusion was obviously not intended to be used out of the box.</li>
<li>Garland looks great, and not so bad on IE6 (resize issues on the latter). Not really a hi-tec look (top region should be bright)</li>
</ul>
<h3>Views</h3>
<p>I wrote a <a title="Drupal 7 Views: Making a block of links to related pages" href="https://billauer.se/blog/2011/07/d7-sql-views-taxonomy-tags/" target="_blank">separate post</a> on this.</p>
<h3>SQL command for strings search in all pages</h3>
<p>This is in particular useful for updating links.</p>
<p>It boils down to this:</p>
<pre>SELECT entity_id, alias FROM field_data_body LEFT JOIN url_alias ON source=CONCAT('node/', entity_id) WHERE body_value LIKE '%<span style="color: #ff0000;"><strong>string-to-search</strong></span>%' ORDER BY entity_id;</pre>
<p>And then access the page with the URL like e.g. https://thesite.com/node/114 or use the alias as the path instead (if such exists).</p>
]]></content:encoded>
			<wfw:commentRss>https://billauer.se/blog/2011/06/drupal-modules-notes/feed/</wfw:commentRss>
		<slash:comments>11</slash:comments>
		</item>
	</channel>
</rss>
