WordPress 3.4
[autoinstalls/wordpress.git] / wp-includes / class-wp-atom-server.php
1 <?php
2
3 /**
4  * WordPress AtomPub API implementation.
5  *
6  * @package WordPress
7  * @subpackage Publishing
8  * @since 2.2.0
9  */
10 class wp_atom_server {
11
12         /**
13          * ATOM content type.
14          *
15          * @since 2.2.0
16          * @var string
17          */
18         var $ATOM_CONTENT_TYPE = 'application/atom+xml';
19
20         /**
21          * Categories ATOM content type.
22          *
23          * @since 2.2.0
24          * @var string
25          */
26         var $CATEGORIES_CONTENT_TYPE = 'application/atomcat+xml';
27
28         /**
29          * Service ATOM content type.
30          *
31          * @since 2.3.0
32          * @var string
33          */
34         var $SERVICE_CONTENT_TYPE = 'application/atomsvc+xml';
35
36         /**
37          * ATOM XML namespace.
38          *
39          * @since 2.3.0
40          * @var string
41          */
42         var $ATOM_NS = 'http://www.w3.org/2005/Atom';
43
44         /**
45          * ATOMPUB XML namespace.
46          *
47          * @since 2.3.0
48          * @var string
49          */
50         var $ATOMPUB_NS = 'http://www.w3.org/2007/app';
51
52         /**
53          * Entries path.
54          *
55          * @since 2.2.0
56          * @var string
57          */
58         var $ENTRIES_PATH = "posts";
59
60         /**
61          * Categories path.
62          *
63          * @since 2.2.0
64          * @var string
65          */
66         var $CATEGORIES_PATH = "categories";
67
68         /**
69          * Media path.
70          *
71          * @since 2.2.0
72          * @var string
73          */
74         var $MEDIA_PATH = "attachments";
75
76         /**
77          * Entry path.
78          *
79          * @since 2.2.0
80          * @var string
81          */
82         var $ENTRY_PATH = "post";
83
84         /**
85          * Service path.
86          *
87          * @since 2.2.0
88          * @var string
89          */
90         var $SERVICE_PATH = "service";
91
92         /**
93          * Media single path.
94          *
95          * @since 2.2.0
96          * @var string
97          */
98         var $MEDIA_SINGLE_PATH = "attachment";
99
100         /**
101          * ATOMPUB parameters.
102          *
103          * @since 2.2.0
104          * @var array
105          */
106         var $params = array();
107
108         /**
109          * Supported ATOMPUB media types.
110          *
111          * @since 2.3.0
112          * @var array
113          */
114         var $media_content_types = array('image/*','audio/*','video/*');
115
116         /**
117          * ATOMPUB content type(s).
118          *
119          * @since 2.2.0
120          * @var array
121          */
122         var $atom_content_types = array('application/atom+xml');
123
124         /**
125          * ATOMPUB methods.
126          *
127          * @since 2.2.0
128          * @var unknown_type
129          */
130         var $selectors = array();
131
132         /**
133          * Whether to do output.
134          *
135          * Support for head.
136          *
137          * @since 2.2.0
138          * @var bool
139          */
140         var $do_output = true;
141
142         /**
143          * Constructor - Sets up object properties.
144          *
145          * @since 2.2.0
146          * @return AtomServer
147          */
148         function __construct() {
149
150                 $var_by_ref = explode( '/', $_SERVER['SCRIPT_NAME'] );
151                 $this->script_name = array_pop( $var_by_ref );
152                 $this->app_base = site_url( $this->script_name . '/' );
153
154                 $this->selectors = array(
155                         '@/service$@' =>
156                                 array('GET' => 'get_service'),
157                         '@/categories$@' =>
158                                 array('GET' => 'get_categories_xml'),
159                         '@/post/(\d+)$@' =>
160                                 array('GET' => 'get_post',
161                                                 'PUT' => 'put_post',
162                                                 'DELETE' => 'delete_post'),
163                         '@/posts/?(\d+)?$@' =>
164                                 array('GET' => 'get_posts',
165                                                 'POST' => 'create_post'),
166                         '@/attachments/?(\d+)?$@' =>
167                                 array('GET' => 'get_attachment',
168                                                 'POST' => 'create_attachment'),
169                         '@/attachment/file/(\d+)$@' =>
170                                 array('GET' => 'get_file',
171                                                 'PUT' => 'put_file',
172                                                 'DELETE' => 'delete_file'),
173                         '@/attachment/(\d+)$@' =>
174                                 array('GET' => 'get_attachment',
175                                                 'PUT' => 'put_attachment',
176                                                 'DELETE' => 'delete_attachment'),
177                 );
178         }
179
180         /**
181          * Handle ATOMPUB request.
182          *
183          * @since 2.2.0
184          */
185         function handle_request() {
186
187                 if ( !empty( $_SERVER['ORIG_PATH_INFO'] ) )
188                         $path = $_SERVER['ORIG_PATH_INFO'];
189                 else
190                         $path = $_SERVER['PATH_INFO'];
191
192                 $method = $_SERVER['REQUEST_METHOD'];
193
194                 $this->process_conditionals();
195                 //$this->process_conditionals();
196
197                 // exception case for HEAD (treat exactly as GET, but don't output)
198                 if ($method == 'HEAD') {
199                         $this->do_output = false;
200                         $method = 'GET';
201                 }
202
203                 // redirect to /service in case no path is found.
204                 if (strlen($path) == 0 || $path == '/')
205                         $this->redirect($this->get_service_url());
206
207                 // check to see if AtomPub is enabled
208                 if ( !get_option( 'enable_app' ) )
209                         $this->forbidden( sprintf( __( 'AtomPub services are disabled on this site. An admin user can enable them at %s' ), admin_url('options-writing.php') ) );
210
211                 // dispatch
212                 foreach ( $this->selectors as $regex => $funcs ) {
213                         if ( preg_match($regex, $path, $matches) ) {
214                                 if ( isset($funcs[$method]) ) {
215
216                                         // authenticate regardless of the operation and set the current
217                                         // user. each handler will decide if auth is required or not.
218                                         if ( !$this->authenticate() ) {
219                                                 $this->auth_required('Credentials required.');
220                                         }
221
222                                         array_shift($matches);
223                                         call_user_func_array(array($this,$funcs[$method]), $matches);
224                                         wp_die();
225                                 } else {
226                                         // only allow what we have handlers for...
227                                         $this->not_allowed(array_keys($funcs));
228                                 }
229                         }
230                 }
231
232                 // oops, nothing found
233                 $this->not_found();
234         }
235
236         /**
237          * Retrieve XML for ATOMPUB service.
238          *
239          * @since 2.2.0
240          */
241         function get_service() {
242
243                 if ( !current_user_can( 'edit_posts' ) )
244                         $this->auth_required( __( 'Sorry, you do not have the right to access this site.' ) );
245
246                 $entries_url = esc_attr($this->get_entries_url());
247                 $categories_url = esc_attr($this->get_categories_url());
248                 $media_url = esc_attr($this->get_attachments_url());
249                 $accepted_media_types = '';
250                 foreach ($this->media_content_types as $med) {
251                         $accepted_media_types = $accepted_media_types . "<accept>" . $med . "</accept>";
252                 }
253                 $atom_prefix="atom";
254                 $atom_blogname = get_bloginfo('name');
255                 $service_doc = <<<EOD
256 <service xmlns="$this->ATOMPUB_NS" xmlns:$atom_prefix="$this->ATOM_NS">
257   <workspace>
258     <$atom_prefix:title>$atom_blogname Workspace</$atom_prefix:title>
259     <collection href="$entries_url">
260       <$atom_prefix:title>$atom_blogname Posts</$atom_prefix:title>
261       <accept>$this->ATOM_CONTENT_TYPE;type=entry</accept>
262       <categories href="$categories_url" />
263     </collection>
264     <collection href="$media_url">
265       <$atom_prefix:title>$atom_blogname Media</$atom_prefix:title>
266       $accepted_media_types
267     </collection>
268   </workspace>
269 </service>
270
271 EOD;
272
273                 $this->output($service_doc, $this->SERVICE_CONTENT_TYPE);
274         }
275
276         /**
277          * Retrieve categories list in XML format.
278          *
279          * @since 2.2.0
280          */
281         function get_categories_xml() {
282
283                 if ( !current_user_can( 'edit_posts' ) )
284                         $this->auth_required( __( 'Sorry, you do not have the right to access this site.' ) );
285
286                 $home = esc_attr(get_bloginfo_rss('url'));
287
288                 $categories = "";
289                 $cats = get_categories(array('hierarchical' => 0, 'hide_empty' => 0));
290                 foreach ( (array) $cats as $cat ) {
291                         $categories .= "    <category term=\"" . esc_attr($cat->name) . "\" />\n";
292                 }
293                 $output = <<<EOD
294 <app:categories xmlns:app="$this->ATOMPUB_NS"
295         xmlns="$this->ATOM_NS"
296         fixed="yes" scheme="$home">
297         $categories
298 </app:categories>
299 EOD;
300                 $this->output($output, $this->CATEGORIES_CONTENT_TYPE);
301         }
302
303         /**
304          * Create new post.
305          *
306          * @since 2.2.0
307          */
308         function create_post() {
309                 global $user_ID;
310                 $this->get_accepted_content_type($this->atom_content_types);
311
312                 $parser = new AtomParser();
313                 if ( !$parser->parse() )
314                         $this->client_error();
315
316                 $entry = array_pop($parser->feed->entries);
317
318                 $catnames = array();
319                 if ( !empty( $entry->categories ) ) {
320                         foreach ( $entry->categories as $cat ) {
321                                 array_push($catnames, $cat["term"]);
322                         }
323                 }
324
325                 $wp_cats = get_categories(array('hide_empty' => false));
326
327                 $post_category = array();
328
329                 foreach ( $wp_cats as $cat ) {
330                         if ( in_array($cat->name, $catnames) )
331                                 array_push($post_category, $cat->term_id);
332                 }
333
334                 $publish = ! ( isset( $entry->draft ) && 'yes' == trim( $entry->draft ) );
335
336                 $cap = ($publish) ? 'publish_posts' : 'edit_posts';
337
338                 if ( !current_user_can($cap) )
339                         $this->auth_required(__('Sorry, you do not have the right to edit/publish new posts.'));
340
341                 $blog_ID = get_current_blog_id();
342                 $post_status = ($publish) ? 'publish' : 'draft';
343                 $post_author = (int) $user_ID;
344
345                 $post_title   = '';
346                 $post_content = '';
347                 $post_excerpt = '';
348                 $pubtimes     = '';
349
350                 if ( isset( $entry->title ) && is_array( $entry->title ) && !empty( $entry->title[1] ) )
351                         $post_title = (string) $entry->title[1];
352                 if ( isset( $entry->content ) && is_array( $entry->content ) && !empty( $entry->content[1] ) )
353                         $post_content = (string) $entry->content[1];
354                 if ( isset( $entry->summary ) && is_array( $entry->summary ) && !empty( $entry->summary[1] ) )
355                         $post_excerpt = (string) $entry->summary[1];
356                 if ( !empty( $entry->published ) )
357                         $pubtimes = (string) $entry->published;
358
359                 $pubtimes = $this->get_publish_time( $pubtimes );
360
361                 $post_date = $pubtimes[0];
362                 $post_date_gmt = $pubtimes[1];
363
364                 if ( isset( $_SERVER['HTTP_SLUG'] ) )
365                         $post_name = $_SERVER['HTTP_SLUG'];
366
367                 $post_data = compact('blog_ID', 'post_author', 'post_date', 'post_date_gmt', 'post_content', 'post_title', 'post_category', 'post_status', 'post_excerpt', 'post_name');
368
369                 $this->escape($post_data);
370
371                 $postID = wp_insert_post($post_data);
372                 if ( is_wp_error( $postID ) )
373                         $this->internal_error($postID->get_error_message());
374
375                 if ( !$postID )
376                         $this->internal_error(__('Sorry, your entry could not be posted. Something wrong happened.'));
377
378                 // getting warning here about unable to set headers
379                 // because something in the cache is printing to the buffer
380                 // could we clean up wp_set_post_categories or cache to not print
381                 // this could affect our ability to send back the right headers
382                 @wp_set_post_categories($postID, $post_category);
383
384                 do_action( 'atompub_create_post', $postID, $entry );
385
386                 $output = $this->get_entry($postID);
387
388                 $this->created($postID, $output);
389         }
390
391         /**
392          * Retrieve post.
393          *
394          * @since 2.2.0
395          *
396          * @param int $postID Post ID.
397          */
398         function get_post($postID) {
399                 global $entry;
400
401                 if ( !current_user_can( 'edit_post', $postID ) )
402                         $this->auth_required( __( 'Sorry, you do not have the right to access this post.' ) );
403
404                 $this->set_current_entry($postID);
405                 $output = $this->get_entry($postID);
406                 $this->output($output);
407
408         }
409
410         /**
411          * Update post.
412          *
413          * @since 2.2.0
414          *
415          * @param int $postID Post ID.
416          */
417         function put_post($postID) {
418                 // checked for valid content-types (atom+xml)
419                 // quick check and exit
420                 $this->get_accepted_content_type($this->atom_content_types);
421
422                 $parser = new AtomParser();
423                 if ( !$parser->parse() )
424                         $this->bad_request();
425
426                 $parsed = array_pop($parser->feed->entries);
427
428                 // check for not found
429                 global $entry;
430                 $this->set_current_entry($postID);
431
432                 if ( !current_user_can('edit_post', $entry['ID']) )
433                         $this->auth_required(__('Sorry, you do not have the right to edit this post.'));
434
435                 $publish = ! ( isset($parsed->draft) && 'yes' == trim($parsed->draft) );
436                 $post_status = ($publish) ? 'publish' : 'draft';
437
438                 extract($entry);
439
440                 $post_title = $parsed->title[1];
441                 $post_content = $parsed->content[1];
442                 $post_excerpt = $parsed->summary[1];
443                 $pubtimes = $this->get_publish_time($entry->published);
444                 $post_date = $pubtimes[0];
445                 $post_date_gmt = $pubtimes[1];
446                 $pubtimes = $this->get_publish_time($parsed->updated);
447                 $post_modified = $pubtimes[0];
448                 $post_modified_gmt = $pubtimes[1];
449
450                 $postdata = compact('ID', 'post_content', 'post_title', 'post_category', 'post_status', 'post_excerpt', 'post_date', 'post_date_gmt', 'post_modified', 'post_modified_gmt');
451                 $this->escape($postdata);
452
453                 $result = wp_update_post($postdata);
454
455                 if ( !$result )
456                         $this->internal_error(__('For some strange yet very annoying reason, this post could not be edited.'));
457
458                 do_action( 'atompub_put_post', $ID, $parsed );
459
460                 $this->ok();
461         }
462
463         /**
464          * Remove post.
465          *
466          * @since 2.2.0
467          *
468          * @param int $postID Post ID.
469          */
470         function delete_post($postID) {
471
472                 // check for not found
473                 global $entry;
474                 $this->set_current_entry($postID);
475
476                 if ( !current_user_can('edit_post', $postID) )
477                         $this->auth_required(__('Sorry, you do not have the right to delete this post.'));
478
479                 if ( $entry['post_type'] == 'attachment' ) {
480                         $this->delete_attachment($postID);
481                 } else {
482                         $result = wp_delete_post($postID);
483
484                         if ( !$result ) {
485                                 $this->internal_error(__('For some strange yet very annoying reason, this post could not be deleted.'));
486                         }
487
488                         $this->ok();
489                 }
490
491         }
492
493         /**
494          * Retrieve attachment.
495          *
496          * @since 2.2.0
497          *
498          * @param int $postID Optional. Post ID.
499          */
500         function get_attachment($postID = null) {
501                 if ( !current_user_can( 'upload_files' ) )
502                         $this->auth_required( __( 'You do not have permission to upload files.' ) );
503
504                 if ( !isset($postID) ) {
505                         $this->get_attachments();
506                 } else {
507                         $this->set_current_entry($postID);
508                         $output = $this->get_entry($postID, 'attachment');
509                         $this->output($output);
510                 }
511         }
512
513         /**
514          * Create new attachment.
515          *
516          * @since 2.2.0
517          */
518         function create_attachment() {
519
520                 $type = $this->get_accepted_content_type();
521
522                 if ( !current_user_can('upload_files') )
523                         $this->auth_required( __( 'You do not have permission to upload files.' ) );
524
525                 $fp = fopen("php://input", "rb");
526                 $bits = null;
527                 while ( !feof($fp) ) {
528                         $bits .= fread($fp, 4096);
529                 }
530                 fclose($fp);
531
532                 $slug = '';
533                 if ( isset( $_SERVER['HTTP_SLUG'] ) )
534                         $slug = $_SERVER['HTTP_SLUG'];
535                 elseif ( isset( $_SERVER['HTTP_TITLE'] ) )
536                         $slug = $_SERVER['HTTP_TITLE'];
537                 elseif ( empty( $slug ) ) // just make a random name
538                         $slug = substr( md5( uniqid( microtime() ) ), 0, 7);
539                 $ext = preg_replace( '|.*/([a-z0-9]+)|', '$1', $_SERVER['CONTENT_TYPE'] );
540                 $slug = sanitize_file_name( "$slug.$ext" );
541                 $file = wp_upload_bits( $slug, null, $bits);
542
543                 $url = $file['url'];
544                 $file = $file['file'];
545
546                 do_action('wp_create_file_in_uploads', $file); // replicate
547
548                 // Construct the attachment array
549                 $attachment = array(
550                         'post_title' => $slug,
551                         'post_content' => $slug,
552                         'post_status' => 'attachment',
553                         'post_parent' => 0,
554                         'post_mime_type' => $type,
555                         'guid' => $url
556                         );
557
558                 // Save the data
559                 $postID = wp_insert_attachment($attachment, $file);
560
561                 if (!$postID)
562                         $this->internal_error(__('Sorry, your entry could not be posted. Something wrong happened.'));
563
564                 $output = $this->get_entry($postID, 'attachment');
565
566                 $this->created($postID, $output, 'attachment');
567         }
568
569         /**
570          * Update attachment.
571          *
572          * @since 2.2.0
573          *
574          * @param int $postID Post ID.
575          */
576         function put_attachment($postID) {
577                 // checked for valid content-types (atom+xml)
578                 // quick check and exit
579                 $this->get_accepted_content_type($this->atom_content_types);
580
581                 $parser = new AtomParser();
582                 if (!$parser->parse()) {
583                         $this->bad_request();
584                 }
585
586                 $parsed = array_pop($parser->feed->entries);
587
588                 // check for not found
589                 global $entry;
590                 $this->set_current_entry($postID);
591
592                 if ( !current_user_can('edit_post', $entry['ID']) )
593                         $this->auth_required(__('Sorry, you do not have the right to edit this post.'));
594
595                 extract($entry);
596
597                 $post_title = $parsed->title[1];
598                 $post_content = $parsed->summary[1];
599                 $pubtimes = $this->get_publish_time($parsed->updated);
600                 $post_modified = $pubtimes[0];
601                 $post_modified_gmt = $pubtimes[1];
602
603                 $postdata = compact('ID', 'post_content', 'post_title', 'post_category', 'post_status', 'post_excerpt', 'post_modified', 'post_modified_gmt');
604                 $this->escape($postdata);
605
606                 $result = wp_update_post($postdata);
607
608                 if ( !$result )
609                         $this->internal_error(__('For some strange yet very annoying reason, this post could not be edited.'));
610
611                 $this->ok();
612         }
613
614         /**
615          * Remove attachment.
616          *
617          * @since 2.2.0
618          *
619          * @param int $postID Post ID.
620          */
621         function delete_attachment($postID) {
622
623                 // check for not found
624                 global $entry;
625                 $this->set_current_entry($postID);
626
627                 if ( !current_user_can('edit_post', $postID) )
628                         $this->auth_required(__('Sorry, you do not have the right to delete this post.'));
629
630                 $location = get_post_meta($entry['ID'], '_wp_attached_file', true);
631                 $filetype = wp_check_filetype($location);
632
633                 if ( !isset($location) || 'attachment' != $entry['post_type'] || empty($filetype['ext']) )
634                         $this->internal_error(__('Error occurred while accessing post metadata for file location.'));
635
636                 // delete file
637                 @unlink($location);
638
639                 // delete attachment
640                 $result = wp_delete_post($postID);
641
642                 if ( !$result )
643                         $this->internal_error(__('For some strange yet very annoying reason, this post could not be deleted.'));
644
645                 $this->ok();
646         }
647
648         /**
649          * Retrieve attachment from post.
650          *
651          * @since 2.2.0
652          *
653          * @param int $postID Post ID.
654          */
655         function get_file($postID) {
656
657                 // check for not found
658                 global $entry;
659                 $this->set_current_entry($postID);
660
661                 // then whether user can edit the specific post
662                 if ( !current_user_can('edit_post', $postID) )
663                         $this->auth_required(__('Sorry, you do not have the right to edit this post.'));
664
665                 $location = get_post_meta($entry['ID'], '_wp_attached_file', true);
666                 $location = get_option ('upload_path') . '/' . $location;
667                 $filetype = wp_check_filetype($location);
668
669                 if ( !isset($location) || 'attachment' != $entry['post_type'] || empty($filetype['ext']) )
670                         $this->internal_error(__('Error occurred while accessing post metadata for file location.'));
671
672                 status_header('200');
673                 header('Content-Type: ' . $entry['post_mime_type']);
674                 header('Connection: close');
675
676                 if ( $fp = fopen($location, "rb") ) {
677                         status_header('200');
678                         header('Content-Type: ' . $entry['post_mime_type']);
679                         header('Connection: close');
680
681                         while ( !feof($fp) ) {
682                                 echo fread($fp, 4096);
683                         }
684
685                         fclose($fp);
686                 } else {
687                         status_header ('404');
688                 }
689
690                 wp_die();
691         }
692
693         /**
694          * Upload file to blog and add attachment to post.
695          *
696          * @since 2.2.0
697          *
698          * @param int $postID Post ID.
699          */
700         function put_file($postID) {
701
702                 // first check if user can upload
703                 if ( !current_user_can('upload_files') )
704                         $this->auth_required(__('You do not have permission to upload files.'));
705
706                 // check for not found
707                 global $entry;
708                 $this->set_current_entry($postID);
709
710                 // then whether user can edit the specific post
711                 if ( !current_user_can('edit_post', $postID) )
712                         $this->auth_required(__('Sorry, you do not have the right to edit this post.'));
713
714                 $upload_dir = wp_upload_dir( );
715                 $location = get_post_meta($entry['ID'], '_wp_attached_file', true);
716                 $filetype = wp_check_filetype($location);
717
718                 $location = "{$upload_dir['basedir']}/{$location}";
719
720                 if (!isset($location) || 'attachment' != $entry['post_type'] || empty($filetype['ext']))
721                         $this->internal_error(__('Error occurred while accessing post metadata for file location.'));
722
723                 $fp = fopen("php://input", "rb");
724                 $localfp = fopen($location, "w+");
725                 while ( !feof($fp) ) {
726                         fwrite($localfp,fread($fp, 4096));
727                 }
728                 fclose($fp);
729                 fclose($localfp);
730
731                 $ID = $entry['ID'];
732                 $pubtimes = $this->get_publish_time($entry->published);
733                 $post_date = $pubtimes[0];
734                 $post_date_gmt = $pubtimes[1];
735                 $pubtimes = $this->get_publish_time($parsed->updated);
736                 $post_modified = $pubtimes[0];
737                 $post_modified_gmt = $pubtimes[1];
738
739                 $post_data = compact('ID', 'post_date', 'post_date_gmt', 'post_modified', 'post_modified_gmt');
740                 $result = wp_update_post($post_data);
741
742                 if ( !$result )
743                         $this->internal_error(__('Sorry, your entry could not be posted. Something wrong happened.'));
744
745                 wp_update_attachment_metadata( $postID, wp_generate_attachment_metadata( $postID, $location ) );
746
747                 $this->ok();
748         }
749
750         /**
751          * Retrieve entries URL.
752          *
753          * @since 2.2.0
754          *
755          * @param int $page Page ID.
756          * @return string
757          */
758         function get_entries_url($page = null) {
759                 if ( isset($GLOBALS['post_type']) && ( $GLOBALS['post_type'] == 'attachment' ) )
760                         $path = $this->MEDIA_PATH;
761                 else
762                         $path = $this->ENTRIES_PATH;
763                 $url = $this->app_base . $path;
764                 if ( isset($page) && is_int($page) )
765                         $url .= "/$page";
766                 return $url;
767         }
768
769         /**
770          * Display entries URL.
771          *
772          * @since 2.2.0
773          *
774          * @param int $page Page ID.
775          */
776         function the_entries_url($page = null) {
777                 echo $this->get_entries_url($page);
778         }
779
780         /**
781          * Retrieve categories URL.
782          *
783          * @since 2.2.0
784          *
785          * @param mixed $deprecated Not used.
786          * @return string
787          */
788         function get_categories_url($deprecated = '') {
789                 if ( !empty( $deprecated ) )
790                         _deprecated_argument( __FUNCTION__, '2.5' );
791                 return $this->app_base . $this->CATEGORIES_PATH;
792         }
793
794         /**
795          * Display category URL.
796          *
797          * @since 2.2.0
798          */
799         function the_categories_url() {
800                 echo $this->get_categories_url();
801         }
802
803         /**
804          * Retrieve attachment URL.
805          *
806          * @since 2.2.0
807          *
808          * @param int $page Page ID.
809          * @return string
810          */
811         function get_attachments_url($page = null) {
812                 $url = $this->app_base . $this->MEDIA_PATH;
813                 if (isset($page) && is_int($page)) {
814                         $url .= "/$page";
815                 }
816                 return $url;
817         }
818
819         /**
820          * Display attachment URL.
821          *
822          * @since 2.2.0
823          *
824          * @param int $page Page ID.
825          */
826         function the_attachments_url($page = null) {
827                 echo $this->get_attachments_url($page);
828         }
829
830         /**
831          * Retrieve service URL.
832          *
833          * @since 2.3.0
834          *
835          * @return string
836          */
837         function get_service_url() {
838                 return $this->app_base . $this->SERVICE_PATH;
839         }
840
841         /**
842          * Retrieve entry URL.
843          *
844          * @since 2.7.0
845          *
846          * @param int $postID Post ID.
847          * @return string
848          */
849         function get_entry_url($postID = null) {
850                 if (!isset($postID)) {
851                         global $post;
852                         $postID = (int) $post->ID;
853                 }
854
855                 $url = $this->app_base . $this->ENTRY_PATH . "/$postID";
856
857                 return $url;
858         }
859
860         /**
861          * Display entry URL.
862          *
863          * @since 2.7.0
864          *
865          * @param int $postID Post ID.
866          */
867         function the_entry_url($postID = null) {
868                 echo $this->get_entry_url($postID);
869         }
870
871         /**
872          * Retrieve media URL.
873          *
874          * @since 2.2.0
875          *
876          * @param int $postID Post ID.
877          * @return string
878          */
879         function get_media_url($postID = null) {
880                 if (!isset($postID)) {
881                         global $post;
882                         $postID = (int) $post->ID;
883                 }
884
885                 $url = $this->app_base . $this->MEDIA_SINGLE_PATH ."/file/$postID";
886
887                 return $url;
888         }
889
890         /**
891          * Display the media URL.
892          *
893          * @since 2.2.0
894          *
895          * @param int $postID Post ID.
896          */
897         function the_media_url($postID = null) {
898                 echo $this->get_media_url($postID);
899         }
900
901         /**
902          * Set the current entry to post ID.
903          *
904          * @since 2.2.0
905          *
906          * @param int $postID Post ID.
907          */
908         function set_current_entry($postID) {
909                 global $entry;
910
911                 if (!isset($postID)) {
912                         // $this->bad_request();
913                         $this->not_found();
914                 }
915
916                 $entry = wp_get_single_post($postID,ARRAY_A);
917
918                 if (!isset($entry) || !isset($entry['ID']))
919                         $this->not_found();
920
921                 return;
922         }
923
924         /**
925          * Display posts XML.
926          *
927          * @since 2.2.0
928          *
929          * @param int $page Optional. Page ID.
930          * @param string $post_type Optional, default is 'post'. Post Type.
931          */
932         function get_posts($page = 1, $post_type = 'post') {
933                 $feed = $this->get_feed($page, $post_type);
934                 $this->output($feed);
935         }
936
937         /**
938          * Display attachment XML.
939          *
940          * @since 2.2.0
941          *
942          * @param int $page Page ID.
943          * @param string $post_type Optional, default is 'attachment'. Post type.
944          */
945         function get_attachments($page = 1, $post_type = 'attachment') {
946                 $GLOBALS['post_type'] = $post_type;
947                 $feed = $this->get_feed($page, $post_type);
948                 $this->output($feed);
949         }
950
951         /**
952          * Retrieve feed XML.
953          *
954          * @since 2.2.0
955          *
956          * @param int $page Page ID.
957          * @param string $post_type Optional, default is post. Post type.
958          * @return string
959          */
960         function get_feed($page = 1, $post_type = 'post') {
961                 global $post, $wp, $wp_query, $posts, $wpdb, $blog_id;
962                 ob_start();
963
964                 $this->ENTRY_PATH = $post_type;
965
966                 if (!isset($page)) {
967                         $page = 1;
968                 }
969                 $page = (int) $page;
970
971                 $count = get_option('posts_per_rss');
972
973                 wp('posts_per_page=' . $count . '&offset=' . ($count * ($page-1)) . '&orderby=modified&post_status=any');
974
975                 $post = $GLOBALS['post'];
976                 $posts = $GLOBALS['posts'];
977                 $wp = $GLOBALS['wp'];
978                 $wp_query = $GLOBALS['wp_query'];
979                 $wpdb = $GLOBALS['wpdb'];
980                 $blog_id = (int) $GLOBALS['blog_id'];
981
982                 $last_page = $wp_query->max_num_pages;
983                 $next_page = (($page + 1) > $last_page) ? null : $page + 1;
984                 $prev_page = ($page - 1) < 1 ? null : $page - 1;
985                 $last_page = ((int)$last_page == 1 || (int)$last_page == 0) ? null : (int) $last_page;
986                 $self_page = $page > 1 ? $page : null;
987 ?><feed xmlns="<?php echo $this->ATOM_NS ?>" xmlns:app="<?php echo $this->ATOMPUB_NS ?>" xml:lang="<?php bloginfo_rss( 'language' ); ?>" <?php do_action('app_ns'); ?> >
988 <id><?php $this->the_entries_url() ?></id>
989 <updated><?php echo mysql2date('Y-m-d\TH:i:s\Z', get_lastpostmodified('GMT'), false); ?></updated>
990 <title type="text"><?php bloginfo_rss('name') ?></title>
991 <subtitle type="text"><?php bloginfo_rss("description") ?></subtitle>
992 <link rel="first" type="<?php echo $this->ATOM_CONTENT_TYPE ?>" href="<?php $this->the_entries_url() ?>" />
993 <?php if (isset($prev_page)): ?>
994 <link rel="previous" type="<?php echo $this->ATOM_CONTENT_TYPE ?>" href="<?php $this->the_entries_url($prev_page) ?>" />
995 <?php endif; ?>
996 <?php if (isset($next_page)): ?>
997 <link rel="next" type="<?php echo $this->ATOM_CONTENT_TYPE ?>" href="<?php $this->the_entries_url($next_page) ?>" />
998 <?php endif; ?>
999 <link rel="last" type="<?php echo $this->ATOM_CONTENT_TYPE ?>" href="<?php $this->the_entries_url($last_page) ?>" />
1000 <link rel="self" type="<?php echo $this->ATOM_CONTENT_TYPE ?>" href="<?php $this->the_entries_url($self_page) ?>" />
1001 <rights type="text">Copyright <?php echo date('Y'); ?></rights>
1002 <?php do_action('app_head'); ?>
1003 <?php if ( have_posts() ) {
1004                         while ( have_posts() ) {
1005                                 the_post();
1006                                 $this->echo_entry();
1007                         }
1008                 }
1009 ?></feed>
1010 <?php
1011                 $feed = ob_get_contents();
1012                 ob_end_clean();
1013                 return $feed;
1014         }
1015
1016         /**
1017          * Display entry XML.
1018          *
1019          * @since 2.2.0
1020          *
1021          * @param int $postID Post ID.
1022          * @param string $post_type Optional, default is post. Post type.
1023          * @return string.
1024          */
1025         function get_entry($postID, $post_type = 'post') {
1026                 ob_start();
1027                 switch($post_type) {
1028                         case 'post':
1029                                 $varname = 'p';
1030                                 break;
1031                         case 'attachment':
1032                                 $this->ENTRY_PATH = 'attachment';
1033                                 $varname = 'attachment_id';
1034                                 break;
1035                 }
1036                 query_posts($varname . '=' . $postID);
1037                 if ( have_posts() ) {
1038                         while ( have_posts() ) {
1039                                 the_post();
1040                                 $this->echo_entry();
1041                                 $entry = ob_get_contents();
1042                                 break;
1043                         }
1044                 }
1045                 ob_end_clean();
1046
1047                 return $entry;
1048         }
1049
1050         /**
1051          * Display post content XML.
1052          *
1053          * @since 2.3.0
1054          */
1055         function echo_entry() { ?>
1056 <entry xmlns="<?php echo $this->ATOM_NS ?>"
1057        xmlns:app="<?php echo $this->ATOMPUB_NS ?>" xml:lang="<?php bloginfo_rss( 'language' ); ?>">
1058         <id><?php the_guid( $GLOBALS['post']->ID ); ?></id>
1059 <?php list($content_type, $content) = prep_atom_text_construct(get_the_title()); ?>
1060         <title type="<?php echo $content_type ?>"><?php echo $content ?></title>
1061         <updated><?php echo get_post_modified_time('Y-m-d\TH:i:s\Z', true); ?></updated>
1062         <published><?php echo get_post_time('Y-m-d\TH:i:s\Z', true); ?></published>
1063         <app:edited><?php echo get_post_modified_time('Y-m-d\TH:i:s\Z', true); ?></app:edited>
1064         <app:control>
1065                 <app:draft><?php echo ($GLOBALS['post']->post_status == 'draft' ? 'yes' : 'no') ?></app:draft>
1066         </app:control>
1067         <author>
1068                 <name><?php the_author()?></name>
1069 <?php if ( get_the_author_meta('url') && get_the_author_meta('url') != 'http://' ) { ?>
1070                 <uri><?php the_author_meta('url') ?></uri>
1071 <?php } ?>
1072         </author>
1073 <?php if ($GLOBALS['post']->post_type == 'attachment') { ?>
1074         <link rel="edit-media" href="<?php $this->the_media_url() ?>" />
1075         <content type="<?php echo $GLOBALS['post']->post_mime_type ?>" src="<?php the_guid() ; ?>"/>
1076 <?php } else { ?>
1077         <link href="<?php the_permalink_rss() ?>" />
1078 <?php if ( strlen( $GLOBALS['post']->post_content ) ) :
1079 list($content_type, $content) = prep_atom_text_construct(get_the_content()); ?>
1080         <content type="<?php echo $content_type ?>"><?php echo $content ?></content>
1081 <?php endif; ?>
1082 <?php } ?>
1083         <link rel="edit" href="<?php $this->the_entry_url() ?>" />
1084         <?php the_category_rss( 'atom' ); ?>
1085 <?php list($content_type, $content) = prep_atom_text_construct(get_the_excerpt()); ?>
1086         <summary type="<?php echo $content_type ?>"><?php echo $content ?></summary>
1087         <?php do_action('app_entry'); ?>
1088 </entry>
1089 <?php }
1090
1091         /**
1092          * Set 'OK' (200) status header.
1093          *
1094          * @since 2.2.0
1095          */
1096         function ok() {
1097                 header('Content-Type: text/plain');
1098                 status_header('200');
1099                 wp_die();
1100         }
1101
1102         /**
1103          * Set 'No Content' (204) status header.
1104          *
1105          * @since 2.2.0
1106          */
1107         function no_content() {
1108                 header('Content-Type: text/plain');
1109                 status_header('204');
1110                 echo "Moved to Trash.";
1111                 wp_die();
1112         }
1113
1114         /**
1115          * Display 'Internal Server Error' (500) status header.
1116          *
1117          * @since 2.2.0
1118          *
1119          * @param string $msg Optional. Status string.
1120          */
1121         function internal_error($msg = 'Internal Server Error') {
1122                 header('Content-Type: text/plain');
1123                 status_header('500');
1124                 echo $msg;
1125                 wp_die();
1126         }
1127
1128         /**
1129          * Set 'Bad Request' (400) status header.
1130          *
1131          * @since 2.2.0
1132          */
1133         function bad_request() {
1134                 header('Content-Type: text/plain');
1135                 status_header('400');
1136                 wp_die();
1137         }
1138
1139         /**
1140          * Set 'Length Required' (411) status header.
1141          *
1142          * @since 2.2.0
1143          */
1144         function length_required() {
1145                 header("HTTP/1.1 411 Length Required");
1146                 header('Content-Type: text/plain');
1147                 status_header('411');
1148                 wp_die();
1149         }
1150
1151         /**
1152          * Set 'Unsupported Media Type' (415) status header.
1153          *
1154          * @since 2.2.0
1155          */
1156         function invalid_media() {
1157                 header("HTTP/1.1 415 Unsupported Media Type");
1158                 header('Content-Type: text/plain');
1159                 wp_die();
1160         }
1161
1162         /**
1163          * Set 'Forbidden' (403) status header.
1164          *
1165          * @since 2.6.0
1166          */
1167         function forbidden($reason='') {
1168                 header('Content-Type: text/plain');
1169                 status_header('403');
1170                 echo $reason;
1171                 wp_die();
1172         }
1173
1174         /**
1175          * Set 'Not Found' (404) status header.
1176          *
1177          * @since 2.2.0
1178          */
1179         function not_found() {
1180                 header('Content-Type: text/plain');
1181                 status_header('404');
1182                 wp_die();
1183         }
1184
1185         /**
1186          * Set 'Not Allowed' (405) status header.
1187          *
1188          * @since 2.2.0
1189          */
1190         function not_allowed($allow) {
1191                 header('Allow: ' . join(',', $allow));
1192                 status_header('405');
1193                 wp_die();
1194         }
1195
1196         /**
1197          * Display Redirect (302) content and set status headers.
1198          *
1199          * @since 2.3.0
1200          */
1201         function redirect($url) {
1202                 $escaped_url = esc_attr($url);
1203                 $content = <<<EOD
1204 <!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML 2.0//EN">
1205 <html>
1206   <head>
1207     <title>302 Found</title>
1208   </head>
1209 <body>
1210   <h1>Found</h1>
1211   <p>The document has moved <a href="$escaped_url">here</a>.</p>
1212   </body>
1213 </html>
1214
1215 EOD;
1216                 header('HTTP/1.1 302 Moved');
1217                 header('Content-Type: text/html');
1218                 header('Location: ' . $url);
1219                 echo $content;
1220                 wp_die();
1221
1222         }
1223
1224         /**
1225          * Set 'Client Error' (400) status header.
1226          *
1227          * @since 2.2.0
1228          */
1229         function client_error($msg = 'Client Error') {
1230                 header('Content-Type: text/plain');
1231                 status_header('400');
1232                 wp_die();
1233         }
1234
1235         /**
1236          * Set created status headers (201).
1237          *
1238          * Sets the 'content-type', 'content-location', and 'location'.
1239          *
1240          * @since 2.2.0
1241          */
1242         function created($post_ID, $content, $post_type = 'post') {
1243                 $edit = $this->get_entry_url($post_ID);
1244                 switch($post_type) {
1245                         case 'post':
1246                                 $ctloc = $this->get_entry_url($post_ID);
1247                                 break;
1248                         case 'attachment':
1249                                 $edit = $this->app_base . "attachments/$post_ID";
1250                                 break;
1251                 }
1252                 header("Content-Type: $this->ATOM_CONTENT_TYPE");
1253                 if (isset($ctloc))
1254                         header('Content-Location: ' . $ctloc);
1255                 header('Location: ' . $edit);
1256                 status_header('201');
1257                 echo $content;
1258                 wp_die();
1259         }
1260
1261         /**
1262          * Set 'Auth Required' (401) headers.
1263          *
1264          * @since 2.2.0
1265          *
1266          * @param string $msg Status header content and HTML content.
1267          */
1268         function auth_required($msg) {
1269                 nocache_headers();
1270                 header('WWW-Authenticate: Basic realm="WordPress Atom Protocol"');
1271                 header("HTTP/1.1 401 $msg");
1272                 header('Status: 401 ' . $msg);
1273                 header('Content-Type: text/html');
1274                 $content = <<<EOD
1275 <!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML 2.0//EN">
1276 <html>
1277   <head>
1278     <title>401 Unauthorized</title>
1279   </head>
1280 <body>
1281     <h1>401 Unauthorized</h1>
1282     <p>$msg</p>
1283   </body>
1284 </html>
1285
1286 EOD;
1287                 echo $content;
1288                 wp_die();
1289         }
1290
1291         /**
1292          * Display XML and set headers with content type.
1293          *
1294          * @since 2.2.0
1295          *
1296          * @param string $xml Display feed content.
1297          * @param string $ctype Optional, default is 'atom+xml'. Feed content type.
1298          */
1299         function output($xml, $ctype = 'application/atom+xml') {
1300                 status_header('200');
1301                 $xml = '<?xml version="1.0" encoding="' . strtolower(get_option('blog_charset')) . '"?>'."\n".$xml;
1302                 header('Connection: close');
1303                 header('Content-Length: '. strlen($xml));
1304                 header('Content-Type: ' . $ctype);
1305                 header('Content-Disposition: attachment; filename=atom.xml');
1306                 header('Date: '. date('r'));
1307                 if ($this->do_output)
1308                         echo $xml;
1309                 wp_die();
1310         }
1311
1312         /**
1313          * Sanitize content for database usage.
1314          *
1315          * @since 2.2.0
1316          *
1317          * @param array $array Sanitize array and multi-dimension array.
1318          */
1319         function escape(&$array) {
1320                 global $wpdb;
1321
1322                 foreach ($array as $k => $v) {
1323                                 if (is_array($v)) {
1324                                                 $this->escape($array[$k]);
1325                                 } else if (is_object($v)) {
1326                                                 //skip
1327                                 } else {
1328                                                 $array[$k] = $wpdb->escape($v);
1329                                 }
1330                 }
1331         }
1332
1333         /**
1334          * Access credential through various methods and perform login.
1335          *
1336          * @since 2.2.0
1337          *
1338          * @return bool
1339          */
1340         function authenticate() {
1341
1342                 // if using mod_rewrite/ENV hack
1343                 // http://www.besthostratings.com/articles/http-auth-php-cgi.html
1344                 if (isset($_SERVER['HTTP_AUTHORIZATION'])) {
1345                         list($_SERVER['PHP_AUTH_USER'], $_SERVER['PHP_AUTH_PW']) =
1346                                 explode(':', base64_decode(substr($_SERVER['HTTP_AUTHORIZATION'], 6)));
1347                 } else if (isset($_SERVER['REDIRECT_REMOTE_USER'])) {
1348                         // Workaround for setups that do not forward HTTP_AUTHORIZATION
1349                         // See http://trac.wordpress.org/ticket/7361
1350                         list($_SERVER['PHP_AUTH_USER'], $_SERVER['PHP_AUTH_PW']) =
1351                                 explode(':', base64_decode(substr($_SERVER['REDIRECT_REMOTE_USER'], 6)));
1352                 }
1353
1354                 // If Basic Auth is working...
1355                 if (isset($_SERVER['PHP_AUTH_USER']) && isset($_SERVER['PHP_AUTH_PW'])) {
1356
1357                         $user = wp_authenticate($_SERVER['PHP_AUTH_USER'], $_SERVER['PHP_AUTH_PW']);
1358                         if ( $user && !is_wp_error($user) ) {
1359                                 wp_set_current_user($user->ID);
1360                                 return true;
1361                         }
1362                 }
1363
1364                 return false;
1365         }
1366
1367         /**
1368          * Retrieve accepted content types.
1369          *
1370          * @since 2.2.0
1371          *
1372          * @param array $types Optional. Content Types.
1373          * @return string
1374          */
1375         function get_accepted_content_type($types = null) {
1376
1377                 if (!isset($types)) {
1378                         $types = $this->media_content_types;
1379                 }
1380
1381                 if (!isset($_SERVER['CONTENT_LENGTH']) || !isset($_SERVER['CONTENT_TYPE'])) {
1382                         $this->length_required();
1383                 }
1384
1385                 $type = $_SERVER['CONTENT_TYPE'];
1386                 list($type,$subtype) = explode('/',$type);
1387                 list($subtype) = explode(";",$subtype); // strip MIME parameters
1388
1389                 foreach($types as $t) {
1390                         list($acceptedType,$acceptedSubtype) = explode('/',$t);
1391                         if ($acceptedType == '*' || $acceptedType == $type) {
1392                                 if ($acceptedSubtype == '*' || $acceptedSubtype == $subtype)
1393                                         return $type . "/" . $subtype;
1394                         }
1395                 }
1396
1397                 $this->invalid_media();
1398         }
1399
1400         /**
1401          * Process conditionals for posts.
1402          *
1403          * @since 2.2.0
1404          */
1405         function process_conditionals() {
1406
1407                 if (empty($this->params)) return;
1408                 if ($_SERVER['REQUEST_METHOD'] == 'DELETE') return;
1409
1410                 switch($this->params[0]) {
1411                         case $this->ENTRY_PATH:
1412                                 global $post;
1413                                 $post = wp_get_single_post($this->params[1]);
1414                                 $wp_last_modified = get_post_modified_time('D, d M Y H:i:s', true);
1415                                 $post = null;
1416                                 break;
1417                         case $this->ENTRIES_PATH:
1418                                 $wp_last_modified = mysql2date('D, d M Y H:i:s', get_lastpostmodified('GMT'), 0).' GMT';
1419                                 break;
1420                         default:
1421                                 return;
1422                 }
1423                 $wp_etag = md5($wp_last_modified);
1424                 @header("Last-Modified: $wp_last_modified");
1425                 @header("ETag: $wp_etag");
1426
1427                 // Support for Conditional GET
1428                 if (isset($_SERVER['HTTP_IF_NONE_MATCH']))
1429                         $client_etag = stripslashes($_SERVER['HTTP_IF_NONE_MATCH']);
1430                 else
1431                         $client_etag = false;
1432
1433                 $client_last_modified = trim( $_SERVER['HTTP_IF_MODIFIED_SINCE']);
1434                 // If string is empty, return 0. If not, attempt to parse into a timestamp
1435                 $client_modified_timestamp = $client_last_modified ? strtotime($client_last_modified) : 0;
1436
1437                 // Make a timestamp for our most recent modification...
1438                 $wp_modified_timestamp = strtotime($wp_last_modified);
1439
1440                 if ( ($client_last_modified && $client_etag) ?
1441                 (($client_modified_timestamp >= $wp_modified_timestamp) && ($client_etag == $wp_etag)) :
1442                 (($client_modified_timestamp >= $wp_modified_timestamp) || ($client_etag == $wp_etag)) ) {
1443                         status_header( 304 );
1444                         wp_die();
1445                 }
1446         }
1447
1448         /**
1449          * Convert RFC3339 time string to timestamp.
1450          *
1451          * @since 2.3.0
1452          *
1453          * @param string $str String to time.
1454          * @return bool|int false if format is incorrect.
1455          */
1456         function rfc3339_str2time($str) {
1457
1458                 $match = false;
1459                 if (!preg_match("/(\d{4}-\d{2}-\d{2})T(\d{2}\:\d{2}\:\d{2})\.?\d{0,3}(Z|[+-]+\d{2}\:\d{2})/", $str, $match))
1460                         return false;
1461
1462                 if ($match[3] == 'Z')
1463                         $match[3] = '+0000';
1464
1465                 return strtotime($match[1] . " " . $match[2] . " " . $match[3]);
1466         }
1467
1468         /**
1469          * Retrieve published time to display in XML.
1470          *
1471          * @since 2.3.0
1472          *
1473          * @param string $published Time string.
1474          * @return string
1475          */
1476         function get_publish_time($published) {
1477
1478                 $pubtime = $this->rfc3339_str2time($published);
1479
1480                 if (!$pubtime) {
1481                         return array(current_time('mysql'),current_time('mysql',1));
1482                 } else {
1483                         return array(date("Y-m-d H:i:s", $pubtime), gmdate("Y-m-d H:i:s", $pubtime));
1484                 }
1485         }
1486
1487 }