akismet.js 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421
  1. jQuery( function ( $ ) {
  2. var mshotRemovalTimer = null;
  3. var mshotRetryTimer = null;
  4. var mshotTries = 0;
  5. var mshotRetryInterval = 1000;
  6. var mshotEnabledLinkSelector = 'a[id^="author_comment_url"], tr.pingback td.column-author a:first-of-type, td.comment p a';
  7. var preloadedMshotURLs = [];
  8. $('.akismet-status').each(function () {
  9. var thisId = $(this).attr('commentid');
  10. $(this).prependTo('#comment-' + thisId + ' .column-comment');
  11. });
  12. $('.akismet-user-comment-count').each(function () {
  13. var thisId = $(this).attr('commentid');
  14. $(this).insertAfter('#comment-' + thisId + ' .author strong:first').show();
  15. });
  16. akismet_enable_comment_author_url_removal();
  17. $( '#the-comment-list' ).on( 'click', '.akismet_remove_url', function () {
  18. var thisId = $(this).attr('commentid');
  19. var data = {
  20. action: 'comment_author_deurl',
  21. _wpnonce: WPAkismet.comment_author_url_nonce,
  22. id: thisId
  23. };
  24. $.ajax({
  25. url: ajaxurl,
  26. type: 'POST',
  27. data: data,
  28. beforeSend: function () {
  29. // Removes "x" link
  30. $("a[commentid='"+ thisId +"']").hide();
  31. // Show temp status
  32. $("#author_comment_url_"+ thisId).html( $( '<span/>' ).text( WPAkismet.strings['Removing...'] ) );
  33. },
  34. success: function (response) {
  35. if (response) {
  36. // Show status/undo link
  37. $("#author_comment_url_"+ thisId)
  38. .attr('cid', thisId)
  39. .addClass('akismet_undo_link_removal')
  40. .html(
  41. $( '<span/>' ).text( WPAkismet.strings['URL removed'] )
  42. )
  43. .append( ' ' )
  44. .append(
  45. $( '<span/>' )
  46. .text( WPAkismet.strings['(undo)'] )
  47. .addClass( 'akismet-span-link' )
  48. );
  49. }
  50. }
  51. });
  52. return false;
  53. }).on( 'click', '.akismet_undo_link_removal', function () {
  54. var thisId = $(this).attr('cid');
  55. var thisUrl = $(this).attr('href');
  56. var data = {
  57. action: 'comment_author_reurl',
  58. _wpnonce: WPAkismet.comment_author_url_nonce,
  59. id: thisId,
  60. url: thisUrl
  61. };
  62. $.ajax({
  63. url: ajaxurl,
  64. type: 'POST',
  65. data: data,
  66. beforeSend: function () {
  67. // Show temp status
  68. $("#author_comment_url_"+ thisId).html( $( '<span/>' ).text( WPAkismet.strings['Re-adding...'] ) );
  69. },
  70. success: function (response) {
  71. if (response) {
  72. // Add "x" link
  73. $("a[commentid='"+ thisId +"']").show();
  74. // Show link. Core strips leading http://, so let's do that too.
  75. $("#author_comment_url_"+ thisId).removeClass('akismet_undo_link_removal').text( thisUrl.replace( /^http:\/\/(www\.)?/ig, '' ) );
  76. }
  77. }
  78. });
  79. return false;
  80. });
  81. // Show a preview image of the hovered URL. Applies to author URLs and URLs inside the comments.
  82. if ( "enable_mshots" in WPAkismet && WPAkismet.enable_mshots ) {
  83. $( '#the-comment-list' ).on( 'mouseover', mshotEnabledLinkSelector, function () {
  84. clearTimeout( mshotRemovalTimer );
  85. if ( $( '.akismet-mshot' ).length > 0 ) {
  86. if ( $( '.akismet-mshot:first' ).data( 'link' ) == this ) {
  87. // The preview is already showing for this link.
  88. return;
  89. }
  90. else {
  91. // A new link is being hovered, so remove the old preview.
  92. $( '.akismet-mshot' ).remove();
  93. }
  94. }
  95. clearTimeout( mshotRetryTimer );
  96. var linkUrl = $( this ).attr( 'href' );
  97. if ( preloadedMshotURLs.indexOf( linkUrl ) !== -1 ) {
  98. // This preview image was already preloaded, so begin with a retry URL so the user doesn't see the placeholder image for the first second.
  99. mshotTries = 2;
  100. }
  101. else {
  102. mshotTries = 1;
  103. }
  104. var mShot = $( '<div class="akismet-mshot mshot-container"><div class="mshot-arrow"></div><img src="' + akismet_mshot_url( linkUrl, mshotTries ) + '" width="450" height="338" class="mshot-image" /></div>' );
  105. mShot.data( 'link', this );
  106. mShot.data( 'url', linkUrl );
  107. mShot.find( 'img' ).on( 'load', function () {
  108. $( '.akismet-mshot' ).data( 'pending-request', false );
  109. } );
  110. var offset = $( this ).offset();
  111. mShot.offset( {
  112. left : Math.min( $( window ).width() - 475, offset.left + $( this ).width() + 10 ), // Keep it on the screen if the link is near the edge of the window.
  113. top: offset.top + ( $( this ).height() / 2 ) - 101 // 101 = top offset of the arrow plus the top border thickness
  114. } );
  115. $( 'body' ).append( mShot );
  116. mshotRetryTimer = setTimeout( retryMshotUntilLoaded, mshotRetryInterval );
  117. } ).on( 'mouseout', 'a[id^="author_comment_url"], tr.pingback td.column-author a:first-of-type, td.comment p a', function () {
  118. mshotRemovalTimer = setTimeout( function () {
  119. clearTimeout( mshotRetryTimer );
  120. $( '.akismet-mshot' ).remove();
  121. }, 200 );
  122. } );
  123. var preloadDelayTimer = null;
  124. $( window ).on( 'scroll resize', function () {
  125. clearTimeout( preloadDelayTimer );
  126. preloadDelayTimer = setTimeout( preloadMshotsInViewport, 500 );
  127. } );
  128. preloadMshotsInViewport();
  129. }
  130. /**
  131. * The way mShots works is if there was no screenshot already recently generated for the URL,
  132. * it returns a "loading..." image for the first request. Then, some subsequent request will
  133. * receive the actual screenshot, but it's unknown how long it will take. So, what we do here
  134. * is continually re-request the mShot, waiting a second after every response until we get the
  135. * actual screenshot.
  136. */
  137. function retryMshotUntilLoaded() {
  138. clearTimeout( mshotRetryTimer );
  139. var imageWidth = $( '.akismet-mshot img' ).get(0).naturalWidth;
  140. if ( imageWidth == 0 ) {
  141. // It hasn't finished loading yet the first time. Check again shortly.
  142. setTimeout( retryMshotUntilLoaded, mshotRetryInterval );
  143. }
  144. else if ( imageWidth == 400 ) {
  145. // It loaded the preview image.
  146. if ( mshotTries == 20 ) {
  147. // Give up if we've requested the mShot 20 times already.
  148. return;
  149. }
  150. if ( ! $( '.akismet-mshot' ).data( 'pending-request' ) ) {
  151. $( '.akismet-mshot' ).data( 'pending-request', true );
  152. mshotTries++;
  153. $( '.akismet-mshot .mshot-image' ).attr( 'src', akismet_mshot_url( $( '.akismet-mshot' ).data( 'url' ), mshotTries ) );
  154. }
  155. mshotRetryTimer = setTimeout( retryMshotUntilLoaded, mshotRetryInterval );
  156. }
  157. else {
  158. // All done.
  159. }
  160. }
  161. function preloadMshotsInViewport() {
  162. var windowWidth = $( window ).width();
  163. var windowHeight = $( window ).height();
  164. $( '#the-comment-list' ).find( mshotEnabledLinkSelector ).each( function ( index, element ) {
  165. var linkUrl = $( this ).attr( 'href' );
  166. // Don't attempt to preload an mshot for a single link twice.
  167. if ( preloadedMshotURLs.indexOf( linkUrl ) !== -1 ) {
  168. // The URL is already preloaded.
  169. return true;
  170. }
  171. if ( typeof element.getBoundingClientRect !== 'function' ) {
  172. // The browser is too old. Return false to stop this preloading entirely.
  173. return false;
  174. }
  175. var rect = element.getBoundingClientRect();
  176. if ( rect.top >= 0 && rect.left >= 0 && rect.bottom <= windowHeight && rect.right <= windowWidth ) {
  177. akismet_preload_mshot( linkUrl );
  178. $( this ).data( 'akismet-mshot-preloaded', true );
  179. }
  180. } );
  181. }
  182. $( '.checkforspam.enable-on-load' ).on( 'click', function( e ) {
  183. if ( $( this ).hasClass( 'ajax-disabled' ) ) {
  184. // Akismet hasn't been configured yet. Allow the user to proceed to the button's link.
  185. return;
  186. }
  187. e.preventDefault();
  188. if ( $( this ).hasClass( 'button-disabled' ) ) {
  189. window.location.href = $( this ).data( 'success-url' ).replace( '__recheck_count__', 0 ).replace( '__spam_count__', 0 );
  190. return;
  191. }
  192. $('.checkforspam').addClass('button-disabled').addClass( 'checking' );
  193. $('.checkforspam-spinner').addClass( 'spinner' ).addClass( 'is-active' );
  194. akismet_check_for_spam(0, 100);
  195. }).removeClass( 'button-disabled' );
  196. var spam_count = 0;
  197. var recheck_count = 0;
  198. function akismet_check_for_spam(offset, limit) {
  199. var check_for_spam_buttons = $( '.checkforspam' );
  200. var nonce = check_for_spam_buttons.data( 'nonce' );
  201. // We show the percentage complete down to one decimal point so even queues with 100k
  202. // pending comments will show some progress pretty quickly.
  203. var percentage_complete = Math.round( ( recheck_count / check_for_spam_buttons.data( 'pending-comment-count' ) ) * 1000 ) / 10;
  204. // Update the progress counter on the "Check for Spam" button.
  205. $( '.checkforspam' ).text( check_for_spam_buttons.data( 'progress-label' ).replace( '%1$s', percentage_complete ) );
  206. $.post(
  207. ajaxurl,
  208. {
  209. 'action': 'akismet_recheck_queue',
  210. 'offset': offset,
  211. 'limit': limit,
  212. 'nonce': nonce
  213. },
  214. function(result) {
  215. if ( 'error' in result ) {
  216. // An error is only returned in the case of a missing nonce, so we don't need the actual error message.
  217. window.location.href = check_for_spam_buttons.data( 'failure-url' );
  218. return;
  219. }
  220. recheck_count += result.counts.processed;
  221. spam_count += result.counts.spam;
  222. if (result.counts.processed < limit) {
  223. window.location.href = check_for_spam_buttons.data( 'success-url' ).replace( '__recheck_count__', recheck_count ).replace( '__spam_count__', spam_count );
  224. }
  225. else {
  226. // Account for comments that were caught as spam and moved out of the queue.
  227. akismet_check_for_spam(offset + limit - result.counts.spam, limit);
  228. }
  229. }
  230. );
  231. }
  232. if ( "start_recheck" in WPAkismet && WPAkismet.start_recheck ) {
  233. $( '.checkforspam' ).click();
  234. }
  235. if ( typeof MutationObserver !== 'undefined' ) {
  236. // Dynamically add the "X" next the the author URL links when a comment is quick-edited.
  237. var comment_list_container = document.getElementById( 'the-comment-list' );
  238. if ( comment_list_container ) {
  239. var observer = new MutationObserver( function ( mutations ) {
  240. for ( var i = 0, _len = mutations.length; i < _len; i++ ) {
  241. if ( mutations[i].addedNodes.length > 0 ) {
  242. akismet_enable_comment_author_url_removal();
  243. // Once we know that we'll have to check for new author links, skip the rest of the mutations.
  244. break;
  245. }
  246. }
  247. } );
  248. observer.observe( comment_list_container, { attributes: true, childList: true, characterData: true } );
  249. }
  250. }
  251. function akismet_enable_comment_author_url_removal() {
  252. $( '#the-comment-list' )
  253. .find( 'tr.comment, tr[id ^= "comment-"]' )
  254. .find( '.column-author a[href^="http"]:first' ) // Ignore mailto: links, which would be the comment author's email.
  255. .each(function () {
  256. if ( $( this ).parent().find( '.akismet_remove_url' ).length > 0 ) {
  257. return;
  258. }
  259. var linkHref = $(this).attr( 'href' );
  260. // Ignore any links to the current domain, which are diagnostic tools, like the IP address link
  261. // or any other links another plugin might add.
  262. var currentHostParts = document.location.href.split( '/' );
  263. var currentHost = currentHostParts[0] + '//' + currentHostParts[2] + '/';
  264. if ( linkHref.indexOf( currentHost ) != 0 ) {
  265. var thisCommentId = $(this).parents('tr:first').attr('id').split("-");
  266. $(this)
  267. .attr("id", "author_comment_url_"+ thisCommentId[1])
  268. .after(
  269. $( '<a href="#" class="akismet_remove_url">x</a>' )
  270. .attr( 'commentid', thisCommentId[1] )
  271. .attr( 'title', WPAkismet.strings['Remove this URL'] )
  272. );
  273. }
  274. });
  275. }
  276. /**
  277. * Generate an mShot URL if given a link URL.
  278. *
  279. * @param string linkUrl
  280. * @param int retry If retrying a request, the number of the retry.
  281. * @return string The mShot URL;
  282. */
  283. function akismet_mshot_url( linkUrl, retry ) {
  284. var mshotUrl = '//s0.wp.com/mshots/v1/' + encodeURIComponent( linkUrl ) + '?w=900';
  285. if ( retry > 1 ) {
  286. mshotUrl += '&r=' + encodeURIComponent( retry );
  287. }
  288. mshotUrl += '&source=akismet';
  289. return mshotUrl;
  290. }
  291. /**
  292. * Begin loading an mShot preview of a link.
  293. *
  294. * @param string linkUrl
  295. */
  296. function akismet_preload_mshot( linkUrl ) {
  297. var img = new Image();
  298. img.src = akismet_mshot_url( linkUrl );
  299. preloadedMshotURLs.push( linkUrl );
  300. }
  301. $( '.akismet-could-be-primary' ).each( function () {
  302. var form = $( this ).closest( 'form' );
  303. form.data( 'initial-state', form.serialize() );
  304. form.on( 'change keyup', function () {
  305. var self = $( this );
  306. var submit_button = self.find( '.akismet-could-be-primary' );
  307. if ( self.serialize() != self.data( 'initial-state' ) ) {
  308. submit_button.addClass( 'akismet-is-primary' );
  309. }
  310. else {
  311. submit_button.removeClass( 'akismet-is-primary' );
  312. }
  313. } );
  314. } );
  315. /**
  316. * Shows the Enter API key form
  317. */
  318. $( '.akismet-enter-api-key-box__reveal' ).on( 'click', function ( e ) {
  319. e.preventDefault();
  320. var div = $( '.akismet-enter-api-key-box__form-wrapper' );
  321. div.show( 500 );
  322. div.find( 'input[name=key]' ).focus();
  323. $( this ).hide();
  324. } );
  325. /**
  326. * Hides the Connect with Jetpack form | Shows the Activate Akismet Account form
  327. */
  328. $( 'a.toggle-ak-connect' ).on( 'click', function ( e ) {
  329. e.preventDefault();
  330. $( '.akismet-ak-connect' ).slideToggle('slow');
  331. $( 'a.toggle-ak-connect' ).hide();
  332. $( '.akismet-jp-connect' ).hide();
  333. $( 'a.toggle-jp-connect' ).show();
  334. } );
  335. /**
  336. * Shows the Connect with Jetpack form | Hides the Activate Akismet Account form
  337. */
  338. $( 'a.toggle-jp-connect' ).on( 'click', function ( e ) {
  339. e.preventDefault();
  340. $( '.akismet-jp-connect' ).slideToggle('slow');
  341. $( 'a.toggle-jp-connect' ).hide();
  342. $( '.akismet-ak-connect' ).hide();
  343. $( 'a.toggle-ak-connect' ).show();
  344. } );
  345. });