Programming

Big Problem : Access Denied when Using Qwinsta.exe or WTSOpenServer API

This article describes the solution for the “access denied” error occurs when using qwinsta.exe/rwinsta.exe or WTSOpenServer Windows API function to access to a Windows XP SP2 PC.

The same thing happens when you are trying to connect to a Windows XP SP2 using Terminal Services Manager from a Windows 2003 Server. 

- Because essentially qwinsta.exe command calls WTS functions (WTSOpenServer, WTSEnumerateSessions, …), you will encounter the same error when using either the command or the API.

As a quick answer, check the following points.
1. Ensure you have the correct credential
Although you do not need to provide a username and a password when executing qwinsta.exe or WTSOpenServer, Windows use its stored credentials automatically, just like accessing a shared folder.

The easiest way to confirm the credential is to use Explorer to access remote server’s C$ share. Also, you can use “net use” command.

For those who want to connect to a remote server programmatically using WTSOpenServer API, WNetAddConnection2 function should be enough to make sure you got a piece of credential before calling WTSOpenServer function.

Of course the user in the credential should be a use on the remote server who has enough privilege to carry out the operation.

2. Open RPC ports on firewall
An API call involves connecting to a remote machine use RPC. In most cases the RPC service is running and you can confirm that from the service management interface. To open RPC ports, simply enable “File and Printer Sharing” in the Windows Firewall setting.

(Generally, if the RPC is blocked by the firewall on remote machine, the error should be "1722 RPC server is unavailable" rather then "5 Access denied")

3. Disable "Force Guest" log on

You can change this option from either local security policy or registery.

Local security policy : run secpol.msc > Secuirty Settings > Local Policies > Security Options > Network access : Sharing and security model for local accounts, and set to "Classic".

Registry : find "forceguest" item in HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Lsa, and set it to 0.

The two ways are equivalent. If this option is set to 1, remote logged in user will be impersonated as Guest account (and if Guest account is disabled, login fails). Set it to 0 enables user logging in as themselves.

4. If you still receive “access denied” and it only happens when the remote OS is Windows XP SP2, it is very probably the following case.

To confirm, you need a packet sniffer such as Wireshark (a great freeware).
When you use a sniffer to capture the SMB packets, you can see the authentication is OK (NTLM if workstation, Kerberos if Windows 2003 domain), but the RPC get nca_s_fault_access_denied (0×00000005) as error code.

This means the remote RPC component failed to execute the requested operation. It is because in Windows XP SP2, it is not allowed to operate on terminal service (Remote Desktop) through RPC by default. To modify this setting to enable Remote Desktop API through RPC, you need to find the following registry key:

HKLM\SYSTEM\CurrentControlSet\Control\Terminal Server

Then add a DWORD value named “AllowRemoteRPC” and change its value to 1.

I googled it and it seems there is no official document on it. (I don’t know why)

Access denied error often misleads us into thinking it must be because of the insufficient privilege, or because that the qwinsta.exe command / WTSOpenServer failed to utilized the credential. It wasted me about 5 hours in researching how to attach a credential to a RPC call, and I even got caught deeply into the complicated SSPI/LSA authentication architecture.

Wordpress 2.0.x bug: wpautop Generates needless br Tag for colgroup

在最近的一篇blog中,使用了colgroup HTML标签。没想到,使用了colgroup的帖子经过XHTML Validator的检查,发现居然不符合XHTML规范。究其原因,说是发现了如下错误:

Error Line 78 column 139: document type does not allow element "br" here.
...ALIGN: center"></col></colgroup><br />

但是奇怪的是,我的源代码中,</colgroup>后并没有<br />。我们可以用W3Shool给出的最简单的例子进行实验:

<table border="1">
  <colgroup span="3" style="color:#FF0000;"></colgroup>
  <tr>
    <td>1</td><td>2</td><td>3</td><td>4</td>
  </tr>
</table>

试着在Wordpress (2.0.x,包括最新的2.0.10。在2.2上则没有这个bug) 中编辑一个新帖子,在代码画面敲入上面这段代码,发布,再去看生成的HTML时就会发现变成了这个样子:

<table border="1">
  <colgroup span="3" style="color:#FF0000;"></colgroup><br />
  <tr>
    <td>1</td><td>2</td><td>3</td><td>4</td>
  </tr>
</table>

由于</colgroup>和<tr>之间是不允许有<br/>的,所以导致XHTML验证错误。

那么追根到底是在什么时候这个<br/>被添加到输出的HTML中的呢?调查了一下数据库,发现在数据库里面的内容还是没有<br/>的,这说明它是在读取post的内容时,由相关的Filter添加的。一个post的内容在被从数据库读出来,到被显示在网页上之间使用的是叫做the_content的Filter Tag。在Wordpress中,默认有如下Filter被添加到the_content的处理中:

add_filter('the_content', 'wptexturize');
add_filter('the_content', 'convert_smilies');
add_filter('the_content', 'convert_chars');
add_filter('the_content', 'wpautop');

经过检查,发现位于/wp-includes/functions-formatting.php中的wpautop函数中有如下代码:

function wpautop($pee, $br = 1) {
 // …
 if ($br) $pee = preg_replace(‘|(?<!<br />)\s*\n|’, "<br />\n", $pee); // optionally make line breaks
 $pee = preg_replace(‘!(</?(?:table|thead|tfoot|caption|tbody|tr|td|th|div|dl|dd|dt|ul|ol|li|pre|select|form|blockquote|address|math|p|h[1-6])[^>]*>)\s*<br />!’, "$1", $pee);
 // …
 return $pee;
}

可见,该函数中用preg_replace函数通过正则表达式删除掉了添加在table,thead,tfoot等后面的<br />,却没有删除colgroup后面的<br/>。因此,只要在这一行添加colgroup即可修正这个bug。将修改后的functions-formatting.php重新上传到服务器后,在此进行XHTML验证,正常通过。

Difference between em and percent font-size in Firefox

刚刚发现了百分比(%)在IE和在Firefox中的表现并不相同,现在居然发现即便是在Firefox内部,em和%的表现也并不一样。

首先先要讨论一下,作为font-size等CSS属性的单位,78%和0.78em到底是否相同?

遗憾的是,我并不是CSS专家,所以我没法回答按照标准这两个单位是否完全相同。但是,根据某些权威网站的说明

Both em and % are relative measures. The difference is what each is relative to. Em is always relative to font size. % is relative to the containing block, usually the body, a div, or a table, unless applied to font-size, in which case it applies to the parent font-size.

也就是说,在用于font-size的时候,两者的涵义应该是相同的。但是,当我用下面在Firefox (ver 2.0.0.3, 默认字体微软雅黑16px)上显示如下HTML片段时,却出现了令我不解的结果:

<body style="font-size:100%">
  <span style="font-size:0.97em">这个测试用来比较不同字体单位下的表示情况。[0.97em]</span><br/>
  <span style="font-size:97%">这个测试用来比较不同字体单位下的表示情况。[97%]</span><br/>
</body>

Firefox
Firefox下的结果

Internet Explorer
IE下的结果

看了这些结果,我真的不明白了:到底font-size单位中的百分比和em是否相同?

此外,有一点让我安心的就是,如果使用em作为单位的话,就没有上一篇文章中提到的,IE和Firefox中大小不一致的现象了。

Difference between IE and Firefox when using percent as font-size unit

font-size IE6/IE7 Firefox
100% 16px 16px
99%
98%
97% 15px
96% 15px
95%
94%
93%
92%
91%
90% 14px 14px
89%
88%
87%
86%
85%
84% 13px 13px
83%
82%
81%
80%
79%
78% 12px 12px
77%
76%
75%
74%
73%
72% 11px
71% 11px
70%
69%
68%
67%
66%

今天在测试IE7和firefox下blog的字体显示时,发现在某些特定的font-size下,IE和firefox所显示的字体大小并不相同。

这里要提前说明的是,我所遇到的问题是在如下环境下发生的:

  • 所测试的字体为宋体 (Simsun)以及目前最时髦的微软雅黑 (Yahei)。测试结果表明,这个问题无论是在宋体还是在微软雅黑都会发生
  • IE版本为IE6或IE7,firefox为2.0.0.3
  • CSS当然是使用em体系,也即百分比单位、或者em单位的font-size (这年头应该很少有人用px来定义文档字体了吧?)
  • Firefox的默认字体大小设置为16px,IE无法设定默认字体(中等大小时)大小(如果哪位知道如何更改默认字体大小,还请告诉我),据有关文档称也是16px。

为了找出问题的根本原因,我写了一段php代码用来输出font-size等于100% (也即1em) 到 50% (0.5em)之间的每隔1% (0.01em)的文字显示结果。该程序的运行结果如下图所示。

Font size test for em/% unit

上图是在Firefox下的测试结果。然后,根据IE和Firefox下的结果,得出了右面的这张表格。

通过这张表格即可看出,虽然在大多数的的情况下IE和Firefox的结果是相同的,但是当设置font-size=97%的时候,IE会显示16px的字体,而Firefox则显示15px的字体。同样font-size=72%时,IE显示12px字体,Firefox则显示11px字体。

实际用计算器计算一下就知道,当font-size为97%时,16px X 97% = 15.52px。也就是说,在IE中15.52px被解释为16px字体,而在Firefox当中15.52px被解释为15px字体 (不知道是不是Firefox的计算方法有问题?或者是W3C规定这样做的?还是我计算错误?)。

补充一条最新发现:刚才试着用CSSViewer这个插件检查了一下font-size=97%时的像素值,发现结果并不是我想象的15.52px,而是15.4667px——不知道Firefox中究竟是按照什么标准计算百分比单位的font-size的呢?

IE和Firefox究竟谁对谁错,实在是难以说清,不过有一点是肯定的——就是在设计CSS的时候,要特别注意这些IE和Firefox之间不同的地方。

XBAP and Silverlight : Product Lining in .NET 3.0/WPF

今天在@IT网站看到一篇关于Expression Blend的文章,这篇文章开头部分对于.NET 3.0/WPF时代微软产品结构、地位作了简单介绍,觉得概括的很精妙,这里就加以借题发挥,做一个WPF入门。

什么是WPF?

要是到微软的网站查找答案,肯定能得到一大堆华丽却让人糊涂的辞藻。WPF (Windows Presentation Foundation)是.NET 3.0的一部分,是一种新的用户界面技术,用术语说是一种新的“图形子系统 (Graphic Subsystem)”。换句话说,在.NET 2.0当中我们可以开发两种应用程序:

  • Windows Forms Application
  • ASP.NET Web Application

那么在.NET 3.0当中,可以开发的应用程序又多了一种:

  • WPF Application

而WPF Application则又可以继续分成下面两种:

  • Windows下的应用程序:何传统的桌面应用程序、Windows Forms应用程序一样,只不过用户界面部分采用了WPF技术 (XAML等)编写的应用程序。这种程序由于可以使用WPF的全部功能,因此可以将WPF丰富绚丽的用户界面表现能力发挥到极致。
  • XAML Browser Application (XBAP),其特点是:
    • 在Internet Explorer中运行
    • 无需安装
    • 像ClickOnce一样,在部分信赖的域中执行

那么最近沸沸扬扬的Silverlight又是什么呢?Silverlight的前身是很早以前就万人期待的WPF/E,其中E的意义是Everywhere。众所周知,要想使用WPF,就必须安装.NET 3.0;而.NET 3.0虽然宣称跨平台,但目前还仅限于Windows XP以上版本。因此,WPF/E最初的目标,就是把WPF/XAML的丰富的用户界面表现能力带到各个平台。

带着这个目标,WPF/E经历了数十个月的秘密开发之后,作为Silverlight诞生了——其诞生之日起,就被人们命名为Flash杀手——因为它和Adobe的Flash太相似了。很多.NET程序员都乐了——以后可以用C#开发Flash了!从这一点,想必Silverlight的特点已经很清晰了:客户端是浏览器,在下载一个插件之后即可享受Silverlight带来的激动人心的用户体验了。

最后,用一张图表来总结各种产品的关系、地位:

.NET 3.0 Products/Technologies

PHP Safe Mode and Wordpress Upload Problem

今天,正当我在Wordpress(版本:2.0.10)中开始一个新帖子,打算上传一张图片的时候,在点击了Upload按钮之后,出现的却是下面的这条消息:

The uploaded file could not be moved to .

奇怪,明明上传目录的权限都没问题,为什么会无法上传呢?查找了一下上面消息的来源,发现wp-admin/admin-functions.php的大概位于1776行处有如下代码:

// Move the file to the uploads dir
$new_file = $uploads[‘path’] . "/$filename";
if ( false === @ move_uploaded_file($file[‘tmp_name’], $new_file) )
    die(printf(__(‘The uploaded file could not be moved to %s.’), $file[‘path’]));

可见,问题的原因是move_uploaded_file函数无法正常将文件拷贝所致。在允许显示错误信息后,可看到类似下面的错误信息:

Warning: move_uploaded_file(): SAFE MODE Restriction in effect. The script whose uid is 1525 is not allowed to access /somefolder/wp-content/uploads/2007/05 owned by uid 48 in /somefolder/wp-admin/admin-functions.php on line 1778

可见,这是因为SAFE MODE功能阻止了uid为1525的脚本访问uid为48的用户所拥有的上传目录(uploads/2007/05)。那么,什么是SAFE MODE呢?

SAFE MODE(安全模式)是为了解决共用服务器(例如一台服务器划分成多台虚拟主机的服务商)中的安全问题而被添加到PHP中的一种功能。该功能通过一些强制性的限制而提高安全性,但是反之这些限制使得程序受到约束,甚至产生问题。

SAFE MODE中很重要的一个功能就是,在PHP脚本进行文件系统操作时,对uid/gid进行比较、核查。当一个用户的脚本试图访问另一个用户的目录时,系统就会拒绝访问并给出上面那样的错误信息。

那么,为什么目录“uploads/2007/05”会是“另一个”用户的目录呢?这“另一个”用户,到底是谁呢?

继续调查,找到了创建这个目录的代码。该代码位于/wp-includes/functions-post.php的约867行开始的wp_mkdir_p()函数中:

if (@ mkdir($target)) {
        $stat = @ stat(dirname($target));
        $dir_perms = $stat[‘mode’] & 0007777// Get the permission bits.
        @ chmod($target, $dir_perms);
        return true;
} else {
        if ( is_dir(dirname($target)) )
                return false;   
}

问题就出在mkdir函数上面。在SAFE MODE模式下,PHP的mkdir()函数所创建的目录,owner并不是script,而是Apache。因此,上面消息中“uploads/2007/05”的owner ID 48,就是Apache进程的uid了。

由此可见,在不改变服务器端设置的前提下(跟服务商打交道比较麻烦,而且让他们更改SAFE MODE不大现实),要想解决这个问题,只能想办法不使用mkdir()了。幸好Wordpress里面,需要创建目录的也就是上传的时候,因此作为workaround,手动用ftp事先创建好每个月的上传目录就是了。因为ftp所使用的帐号是和自己的www服务挂钩的,因此不存在uid不同的问题。

那么,对于必须使用mkdir的程序,有什么办法呢?据说,PHP6里面就会取消SAFE MODE——不过,等PHP6大功告成,再等保守的服务提供商们更新换代,估计都到了Web 4.0时代了。目前的解决方案来说,既不改变服务器设置、也不对应用程序本身进行大规模的重新设计的workaround,就是以ftp_mkdir()替换mkdir()。原理就是因为ftp是以和script相同的用户操作文件系统的。当然,这个替换并不是简单的把mkdir换成ftp_mkdir,而是需要ftp服务器的IP地址,用户名和密码,很有局限性,使用前需要仔细权衡。

Character Set Problem in PHP + MySQL4.1+

和许多人一样,我也是在转移blog时才发现这个问题。虽然是一个很老的问题了,为避免沉痛教训,这里就把相关知识做一总结,以方便后人。

【现象】

本来我的blog是放在家里的服务器的,最近因为要迁到租用的主机上,就开始了搬家工作。首先是文件的拷贝,一切顺利;接下来是把数据库从家里的MySQL中导出,然后导入到主机提供商的MySQL上去。由于两边虽然MySQL版本不同(家里是5.x,租用的主机那边是4.1x),但是由于都安装有PMA(PHPMyAdmin),应该没什么问题。

这么想着,等我在家里的PMA里执行“导出”之后,就犯嘀咕了——怎么打开生成的sql文件一看,wp_posts的贴子内容都是乱码啊?通过查看了一下数据库、各个表,发现collation一栏里面写的都是utf8_general_ci……虽然不大懂collation,但是应该字符编码都是utf-8,怎么会是乱码呢?不管三七二十一,先导入到租用的主机上再说!——结果,导入到租用主机上,仍然是乱码。

【预备知识】

为了解决问题,有必要学习和复习一下相关的基础知识。

首先是MySQL里面关于character set(字符集)和collation(整理?我认为翻译成比较规则可能更贴切)的概念

Character set顾名思义,就是字符、以及字符对应的编码的集合。例如简体中文字符集gb2312就包括简体中文中的所有规定汉字,以及每个汉字对应的代码。

Collation,是指比较字符的规则的集合。有了比较规则,才能够将一组数据排序——例如按照英文字母顺序排序、汉字按照拼音顺序排序等等。显然,针对同样一组字符集可以有不同的排序标准、规则。例如汉字可以按照拼音排序,也可以按照笔画多少排序。尤其是Unicode的字符集,由于其可以包含不同种类的语言,所以可以按照各种语言的排序方法排序。此外,完全按照字符在字符集里的编码进行比较的方式称为binary比较。

到了这里我们就容易理解了。举例来说,MySQL支持的gb2312字符集中,有gb2312_bin和gb2312_general_ci两种collation。很显然前者是binary比较规则,后者是一般的中文字符比较规则。

每种字符集都有其默认的collation。对于utf8字符集来说,其默认collation是utf8_general_ci。要获得MySQL里面支持的字符集和默认collation列表,可以使用SHOW CHARACTER SET语句:

mysql> SHOW CHARACTER SET;
+----------+-----------------------------+---------------------+
| Charset  | Description                 | Default collation   |
+----------+-----------------------------+---------------------+
| big5     | Big5 Traditional Chinese    | big5_chinese_ci     |
| dec8     | DEC West European           | dec8_swedish_ci     |
| cp850    | DOS West European           | cp850_general_ci    |
...

其次,是MySQL中,在哪些地方需要这些字符集和collation。总体上分,在MySQL的体系中有三处字符集和collation:服务器(数据),连接,客户端。乍一看体系清楚明了,其实并不是这样。下面就一一介绍。

[1] 服务器(数据)端的字符集和collation,可以分成四级逐层指定——server, database, table, column。当MySQL存取位于某一列(column)的数据时,如果column的字符集和collation没有指定,就会向上追溯table的;如果table也没有指定字符集和collation,就以database的字符集和collation作为默认值;如果database仍旧没有指定,那么就以服务器的字符集和collation作为默认值。

那么server的字符集和collation的默认值又是从哪里来的呢?答案是,配置文件(my.ini)和mysqld(或者mysqld-nt)的命令行参数中都可以指定。如果不幸的,你根本没有在my.ini或者命令行中指定,那么MySQL就会使用编译MySQL时指定的默认字符集——latin1。

但是,需要注意的是,如果安装MySQL时选择了多语言支持(一般用中文的都会选择吧),安装程序会自动在配置文件中设置default-character-set=utf8

这样,所有创建的数据库、表,除非明确指出使用其它字符集,都会默认的使用utf作为数据的字符集(同时使用utf8_general_ci作为默认collation,因为它是utf8的默认collation)。

相关系统变量

character_set_server:服务器的字符集
collation_server:服务器的collation
character_set_database:数据库字符集
collation_database:数据库的collation

 

[2] 客户端。对于客户端传送来的literal string(例如INSERT,UPDATE语句当中的值),MySQL需要知道它们是什么编码。同时,MySQL返回给客户端的值(例如SELECT语句的返回值),也可以按照指定的编码返回。

相关系统变量

character_set_client:客户端发送过来文字的字符集
character_set_results:发送给客户端的结果所使用的字符集

 

[3] 连接。用于连接的字符集和collation,是指MySQL在接受到客户端发送来的文本之后,转换成何种字符集,用什么规则进行比较。需要注意的是,如果是将文本和数据库中某个column的值比较,将优先使用该column的字符集和collation。

相关系统变量

character_set_connection:用于连接的字符集
collation_connection:用于连接的collation

 

【问题的分析】

有了上面的预备知识,我们就开始分析最初的问题:本来是应该作为UTF-8字符保存的数据,为什么到了数据库中就变成了“乱码”?而且这些乱码居然还能毫无问题地被wordpress显示?为什么一旦导入到租用的主机那里就不能正常显示了呢?

首先让我们来看一下,我家里的服务器上,MySQL的系统变量(System Variables)是如何设置的。

注意:因为一些系统变量是根据客户端不同而不同的,所以用mysql命令行登陆所看到的和PHP下看到的并不相同。此外,似乎也不能用PMA查看——似乎在PMA中也已经更改了默认的系统变量。因此,要查看PHP作为客户端时的默认系统变量,可以编写一个类似下面的PHP小程序:

mysql_connect(localhost, $user, $pass);
$query="SHOW VARIABLES";
$result=mysql_query($query);

其中$result就包含着所有系统变量。在我家里的服务器上得到了如下结果(以下只列出跟字符集有关的系统变量):

character_set_client  latin1
character_set_connection  latin1
character_set_database  utf8
character_set_filesystem  binary
character_set_results  latin1
character_set_server  utf8
character_set_system  utf8
collation_connection  latin1_swedish_ci
collation_database  utf8_general_ci
collation_server  utf8_general_ci

可见,默认的客户端编码、默认的连接编码是latin1——这也就是说,虽然实际上wordpress传递给MySQL的文本都是用UTF-8编码的,但是由于上述系统变量设置不当,这些UTF-8编码的文本被MySQL当作是latin1编码的,并且由于数据库本身是utf8,因此把这些“latin1文本”又转换成了utf8。这样,一个汉字居然需要6bytes(一个汉字作为UTF-8是3bytes,被当作latin1进行了转换,每个latin1字符转换成2bytes的UTF-8编码)。这就不难理解为什么数据库存储的是“乱码”了。

那么为什么这些“乱码”在wordpress显示时没问题呢?这是因为,character_set_result也是latin1,也就是说MySQL在取出数据交给wordpress时,把这些数据从utf8转换回latin1,然后wordpress将这些latin1又当作了utf8——正好是上面的逆过程。

那么,为什么到另一台服务器上面就无法正常显示了呢?请看看那台租用主机的系统变量设置:

character_set_client  ujis
character_set_connection  ujis
character_set_database  ujis
character_set_results  ujis
character_set_server  ujis
character_set_system  utf8
collation_connection  ujis_japanese_ci
collation_database  ujis_japanese_ci
collation_server  ujis_japanese_ci

可见,其默认的客户端编码是ujis。也就是说,MySQL把utf8数据取出后,将会转换成ujis并传递给wordpress。这经历了latin1 – utf8 – ujis转换的原本是utf8的字符,早已面目全非了……

【解决方案】

解决方案在很多论坛、网页上已经有提到了,在wordpress的trac也已经有人提出过。

但是在解决问题之前,我却很想知道一个问题的答案,那就是:这到底是MySQL的问题,还是PHP(特别是php_mysql extension)的问题,还是wordpress的问题?甚至是用户配置的问题?我倾向于认为这是一个wordpress的问题。因为无论MySQL还是PHP都不知道wordpress使用了什么字符编码,所以无法更改客户端字符集;而作为一般的wordpress用户,要求他们设置字符编码——可以,但是必须要提供一个用户界面,而不是直接修改源程序。

那么解决方案(或者说只是一个workaround)就是,修改wordpress的\wp-uncludes\wp-db.php。在第40多行的function wpdb中,在$this->select($dbname);之前添加一句

$this->query("SET NAMES latin1");

SET NAMES语句的功能就是,执行了SET NAMES ‘x’相当于下面三条语句的功能。

SET character_set_client = x;
SET character_set_results = x;
SET character_set_connection = x;

这样,在默认客户端字符集是ujis的租用主机上,导入的wordpress文章也能正常显示了。当然,这不是彻底的解决方案——这只是“将错就错”,反正数据库里面存储的已经是被当作latin1而转换成utf8的utf8了,那么就将其转换回所谓的latin1就是了。这样做将使其他程序无法读取wordpress的数据,并且更重要的是,数据库中存储的“utf8数据”无法真正按照utf8应有的排序规则来排序。

那么最彻底的做法,就是在安装wordpress时就添加上面所说的SET NAMES语句,并且设置客户端的字符集为utf8:

$this->query("SET NAMES utf8");

但是这样做的话,已经被当作latin1写到数据库里面的文章就会无法正常显示了。要让他们正常显示,必须经过utf8 – latin1的转换。如果数量较多,可以考虑编写一个程序进行转换;数量较少的话……手动转换吧。

BTW,国内高手们汉化的中文版的wordpress中已经添加好这一句了,上面的信息只适用于那些使用英文wordpress的朋友,以及喜欢追根问底的朋友。

最后推荐一篇参考文章:Portable php-mysql connection charset fix

Write Outlook add-ins with C#

针对Microsoft Office系列的编程一直是比较麻烦的领域;这主要是因为Office里面大量的COM对象构成的庞大的体系结构让人望而生畏。

编写Outlook插件也是如此。虽然.Net的到来使得深入系统底层的RAD开发成为了可能,但是由于Outlook仍然是COM接口,要写Outlook插件,从根本上来说仍旧是编写Outlook COM插件的过程。凭借.Net和COM的互操作能力,才使得C#/VB.Net等语言编写的插件能够被Outlook等Office程序调用。

首先,无论是Outlook也好,Word也好,所有的Office应用程序都使用了共同的COM接口_IDTExtensibility2来与插件进行通信。因此,要开发一个Outlook插件(当然其他Office插件也一样),必须实现这个接口。这个接口定义在AddInDesigner Object Library当中,位于<drive>\Program Files\Common Files\DESIGNER\MSADDNDR.DLL文件中。在Visual Studio.Net当中,对这个COM接口进行了包装,包装后的Assembly名为Extensibility,该接口名为IDTExtensibility2。

下面就通过一个演示项目来说明一下。

在Outlook当中有一个BUG/设计缺陷,就是当回复、转发一封邮件的时候,这封回复/转发邮件的文字编码会自动变得和收到的邮件相同。例如,我经常从美国收到编码为US-ASCII的邮件,当我用中文添加一些评注,转发给中国人的朋友时,如果我忘记了手动把编码改成UTF-8或GB2312,邮件内容就会变成乱码。即便我在“新邮件选项”里面把新邮件的默认编码设置为UTF-8也没有用。

这种时候,如果有一个Outlook插件能够自动将所发送邮件的编码改成UTF-8有多好啊。于是,就有了下面的制作过程。

Step 1 创建Outlook插件项目

首先,在Visual Studio 2005(其他版本也可。这里以2005为例说明)中,新建一个Project,类型为Other Project Types > Extensibility > Shared Add-in,并在Application Host画面选择要使用这个Add-in的Office程序。这样一个Add-in Project就生成了。在这个Project当中,我们可以看到如下两个平时不常见的Reference被添加到了项目当中:

  • Extensibility:这就是所有Office插件都要实现的接口所在的Assembly
  • Microsoft.Office.Core:Office插件的共通组件库

Step 2 实现IDTExtensibility2接口

打开Connect.cs文件,我们就可以看到VS已经帮我们创建好了一个叫做Connect的类,来实现IDTExtensibility2接口。这个接口和一般的Host/Add-in体系结构类似,内容如下:

  • void OnConnection(object Application, ext_ConnectMode ConnectMode, object AddInInst, ref Array custom);
    当COM add-in被连接到host应用程序时OnConnection事件将被触发。

    • Application – 指向加载这个add-in的host应用程序,如Outlook。
    • ConnectMode – 指定了add-in的加载模式。
    • AddInInst – 指向一个代表当前add-in的COMAddIn对象(定义在Microsoft.Office.Core当中)。
    • Custom – 用户自定义参数。
  • void OnDisconnection(ext_DisconnectMode RemoveMode, ref Array custom);
    与上一个相反,这个是把add-in从host程序中卸载时引发的事件。RemoveMode指定了卸载模式。
  • void OnAddInsUpdate(ref Array custom);
    当COM add-in被安装或移除的时候引发该事件。
  • void OnStartupComplete(ref Array custom);
  • void OnBeginShutdown(ref Array custom);
    上面两个事件在host程序完全结束加载、以及即将开始关闭的时候引发。

简单实现上面几个接口的例子可以参考微软知识库的302901号文章

Step 3 截获ItemSend事件 更改MailItem属性

实现了上面的接口,我们就要在其中添加代码了。由于我们的目的是在发送邮件时强制改变邮件的编码(encoding),因此只需要在OnConnection当中添加截获发送邮件的事件即可:

public void OnConnection(object application, Extensibility.ext_ConnectMode connectMode, object addInInst, ref System.Array custom)
{
    outlookApplication = application as Outlook.Application;
    addInInstance = addInInst as COMAddIn;
    if (outlookApplication == null || addInInstance == null)
    { return; }
    outlookApplication.ItemSend += new Outlook.ApplicationEvents_11_ItemSendEventHandler(outlookApplication_ItemSend);
    if (connectMode != Extensibility.ext_ConnectMode.ext_cm_Startup)
    {
        OnStartupComplete(ref custom);
    }
}

之后,我们就可以在事件函数中控制邮件的编码了:

void outlookApplication_ItemSend(object Item, ref bool Cancel)
{
    Outlook.MailItem mitem = (Outlook.MailItem)Item;
    mitem.InternetCodepage = 65001; //Code page for UTF-8
}

在完成上面的代码之后,我们只需要打开Outlook,就可以测试这个插件的效果了。要确认该插件是否安装,可以在Outlook的菜单上选择Tools > Options > Other,然后按Advances Options按钮,COM Add-ins即可看到插件的列表,以及启用和禁用插件。

How to get the path of temporary folder in .Net

有时候在编程当中我们需要获取临时目录的路径。临时目录在DOS/Win9x时代一直是C:\Temp之类形式,以环境变量来指定;到了多用户的WinXP,虽然默认路径变成了C:\Documents and Settings\Username\Local Settings\Temp,但是仍然可以通过TMP或者TEMP环境变量来指定。因此我们首先可以通过直接读取环境变量的方式找到临时目录:

Environment.GetEnvironmentVariable("TMP");

或者,

Environment.GetEnvironmentVariable("TEMP");

这时就有一个问题随之而来,万一TMP和TEMP两个环境变量的值不同时,应该以哪个为准呢?此外,万一两个变量都不存在时,应该以哪个目录为临时目录呢? 看来,这个方法不够“标准”。

实际上,Win32中关于文件系统的API里,已经为我们提供了这样一个API:

DWORD GetTempPath(
  DWORD nBufferLength,
  LPTSTR lpBuffer
);

GetTempPath函数会按照如下的顺序确定临时目录:

1. The path specified by the TMP environment variable.
2. The path specified by the TEMP environment variable.
3. The path specified by the USERPROFILE environment variable.
4. The Windows directory.

而在.Net里面,对于这个API已经有了封装,这就是Path.GetTempPath()方法。因此在.Net当中获取临时目录时,只需用如下语句:

string tempFolder = Path.GetTempPath();

 

给.Net程序员的PInvoke Tips [2]: Are Strings Immutable?

早在Java到来之际,程序员们都已逐渐接受并乐于接受String的这一特性:immutable。从C/C++转到.Net/C#的程序员们,在最初可能非常不适应把char[]和string分开对待,但是一旦习惯了就会觉得非常方便,尤其是直接以+进行连接,以及支持switch…case等。

这个时候问题来了,string真的是immutable的吗?

cbrumme的blog上给出了一个例子:

using System;
using System.Runtime.InteropServices;
 
public class Class1
{
    static void Main(string[] args)
    {
        String computerName = "strings are always immutable";
        String otherString = "strings are always immutable";
 
        int len = computerName.Length;
        GetComputerName(computerName, ref len);
 
        Console.WriteLine(otherString);
    }
 
    [DllImport("kernel32", CharSet=CharSet.Unicode)]
    static extern bool GetComputerName(
        [MarshalAs (UnmanagedType.LPWStr)] string name,
        ref int len);
}

该程序的执行结果也许正在你的预料之中,输出的是类似MYCOMPUTERNAMElways immutable之类的字符串,也就是说原字符串的前面一部分被计算机名覆盖掉了。

对上面的程序,我们可以做出如下分析:
1,computerName和otherString 的文本相同,因此由于编译器的Interning的结果,二者其实指向同一个字符串,用Object.ReferenceEquals()可以验证其相等。
2,红色部分标出的Marshal指令,使得该string被marshal为一个unmanaged pointer(LPWSTR)传递给了GetComputerName函数;
3,GetComputerName函数直接改写了computerName指向的缓冲区,string的immutable特性即被破坏。

由此我们可以看到,在与Unamanaged代码进行交互操作时必须额外小心,因为从某种意义上来说Unmanaged代码权限更大,破坏力也就更大,也就更容易引起意想不到的问题。

那么,上面那段使用GetComputerName的代码中,对该函数的包装要如何改进呢?

首先,在使用一个API之前应该注意其各个参数的in, out性质,例如关于GetComputerName,MSDN上有如下一段:

BOOL GetComputerName( LPTSTR lpBuffer, LPDWORD lpnSize);
lpBuffer : [out] Pointer to a buffer that receives a null-terminated string containing the computer name or the cluster virtual server name. The buffer size should be large enough to contain MAX_COMPUTERNAME_LENGTH + 1 characters.

很显然,lpBuffer应该是用来输出的缓冲区,因此不应该用string,而是用byte[],StringBuilder之类的类型与之对应;即便一定要用String,也绝对不能Marshal为LPWSTR/LPTSTR,而是Marshal为VBByRefStr,以确保Managed代码侧string的immutable性质。

此外,使用.Net中unsafe代码也可以打破String的immutable,这和使用Unmanaged代码的本质是相同的。