search_api_saved_searches.module 42 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211
  1. <?php
  2. /**
  3. * @file
  4. * Offers the ability to save searches and be notified of new results.
  5. */
  6. /**
  7. * Implements hook_menu().
  8. */
  9. function search_api_saved_searches_menu() {
  10. $items['admin/config/search/search_api/index/%search_api_index/saved_searches'] = array(
  11. 'title' => 'Saved searches',
  12. 'description' => 'Let users save searches on this index.',
  13. 'page callback' => 'drupal_get_form',
  14. 'page arguments' => array('search_api_saved_searches_index_edit', 5),
  15. 'access arguments' => array('administer search_api_saved_searches'),
  16. 'weight' => -1,
  17. 'type' => MENU_LOCAL_TASK,
  18. 'context' => MENU_CONTEXT_INLINE | MENU_CONTEXT_PAGE,
  19. 'file' => 'search_api_saved_searches.admin.inc',
  20. );
  21. $items['user/%user/saved-searches'] = array(
  22. 'title' => 'Saved searches',
  23. 'description' => 'View and edit your saved searches.',
  24. 'page callback' => 'search_api_saved_searches_user_listing',
  25. 'page arguments' => array(1),
  26. 'access callback' => 'search_api_saved_search_edit_access',
  27. 'access arguments' => array(1),
  28. 'weight' => 5,
  29. 'type' => MENU_LOCAL_TASK,
  30. 'file' => 'search_api_saved_searches.pages.inc',
  31. );
  32. $items['user/%user/saved-searches/add'] = array(
  33. 'title' => 'Create saved search',
  34. 'description' => 'Create a new saved search.',
  35. 'page callback' => 'search_api_saved_searches_create_manual',
  36. 'access callback' => 'search_api_saved_search_create_access',
  37. 'access arguments' => array(NULL, TRUE),
  38. 'type' => MENU_LOCAL_ACTION,
  39. 'file' => 'search_api_saved_searches.pages.inc',
  40. );
  41. $items['search-api/saved-searches/add'] = array(
  42. 'title' => 'Create saved search',
  43. 'description' => 'Create a new saved search.',
  44. 'page callback' => 'search_api_saved_searches_create_manual',
  45. 'access callback' => 'search_api_saved_search_create_access',
  46. 'access arguments' => array(NULL, TRUE),
  47. 'file' => 'search_api_saved_searches.pages.inc',
  48. );
  49. $items['search-api/saved-searches/add/%search_api_saved_searches_settings'] = array(
  50. 'title' => 'Create saved search',
  51. 'description' => 'Create a new saved search.',
  52. 'page callback' => 'search_api_saved_searches_create_manual',
  53. 'page arguments' => array(3),
  54. 'access callback' => 'search_api_saved_search_create_access',
  55. 'access arguments' => array(3, TRUE),
  56. 'file' => 'search_api_saved_searches.pages.inc',
  57. );
  58. $items['search-api/saved-search/%search_api_saved_search/activate/%'] = array(
  59. 'title' => 'Activate saved search',
  60. 'description' => 'Activate a new saved search.',
  61. 'page callback' => 'search_api_saved_searches_activate_page',
  62. 'page arguments' => array(2, 4),
  63. 'access callback' => 'search_api_saved_search_edit_access',
  64. 'access arguments' => array(NULL, 2, 4),
  65. 'file' => 'search_api_saved_searches.pages.inc',
  66. );
  67. $items['search-api/saved-search/%search_api_saved_search/edit'] = array(
  68. 'title' => 'Edit saved search',
  69. 'description' => 'Edit a saved search.',
  70. 'page callback' => 'drupal_get_form',
  71. 'page arguments' => array('search_api_saved_searches_search_edit_form', 2),
  72. 'access callback' => 'search_api_saved_search_edit_access',
  73. 'access arguments' => array(NULL, 2, 4),
  74. 'file' => 'search_api_saved_searches.pages.inc',
  75. );
  76. $items['search-api/saved-search/%search_api_saved_search/delete'] = array(
  77. 'title' => 'Delete saved search',
  78. 'description' => 'Delete a saved search.',
  79. 'page callback' => 'drupal_get_form',
  80. 'page arguments' => array('search_api_saved_searches_search_delete_form', 2),
  81. 'access callback' => 'search_api_saved_search_edit_access',
  82. 'access arguments' => array(NULL, 2, 4),
  83. 'file' => 'search_api_saved_searches.pages.inc',
  84. );
  85. return $items;
  86. }
  87. /**
  88. * Implements hook_permission();
  89. */
  90. function search_api_saved_searches_permission() {
  91. $perms['administer search_api_saved_searches'] = array(
  92. 'title' => t('Administer saved searches'),
  93. 'description' => t('Enable and configure saved searches for search indexes.'),
  94. );
  95. $perms['use search_api_saved_searches'] = array(
  96. 'title' => t('Use saved searches'),
  97. 'description' => t('Save searches and receive e-mail notifications.'),
  98. );
  99. return $perms;
  100. }
  101. /**
  102. * Implements hook_entity_info().
  103. */
  104. function search_api_saved_searches_entity_info() {
  105. $info['search_api_saved_searches_settings'] = array(
  106. 'label' => t('Saved search settings'),
  107. 'controller class' => 'EntityAPIControllerExportable',
  108. 'entity class' => 'SearchApiSavedSearchesSettings',
  109. 'base table' => 'search_api_saved_searches_settings',
  110. 'uri callback' => 'search_api_saved_searches_settings_url',
  111. 'access callback' => 'search_api_saved_searches_settings_access',
  112. 'module' => 'search_api_saved_searches',
  113. 'exportable' => TRUE,
  114. 'entity keys' => array(
  115. 'id' => 'id',
  116. 'name' => 'delta',
  117. 'label' => 'delta',
  118. ),
  119. );
  120. $info['search_api_saved_search'] = array(
  121. 'label' => t('Saved search'),
  122. 'controller class' => 'EntityAPIController',
  123. 'entity class' => 'SearchApiSavedSearch',
  124. 'base table' => 'search_api_saved_search',
  125. 'access callback' => 'search_api_saved_search_access',
  126. 'module' => 'search_api_saved_searches',
  127. 'exportable' => FALSE,
  128. 'entity keys' => array(
  129. 'id' => 'id',
  130. 'label' => 'name',
  131. ),
  132. );
  133. return $info;
  134. }
  135. /**
  136. * Implements hook_entity_property_info_alter().
  137. *
  138. * Corrects the types which the Entity API automatically infers from the schema.
  139. * Otherwise, the "token" types would be "text", and "boolean" and "date" would
  140. * be "integer". Also, changes saved search results to be a list, not just a CSV
  141. * string.
  142. *
  143. * Fixing this here automatically also fixes the Views integration provided by
  144. * the Entity API, regarding these types.
  145. */
  146. function search_api_saved_searches_entity_property_info_alter(array &$info) {
  147. $settings = &$info['search_api_saved_searches_settings']['properties'];
  148. $settings['index_id']['type'] = 'token';
  149. $settings['enabled']['type'] = 'boolean';
  150. $settings['module']['type'] = 'token';
  151. $searches = &$info['search_api_saved_search']['properties'];
  152. $searches['settings_id']['type'] = 'token';
  153. $searches['enabled']['type'] = 'boolean';
  154. $searches['created']['type'] = 'date';
  155. $searches['last_execute']['type'] = 'date';
  156. // We can't assign "duration" until Entity API Views integration supports
  157. // this.
  158. //$searches['notify_interval']['type'] = 'duration';
  159. $searches['results']['type'] = 'list<token>';
  160. $searches['results']['getter callback'] = 'search_api_saved_searches_get_results_property';
  161. }
  162. /**
  163. * Getter callback for the saved search results property.
  164. *
  165. * @param SearchApiSavedSearch $search
  166. * The search whose results should be returned.
  167. * @param array $options
  168. * Options for the property. Are ignored.
  169. * @param string $property
  170. * The property to retrieve. Will always be "results".
  171. * @param string $entity_type
  172. * The entity type. Will always be "search_api_saved_search".
  173. *
  174. * @return array
  175. * An array with the IDs of all stored results.
  176. */
  177. function search_api_saved_searches_get_results_property(SearchApiSavedSearch $search, array $options, $property, $entity_type) {
  178. return $search->results ? explode(',', $search->results) : array();
  179. }
  180. /**
  181. * Implements hook_views_api().
  182. */
  183. function search_api_saved_searches_views_api() {
  184. return array(
  185. 'api' => 3,
  186. 'path' => drupal_get_path('module', 'search_api_saved_searches') . '/views',
  187. );
  188. }
  189. /**
  190. * URL callback for settings entities.
  191. */
  192. function search_api_saved_searches_settings_url(SearchApiSavedSearchesSettings $settings) {
  193. return array('path' => 'admin/config/search/search_api/index/' . $settings->index_id . '/saved_searches');
  194. }
  195. /**
  196. * Access callback for settings entities.
  197. *
  198. * @param string $op
  199. * The operation being performed. One of "view", "update", "create" or
  200. * "delete".
  201. * @param SearchApiSavedSearchesSettings|null $settings
  202. * (optional) The entity to check access for. If NULL is given, it will be
  203. * determined whether access is allowed for all settings.
  204. * @param object|null $account
  205. * The user to check for. NULL to check for the global user.
  206. *
  207. * @return bool
  208. * Whether access is allowed or not.
  209. *
  210. * @see entity_access
  211. */
  212. function search_api_saved_searches_settings_access($op, SearchApiSavedSearchesSettings $settings = NULL, $account = NULL) {
  213. return user_access('administer search_api_saved_searches', $account);
  214. }
  215. /**
  216. * Access callback for saved search entities.
  217. *
  218. * @param string $op
  219. * The operation being performed. One of "view", "update", "create" or
  220. * "delete".
  221. * @param SearchApiSavedSearch|null $search
  222. * (optional) The entity to check access for. If NULL is given, it will be
  223. * determined whether access is allowed for all searches.
  224. * @param object|null $account
  225. * The user to check for. NULL to check for the global user.
  226. *
  227. * @return bool
  228. * Whether access is allowed or not.
  229. *
  230. * @see entity_access
  231. */
  232. function search_api_saved_search_access($op, SearchApiSavedSearch $search = NULL, $account = NULL) {
  233. if (user_access('administer search_api_saved_searches', $account)) {
  234. return TRUE;
  235. }
  236. if (!$account) {
  237. global $user;
  238. $account = $user;
  239. }
  240. switch ($op) {
  241. case 'create':
  242. return user_access('use search_api_saved_searches', $account);
  243. default:
  244. // If the search was created by an anonymous user, there's no way we can
  245. // correctly determine access here.
  246. if (!$search || !$search->uid) {
  247. return FALSE;
  248. }
  249. return $search->uid == $account->uid;
  250. }
  251. }
  252. /**
  253. * Implements hook_user_insert().
  254. *
  255. * If a new user already has saved searches with the same mail address,
  256. * associate them with the new user. However, only do this if the user is
  257. * already active.
  258. */
  259. function search_api_saved_searches_user_insert(&$edit, $account, $category) {
  260. if (!empty($account->status)) {
  261. foreach (search_api_saved_search_load_multiple(FALSE, array('mail' => $account->mail, 'uid' => 0)) as $search) {
  262. $search->uid = $account->uid;
  263. if (empty($search->settings()->options['registered_user_delete_key'])) {
  264. unset($search->options['key']);
  265. }
  266. $search->save();
  267. }
  268. }
  269. }
  270. /**
  271. * Implements hook_user_update().
  272. *
  273. * If a user gets activated, associate saved searches with the same mail address
  274. * with them.
  275. *
  276. * Also, change mail address of saved searches when the user mail address
  277. * changes.
  278. */
  279. function search_api_saved_searches_user_update(&$edit, $account, $category) {
  280. // Changes of mail and status.
  281. if (!empty($account->status) && empty($account->original->status)) {
  282. foreach (search_api_saved_search_load_multiple(FALSE, array('mail' => $account->mail, 'uid' => 0)) as $search) {
  283. $search->uid = $account->uid;
  284. if (empty($search->settings()->options['registered_user_delete_key'])) {
  285. unset($search->options['key']);
  286. }
  287. $search->save();
  288. }
  289. }
  290. if ($account->mail != $account->original->mail) {
  291. foreach (search_api_saved_search_load_multiple(FALSE, array('mail' => $account->mail, 'uid' => $account->uid)) as $search) {
  292. $search->mail = $account->mail;
  293. $search->save();
  294. }
  295. }
  296. }
  297. /**
  298. * Implements hook_user_delete().
  299. *
  300. * If a user is deleted, delete their saved searches, too.
  301. */
  302. function search_api_saved_searches_user_delete($account) {
  303. entity_delete_multiple('search_api_saved_search', array_keys(search_api_saved_search_load_multiple(FALSE, array('uid' => $account->uid))));
  304. }
  305. // @todo Rules integration
  306. /**
  307. * Implements hook_search_api_index_update().
  308. *
  309. * If the index got disabled, do the same with its search settings.
  310. */
  311. function search_api_saved_searches_search_api_index_update(SearchApiIndex $index) {
  312. if (!$index->enabled && $index->original->enabled) {
  313. foreach (search_api_saved_searches_settings_load_multiple(FALSE, array('index_id' => $index->machine_name)) as $settings) {
  314. if ($settings->enabled) {
  315. $settings->enabled = FALSE;
  316. $settings->save();
  317. }
  318. }
  319. }
  320. }
  321. /**
  322. * Implements hook_search_api_index_delete().
  323. *
  324. * Deletes the settings associated with a search index.
  325. */
  326. function search_api_saved_searches_search_api_index_delete(SearchApiIndex $index) {
  327. // Only react on real delete, not revert.
  328. if ($index->status & ENTITY_IN_CODE) {
  329. return;
  330. }
  331. foreach (search_api_saved_searches_settings_load_multiple(FALSE, array('index_id' => $index->machine_name)) as $settings) {
  332. $settings->delete();
  333. }
  334. }
  335. /**
  336. * Implements hook_search_api_saved_searches_settings_insert().
  337. *
  338. * Clear block caches when new enabled saved search settings are saved.
  339. */
  340. function search_api_saved_searches_search_api_saved_searches_settings_insert(SearchApiSavedSearchesSettings $settings) {
  341. if ($settings->enabled) {
  342. block_flush_caches();
  343. cache_clear_all('*', 'cache_block', TRUE);
  344. }
  345. }
  346. /**
  347. * Implements hook_search_api_saved_searches_settings_update().
  348. *
  349. * Clear block caches when saved search settings are enabled or disabled.
  350. */
  351. function search_api_saved_searches_search_api_saved_searches_settings_update(SearchApiSavedSearchesSettings $settings) {
  352. if ($settings->enabled != $settings->original->enabled) {
  353. block_flush_caches();
  354. cache_clear_all('*', 'cache_block', TRUE);
  355. }
  356. // React if the new results determination method was switched to/from the
  357. // ID-based method.
  358. $options = $settings->options + array('date_field' => NULL);
  359. $orig_options = $settings->original->options + array('date_field' => NULL);
  360. if ($options['date_field'] != $orig_options['date_field']) {
  361. if (!$options['date_field']) {
  362. // When we switch to the ID-based method from another one, we need to save
  363. // the current results.
  364. foreach (search_api_saved_search_load_multiple(FALSE, array('settings_id' => $settings->delta)) as $search) {
  365. // This will automatically populate the results.
  366. $search->save();
  367. }
  368. }
  369. elseif (!$orig_options['date_field']) {
  370. // If we previously used the ID-based method and are now using a
  371. // field-based one, set the saved results for all searches to NULL.
  372. db_update('search_api_saved_search')
  373. ->fields(array(
  374. 'results' => NULL,
  375. ))
  376. ->condition('settings_id', $settings->delta)
  377. ->execute();
  378. }
  379. }
  380. }
  381. /**
  382. * Implements hook_search_api_saved_searches_settings_delete().
  383. *
  384. * Clear block caches when enabled saved search settings are deleted.
  385. */
  386. function search_api_saved_searches_search_api_saved_searches_settings_delete(SearchApiSavedSearchesSettings $settings) {
  387. // Only react on real delete, not revert.
  388. if ($settings->status & ENTITY_IN_CODE) {
  389. return;
  390. }
  391. foreach (search_api_saved_search_load_multiple(FALSE, array('settings_id' => $settings->delta)) as $search) {
  392. $search->delete();
  393. }
  394. if ($settings->enabled) {
  395. block_flush_caches();
  396. cache_clear_all('*', 'cache_block', TRUE);
  397. }
  398. }
  399. /**
  400. * Loads a single settings object.
  401. *
  402. * @param int|string $id
  403. * The settings' identifier or delta.
  404. * @param bool $reset
  405. * If TRUE, will reset the internal entity cache.
  406. *
  407. * @return SearchApiSavedSearchesSettings
  408. * The requested entity, or FALSE if no settings for that ID exist.
  409. */
  410. function search_api_saved_searches_settings_load($id, $reset = FALSE) {
  411. $ret = search_api_saved_searches_settings_load_multiple(array($id), array(), $reset);
  412. return $ret ? reset($ret) : FALSE;
  413. }
  414. /**
  415. * Loads multiple settings objects.
  416. *
  417. * @param array|false $ids
  418. * The settings' identifiers or deltas; or FALSE to load all settings objects.
  419. * @param array $conditions
  420. * Associative array of field => value conditions that returned objects must
  421. * satisfy.
  422. * @param bool $reset
  423. * If TRUE, will reset the internal entity cache.
  424. *
  425. * @return array
  426. * An array of SearchApiSavedSearchesSettings objects matching the conditions,
  427. * keyed by delta.
  428. */
  429. function search_api_saved_searches_settings_load_multiple($ids = FALSE, array $conditions = array(), $reset = FALSE) {
  430. $settings = entity_load('search_api_saved_searches_settings', $ids, $conditions, $reset);
  431. return entity_key_array_by_property($settings, 'delta');
  432. }
  433. /**
  434. * Loads a single saved search object.
  435. *
  436. * @param $id
  437. * The saved search's ID.
  438. * @param $reset
  439. * If TRUE, will reset the internal entity cache.
  440. *
  441. * @return SearchApiSavedSearch
  442. * The requested entity, or FALSE if no settings for that ID exist.
  443. */
  444. function search_api_saved_search_load($id, $reset = FALSE) {
  445. $ret = entity_load('search_api_saved_search', array($id), array(), $reset);
  446. return $ret ? reset($ret) : FALSE;
  447. }
  448. /**
  449. * Loads multiple saved search objects.
  450. *
  451. * @param $ids
  452. * The saved search's IDs; or FALSE to load all saved searches.
  453. * @param array $conditions
  454. * Associative array of field => value conditions that returned objects must
  455. * satisfy.
  456. * @param $reset
  457. * If TRUE, will reset the internal entity cache.
  458. *
  459. * @return array
  460. * An array of SearchApiSavedSearch objects matching the conditions.
  461. */
  462. function search_api_saved_search_load_multiple($ids = FALSE, array $conditions = array(), $reset = FALSE) {
  463. return entity_load('search_api_saved_search', $ids, $conditions, $reset);
  464. }
  465. /**
  466. * Determine whether the current user can create a saved search for specific settings.
  467. *
  468. * @param SearchApiSavedSearchesSettings $settings
  469. * The settings to check for. May be NULL, if $manual is TRUE, to check if any
  470. * saved searches can be created manually.
  471. * @param boolean $manual
  472. * (optional) If TRUE, check access for creating a saved search manually.
  473. *
  474. * @return boolean
  475. * TRUE iff the current user is allowed to create a new saved search.
  476. */
  477. function search_api_saved_search_create_access(SearchApiSavedSearchesSettings $settings = NULL, $manual = FALSE) {
  478. if ($manual) {
  479. if (isset($settings)) {
  480. if (!$settings->enabled || empty($settings->options['manual']['allow'])) {
  481. return FALSE;
  482. }
  483. }
  484. else {
  485. foreach (search_api_saved_searches_settings_load_multiple(FALSE, array('enabled' => TRUE)) as $settings) {
  486. if (!empty($settings->options['manual']['allow'])) {
  487. $found = TRUE;
  488. break;
  489. }
  490. }
  491. if (empty($found)) {
  492. return FALSE;
  493. }
  494. }
  495. }
  496. elseif (!$settings->enabled) {
  497. return FALSE;
  498. }
  499. if (user_access('administer search_api_saved_searches')) {
  500. return TRUE;
  501. }
  502. if (!user_access('use search_api_saved_searches')) {
  503. return FALSE;
  504. }
  505. if (!isset($settings)) {
  506. return TRUE;
  507. }
  508. // @todo Check settings-specific access rules, when there are any.
  509. return TRUE;
  510. }
  511. /**
  512. * Determine access to the edit interface for saved searches of a given user.
  513. *
  514. * This is both used to determine whether the current user can edit a specific
  515. * saved search, or whether she can display the overview of the user's saved
  516. * searches.
  517. * For anonymous users' searches an access key is generated that allows
  518. * accessing and editing the searches.
  519. *
  520. * @param $account
  521. * (optional) The user whose saved search(es) would be edited. NULL for guest.
  522. * @param SearchApiSavedSearch $search
  523. * (optional) The saved search involved, if there is just a single one.
  524. * @param string $key
  525. * (optional) The secret key to access the search.
  526. *
  527. * @return boolean
  528. * TRUE iff the current user is allowed to edit the saved search(es).
  529. */
  530. function search_api_saved_search_edit_access($account = NULL, SearchApiSavedSearch $search = NULL, $key = NULL) {
  531. global $user;
  532. if (empty($account)) {
  533. if (empty($search)) {
  534. return FALSE;
  535. }
  536. $account = (object) array('uid' => $search->uid);
  537. }
  538. if (user_access('administer search_api_saved_searches')) {
  539. return TRUE;
  540. }
  541. // Barring admins, the only way to edit anonymous users' saved searches is by
  542. // providing the access key. There is no overview of all saved searches.
  543. if (!empty($key) && !empty($search->options['key']) && $search->options['key'] == $key) {
  544. return TRUE;
  545. }
  546. if ($account->uid == 0) {
  547. return FALSE;
  548. }
  549. if ($account->uid != $user->uid || !user_access('use search_api_saved_searches')) {
  550. return FALSE;
  551. }
  552. if (isset($search)) {
  553. return $search->uid == $account->uid;
  554. }
  555. foreach (search_api_saved_searches_settings_load_multiple() as $settings) {
  556. // Allow access if users can manually create searches.
  557. if (!empty($settings->options['manual']['allow'])) {
  558. return TRUE;
  559. }
  560. // Allow access if the list should always be displayed.
  561. if (!empty($settings->options['show_empty_list'])) {
  562. return TRUE;
  563. }
  564. }
  565. // Let the user view the listing if there are any saved searches.
  566. $select = db_select('search_api_saved_search', 's')
  567. ->condition('uid', $account->uid);
  568. $select->addExpression('COUNT(1)');
  569. return (bool) $select->execute()->fetchField();
  570. }
  571. /**
  572. * Implements hook_block_info().
  573. */
  574. function search_api_saved_searches_block_info() {
  575. $blocks = array();
  576. foreach (search_api_saved_searches_settings_load_multiple(FALSE, array('enabled' => TRUE)) as $settings) {
  577. try {
  578. $blocks[$settings->delta] = array(
  579. 'info' => t('!index: Save search', array('!index' => $settings->index()->name)),
  580. // @todo Is this cache setting correct?
  581. 'cache' => DRUPAL_CACHE_PER_ROLE | DRUPAL_CACHE_PER_PAGE,
  582. );
  583. }
  584. catch (SearchApiException $e) {}
  585. }
  586. return $blocks;
  587. }
  588. /**
  589. * Implements hook_block_configure().
  590. */
  591. function search_api_saved_searches_block_configure($delta = '') {
  592. $settings = search_api_saved_searches_settings_load($delta);
  593. $form['settings_link'] = array(
  594. '#markup' => l(t('To saved search settings'), 'admin/config/search/search_api/index/' . $settings->index_id . '/saved_searches'),
  595. );
  596. return $form;
  597. }
  598. /**
  599. * Implements hook_block_view().
  600. */
  601. function search_api_saved_searches_block_view($delta = '') {
  602. $searches = search_api_current_search();
  603. if (!$searches) {
  604. return;
  605. }
  606. if (!user_access('use search_api_saved_searches')) {
  607. return;
  608. }
  609. $settings = search_api_saved_searches_settings_load($delta);
  610. if (!$settings || !search_api_saved_search_create_access($settings)) {
  611. return;
  612. }
  613. $index_id = $settings->index_id;
  614. $options = $settings->options;
  615. $ids_list = drupal_map_assoc($options['ids_list']);
  616. $search_ids = variable_get('search_api_saved_searches_search_ids', array());
  617. foreach ($searches as $id => $data) {
  618. if ($data[0]->getIndex()->machine_name == $index_id){
  619. if (!isset($search_ids[$index_id][$id])) {
  620. $search_ids[$index_id][$id] = $id;
  621. $search_ids_updated = TRUE;
  622. }
  623. if (isset($ids_list[$id]) != $options['default_true']) {
  624. if (isset($query)) {
  625. watchdog('search_api_saved_searches', 'Two matching searches on index %index for saved search block.',
  626. array('%index' => $settings->index()->name), WATCHDOG_WARNING,
  627. l(t('view page'), $_GET['q'], array('query' => drupal_get_query_parameters())));
  628. }
  629. else {
  630. list($query, $results) = $data;
  631. }
  632. }
  633. }
  634. }
  635. if (isset($search_ids_updated)) {
  636. variable_set('search_api_saved_searches_search_ids', $search_ids);
  637. }
  638. if (empty($query)) {
  639. return;
  640. }
  641. return array(
  642. 'subject' => t('Save search'),
  643. 'content' => array('form' => drupal_get_form('search_api_saved_searches_save_form', $settings, $query)),
  644. );
  645. }
  646. /**
  647. * Form builder for creating a new saved search.
  648. *
  649. * @param SearchApiSavedSearchesSettings $settings
  650. * The saved search settings with which to create a new saved search.
  651. * @param SearchApiQueryInterface $query
  652. * (optional) If creating a saved search for an already executed query, the
  653. * query.
  654. *
  655. * @see search_api_saved_searches_save_form_validate()
  656. * @see search_api_saved_searches_save_form_submit()
  657. * @ingroup forms
  658. */
  659. function search_api_saved_searches_save_form(array $form, array &$form_state, SearchApiSavedSearchesSettings $settings, SearchApiQueryInterface $query = NULL) {
  660. global $user;
  661. if (!isset($form_state['query']) && isset($query)) {
  662. $options = $query->getOptions();
  663. // When checking for new results, we need all results.
  664. // @todo Make this configurable?
  665. unset($options['offset'], $options['limit']);
  666. $options['search id'] = $settings->delta . ':' . 'saved-search';
  667. $form_state['query'] = array(
  668. 'index_id' => $query->getIndex()->machine_name,
  669. 'keys' => $query->getKeys(),
  670. 'original_keys' => $query->getOriginalKeys(),
  671. 'fields' => $query->getFields(),
  672. 'filters' => $query->getFilter()->getFilters(),
  673. 'options' => $options,
  674. );
  675. }
  676. $form_state['settings'] = $settings;
  677. $description = $settings->getTranslatedOption('description');
  678. if (!empty($description)) {
  679. $form['description'] = array(
  680. '#type' => 'item',
  681. '#description' => _filter_autop(check_plain($description)),
  682. );
  683. }
  684. if (empty($form_state['query'])) {
  685. $form['query'] = _search_api_saved_searches_create_search_form($settings);
  686. $form['name'] = array(
  687. '#type' => 'textfield',
  688. '#title' => t('Name'),
  689. '#description' => t('Enter the name that will be displayed for this saved search.'),
  690. '#maxlength' => 50,
  691. );
  692. }
  693. else {
  694. $form['#prefix'] = '<div id="search-api-saved-searches-save-form-wrapper">';
  695. $form['#suffix'] = '</div>';
  696. if (empty($settings->options['choose_name'])) {
  697. $form['name'] = array(
  698. '#type' => 'value',
  699. '#value' => _search_api_saved_searches_create_name($form_state['query']),
  700. );
  701. }
  702. else {
  703. $form['name'] = array(
  704. '#type' => 'textfield',
  705. '#title' => t('Name'),
  706. '#maxlength' => 50,
  707. '#size' => 16,
  708. '#required' => TRUE,
  709. '#default_value' => _search_api_saved_searches_create_name($form_state['query']),
  710. );
  711. }
  712. }
  713. if (empty($user->mail) || $settings->options['registered_choose_mail']) {
  714. $form['mail'] = array(
  715. '#type' => 'textfield',
  716. '#title' => t('E-mail address'),
  717. '#maxlength' => 100,
  718. '#size' => 16,
  719. '#default_value' => isset($user->mail) ? $user->mail : '',
  720. '#required' => TRUE,
  721. );
  722. }
  723. else {
  724. $form['mail'] = array(
  725. '#type' => 'value',
  726. '#value' => $user->mail,
  727. );
  728. }
  729. if ($settings->options['user_select_interval'] && count($settings->options['interval_options']) > 1) {
  730. $form['notify_interval'] = array(
  731. '#type' => 'select',
  732. '#title' => t('Notification interval'),
  733. '#options' => $settings->getTranslatedOption('interval_options'),
  734. '#required' => TRUE,
  735. );
  736. }
  737. else {
  738. $form['notify_interval'] = array(
  739. '#type' => 'value',
  740. '#value' => $settings->options['user_select_interval'] ? reset($settings->options['interval_options']) : $settings->options['set_interval'],
  741. );
  742. }
  743. if (!empty($form_state['query'])) {
  744. $form_state['page'] = array(
  745. 'path' => $_GET['q'],
  746. 'query' => drupal_get_query_parameters(),
  747. );
  748. }
  749. $form['submit'] = array(
  750. '#type' => 'submit',
  751. '#value' => t('Save search'),
  752. '#ajax' => array(
  753. 'callback' => 'search_api_saved_searches_save_form_ajax',
  754. 'wrapper' => 'search-api-saved-searches-save-form-wrapper',
  755. 'effect' => 'fade',
  756. 'method' => 'replace',
  757. ),
  758. '#executes_submit_callback' => TRUE,
  759. );
  760. // For manual search creation we don't need AJAX functionality.
  761. if (empty($form_state['query'])) {
  762. unset($form['submit']['#ajax']);
  763. }
  764. return $form;
  765. }
  766. /**
  767. * Helper function for creating a form for manually creating a saved search.
  768. */
  769. function _search_api_saved_searches_create_search_form(SearchApiSavedSearchesSettings $settings) {
  770. $index = $settings->index();
  771. $wrapper = $index->entityWrapper();
  772. $options = isset($settings->options['manual']) ? $settings->options['manual'] : array();
  773. $form['#tree'] = TRUE;
  774. $form['fields'] = array(
  775. '#type' => 'fieldset',
  776. '#title' => t('Search'),
  777. );
  778. if (!empty($options['fulltext'])) {
  779. $form['fields']['search_api_saved_searches_fulltext'] = array(
  780. '#type' => 'textfield',
  781. '#title' => t('Keywords'),
  782. );
  783. }
  784. if (!empty($options['fields'])) {
  785. foreach ($options['fields'] as $field) {
  786. if (!empty($index->options['fields'][$field])) {
  787. // Extract the necessary field information out of the wrapper.
  788. $tmp = $wrapper;
  789. foreach (explode(':', $field) as $part) {
  790. if (!isset($tmp->$part)) {
  791. continue 2;
  792. }
  793. $tmp = $tmp->$part;
  794. }
  795. $info = $tmp->info();
  796. $form['fields'][$field]['#title'] = isset($info['label']) ? $info['label'] : $field;
  797. if ($optList = $tmp->optionsList('view')) {
  798. $optList = array(NULL => t('- Any -')) + $optList;
  799. $form['fields'][$field]['#type'] = 'select';
  800. $form['fields'][$field]['#options'] = $optList;
  801. }
  802. else {
  803. $form['fields'][$field]['#type'] = 'textfield';
  804. }
  805. }
  806. }
  807. }
  808. return $form;
  809. }
  810. /**
  811. * AJAX submit handler for search_api_saved_searches_save_form().
  812. */
  813. function search_api_saved_searches_save_form_ajax(array $form, array &$form_state) {
  814. return form_get_errors() ? $form : array('#theme' => 'status_messages');
  815. }
  816. /**
  817. * Form validation handler for search_api_saved_searches_save_form().
  818. *
  819. * @see search_api_saved_searches_save_form()
  820. * @see search_api_saved_searches_save_form_submit()
  821. */
  822. function search_api_saved_searches_save_form_validate(array $form, array &$form_state) {
  823. if ($msg = user_validate_mail($form_state['values']['mail'])) {
  824. form_error($form['mail'], $msg);
  825. }
  826. }
  827. /**
  828. * Form validation handler for search_api_saved_searches_save_form().
  829. *
  830. * @return boolean
  831. * TRUE iff the search was successfully saved.
  832. *
  833. * @see search_api_saved_searches_save_form()
  834. * @see search_api_saved_searches_save_form_validate()
  835. */
  836. function search_api_saved_searches_save_form_submit(array $form, array &$form_state) {
  837. global $user;
  838. $values = $form_state['values'];
  839. $settings = $form_state['settings'];
  840. if (empty($form_state['query'])) {
  841. $fields = $values['query']['fields'];
  842. $query = array(
  843. 'keys' => isset($fields['search_api_saved_searches_fulltext']) ? $fields['search_api_saved_searches_fulltext'] : NULL,
  844. 'fields' => NULL,
  845. 'filters' => array(),
  846. 'options' => array(
  847. 'search id' => $settings->delta . ':' . 'saved-search',
  848. ),
  849. );
  850. unset($fields['search_api_saved_searches_fulltext']);
  851. foreach ($fields as $field => $value) {
  852. if ($value || is_numeric($value)) {
  853. $query['filters'][] = array($field, $value, '=');
  854. }
  855. else {
  856. unset($fields[$field]);
  857. }
  858. }
  859. if (empty($values['name'])) {
  860. $query['original_keys'] = $query['keys'];
  861. $values['name'] = _search_api_saved_searches_create_name($query);
  862. unset($query['original_keys']);
  863. }
  864. if (empty($form_state['page']) && !empty($settings->options['manual']['page']['path'])) {
  865. $page_options = $settings->options['manual']['page'];
  866. $form_state['page'] = array(
  867. 'path' => $page_options['path'],
  868. 'query' => array(),
  869. );
  870. if (isset($query['keys'])) {
  871. if (empty($page_options['fulltext'])) {
  872. $form_state['page']['path'] .= '/' . $query['keys'];
  873. }
  874. else {
  875. $form_state['page']['query'][$page_options['fulltext']] = $query['keys'];
  876. }
  877. }
  878. foreach ($fields as $field => $value) {
  879. if (empty($page_options['direct_filter_params'])) {
  880. $form_state['page']['query']['filter'][$field][] = '"' . $value . '"';
  881. }
  882. else {
  883. $form_state['page']['query'][$field] = $value;
  884. }
  885. }
  886. }
  887. }
  888. else {
  889. $query = array_intersect_key($form_state['query'], drupal_map_assoc(array('keys', 'fields', 'filters', 'options')));
  890. }
  891. // Enable the saved search right away, if a logged-in user uses their own mail
  892. // address, or when they have admin privileges, or when activation mails are
  893. // generally deactivated, or if there are already active saved searches for
  894. // that user with that mail address. Otherwise, an activation mail will be
  895. // sent.
  896. $enabled = (!empty($user->mail) && $user->mail == $values['mail'])
  897. || user_access('administer search_api_saved_searches')
  898. || empty($settings->options['mail']['activate']['send'])
  899. || ($user->uid && search_api_saved_search_load_multiple(FALSE, array('enabled' => TRUE, 'uid' => $user->uid, 'mail' => $values['mail'])));
  900. // If an anonymous user uses an existing user's mail address to create a
  901. // saved search, file the saved search under that user right away.
  902. $uid = $user->uid;
  903. if (!$uid && ($users = user_load_multiple(FALSE, array('mail' => $values['mail'], 'status' => 1)))) {
  904. $uid = key($users);
  905. }
  906. $search = entity_create('search_api_saved_search', array(
  907. 'uid' => $uid,
  908. 'settings_id' => $settings->delta,
  909. 'enabled' => $enabled,
  910. 'name' => $values['name'],
  911. 'mail' => $values['mail'],
  912. 'created' => REQUEST_TIME,
  913. 'last_execute' => REQUEST_TIME,
  914. 'notify_interval' => $values['notify_interval'],
  915. 'query' => $query,
  916. 'options' => array(),
  917. ));
  918. // Choose where to redirect.
  919. if (!empty($form_state['page'])) {
  920. $search->options['page'] = $form_state['page'];
  921. $form_state['redirect'] = array($form_state['page']['path'], $form_state['page']);
  922. }
  923. elseif ($user->uid) {
  924. $form_state['redirect'] = 'user/' . $user->uid . '/saved-searches';
  925. }
  926. // Save saved search.
  927. $ret = $search->save();
  928. // Display success or error message.
  929. if (!$ret) {
  930. drupal_set_message(t('An error occurred while trying to save the search. Please contact the site administrator.'), 'error');
  931. $form_state['rebuild'] = TRUE;
  932. return FALSE;
  933. }
  934. else {
  935. if ($enabled) {
  936. if ($search->notify_interval < 0) {
  937. drupal_set_message(t('Your saved search was successfully created.'));
  938. }
  939. else {
  940. drupal_set_message(t('Your saved search was successfully created. You will receive e-mail notifications for new results in the future.'));
  941. }
  942. }
  943. else {
  944. drupal_set_message(t('Your saved search was successfully created. You will soon receive an e-mail with a confirmation link to activate it.'));
  945. }
  946. return TRUE;
  947. }
  948. }
  949. /**
  950. * Helper function for creating a name for a saved search with the given query.
  951. */
  952. function _search_api_saved_searches_create_name(array $query) {
  953. if (!empty($query['original_keys']) && is_scalar($query['original_keys'])) {
  954. $ret[] = $query['original_keys'];
  955. }
  956. $name = isset($ret) ? implode(' / ', $ret) : t('Saved search');
  957. drupal_alter('search_api_saved_search_create_name', $name, $query);
  958. return $name;
  959. }
  960. /**
  961. * Implements hook_mail().
  962. *
  963. * Two mails are provided, which expect the following values in the $params
  964. * array:
  965. * - activate:
  966. * - search: The SearchApiSavedSearch object that should be activated.
  967. * - user: The user object to which the saved search belongs.
  968. * - notify:
  969. * - user: The user to which the executed searches belong.
  970. * - settings: The settings with which the searches are associated.
  971. * - searches: An array containing arrays with the following keys:
  972. * - search: A SearchApiSavedSearch object that was checked.
  973. * - num_results: The number of new results for that saved search.
  974. * - results: An array of entities representing the new results for that
  975. * saved search.
  976. */
  977. function search_api_saved_searches_mail($key, array &$message, array $params) {
  978. $language = $message['language'];
  979. switch ($key) {
  980. case 'activate':
  981. $search = $params['search'];
  982. $settings = $search->settings();
  983. $data = array(
  984. 'user' => $params['user'],
  985. 'search_api_saved_search_info' => array(
  986. 'search' => $search,
  987. 'results' => array(),
  988. ),
  989. );
  990. $title = $settings->getTranslatedOption('mail.activate.title', $language->language);
  991. $message['subject'] .= token_replace($title, $data, array('language' => $language, 'sanitize' => FALSE));
  992. $body = $settings->getTranslatedOption('mail.activate.body', $language->language);
  993. $message['body'][] = token_replace($body, $data, array('language' => $language, 'sanitize' => FALSE));
  994. break;
  995. case 'notify':
  996. $settings = $params['settings'];
  997. $data = array(
  998. 'user' => $params['user'],
  999. 'search_api_saved_searches' => $params['searches'],
  1000. );
  1001. $title = $settings->getTranslatedOption('mail.notify.title', $language->language);
  1002. $message['subject'] .= token_replace($title, $data, array('language' => $language, 'sanitize' => FALSE));
  1003. $body = $settings->getTranslatedOption('mail.notify.body', $language->language);
  1004. $message['body'][] = token_replace($body, $data, array('language' => $language, 'sanitize' => FALSE));
  1005. break;
  1006. }
  1007. }
  1008. /**
  1009. * Implements hook_cron().
  1010. *
  1011. * Queue the saved searches that should be checked for new items.
  1012. */
  1013. function search_api_saved_searches_cron() {
  1014. // Get all searches whose last execution lies more than the notify_interval
  1015. // in the past. Add a small amount to the current time, so small differences
  1016. // in execution time don't result in a delay until the next cron run.
  1017. $ids = db_select('search_api_saved_search', 's')
  1018. ->fields('s', array('id'))
  1019. ->condition('enabled', 1)
  1020. ->where('notify_interval >= 0 AND last_execute + notify_interval < :time', array(':time' => REQUEST_TIME + 15))
  1021. ->execute()
  1022. ->fetchCol();
  1023. if (!$ids) {
  1024. return;
  1025. }
  1026. // Get the queue and load the queries.
  1027. $queue = DrupalQueue::get('search_api_saved_searches_check_updates');
  1028. $searches = search_api_saved_search_load_multiple($ids);
  1029. // Group the search according to mail and settings. Grouping by mail prevents
  1030. // a user from getting several mails at once, for different searches. Grouping
  1031. // by settings is necessary since the mails can differ between settings.
  1032. foreach ($searches as $search) {
  1033. $user_searches[$search->mail . ' ' . $search->settings_id][] = $search;
  1034. }
  1035. foreach ($user_searches as $searches) {
  1036. $queue->createItem($searches);
  1037. }
  1038. // Set the last execution timestamp now, so the interval doesn't move and we
  1039. // don't get problems if the next cron run occurrs before the queue is
  1040. // completely executed.
  1041. db_update('search_api_saved_search')
  1042. ->fields(array(
  1043. 'last_execute' => REQUEST_TIME,
  1044. ))
  1045. ->condition('id', $ids, 'IN')
  1046. ->execute();
  1047. }
  1048. /**
  1049. * Implements hook_cron_queue_info().
  1050. *
  1051. * Defines a queue for saved searches that should be checked for new items.
  1052. */
  1053. function search_api_saved_searches_cron_queue_info() {
  1054. return array(
  1055. 'search_api_saved_searches_check_updates' => array(
  1056. 'worker callback' => 'search_api_saved_searches_check_updates',
  1057. 'time' => 10,
  1058. ),
  1059. );
  1060. }
  1061. /**
  1062. * Checks for new results for saved searches, and sends a mail if necessary.
  1063. *
  1064. * @param array $searches
  1065. * An array of SearchApiSavedSearch objects to check for new results. All of
  1066. * these should have the same mail address and base settings.
  1067. */
  1068. function search_api_saved_searches_check_updates(array $searches) {
  1069. try {
  1070. $search = reset($searches);
  1071. $settings = $search->settings();
  1072. $index = $settings->index();
  1073. $mail_params = array();
  1074. foreach ($searches as $search) {
  1075. // Make sure we run the query as the user who owns the saved search.
  1076. // Otherwise node access will not work properly.
  1077. $search->query['options']['search_api_access_account'] = $search->uid;
  1078. // Get actual results for the query.
  1079. $query = $search->query();
  1080. // If a date field is set, use that to filter results.
  1081. if (!empty($settings->options['date_field'])) {
  1082. $query->condition($settings->options['date_field'], $search->last_execute, '>');
  1083. }
  1084. $response = $query->execute();
  1085. if (!empty($response['results'])) {
  1086. $new = $results = drupal_map_assoc(array_keys($response['results']));
  1087. if (empty($settings->options['date_field'])) {
  1088. // ID-based method: Compare these results to the old ones.
  1089. $old = drupal_map_assoc(explode(',', $search->results));
  1090. $new = array_diff_key($results, $old);
  1091. }
  1092. if ($new) {
  1093. // We have new results: send them to the user.
  1094. // Only load those items that will be sent.
  1095. $sent_new = $new;
  1096. if (!empty($settings->options['mail']['notify']['max_results'])) {
  1097. $sent_new = array_slice($new, 0, $settings->options['mail']['notify']['max_results']);
  1098. }
  1099. $sent_new = $index->loadItems($sent_new);
  1100. $new_results = $sent_new + $new;
  1101. // Let other modules alter these results.
  1102. drupal_alter('search_api_saved_searches_new_results', $new_results, $search);
  1103. if ($new_results) {
  1104. // We have to slice again in case some items were moved around or
  1105. // removed by alter hooks.
  1106. $sent_new = $new_results;
  1107. if (!empty($settings->options['mail']['notify']['max_results'])) {
  1108. $sent_new = array_slice($new_results, 0, $settings->options['mail']['notify']['max_results']);
  1109. // Now some of the top results might still be unloaded.
  1110. if ($unloaded = array_filter($sent_new, 'is_scalar')) {
  1111. $sent_new = $index->loadItems($unloaded) + $sent_new;
  1112. }
  1113. }
  1114. $num_results = count($new_results);
  1115. $mail_params['searches'][] = array(
  1116. 'search' => $search,
  1117. 'num_results' => $num_results,
  1118. 'results' => $sent_new,
  1119. );
  1120. }
  1121. }
  1122. if (empty($settings->options['date_field']) && ($new || array_diff($old, $results))) {
  1123. // The results changed in some way: store the latest version.
  1124. $search->results = implode(',', $results);
  1125. $search->save();
  1126. }
  1127. }
  1128. }
  1129. // If we set any searches in the mail parameters, send the mail.
  1130. if ($mail_params) {
  1131. $mail_params['user'] = user_load($search->uid);
  1132. $mail_params['settings'] = $settings;
  1133. $message = drupal_mail('search_api_saved_searches', 'notify', $search->mail,
  1134. user_preferred_language($mail_params['user']), $mail_params);
  1135. if ($message['result']) {
  1136. watchdog('search_api_saved_searches', 'A mail with new saved search results was sent to @mail.',
  1137. array('@mail' => $search->mail), WATCHDOG_INFO);
  1138. }
  1139. }
  1140. }
  1141. catch (SearchApiException $e) {
  1142. watchdog('search_api_saved_searches', $e->getMessage(), NULL, WATCHDOG_ERROR);
  1143. }
  1144. }