sae 应用仓库指南

转载:http://blog.sina.com.cn/s/blog_73b89cd30101230u.html
一,为什么要移植应用
 
 
 
    SAE禁止IO写操作,代码目录不能写入。这意味着普通程序的上传图片、生成缓存等操作都不能在SAE上正常运行,这时候你需要对这些代码进行修改后才能让你的程序运行在SAE上。
 
 
 
SAE为什么要禁止IO写操作?
 
 
 
    SAE采用分布式架构设计, 应用代码将部署在多台前端服务器上, 每次访问请求可能到达不同服务器。(如下图):
 
 
    假设现在有A、B、C、D四台服务器。用户上传一张图片到A服务器,第二次访问请求可能到达B服务器,此时将无法获取保存在A服务器上的图片。
 
 
 
    SAE使用MemcacheX、Storage等存储型服务代替传统IO操作,效率比传统IO读写操作高,有效解决因IO瓶颈导致程序性能低下的问题。
 
 
 
    另外,很多网站被攻击都是因为服务器有写的权限,程序代码能被黑客修改,SAE禁止写操作,也提升了服务器的安全性。
 
    所以SAE为了提升性能和安全,禁止本地IO写操作。开发者可以使用StorageMemcacheKVDB等服务存储需写入的数据。
 
 
二,快速移植你的程序
 
 
使用Wrappers
 
    SAE虽然禁止了IO写操作,但是并没有禁止fwrite,file_put_contents等写操作函数,同时SAE还提供了Wrappers服务,这使得移植程序也比较简单。 假设我们要将下面的代码移植到SAE上。
 
  file_put_contents(‘test.php’,”); 
  include ‘test.php’;
?>
    只需要给文件地址加上前缀就可以了
 
  file_put_contents(‘saemc://test.php’,”); 
  include ‘saemc://test.php’;
?>
 
    如果地址的前缀为saemc://  表示在Memcache中进行读写操作 ,如果前缀为saestor:// 表示在Storage中进行读写操作。前缀为saekv://就是对KVDB的操作。这就是Wrappers的功能。
 
注意:
 
    1,在使用Wappers前,需要先初始化相应的服务。
 
    初始化Memcache:进入Memcache服务管理后台 http://sae.sina.com.cn/?m=mcmng ,然后在网页右上角“我的应用中”选择指定应用。点击“点此初始化MC”的按钮就可初始化Memcache
 
    初始化Storage:进入Storage服务管理后台http://sae.sina.com.cn/?m=storage ,选择指定应用后 ,“点击创建一个domain”按钮进行创建。这里允许创建多个domain, 开发者使用Wrappers将文件上传到哪个domain,取决于文件地址的第一个目录名称。 如 “saestor://upload/image/1.jpg” 这样的地址,会将文件保存在名为upload的domain中。
 
    另外MysqlKVDB也是需要先初始化才能使用,如果程序有用到这些服务,请先初始化。
 
    2,操作频繁的缓存不适合存储在Storage中。
 
    Storage读写效率比KVDBMemcache低很多。操作频繁的缓存不适合存储在storage中,所以除操作用户上传的文件外,尽量不要使用saestor://。
 
    尤其是一些程序代码片段的缓存。如上面的例子,如果使用如 include ‘saestor://xxx’ 的代码来实现会使得程序性能较低。
    
    理解了Wrappers的用法后, 就可以开始移植程序了。
 
利用报错快速定位
 
    开发者可以利用SAE的错误提示,快速的定位到程序需要修改的代码。
 
    首先保证你的应用开启了错误提示。进入应用的“代码管理”后台http://sae.sina.com.cn/?m=vermng, 看看对应版本的“错误提示”是否开启(默认是开启的)。 还有检查一下程序有没有类似error_reporting(0)的代码,屏蔽了错误提示。如果有,请先将他们注释掉。
 
    然后将程序上传到SAE。如果有不兼容的地方,错误提示会告诉你具体文件的具体行。
 
    如果出现类似这样的提示:
 
SAE_Warning: file_put_contents(./xxx.php) [function.file-put-contents]: failed to open stream: Permission denied in xxx.php on line 2
 
    说明程序正在进行IO写操作, 只需给地址加上Wrappers的前缀,就能实现兼容。 
 
    如果你的程序的封装性好,更改IO写操作的代码, 往往很简单。比如ThinkPHP定义了RUNTIME_PATH常量。所有IO写操作产生的文件都会存放在 RUNTIME_PATH常量指定的文件夹中, 移植ThinkPHP程序, 只需要更改RUNTIME_PATH常量,加上saemc:// 前缀就可以了。其实其他框架或封装性比较好的程序,也是如此,只需更改一行代码,就能兼容IO写操作。如果你是重新用ThinkPHP开发程序的话,建议 使用SAE版ThinkPHP。
   
    注:使用Wrappers后,有些操作是无效的。 比如file_exists ,大家可以直接将这样的函数注释掉。
 
 
    如果出现类似下面的提示:
 
SAE_Warning: mysql_connect() [function.mysql-connect]: this app is not authorised in xxx.php on line 2
 
    说明程序正在使用Mysql数据库,这时候需要初始化Mysql,并导入数据库。你可以在初始化后点击“管理Mysql”按钮,使用PHPMyAdmin进行数据导入。 当你数据比较大时,可以用DeferredJob服务来导入。
 
    导入数据库后,还要将原程序中数据库用户名、密码等更改为SAE的。 SAE的数据库用户名、密码、数据库名使用常量表示。可以在mysql服务管理处看到。SAE的Mysql数据库支持分布式,能连接两个数据库。如果你的原程序只支持单数据库操作,请连接主库域名。
 
     当你的程序在SAE上没有出现错误提示了,你的程序已经初步能在SAE上面运行了。你还要检查一下一些细节的功能是否实现。比如上传图片功能等。建议将上传的图片或其他附件存放在Storage中。图片存储在Storage中后, 图片的访问地址也和以前不一样。你需要使用Storage的getUrl方法获得图片访问地址,如:
   
$s=new SaeStorage();
$url=$s->getUrl(‘domain’,'filepath’);//获得图片地址,filepath为图片在storage中的路径
?>
 
     接下来,开发者可以对程序进行进一步的优化,让程序达到更好的性能。
 
三,性能优化
 
    我们提供了一些优化的建议。
   
IO操作产生的文件进行区分存储
 
      (1)建议将模板编译缓存或其他程序代码片段的缓存放在Memcache中,坚决反对将模板编译缓存放在Storage中, 因为Storage的读写效率比Memcache低,将它们存入Storage会导致你程序运行较慢。
 
      (2)将上传的图片或附件存储在Storage中。如果你的图片需要防盗链等功能, 你还可以在SAE的服务管理后台,通过设置domain的属性能轻松的实现防盗链。
 
      (3)将一些固定缓存存入KVDB中。有一些缓存不适合存储在Memcache中, 因为Memcache中的缓存有可能会消失,比如Memcache空间不足时,最早存入Memcache中的缓存就会被删除。所以你存入到Memcache的缓存,在读取时必须需要判断缓存是否存在,如果不存在重新生成。但有一些缓存生成一次后一般都不会变了,在读取缓存的时候不用判断它是否存在。 这类缓存建议使用KVDB进行存储。
 
 实现数据库的读写分离
 
    SAE MySQL数据库主库可读写,从库只读。查询使用从库消耗的云豆更少,且响应更快,所以建议大家尽量实现主从分离。现在很多框架,程序都支持读写分离,只需简单配置即可。如果程序只支持单数据库 可以尝试在执行SQL时进行判断如果是Select则连从库。
 
多使用SAE提供的服务
 
    SAE提供了很多高效的服务,建议程序能使用SAE服务的地方尽量使用SAE服务。比如程序有排行榜的功能, 可以使用Rank服务实现;验证码的功能, 可以使用SaeVCode服务;发送邮件的功能, 可以使用Mail服务等等。请大家查看各个服务相关的文档进行学习。
 
考虑使用原生接口
 
    Wrappers固然方便简单, 但它的效率肯定不如原生的MemcacheStorage接口。如果你很在乎效率问题, 可以考虑使用原始接口实现移植。 使用原生接口实现移植的方法可参考:http://qing.weibo.com/1631767865/6142cd3933000cj9.html
 
四,常见问题
 
实现伪静态
 
    SAE不支持.htaccess文件,但是可以通过AppConfig服务实现伪静态。
 
生成静态页面
 
    有些程序有生成纯静态html的机制。大家可以使用KVDB存储html静态页面数据,由于SAE禁止IO写操作,不能实现真正的纯静态,我们可以用伪静态的方式到达同样的效果,下面举一个简单的例子。
    假设程序在未移植之前通过以下代码生成html文件:
 
file_put_contents(‘html/index.html’, ‘htmlcontent’);
?>
 
    这样用户通过浏览器输入地址 http://你的域名/html/index.html  访问到的是一个纯静态页面。
    现在我们要将程序移植到SAE上。
    首先将html数据保存到KVDB上。
 
file_put_contents(‘saekv://html/index.html’, ‘htmlcontent’);
?>
 
    使用config.yaml, 写一条伪静态语句
 
- rewrite: if (path ~ “/html/(.*)”) goto “/readhtml.php?path=html/$1″
 
    如果用户访问了html目录会伪静态到readhtml.php文件进行读取静态数据。
    readhtml.php的代码为:
 
echo file_get_contents(‘saekv://’.$_GET['path']);
?>
 
     这样,在SAE上用户也能通过浏览器输入地址 http://你的域名/html/index.html 访问到数据。
 
日志记录。
有些程序有日志记录功能, 如果日志读写太频繁,不适合将日志文件存入storage中,建议使用sae_debug实现日志记录。 但是sae_debug记录日志的同时也会向浏览器输出日志内容。很多程序希望能现实暗地记录日志, 大家可以通过封装函数实现。
如:
function sae_log($msg){
    sae_set_display_errors(false);//关闭信息输出
    sae_debug($msg);//记录日志
sae_set_display_errors(true);//记录日志后再打开信息输出,否则会阻止正常的错误信息的显示
}
?>
 
通过sae_debug函数记录的日志可以在应用管理后台中的“日志中心”查看,它属于debug类型的日志, 大家需要在搜索框中下拉菜单中选择debug类型进行查看。
 
缓存共享问题
 
     SAE每一个应用能创建多个版本,但是这多个版本共用同一个MemcacheKVDB等 服务,有时候容易出现缓存共享问题。比如一个应用创建了两个版本,放有相同的程序, 程序有模版缓存机制,可能会出现,只修改了版本1的模板,却发现版本2的内容也被修改了。我们要避免不同版本之间缓存名称相同的现象。开发者可以使 用$_SERVER['HTTP_APPVERSION']变量给缓存名称加上应用的版本号
 
Memcache缓存需要有更新机制
 
    Memcache缓存因为有消失的可能, 所以在读取Memcache缓存时应该要判断缓存是否存在,如果不存在重新生成缓存,否则的Memcache缓存一旦被删除而程序又没有更新缓存的机制,将可能导致网站不能正常访问。开发者可以在程序移植完成后在应用的Memcahe服务管理后台手动清空Memcache缓存,再测试一下网站是否能正常运行。 建议将不需要更新机制的缓存使用KVDB存储。
 
6 如何获得Storage的域名
 
    有时候程序的图片访问路径可以定义前缀, 我们只需要将前缀替换为Storage的域名就可以了。 获得Storage域名的方法:
 
$s=new SaeStorage();
$domain=rtrim($s->getUrl(‘domain’,”),’/');//注意getUrl的第二个参数为空
?>
 
7 有些地方不能用Wrappers
    并不是所有文件地址前加上Wrappers相应的前缀都能实现兼容。 比如以下代码:
 
$img=imagecreatefrompng(“http://sae.sina.com.cn/static/image/logo.beta.new.png”);
//…经过了一些列的图片处理函数处理…
imagepng($img,’saestor://upload/logo.png’);//想通过Wrappers保存处理后的图片
?>
 
    上面的代码是不能将图片保存到storage中的。 
 
    开发者可以使用临时文件解决上面的问题。
 
$img=imagecreatefrompng(“http://sae.sina.com.cn/static/image/logo.beta.new.png”);
//…经过了一些列的图片处理函数处理…
imagepng($img,SAE_TMP_PATH.’logo.png’);//保存为临时文件
file_put_contents(‘saestor://upload/logo.png’,  file_get_contents(SAE_TMP_PATH.’logo.png’));
?>
 
    所以要注意,虽然大家把程序中文件地址加上了wrappers的前缀,也要检查一下 文件地址是否在用在了不支持Wrappers的函数上。
 
    一般的文件操作函数都是支持Wrappers的,如file_put_contents,fwrite,file_get_contents,include,file_exists, filemtime,move_uploaded_file等等。  只有个别带特殊功能的函数不支持Wrappers
 
8 SAE禁用了一些函数和类
 
    出于平台安全性考虑,SAE禁用了一些函数了类,详情查看
http://sae.sina.com.cn/?m=devcenter&catId=220 
 
    请使用功能相同的其他函数代替禁用的函数,如可用SimpleXML代替DoMDocument实现XML解析。
 
五,提交到应用仓库
 
    开发者可以将移植好的程序提交到SAE的应用仓库, 这样别人可以通过应用仓库一键安装就能快速获得你的程序。
 
    建议大家在移植程序的时候写一篇“移植记录”。提交应用时一并将移植记录提交给我们。这样方便我们审核你的应用。
 
    移植记录需要写清楚移植程序时哪些地方做了修改,使用了什么SAE服务等。
   
    提交到应用仓库的更多细节请见 http://sae.sina.com.cn/?m=devcenter&catId=230
    或直接访问以下地址:http://sae.sina.com.cn/?m=apps&a=step_sae_app

SAE的storage服务

今天终于弄懂了sae的storage怎么使用。

这里是sae的storage的概述地址:

http://sae.sina.com.cn/doc/php/storage.html。

因为sae不支持文件权限设置,所有的代码文件都是不可数的。但是sae提供了特别好的一个分布式文件存储服务,用来存放持久文件。其实就是用来存放网站的素材文件,如图片,文件等等的。

所以,当需要存储文件数据的时候,可以使用使用sae。

 

使用sae一共有两种方案。

官方提供的一种方案是:

使用官方的storage类,直接可以进行文件操作,如存放文件,删除文件,修改文件等等。

官方API文档地址:http://apidoc.sinaapp.com/sae/SaeStorage.html

 

还有一种方法是我在sae的应用商店看到的,里面有一个合成的wordpress for sae。

我们可以看到详细的代码,我现在将自己简单的测试代码贴到下面:(只为实现storage,不适用于开发场景)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
<html>
<body>
 
<form action="" method="post" enctype="multipart/form-data">
<label for="file">Filename:</label>
<input type="file" name="file" id="file" /> 
<br />
<input type="submit" name="submit" value="Submit" />
</form>
 
</body>
</html>
 
<?php
 
$domain = "test";
$upload_dir = "saestor://" . $domain . "/qiao1/2/3/4/5/";
 
if(!is_dir($upload_dir))
{
	//mkdir($upload_dir , 0777);
}
 
if ($_FILES["file"]["error"] > 0)
  {
  echo "Error: " . $_FILES["file"]["error"] . "<br />";
  }
else
  {
  echo "Upload: " . $_FILES["file"]["name"] . "<br />";
  echo "Type: " . $_FILES["file"]["type"] . "<br />";
  echo "Size: " . ($_FILES["file"]["size"] / 1024) . " Kb<br />";
  echo "Stored in: " . $_FILES["file"]["tmp_name"];
 
  }
if(move_uploaded_file($_FILES["file"]["tmp_name"],$upload_dir . $_FILES["file"]['name']))
{
	echo 'ok';
}
 
 
//mkdir($upload_dir . 'qiao' , 0777);
 
?>

我们可以直接使用move_uploaded_file函数将临时文件上传。要注意的是第二个参数$upload_dir=”saestor://” . $domain . “/” . $dir。

其中,$domain就是storage中创建的storage服务名称。$dir就是要存放的目录。

如要将文件存放在saestor://mydomain/dir1/dir2/dir3/下。可以直接使用该url,而不需要使用mkdir函数进行创建,因为stotage已经将该目录自动进行了创建。

saefetchurl.class.php

源码地址:http://apidoc.sinaapp.com/sae/_saefetchurl.class.php.html

作用:SAE数据抓取class

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
<?php
/**
 * SAE数据抓取服务
 *
 * @author  zhiyong
 * @version $Id$
 * @package sae
 *
 */
 
/**
 * SAE数据抓取class
 *
 * SaeFetchurl用于抓取外部数据。支持的协议为http/https。<br />
 * 该类已停止更新,如不能满足需求,请直接使用curl
 *
 * 默认超时时间:
 *  - 连接超时: 5秒
 *  - 发送数据超时: 30秒
 *  - 接收数据超时: 40秒
 *
 * 抓取页面
 * <code>
 * $f = new SaeFetchurl();
 * $content = $f->fetch('http://sina.cn');
 * </code>
 *
 * 发起POST请求
 * <code>
 * $f = new SaeFetchurl();
 * $f->setMethod('post');
 * $f->setPostData( array('name'=> 'easychen' , 'email' => 'easychen@gmail.com' , 'file' => '@本地文件地址') );
 * $ret = $f->fetch('http://photo.sinaapp.com/save.php');
 * 
 * //抓取失败时输出错误码和错误信息
 * if ($ret === false)
 *         var_dump($f->errno(), $f->errmsg());
 * </code>
 *
 * 错误码参考:
 *  - errno: 0         成功
 *  - errno: 600     fetchurl 服务内部错误
 *  - errno: 601     accesskey 不存在
 *  - errno: 602     认证错误,可能是secretkey错误
 *  - errno: 603     超出fetchurl的使用配额
 *  - errno: 604     REST 协议错误,相关的header不存在或其它错误,建议使用SAE提供的fetch_url函数
 *  - errno: 605     请求的URI格式不合法
 *  - errno: 606     请求的URI,服务器不可达。
 *
 * @author  zhiyong
 * @version $Id$
 * @package sae
 *
 */
class SaeFetchurl extends SaeObject
{
    function __construct( $akey = NULL , $skey = NULL )
    {
        if( $akey === NULL )
            $akey = SAE_ACCESSKEY;
 
        if( $skey === NULL )
            $skey = SAE_SECRETKEY;
 
        $this->impl_ = new FetchUrl($akey, $skey);
        $this->method_ = "get";
        $this->cookies_ = array();
        $this->opt_ = array();
        $this->headers_ = array();
    }
 
    /**
     * 设置acccesskey和secretkey
     *
     * 使用当前的应用的key时,不需要调用此方法
     *
     * @param string $akey 
     * @param string $skey 
     * @return void 
     * @author zhiyong
     * @ignore
     */
    public function setAuth( $akey , $skey )
    {
        $this->impl_->setAccesskey($akey);
        $this->impl_->setSecretkey($skey);
    }
 
    /**
     * @ignore
     */
    public function setAccesskey( $akey )
    {
        $this->impl_->setAccesskey($akey);
    }
 
    /**
     * @ignore
     */
    public function setSecretkey( $skey )
    {
        $this->impl_->setSecretkey($skey);
    }
 
    /**
     * 设置请求的方法(POST/GET/PUT... )
     *
     * @param string $method 
     * @return void 
     * @author zhiyong
     */
    public function setMethod( $method )
    {
        $this->method_ = trim($method);
        $this->opt_['method'] = trim($method);
    }
 
    /**
     * 设置POST方法的数据
     *
     * @param array|string$post_data 当格式为array时,key为变量名称,value为变量值,使用multipart方式提交。当格式为string时,直接做为post的content提交。与curl_setopt($ch, CURLOPT_POSTFIELDS, $data)中$data的格式相同。
     * @param bool $multipart value是否为二进制数据
     * @return bool 
     * @author zhiyong
     */
    public function setPostData( $post_data , $multipart = false )
    {
        $this->opt_["post"] = $post_data;
        $this->opt_["multipart"] = $multipart;
 
        return true;
    }
 
    /**
     * 在发起的请求中,添加请求头
     *
     * 不可以使用此方法设定的头:
     *  - Content-Length
     *  - Vary
     *  - Via
     *  - X-Forwarded-For
     *  - FetchUrl
     *  - AccessKey
     *  - TimeStamp
     *  - Signature
     *  - AllowTruncated    //可使用setAllowTrunc方法来进行设定
     *  - ConnectTimeout    //可使用setConnectTimeout方法来进行设定
     *  - SendTimeout        //可使用setSendTimeout方法来进行设定
     *  - ReadTimeout        //可使用setReadTimeout方法来进行设定
     *
     *
     * @param string $name 
     * @param string $value 
     * @return bool 
     * @author zhiyong
     */
    public function setHeader( $name , $value )
    {
        $name = trim($name);
        if (!in_array(strtolower($name), FetchUrl::$disabledHeaders)) {
            $this->headers_[$name] = $value;
            return true;
        } else {
            trigger_error("Disabled FetchUrl Header:" . $name, E_USER_NOTICE);
            return false;
        }
    }
 
    /**
     * 设置FetchUrl参数
     *
     * 参数列表:
     *  - truncated        布尔        是否截断
     *  - redirect            布尔        是否支持重定向
     *  - username            字符串        http认证用户名
     *  - password            字符串        http认证密码
     *  - useragent        字符串        自定义UA
     *
     * @param string $name 
     * @param string $value 
     * @return void 
     * @author Elmer Zhang
     * @ignore
     */
    public function setOpt( $name , $value )
    {
        $name = trim($name);
        $this->opt_[$name] = $value;
    }
 
    /**
     * 在发起的请求中,批量添加cookie数据
     *
     * @param array $cookies 要添加的Cookies,格式:array('key1' => 'value1', 'key2' => 'value2', ....)
     * @return void 
     * @author zhiyong
     */
    public function setCookies( $cookies = array() )
    {
        if ( is_array($cookies) and !empty($cookies) ) {
            foreach ( $cookies as $k => $v ) {
                $this->setCookie($k, $v);
            }
        }
    }
 
    /**
     * 在发起的请求中,添加cookie数据,此函数可多次调用,添加多个cookie
     *
     * @param string $name 
     * @param string $value 
     * @return void 
     * @author zhiyong
     */
    public function setCookie( $name , $value )
    {
        $name = trim($name);
        array_push($this->cookies_, "$name=$value");
    }
 
    /**
     * 是否允许截断,默认为不允许
     *
     * 如果设置为true,当发送数据超过允许大小时,自动截取符合大小的部分;<br />
     * 如果设置为false,当发送数据超过允许大小时,直接返回false;
     *
     * @param bool $allow 
     * @return void 
     * @author zhiyong
     */
    public function setAllowTrunc($allow) {
        $this->opt_["truncated"] = $allow;
    }
 
    /**
     * 设置连接超时时间,此时间必须小于SAE系统设置的时间,否则以SAE系统设置为准(默认为5秒)
     *
     * @param int $ms 毫秒
     * @return void 
     * @author zhiyong
     */
    public function setConnectTimeout($ms) {
        $this->opt_["connecttimeout"] = $ms;
    }
 
    /**
     * 设置发送超时时间,此时间必须小于SAE系统设置的时间,否则以SAE系统设置为准(默认为20秒)
     *
     * @param int $ms 毫秒
     * @return void 
     * @author zhiyong
     */
    public function setSendTimeout($ms) {
        $this->opt_["sendtimeout"] = $ms;
    }
 
    /**
     * 设置读取超时时间,此时间必须小于SAE系统设置的时间,否则以SAE系统设置为准(默认为60秒)
     *
     * @param int $ms 毫秒
     * @return void 
     * @author zhiyong
     */
    public function setReadTimeout($ms) {
        $this->opt_["ReadTimeout"] = $ms;
    }
 
    /**
     * 当请求页面是转向页时,是否允许跳转,SAE最大支持5次跳转(默认允许跳转)
     *
     * @param bool $allow 是否允许跳转。true:允许,false:禁止,默认为true
     * @return void 
     * @author zhiyong
     */
    public function setAllowRedirect($allow = true) {
        $this->opt_["redirect"] = $allow;
    }
 
    /**
     * 设置HTTP认证用户名密码
     *
     * @param string $username HTTP认证用户名
     * @param string $password HTTP认证密码
     * @return void 
     * @author zhiyong
     */
    public function setHttpAuth($username, $password) {
        $this->opt_["username"] = $username;
        $this->opt_["password"] = $password;
    }
 
    /**
     * 发起请求
     *
     * <code>
     * <?php
     * echo "Use callback function\n";
     *
     * function demo($content) {
     *         echo strtoupper($content);
     * }
     * 
     * $furl = new SaeFetchurl();
     * $furl->fetch($url, $opt, 'demo');
     * 
     * echo "Use callback class\n";
     * 
     * class Ctx {
     *      public function demo($content) {
     *                 $this->c .= $content;
     *         }
     *         public $c;
     * };
     * 
     * $ctx = new Ctx;
     * $furl = new SaeFetchurl();
     * $furl->fetch($url, $opt, array($ctx, 'demo'));
     * echo $ctx->c;
     * ?>
     * </code>
     *
     * @param string $url 
     * @param array $opt 请求参数,格式:array('key1'=>'value1', 'key2'=>'value2', ... )。参数列表:
     *   - truncated        布尔        是否截断
     *   - redirect            布尔        是否支持重定向
     *   - username            字符串        http认证用户名
     *   - password            字符串        http认证密码
     *   - useragent        字符串        自定义UA
     * @param callback $callback 用来处理返回的数据的函数。可以为函数名或某个实例对象的方法。
     * @return string 成功时读取到的内容,否则返回false
     * @author zhiyong
     */
    public function fetch( $url, $opt = NULL, $callback=NULL )
    {
        if (count($this->cookies_) != 0) {
            $this->opt_["cookie"] = join("; ", $this->cookies_);
        }
        $opt = ($opt) ?  array_merge($this->opt_, $opt) : $this->opt_;
        return $this->impl_->fetch($url, $opt, $this->headers_, $callback);
    }
 
    /**
     * 返回数据的header信息
     *
     * @param bool $parse 是否解析header,默认为true。
     * @return array 
     * @author zhiyong
     */
    public function responseHeaders($parse = true)
    {
        $items = explode("\r\n", $this->impl_->headerContent());
        if (!$parse) {
            return $items;
        }
        array_shift($items);
        $headers = array();
        foreach ($items as $_) {
            $pos = strpos($_, ":");
            $key = trim(substr($_, 0, $pos));
            $value = trim(substr($_, $pos + 1));
            if ($key == "Set-Cookie") {
                if (array_key_exists($key, $headers)) {
                    array_push($headers[$key], trim($value));
                } else {
                    $headers[$key] = array(trim($value));
                }
            } else {
                $headers[$key] = trim($value);
            }
        }
        return $headers;
    }
 
    /**
     * 返回HTTP状态码
     *
     * @return int 
     * @author Elmer Zhang
     */
    public function httpCode() {
        return $this->impl_->httpCode();
    }
 
    /**
     * 返回网页内容
     * 常用于fetch()方法返回false时
     *
     * @return string 
     * @author Elmer Zhang
     */
    public function body() {
        return $this->impl_->body();
    }
 
    /**
     * 返回头里边的cookie信息
     * 
     * @param bool $all 是否返回完整Cookies信息。为true时,返回Cookie的name,value,path,max-age,为false时,只返回Cookies的name, value
     * @return array 
     * @author zhiyong
     */
    public function responseCookies($all = true)
    {
        $header = $this->impl_->headerContent();
        $matchs = array();
        $cookies = array();
        $kvs = array();
        if (preg_match_all('/Set-Cookie:\s([^\r\n]+)/i', $header, $matchs)) {
            foreach ($matchs[1] as $match) {
                $cookie = array();
                $items = explode(";", $match);
                foreach ($items as $_) {
                    $item = explode("=", trim($_));
                    $cookie[$item[0]]= $item[1];
                }
                array_push($cookies, $cookie);
                $kvs = array_merge($kvs, $cookie);
            }
        }
        if ($all) {
            return $cookies;
        } else {
            unset($kvs['path']);
            unset($kvs['max-age']);
            return $kvs;
        }
    }
 
    /**
     * 返回错误码
     *
     * @return int 
     * @author zhiyong
     */
    public function errno()
    {
        if ($this->impl_->errno() != 0) {
            return $this->impl_->errno();
        } else {
            if ($this->impl_->httpCode() != 200) {
                return $this->impl_->httpCode();
            }
        }
        return 0;
    }
 
    /**
     * 返回错误信息
     *
     * @return string 
     * @author zhiyong
     */
    public function errmsg()
    {
        if ($this->impl_->errno() != 0) {
            return $this->impl_->error();
        } else {
            if ($this->impl_->httpCode() != 200) {
                return $this->impl_->httpDesc();
            }
        }
        return "";
    }
 
    /**
     * 将对象的数据重新初始化,用于多次重用一个SaeFetchurl对象
     *
     * @return void 
     * @author Elmer Zhang
     */
    public function clean() {
        $this->__construct();
    }
 
    /**
     * 开启/关闭调试模式
     *
     * @param bool $on true:开启调试;false:关闭调试
     * @return void 
     * @author Elmer Zhang
     */
    public function debug($on) {
        if ($on) {
            $this->impl_->setDebugOn();
        } else {
            $this->impl_->setDebugOff();
        }
    }
 
 
    private $impl_;
    private $opt_;
    private $headers_;
 
}
 
 
/**
 * FetchUrl , the sub class of SaeFetchurl
 *
 *
 * @package sae
 * @subpackage fetchurl
 * @author  zhiyong
 * @ignore
 */
class FetchUrl {
    const end_         = "http://fetchurl.sae.sina.com.cn/";
    const maxRedirect_ = 5;
    public static $disabledHeaders = array(
        'content-length',
        'vary',
        'via',
        'x-forwarded-for',
        'fetchurl',
        'accesskey',
        'timestamp',
        'signature',
        'allowtruncated',
        'connecttimeout',
        'sendtimeout',
        'readtimeout',
    );
 
    public function __construct($accesskey, $secretkey) {
        $accesskey = trim($accesskey);
        $secretkey = trim($secretkey);
 
        $this->accesskey_ = $accesskey;
        $this->secretkey_ = $secretkey;
 
        $this->errno_ = 0;
        $this->error_ = null;
        $this->debug_ = false;
    }
 
    public function __destruct() {
        // do nothing
    }
 
    public function setAccesskey($accesskey) {
        $accesskey = trim($accesskey);
        $this->accesskey_ = $accesskey;
    }
 
    public function setSecretkey($secretkey) {
        $secretkey = trim($secretkey);
        $this->secretkey_ = $secretkey;
    }
 
    public function setDebugOn() {
        $this->debug_ = true;
    }
 
    public function setDebugOff() {
        $this->debug_ = false;
    }
 
    public function fetch($url, $opt = null, $headers = null, $callback = null) {
 
        $url = trim($url);
        if (substr($url, 0, 7) != 'http://' && substr($url, 0, 8) != 'https://') {
            $url = 'http://' . $url;
        }
 
        $this->callback_ = $callback;
 
        $maxRedirect = FetchUrl::maxRedirect_;
        if (is_array($opt) && array_key_exists('redirect',$opt) && !$opt['redirect']) {
            $maxRedirect = 1;
        }
 
        for ($i = 0; $i < $maxRedirect; ++$i) {
            $this->dofetch($url, $opt, $headers);
            if ($this->errno_ == 0) {
                if ($this->httpCode_ == 301 || $this->httpCode_ == 302) {
                    $matchs = array();
                    if (preg_match('/Location:\s([^\r\n]+)/i', $this->header_, $matchs)) {
                        $newUrl = $matchs[1];
                        // if new domain
                        if (strncasecmp($newUrl, "http://", strlen("http://")) == 0) {
                            $url = $newUrl;
                        } else {
                            $url = preg_replace('/^((?:https?:\/\/)?[^\/]+)\/(.*)$/i', '$1', $url) . "/". $newUrl;
                        }
 
                        if ($this->debug_) {
                            echo "[debug] redirect to $url\n";
                        }
                        continue;
                    }
                }
            }
            break;
        }
 
        if ($this->errno_ == 0 && $this->httpCode_ == 200) {
            return $this->body_;
        } else {
            return false;
        }
    }
 
    public function headerContent() {
        return $this->header_;
    }
 
    public function errno() {
        return $this->errno_;
    }
 
    public function error() {
        return $this->error_;
    }
 
    public function httpCode() {
        return $this->httpCode_;
    }
 
    public function body() {
        return $this->body_;
    }
 
    public function httpDesc() {
        return $this->httpDesc_;
    }
 
    private function signature($url, $timestamp) {
        $content = "FetchUrl"  . $url .
            "TimeStamp" . $timestamp .
            "AccessKey" . $this->accesskey_;
        $signature = (base64_encode(hash_hmac('sha256',$content,$this->secretkey_,true)));
        if ($this->debug_) {
            echo "[debug] content: $content" . "\n";
            echo "[debug] signature: $signature" . "\n";
        }
        return $signature;
    }
 
    // we have to set wirteBody & writeHeader public
    // for we used them in curl_setopt()
    public function writeBody($ch, $body) {
        if ($this->callback_) {
            call_user_func($this->callback_, $body);
        } else {
            $this->body_ .= $body;    
        }
        if ($this->debug_) {
            echo "[debug] body => $body";
        }
        return strlen($body);
    }
 
    public function writeHeader($ch, $header) {
        $this->header_ .= $header;
        if ($this->debug_) {
            echo "[debug] header => $header";    
        }
        return strlen($header);    
    }
 
    private function dofetch($url, $opt, $headers_) {
 
        $this->header_ = $this->body_ = null;
        $headers = array();
 
        $ch = curl_init();
        curl_setopt($ch, CURLOPT_URL, $url);
        curl_setopt($ch, CURLOPT_WRITEFUNCTION, array($this, 'writeBody'));
        curl_setopt($ch, CURLOPT_HEADERFUNCTION, array($this, 'writeHeader'));
        if ($this->debug_) {
            curl_setopt($ch, CURLOPT_VERBOSE, true);
        }
 
        if (is_array($opt) && !empty($opt)) {
            foreach( $opt as $k => $v) {
                switch(strtolower($k)) {
                case 'username':
                    if (array_key_exists("password",$opt)) {
                        curl_setopt($ch, CURLOPT_USERPWD, $v . ":" . $opt["password"]);
                    }
                    break;
                case 'password':
                    if (array_key_exists("username",$opt)) {
                        curl_setopt($ch, CURLOPT_USERPWD, $opt["username"] . ":" . $v);
                    }
                    break;
                case 'useragent':
                    curl_setopt($ch, CURLOPT_USERAGENT, $v);
                    break;
                case 'post':
                    curl_setopt($ch, CURLOPT_POSTFIELDS, $v);
                    break;
                case 'cookie':
                    curl_setopt($ch, CURLOPT_COOKIESESSION, true);
                    curl_setopt($ch, CURLOPT_COOKIE, $v);
                    break;
                case 'multipart':
                    if ($v) array_push($headers, "Content-Type: multipart/form-data");
                    break;
                case 'truncated':
                    array_push($headers, "AllowTruncated:" . $v);
                    break;
                case 'connecttimeout':
                    array_push($headers, "ConnectTimeout:" . intval($v));
                    curl_setopt($ch, 156, intval($v));
                    break;
                case 'sendtimeout':
                    array_push($headers, "SendTimeout:" . intval($v));
                    curl_setopt($ch, 156, intval($v));
                    break;
                case 'readtimeout':
                    array_push($headers, "ReadTimeout:" . intval($v));
                    curl_setopt($ch, 156, intval($v));
                    break;
                default:
                    break;
 
                }
            }
        }
 
        if (isset($opt['method'])) {
            if (strtolower($opt['method']) == 'get') {
                curl_setopt($ch, CURLOPT_HTTPGET, true);
            }
        }
 
        if (is_array($headers_) && !empty($headers_)) {
            foreach($headers_ as $k => $v) {
                if (!in_array(strtolower($k), FetchUrl::$disabledHeaders)) {
                    array_push($headers, "{$k}:" . $v);
                }
            }
        }
 
        curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
        curl_setopt($ch, CURLOPT_ENCODING, "");
        curl_setopt($ch, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_1_0);
        curl_setopt($ch, CURLINFO_HEADER_OUT, true);
 
        curl_exec($ch);
        $info = curl_getinfo($ch);
        if ($this->debug_) {
            echo "[debug] curl_getinfo => " . print_r($info, true) . "\n";
        }
        $this->errno_ = curl_errno($ch);
        $this->error_ = curl_error($ch);
 
        if ($this->errno_ == 0) {
            $matchs = array();
            if (preg_match('/^(?:[^\s]+)\s([^\s]+)\s([^\r\n]+)/', $this->header_, $matchs)) {
                $this->httpCode_ = $matchs[1];
                $this->httpDesc_ = $matchs[2];
                if ($this->debug_) {
                    echo "[debug] httpCode = " . $this->httpCode_ . "  httpDesc = " . $this->httpDesc_ . "\n";
                }
            } else {
                $this->errno_ = -1;
                $this->error_ = "invalid response";
            }
        }
        curl_close($ch);
    }
 
    private $accesskey_;
    private $secretkey_;
 
    private $errno_;
    private $error_;
 
    private $httpCode_;
    private $httpDesc_;
    private $header_;
    private $body_;
 
    private $debug_;
 
}