mollom.module 134 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935193619371938193919401941194219431944194519461947194819491950195119521953195419551956195719581959196019611962196319641965196619671968196919701971197219731974197519761977197819791980198119821983198419851986198719881989199019911992199319941995199619971998199920002001200220032004200520062007200820092010201120122013201420152016201720182019202020212022202320242025202620272028202920302031203220332034203520362037203820392040204120422043204420452046204720482049205020512052205320542055205620572058205920602061206220632064206520662067206820692070207120722073207420752076207720782079208020812082208320842085208620872088208920902091209220932094209520962097209820992100210121022103210421052106210721082109211021112112211321142115211621172118211921202121212221232124212521262127212821292130213121322133213421352136213721382139214021412142214321442145214621472148214921502151215221532154215521562157215821592160216121622163216421652166216721682169217021712172217321742175217621772178217921802181218221832184218521862187218821892190219121922193219421952196219721982199220022012202220322042205220622072208220922102211221222132214221522162217221822192220222122222223222422252226222722282229223022312232223322342235223622372238223922402241224222432244224522462247224822492250225122522253225422552256225722582259226022612262226322642265226622672268226922702271227222732274227522762277227822792280228122822283228422852286228722882289229022912292229322942295229622972298229923002301230223032304230523062307230823092310231123122313231423152316231723182319232023212322232323242325232623272328232923302331233223332334233523362337233823392340234123422343234423452346234723482349235023512352235323542355235623572358235923602361236223632364236523662367236823692370237123722373237423752376237723782379238023812382238323842385238623872388238923902391239223932394239523962397239823992400240124022403240424052406240724082409241024112412241324142415241624172418241924202421242224232424242524262427242824292430243124322433243424352436243724382439244024412442244324442445244624472448244924502451245224532454245524562457245824592460246124622463246424652466246724682469247024712472247324742475247624772478247924802481248224832484248524862487248824892490249124922493249424952496249724982499250025012502250325042505250625072508250925102511251225132514251525162517251825192520252125222523252425252526252725282529253025312532253325342535253625372538253925402541254225432544254525462547254825492550255125522553255425552556255725582559256025612562256325642565256625672568256925702571257225732574257525762577257825792580258125822583258425852586258725882589259025912592259325942595259625972598259926002601260226032604260526062607260826092610261126122613261426152616261726182619262026212622262326242625262626272628262926302631263226332634263526362637263826392640264126422643264426452646264726482649265026512652265326542655265626572658265926602661266226632664266526662667266826692670267126722673267426752676267726782679268026812682268326842685268626872688268926902691269226932694269526962697269826992700270127022703270427052706270727082709271027112712271327142715271627172718271927202721272227232724272527262727272827292730273127322733273427352736273727382739274027412742274327442745274627472748274927502751275227532754275527562757275827592760276127622763276427652766276727682769277027712772277327742775277627772778277927802781278227832784278527862787278827892790279127922793279427952796279727982799280028012802280328042805280628072808280928102811281228132814281528162817281828192820282128222823282428252826282728282829283028312832283328342835283628372838283928402841284228432844284528462847284828492850285128522853285428552856285728582859286028612862286328642865286628672868286928702871287228732874287528762877287828792880288128822883288428852886288728882889289028912892289328942895289628972898289929002901290229032904290529062907290829092910291129122913291429152916291729182919292029212922292329242925292629272928292929302931293229332934293529362937293829392940294129422943294429452946294729482949295029512952295329542955295629572958295929602961296229632964296529662967296829692970297129722973297429752976297729782979298029812982298329842985298629872988298929902991299229932994299529962997299829993000300130023003300430053006300730083009301030113012301330143015301630173018301930203021302230233024302530263027302830293030303130323033303430353036303730383039304030413042304330443045304630473048304930503051305230533054305530563057305830593060306130623063306430653066306730683069307030713072307330743075307630773078307930803081308230833084308530863087308830893090309130923093309430953096309730983099310031013102310331043105310631073108310931103111311231133114311531163117311831193120312131223123312431253126312731283129313031313132313331343135313631373138313931403141314231433144314531463147314831493150315131523153315431553156315731583159316031613162316331643165316631673168316931703171317231733174317531763177317831793180318131823183318431853186318731883189319031913192319331943195319631973198319932003201320232033204320532063207320832093210321132123213321432153216321732183219322032213222322332243225322632273228322932303231323232333234323532363237323832393240324132423243324432453246324732483249325032513252325332543255325632573258325932603261326232633264326532663267326832693270327132723273327432753276327732783279328032813282328332843285328632873288328932903291329232933294329532963297329832993300330133023303330433053306330733083309331033113312331333143315331633173318331933203321332233233324332533263327332833293330333133323333333433353336333733383339334033413342334333443345334633473348334933503351335233533354335533563357335833593360336133623363336433653366336733683369337033713372337333743375337633773378337933803381338233833384338533863387338833893390339133923393339433953396339733983399340034013402340334043405340634073408340934103411341234133414341534163417341834193420342134223423342434253426342734283429343034313432343334343435343634373438343934403441344234433444344534463447344834493450345134523453345434553456345734583459346034613462346334643465346634673468346934703471347234733474347534763477347834793480348134823483348434853486348734883489349034913492349334943495349634973498349935003501350235033504350535063507350835093510351135123513351435153516351735183519352035213522352335243525352635273528352935303531353235333534353535363537353835393540354135423543354435453546354735483549355035513552355335543555355635573558355935603561356235633564356535663567356835693570357135723573357435753576357735783579358035813582358335843585358635873588358935903591359235933594359535963597359835993600360136023603360436053606360736083609361036113612361336143615361636173618361936203621362236233624362536263627362836293630363136323633363436353636363736383639364036413642364336443645364636473648364936503651365236533654365536563657365836593660366136623663366436653666366736683669367036713672367336743675367636773678367936803681368236833684368536863687368836893690369136923693369436953696369736983699370037013702370337043705370637073708370937103711371237133714371537163717371837193720372137223723372437253726372737283729373037313732373337343735373637373738373937403741374237433744374537463747374837493750375137523753375437553756375737583759376037613762376337643765376637673768376937703771377237733774377537763777377837793780378137823783378437853786378737883789379037913792379337943795379637973798379938003801380238033804380538063807380838093810381138123813381438153816381738183819382038213822382338243825382638273828382938303831383238333834383538363837383838393840384138423843384438453846384738483849385038513852385338543855385638573858385938603861386238633864386538663867386838693870
  1. <?php
  2. /**
  3. * @file
  4. * Main Mollom integration module functions.
  5. */
  6. /**
  7. * Form protection mode: No protection.
  8. */
  9. define('MOLLOM_MODE_DISABLED', 0);
  10. /**
  11. * Form protection mode: CAPTCHA-only protection.
  12. */
  13. define('MOLLOM_MODE_CAPTCHA', 1);
  14. /**
  15. * Form protection mode: Text analysis with fallback to CAPTCHA.
  16. */
  17. define('MOLLOM_MODE_ANALYSIS', 2);
  18. /**
  19. * Server communication failure fallback mode: Block all submissions of protected forms.
  20. */
  21. define('MOLLOM_FALLBACK_BLOCK', 0);
  22. /**
  23. * Server communication failure fallback mode: Accept all submissions of protected forms.
  24. */
  25. define('MOLLOM_FALLBACK_ACCEPT', 1);
  26. /**
  27. * Implements hook_hook_info().
  28. */
  29. function mollom_hook_info() {
  30. $hooks = array(
  31. 'mollom_form_list',
  32. 'mollom_form_list_alter',
  33. 'mollom_form_info',
  34. 'mollom_form_info_alter',
  35. 'mollom_form_insert',
  36. 'mollom_form_update',
  37. 'mollom_form_delete',
  38. 'mollom_data_insert',
  39. 'mollom_data_update',
  40. 'mollom_data_delete',
  41. 'mollom_content_alter',
  42. );
  43. $hooks = array_fill_keys($hooks, array(
  44. 'group' => 'mollom',
  45. ));
  46. return $hooks;
  47. }
  48. /**
  49. * Implements hook_help().
  50. */
  51. function mollom_help($path, $arg) {
  52. $output = '';
  53. if ($path == 'admin/config/content/mollom') {
  54. $output .= '<p>';
  55. $output .= t('All listed forms below are protected by Mollom, unless users are able to <a href="@permissions-url">bypass Mollom\'s protection</a>.', array(
  56. '@permissions-url' => url('admin/people/permissions', array('fragment' => 'module-mollom')),
  57. ));
  58. $output .= ' ';
  59. $output .= t('You can <a href="@add-form-url">add a form</a> to protect, configure already protected forms, or remove the protection.', array(
  60. '@add-form-url' => url('admin/config/content/mollom/add'),
  61. ));
  62. $output .= '</p>';
  63. return $output;
  64. }
  65. if ($path == 'admin/config/content/mollom/blacklist') {
  66. $output = '<p>';
  67. $output .= t('Mollom automatically blocks unwanted content and learns from all participating sites to improve its filters. On top of automatic filtering, you can define a custom blacklist.');
  68. $output .= '</p>';
  69. $output .= '<p>';
  70. $output .= t('Use an "exact" match for short, single words that could be contained within another word.');
  71. $output .= '</p>';
  72. return $output;
  73. }
  74. if ($path == 'admin/help#mollom') {
  75. $output = '<p>';
  76. $output .= t('Allowing users to react, participate and contribute while still keeping your site\'s content under control can be a huge challenge. <a href="@mollom-website">Mollom</a> is a web service that helps you identify content quality and, most importantly, helps you stop spam. When content moderation becomes easier, you have more time and energy to interact with your site visitors and community. For more information, see <a href="@mollom-works">How Mollom Works</a> and the <a href="@mollom-faq">Mollom FAQ</a>.', array(
  77. '@mollom-website' => 'https://www.mollom.com',
  78. '@mollom-works' => 'https://www.mollom.com/how-mollom-works',
  79. '@mollom-faq' => 'https://www.mollom.com/faq',
  80. ));
  81. $output .= '</p><p>';
  82. $output .= t('Mollom can protect forms your site from unwanted posts. Each form can be set to one of the following options:');
  83. $output .= '</p><ul>';
  84. $output .= '<li><p><strong>';
  85. $output .= t('Text analysis with CAPTCHA backup');
  86. $output .= '</strong></p><p>';
  87. $output .= t('Mollom analyzes the data submitted on the form and presents a CAPTCHA challenge if necessary. This option is strongly recommended, as it takes full advantage of the Mollom service to categorize posts into ham (not spam) and spam.');
  88. $output .= '</p></li>';
  89. $output .= '<li><p><strong>';
  90. $output .= t('CAPTCHA only');
  91. $output .= '</strong></p><p>';
  92. $output .= t('The form data is not sent to Mollom for analysis, and a remotely-hosted CAPTCHA challenge is always presented. This option is useful when you want to send less data to the Mollom network. Note, however, that forms displayed with a CAPTCHA are never cached, so always displaying a CAPTCHA challenge may reduce performance.');
  93. $output .= '</p></li>';
  94. $output .= '</ul><p>';
  95. $output .= t('Data is processed and stored as explained in the <a href="@mollom-privacy">Mollom Web Service Privacy Policy</a>. It is your responsibility to provide necessary notices and obtain the appropriate consent regarding Mollom\'s use of submitted data.', array(
  96. '@mollom-privacy' => 'https://www.mollom.com/web-service-privacy-policy',
  97. '@mollom-works' => 'https://www.mollom.com/how-mollom-works',
  98. '@mollom-faq' => 'https://www.mollom.com/faq',
  99. ));
  100. $output .= '</p>';
  101. $output .= '<p>';
  102. $output .= t('If Mollom may not block a spam post for any reason, you can help to train and improve its filters by choosing the appropriate feedback option when deleting the post on your site.');
  103. $output .= '</p>';
  104. $output .= '<h3>' . t('Mollom blacklist') . '</h3>';
  105. $output .= '<p>';
  106. $output .= t("Mollom's filters are shared and trained globally over all participating sites. Due to this, unwanted content might still be accepted on your site, even after sending feedback to Mollom. By using the site-specific blacklist, the filters can be customized to your specific needs. Each entry specifies a reason for why it has been blacklisted, which further helps in improving Mollom's automated filtering.");
  107. $output .= '</p>';
  108. $output .= '<p>';
  109. $output .= t("All blacklist entries are applied to a context: the entire submitted post, or only links in the post. When limiting the context to links, both the link URL and the link text is taken into account.");
  110. $output .= '</p>';
  111. $output .= '<p>';
  112. $output .= t('Each blacklist entry defines how it matches:');
  113. $output .= '</p>';
  114. $output .= '<ul>';
  115. $output .= '<li>';
  116. $output .= t('Use "contains" matching to find a term within any other string.');
  117. $output .= '</li><li>';
  118. $output .= t('Use "exact" matching for terms made up of short, single words that could be contained within a larger permissible word.');
  119. $output .= '</li>';
  120. $output .= '</ul>';
  121. $output .= '<p>';
  122. $output .= t("If a blacklist entry contains multiple words, various combinations will be matched. For example, when adding \"<code>replica&nbsp;watches</code>\" limited to links, the following links will be blocked:");
  123. $output .= '</p>';
  124. $output .= '<ul>
  125. <li><code>http://replica-watches.com</code></li>
  126. <li><code>http://replica-watches.com/some/path</code></li>
  127. <li><code>http://replicawatches.net</code></li>
  128. <li><code>http://example.com/replica/watches</code></li>
  129. <li><code>&lt;a href="http://example.com"&gt;replica watches&lt;/a&gt;</code></li>
  130. </ul>';
  131. $output .= '<p>';
  132. $output .= t("The blacklist is optional. There is no whitelist, i.e., if a blacklist entry is matched in a post, it overrides any other filter result and the post will not be accepted. Blacklisting potentially ambiguous words should be avoided.");
  133. $output .= '</p>';
  134. return $output;
  135. }
  136. }
  137. /**
  138. * Implements hook_exit().
  139. */
  140. function mollom_exit() {
  141. // Write log messages.
  142. mollom_log_write();
  143. }
  144. /**
  145. * Implements hook_menu().
  146. */
  147. function mollom_menu() {
  148. $items['mollom/report/%/%'] = array(
  149. 'title' => 'Report to Mollom',
  150. 'page callback' => 'drupal_get_form',
  151. 'page arguments' => array('mollom_report_form', 2, 3),
  152. 'access callback' => 'mollom_report_access',
  153. 'access arguments' => array(2, 3),
  154. 'file' => 'mollom.pages.inc',
  155. 'type' => MENU_CALLBACK,
  156. );
  157. $items['admin/config/content/mollom'] = array(
  158. 'title' => 'Mollom content moderation',
  159. 'description' => 'Configure how the Mollom service moderates user-submitted content such as spam and profanity.',
  160. 'page callback' => 'mollom_admin_form_list',
  161. 'access arguments' => array('administer mollom'),
  162. 'file' => 'mollom.admin.inc',
  163. );
  164. $items['admin/config/content/mollom/forms'] = array(
  165. 'title' => 'Forms',
  166. 'type' => MENU_DEFAULT_LOCAL_TASK,
  167. 'weight' => -10,
  168. );
  169. $items['admin/config/content/mollom/add'] = array(
  170. 'title' => 'Add form',
  171. 'page callback' => 'drupal_get_form',
  172. 'page arguments' => array('mollom_admin_configure_form'),
  173. 'access arguments' => array('administer mollom'),
  174. 'type' => MENU_LOCAL_ACTION,
  175. 'file' => 'mollom.admin.inc',
  176. );
  177. $items['admin/config/content/mollom/manage/%mollom_form'] = array(
  178. 'title' => 'Configure',
  179. 'page callback' => 'drupal_get_form',
  180. 'page arguments' => array('mollom_admin_configure_form', 5),
  181. 'access arguments' => array('administer mollom'),
  182. 'file' => 'mollom.admin.inc',
  183. );
  184. $items['admin/config/content/mollom/unprotect/%mollom_form'] = array(
  185. 'title' => 'Unprotect form',
  186. 'page callback' => 'drupal_get_form',
  187. 'page arguments' => array('mollom_admin_unprotect_form', 5),
  188. 'access arguments' => array('administer mollom'),
  189. 'file' => 'mollom.admin.inc',
  190. );
  191. $items['admin/config/content/mollom/blacklist'] = array(
  192. 'title' => 'Blacklists',
  193. 'description' => 'Configure blacklists.',
  194. 'page callback' => 'drupal_get_form',
  195. 'page arguments' => array('mollom_admin_blacklist_form'),
  196. 'access callback' => '_mollom_access',
  197. 'access arguments' => array('administer mollom'),
  198. 'type' => MENU_LOCAL_TASK,
  199. 'file' => 'mollom.admin.inc',
  200. );
  201. $items['admin/config/content/mollom/blacklist/spam'] = array(
  202. 'title' => 'Spam',
  203. 'description' => 'Configure spam blacklist entries.',
  204. 'type' => MENU_DEFAULT_LOCAL_TASK,
  205. 'weight' => -10,
  206. );
  207. $items['admin/config/content/mollom/blacklist/profanity'] = array(
  208. 'title' => 'Profanity',
  209. 'description' => 'Configure profanity blacklist entries.',
  210. 'page callback' => 'drupal_get_form',
  211. 'page arguments' => array('mollom_admin_blacklist_form', 5),
  212. 'access callback' => '_mollom_access',
  213. 'access arguments' => array('administer mollom'),
  214. 'type' => MENU_LOCAL_TASK,
  215. 'file' => 'mollom.admin.inc',
  216. );
  217. $items['admin/config/content/mollom/blacklist/unwanted'] = array(
  218. 'title' => 'Unwanted',
  219. 'description' => 'Configure unwanted blacklist entries.',
  220. 'page callback' => 'drupal_get_form',
  221. 'page arguments' => array('mollom_admin_blacklist_form', 5),
  222. 'access callback' => '_mollom_access',
  223. 'access arguments' => array('administer mollom'),
  224. 'type' => MENU_LOCAL_TASK,
  225. 'file' => 'mollom.admin.inc',
  226. );
  227. $items['admin/config/content/mollom/blacklist/delete'] = array(
  228. 'title' => 'Delete blacklist entry',
  229. 'page callback' => 'drupal_get_form',
  230. 'page arguments' => array('mollom_admin_blacklist_delete'),
  231. 'access callback' => '_mollom_access',
  232. 'access arguments' => array('administer mollom'),
  233. 'type' => MENU_CALLBACK,
  234. 'file' => 'mollom.admin.inc',
  235. );
  236. $items['admin/config/content/mollom/settings'] = array(
  237. 'title' => 'Settings',
  238. 'description' => 'Configure Mollom keys and global settings.',
  239. 'page callback' => 'drupal_get_form',
  240. 'page arguments' => array('mollom_admin_settings'),
  241. 'access arguments' => array('administer mollom'),
  242. 'type' => MENU_LOCAL_TASK,
  243. 'file' => 'mollom.admin.inc',
  244. );
  245. $items['admin/reports/mollom'] = array(
  246. 'title' => 'Mollom statistics',
  247. 'description' => 'Reports and usage statistics for the Mollom module.',
  248. 'page callback' => 'drupal_get_form',
  249. 'page arguments' => array('mollom_reports_page'),
  250. 'access callback' => '_mollom_access',
  251. 'access arguments' => array('access mollom statistics'),
  252. 'file' => 'mollom.admin.inc',
  253. );
  254. // AJAX callback to request new CAPTCHA.
  255. $items['mollom/captcha/%/%'] = array(
  256. 'page callback' => 'mollom_captcha_js',
  257. 'page arguments' => array(2, 3),
  258. 'access callback' => '_mollom_access',
  259. 'file' => 'mollom.pages.inc',
  260. 'type' => MENU_CALLBACK,
  261. );
  262. $items['mollom/fba'] = array(
  263. 'page callback' => 'mollom_fba_js',
  264. 'access callback' => '_mollom_access',
  265. 'type' => MENU_CALLBACK,
  266. );
  267. // Report as inappropriate.
  268. $items['mollom/flag/%/%/%/%'] = array(
  269. 'page callback' => '_mollom_flag',
  270. 'page arguments' => array(2, 3, 4),
  271. 'access callback' => '_mollom_flag_access',
  272. 'access arguments' => array(3, 4),
  273. 'type' => MENU_CALLBACK,
  274. 'file' => 'mollom.flag.inc',
  275. );
  276. return $items;
  277. }
  278. /**
  279. * Implements hook_menu_local_tasks_alter().
  280. */
  281. function mollom_menu_local_tasks_alter(&$data, $router_item, $root_path) {
  282. if ($router_item['tab_root'] === 'admin/config/content/mollom' && user_access('access mollom statistics')) {
  283. // Inject link to 'Statistics' before the last 'Settings' tab.
  284. // The render array supports the regular #weight, but D7 core does not
  285. // assign a #weight property for local tasks derived from the menu router.
  286. // This causes element_children() to re-sort the existing local tasks
  287. // (without weights) and they appear in an arbitrary order.
  288. // @see http://drupal.org/node/1864066
  289. array_splice($data['tabs'][0]['output'], -1, 0, array(array(
  290. '#theme' => 'menu_local_task',
  291. '#link' => array(
  292. 'title' => t('Statistics'),
  293. 'href' => 'admin/reports/mollom',
  294. 'localized_options' => array('html' => FALSE),
  295. ),
  296. )));
  297. }
  298. }
  299. /**
  300. * Menu access callback; Checks if the module is operational.
  301. *
  302. * @param $permission
  303. * An optional permission string to check with user_access().
  304. *
  305. * @return
  306. * TRUE if the module has been configured and user_access() has been checked,
  307. * FALSE otherwise.
  308. */
  309. function _mollom_access($permission = FALSE) {
  310. $status = _mollom_status();
  311. return $status['isVerified'] && (!$permission || user_access($permission));
  312. }
  313. /**
  314. * Menu access callback; Determine access to report to Mollom.
  315. *
  316. * There are two special $entity types "mollom_content" and "mollom_captcha",
  317. * which do not map to actual entity types in the Drupal system. They are
  318. * primarily used for mails, messages, and posts, which pertain to forms
  319. * protected by Mollom that do no result in stored entities after submission.
  320. * For example, Contact module's contact form. They can be reported by anyone
  321. * having the link. $id is expected to be either a {mollom}.content_id or
  322. * {mollom}.captcha_id respectively.
  323. *
  324. * @see mollom_mail_add_report_link()
  325. *
  326. * @param $entity
  327. * The entity type of the data to report.
  328. * @param $id
  329. * The entity id of the data to report.
  330. *
  331. * @todo Revamp this based on new {mollom}.form_id info.
  332. */
  333. function mollom_report_access($entity, $id) {
  334. // The special entity types can be reported by anyone.
  335. if ($entity == 'mollom_content' || $entity == 'mollom_captcha') {
  336. return !empty($id) ? TRUE : FALSE;
  337. }
  338. // Retrieve information about all protectable forms. We use the first valid
  339. // definition, because we assume that multiple form definitions just denote
  340. // variations of the same entity (e.g. node content types).
  341. foreach (mollom_form_list() as $form_id => $info) {
  342. if (!isset($info['entity']) || $info['entity'] != $entity) {
  343. continue;
  344. }
  345. // If there is a 'report access callback', invoke it.
  346. if (isset($info['report access callback']) && function_exists($info['report access callback'])) {
  347. $function = $info['report access callback'];
  348. return $function($entity, $id);
  349. }
  350. // Otherwise, if there is a 'report access' list of permissions, iterate
  351. // over them.
  352. if (isset($info['report access'])) {
  353. foreach ($info['report access'] as $permission) {
  354. if (user_access($permission)) {
  355. return TRUE;
  356. }
  357. }
  358. }
  359. }
  360. // If we end up here, then the current user is not permitted to report this
  361. // content.
  362. return FALSE;
  363. }
  364. /**
  365. * Implements hook_permission().
  366. */
  367. function mollom_permission() {
  368. return array(
  369. 'administer mollom' => array(
  370. 'title' => t('Administer Mollom-protected forms and Mollom settings'),
  371. ),
  372. 'bypass mollom protection' => array(
  373. 'title' => t('Bypass Mollom protection on forms'),
  374. ),
  375. 'access mollom statistics' => array(
  376. 'title' => t('View Mollom statistics'),
  377. ),
  378. 'report to mollom' => array(
  379. 'title' => t('Report content as inappropriate'),
  380. )
  381. );
  382. }
  383. /**
  384. * Implements hook_modules_installed().
  385. */
  386. function mollom_modules_installed($modules) {
  387. drupal_static_reset('mollom_get_form_info');
  388. }
  389. /**
  390. * Implements hook_modules_uninstalled().
  391. */
  392. function mollom_modules_uninstalled($modules) {
  393. db_delete('mollom_form')->condition('module', $modules)->execute();
  394. }
  395. /**
  396. * Implements hook_cron().
  397. */
  398. function mollom_cron() {
  399. // Mollom session data auto-expires after 6 months.
  400. $expired = REQUEST_TIME - 86400 * 30 * 6;
  401. db_delete('mollom')
  402. ->condition('changed', $expired, '<')
  403. ->execute();
  404. }
  405. /**
  406. * Helper function to convert database column names to variable names.
  407. *
  408. * Database column names are separated by underscore, while some variable names
  409. * are camelcased for backwards compatibility.
  410. *
  411. * @param stdClass $db_result
  412. * The database result object to convert.
  413. * @param bool $reverse
  414. * True if the conversion should be run in reverse, from variable names to
  415. * database names.
  416. * @return stdClass
  417. * The updated object with converted field names.
  418. */
  419. function _mollom_convert_db_names($db_result, $reverse = FALSE) {
  420. if (!is_object($db_result)) {
  421. return $db_result;
  422. }
  423. $replace = array(
  424. 'content_id' => 'contentId',
  425. 'captcha_id' => 'captchaId',
  426. 'spam_score' => 'spamScore',
  427. 'spam_classification' => 'spamClassification',
  428. 'quality_score' => 'qualityScore',
  429. 'profanity_score' => 'profanityScore',
  430. );
  431. if ($reverse) {
  432. $replace = array_flip($replace);
  433. }
  434. // Don't update the original data object but return a new converted clone.
  435. $clone = new stdClass();
  436. foreach($db_result as $prop => $value) {
  437. if (array_key_exists($prop, $replace)) {
  438. $clone->{$replace[$prop]} = $value;
  439. }
  440. else {
  441. $clone->{$prop} = $value;
  442. }
  443. }
  444. return $clone;
  445. }
  446. /**
  447. * Load a Mollom data record by contentId.
  448. *
  449. * @param $contentId
  450. * The contentId to retrieve data for.
  451. */
  452. function mollom_content_load($contentId) {
  453. $data = mollom_db_query_range('SELECT * FROM {mollom} WHERE content_id = :contentId', 0, 1, array(':contentId' => $contentId))->fetchObject();
  454. return _mollom_convert_db_names($data);
  455. }
  456. /**
  457. * Load a Mollom data record from the database.
  458. *
  459. * @param $entity
  460. * The entity type to retrieve data for.
  461. * @param $id
  462. * The entity id to retrieve data for.
  463. */
  464. function mollom_data_load($entity, $id) {
  465. $data = mollom_db_query_range('SELECT * FROM {mollom} WHERE entity = :entity AND id = :id', 0, 1, array(':entity' => $entity, ':id' => $id))->fetchObject();
  466. return _mollom_convert_db_names($data);
  467. }
  468. /**
  469. * Loads the Mollom data records from the database for a specific entity type.
  470. *
  471. * @param $entity
  472. * The entity type to retrieve data for.
  473. *
  474. * @return array
  475. * The matching Mollom data as an array keyed by entity id.
  476. */
  477. function mollom_entity_type_load($type) {
  478. $data = mollom_db_query('SELECT * FROM {mollom} WHERE entity = :entity', array(':entity' => $type))->fetchAllAssoc('id');
  479. return _mollom_convert_db_names($data);
  480. }
  481. /**
  482. * Executes database queries with natural letter casing.
  483. *
  484. * Drupal core enforces lowercase column names in PDO statements for no
  485. * particular reason.
  486. *
  487. * @see http://drupal.org/node/1171866
  488. */
  489. function mollom_db_query($query, array $args = array(), array $options = array()) {
  490. if (empty($options['target'])) {
  491. $options['target'] = 'default';
  492. }
  493. $connection = Database::getConnection($options['target']);
  494. // Backup PDO::ATTR_CASE to restore it afterwards, sticks on the connection.
  495. $backup = $connection->getAttribute(PDO::ATTR_CASE);
  496. $connection->setAttribute(PDO::ATTR_CASE, PDO::CASE_NATURAL);
  497. $result = $connection->query($query, $args, $options);
  498. $connection->setAttribute(PDO::ATTR_CASE, $backup);
  499. return $result;
  500. }
  501. /**
  502. * Fetches a database record with natural letter casing.
  503. *
  504. * Drupal core enforces lowercase column names in PDO statements for no
  505. * particular reason.
  506. *
  507. * @see http://drupal.org/node/1171866
  508. */
  509. function mollom_db_query_range($query, $from, $count, array $args = array(), array $options = array()) {
  510. if (empty($options['target'])) {
  511. $options['target'] = 'default';
  512. }
  513. $connection = Database::getConnection($options['target']);
  514. // Backup PDO::ATTR_CASE to restore it afterwards, sticks on the connection.
  515. $backup = $connection->getAttribute(PDO::ATTR_CASE);
  516. $connection->setAttribute(PDO::ATTR_CASE, PDO::CASE_NATURAL);
  517. $result = $connection->queryRange($query, $from, $count, $args, $options);
  518. $connection->setAttribute(PDO::ATTR_CASE, $backup);
  519. return $result;
  520. }
  521. /**
  522. * Save Mollom validation data to the database.
  523. *
  524. * Based on the specified entity type and id, this function stores the
  525. * validation results returned by Mollom in the database.
  526. *
  527. * The special $entity type "session" may be used for mails and messages, which
  528. * originate from form submissions protected by Mollom, and can be reported by
  529. * anyone; $id is expected to be a Mollom session id instead of an entity id
  530. * then.
  531. *
  532. * @param $data
  533. * An object containing Mollom session data for the entity, containing at
  534. * least the following properties:
  535. * - entity: The entity type of the data to save.
  536. * - id: The entity ID the data belongs to.
  537. * - form_id: The form ID the session data belongs to.
  538. * - session_id: The session ID returned by Mollom.
  539. * And optionally:
  540. * - spam: A spam check result double returned by Mollom.
  541. * - spamClassification: A final spam classification result string; 'ham',
  542. * 'spam', or 'unsure'.
  543. * - quality: A rating of the content's quality, in the range of 0 and 1.0.
  544. * - profanity: A profanity check rating returned by Mollom, in the range of
  545. * 0 and 1.0.
  546. * - languages: An array containing language codes the content might be
  547. * written in.
  548. * - flags_spam: Total count of spam feedback reports.
  549. * - flags_ham: Total count of ham feedback reports.
  550. * - flags_profanity: Total count of profanity feedback reports.
  551. * - flags_quality: Total count of low quality feedback reports.
  552. * - flags_unwanted: Total count of unwanted feedback reports.
  553. */
  554. function mollom_data_save($data) {
  555. $data->changed = REQUEST_TIME;
  556. // Convert languages array into a string.
  557. if (isset($data->languages) && is_array($data->languages)) {
  558. $languages = array();
  559. foreach ($data->languages as $language) {
  560. $languages[] = $language['languageCode'];
  561. }
  562. $data->languages = implode(',', $languages);
  563. }
  564. // Convert mixed case variable names to lower-case _ separated database names.
  565. $converted = _mollom_convert_db_names($data, TRUE);
  566. $update = db_query_range("SELECT 'id' FROM {mollom} WHERE entity = :entity AND id = :id", 0, 1, array(
  567. ':entity' => $data->entity,
  568. ':id' => $data->id,
  569. ))->fetchField();
  570. drupal_write_record('mollom', $converted, $update ? array('entity', $update) : array());
  571. // Pass unconverted data to other modules for backwards compatibility.
  572. if (!$update) {
  573. module_invoke_all('mollom_data_insert', $data);
  574. }
  575. else {
  576. module_invoke_all('mollom_data_update', $data);
  577. }
  578. return $data;
  579. }
  580. /**
  581. * Updates stored Mollom session data to mark a bad post as moderated.
  582. *
  583. * @param $entity
  584. * The entity type of the moderated post.
  585. * @param $id
  586. * The entity id of the moderated post.
  587. */
  588. function mollom_data_moderate($entity, $id) {
  589. $data = mollom_data_load($entity, $id);
  590. // Nothing to do, if no data exists.
  591. if (!$data) {
  592. return;
  593. }
  594. // Report the session to Mollom.
  595. _mollom_send_feedback($data, 'approve', 'moderate', 'mollom_data_moderate');
  596. // Mark the session data as moderated.
  597. $data->moderate = 0;
  598. mollom_data_save($data);
  599. }
  600. /**
  601. * Deletes a Mollom session data record from the database.
  602. *
  603. * @param $entity
  604. * The entity type to delete data for.
  605. * @param $id
  606. * The entity id to delete data for.
  607. */
  608. function mollom_data_delete($entity, $id) {
  609. return mollom_data_delete_multiple($entity, array($id));
  610. }
  611. /**
  612. * Deletes multiple Mollom session data records from the database.
  613. *
  614. * @param $entity
  615. * The entity type to delete data for.
  616. * @param $ids
  617. * An array of entity ids to delete data for.
  618. */
  619. function mollom_data_delete_multiple($entity, array $ids) {
  620. foreach ($ids as $id) {
  621. $data = mollom_data_load($entity, $id);
  622. if ($data) {
  623. module_invoke_all('mollom_data_delete', $data);
  624. }
  625. }
  626. return db_delete('mollom')->condition('entity', $entity)->condition('id', $ids)->execute();
  627. }
  628. /**
  629. * Helper function to add Mollom feedback options to confirmation forms.
  630. */
  631. function mollom_data_delete_form_alter(&$form, &$form_state) {
  632. if (!isset($form['description']['#weight'])) {
  633. $form['description']['#weight'] = 90;
  634. }
  635. $form['mollom'] = array(
  636. '#tree' => TRUE,
  637. '#weight' => 80,
  638. );
  639. $form['mollom']['feedback'] = array(
  640. '#type' => 'radios',
  641. '#title' => t('Report as…'),
  642. '#options' => array(
  643. 'spam' => t('Spam, unsolicited advertising'),
  644. 'profanity' => t('Profane, obscene, violent'),
  645. 'quality' => t('Low-quality'),
  646. 'unwanted' => t('Unwanted, taunting, off-topic'),
  647. '' => t('Do not report'),
  648. ),
  649. '#default_value' => 'spam',
  650. '#description' => t('Sending feedback to <a href="@mollom-url">Mollom</a> improves the automated moderation of new submissions.', array('@mollom-url' => 'https://www.mollom.com')),
  651. );
  652. }
  653. /**
  654. * Send feedback to Mollom and delete Mollom data.
  655. *
  656. * @see mollom_form_alter()
  657. */
  658. function mollom_data_delete_form_submit($form, &$form_state) {
  659. $forms = mollom_form_cache();
  660. $mollom_form = mollom_form_load($forms['delete'][$form_state['values']['form_id']]);
  661. $data = mollom_form_get_values($form_state, $mollom_form['enabled_fields'], $mollom_form['mapping']);
  662. $entity = $mollom_form['entity'];
  663. $id = $data['postId'];
  664. if (!empty($form_state['values']['mollom']['feedback'])) {
  665. if (mollom_data_report($entity, $id, $form_state['values']['mollom']['feedback'], 'moderate', 'mollom_data_delete_form_submit')) {
  666. drupal_set_message(t('The content was successfully reported as inappropriate.'));
  667. }
  668. }
  669. // Remove Mollom session data.
  670. mollom_data_delete($entity, $id);
  671. }
  672. /**
  673. * Sends feedback for a Mollom session data record.
  674. *
  675. * @param $entity
  676. * The entity type to send feedback for.
  677. * @param $id
  678. * The entity id to send feedback for.
  679. * @param $feedback
  680. * The feedback reason for reporting content.
  681. * @param $type
  682. * The type of feedback, one of 'moderate' or 'flag'.
  683. * @param $source
  684. * An optional single word string identifier for the user interface source.
  685. * This is tracked along with the feedback to provide a more complete picture
  686. * of how feedback is used and submitted on the site.
  687. */
  688. function mollom_data_report($entity, $id, $feedback, $type = 'moderate', $source = 'mollom_data_report') {
  689. return mollom_data_report_multiple($entity, array($id), $feedback, $type, $source);
  690. }
  691. /**
  692. * Sends feedback for multiple Mollom session data records.
  693. *
  694. * @param $entity
  695. * The entity type to send feedback for.
  696. * @param $ids
  697. * An array of entity ids to send feedback for.
  698. * @param $feedback
  699. * The feedback reason for reporting content.
  700. * @param $type
  701. * The type of feedback, one of 'moderate' or 'flag'.
  702. * @param $source
  703. * An optional single word string identifier for the user interface source.
  704. * This is tracked along with the feedback to provide a more complete picture
  705. * of how feedback is used and submitted on the site.
  706. */
  707. function mollom_data_report_multiple($entity, array $ids, $feedback, $type = 'moderate', $source = 'mollom_data_report_multiple') {
  708. $return = TRUE;
  709. foreach ($ids as $id) {
  710. // Load the Mollom session data.
  711. $data = mollom_data_load($entity, $id);
  712. // Send feedback, if we have session data.
  713. if (!empty($data->contentId) || !empty($data->captchaId)) {
  714. $result = _mollom_send_feedback($data, $feedback, $type, $source);
  715. $return = $return && $result;
  716. }
  717. }
  718. return $return;
  719. }
  720. /**
  721. * Implements hook_form_alter().
  722. *
  723. * Protects all configured forms with Mollom.
  724. *
  725. * @see mollom_element_info()
  726. * @see mollom_process_mollom()
  727. * @see mollom_pre_render_mollom()
  728. */
  729. function mollom_form_alter(&$form, &$form_state, $form_id) {
  730. // Skip installation and update forms.
  731. if (defined('MAINTENANCE_MODE')) {
  732. return;
  733. }
  734. // Retrieve a list of all protected forms once.
  735. $forms = mollom_form_cache();
  736. // Remind of enabled testing mode on all protected forms.
  737. if (isset($forms['protected'][$form_id]) || strpos($_GET['q'], 'admin/config/content/mollom') === 0) {
  738. _mollom_testing_mode_warning();
  739. }
  740. // Site administrators don't have their content checked with Mollom.
  741. if (!user_access('bypass mollom protection')) {
  742. // Retrieve configuration for this form.
  743. if (isset($forms['protected'][$form_id]) && ($mollom_form = mollom_form_load($form_id))) {
  744. // Determine whether to bypass validation for the current user.
  745. foreach ($mollom_form['bypass access'] as $permission) {
  746. if (user_access($permission)) {
  747. return;
  748. }
  749. }
  750. // Verify global Mollom configuration status.
  751. // Only do this if the form is actually protected and if the current user
  752. // is not privileged to bypass the Mollom protection. Otherwise, if e.g.
  753. // the Testing API is down, then every hook_form_alter() for every single
  754. // form on the page would potentially cause a (two) API keys verification
  755. // requests (in case caches are disabled).
  756. // If API keys have been configured, then the form has to be processed,
  757. // regardless of whether API keys could be verified; otherwise, the
  758. // fallback mode would not be triggered.
  759. $status = _mollom_status();
  760. if (!$status['isConfigured']) {
  761. return;
  762. }
  763. // Add Mollom form widget.
  764. $form['mollom'] = array(
  765. '#type' => 'mollom',
  766. '#mollom_form' => $mollom_form,
  767. // #type 'actions' defaults to 100.
  768. '#weight' => (isset($form['actions']['#weight']) ? $form['actions']['#weight'] - 1 : 99),
  769. '#tree' => TRUE,
  770. );
  771. // Add Mollom form validation handlers.
  772. // Form-level validation handlers are required, since we need access to
  773. // all validated and submitted form values. _form_validate() invokes
  774. // #element_validate handlers while it is recursing into the form.
  775. $form['#validate'][] = 'mollom_validate_captcha';
  776. $form['#validate'][] = 'mollom_validate_analysis';
  777. $form['#validate'][] = 'mollom_validate_post';
  778. // Append a submit handler to store Mollom session data. Requires that
  779. // the primary submit handler has run already, so a potential 'post_id'
  780. // mapping can be retrieved from $form_state['values'].
  781. // @todo Core: node_form_submit() uses a button-level submit handler,
  782. // which invokes form-level submit handlers before the node/entity is
  783. // saved, so $form_state does not contain the new node ID yet. There is
  784. // no #post_submit property or form processing phase, we could rely on.
  785. // Potentially applies to other contrib entities, too.
  786. // @see http://drupal.org/node/1150756
  787. if (isset($form_state['build_info']['base_form_id']) && $form_state['build_info']['base_form_id'] == 'node_form') {
  788. $form_submit_key = &$form['actions']['submit'];
  789. }
  790. else {
  791. $form_submit_key = &$form;
  792. }
  793. $form_submit_key['#submit'][] = 'mollom_form_submit';
  794. // Add link to privacy policy on forms protected via textual analysis,
  795. // if enabled.
  796. if ($mollom_form['mode'] == MOLLOM_MODE_ANALYSIS && variable_get('mollom_privacy_link', 1)) {
  797. $form['mollom']['privacy'] = array(
  798. '#prefix' => '<div class="description mollom-privacy">',
  799. '#suffix' => '</div>',
  800. '#markup' => t('By submitting this form, you accept the <a href="@privacy-policy-url" class="mollom-target" rel="nofollow">Mollom privacy policy</a>.', array(
  801. '@privacy-policy-url' => 'https://www.mollom.com/web-service-privacy-policy',
  802. )),
  803. '#weight' => 10,
  804. );
  805. }
  806. }
  807. }
  808. // Integrate with delete confirmation forms to send feedback to Mollom.
  809. if (isset($forms['delete'][$form_id])) {
  810. // Check whether the user is allowed to report to Mollom. Limiting report
  811. // access is optional for forms integrating via 'delete form' and allowed by
  812. // default, since we assume that users being able to delete entities are
  813. // sufficiently trusted to also report to Mollom.
  814. $access = TRUE;
  815. // Retrieve information about the protected form; the form cache maps delete
  816. // confirmation forms to protected form_ids, and protected form_ids to their
  817. // originating modules.
  818. $mollom_form_id = $forms['delete'][$form_id];
  819. $module = $forms['protected'][$mollom_form_id];
  820. $form_info = mollom_form_load($mollom_form_id, $module);
  821. // For entities, there is only one delete confirmation form per entity type.
  822. // But not all of its bundles may be protected. We therefore need to figure
  823. // out whether the bundle of the entity being deleted is protected - which
  824. // is a reverse-mapping that does not exist in D7.
  825. $is_protected = TRUE;
  826. $is_entity = !empty($form_info['entity']);
  827. $has_entity_argument = isset($form_state['build_info']['args'][0]) && is_object($form_state['build_info']['args'][0]);
  828. if ($is_entity && $has_entity_argument) {
  829. list(, , $bundle) = entity_extract_ids($form_info['entity'], $form_state['build_info']['args'][0]);
  830. $is_protected = db_query_range('SELECT 1 FROM {mollom_form} WHERE entity = :entity AND bundle = :bundle', 0, 1, array(
  831. ':entity' => $form_info['entity'],
  832. ':bundle' => $bundle,
  833. ))->fetchField();
  834. }
  835. if (!$is_protected) {
  836. return;
  837. }
  838. // Check access, if there is a 'report access' permission list.
  839. if (isset($form_info['report access'])) {
  840. $access = FALSE;
  841. foreach ($form_info['report access'] as $permission) {
  842. if (user_access($permission)) {
  843. $access = TRUE;
  844. break;
  845. }
  846. }
  847. }
  848. if ($access) {
  849. mollom_data_delete_form_alter($form, $form_state);
  850. // Report before deleting. This needs to be handled here, since
  851. // mollom_data_delete_form_alter() is re-used for mass-operation forms.
  852. array_unshift($form['#submit'], 'mollom_data_delete_form_submit');
  853. }
  854. }
  855. }
  856. /**
  857. * Returns a cached mapping of protected and delete confirmation form ids.
  858. *
  859. * @param $reset
  860. * (optional) Boolean whether to reset the static cache, flush the database
  861. * cache, and return nothing (TRUE). Defaults to FALSE.
  862. *
  863. * @return
  864. * An associative array containing:
  865. * - protected: An associative array whose keys are protected form IDs and
  866. * whose values are the corresponding module names the form belongs to.
  867. * - delete: An associative array whose keys are 'delete form' ids and whose
  868. * values are protected form ids; e.g.
  869. * @code
  870. * array(
  871. * 'node_delete_confirm' => 'article_node_form',
  872. * )
  873. * @endcode
  874. * A single delete confirmation form id can map to multiple registered
  875. * $form_ids, but only the first is taken into account. As in above example,
  876. * we assume that all 'TYPE_node_form' definitions belong to the same entity
  877. * and therefore have an identical 'post_id' mapping.
  878. */
  879. function mollom_form_cache($reset = FALSE) {
  880. $forms = &drupal_static(__FUNCTION__);
  881. if ($reset) {
  882. // This catches both 'mollom:form_cache' as well as mollom_form_load()'s
  883. // 'mollom:form:*' entries.
  884. cache_clear_all('mollom:form', 'cache', TRUE);
  885. unset($forms);
  886. return;
  887. }
  888. if (isset($forms)) {
  889. return $forms;
  890. }
  891. if ($cache = cache_get('mollom:form_cache')) {
  892. $forms = $cache->data;
  893. return $forms;
  894. }
  895. $forms['protected'] = db_query("SELECT form_id, module FROM {mollom_form}")->fetchAllKeyed();
  896. // Build a list of delete confirmation forms of entities integrating with
  897. // Mollom, so we are able to alter the delete confirmation form to display
  898. // our feedback options.
  899. $forms['delete'] = array();
  900. foreach (mollom_form_list() as $form_id => $info) {
  901. if (!isset($info['delete form']) || !isset($info['entity'])) {
  902. continue;
  903. }
  904. // We expect that the same delete confirmation form uses the same form
  905. // element mapping, so multiple 'delete form' definitions are only processed
  906. // once. Additionally, we only care for protected forms.
  907. if (!isset($forms['delete'][$info['delete form']]) && isset($forms['protected'][$form_id])) {
  908. // A delete confirmation form integration requires a 'post_id' mapping.
  909. $form_info = mollom_form_info($form_id, $info['module']);
  910. if (isset($form_info['mapping']['post_id'])) {
  911. $forms['delete'][$info['delete form']] = $form_id;
  912. }
  913. }
  914. }
  915. cache_set('mollom:form_cache', $forms);
  916. return $forms;
  917. }
  918. /**
  919. * Returns a list of protectable forms registered via hook_mollom_form_info().
  920. */
  921. function mollom_form_list() {
  922. $form_list = array();
  923. foreach (module_implements('mollom_form_list') as $module) {
  924. $function = $module . '_mollom_form_list';
  925. $module_forms = $function();
  926. foreach ($module_forms as $form_id => $info) {
  927. $form_list[$form_id] = $info;
  928. $form_list[$form_id] += array(
  929. 'form_id' => $form_id,
  930. 'module' => $module,
  931. );
  932. }
  933. }
  934. // Allow modules to alter the form list.
  935. drupal_alter('mollom_form_list', $form_list);
  936. return $form_list;
  937. }
  938. /**
  939. * Returns information about a form registered via hook_mollom_form_info().
  940. *
  941. * @param $form_id
  942. * The form id to return information for.
  943. * @param $module
  944. * The module name $form_id belongs to.
  945. * @param array $form_list
  946. * (optional) The return value of hook_mollom_form_list() of $module, if
  947. * already kown. Primarily used by mollom_form_load().
  948. */
  949. function mollom_form_info($form_id, $module, $form_list = NULL) {
  950. // Default properties.
  951. $form_info = array(
  952. // Base properties.
  953. 'form_id' => $form_id,
  954. 'title' => $form_id,
  955. 'module' => $module,
  956. 'entity' => NULL,
  957. 'bundle' => NULL,
  958. // Configuration properties.
  959. 'mode' => NULL,
  960. 'checks' => array(),
  961. 'enabled_fields' => array(),
  962. 'strictness' => 'normal',
  963. 'unsure' => 'captcha',
  964. 'discard' => 1,
  965. 'moderation' => 0,
  966. // Meta information.
  967. 'bypass access' => array(),
  968. 'elements' => array(),
  969. 'mapping' => array(),
  970. 'mail ids' => array(),
  971. 'orphan' => TRUE,
  972. );
  973. // Fetch the basic form information from hook_mollom_form_list() first.
  974. // This makes the integrating module (needlessly) rebuild all of its available
  975. // forms, but the base properties are absolutely required here, so we can
  976. // apply the default properties below.
  977. if (!isset($form_list)) {
  978. $form_list = module_invoke($module, 'mollom_form_list');
  979. }
  980. // If it is not listed, then the form has vanished.
  981. if (!isset($form_list[$form_id])) {
  982. return $form_info;
  983. }
  984. $module_form_info = module_invoke($module, 'mollom_form_info', $form_id);
  985. // If no form info exists, then the form has vanished.
  986. if (!isset($module_form_info)) {
  987. return $form_info;
  988. }
  989. unset($form_info['orphan']);
  990. // Any information in hook_mollom_form_info() overrides the list info.
  991. $form_info = array_merge($form_info, $form_list[$form_id]);
  992. $form_info = array_merge($form_info, $module_form_info);
  993. // Allow modules to alter the default form information.
  994. drupal_alter('mollom_form_info', $form_info, $form_id);
  995. return $form_info;
  996. }
  997. /**
  998. * Helper function to add field form element mappings for fieldable entities.
  999. *
  1000. * May be used by hook_mollom_form_info() implementations to automatically
  1001. * populate the 'elements' definition with attached text fields on the entity
  1002. * type's bundle.
  1003. *
  1004. * @param array $form_info
  1005. * The basic information about the registered form. Taken by reference.
  1006. * @param string $entity_type
  1007. * The entity type; e.g., 'node'.
  1008. * @param string $bundle
  1009. * The entity bundle name; e.g., 'article'.
  1010. *
  1011. * @return void
  1012. * $form_info is taken by reference and enhanced with any attached field
  1013. * mappings; e.g.:
  1014. * @code
  1015. * $form_info['elements']['field_name][und][0][value'] = 'Field label';
  1016. * @endcode
  1017. */
  1018. function mollom_form_info_add_fields(&$form_info, $entity_type, $bundle) {
  1019. if (!$entity_info = entity_get_info($entity_type)) {
  1020. return;
  1021. }
  1022. $form_info['mapping']['post_id'] = $entity_info['entity keys']['id'];
  1023. if (!empty($entity_info['fieldable'])) {
  1024. // Add form element mappings for any text fields attached to the bundle.
  1025. $fields = field_info_fields();
  1026. foreach (field_info_instances($entity_type, $bundle) as $field_name => $field) {
  1027. if (in_array($fields[$field_name]['type'], array('text', 'text_long', 'text_with_summary'))) {
  1028. $form_info['elements'][$field_name] = check_plain(t($field['label']));
  1029. }
  1030. }
  1031. }
  1032. }
  1033. /**
  1034. * Creates a bare Mollom form configuration.
  1035. *
  1036. * @param $form_id
  1037. * The form ID to create the Mollom form configuration for.
  1038. */
  1039. function mollom_form_new($form_id) {
  1040. $mollom_form = array();
  1041. $form_list = mollom_form_list();
  1042. if (isset($form_list[$form_id])) {
  1043. $mollom_form += $form_list[$form_id];
  1044. }
  1045. $mollom_form += mollom_form_info($form_id, $form_list[$form_id]['module'], $form_list);
  1046. // Enable all fields for textual analysis by default.
  1047. $mollom_form['checks'] = array('spam');
  1048. $mollom_form['enabled_fields'] = array_keys($mollom_form['elements']);
  1049. return $mollom_form;
  1050. }
  1051. /**
  1052. * Menu argument loader; Loads Mollom configuration and form information for a given form id.
  1053. */
  1054. function mollom_form_load($form_id) {
  1055. $cid = 'mollom:form:' . $form_id;
  1056. if ($cache = cache_get($cid)) {
  1057. return $cache->data;
  1058. }
  1059. else {
  1060. $mollom_form = db_query('SELECT * FROM {mollom_form} WHERE form_id = :form_id', array(':form_id' => $form_id))->fetchAssoc();
  1061. if ($mollom_form) {
  1062. $mollom_form['checks'] = unserialize($mollom_form['checks']);
  1063. $mollom_form['enabled_fields'] = unserialize($mollom_form['enabled_fields']);
  1064. // Attach form registry information.
  1065. $form_info = mollom_form_info($form_id, $mollom_form['module']);
  1066. $mollom_form += $form_info;
  1067. // Override entity type and bundle information with current values from
  1068. // the form registry. These properties were originally not stored in
  1069. // {mollom_form} and only introduced in 7.x-2.2. The update path is only
  1070. // able to map entity types/bundles in Drupal core. Any other form
  1071. // protections need to be updated manually. That is the situation in which
  1072. // $mollom_form has NULL values from the database, but the form registry
  1073. // actually contains the proper values.
  1074. // @todo Remove in later versions.
  1075. // @todo Clean up _list() + _info() hook API design and pass the base
  1076. // $form_info from _list() into _info(), so that it extends that
  1077. // definition instead of replacing it.
  1078. $mollom_form['entity'] = $form_info['entity'];
  1079. $mollom_form['bundle'] = $form_info['bundle'];
  1080. cache_set($cid, $mollom_form);
  1081. }
  1082. }
  1083. return $mollom_form;
  1084. }
  1085. /**
  1086. * Saves a Mollom form configuration.
  1087. */
  1088. function mollom_form_save(&$mollom_form) {
  1089. $exists = db_query_range('SELECT 1 FROM {mollom_form} WHERE form_id = :form_id', 0, 1, array(':form_id' => $mollom_form['form_id']))->fetchField();
  1090. $status = drupal_write_record('mollom_form', $mollom_form, ($exists ? 'form_id' : array()));
  1091. // Allow modules to react on saved form configurations.
  1092. if ($status === SAVED_NEW) {
  1093. module_invoke_all('mollom_form_insert', $mollom_form);
  1094. }
  1095. else {
  1096. module_invoke_all('mollom_form_update', $mollom_form);
  1097. }
  1098. // Flush cached Mollom forms and the Mollom form mapping cache.
  1099. mollom_form_cache(TRUE);
  1100. return $status;
  1101. }
  1102. /**
  1103. * Deletes a Mollom form configuration.
  1104. */
  1105. function mollom_form_delete($form_id) {
  1106. $mollom_form = mollom_form_load($form_id);
  1107. db_delete('mollom_form')
  1108. ->condition('form_id', $form_id)
  1109. ->execute();
  1110. // Allow modules to react on saved form configurations.
  1111. module_invoke_all('mollom_form_delete', $mollom_form);
  1112. // Flush cached Mollom forms and the Mollom form mapping cache.
  1113. mollom_form_cache(TRUE);
  1114. }
  1115. /**
  1116. * Given an array of values and an array of fields, extract data for use.
  1117. *
  1118. * This function generates the data to send for validation to Mollom by walking
  1119. * through the submitted form values and
  1120. * - copying element values as specified via 'mapping' in hook_mollom_form_info()
  1121. * into the dedicated data properties
  1122. * - collecting and concatenating all fields that have been selected for textual
  1123. * analysis into the 'post_body' property
  1124. *
  1125. * The processing accounts for the following possibilities:
  1126. * - A field was selected for textual analysis, but there is no submitted form
  1127. * value. The value should have been appended to the 'post_body' property, but
  1128. * will be skipped.
  1129. * - A field is contained in the 'mapping' and there is a submitted form value.
  1130. * The value will not be appended to the 'post_body', but instead be assigned
  1131. * to the specified data property.
  1132. * - All fields specified in 'mapping', for which there is a submitted value,
  1133. * but which were NOT selected for textual analysis, are assigned to the
  1134. * specified data property. This is usually the case for form elements that
  1135. * hold system user information.
  1136. *
  1137. * @param $form_state
  1138. * An associative array containing
  1139. * - values: The submitted form values.
  1140. * - buttons: A list of button form elements. See form_state_values_clean().
  1141. * @param $fields
  1142. * A list of strings representing form elements to extract. Nested fields are
  1143. * in the form of 'parent][child'.
  1144. * @param $mapping
  1145. * An associative array of form elements to map to Mollom's dedicated data
  1146. * properties. See hook_mollom_form_info() for details.
  1147. *
  1148. * @see hook_mollom_form_info()
  1149. */
  1150. function mollom_form_get_values(&$form_state, $fields, $mapping) {
  1151. global $user;
  1152. // @todo Unless mollom_form_submit() directly attempts to retrieve 'postId'
  1153. // from $form_state['values'], the resulting content properties of this
  1154. // function cannot be cached.
  1155. // Remove all button values from $form_state['values'].
  1156. $form_state_copy = $form_state;
  1157. form_state_values_clean($form_state_copy);
  1158. $form_values = $form_state_copy['values'];
  1159. // All elements specified in $mapping must be excluded from $fields, as they
  1160. // are used for dedicated $data properties instead. To reduce the parsing code
  1161. // size, we are turning a given $mapping of f.e.
  1162. // array('post_title' => 'title_form_element')
  1163. // into
  1164. // array('title_form_element' => 'post_title')
  1165. // and we reset $mapping afterwards.
  1166. // When iterating over the $fields, this allows us to quickly test whether the
  1167. // current field should be excluded, and if it should, we directly get the
  1168. // mapped property name to rebuild $mapping with the field values.
  1169. $exclude_fields = array();
  1170. if (!empty($mapping)) {
  1171. $exclude_fields = array_flip($mapping);
  1172. }
  1173. $mapping = array();
  1174. // Process all fields that have been selected for text analysis.
  1175. $post_body = array();
  1176. foreach ($fields as $field) {
  1177. // Nested elements use a key of 'parent][child', so we need to recurse.
  1178. $parents = explode('][', $field);
  1179. $value = $form_values;
  1180. foreach ($parents as $key) {
  1181. $value = isset($value[$key]) ? $value[$key] : NULL;
  1182. }
  1183. // If this field was contained in $mapping and should be excluded, add it to
  1184. // $mapping with the actual form element value, and continue to the next
  1185. // field. Also unset this field from $exclude_fields, so we can process the
  1186. // remaining mappings below.
  1187. if (isset($exclude_fields[$field])) {
  1188. $mapping[$exclude_fields[$field]] = $value;
  1189. unset($exclude_fields[$field]);
  1190. continue;
  1191. }
  1192. // Only add form element values that are not empty.
  1193. if (isset($value)) {
  1194. // UTF-8 validation happens later.
  1195. if (is_string($value) && strlen($value)) {
  1196. $post_body[$field] = $value;
  1197. }
  1198. // Recurse into nested values (e.g. multiple value fields).
  1199. elseif (is_array($value) && !empty($value)) {
  1200. // Ensure we have a flat, indexed array to implode(); form values of
  1201. // field_attach_form() use several subkeys.
  1202. $value = _mollom_flatten_form_values($value);
  1203. $post_body = array_merge($post_body, $value);
  1204. }
  1205. }
  1206. }
  1207. $post_body = implode("\n", $post_body);
  1208. // Try to assign any further form values by processing the remaining mappings,
  1209. // which have been turned into $exclude_fields above. All fields that were
  1210. // already used for 'post_body' no longer exist in $exclude_fields.
  1211. foreach ($exclude_fields as $field => $property) {
  1212. // If the postTitle field was not included in the enabled fields, then don't
  1213. // set it's mapping here.
  1214. if ($property === 'post_title' && !in_array($field, $fields)) {
  1215. continue;
  1216. }
  1217. // Nested elements use a key of 'parent][child', so we need to recurse.
  1218. $parents = explode('][', $field);
  1219. $value = $form_values;
  1220. foreach ($parents as $key) {
  1221. $value = isset($value[$key]) ? $value[$key] : NULL;
  1222. }
  1223. if (isset($value)) {
  1224. if (is_array($value)) {
  1225. $value = _mollom_flatten_form_values($value);
  1226. $value = implode(' ', $value);
  1227. }
  1228. $mapping[$property] = $value;
  1229. }
  1230. }
  1231. // Mollom's XML-RPC methods only accept data properties that are defined. We
  1232. // also do not want to send more than we have to, so we need to build an
  1233. // exact data structure.
  1234. $data = array();
  1235. // Post id; not sent to Mollom.
  1236. // @see mollom_form_submit()
  1237. if (!empty($mapping['post_id'])) {
  1238. $data['postId'] = $mapping['post_id'];
  1239. }
  1240. // Post title if included in enabled fields for the form.
  1241. if (!empty($mapping['post_title'])) {
  1242. $data['postTitle'] = $mapping['post_title'];
  1243. }
  1244. // Post body.
  1245. if (!empty($post_body)) {
  1246. $data['postBody'] = $post_body;
  1247. }
  1248. // Author ID.
  1249. // If a non-anonymous user ID was mapped via form values, use that.
  1250. if (!empty($mapping['author_id'])) {
  1251. $data['authorId'] = $mapping['author_id'];
  1252. }
  1253. // Otherwise, the currently logged-in user is the author.
  1254. elseif (!empty($user->uid)) {
  1255. $data['authorId'] = $user->uid;
  1256. }
  1257. // Load the user account of the author, if any, for the following author*
  1258. // property assignments.
  1259. $account = FALSE;
  1260. if (isset($data['authorId'])) {
  1261. $account = user_load($data['authorId']);
  1262. }
  1263. // Author creation date.
  1264. if (!empty($account->created)) {
  1265. $data['authorCreated'] = $account->created;
  1266. }
  1267. // Author name.
  1268. // A form value mapping always has precedence.
  1269. if (!empty($mapping['author_name'])) {
  1270. $data['authorName'] = $mapping['author_name'];
  1271. }
  1272. // In case a post of a registered user is edited and a form value mapping
  1273. // exists for author_id, but no form value mapping exists for author_name,
  1274. // use the name of the user account associated with author_id.
  1275. // $account may be the same as the currently logged-in $user at this point.
  1276. elseif (!empty($account->name)) {
  1277. $data['authorName'] = $account->name;
  1278. }
  1279. // Author e-mail.
  1280. if (!empty($mapping['author_mail'])) {
  1281. $data['authorMail'] = $mapping['author_mail'];
  1282. }
  1283. elseif (!empty($account->mail)) {
  1284. $data['authorMail'] = $account->mail;
  1285. }
  1286. // Author homepage.
  1287. if (!empty($mapping['author_url'])) {
  1288. $data['authorUrl'] = $mapping['author_url'];
  1289. }
  1290. // Author OpenID.
  1291. if (!empty($mapping['author_openid'])) {
  1292. $data['authorOpenid'] = $mapping['author_openid'];
  1293. }
  1294. elseif (!empty($account) && ($openid = _mollom_get_openid($account))) {
  1295. $data['authorOpenid'] = $openid;
  1296. }
  1297. // Author IP.
  1298. $data['authorIp'] = ip_address();
  1299. // Honeypot.
  1300. // For the Mollom backend, it only matters whether 'honeypot' is non-empty.
  1301. // The submitted value is only taken over to allow site administrators to
  1302. // see the actual honeypot value in watchdog log entries.
  1303. if (isset($form_values['mollom']['homepage']) && $form_values['mollom']['homepage'] !== '') {
  1304. $data['honeypot'] = $form_values['mollom']['homepage'];
  1305. }
  1306. // Add the contextCreated parameter if a callback exists.
  1307. if (isset($form_state['mollom']['context created callback']) && function_exists($form_state['mollom']['context created callback'])) {
  1308. if (!empty($mapping['context_id'])) {
  1309. $context_id_field = $mapping['context_id'];
  1310. $contextCreated = call_user_func($form_state['mollom']['context created callback'], $mapping['context_id']);
  1311. if ($contextCreated !== FALSE) {
  1312. $data['contextCreated'] = $contextCreated;
  1313. }
  1314. }
  1315. }
  1316. // Ensure that all $data values contain valid UTF-8. Invalid UTF-8 would be
  1317. // sanitized into an empty string, so the Mollom backend would not receive
  1318. // any value.
  1319. $invalid_utf8 = FALSE;
  1320. $invalid_xml = FALSE;
  1321. // Include the CAPTCHA solution user input in the UTF-8 validation.
  1322. $solution = isset($form_values['mollom']['captcha']) ? array('solution' => $form_values['mollom']['captcha']) : array();
  1323. foreach ($data + $solution as $key => $value) {
  1324. // Check for invalid UTF-8 byte sequences first.
  1325. if (!drupal_validate_utf8($value)) {
  1326. $invalid_utf8 = TRUE;
  1327. // Replace the bogus string, since $data will be logged as
  1328. // check_plain(var_export($data)), and check_plain() would empty the
  1329. // entire exported variable string otherwise.
  1330. $data[$key] = '- Invalid UTF-8 -';
  1331. }
  1332. // Since values are transmitted over XML-RPC and not merely output as
  1333. // (X)HTML, they have to be valid XML characters.
  1334. // @see http://www.w3.org/TR/2000/REC-xml-20001006#charsets
  1335. // @see http://drupal.org/node/882298
  1336. elseif (preg_match('@[^\x9\xA\xD\x20-\x{D7FF}\x{E000}-\x{FFFD}\x{10000}-\x{10FFFF}]@u', $value)) {
  1337. $invalid_xml = TRUE;
  1338. }
  1339. }
  1340. if ($invalid_utf8 || $invalid_xml) {
  1341. form_set_error('', t('Your submission contains invalid characters and will not be accepted.'));
  1342. mollom_log(array(
  1343. 'message' => 'Invalid !type in form values',
  1344. 'arguments' => array('!type' => $invalid_utf8 ? 'UTF-8' : 'XML characters'),
  1345. 'Data:' => $data,
  1346. ));
  1347. $data = FALSE;
  1348. }
  1349. return $data;
  1350. }
  1351. /**
  1352. * Recursive helper function to flatten nested form values.
  1353. *
  1354. * Takes a potentially nested array and returns all non-empty string values in
  1355. * nested keys as new indexed array.
  1356. */
  1357. function _mollom_flatten_form_values($values) {
  1358. $flat_values = array();
  1359. foreach ($values as $value) {
  1360. if (is_array($value)) {
  1361. // Only text fields are supported at this point; their values are in the
  1362. // 'summary' (optional) and 'value' keys.
  1363. if (isset($value['value'])) {
  1364. if (isset($value['summary']) && $value['summary'] !== '') {
  1365. $flat_values[] = $value['summary'];
  1366. }
  1367. if ($value['value'] !== '') {
  1368. $flat_values[] = $value['value'];
  1369. }
  1370. }
  1371. elseif (!empty($value)) {
  1372. $flat_values = array_merge($flat_values, _mollom_flatten_form_values($value));
  1373. }
  1374. }
  1375. elseif (is_string($value) && strlen($value)) {
  1376. $flat_values[] = $value;
  1377. }
  1378. }
  1379. return $flat_values;
  1380. }
  1381. /**
  1382. * Helper function to return OpenID identifiers associated with a given user account.
  1383. */
  1384. function _mollom_get_openid($account) {
  1385. if (isset($account->uid)) {
  1386. $ids = db_query('SELECT authname FROM {authmap} WHERE module = :module AND uid = :uid', array(':module' => 'openid', ':uid' => $account->uid))->fetchCol();
  1387. if (!empty($ids)) {
  1388. return implode($ids, ' ');
  1389. }
  1390. }
  1391. }
  1392. /**
  1393. * Helper function to determine protected forms for an entity.
  1394. *
  1395. * @param $type
  1396. * The type of entity to check.
  1397. * @param $bundle
  1398. * An array of bundle names to check.
  1399. *
  1400. * @return array
  1401. * An array of protected bundles for this entity type.
  1402. */
  1403. function _mollom_get_entity_forms_protected($type, $bundles = array()) {
  1404. // Find out if this entity bundle is protected.
  1405. $protected = &drupal_static(__FUNCTION__,array());
  1406. if (empty($bundles)) {
  1407. $info = entity_get_info($type);
  1408. $bundles = array_keys($info['bundles']);
  1409. }
  1410. $protected_bundles = array();
  1411. foreach ($bundles as $bundle) {
  1412. if (!isset($protected[$type][$bundle])) {
  1413. $protected[$type][$bundle] = db_query_range('SELECT 1 FROM {mollom_form} WHERE entity = :entity AND bundle = :bundle', 0, 1, array(
  1414. ':entity' => $type,
  1415. ':bundle' => isset($bundle) ? $bundle : $type,
  1416. ))->fetchField();
  1417. }
  1418. if (!empty($protected[$type][$bundle])) {
  1419. $protected_bundles[] = $bundle;
  1420. }
  1421. }
  1422. return $protected_bundles;
  1423. }
  1424. /**
  1425. * Returns the (last known) status of the configured Mollom API keys.
  1426. *
  1427. * @param bool $force
  1428. * (optional) Boolean whether to ignore the cached state and re-check.
  1429. * Defaults to FALSE.
  1430. * @param bool $update
  1431. * (optional) Whether to update Mollom with locally stored configuration.
  1432. * Defaults to FALSE.
  1433. *
  1434. * @return array
  1435. * An associative array describing the current status of the module:
  1436. * - isConfigured: Boolean whether Mollom API keys have been configured.
  1437. * - isVerified: Boolean whether Mollom API keys have been verified.
  1438. * - response: The response error code of the API verification request.
  1439. * - ...: The full site resource, as returned by the Mollom API.
  1440. *
  1441. * @see mollom_init()
  1442. * @see mollom_admin_settings()
  1443. * @see mollom_requirements()
  1444. */
  1445. function _mollom_status($force = FALSE, $update = FALSE) {
  1446. $static_cache = &drupal_static(__FUNCTION__, array());
  1447. $testing_mode = (int) variable_get('mollom_testing_mode', 0);
  1448. $status = &$static_cache[$testing_mode];
  1449. if (!$force && isset($status)) {
  1450. return $status;
  1451. }
  1452. // Check the cached status.
  1453. $cid = 'mollom_status:' . $testing_mode;
  1454. $expire_valid = 86400; // once per day
  1455. $expire_invalid = 3600; // once per hour
  1456. if (!$force && $cache = cache_get($cid, 'cache')) {
  1457. if ($cache->expire > REQUEST_TIME) {
  1458. $status = $cache->data;
  1459. return $status;
  1460. }
  1461. }
  1462. // Re-check configuration status.
  1463. $mollom = mollom();
  1464. $status = array(
  1465. 'isConfigured' => FALSE,
  1466. 'isVerified' => FALSE,
  1467. 'isTesting' => (bool) $testing_mode,
  1468. 'response' => NULL,
  1469. 'publicKey' => $mollom->loadConfiguration('publicKey'),
  1470. 'privateKey' => $mollom->loadConfiguration('privateKey'),
  1471. 'expectedLanguages' => $mollom->loadConfiguration('expectedLanguages'),
  1472. );
  1473. $status['isConfigured'] = (!empty($status['publicKey']) && !empty($status['privateKey']));
  1474. $status['expectedLanguages'] = is_array($status['expectedLanguages']) ? array_values($status['expectedLanguages']) : array();
  1475. if ($testing_mode || $status['isConfigured']) {
  1476. $old_status = $status;
  1477. $data = array();
  1478. if ($update) {
  1479. // Ensure to use the most current API keys (might have been changed).
  1480. $mollom->publicKey = $status['publicKey'];
  1481. $mollom->privateKey = $status['privateKey'];
  1482. $data += array(
  1483. 'expectedLanguages' => $status['expectedLanguages'],
  1484. );
  1485. }
  1486. $data += $mollom->getClientInformation();
  1487. $response = $mollom->updateSite($data);
  1488. if (is_array($response) && $mollom->lastResponseCode === TRUE) {
  1489. $status = array_merge($status,$response);
  1490. $status['isVerified'] = TRUE;
  1491. mollom_log(array(
  1492. 'message' => 'API keys are valid.',
  1493. ), WATCHDOG_INFO);
  1494. // Unless we just updated, update local configuration with remote.
  1495. if (!$update) {
  1496. $languages_expected = is_array($status['expectedLanguages']) ? array_values($status['expectedLanguages']) : array();
  1497. if ($old_status['expectedLanguages'] != $status['expectedLanguages']) {
  1498. $mollom->saveConfiguration('expectedLanguages', $status['expectedLanguages']);
  1499. }
  1500. }
  1501. }
  1502. elseif ($response === Mollom::AUTH_ERROR) {
  1503. $status['response'] = $response;
  1504. mollom_log(array(
  1505. 'message' => 'Invalid API keys.',
  1506. ), WATCHDOG_ERROR);
  1507. }
  1508. elseif ($response === Mollom::REQUEST_ERROR) {
  1509. $status['response'] = $response;
  1510. mollom_log(array(
  1511. 'message' => 'Invalid client configuration.',
  1512. ), WATCHDOG_ERROR);
  1513. }
  1514. else {
  1515. $status['response'] = $response;
  1516. // A NETWORK_ERROR and other possible responses may be caused by the
  1517. // client-side environment, but also by Mollom service downtimes. Try to
  1518. // recover as soon as possible.
  1519. $expire_invalid = 60 * 5;
  1520. mollom_log(array(
  1521. 'message' => 'API keys could not be verified.',
  1522. ), WATCHDOG_ERROR);
  1523. }
  1524. }
  1525. cache_set($cid, $status, 'cache', REQUEST_TIME + ($status === TRUE ? $expire_valid : $expire_invalid));
  1526. return $status;
  1527. }
  1528. /**
  1529. * Outputs a warning message about enabled testing mode (once).
  1530. */
  1531. function _mollom_testing_mode_warning() {
  1532. // drupal_set_message() starts a session and disables page caching, which
  1533. // breaks cache-related tests. Thus, tests set the verbose variable to TRUE.
  1534. $warned = &drupal_static(__FUNCTION__, variable_get('mollom_testing_mode_omit_warning', NULL));
  1535. if (isset($warned)) {
  1536. return;
  1537. }
  1538. $warned = TRUE;
  1539. if (variable_get('mollom_testing_mode', 0) && empty($_POST)) {
  1540. $admin_message = '';
  1541. if (user_access('administer mollom') && $_GET['q'] != 'admin/config/content/mollom/settings') {
  1542. $admin_message = t('Visit the <a href="@settings-url">Mollom settings page</a> to disable it.', array(
  1543. '@settings-url' => url('admin/config/content/mollom/settings'),
  1544. ));
  1545. }
  1546. $message = t('Mollom testing mode is still enabled. !admin-message', array(
  1547. '!admin-message' => $admin_message,
  1548. ));
  1549. drupal_set_message($message, 'warning');
  1550. }
  1551. }
  1552. /**
  1553. * Helper function to log and optionally output an error message when Mollom servers are unavailable.
  1554. */
  1555. function _mollom_fallback() {
  1556. $fallback = variable_get('mollom_fallback', MOLLOM_FALLBACK_BLOCK);
  1557. if ($fallback == MOLLOM_FALLBACK_BLOCK) {
  1558. form_set_error('mollom', t("The spam filter installed on this site is currently unavailable. Per site policy, we are unable to accept new submissions until that problem is resolved. Please try resubmitting the form in a couple of minutes."));
  1559. }
  1560. }
  1561. /**
  1562. * Formats a message for end-users to report false-positives.
  1563. *
  1564. * @param array $form_state
  1565. * The current state of the form.
  1566. * @param array $data
  1567. * The latest Mollom session data pertaining to the form submission attempt.
  1568. *
  1569. * @return string
  1570. * A message string containing a specially crafted link to Mollom's
  1571. * false-positive report form, supplying these parameters:
  1572. * - public_key: The public API key of this site.
  1573. * - url: The current, absolute URL of the form.
  1574. * At least one or both of:
  1575. * - contentId: The content ID of the Mollom session.
  1576. * - captchaId: The CAPTCHA ID of the Mollom session.
  1577. * If available, to speed up and simplify the false-positive report form:
  1578. * - authorName: The author name, if supplied.
  1579. * - authorMail: The author's e-mail address, if supplied.
  1580. */
  1581. function _mollom_format_message_falsepositive($form_state, $data) {
  1582. $mollom = mollom();
  1583. $report_url = 'https://www.mollom.com/false-positive';
  1584. $params = array(
  1585. 'public_key' => $mollom->loadConfiguration('publicKey'),
  1586. );
  1587. $params += array_intersect_key($form_state['values']['mollom'], array_flip(array('contentId', 'captchaId')));
  1588. $params += array_intersect_key($data, array_flip(array('authorName', 'authorMail')));
  1589. $params['url'] = $GLOBALS['base_root'] . request_uri();
  1590. $report_url .= '?' . drupal_http_build_query($params);
  1591. return t('If you feel this is in error, please <a href="@report-url" class="mollom-target">report that you are blocked</a>.', array(
  1592. '@report-url' => $report_url,
  1593. ));
  1594. }
  1595. /**
  1596. * Implements hook_element_info().
  1597. */
  1598. function mollom_element_info() {
  1599. return array(
  1600. 'mollom' => array(
  1601. '#process' => array(
  1602. 'mollom_process_mollom',
  1603. ),
  1604. '#pre_render' => array('mollom_pre_render_mollom'),
  1605. ),
  1606. );
  1607. }
  1608. /**
  1609. * Implements hook_theme().
  1610. */
  1611. function mollom_theme() {
  1612. $base_path = base_path() . drupal_get_path('module', 'mollom');
  1613. return array(
  1614. 'mollom_admin_blacklist_form' => array(
  1615. 'render element' => 'form',
  1616. 'file' => 'mollom.admin.inc',
  1617. ),
  1618. 'mollom_captcha_audio' => array(
  1619. 'variables' => array(
  1620. 'captcha_url' => NULL,
  1621. 'flash_fallback_player' => $base_path . '/mollom-captcha-player.swf',
  1622. ),
  1623. 'template' => 'mollom-captcha-audio',
  1624. ),
  1625. 'mollom_captcha_image' => array(
  1626. 'variables' => array(
  1627. 'captcha_url' => NULL,
  1628. 'audio_enabled' => TRUE,
  1629. ),
  1630. 'template' => 'mollom-captcha-image',
  1631. ),
  1632. );
  1633. }
  1634. /**
  1635. * Helper function to determine if "report to Mollom" is available
  1636. * and activated for a specific entity type.
  1637. *
  1638. * @param $type
  1639. * The entity type to check.
  1640. * @param $bundles
  1641. * An array of entity bundles to check.
  1642. */
  1643. function mollom_flag_entity_type_access($type, $bundles = array()) {
  1644. // Check to see if flag as inappropriate is enabled for this entity type.
  1645. $allowed = variable_get('mollom_fai_entity_types', array('comment' => 1));
  1646. if (empty($allowed[$type])) {
  1647. return;
  1648. }
  1649. // Check to see if there are protected forms for this content type.
  1650. $protected = _mollom_get_entity_forms_protected($type, $bundles);
  1651. if (empty($protected)) {
  1652. return FALSE;
  1653. }
  1654. // Make sure that flag as inappropriate is active for this type.
  1655. // If any form for this entity type is reportable, then show data.
  1656. $forms = mollom_form_list();
  1657. foreach ($forms as $info) {
  1658. if (!isset($info['entity']) || $info['entity'] != $type) {
  1659. continue;
  1660. }
  1661. if (isset($info['entity report access callback'])) {
  1662. $function = $info['entity report access callback'];
  1663. if ($function()) {
  1664. return TRUE;
  1665. }
  1666. }
  1667. }
  1668. return FALSE;
  1669. }
  1670. /**
  1671. * Implements hook_library().
  1672. */
  1673. function mollom_library() {
  1674. $libraries['flag'] = array(
  1675. 'title' => 'Flag as Inappropriate',
  1676. 'version' => '1.0',
  1677. 'js' => array(
  1678. drupal_get_path('module', 'mollom') . '/mollom.flag.js' => array(),
  1679. ),
  1680. 'css' => array(
  1681. drupal_get_path('module', 'mollom') . '/mollom.flag.position.css' => array(),
  1682. drupal_get_path('module', 'mollom') . '/mollom.flag.css' => array(),
  1683. ),
  1684. 'dependencies' => array(
  1685. array('system', 'drupal.ajax'),
  1686. )
  1687. );
  1688. return $libraries;
  1689. }
  1690. /**
  1691. * Helper function to determine if "report to mollom" is available for the
  1692. * current entity.
  1693. *
  1694. * @param $type
  1695. * The type of the entity to check.
  1696. * @param $entity
  1697. * The entity to check. This can be either the entity object or an id.
  1698. * @return bool
  1699. * True if reporting is available and false if not available.
  1700. */
  1701. function _mollom_flag_access($type, $entity) {
  1702. // make sure that Mollom is active and user is able to report.
  1703. if (!_mollom_access('report to mollom')) {
  1704. return FALSE;
  1705. }
  1706. // Find out if this entity bundle is protected.
  1707. if (!is_object($entity)) {
  1708. $entities = entity_load($type, array($entity));
  1709. $entity = $entities[$entity];
  1710. }
  1711. list($id, $rid, $bundle) = entity_extract_ids($type, $entity);
  1712. return mollom_flag_entity_type_access($type, array($bundle));
  1713. }
  1714. /**
  1715. * Implements hook_entity_view().
  1716. */
  1717. function mollom_entity_view($entity, $type, $view_mode, $langcode) {
  1718. module_load_include('inc', 'mollom', 'mollom.flag');
  1719. mollom_flag_entity_view($entity, $type, $view_mode, $langcode);
  1720. }
  1721. /**
  1722. * Implements hook_form_FORMID_alter().
  1723. */
  1724. function mollom_form_comment_form_alter(&$form, &$form_state, $form_id) {
  1725. module_load_include('inc', 'mollom', 'mollom.flag');
  1726. mollom_flag_comment_form_alter($form, $form_state, $form_id);
  1727. }
  1728. /**
  1729. * Ajax callback for retrieving a form behavior analysis image.
  1730. *
  1731. * Outputs the JSON encoded tracking information received from Mollom. This
  1732. * will include keys of:
  1733. * - tracking_url: the URL to the tracking image
  1734. * - tracking_id: an ID to track for the image
  1735. */
  1736. function mollom_fba_js() {
  1737. // Deny GET requests to make automated security audit tools not complain
  1738. // about a JSON Hijacking possibility.
  1739. // @see http://capec.mitre.org/data/definitions/111.html
  1740. // @see http://haacked.com/archive/2009/06/24/json-hijacking.aspx
  1741. if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
  1742. header($_SERVER['SERVER_PROTOCOL'] . ' 405 Method Not Allowed');
  1743. // A HTTP 405 response MUST specify allowed methods.
  1744. header('Allow: POST');
  1745. drupal_exit();
  1746. }
  1747. $mollom = mollom();
  1748. $tracking = $mollom->getTrackingImage();
  1749. drupal_json_output($tracking);
  1750. }
  1751. /**
  1752. * #process callback for #type 'mollom'.
  1753. *
  1754. * Mollom supports two fundamentally different protection modes:
  1755. * - For text analysis, the state of a post is essentially tracked by the Mollom
  1756. * API/backend:
  1757. * - Every form submission attempt (re-)sends the post data to Mollom, and the
  1758. * API ensures to return the correct spamClassification each time.
  1759. * - The client-side logic fully relies on the returned spamClassification
  1760. * value to trigger the corresponding actions and does not track the state
  1761. * of the form submission attempt locally.
  1762. * - For example, when Mollom is "unsure" and the user solved the CAPTCHA,
  1763. * then subsequent Content API responses will return "ham" instead of
  1764. * "unsure", so the user is not asked to solve more than one CAPTCHA.
  1765. * - For CAPTCHA-only, the solution state of a CAPTCHA has to be tracked locally:
  1766. * - Unlike text analysis, a CAPTCHA can only be solved or not. Additionally,
  1767. * a CAPTCHA cannot be solved more than once. The Mollom API only returns
  1768. * (once) whether a CAPTCHA has been solved correctly. A previous state
  1769. * cannot be queried from Mollom.
  1770. * - State-tracking would not be necessary, if there could not be other form
  1771. * validation errors preventing the form from submitting directly, as well as
  1772. * "Preview" buttons that may rebuild the entire form from scratch (if there
  1773. * are no validation errors).
  1774. * - To track state, the Form API cache is enabled, which allows to store and
  1775. * retrieve the entire $form_state of a previous submission (attempt).
  1776. * - Furthermore, page caching is force-disabled, so as to ensure that cached
  1777. * form data is not re-used by different users/visitors.
  1778. * - In combination with the Form API cache, this is essentially equal to
  1779. * force-starting a session for all users that try to submit a CAPTCHA-only
  1780. * protected form. However, a session would persist across other pages.
  1781. *
  1782. * @see mollom_form_alter()
  1783. * @see mollom_element_info()
  1784. * @see mollom_pre_render_mollom()
  1785. */
  1786. function mollom_process_mollom($element, &$form_state, $complete_form) {
  1787. // Setup initial Mollom session and form information.
  1788. $form_state += array('mollom' => array());
  1789. $form_state['mollom'] += array(
  1790. // Only TRUE if the form is protected by text analysis.
  1791. 'require_analysis' => $element['#mollom_form']['mode'] == MOLLOM_MODE_ANALYSIS,
  1792. // Becomes TRUE whenever a CAPTCHA needs to be solved.
  1793. 'require_captcha' => $element['#mollom_form']['mode'] == MOLLOM_MODE_CAPTCHA,
  1794. // Becomes TRUE when the CAPTCHA has been solved.
  1795. // Only applies to CAPTCHA-only protected forms. Not necessarily TRUE for
  1796. // text analysis, even if a CAPTCHA has been solved previously.
  1797. 'passed_captcha' => FALSE,
  1798. // The type of CAPTCHA to show; 'image' or 'audio'.
  1799. 'captcha_type' => 'image',
  1800. // Becomes TRUE if the form is protected by text analysis and the submitted
  1801. // entity should be unpublished.
  1802. 'require_moderation' => FALSE,
  1803. // Internally used bag for last Mollom API responses.
  1804. 'response' => array(
  1805. ),
  1806. );
  1807. // Add remaining information about the registered form.
  1808. $form_state['mollom'] += $element['#mollom_form'];
  1809. // By default, bad form submissions are discarded, unless the form was
  1810. // configured to moderate bad posts. 'discard' may only be FALSE, if there is
  1811. // a valid 'moderation callback'. Otherwise, it must be TRUE.
  1812. if (empty($form_state['mollom']['moderation callback']) || !function_exists($form_state['mollom']['moderation callback'])) {
  1813. $form_state['mollom']['discard'] = TRUE;
  1814. }
  1815. // Add the JavaScript.
  1816. $element['#attached']['js'][] = drupal_get_path('module', 'mollom') . '/mollom.js';
  1817. // Add the Mollom session data elements.
  1818. // These elements resemble the {mollom} database schema. The form validation
  1819. // handlers will pollute them with values returned by Mollom. For entity
  1820. // forms, the submitted values will appear in a $entity->mollom property,
  1821. // which in turn represents the Mollom session data record to be stored.
  1822. $element['entity'] = array(
  1823. '#type' => 'value',
  1824. '#value' => isset($form_state['mollom']['entity']) ? $form_state['mollom']['entity'] : 'mollom_content',
  1825. );
  1826. $element['id'] = array(
  1827. '#type' => 'value',
  1828. '#value' => NULL,
  1829. );
  1830. $element['contentId'] = array(
  1831. '#type' => 'hidden',
  1832. // There is no default value; Form API will always use the value that was
  1833. // submitted last (including rebuild scenarios).
  1834. '#attributes' => array('class' => 'mollom-content-id'),
  1835. );
  1836. $element['captchaId'] = array(
  1837. '#type' => 'hidden',
  1838. '#attributes' => array('class' => 'mollom-captcha-id'),
  1839. );
  1840. $element['form_id'] = array(
  1841. '#type' => 'value',
  1842. '#value' => $form_state['mollom']['form_id'],
  1843. );
  1844. $element['moderate'] = array(
  1845. '#type' => 'value',
  1846. '#value' => 0,
  1847. );
  1848. $data_spec = array(
  1849. '#type' => 'value',
  1850. '#value' => NULL,
  1851. );
  1852. $element['spamScore'] = $data_spec;
  1853. $element['spamClassification'] = $data_spec;
  1854. $element['qualityScore'] = $data_spec;
  1855. $element['profanityScore'] = $data_spec;
  1856. $element['languages'] = $data_spec;
  1857. $element['reason'] = $data_spec;
  1858. $element['solved'] = $data_spec;
  1859. // Add the CAPTCHA element.
  1860. // - Cannot be #required, since that would cause _form_validate() to output a
  1861. // validation error in situations in which the CAPTCHA is not required.
  1862. // - #access can also not start with FALSE, since the form structure may be
  1863. // cached, and Form API ignores all user input for inaccessible elements.
  1864. // Since this element needs to be hidden by the #pre_render callback, but that
  1865. // callback does not have access to $form_state, the 'passed_captcha' state is
  1866. // assigned as Boolean #solved = TRUE element property when solved correctly.
  1867. $element['captcha'] = array(
  1868. '#type' => 'textfield',
  1869. '#title' => t('Verification'),
  1870. '#size' => 10,
  1871. '#default_value' => '',
  1872. );
  1873. // Disable browser autocompletion, unless testing mode is enabled, in which
  1874. // case autocompletion for 'correct' and 'incorrect' is handy.
  1875. if (!variable_get('mollom_testing_mode', 0)) {
  1876. $element['captcha']['#attributes']['autocomplete'] = 'off';
  1877. }
  1878. // For CAPTCHA-only protected forms:
  1879. if (!$form_state['mollom']['require_analysis'] && $form_state['mollom']['require_captcha']) {
  1880. // Retrieve and show an initial CAPTCHA.
  1881. if (empty($form_state['process_input'])) {
  1882. // Enable Form API caching, in order to track the state of the CAPTCHA.
  1883. $form_state['cache'] = TRUE;
  1884. // mollom_form_add_captcha() adds the CAPTCHA and disables page caching.
  1885. mollom_form_add_captcha($element, $form_state);
  1886. }
  1887. // If the CAPTCHA was solved in a previous submission already, resemble
  1888. // mollom_validate_captcha(). This case is only reached in case the form
  1889. // 1) is not cached, 2) fully validated, 3) was submitted, and 4) is getting
  1890. // rebuilt; e.g., "Preview" on comment and node forms.
  1891. if ($form_state['mollom']['passed_captcha']) {
  1892. $element['captcha']['#solved'] = TRUE;
  1893. }
  1894. }
  1895. // Add a spambot trap. Purposively use 'homepage' as field name.
  1896. // This form input element is only supposed to be visible for robots. It has
  1897. // - no label, since some screen-readers do not notice that the label is
  1898. // attached to an input that is hidden.
  1899. // - no 'title' attribute, since some JavaScript libraries that are trying to
  1900. // mimic HTML5 placeholders are injecting the 'title' into the input's value
  1901. // and fail to clean up and remove the placeholder value upon form submission,
  1902. // causing false-positive spam classifications.
  1903. $element['homepage'] = array(
  1904. '#type' => 'textfield',
  1905. // Include the title field for accessibility scanners but it will not be
  1906. // shown due to the prefix.
  1907. '#title' => t('Home page'),
  1908. // Wrap the entire honeypot form element markup into a hidden container, so
  1909. // robots cannot simply check for a style attribute, but instead have to
  1910. // implement advanced DOM processing to figure out whether they are dealing
  1911. // with a honeypot field.
  1912. '#prefix' => '<div style="display: none;">',
  1913. '#suffix' => '</div>',
  1914. '#attributes' => array(
  1915. // Disable browser autocompletion.
  1916. 'autocomplete' => 'off',
  1917. ),
  1918. );
  1919. // Add the form behavior analysis web tracking beacon field holder if enabled.
  1920. if (variable_get('mollom_fba_enabled', 0) && $form_state['mollom']['require_analysis']) {
  1921. $element['fba'] = array(
  1922. '#type' => 'hidden',
  1923. );
  1924. }
  1925. // Make Mollom form and session information available to entirely different
  1926. // functions.
  1927. // @see mollom_mail_alter()
  1928. $GLOBALS['mollom'] = &$form_state['mollom'];
  1929. return $element;
  1930. }
  1931. /**
  1932. * Adds a Mollom CAPTCHA to a Mollom-protected form.
  1933. *
  1934. * @param $element
  1935. * The form element structure contained in $form['mollom']. Passed by
  1936. * reference.
  1937. * @param $form_state
  1938. * The current state of the form. Passed by reference.
  1939. *
  1940. * @return bool
  1941. * TRUE if a CAPTCHA was added, FALSE if not.
  1942. */
  1943. function mollom_form_add_captcha(&$element, &$form_state) {
  1944. // Prevent the page cache from storing a form containing a CAPTCHA element.
  1945. drupal_page_is_cacheable(FALSE);
  1946. $captcha = mollom_get_captcha($form_state);
  1947. // If we get a response, add the image CAPTCHA to the form element.
  1948. if (!empty($captcha)) {
  1949. $element['captcha']['#access'] = TRUE;
  1950. $element['captcha']['#field_prefix'] = $captcha;
  1951. $element['captcha']['#attributes'] = array('title' => t('Enter the characters from the verification above.'));
  1952. _mollom_attach_captcha_script($element['captcha']);
  1953. // Ensure that the latest CAPTCHA ID is output as value.
  1954. $element['captchaId']['#value'] = $form_state['mollom']['response']['captcha']['id'];
  1955. $form_state['values']['mollom']['captchaId'] = $form_state['mollom']['response']['captcha']['id'];
  1956. return TRUE;
  1957. }
  1958. // Otherwise, we have a communication or configuration error.
  1959. else {
  1960. $element['captcha']['#access'] = FALSE;
  1961. // Trigger fallback mode.
  1962. _mollom_fallback();
  1963. return FALSE;
  1964. }
  1965. }
  1966. /**
  1967. * Form validation handler to perform textual analysis on submitted form values.
  1968. */
  1969. function mollom_validate_analysis(&$form, &$form_state) {
  1970. if (!$form_state['mollom']['require_analysis']) {
  1971. return;
  1972. }
  1973. // Perform textual analysis.
  1974. $all_data = mollom_form_get_values($form_state, $form_state['mollom']['enabled_fields'], $form_state['mollom']['mapping']);
  1975. // Cancel processing upon invalid UTF-8 data.
  1976. if ($all_data === FALSE) {
  1977. return;
  1978. }
  1979. $data = $all_data;
  1980. // Remove postId property; only used by mollom_form_submit().
  1981. if (isset($data['postId'])) {
  1982. unset($data['postId']);
  1983. }
  1984. if (!empty($form_state['values']['mollom']['contentId'])) {
  1985. $data['id'] = $form_state['values']['mollom']['contentId'];
  1986. }
  1987. $data['checks'] = $form_state['mollom']['checks'];
  1988. $data['strictness'] = $form_state['mollom']['strictness'];
  1989. if (isset($form_state['mollom']['type'])) {
  1990. $data['type'] = $form_state['mollom']['type'];
  1991. }
  1992. if (in_array('spam', $data['checks']) && $form_state['mollom']['unsure'] == 'binary') {
  1993. $data['unsure'] = 0;
  1994. }
  1995. // Only pass the tracking id if this is the first textual evaluation.
  1996. if (isset($form_state['values']['mollom']['fba']) && empty($data['id'])) {
  1997. if (empty($form_state['values']['mollom']['fba'])) {
  1998. $data['trackingImageId'] = -1;
  1999. }
  2000. else {
  2001. $data['trackingImageId'] = $form_state['values']['mollom']['fba'];
  2002. }
  2003. }
  2004. // Allow modules to alter data sent.
  2005. drupal_alter('mollom_content', $data);
  2006. $result = mollom()->checkContent($data);
  2007. // Use all available data properties for log messages below.
  2008. $data += $all_data;
  2009. // Trigger global fallback behavior if there is a unexpected result.
  2010. if (!is_array($result) || !isset($result['id'])) {
  2011. return _mollom_fallback();
  2012. }
  2013. // Store the response returned by Mollom.
  2014. $form_state['mollom']['response']['content'] = $result;
  2015. // Set form values accordingly. Do not overwrite the entity ID.
  2016. // @todo Rename 'id' to 'entity_id'.
  2017. $result['contentId'] = $result['id'];
  2018. unset($result['id']);
  2019. $form_state['values']['mollom'] = array_merge($form_state['values']['mollom'], $result);
  2020. // Ensure the latest content ID is output as value.
  2021. // form_set_value() is effectless, as this is not a element-level but a
  2022. // form-level validation handler.
  2023. $form['mollom']['contentId']['#value'] = $result['contentId'];
  2024. // Prepare watchdog message teaser text.
  2025. $teaser = '--';
  2026. if (isset($data['postTitle'])) {
  2027. $teaser = truncate_utf8(strip_tags($data['postTitle']), 40);
  2028. }
  2029. elseif (isset($data['postBody'])) {
  2030. $teaser = truncate_utf8(strip_tags($data['postBody']), 40);
  2031. }
  2032. // Handle the profanity check result.
  2033. if (isset($result['profanityScore']) && $result['profanityScore'] >= 0.5) {
  2034. if ($form_state['mollom']['discard']) {
  2035. form_set_error('mollom', t('Your submission has triggered the profanity filter and will not be accepted until the inappropriate language is removed.'));
  2036. }
  2037. else {
  2038. $form_state['mollom']['require_moderation'] = TRUE;
  2039. }
  2040. mollom_log(array(
  2041. 'message' => 'Profanity: %teaser',
  2042. 'arguments' => array('%teaser' => $teaser),
  2043. ));
  2044. }
  2045. // Handle the spam check result.
  2046. // The Mollom API takes over state tracking for each content ID/session. The
  2047. // spamClassification will usually turn into 'ham' after solving a CAPTCHA.
  2048. // It may also change to 'spam', if the user replaced the values with very
  2049. // spammy content. In any case, we always do what we are told to do.
  2050. // Note: The returned spamScore may diverge from the spamClassification.
  2051. $form_state['mollom']['require_captcha'] = FALSE;
  2052. $form['mollom']['captcha']['#access'] = FALSE;
  2053. if (isset($result['spamClassification'])) {
  2054. switch ($result['spamClassification']) {
  2055. case 'ham':
  2056. mollom_log(array(
  2057. 'message' => 'Ham: %teaser',
  2058. 'arguments' => array('%teaser' => $teaser),
  2059. ), WATCHDOG_INFO);
  2060. break;
  2061. case 'spam':
  2062. if ($form_state['mollom']['discard']) {
  2063. form_set_error('mollom', t('Your submission has triggered the spam filter and will not be accepted.') . ' ' . _mollom_format_message_falsepositive($form_state, $data));
  2064. }
  2065. else {
  2066. $form_state['mollom']['require_moderation'] = TRUE;
  2067. }
  2068. mollom_log(array(
  2069. 'message' => 'Spam: %teaser',
  2070. 'arguments' => array('%teaser' => $teaser),
  2071. ));
  2072. break;
  2073. case 'unsure':
  2074. if ($form_state['mollom']['unsure'] == 'moderate') {
  2075. $form_state['mollom']['require_moderation'] = TRUE;
  2076. }
  2077. else {
  2078. $form_state['mollom']['require_captcha'] = TRUE;
  2079. $form_state['mollom']['passed_captcha'] = FALSE;
  2080. // Retrieve a new CAPTCHA and throw an error.
  2081. if (mollom_form_add_captcha($form['mollom'], $form_state)) {
  2082. $form['mollom']['captcha']['#access'] = TRUE;
  2083. if (!empty($form_state['temporary']['mollom']['had_captcha'])) {
  2084. form_error($form['mollom']['captcha'], t('The word verification was not completed correctly. Please complete this new word verification and try again.') . ' ' . _mollom_format_message_falsepositive($form_state, $data));
  2085. }
  2086. else {
  2087. form_error($form['mollom']['captcha'], t('To complete this form, please complete the word verification below.'));
  2088. }
  2089. }
  2090. }
  2091. mollom_log(array(
  2092. 'message' => 'Unsure: %teaser',
  2093. 'arguments' => array('%teaser' => $teaser),
  2094. ), WATCHDOG_INFO);
  2095. break;
  2096. case MOLLOM_ANALYSIS_UNKNOWN:
  2097. default:
  2098. // If we end up here, Mollom responded with a unknown spamClassification.
  2099. // Normally, this should not happen, but if it does, log it. As there
  2100. // could be multiple reasons for this, it is not safe to trigger the
  2101. // fallback mode.
  2102. mollom_log(array(
  2103. 'message' => 'Unknown: %teaser',
  2104. 'arguments' => array('%teaser' => $teaser),
  2105. ), WATCHDOG_ERROR);
  2106. break;
  2107. }
  2108. }
  2109. // Prevent the CAPTCHA element from being rendered, in case the form will be
  2110. // rebuilt after submission (e.g., comment preview).
  2111. // Unless text analysis was unsure, no CAPTCHA ID is required. But a previous
  2112. // submission attempt might have been unsure. If this submit will pass
  2113. // validation, then the rebuilt form will have no indication that it passed
  2114. // analysis and will be auto-populated with values from $form_state['input'].
  2115. if (!$form_state['mollom']['require_captcha']) {
  2116. $form_state['input']['mollom']['captchaId'] = '';
  2117. }
  2118. }
  2119. /**
  2120. * Form validation handler for Mollom's CAPTCHA form element.
  2121. *
  2122. * Validates whether a CAPTCHA was solved correctly. A form may contain a
  2123. * CAPTCHA, if it was configured to be protected by a CAPTCHA only, or when the
  2124. * text analysis result is "unsure".
  2125. */
  2126. function mollom_validate_captcha(&$form, &$form_state) {
  2127. if ($form_state['mollom']['require_analysis']) {
  2128. // For text analysis, only validate the CAPTCHA if there is an ID. If the ID
  2129. // is maliciously removed from the form values, text analysis will punish
  2130. // the author's reputation and present a new CAPTCHA to solve.
  2131. if (empty($form_state['values']['mollom']['captchaId'])) {
  2132. return;
  2133. }
  2134. }
  2135. else {
  2136. // Otherwise, this form is protected with a CAPTCHA only, unless disabled by
  2137. // another module.
  2138. if (!$form_state['mollom']['require_captcha']) {
  2139. return;
  2140. }
  2141. // If there is no CAPTCHA ID yet, retrieve one and throw an error.
  2142. if (empty($form_state['values']['mollom']['captchaId'])) {
  2143. if (mollom_form_add_captcha($form['mollom'], $form_state)) {
  2144. form_error($form['mollom']['captcha'], t('To complete this form, please complete the word verification below.'));
  2145. }
  2146. return;
  2147. }
  2148. }
  2149. // Inform text analysis validation that a CAPTCHA was validated, so the
  2150. // appropriate error message can be output.
  2151. $form_state['temporary']['mollom']['had_captcha'] = TRUE;
  2152. // $form_state['mollom']['passed_captcha'] may only ever be set by this
  2153. // validation handler and must not be changed elsewhere.
  2154. // This only becomes TRUE for CAPTCHA-only protected forms, for which the
  2155. // CAPTCHA state is locally tracked in $form_state. For text analysis, the
  2156. // primary 'require_captcha' condition will not be TRUE unless needed in the
  2157. // first place.
  2158. if ($form_state['mollom']['passed_captcha']) {
  2159. $form['mollom']['captcha']['#access'] = FALSE;
  2160. $form['mollom']['captcha']['#solved'] = TRUE;
  2161. return;
  2162. }
  2163. // Check the CAPTCHA result.
  2164. // Next to the Mollom session id and captcha result, the Mollom back-end also
  2165. // takes into account the author's IP and local user id (if registered). Any
  2166. // other values are ignored.
  2167. $all_data = mollom_form_get_values($form_state, $form_state['mollom']['enabled_fields'], $form_state['mollom']['mapping']);
  2168. // Cancel processing upon invalid UTF-8 data.
  2169. if ($all_data === FALSE) {
  2170. return;
  2171. }
  2172. $data = array(
  2173. 'id' => $form_state['values']['mollom']['captchaId'],
  2174. 'solution' => $form_state['values']['mollom']['captcha'],
  2175. 'authorIp' => $all_data['authorIp'],
  2176. );
  2177. if (isset($all_data['authorId'])) {
  2178. $data['authorId'] = $all_data['authorId'];
  2179. }
  2180. if (isset($all_data['authorCreated'])) {
  2181. $data['authorCreated'] = $all_data['authorCreated'];
  2182. }
  2183. if (isset($all_data['honeypot'])) {
  2184. $data['honeypot'] = $all_data['honeypot'];
  2185. }
  2186. $result = mollom()->checkCaptcha($data);
  2187. // Use all available data properties for log messages below.
  2188. $data += $all_data;
  2189. // Handle the result, unless it is FALSE (bogus CAPTCHA ID input).
  2190. if ($result !== FALSE) {
  2191. // Trigger global fallback behavior if there is a unexpected result.
  2192. if (!is_array($result) || !isset($result['id'])) {
  2193. return _mollom_fallback();
  2194. }
  2195. // Store the response for #submit handlers.
  2196. $form_state['mollom']['response']['captcha'] = $result;
  2197. // Set form values accordingly. Do not overwrite the entity ID.
  2198. // @todo Rename 'id' to 'entity_id'.
  2199. $result['captchaId'] = $result['id'];
  2200. unset($result['id']);
  2201. $form_state['values']['mollom'] = array_merge($form_state['values']['mollom'], $result);
  2202. // Ensure the latest CAPTCHA ID is output as value.
  2203. // form_set_value() is effectless, as this is not a element-level but a
  2204. // form-level validation handler.
  2205. $form['mollom']['captchaId']['#value'] = $result['captchaId'];
  2206. }
  2207. if (!empty($result['solved'])) {
  2208. // For text analysis, remove the CAPTCHA ID from the output if it was
  2209. // solved, so this validation handler does not run again.
  2210. if ($form_state['mollom']['require_analysis']) {
  2211. $form['mollom']['captchaId']['#value'] = '';
  2212. }
  2213. $form_state['mollom']['passed_captcha'] = TRUE;
  2214. $form['mollom']['captcha']['#access'] = FALSE;
  2215. $form['mollom']['captcha']['#solved'] = TRUE;
  2216. mollom_log(array(
  2217. 'message' => 'Correct CAPTCHA',
  2218. ), WATCHDOG_INFO);
  2219. }
  2220. else {
  2221. // Text analysis will re-check the content and may trigger a CAPTCHA on its
  2222. // own again (not guaranteed).
  2223. if (!$form_state['mollom']['require_analysis']) {
  2224. form_set_error('mollom][captcha', t('The word verification was not completed correctly. Please complete this new word verification and try again.') . ' ' . _mollom_format_message_falsepositive($form_state, $data));
  2225. mollom_form_add_captcha($form['mollom'], $form_state);
  2226. }
  2227. mollom_log(array(
  2228. 'message' => 'Incorrect CAPTCHA',
  2229. ));
  2230. }
  2231. }
  2232. /**
  2233. * #pre_render callback for #type 'mollom'.
  2234. *
  2235. * - Hides the CAPTCHA if it is not required or the solution was correct.
  2236. * - Marks the CAPTCHA as required.
  2237. */
  2238. function mollom_pre_render_mollom($element) {
  2239. // If there is no CAPTCHA ID, then there is no CAPTCHA that can be displayed.
  2240. // If a CAPTCHA was solved, then the widget makes no sense either.
  2241. if (empty($element['captchaId']['#value']) || !empty($element['captcha']['#solved'])) {
  2242. $element['captcha']['#access'] = FALSE;
  2243. }
  2244. else {
  2245. // The form element cannot be marked as #required, since _form_validate()
  2246. // would throw an element validation error on an empty value otherwise,
  2247. // before the form-level validation handler is executed.
  2248. // #access cannot default to FALSE, since the $form may be cached, and
  2249. // Form API ignores user input for all elements that are not accessible.
  2250. $element['captcha']['#required'] = TRUE;
  2251. }
  2252. // UX: Empty the CAPTCHA field value, as the user has to re-enter a new one.
  2253. $element['captcha']['#value'] = '';
  2254. // DX: Debugging helpers.
  2255. // $element['#suffix'] = 'contentId: ' . $element['contentId']['#value'] . '<br>';
  2256. // $element['#suffix'] .= 'captchaId: ' . $element['captchaId']['#value'] . '<br>';
  2257. return $element;
  2258. }
  2259. /**
  2260. * Form validation handler to perform post-validation tasks.
  2261. */
  2262. function mollom_validate_post(&$form, &$form_state) {
  2263. // Retain a post instead of discarding it. If 'discard' is FALSE, then the
  2264. // 'moderation callback' is responsible for altering $form_state in a way that
  2265. // the post ends up in a moderation queue. Most callbacks will only want to
  2266. // set or change a value in $form_state.
  2267. if ($form_state['mollom']['require_moderation']) {
  2268. $form_state['values']['mollom']['moderate'] = 1;
  2269. $function = $form_state['mollom']['moderation callback'];
  2270. $function($form, $form_state);
  2271. }
  2272. }
  2273. /**
  2274. * Form submit handler to flush Mollom session and form information from cache.
  2275. *
  2276. * @todo Check whether this is still needed with mollom_entity_insert(). For
  2277. * entity forms, this approach never really worked, since:
  2278. * - The primary submit handler fails to set the new ID of a newly stored
  2279. * entity in the submitted form values (which has been standardized in core,
  2280. * but is not enforced anywhere), so the postId cannot be extracted from
  2281. * submitted form values.
  2282. * - This submit handler is invoked too early, before the primary submit
  2283. * handler processed and saved the entity, so the postId cannot be extracted
  2284. * from submitted form values.
  2285. * - This submit handler is invoked too late; the primary submit handler might
  2286. * send out e-mails directly after saving the entity (e.g.,
  2287. * user_register_form_submit()), so mollom_mail_alter() is invoked before
  2288. * Mollom session data has been saved.
  2289. */
  2290. function mollom_form_submit($form, &$form_state) {
  2291. // Some modules are implementing multi-step forms without separate form
  2292. // submit handlers. In case we reach here and the form will be rebuilt, we
  2293. // need to defer our submit handling until final submission.
  2294. if (!empty($form_state['rebuild'])) {
  2295. return;
  2296. }
  2297. // If an 'entity' and a 'post_id' mapping was provided via
  2298. // hook_mollom_form_info(), try to automatically store Mollom session data.
  2299. if (!empty($form_state['mollom']['entity']) && isset($form_state['mollom']['mapping']['post_id'])) {
  2300. // For new entities, the entity's form submit handler will have added the
  2301. // new entity id value into $form_state['values'], so we need to rebuild the
  2302. // data mapping. We do not care for the actual fields, only for the value of
  2303. // the mapped postId.
  2304. // @todo Directly extract 'postId' from submitted form values.
  2305. $values = mollom_form_get_values($form_state, $form_state['mollom']['enabled_fields'], $form_state['mollom']['mapping']);
  2306. // We only consider non-empty and non-zero values as valid entity ids.
  2307. if (!empty($values['postId'])) {
  2308. // Save the Mollom session data.
  2309. $data = (object) $form_state['values']['mollom'];
  2310. $data->id = $values['postId'];
  2311. // Set the moderation flag for forms accepting bad posts.
  2312. $data->moderate = $form_state['mollom']['require_moderation'];
  2313. $form_state['mollom']['data'] = mollom_data_save($data);
  2314. }
  2315. }
  2316. }
  2317. /**
  2318. * Instantiates a new Mollom client.
  2319. *
  2320. * @param $class
  2321. * (optional) The name of a Mollom client implementation class to instantiate.
  2322. * Overrides the 'mollom_class' configuration variable. Debug use only.
  2323. * @param $force
  2324. * (optional) If true, then a new class is always instantiated.
  2325. */
  2326. function mollom($class = NULL, $force = FALSE) {
  2327. $instances = &drupal_static(__FUNCTION__, array());
  2328. if (!isset($class)) {
  2329. if (variable_get('mollom_testing_mode', 0)) {
  2330. $class = 'MollomDrupalTest';
  2331. }
  2332. else {
  2333. $class = variable_get('mollom_class', 'MollomDrupal');
  2334. }
  2335. }
  2336. // If there is no instance yet or if it is not of the desired class, create a
  2337. // new one.
  2338. if ($force || !isset($instances[$class]) || !($instances[$class] instanceof $class)) {
  2339. $instances[$class] = new $class();
  2340. }
  2341. return $instances[$class];
  2342. }
  2343. /**
  2344. * Adds a log entry to a global log (per-request).
  2345. *
  2346. * The Mollom client may perform multiple requests, and the client is able to
  2347. * recover from certain errors. The details of each request are important for
  2348. * support and debugging, but individual log messages for each request are too
  2349. * much and would confuse users, especially when (false-)errors appear in
  2350. * between.
  2351. *
  2352. * Therefore, the Mollom module collects all messages generated by the module
  2353. * integration code as well as by the Mollom client class within a single
  2354. * request, and only logs a single message when the request ends.
  2355. *
  2356. * This collection expects that at least one entry is logged that contains the
  2357. * primary log message and its severity.
  2358. *
  2359. * @param array $entry
  2360. * (optional) An associative array describing the entry to add to the log.
  2361. * If supplied, the special keys 'message' and 'arguments' are taken over as
  2362. * primary log message. All other key/value pairs will be appended to the
  2363. * resulting log message, whereas the key denotes a label/heading and the
  2364. * value is var_export()ed afterwards, unless NULL.
  2365. * @param int $severity
  2366. * (optional) The severity of the primary log message, as per RFC 3164.
  2367. * Possible values are WATCHDOG_ERROR, WATCHDOG_WARNING, etc. See watchdog()
  2368. * for details. Defaults to WATCHDOG_NOTICE when a 'message' is passed.
  2369. * @param bool $reset
  2370. * (optional) Whether to empty the log and return its contents.
  2371. *
  2372. * @return array
  2373. * An associative array containing the log:
  2374. * - message: The primary log message.
  2375. * - arguments: An array of placeholder token replacement values for
  2376. * _mollom_format_string().
  2377. * - severity: The severity of the primary log message.
  2378. * - entries: A list of all $entry items that have been passed in.
  2379. *
  2380. * @see mollom_exit()
  2381. */
  2382. function mollom_log(array $entry = NULL, $severity = NULL, $reset = FALSE) {
  2383. // Start with debug severity level.
  2384. $log = &drupal_static(__FUNCTION__, array());
  2385. if ($reset) {
  2386. $return = $log;
  2387. $log = array();
  2388. return $return;
  2389. }
  2390. if (!isset($entry)) {
  2391. return $log;
  2392. }
  2393. // Take over the primary message.
  2394. // Only the module integration code sets a message.
  2395. if (isset($entry['message'])) {
  2396. $log['message'] = $entry['message'];
  2397. $log['arguments'] = isset($entry['arguments']) ? $entry['arguments'] : array();
  2398. // Default to notice severity for module messages.
  2399. if (!isset($severity)) {
  2400. $severity = WATCHDOG_NOTICE;
  2401. }
  2402. }
  2403. if (!isset($log['severity'])) {
  2404. $log['severity'] = WATCHDOG_DEBUG;
  2405. }
  2406. // Update severity, if the entry is more severe than existing.
  2407. // Fail-over handling for requests is encapsulated in the Mollom class, which
  2408. // only passes the final severity already.
  2409. if (isset($severity) && $severity < $log['severity']) {
  2410. $log['severity'] = $severity;
  2411. }
  2412. $log['entries'][] = $entry;
  2413. return $log;
  2414. }
  2415. /**
  2416. * Logs a single system message potentially containing multiple Mollom log entries.
  2417. *
  2418. * @see mollom_log()
  2419. * @see _mollom_format_log()
  2420. * @see watchdog()
  2421. */
  2422. function mollom_log_write() {
  2423. // Retrieve the log and reset it.
  2424. $log = mollom_log(NULL, NULL, TRUE);
  2425. if (empty($log)) {
  2426. return;
  2427. }
  2428. // Only log if severity if it meets configured minimum severity, or if testing
  2429. // mode is enabled.
  2430. if (variable_get('mollom_testing_mode', 0) || $log['severity'] <= variable_get('mollom_log_minimum_severity', WATCHDOG_WARNING)) {
  2431. list($message, $arguments) = _mollom_format_log($log);
  2432. watchdog('mollom', $message, $arguments, $log['severity']);
  2433. }
  2434. }
  2435. /**
  2436. * Log a Mollom system message.
  2437. *
  2438. * @param $log
  2439. * @todo A list of message parts. Each item is an associative array whose keys are
  2440. * log message strings and whose corresponding values are t()-style
  2441. * replacement token arguments. At least one part is required.
  2442. *
  2443. * In essence, this is a poor man's YAML implementation (give or take some
  2444. * details, but note especially the indentation for array sub-elements).
  2445. */
  2446. function _mollom_format_log(array $log) {
  2447. $message = isset($log['message']) ? $log['message'] : '';
  2448. $arguments = isset($log['arguments']) ? $log['arguments'] : array();
  2449. // Hide further message details in the log overview table, if any.
  2450. // @see theme_dblog_message()
  2451. if (!empty($log['entries'])) {
  2452. // A <br> would be more appropriate, but filter_xss_admin() does not allow it.
  2453. //$message = '<p>' . $message . '</p>' . "\n\n";
  2454. $message = $message . "<p>\n\n</p>";
  2455. }
  2456. // Walk through each log entry to prepare and format its message and arguments.
  2457. $i = 0;
  2458. foreach ($log['entries'] as $entry) {
  2459. // Take over message and arguments literally (if any).
  2460. if (isset($entry['message'])) {
  2461. $message .= '<p>';
  2462. if (!empty($entry['arguments'])) {
  2463. $message .= _mollom_format_string($entry['message'], $entry['arguments']);
  2464. unset($entry['arguments']);
  2465. }
  2466. else {
  2467. $message .= $entry['message'];
  2468. }
  2469. unset($entry['message']);
  2470. $message .= "</p>\n";
  2471. }
  2472. unset($entry['severity']);
  2473. // Prettify replacement token values, if possible.
  2474. foreach ($entry as $token => $array) {
  2475. // Only prettify non-scalar values plus Booleans.
  2476. // I.e., NULL, TRUE, FALSE, array, and object.
  2477. if (is_scalar($array) && !is_bool($array)) {
  2478. $value = $array;
  2479. }
  2480. else {
  2481. $flat_value = NULL;
  2482. // Convert arrays and objects.
  2483. // @todo Objects?
  2484. if (isset($array) && !is_scalar($array)) {
  2485. $ref = &$array;
  2486. $key = key($ref);
  2487. $parents = array();
  2488. $flat_value = '';
  2489. while ($key !== NULL) {
  2490. if (is_scalar($ref[$key]) || is_bool($ref[$key]) || is_null($ref[$key])) {
  2491. $value = var_export($ref[$key], TRUE);
  2492. // Indent all values to have a visual separation from the last.
  2493. $flat_value .= str_repeat(' ', count($parents) + 1) . "{$key} = {$value}\n";
  2494. }
  2495. if (is_array($ref[$key])) {
  2496. $flat_value .= str_repeat(' ', count($parents) + 1) . "{$key} =\n";
  2497. }
  2498. // Recurse into nested keys, if the current key is not scalar.
  2499. if (is_array($ref[$key]) && !empty($ref[$key])) {
  2500. $parents[] = &$ref;
  2501. $ref = &$ref[$key];
  2502. $key = key($ref);
  2503. }
  2504. else {
  2505. // Move to next key if there is one.
  2506. next($ref);
  2507. if (key($ref) !== NULL) {
  2508. $key = key($ref);
  2509. }
  2510. // Move back to parent key, if there is one.
  2511. elseif ($parent = array_pop($parents)) {
  2512. $ref = &$parent;
  2513. next($ref);
  2514. $key = key($ref);
  2515. }
  2516. // Otherwise, reached the end of array and recursion.
  2517. else {
  2518. $key = NULL;
  2519. }
  2520. }
  2521. }
  2522. }
  2523. $value = NULL;
  2524. // Use prettified string representation.
  2525. if ($flat_value !== NULL) {
  2526. $value = $flat_value;
  2527. }
  2528. // Use var_export() for Booleans.
  2529. // Do not output NULL values on the top-level to allow for labels without
  2530. // following value.
  2531. elseif ($array !== NULL) {
  2532. $value = var_export($array, TRUE);
  2533. }
  2534. }
  2535. // Inject all other key/value pairs as @headingN (and optional
  2536. // '<pre>@valueN</pre>') placeholders.
  2537. if (isset($value)) {
  2538. $message .= "@heading{$i}\n<pre>@value{$i}</pre>\n";
  2539. $arguments += array(
  2540. '@heading' . $i => $token,
  2541. '@value' . $i => $value,
  2542. );
  2543. }
  2544. else {
  2545. $message .= "<p>@heading{$i}</p>\n";
  2546. $arguments += array(
  2547. '@heading' . $i => $token,
  2548. );
  2549. }
  2550. $i++;
  2551. }
  2552. }
  2553. return array($message, $arguments);
  2554. }
  2555. /**
  2556. * Replaces placeholders with sanitized values in a string.
  2557. *
  2558. * Backported from Drupal 8.
  2559. *
  2560. * @param $string
  2561. * A string containing placeholders.
  2562. * @param $args
  2563. * An associative array of replacements to make. Occurrences in $string of
  2564. * any key in $args are replaced with the corresponding value, after
  2565. * sanitization. The sanitization function depends on the first character of
  2566. * the key:
  2567. * - !variable: Inserted as is. Use this for text that has already been
  2568. * sanitized.
  2569. * - @variable: Escaped to HTML using check_plain(). Use this for anything
  2570. * displayed on a page on the site.
  2571. * - %variable: Escaped as a placeholder for user-submitted content using
  2572. * drupal_placeholder(), which shows up as <em>emphasized</em> text.
  2573. *
  2574. * @see t()
  2575. * @ingroup sanitization
  2576. */
  2577. function _mollom_format_string($string, array $args = array()) {
  2578. // Transform arguments before inserting them.
  2579. foreach ($args as $key => $value) {
  2580. switch ($key[0]) {
  2581. case '@':
  2582. // Escaped only.
  2583. $args[$key] = check_plain($value);
  2584. break;
  2585. case '%':
  2586. default:
  2587. // Escaped and placeholder.
  2588. $args[$key] = drupal_placeholder($value);
  2589. break;
  2590. case '!':
  2591. // Pass-through.
  2592. }
  2593. }
  2594. return strtr($string, $args);
  2595. }
  2596. /**
  2597. * Send feedback to Mollom.
  2598. *
  2599. * @param $data
  2600. * A Mollom data record containing one or both of:
  2601. * - contentId: The content ID to send feedback for.
  2602. * - captchaId: The CAPTCHA ID to send feedback for.
  2603. * @param $reason
  2604. * The feedback to send, one of 'spam', 'profanity', 'quality', 'unwanted',
  2605. * 'approve'.
  2606. * @param $type
  2607. * The type of feedback, one of 'moderate' or 'flag'.
  2608. * @param $source
  2609. * An optional single word string identifier for the user interface source.
  2610. * This is tracked along with the feedback to provide a more complete picture
  2611. * of how feedback is used and submitted on the site.
  2612. */
  2613. function _mollom_send_feedback($data, $reason = 'spam', $type = 'moderate', $source = NULL) {
  2614. global $user;
  2615. $params = array();
  2616. if (!empty($data->captchaId)) {
  2617. $params['captchaId'] = $data->captchaId;
  2618. $resource = 'CAPTCHA';
  2619. $id = $data->captchaId;
  2620. }
  2621. // In case we also have a contentId, also pass that, and override $resource
  2622. // and $id for the log message.
  2623. if (!empty($data->contentId)) {
  2624. $params['contentId'] = $data->contentId;
  2625. $resource = 'content';
  2626. $id = $data->contentId;
  2627. }
  2628. if (!isset($id)) {
  2629. return FALSE;
  2630. }
  2631. $params += array(
  2632. 'reason' => $reason,
  2633. 'type' => $type,
  2634. 'authorIp' => ip_address(),
  2635. );
  2636. if (!empty($source)) {
  2637. $params['source'] = $source;
  2638. }
  2639. if ($user->uid > 0) {
  2640. $params['authorId'] = $user->uid;
  2641. // Passing the user rather than account object because only the uid property
  2642. // is used by _mollom_get_openid.
  2643. $authorOpenId = _mollom_get_openid($user);
  2644. if (!empty($authorOpenId)) {
  2645. $params['authorOpenId'] = $authorOpenId;
  2646. }
  2647. }
  2648. $result = mollom()->sendFeedback($params);
  2649. mollom_log(array(
  2650. 'message' => 'Reported %feedback for @resource %id from %source - %type.',
  2651. 'arguments' => array(
  2652. '%type' => $type,
  2653. '%feedback' => $reason,
  2654. '@resource' => $resource,
  2655. '%id' => $id,
  2656. '%source' => $source,
  2657. ),
  2658. ));
  2659. return $result;
  2660. }
  2661. /**
  2662. * Fetch the site's Mollom statistics from the API.
  2663. *
  2664. * @param $refresh
  2665. * A boolean if TRUE, will force the statistics to be re-fetched and stored
  2666. * in the cache.
  2667. *
  2668. * @return array
  2669. * An array of statistics.
  2670. */
  2671. function mollom_get_statistics($refresh = FALSE) {
  2672. $statistics = FALSE;
  2673. $cache = cache_get('mollom:statistics');
  2674. // Only fetch if $refresh is TRUE, the cache is empty, or the cache is expired.
  2675. if ($refresh || !$cache || REQUEST_TIME >= $cache->expire) {
  2676. $status = _mollom_status();
  2677. if ($status['isVerified']) {
  2678. $statistics = drupal_map_assoc(array(
  2679. 'total_days',
  2680. 'total_accepted',
  2681. 'total_rejected',
  2682. 'yesterday_accepted',
  2683. 'yesterday_rejected',
  2684. 'today_accepted',
  2685. 'today_rejected',
  2686. ));
  2687. foreach ($statistics as $statistic) {
  2688. $result = mollom()->getStatistics(array('type' => $statistic));
  2689. if ($result === Mollom::NETWORK_ERROR || $result === Mollom::AUTH_ERROR) {
  2690. // If there was an error, stop fetching statistics and store FALSE
  2691. // in the cache. This will help prevent from making unnecessary
  2692. // requests to Mollom if the service is down or the server cannot
  2693. // connect to the Mollom service.
  2694. $statistics = FALSE;
  2695. break;
  2696. }
  2697. else {
  2698. $statistics[$statistic] = $result;
  2699. }
  2700. }
  2701. }
  2702. // Cache the statistics and set them to expire in one hour.
  2703. cache_set('mollom:statistics', $statistics, 'cache', REQUEST_TIME + 3600);
  2704. }
  2705. else {
  2706. $statistics = $cache->data;
  2707. }
  2708. return $statistics;
  2709. }
  2710. /**
  2711. * Implements hook_field_extra_fields().
  2712. *
  2713. * Allow users to re-order Mollom form additions through Field UI.
  2714. */
  2715. function mollom_field_extra_fields() {
  2716. $extras = array();
  2717. $forms = array_flip(db_query('SELECT form_id FROM {mollom_form}')->fetchCol());
  2718. foreach (mollom_form_list() as $form_id => $info) {
  2719. // @todo Technically, an 'entity' does not need to be a Entity/Field API
  2720. // kind of entity. Ideally of course, developers should use fieldable
  2721. // entities, but contributed/custom code may not. It is not clear whether
  2722. // registering extra fields for non-existing entities/bundles can break
  2723. // anything, so leaving it this way for now.
  2724. if (isset($info['entity']) && isset($forms[$form_id])) {
  2725. // If the entity type does not implement bundles, then entity_get_info()
  2726. // assumes a single bundle named after the entity.
  2727. $entity_type = $info['entity'];
  2728. $bundle = (isset($info['bundle']) ? $info['bundle'] : $entity_type);
  2729. $extras[$entity_type][$bundle]['form']['mollom'] = array(
  2730. 'label' => t('Mollom'),
  2731. 'description' => t('Mollom CAPTCHA or privacy policy link'),
  2732. 'weight' => 99,
  2733. );
  2734. }
  2735. }
  2736. return $extras;
  2737. }
  2738. /**
  2739. * Get the HTML markup for a Mollom CAPTCHA.
  2740. *
  2741. * @param $form_state
  2742. * The current state of a form.
  2743. *
  2744. * @return string
  2745. * The markup of the CAPTCHA HTML.
  2746. */
  2747. function mollom_get_captcha(&$form_state) {
  2748. // @todo Re-use existing CAPTCHA URL when the Mollom server response for
  2749. // verifying a CAPTCHA solution returns the existing URL.
  2750. $data = array(
  2751. 'type' => $form_state['mollom']['captcha_type'],
  2752. 'ssl' => (int) (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] == 'on'),
  2753. );
  2754. if (!empty($form_state['values']['mollom']['contentId'])) {
  2755. $data['contentId'] = $form_state['values']['mollom']['contentId'];
  2756. }
  2757. $result = mollom()->createCaptcha($data);
  2758. // Add a log message to prevent the request log from appearing without a
  2759. // message on CAPTCHA-only protected forms.
  2760. mollom_log(array(
  2761. 'message' => 'Retrieved new CAPTCHA',
  2762. ), WATCHDOG_INFO);
  2763. if (is_array($result) && isset($result['url'])) {
  2764. $url = $result['url'];
  2765. $form_state['mollom']['response']['captcha'] = $result;
  2766. }
  2767. else {
  2768. return '';
  2769. }
  2770. // Theme CAPTCHA output and return.
  2771. $audio_enabled = variable_get('mollom_audio_captcha_enabled', 1);
  2772. if ($audio_enabled && $form_state['mollom']['captcha_type'] == 'audio') {
  2773. return theme('mollom_captcha_audio', array(
  2774. 'captcha_url' => $url,
  2775. ));
  2776. }
  2777. else {
  2778. return theme('mollom_captcha_image', array(
  2779. 'captcha_url' => $url,
  2780. 'audio_enabled' => $audio_enabled,
  2781. ));
  2782. }
  2783. }
  2784. /**
  2785. * Attach SWFObject script to render element when available.
  2786. *
  2787. * @param $element
  2788. * A render element to attach the script to.
  2789. *
  2790. * @return bool
  2791. * True if the library can be found, false otherwise.
  2792. */
  2793. function _mollom_attach_captcha_script(&$element = NULL) {
  2794. $libraries = &drupal_static(__FUNCTION__);
  2795. if (empty($libraries['swfobject'])) {
  2796. $lib = array(
  2797. 'found' => FALSE,
  2798. );
  2799. // Try to load via libraries module if enabled.
  2800. if (module_exists('libraries') && function_exists('libraries_detect ')) {
  2801. if (($library = libraries_detect('swfobject')) && !empty($library['installed'])) {
  2802. $lib = array(
  2803. 'found' => TRUE,
  2804. 'libraries' => TRUE,
  2805. );
  2806. }
  2807. }
  2808. if (!$lib['found']) {
  2809. // Check for SWFObject in standard library locations.
  2810. $profile = drupal_get_path('profile', drupal_get_profile());
  2811. $config = conf_path();
  2812. $search = array(
  2813. 'libraries',
  2814. "$profile/libraries",
  2815. "sites/all/libraries",
  2816. "$config/libraries",
  2817. );
  2818. foreach ($search as $dir) {
  2819. if (is_dir($dir) && (
  2820. file_exists("$dir/swfobject.js") || file_exists("$dir/swfobject/swfobject.js")
  2821. )) {
  2822. $lib = array(
  2823. 'found' => TRUE,
  2824. 'libraries' => FALSE,
  2825. 'path' => file_exists("$dir/swfobject.js") ? "$dir/swfobject.js" : "$dir/swfobject/swfobject.js",
  2826. );
  2827. break;
  2828. }
  2829. }
  2830. }
  2831. $libraries['swfobject'] = $lib;
  2832. }
  2833. if ($libraries['swfobject']['found']) {
  2834. if (isset($element)) {
  2835. if ($libraries['swfobject']['libraries']) {
  2836. $element['#attached']['libraries_load'][] = array('swfobject');
  2837. }
  2838. else {
  2839. $element['#attached'] = array(
  2840. 'js' => array($libraries['swfobject']['path']),
  2841. );
  2842. }
  2843. }
  2844. return TRUE;
  2845. }
  2846. return FALSE;
  2847. }
  2848. /**
  2849. * Implements hook_mail_alter().
  2850. *
  2851. * Adds a "report as inappropriate" link to e-mails sent after Mollom-protected
  2852. * form submissions.
  2853. *
  2854. * @see mollom_mail_add_report_link()
  2855. *
  2856. * @todo With mollom_entity_insert(), $message['params'] might contain an array
  2857. * key that has a ::$mollom property holding the Mollom session data,
  2858. * potentially eliminating the need for $GLOBALS['mollom'].
  2859. */
  2860. function mollom_mail_alter(&$message) {
  2861. // Attaches the Mollom report link to any mails with IDs specified from the
  2862. // submitted form's hook_mollom_form_info(). This should ensure that the
  2863. // report link is added to mails sent by actual users and not any mails sent
  2864. // by Drupal since they should never be reported as spam.
  2865. if (!empty($GLOBALS['mollom']['mail ids']) && in_array($message['id'], $GLOBALS['mollom']['mail ids'])) {
  2866. mollom_mail_add_report_link($message, $GLOBALS['mollom']);
  2867. }
  2868. }
  2869. /**
  2870. * Add the 'Report as inappropriate' link to an e-mail message.
  2871. *
  2872. * @param array $message
  2873. * The message to alter.
  2874. * @param array $mollom
  2875. * The Mollom state for the mail; typically $form_state['mollom'], as set up
  2876. * by mollom_process_mollom().
  2877. *
  2878. * @see mollom_mail_alter()
  2879. */
  2880. function mollom_mail_add_report_link(array &$message, array $mollom) {
  2881. if (!empty($mollom['response']['content']['id']) || !empty($mollom['response']['captcha']['id'])) {
  2882. // Check whether an entity was stored with the submission.
  2883. $data = FALSE;
  2884. if (!empty($mollom['response']['content']['id'])) {
  2885. $data = mollom_content_load($mollom['response']['content']['id']);
  2886. }
  2887. elseif (!empty($mollom['response']['captcha']['id'])) {
  2888. $db_data = mollom_db_query_range('SELECT * FROM {mollom} WHERE captcha_id = :captchaId', 0, 1, array(':captchaId' => $mollom['response']['captcha']['id']))->fetchObject();
  2889. $data = _mollom_convert_db_names($db_data);
  2890. }
  2891. if (!$data) {
  2892. // @todo Mollom session data should have been saved earlier already;
  2893. // eliminate this.
  2894. $data = (object) $mollom['response'];
  2895. if (!empty($mollom['response']['content']['id'])) {
  2896. $data->entity = 'mollom_content';
  2897. $data->id = $data->content['id'];
  2898. $data->contentId = $data->content['id'];
  2899. }
  2900. else {
  2901. $data->entity = 'mollom_captcha';
  2902. $data->id = $data->captcha['id'];
  2903. $data->captchaId = $data->captcha['id'];
  2904. }
  2905. $data->form_id = $mollom['form_id'];
  2906. mollom_data_save($data);
  2907. }
  2908. // Determine report URI.
  2909. $mollom_form = mollom_form_load($data->form_id);
  2910. if (isset($mollom_form['report path'])) {
  2911. $path = strtr($mollom_form['report path'], array(
  2912. '%id' => $data->id,
  2913. ));
  2914. }
  2915. else {
  2916. $path = "mollom/report/{$data->entity}/{$data->id}";
  2917. }
  2918. $report_link = t('Report as inappropriate: @link', array(
  2919. '@link' => url($path, array('absolute' => TRUE)),
  2920. ));
  2921. $message['body'][] = $report_link;
  2922. }
  2923. }
  2924. /**
  2925. * Implements hook_entity_insert().
  2926. */
  2927. function mollom_entity_insert($entity, $type) {
  2928. list($id) = entity_extract_ids($type, $entity);
  2929. if (!empty($entity->mollom) && !empty($id)) {
  2930. $entity->mollom['id'] = $id;
  2931. $data = (object) $entity->mollom;
  2932. mollom_data_save($data);
  2933. }
  2934. }
  2935. /**
  2936. * Implements hook_entity_update().
  2937. */
  2938. function mollom_entity_update($entity, $type) {
  2939. // A user account's status transitions from 0 to 1 upon first login; do not
  2940. // mark the account as moderated in that case.
  2941. if ($type == 'user' && $entity->uid == $GLOBALS['user']->uid) {
  2942. return;
  2943. }
  2944. // If an existing entity is published and we have session data stored for it,
  2945. // mark the data as moderated.
  2946. $update = FALSE;
  2947. // If the entity update function provides the original entity, only mark the
  2948. // data as moderated when the entity's status transitioned to published.
  2949. if (isset($entity->original->status)) {
  2950. if (empty($entity->original->status) && !empty($entity->status)) {
  2951. $update = TRUE;
  2952. }
  2953. }
  2954. // If there is no original entity to compare against, check for the current
  2955. // status only.
  2956. elseif (!empty($entity->status)) {
  2957. $update = TRUE;
  2958. }
  2959. if ($update) {
  2960. list($id) = entity_extract_ids($type, $entity);
  2961. mollom_data_moderate($type, $id);
  2962. }
  2963. }
  2964. /**
  2965. * Implements hook_entity_delete().
  2966. */
  2967. function mollom_entity_delete($entity, $type) {
  2968. list($id) = entity_extract_ids($type, $entity);
  2969. mollom_data_delete($type, $id);
  2970. }
  2971. /**
  2972. * @name mollom_moderation Mollom Moderation integration.
  2973. * @{
  2974. */
  2975. /**
  2976. * Implements hook_mollom_data_insert().
  2977. */
  2978. function mollom_mollom_data_insert($data) {
  2979. // Only content can be updated.
  2980. if (empty($data->contentId)) {
  2981. return;
  2982. }
  2983. // Indicate that this content session is complete.
  2984. $params['id'] = $data->contentId;
  2985. $params['finalized'] = 1;
  2986. // Get additional information for submissions that result in a locally stored
  2987. // entity.
  2988. if ($data->entity != 'mollom_content' && $data->entity != 'mollom_captcha') {
  2989. // Add the URL of the posted content itself.
  2990. $entity_info = entity_get_info();
  2991. if (isset($entity_info[$data->entity])) {
  2992. $entity = entity_load($data->entity, array($data->id));
  2993. $entity = (isset($entity[$data->id]) ? $entity[$data->id] : FALSE);
  2994. }
  2995. if (!empty($entity)) {
  2996. $options = entity_uri($data->entity, $entity);
  2997. $options['absolute'] = TRUE;
  2998. $params['url'] = url($options['path'], $options);
  2999. // Add the title and URL of the parent content/context of the post, if any.
  3000. // @todo Figure out how to do this in a generic way.
  3001. if ($data->entity == 'comment') {
  3002. $node = node_load($entity->nid);
  3003. $options = entity_uri('node', $node);
  3004. $options['absolute'] = TRUE;
  3005. $params['contextUrl'] = url($options['path'], $options);
  3006. $params['contextTitle'] = entity_label('node', $node);
  3007. }
  3008. // Associate the new user ID for newly registered user accounts.
  3009. elseif ($data->entity == 'user') {
  3010. $params['authorId'] = $data->id;
  3011. }
  3012. }
  3013. }
  3014. $result = mollom()->checkContent($params);
  3015. }
  3016. /**
  3017. * @} End of "name mollom_moderation".
  3018. */
  3019. /**
  3020. * @name mollom_node Node module integration for Mollom.
  3021. * @{
  3022. */
  3023. /**
  3024. * Implements hook_mollom_form_list().
  3025. */
  3026. function node_mollom_form_list() {
  3027. $forms = array();
  3028. foreach (node_type_get_types() as $type) {
  3029. $form_id = $type->type . '_node_form';
  3030. $forms[$form_id] = array(
  3031. 'title' => t('@name form', array('@name' => $type->name)),
  3032. 'entity' => 'node',
  3033. 'bundle' => $type->type,
  3034. 'delete form' => 'node_delete_confirm',
  3035. 'delete form file' => array(
  3036. 'name' => 'node.pages',
  3037. ),
  3038. 'report access' => array('bypass node access', 'administer nodes'),
  3039. 'entity report access callback' => 'node_mollom_entity_report_access',
  3040. );
  3041. }
  3042. return $forms;
  3043. }
  3044. /**
  3045. * Implements hook_mollom_form_info().
  3046. */
  3047. function node_mollom_form_info($form_id) {
  3048. // Retrieve internal type from $form_id.
  3049. $nodetype = drupal_substr($form_id, 0, -10);
  3050. if (!$type = node_type_get_type($nodetype)) {
  3051. return;
  3052. }
  3053. $form_info = array(
  3054. // @todo This is incompatible with node access.
  3055. 'bypass access' => array('bypass node access'),
  3056. 'bundle' => $type->type,
  3057. 'moderation callback' => 'node_mollom_form_moderation',
  3058. 'context created callback' => 'node_mollom_context_created',
  3059. 'elements' => array(),
  3060. 'mapping' => array(
  3061. 'author_name' => 'name',
  3062. 'context_id' => 'nid',
  3063. ),
  3064. );
  3065. // @see node_permission()
  3066. if (in_array($type->type, node_permissions_get_configured_types())) {
  3067. $form_info['bypass access'][] = 'edit any ' . $type->type . ' content';
  3068. $form_info['bypass access'][] = 'delete any ' . $type->type . ' content';
  3069. }
  3070. // @see node_content_form()
  3071. if ($type->has_title) {
  3072. $form_info['elements']['title'] = check_plain($type->title_label);
  3073. $form_info['mapping']['post_title'] = 'title';
  3074. }
  3075. mollom_form_info_add_fields($form_info, 'node', $type->type);
  3076. return $form_info;
  3077. }
  3078. /**
  3079. * Mollom form moderation callback for nodes.
  3080. */
  3081. function node_mollom_form_moderation(&$form, &$form_state) {
  3082. $form_state['values']['status'] = 0;
  3083. }
  3084. /**
  3085. * Implements hook_form_FORMID_alter().
  3086. */
  3087. function mollom_form_node_admin_content_alter(&$form, &$form_state) {
  3088. // @see node_admin_content()
  3089. if (isset($form_state['values']['operation']) && $form_state['values']['operation'] == 'delete') {
  3090. mollom_data_delete_form_alter($form, $form_state);
  3091. // Report before deletion.
  3092. array_unshift($form['#submit'], 'mollom_form_node_multiple_delete_confirm_submit');
  3093. }
  3094. else {
  3095. module_load_include('inc', 'mollom', 'mollom.flag');
  3096. _mollom_table_add_flag_counts('node', $form['admin']['nodes']['#header'], $form['admin']['nodes']['#options']);
  3097. }
  3098. }
  3099. /**
  3100. * Form submit handler for node_multiple_delete_confirm().
  3101. */
  3102. function mollom_form_node_multiple_delete_confirm_submit($form, &$form_state) {
  3103. $nids = array_keys($form_state['values']['nodes']);
  3104. if (!empty($form_state['values']['mollom']['feedback'])) {
  3105. if (mollom_data_report_multiple('node', $nids, $form_state['values']['mollom']['feedback'])) {
  3106. drupal_set_message(t('The posts were successfully reported as inappropriate.'));
  3107. }
  3108. }
  3109. mollom_data_delete_multiple('node', $nids);
  3110. }
  3111. /**
  3112. * Entity report access callback for nodes.
  3113. * This enables the flag as inappropriate feature for nodes.
  3114. *
  3115. * @param $entity
  3116. * Optional entity object to check access to a specific entity.
  3117. */
  3118. function node_mollom_entity_report_access($entity = NULL) {
  3119. // All nodes can be reported as long as the user has access to view.
  3120. if (!empty($entity) && isset($entity->nid)) {
  3121. return node_access('view', $entity->nid);
  3122. }
  3123. else {
  3124. // Generally turned on when this function is enabled as a callback.
  3125. return TRUE;
  3126. }
  3127. }
  3128. /**
  3129. * Entity context created callback for nodes.
  3130. *
  3131. * @param $id
  3132. * The id of the node.
  3133. */
  3134. function node_mollom_context_created($id = NULL) {
  3135. if (empty($id)) {
  3136. return FALSE;
  3137. }
  3138. $node = node_load($id);
  3139. if (empty($node)) {
  3140. return FALSE;
  3141. }
  3142. return $node->created;
  3143. }
  3144. /**
  3145. * @} End of "name mollom_node".
  3146. */
  3147. /**
  3148. * @name mollom_comment Comment module integration for Mollom.
  3149. * @{
  3150. */
  3151. /**
  3152. * Implements hook_mollom_form_list().
  3153. */
  3154. function comment_mollom_form_list() {
  3155. $forms = array();
  3156. foreach (node_type_get_types() as $type) {
  3157. $form_id = "comment_node_{$type->type}_form";
  3158. $forms[$form_id] = array(
  3159. 'title' => t('@name comment form', array('@name' => $type->name)),
  3160. 'entity' => 'comment',
  3161. 'bundle' => 'comment_node_' . $type->type,
  3162. 'delete form' => 'comment_confirm_delete',
  3163. 'delete form file' => array(
  3164. 'name' => 'comment.admin',
  3165. ),
  3166. 'report access' => array('administer comments'),
  3167. 'entity delete multiple callback' => 'comment_delete_multiple',
  3168. 'entity report access callback' => 'comment_mollom_entity_report_access',
  3169. );
  3170. }
  3171. return $forms;
  3172. }
  3173. /**
  3174. * Implements hook_mollom_form_info().
  3175. */
  3176. function comment_mollom_form_info($form_id) {
  3177. $form_info = array(
  3178. 'mode' => MOLLOM_MODE_ANALYSIS,
  3179. 'bypass access' => array('administer comments'),
  3180. 'moderation callback' => 'comment_mollom_form_moderation',
  3181. 'context created callback' => 'node_mollom_context_created',
  3182. 'elements' => array(
  3183. 'subject' => t('Subject'),
  3184. ),
  3185. 'mapping' => array(
  3186. 'post_title' => 'subject',
  3187. 'author_name' => 'name',
  3188. 'author_mail' => 'mail',
  3189. 'author_url' => 'homepage',
  3190. 'context_id' => 'nid',
  3191. ),
  3192. );
  3193. // Retrieve internal type from $form_id.
  3194. $comment_bundle = drupal_substr($form_id, 0, -5);
  3195. mollom_form_info_add_fields($form_info, 'comment', $comment_bundle);
  3196. return $form_info;
  3197. }
  3198. /**
  3199. * Entity report access callback for comments.
  3200. *
  3201. * @param $entity
  3202. * Optional entity object to check access to a specific entity.
  3203. */
  3204. function comment_mollom_entity_report_access($entity = NULL) {
  3205. // All comments can be reported as long as the user has access to view the
  3206. // node and it's comments.
  3207. if (!user_access('access comments')) {
  3208. return FALSE;
  3209. }
  3210. if (!empty($entity)) {
  3211. return node_access('view', node_load($entity->nid));
  3212. }
  3213. return TRUE;
  3214. }
  3215. /**
  3216. * Mollom form moderation callback for comments.
  3217. */
  3218. function comment_mollom_form_moderation(&$form, &$form_state) {
  3219. $form_state['values']['status'] = COMMENT_NOT_PUBLISHED;
  3220. }
  3221. /**
  3222. * Implements hook_form_FORMID_alter().
  3223. */
  3224. function mollom_form_comment_multiple_delete_confirm_alter(&$form, &$form_state) {
  3225. mollom_data_delete_form_alter($form, $form_state);
  3226. // Report before deletion.
  3227. array_unshift($form['#submit'], 'mollom_form_comment_multiple_delete_confirm_submit');
  3228. }
  3229. /**
  3230. * Form submit handler for node_multiple_delete_confirm().
  3231. */
  3232. function mollom_form_comment_multiple_delete_confirm_submit($form, &$form_state) {
  3233. $cids = array_keys($form_state['values']['comments']);
  3234. if (!empty($form_state['values']['mollom']['feedback'])) {
  3235. if (mollom_data_report_multiple('comment', $cids, $form_state['values']['mollom']['feedback'])) {
  3236. drupal_set_message(t('The posts were successfully reported as inappropriate.'));
  3237. }
  3238. }
  3239. mollom_data_delete_multiple('comment', $cids);
  3240. }
  3241. /**
  3242. * Implements hook_form_FORMID_alter().
  3243. */
  3244. function mollom_form_comment_admin_overview_alter(&$form, &$form_state) {
  3245. module_load_include('inc', 'mollom', 'mollom.flag');
  3246. _mollom_table_add_flag_counts('comment', $form['comments']['#header'], $form['comments']['#options']);
  3247. }
  3248. /**
  3249. * Implements hook_form_FORMID_alter().
  3250. */
  3251. function mollom_form_node_form_alter(&$form, &$form_state, $form_id) {
  3252. module_load_include('inc', 'mollom', 'mollom.flag');
  3253. mollom_flag_node_form_alter($form, $form_state, $form_id);
  3254. }
  3255. /**
  3256. * @} End of "name mollom_comment".
  3257. */
  3258. /**
  3259. * @name mollom_user User module integration for Mollom.
  3260. * @{
  3261. */
  3262. /**
  3263. * Implements hook_mollom_form_list().
  3264. */
  3265. function user_mollom_form_list() {
  3266. $forms['user_register_form'] = array(
  3267. 'mode' => MOLLOM_MODE_CAPTCHA,
  3268. 'title' => t('User registration form'),
  3269. 'type' => 'user',
  3270. 'entity' => 'user',
  3271. 'bundle' => 'user',
  3272. 'delete form' => 'user_cancel_confirm_form',
  3273. 'report path' => 'user/%id/cancel',
  3274. 'report access' => array('administer users'),
  3275. );
  3276. $forms['user_profile_form'] = $forms['user_register_form'];
  3277. $forms['user_profile_form']['title'] = t('User profile form');
  3278. $forms['user_pass'] = array(
  3279. 'mode' => MOLLOM_MODE_CAPTCHA,
  3280. 'title' => t('User password request form'),
  3281. );
  3282. return $forms;
  3283. }
  3284. /**
  3285. * Implements hook_mollom_form_info().
  3286. */
  3287. function user_mollom_form_info($form_id) {
  3288. switch ($form_id) {
  3289. case 'user_register_form':
  3290. case 'user_profile_form':
  3291. $form_info = array(
  3292. 'bypass access' => array('administer users'),
  3293. 'moderation callback' => 'user_mollom_form_moderation',
  3294. 'mail ids' => array('user_register_pending_approval_admin'),
  3295. 'mapping' => array(
  3296. 'author_name' => 'name',
  3297. 'author_mail' => 'mail',
  3298. ),
  3299. );
  3300. mollom_form_info_add_fields($form_info, 'user', 'user');
  3301. return $form_info;
  3302. case 'user_pass':
  3303. $form_info = array(
  3304. 'bypass access' => array('administer users'),
  3305. 'mapping' => array(
  3306. 'post_id' => 'uid',
  3307. 'author_name' => 'name',
  3308. // The 'name' form element accepts either a username or mail address.
  3309. 'author_mail' => 'name',
  3310. ),
  3311. );
  3312. return $form_info;
  3313. }
  3314. }
  3315. /**
  3316. * Mollom form moderation callback for user accounts.
  3317. */
  3318. function user_mollom_form_moderation(&$form, &$form_state) {
  3319. $form_state['values']['status'] = 0;
  3320. }
  3321. /**
  3322. * Implements hook_form_FORMID_alter().
  3323. */
  3324. function mollom_form_user_multiple_cancel_confirm_alter(&$form, &$form_state) {
  3325. mollom_data_delete_form_alter($form, $form_state);
  3326. // Report before deletion.
  3327. array_unshift($form['#submit'], 'mollom_form_user_multiple_cancel_confirm_submit');
  3328. }
  3329. /**
  3330. * Form submit handler for node_multiple_delete_confirm().
  3331. */
  3332. function mollom_form_user_multiple_cancel_confirm_submit($form, &$form_state) {
  3333. $uids = array_keys($form_state['values']['accounts']);
  3334. if (!empty($form_state['values']['mollom']['feedback'])) {
  3335. if (mollom_data_report_multiple('user', $uids, $form_state['values']['mollom']['feedback'])) {
  3336. drupal_set_message(t('The users were successfully reported.'));
  3337. }
  3338. }
  3339. mollom_data_delete_multiple('user', $uids);
  3340. }
  3341. /**
  3342. * @} End of "name mollom_user".
  3343. */
  3344. /**
  3345. * @name mollom_contact Contact module integration for Mollom.
  3346. * @{
  3347. */
  3348. /**
  3349. * Implements hook_mollom_form_list().
  3350. */
  3351. function contact_mollom_form_list() {
  3352. $forms['contact_site_form'] = array(
  3353. 'title' => t('Site-wide contact form'),
  3354. );
  3355. $forms['contact_personal_form'] = array(
  3356. 'title' => t('User contact form'),
  3357. );
  3358. return $forms;
  3359. }
  3360. /**
  3361. * Implements hook_mollom_form_info().
  3362. */
  3363. function contact_mollom_form_info($form_id) {
  3364. switch ($form_id) {
  3365. case 'contact_site_form':
  3366. $form_info = array(
  3367. 'mode' => MOLLOM_MODE_ANALYSIS,
  3368. 'bypass access' => array('administer contact forms'),
  3369. 'mail ids' => array('contact_page_mail'),
  3370. 'elements' => array(
  3371. 'subject' => t('Subject'),
  3372. 'message' => t('Message'),
  3373. ),
  3374. 'mapping' => array(
  3375. 'post_title' => 'subject',
  3376. 'author_name' => 'name',
  3377. 'author_mail' => 'mail',
  3378. ),
  3379. );
  3380. return $form_info;
  3381. case 'contact_personal_form':
  3382. $form_info = array(
  3383. 'mode' => MOLLOM_MODE_ANALYSIS,
  3384. 'bypass access' => array('administer users'),
  3385. 'mail ids' => array('contact_user_mail'),
  3386. 'elements' => array(
  3387. 'subject' => t('Subject'),
  3388. 'message' => t('Message'),
  3389. ),
  3390. 'mapping' => array(
  3391. 'post_title' => 'subject',
  3392. 'author_name' => 'name',
  3393. 'author_mail' => 'mail',
  3394. ),
  3395. );
  3396. return $form_info;
  3397. }
  3398. }
  3399. /**
  3400. * @} End of "name mollom_contact".
  3401. */
  3402. /**
  3403. * @name mollom_profile Profile module integration for Mollom.
  3404. * @{
  3405. */
  3406. /**
  3407. * Implements hook_mollom_form_info_alter().
  3408. *
  3409. * Adds profile fields exposed on the user registration form.
  3410. */
  3411. function profile_mollom_form_info_alter(&$form_info, $form_id) {
  3412. if ($form_id != 'user_register_form') {
  3413. return;
  3414. }
  3415. // @see profile_form_profile()
  3416. $result = db_query("SELECT name, title FROM {profile_field} WHERE register = 1 AND type IN (:types)", array(
  3417. ':types' => array('textfield', 'textarea', 'url', 'list'),
  3418. ));
  3419. foreach ($result as $field) {
  3420. $form_info['elements'][$field->name] = check_plain($field->title);
  3421. }
  3422. }
  3423. /**
  3424. * @} End of "name mollom_profile".
  3425. */
  3426. /**
  3427. * @name mollom_action Actions module integration for Mollom.
  3428. * @{
  3429. */
  3430. /**
  3431. * Implements hook_action_info().
  3432. */
  3433. function mollom_action_info() {
  3434. return array(
  3435. // Unpublish comment action.
  3436. 'mollom_action_unpublish_comment' => array(
  3437. 'label' => t('Report comment to Mollom as spam and unpublish'),
  3438. 'type' => 'comment',
  3439. 'configurable' => FALSE,
  3440. 'triggers' => array(
  3441. 'comment_insert',
  3442. 'comment_update',
  3443. ),
  3444. 'aggregate' => TRUE,
  3445. ),
  3446. // Unpublish node action.
  3447. 'mollom_action_unpublish_node' => array(
  3448. 'label' => t('Report node to Mollom as spam and unpublish'),
  3449. 'type' => 'node',
  3450. 'configurable' => FALSE,
  3451. 'triggers' => array(
  3452. 'node_insert',
  3453. 'node_update',
  3454. ),
  3455. 'aggregate' => TRUE,
  3456. ),
  3457. );
  3458. }
  3459. /**
  3460. * Action callback to report a comment to mollom and unpublish.
  3461. */
  3462. function mollom_action_unpublish_comment($comments, $context = array()) {
  3463. _mollom_action_unpublish('comment', $comments);
  3464. }
  3465. /**
  3466. * Action callback to report a node to mollom and unpublish.
  3467. */
  3468. function mollom_action_unpublish_node($nodes, $context = array()) {
  3469. _mollom_action_unpublish('node', $nodes);
  3470. }
  3471. /**
  3472. * Unpublish content and report to Mollom as spam.
  3473. *
  3474. * @param $entity_type
  3475. * The type of entity; one of "comment" or "node".
  3476. * @param $entities
  3477. * An array of entities to perform the action upon.
  3478. */
  3479. function _mollom_action_unpublish($entity_type, $entities) {
  3480. // Make sure this is a supported entity type.
  3481. if (!in_array($entity_type, array('node', 'comment'))) {
  3482. watchdog('Mollom', 'Called unpublish action for an unsupported entity type: @type', array('@type' => $entity_type), WATCHDOG_ERROR);
  3483. return;
  3484. }
  3485. // Determine the entities for which moderation is allowed.
  3486. list($allowed, $nids, $cids) = _mollom_actions_access_callbacks($entity_type, $entities);
  3487. // Send feedback to Mollom.
  3488. $ids = $entity_type === 'comment' ? $cids : $nids;
  3489. mollom_data_report_multiple($entity_type, $ids, 'spam', 'moderate', "mollom_action_unpublish_{$entity_type}");
  3490. if ($entity_type === 'comment') {
  3491. // Unpublish the comment.
  3492. db_update("comment")
  3493. ->fields(array("status" => COMMENT_NOT_PUBLISHED))
  3494. ->condition("cid", $cids)
  3495. ->execute();
  3496. foreach ($nids as $nid) {
  3497. _comment_update_node_statistics($nid);
  3498. }
  3499. }
  3500. else if ($entity_type === 'node') {
  3501. // Unpublish the node.
  3502. db_update("node")
  3503. ->fields(array("status" => NODE_NOT_PUBLISHED))
  3504. ->condition("nid", $nids)
  3505. ->execute();
  3506. }
  3507. }
  3508. /**
  3509. * Gets all callbacks and checks permissions for entities.
  3510. *
  3511. * @param $entity_type
  3512. * The type of entity to check.
  3513. * @param $entities
  3514. * An array of entities to check.
  3515. *
  3516. * @return array
  3517. * An indexed array of allowed entities
  3518. * - 0 An array of allowed entities objects
  3519. * - 1 An array of node ids
  3520. * - 2 An array of comment ids (if entity_type is comment).
  3521. */
  3522. function _mollom_actions_access_callbacks($entity_type, $entities) {
  3523. $cids = array();
  3524. $nids = array();
  3525. // Retrieve any relevant callback for comments
  3526. $report_access_callbacks = array();
  3527. $access_permissions = array();
  3528. $entity_access_callbacks = array();
  3529. $allowed = array();
  3530. foreach (mollom_form_list() as $form_id => $info) {
  3531. if (!isset($info['entity']) || $info['entity'] != $entity_type) {
  3532. continue;
  3533. }
  3534. // If there is a 'report access callback' add it to the list.
  3535. if (isset($info['report access callback'])
  3536. && function_exists($info['report access callback'])
  3537. && !in_array($info['report access callback'], $report_access_callbacks)) {
  3538. $report_access_callbacks[] = $info['report access callback'];
  3539. }
  3540. // Otherwise add any access permissions.
  3541. else if (isset($info['report access']) && !in_array($info['report access'], $access_permissions)) {
  3542. $access_permissions[] = $info['report access'];
  3543. }
  3544. // Check for entity report access callbacks.
  3545. if (isset($info['entity report access callback'])
  3546. && function_exists($info['entity report access callback'])
  3547. && !in_array($info['entity report access callback'], $entity_access_callbacks)) {
  3548. $entity_access_callbacks[] = $info['entity report access callback'];
  3549. }
  3550. }
  3551. // Check access for this comment.
  3552. foreach ($entities as $entity) {
  3553. list($entity_id, $rev_id, $bundle) = entity_extract_ids($entity_type, $entity);
  3554. if ($entity_type === 'comment') {
  3555. $cids[$entity_id] = $entity_id;
  3556. $nids[$entity->nid] = $entity->nid;
  3557. }
  3558. else {
  3559. $nids[$entity_id] = $entity_id;
  3560. }
  3561. // Check reporting callbacks.
  3562. foreach($report_access_callbacks as $callback) {
  3563. if (!$callback($entity_type, $entity_id)) {
  3564. break;
  3565. }
  3566. }
  3567. // Check reporting user permissions.
  3568. foreach($access_permissions as $permission) {
  3569. if (!user_access($permission)) {
  3570. break;
  3571. }
  3572. }
  3573. // Check entity reporting callbacks.
  3574. foreach($report_access_callbacks as $callback) {
  3575. if (!$callback($entity)) {
  3576. break;
  3577. }
  3578. }
  3579. // If still here, then user has access to report this entity.
  3580. $allowed[] = $entity;
  3581. }
  3582. return array($allowed, $nids, $cids);
  3583. }
  3584. /**
  3585. * @} End of "name mollom_action".
  3586. */