facetapi.module 39 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181
  1. <?php
  2. /**
  3. * @file
  4. * An abstracted facet API that can be used by various search backends.
  5. */
  6. /**
  7. * Constant for the "AND" operator.
  8. */
  9. define('FACETAPI_OPERATOR_AND', 'and');
  10. /**
  11. * Constant for the "OR" operator.
  12. */
  13. define('FACETAPI_OPERATOR_OR', 'or');
  14. /**
  15. * String that represents a time gap of a year between two dates.
  16. */
  17. define('FACETAPI_DATE_YEAR', 'YEAR');
  18. /**
  19. * String that represents a time gap of a month between two dates.
  20. */
  21. define('FACETAPI_DATE_MONTH', 'MONTH');
  22. /**
  23. * String that represents a time gap of a day between two dates.
  24. */
  25. define('FACETAPI_DATE_DAY', 'DAY');
  26. /**
  27. * String that represents a time gap of an hour between two dates.
  28. */
  29. define('FACETAPI_DATE_HOUR', 'HOUR');
  30. /**
  31. * String that represents a time gap of a minute between two dates.
  32. */
  33. define('FACETAPI_DATE_MINUTE', 'MINUTE');
  34. /**
  35. * String that represents a time gap of a second between two dates.
  36. */
  37. define('FACETAPI_DATE_SECOND', 'SECOND');
  38. /**
  39. * Date string for ISO 8601 date formats.
  40. */
  41. define('FACETAPI_DATE_ISO8601', 'Y-m-d\TH:i:s\Z');
  42. /**
  43. * Regex pattern for range queries.
  44. */
  45. define('FACETAPI_REGEX_RANGE', '/^[\[\{](\S+) TO (\S+)[\]\}]$/');
  46. /**
  47. * Regex pattern for date queries.
  48. */
  49. define('FACETAPI_REGEX_DATE', '/^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})Z$/');
  50. /**
  51. * Regex pattern for date ranges.
  52. */
  53. define('FACETAPI_REGEX_DATE_RANGE', '/^\[(' . trim(FACETAPI_REGEX_DATE, '/^$') . ') TO (' . trim(FACETAPI_REGEX_DATE, '/^$') . ')\]$/');
  54. // Calls block specific hooks and overrides.
  55. module_load_include('inc', 'facetapi', 'facetapi.block');
  56. /**
  57. * Implements hook_facetapi_hook_info().
  58. */
  59. function facetapi_hook_info() {
  60. return array(
  61. 'facetapi_adapters' => array(
  62. 'group' => 'facetapi',
  63. ),
  64. 'facetapi_dependencies' => array(
  65. 'group' => 'facetapi',
  66. ),
  67. 'facetapi_empty_behaviors' => array(
  68. 'group' => 'facetapi',
  69. ),
  70. 'facetapi_facet_info' => array(
  71. 'group' => 'facetapi',
  72. ),
  73. 'facetapi_filters' => array(
  74. 'group' => 'facetapi',
  75. ),
  76. 'facetapi_query_types' => array(
  77. 'group' => 'facetapi',
  78. ),
  79. 'facetapi_realm_info' => array(
  80. 'group' => 'facetapi',
  81. ),
  82. 'facetapi_sort_info' => array(
  83. 'group' => 'facetapi',
  84. ),
  85. 'facetapi_url_processors' => array(
  86. 'group' => 'facetapi',
  87. ),
  88. 'facetapi_widgets' => array(
  89. 'group' => 'facetapi',
  90. ),
  91. 'current_search_items' => array(
  92. 'group' => 'facetapi',
  93. ),
  94. );
  95. }
  96. /**
  97. * Implements hook_menu().
  98. */
  99. function facetapi_menu() {
  100. $items = array();
  101. // Builds the realm settings forms for each searcher.
  102. foreach (facetapi_get_searcher_info() as $searcher => $searcher_info) {
  103. // Only build router items automatically if a path is provided.
  104. if (empty($searcher_info['path'])) {
  105. continue;
  106. }
  107. // Builds realm settings.
  108. $first = TRUE;
  109. foreach (facetapi_get_realm_info() as $realm_name => $realm) {
  110. if ($first) {
  111. $first = FALSE;
  112. // Add the first realm as a default local task.
  113. $items[$searcher_info['path'] . '/facets'] = array(
  114. 'title' => 'Facets',
  115. 'page callback' => 'drupal_get_form',
  116. 'page arguments' => array('facetapi_realm_settings_form', $searcher, $realm_name),
  117. 'access callback' => 'facetapi_access_callback',
  118. 'type' => MENU_LOCAL_TASK,
  119. 'file' => 'facetapi.admin.inc',
  120. );
  121. $items[$searcher_info['path'] . '/facets/' . $realm_name] = array(
  122. 'title' => $realm['label'],
  123. 'type' => MENU_DEFAULT_LOCAL_TASK,
  124. 'weight' => $realm['weight'],
  125. );
  126. }
  127. else {
  128. // Add all additional realms as local tasks.
  129. $items[$searcher_info['path'] . '/facets/' . $realm_name] = array(
  130. 'title' => $realm['label'],
  131. 'page callback' => 'drupal_get_form',
  132. 'page arguments' => array('facetapi_realm_settings_form', $searcher, $realm_name),
  133. 'access callback' => 'facetapi_access_callback',
  134. 'type' => MENU_LOCAL_TASK,
  135. 'file' => 'facetapi.admin.inc',
  136. );
  137. }
  138. }
  139. }
  140. $items['admin/config/search/facetapi/%facetapi_adapter/%facetapi_realm/%facetapi_facet/edit'] = array(
  141. 'title' => 'Configure facet display',
  142. 'load arguments' => array(4),
  143. 'page callback' => 'drupal_get_form',
  144. 'page arguments' => array('facetapi_facet_display_form', 4, 5, 6),
  145. 'access callback' => 'facetapi_access_callback',
  146. 'type' => MENU_LOCAL_ACTION,
  147. 'context' => MENU_CONTEXT_INLINE,
  148. 'weight' => -15,
  149. 'file' => 'facetapi.admin.inc',
  150. );
  151. $items['admin/config/search/facetapi/%facetapi_adapter/%facetapi_realm/%facetapi_dependencies/dependencies'] = array(
  152. 'title' => 'Configure facet dependencies',
  153. 'load arguments' => array(4),
  154. 'page callback' => 'drupal_get_form',
  155. 'page arguments' => array('facetapi_facet_dependencies_form', 4, 5, 6),
  156. 'access callback' => 'facetapi_access_callback',
  157. 'type' => MENU_LOCAL_ACTION,
  158. 'context' => MENU_CONTEXT_INLINE,
  159. 'weight' => -10,
  160. 'file' => 'facetapi.admin.inc',
  161. );
  162. $items['admin/config/search/facetapi/%facetapi_adapter/%facetapi_realm/%facetapi_filters/filters'] = array(
  163. 'title' => 'Configure facet filters',
  164. 'load arguments' => array(4),
  165. 'page callback' => 'drupal_get_form',
  166. 'page arguments' => array('facetapi_facet_filters_form', 4, 5, 6),
  167. 'access callback' => 'facetapi_access_callback',
  168. 'type' => MENU_LOCAL_ACTION,
  169. 'context' => MENU_CONTEXT_INLINE,
  170. 'weight' => -5,
  171. 'file' => 'facetapi.admin.inc',
  172. );
  173. $items['admin/config/search/facetapi/%facetapi_adapter/%facetapi_realm/%facetapi_facet/export'] = array(
  174. 'title' => 'Export facet configuration',
  175. 'load arguments' => array(4),
  176. 'page callback' => 'drupal_get_form',
  177. 'page arguments' => array('facetapi_export_form', 4, 5, 6),
  178. 'access callback' => 'facetapi_access_callback',
  179. 'type' => MENU_NORMAL_ITEM,
  180. 'file' => 'facetapi.admin.inc',
  181. );
  182. $items['admin/config/search/facetapi/%facetapi_adapter/%facetapi_realm/%facetapi_facet/revert'] = array(
  183. 'title' => 'Revert facet configuration',
  184. 'load arguments' => array(4),
  185. 'page callback' => 'drupal_get_form',
  186. 'page arguments' => array('facetapi_revert_form', 4, 5, 6),
  187. 'access callback' => 'facetapi_access_callback',
  188. 'type' => MENU_NORMAL_ITEM,
  189. 'file' => 'facetapi.admin.inc',
  190. );
  191. return $items;
  192. }
  193. /**
  194. * Implements hook_ctools_plugin_type().
  195. */
  196. function facetapi_ctools_plugin_type() {
  197. return array(
  198. 'adapters' => array(
  199. 'use hooks' => TRUE,
  200. ),
  201. 'dependencies' => array(
  202. 'use hooks' => TRUE,
  203. ),
  204. 'empty_behaviors' => array(
  205. 'use hooks' => TRUE,
  206. ),
  207. 'filters' => array(
  208. 'use hooks' => TRUE,
  209. ),
  210. 'query_types' => array(
  211. 'use hooks' => TRUE,
  212. ),
  213. 'url_processors' => array(
  214. 'use hooks' => TRUE,
  215. ),
  216. 'widgets' => array(
  217. 'use hooks' => TRUE,
  218. ),
  219. );
  220. }
  221. /**
  222. * Implements hook_theme().
  223. */
  224. function facetapi_theme() {
  225. return array(
  226. 'facetapi_title' => array(
  227. 'variables' => array('title' => NULL, 'facet' => array()),
  228. 'file' => 'facetapi.theme.inc',
  229. ),
  230. 'facetapi_facet_missing' => array(
  231. 'variables' => array('field_name' => NULL),
  232. 'file' => 'facetapi.theme.inc',
  233. ),
  234. 'facetapi_count' => array(
  235. 'variables' => array('count' => NULL),
  236. 'file' => 'facetapi.theme.inc',
  237. ),
  238. 'facetapi_link_inactive' => array(
  239. 'variables' => array('text' => NULL, 'path' => NULL, 'options' => array(), 'count' => 0),
  240. 'file' => 'facetapi.theme.inc',
  241. ),
  242. 'facetapi_link_active' => array(
  243. 'variables' => array('text' => NULL, 'path' => NULL, 'options' => array()),
  244. 'file' => 'facetapi.theme.inc',
  245. ),
  246. 'facetapi_deactivate_widget' => array(
  247. 'variables' => array('text' => NULL),
  248. 'file' => 'facetapi.theme.inc',
  249. ),
  250. 'facetapi_accessible_markup' => array(
  251. 'variables' => array('text' => NULL, 'active' => NULL),
  252. 'file' => 'facetapi.theme.inc',
  253. ),
  254. 'facetapi_realm_settings_table' => array(
  255. 'render element' => 'element',
  256. 'file' => 'facetapi.admin.inc',
  257. ),
  258. 'facetapi_sort_settings_table' => array(
  259. 'render element' => 'element',
  260. 'file' => 'facetapi.admin.inc',
  261. ),
  262. 'facetapi_filter_settings_table' => array(
  263. 'render element' => 'element',
  264. 'file' => 'facetapi.admin.inc',
  265. ),
  266. );
  267. }
  268. /**
  269. * Custom access callback. Checks if the user has either the "administer search"
  270. * OR "administer facets" permissions.
  271. *
  272. * @param stdClass|NULL $account
  273. * (optional) The account to check, if not given use currently logged in user.
  274. *
  275. * @return boolean
  276. * TRUE if the user has access to the resource, FALSE otherwise.
  277. */
  278. function facetapi_access_callback($account = NULL) {
  279. global $user;
  280. if (!isset($account)) {
  281. $account = $user;
  282. }
  283. return user_access('administer search', $account) || user_access('administer facets', $account);
  284. }
  285. /**
  286. * Implements hook_permission().
  287. */
  288. function facetapi_permission() {
  289. return array(
  290. 'administer facets' => array(
  291. 'title' => t('Administer Facets'),
  292. ),
  293. );
  294. }
  295. ////
  296. ////
  297. //// facetapi_*_load() functions
  298. ////
  299. ////
  300. /**
  301. * Instantiates the adapter plugin associated with the searcher.
  302. *
  303. * @param $searcher
  304. * The machine readable name of searcher.
  305. *
  306. * @return FacetapiAdapter
  307. * The adapter object, FALSE if the object can't be loaded.
  308. */
  309. function facetapi_adapter_load($searcher) {
  310. $adapters = &drupal_static(__FUNCTION__, array());
  311. if (!isset($adapters[$searcher])) {
  312. $searcher_info = facetapi_get_searcher_info();
  313. if (isset($searcher_info[$searcher]['adapter'])) {
  314. ctools_include('plugins');
  315. $id = $searcher_info[$searcher]['adapter'];
  316. $class = ctools_plugin_load_class('facetapi', 'adapters', $id, 'handler');
  317. $adapters[$searcher] = ($class) ? new $class($searcher_info[$searcher]) : FALSE;
  318. }
  319. else {
  320. $adapters[$searcher] = FALSE;
  321. }
  322. }
  323. return $adapters[$searcher];
  324. }
  325. /**
  326. * Loads the dependency plugins associated with the facet.
  327. *
  328. * @param $facet_name
  329. * The machine readable name of the facet.
  330. * @param $searcher
  331. * The machine readable name of the searcher module.
  332. *
  333. * @return array
  334. * An array of FacetapiDependency objects, FALSE if no plugins.
  335. */
  336. function facetapi_dependencies_load($facet_name, $searcher) {
  337. $dependencies = array();
  338. $facet = facetapi_facet_load($facet_name, $searcher);
  339. if ($facet && ($adapter = facetapi_adapter_load($searcher))) {
  340. foreach ($facet['dependency plugins'] as $id) {
  341. // NOTE: CTools plugin component is loaded by facetapi_adapter_load().
  342. $class = ctools_plugin_load_class('facetapi', 'dependencies', $id, 'handler');
  343. $settings = $adapter->getFacet($facet)->getSettings();
  344. $dependencies[] = new $class($id, $adapter, $facet, $settings);
  345. }
  346. }
  347. return ($dependencies) ? $dependencies : FALSE;
  348. }
  349. /**
  350. * Returns array of filter options available to the facet.
  351. *
  352. * @param $facet_name
  353. * The machine readable name of the facet.
  354. * @param $searcher
  355. * The machine readable name of the searcher.
  356. *
  357. * @return array
  358. * An array of FacetapiFilter objects, FALSE if no plugins.
  359. */
  360. function facetapi_filters_load($facet_name, $searcher) {
  361. $filters = array('plugins' => array());
  362. if ($filters['facet'] = facetapi_facet_load($facet_name, $searcher)) {
  363. $filters['plugins'] = facetapi_get_filters($filters['facet']);
  364. }
  365. return ($filters['plugins']) ? $filters : FALSE;
  366. }
  367. /**
  368. * Returns a realm definition.
  369. *
  370. * @param $realm_name
  371. * The machine readable name of the realm.
  372. *
  373. * @return array
  374. * An array containing the realm definition, FALSE if the realm is invalid.
  375. * - name: The machine name of the realm.
  376. * - label: The human readable name of the realm displayed in the admin UI.
  377. * - description: The description of the realm displayed in the admin UI.
  378. * - element type: The type of element facets are rendered as, such as 'links'
  379. * or 'form elements'.
  380. * - default widget: The default widget plugin id for facets in the realm.
  381. * - settings callback: A callback that alters the realm settings form.
  382. * - sortable: Whether the facets can be sorted via the admin UI.
  383. * - weight: The weight of the realm's menu item in comparison to the others.
  384. */
  385. function facetapi_realm_load($realm_name) {
  386. $realm_info = facetapi_get_realm_info();
  387. return (isset($realm_info[$realm_name])) ? $realm_info[$realm_name] : FALSE;
  388. }
  389. /**
  390. * Returns a facet definition.
  391. *
  392. * @param $facet_name
  393. * The machine readable name of the facet.
  394. * @param $searcher
  395. * The machine readable name of the searcher.
  396. *
  397. * @return
  398. * An array containing the facet definition, FALSE if the facet is invalid.
  399. * - name: Machine readable name of the facet.
  400. * - label: Human readable name of the facet displayed in settings forms.
  401. * - description: Description of the facet displayed in settings forms.
  402. * - field: The field name used by the backend to store and retrieve data from
  403. * the search index it is associated with.
  404. * - field alias: The query string variable inside of the filter key used to
  405. * pass the filter information through the query string.
  406. * - field api name: The machine readable name of the Field API field data the
  407. * facet is associated with, FALSE if it is not associated with a field.
  408. * - field api bundles: An array of entity names that this field contains
  409. * bundle information for.
  410. * - query types: The query type plugins that that this facet supports. For
  411. * example, numeric fields support "term" and "range_filter" queries.
  412. * - alter callbacks: Callbacks that alter the initialized render array
  413. * returned by the query type plugin. Defaults to an empty array.
  414. * - dependency plugins: An array of dependency plugin IDs that are supported
  415. * by this facet.
  416. * - default widget: The widget plugin ID used if no plugin has been selected
  417. * or the one selected is not valid.
  418. * - allowed operators: An array keyed by operator constant to boolean values
  419. * specifying whether the operator is supported.
  420. * - facet missing allowed: Whether or not missing facets are allowed.
  421. * - facet mincount allowed: Whether or not the facet supports the "minimum
  422. * facet count" setting.
  423. * - weight: The weight of the facet
  424. * - map callback: The callback used to map the raw values returned by the
  425. * index to something human readable.
  426. * - map options: An array of options passed to the map callback.
  427. * - hierarchy callback: A callback that maps the parent / child relationships
  428. * of the facet data, defaults to FALSE meaning the list is flat.
  429. * - values callback: In instances where facet data is not returned by the
  430. * backend, provide a list of values that can be used.
  431. * - min callback: For facets containing ranges, a callback returning the
  432. * minimum value in the index.
  433. * - max callback: For facets containing ranges, a callback returning the
  434. * maximum value in the index.
  435. * - default sorts: An array of available sorts. Each item is an array
  436. * containing two values, the first being the item being filtered on, the
  437. * second being the SORT_* constant.
  438. */
  439. function facetapi_facet_load($facet_name, $searcher) {
  440. $facet_info = facetapi_get_facet_info($searcher);
  441. return (isset($facet_info[$facet_name])) ? $facet_info[$facet_name] : FALSE;
  442. }
  443. ////
  444. ////
  445. //// facetapi_get_*() functions
  446. ////
  447. ////
  448. /**
  449. * Returns all defined searcher definitions.
  450. *
  451. * @return array
  452. * An array of searcher information. Each array is keyed by the machine
  453. * readable searcher name and contains the following items:
  454. * - name: The machine readable name of the searcher.
  455. * - label: The human readable name of the searcher displayed in the admin UI.
  456. * - adapter: The adapter plugin ID associated with the searcher.
  457. * - url processor: The URL processor plugin ID associated with the searcher.
  458. * - types: An array containing the types of content indexed by the searcher.
  459. * A type is usually an entity such as 'node', but it can be a non-entity
  460. * value as well.
  461. * - path: The MENU_DEFAULT_LOCAL_TASK item which the admin UI page is added
  462. * to as a MENU_LOCAL_TASK. An empty string if the backend manages the admin
  463. * UI menu items internally.
  464. * - supports facet missing: TRUE if the searcher supports "missing" facets.
  465. * - supports facet mincount: TRUE if the searcher supports the minimum facet
  466. * count setting.
  467. * - include default facets: TRUE if the searcher should include the facets
  468. * defined in facetapi_facetapi_facet_info() when indexing node content,
  469. * FALSE if they should be skipped.
  470. */
  471. function facetapi_get_searcher_info() {
  472. $searcher_info = array();
  473. foreach (module_implements('facetapi_searcher_info') as $module) {
  474. // Iterates over the module's searcher definition.
  475. foreach ((array) module_invoke($module, 'facetapi_searcher_info') as $searcher => $info) {
  476. // @see http://drupal.org/node/1167974
  477. // Converts "type" to an array and stores in "types".
  478. // @todo Remove in later versions.
  479. if (isset($info['type']) && !isset($info['types'])) {
  480. $info['types'] = array($info['type']);
  481. }
  482. // @see http://drupal.org/node/1304010
  483. // Converts "url_processor" to "url processor" for consistency.
  484. // @todo Remove in later versions.
  485. if (isset($info['url_processor']) && !isset($info['url processor'])) {
  486. $info['url processor'] = $info['url_processor'];
  487. }
  488. $info += array(
  489. 'module' => $module,
  490. 'name' => $searcher,
  491. 'path' => '',
  492. 'types' => array('node'),
  493. 'url processor' => 'standard',
  494. 'supports facet missing' => FALSE,
  495. 'supports facet mincount' => FALSE,
  496. 'include default facets' => TRUE,
  497. );
  498. // @see http://drupal.org/node/1167974
  499. // Makes sure old style "type" is present.
  500. if (!isset($info['type'])) {
  501. $info['type'] = $info['types'][key($info['types'])];
  502. }
  503. // Maps "types" so we can do faster lookups via isset().
  504. $info['types'] = drupal_map_assoc($info['types']);
  505. $searcher_info[$searcher] = $info;
  506. }
  507. }
  508. drupal_alter('facetapi_searcher_info', $searcher_info);
  509. array_walk($searcher_info, 'facetapi_map_assoc', 'types');
  510. return $searcher_info;
  511. }
  512. /**
  513. * Returns a list of active searchers.
  514. *
  515. * An active searcher means that facet data is parsed and processed by the
  516. * backend. Any searcher's adapter who's FacetapiAdapter::addActiveFilters() was
  517. * called is automatically added to this list.
  518. *
  519. * @return array
  520. * An associative array of active adapters
  521. */
  522. function facetapi_get_active_searchers() {
  523. $searchers = &drupal_static('facetapi_active_searchers', array());
  524. return $searchers;
  525. }
  526. /**
  527. * Returns all defined realm definitions.
  528. *
  529. * @return array
  530. * An array of realm definitions. Each definition is an array keyed by the
  531. * machine readable name of the realm. See the return value of the
  532. * facetapi_realm_load() function for the structure of the definitions.
  533. */
  534. function facetapi_get_realm_info() {
  535. $realm_info = &drupal_static(__FUNCTION__);
  536. if (NULL === $realm_info) {
  537. $realm_info = module_invoke_all('facetapi_realm_info');
  538. foreach ($realm_info as $realm_name => $realm) {
  539. $realm_info[$realm_name] += array(
  540. 'name' => $realm_name,
  541. 'label' => $realm_name,
  542. 'description' => '',
  543. 'default widget' => '',
  544. 'settings callback' => FALSE,
  545. 'element type' => 'links',
  546. 'sortable' => TRUE,
  547. 'weight' => 0,
  548. );
  549. }
  550. drupal_alter('facetapi_realm_info', $realm_info);
  551. uasort($realm_info, 'drupal_sort_weight');
  552. }
  553. return $realm_info;
  554. }
  555. /**
  556. * Returns all defined facet definitions available to the searcher.
  557. *
  558. * @param $searcher
  559. * A string containing the machine readable name of the searcher.
  560. *
  561. * @return array
  562. * An array of facet definitions. Each definition is an array keyed by the
  563. * machine readable name of the facet. See the return value of the
  564. * facetapi_facet_load() function for the structure of the definitions.
  565. */
  566. function facetapi_get_facet_info($searcher) {
  567. $facet_info = &drupal_static(__FUNCTION__, array());
  568. // Gets facet info if we haven't gotten it already.
  569. if (!isset($facet_info[$searcher])) {
  570. // Builds cache ID.
  571. global $language;
  572. $cid = 'facetapi:facet_info:' . $searcher . ':' . $language->language;
  573. // Checks if our results are cached.
  574. $cache = cache_get($cid);
  575. if (!empty($cache->data)) {
  576. $facet_info[$searcher] = $cache->data;
  577. }
  578. else {
  579. $searcher_info = facetapi_get_searcher_info();
  580. $facet_info[$searcher] = array();
  581. // Invokes hook_facetapi_facet_info(), normalizes facets.
  582. foreach (module_implements('facetapi_facet_info') as $module) {
  583. $facets = call_user_func($module . '_facetapi_facet_info', $searcher_info[$searcher]);
  584. if (!$facets || !is_array($facets)) {
  585. $facets = array();
  586. }
  587. // Iterates over facet definitions, merges defaults.
  588. foreach ($facets as $facet_name => $info) {
  589. // @see http://drupal.org/node/1161434
  590. // Converts "query type" to an array and stores in "query types".
  591. // @todo Remove in later versions.
  592. if (isset($info['query type']) && !isset($info['query types'])) {
  593. $info['query types'] = array($info['query type']);
  594. }
  595. $facet_info[$searcher][$facet_name] = $info;
  596. $facet_info[$searcher][$facet_name] += array(
  597. 'name' => $facet_name,
  598. 'label' => $facet_name,
  599. 'description' => '',
  600. 'field' => $facet_name,
  601. 'field alias' => isset($info['field']) ? $info['field'] : $facet_name,
  602. 'field api name' => FALSE,
  603. 'field api bundles' => array(),
  604. 'query types' => array('term'),
  605. 'alter callbacks' => array(),
  606. 'dependency plugins' => array(),
  607. 'default widget' => FALSE,
  608. 'allowed operators' => array(FACETAPI_OPERATOR_AND => TRUE, FACETAPI_OPERATOR_OR => TRUE),
  609. 'facet missing allowed' => FALSE,
  610. 'facet mincount allowed' => FALSE,
  611. 'weight' => 0,
  612. 'map callback' => FALSE,
  613. 'map options' => array(),
  614. 'hierarchy callback' => FALSE,
  615. 'values callback' => FALSE,
  616. 'min callback' => FALSE,
  617. 'max callback' => FALSE,
  618. 'default sorts' => array(
  619. array('active', SORT_DESC),
  620. array('count', SORT_DESC),
  621. array('display', SORT_ASC),
  622. ),
  623. );
  624. // @see http://drupal.org/node/1161434
  625. // Makes sure old style "query type" is present.
  626. // @todo Remove in later versions.
  627. if (!isset($facet_info[$searcher][$facet_name]['query type'])) {
  628. $type = key($facet_info[$searcher][$facet_name]['query types']);
  629. $facet_info[$searcher][$facet_name]['type'] = $type;
  630. }
  631. }
  632. }
  633. // Invokes alter hook, sorts and returns.
  634. drupal_alter('facetapi_facet_info', $facet_info[$searcher], $searcher_info[$searcher]);
  635. array_walk($facet_info[$searcher], 'facetapi_map_assoc', 'field api bundles');
  636. uasort($facet_info[$searcher], 'drupal_sort_weight');
  637. // Caches the result.
  638. cache_set($cid, $facet_info[$searcher], 'cache', CACHE_TEMPORARY);
  639. }
  640. }
  641. return $facet_info[$searcher];
  642. }
  643. /**
  644. * Implements hook_field_create_instance().
  645. */
  646. function facetapi_field_create_instance($instance) {
  647. cache_clear_all('facetapi:facet_info:', 'cache', TRUE);
  648. }
  649. /**
  650. * Implements hook_field_delete_instance().
  651. */
  652. function facetapi_field_delete_instance($instance) {
  653. cache_clear_all('facetapi:facet_info:', 'cache', TRUE);
  654. }
  655. /**
  656. * Wrapper around drupal_map_assoc() for a key in all items of an array.
  657. *
  658. * Useful as an array_walk() callback.
  659. *
  660. * @param array &$array
  661. * The array being modified.
  662. * @param $name
  663. * The key of the array being modified, usually the name of a definition.
  664. * @param $key
  665. * The key in the array being passed to drupal_map_assoc().
  666. */
  667. function facetapi_map_assoc(&$array, $name, $key) {
  668. $array[$key] = drupal_map_assoc($array[$key]);
  669. }
  670. /**
  671. * Returns all sort definitions.
  672. *
  673. * @return array
  674. * An associative array of sort definitions keyed by sort name. Each sort
  675. * definition contains:
  676. * - name: The machine readable name of the sort.
  677. * - title: The human readable name of the sort displayed in the admin UI.
  678. * - callback: The uasort() callback the render array is passed to.
  679. * - description: The description of the sort displayed in the admin UI.
  680. * - weight: The default weight of the sort specifying its processing order.
  681. */
  682. function facetapi_get_sort_info() {
  683. $sort_info = &drupal_static(__FUNCTION__);
  684. if (NULL === $sort_info) {
  685. $sort_info = module_invoke_all('facetapi_sort_info');
  686. foreach ($sort_info as $sort_name => $info) {
  687. $sort_info[$sort_name] += array(
  688. 'name' => $sort_name,
  689. 'label' => $sort_name,
  690. 'callback' => '',
  691. 'requirements' => array(),
  692. 'description' => '',
  693. 'weight' => 0,
  694. );
  695. }
  696. drupal_alter('facetapi_sort_info', $sort_info);
  697. }
  698. return $sort_info;
  699. }
  700. /**
  701. * Returns all filter definitions available to the facet.
  702. *
  703. * Each filter plugin must pass all the requirements checks specified in its
  704. * definition. Only plugins that pass all requirements are returned.
  705. *
  706. * @param array $facet
  707. * The facet definition as returned by facetapi_facet_load().
  708. *
  709. * @return array
  710. * An associative array of filter plugin definitions keyed by the plugin ID.
  711. * Each filter plugin definition contains:
  712. * - handler: An associative array containing:
  713. * - label: The human readable name of the plugin displayed in the admin UI.
  714. * - description: The description of the plugin displayed in the admin UI.
  715. * - class: The class containing the plugin.
  716. */
  717. function facetapi_get_filters(array $facet) {
  718. $plugins = array();
  719. // Iterates over all defined plugins.
  720. foreach (ctools_get_plugins('facetapi', 'filters') as $id => $plugin) {
  721. $plugin['handler'] += array(
  722. 'label' => $id,
  723. 'description' => '',
  724. 'requirements' => array(),
  725. );
  726. // Checks requirements.
  727. if (facetapi_check_requirements($plugin['handler']['requirements'], array(), $facet)) {
  728. $plugins[$id] = $plugin;
  729. }
  730. }
  731. return $plugins;
  732. }
  733. /**
  734. * Gets raw settings from the datbase, caches as a satic variable.
  735. *
  736. * Avoid using this function directly as it will not load default settings. Use
  737. * the FacetapiAdapter::getFacetSettings*() method instead.
  738. *
  739. * @param $searcher
  740. * A string containing the searcher.
  741. *
  742. * @return array
  743. * An array of settings keyed by name.
  744. *
  745. * @see FacetapiAdapter::getFacetSettings()
  746. * @see FacetapiAdapter::getFacetSettingsGlobal()
  747. */
  748. function facetapi_get_searcher_settings($searcher) {
  749. $settings = &drupal_static(__FUNCTION__, array());
  750. if (!isset($settings[$searcher])) {
  751. ctools_include('export');
  752. $args = array('searcher' => $searcher);
  753. $settings[$searcher] = ctools_export_load_object('facetapi', 'conditions', $args);
  754. }
  755. return $settings[$searcher];
  756. }
  757. /**
  758. * Returns all enabled facet definitions available to the searcher.
  759. *
  760. * If a realm is passed, this function returns all facets enabled in the realm.
  761. * If no realm is passed, this function returns all facets that are enabled in
  762. * at least one realm.
  763. *
  764. * @param $searcher
  765. * The machine readable name of the searcher.
  766. * @param $realm_name
  767. * The machine readable name of the realm, pass NULL to return all facets that
  768. * are enabled in at least one realm.
  769. *
  770. * @return array
  771. * An array of facet definitions. Each definition is an array keyed by the
  772. * machine readable name of the facet. See the return value of the
  773. * facetapi_facet_load() function for the structure of the definitions.
  774. */
  775. function facetapi_get_enabled_facets($searcher, $realm_name = NULL) {
  776. $enabled_facets = &drupal_static(__FUNCTION__, array());
  777. $cid = $searcher . ':' . (string) $realm_name;
  778. if (!isset($enabled_facets[$cid])) {
  779. $facets = array();
  780. // Gets cached settings, finds enabled facets.
  781. $cached = facetapi_get_searcher_settings($searcher);
  782. foreach ($cached as $name => $settings) {
  783. $test_enabled = (NULL === $realm_name || $realm_name == $settings->realm);
  784. if ($test_enabled && $settings->enabled) {
  785. $facets[$settings->facet] = $settings->facet;
  786. }
  787. }
  788. // Gets facet definitions for all enabled facets.
  789. $facet_info = facetapi_get_facet_info($searcher);
  790. $enabled_facets[$cid] = array_intersect_key($facet_info, $facets);
  791. // Alter enabled facets on the fly.
  792. drupal_alter('facetapi_enabled_facets', $enabled_facets[$cid], $searcher, $realm_name);
  793. }
  794. return $enabled_facets[$cid];
  795. }
  796. ////
  797. ////
  798. //// Utility functions
  799. ////
  800. ////
  801. /**
  802. * Translates a string via the translator module.
  803. *
  804. * @param $name
  805. * The name of the string in "textgroup:object_type:object_key:property_name"
  806. * format.
  807. * @param $string
  808. * The string being translated.
  809. * @param $langcode
  810. * The language code to translate to a language other than what is used to
  811. * display the page. Defaults to NULL, which uses the current language.
  812. *
  813. * @return
  814. * The translated string.
  815. */
  816. function facetapi_translate_string($name, $string, $langcode = NULL) {
  817. if ($module = variable_get('facetapi:translator_module', NULL)) {
  818. $function = $module . '_facetapi_translate_string';
  819. if (function_exists($function)) {
  820. $string = $function($name, $string, $langcode);
  821. }
  822. }
  823. return $string;
  824. }
  825. /**
  826. * Tests whether a searcher is active or not.
  827. *
  828. * @param $searcher
  829. * The machine readable name of the searcher.
  830. *
  831. * @return FacetapiAdapter
  832. * The adapter object, FALSE if the object can't be loaded.
  833. */
  834. function facetapi_is_active_searcher($searcher) {
  835. $searchers = facetapi_get_active_searchers();
  836. return (isset($searchers[$searcher]));
  837. }
  838. /**
  839. * Adds an active searcher to the list.
  840. *
  841. * @param $searcher
  842. * The machine readable name of the searcher.
  843. *
  844. * @see facetapi_get_active_searchers();
  845. */
  846. function facetapi_add_active_searcher($searcher) {
  847. $searchers = &drupal_static('facetapi_active_searchers', array());
  848. $searchers[$searcher] = $searcher;
  849. }
  850. /**
  851. * Tests whether a single facet is enabled in a given realm.
  852. *
  853. * @param $searcher
  854. * The machine readable name of the searcher.
  855. * @param $realm_name
  856. * The machine readable name of the realm, pass NULL to test if the facet is
  857. * enabled in at least one realm.
  858. * @param $facet_name
  859. * The machine readable name of the facet.
  860. *
  861. * @return
  862. * A boolean flagging whether the facet is enabled in the passed realm.
  863. */
  864. function facetapi_facet_enabled($searcher, $realm_name, $facet_name) {
  865. $enabled_facets = facetapi_get_enabled_facets($searcher, $realm_name, $facet_name);
  866. return isset($enabled_facets[$facet_name]);
  867. }
  868. /**
  869. * Builds a facet realm.
  870. *
  871. * Converts the facet data into a render array suitable for passing to the
  872. * drupal_render() function.
  873. *
  874. * @param $searcher
  875. * The machine readable name of the searcher.
  876. * @param $realm_name
  877. * The machine readable name of the realm.
  878. *
  879. * @return array
  880. * The realm's render array.
  881. */
  882. function facetapi_build_realm($searcher, $realm_name) {
  883. $adapter = facetapi_adapter_load($searcher);
  884. return ($adapter) ? $adapter->buildRealm($realm_name) : array();
  885. }
  886. /**
  887. * Checks requirements.
  888. *
  889. * Requirements fail if at least one requirements callback returns FALSE. Note
  890. * that if no requirement callbacks are passed, this function will return TRUE.
  891. *
  892. * @param array $requirements
  893. * The requirements keyed by callback to options.
  894. * @param array $realm
  895. * The realm definition.
  896. * @param array $facet
  897. * The facet definition.
  898. *
  899. * @return
  900. * A boolean flagging whether all requirements were passed.
  901. */
  902. function facetapi_check_requirements(array $requirements, array $realm, array $facet, $operator = 'AND') {
  903. $return = TRUE;
  904. module_load_include('inc', 'facetapi', 'facetapi.requirements');
  905. foreach ($requirements as $callback => $options) {
  906. if (!call_user_func($callback, $realm, $facet, $options, $operator)) {
  907. $return = FALSE;
  908. break;
  909. }
  910. }
  911. return $return;
  912. }
  913. /**
  914. * Enables or disables a facet for this page load only.
  915. *
  916. * @param $searcher
  917. * The machine readable name of the searcher.
  918. * @param $realm_name
  919. * The machine readable name of the realm, pass NULL for all realms.
  920. * @param $facet_name
  921. * The machine readable name of the facet.
  922. * @param $status
  923. * A boolean flagging whether the facet is enabled or disabled.
  924. * @param $batch_process
  925. * A boolean flagging whether batch processing is being performed. If set to
  926. * TRUE, the list of enabled facets won't be rebuild and the active items
  927. * won't be re-processed. Note that these tasks will have to be performed
  928. * manually in order for the status to be properly set.
  929. */
  930. function facetapi_set_facet_status($searcher, $realm_name, $facet_name, $status, $batch_process) {
  931. // Rebuild static if not batch processing.
  932. if (!$batch_process) {
  933. drupal_static('facetapi_get_enabled_facets', array(), TRUE);
  934. }
  935. // Pulls the list of enabled facets so we can modify it here.
  936. facetapi_get_enabled_facets($searcher, $realm_name);
  937. $enabled_facets = &drupal_static('facetapi_get_enabled_facets', array());
  938. // Performs the operation by setting or unsetting the facet.
  939. $cid = $searcher . ':' . (string) $realm_name;
  940. if ($status && !isset($enabled_facets[$cid][$facet_name])) {
  941. if ($facet = facetapi_facet_load($facet_name, $searcher)) {
  942. // Add facet to static, which enables it.
  943. $enabled_facets[$cid][$facet_name] = $facet;
  944. // If facet isn't already globally enabled, enable it.
  945. if (!isset($enabled_facets[$searcher . ':'][$facet_name])) {
  946. // Ensure sure static is set before modifying it.
  947. facetapi_get_enabled_facets($searcher, NULL);
  948. $enabled_facets[$searcher . ':'][$facet_name] = $facet;
  949. }
  950. }
  951. }
  952. elseif (!$status && isset($enabled_facets[$cid][$facet_name])) {
  953. // Removes facet to static, which disables it.
  954. unset($enabled_facets[$cid][$facet_name]);
  955. // If acting globally, disable facet in all realms.
  956. if (!$realm_name) {
  957. foreach (facetapi_get_realm_info() as $realm) {
  958. // Ensure sure static is set before unsetting the facet.
  959. facetapi_get_enabled_facets($searcher, $realm['name']);
  960. unset($enabled_facets[$searcher . ':' . $realm['name']][$facet_name]);
  961. }
  962. }
  963. }
  964. else {
  965. return;
  966. }
  967. // Re-process the active items since the list of active facets has changed.
  968. if (!$batch_process && ($adapter = facetapi_adapter_load($searcher))) {
  969. $adapter->processActiveItems();
  970. }
  971. }
  972. /**
  973. * Enables a facet for this page load only.
  974. *
  975. * If you are enabling facets in the block realm, you will have to force the
  976. * delta mapping so that the block can be configured even if it is disabled via
  977. * the Facet API interface. Otherwise you will not be able to assign the block
  978. * to a region because it won't be available in admin/structure/block.
  979. *
  980. * @param $searcher
  981. * The machine readable name of the searcher.
  982. * @param $realm_name
  983. * The machine readable name of the realm, pass NULL for all realms.
  984. * @param $facet_name
  985. * The machine readable name of the facet.
  986. * @param $batch_process
  987. * A boolean flagging whether batch processing is being performed.
  988. *
  989. * @see facetapi_set_facet_status()
  990. * @see hook_facetapi_force_delta_mapping().
  991. */
  992. function facetapi_set_facet_enabled($searcher, $realm_name, $facet_name, $batch_process = FALSE) {
  993. return facetapi_set_facet_status($searcher, $realm_name, $facet_name, TRUE, $batch_process);
  994. }
  995. /**
  996. * Disables a facet for this page load only.
  997. *
  998. * @param $searcher
  999. * The machine readable name of the searcher.
  1000. * @param $realm_name
  1001. * The machine readable name of the realm, pass NULL for all realms.
  1002. * @param $facet_name
  1003. * The machine readable name of the facet.
  1004. * @param $batch_process
  1005. * A boolean flagging whether batch processing is being performed.
  1006. *
  1007. * @see facetapi_set_facet_status()
  1008. */
  1009. function facetapi_set_facet_disabled($searcher, $realm_name, $facet_name, $batch_process = FALSE) {
  1010. return facetapi_set_facet_status($searcher, $realm_name, $facet_name, FALSE, $batch_process);
  1011. }
  1012. /**
  1013. * Sets the facet status in a given realm, stores settings in the database.
  1014. *
  1015. * @param FacetapiAdapter $adapter
  1016. * The adapter object of the searcher the settings are being modified for.
  1017. * @param array $realm
  1018. * The realm definition as returned by facetapi_realm_load().
  1019. * @param array $facet
  1020. * The facet definition as returned by facetapi_facet_load().
  1021. * @param $status
  1022. * Flags whether or not the facet is being enabled or disabled.
  1023. * @param $weight
  1024. * If the realm is sortable, allows the assigning of a weight. Pass FALSE to
  1025. * maintain the previously stored value.
  1026. * @param $batch_process
  1027. * A boolean flagging whether batch processing is being performed. If set to
  1028. * TRUE, the caches and statics won't be reset.
  1029. *
  1030. * @reutrn
  1031. * TRUE if the operation succeeded, FALSE otherwise.
  1032. */
  1033. function facetapi_save_facet_status(FacetapiAdapter $adapter, array $realm, array $facet, $status, $weight, $batch_process) {
  1034. // Loads the realm settings, sets enabled flag and weight.
  1035. $settings = $adapter->getFacet($facet)->getSettings($realm);
  1036. $settings->enabled = ($status) ? 1 : 0;
  1037. if (FALSE !== $weight) {
  1038. $settings->settings['weight'] = $realm['sortable'] ? $weight : 0;
  1039. }
  1040. // Saves the settings in the database, stores the result.
  1041. // NOTE: CTools export componenet loaded in the getSettings() method.
  1042. $success = (FALSE !== ctools_export_crud_save('facetapi', $settings));
  1043. // Clears caches and statics if we are not batch processing.
  1044. if ($success && !$batch_process) {
  1045. drupal_static('facetapi_get_searcher_settings', array(), TRUE);
  1046. drupal_static('facetapi_get_enabled_facets', array(), TRUE);
  1047. if ('block' == $realm['name']) {
  1048. cache_clear_all(NULL, 'cache_block');
  1049. cache_clear_all('facetapi:delta_map', 'cache');
  1050. }
  1051. }
  1052. return $success;
  1053. }
  1054. /**
  1055. * Enables a facet in a given realm, stores settings in the database.
  1056. *
  1057. * @param FacetapiAdapter $adapter
  1058. * The adapter object of the searcher the settings are being modified for.
  1059. * @param array $realm
  1060. * The realm definition as returned by facetapi_realm_load().
  1061. * @param array $facet
  1062. * The facet definition as returned by facetapi_facet_load().
  1063. * @param $weight
  1064. * If the realm is sortable, allows the assigning of a weight. Pass FALSE to
  1065. * maintain the previously stored value.
  1066. * @param $batch_process
  1067. * A boolean flagging whether batch processing is being performed.
  1068. *
  1069. * @reutrn
  1070. * TRUE if the operation succeeded, FALSE otherwise.
  1071. */
  1072. function facetapi_save_facet_enabled(FacetapiAdapter $adapter, array $realm, array $facet, $weight = FALSE, $batch_process = FALSE) {
  1073. return facetapi_save_facet_status($adapter, $realm, $facet, TRUE, $weight, $batch_process);
  1074. }
  1075. /**
  1076. * Disables a facet in a given realm, stores settings in the database.
  1077. *
  1078. * @param FacetapiAdapter $adapter
  1079. * The adapter object of the searcher the settings are being modified for.
  1080. * @param array $realm
  1081. * The realm definition as returned by facetapi_realm_load().
  1082. * @param array $facet
  1083. * The facet definition as returned by facetapi_facet_load().
  1084. * @param $weight
  1085. * If the realm is sortable, allows the assigning of a weight. Pass FALSE to
  1086. * maintain the previously stored value.
  1087. * @param $batch_process
  1088. * A boolean flagging whether batch processing is being performed.
  1089. *
  1090. * @reutrn
  1091. * TRUE if the operation succeeded, FALSE otherwise.
  1092. */
  1093. function facetapi_save_facet_disabled(FacetapiAdapter $adapter, array $realm, array $facet, $weight = FALSE, $batch_process = FALSE) {
  1094. return facetapi_save_facet_status($adapter, $realm, $facet, FALSE, $weight, $batch_process);
  1095. }