search_api.admin.inc 79 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939194019411942194319441945194619471948194919501951195219531954195519561957195819591960196119621963196419651966196719681969197019711972197319741975197619771978197919801981198219831984198519861987198819891990199119921993199419951996199719981999200020012002200320042005200620072008200920102011201220132014201520162017201820192020202120222023202420252026202720282029203020312032203320342035203620372038203920402041204220432044204520462047204820492050205120522053205420552056205720582059206020612062206320642065206620672068206920702071207220732074207520762077207820792080208120822083208420852086208720882089209020912092209320942095209620972098209921002101210221032104210521062107210821092110211121122113211421152116211721182119212021212122212321242125212621272128212921302131213221332134213521362137213821392140214121422143214421452146214721482149215021512152215321542155215621572158215921602161216221632164216521662167216821692170217121722173217421752176217721782179218021812182218321842185218621872188218921902191219221932194219521962197219821992200220122022203220422052206220722082209221022112212221322142215221622172218221922202221222222232224
  1. <?php
  2. /**
  3. * @file
  4. * Administration page callbacks for the Search API module.
  5. */
  6. /**
  7. * Page callback that shows an overview of defined servers and indexes.
  8. *
  9. * @see search_api_menu()
  10. */
  11. function search_api_admin_overview() {
  12. $base_path = drupal_get_path('module', 'search_api') . '/';
  13. drupal_add_css($base_path . 'search_api.admin.css');
  14. drupal_add_js($base_path . 'search_api.admin.js');
  15. $servers = search_api_server_load_multiple(FALSE);
  16. $indexes = array();
  17. // When any entity was not normally created in the database, then show status
  18. // for all.
  19. $show_config_status = FALSE;
  20. foreach (search_api_index_load_multiple(FALSE) as $index) {
  21. $indexes[$index->server][$index->machine_name] = $index;
  22. if (!$show_config_status && $index->status != ENTITY_CUSTOM) {
  23. $show_config_status = TRUE;
  24. }
  25. }
  26. // Show disabled servers after enabled ones.
  27. foreach ($servers as $id => $server) {
  28. if (!$server->enabled) {
  29. unset($servers[$id]);
  30. $servers[$id] = $server;
  31. }
  32. if (!$show_config_status && $server->status != ENTITY_CUSTOM) {
  33. $show_config_status = TRUE;
  34. }
  35. }
  36. $rows = array();
  37. $t_server = array('data' => t('Server'), 'colspan' => 2);
  38. $t_index = t('Index');
  39. $t_enabled['data'] = array(
  40. '#theme' => 'image',
  41. '#path' => $base_path . 'enabled.png',
  42. '#alt' => t('enabled'),
  43. '#title' => t('enabled'),
  44. );
  45. $t_enabled['class'] = array('search-api-status');
  46. $t_disabled['data'] = array(
  47. '#theme' => 'image',
  48. '#path' => $base_path . 'disabled.png',
  49. '#alt' => t('disabled'),
  50. '#title' => t('disabled'),
  51. );
  52. $t_disabled['class'] = array('search-api-status');
  53. $t_enable = t('Enable');
  54. $pre_server = 'admin/config/search/search_api/server';
  55. $pre_index = 'admin/config/search/search_api/index';
  56. $enable = '/enable';
  57. foreach ($servers as $server) {
  58. $url = $pre_server . '/' . $server->machine_name;
  59. $row = array();
  60. $row[] = $server->enabled ? $t_enabled : $t_disabled;
  61. if ($show_config_status) {
  62. $row[] = theme('entity_status', array('status' => $server->status));
  63. }
  64. $row[] = $t_server;
  65. $row[] = l($server->name, $url);
  66. $links = array();
  67. // The "Enable" function has no menu link, since a token is required. We add
  68. // it as the first link, since it will most likely be the most useful link
  69. // for a disabled server. (Same for indexes below.)
  70. if (!$server->enabled) {
  71. $links[] = array(
  72. 'title' => $t_enable,
  73. 'href' => $url . $enable,
  74. 'query' => array('token' => drupal_get_token($server->machine_name))
  75. );
  76. }
  77. $links = array_merge($links, menu_contextual_links('search-api-server', $pre_server, array($server->machine_name)));
  78. $row[] = theme('search_api_dropbutton', array('links' => $links));
  79. $rows[] = _search_api_deep_copy($row);
  80. if (!empty($indexes[$server->machine_name])) {
  81. foreach ($indexes[$server->machine_name] as $index) {
  82. $url = $pre_index . '/' . $index->machine_name;
  83. $row = array();
  84. $row[] = $index->enabled ? $t_enabled : $t_disabled;
  85. if ($show_config_status) {
  86. $row[] = theme('entity_status', array('status' => $index->status));
  87. }
  88. $row[] = ' ';
  89. $row[] = $t_index;
  90. $row[] = l($index->name, $url);
  91. $links = array();
  92. if (!$index->enabled && $server->enabled) {
  93. $links[] = array(
  94. 'title' => $t_enable,
  95. 'href' => $url . $enable,
  96. 'query' => array('token' => drupal_get_token($index->machine_name))
  97. );
  98. }
  99. $links = array_merge($links, menu_contextual_links('search-api-index', $pre_index, array($index->machine_name)));
  100. $row[] = theme('search_api_dropbutton', array('links' => $links));
  101. $rows[] = _search_api_deep_copy($row);
  102. }
  103. }
  104. }
  105. if (!empty($indexes[''])) {
  106. foreach ($indexes[''] as $index) {
  107. $url = $pre_index . '/' . $index->machine_name;
  108. $row = array();
  109. $row[] = $t_disabled;
  110. if ($show_config_status) {
  111. $row[] = theme('entity_status', array('status' => $index->status));
  112. }
  113. $row[] = array('data' => $t_index, 'colspan' => 2);
  114. $row[] = l($index->name, $url);
  115. $links = menu_contextual_links('search-api-index', $pre_index, array($index->machine_name));
  116. $row[] = theme('search_api_dropbutton', array('links' => $links));
  117. $rows[] = _search_api_deep_copy($row);
  118. }
  119. }
  120. $header = array();
  121. $header[] = t('Status');
  122. if ($show_config_status) {
  123. $header[] = t('Configuration');
  124. }
  125. $header[] = array('data' => t('Type'), 'colspan' => 2);
  126. $header[] = t('Name');
  127. $header[] = array('data' => t('Operations'));
  128. return array(
  129. '#theme' => 'table',
  130. '#header' => $header,
  131. '#rows' => $rows,
  132. '#attributes' => array('class' => array('search-api-overview')),
  133. '#empty' => t('There are no search servers or indexes defined yet.'),
  134. );
  135. }
  136. /**
  137. * Returns HTML for a drobutton list of links.
  138. *
  139. * When using this, you have to
  140. *
  141. * @param array $variables
  142. * An associative array containing the following keys:
  143. * - links: An array of links, as expected by theme_links().
  144. *
  145. * @return string
  146. * HTML for the dropbutton link list.
  147. */
  148. function theme_search_api_dropbutton(array &$variables) {
  149. $base_path = drupal_get_path('module', 'search_api') . '/';
  150. drupal_add_css($base_path . 'search_api.admin.css');
  151. drupal_add_js($base_path . 'search_api.admin.js');
  152. $variables['attributes']['class'][] = 'dropbutton';
  153. $list = theme('links', $variables);
  154. return "<div class=\"dropbutton-wrapper\">
  155. <div class=\"dropbutton-widget\">
  156. $list
  157. </div>
  158. </div>";
  159. }
  160. /**
  161. * Form callback showing a form for adding a server.
  162. */
  163. function search_api_admin_add_server(array $form, array &$form_state) {
  164. drupal_set_title(t('Add server'));
  165. $class = empty($form_state['values']['class']) ? '' : $form_state['values']['class'];
  166. $form_state['server'] = entity_create('search_api_server', array());
  167. if (empty($form_state['storage']['step_one'])) {
  168. $form['name'] = array(
  169. '#type' => 'textfield',
  170. '#title' => t('Server name'),
  171. '#description' => t('Enter the displayed name for the new server.'),
  172. '#maxlength' => 50,
  173. '#required' => TRUE,
  174. );
  175. $form['machine_name'] = array(
  176. '#type' => 'machine_name',
  177. '#maxlength' => 50,
  178. '#machine_name' => array(
  179. 'exists' => 'search_api_server_load',
  180. ),
  181. );
  182. $form['enabled'] = array(
  183. '#type' => 'checkbox',
  184. '#title' => t('Enabled'),
  185. '#description' => t('Select if the new server will be enabled after creation.'),
  186. '#default_value' => TRUE,
  187. );
  188. $form['description'] = array(
  189. '#type' => 'textarea',
  190. '#title' => t('Server description'),
  191. '#description' => t('Enter a description for the new server.'),
  192. );
  193. $form['class'] = array(
  194. '#type' => 'select',
  195. '#title' => t('Service class'),
  196. '#description' => t('Choose a service class to use for this server.'),
  197. '#options' => array('' => '< ' . t('Choose a service class') . ' >'),
  198. '#required' => TRUE,
  199. '#default_value' => $class,
  200. '#ajax' => array(
  201. 'callback' => 'search_api_admin_add_server_ajax_callback',
  202. 'wrapper' => 'search-api-class-options',
  203. ),
  204. );
  205. }
  206. elseif (!$class) {
  207. $class = $form_state['storage']['step_one']['class'];
  208. }
  209. foreach (search_api_get_service_info() as $id => $info) {
  210. if (empty($form_state['storage']['step_one'])) {
  211. $form['class']['#options'][$id] = $info['name'];
  212. }
  213. if (!$class || $class != $id) {
  214. continue;
  215. }
  216. $service = NULL;
  217. if (class_exists($info['class'])) {
  218. $service = new $info['class']($form_state['server']);
  219. }
  220. if (!($service instanceof SearchApiServiceInterface)) {
  221. watchdog('search_api', t('Service class @id specifies an illegal class: @class', array('@id' => $id, '@class' => $info['class'])), NULL, WATCHDOG_ERROR);
  222. continue;
  223. }
  224. $service_form = isset($form['options']['form']) ? $form['options']['form'] : array();
  225. $service_form = $service->configurationForm($service_form, $form_state);
  226. $form['options']['form'] = $service_form ? $service_form : array('#markup' => t('There are no configuration options for this service class.'));
  227. $form['options']['class']['#type'] = 'value';
  228. $form['options']['class']['#value'] = $class;
  229. $form['options']['#type'] = 'fieldset';
  230. $form['options']['#tree'] = TRUE;
  231. $form['options']['#collapsible'] = TRUE;
  232. $form['options']['#title'] = $info['name'];
  233. $form['options']['#description'] = $info['description'];
  234. }
  235. $form['options']['#prefix'] = '<div id="search-api-class-options">';
  236. $form['options']['#suffix'] = '</div>';
  237. $form['submit'] = array(
  238. '#type' => 'submit',
  239. '#value' => t('Create server'),
  240. );
  241. return $form;
  242. }
  243. /**
  244. * Form AJAX handler for search_api_admin_add_server().
  245. *
  246. * Just returns the "options" array of the already built form array.
  247. */
  248. function search_api_admin_add_server_ajax_callback(array $form, array &$form_state) {
  249. return $form['options'];
  250. }
  251. /**
  252. * Form validation handler for adding a server.
  253. *
  254. * Validates the machine name and calls the service class' validation handler.
  255. */
  256. function search_api_admin_add_server_validate(array $form, array &$form_state) {
  257. if (!empty($form_state['values']['machine_name'])) {
  258. $name = $form_state['values']['machine_name'];
  259. if (is_numeric($name)) {
  260. form_set_error('machine_name', t('The machine name must not be a pure number.'));
  261. }
  262. }
  263. if (empty($form_state['values']['options']['class'])) {
  264. return;
  265. }
  266. $class = $form_state['values']['options']['class'];
  267. $info = search_api_get_service_info($class);
  268. $service = NULL;
  269. if (class_exists($info['class'])) {
  270. $service = new $info['class']($form_state['server']);
  271. }
  272. if (!($service instanceof SearchApiServiceInterface)) {
  273. form_set_error('class', t('There seems to be something wrong with the selected service class.'));
  274. return;
  275. }
  276. $form_state['values']['options']['service'] = $service;
  277. $values = isset($form_state['values']['options']['form']) ? $form_state['values']['options']['form'] : array();
  278. $service->configurationFormValidate($form['options']['form'], $values, $form_state);
  279. }
  280. /**
  281. * Form submission handler for adding a server.
  282. */
  283. function search_api_admin_add_server_submit(array $form, array &$form_state) {
  284. form_state_values_clean($form_state);
  285. $values = $form_state['values'];
  286. if (!empty($form_state['storage']['step_one'])) {
  287. $values += $form_state['storage']['step_one'];
  288. unset($form_state['storage']);
  289. }
  290. if (empty($values['options']) || ($values['class'] != $values['options']['class'])) {
  291. unset($values['options']);
  292. $form_state['storage']['step_one'] = $values;
  293. $form_state['rebuild'] = TRUE;
  294. drupal_set_message(t('Please configure the used service.'));
  295. return;
  296. }
  297. $options = isset($values['options']['form']) ? $values['options']['form'] : array();
  298. unset($values['options']);
  299. $form_state['server'] = $server = entity_create('search_api_server', $values);
  300. $server->configurationFormSubmit($form['options']['form'], $options, $form_state);
  301. $server->save();
  302. $form_state['redirect'] = 'admin/config/search/search_api/server/' . $server->machine_name;
  303. drupal_set_message(t('The server was successfully created.'));
  304. }
  305. /**
  306. * Title callback for viewing or editing a server or index.
  307. */
  308. function search_api_admin_item_title($object) {
  309. return $object->name;
  310. }
  311. /**
  312. * Page callback: Displays information about a server.
  313. *
  314. * @param SearchApiServer $server
  315. * The server to display.
  316. * @param string|null $action
  317. * (optional) An action to execute for the server. One of 'enable', 'disable'
  318. * or 'clear'.
  319. *
  320. * @see search_api_menu()
  321. */
  322. function search_api_admin_server_view(SearchApiServer $server, $action = NULL) {
  323. if (!empty($action)) {
  324. if ($action == 'enable') {
  325. if (isset($_GET['token']) && drupal_valid_token($_GET['token'], $server->machine_name)) {
  326. if ($server->update(array('enabled' => 1))) {
  327. drupal_set_message(t('The server was successfully enabled.'));
  328. }
  329. else {
  330. drupal_set_message(t('The server could not be enabled. Check the logs for details.'), 'error');
  331. }
  332. drupal_goto('admin/config/search/search_api/server/' . $server->machine_name);
  333. }
  334. else {
  335. return MENU_ACCESS_DENIED;
  336. }
  337. }
  338. else {
  339. $ret = drupal_get_form('search_api_admin_confirm', 'server', $action, $server);
  340. if (!empty($ret['actions'])) {
  341. return $ret;
  342. }
  343. }
  344. }
  345. drupal_set_title(search_api_admin_item_title($server));
  346. $class = search_api_get_service_info($server->class);
  347. $options = $server->viewSettings();
  348. $indexes = array();
  349. foreach (search_api_index_load_multiple(FALSE, array('server' => $server->machine_name)) as $index) {
  350. if (!$indexes) {
  351. $indexes['#theme'] = 'links';
  352. $indexes['#attributes']['class'] = array('inline');
  353. }
  354. $indexes['#links'][] = array(
  355. 'title' => $index->name,
  356. 'href' => 'admin/config/search/search_api/index/' . $index->machine_name,
  357. );
  358. }
  359. $render['view'] = array(
  360. '#theme' => 'search_api_server',
  361. '#id' => $server->id,
  362. '#name' => $server->name,
  363. '#machine_name' => $server->machine_name,
  364. '#description' => $server->description,
  365. '#enabled' => $server->enabled,
  366. '#class_id' => $server->class,
  367. '#class_name' => $class['name'],
  368. '#class_description' => $class['description'],
  369. '#indexes' => $indexes,
  370. '#options' => $options,
  371. '#status' => $server->status,
  372. '#extra' => $server->getExtraInformation(),
  373. );
  374. $render['#attached']['css'][] = drupal_get_path('module', 'search_api') . '/search_api.admin.css';
  375. if ($server->enabled) {
  376. $render['form'] = drupal_get_form('search_api_server_status_form', $server);
  377. }
  378. return $render;
  379. }
  380. /**
  381. * Returns HTML for displaying a server.
  382. *
  383. * @param array $variables
  384. * An associative array containing:
  385. * - id: The server's id.
  386. * - name: The server's name.
  387. * - machine_name: The server's machine name.
  388. * - description: The server's description.
  389. * - enabled: Boolean indicating whether the server is enabled.
  390. * - class_id: The used service class' ID.
  391. * - class_name: The used service class' display name.
  392. * - class_description: The used service class' description.
  393. * - indexes: A list of indexes associated with this server, either as an HTML
  394. * string or a render array.
  395. * - options: An HTML string or render array containing information about the
  396. * server's service-specific settings.
  397. * - status: The entity configuration status (in database, in code, etc.).
  398. * - extra: An array of additional server information in the format specified
  399. * by SearchApiAbstractService::getExtraInformation().
  400. *
  401. * @return string
  402. * HTML for displaying a server.
  403. *
  404. * @ingroup themeable
  405. */
  406. function theme_search_api_server(array $variables) {
  407. $machine_name = $variables['machine_name'];
  408. $description = $variables['description'];
  409. $enabled = $variables['enabled'];
  410. $class_id = $variables['class_id'];
  411. $class_name = $variables['class_name'];
  412. $indexes = $variables['indexes'];
  413. $options = $variables['options'];
  414. $status = $variables['status'];
  415. $extra = $variables['extra'];
  416. // First, output the index description if there is one set.
  417. $output = '';
  418. if ($description) {
  419. $output .= '<p class="description">' . nl2br(check_plain($description)) . '</p>';
  420. }
  421. // Then, display a table summarizing the index's status.
  422. $rows = array();
  423. // Create a row template with references so we don't have to deal with the
  424. // complicated structure for each individual row.
  425. $row = array(
  426. 'data' => array(
  427. array('header' => TRUE),
  428. '',
  429. ),
  430. 'class' => array(''),
  431. );
  432. $label = & $row['data'][0]['data'];
  433. $info = & $row['data'][1];
  434. $class = & $row['class'][0];
  435. if ($enabled) {
  436. $class = 'ok';
  437. $info = t('enabled (!disable_link)', array('!disable_link' => l(t('disable'), 'admin/config/search/search_api/server/' . $machine_name . '/disable')));
  438. }
  439. else {
  440. $class = 'warning';
  441. $info = t('disabled (!enable_link)', array('!enable_link' => l(t('enable'), 'admin/config/search/search_api/server/' . $machine_name . '/enable', array('query' => array('token' => drupal_get_token($machine_name))))));
  442. }
  443. $label = t('Status');
  444. $rows[] = _search_api_deep_copy($row);
  445. $class = '';
  446. $label = t('Service class');
  447. if (module_exists('help')) {
  448. $url_options['fragment'] = drupal_clean_css_identifier($class_id);
  449. $info = l($class_name, 'admin/help/search_api', $url_options);
  450. }
  451. else {
  452. $info = check_plain($class_name);
  453. }
  454. $rows[] = _search_api_deep_copy($row);
  455. if ($indexes) {
  456. $label = t('Search indexes');
  457. $info = render($indexes);
  458. $rows[] = _search_api_deep_copy($row);
  459. }
  460. if ($options) {
  461. $label = t('Service options');
  462. $info = render($options);
  463. $rows[] = _search_api_deep_copy($row);
  464. }
  465. if ($status != ENTITY_CUSTOM) {
  466. $label = t('Configuration status');
  467. $info = theme('entity_status', array('status' => $status));
  468. $class = ($status == ENTITY_OVERRIDDEN) ? 'warning' : 'ok';
  469. $rows[] = _search_api_deep_copy($row);
  470. $class = '';
  471. }
  472. if ($extra) {
  473. foreach ($extra as $information) {
  474. $label = $information['label'];
  475. $info = $information['info'];
  476. $class = !empty($information['status']) ? $information['status'] : '';
  477. $rows[] = _search_api_deep_copy($row);
  478. }
  479. }
  480. $theme['rows'] = $rows;
  481. $theme['attributes']['class'][] = 'search-api-summary';
  482. $theme['attributes']['class'][] = 'search-api-server-summary';
  483. $theme['attributes']['class'][] = 'system-status-report';
  484. $output .= theme('table', $theme);
  485. return $output;
  486. }
  487. /**
  488. * Form constructor for completely clearing a server.
  489. *
  490. * @param SearchApiServer $server
  491. * The server for which the form is displayed.
  492. *
  493. * @ingroup forms
  494. *
  495. * @see search_api_server_status_form_submit()
  496. */
  497. function search_api_server_status_form(array $form, array &$form_state, SearchApiServer $server) {
  498. $form_state['server'] = $server;
  499. $form['clear'] = array(
  500. '#type' => 'submit',
  501. '#value' => t('Delete all indexed data on this server'),
  502. );
  503. return $form;
  504. }
  505. /**
  506. * Form submission handler for search_api_server_status_form().
  507. */
  508. function search_api_server_status_form_submit(array $form, array &$form_state) {
  509. $server_id = $form_state['server']->machine_name;
  510. $form_state['redirect'] = "admin/config/search/search_api/server/$server_id/clear";
  511. }
  512. /**
  513. * Form constructor for editing a server's settings.
  514. *
  515. * @param SearchApiServer $server
  516. * The server to edit.
  517. *
  518. * @ingroup forms
  519. *
  520. * @see search_api_admin_server_edit_validate()
  521. * @see search_api_admin_server_edit_submit()
  522. */
  523. function search_api_admin_server_edit(array $form, array &$form_state, SearchApiServer $server) {
  524. $form_state['server'] = $server;
  525. $form['name'] = array(
  526. '#type' => 'textfield',
  527. '#title' => t('Server name'),
  528. '#description' => t('Enter the displayed name for the server.'),
  529. '#maxlength' => 50,
  530. '#default_value' => $server->name,
  531. '#required' => TRUE,
  532. );
  533. $form['enabled'] = array(
  534. '#type' => 'checkbox',
  535. '#title' => t('Enabled'),
  536. '#default_value' => $server->enabled,
  537. );
  538. $form['description'] = array(
  539. '#type' => 'textarea',
  540. '#title' => t('Server description'),
  541. '#description' => t('Enter a description for the new server.'),
  542. '#default_value' => $server->description,
  543. );
  544. $class = search_api_get_service_info($server->class);
  545. $service_options = array();
  546. $service_options = $server->configurationForm($service_options, $form_state);
  547. if ($service_options) {
  548. $form['options']['form'] = $service_options;
  549. }
  550. $form['options']['#type'] = 'fieldset';
  551. $form['options']['#tree'] = TRUE;
  552. $form['options']['#collapsible'] = TRUE;
  553. $form['options']['#title'] = $class['name'];
  554. $form['options']['#description'] = $class['description'];
  555. $form['actions']['#type'] = 'actions';
  556. $form['actions']['submit'] = array(
  557. '#type' => 'submit',
  558. '#value' => t('Save settings'),
  559. );
  560. $form['actions']['delete'] = array(
  561. '#type' => 'submit',
  562. '#value' => t('Delete'),
  563. '#submit' => array('search_api_admin_form_delete_submit'),
  564. '#limit_validation_errors' => array(),
  565. );
  566. return $form;
  567. }
  568. /**
  569. * Form validation handler for search_api_admin_server_edit().
  570. *
  571. * @see search_api_admin_server_edit_submit()
  572. */
  573. function search_api_admin_server_edit_validate(array $form, array &$form_state) {
  574. $form_state['server']->configurationFormValidate($form['options']['form'], $form_state['values']['options']['form'], $form_state);
  575. }
  576. /**
  577. * Form submission handler for search_api_admin_server_edit().
  578. *
  579. * @see search_api_admin_server_edit_validate()
  580. */
  581. function search_api_admin_server_edit_submit(array $form, array &$form_state) {
  582. form_state_values_clean($form_state);
  583. $values = $form_state['values'];
  584. $server = $form_state['server'];
  585. if (isset($values['options'])) {
  586. $server->configurationFormSubmit($form['options']['form'], $values['options']['form'], $form_state);
  587. }
  588. unset($values['options']);
  589. $server->update($values);
  590. $form_state['redirect'] = 'admin/config/search/search_api/server/' . $server->machine_name;
  591. drupal_set_message(t('The search server was successfully edited.'));
  592. }
  593. /**
  594. * Form submission handler for search_api_admin_server_edit().
  595. *
  596. * Handles the 'Delete' button on the server and index edit forms.
  597. *
  598. * @see search_api_admin_server_edit()
  599. * @see search_api_admin_index_edit()
  600. */
  601. function search_api_admin_form_delete_submit($form, &$form_state) {
  602. $destination = array();
  603. if (isset($_GET['destination'])) {
  604. $destination = drupal_get_destination();
  605. unset($_GET['destination']);
  606. }
  607. if (isset($form_state['server'])) {
  608. $server = $form_state['server'];
  609. $form_state['redirect'] = array('admin/config/search/search_api/server/' . $server->machine_name . '/delete', array('query' => $destination));
  610. }
  611. elseif (isset($form_state['index'])) {
  612. $index = $form_state['index'];
  613. $form_state['redirect'] = array('admin/config/search/search_api/index/' . $index->machine_name . '/delete', array('query' => $destination));
  614. }
  615. }
  616. /**
  617. * Form constructor for adding an index.
  618. *
  619. * @ingroup forms
  620. *
  621. * @see search_api_admin_add_index_validate()
  622. * @see search_api_admin_add_index_submit()
  623. */
  624. function search_api_admin_add_index(array $form, array &$form_state) {
  625. drupal_set_title(t('Add index'));
  626. $form['#attached']['css'][] = drupal_get_path('module', 'search_api') . '/search_api.admin.css';
  627. $form['#tree'] = TRUE;
  628. $form['name'] = array(
  629. '#type' => 'textfield',
  630. '#title' => t('Index name'),
  631. '#maxlength' => 50,
  632. '#required' => TRUE,
  633. );
  634. $form['machine_name'] = array(
  635. '#type' => 'machine_name',
  636. '#maxlength' => 50,
  637. '#machine_name' => array(
  638. 'exists' => 'search_api_index_load',
  639. ),
  640. );
  641. $form['item_type'] = array(
  642. '#type' => 'select',
  643. '#title' => t('Item type'),
  644. '#description' => t('Select the type of items that will be indexed in this index. ' .
  645. 'This setting cannot be changed afterwards.'),
  646. '#options' => array(),
  647. '#required' => TRUE,
  648. );
  649. foreach (search_api_get_item_type_info() as $type => $info) {
  650. $form['item_type']['#options'][$type] = $info['name'];
  651. }
  652. $form['enabled'] = array(
  653. '#type' => 'checkbox',
  654. '#title' => t('Enabled'),
  655. '#description' => t('This will only take effect if the selected server is also enabled.'),
  656. '#default_value' => TRUE,
  657. );
  658. $form['description'] = array(
  659. '#type' => 'textarea',
  660. '#title' => t('Index description'),
  661. );
  662. $form['server'] = array(
  663. '#type' => 'select',
  664. '#title' => t('Server'),
  665. '#description' => t('Select the server this index should reside on.'),
  666. '#default_value' => '',
  667. '#options' => array('' => t('< No server >'))
  668. );
  669. $servers = search_api_server_load_multiple(FALSE, array('enabled' => 1));
  670. // List enabled servers first.
  671. foreach ($servers as $server) {
  672. $form['server']['#options'][$server->machine_name] = $server->name;
  673. }
  674. $form['read_only'] = array(
  675. '#type' => 'checkbox',
  676. '#title' => t('Read only'),
  677. '#description' => t('Do not write to this index or track the status of items in this index.'),
  678. '#default_value' => FALSE,
  679. );
  680. $form['options']['index_directly'] = array(
  681. '#type' => 'checkbox',
  682. '#title' => t('Index items immediately'),
  683. '#description' => t('Immediately index new or updated items instead of waiting for the next cron run. ' .
  684. 'This might have serious performance drawbacks and is generally not advised for larger sites.'),
  685. '#default_value' => FALSE,
  686. );
  687. $form['options']['cron_limit'] = array(
  688. '#type' => 'textfield',
  689. '#title' => t('Cron batch size'),
  690. '#description' => t('Set how many items will be indexed at once when indexing items during a cron run. ' .
  691. '"0" means that no items will be indexed by cron for this index, "-1" means that cron should index all items at once.'),
  692. '#default_value' => SEARCH_API_DEFAULT_CRON_LIMIT,
  693. '#size' => 4,
  694. '#attributes' => array('class' => array('search-api-cron-limit')),
  695. );
  696. $form['submit'] = array(
  697. '#type' => 'submit',
  698. '#value' => t('Create index'),
  699. );
  700. return $form;
  701. }
  702. /**
  703. * Form validation handler for search_api_admin_add_index().
  704. *
  705. * @see search_api_admin_add_index_submit()
  706. */
  707. function search_api_admin_add_index_validate(array $form, array &$form_state) {
  708. $name = $form_state['values']['machine_name'];
  709. if (is_numeric($name)) {
  710. form_set_error('machine_name', t('The machine name must not be a pure number.'));
  711. }
  712. $cron_limit = $form_state['values']['options']['cron_limit'];
  713. if ($cron_limit != '' . ((int) $cron_limit)) {
  714. // We don't enforce stricter rules and treat all negative values as -1.
  715. form_set_error('options[cron_limit]', t('The cron batch size must be an integer.'));
  716. }
  717. }
  718. /**
  719. * Form submission handler for search_api_admin_add_index().
  720. *
  721. * @see search_api_admin_add_index_validate()
  722. */
  723. function search_api_admin_add_index_submit(array $form, array &$form_state) {
  724. form_state_values_clean($form_state);
  725. $values = $form_state['values'];
  726. // Validation of whether the server of an index is enabled is done in the
  727. // SearchApiIndex::save() method.
  728. search_api_index_insert($values);
  729. drupal_set_message(t('The index was successfully created. Please set up its indexed fields now.'));
  730. $form_state['redirect'] = 'admin/config/search/search_api/index/' . $values['machine_name'] . '/fields';
  731. }
  732. /**
  733. * Page callback for displaying an index's status.
  734. *
  735. * @param SearchApiIndex $index
  736. * The index to display.
  737. * @param string|null $action
  738. * (optional) An action to execute for the index. One of "reindex", "clear",
  739. * "enable" or "disable". For "disable", a confirm dialog will be shown.
  740. *
  741. * @see search_api_menu()
  742. */
  743. function search_api_admin_index_view(SearchApiIndex $index, $action = NULL) {
  744. if (!empty($action)) {
  745. if ($action == 'enable') {
  746. if (isset($_GET['token']) && drupal_valid_token($_GET['token'], $index->machine_name)) {
  747. if ($index->update(array('enabled' => 1))) {
  748. drupal_set_message(t('The index was successfully enabled.'));
  749. }
  750. else {
  751. drupal_set_message(t('The index could not be enabled. Check the logs for details.'), 'error');
  752. }
  753. drupal_goto('admin/config/search/search_api/index/' . $index->machine_name);
  754. }
  755. else {
  756. return MENU_ACCESS_DENIED;
  757. }
  758. }
  759. else {
  760. $ret = drupal_get_form('search_api_admin_confirm', 'index', $action, $index);
  761. if (!empty($ret['actions'])) {
  762. return $ret;
  763. }
  764. }
  765. }
  766. $status = search_api_index_status($index);
  767. $ret['view'] = array(
  768. '#theme' => 'search_api_index',
  769. '#id' => $index->id,
  770. '#name' => $index->name,
  771. '#machine_name' => $index->machine_name,
  772. '#description' => $index->description,
  773. '#item_type' => $index->item_type,
  774. '#enabled' => $index->enabled,
  775. '#server' => $index->server(),
  776. '#options' => $index->options,
  777. '#fields' => $index->getFields(),
  778. '#indexed_items' => $status['indexed'],
  779. '#on_server' => _search_api_get_items_on_server($index),
  780. '#total_items' => $status['total'],
  781. '#status' => $index->status,
  782. '#read_only' => $index->read_only,
  783. );
  784. if ($index->enabled && !$index->read_only) {
  785. $ret['form'] = drupal_get_form('search_api_admin_index_status_form', $index, $status);
  786. }
  787. return $ret;
  788. }
  789. /**
  790. * Returns HTML for a search index.
  791. *
  792. * @param array $variables
  793. * An associative array containing:
  794. * - id: The index's id.
  795. * - name: The index' name.
  796. * - machine_name: The index' machine name.
  797. * - description: The index' description.
  798. * - item_type: The type of items stored in this index.
  799. * - enabled: Boolean indicating whether the index is enabled.
  800. * - server: The server this index currently rests on, if any.
  801. * - options: The index' options, like cron limit.
  802. * - fields: All indexed fields of the index.
  803. * - indexed_items: The number of items already indexed in their latest
  804. * version on this index.
  805. * - on_server: The number of items actually indexed on the server.
  806. * - total_items: The total number of items that have to be indexed for this
  807. * index.
  808. * - status: The entity configuration status (in database, in code, etc.).
  809. * - read_only: Boolean indicating whether this index is read only.
  810. *
  811. * @return string
  812. * HTML for a search index.
  813. *
  814. * @ingroup themeable
  815. */
  816. function theme_search_api_index(array $variables) {
  817. $machine_name = $variables['machine_name'];
  818. $description = $variables['description'];
  819. $enabled = $variables['enabled'];
  820. $item_type = $variables['item_type'];
  821. $server = $variables['server'];
  822. $options = $variables['options'];
  823. $status = $variables['status'];
  824. $indexed_items = $variables['indexed_items'];
  825. $on_server = $variables['on_server'];
  826. $total_items = $variables['total_items'];
  827. // First, output the index description if there is one set.
  828. $output = '';
  829. if ($description) {
  830. $output .= '<p class="description">' . nl2br(check_plain($description)) . '</p>';
  831. }
  832. // Then, display a table summarizing the index's status.
  833. $rows = array();
  834. // Create a row template with references so we don't have to deal with the
  835. // complicated structure for each individual row.
  836. $row = array(
  837. 'data' => array(
  838. array('header' => TRUE),
  839. '',
  840. ),
  841. 'class' => array(''),
  842. );
  843. $label = &$row['data'][0]['data'];
  844. $info = &$row['data'][1];
  845. $class = &$row['class'][0];
  846. $class = 'warning';
  847. if ($enabled) {
  848. $info = t('enabled (!disable_link)', array('!disable_link' => l(t('disable'), 'admin/config/search/search_api/index/' . $machine_name . '/disable')));
  849. $class = 'ok';
  850. }
  851. elseif ($server) {
  852. $info = t('disabled (!enable_link)', array('!enable_link' => l(t('enable'), 'admin/config/search/search_api/index/' . $machine_name . '/enable', array('query' => array('token' => drupal_get_token($machine_name))))));
  853. }
  854. else {
  855. $info = t('disabled');
  856. }
  857. $label = t('Status');
  858. $rows[] = _search_api_deep_copy($row);
  859. $class = '';
  860. $label = t('Item type');
  861. $type = search_api_get_item_type_info($item_type);
  862. $item_type = !empty($type['name']) ? $type['name'] : $item_type;
  863. $info = check_plain($item_type);
  864. $rows[] = _search_api_deep_copy($row);
  865. if ($server) {
  866. $label = t('Server');
  867. $info = l($server->name, 'admin/config/search/search_api/server/' . $server->machine_name);
  868. $rows[] = _search_api_deep_copy($row);
  869. }
  870. if ($enabled) {
  871. $options += array('cron_limit' => SEARCH_API_DEFAULT_CRON_LIMIT);
  872. if ($options['cron_limit']) {
  873. $class = 'ok';
  874. $info = format_plural(
  875. $options['cron_limit'],
  876. 'During cron runs, 1 item will be indexed per batch.',
  877. 'During cron runs, @count items will be indexed per batch.'
  878. );
  879. }
  880. else {
  881. $class = 'warning';
  882. $info = t('No items will be indexed during cron runs.');
  883. }
  884. $label = t('Cron batch size');
  885. $rows[] = _search_api_deep_copy($row);
  886. $theme = array(
  887. 'percent' => (int) (100 * $indexed_items / $total_items),
  888. 'message' => t('@indexed/@total indexed', array('@indexed' => $indexed_items, '@total' => $total_items)),
  889. );
  890. $output .= '<h3>' . t('Index status') . '</h3>';
  891. $output .= '<div class="search-api-index-status">' . theme('progress_bar', $theme) . '</div>';
  892. $vars['@url'] = url('https://drupal.org/node/2009804#server-index-status');
  893. $info = format_plural($on_server, 'There is 1 item indexed on the server for this index. (<a href="@url">More information</a>)', 'There are @count items indexed on the server for this index. (<a href="@url">More information</a>)', $vars);
  894. $class = '';
  895. $label = t('Server index status');
  896. $rows[] = _search_api_deep_copy($row);
  897. }
  898. if ($status != ENTITY_CUSTOM) {
  899. $label = t('Configuration status');
  900. $info = theme('entity_status', array('status' => $status));
  901. $class = ($status == ENTITY_OVERRIDDEN) ? 'warning' : 'ok';
  902. $rows[] = _search_api_deep_copy($row);
  903. }
  904. $theme['rows'] = $rows;
  905. $theme['attributes']['class'][] = 'search-api-summary';
  906. $theme['attributes']['class'][] = 'search-api-index-summary';
  907. $theme['attributes']['class'][] = 'system-status-report';
  908. $output .= theme('table', $theme);
  909. return $output;
  910. }
  911. /**
  912. * Form constructor for an index status form.
  913. *
  914. * Should only be used for enabled indexes which aren't read-only.
  915. *
  916. * @param SearchApiIndex $index
  917. * The index whose status should be displayed.
  918. * @param array $status
  919. * The indexing status of the index, as returned by search_api_index_status().
  920. *
  921. * @ingroup forms
  922. *
  923. * @see search_api_admin_index_status_form_validate()
  924. * @see search_api_admin_index_status_form_submit()
  925. */
  926. function search_api_admin_index_status_form(array $form, array &$form_state, SearchApiIndex $index, array $status) {
  927. $form['#attached']['css'][] = drupal_get_path('module', 'search_api') . '/search_api.admin.css';
  928. $form_state['index'] = $index;
  929. $form['index'] = array(
  930. '#type' => 'fieldset',
  931. '#title' => t('Index now'),
  932. );
  933. $form['index']['#attributes']['class'][] = 'container-inline';
  934. $allow_indexing = ($status['indexed'] < $status['total']);
  935. $all = t('all', array(), array('context' => 'items to index'));
  936. $limit = array(
  937. '#type' => 'textfield',
  938. '#default_value' => $all,
  939. '#size' => 4,
  940. '#attributes' => array('class' => array('search-api-limit')),
  941. '#disabled' => !$allow_indexing,
  942. );
  943. $batch_size = empty($index->options['cron_limit']) ? SEARCH_API_DEFAULT_CRON_LIMIT : $index->options['cron_limit'];
  944. $batch_size = $batch_size > 0 ? $batch_size : $all;
  945. $batch_size = array(
  946. '#type' => 'textfield',
  947. '#default_value' => $batch_size,
  948. '#size' => 4,
  949. '#attributes' => array('class' => array('search-api-batch-size')),
  950. '#disabled' => !$allow_indexing,
  951. );
  952. // Here it gets complicated. We want to build a sentence from the form input
  953. // elements, but to translate that we have to make the two form elements (for
  954. // limit and batch size) pseudo-variables in the t() call. Since we can't
  955. // pass them directly, we split the translated sentence (which still has the
  956. // two tokens), figure out their order and then put the pieces together again
  957. // using the form elements' #prefix and #suffix properties.
  958. $sentence = t('Index @limit items in batches of @batch_size items');
  959. $sentence = preg_split('/@(limit|batch_size)/', $sentence, -1, PREG_SPLIT_DELIM_CAPTURE);
  960. if (count($sentence) == 5) {
  961. $first = $sentence[1];
  962. $form['index'][$first] = $$first;
  963. $form['index'][$first]['#prefix'] = $sentence[0];
  964. $form['index'][$first]['#suffix'] = $sentence[2];
  965. $second = $sentence[3];
  966. $form['index'][$second] = $$second;
  967. $form['index'][$second]['#suffix'] = $sentence[4] . ' ';
  968. }
  969. else {
  970. // PANIC!
  971. $limit['#title'] = t('Number of items to index');
  972. $form['index']['limit'] = $limit;
  973. $batch_size['#title'] = t('Number of items per batch run');
  974. $form['index']['batch_size'] = $batch_size;
  975. }
  976. $form['index']['button'] = array(
  977. '#type' => 'submit',
  978. '#value' => t('Index now'),
  979. '#disabled' => !$allow_indexing,
  980. );
  981. $form['index']['total'] = array(
  982. '#type' => 'value',
  983. '#value' => $status['total'],
  984. );
  985. $form['index']['remaining'] = array(
  986. '#type' => 'value',
  987. '#value' => $status['total'] - $status['indexed'],
  988. );
  989. $form['index']['all'] = array(
  990. '#type' => 'value',
  991. '#value' => $all,
  992. );
  993. $form['reindex'] = array(
  994. '#type' => 'submit',
  995. '#value' => t('Queue all items for reindexing'),
  996. '#prefix' => '<div>',
  997. '#suffix' => '</div>',
  998. );
  999. $form['clear'] = array(
  1000. '#type' => 'submit',
  1001. '#value' => t('Clear all indexed data'),
  1002. '#prefix' => '<div>',
  1003. '#suffix' => '</div>',
  1004. );
  1005. return $form;
  1006. }
  1007. /**
  1008. * Form validation handler for search_api_admin_index_status_form().
  1009. *
  1010. * @see search_api_admin_index_status_form_submit()
  1011. */
  1012. function search_api_admin_index_status_form_validate(array $form, array &$form_state) {
  1013. $values = $form_state['values'];
  1014. if ($values['op'] == t('Index now')) {
  1015. $all_lower = drupal_strtolower($values['all']);
  1016. foreach (array('limit', 'batch_size') as $field) {
  1017. $val = trim($values[$field]);
  1018. if (drupal_strtolower($val) == $all_lower) {
  1019. $val = -1;
  1020. }
  1021. elseif (!$val || !is_numeric($val) || ((int) $val) != $val) {
  1022. form_error($form['index'][$field], t('Enter a non-zero integer. Use "-1" or "@all" for "all items".', array('@all' => $values['all'])));
  1023. }
  1024. else {
  1025. $val = (int) $val;
  1026. }
  1027. $form_state['values'][$field] = $val;
  1028. }
  1029. }
  1030. }
  1031. /**
  1032. * Form submission handler for search_api_admin_index_status_form().
  1033. *
  1034. * @see search_api_admin_index_status_form_validate()
  1035. */
  1036. function search_api_admin_index_status_form_submit(array $form, array &$form_state) {
  1037. $values = $form_state['values'];
  1038. $index = $form_state['index'];
  1039. $form_state['redirect'] = 'admin/config/search/search_api/index/' . $index->machine_name;
  1040. // There is a Form API bug here that will let a user submit the form via the
  1041. // "Index now" button even if it is disabled, and then just set "op" to the
  1042. // value of an arbitrary other button. We therefore have to take care to spot
  1043. // this case ourselves.
  1044. if ($form_state['input']['op'] == t('Index now') && !empty($form['index']['button']['#disabled'])) {
  1045. drupal_set_message(t('All items have already been indexed.'), 'warning');
  1046. return;
  1047. }
  1048. switch ($values['op']) {
  1049. case t('Index now'):
  1050. if (!_search_api_batch_indexing_create($index, $values['batch_size'], $values['limit'], $values['remaining'])) {
  1051. drupal_set_message(t("Couldn't create a batch, please check the batch size and limit."), 'warning');
  1052. }
  1053. break;
  1054. case t('Queue all items for reindexing'):
  1055. $form_state['redirect'] .= '/reindex';
  1056. break;
  1057. case t('Clear all indexed data'):
  1058. $form_state['redirect'] .= '/clear';
  1059. break;
  1060. }
  1061. }
  1062. /**
  1063. * Form constructor for editing an index's settings.
  1064. *
  1065. * @param SearchApiIndex $index
  1066. * The index to edit.
  1067. *
  1068. * @ingroup forms
  1069. *
  1070. * @see search_api_admin_index_edit_submit()
  1071. */
  1072. function search_api_admin_index_edit(array $form, array &$form_state, SearchApiIndex $index) {
  1073. $form_state['index'] = $index;
  1074. $form['#attached']['css'][] = drupal_get_path('module', 'search_api') . '/search_api.admin.css';
  1075. $form['#tree'] = TRUE;
  1076. $form['name'] = array(
  1077. '#type' => 'textfield',
  1078. '#title' => t('Index name'),
  1079. '#maxlength' => 50,
  1080. '#default_value' => $index->name,
  1081. '#required' => TRUE,
  1082. );
  1083. $form['enabled'] = array(
  1084. '#type' => 'checkbox',
  1085. '#title' => t('Enabled'),
  1086. '#default_value' => $index->enabled,
  1087. // Can't enable an index lying on a disabled server, or no server at all.
  1088. '#disabled' => !$index->enabled && (!$index->server() || !$index->server()->enabled),
  1089. );
  1090. $form['description'] = array(
  1091. '#type' => 'textarea',
  1092. '#title' => t('Index description'),
  1093. '#default_value' => $index->description,
  1094. );
  1095. $form['server'] = array(
  1096. '#type' => 'select',
  1097. '#title' => t('Server'),
  1098. '#description' => t('Select the server this index should reside on.'),
  1099. '#default_value' => $index->server,
  1100. '#options' => array('' => t('< No server >'))
  1101. );
  1102. $servers = search_api_server_load_multiple(FALSE, array('enabled' => 1));
  1103. // List enabled servers first.
  1104. foreach ($servers as $server) {
  1105. $form['server']['#options'][$server->machine_name] = $server->name;
  1106. }
  1107. $form['read_only'] = array(
  1108. '#type' => 'checkbox',
  1109. '#title' => t('Read only'),
  1110. '#description' => t('Do not write to this index or track the status of items in this index.'),
  1111. '#default_value' => $index->read_only,
  1112. );
  1113. $form['options']['index_directly'] = array(
  1114. '#type' => 'checkbox',
  1115. '#title' => t('Index items immediately'),
  1116. '#description' => t('Immediately index new or updated items instead of waiting for the next cron run. ' .
  1117. 'This might have serious performance drawbacks and is generally not advised for larger sites.'),
  1118. '#default_value' => !empty($index->options['index_directly']),
  1119. '#states' => array(
  1120. 'invisible' => array(':input[name="read_only"]' => array('checked' => TRUE)),
  1121. ),
  1122. );
  1123. $form['options']['cron_limit'] = array(
  1124. '#type' => 'textfield',
  1125. '#title' => t('Cron batch size'),
  1126. '#description' => t('Set how many items will be indexed at once when indexing items during a cron run. ' .
  1127. '"0" means that no items will be indexed by cron for this index, "-1" means that cron should index all items at once.'),
  1128. '#default_value' => isset($index->options['cron_limit']) ? $index->options['cron_limit'] : SEARCH_API_DEFAULT_CRON_LIMIT,
  1129. '#size' => 4,
  1130. '#attributes' => array('class' => array('search-api-cron-limit')),
  1131. '#element_validate' => array('_element_validate_integer'),
  1132. '#states' => array(
  1133. 'invisible' => array(':input[name="read_only"]' => array('checked' => TRUE)),
  1134. ),
  1135. );
  1136. $form['actions']['#type'] = 'actions';
  1137. $form['actions']['submit'] = array(
  1138. '#type' => 'submit',
  1139. '#value' => t('Save settings'),
  1140. );
  1141. $form['actions']['delete'] = array(
  1142. '#type' => 'submit',
  1143. '#value' => t('Delete'),
  1144. '#submit' => array('search_api_admin_form_delete_submit'),
  1145. '#limit_validation_errors' => array(),
  1146. );
  1147. return $form;
  1148. }
  1149. /**
  1150. * Form submission handler for search_api_admin_index_edit().
  1151. */
  1152. function search_api_admin_index_edit_submit(array $form, array &$form_state) {
  1153. form_state_values_clean($form_state);
  1154. $values = $form_state['values'];
  1155. $index = $form_state['index'];
  1156. $values['options'] += $index->options;
  1157. $ret = $index->update($values);
  1158. $form_state['redirect'] = 'admin/config/search/search_api/index/' . $index->machine_name;
  1159. if ($ret) {
  1160. drupal_set_message(t('The search index was successfully edited.'));
  1161. }
  1162. else {
  1163. drupal_set_message(t('No values were changed.'));
  1164. }
  1165. }
  1166. /**
  1167. * Form constructor for editing an index's data alterations and processors.
  1168. *
  1169. * @param SearchApiIndex $index
  1170. * The index to edit.
  1171. *
  1172. * @ingroup forms
  1173. *
  1174. * @see search_api_admin_index_workflow_validate()
  1175. * @see search_api_admin_index_workflow_submit()
  1176. */
  1177. // Copied from filter_admin_format_form()
  1178. function search_api_admin_index_workflow(array $form, array &$form_state, SearchApiIndex $index) {
  1179. $callback_info = search_api_get_alter_callbacks();
  1180. $processor_info = search_api_get_processors();
  1181. $options = empty($index->options) ? array() : $index->options;
  1182. $form_state['index'] = $index;
  1183. $form['#tree'] = TRUE;
  1184. $form['#attached']['js'][] = drupal_get_path('module', 'search_api') . '/search_api.admin.js';
  1185. // Callbacks
  1186. $callbacks = empty($options['data_alter_callbacks']) ? array() : $options['data_alter_callbacks'];
  1187. $callback_objects = isset($form_state['callbacks']) ? $form_state['callbacks'] : array();
  1188. foreach ($callback_info as $name => $callback) {
  1189. if (!isset($callbacks[$name])) {
  1190. $callbacks[$name]['status'] = 0;
  1191. $callbacks[$name]['weight'] = $callback['weight'];
  1192. }
  1193. $settings = empty($callbacks[$name]['settings']) ? array() : $callbacks[$name]['settings'];
  1194. if (empty($callback_objects[$name]) && class_exists($callback['class'])) {
  1195. $callback_objects[$name] = new $callback['class']($index, $settings);
  1196. }
  1197. if (!(class_exists($callback['class']) && $callback_objects[$name] instanceof SearchApiAlterCallbackInterface)) {
  1198. watchdog('search_api', t('Data alteration @id specifies illegal callback class @class.', array('@id' => $name, '@class' => $callback['class'])), NULL, WATCHDOG_WARNING);
  1199. unset($callback_info[$name]);
  1200. unset($callbacks[$name]);
  1201. unset($callback_objects[$name]);
  1202. continue;
  1203. }
  1204. if (!$callback_objects[$name]->supportsIndex($index)) {
  1205. unset($callback_info[$name]);
  1206. unset($callbacks[$name]);
  1207. unset($callback_objects[$name]);
  1208. continue;
  1209. }
  1210. }
  1211. $form_state['callbacks'] = $callback_objects;
  1212. $form['#callbacks'] = $callbacks;
  1213. $form['callbacks'] = array(
  1214. '#type' => 'fieldset',
  1215. '#title' => t('Data alterations'),
  1216. '#description' => t('Select the alterations that will be executed on indexed items, and their order.'),
  1217. '#collapsible' => TRUE,
  1218. );
  1219. // Callback status.
  1220. $form['callbacks']['status'] = array(
  1221. '#type' => 'item',
  1222. '#title' => t('Enabled data alterations'),
  1223. '#prefix' => '<div class="search-api-status-wrapper">',
  1224. '#suffix' => '</div>',
  1225. );
  1226. foreach ($callback_info as $name => $callback) {
  1227. $form['callbacks']['status'][$name] = array(
  1228. '#type' => 'checkbox',
  1229. '#title' => $callback['name'],
  1230. '#default_value' => $callbacks[$name]['status'],
  1231. '#parents' => array('callbacks', $name, 'status'),
  1232. '#description' => $callback['description'],
  1233. '#weight' => $callback['weight'],
  1234. );
  1235. }
  1236. // Callback order (tabledrag).
  1237. $form['callbacks']['order'] = array(
  1238. '#type' => 'item',
  1239. '#title' => t('Data alteration processing order'),
  1240. '#theme' => 'search_api_admin_item_order',
  1241. '#table_id' => 'search-api-callbacks-order-table',
  1242. );
  1243. foreach ($callback_info as $name => $callback) {
  1244. $form['callbacks']['order'][$name]['item'] = array(
  1245. '#markup' => $callback['name'],
  1246. );
  1247. $form['callbacks']['order'][$name]['weight'] = array(
  1248. '#type' => 'weight',
  1249. '#delta' => 50,
  1250. '#default_value' => $callbacks[$name]['weight'],
  1251. '#parents' => array('callbacks', $name, 'weight'),
  1252. );
  1253. $form['callbacks']['order'][$name]['#weight'] = $callbacks[$name]['weight'];
  1254. }
  1255. // Callback settings.
  1256. $form['callbacks']['settings_title'] = array(
  1257. '#type' => 'item',
  1258. '#title' => t('Callback settings'),
  1259. );
  1260. $form['callbacks']['settings'] = array(
  1261. '#type' => 'vertical_tabs',
  1262. );
  1263. foreach ($callback_info as $name => $callback) {
  1264. $settings_form = $callback_objects[$name]->configurationForm();
  1265. if (!empty($settings_form)) {
  1266. $form['callbacks']['settings'][$name] = array(
  1267. '#type' => 'fieldset',
  1268. '#title' => $callback['name'],
  1269. '#parents' => array('callbacks', $name, 'settings'),
  1270. '#weight' => $callback['weight'],
  1271. );
  1272. $form['callbacks']['settings'][$name] += $settings_form;
  1273. }
  1274. }
  1275. // Processors
  1276. $processors = empty($options['processors']) ? array() : $options['processors'];
  1277. $processor_objects = isset($form_state['processors']) ? $form_state['processors'] : array();
  1278. foreach ($processor_info as $name => $processor) {
  1279. if (!isset($processors[$name])) {
  1280. $processors[$name]['status'] = 0;
  1281. $processors[$name]['weight'] = $processor['weight'];
  1282. }
  1283. $settings = empty($processors[$name]['settings']) ? array() : $processors[$name]['settings'];
  1284. if (empty($processor_objects[$name]) && class_exists($processor['class'])) {
  1285. $processor_objects[$name] = new $processor['class']($index, $settings);
  1286. }
  1287. if (!(class_exists($processor['class']) && $processor_objects[$name] instanceof SearchApiProcessorInterface)) {
  1288. watchdog('search_api', t('Processor @id specifies illegal processor class @class.', array('@id' => $name, '@class' => $processor['class'])), NULL, WATCHDOG_WARNING);
  1289. unset($processor_info[$name]);
  1290. unset($processors[$name]);
  1291. unset($processor_objects[$name]);
  1292. continue;
  1293. }
  1294. if (!$processor_objects[$name]->supportsIndex($index)) {
  1295. unset($processor_info[$name]);
  1296. unset($processors[$name]);
  1297. unset($processor_objects[$name]);
  1298. continue;
  1299. }
  1300. }
  1301. $form_state['processors'] = $processor_objects;
  1302. $form['#processors'] = $processors;
  1303. $form['processors'] = array(
  1304. '#type' => 'fieldset',
  1305. '#title' => t('Processors'),
  1306. '#description' => t('Select processors which will pre- and post-process data at index and search time, and their order. ' .
  1307. 'Most processors will only influence fulltext fields, but refer to their individual descriptions for details regarding their effect.'),
  1308. '#collapsible' => TRUE,
  1309. );
  1310. // Processor status.
  1311. $form['processors']['status'] = array(
  1312. '#type' => 'item',
  1313. '#title' => t('Enabled processors'),
  1314. '#prefix' => '<div class="search-api-status-wrapper">',
  1315. '#suffix' => '</div>',
  1316. );
  1317. foreach ($processor_info as $name => $processor) {
  1318. $form['processors']['status'][$name] = array(
  1319. '#type' => 'checkbox',
  1320. '#title' => $processor['name'],
  1321. '#default_value' => $processors[$name]['status'],
  1322. '#parents' => array('processors', $name, 'status'),
  1323. '#description' => $processor['description'],
  1324. '#weight' => $processor['weight'],
  1325. );
  1326. }
  1327. // Processor order (tabledrag).
  1328. $form['processors']['order'] = array(
  1329. '#type' => 'item',
  1330. '#title' => t('Processor processing order'),
  1331. '#description' => t('Set the order in which preprocessing will be done at index and search time. ' .
  1332. 'Postprocessing of search results will be in the exact opposite direction.'),
  1333. '#theme' => 'search_api_admin_item_order',
  1334. '#table_id' => 'search-api-processors-order-table',
  1335. );
  1336. foreach ($processor_info as $name => $processor) {
  1337. $form['processors']['order'][$name]['item'] = array(
  1338. '#markup' => $processor['name'],
  1339. );
  1340. $form['processors']['order'][$name]['weight'] = array(
  1341. '#type' => 'weight',
  1342. '#delta' => 50,
  1343. '#default_value' => $processors[$name]['weight'],
  1344. '#parents' => array('processors', $name, 'weight'),
  1345. );
  1346. $form['processors']['order'][$name]['#weight'] = $processors[$name]['weight'];
  1347. }
  1348. // Processor settings.
  1349. $form['processors']['settings_title'] = array(
  1350. '#type' => 'item',
  1351. '#title' => t('Processor settings'),
  1352. );
  1353. $form['processors']['settings'] = array(
  1354. '#type' => 'vertical_tabs',
  1355. );
  1356. foreach ($processor_info as $name => $processor) {
  1357. $settings_form = $processor_objects[$name]->configurationForm();
  1358. if (!empty($settings_form)) {
  1359. $form['processors']['settings'][$name] = array(
  1360. '#type' => 'fieldset',
  1361. '#title' => $processor['name'],
  1362. '#parents' => array('processors', $name, 'settings'),
  1363. '#weight' => $processor['weight'],
  1364. );
  1365. $form['processors']['settings'][$name] += $settings_form;
  1366. }
  1367. }
  1368. $form['actions'] = array('#type' => 'actions');
  1369. $form['actions']['submit'] = array('#type' => 'submit', '#value' => t('Save configuration'));
  1370. return $form;
  1371. }
  1372. /**
  1373. * Returns HTML for a processor/callback order form.
  1374. *
  1375. * @param array $variables
  1376. * An associative array containing:
  1377. * - element: A render element representing the form.
  1378. */
  1379. function theme_search_api_admin_item_order(array $variables) {
  1380. $element = $variables['element'];
  1381. $rows = array();
  1382. foreach (element_children($element, TRUE) as $name) {
  1383. $element[$name]['weight']['#attributes']['class'][] = 'search-api-order-weight';
  1384. $rows[] = array(
  1385. 'data' => array(
  1386. drupal_render($element[$name]['item']),
  1387. drupal_render($element[$name]['weight']),
  1388. ),
  1389. 'class' => array('draggable'),
  1390. );
  1391. }
  1392. $output = drupal_render_children($element);
  1393. $output .= theme('table', array('rows' => $rows, 'attributes' => array('id' => $element['#table_id'])));
  1394. drupal_add_tabledrag($element['#table_id'], 'order', 'sibling', 'search-api-order-weight', NULL, NULL, TRUE);
  1395. return $output;
  1396. }
  1397. /**
  1398. * Form validation handler for search_api_admin_index_workflow().
  1399. *
  1400. * @see search_api_admin_index_workflow_submit()
  1401. */
  1402. function search_api_admin_index_workflow_validate(array $form, array &$form_state) {
  1403. // Call validation functions.
  1404. foreach ($form_state['callbacks'] as $name => $callback) {
  1405. if (isset($form['callbacks']['settings'][$name]) && isset($form_state['values']['callbacks'][$name]['settings'])) {
  1406. $callback->configurationFormValidate($form['callbacks']['settings'][$name], $form_state['values']['callbacks'][$name]['settings'], $form_state);
  1407. }
  1408. }
  1409. foreach ($form_state['processors'] as $name => $processor) {
  1410. if (isset($form['processors']['settings'][$name]) && isset($form_state['values']['processors'][$name]['settings'])) {
  1411. $processor->configurationFormValidate($form['processors']['settings'][$name], $form_state['values']['processors'][$name]['settings'], $form_state);
  1412. }
  1413. }
  1414. }
  1415. /**
  1416. * Form submission handler for search_api_admin_index_workflow().
  1417. *
  1418. * @see search_api_admin_index_workflow_validate()
  1419. */
  1420. function search_api_admin_index_workflow_submit(array $form, array &$form_state) {
  1421. $values = $form_state['values'];
  1422. unset($values['callbacks']['settings']);
  1423. unset($values['processors']['settings']);
  1424. $index = $form_state['index'];
  1425. $options = empty($index->options) ? array() : $index->options;
  1426. // Store callback and processor settings.
  1427. foreach ($form_state['callbacks'] as $name => $callback) {
  1428. $callback_form = isset($form['callbacks']['settings'][$name]) ? $form['callbacks']['settings'][$name] : array();
  1429. $values['callbacks'][$name] += array('settings' => array());
  1430. $values['callbacks'][$name]['settings'] = $callback->configurationFormSubmit($callback_form, $values['callbacks'][$name]['settings'], $form_state);
  1431. }
  1432. foreach ($form_state['processors'] as $name => $processor) {
  1433. $processor_form = isset($form['processors']['settings'][$name]) ? $form['processors']['settings'][$name] : array();
  1434. $values['processors'][$name] += array('settings' => array());
  1435. $values['processors'][$name]['settings'] = $processor->configurationFormSubmit($processor_form, $values['processors'][$name]['settings'], $form_state);
  1436. }
  1437. $types = search_api_field_types();
  1438. foreach ($form_state['callbacks'] as $name => $callback) {
  1439. // Check whether callback status has changed.
  1440. if ($values['callbacks'][$name]['status'] == empty($options['data_alter_callbacks'][$name]['status'])) {
  1441. if ($values['callbacks'][$name]['status']) {
  1442. // Callback was just enabled, add its fields.
  1443. $properties = $callback->propertyInfo();
  1444. if ($properties) {
  1445. foreach ($properties as $key => $field) {
  1446. $type = $field['type'];
  1447. $inner = search_api_extract_inner_type($type);
  1448. if ($inner != 'token' && empty($types[$inner])) {
  1449. // Someone apparently added a structure or entity as a property in
  1450. // a data alteration.
  1451. continue;
  1452. }
  1453. if ($inner == 'token' || (search_api_is_text_type($inner) && !empty($field['options list']))) {
  1454. $old = $type;
  1455. $type = 'string';
  1456. while (search_api_is_list_type($old)) {
  1457. $old = substr($old, 5, -1);
  1458. $type = "list<$type>";
  1459. }
  1460. }
  1461. $index->options['fields'][$key] = array(
  1462. 'type' => $type,
  1463. );
  1464. }
  1465. }
  1466. }
  1467. else {
  1468. // Callback was just disabled, remove its fields.
  1469. $properties = $callback->propertyInfo();
  1470. if ($properties) {
  1471. foreach ($properties as $key => $field) {
  1472. unset($index->options['fields'][$key]);
  1473. }
  1474. }
  1475. }
  1476. }
  1477. }
  1478. if (!isset($options['data_alter_callbacks']) || !isset($options['processors'])
  1479. || $options['data_alter_callbacks'] != $values['callbacks']
  1480. || $options['processors'] != $values['processors']) {
  1481. $index->options['data_alter_callbacks'] = $values['callbacks'];
  1482. $index->options['processors'] = $values['processors'];
  1483. // Save the already sorted arrays to avoid having to sort them at each use.
  1484. uasort($index->options['data_alter_callbacks'], 'search_api_admin_element_compare');
  1485. uasort($index->options['processors'], 'search_api_admin_element_compare');
  1486. // Reset the index's internal property cache to correctly incorporate the
  1487. // new data alterations.
  1488. $index->resetCaches();
  1489. $index->save();
  1490. $index->reindex();
  1491. drupal_set_message(t("The indexing workflow was successfully edited. All content was scheduled for re-indexing so the new settings can take effect."));
  1492. }
  1493. else {
  1494. drupal_set_message(t('No values were changed.'));
  1495. }
  1496. $form_state['redirect'] = 'admin/config/search/search_api/index/' . $index->machine_name . '/workflow';
  1497. }
  1498. /**
  1499. * Sort callback sorting array elements by their "weight" key, if present.
  1500. *
  1501. * @see element_sort()
  1502. */
  1503. function search_api_admin_element_compare($a, $b) {
  1504. $a_weight = (is_array($a) && isset($a['weight'])) ? $a['weight'] : 0;
  1505. $b_weight = (is_array($b) && isset($b['weight'])) ? $b['weight'] : 0;
  1506. if ($a_weight == $b_weight) {
  1507. return 0;
  1508. }
  1509. return ($a_weight < $b_weight) ? -1 : 1;
  1510. }
  1511. /**
  1512. * Form constructor for setting the indexed fields.
  1513. *
  1514. * @ingroup forms
  1515. *
  1516. * @see search_api_admin_index_fields_submit()
  1517. */
  1518. function search_api_admin_index_fields(array $form, array &$form_state, SearchApiIndex $index) {
  1519. $options = $index->getFields(FALSE, TRUE);
  1520. $fields = $options['fields'];
  1521. $additional = $options['additional fields'];
  1522. // An array of option arrays for types, keyed by nesting level.
  1523. $types = array(0 => search_api_field_types());
  1524. $entity_types = entity_get_info();
  1525. //$boosts = drupal_map_assoc(array('0.1', '0.2', '0.3', '0.5', '0.8', '1.0', '2.0', '3.0', '5.0', '8.0', '13.0', '21.0'));
  1526. $boosts = drupal_map_assoc(array('0.1', '0.2', '0.3', '0.5', '0.8', '1.0', '2.0', '3.0', '5.0', '8.0', '13.0', '21.0', '100', '1000', '1010', '1020', '1030', '1040', '1050', '1060'));
  1527. $fulltext_types = array(0 => array('text'));
  1528. // Add all custom data types with fallback "text" to fulltext types as well.
  1529. foreach (search_api_get_data_type_info() as $id => $type) {
  1530. if ($type['fallback'] != 'text') {
  1531. continue;
  1532. }
  1533. $fulltext_types[0][] = $id;
  1534. }
  1535. $form_state['index'] = $index;
  1536. $form['#theme'] = 'search_api_admin_fields_table';
  1537. $form['#tree'] = TRUE;
  1538. $form['description'] = array(
  1539. '#type' => 'item',
  1540. '#title' => t('Select fields to index'),
  1541. '#description' => t('<p>The datatype of a field determines how it can be used for searching and filtering. Fields indexed with type "Fulltext" and multi-valued fields (marked with <sup>1</sup>) cannot be used for sorting. ' .
  1542. 'The boost is used to give additional weight to certain fields, e.g. titles or tags. It only takes effect for fulltext fields.</p>' .
  1543. '<p>Whether detailed field types are supported depends on the type of server this index resides on. ' .
  1544. 'In any case, fields of type "Fulltext" will always be fulltext-searchable.</p>'),
  1545. );
  1546. if ($index->server) {
  1547. $form['description']['#description'] .= '<p>' . t('Check the <a href="@server-url">' . "server's</a> service class description for details.",
  1548. array('@server-url' => url('admin/config/search/search_api/server/' . $index->server))) . '</p>';
  1549. }
  1550. foreach ($fields as $key => $info) {
  1551. $form['fields'][$key]['title']['#markup'] = check_plain($info['name']);
  1552. if (search_api_is_list_type($info['type'])) {
  1553. $form['fields'][$key]['title']['#markup'] .= ' <sup><a href="#note-multi-valued" class="note-ref">1</a></sup>';
  1554. $multi_valued_field_present = TRUE;
  1555. }
  1556. $form['fields'][$key]['machine_name']['#markup'] = check_plain($key);
  1557. if (isset($info['description'])) {
  1558. $form['fields'][$key]['description'] = array(
  1559. '#type' => 'value',
  1560. '#value' => $info['description'],
  1561. );
  1562. }
  1563. $form['fields'][$key]['indexed'] = array(
  1564. '#type' => 'checkbox',
  1565. '#default_value' => $info['indexed'],
  1566. );
  1567. if (empty($info['entity_type'])) {
  1568. // Determine the correct type options (with the correct nesting level).
  1569. $level = search_api_list_nesting_level($info['type']);
  1570. if (empty($types[$level])) {
  1571. $type_prefix = str_repeat('list<', $level);
  1572. $type_suffix = str_repeat('>', $level);
  1573. $types[$level] = array();
  1574. foreach ($types[0] as $type => $name) {
  1575. // We use the singular name for list types, since the user usually
  1576. // doesn't care about the nesting level.
  1577. $types[$level][$type_prefix . $type . $type_suffix] = $name;
  1578. }
  1579. foreach ($fulltext_types[0] as $type) {
  1580. $fulltext_types[$level][] = $type_prefix . $type . $type_suffix;
  1581. }
  1582. }
  1583. $css_key = '#edit-fields-' . drupal_clean_css_identifier($key);
  1584. $form['fields'][$key]['type'] = array(
  1585. '#type' => 'select',
  1586. '#options' => $types[$level],
  1587. '#default_value' => isset($info['real_type']) ? $info['real_type'] : $info['type'],
  1588. '#states' => array(
  1589. 'visible' => array(
  1590. $css_key . '-indexed' => array('checked' => TRUE),
  1591. ),
  1592. ),
  1593. );
  1594. $form['fields'][$key]['boost'] = array(
  1595. '#type' => 'select',
  1596. '#options' => $boosts,
  1597. '#default_value' => $info['boost'],
  1598. '#states' => array(
  1599. 'visible' => array(
  1600. $css_key . '-indexed' => array('checked' => TRUE),
  1601. ),
  1602. ),
  1603. );
  1604. // Only add the multiple visible states if the VERSION string is >= 7.14.
  1605. // See https://drupal.org/node/1464758.
  1606. if (version_compare(VERSION, '7.14', '>=')) {
  1607. foreach ($fulltext_types[$level] as $type) {
  1608. $form['fields'][$key]['boost']['#states']['visible'][$css_key . '-type'][] = array('value' => $type);
  1609. }
  1610. }
  1611. else {
  1612. $form['fields'][$key]['boost']['#states']['visible'][$css_key . '-type'] = array('value' => reset($fulltext_types[$level]));
  1613. }
  1614. }
  1615. else {
  1616. // This is an entity.
  1617. $label = $entity_types[$info['entity_type']]['label'];
  1618. if (!isset($entity_description_added)) {
  1619. $form['description']['#description'] .= '<p>' .
  1620. t('Note that indexing an entity-valued field (like %field, which has type %type) directly will only index the entity ID. ' .
  1621. 'This will be used for filtering and also sorting (which might not be what you expect). ' .
  1622. 'The entity label will usually be used when displaying the field, though. ' .
  1623. 'Use the "Add related fields" option at the bottom for indexing other fields of related entities.',
  1624. array('%field' => $info['name'], '%type' => $label)) . '</p>';
  1625. $entity_description_added = TRUE;
  1626. }
  1627. $form['fields'][$key]['type'] = array(
  1628. '#type' => 'value',
  1629. '#value' => $info['type'],
  1630. );
  1631. $form['fields'][$key]['entity_type'] = array(
  1632. '#type' => 'value',
  1633. '#value' => $info['entity_type'],
  1634. );
  1635. $form['fields'][$key]['type_name'] = array(
  1636. '#markup' => check_plain($label),
  1637. );
  1638. $form['fields'][$key]['boost'] = array(
  1639. '#type' => 'value',
  1640. '#value' => $info['boost'],
  1641. );
  1642. $form['fields'][$key]['boost_text'] = array(
  1643. '#markup' => '&nbsp;',
  1644. );
  1645. }
  1646. if ($key == 'search_api_language') {
  1647. // Is treated specially to always index the language.
  1648. $form['fields'][$key]['type']['#default_value'] = 'string';
  1649. $form['fields'][$key]['type']['#disabled'] = TRUE;
  1650. $form['fields'][$key]['boost']['#default_value'] = '1.0';
  1651. $form['fields'][$key]['boost']['#disabled'] = TRUE;
  1652. $form['fields'][$key]['indexed']['#default_value'] = 1;
  1653. $form['fields'][$key]['indexed']['#disabled'] = TRUE;
  1654. }
  1655. }
  1656. if (!empty($multi_valued_field_present)) {
  1657. $form['note']['#markup'] = '<div id="note-multi-valued"><small><sup>1</sup> ' . t('Multi-valued field') . '</small></div>';
  1658. }
  1659. $form['submit'] = array(
  1660. '#type' => 'submit',
  1661. '#value' => t('Save changes'),
  1662. );
  1663. if ($additional) {
  1664. reset($additional);
  1665. $form['additional'] = array(
  1666. '#type' => 'fieldset',
  1667. '#title' => t('Add related fields'),
  1668. '#description' => t('There are entities related to entities of this type. ' .
  1669. 'You can add their fields to the list above so they can be indexed too.') . '<br />',
  1670. '#collapsible' => TRUE,
  1671. '#collapsed' => TRUE,
  1672. '#attributes' => array('class' => array('container-inline')),
  1673. 'field' => array(
  1674. '#type' => 'select',
  1675. '#options' => $additional,
  1676. '#default_value' => key($additional),
  1677. ),
  1678. 'add' => array(
  1679. '#type' => 'submit',
  1680. '#value' => t('Add fields'),
  1681. ),
  1682. );
  1683. }
  1684. return $form;
  1685. }
  1686. /**
  1687. * Helper function for building the field list for an index.
  1688. *
  1689. * @deprecated Use SearchApiIndex::getFields() instead.
  1690. */
  1691. function _search_api_admin_get_fields(SearchApiIndex $index, EntityMetadataWrapper $wrapper) {
  1692. $fields = empty($index->options['fields']) ? array() : $index->options['fields'];
  1693. $additional = array();
  1694. $entity_types = entity_get_info();
  1695. // First we need all already added prefixes.
  1696. $added = array();
  1697. foreach (array_keys($fields) as $key) {
  1698. $key = substr($key, 0, strrpos($key, ':'));
  1699. $added[$key] = TRUE;
  1700. }
  1701. // Then we walk through all properties and look if they are already contained
  1702. // in one of the arrays. Since this uses an iterative instead of a recursive
  1703. // approach, it is a bit complicated, with three arrays tracking the current
  1704. // depth.
  1705. // A wrapper for a specific field name prefix, e.g. 'user:' mapped to the user
  1706. // wrapper
  1707. $wrappers = array('' => $wrapper);
  1708. // Display names for the prefixes
  1709. $prefix_names = array('' => '');
  1710. // The list nesting level for entities with a certain prefix
  1711. $nesting_levels = array('' => 0);
  1712. $types = search_api_default_field_types();
  1713. $flat = array();
  1714. while ($wrappers) {
  1715. foreach ($wrappers as $prefix => $wrapper) {
  1716. $prefix_name = $prefix_names[$prefix];
  1717. // Deal with lists of entities.
  1718. $nesting_level = $nesting_levels[$prefix];
  1719. $type_prefix = str_repeat('list<', $nesting_level);
  1720. $type_suffix = str_repeat('>', $nesting_level);
  1721. if ($nesting_level) {
  1722. $info = $wrapper->info();
  1723. // The real nesting level of the wrapper, not the accumulated one.
  1724. $level = search_api_list_nesting_level($info['type']);
  1725. for ($i = 0; $i < $level; ++$i) {
  1726. $wrapper = $wrapper[0];
  1727. }
  1728. }
  1729. // Now look at all properties.
  1730. foreach ($wrapper as $property => $value) {
  1731. $info = $value->info();
  1732. // We hide the complexity of multi-valued types from the user here.
  1733. $type = search_api_extract_inner_type($info['type']);
  1734. // Treat Entity API type "token" as our "string" type.
  1735. // Also let text fields with limited options be of type "string" by
  1736. // default.
  1737. if ($type == 'token' || ($type == 'text' && !empty($info['options list']))) {
  1738. // Inner type is changed to "string".
  1739. $type = 'string';
  1740. // Set the field type accordingly.
  1741. $info['type'] = search_api_nest_type('string', $info['type']);
  1742. }
  1743. $info['type'] = $type_prefix . $info['type'] . $type_suffix;
  1744. $key = $prefix . $property;
  1745. if (isset($types[$type]) || isset($entity_types[$type])) {
  1746. if (isset($fields[$key])) {
  1747. // This field is already known in the index configuration.
  1748. $fields[$key]['name'] = $prefix_name . $info['label'];
  1749. $fields[$key]['description'] = empty($info['description']) ? NULL : $info['description'];
  1750. $flat[$key] = $fields[$key];
  1751. // Update its type.
  1752. if (isset($entity_types[$type])) {
  1753. // Always enforce the proper entity type.
  1754. $flat[$key]['type'] = $info['type'];
  1755. }
  1756. else {
  1757. // Else, only update the nesting level.
  1758. $set_type = search_api_extract_inner_type(isset($flat[$key]['real_type']) ? $flat[$key]['real_type'] : $flat[$key]['type']);
  1759. $flat[$key]['type'] = $info['type'];
  1760. $flat[$key]['real_type'] = search_api_nest_type($set_type, $info['type']);
  1761. }
  1762. }
  1763. else {
  1764. $flat[$key] = array(
  1765. 'name' => $prefix_name . $info['label'],
  1766. 'description' => empty($info['description']) ? NULL : $info['description'],
  1767. 'type' => $info['type'],
  1768. 'boost' => '1.0',
  1769. 'indexed' => FALSE,
  1770. );
  1771. }
  1772. }
  1773. if (empty($types[$type])) {
  1774. if (isset($added[$key])) {
  1775. // Visit this entity/struct in a later iteration.
  1776. $wrappers[$key . ':'] = $value;
  1777. $prefix_names[$key . ':'] = $prefix_name . $info['label'] . ' » ';
  1778. $nesting_levels[$key . ':'] = search_api_list_nesting_level($info['type']);
  1779. }
  1780. else {
  1781. $name = $prefix_name . $info['label'];
  1782. // Add machine names to discern fields with identical labels.
  1783. if (isset($used_names[$name])) {
  1784. if ($used_names[$name] !== FALSE) {
  1785. $additional[$used_names[$name]] .= ' [' . $used_names[$name] . ']';
  1786. $used_names[$name] = FALSE;
  1787. }
  1788. $name .= ' [' . $key . ']';
  1789. }
  1790. $additional[$key] = $name;
  1791. $used_names[$name] = $key;
  1792. }
  1793. }
  1794. }
  1795. unset($wrappers[$prefix]);
  1796. }
  1797. }
  1798. $options = array();
  1799. $options['fields'] = $flat;
  1800. $options['additional fields'] = $additional;
  1801. return $options;
  1802. }
  1803. /**
  1804. * Returns HTML for a field list form.
  1805. *
  1806. * @param array $variables
  1807. * An associative array containing:
  1808. * - element: A render element representing the form.
  1809. */
  1810. function theme_search_api_admin_fields_table($variables) {
  1811. $form = $variables['element'];
  1812. $header = array(t('Field'), t('Machine name'), t('Indexed'), t('Type'), t('Boost'));
  1813. $rows = array();
  1814. foreach (element_children($form['fields']) as $name) {
  1815. $row = array();
  1816. foreach (element_children($form['fields'][$name]) as $field) {
  1817. if ($cell = render($form['fields'][$name][$field])) {
  1818. $row[] = $cell;
  1819. }
  1820. }
  1821. if (empty($form['fields'][$name]['description']['#value'])) {
  1822. $rows[] = _search_api_deep_copy($row);
  1823. }
  1824. else {
  1825. $rows[] = array(
  1826. 'data' => $row,
  1827. 'title' => strip_tags($form['fields'][$name]['description']['#value']),
  1828. );
  1829. }
  1830. }
  1831. $note = isset($form['note']) ? $form['note'] : '';
  1832. $submit = $form['submit'];
  1833. $additional = isset($form['additional']) ? $form['additional'] : FALSE;
  1834. unset($form['note'], $form['submit'], $form['additional']);
  1835. $output = drupal_render_children($form);
  1836. $output .= theme('table', array('header' => $header, 'rows' => $rows));
  1837. $output .= render($note);
  1838. $output .= render($submit);
  1839. if ($additional) {
  1840. $output .= render($additional);
  1841. }
  1842. return $output;
  1843. }
  1844. /**
  1845. * Form submission handler for search_api_admin_index_fields().
  1846. */
  1847. function search_api_admin_index_fields_submit(array $form, array &$form_state) {
  1848. $index = $form_state['index'];
  1849. $options = isset($index->options) ? $index->options : array();
  1850. if ($form_state['values']['op'] == t('Save changes')) {
  1851. $fields = $form_state['values']['fields'];
  1852. $default_types = search_api_default_field_types();
  1853. $custom_types = search_api_get_data_type_info();
  1854. foreach ($fields as $name => $field) {
  1855. if (empty($field['indexed'])) {
  1856. unset($fields[$name]);
  1857. }
  1858. else {
  1859. // Don't store the description. "indexed" is implied.
  1860. unset($fields[$name]['description'], $fields[$name]['indexed']);
  1861. // For non-default types, set type to the fallback and only real_type to
  1862. // the custom type.
  1863. $inner_type = search_api_extract_inner_type($field['type']);
  1864. if (!isset($default_types[$inner_type])) {
  1865. $fields[$name]['real_type'] = $field['type'];
  1866. $fields[$name]['type'] = search_api_nest_type($custom_types[$inner_type]['fallback'], $field['type']);
  1867. }
  1868. // Boost defaults to 1.0.
  1869. if ($field['boost'] == '1.0') {
  1870. unset($fields[$name]['boost']);
  1871. }
  1872. }
  1873. }
  1874. $options['fields'] = $fields;
  1875. unset($options['additional fields']);
  1876. $ret = $index->update(array('options' => $options));
  1877. if ($ret) {
  1878. drupal_set_message(t('The indexed fields were successfully changed. ' .
  1879. 'The index was cleared and will have to be re-indexed with the new settings.'));
  1880. }
  1881. else {
  1882. drupal_set_message(t('No values were changed.'));
  1883. }
  1884. if (isset($index->options['data_alter_callbacks']) || isset($index->options['processors'])) {
  1885. $form_state['redirect'] = 'admin/config/search/search_api/index/' . $index->machine_name . '/fields';
  1886. }
  1887. else {
  1888. drupal_set_message(t('Please set up the indexing workflow.'));
  1889. $form_state['redirect'] = 'admin/config/search/search_api/index/' . $index->machine_name . '/workflow';
  1890. }
  1891. return;
  1892. }
  1893. // Adding a related entity's fields.
  1894. $prefix = $form_state['values']['additional']['field'];
  1895. $options['additional fields'][$prefix] = $prefix;
  1896. $ret = $index->update(array('options' => $options));
  1897. if ($ret) {
  1898. drupal_set_message(t('The available fields were successfully changed.'));
  1899. }
  1900. else {
  1901. drupal_set_message(t('No values were changed.'));
  1902. }
  1903. $form_state['redirect'] = 'admin/config/search/search_api/index/' . $index->machine_name . '/fields';
  1904. }
  1905. /**
  1906. * Form constructor for a generic confirmation form.
  1907. *
  1908. * @param $type
  1909. * The type of entity (not the real "entity type"). Either "server" or
  1910. * "index".
  1911. * @param $action
  1912. * The action that would be executed for this entity after confirming. One of
  1913. * "reindex" ("index" type only), "clear", "disable" or "delete".
  1914. * @param Entity $entity
  1915. * The entity for which the action would be performed. Must have a "name"
  1916. * property.
  1917. *
  1918. * @return array|false
  1919. * Either a form array, or FALSE if this combination of type and action is
  1920. * not supported.
  1921. */
  1922. function search_api_admin_confirm(array $form, array &$form_state, $type, $action, Entity $entity) {
  1923. switch ($type) {
  1924. case 'server':
  1925. switch ($action) {
  1926. case 'clear':
  1927. $text = array(
  1928. t('Clear server @name', array('@name' => $entity->name)),
  1929. t('Do you really want to clear all indexed data from this server?'),
  1930. t('This will permanently remove all data currently indexed on this server. Before the data is reindexed, searches on the indexes associated with this server will not return any results. This action cannot be undone. <strong>Use with caution!</strong>'),
  1931. t("The server's indexed data was successfully cleared."),
  1932. );
  1933. break;
  1934. case 'disable':
  1935. $text = array(
  1936. t('Disable server @name', array('@name' => $entity->name)),
  1937. t('Do you really want to disable this server?'),
  1938. t('This will disconnect all indexes from this server and disable them. Searches on these indexes will not be available until they are added to another server and re-enabled. All indexed data (except for read-only indexes) on this server will be cleared.'),
  1939. t('The server and its indexes were successfully disabled.'),
  1940. );
  1941. break;
  1942. case 'delete':
  1943. if ($entity->hasStatus(ENTITY_OVERRIDDEN)) {
  1944. $text = array(
  1945. t('Revert server @name', array('@name' => $entity->name)),
  1946. t('Do you really want to revert this server?'),
  1947. t('This will revert all settings for this server back to the defaults. This action cannot be undone.'),
  1948. t('The server settings have been successfully reverted.'),
  1949. );
  1950. }
  1951. else {
  1952. $text = array(
  1953. t('Delete server @name', array('@name' => $entity->name)),
  1954. t('Do you really want to delete this server?'),
  1955. t('This will delete the server and disable all associated indexes. ' .
  1956. "Searches on these indexes won't be available until they are moved to another server and re-enabled."),
  1957. t('The server was successfully deleted.'),
  1958. );
  1959. }
  1960. break;
  1961. default:
  1962. return FALSE;
  1963. }
  1964. break;
  1965. case 'index':
  1966. switch ($action) {
  1967. case 'reindex':
  1968. $text = array(
  1969. t('Re-index index @name', array('@name' => $entity->name)),
  1970. t('Do you really want to queue all items on this index for re-indexing?'),
  1971. t('This will mark all items for this index to be marked as needing to be indexed. Searches on this index will continue to yield results while the items are being re-indexed. This action cannot be undone.'),
  1972. t('The index was successfully marked for re-indexing.'),
  1973. );
  1974. break;
  1975. case 'clear':
  1976. $text = array(
  1977. t('Clear index @name', array('@name' => $entity->name)),
  1978. t('Do you really want to clear the indexed data of this index?'),
  1979. t('This will remove all data currently indexed for this index. Before the data is reindexed, searches on the index will not return any results. This action cannot be undone.'),
  1980. t('The index was successfully cleared.'),
  1981. );
  1982. break;
  1983. case 'disable':
  1984. $text = array(
  1985. t('Disable index @name', array('@name' => $entity->name)),
  1986. t('Do you really want to disable this index?'),
  1987. t("Searches on this index won't be available until it is re-enabled."),
  1988. t('The index was successfully disabled.'),
  1989. );
  1990. break;
  1991. case 'delete':
  1992. if ($entity->hasStatus(ENTITY_OVERRIDDEN)) {
  1993. $text = array(
  1994. t('Revert index @name', array('@name' => $entity->name)),
  1995. t('Do you really want to revert this index?'),
  1996. t('This will revert all settings on this index back to the defaults. This action cannot be undone.'),
  1997. t('The index settings have been successfully reverted.'),
  1998. );
  1999. }
  2000. else {
  2001. $text = array(
  2002. t('Delete index @name', array('@name' => $entity->name)),
  2003. t('Do you really want to delete this index?'),
  2004. t('This will remove the index from the server and delete all settings. ' .
  2005. 'All data on this index will be lost.'),
  2006. t('The index has been successfully deleted.'),
  2007. );
  2008. }
  2009. break;
  2010. default:
  2011. return FALSE;
  2012. }
  2013. break;
  2014. default:
  2015. return FALSE;
  2016. }
  2017. $form = array(
  2018. 'type' => array(
  2019. '#type' => 'value',
  2020. '#value' => $type,
  2021. ),
  2022. 'action' => array(
  2023. '#type' => 'value',
  2024. '#value' => $action,
  2025. ),
  2026. 'id' => array(
  2027. '#type' => 'value',
  2028. '#value' => $entity->machine_name,
  2029. ),
  2030. 'message' => array(
  2031. '#type' => 'value',
  2032. '#value' => $text[3],
  2033. ),
  2034. );
  2035. $desc = "<h3>{$text[1]}</h3><p>{$text[2]}</p>";
  2036. return confirm_form($form, $text[0], "admin/config/search/search_api/$type/{$entity->machine_name}", $desc);
  2037. }
  2038. /**
  2039. * Submit function for search_api_admin_confirm().
  2040. */
  2041. function search_api_admin_confirm_submit(array $form, array &$form_state) {
  2042. $values = $form_state['values'];
  2043. $type = $values['type'];
  2044. $action = $values['action'];
  2045. $id = $values['id'];
  2046. $function = "search_api_{$type}_{$action}";
  2047. if ($function($id)) {
  2048. drupal_set_message($values['message']);
  2049. }
  2050. else {
  2051. drupal_set_message(t('An error has occurred while performing the desired action. Check the logs for details.'), 'error');
  2052. }
  2053. $form_state['redirect'] = $action == 'delete'
  2054. ? "admin/config/search/search_api"
  2055. : "admin/config/search/search_api/$type/$id";
  2056. }