1. <?php
  2. /*
  3. Plugin Name: Page Links To
  4. Plugin URI: http://txfx.net/wordpress-plugins/page-links-to/
  5. Description: Allows you to point WordPress pages or posts to a URL of your choosing. Good for setting up navigational links to non-WP sections of your site or to off-site resources.
  6. Version: 2.9.9
  7. Author: Mark Jaquith
  8. Author URI: http://coveredwebservices.com/
  9. Text Domain: page-links-to
  10. Domain Path: /languages
  11. */
  12. /* Copyright 2005-2017 Mark Jaquith
  13. This program is free software; you can redistribute it and/or modify
  14. it under the terms of the GNU General Public License as published by
  15. the Free Software Foundation; either version 2 of the License, or
  16. (at your option) any later version.
  17. This program is distributed in the hope that it will be useful,
  18. but WITHOUT ANY WARRANTY; without even the implied warranty of
  19. MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  20. GNU General Public License for more details.
  21. You should have received a copy of the GNU General Public License
  22. along with this program; if not, write to the Free Software
  23. Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
  24. */
  25. // Pull in WP Stack plugin library
  26. include( dirname( __FILE__ ) . '/lib/wp-stack-plugin.php' );
  27. class CWS_PageLinksTo extends WP_Stack_Plugin {
  28. static $instance;
  29. const LINKS_CACHE_KEY = 'plt_cache__links';
  30. const TARGETS_CACHE_KEY = 'plt_cache__targets';
  31. const LINK_META_KEY = '_links_to';
  32. const TARGET_META_KEY = '_links_to_target';
  33. const VERSION_KEY = 'txfx_plt_schema_version';
  34. const FILE = __FILE__;
  35. const CSS_JS_VERSION = '2.9.8';
  36. function __construct() {
  37. self::$instance = $this;
  38. $this->hook( 'init' );
  39. }
  40. /**
  41. * Bootstraps the upgrade process and registers all the hooks.
  42. */
  43. function init() {
  44. // Check to see if any of our data needs to be upgraded
  45. $this->maybe_upgrade();
  46. // Load translation files
  47. load_plugin_textdomain( 'page-links-to', false, basename( dirname( self::FILE ) ) . '/languages' );
  48. // Register hooks
  49. $this->register_hooks();
  50. }
  51. /**
  52. * Registers all the hooks
  53. */
  54. function register_hooks() {
  55. // Hook in to URL generation
  56. $this->hook( 'page_link', 'link', 20 );
  57. $this->hook( 'post_link', 'link', 20 );
  58. $this->hook( 'post_type_link', 'link', 20 );
  59. $this->hook( 'attachment_link', 'link', 20 );
  60. // Non-standard priority hooks
  61. $this->hook( 'do_meta_boxes', 20 );
  62. // $this->hook( 'wp_enqueue_scripts', 'start_buffer', -9999 );
  63. $this->hook( 'wp_enqueue_scripts' );
  64. // $this->hook( 'wp_enqueue_scripts', 'jquery_protection', 9999 );
  65. // $this->hook( 'wp_head', 'end_buffer', 9999 );
  66. // Non-standard callback hooks
  67. $this->hook( 'load-post.php', 'load_post' );
  68. $this->hook( 'wp_default_scripts', 'log_jquery' );
  69. // Standard hooks
  70. $this->hook( 'wp_list_pages' );
  71. $this->hook( 'template_redirect' );
  72. $this->hook( 'save_post' );
  73. $this->hook( 'edit_attachment' );
  74. $this->hook( 'wp_nav_menu_objects' );
  75. $this->hook( 'plugin_row_meta' );
  76. // Metadata validation grants users editing privileges for our custom fields
  77. register_meta( 'post', self::LINK_META_KEY, null, '__return_true' );
  78. register_meta( 'post', self::TARGET_META_KEY, null, '__return_true' );
  79. }
  80. /**
  81. * Performs an upgrade for older versions
  82. *
  83. * * Version 3: Underscores the keys so they only show in the plugin's UI.
  84. */
  85. function maybe_upgrade() {
  86. // In earlier versions, the meta keys were stored without a leading underscore.
  87. // Since then, underscore has been codified as the standard for "something manages this" post meta.
  88. if ( get_option( self::VERSION_KEY ) < 3 ) {
  89. global $wpdb;
  90. $total_affected = 0;
  91. foreach ( array( '', '_target', '_type' ) as $meta_key ) {
  92. $meta_key = 'links_to' . $meta_key;
  93. $affected = $wpdb->update( $wpdb->postmeta, array( 'meta_key' => '_' . $meta_key ), compact( 'meta_key' ) );
  94. if ( $affected ) {
  95. $total_affected += $affected;
  96. }
  97. }
  98. // Only flush the cache if something changed
  99. if ( $total_affected > 0 ) {
  100. wp_cache_flush();
  101. }
  102. if ( update_option( self::VERSION_KEY, 3 ) ) {
  103. $this->flush_links_cache();
  104. $this->flush_targets_cache();
  105. }
  106. }
  107. }
  108. /**
  109. * Logs data about core's jQuery
  110. */
  111. public function log_jquery( $wp_scripts ) {
  112. $this->jquery = $wp_scripts->registered['jquery'];
  113. $this->jquery_core = $wp_scripts->registered['jquery-core'];
  114. }
  115. /**
  116. * Prevents jQuery from being incorrectly overwritten
  117. */
  118. public function jquery_protection() {
  119. global $wp_scripts;
  120. if ( $wp_scripts->registered['jquery-core'] !== $this->jquery_core ) {
  121. $wp_scripts->registered['jquery-core'] = $this->jquery_core;
  122. }
  123. if ( $wp_scripts->registered['jquery'] !== $this->jquery ) {
  124. $wp_scripts->registered['jquery'] = $this->jquery;
  125. }
  126. }
  127. /**
  128. * Enqueues front end scripts
  129. */
  130. function wp_enqueue_scripts() {
  131. wp_enqueue_script( 'page-links-to', $this->get_url() . 'js/new-tab.min.js', array( 'jquery' ), self::CSS_JS_VERSION, true );
  132. }
  133. /**
  134. * Starts a buffer, for rescuing the jQuery object
  135. */
  136. function start_buffer() {
  137. ob_start( array( $this, 'buffer_callback' ) );
  138. }
  139. /**
  140. * Collects the buffer, and injects a `jQueryWP` JS object as a
  141. * copy of `jQuery`, so that dumb themes and plugins can't hurt it
  142. *
  143. * @return string the modified buffer
  144. */
  145. function buffer_callback( $content ) {
  146. $pattern = "#wp-includes/js/jquery/jquery\.js\?ver=([^']+)'></script>#";
  147. if ( preg_match( $pattern, $content ) ) {
  148. $content = preg_replace( $pattern, '$0<script>jQueryWP = jQuery;</script>', $content );
  149. }
  150. return $content;
  151. }
  152. /**
  153. * Flushes the buffer
  154. */
  155. function end_buffer() {
  156. ob_end_flush();
  157. }
  158. /**
  159. * Returns post ids and meta values that have a given key
  160. *
  161. * @param string $key post meta key
  162. * @return array|false objects with post_id and meta_value properties
  163. */
  164. function meta_by_key( $key ) {
  165. global $wpdb;
  166. return $wpdb->get_results( $wpdb->prepare( "SELECT post_id, meta_value FROM $wpdb->postmeta WHERE meta_key = %s", $key ) );
  167. }
  168. /**
  169. * Returns a single piece of post meta
  170. * @param integer $post_id a post ID
  171. * @param string $key a post meta key
  172. * @return string|false the post meta, or false, if it doesn't exist
  173. */
  174. function get_post_meta( $post_id, $key ) {
  175. $meta = get_post_meta( absint( $post_id ), $key, true );
  176. if ( '' === $meta ) {
  177. return false;
  178. }
  179. return $meta;
  180. }
  181. /**
  182. * Returns all links for the current site
  183. *
  184. * @return array an array of links, keyed by post ID
  185. */
  186. function get_links() {
  187. if ( false === $links = get_transient( self::LINKS_CACHE_KEY ) ) {
  188. $db_links = $this->meta_by_key( self::LINK_META_KEY );
  189. $links = array();
  190. if ( $db_links ) {
  191. foreach ( $db_links as $link ) {
  192. $links[ intval( $link->post_id ) ] = $link->meta_value;
  193. }
  194. }
  195. set_transient( self::LINKS_CACHE_KEY, $links, 10 * 60 );
  196. }
  197. return $links;
  198. }
  199. /**
  200. * Returns the link for the specified post ID
  201. *
  202. * @param integer $post_id a post ID
  203. * @return mixed either a URL or false
  204. */
  205. function get_link( $post_id ) {
  206. return $this->get_post_meta( $post_id, self::LINK_META_KEY );
  207. }
  208. /**
  209. * Returns all targets for the current site
  210. *
  211. * @return array an array of targets, keyed by post ID
  212. */
  213. function get_targets() {
  214. if ( false === $targets = get_transient( self::TARGETS_CACHE_KEY ) ) {
  215. $db_targets = $this->meta_by_key( self::TARGET_META_KEY );
  216. $targets = array();
  217. if ( $db_targets ) {
  218. foreach ( $db_targets as $target ) {
  219. $targets[ intval( $target->post_id ) ] = true;
  220. }
  221. }
  222. set_transient( self::TARGETS_CACHE_KEY, $targets, 10 * 60 );
  223. }
  224. return $targets;
  225. }
  226. /**
  227. * Returns the _blank target status for the specified post ID
  228. *
  229. * @param integer $post_id a post ID
  230. * @return bool whether it should open in a new tab
  231. */
  232. function get_target( $post_id ) {
  233. return (bool) $this->get_post_meta( $post_id, self::TARGET_META_KEY );
  234. }
  235. /**
  236. * Adds the meta box to the post or page edit screen
  237. *
  238. * @param string $page the name of the current page
  239. * @param string $context the current context
  240. */
  241. function do_meta_boxes( $page, $context ) {
  242. // Plugins that use custom post types can use this filter to hide the
  243. // PLT UI in their post type.
  244. $plt_post_types = apply_filters( 'page-links-to-post-types', array_keys( get_post_types( array('show_ui' => true ) ) ) );
  245. if ( in_array( $page, $plt_post_types ) && 'advanced' === $context ) {
  246. add_meta_box( 'page-links-to', _x( 'Page Links To', 'Meta box title', 'page-links-to'), array( $this, 'meta_box' ), $page, 'advanced', 'low' );
  247. }
  248. }
  249. /**
  250. * Outputs the Page Links To post screen meta box
  251. */
  252. function meta_box() {
  253. $null = null;
  254. $post = get_post( $null );
  255. echo '<p>';
  256. wp_nonce_field( 'cws_plt_' . $post->ID, '_cws_plt_nonce', false, true );
  257. echo '</p>';
  258. $url = $this->get_link( $post->ID );
  259. if ( ! $url ) {
  260. $linked = false;
  261. $url = 'http://';
  262. } else {
  263. $linked = true;
  264. }
  265. ?>
  266. <p><?php _e( 'Point this content to:', 'page-links-to' ); ?></p>
  267. <p><label><input type="radio" id="cws-links-to-choose-wp" name="cws_links_to_choice" value="wp" <?php checked( !$linked ); ?> /> <?php _e( 'Its normal WordPress URL', 'page-links-to' ); ?></label></p>
  268. <p><label><input type="radio" id="cws-links-to-choose-custom" name="cws_links_to_choice" value="custom" <?php checked( $linked ); ?> /> <?php _e( 'A custom URL', 'page-links-to' ); ?></label></p>
  269. <div style="webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box;margin-left: 30px;" id="cws-links-to-custom-section" class="<?php echo ! $linked ? 'hide-if-js' : ''; ?>">
  270. <p><input name="cws_links_to" type="text" style="width:75%" id="cws-links-to" value="<?php echo esc_attr( $url ); ?>" /></p>
  271. <p><label for="cws-links-to-new-tab"><input type="checkbox" name="cws_links_to_new_tab" id="cws-links-to-new-tab" value="_blank" <?php checked( (bool) $this->get_target( $post->ID ) ); ?>> <?php _e( 'Open this link in a new tab', 'page-links-to' ); ?></label></p>
  272. </div>
  273. <script src="<?php echo $this->get_url() . 'js/page-links-to.min.js?v=' . self::CSS_JS_VERSION; ?>"></script>
  274. <?php
  275. }
  276. /**
  277. * Saves data on attachment save
  278. *
  279. * @param int $post_id
  280. * @return int the attachment post ID that was passed in
  281. */
  282. function edit_attachment( $post_id ) {
  283. return $this->save_post( $post_id );
  284. }
  285. /**
  286. * Saves data on post save
  287. *
  288. * @param int $post_id a post ID
  289. * @return int the post ID that was passed in
  290. */
  291. function save_post( $post_id ) {
  292. if ( isset( $_REQUEST['_cws_plt_nonce'] ) && wp_verify_nonce( $_REQUEST['_cws_plt_nonce'], 'cws_plt_' . $post_id ) ) {
  293. if ( ( ! isset( $_POST['cws_links_to_choice'] ) || 'custom' == $_POST['cws_links_to_choice'] ) && isset( $_POST['cws_links_to'] ) && strlen( $_POST['cws_links_to'] ) > 0 && $_POST['cws_links_to'] !== 'http://' ) {
  294. $url = $this->clean_url( stripslashes( $_POST['cws_links_to'] ) );
  295. $this->flush_links_if( $this->set_link( $post_id, $url ) );
  296. if ( isset( $_POST['cws_links_to_new_tab'] ) )
  297. $this->flush_targets_if( $this->set_link_new_tab( $post_id ) );
  298. else
  299. $this->flush_targets_if( $this->set_link_same_tab( $post_id ) );
  300. } else {
  301. $this->flush_links_if( $this->delete_link( $post_id ) );
  302. }
  303. }
  304. return $post_id;
  305. }
  306. /**
  307. * Cleans up a URL
  308. *
  309. * @param string $url URL
  310. * @return string cleaned up URL
  311. */
  312. function clean_url( $url ) {
  313. $url = trim( $url );
  314. // Starts with 'www.'. Probably a mistake. So add 'http://'.
  315. if ( 0 === strpos( $url, 'www.' ) ) {
  316. $url = 'http://' . $url;
  317. }
  318. return $url;
  319. }
  320. /**
  321. * Have a post point to a custom URL
  322. *
  323. * @param int $post_id post ID
  324. * @param string $url the URL to point the post to
  325. * @return bool whether anything changed
  326. */
  327. function set_link( $post_id, $url ) {
  328. return $this->flush_links_if( (bool) update_post_meta( $post_id, self::LINK_META_KEY, $url ) );
  329. }
  330. /**
  331. * Tell an custom URL post to open in a new tab
  332. *
  333. * @param int $post_id post ID
  334. * @return bool whether anything changed
  335. */
  336. function set_link_new_tab( $post_id ) {
  337. return $this->flush_targets_if( (bool) update_post_meta( $post_id, self::TARGET_META_KEY, '_blank' ) );
  338. }
  339. /**
  340. * Tell an custom URL post to open in the same tab
  341. *
  342. * @param int $post_id post ID
  343. * @return bool whether anything changed
  344. */
  345. function set_link_same_tab( $post_id ) {
  346. return $this->flush_targets_if( delete_post_meta( $post_id, self::TARGET_META_KEY ) );
  347. }
  348. /**
  349. * Discard a custom URL and point a post to its normal URL
  350. *
  351. * @param int $post_id post ID
  352. * @return bool whether the link was deleted
  353. */
  354. function delete_link( $post_id ) {
  355. $return = $this->flush_links_if( delete_post_meta( $post_id, self::LINK_META_KEY ) );
  356. $this->flush_targets_if( delete_post_meta( $post_id, self::TARGET_META_KEY ) );
  357. // Old, unused data that we can delete on the fly
  358. delete_post_meta( $post_id, '_links_to_type' );
  359. return $return;
  360. }
  361. /**
  362. * Flushes the links transient cache if the condition is true
  363. *
  364. * @param bool $condition whether to proceed with the flush
  365. * @return bool whether the flush happened
  366. */
  367. function flush_links_if( $condition ) {
  368. if ( $condition ) {
  369. $this->flush_links_cache();
  370. return true;
  371. } else {
  372. return false;
  373. }
  374. }
  375. /**
  376. * Flushes the targets transient cache if the condition is true
  377. *
  378. * @param bool $condition whether to proceed with the flush
  379. * @return bool whether the flush happened
  380. */
  381. function flush_targets_if( $condition ) {
  382. if ( $condition ) {
  383. $this->flush_targets_cache();
  384. return true;
  385. } else {
  386. return false;
  387. }
  388. }
  389. /**
  390. * Flushes the links transient cache
  391. *
  392. * @param bool $condition whether to flush the cache
  393. * @param string $type which cache to flush
  394. * @return bool whether the flush attempt occurred
  395. */
  396. function flush_links_cache() {
  397. delete_transient( self::LINKS_CACHE_KEY );
  398. }
  399. /**
  400. * Flushes the targets transient cache
  401. *
  402. * @param bool $condition whether to flush the cache
  403. * @param string $type which cache to flush
  404. * @return bool whether the flush attempt occurred
  405. */
  406. function flush_targets_cache() {
  407. delete_transient( self::TARGETS_CACHE_KEY );
  408. }
  409. /**
  410. * Filter for Post links
  411. *
  412. * @param string $link the URL for the post or page
  413. * @param int|WP_Post $post post ID or object
  414. * @return string output URL
  415. */
  416. function link( $link, $post ) {
  417. $post = get_post( $post );
  418. $meta_link = $this->get_link( $post->ID );
  419. if ( $meta_link ) {
  420. $link = esc_url( $meta_link );
  421. if ( ! is_admin() && $this->get_target( $post->ID ) ) {
  422. $link .= '#new_tab';
  423. }
  424. }
  425. return $link;
  426. }
  427. /**
  428. * Performs a redirect
  429. */
  430. function template_redirect() {
  431. $link = $this->get_redirect();
  432. if ( $link ) {
  433. wp_redirect( $link, 301 );
  434. exit;
  435. }
  436. }
  437. /**
  438. * gets the redirection URL
  439. *
  440. * @return string|bool the redirection URL, or false
  441. */
  442. function get_redirect() {
  443. if ( ! is_singular() || ! get_queried_object_id() ) {
  444. return false;
  445. }
  446. $link = $this->get_link( get_queried_object_id() );
  447. // Convert server- and protocol-relative URLs to absolute URLs
  448. if ( "/" === $link[0] ) {
  449. // Protocol-relative
  450. if ( "/" === $link[1] ) {
  451. $link = set_url_scheme( 'http:' . $link );
  452. } else {
  453. // Host-relative
  454. $link = set_url_scheme( 'http://' . $_SERVER["HTTP_HOST"] . $link );
  455. }
  456. }
  457. if ( 'mailto' !== parse_url( $link, PHP_URL_SCHEME ) ) {
  458. $link = str_replace( '@', '%40', $link );
  459. }
  460. return $link;
  461. }
  462. /**
  463. * Filters the list of pages to alter the links and targets
  464. *
  465. * @param string $pages the wp_list_pages() HTML block from WordPress
  466. * @return string the modified HTML block
  467. */
  468. function wp_list_pages( $pages ) {
  469. $highlight = false;
  470. // We use the "fetch all" versions here, because the pages might not be queried here
  471. $links = $this->get_links();
  472. $targets = $this->get_targets();
  473. $targets_by_url = array();
  474. foreach( array_keys( $targets ) as $targeted_id )
  475. $targets_by_url[$links[$targeted_id]] = true;
  476. if ( ! $links ) {
  477. return $pages;
  478. }
  479. $this_url = ( is_ssl() ? 'https' : 'http' ) . '://' . $_SERVER['HTTP_HOST'] . $_SERVER['REQUEST_URI'];
  480. foreach ( (array) $links as $id => $page ) {
  481. if ( isset( $targets_by_url[$page] ) ) {
  482. $page .= '#new_tab';
  483. }
  484. if ( str_replace( 'http://www.', 'http://', $this_url ) === str_replace( 'http://www.', 'http://', $page ) || ( is_home() && str_replace( 'http://www.', 'http://', trailingslashit( get_bloginfo( 'url' ) ) ) === str_replace( 'http://www.', 'http://', trailingslashit( $page ) ) ) ) {
  485. $highlight = true;
  486. $current_page = esc_url( $page );
  487. }
  488. }
  489. if ( count( $targets_by_url ) ) {
  490. foreach ( array_keys( $targets_by_url ) as $p ) {
  491. $p = esc_url( $p . '#new_tab' );
  492. $pages = str_replace( '<a href="' . $p . '"', '<a href="' . $p . '" target="_blank"', $pages );
  493. }
  494. }
  495. if ( $highlight ) {
  496. $pages = preg_replace( '| class="([^"]+)current_page_item"|', ' class="$1"', $pages ); // Kill default highlighting
  497. $pages = preg_replace( '|<li class="([^"]+)"><a href="' . preg_quote( $current_page ) . '"|', '<li class="$1 current_page_item"><a href="' . $current_page . '"', $pages );
  498. }
  499. return $pages;
  500. }
  501. /**
  502. * Filters nav menu objects and adds target=_blank to the ones that need it
  503. *
  504. * @param array $items nav menu items
  505. * @return array modified nav menu items
  506. */
  507. function wp_nav_menu_objects( $items ) {
  508. $new_items = array();
  509. foreach ( $items as $item ) {
  510. if ( isset( $item->object_id ) && $this->get_target( $item->object_id ) ) {
  511. $item->target = '_blank';
  512. }
  513. $new_items[] = $item;
  514. }
  515. return $new_items;
  516. }
  517. /**
  518. * Hooks in as a post is being loaded for editing and conditionally adds a notice
  519. */
  520. function load_post() {
  521. if ( isset( $_GET['post'] ) && $this->get_link( (int) $_GET['post'] ) ) {
  522. $this->hook( 'admin_notices', 'notify_of_external_link' );
  523. }
  524. }
  525. /**
  526. * Outputs a notice that the current post item is pointed to a custom URL
  527. */
  528. function notify_of_external_link() {
  529. ?><div class="updated"><p><?php _e( '<strong>Note</strong>: This content is pointing to a custom URL. Use the &#8220;Page Links To&#8221; box to change this behavior.', 'page-links-to' ); ?></p></div><?php
  530. }
  531. /**
  532. * Adds a GitHub link to the plugin meta
  533. *
  534. * @param array $links the current array of links
  535. * @param string $file the current plugin being processed
  536. * @return array the modified array of links
  537. */
  538. function plugin_row_meta( $links, $file ) {
  539. if ( $file === plugin_basename( self::FILE ) ) {
  540. return array_merge(
  541. $links,
  542. array( '<a href="https://github.com/markjaquith/page-links-to" target="_blank">GitHub</a>' )
  543. );
  544. } else {
  545. return $links;
  546. }
  547. }
  548. /**
  549. * Returns the URL of this plugin's directory
  550. *
  551. * @return string this plugin's directory URL
  552. */
  553. public function get_url() {
  554. return plugin_dir_url( self::FILE );
  555. }
  556. /**
  557. * Returns the filesystem path of this plugin's directory
  558. *
  559. * @return string this plugin's directory filesystem path
  560. */
  561. public function get_path() {
  562. return plugin_dir_path( self::FILE );
  563. }
  564. }
  565. // Bootstrap everything
  566. new CWS_PageLinksTo;