'textfield', '#title' => t('Default Number of Hits'), '#default_value' => variable_get('content_share_limit', 10), '#size' => 5, '#maxlength' => 5, '#description' => t("The default maximum number of hits to return per query. Can be overidden in query and in Max number."), '#required' => TRUE, ); $form['content_share_cap'] = array( '#type' => 'textfield', '#title' => t('Query Cap'), '#default_value' => variable_get('content_share_cap', 200), '#size' => 5, '#maxlength' => 5, '#description' => t("Max number of hits to return in any single query. Overrides the default and user limits. Can be used to cap the server load."), '#required' => TRUE, ); $form['content_share_batch'] = array( '#type' => 'textfield', '#title' => t('Batch Size'), '#default_value' => variable_get('content_share_batch', 200), '#size' => 5, '#maxlength' => 5, '#description' => t("The number of hits to return per batch. To use with the 'batch' query term."), '#required' => TRUE, ); $form['content_share_require_authentication'] = array( '#type' => 'checkbox', '#title' => t('Block Anonymous Users, Require Authentication'), '#default_value' => variable_get('content_share_require_authentication', 0), '#description' => t("Block all anonymous usage and require Session Authentication from every user. Needs Session Authentication in Services Module Enabled"), '#required' => TRUE, ); // user roles $user_roles = user_roles($membersonly = TRUE, $permission = NULL); $form['content_share_roles'] = array( '#type' => 'checkboxes', '#title' => t('Allowed Roles'), '#options' => $user_roles, '#default_value' => variable_get('content_share_roles', $user_roles), '#description' => t("Roles that can use Content Share when authentication is required."), '#required' => TRUE, ); // content types $content_types = node_get_types('names'); // use this array to default to some, not all content types foreach ($content_types as $key => $value) { $default_types[$key] = $key; } $stored_default_types_auth = variable_get('content_share_content_types_auth', $default_types); $form['content_share_content_types_auth'] = array( '#type' => 'checkboxes', '#title' => t('Content Types Exposed to Logged in User'), '#options' => $content_types, '#default_value' => $stored_default_types_auth, '#description' => t("Select content types available via the Content Share API to a Logged in User'),."), '#required' => TRUE, ); $stored_default_types_anon = variable_get('content_share_content_types_anon', $default_types); $form['content_share_content_types_anon'] = array( '#type' => 'checkboxes', '#title' => t('Content Types Exposed to Anonymous User'), '#options' => $content_types, '#default_value' => $stored_default_types_anon, '#description' => t("Select content types available via the Content Share API to an Anonymous User."), '#required' => TRUE, ); // content fields $core_fields = array( 'nid' => 'nid', 'type' => 'type', 'created' => 'created', 'title' => 'title', 'body' => 'body', 'teaser' => 'teaser', 'taxonomy' => 'taxonomy', 'language' => 'language', 'uid' => 'uid', 'url' => 'url', 'uuid' => 'uuid', 'status' => 'status', 'changed' => 'changed', 'comment' => 'comment', 'promote' => 'promote', 'moderate' => 'moderate', 'sticky' => 'sticky', 'tnid' => 'tnid', 'translate' => 'translate', 'vid' => 'vid', 'revision_uid' => 'revision_uid', 'log' => 'log', 'revision_timestamp' => 'revision_timestamp', 'format' => 'format', 'name' => 'name', 'picture' => 'picture', 'data' => 'data', 'last_comment_timestamp' => 'last_comment_timestamp', 'last_comment_name' => 'last_comment_name', 'comment_count' => 'comment_count', ); $cck_fields = array_keys(content_fields()); //call a Content module function to get a nested array of all CCK fields on site. Get their keys only. $flat_cck = array_combine($cck_fields, $cck_fields); //combine into associative array with same keys and value $field_options = array_merge($core_fields, $flat_cck); $content_share_default_fields = array( 'nid' => 'nid', 'type' => 'type', 'created' => 'created', 'title' => 'title', 'body' => 'body', 'teaser' => 'teaser', 'taxonomy' => 'taxonomy', ); $stored_default_fields_anon = variable_get('content_share_content_fields_anon', $content_share_default_fields); $form['content_share_content_fields_anon'] = array( '#type' => 'checkboxes', '#title' => t('Expose Fields to Anonymous User'), '#options' => $field_options, '#default_value' => $stored_default_fields_anon, '#description' => t("Select fields available via the Content Share API to an Anonymous User."), '#required' => TRUE, ); $stored_default_fields_auth = variable_get('content_share_content_fields_auth', $content_share_default_fields); $form['content_share_content_fields_auth'] = array( '#type' => 'checkboxes', '#title' => t('Expose Fields to Logged in User'), '#options' => $field_options, '#default_value' => $stored_default_fields_auth, '#description' => t("Select fields available via the Content Share API to a Logged in User."), '#required' => TRUE, ); $form = system_settings_form($form); // this calls the theme_content_share_settings function which rewrites most elements of the form // with custom style and converts multi checkboxes into tables $form['#theme'] = 'content_share_settings'; return $form; } /** * This function rewrites the form elements with and converts some of them into tables * with the help of two custom js classes * note that submit and reset buttons are not rewritten by this function and returned as is * @param array $form * The form generated by content_share_settings(). * @return array * The re-styled form. */ function theme_content_share_settings($form) { drupal_set_html_head(' '); // rewrite all textfield and single checkboxes menu foreach (element_children($form) as $element) { if (preg_match('/textfield|checkbox$/', $form[$element]['#type'])) { $rewritten_textfields = $rewritten_textfields . theme($form[$element]['#type'], $form[$element]); unset($form[$element]); } } // rewrite roles menu $role_header = array( $form['content_share_roles']['#title'], theme('table_select_header_cell'), 'check/uncheck all', ); $role_rows[] = array( array( 'data' => 'ROLE NUMBER', ), array( 'data' => 'ROLE NAME', 'colspan' => 2, ), ); foreach (element_children($form['content_share_roles']) as $child) { $checkboxes_role = drupal_render($form['content_share_roles'][$child]); // first cell, showing the actual option value $role_row = array( $child, // second cell array('data' => "$checkboxes_role", 'style' => 'text-align: left', 'colspan' => 2, ), ); $role_rows[] = $role_row; } // Now render the table representing content_share_content_types $rewritten_roles = theme('table', $role_header, $role_rows, array('class' => 'special-single-checkbox')); unset($form['content_share_roles']); // then remove rewritten elements before rendering // Rewrite expose content types menu. $header = array( 'Exposed Types', theme('table_select_header_cell'), 'check/uncheck all', ); $rows[] = array( array( 'data' => 'TYPE', ), array( 'data' => 'ANONYMOUS', 'colspan' => 2, ), array( 'data' => 'LOGGED IN', 'colspan' => 2, ), ); foreach (element_children($form['content_share_content_types_auth']) as $child) { $checkboxes_anon = drupal_render($form['content_share_content_types_anon'][$child]); $checkboxes_auth = drupal_render($form['content_share_content_types_auth'][$child]); $row = array( // first cell, showing the actual option value array( 'data' => "$child", ), // second cell array( 'data' => "$checkboxes_anon", 'style' => 'text-align: left', 'colspan' => 2, ), array( // second cell 'data' => "$checkboxes_auth", 'style' => 'text-align: left', 'colspan' => 2, ), ); $rows[] = $row; } // Now render the table representing content_share_content_types $rewritten_types = theme('table', $header, $rows, array('class' => 'special-checkboxes')); // $table_attributes = array('width' => '100%', 'border' => 0, 'cellspacing' => 5, 'cellpadding' => 5); //can use this in the array for theme args unset($form['content_share_content_types_auth']); // then remove rewritten elements before rendering unset($form['content_share_content_types_anon']); // Rewrite expose fields menu. $field_header = array( 'Exposed Fields', theme('table_select_header_cell'), 'check/uncheck all', ); $fields_rows[] = array( array( 'data' => 'FIELD', ), array( 'data' => 'ANONYMOUS', 'colspan' => 2, ), array( 'data' => 'LOGGED IN', 'colspan' => 2, ), ); foreach (element_children($form['content_share_content_fields_anon']) as $child) { $fields_anon = drupal_render($form['content_share_content_fields_anon'][$child]); $fields_auth = drupal_render($form['content_share_content_fields_auth'][$child]); $fields_row = array( // first cell, showing the actual option value array( 'data' => "$child", ), // second cell array( 'data' => "$fields_anon", 'style' => 'text-align: left', 'colspan' => 2, ), // second cell array( 'data' => "$fields_auth", 'style' => 'text-align: left', 'colspan' => 2, ), ); $fields_rows[] = $fields_row; } // Now render the table representing content_share_content_types $rewritten_fields = theme('table', $field_header, $fields_rows, array('class' => 'special-checkboxes')); // $table_attributes = array('width' => '100%', 'border' => 0, 'cellspacing' => 5, 'cellpadding' => 5); //can use this in the array for theme args unset($form['content_share_content_fields_auth']); // then remove rewritten elements before rendering unset($form['content_share_content_fields_anon']); // Do a final rendering of the form and return the result. $rewritten_form = $rewritten_textfields . $rewritten_roles . $rewritten_types . $rewritten_fields . drupal_render($form); return $rewritten_form; } /** * Implements hook_theme(). */ function content_share_theme() { return array( 'content_share_settings' => array( 'arguments' => array('form' => NULL), ), ); } /** * Implements hook_menu(). */ function content_share_menu() { $items = array(); $items['content_share/test'] = array( 'title' => 'content_share API testing page.', 'page callback' => '_content_share_test', 'access callback' => TRUE, 'type' => MENU_CALLBACK ); $items['admin/settings/content_share'] = array( 'title' => t('Content Share module settings'), 'description' => t('Description of the Content Share settings page'), 'page callback' => 'drupal_get_form', 'access callback' => 'user_access', 'page arguments' => array('content_share_settings'), 'access arguments' => array('access administration pages'), 'type' => MENU_NORMAL_ITEM, ); return $items; } /** * Validate settings input * @param array, array ref * The current form and its state * @return void */ function content_share_settings_validate($form, &$form_state) { $limit = $form_state['values']['content_share_limit']; $cap = $form_state['values']['content_share_cap']; if (!is_numeric($limit)) { form_set_error('content_share_limit', t('You must enter an integer for the Default number of hits.')); } elseif ($limit <= 0) { form_set_error('content_share_limit', t('Default number of hits must be positive.')); } elseif ($limit > $cap) { form_set_error('content_share_limit', t('Default number of hits cannot be larger than Query Cap.')); } } /** * Implements hook_views_api(). */ function content_share_views_api() { return array( 'api' => 2, 'path' => drupal_get_path('module', 'content_share'), ); } /** * Implements hook_services_resources(). */ function content_share_services_resources() { return array( 'content' => array( 'retrieve' => array( 'help' => 'Retrieves content', 'file' => array('file' => 'module', 'module' => 'content_share'), 'callback' => '_content_share_retrieve', 'access callback' => '_content_share_access', 'access arguments' => array('view'), 'access arguments append' => TRUE, 'args' => array( array( 'name' => 'id', 'type' => 'int', 'description' => 'The id of the content to get', 'source' => array('path' => '0'), 'optional' => FALSE, ), ), ), 'index' => array( 'help' => 'Retrieves a listing of contents', 'file' => array('file' => 'module', 'module' => 'content_share'), 'callback' => '_content_share_index', 'access callback' => 'user_access', 'access arguments' => array('access content'), 'access arguments append' => FALSE, 'args' => array(array( 'name' => 'page', 'type' => 'int', 'description' => '', 'source' => array( 'param' => 'page', ), 'optional' => TRUE, 'default value' => 0, ), array( 'name' => 'parameters', 'type' => 'array', 'description' => '', 'source' => 'param', 'optional' => TRUE, 'default value' => array(), ), ), ), ), ); } // *************************** Hooks end *************************** // ********************************** // **** Tester function(s) begins *************** /** * Writes to db when hooks are called from services * @param string * @return void */ function _content_share_test_write($test_string) { $string = new stdClass(); date_default_timezone_set('America/New_York'); $string->timestamp = date('c'); $string->query = $_SERVER['QUERY_STRING']; $string->error = $test_string; $write_result = drupal_write_record('content_share_stats', $string); //$write_result is FALSE if it fails to write to bd. For debuging only. } /** * function for testing over a URL, called by hook_menu * @param * @return string */ function _content_share_test() { _content_share_index(); return 'testing complete'; } // *************************** Tester function(s) end *************************** // ********************************** /** * Function for retrieving nodes. Takes an nid and an error object * gets field if they are in the query and processes node field according * to permissions and what is requested in the query * @param string, object ref * @return object */ function _content_share_retrieve($nid, &$error_object) { $node = node_load($nid); // check the fields white list and remove those not in it, provide a default value if variable_get fails $exposed_fields = _content_share_exposed('fields'); // add field in the URL query to the white list parse_str($_SERVER['QUERY_STRING'], $fields_request_array); // parse the user URL input // field to return from URL request input, defaults to settings if no user input if (isset($fields_request_array['field'])) { $fieldParse = new ParseField($fields_request_array['field']); if ($fieldParse->isValid()) { $exposed_fields = array_merge($exposed_fields, $fieldParse->getField()); } else{ $error_object->error = 'error'; $error_object->field = $fieldParse->errorMsg; } } // Process CCK field permissions. If the current user has no permissions specified by // the Content module to view the CCK field, unset the field call a Content module function // to get a nested array of all CCK fields on site. Get their keys only. $cck_fields = array_keys(content_fields()); if (module_exists('content_permissions')) { foreach ($cck_fields as $key => $field) { if (!content_access('view', $field)) { unset($exposed_fields[$field]); } } } // iterate through the node object // unset if an object property is not in the white field list. foreach ($node as $property => $value) { if ($property !== $exposed_fields[$property]) { unset($node->$property); } } // Create and add custom fields to output if ($exposed_fields['url']) { $url_ops = array('absolute' => TRUE); $node->url = url('node/' . $node->nid, $url_ops); } return $node; } /** * Function to get access permissions for nodes * @param string, array * @return bool */ function _content_share_access($op = 'view', $args = array()) { $object = (object) $args[0]; $type = $object->type; return node_access($op, $type); } /** Function for cheking authentication and roles * @params error object by reference * @return bool */ function _content_share_user_validate(&$error_object) { global $user; // Check if user is authenticated if ($user->uid == 0) { $error_object->error = 'error'; $error_object->auth = 'You need to be log in to access this API service'; return FALSE; } // Check if the authenticated user has roles that approve api's use. $approved_roles = variable_get('content_share_roles', FALSE); $role_flag = FALSE; if (is_array($approved_roles) && is_array($user->roles)) { foreach ($user->roles as $role_id => $role_name) { if ($approved_roles[$role_id] == $role_id) { $role_flag = TRUE; } } if ($role_flag == FALSE) { $error_object->error = 'error'; $error_object->auth = 'Your roles are not approved by the admin for using Content Share API'; return FALSE; } } // If all test are passed return true return TRUE; } /** * Function for providing an index (list) of nodes to * convert to output format and return to user * @param void * @return array of objects */ function _content_share_index() { //return array with node object as values $index = array(); //array with parsed user input request $request_array = array(); // parse the user URL input parse_str($_SERVER['QUERY_STRING'], $request_array); // Create an error object to collect all errors. $error_object = new stdClass(); // A switch to see if authentication is required to use api and a function to validate user $require_auth = variable_get('content_share_require_authentication', 0); // Return if the user is not authenticated, write stats. if ($require_auth == 1 && _content_share_user_validate($error_object) == FALSE) { $index[] = $error_object; _content_share_stat_write($error_object); return $index; } // If the checks above are passed and user has validated, continue. // Check if "help" is in the query and if it is, return only the help response. if (isset($request_array['help'])) { $helpParse = new ParseHelp($request_array['help']); if ($helpParse->isValid()) { _content_share_stat_write($error_object); return array($helpParse->getHelp()); } else{ $error_object->error = 'error'; $error_object->help = $helpParse->errorMsg; _content_share_stat_write($error_object); } } // Get the nodes from db and track errors. $nids = _content_share_db_get($error_object); // Check if count is present in the query request and return just the number of nodes if it is. if (isset($request_array['size']) && $error_object->error != 'error') { $sizeParse = new ParseSize($request_array['size']); if ($sizeParse->isValid()) { $hit_count = sizeof($nids); return array('size' => $hit_count); } } if (!empty($nids)) { // If batch processing is set, limit the number of nodes to retrieve. if (isset($request_array['batch'])) { $batchParse = new ParseBatch($request_array['batch']); if ($batchParse->isValid()) { $batch_offset = ($batchParse->getBatch() - 1) * variable_get('content_share_batch', 200); $nids = array_slice($nids, $batch_offset, variable_get('content_share_batch', 200), TRUE); //NB: RESIZING $nids ARRAY TO ONLY THE CURRENT CHUNK } else{ $error_object->error = 'error'; $error_object->batch = $batchParse->errorMsg; $index[] = $error_object; _content_share_stat_write($error_object); return $index;} } // Nb: here $nids can be either the full list or the current chunk of nids. foreach ($nids as $key => $nid) { // This is cpu-intensive only retrieve nodes if there are no errors. $retrieved_node = _content_share_retrieve($nid, $error_object); // Index only node objects. if (is_object($retrieved_node)) { $index[] = $retrieved_node; } } } //*** CHECK IF THERE WERE ANY ERRORS DURING NODE RETRIEVAL if ($error_object->error == 'error') { $index = array(); //if they were field validation errors, discard all results $index[] = $error_object; //and output only the error } _content_share_stat_write($error_object); return $index; } /** * Function to construct a db query based on the user input * and settings, to run the query over the db and return an array of * matched nodes * @param $request * @return return an array of NIDs matching the query */ function _content_share_db_get(&$error_object) { $nids = array(); //return array $request_array = array(); //contains parsed URL query $where_clause = array(); //holds the SQL clauses $select_term_count = 0; parse_str($_SERVER['QUERY_STRING'], $request_array); // Parse input and construct sql clauses. foreach ($request_array as $term => $value) { switch ($term) { case "after": $afterParser = new ParseAfter($value); if ($afterParser->isValid()) { $where_clause[] = $afterParser->getSQL(); $select_term_count++; continue; } else{ $error_object->error = 'error'; $error_object->after = $afterParser->errorMsg; break; } case "before": $beforeParser = new ParseBefore($value); if ($beforeParser->isValid()) { $where_clause[] = $beforeParser->getSQL(); $select_term_count++; continue; } else{ $error_object->before = $beforeParser->errorMsg; break; } case "type": $typeParser = new ParseType($value); if ($typeParser->isValid()) { $where_clause[] = $typeParser->getSQL(); $select_term_count++; continue; } else{ $error_object->error = 'error'; $error_object->type = $typeParser->errorMsg; break; } case "limit": $limitParser = new ParseLimit($value); if ($limitParser->isValid()) { $user_limit = $limitParser->getLimit(); } else{ $error_object->error = 'error'; $error_object->limit = $limitParser->errorMsg; break; } continue; case "search": $searchParser = new ParseSearch($value); if ($searchParser->isValid()) { $where_clause[] = $searchParser->getSQL(); $select_term_count++; continue;} else{ $error_object->error = 'error'; $error_object->search = $searchParser->errorMsg; break; } case "sort": $sortParser = new ParseSort($value); if ($sortParser->isValid()) { $order_clause = $sortParser->getSQL(); } else{ $error_object->error = 'error'; $error_object->sorting = $sortParser->errorMsg; break; } } } // Check if the query is empty or does not have any recognizable terms. if ($select_term_count == 0) { $error_object->error = 'error'; $error_object->query = 'Query does not have any known selection terms'; return $nids; } // Construct the query from parsed input and sql clauses. if ($error_object->error != 'error') { $sql = 'SELECT distinct nid, type FROM {node} WHERE '; // Construct the limit clause for SQL. if (is_numeric($user_limit)) { $limit = min($user_limit, variable_get('content_share_cap', 200)); } else{ $limit = min(variable_get('content_share_limit', 10), variable_get('content_share_cap', 200)); } // check if "batch" term is set parse_str($_SERVER['QUERY_STRING'], $batch_request_array); if (isset($batch_request_array['batch'])) { // if batch is set no limit is imposed on the QUERY $limit_clause = ''; } else{ $limit_clause = ' LIMIT 0, ' . $limit; } // Check if the node is published. If not, do not return it. $where_clause[] = 'status = 1'; // Construct the "oder by" clause. $order_clause = isset($order_clause) ? $order_clause : ' ORDER BY created DESC '; // Get an array with the list of exposed types. $exposed_types_array = _content_share_exposed('types'); // Check if the indexed node is of the type in exposed types. if (empty($exposed_types_array)) { $error_object->error = 'error'; $error_object->types = 'No content types are set to be exposed via Content Share'; return $nids; } // Construct the "type in" where clause for types exposed in settings. else { // pad content types with " quotes for SQL foreach ($exposed_types_array as $type_key => $type_value) { if ($type_key === $type_value) { $padded_exposed_types_array[] = '"' . $type_key . '"'; } } $where_clause[] = "type IN (" . implode( ', ', $padded_exposed_types_array) . ")"; } // Put the query together and run it. $whole_query = $sql . implode(' AND ', $where_clause) . $order_clause . $limit_clause; $whole_query = db_rewrite_sql($whole_query); $db_result = db_query($whole_query); while ($node = db_fetch_object($db_result)) { if (is_object($node)) { $nids[] = $node->nid; } } } return $nids; } /** * This function checks if the type of the node is listed as accessible for * the user (logged or anon) * @param string * @return array of strings */ function _content_share_exposed($item) { $require_auth = variable_get('content_share_require_authentication', 0); $item_array = array(); switch ($require_auth) { case "1": $item_array = variable_get('content_share_content_' . $item . '_auth', array()); break; case "0": global $user; if ($user->uid == 0) { $item_array = variable_get('content_share_content_' . $item . '_anon', array()); } else{ $item_array = variable_get('content_share_content_' . $item . '_auth', array()); } break; default: $item_array = array(); } return $item_array; } /**function _content_share_stat_write() for writing to stats in DB * @param object ref - object with error messages from this module and Solr * @return void */ function _content_share_stat_write(&$error_object) { global $user; $stats = new stdClass(); $stats->uid = $user->uid; $stats->error = serialize($error_object); date_default_timezone_set('America/New_York'); $stats->timestamp = date('c'); // NB test this function on real ANONYMOUS requests to make sure it records IP of the origin, // not the cashing proxy $stats->ip = ip_address(); $stats->query = $_SERVER['QUERY_STRING']; // $write_result is FALSE if it fails to write to bd. For debuging only. $write_result = drupal_write_record('content_share_stats', $stats); }