root/trunk/wp-app.php

Revision 1519, 39.0 kB (checked in by donncha, 6 days ago)

WP Merge

  • Property svn:eol-style set to native
Line 
1 <?php
2 /**
3  * Atom Publishing Protocol support for WordPress
4  *
5  * @author Original by Elias Torres <http://torrez.us/archives/2006/08/31/491/>
6  * @author Modified by Dougal Campbell <http://dougal.gunters.org/>
7  * @version 1.0.5-dc
8  */
9
10 /**
11  * WordPress is handling an Atom Publishing Protocol request.
12  *
13  * @var bool
14  */
15 define('APP_REQUEST', true);
16
17 /** Set up WordPress environment */
18 require_once('./wp-load.php');
19
20 /** Post Template API */
21 require_once(ABSPATH . WPINC . '/post-template.php');
22
23 /** Atom Publishing Protocol Class */
24 require_once(ABSPATH . WPINC . '/atomlib.php');
25
26 /** Feed Handling API */
27 require_once(ABSPATH . WPINC . '/feed.php');
28
29 $_SERVER['PATH_INFO'] = preg_replace( '/.*\/wp-app\.php/', '', $_SERVER['REQUEST_URI'] );
30
31 /**
32  * Whether to enable Atom Publishing Protocol Logging.
33  *
34  * @name app_logging
35  * @var int|bool
36  */
37 $app_logging = 0;
38
39 /**
40  * Whether to always authenticate user. Permanently set to true.
41  *
42  * @name always_authenticate
43  * @var int|bool
44  * @todo Should be an option somewhere
45  */
46 $always_authenticate = 1;
47
48 /**
49  * Writes logging info to a file.
50  *
51  * @since 2.2.0
52  * @uses $app_logging
53  * @package WordPress
54  * @subpackage Logging
55  *
56  * @param string $label Type of logging
57  * @param string $msg Information describing logging reason.
58  */
59 function log_app($label,$msg) {
60     global $app_logging;
61     if ($app_logging) {
62         $fp = fopen( 'wp-app.log', 'a+');
63         $date = gmdate( 'Y-m-d H:i:s' );
64         fwrite($fp, "\n\n$date - $label\n$msg\n");
65         fclose($fp);
66     }
67 }
68
69 if ( !function_exists('wp_set_current_user') ) :
70 /**
71  * @ignore
72  */
73 function wp_set_current_user($id, $name = '') {
74     global $current_user;
75
76     if ( isset($current_user) && ($id == $current_user->ID) )
77         return $current_user;
78
79     $current_user = new WP_User($id, $name);
80
81     return $current_user;
82 }
83 endif;
84
85 /**
86  * Filter to add more post statuses.
87  *
88  * @since 2.2.0
89  *
90  * @param string $where SQL statement to filter.
91  * @return string Filtered SQL statement with added post_status for where clause.
92  */
93 function wa_posts_where_include_drafts_filter($where) {
94     $where = str_replace("post_status = 'publish'","post_status = 'publish' OR post_status = 'future' OR post_status = 'draft' OR post_status = 'inherit'", $where);
95     return $where;
96
97 }
98 add_filter('posts_where', 'wa_posts_where_include_drafts_filter');
99
100 /**
101  * WordPress AtomPub API implementation.
102  *
103  * @package WordPress
104  * @subpackage Publishing
105  * @since 2.2.0
106  */
107 class AtomServer {
108
109     /**
110      * ATOM content type.
111      *
112      * @since 2.2.0
113      * @var string
114      */
115     var $ATOM_CONTENT_TYPE = 'application/atom+xml';
116
117     /**
118      * Categories ATOM content type.
119      *
120      * @since 2.2.0
121      * @var string
122      */
123     var $CATEGORIES_CONTENT_TYPE = 'application/atomcat+xml';
124
125     /**
126      * Service ATOM content type.
127      *
128      * @since 2.3.0
129      * @var string
130      */
131     var $SERVICE_CONTENT_TYPE = 'application/atomsvc+xml';
132
133     /**
134      * ATOM XML namespace.
135      *
136      * @since 2.3.0
137      * @var string
138      */
139     var $ATOM_NS = 'http://www.w3.org/2005/Atom';
140
141     /**
142      * ATOMPUB XML namespace.
143      *
144      * @since 2.3.0
145      * @var string
146      */
147     var $ATOMPUB_NS = 'http://www.w3.org/2007/app';
148
149     /**
150      * Entries path.
151      *
152      * @since 2.2.0
153      * @var string
154      */
155     var $ENTRIES_PATH = "posts";
156
157     /**
158      * Categories path.
159      *
160      * @since 2.2.0
161      * @var string
162      */
163     var $CATEGORIES_PATH = "categories";
164
165     /**
166      * Media path.
167      *
168      * @since 2.2.0
169      * @var string
170      */
171     var $MEDIA_PATH = "attachments";
172
173     /**
174      * Entry path.
175      *
176      * @since 2.2.0
177      * @var string
178      */
179     var $ENTRY_PATH = "post";
180
181     /**
182      * Service path.
183      *
184      * @since 2.2.0
185      * @var string
186      */
187     var $SERVICE_PATH = "service";
188
189     /**
190      * Media single path.
191      *
192      * @since 2.2.0
193      * @var string
194      */
195     var $MEDIA_SINGLE_PATH = "attachment";
196
197     /**
198      * ATOMPUB parameters.
199      *
200      * @since 2.2.0
201      * @var array
202      */
203     var $params = array();
204
205     /**
206      * Supported ATOMPUB media types.
207      *
208      * @since 2.3.0
209      * @var array
210      */
211     var $media_content_types = array('image/*','audio/*','video/*');
212
213     /**
214      * ATOMPUB content type(s).
215      *
216      * @since 2.2.0
217      * @var array
218      */
219     var $atom_content_types = array('application/atom+xml');
220
221     /**
222      * ATOMPUB methods.
223      *
224      * @since 2.2.0
225      * @var unknown_type
226      */
227     var $selectors = array();
228
229     /**
230      * Whether to do output.
231      *
232      * Support for head.
233      *
234      * @since 2.2.0
235      * @var bool
236      */
237     var $do_output = true;
238
239     /**
240      * PHP4 constructor - Sets up object properties.
241      *
242      * @since 2.2.0
243      * @return AtomServer
244      */
245     function AtomServer() {
246
247         $this->script_name = array_pop(explode('/',$_SERVER['SCRIPT_NAME']));
248         $this->app_base = get_bloginfo('url') . '/' . $this->script_name . '/';
249         if ( isset($_SERVER['HTTPS']) && strtolower($_SERVER['HTTPS']) == 'on' ) {
250             $this->app_base = preg_replace( '/^http:\/\//', 'https://', $this->app_base );
251         }
252
253         $this->selectors = array(
254             '@/service$@' =>
255                 array('GET' => 'get_service'),
256             '@/categories$@' =>
257                 array('GET' => 'get_categories_xml'),
258             '@/post/(\d+)$@' =>
259                 array('GET' => 'get_post',
260                         'PUT' => 'put_post',
261                         'DELETE' => 'delete_post'),
262             '@/posts/?(\d+)?$@' =>
263                 array('GET' => 'get_posts',
264                         'POST' => 'create_post'),
265             '@/attachments/?(\d+)?$@' =>
266                 array('GET' => 'get_attachment',
267                         'POST' => 'create_attachment'),
268             '@/attachment/file/(\d+)$@' =>
269                 array('GET' => 'get_file',
270                         'PUT' => 'put_file',
271                         'DELETE' => 'delete_file'),
272             '@/attachment/(\d+)$@' =>
273                 array('GET' => 'get_attachment',
274                         'PUT' => 'put_attachment',
275                         'DELETE' => 'delete_attachment'),
276         );
277     }
278
279     /**
280      * Handle ATOMPUB request.
281      *
282      * @since 2.2.0
283      */
284     function handle_request() {
285         global $always_authenticate;
286
287         if( !empty( $_SERVER['ORIG_PATH_INFO'] ) )
288             $path = $_SERVER['ORIG_PATH_INFO'];
289         else
290             $path = $_SERVER['PATH_INFO'];
291
292         $method = $_SERVER['REQUEST_METHOD'];
293
294         log_app('REQUEST',"$method $path\n================");
295
296         $this->process_conditionals();
297         //$this->process_conditionals();
298
299         // exception case for HEAD (treat exactly as GET, but don't output)
300         if($method == 'HEAD') {
301             $this->do_output = false;
302             $method = 'GET';
303         }
304
305         // redirect to /service in case no path is found.
306         if(strlen($path) == 0 || $path == '/') {
307             $this->redirect($this->get_service_url());
308         }
309
310         // check to see if AtomPub is enabled
311         if( !get_option( 'enable_app' ) )
312             $this->forbidden( sprintf( __( 'AtomPub services are disabled on this blog.  An admin user can enable them at %s' ), admin_url('options-writing.php') ) );
313
314         // dispatch
315         foreach($this->selectors as $regex => $funcs) {
316             if(preg_match($regex, $path, $matches)) {
317             if(isset($funcs[$method])) {
318
319                 // authenticate regardless of the operation and set the current
320                 // user. each handler will decide if auth is required or not.
321                 if(!$this->authenticate()) {
322                     if ($always_authenticate) {
323                         $this->auth_required('Credentials required.');
324                     }
325                 }
326
327                 array_shift($matches);
328                 call_user_func_array(array(&$this,$funcs[$method]), $matches);
329                 exit();
330             } else {
331                 // only allow what we have handlers for...
332                 $this->not_allowed(array_keys($funcs));
333             }
334             }
335         }
336
337         // oops, nothing found
338         $this->not_found();
339     }
340
341     /**
342      * Retrieve XML for ATOMPUB service.
343      *
344      * @since 2.2.0
345      */
346     function get_service() {
347         log_app('function','get_service()');
348
349         if( !current_user_can( 'edit_posts' ) )
350             $this->auth_required( __( 'Sorry, you do not have the right to access this blog.' ) );
351
352         $entries_url = attribute_escape($this->get_entries_url());
353         $categories_url = attribute_escape($this->get_categories_url());
354         $media_url = attribute_escape($this->get_attachments_url());
355         foreach ($this->media_content_types as $med) {
356             $accepted_media_types = $accepted_media_types . "<accept>" . $med . "</accept>";
357         }
358         $atom_prefix="atom";
359         $atom_blogname=get_bloginfo('name');
360         $service_doc = <<<EOD
361 <service xmlns="$this->ATOMPUB_NS" xmlns:$atom_prefix="$this->ATOM_NS">
362   <workspace>
363     <$atom_prefix:title>$atom_blogname Workspace</$atom_prefix:title>
364     <collection href="$entries_url">
365       <$atom_prefix:title>$atom_blogname Posts</$atom_prefix:title>
366       <accept>$this->ATOM_CONTENT_TYPE;type=entry</accept>
367       <categories href="$categories_url" />
368     </collection>
369     <collection href="$media_url">
370       <$atom_prefix:title>$atom_blogname Media</$atom_prefix:title>
371       $accepted_media_types
372     </collection>
373   </workspace>
374 </service>
375
376 EOD;
377
378         $this->output($service_doc, $this->SERVICE_CONTENT_TYPE);
379     }
380
381     /**
382      * Retrieve categories list in XML format.
383      *
384      * @since 2.2.0
385      */
386     function get_categories_xml() {
387         log_app('function','get_categories_xml()');
388
389         if( !current_user_can( 'edit_posts' ) )
390             $this->auth_required( __( 'Sorry, you do not have the right to access this blog.' ) );
391
392         $home = attribute_escape(get_bloginfo_rss('home'));
393
394         $categories = "";
395         $cats = get_categories("hierarchical=0&hide_empty=0");
396         foreach ((array) $cats as $cat) {
397             $categories .= "    <category term=\"" . attribute_escape($cat->name) .  "\" />\n";
398 }
399         $output = <<<EOD
400 <app:categories xmlns:app="$this->ATOMPUB_NS"
401     xmlns="$this->ATOM_NS"
402     fixed="yes" scheme="$home">
403     $categories
404 </app:categories>
405 EOD;
406     $this->output($output, $this->CATEGORIES_CONTENT_TYPE);
407 }
408
409     /**
410      * Create new post.
411      *
412      * @since 2.2.0
413      */
414     function create_post() {
415         global $blog_id, $user_ID;
416         $this->get_accepted_content_type($this->atom_content_types);
417
418         $parser = new AtomParser();
419         if(!$parser->parse()) {
420             $this->client_error();
421         }
422
423         $entry = array_pop($parser->feed->entries);
424
425         log_app('Received entry:', print_r($entry,true));
426
427         $catnames = array();
428         foreach($entry->categories as $cat)
429             array_push($catnames, $cat["term"]);
430
431         $wp_cats = get_categories(array('hide_empty' => false));
432
433         $post_category = array();
434
435         foreach($wp_cats as $cat) {
436             if(in_array($cat->name, $catnames))
437                 array_push($post_category, $cat->term_id);
438         }
439
440         $publish = (isset($entry->draft) && trim($entry->draft) == 'yes') ? false : true;
441
442         $cap = ($publish) ? 'publish_posts' : 'edit_posts';
443
444         if(!current_user_can($cap))
445             $this->auth_required(__('Sorry, you do not have the right to edit/publish new posts.'));
446
447         $blog_ID = (int ) $blog_id;
448         $post_status = ($publish) ? 'publish' : 'draft';
449         $post_author = (int) $user_ID;
450         $post_title = $entry->title[1];
451         $post_content = $entry->content[1];
452         $post_excerpt = $entry->summary[1];
453         $pubtimes = $this->get_publish_time($entry->published);
454         $post_date = $pubtimes[0];
455         $post_date_gmt = $pubtimes[1];
456
457         if ( isset( $_SERVER['HTTP_SLUG'] ) )
458             $post_name = $_SERVER['HTTP_SLUG'];
459
460         $post_data = compact('blog_ID', 'post_author', 'post_date', 'post_date_gmt', 'post_content', 'post_title', 'post_category', 'post_status', 'post_excerpt', 'post_name');
461
462         $this->escape($post_data);
463         log_app('Inserting Post. Data:', print_r($post_data,true));
464
465         $postID = wp_insert_post($post_data);
466         if ( is_wp_error( $postID ) )
467             $this->internal_error($postID->get_error_message());
468
469         if (!$postID)
470             $this->internal_error(__('Sorry, your entry could not be posted. Something wrong happened.'));
471
472         // getting warning here about unable to set headers
473         // because something in the cache is printing to the buffer
474         // could we clean up wp_set_post_categories or cache to not print
475         // this could affect our ability to send back the right headers
476         @wp_set_post_categories($postID, $post_category);
477
478         $output = $this->get_entry($postID);
479
480         log_app('function',"create_post($postID)");
481         $this->created($postID, $output);
482     }
483
484     /**
485      * Retrieve post.
486      *
487      * @since 2.2.0
488      *
489      * @param int $postID Post ID.
490      */
491     function get_post($postID) {
492         global $entry;
493
494         if( !current_user_can( 'edit_post', $postID ) )
495             $this->auth_required( __( 'Sorry, you do not have the right to access this post.' ) );
496
497         $this->set_current_entry($postID);
498         $output = $this->get_entry($postID);
499         log_app('function',"get_post($postID)");
500         $this->output($output);
501
502     }
503
504     /**
505      * Update post.
506      *
507      * @since 2.2.0
508      *
509      * @param int $postID Post ID.
510      */
511     function put_post($postID) {
512         // checked for valid content-types (atom+xml)
513         // quick check and exit
514         $this->get_accepted_content_type($this->atom_content_types);
515
516         $parser = new AtomParser();
517         if(!$parser->parse()) {
518             $this->bad_request();
519         }
520
521         $parsed = array_pop($parser->feed->entries);
522
523         log_app('Received UPDATED entry:', print_r($parsed,true));
524
525         // check for not found
526         global $entry;
527         $this->set_current_entry($postID);
528
529         if(!current_user_can('edit_post', $entry['ID']))
530             $this->auth_required(__('Sorry, you do not have the right to edit this post.'));
531
532         $publish = (isset($parsed->draft) && trim($parsed->draft) == 'yes') ? false : true;
533         $post_status = ($publish) ? 'publish' : 'draft';
534
535         extract($entry);
536
537         $post_title = $parsed->title[1];
538         $post_content = $parsed->content[1];
539         $post_excerpt = $parsed->summary[1];
540         $pubtimes = $this->get_publish_time($entry->published);
541         $post_date = $pubtimes[0];
542         $post_date_gmt = $pubtimes[1];
543         $pubtimes = $this->get_publish_time($parsed->updated);
544         $post_modified = $pubtimes[0];
545         $post_modified_gmt = $pubtimes[1];
546
547         $postdata = compact('ID', 'post_content', 'post_title', 'post_category', 'post_status', 'post_excerpt', 'post_date', 'post_date_gmt', 'post_modified', 'post_modified_gmt');
548         $this->escape($postdata);
549
550         $result = wp_update_post($postdata);
551
552         if (!$result) {
553             $this->internal_error(__('For some strange yet very annoying reason, this post could not be edited.'));
554         }
555
556         log_app('function',"put_post($postID)");
557         $this->ok();
558     }
559
560     /**
561      * Remove post.
562      *
563      * @since 2.2.0
564      *
565      * @param int $postID Post ID.
566      */
567     function delete_post($postID) {
568
569         // check for not found
570         global $entry;
571         $this->set_current_entry($postID);
572
573         if(!current_user_can('edit_post', $postID)) {
574             $this->auth_required(__('Sorry, you do not have the right to delete this post.'));
575         }
576
577         if ($entry['post_type'] == 'attachment') {
578             $this->delete_attachment($postID);
579         } else {
580             $result = wp_delete_post($postID);
581
582             if (!$result) {
583                 $this->internal_error(__('For some strange yet very annoying reason, this post could not be deleted.'));
584             }
585
586             log_app('function',"delete_post($postID)");
587             $this->ok();
588         }
589
590     }
591
592     /**
593      * Retrieve attachment.
594      *
595      * @since 2.2.0
596      *
597      * @param int $postID Optional. Post ID.
598      */
599     function get_attachment($postID = null) {
600         if( !