tag:blogger.com,1999:blog-324424242024-03-13T21:13:08.400+00:00Chris Cant's developer blogHere are some programming tips, be it ASP.NET, C#, CSS, Java, JavaScript, PHP, SQL, XHTML, etc. I am director of PHD Computer Consultants Ltd, based in Cumbria, England, UK - we sell our own software and undertake software projects and consultancy.Chris Canthttp://www.blogger.com/profile/11367082039820244178noreply@blogger.comBlogger58125tag:blogger.com,1999:blog-32442424.post-57144557036617367662016-06-26T21:14:00.007+01:002016-08-25T14:50:32.677+01:00Updating from deprecated PHP 5.6 PPAI'm running an <!--StartFragment -->Ubuntu 14.04.4 LTS server with Apache2 and phpMyAdmin running several Drupal 7 and 8 sites. I saw this message when getting updates and this is how I updated my system to the new PHP5.6 PPA:<br />
<div class="separator" style="clear: both; text-align: center;">
<a href="https://3.bp.blogspot.com/-tBYIrwVX5OY/V3A1InuL0nI/AAAAAAAAAes/BYWXhKrqbhQHk05zIYUM1BlyuRs6z05LQCLcB/s1600/ppa56.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" height="386" src="https://3.bp.blogspot.com/-tBYIrwVX5OY/V3A1InuL0nI/AAAAAAAAAes/BYWXhKrqbhQHk05zIYUM1BlyuRs6z05LQCLcB/s640/ppa56.png" width="640" /></a></div>
<div class="separator" style="clear: both; text-align: center;">
<br /></div>
Following the instructions from root I ran these commands:<br />
<!--StartFragment --><span style="font-family: "courier new" , "courier" , monospace;">LC_ALL=en_US.UTF-8 add-apt-repository ppa:ondrej/php5</span><br />
<!--StartFragment --><span style="font-family: "courier new" , "courier" , monospace;">apt-get dist-upgrade</span><br />
<br />
This got PHP 7.0 on my system. To get PHP 5.6 I ran this:<br />
<span style="font-family: "courier new" , "courier" , monospace;">apt-get install php5.6</span><br />
I later found that I needed these extensions:<br />
<!--StartFragment --><span style="font-family: "courier new" , "courier" , monospace;">apt-get install php5.6-mysql<br />
apt-get install php5.6-xml<br />
apt-get install php5.6-gd</span><br />
<span style="font-family: "courier new" , "courier" , monospace;">apt-get install php5.6-mbstring</span><br />
<span style="font-family: "courier new" , "courier" , monospace;">apt-get install php5.6-mcrypt</span><br />
<span style="font-family: Courier New;">apt-get install php5.6-zip</span><br />
<br />
Apache was still running the old version, I disabled the php5 module, enabled php5.6 and restarted:<br />
<!--StartFragment --><span style="font-family: "courier new" , "courier" , monospace;">a2dismod php5<br />
a2enmod php5.6<br />
service apache2 restart</span><br />
<br />
I had changed my PHP.INI file - which was in <span style="font-family: "courier new" , "courier" , monospace;">/etc/php5/apache2/php.ini</span><br />
<!--StartFragment -->So I did similar changes in here <span style="font-family: "courier new" , "courier" , monospace;">/etc/php/5.6/apache2/php.ini</span><br />
and restarted: <span style="font-family: "courier new";">service apache2 restart</span><br />
<br />
<span style="font-family: inherit;">The command line php was now running 7.0 so I changed it using this command:<br /><!--StartFragment --></span><span style="font-family: "courier new" , "courier" , monospace;">update-alternatives --config php</span><br />
<br />
<span style="font-family: inherit;">Thanks to Ondřej Surý at <a href="https://launchpad.net/~ondrej/+archive/ubuntu/php">https://launchpad.net/~ondrej/+archive/ubuntu/php</a></span>Chris Canthttp://www.blogger.com/profile/11367082039820244178noreply@blogger.com0tag:blogger.com,1999:blog-32442424.post-20955242696265414032016-06-04T16:29:00.001+01:002016-06-04T16:39:44.564+01:00Microsoft SQL Server Express copying tables using Identity InsertI recently needed to copy some tables from one SQL Server Express database to another - as part of a server move on a CMS where I wanted a fresh install to clean things up. I needed to copy several tables across, keeping the primary keys the same so as to ensure the data integrity. Here's what I had to do to do the transfer:
<br />
<ol>
<li>Ensure that <em>SQL Server Management Studio</em> and <em>Import and Export Data</em> tools are installed.</li>
<li>Ensure that the <em>SQL Server (SQLEXPRESS)</em> and <!--StartFragment --><em>SQL Server Browser</em> services are started.</li>
<li>You probably need to make sure that your login has admin access to SQL Server Express.</li>
<li>I started by copying the MDF and LDF files (for both databases) to a new location so I can work with a clean copy that won't be used by something else</li>
<li>Open Management Studio and connect to <em>(local)\SQLEXPRESS</em></li>
<li>Click right on Databases then select Attach, then click Add and choose your MDF</li>
<li>Amend "Attach as" if desired and check the MDF and LDF file paths below, then click OK</li>
<li>Do the same for the other database</li>
<li>Open the Import and Export Wizard</li>
<li>Select the <em>SQL Server Native Client</em></li>
<li>Enter <em>(local)\SQLEXPRESS</em> in the Server name box</li>
<li>Choose the required source database below, then click Next</li>
<li>Do the same to choose the destination database</li>
<li>Select "Copy data from one or more tables and views"</li>
<li>Select which tables you wish to copy</li>
<li>For each table, click Edit Mappings.</li>
<li>Ensure that "Enable identity insert" is ticked</li>
<li>Choose the appropriate action on the left, eg "Delete rows in destination table", then click OK</li>
<li>Remember: do this for each table</li>
<li>Click Next, then Next then Finish</li>
<li>Open up the destination database in Management Studio to confirm that the tables have got the correct data.</li>
<li>You had probably best use Tasks+Detach in Management Studio to make sure that all connections are dropped before copy the destination database files into the desired location</li>
</ol>
Not so bad after all. To celebrate, here's a photo of some nicely padded swallow eggs:<br />
<div class="separator" style="clear: both; text-align: center;">
<a href="https://1.bp.blogspot.com/-Oe0rdrY7K7E/V1LzhBGY3sI/AAAAAAAAAeU/bJFINuAEmWkPPr68VV1mImWP8NL6IlTqgCLcB/s1600/WP_20160602_002a.jpg" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" height="179" src="https://1.bp.blogspot.com/-Oe0rdrY7K7E/V1LzhBGY3sI/AAAAAAAAAeU/bJFINuAEmWkPPr68VV1mImWP8NL6IlTqgCLcB/s320/WP_20160602_002a.jpg" width="320" /></a></div>
Chris Canthttp://www.blogger.com/profile/11367082039820244178noreply@blogger.com0tag:blogger.com,1999:blog-32442424.post-73688655629890770182013-02-02T13:07:00.000+00:002013-02-02T13:10:56.459+00:00Code signing jar and exe files<p>It is possible to buy a single code signing certificate that can be used to sign both Java jar files and Windows executable exe files.</p>
<p>A cheap source for a certificate is <a href="https://author.tucows.com/certs.php">Tucows</a> with one year currently costing US$75. Tucows are a reseller for Comodo, but the certificates are more expensive from Comodo direct.</p>
<p>Along the way, I'm going to create P12, 2 x PFX, PEM, PVK, CERT and SPC files. You'll need one of the PFX files to sign JARs and the SPC/PVK files to sign EXEs.</p>
<h3>Getting the certificates</h3>
<p>Working in Windows, I bought the certificate in Firefox. Within a couple of days I was phoned by Comodo to confirm my identity and the certificate issued. I collected the certificate in the same browser, ie it was installed in Firefox.</p>
<p>Using <a href="https://support.comodo.com/index.php?_m=knowledgebase&_a=viewarticle&kbarticleid=1221">these instructions</a>, I saved the certificate from Firefox into a .P12 file.</p>
<p>The next task is to import the certificate into Windows Internet Explorer. In Windows Explorer, double-click on the P12 file to start the Certificate Import Wizard. Choose your .p12 file. Tick (a) Enable strong private key encryption (b) Mark this key as exportable and (c) Include all extended properties. Click through until you can set the security level to High with a password of your choice.</p>
<p>The certificate should now be installed in Internet Explorer. Find it in Tools, Internet Options, Content, Certificates. <a href="https://support.comodo.com/index.php?_m=knowledgebase&_a=viewarticle&kbarticleid=225">Follow these instructions</a> to create a PFX file suitable for signing JAR files. As per the instructions, tick the "Include all certificates" option. I saved the eventual file with a name like mycert.jar.pfx</p>
<p>To get the SPC/PVK files to sign EXEs, you need to run the Internet Explorer certificate export wizard again. This time do not tick "Include all certificates". I saved the eventual file with a name like mycert.exe.pfx</p>
<p>Now continue with <a href="https://support.comodo.com/index.php?_m=knowledgebase&_a=viewarticle&kbarticleid=1089">these instructions</a> to create the PVK and SPC files. You will need to install openssl if you don't have it already. I ran these from the openssl bin directory:</p>
<pre>openssl pkcs12 -in \certs\mycert.exe.pfx -nocerts -nodes -out \certs\mycert.pem
openssl rsa -in \certs\mycert.pem -outform PVK -pvk-strong -out \certs\mycert.pvk
openssl pkcs12 -in \certs\mycert.exe.pfx -nokeys -out \certs\mycert.cert
openssl crl2pkcs7 -nocrl -certfile \certs\mycert.cert -outform DER -out \certs\mycert.spc</pre>
<p>I ignored the warning "WARNING: can't open config file: /usr/local/ssl/openssl.cnf"</p>
<p>Backup all the created files carefully.</p>
<h3>Signing JAR files</h3>
<p>Follow <a href="https://support.comodo.com/index.php?_m=knowledgebase&_a=viewarticle&kbarticleid=1072">these instructions</a> to find the alias you have been given - before the first comma which is followed by a date. This can either be a friendly name or a {GUID}. Make a note of the alias.</p>
<pre>keytool -list -storetype pkcs12 -keystore \certs\mycert.jar.pfx</pre>
<p>You can then sign JAR files like this:</p>
<pre>jarsigner -storetype pkcs12 -keystore \certs\mycert.jar.pfx myfile.jar "myalias"
jarsigner.exe -verify -certs myfile.jar</pre>
<h3>Signing EXE files</h3>
<p>Sign EXE files like this, replacing the Description and the website with something appropriate:</p>
<pre>signcode -spc \certs\mycert.spc -v \certs\mycert.pvk -n "Description" -i "http://www.example.com/" -t http://timestamp.verisign.com/scripts/timstamp.dll myfile.exe</pre>Chris Canthttp://www.blogger.com/profile/11367082039820244178noreply@blogger.com0tag:blogger.com,1999:blog-32442424.post-43210919323582495252012-07-12T14:44:00.002+01:002012-07-25T15:25:04.361+01:00CiviCRM profile search block<p>
On a CiviCRM 4 site I help run, there is a profile search form exposed to authorised public users of the Drupal site. This form provides many search options including the selection of a region of the UK.
</p>
<p>
My task is to provide a Drupal block on the site home page that shows a map of the UK with each region clickable - when you click, a map of the results is shown, not a text listing of results.
</p>
<p>
My solution was to build on the existing profile, as it's display templates have been customised in various ways.
To work out what to do, you will have to analyse what is generated by CiviCRM for the actual search form, in particular the (custom) fields that are used.
</p>
<h3>
Basic block structure</h3>
<p>
My block creates an HTML form that is posts to CiviCRM.
Civi uses session keys to differentiate between each user's search.
A crucial task is therefore to set a valid "qfKey" hidden form parameter.
To get a suitable key, the PHP code creates a suitable form controller and retrieves the key. The code below shows the PHP and a skeleton of the form itself.
</p>
<pre style="font-size: small;"><?php
civicrm_initialize(TRUE);
require_once 'CRM/Core/Controller/Simple.php';
$formController = new CRM_Core_Controller_Simple( 'CRM_Profile_Form_Search', ts('Search Profile'), CRM_Core_Action::ADD );
$formController->setEmbedded( true );
$formController->set( 'gid', 123 );
$ky = $formController->_key;
?>
<form action="/civicrm/profile?gid=123" method="post" name="RegionMap" id="RegionMap">
<input name="qfKey" type="hidden" value="<?php echo $ky ?>" />
<input name="_qf_default" type="hidden" value="Search:refresh" />
...
<input name="_qf_Search_refresh" value="Search" type="submit"/>
</form></pre>
<h3>
Form contents</h3>
<p>
To work out what needs to go in the form, you will have to copy what's in the standard search form generated by CiviCRM. It seems like you only need to include the chunks for the sub-search you wish to do, ie you do not need to have dummy/hidden entries for all fields.
</p>
<p>
For my region search, the standard form output consists of a hidden INPUT, a checkbox INPUT and an associated LABEL. You can slim it right down to one hidden INPUT like this:
</p>
<p>
<span style="font-family: Courier New; font-size: small;"> <input type="hidden" id="northeast" name="custom_21[1.00]" value="" /></span>
</p>
<p>
"custom_21" is the Civi custom data field internal name. The string in square brackets is the field value - in my case, one of the mulitple choice option values. I have added the "northeast" id to this INPUT.
</p>
<p>
If you set the value for the above hidden field to "1" and submit the form, you should see the standard text listing of results that match that search. On that page, there should be a "Map these contacts" link.
</p>
<h3>Showing the result map straight away</h3>
<p>
To show a map of results straight away, you'd think you'd simply copy the URL of the map page, and it would work:
</p>
<p>
<span style="font-family: Courier New; font-size: small;"><form action="/civicrm/profile/map?map=1&gid=123&reset=1" method="post" name="RegionMap" id="RegionMap" ></span>
</p>
<p>
You do need to do that change, but by itself it doesn't work. If you try it, you will find that it always uses the last proper search terms that you used. Civi is storing the last search parameters in the "profileParams" session variable. So, our first task is to clear this session variable when our block is initialised, ie in the above code:
</p>
<p>
<span style="font-family: Courier New; font-size: small;"> $session = CRM_Core_Session::singleton();<br />
$session->set('profileParams');</span>
</p>
<p>
Here's how I set the profileParams session value. First, I created another hidden field which will be set by JavsScript to contain the value chosen on my block:
</p>
<p>
<span style="font-family: Courier New; font-size: small;"><input name="regionCode" type="hidden" value="" /></span>
</p>
<p>
In the custom CiviCRM code for my site, I amended CRM/Profile/Page/Listings.php in getProfileContact() to rebuild the profileParams session value if my new hidden variable is set:
</p>
<p>
<span style="font-family: Courier New; font-size: small;">$regionCode = CRM_Utils_Array::value( 'regionCode', $_REQUEST );<br />
if ( isset($regionCode))<br />
{<br />
$params = array();<br />
$custom_21 = array();<br />
$custom_21[$regionCode] = 1;<br />
$params["custom_21"] = $custom_21;<br />
$session->set('profileParams',$params);<br />
}</span>
</p>
<p>
For this to work, the regionCode hidden field needs to be set to a suitable value, such as "1.00" in my case.
</p>
<h3>
And the block image map...</h3>
<p>
The block uses a standard HTML image map along with JavaScript and jQuery to submit the form when the user clicks on a region. The map area code looks like the following:
</p>
<p>
<span style="font-family: Courier New; font-size: small;"> <area shape="poly" coords="149,123,158,101,205,169,187,166,181,190" href="/" alt="North East" title="North East England" onclick="return ShowRegion('northeast');" /></span>
</p>
<p>
The following JavaScript is used to respond to map clicks:
</p>
<pre style="font-size: small;"><script type="text/javascript">
var cj = jQuery.noConflict(true);
function ShowRegion(regionname){
var regionField = cj('input[id="'+regionname+'"]');
regionField.val("1");
var regionFieldName = regionField.attr("name");
var regionCode = null;
var lbpos = regionFieldName.indexOf("[");
if( lbpos!==-1){
var rbpos = regionFieldName.indexOf("]",lbpos);
if( lbpos!==-1){
regionCode = regionFieldName.substring(lbpos+1,rbpos);
}
}
if( regionCode==null){
alert("Sorry, no regionCode found");
return false;
}
var regionSpecialField = cj('input[name="regionCode"]');
regionSpecialField.val(regionCode);
cj("#RegionMap").submit();
return false;
}
</script></pre>Chris Canthttp://www.blogger.com/profile/11367082039820244178noreply@blogger.com0tag:blogger.com,1999:blog-32442424.post-11267840595214842732011-08-04T09:22:00.007+01:002011-08-04T10:03:25.966+01:00Using PHDCC CodeModule with javascriptphdcc Director <i>John Cant</i> writes:<br /><br />My use of DNN has become - <br /><br />[a] Maintain any HTML in a text file / copy and paste it into the basic editor / convert to raw / save. (Online editors are too hard to use and keep slim)<br /><br />[b] Put any functionality into a <a href="http://www.phdcc.com/phdcc.CodeModule/">phdcc.CodeModule</a><br /><br />Much valuable functionality - jquery-ui / google maps / facebook / janrain / plupload / galleria - is available as javascript libraries. To get them onto a DNN Page, I have created a template CodeModule as follows.<br /><br />ASP.NET gives you limited control over where in the page javascript can be inserted - RegisterClientScriptBlock inserts somewhere near the top; RegisterStartupScript somewhere near the bottom. All I can say is - the template works on all browsers tested.<br /><br />In this example, I load the jquery-ui library at the top, and daisy chain the DNN onload handler at the bottom. The javascript could of course be located in a separate file.<br /><br />In the onload handler, I create an invisible empty jquery-ui dialog and initialise a jquery-ui accordion. I bind the submit button of a PHDCC form to a handler and examine one of the form fields to give feedback via the dialog.<br /><br />I add the CodeModule somewhere out of the way on the page - the bottom pane is a good place. Once there, accordions and dialogs can be used anywhere on the page.<br /><br />If you need any help with this or any of the suggested libraries, please get back to us.<br /><pre><br /><%@ Control Language="C#" ClassName="JCformWidgets" %><br /><br /><script runat="server"><br /><br />//--------------------------------------<br />private void loadJS()<br />{<br /> string scriptKey = "JCjsIncludes:" + this.UniqueID;<br /> if( Page.IsStartupScriptRegistered( scriptKey) || Page.IsPostBack) return;<br /><br /> ClientScriptManager cs = Page.ClientScript;<br /> Type cstype = this.GetType();<br /><br /> // ----------------------------<br /> // load JS files at the top of the page<br /> // ----------------------------<br /> string scriptBlock = "\n";<br /><br /> scriptBlock += "<link type='text/css' href='/jqui/jquery-ui-1.8.14.custom.css' rel='Stylesheet'>\n";<br /> scriptBlock += "<script src='/jqui/jquery-ui-1.8.14.custom.min.js' type='text/javascript'>";<br /> scriptBlock += "</";<br /> scriptBlock += "script>\n"; // avoid microsoft parsing bug<br /><br /> cs.RegisterClientScriptBlock( cstype, scriptKey, scriptBlock);<br /><br /> // ----------------------------<br /> // load onload handler etc. at the bottom of the page<br /> // ----------------------------<br /> string startupScript = <br /> @"<script language='JavaScript'><br /><!--<br /><br />function JConload(){<br /><br /> var oldOnLoad = window.onload;<br /><br /> var myDialog = $('<div></div>').dialog( {autoOpen: false} );<br /> <br /> this.newOnload = function(){<br /> <br /> // call existing handler<br /> if(typeof oldOnLoad === 'function'){ <br /> oldOnLoad();<br /> }<br /><br /> // -------------------------------------<br /><br /> $(function() {<br /> $( '#accordion' ).accordion();<br /> });<br /> <br /> // -------------------------------------<br /><br /> $('#' + 'dnn_ctr422_ViewForm_btnSubmit').bind('click', function() {<br /><br /> var theseSkills = $( '#' + 'dnn_ctr422_ViewForm_Group_157_400_Question_157_400_1563_form_157_text_1563').val();<br /> var thisDialogText = '';<br /> <br /> if( theseSkills.length <= 0) {<br /> thisDialogText = 'No skills given<br>Please try again.';<br /><br /> myDialog.html( thisDialogText);<br /> myDialog.dialog( 'option', 'title', 'Things you forgot ...' );<br /> <br /> myDialog.dialog( 'open');<br /> return false;<br /> }<br /> });<br /><br /> // -------------------------------------<br /><br /> };<br /><br />}<br /><br />(function(){<br /> var JCOL = new JConload();<br /> window.onload = JCOL.newOnload;<br />}());<br /><br /> // -->";<br /><br /> startupScript += "</sc";<br /> startupScript += "ript>"; // avoid microsoft parsing bug<br /><br /> cs.RegisterStartupScript( cstype, scriptKey, startupScript);<br />}<br /><br />//--------------------------------------<br />protected void Page_Load(object sender, EventArgs e)<br />{<br /> loadJS();<br />}<br /><br /></script><br /> <br /> <br /> </pre>Chris Canthttp://www.blogger.com/profile/11367082039820244178noreply@blogger.com0tag:blogger.com,1999:blog-32442424.post-38854953996127176132011-05-12T11:17:00.000+01:002011-05-13T21:36:26.957+01:00Scheduled task ping for plesk in WindowsI've been moving several domains to a Windows cloud hosting package at <a href="http://www.webhosting.uk.com/">http://www.webhosting.uk.com/</a>. I'm currently using Cloud Beginner hosting that gives 10 domains and a dedicated IP address for £12/month. This package lets us run DotNetNuke (DNN) sites - and fast too as there is not much contention on the included SQL Server databases. The cloud is UK based so the ping time is less from the UK and nearby.<br /><br />The account is run using Windows plesk which lets you control most things. However intervention is often needed from the webhosting.uk.com staff. They are usually very responsive and competent on their Live Chat service. Typical requests include setting ASP.NET modify permissions for the Network Service user and setting Application Starting Points. You need to insist that you get the dedicated IP address if you've asked for it. Email is included using the Horde web client, which isn't great; perhaps using Google Apps for email might be the best solution. No web stats package is included. Nor are file or database backups.<br /><br />For one domain, one task that I needed to do was to set up scheduled tasks to ping various URLs. While this could be done externally, I decided to use the Plesk scheduled task feature. When FTPing in, the main site is at /httpdocs/. The scheduled task VBS scripts should be placed in /cgi-bin/<br /><br />Using a couple of resources on the Internet, I put together this script, called <span style="font-family:courier new;">pingurls.vbs</span>:<br /><br /><pre>Call DoScheduledTask()<br /><br />Sub DoScheduledTask()<br /> On Error Resume Next<br /><br /> Const ForReading = 1<br /><br /> Set objFSO = CreateObject("Scripting.FileSystemObject")<br /> Set objFile = objFSO.OpenTextFile("E:\inetpub\vhosts\mydomainname.com\cgi-bin\urls2ping.txt", ForReading)<br /><br /> Dim URL<br /> Do Until objFile.AtEndOfStream<br /> URL = objFile.ReadLine<br /> if Len(URL)>0 then PingURL(URL)<br /> Loop<br /> objFile.Close<br /><br />end sub<br /><br />sub PingURL(URL)<br /><br /> Dim RequestObj<br /> Set RequestObj = CreateObject("Microsoft.XMLHTTP")<br /> 'Open request and pass the URL<br /> RequestObj.open "POST", URL , false<br /> 'Send Request<br /> RequestObj.Send<br /> 'cleanup<br /> Set RequestObj = Nothing<br /><br />End Sub</pre><br />This script reads a text file <span style="font-family:courier new;">urls2ping.txt</span>, also in the /cgi-bin/ directory. Each line contains a URL to be pinged.<br /><br />How do you know what absolute path to use to get to the text file? This ASP.NET code, say in MyPath.aspx, will tell you the path it is running from:<br /><pre><%@ Page Language="VB" %><br /><script runat="server"><br />Sub Page_Load()<br /> lblPath.Text = Request.MapPath("~")<br />End Sub<br /></script><br /><html><br /><head><br /> <title>Web app path using Request.MapPath</title><br /></head><br /><body><br /> <asp:Label ID="lblPath" Runat="server" /><br /></body><br /></html></pre><br /><br />The final stage is to set up the scheduled task in plesk. This is straight forward using [Scheduled Tasks] then [Add New Task]. The Path to Executable file should be similar to that used above, ie <span style="font-family:courier new;">E:\inetpub\vhosts\mydomainname.com\cgi-bin\pingurls.vbs</span>. Set the times you want the task to run.Chris Canthttp://www.blogger.com/profile/11367082039820244178noreply@blogger.com3tag:blogger.com,1999:blog-32442424.post-27653775854180457642010-11-20T09:14:00.012+00:002010-11-20T15:34:47.803+00:00Installing Tomcat onto Ubuntu Apache2<p>This post describes how to install Tomcat 6 on an Ubuntu 10.10 server and connect it up with the Apache2 server using the jk module. Although there are plenty of posts about this, none gave a full list of instructions; this is what I aim to provide - let me know if you have any corrections.</p><p>I assume that you have Apache2 installed.</p><p>You will need to do most of the following actions at a root shell prompt. Get this from your login using: <code>sudu su -</code> You will need to edit various text files. You can do this using vi at the shell prompt. However I usually do this on my local computer using my preferred editor, transferring files to and fro using FTP. </p><p><strong>Installing Tomcat</strong></p><p>This command gets Tomcat 6 - and Java if need be:<br /><code>aptitude install tomcat6</code></p><p>I'm not sure if that gets all the Tomcat files, so enter this to get them all:<br /><code>apt-get install tomcat6 tomcat6-admin tomcat6-common tomcat6-user tomcat6-docs tomcat6-examples</code></p><p>On my system, Java was installed to here: <code>/usr/lib/jvm/default-java/</code></p><p>You now need to set up Java environment variables in text script file <code>/etc/environment</code><br />Add this to the PATH variable: <code>/usr/lib/jvm/default-java/bin</code> after a colon separator. Add these lines:</p><pre>JAVA_HOME=/usr/lib/jvm/default-java<br />JDK_HOME=/usr/lib/jvm/default-java</pre><p>You will now need to reboot so these changes are used.</p><p>Tomcat should now be running. You can start, stop or restart it as follows:</p><pre>/etc/init.d/tomcat6 stop<br />/etc/init.d/tomcat6 status<br />/etc/init.d/tomcat6 restart</pre><p>or more easily like this:</p><pre>service tomcat6 restart</pre><p>Tomcat should now be running on port 8080 at your host, eg http://www.example.com:8080/ If you go there, you should see an <strong>It works</strong> page. To test it further, I put a JSP file in here: <code>/var/lib/tomcat6/webapps/ROOT/</code> and saw that it ran OK. I then put a servlet WAR file in <code>/var/lib/tomcat6/webapps/</code>. This was soon expanded automatically by Tomcat and I was able to navigate to the servlet directory (still on the 8080 port).</p><p>You may find it useful to run the <strong>manager</strong> and <strong>host-manager</strong> Tomcat applications. To enable these you need to edit the Tomcat configuration files in <code>/etc/tomcat6/</code>. Edit <code>tomcat-users.xml</code> to include the following:</p> <pre><role rolename="manager"/><br /><role rolename="admin"/><br /><user name="admin" password="secret_password" roles="manager,admin"/></pre><p>You should now be able to access these apps at /manager/html and /host-manager/html. You will need to enter your credentials; possibly a couple of times on first access.</p><p>The Tomcat logs are at <code>/var/log/tomcat6/</code>. By default Tomcat creates a different log file for each day. Keep an eye on these, and the disk space they consume.</p><p><strong>Connecting Tomcat to Apache</strong></p><p>Hopefully you already know these commands to test your Apache configuration and restart Apache. You'll need these later.</p><pre>apache2ctl configtest<br />apache2ctl restart</pre><p>First, tell Tomcat to listen out for connections from Apache. Edit <code>/etc/tomcat6/server.xml</code> to uncomment this line: </p><pre><Connector port="8009" protocol="AJP/1.3" redirectPort="8443" /></pre><p>Restart Tomcat. </p><p>Install the <strong>jk</strong> module that acts as the connector between Apache and Tomcat:<br /><code>apt-get install libapache2-mod-jk</code></p><p>You should see a file called <code>jk.load</code> in <code>/etc/apache2/mods-available/</code>. I edited this to contain:</p><pre>LoadModule jk_module /usr/lib/apache2/modules/mod_jk.so<br />JkWorkersFile /etc/libapache2-mod-jk/workers.properties<br />JkLogFile /var/log/apache2/mod_jk.log<br />JkShmFile /var/log/apache2/jk-runtime-status<br />JkLogLevel info</pre><p>You can also add JkMount instructions here, but I found that I needed to put them in each VirtualHost file in <code>/etc/apache2/sites-available/</code>. Eg in my <code> default</code> VirtualHost file I added:</p><pre>JkMount /*.jsp ajp13_worker<br />JkMount /manager ajp13_worker<br />JkMount /manager/* ajp13_worker<br />JkMount /host-manager ajp13_worker<br />JkMount /host-manager/* ajp13_worker</pre><p>An alternative is to define JkMount commands in <code>jk.load</code> and add a JkMountCopy command to copy these settings to all virtual hosts.</p><p>You now need to edit the jk worker file <code>/etc/libapache2-mod-jk/workers.properties</code>. Make sure it has code as follows, ie to tell jk to use the Tomcat connection we defined earlier:</p><pre>worker.list=ajp13_worker<br />worker.ajp13_worker.port=8009<br />worker.ajp13_worker.host=localhost<br />worker.ajp13_worker.type=ajp13</pre><p>Restart apache.</p><p>I was now able to access the Tomcat manager here: http://www.example.com/manager/html.</p><p>Edit your other VirtualHost files to expose any other apps that you need.</p><p>You'll find the Apache and jk logs in here: <code>/var/log/apache2/</code>. As above, keep an eye on these files and the disk space they use. </p><p>If you see "missing uri map" in log file <code>mod_jk.log</code> then you need to define JkMount in your VirtualHost.</p><p>As a final task, if you wish, disable the Tomcat 8080 port by editing <code>/etc/tomcat6/server.xml</code> and then restart Tomcat.</p>Chris Canthttp://www.blogger.com/profile/11367082039820244178noreply@blogger.com7tag:blogger.com,1999:blog-32442424.post-48410390571825514642010-09-21T17:29:00.008+01:002010-11-08T17:05:19.375+00:00Windows 7 system drive letters<p>I've previously blogged about <a href="http://chriscant.phdcc.com/2010/01/wandering-drive-letters-in-windows.html">Wandering drive letters in Windows 7</a>. I've now a little more hard-won knowledge of what's going on.</p><p><strong>Be very careful if you resize a Windows 7 system partition when the partition letter is not C. In fact, don't do it!</strong></p><p>I am revisiting this issue because I ran into a problem. Our family shared laptop came with Vista. However this started playing up showing a DOS box regularly and Windows Update wouldn't install a particular patch. So I decided to install Windows 7 on drive D. This worked fine with W7 running with its system drive as D:.</p><p>Recently, drive D: started getting full so I decided to resize the partitions with the great <a href="http://www.partitionwizard.com/">Partition Wizard</a>. This seemed to go OK. However, Windows 7 started playing up very badly, showing DwmHintDxUpdate error messages and worse. I eventually tracked the problem down to the fact that Windows 7 had decided that drive D: would now be called drive C: which is a serious problem as Windows and other software will store many full file locations (in the registry and elsewhere). If W7 thinks a file should be on drive D when in fact it is on drive C, then there's going to be serious problems. It's amazing that it booted at all.</p><p>The post by MarcusOS7 <a href="http://social.technet.microsoft.com/Forums/en-US/w7itproinstall/thread/47bcc58e-9792-409a-889f-796a07746a5e">here</a> explained what was going on, ie that Windows 7 tries to rename its system partition to drive C if the drive is resized, ie when the drive identifier changes. <strong>This is a serious flaw in Windows 7 and must be fixed.</strong></p><p>I had hoped that with this information, I would be able to mend my W7 system. Even though I could just run regedit, I did not know what magic to do to change the system drive letter.</p><p>Eventually I decided to reinstall Windows 7. I saved the data over to an external USB drive. For this installation I wanted to be sure that W7 would install itself on drive C. Previously I must have done the installation when running the Vista system which somehow prevented W7 from using the C drive. This time, I was careful to do the install after booting into the Windows DVD. This trick ensured that W7 marked its install partition as drive C (even though it was not on the first partition).</p><p>In fact: before doing this, I re-partitioned the system so I had 3 partitions in the laptop, one each for Vista and W7, and one as a data drive. This process also helped me clear the previous W7 installation which was quite stubborn to remove, permission-wise. I eventually had to clear the partition by reformatting the drive.</p><p>Back in the new W7 system, I was able to restore the desired user data from the backup. And then I had to set up W7 again from scratch for each user, and installing all my development tools. As usual one of my first tasks is to turn off various system sounds, particularly the annoying Start Navigation click...</p>Chris Canthttp://www.blogger.com/profile/11367082039820244178noreply@blogger.com0tag:blogger.com,1999:blog-32442424.post-60682174295138977802010-08-25T22:21:00.002+01:002010-08-25T22:35:49.162+01:00Drupal staging site securitySuppose you have a Drupal staging site where you are preparing to go live or testing new features. You could install this on a separate domain that you have. Here's how to redirect casual users to your live site, while giving those in the know easy entry.<br /><br />The crucial trick is to use a Session variable to indicate an authorised user. All Drupal access is via the root index.php file (except use of static files). index.php is amended to redirect users who do not have the session variable set correctly. Another secret file eg password.php, is used to let you get into the site by setting the session variable.<br /><br />In the following example, www.example.com is your live domain and www.example.info is the staging server. The following code is on the staging server.<br /><br />In index.php add this code after the line that contains drupal_bootstrap...<br /><br /><pre>if( $_SESSION['password']!='asecret')<br />{<br /> header('Location: http://www.example.com/');<br /> exit;<br />}</pre><br /><br />Create a secret file in the root directory eg password.php with content like this:<br /><br /><pre><?php<br /><br />require_once './includes/bootstrap.inc';<br />drupal_bootstrap(DRUPAL_BOOTSTRAP_FULL);<br /><br />if( $_SESSION['password']=='asecret')<br />{<br /> header('Location: http://www.example.info/');<br /> die();<br />}<br /><br />$pwd = trim($_POST['pwd']);<br />if( get_magic_quotes_gpc())<br />{<br /> $pwd = stripslashes($pwd);<br />}<br />if( $pwd=='asecret')<br />{<br /> $_SESSION['password'] = $pwd;<br /> header('Location: http://www.example.info/');<br /> die();<br />}<br />?><br /><br /><html><br /><body><br /><br /><form method="post"><br /><br />Security:<br /><input type="text" name="pwd" /><br /><input type="submit" value="Go" /><br /><br /></form><br /><br /></body><br /></html></pre>Chris Canthttp://www.blogger.com/profile/11367082039820244178noreply@blogger.com0tag:blogger.com,1999:blog-32442424.post-81652357739730495772010-04-23T13:59:00.002+01:002010-04-23T14:09:40.473+01:00Tool to find maps KML Lat/LngUse this tool if you want to find a Google Maps KML GLatLng location, eg to use within your own code: <a href="http://www.phdcc.com/GoogleLatLng.htm">http://www.phdcc.com/GoogleLatLng.htm</a> Either drag the initial marker or enter a search term. The latitude and longitude values are listed below, to 6 decimal places of accuracy.<br /><br />The map is centred on the UK, as this is where I will want to use it most. It uses the Google AJAX Search API to work out UK postcodes well. Look at the source to see how it works.<br /><br />This is based on the code by cmarshall at <a href="http://www.webmasterworld.com/xml/3542700.htm">http://www.webmasterworld.com/xml/3542700.htm</a>Chris Canthttp://www.blogger.com/profile/11367082039820244178noreply@blogger.com0tag:blogger.com,1999:blog-32442424.post-25198415631730395002010-04-19T12:45:00.005+01:002010-04-19T13:03:54.486+01:00DNN5 User soft-delete issuesIn DotNetNuke DNN5 when you delete a user that login is no longer removed from the system. Instead they are soft-deleted, ie a new IsDeleted flag column is set in the UserPortals table. (Note that IsDeleted in the Users table is *not* set - is this ever set?)<br /><br />In DNN5, code that calls DotNetNuke.Entities.Users.UserController.GetUser() etc will return a UserInfo object even if the user is deleted. Therefore you may have to check the UserInfo.IsDeleted property every time you get a user.<br /><br />I would have not have implemented it this way. I'd keep the DNN4 functionality and have extra API calls to find deleted users. I wonder: does the DNN core code always check IsDeleted now?<br /><br />Anyway, UserInfo.IsDeleted is not available in DNN4. As I do not want different versions of my code for DNN4 and DNN5, I have written this isUserDeleted() static method that uses reflection to detect if the IsDeleted property is available and if so calls it. It returns true if the UserInfo is null. After that, for DNN4 it always returns false.<br /><br /><pre>public static bool isUserDeleted(UserInfo ui)<br />{<br /> if (ui == null) return true;<br /> try {<br /> Type tUserInfo = ui.GetType();<br /><br /> PropertyInfo piIsDeleted = tUserInfo.GetProperty("IsDeleted");<br /> if (piIsDeleted != null)<br /> {<br /> bool IsDeleted = (bool)piIsDeleted.GetValue(ui, null);<br /> return IsDeleted;<br /> }<br /> }<br /> catch (Exception) { }<br /> return false;<br />}</pre>Chris Canthttp://www.blogger.com/profile/11367082039820244178noreply@blogger.com0tag:blogger.com,1999:blog-32442424.post-84886355055395818642010-04-10T09:00:00.021+01:002010-04-14T17:09:09.445+01:00Freegle Find a Group mashup<p>This blog describes how I've used one JavaScript script to power several different incarnations of a Google Maps tool to help people Find a Freegle Group. Usage instructions for the Find a Group tool are on the <a href="http://wiki.ilovefreegle.org/Find_a_Group_maps">Freegle wiki</a>. You can also see the Find a Group gadger lower down on this page on the right.</p><p><b>This is primarily a technical article - for usage instructions, see the above link.</b><p><img src="http://maps.iLoveFreegle.org/freegle_gadget_screenshot_280x246.png" alt="Freegle Find a Group screenshot" title="Freegle Find a Group screenshot" style="float:right;padding:2px;border:1px;" /><a href="http://www.ilovefreegle.org/">Freegle</a> is a network of local re-use groups in the UK. Group members give away and receive any unwanted items for free. This helps conserve the world's resources, clears clutter and helps others.</p><p>The Find a Group tool shows a UK map with pinpoint markers for each Freegle group. You can move around and zoom in as normal - clicking on a marker shows an information bubble - clicking on the link in the bubble takes you to the group web site.</p><p>Find a Group also has a search box. The tool uses Google to search for that location in the UK. If found, the tool zooms to that location and adds a marker - you are invited to look for the nearest Freegle group marker. The nearest groups are listed below</p><h2>Source data</h2><p>The green group markers come from <a href="http://maps.google.co.uk/maps/ms?hl=en&ie=UTF8&oe=UTF8&t=p&msa=0&msid=104032218577461097800.000478fbc5161363404bd&output=kml">this KML file</a> - maintained by another Freegle member. This is the latest data so the tool should always be up to date.</p><p>Actually, that's not the whole truth. The tool needs to load the KML file into memory so it can search for groups. Loading the above Google Maps KML link doesn't work because of cross-site scripting security (as I understand it). Therefore the code loads a separate snapshot of the KML file from a URL that it can access.</p><p>There's a plan to resolve this issue by having the primary KML file on the iLoveFreegle.org servers. That way, my code would only need to reference one file. And the plan is to have this file generated directly from the main Freegle groups database, so it is always up to date. (Currently the Google Maps KML file has to be updated by hand.)</p><h2>Mashup variants</h2> <h3>(P) Full web page</h3><p>The Find a Group tool occupies a full web page here: <a href="http://maps.ilovefreegle.org/">maps.ilovefreegle.org</a>.</p><h3>(M) Google Mapplet</h3><p>The Find a Group tool appears in the Google mapplets directory <a href="http://maps.google.co.uk/gadgets/directory?synd=mpl&hl=en&gl=en&url=http%3A%2F%2Fmaps.ilovefreegle.org%2Ffreegle_mapplet.xml">here</a>. When added to My Maps it shows filling the browser window.</p><h3>(I) Iframe</h3><p>The Iframe version of the Find a Group tool is designed to fit into a small window within another page. The <a href="http://wiki.ilovefreegle.org/Find_a_Group_maps#IFrame">usage instructions</a> describe the various configuration parameters.</p><p>You can see the iframe in action at the <a href="http://groups.yahoo.com/group/PenrithEdenFreegle/">Penrith and Eden District Freegle group</a> and its associated <a href="http://www.penrithedenfreegle.org.uk/">helper web site</a>.</p><h3>(G) Google gadget</h3><p>The Find a Group tool is packaged as a Google Gadget, <a href="http://maps.ilovefreegle.org/freegle_gadget.xml">defined here</a>. You can therefore add it to iGoogle, add it to a blog, and get the code to put on a web site. The gadget has similar options to the iframe.</p><p>The gadget has a Content type of url, with the main gadget code <a href="http://maps.iLoveFreegle.org/freegle_gadget.php">here</a>. The gadget options are passed as URL QueryString parameters.</p><h3>(F) Facebook application</h3><p>The Find a Group tool is being developed as a simple Facebook iframe application.</p><h2>Common Page Layout</h2><p>Each of the above tools has HTML which has the same page elements:</p><ul><li>Map div called "map_canvas"</li><li>Noscript block</li><li>Div called "jsBlock" that wraps the search form</li><li>Input text box called "search"</li><li>Submit button that has an onclick handler "return doSearch();" that always returns false</li><li>Information div called "idInfo"</li><li>Debug div called "idDebug" that shows version info by default</li></ul><h2>Accessibility</h2><p>The tool has a noscripts block that has a link to the main site groups list - this is shown if the browser has no JavaScript or it is turned off. However Chrome doesn't show this text if JavaScript is turned off - sounds like a bug to me.</p><p>The "jsBlock" search form div is not displayed by default; the JavaScript makes this block visible. This avoids confusion if JavaScript is not available.</p><p>The search text box has an accesskey of "4" defined. For the larger variants, a label is defined for this text box.</p><p>The tool HTML is hopefully valid XHTML and CSS.</p><h2>API and api keys</h2><p>The Google Maps/Mapplets API is used in the JavaScript. There are various differences depending on whether it is a mapplet or not. Therefore one of the JavaScript initialise() parameters is a mapplet boolean; this is stored in a global for later use throughout the code.</p><p>To use the Google Maps API, each site needs an API key. All these tools run on the maps.iLoveFreegle.org domain so the iLoveFreegle.org API key is used. The mapplet code does not need an API key.</p><h2>JavaScript structure</h2><p>The JavaScript is pretty straightforward procedural code, with some globals, an <i>initialise()</i> function and a function <i>doSearch()</i> that is called when a user does a search. The JavaScript code is here: <a href="http://maps.ilovefreegle.org/freegle_maps.js">http://maps.ilovefreegle.org/freegle_maps.js</a>.</p><h3>initialise() function</h3><p>This takes parameter specifying whether big map controls are wanted, and the mapplet boolean.</p><ul><li>If not a mapplet, check that the browser is Google Maps compatible</li><li>Get references to the "idInfo" and "idDebug" page elements - add the js version to idDebug</li><li>If the "jsBlock" page element is present, change its display style to block to make it show.</li><li>Get the map object and set the map height if it has been specified in a parameter.</li><li>Get the z SearchZoom integer parameter for later use</li><br /><li>Set the map to be the approx centre of the UK at a suitable scale</li><li>Use GGeoXml to load the primary KML file and add it as an overlay - the code cannot access the contents of the overlay, so...</li><li>Initiate a separate load of the (secondary) KML for processing, either using _IG_FetchXmlContent() or GDownloadUrl()</li><li>Finally set the focus to the search box if required</li></ul><h3>processKML() function</h3><p>This parses the received KML XML file and stores info about each Group placemark object in the Groups array.</p><p>Note that the KML file stores each coordinate the wrong way round, ie longitude, latitude then height. The GLatLng.fromUrlValue() function needs just the first two values but reversed.</p><p>Once the KML is loaded, it sees if a search has been requested and does it.</p><h3>doSearch() function</h3><p>The search function does a Google GClientGeocoder search and zooms in to show the result. The search also lists the nearest groups, calculated using GLatLng.distanceFrom().</p><p>doSearch() uses showAddressResult(), SearchGroups() and createMarker().</p><h3>Helper functions</h3><p>String.prototype.trim, PageQuery, queryString() and isInteger() were obtained from code on the Internet. I added queryString2() helper function.</p><p>I have updated PageQuery to cope with URL parameters with key=value pairs where "=value" is missing.</p>Chris Canthttp://www.blogger.com/profile/11367082039820244178noreply@blogger.com0tag:blogger.com,1999:blog-32442424.post-78693387406351186802010-03-19T19:41:00.007+00:002010-03-19T19:49:54.445+00:00800700C1 KB977165 and verclsidOur Vista laptop has developed 3 problems recently, which are probably all related. Can anyone help?<br /><br />Windows Update keeps on trying to install KB977165, sometimes successfully, but it keeps re-appearing.<br /><br />Clicking on Check for Updates now fails with error 800700C1.<br /><br />It also often shows an error with a DOS box title C:\Windows\system32\verclsid.exe and a message 16 bit MS-DOS Subsystem saying The NTVDM CPU has encountered an illegal instruction.<br /><br />We did have some software for 2 HP printers installed. I've uninstalled one of them as this was suggested as being a problem.<br /><br />Any other ideas pls?<br /><br /><a href="http://2.bp.blogspot.com/_hkkBsHR0r5s/S6PU1MtMI0I/AAAAAAAAAMY/-yJpE2Nv8u4/s1600-h/WindowsVistaKB977165.jpg"><img style="cursor:pointer; cursor:hand;width: 200px; height: 88px;" src="http://2.bp.blogspot.com/_hkkBsHR0r5s/S6PU1MtMI0I/AAAAAAAAAMY/-yJpE2Nv8u4/s200/WindowsVistaKB977165.jpg" border="0" alt=""id="BLOGGER_PHOTO_ID_5450433984457352002" /></a><br /><br /><a href="http://1.bp.blogspot.com/_hkkBsHR0r5s/S6PU1dJGUSI/AAAAAAAAAMg/O_Fg7nUaaeA/s1600-h/WindowsUpdate800700C1.jpg"><img style="cursor:pointer; cursor:hand;width: 200px; height: 112px;" src="http://1.bp.blogspot.com/_hkkBsHR0r5s/S6PU1dJGUSI/AAAAAAAAAMg/O_Fg7nUaaeA/s200/WindowsUpdate800700C1.jpg" border="0" alt=""id="BLOGGER_PHOTO_ID_5450433988869378338" /></a><br /><br /><a href="http://3.bp.blogspot.com/_hkkBsHR0r5s/S6PU1j8XNLI/AAAAAAAAAMo/tcXZ4sN0dHY/s1600-h/NTVDMerror2.jpg"><img style="cursor:pointer; cursor:hand;width: 200px; height: 102px;" src="http://3.bp.blogspot.com/_hkkBsHR0r5s/S6PU1j8XNLI/AAAAAAAAAMo/tcXZ4sN0dHY/s200/NTVDMerror2.jpg" border="0" alt=""id="BLOGGER_PHOTO_ID_5450433990695007410" /></a>Chris Canthttp://www.blogger.com/profile/11367082039820244178noreply@blogger.com0tag:blogger.com,1999:blog-32442424.post-9881044175049354032010-03-09T10:42:00.003+00:002010-03-10T20:49:44.769+00:00Crystaltech to 3essentials DNN copy<p>PHDCC Director John Cant describes the process of transferring a DNN site from one shared host to another, at this stage simply as a backup using a different/private domain.</p><p>The site, ourdnnsite.com is being copied from Newtek (Crystaltech) hosting to 3Essentials with domain ourdnnsite.info. Both support services are very helpful but it currently seems that 3essentials runs faster than Crystaltech, probably because of a faster database server.</p><p>The existing site runs only one portal but this contains many users and many files in /Portals/0/. Maintaining the UserIDs is crucial as much custom information in the database is keyed off this value.</p><p><a href="http://knowledge.3essentials.com/web-hosting/article/369/What-ASP.NET-versions-1.1-2.0-3.5-does-3Essentials-support-and-how-do-I-enable-change-versions.html">Enable ASP.NET 3.5 at the 3essentials site.</a></p><p>First, place a holding page at ourdnnsite.info, eg index.html, that will be shown instead of Default.aspx for people accessing the root domain URL.</p><p>Make sure to truncate transaction log prior to the upgrade, clear the eventlog and maybe even sitelog and search tables, if those are exaggeratingly large.</p><p>The 3essentials site currently runs SQL Server 2005/2003. They need a .BAK backup file so that the database copy can be restored correctly.</p><p>Do a database backup on the Crystaltech site and ask for the entire site to be zipped, to get these files:</p><ul><li>ourdnnsitedb_1002050754.bak</li><li>ourdnnsite.zip</li><br /></ul><p>Load these to the 3essentials site ourdnnsite.info /private/</p><p>Submit a 3essentials site support request, asking for:</p><ul><li>the database to be restored from /private/ourdnnsitedb_1002050754.bak</li><li>the site files to be unzipped from /private/ourdnnsite.zip</li><li>the site file permissions to be set correctly:<br />"I have corrected this issue, this involves giving the httpdocs directory IWPD User - Modify permissions which you are not able to do via Plesk."</li></ul><p>Check the Web.Config file at the 3essentials site:</p><ul><br /><li>set the new database connection string twice</li><li>check the ObjectQualifier</li><li>set CustomErrors to 'Off' to see the errors</li><li>ensure the machinekey values are the same as the original values from the Crystaltech site</li><li>set AutoUpgrade off</li></ul><p>On the database:</p><ul><li>Update the portal alias, eg:<br /><code>UPDATE [DNN_PortalAlias] SET<br />HTTPAlias='www.ourdnnsite.info' WHERE PortalAliasID=0</code></li></ul><p>Access the site by visiting Default.aspx</p><p><br />If any errors are visible, keep deleting the sections of Web.Config that cause errors.</p><p>If you cannot log in, use Chris' technique, <a href="http://knowledge.3essentials.com/web-hosting/article/376/Reset-DNN-host-password.html">described here</a>. To access the DNN database, log in using myLittleAdmin here: <a href="http://db1.3essentials.com/mla/">http://db1.3essentials.com/mla/</a></p><p>To prevent unauthorised access to the ourdnnsite.info site, add to the Default.aspx file a check that a cookie is set. Provide a means of setting the cookie using another private page.</p><p>There are a couple of other additions that we usually make:</p><ul><li>As well as creating an app_offline.htm file, we use the Cookie technique in Default.aspx to ensure that the site is offline during maintenance, redirecting to an offline page.</li><li>In the skin, we warn people who are online that the site is about to go down; this also helps us see if anyone is online now.</li></ul><p>More details on these Default.aspx techniques later if you wish.</p>Chris Canthttp://www.blogger.com/profile/11367082039820244178noreply@blogger.com0tag:blogger.com,1999:blog-32442424.post-91561080825975037272010-02-24T12:02:00.005+00:002010-02-24T12:17:41.425+00:00.NET app using Jet on x64If you are trying to use an Access MDB file using the Microsoft Jet OleDb driver in .NET or ASP.NET, you must set your platform target to x86 (32 bit).<br /><br />If you are running on a 64 bit system and your assembly target is Any CPU or x64 then you will see this exception:<br /><em>The 'Microsoft.Jet.OLEDB.4.0' provider is not registered on the local machine. </em><br /><br />The Microsoft ACE.OLEDB driver for MDB and ACCDB files works OK whichever platform is targeted. However this driver is not installed by default on user computers unlike the Jet driver.<br /><br />This fix can be used to make existing code work with Jet/MDB, eg the Cassini server.<br /><br />Useful blog links:<br /><ul><br /><li><a href="http://www.lostechies.com/blogs/gabrielschenker/archive/2009/10/21/force-net-application-to-run-in-32bit-process-on-64bit-os.aspx"> Force .NET application to run in 32bit process on 64bit OS</a> by Gabriel Schenker</li><br /><li><a href="http://www.hanselman.com/blog/BackToBasics32bitAnd64bitConfusionAroundX86AndX64AndTheNETFrameworkAndCLR.aspx">Back to Basics: 32-bit and 64-bit confusion around x86 and x64 and the .NET Framework and CLR</a> by Scott Hanselman</li></ul>Chris Canthttp://www.blogger.com/profile/11367082039820244178noreply@blogger.com0tag:blogger.com,1999:blog-32442424.post-45817099633623209712010-02-22T14:04:00.006+00:002010-02-22T15:07:29.799+00:00System.String hidden UTF8 BOMIn .NET, a string (System.String) can contain an initial UTF-8 Byte Order Mark (BOM) which might not be seen in ordinary processing but is present when converted to a character array or into an encoding byte array.<br /><br />For example, a text file might be saved in UTF8 format with UTF-8 Byte Order Mark bytes at the start, ie 0xEF 0xBB 0xBF. You might receive this file in ASP.NET using a FileUpload control, or read it directly in a Forms .NET app in C#:<br /><br /><pre>byte[] FileBytes = File.ReadAllBytes(path);<br />string content = Encoding.UTF8.GetString(FileBytes);</pre><br /><br />If the file contains these 7 bytes (in hex) EF BB BF 44 65 61 72 then content will superficially contain the single word "Dear", eg as seen in the debugger, and content.StartsWith("Dear") will return true.<br /><br />However, content.Length is 5 and content.ToCharArray() will return an array with 5 elements, the first being set to 0xFEFF. Similarly, Encoding.UTF8.GetBytes(content) will return the same 7 bytes as was used in the first place.<br /><br />(Note that that has nothing to do with the encoderShouldEmitUTF8Identifier optional parameter for the UTF8Encoding constructor.)<br /><br />As this hidden extra character can be misleading, I have written the following snippet that detects the presence of the UTF8 Byte Order Mark preamble and ignores it if present:<br /><br /><pre>byte[] FileBytes = File.ReadAllBytes(path);<br />int StartPoint = 0;<br />int Count = FileBytes.Length;<br />if ( Count>= 3 && FileBytes[0] == 0xEF && FileBytes[1] == 0xBB && FileBytes[2] == 0xBF)<br />{<br /> StartPoint += 3;<br /> Count -= 3;<br />}<br />content = Encoding.UTF8.GetString(FileBytes, StartPoint, Count);</pre><br /><br />PS The code could not doubt be improved using Encoding.GetPreamble()Chris Canthttp://www.blogger.com/profile/11367082039820244178noreply@blogger.com2tag:blogger.com,1999:blog-32442424.post-82067480544487569842010-02-07T12:59:00.006+00:002010-02-07T14:01:08.727+00:00Clearing nested CSS floatsHere's a XHTML/CSS technique to ensure you only clear the desired floats and get the right background areas for your DIV.<br /><br />The code examples are here: <a href="http://www.phdcc.com/CSS_ClearingFloats.htm">http://www.phdcc.com/CSS_ClearingFloats.htm</a> - look at the source code for full details.<br /><br />My starting point is to have a DIV set with floats left and right. Within the middle unfloated DIV, I have another DIV set with floats and right. I want to clear just the middle floats, not the outer ones.<br /><br />The crucial trick is to add CSS style 'overflow:hidden' to create a Block Formatting Context. Any CSS style 'clear:both' then only applies to the 'nearest'/current Block Formatting Context.<br /><br />An associated problem was the fact that the background of the middle unfloated DIV goes wide to the border of the current Block Formatting Context. Adding CSS style 'overflow:hidden' to create a new context constrains the background to the expected area.<br /><br />Using 'overflow:hidden' feels slightly naff: shouldn't there be an explicit CSS style to define a Block Formatting Context, instead of using something that does this as a side effect? Also, you may be concerned about what overflow is being hidden - as long as you have the default width:auto and/or height:auto set, you will not be hiding anything.Chris Canthttp://www.blogger.com/profile/11367082039820244178noreply@blogger.com5tag:blogger.com,1999:blog-32442424.post-15933323433961105082010-02-05T15:01:00.014+00:002010-02-05T16:01:00.066+00:00Adding Access key support to DotNetNukeThis page explains how to add "access key" support to a DNN5 web site.<br />Access keys provide keyboard short-cuts to improve web accessibility, as described here - which also lists the UK government recommendations:<br /><a href="http://en.wikipedia.org/wiki/Access_key" title="Access key keyboard short-cuts">http://en.wikipedia.org/wiki/Access_key</a><br /><br />I have implemented access keys using the accesskey attribute rather than the<br /><a href="http://www.w3.org/TR/2005/WD-xhtml2-20050527/mod-role.html#s_rolemodule">Role Access Model</a><br /><br /><strong>How it works for a user</strong><br /><br />For standard users, each browser uses different modifier keys and acts minorly differently when pressed. Consider accesskey '1'. In Windows Internet Explorer, pressing Alt+1 selects the corresponding link - you must press Enter to go to that link. In Firefox, pressing Alt+Shift+1 goes to the associated link immediately.<br /><br />These accesskey shortcuts take precedence over standard Window commands, so if you define an accesskey 'f' it will be acted on rather than show the File menu in Windows.<br /><br />In Internet Explorer, the link is not selected if the link has a style of display:none.<br /><br /><strong>Search accesskey</strong><br /><br />Most of the access keys can be defined in your DNN skin using extra HTML that is not normally seen. However, accesskey 4 takes you straight to the Search text box. To make this happen, I had to amend <code>admin/Skins/search.ascx</code> to add AccessKey="4" to the txtSearchNew asp:TextBox. <br /><br />While there, I took the opportunity to add a label for this box, as recommended for all fields to improve accessibility. This label is present but not normally displayed:<br /><code><asp:Label AssociatedControlID="txtSearchNew" style="display:none;" runat="server">Search:</asp:Label></code><br /><br /><strong>Main accesskeys</strong><br /><br />The main site access keys can be defined using code like this at the top of your skin, which I adapted from another web site:<br /><br /><code><ul id="skips"><br /><li><a href="#content" accesskey="s">Skip to page content, accesskey=s</a></li><br /><li><asp:HyperLink ID="SkipToHome" NavigateUrl="~/" AccessKey="1" runat="server">Skip to Home page, accesskey=1</asp:HyperLink></li><br /></ul></code><br /><br />CSS (below) can then be used to make sure that this information is not normally seen, but each link become visible when it has the focus or is active. The list-style is set to none to avoid bullet points etc. When not in focus, the links have width and height 0; when in focus these are set to auto. Tweak the other settings as you wish.<br /><br />Finally, don't forget to provide an anchor within your skin for the skip to navigation link:<br /><a id="content" name="content"></a><br /><br /><strong>CSS code:</strong><br /><code>ul#skips { list-style: none; }<br />#skips li { list-style: none; display: inline; }<br />#skips a<br />{<br /> color: white; width: 0; height: 0; overflow: hidden; z-index: 1000;<br /> position: absolute; top: 15px; left: 100px;<br />}<br />#skips a:active, #skips a:focus<br />{<br /> color: red; border: 1px solid red;<br /> width: auto; height: auto;<br /> display: block; overflow: visible;<br /> padding: 3px;<br />}</code>Chris Canthttp://www.blogger.com/profile/11367082039820244178noreply@blogger.com5tag:blogger.com,1999:blog-32442424.post-33460762603059523082010-02-01T15:10:00.015+00:002010-02-01T15:54:19.034+00:00DNN local install with SQL Server<div>The post summarises the steps required to set up DotNetNuke 5 (DNN5) locally on Windows using the full Microsoft SQL Server using SQL Server authentication, ie not the Express version. Getting the Logins and Users right is the crucial trick that I want to remember.<br /><br />Click on each image to see it larger.<br /><ol><br /><li>In Microsoft SQL Server Management Studio, connect to your local server. Right-click on Databases on the left. Select "New database". In the following screen, enter a database name, eg "DNN522pen" and click OK without changing anything else.<br /><a href="http://2.bp.blogspot.com/_hkkBsHR0r5s/S2bviYI8U5I/AAAAAAAAALI/Sv40At0fMOw/s1600-h/LocalServer1.png"><img style="WIDTH: 200px; HEIGHT: 180px; CURSOR: hand" id="BLOGGER_PHOTO_ID_5433293374343631762" border="0" alt="" src="http://2.bp.blogspot.com/_hkkBsHR0r5s/S2bviYI8U5I/AAAAAAAAALI/Sv40At0fMOw/s200/LocalServer1.png" /></a><br /></li><br /><li>Now, find the server Security and right-click on Logins and select "New Login...":<br /><a href="http://1.bp.blogspot.com/_hkkBsHR0r5s/S2bzDR67gqI/AAAAAAAAAMI/SfsDLTUGSdk/s1600-h/LocalServer1half.png"><img style="WIDTH: 197px; HEIGHT: 200px; CURSOR: hand" id="BLOGGER_PHOTO_ID_5433297238144811682" border="0" alt="" src="http://1.bp.blogspot.com/_hkkBsHR0r5s/S2bzDR67gqI/AAAAAAAAAMI/SfsDLTUGSdk/s200/LocalServer1half.png" /></a><br /></li><br /><li>Enter a login name such as "dnn522pen", select "SQL Server Authentication" and enter the password. Untick "Enforce password policy" if you wish. Do not set the Default database. Click OK.<br /><a href="http://4.bp.blogspot.com/_hkkBsHR0r5s/S2bvitHhJfI/AAAAAAAAALQ/tk3Us7vxkH4/s1600-h/LocalServer2.png"><img style="WIDTH: 200px; HEIGHT: 180px; CURSOR: hand" id="BLOGGER_PHOTO_ID_5433293379974800882" border="0" alt="" src="http://4.bp.blogspot.com/_hkkBsHR0r5s/S2bvitHhJfI/AAAAAAAAALQ/tk3Us7vxkH4/s200/LocalServer2.png" /></a><br /></li><br /><li>Open up your database tree on the left: open "dnn522pen" then Security then Users. Right-click and select "New user...":<br /><a href="http://2.bp.blogspot.com/_hkkBsHR0r5s/S2bvi_r6loI/AAAAAAAAALY/aV_P7_fGMM0/s1600-h/LocalServer3.png"><img style="WIDTH: 138px; HEIGHT: 200px; CURSOR: hand" id="BLOGGER_PHOTO_ID_5433293384959301250" border="0" alt="" src="http://2.bp.blogspot.com/_hkkBsHR0r5s/S2bvi_r6loI/AAAAAAAAALY/aV_P7_fGMM0/s200/LocalServer3.png" /></a><br /></li><br /><li>In the new user dialog, enter a User name such as "dnn522pen" and the Login name you used before eg "dnn522pen". Tick "db_owner" twice below and click OK:<br /><a href="http://2.bp.blogspot.com/_hkkBsHR0r5s/S2bvjDJpv_I/AAAAAAAAALg/FrHIYiN2ItM/s1600-h/LocalServer4.png"><img style="WIDTH: 200px; HEIGHT: 180px; CURSOR: hand" id="BLOGGER_PHOTO_ID_5433293385889333234" border="0" alt="" src="http://2.bp.blogspot.com/_hkkBsHR0r5s/S2bvjDJpv_I/AAAAAAAAALg/FrHIYiN2ItM/s200/LocalServer4.png" /></a><br /></li><br /><li>Next, create a new folder on disk somewhere, eg in directory "D:\dnn522pen\". Unzip the DotNetNuke community installation file in there <code>DotNetNuke_Community_05.02.02_Install.zip</code>.<br /></li><br /><li>Open up Internet Information Services (IIS 7) Manager. Expand the menu on the left to open Sites and Default Web Site. Click on "Add Application..." on the right.<br /><a href="http://2.bp.blogspot.com/_hkkBsHR0r5s/S2bvjdZQKqI/AAAAAAAAALo/hI0HeZPqQak/s1600-h/LocalServer5.png"><img style="WIDTH: 200px; HEIGHT: 200px; CURSOR: hand" id="BLOGGER_PHOTO_ID_5433293392934087330" border="0" alt="" src="http://2.bp.blogspot.com/_hkkBsHR0r5s/S2bvjdZQKqI/AAAAAAAAALo/hI0HeZPqQak/s200/LocalServer5.png" /></a><br /></li><br /><li>Enter a suitable alias and physical path, eg "dnn522pen" and "D:\dnn522pen\":<br /><a href="http://1.bp.blogspot.com/_hkkBsHR0r5s/S2b2agJS6fI/AAAAAAAAAMQ/_3g6oKJeb3k/s1600-h/LocalServer5half.png"><img style="cursor:pointer; cursor:hand;width: 200px; height: 138px;" src="http://1.bp.blogspot.com/_hkkBsHR0r5s/S2b2agJS6fI/AAAAAAAAAMQ/_3g6oKJeb3k/s200/LocalServer5half.png" border="0" alt=""id="BLOGGER_PHOTO_ID_5433300935635036658" /></a><br /></li><br /><li>In IIS Manager, select the new alias on the left, select Content View and click "Browse" to start the DNN installation:<br /><a href="http://2.bp.blogspot.com/_hkkBsHR0r5s/S2bvwq8vsSI/AAAAAAAAALw/kKecbggrhiM/s1600-h/LocalServer6.png"><img style="WIDTH: 200px; HEIGHT: 200px; CURSOR: hand" id="BLOGGER_PHOTO_ID_5433293619910914338" border="0" alt="" src="http://2.bp.blogspot.com/_hkkBsHR0r5s/S2bvwq8vsSI/AAAAAAAAALw/kKecbggrhiM/s200/LocalServer6.png" /></a><br /></li><br /><li>After a delay you should see the first DNN install screen. Choose custom:<br /><a href="http://1.bp.blogspot.com/_hkkBsHR0r5s/S2bvw5rZzgI/AAAAAAAAAL4/kNvsBcVm0co/s1600-h/LocalServer7.png"><img style="WIDTH: 200px; HEIGHT: 168px; CURSOR: hand" id="BLOGGER_PHOTO_ID_5433293623864708610" border="0" alt="" src="http://1.bp.blogspot.com/_hkkBsHR0r5s/S2bvw5rZzgI/AAAAAAAAAL4/kNvsBcVm0co/s200/LocalServer7.png" /></a><br /></li><br /><li>Test file permissions.<br /></li><br /><li>Now, set up your database connection:<br /><ul><br /><li>First, click on "SQL Server 2005/2008 Database" and wait for the screen to refresh</li><br /><li>Enter "(local)" for the Server</li><br /><li>Enter the database name, eg "dnn522pen"</li><br /><li>Uncheck "Integrated Security" and wait for the screen to refresh</li><br /><li>Enter the User ID and password</li><br /><li>Keep "Run as db Owner" checked</li><br /><li>If desired, enter a database table name qualifier, eg "DNN_"</li><br /><li>Click on "Test Database Connection"</li><br /><li>If OK, click on Next</li><br /></ul><br /><a href="http://4.bp.blogspot.com/_hkkBsHR0r5s/S2bvxHwvLmI/AAAAAAAAAMA/bge8bEmLOUk/s1600-h/LocalServer8.png"><img style="WIDTH: 200px; HEIGHT: 168px; CURSOR: hand" id="BLOGGER_PHOTO_ID_5433293627645177442" border="0" alt="" src="http://4.bp.blogspot.com/_hkkBsHR0r5s/S2bvxHwvLmI/AAAAAAAAAMA/bge8bEmLOUk/s200/LocalServer8.png" /></a><br /></li><br /><li>Proceed with the rest of the DNN installation as normal.<br /></li><br /><br /></ol></div>Chris Canthttp://www.blogger.com/profile/11367082039820244178noreply@blogger.com0tag:blogger.com,1999:blog-32442424.post-30910232143020489552010-01-15T15:25:00.007+00:002010-01-23T12:35:17.870+00:00Importing Identity column data into SQL ServerUpdated 23/1/10:<br />I am in the process of porting some (DNN module) tables from one Microsoft SQL Server database to another. For various reasons, I want the primary key identity column values to remain the same. This is on a shared SQL server, so we don't have admin access, eg to the command line and file system.<br /><br />The SQL Server Import and Export wizard transfers the data nicely and will preserve the identity values if you do it correctly.<br /><br />There are two crucial tricks:<br />- set up the destination table(s) with the correct primary keys etc before the copy<br />- in the wizard, click Edit Mapping and tick Enable Identity Insert.<br /><br />----------------------<br />This is alternative technique which should no longer be needed:<br /><br />I first export the original data to a temporary database. This creates the tables but but does not create primary keys, set default values etc. However, the copy process does preserve the original primary key identity column values.<br /><br />By installing the DNN module on the destination system, the tables are created correctly there. However, a wizard import into these tables (even with "Enable identity insert" set) does not preserve the key values.<br /><br />The trick I found is to upload the data into a new table eg "Form2", and then use the following code to copy the data into the correct "Form" table. The trick is to use the "set identity_insert" statement.<br /><pre>set identity_insert DNN_Form on;<br /><br />insert into DNN_Form ([FormID],[FormName])<br />select * from DNN_Form2;<br /><br />set identity_insert DNN_Form off;</pre><br />You must list all the required columns in the insert statement.<br />Only one table can have identity_insert on at a time.<br /><br />Finally, delete the "Form2" table.<br /><br />You can check the current insert identity value before and after as follows:<br /><pre>DBCC CHECKIDENT(DNN_Form);</pre>Chris Canthttp://www.blogger.com/profile/11367082039820244178noreply@blogger.com0tag:blogger.com,1999:blog-32442424.post-91519857173874524052010-01-14T20:37:00.003+00:002010-01-14T20:46:33.852+00:00Wandering drive letters in WindowsMy main computer's motherboard died - long live the new computer! Thankfully, all the data on the two existing drives were OK. A hastily bought 3.5 inch USB drive caddy worked a treat, keeping me going on the laptop.<br /><br />The new computer was pre-installed with Windows 7 Home Premium on drive C with a separate partition on drive D for data. I have an MSDN subscription and I wanted to set up various versions of Windows multi-booting on the same computer. I have two different systems that I want to run normally, one with Visual Studio 2008 and the other with various older bits of software. I also want to have some different versions of Windows for software testing, ideally Windows 7, Vista and XP, both in x86 and x64 versions.<br /><br />OK - I know I can run a virtual PC, but I want to use two of these systems regularly at top speed. I haven't tried virtual PC yet, so I decided to stick with what I knew, which is the HyperOs multibooter, though I had to upgrade to the latest version.<br /><br />The first task was to repartition the disk, to give more partitions, each with a Windows installation, as well as big photo/video partition. I found that there were already two extra partitions, a (Packard Bell?) Recovery Partition and a small System Reserved partition set up by Windows 7, ie 4 Primary partitions in total. The W7 Disk Management tool wanted to convert my disk to Dynamic Disks to give me more partitions, but these cannot be used for booting, so a swift exit was called for.<br /><br />HyperOS mentioned the Acronis partitioner but this wasn't playing - not sure if it was Windows 7 or the size of the disk. I did have a copy of gparted on CD, but this hadn't worked for me before. Eventually I found Partition Wizard www.partitionwizard.com which has worked brilliantly. I got a free commercial licence for this. <br /><br />Using Partition Wizard, I deleted (empty) drive D and made quite a few other Logical partitions. Partition Wizard is clever enough to know that it cannot do changes in some circumstances, and so does it on reboot. Partition Wizard can also resize partitions, moving data if need be. So far that's worked fine. (Check your computer power settings don't shut your disk down at an awkward moment.)<br /><br />Having done that, I could now copy all my precious data onto the new hard disk from the drive caddy.<br /><br />Anyway, I've now done various Windows installations, some by installing from DVD from Windows, but mostly by installing from DVD at reboot, ie choosing DVD at boot up from the BIOS boot menu - and choosing Custom Setup to choose the install partition. The problem that I have found is that the drive letters that Windows uses and sees seems to change a lot. The original W7 system is on drive C on the "first" partition. I have another W7 on the fifth partition which thinks of itself as being on drive M - fine. However, most other W7 and Vista installations think of themselves as being drive C, even though they are on partition 4, 7 or 8. When I have rebooted in one of these systems, the drive letters are assigned in a fairly random way. The DVD drive is usually drive E but not always.<br /><br />In Windows Disk Management you change the drive letters - for some drives at least. But there's limited scope for what you can change to. Partition Wizard can do this, and is probably more successful. However there appears to be no way of persuading a Windows on partition 4 (that thinks it is at drive C) to think of itself as being drive L for example.<br /><br />All these wandering drive letters might not be problem. However my software development stuff and business data has always been carefully set up (for various reasons) to be stored on both drives C and D. I don't tend to put data in "My Documents", "Documents", "Pictures", etc because these are (usually) stored in different locations for each version of Windows. Anyway, I have persuaded partition 2 to be drive D in all the installations so far. However it was a problem that partition 1 kept wandering all over the shop. My solution was to move all my crucial data from drive C onto a bigger drive D. Ok - fairly simple in itself, but I'm still having to work out what dependencies there are in all my scripts.<br /><br />Another complication in this process was that Windows XP was dying during installation (with a BSOD). This turned out to be because the SATA drives were being accessed using AHCI. Changing the BIOS to use the SATA setting "Native IDE" got the XP installation to work. However I did not want to leave this setting as is, so I change it whenever I want to switch to XP. XP also doesn't recognise many of the motherboard peripherals, eg Ethernet, so the installation is not very useful. The option to press F6 during installation would let me install a suitable driver, but (a) I don't have the driver and (b) the system doesn't have a floppy; it does look as though new motherboard has a floppy interface, but there's no connector soldered in there!<br /><br />I was also able to add an IDE/PATA cable and drive to the system to connect my old drives, but the installation still did not work if I was in AHCI SATA mode.<br /><br />I've still many applications to configure and systems to set up, but I'm getting there.Chris Canthttp://www.blogger.com/profile/11367082039820244178noreply@blogger.com2tag:blogger.com,1999:blog-32442424.post-41114207891514862062009-09-25T11:49:00.017+01:002011-09-17T09:05:02.131+01:00Starting to program vtiger CRM<p>This article applies to vtiger 5.1.0 - some of the advice/code may not be accurate for later versions.</p><br /><p>We've been investigating the vtiger CRM as a component in an open source set of tools that a business or organisation might need. A crucial idea is to link various tools together, so I have been looking at how to get information into and out of vtiger.</p><p>As a start, we're looking at an Outlook add-in to add an email address/name to the vtiger Contacts list, and how to export to an accounts package. There is already an Outlook vtiger add-in, but it seems pretty flaky and only does synchronisation; we want it to check a new email for existing matches and only add it if need be.</p><p><strong>About vtiger</strong><br /><br />vtiger is a Customer Relationship Manager (CRM). vtiger deals with Leads who might turn into Contacts, with a Contact associated with an Account. You can set up Products with stock levels (and Services as well) and then generate Quotes and Invoices which can be downloaded as PDFs or sent by email. All that sort of thing. It is important to know that vtiger is designed for use only by a business' staff - it is not intended to be used by customers, although vtiger can check incoming support emails.</p><p>vtiger is extensible and there is some basic documentation to get you started making your own modules. The instructions effectively describe how to create a new object type (eg a time sheet), represented as a table or two in the vtiger database. You can link your module and its new object type into vtiger, so that eg menu [Tools][Time sheet] shows a list of time sheets in the standard vtiger style, clicking on a record shows the detail, with an Edit option available.</p><p>For the rest of this piece, I'm looking at adding functionality to an existing module. This will hopefully help you even if you are starting a complete module. Be careful if you alter existing code - it may well be overwritten if you upgrade vtiger.</p><p><strong>The vtiger database</strong><br /><br />You will almost certainly want to have access to the database that vtiger uses, partly so you can see what's in there, but also so you see any changes as they really are. For MySQL, using phpMyAdmin will probably be the available tool - make sure you can get in to your database.</p><p>If there aren't any already, make some test Contacts within vtiger. In phpMyAdmin have a look at these tables:</p><ul><li><span style="font-family:courier new;">vtiger_contactdetails</span></li><li><span style="font-family:courier new;">vtiger_contactscf</span></li><li><span style="font-family:courier new;">vtiger_contactaddress</span></li><li><span style="font-family:courier new;">vtiger_contactsubdetails</span></li></ul><p>Each of these tables has a column named 'contactid' or similar as a primary or foreign key. Note that the "contact_no" column is what is displayed is vtiger as the "Contact Id", eg 'CON1'.</p><p>The table <span style="font-family:courier new;">vtiger_modentity_num</span> is used when generating the next "contact_no" value, for the appropriate module, so 2 might be next free Contact number to make 'CON2'. Later, we'll use vtiger <span style="color:#33cc00;">CRMEntity</span> class <span style="color:#33cc00;">setModuleSeqNumber()</span> function to get this value.</p><p>The 'contactid' numbers identifying the contact don't start at 0 or 1. In fact, vtiger assigns a unique id number across all objects, so object 792 might be a Contact and 793 might be an Invoice. These details numbers are stored in the <span style="font-family:courier new;">vtiger_crmentity</span> table - a deleted flag in there is set when items are first deleted - so they can recovered from the recycle bin. Later, we'll use the <span style="color:#33cc00;">insertIntoCrmEntity()</span> function to get a new object id.</p><p><strong>vtiger command structure</strong><br /><br />All vtiger accesses come through one root file <span style="font-family:arial;">index.php</span>. The parameters determine which module operation is carried out. For example, consider this URL<br /><br /><span style="font-family:courier new;">/index.php?action=DetailView&module=Invoice&record=800&parenttab=Inventory</span><br /><br />It runs the code <span style="font-family:arial;">DetailView.php</span> in the directory <span style="font-family:arial;">/modules/Invoice/</span>. The code that runs in there, finds the "record" value and shows the relevant invoice. So, you can add to the "Detail View" functionality by adding code to this file.</p><p>You can create your own actions by making a new file in the relevant directory. You then need to provide a means of invoking that action - more later.</p><p><strong>Smarty templates</strong><br /><br />vtiger uses the Smarty template system to generate its output. By and large, the vtiger code doesn't generate any HTML - instead, it loads up a template from TPL files in the <span style="font-family:arial;">/Smarty/templates/</span> directory. vtiger PHP code sets various values that are combined into the template to form the final output. Smarty has an extensive control language available for use in a template, so it can cope with complicated structures, eg it can iterate through arrays to generate multiple table rows. One Smarty template can invoke another, so finding where something is generated can be tricky.</p><p><strong>Adding a tool option to an Invoice</strong><br /><br />If you view an Invoice there are Tools listed on the right hand side: "Export to PDF" and "Send Email with PDF". To add another option here, you have to drill down through the tpl files: <span style="font-family:arial;">DetailView.php</span> invokes template <span style="font-family:arial;">Inventory/InventoryDetailView.tpl</span> which includes template <span style="font-family:arial;">Inventory/InventoryActions.tpl</span>.</p><p>In <span style="font-family:arial;">InventoryActions.tpl</span>, after this line:</p><pre><!-- To display the Export To PDF link for PO, SO, Quotes and Invoice - ends --></pre><p>add in this code:</p><pre>{if $MODULE eq 'Invoice'}<br />{assign var=send_accounts_action value="SendAccounts"}<br /><tr><td align="left" style="padding-left:10px;"><br /><a href="index.php?module={$MODULE}&action={$send_accounts_action}&return_module={$MODULE}&return_action=DetailView&record={$ID}&return_id={$ID}" class="webMnu"><img src="{'actionGenerateInvoice.gif'@vtiger_imageurl:$THEME}" hspace="5" align="absmiddle" border="0"/></a><br /><a href="index.php?module={$MODULE}&action={$send_accounts_action}&return_module={$MODULE}&return_action=DetailView2&record={$ID}&return_id={$ID}" class="webMnu">Send to Accounts</a><br /></td><br /></tr><br />{/if}</pre><p>Upload <span style="font-family:arial;">InventoryActions.tpl</span> and view an Invoice - check that "Send to Accounts" is visible on the right.</p><p><strong>Adding to accounts</strong><br /><br />Now, create a file <span style="font-family:arial;">SendAccounts.php</span> - probably copying an existing file such as <span style="font-family:arial;">DetailView.php</span> makes sense.</p><p>OK - this bit is skimpy for now, but it might get you started. Near the end, add in the following code to query the vtiger database and display a little info from each record found. To get the list of products ordered you will also have to query the <span style="font-family:courier new;">vtiger_inventoryproductrel</span> table.</p><pre>$query="select * from vtiger_invoice where invoiceid=?";<br />$invoice_id = $focus->id;<br />$result = $adb->pquery($query, array($invoice_id));<br />$noofrows = $adb->num_rows($result);<br />$final_output = "";<br />while ($inv_info = $adb->fetch_array($result))<br />{<br />$invoice_no = $inv_info['invoice_no'];<br />$subject = $inv_info['subject'];<br />$invoicedate = $inv_info['invoicedate'];<br />$accountid = $inv_info['accountid'];<br />$total = $inv_info['total'];<br />$final_output .= "$invoice_no: $subject $invoicedate $accountid $total<br/>";<br />}<br />$smarty->assign("SEND_DETAILS", $final_output);<br />$smarty->display("Inventory/SendToAccounts.tpl");</pre><p>At the end of our new code, the Smarty variable "<span style="color:#3366ff;">SEND_DETAILS</span>" is set to the string we want to display. Then the Smarty template <span style="font-family:arial;">SendToAccounts.tpl</span> is run.</p><p>You had best create the template file <span style="font-family:arial;">SendToAccounts.tpl</span> initially as a copy of an existing template, eg <span style="font-family:arial;">InventoryDetailView.tpl</span>. You will need to pare it down - and eventually add in a line like the following to display your output:</p><pre><p><b>Storing data</b>: {$SEND_DETAILS}</p></pre><p>OK - all we've done so far is display the invoice details. Actually talking to an accounts system is beyond the scope of this article.</p><p><strong>Adding a new contact</strong><br /><br />As I said earlier, I want to have an Outlook add-in that can be used to add a contact to the vtiger database, after checking to see whether the contact already exists. As the first step to getting this done, I set up a small web form that has First name, Last name and Email fields. The Go button is set up to go to this vtiger URL eg:<br /><br /><a><span style="font-family:courier new;">/index.php?action=AddOutlookContact&module=Contacts&FirstName=Chris&LastName=Cant&Email=sales@phdcc.com</span></a></p><p>As per usual, vtiger uses its standard rules so that this script is called:<br /><span style="font-family:arial;">/modules/Contacts/AddOutlookContact.php</span></p><p>I made <span style="font-family:arial;">AddOutlookContact.php</span> in a similar way to that described above, using the title "Import from Outlook" for the main content tab. The code generates the output HTML in the PHP script - a neater final approach would put the raw HTML within the template. Anyway, the PHP code generates its HTML in <span style="color:#33cc00;">$Output</span> and then invokes my new template <span style="font-family:arial;">AddOutlookContact.tpl</span>:</p><pre>$smarty->assign("IMPORT_DETAILS", $Output);<br />$smarty->display("AddOutlookContact.tpl");</pre><p>In the guts of <span style="font-family:arial;">AddOutlookContact.tpl</span>, the output is reproduced:</p><pre><div>{$IMPORT_DETAILS}</div></pre><p>The tasks within <span style="font-family:arial;">AddOutlookContact.php</span> are:</p><ul><li>Get passed parameters</li><li>Check parameters</li><li>Find any matching names or emails</li><li>Show matches to user</li><li>Show Add Contact button</li></ul><p>So: the user can see any existing matches, and can decide whether or not to press the Add Contact button, which goes to another script <span style="font-family:arial;">AddOutlookContact2.php.</span></p><p>The SQL script to query the existing contacts is as follows:</p><pre>SELECT c.*, a.accountname, crm.deleted FROM vtiger_contactdetails AS c<br />LEFT JOIN vtiger_account AS a USING (accountid)<br />INNER JOIN vtiger_crmentity AS crm ON crm.crmid=c.contactid<br />WHERE lastname LIKE '%$QuotedLastName%' OR email LIKE '%$QuotedEmail%'";</pre><p>Note that I also use the function <span style="color:#33cc00;">mysql_real_escape_string()</span> to ensure that the passed strings cope with single and double quotes etc.</p><p>The SQL links <span style="font-family:courier new;">vtiger_contactdetails</span> with the <span style="font-family:courier new;">vtiger_crmentity</span> table so the Deleted column can be picked up. The <span style="font-family:courier new;">vtiger_account</span> table is also included so any associated account name can be found.</p><p>The code goes through all the found matches and displays the results to the user. I provided a link to each contact - this is the code for the link:</p><pre>$MatchLink = "index.php?action=DetailView&module=Contacts&record=$Match_ContactId&parenttab=Support";</pre><p>The following form shows a suitable "Add new contact" button:</p><pre><form action='index.php' method='post' onsubmit='VtigerJS_DialogBox.block();'><br /><input type='hidden' value='AddOutlookContact2' name='action' /><br /><input type='hidden' value='Contacts' name='module' /><br /><input type='hidden' value='$htmlLastName' name='LastName' /><br /><input type='hidden' value='$htmlFirstName' name='FirstName' /><br /><input type='hidden' value='$htmlEmail' name='Email' /><br /><input type='submit' value='Add new contact' /><br /></form></pre><p><span style="font-family:arial;">AddOutlookContact2.php</span> has a similar core as before, except it generates HTML that shows what it has done. Here's what I did to add a new contact. This is probably not the recommended vtiger method, but it seems to get the job done.</p><pre>$adb->startTransaction();<br />$NewContactNo = $focus->setModuleSeqNumber("increment",'Contacts'); // updates vtiger_modentity_num<br />$focus->insertIntoCrmEntity('Contacts'); // updates vtiger_crmentity and sets $focus->id<br /><br />$query = "insert into vtiger_contactdetails (contactid,contact_no,firstname,lastname,email) values(?,?,?,?,?)";<br />$qparams = array($focus->id, $NewContactNo, $FirstName, $LastName, $Email);<br />$adb->pquery($query, $qparams);<br /><br />$query = "insert into vtiger_contactscf (contactid) values(?)";<br />$qparams = array($focus->id);<br />$adb->pquery($query, $qparams);<br /><br />$query = "insert into vtiger_contactaddress (contactaddressid) values(?)";<br />$qparams = array($focus->id);<br />$adb->pquery($query, $qparams);<br /><br />$query = "insert into vtiger_contactsubdetails (contactsubscriptionid) values(?)";<br />$qparams = array($focus->id);<br />$adb->pquery($query, $qparams);<br /><br />$adb->completeTransaction();</pre><p>Here are the steps:</p><ul><li>Call <span style="color:#33cc00;">setModuleSeqNumber()</span> to get the next contact no eg CON2</li><li>Call <span style="color:#33cc00;">insertIntoCrmEntity()</span> to get the next contact id, eg 794</li><li>Insert the basic details into <span style="font-family:arial;">vtiger_contactdetails</span></li><li>Insert the contact id into <span style="font-family:arial;">vtiger_contactscf</span></li><li>Insert basic rows into <span style="font-family:arial;">vtiger_contactaddress</span> and <span style="font-family:arial;">vtiger_contactsubdetails</span></li></ul><p>The output provides a suitable link to the new Contact.</p><p>Later, I made a real Outlook 2007 addin to call this vtiger code. An extra button is shown on the ribbon when you read an email. When you click the button, it picks up the sender's name and email address and shows the correct URL with parameters in the user's default browser.</p>Chris Canthttp://www.blogger.com/profile/11367082039820244178noreply@blogger.com10tag:blogger.com,1999:blog-32442424.post-60660668241921119272009-08-26T15:28:00.010+01:002010-03-17T20:48:48.457+00:00DNN no container span causes validation errorIf you turn off the DNN container for a module, then DNN by default inserts an extra SPAN that can cause a W3C validation error. One solution is to have a minimal container.<br /><br />I had turned off the "Display Container" option for some modules so that no title is displayed. (In Admin Edit mode, the default container is used so that you can get at settings etc.) The HTML produced by DNN looked like this, within the skin pane:<br /><span style="font-family:courier new;"><a name="402"></a>/<br /><span id="dnn_ctr402_ContentPane" class="DNNAlignleft"><br /><!-- Start_Module_402 --><br /><div id="dnn_ctr402_ModuleContent"></span><br />and then the module content.<br /><br />This causes a validation error because you cannot nest a DIV within a SPAN.<br /><br />My solution is not to use the "Display Container" option - instead, I created a container with no title and used that instead. In the replacement container, I moved the [ACTIONS] and [ICON] into the footer. This NoTitleContainer container files are available here: <a href="http://www.phdcc.com/download/DNN/NoTitleContainer.zip">http://www.phdcc.com/download/DNN/NoTitleContainer.zip</a>. Unzip in /Portals/_default/Containers/MinimalExtropy. I'm not sure if you need to go to [Admin][Skins] to make the container visible.Chris Canthttp://www.blogger.com/profile/11367082039820244178noreply@blogger.com9tag:blogger.com,1999:blog-32442424.post-12312422584208403182009-06-25T22:26:00.005+01:002009-06-26T09:36:56.589+01:00DNN Event Viewer spider errorsIf you are getting "Page Load Exception" errors in the DotNetNuke (DNN) [Admin][Event Viewer] with the UserAgent indicating a web crawling spider, then you need this fix:<br /><br /><strong>The fix: </strong>Get the <a href="http://www.oliverhine.com/DotNetNuke/Downloads.aspx">Browser Caps - v2</a> by Oliver Hine. Unzip the file <code>App_Browsers.zip</code> to get the file <code>OtherSpiders.browser</code> - put this in the <code>App_Browsers</code> directory of your DNN site. Note that your DNN site will automatically restart, so perhaps do this at a quiet time.<br /><br />Alternatively (if you want), you should be able to stop these spiders accessing your site using a suitable <code>robots.txt</code> file.<br /><br />This patch is needed for DNN 4.9.4 and possibly earlier. I think that it may be fixed in DNN 5.1+.<br /><br /><strong>Spiders fixed: </strong>This file has fixes for these spiders: Baiduspider, Yandex, ia_archiver, Sphere, Feedfetcher-Google, Yanga, worio, zibber, <strike>twiceler, voliabot</strike>, aisearchbot, robotgenius and R6_FeedFetcher.<br /><br /><strong>Spiders not fixed:</strong> However it doesn't fix TweetmemeBot - I failed to add support for this. Read this <a href="http://mark.kolich.com/2009/04/tweetmemebots-invalid-user-agent-string.html">Tweetmeme</a>.<br /><br />Update: doesn't seem to work for Twiceler or voilabot.<br /><br /><strong>What's happening: </strong>DNN code is asking ASP.NET for details of the browser capabilities. ASP.NET gets this from the UserAgent string. DNN asks for the major and minor version numbers of the browser and causes an exception if these cannot be determined from the UserAgent. ASP.NET uses various <code>.browser</code> files (in the framework directories and in the web app's <code>App_Browsers</code> directory) to work out what browsers can do. As the UserAgent provided by these spiders doesn't follow a standard regex pattern, ASP.NET cannot get the major and minor versions.<br /><br /><strong>Thanks to: </strong><a href="http://twitter.com/BarrySweeney">Barry Sweeney</a> for his prompt help on Twitter - and Oliver Hine of course.<br /><br />Below is the DNN exception that I get before applying this fix:<br /><br /><hr /><br /><br /><br />Message: DotNetNuke.Services.Exceptions.PageLoadException: Value cannot be null. Parameter name: String ---> System.ArgumentNullException: Value cannot be null. Parameter name: String at System.Number.StringToNumber(String str, NumberStyles options, NumberBuffer& number, NumberFormatInfo info, Boolean parseDecimal) at System.Number.ParseInt32(String s, NumberStyles style, NumberFormatInfo info) at System.Web.Configuration.HttpCapabilitiesBase.get_MajorVersion() at DotNetNuke.UI.Utilities.ClientAPI.BrowserSupportsFunctionality(ClientFunctionality eFunctionality) at DotNetNuke.UI.Utilities.ClientAPI.get_ROW_DELIMITER() at DotNetNuke.UI.Utilities.ClientAPI.RegisterClientVariable(Page objPage, String strVar, String strValue, Boolean blnOverwrite) at DotNetNuke.UI.Skins.Controls.Search.Page_PreRender(Object sender, EventArgs e) at System.Web.UI.Control.OnPreRender(EventArgs e) at System.Web.UI.Control.PreRenderRecursiveInternal() at System.Web.UI.Control.PreRenderRecursiveInternal() at System.Web.UI.Control.PreRenderRecursiveInternal() at System.Web.UI.Control.PreRenderRecursiveInternal() at System.Web.UI.Control.PreRenderRecursiveInternal() at System.Web.UI.Control.PreRenderRecursiveInternal() at System.Web.UI.Page.ProcessRequestMain(Boolean includeStagesBeforeAsyncPoint, Boolean includeStagesAfterAsyncPoint) --- End of inner exception stack trace ---Chris Canthttp://www.blogger.com/profile/11367082039820244178noreply@blogger.com6tag:blogger.com,1999:blog-32442424.post-91060456066623902582009-05-27T16:41:00.007+01:002009-05-27T18:11:33.974+01:00DNN Skin token support in a moduleThis article shows how to get a DotNetNuke (DNN) module to support tokens in skins. It will use, as an example, how I added support for the [CODEMODULE] skin token in our <a href="http://www.phdcc.com/phdcc.CodeModule/">phdcc.CodeModule</a> retail module.<br /><br /><strong>Introduction</strong>: skins are used by DNN as a template for the whole site output, and a container template is used to wrap an individual module. Skins and containers can contain standard tokens such as [LOGO] - this is replaced at runtime by the logo image chosen by the site administrator.<br /><br /><strong>Doing it manually</strong><br /><br />The crucial trick is to add a suitable entry to the <span style="font-family:courier new;">ModuleControls</span> table.<br /><br />This table primarily contains a mapping between the blank (View), Edit and Settings control keys for a module and the actual controls that implement them, eg the "Settings" ControlKey is set to "DesktopModules/phdcc.CodeModule/Settings.ascx" for my module.<br /><br />To support your new skin token, add a new row with your skin token name in the <em>ControlKey</em> column (eg "CodeModule" with no quotes or brackets), a NULL for the <em>ControlTitle</em>, your view control in <em>ControlSrc</em> and -2 in <em>ControlType</em>. For my module, the <em>ControlSrc</em> is "DesktopModules/phdcc.CodeModule/View.ascx"<br /><br /><strong>Adding a token to a skin</strong><br /><br />People work with skins in various ways. The standard method is to have an HTML template file that is parsed into an ASCX when the skin is uploaded. If you edit the HTML file on a live site then you can click on [Parse Skin Package] to re-make the ASCX.<br /><br />Anyway, add your skin token to your HTML skin somewhere, eg add "[CODEMODULE]", without quotes but with brackets - and in upper case. Either package it up and upload, or edit the live version and click on [Parse Skin Package]. The parse log should show your token being replaced. The ASCX should have your token replaced, as per the example below.<br /><br />If you work with an ASCX template file, then you need to add a suitable Register directive at the top of the ASCX, eg:<br /><span style="font-family:courier new;"><%@ Register TagPrefix="dnn" TagName="CODEMODULE" Src="~/DesktopModules/phdcc.CodeModule/View.ascx" %></span><br />then add instances of this control later on, eg:<br /><span style="font-family:courier new;"><dnn:codemodule runat="server" id="dnnCODEMODULE" /></span><br /><br />Hopefully your view control should now be called whenever this skin is used. If so, hurrah! Note that the PortalModuleBase TabModuleId is set to -1 so you could use this to tell when you are being called from a skin.<br /><br /><strong>Token parameters<br /></strong><br />It is useful to be able to pass parameters to your module, eg so it knows what to display. The parameters are set in the skin.xml file alongside your HTML template. For my module, I wanted to add a ControlFile parameter to tell my module what to do. I added this to skin.xml, after <Objects><br /><br /><span style="font-family:courier new;"><object><br /> <token>[CODEMODULE]</token><br /> <settings><br /> <setting><br /> <name>ControlFile</name><br /> <value>viewSkin.ascx</value><br /> </setting><br /> </settings><br /></object><br /></span><br />You can specify several settings for your token. In this case, I set the "ControlFile" setting to value "viewSkin.ascx". Make sure [CODEMODULE] is in upper case.<br /><br />To support each parameter, you must add a corresponding property to your control. So, in this case, I must add a property called "ControlFile". In VB, this could be:<br /><br /><span style="font-family:courier new;">Private _ControlFile As String<br /></span><br /><span style="font-family:courier new;">Public Property ControlFile() As String<br /> Get<br /> Return _ControlFile<br /> End Get<br /> Set(ByVal value As String)<br /> _ControlFile = value<br /> End Set<br />End Property</span><br /><br />If you upload or re-parse your skin, you should find that the parameter has now been set in the ASCX, eg:<br /><span style="font-family:courier new;"><dnn:codemodule runat="server" id="dnnCODEMODULE" ControlFile="viewSkin.ascx" /> </span><br /><br />When your view control is now run, the ControlFile property should have been set before your Page_Load is called. You can use this to tell when you have been called from a skin - and act accordingly.<br /><br /><strong>Automatic installation</strong><br /><br />Earlier on, we added the crucial row to the <span style="font-family:courier new;">ModuleControls</span> table by hand. To set this automatically during installation, you need to add an appropriate data provider. For example, this might be called 01.00.00.SqlDataProvider. Add in SQL code to delete any existing ControlKey and add the correct one, eg:<br /><span style="font-family:courier new;">DELETE FROM {databaseOwner}{objectQualifier}ModuleControls<br /> WHERE ControlKey='CodeModule'<br />INSERT INTO {databaseOwner}{objectQualifier}ModuleControls<br /> (ControlKey,ControlTitle,ControlSrc,ControlType)<br /> VALUES ('CodeModule',NULL,'DesktopModules/phdcc.CodeModule/View.ascx',-2) </span><br /><br />Finally, add in an entry to your Uninstall.SqlDataProvider file to remove this entry if your module is removed:<br /><span style="font-family:courier new;">DELETE FROM {databaseOwner}{objectQualifier}ModuleControls<br /> WHERE ControlKey='CodeModule'</span>Chris Canthttp://www.blogger.com/profile/11367082039820244178noreply@blogger.com4