| 1 |
<?php
|
| 2 |
// $Id: subscribe.module,v 1.26 2006/11/11 05:05:31 jvandyk Exp $
|
| 3 |
|
| 4 |
/**
|
| 5 |
* @file
|
| 6 |
* Allow site to receive content via XML-RPC from Drupal sites running the publish module.
|
| 7 |
*/
|
| 8 |
|
| 9 |
/********************************************************************
|
| 10 |
* Drupal Hooks :: General Overview
|
| 11 |
********************************************************************/
|
| 12 |
|
| 13 |
/**
|
| 14 |
* Implementation of hook_help().
|
| 15 |
*/
|
| 16 |
function subscribe_help($section) {
|
| 17 |
switch ($section) {
|
| 18 |
case 'admin/modules#description':
|
| 19 |
return t('Allow site receive content from Drupal sites running the publish module.');
|
| 20 |
case strstr($section, 'admin/subscribe/pull'):
|
| 21 |
return t('<p>Manually request updated content from the channel. Note that there may be a long pause after you initiate content tranfer.</p>');
|
| 22 |
}
|
| 23 |
}
|
| 24 |
|
| 25 |
/**
|
| 26 |
* Implementation of hook_menu().
|
| 27 |
*/
|
| 28 |
function subscribe_menu($may_cache) {
|
| 29 |
$items = array();
|
| 30 |
$access = user_access('manage content subscriptions');
|
| 31 |
|
| 32 |
if ($may_cache) {
|
| 33 |
$items[] = array('path' => 'admin/subscribe', 'title' => t('subscribe'),
|
| 34 |
'callback' => 'subscribe_overview',
|
| 35 |
'access' => $access);
|
| 36 |
$items[] = array('path' => 'admin/subscribe/overview', 'title' => t('list'),
|
| 37 |
'type' => MENU_DEFAULT_LOCAL_TASK,
|
| 38 |
'weight' => -10);
|
| 39 |
$items[] = array('path' => 'admin/subscribe/sub/wizard', 'title' => t('subscribe to channel'),
|
| 40 |
'callback' => 'subscribe_url_form',
|
| 41 |
'access' => $access,
|
| 42 |
'type' => MENU_LOCAL_TASK);
|
| 43 |
$items[] = array('path' => 'admin/subscribe/sub/vocabmap', 'title' => t('Channel options'),
|
| 44 |
'callback' => 'subscribe_vocabmap_form',
|
| 45 |
'access' => $access,
|
| 46 |
'type' => MENU_CALLBACK);
|
| 47 |
$items[] = array('path' => 'admin/subscribe/delete', 'title' => t('Delete subscription'),
|
| 48 |
'callback' => 'subscribe_delete_form',
|
| 49 |
'access' => $access,
|
| 50 |
'type' => MENU_CALLBACK);
|
| 51 |
$items[] = array('path' => 'admin/subscribe/pull', 'title' => t('update content'),
|
| 52 |
'callback' => 'subscribe_pull_form',
|
| 53 |
'access' => $access,
|
| 54 |
'type' => MENU_CALLBACK);
|
| 55 |
}
|
| 56 |
|
| 57 |
return $items;
|
| 58 |
}
|
| 59 |
|
| 60 |
/**
|
| 61 |
* Implementation of hook_perm().
|
| 62 |
*/
|
| 63 |
function subscribe_perm() {
|
| 64 |
return array('manage content subscriptions');
|
| 65 |
}
|
| 66 |
|
| 67 |
/********************************************************************
|
| 68 |
* Drupal Hooks :: Core
|
| 69 |
********************************************************************/
|
| 70 |
|
| 71 |
/**
|
| 72 |
* Implementation of hook_xmlrpc().
|
| 73 |
*
|
| 74 |
* This is the server side of the xml-rpc request.
|
| 75 |
* Registering xml-rpc methods to callback functions.
|
| 76 |
*/
|
| 77 |
function subscribe_xmlrpc() {
|
| 78 |
return array('drupal.subscribe.receive' => 'subscribe_xmls_receive');
|
| 79 |
}
|
| 80 |
|
| 81 |
/**
|
| 82 |
* Implementation of hook_nodeapi().
|
| 83 |
*/
|
| 84 |
function subscribe_nodeapi(&$node, $op, $teaser = NULL, $page = NULL) {
|
| 85 |
switch ($op) {
|
| 86 |
case 'load':
|
| 87 |
if ($remote = subscribe_node_load($node->nid)) {
|
| 88 |
foreach ($remote as $key => $value) {
|
| 89 |
$node->$key = $value;
|
| 90 |
}
|
| 91 |
}
|
| 92 |
break;
|
| 93 |
|
| 94 |
case 'view':
|
| 95 |
if (isset($node->remote_nid)) {
|
| 96 |
$node = theme_external_node($node, 0, 1);
|
| 97 |
}
|
| 98 |
break;
|
| 99 |
|
| 100 |
case 'delete':
|
| 101 |
if (isset($node->remote_nid) && isset($node->sid)) {
|
| 102 |
return db_query('DELETE FROM {subscribe_node} WHERE remote_nid = %d AND sid = %d', $node->remote_nid, $node->sid);
|
| 103 |
}
|
| 104 |
break;
|
| 105 |
}
|
| 106 |
}
|
| 107 |
|
| 108 |
/**
|
| 109 |
* Return the remote metadata for a node that's been imported.
|
| 110 |
*
|
| 111 |
* @param $nid
|
| 112 |
* the local nid of node
|
| 113 |
*/
|
| 114 |
function subscribe_node_load($nid) {
|
| 115 |
return db_fetch_object(db_query('SELECT sn.remote_nid, sn.remote_uid, sn.remote_author, sn.sid, ss.base_url as remote_site_url, ss.name as remote_site FROM {subscribe_node} sn INNER JOIN {subscribe_subscriptions} ss ON ss.sid = sn.sid WHERE sn.nid = %d', $nid));
|
| 116 |
|
| 117 |
}
|
| 118 |
|
| 119 |
/**
|
| 120 |
* Theme function to control the look and feel of an external node.
|
| 121 |
*/
|
| 122 |
function theme_external_node($node, $teaser = 0, $page = 0) {
|
| 123 |
$node->title = t('%node-title (from %remote-url)', array('%node-title' => $node->title, '%remote-url' => $node->remote_site_url));
|
| 124 |
|
| 125 |
$remote_url = $node->remote_site_url. '/';
|
| 126 |
$item[] = t('<a href="%remote-url">Visit this page</a>', array('%remote-url' => $remote_url. "node/$node->remote_nid"));
|
| 127 |
$item[] = t('Authored by: <a href="%remote-author-url">%remote-author</a>', array('%remote-author' => $node->remote_author, '%remote-author-url' => $remote_url. "user/$node->remote_uid"));
|
| 128 |
|
| 129 |
$referring_info = theme('item_list', $item);
|
| 130 |
|
| 131 |
$node->body .= $referring_info;
|
| 132 |
$node->teaser .= $referring_info;
|
| 133 |
|
| 134 |
return $node;
|
| 135 |
}
|
| 136 |
|
| 137 |
/********************************************************************
|
| 138 |
* Module Functions :: Subscribing
|
| 139 |
********************************************************************/
|
| 140 |
|
| 141 |
/**
|
| 142 |
*
|
| 143 |
* Display all subscriptions.
|
| 144 |
*/
|
| 145 |
function subscribe_overview() {
|
| 146 |
$output = '<h3>'. t('Subscribed to:') .'</h3>';
|
| 147 |
$header = array(t('Name'),t('URI'), t('Last Received'), t('Operations'));
|
| 148 |
$result = db_query('SELECT * FROM {subscribe_subscriptions}');
|
| 149 |
$row = array();
|
| 150 |
while ($sub = db_fetch_object($result)) {
|
| 151 |
$row[] = array(
|
| 152 |
$sub->name,
|
| 153 |
$sub->url,
|
| 154 |
$sub->last_update ? format_date($sub->last_update) : t('never'),
|
| 155 |
'<table><tr><td>' .
|
| 156 |
// l(t('edit'), "admin/subscribe/sub/$sub->sid/edit") . '</td><td>' .
|
| 157 |
l(t('pull'), "admin/subscribe/pull/$sub->sid") . '</td><td>' .
|
| 158 |
l(t('delete'), "admin/subscribe/delete/$sub->sid") . '</td></tr></table>'
|
| 159 |
);
|
| 160 |
}
|
| 161 |
$output .= ($row) ? theme('table', $header, $row) : '<p>' . t('You currently have no subscriptions.') . '</p><p>' . t('Would you like to') . ' ' . l(t('subscribe to a channel'),'admin/subscribe/sub/wizard') . '?</p>';
|
| 162 |
|
| 163 |
return $output;
|
| 164 |
}
|
| 165 |
|
| 166 |
function subscribe_delete_form() {
|
| 167 |
$sid = arg(3);
|
| 168 |
$sub = subscribe_subscription_load($sid);
|
| 169 |
if (!$sid || !$sub->sid) {
|
| 170 |
drupal_set_message(t('Invalid channel ID.', 'error'));
|
| 171 |
drupal_goto('admin/subscribe');
|
| 172 |
}
|
| 173 |
$form['sid'] = array('#type' => 'value', '#value' => $sid);
|
| 174 |
$output = confirm_form('subscribe_delete_form', $form,
|
| 175 |
t('Are you sure you want to delete %title?', array('%title' => theme('placeholder', $sub->name))),
|
| 176 |
'admin/subscribe', t('This action cannot be undone.'),
|
| 177 |
t('Delete'), t('Cancel') );
|
| 178 |
return $output;
|
| 179 |
}
|
| 180 |
|
| 181 |
function subscribe_delete_form_submit($form, $form_values) {
|
| 182 |
if ($form_values['confirm']) {
|
| 183 |
subscribe_subscription_delete($form_values['sid']);
|
| 184 |
}
|
| 185 |
return 'admin/subscribe';
|
| 186 |
}
|
| 187 |
|
| 188 |
function subscribe_delete($edit) {
|
| 189 |
$sub = subscribe_subscription_load($edit['sid']);
|
| 190 |
if ($edit['confirm']) {
|
| 191 |
global $user;
|
| 192 |
db_query('DELETE FROM {subscribe_subscriptions} WHERE sid = %d', $sub->sid);
|
| 193 |
watchdog('subscribe', t('%u: deleted %n', array('%u' => $user->name, '%n' => $sub->name)));
|
| 194 |
$result = subscribe_xmlc_cancel_subscription($sub);
|
| 195 |
}
|
| 196 |
else {
|
| 197 |
$extra = form_hidden('sid', $sub->sid);
|
| 198 |
$output = theme('confirm',
|
| 199 |
t('Are you sure you want to delete %title?', array('%title' => '<em>'. $sub->name .'</em>')),
|
| 200 |
$_GET['destination'] ? $_GET['destination'] : 'admin/subscribe',
|
| 201 |
t('This action cannot be undone. Nodes imported from this channel will not be deleted.'),
|
| 202 |
t('Delete'),
|
| 203 |
t('Cancel'),
|
| 204 |
$extra);
|
| 205 |
}
|
| 206 |
|
| 207 |
return $output;
|
| 208 |
}
|
| 209 |
|
| 210 |
function subscribe_url_form() {
|
| 211 |
// this form is a great opportunity for AJAXification
|
| 212 |
$edit = isset($_SESSION['edit']) ? $_SESSION['edit'] : array();
|
| 213 |
$form = array();
|
| 214 |
$form['channel'] = array(
|
| 215 |
'#type' => 'fieldset',
|
| 216 |
'#title' => 'Subscribe to channel'
|
| 217 |
);
|
| 218 |
$form['channel']['url'] = array(
|
| 219 |
'#type' => 'textfield',
|
| 220 |
'#title' => t('URL of site including XML-RPC endpoint'),
|
| 221 |
'#size' => '70',
|
| 222 |
'#maxlength' => '255',
|
| 223 |
'#description' => t('E.g. http://example.com/xmlrpc.php'),
|
| 224 |
'#default_value' => isset($edit['url']) ? $edit['url'] : 'http://'
|
| 225 |
);
|
| 226 |
$form['channel']['channel']['channel_id'] = array(
|
| 227 |
'#type' => 'textfield',
|
| 228 |
'#title' => 'Channel identifier',
|
| 229 |
'#size' => '10',
|
| 230 |
'#maxlength' => '255',
|
| 231 |
'#description' => t('Typically an integer corresponding to the channel ID'),
|
| 232 |
'#default_value' => isset($edit['channel_id']) ? $edit['channel_id'] : ''
|
| 233 |
);
|
| 234 |
$form['channel']['username'] = array(
|
| 235 |
'#type' => 'textfield',
|
| 236 |
'#title' => t('Username'),
|
| 237 |
'#size' => '10',
|
| 238 |
'#maxlength' => '60',
|
| 239 |
'#default_value' => isset($edit['username']) ? $edit['username'] : ''
|
| 240 |
);
|
| 241 |
$form['channel']['pass'] = array(
|
| 242 |
'#type' => 'password',
|
| 243 |
'#title' => t('Password'),
|
| 244 |
'#size' => '10',
|
| 245 |
'#maxlength' => '32'
|
| 246 |
);
|
| 247 |
$form['submit'] = array(
|
| 248 |
'#type' => 'submit',
|
| 249 |
'#value' => t('Continue')
|
| 250 |
);
|
| 251 |
unset($_SESSION['edit']);
|
| 252 |
return drupal_get_form('subscribe_url_form', $form);
|
| 253 |
}
|
| 254 |
|
| 255 |
/**
|
| 256 |
* Validator for initial subscription form.
|
| 257 |
*/
|
| 258 |
function subscribe_url_form_validate($form_id, $form_values) {
|
| 259 |
if ($form_values['url'] == '') {
|
| 260 |
form_set_error('url', t('You must specify the URL of the channel.'));
|
| 261 |
}
|
| 262 |
elseif (!valid_url($form_values['url'], TRUE)) {
|
| 263 |
form_set_error('url', t('That is not a valid URL.'));
|
| 264 |
}
|
| 265 |
if (form_get_errors()) {
|
| 266 |
$_SESSION['edit'] = $form_values;
|
| 267 |
}
|
| 268 |
}
|
| 269 |
|
| 270 |
function subscribe_url_form_submit($form_id, $form_values) {
|
| 271 |
$url = $form_values['url'];
|
| 272 |
$channel_id = check_plain($form_values['channel_id']);
|
| 273 |
$username = check_plain($form_values['username']);
|
| 274 |
$pass = check_plain($form_values['pass']);
|
| 275 |
$result = subscribe_xmlc_may_subscribe($url, $channel_id, $username, $pass);
|
| 276 |
if ($result[0]) { // if user may subscribe
|
| 277 |
$form_values = array();
|
| 278 |
$form_values['name'] = $result[1];
|
| 279 |
$form_values['node_types'] = $result[2];
|
| 280 |
$form_values['vocabularies'] = $result[3];
|
| 281 |
$form_values['base_url'] = $result[4];
|
| 282 |
$form_values['channel_id'] = $channel_id;
|
| 283 |
$form_values['username'] = $username;
|
| 284 |
$form_values['pass'] = $pass;
|
| 285 |
$form_values['url'] = $url;
|
| 286 |
|
| 287 |
// now that we have a response we can do preliminary validation
|
| 288 |
|
| 289 |
if (!$form_values['node_types']) {
|
| 290 |
drupal_set_message(t('The channel is not currently publishing any node types. This is probably a misconfiguration on the publishing site.'), 'error');
|
| 291 |
drupal_goto('admin/subscribe/sub/wizard');
|
| 292 |
}
|
| 293 |
|
| 294 |
$nodetypes = node_get_types();
|
| 295 |
$missing = array();
|
| 296 |
foreach (array_keys($form_values['node_types']) as $type) {
|
| 297 |
if (!key_exists($type, $nodetypes)) {
|
| 298 |
$missing[] = $type;
|
| 299 |
}
|
| 300 |
}
|
| 301 |
if ($missing) {
|
| 302 |
if (count($missing) == 1) {
|
| 303 |
$type = $missing[0];
|
| 304 |
drupal_set_message(t('The remote site is publishing nodes of type %type but you do not support nodes of this type on your site. Please enable the appropriate module.', array('%type' => theme('placeholder', $type))), 'error');
|
| 305 |
}
|
| 306 |
else {
|
| 307 |
$phrase = '';
|
| 308 |
foreach ($missing as $type) {
|
| 309 |
$phrase .= ', ' . $type;
|
| 310 |
}
|
| 311 |
drupal_set_message(t('The remote site is publishing nodes of the following types: %types; but you do not support nodes of these types on your site. Please enable the appropriate modules.', array('%types' => theme('placeholder', ltrim($phrase, ',')), 'error')));
|
| 312 |
}
|
| 313 |
drupal_goto('admin/subscribe/sub/wizard');
|
| 314 |
}
|
| 315 |
}
|
| 316 |
else {
|
| 317 |
$msg = t('Unable to subscribe.');
|
| 318 |
drupal_set_message($msg, 'error');
|
| 319 |
$_SESSION['edit'] = $form_values;
|
| 320 |
drupal_goto('admin/subscribe/sub/wizard');
|
| 321 |
}
|
| 322 |
$_SESSION['edit'] = $form_values;
|
| 323 |
return ('admin/subscribe/sub/vocabmap');
|
| 324 |
}
|
| 325 |
|
| 326 |
|
| 327 |
/**
|
| 328 |
* The form for mapping vocabularies from remote vocabularies to local vocabularies
|
| 329 |
* if the publishing site is publishing vocabularies; also sets push/pull.
|
| 330 |
*/
|
| 331 |
function subscribe_vocabmap_form() {
|
| 332 |
if (!isset($_SESSION['edit'])) {
|
| 333 |
drupal_set_message(t('Unable to retrieve session data.'), 'error');
|
| 334 |
drupal_goto('/subscribe/sub/wizard');
|
| 335 |
}
|
| 336 |
$edit = $_SESSION['edit'];
|
| 337 |
$remote_vocabularies = array();
|
| 338 |
|
| 339 |
foreach ($edit['vocabularies'] as $vid => $va) {
|
| 340 |
$remote_vocabularies[$vid] = (object)$va;
|
| 341 |
}
|
| 342 |
|
| 343 |
$vocabularies = taxonomy_get_vocabularies();
|
| 344 |
$voc_names = array();
|
| 345 |
$voc_options = array();
|
| 346 |
if (!$vocabularies && $remote_vocabularies) {
|
| 347 |
drupal_set_message(t('In order to map remote vocabularies to local vocabularies, you must first create your local vocabularies using administer > categories.'), 'error');
|
| 348 |
}
|
| 349 |
|
| 350 |
foreach ($vocabularies as $v) {
|
| 351 |
$voc_names[$v->name] = $v->vid;
|
| 352 |
$voc_options[$v->vid] = $v->name;
|
| 353 |
}
|
| 354 |
$voc_options['0'] = t('discard');
|
| 355 |
|
| 356 |
// lots of error checking needs to go here to make sure
|
| 357 |
// the local vocabulary select box only includes vocabularies
|
| 358 |
// whose attributes match the remote vocabularies
|
| 359 |
|
| 360 |
$form = array();
|
| 361 |
$form['vocabmap'] = array();
|
| 362 |
|
| 363 |
foreach ($remote_vocabularies as $vid => $v) {
|
| 364 |
// autoselect
|
| 365 |
$selected_voc = '';
|
| 366 |
if (isset($voc_names[$v->name])) {
|
| 367 |
$selected_voc = $voc_names[$v->name];
|
| 368 |
}
|
| 369 |
|
| 370 |
$form['vocabmap']['localvoc' . $vid] = array(
|
| 371 |
'#type' => 'select',
|
| 372 |
'#options' => $voc_options,
|
| 373 |
'#default_value' => $selected_voc,
|
| 374 |
'#vname' => $v->name
|
| 375 |
);
|
| 376 |
}
|
| 377 |
|
| 378 |
$form['receiving'] = array(
|
| 379 |
'#type' => 'fieldset',
|
| 380 |
'#title' => t('Receiving Content')
|
| 381 |
);
|
| 382 |
$form['receiving']['pushpull'] = array(
|
| 383 |
'#type' => 'radios',
|
| 384 |
'#title' => t('Method'),
|
| 385 |
'#default_value' => isset($edit['pushpull']) ? $edit['pushpull'] : 0,
|
| 386 |
'#options' => array(t('Push: Receive content from channel automatically when the channel decides to send it.'), t('Pull: Receive content from channel only when you manually request it (your server must allow HTTP out).')),
|
| 387 |
'#description' => t('Choose how you would like this site to receive content.')
|
| 388 |
);
|
| 389 |
$form['submit'] = array(
|
| 390 |
'#type' => 'submit',
|
| 391 |
'#value' => t('Subscribe to channel')
|
| 392 |
);
|
| 393 |
|
| 394 |
return drupal_get_form('subscribe_vocabmap_form', $form);
|
| 395 |
}
|
| 396 |
|
| 397 |
function theme_subscribe_vocabmap_form($form) {
|
| 398 |
$header = array(t('Remote Vocabulary'), '', t('Local Vocabulary'));
|
| 399 |
$rows = array();
|
| 400 |
foreach (element_children($form['vocabmap']) as $key) {
|
| 401 |
$rows[] = array(
|
| 402 |
array('data' => $form['vocabmap'][$key]['#vname']),
|
| 403 |
array('data' => '→'),
|
| 404 |
array('data' => form_render($form['vocabmap'][$key]))
|
| 405 |
);
|
| 406 |
}
|
| 407 |
if ($rows) {
|
| 408 |
$output = theme('fieldset', array('#title' => t('Vocabulary Mapping'), '#children' => theme_table($header, $rows)));
|
| 409 |
}
|
| 410 |
$output .= form_render($form);
|
| 411 |
return $output;
|
| 412 |
}
|
| 413 |
|
| 414 |
function subscribe_vocabmap_form_validate($form, $form_values) {
|
| 415 |
if (!isset($_SESSION['edit'])) {
|
| 416 |
drupal_set_message(t('Unable to retrieve session data.'), 'error');
|
| 417 |
drupal_goto('/subscribe/sub/wizard');
|
| 418 |
}
|
| 419 |
$edit = $_SESSION['edit'];
|
| 420 |
if (!subscribe_subscription_validate($edit['url'], $edit['channel_id'])) {
|
| 421 |
drupal_set_message(t('You are already subscribed to this channel.'), 'error');
|
| 422 |
drupal_goto('admin/subscribe');
|
| 423 |
}
|
| 424 |
}
|
| 425 |
|
| 426 |
function subscribe_vocabmap_form_submit($form, $edit) {
|
| 427 |
if (!isset($_SESSION['edit'])) {
|
| 428 |
drupal_set_message(t('Unable to retrieve session data.'), 'error');
|
| 429 |
drupal_goto('/subscribe/sub/wizard');
|
| 430 |
}
|
| 431 |
$edit = array_merge($_SESSION['edit'], $edit);
|
| 432 |
unset($_SESSION['edit']);
|
| 433 |
$edit['channel_id'] = check_plain($edit['channel_id']);
|
| 434 |
$edit['username'] = check_plain($edit['username']);
|
| 435 |
$edit['pass'] = check_plain($edit['pass']);
|
| 436 |
|
| 437 |
$remote_vids = array();
|
| 438 |
$local_vids = array();
|
| 439 |
$vocab_map = _subscribe_extract_vocab_map($edit);
|
| 440 |
foreach ($vocab_map as $rvid => $local_vid) {
|
| 441 |
if ($local_vid != 0) {
|
| 442 |
$remote_vids[] = $rvid;
|
| 443 |
$local_vids[] = $local_vid;
|
| 444 |
}
|
| 445 |
}
|
| 446 |
|
| 447 |
$result = subscribe_xmlc_subscribe($edit['url'], $edit['channel_id'], $edit['username'], $edit['pass'], $edit['pushpull'], $local_vids, $remote_vids, array());
|
| 448 |
if ($result['status'] = 'ok') {
|
| 449 |
$edit['rsid'] = $result['sid'];
|
| 450 |
$edit['method'] = $edit['pushpull'];
|
| 451 |
$edit['sid'] = subscribe_subscription_save($edit);
|
| 452 |
subscribe_vocabmap_save($vocab_map, $edit['sid']);
|
| 453 |
drupal_set_message(t('Subscription to channel %name established.', array('%name' => theme('placeholder', $edit['name']))));
|
| 454 |
drupal_goto('admin/subscribe');
|
| 455 |
}
|
| 456 |
else {
|
| 457 |
drupal_set_message(t('Could not subscribe.'), 'error');
|
| 458 |
drupal_goto('admin/subscribe');
|
| 459 |
}
|
| 460 |
}
|
| 461 |
|
| 462 |
/**
|
| 463 |
* Save local-remote vocabulary mapping.
|
| 464 |
*
|
| 465 |
* @param $local_vocabularies
|
| 466 |
* Array which must contain local/remote mappings
|
| 467 |
* encoded as e.g., $edit['localvoc2'] = 4.
|
| 468 |
* @param $sid subscription id
|
| 469 |
*
|
| 470 |
* @return
|
| 471 |
*
|
| 472 |
*/
|
| 473 |
function subscribe_vocabmap_save($local_vocabularies, $sid) {
|
| 474 |
db_query("DELETE FROM {subscribe_vocab_map} WHERE sid = '%s'", $sid);
|
| 475 |
foreach ($local_vocabularies as $lvid => $rvid) {
|
| 476 |
db_query("INSERT INTO {subscribe_vocab_map} (sid, remote_vid, local_vid) VALUES (%d, %d, %d)", $sid, $rvid, $lvid);
|
| 477 |
}
|
| 478 |
}
|
| 479 |
|
| 480 |
/**
|
| 481 |
* Generate a mapping of local vocabularies for a subscription
|
| 482 |
* keyed by remote vocabulary ID: $vocabmap[3] = 4 means that
|
| 483 |
* vocabulary 4 on the remote site maps to vocabulary 3 on this site.
|
| 484 |
*
|
| 485 |
* @param $sid
|
| 486 |
* the local subscription ID
|
| 487 |
*/
|
| 488 |
function subscribe_get_vocabmap($sid) {
|
| 489 |
$vocabmap = array();
|
| 490 |
$result = db_query("SELECT * FROM {subscribe_vocab_map} WHERE sid = %d", $sid);
|
| 491 |
while ($data = db_fetch_object($result)) {
|
| 492 |
$vocabmap[$data->remote_vid] = taxonomy_get_vocabulary($data->local_vid);
|
| 493 |
}
|
| 494 |
|
| 495 |
return $vocabmap;
|
| 496 |
}
|
| 497 |
|
| 498 |
|
| 499 |
/**
|
| 500 |
* Using the URL of the channel, determine whether we already
|
| 501 |
* have a subscription to this channel.
|
| 502 |
*
|
| 503 |
* @param $url
|
| 504 |
* the URL of the channel
|
| 505 |
* @param $channel_id
|
| 506 |
* the id of the channel
|
| 507 |
*/
|
| 508 |
function subscribe_subscription_validate($url, $channel_id) {
|
| 509 |
$result = db_query("SELECT * FROM {subscribe_subscriptions} WHERE url = '%s' AND channel_id = %d", $url, $channel_id);
|
| 510 |
$data = db_fetch_object($result);
|
| 511 |
return !$data;
|
| 512 |
}
|
| 513 |
|
| 514 |
/**
|
| 515 |
* Return a subscription object representing a database row for a given sid.
|
| 516 |
*
|
| 517 |
* @param $sid
|
| 518 |
* the local ID of the subscription
|
| 519 |
*/
|
| 520 |
function subscribe_subscription_load($sid) {
|
| 521 |
return db_fetch_object(db_query('SELECT * FROM {subscribe_subscriptions} WHERE sid = %d', $sid));
|
| 522 |
}
|
| 523 |
|
| 524 |
/**
|
| 525 |
* Save a subscription to a channel to the database.
|
| 526 |
*
|
| 527 |
* @param $edit
|
| 528 |
* array from form;
|
| 529 |
*/
|
| 530 |
function subscribe_subscription_save($edit) {
|
| 531 |
global $base_url;
|
| 532 |
global $user;
|
| 533 |
$fields = _subscribe_db_fields('subscribe_subscriptions');
|
| 534 |
// sid rsid name base_url url channel_id domain username pass token method last_update
|
| 535 |
|
| 536 |
$parts = parse_url($edit['url']);
|
| 537 |
$edit['domain'] = $parts['host'];
|
| 538 |
|
| 539 |
if ($edit['sid'] && db_result(db_query('SELECT COUNT(sid) FROM {subscribe_subscriptions} WHERE sid = %d', $edit['sid']))) { // existing subscription
|
| 540 |
// Prepare the query:
|
| 541 |
foreach ($edit as $key => $value) {
|
| 542 |
if (in_array($key, $fields)) {
|
| 543 |
$q[] = db_escape_string($key) ." = '%s'";
|
| 544 |
$v[] = $value;
|
| 545 |
}
|
| 546 |
}
|
| 547 |
db_query('UPDATE {subscribe_subscriptions} SET '. implode(', ', $q) .' WHERE sid = '. db_escape_string($edit['sid']), $v);
|
| 548 |
}
|
| 549 |
else { // new subscription
|
| 550 |
$edit['sid'] = db_next_id('subscribe');
|
| 551 |
$edit['token'] = md5($edit['username'] . $edit['pass'] . $base_url);
|
| 552 |
// Prepare the query:
|
| 553 |
foreach ($edit as $key => $value) {
|
| 554 |
if (in_array((string) $key, $fields)) {
|
| 555 |
$k[] = db_escape_string($key);
|
| 556 |
$v[] = $value;
|
| 557 |
$s[] = "'%s'";
|
| 558 |
}
|
| 559 |
}
|
| 560 |
// Insert the subscription record into the database:
|
| 561 |
db_query('INSERT INTO {subscribe_subscriptions} ('. implode(", ", $k) .') VALUES('. implode(', ', $s) .')', $v);
|
| 562 |
watchdog('subscribe', t('%u subscribed to channel %c', array('%u' => $user->name, '%c' => $edit['name'])));
|
| 563 |
}
|
| 564 |
|
| 565 |
return $edit['sid'];
|
| 566 |
}
|
| 567 |
|
| 568 |
function subscribe_subscription_delete($sid) {
|
| 569 |
global $user;
|
| 570 |
$sub = subscribe_subscription_load($sid);
|
| 571 |
db_query('DELETE FROM {subscribe_subscriptions} WHERE sid = %d', $sid);
|
| 572 |
db_query('DELETE FROM {subscribe_vocab_map WHERE sid = %d', $sid);
|
| 573 |
watchdog('subscribe', t('%u: deleted subscription %n', array('%u' => $user->name, '%n' => $sub->name)));
|
| 574 |
$result = subscribe_xmlc_cancel_subscription($sub);
|
| 575 |
}
|
| 576 |
|
| 577 |
/**
|
| 578 |
* Present the form where a user can request to receive updated data
|
| 579 |
* from a channel.
|
| 580 |
*
|
| 581 |
* @param $sid
|
| 582 |
* the local subscription ID
|
| 583 |
*/
|
| 584 |
function subscribe_pull_form() {
|
| 585 |
|
| 586 |
$sid = arg(3);
|
| 587 |
$sub = subscribe_subscription_load($sid);
|
| 588 |
if (!$sid || !$sub->sid) {
|
| 589 |
drupal_set_message(t('Invalid channel ID.', 'error'));
|
| 590 |
drupal_goto('admin/subscribe');
|
| 591 |
}
|
| 592 |
$form['sid'] = array(
|
| 593 |
'#type' => 'value',
|
| 594 |
'#value' => $sid
|
| 595 |
);
|
| 596 |
$form['update'] = array(
|
| 597 |
'#type' => 'fieldset',
|
| 598 |
'#title' => t('Request content from channel %channel', array('%channel' => theme('placeholder', $sub->name)))
|
| 599 |
);
|
| 600 |
$form['update']['submit'] = array('#type' => 'submit',
|
| 601 |
'#value' => t('Update Content')
|
| 602 |
);
|
| 603 |
|
| 604 |
return drupal_get_form('subscribe_pull_form', $form);
|
| 605 |
}
|
| 606 |
|
| 607 |
function subscribe_pull_form_submit($form, $form_values) {
|
| 608 |
if ($form_values['sid']) {
|
| 609 |
$sub = subscribe_subscription_load($form_values['sid']);
|
| 610 |
subscribe_pull($sub);
|
| 611 |
}
|
| 612 |
return 'admin/subscribe';
|
| 613 |
}
|
| 614 |
|
| 615 |
/**
|
| 616 |
* Used to invoke content transfer at subscribing site's request.
|
| 617 |
*
|
| 618 |
* @param $subscription
|
| 619 |
* a subscription object, e.g. as returned by subscribe_subscription_load()
|
| 620 |
* @param $cond
|
| 621 |
* an array of triplets to constrain the results (see publish_publish())
|
| 622 |
*/
|
| 623 |
function subscribe_pull($subscription, $cond = array()) {
|
| 624 |
$changed_filter_present = FALSE;
|
| 625 |
foreach ($cond as $triplet) {
|
| 626 |
if ($triplet[0] == 'changed') {
|
| 627 |
$changed_filter_present = TRUE;
|
| 628 |
}
|
| 629 |
}
|
| 630 |
|
| 631 |
if (!$changed_filter_present) {
|
| 632 |
$cond[] = array('changed', '>', $subscription->last_update);
|
| 633 |
}
|
| 634 |
|
| 635 |
$result = subscribe_xmlc_pull($subscription->url, $subscription->channel_id, $subscription->username, $subscription->pass, $subscription->rsid, $cond);
|
| 636 |
if (!$result) {
|
| 637 |
$error = xmlrpc_error_msg();
|
| 638 |
drupal_set_message($error, 'error');
|
| 639 |
}
|
| 640 |
if ($result['count']) {
|
| 641 |
$site_metadata = array('site_url' => $result['site_url'], 'site_name' => $result['site_name'], 'sid' => $subscription->sid);
|
| 642 |
list($n_counter, $u_counter, $t_counter) = subscribe_import($result['nodes'], $site_metadata);
|
| 643 |
drupal_set_message(t("New content from %name. Nodes imported: %ncount; nodes updated: %ucount; terms imported: %tcount.", array('%ncount' => $n_counter, '%ucount' => $u_counter, '%tcount' => $t_counter, '%name' => $subscription->name)));
|
| 644 |
db_query("UPDATE {subscribe_subscriptions} SET last_update = %d WHERE sid = %d", time(), $subscription->sid);
|
| 645 |
drupal_goto('admin/subscribe');
|
| 646 |
}
|
| 647 |
else {
|
| 648 |
drupal_set_message(t('Pull attempt failed for %url.', array('%url' => theme('placeholder', check_plain($subscription->url)))), 'error');
|
| 649 |
}
|
| 650 |
}
|
| 651 |
|
| 652 |
/********************************************************************
|
| 653 |
* Module Functions :: Node Importation
|
| 654 |
********************************************************************/
|
| 655 |
|
| 656 |
/**
|
| 657 |
* Import nodes into this Drupal site.
|
| 658 |
*
|
| 659 |
* @param $nodes
|
| 660 |
* an array of nodes, typically from a remote site
|
| 661 |
* @param $site_metadata
|
| 662 |
* url and name of remote site and local subscription ID
|
| 663 |
* corresponding to the channel of the remote site
|
| 664 |
*
|
| 665 |
* @return an array of count of nodes imported, count of taxonomy terms imported
|
| 666 |
*/
|
| 667 |
function subscribe_import($nodes, $site_metadata) {
|
| 668 |
|
| 669 |
// required metadata
|
| 670 |
$site_url = $site_metadata['site_url'];
|
| 671 |
$site_name = $site_metadata['site_name'];
|
| 672 |
$sid = $site_metadata['sid'];
|
| 673 |
|
| 674 |
$vocab_map = subscribe_get_vocabmap($sid);
|
| 675 |
$n_counter = 0;
|
| 676 |
$u_counter = 0;
|
| 677 |
$t_counter = 0;
|
| 678 |
|
| 679 |
foreach ($nodes as $n) {
|
| 680 |
$node = (object)$n;
|
| 681 |
|
| 682 |
// get "external" information
|
| 683 |
$remote_url = $node->url;
|
| 684 |
$remote_nid = $node->nid;
|
| 685 |
$remote_terms = $node->taxonomy_terms;
|
| 686 |
$remote_uid = $node->uid;
|
| 687 |
$remote_author = $node->name;
|
| 688 |
|
| 689 |
// discard remote taxonomy
|
| 690 |
unset($node->taxonomy);
|
| 691 |
|
| 692 |
// validate or map user
|
| 693 |
// currently we just assign to uid 1
|
| 694 |
$node->uid = '1';
|
| 695 |
|
| 696 |
// do we already have a copy of this node?
|
| 697 |
$local_nid = db_result(db_query("SELECT nid FROM {subscribe_node} WHERE remote_nid = %d AND sid = %d", $node->nid, $sid));
|
| 698 |
|
| 699 |
if ($local_nid) {
|
| 700 |
// substitute the local nid for the remote nid
|
| 701 |
$node->nid = $local_nid;
|
| 702 |
|
| 703 |
// we assign the revision ID of the old local node to the incoming, updated node
|
| 704 |
$local_node = node_load($local_nid);
|
| 705 |
$node->vid = $local_node->vid;
|
| 706 |
|
| 707 |
node_save($node);
|
| 708 |
$u_counter = $u_counter + 1;
|
| 709 |
|
| 710 |
// will terms be orphaned now if remote terms have changed?
|
| 711 |
}
|
| 712 |
else {
|
| 713 |
// force a new local nid
|
| 714 |
$node->nid = '';
|
| 715 |
|
| 716 |
// import node
|
| 717 |
node_save($node);
|
| 718 |
$local_nid = $node->nid;
|
| 719 |
watchdog('subscribe', t('%type: added %title from site %site.', array('%type' => theme('placeholder', t($node->type)), '%title' => theme('placeholder', $node->title), '%site' => theme('placeholder', $site_name))), WATCHDOG_NOTICE, l(t('view'), 'node/'. $node->nid));
|
| 720 |
$n_counter = $n_counter + 1;
|
| 721 |
|
| 722 |
// save map of remote/local node ID's (and uids?)
|
| 723 |
db_query("INSERT INTO {subscribe_node} (nid, sid, remote_nid, remote_uid, remote_author) VALUES (%d, %d, %d, %d, '%s')", $local_nid, $sid, $remote_nid, $remote_uid, $remote_author);
|
| 724 |
|
| 725 |
// add vocabulary information
|
| 726 |
foreach ($node->taxonomy_terms as $remote_term) {
|
| 727 |
// which vocabulary will we be adding it to?
|
| 728 |
$local_vid = $vocab_map[$remote_term['vid']]->vid;
|
| 729 |
if (!$local_vid) {
|
| 730 |
watchdog('subscribe', t('Discarding term %term because remote vocabulary id %vid is not mapped to a local vocabulary.', array('%term' => check_plain($remote_term->name), '%vid' => $remote_term->vid)));
|
| 731 |
continue;
|
| 732 |
}
|
| 733 |
|
| 734 |
$db_result = db_query("SELECT * FROM {term_data} WHERE LOWER('%s') LIKE LOWER(name) AND vid = %d", trim($remote_term['name']), $local_vid);
|
| 735 |
$local_term = db_fetch_object($db_result);
|
| 736 |
|
| 737 |
if ($local_term) { // existing term
|
| 738 |
// only add if this term is not already mapped to this node
|
| 739 |
$result = db_query("SELECT nid FROM {term_node} WHERE nid = %d AND tid = %d", $local_nid, $local_term->tid);
|
| 740 |
$nid = db_result($result);
|
| 741 |
if (!$nid) { // this nid-tid pair has not yet been mapped
|
| 742 |
db_query('INSERT INTO {term_node} (nid, tid) VALUES (%d, %d)', $local_nid, $local_term->tid);
|
| 743 |
}
|
| 744 |
}
|
| 745 |
else { // term does not exist locally; add new term
|
| 746 |
unset($remote_term['tid']); // local Drupal will assign a new tid
|
| 747 |
$remote_term['vid'] = $local_vid;
|
| 748 |
$remote_term['weight'] = 0; // discard remote weights
|
| 749 |
taxonomy_save_term($remote_term); // sets $remote_term->tid
|
| 750 |
$t_counter = $t_counter + 1;
|
| 751 |
db_query('INSERT INTO {term_node} (nid, tid) VALUES (%d, %d)', $local_nid, $remote_term->tid);
|
| 752 |
}
|
| 753 |
}
|
| 754 |
}
|
| 755 |
}
|
| 756 |
return array($n_counter, $u_counter, $t_counter);
|
| 757 |
}
|
| 758 |
|
| 759 |
|
| 760 |
/********************************************************************
|
| 761 |
* Module Functions :: Sending XML-RPC
|
| 762 |
********************************************************************/
|
| 763 |
|
| 764 |
/**
|
| 765 |
* Ask the channel if we may subscribe to it.
|
| 766 |
*
|
| 767 |
* @param $edit
|
| 768 |
* array containing a url key with url of the channel
|
| 769 |
*
|
| 770 |
* @return
|
| 771 |
* array containing maySubscribe (boolean), ppName (string),
|
| 772 |
* nodeTypes (array of published node types), vocabularies (array
|
| 773 |
* of published vocabularies)
|
| 774 |
*/
|
| 775 |
function subscribe_xmlc_may_subscribe($url, $channel_id, $username, $pass) {
|
| 776 |
|
| 777 |
$result = xmlrpc($url, 'drupal.publish.maySubscribe', (int)$channel_id, $username, $pass);
|
| 778 |
if (!$result) {
|
| 779 |
$error = xmlrpc_error_msg();
|
| 780 |
if (!$error) {
|
| 781 |
$error = t('Are you sure you entered a valid XML-RPC endpoint?');
|
| 782 |
}
|
| 783 |
drupal_set_message($error, 'error');
|
| 784 |
watchdog('error', $error);
|
| 785 |
}
|
| 786 |
else {
|
| 787 |
return $result;
|
| 788 |
}
|
| 789 |
}
|
| 790 |
|
| 791 |
/**
|
| 792 |
* XMLRPC client to request that remote site which has the publish
|
| 793 |
* module installed establish a publishing relationship with client
|
| 794 |
* site (that is, this site).
|
| 795 |
*
|
| 796 |
* @param $url
|
| 797 |
* the URL of the channel on the publishing site
|
| 798 |
* which generally looks like http://drupal.org/publish/6
|
| 799 |
* @param $username
|
| 800 |
* the username with access on the publishing site
|
| 801 |
* @param $password
|
| 802 |
* the password with access on the publishing site
|
| 803 |
* @param $cond
|
| 804 |
* array of condition triplets; see publish_publish()
|
| 805 |
* e.g. ['nid', '=', '3']
|
| 806 |
*
|
| 807 |
* @return
|
| 808 |
* Subscription ID, a numeric key to to our subscription on the
|
| 809 |
* publishing site (similar to a user ID).
|
| 810 |
*/
|
| 811 |
function subscribe_xmlc_subscribe($url, $channel_id, $username, $pass, $method, $local_vids, $remote_vids, $cond = array()) {
|
| 812 |
global $base_url;
|
| 813 |
$destination = $base_url . '/xmlrpc.php'; // so the remote server knows where to send updates
|
| 814 |
|
| 815 |
$result = xmlrpc($url, 'drupal.publish.subscribe', (int)$channel_id, $username, $pass, $destination, $method, $local_vids, $remote_vids, $cond);
|
| 816 |
|
| 817 |
if (!$result) {
|
| 818 |
$error = xmlrpc_error_msg();
|
| 819 |
drupal_set_message($error, 'error');
|
| 820 |
watchdog('error', $error);
|
| 821 |
}
|
| 822 |
else {
|
| 823 |
return $result;
|
| 824 |
}
|
| 825 |
}
|
| 826 |
|
| 827 |
/**
|
| 828 |
* XMLRPC client to request that remote site which has the publish
|
| 829 |
* module installed send an array of nodes to the client
|
| 830 |
* site (that is, this site).
|
| 831 |
*
|
| 832 |
* @param $rsid
|
| 833 |
* the subscription ID of this client on the publishing site
|
| 834 |
* @param $username
|
| 835 |
* the username with access on the publishing site
|
| 836 |
* @param $password
|
| 837 |
* the password with access on the publishing site
|
| 838 |
* @param $url
|
| 839 |
* the URI of the XML-RPC endpoint on the publishing site
|
| 840 |
* which generally looks like http://example.org/xmlrpc.php
|
| 841 |
* @param $cond
|
| 842 |
* array of condition triplets; see publish_publish()
|
| 843 |
* e.g. ['nid', '=', '3']
|
| 844 |
*
|
| 845 |
* @return
|
| 846 |
* Subscription ID, a numeric key to to our subscription on the
|
| 847 |
* publishing site (similar to a user ID).
|
| 848 |
*/
|
| 849 |
function subscribe_xmlc_pull($url, $channel_id, $username, $pass, $rsid, $cond = array()) {
|
| 850 |
|
| 851 |
$result = xmlrpc($url, 'drupal.publish.pull', (int)$channel_id, (int)$rsid, $username, $pass, $cond);
|
| 852 |
|
| 853 |
if (!$result) {
|
| 854 |
$error = xmlrpc_error_msg();
|
| 855 |
drupal_set_message($error, 'error');
|
| 856 |
watchdog('error', $error);
|
| 857 |
}
|
| 858 |
else {
|
| 859 |
return $result;
|
| 860 |
}
|
| 861 |
}
|
| 862 |
|
| 863 |
function subscribe_xmlc_cancel_subscription($sub) {
|
| 864 |
$result = xmlrpc($sub->url, 'drupal.publish.cancelSubscription', (int)$sub->rsid, $sub->token);
|
| 865 |
if (!$result) {
|
| 866 |
$error = xmlrpc_error_msg();
|
| 867 |
drupal_set_message($error, 'error');
|
| 868 |
watchdog('error', $error);
|
| 869 |
}
|
| 870 |
|
| 871 |
return $result;
|
| 872 |
}
|
| 873 |
|
| 874 |
|
| 875 |
/********************************************************************
|
| 876 |
* Module Functions :: Receiving XML-RPC
|
| 877 |
********************************************************************/
|
| 878 |
|
| 879 |
/*
|
| 880 |
* Receive nodes that a remote site is pushing to us
|
| 881 |
*
|
| 882 |
$message = array(
|
| 883 |
'token' => token,
|
| 884 |
'count' => count($nodes),
|
| 885 |
'site_url' => $base_url,
|
| 886 |
'site_name' => variable_get('site_name', 'drupal'),
|
| 887 |
'nodes' => $nodes
|
| 888 |
);
|
| 889 |
*
|
| 890 |
*/
|
| 891 |
|
| 892 |
function subscribe_xmls_receive($message) {
|
| 893 |
$subscription = db_fetch_object(db_query("SELECT * FROM {subscribe_subscriptions} WHERE token = '%s' AND method = 'push' AND rsid = %d", $message['token'], $message['rsid']));
|
| 894 |
if (!$subscription) {
|
| 895 |
return xmlrpc_error(801, t('No such subscription'));
|
| 896 |
}
|
| 897 |
|
| 898 |
|
| 899 |
// verify that it's from a domain that we trust
|
| 900 |
$ip = $_SERVER['REMOTE_ADDR'];
|
| 901 |
if ($ip != '127.0.0.1') {
|
| 902 |
$host = gethostbyaddr($ip);
|
| 903 |
$parts = parse_url($host);
|
| 904 |
if ($parts['host'] != $subscription->domain) {
|
| 905 |
return xmlrpc_error(701, t('Publishing domain not trusted.'));
|
| 906 |
}
|
| 907 |
}
|
| 908 |
|
| 909 |
$site_metadata = array('site_url' => $message['site_url'], 'site_name' => $message['site_name'], 'sid' => $subscription->sid);
|
| 910 |
list($n_counter, $u_counter, $t_counter) = subscribe_import($message['nodes'], $site_metadata);
|
| 911 |
$result = t("New content from %name. Nodes imported: %ncount; noded updated: %ucount; terms imported: %tcount.", array('%ncount' => $n_counter, '%ucount' => $u_counter, '%tcount' => $t_counter, '%name' => $subscription->name));
|
| 912 |
|
| 913 |
return $result;
|
| 914 |
}
|
| 915 |
|
| 916 |
/********************************************************************
|
| 917 |
* Module Functions :: Private
|
| 918 |
********************************************************************/
|
| 919 |
|
| 920 |
/**
|
| 921 |
* Utility function to get vocabulary mapping out of our encoded form.
|
| 922 |
*
|
| 923 |
* @return
|
| 924 |
* associative array keyed by local vocabulary ID
|
| 925 |
*/
|
| 926 |
function _subscribe_extract_vocab_map($edit) {
|
| 927 |
$localvoc = array();
|
| 928 |
foreach ($edit as $key => $val) {
|
| 929 |
if (strstr($key, 'localvoc')) {
|
| 930 |
$lvid = substr($key, 8);
|
| 931 |
if (is_numeric($lvid) && ($lvid > 0)) $localvoc[$lvid] = $val;
|
| 932 |
}
|
| 933 |
}
|
| 934 |
return $localvoc;
|
| 935 |
}
|
| 936 |
|
| 937 |
/**
|
| 938 |
* Return an array of database column names for a given table.
|
| 939 |
*
|
| 940 |
* This function makes changing the database and module updates a little easier.
|
| 941 |
*/
|
| 942 |
function _subscribe_db_fields($table_name) {
|
| 943 |
switch ($table_name) {
|
| 944 |
case 'subscribe_subscriptions':
|
| 945 |
return array('sid', 'rsid', 'name', 'base_url', 'url', 'channel_id', 'domain', 'username', 'pass', 'token', 'method', 'last_update');
|
| 946 |
}
|
| 947 |
}
|