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