search_api_saved_searches.module 48 KB

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