permanent-anchors.pl 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295
  1. # Copyright (C) 2003, 2004, 2005, 2006, 2007 Alex Schroeder <alex@gnu.org>
  2. #
  3. # This program is free software; you can redistribute it and/or modify
  4. # it under the terms of the GNU General Public License as published by
  5. # the Free Software Foundation; either version 3 of the License, or
  6. # (at your option) any later version.
  7. #
  8. # This program is distributed in the hope that it will be useful,
  9. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  10. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  11. # GNU General Public License for more details.
  12. #
  13. # You should have received a copy of the GNU General Public License
  14. # along with this program. If not, see <http://www.gnu.org/licenses/>.
  15. use strict;
  16. use v5.10;
  17. AddModuleDescription('permanent-anchors.pl', 'Permanent Anchors');
  18. our ($q, $OpenPageName, %IndexHash, $DataDir, $ScriptName, @MyRules, @MyInitVariables, $FS, $FreeLinkPattern, @IndexOptions);
  19. =encoding utf8
  20. =head1 Permanent Anchors
  21. This module allows you to create link targets within a page. These
  22. link targets are called named anchors in HTML. The anchors provided by
  23. this module are permanent, because moving the anchor from one page to
  24. another does not affect the links pointing to it. You link to these
  25. named anchors as if they were pagenames. For users, it makes no
  26. difference.
  27. =cut
  28. our (%PermanentAnchors, %PagePermanentAnchors, $PermanentAnchorsFile);
  29. $PermanentAnchorsFile = "$DataDir/permanentanchors";
  30. =head2 Definition
  31. Permanent anchors are defined by using square brackets and a double
  32. colon, like this: C<[::Example]>.
  33. If you define a permanent anchor that already exists, the new
  34. definition will have no effect. Instead you will be shown a link to
  35. the existing permanent anchor so that you can easily resolve the
  36. conflict.
  37. If you define a permanent anchor and a page of the same name already
  38. exists, the definition will work, and all links will point to the
  39. permanent anchor. You will also be given a link to the existing page
  40. so that you can easily resolve the conflict (eg. by deleting the
  41. page). Note that if you mark the page for deletion, you will still
  42. have to wait for page expiry to kick in and actually delete the page
  43. before the message disappears.
  44. During anchor definition a lock is created in the temporary directory.
  45. If Oddmuse encounters a lock while defining a permanent anchor, it
  46. will wait a few seconds and try again. If the lock cannot be obtained,
  47. the definition fails. The unlock action available from the
  48. administration page allows you to remove any stale locks once you're
  49. sure the locks have been left behind by a crash. After having removed
  50. the stale lock, edit the page with the permanent anchor definition
  51. again.
  52. When linking to a permanent anchor on the same page, you'll notice
  53. that this only works flawlessly if the definition comes first. When
  54. rendering a page, permanent anchor definitions and links are parsed in
  55. order. Thus, if the link comes first, the permanent anchor definition
  56. is not yet available. Once you invalidate the HTML cache (by editing
  57. another page or by removing the C<pageidx> file from the data
  58. directory), this situation will have fixed itself.
  59. =cut
  60. push(@MyRules, \&PermanentAnchorsRule);
  61. sub PermanentAnchorsRule {
  62. my ($locallinks, $withanchors) = @_;
  63. if (m/\G(\[::$FreeLinkPattern\])/cg) {
  64. #[::Free Link] permanent anchor create only $withanchors
  65. Dirty($1);
  66. if ($withanchors) {
  67. print GetPermanentAnchor($2);
  68. } else {
  69. print $q->span({-class=>'permanentanchor'}, $2);
  70. }
  71. }
  72. return;
  73. }
  74. sub GetPermanentAnchor {
  75. my $id = FreeToNormal(shift);
  76. my $text = NormalToFree($id);
  77. my ($class, $resolved, $title, $exists) = ResolveId($id);
  78. if ($class eq 'alias' and $title ne $OpenPageName) {
  79. return '[' . Ts('anchor first defined here: %s',
  80. ScriptLink(UrlEncode($resolved), $text, 'alias')) . ']';
  81. } elsif ($PermanentAnchors{$id} ne $OpenPageName
  82. # 10 tries, 3 second wait, die on error
  83. and RequestLockDir('permanentanchors', 10, 3, 1)) {
  84. # Somebody may have added a permanent anchor in the mean time.
  85. # Comparing $LastUpdate to the $IndexFile mtime does not work for
  86. # subsecond changes and updates are rare, so just reread the file!
  87. PermanentAnchorsInit();
  88. $PermanentAnchors{$id} = $OpenPageName;
  89. WritePermanentAnchors();
  90. ReleaseLockDir('permanentanchors');
  91. }
  92. $PagePermanentAnchors{$id} = 1; # add to the list of anchors in page
  93. my $html = GetSearchLink($id, 'definition', $id,
  94. T('Click to search for references to this permanent anchor'));
  95. $html .= ' [' . Ts('the page %s also exists',
  96. ScriptLink("action=browse;anchor=0;id="
  97. . UrlEncode($id), NormalToFree($id), 'local'))
  98. . ']' if $exists;
  99. return $html;
  100. }
  101. =head2 Storage
  102. Permanent anchor definitions need to be stored in a separate file.
  103. Otherwise linking to a permanent anchor would require a search of the
  104. entire page database. The permanent anchors are stored in a file
  105. called C<permanentanchors> in the data directory. The location can be
  106. changed by setting C<$PermanentAnchorsFile>.
  107. The format of the file is simple: permanent anchor names and the name
  108. of the page they are defined on follow each other, separated by
  109. whitespace. Spaces within permanent anchor names and page names are
  110. replaced with underlines, as always. Thus, the keys of
  111. C<%PermanentAnchors> is the name of the permanent anchor, and
  112. C<$PermanentAnchors{$name}> is the name of the page it is defined on.
  113. =cut
  114. push(@MyInitVariables, \&PermanentAnchorsInit);
  115. sub PermanentAnchorsInit {
  116. %PagePermanentAnchors = %PermanentAnchors = ();
  117. my ($status, $data) = ReadFile($PermanentAnchorsFile);
  118. return unless $status; # not fatal
  119. # $FS was used in 1.417 and earlier!
  120. %PermanentAnchors = split(/\n| |$FS/,$data);
  121. }
  122. sub WritePermanentAnchors {
  123. my $data = '';
  124. foreach my $name (keys %PermanentAnchors) {
  125. $data .= $name . ' ' . $PermanentAnchors{$name} ."\n";
  126. }
  127. WriteStringToFile($PermanentAnchorsFile, $data);
  128. }
  129. =head2 Deleting Anchors
  130. When deleting a page Oddmuse needs to delete the corresponding
  131. permanent anchors from its file. This is why the
  132. C<DeletePermanentAnchors> function is called from C<DeletePage>.
  133. When a page is edited, we want to make sure that Oddmuse deletes the
  134. permanent anchors no longer needed from its file. The safest way to do
  135. this is to delete all permanent anchors defined on the page being
  136. edited and redefine them when it is rendered for the first time. This
  137. is achieved by calling C<DeletePermanentAnchors> from C<Save>. After
  138. hitting the save button, the user is automatically redirected to the
  139. new page. This will render the page, and redefine all permanent
  140. anchors.
  141. =cut
  142. *OldPermanentAnchorsDeletePage = \&DeletePage;
  143. *DeletePage = \&NewPermanentAnchorsDeletePage;
  144. sub NewPermanentAnchorsDeletePage {
  145. my $status = OldPermanentAnchorsDeletePage(@_);
  146. return $status if $status; # this would be the error message
  147. DeletePermanentAnchors(@_); # the only parameter is $id
  148. }
  149. *OldPermanentAnchorsSave = \&Save;
  150. *Save = \&NewPermanentAnchorsSave;
  151. sub NewPermanentAnchorsSave {
  152. OldPermanentAnchorsSave(@_);
  153. DeletePermanentAnchors(@_); # the first parameter is $id
  154. }
  155. sub DeletePermanentAnchors {
  156. my $id = shift;
  157. # 10 tries, 3 second wait, die on error
  158. RequestLockDir('permanentanchors', 10, 3, 1);
  159. foreach (keys %PermanentAnchors) {
  160. if ($PermanentAnchors{$_} eq $id and !$PagePermanentAnchors{$_}) {
  161. delete($PermanentAnchors{$_}) ;
  162. }
  163. }
  164. WritePermanentAnchors();
  165. ReleaseLockDir('permanentanchors');
  166. }
  167. =head2 Name Resolution
  168. Name resolution is done by C<ResolveId>. This function returns a list
  169. of several items: The CSS class to use, the resolved id, the title
  170. (eg. for popups), and a boolean saying whether the page actually
  171. exists or not. When resolving a permanent anchor, the CSS class used
  172. will be “alias”, the resolved id will be the C<pagename#anchorname>,
  173. the title will be the page name.
  174. You can override this behaviour by providing the parameter
  175. C<anchor=0>. This is used for the link in the warning message “the
  176. page foo also exists.”
  177. =cut
  178. *OldPermanentAnchorsResolveId = \&ResolveId;
  179. *ResolveId = \&NewPermanentAnchorsResolveId;
  180. sub NewPermanentAnchorsResolveId {
  181. my $id = shift;
  182. my $page = $PermanentAnchors{$id};
  183. if (GetParam('anchor', 1) and $page and $page ne $id) {
  184. return ('alias', $page . '#' . $id, $page, $IndexHash{$id})
  185. } else {
  186. return OldPermanentAnchorsResolveId($id, @_);
  187. }
  188. }
  189. =head2 Anchor Objects
  190. An anchor object is the text that starts after the anchor definition
  191. and goes up to the next heading, horizontal line, or the end of the
  192. page. By redefining C<GetPageContent> to work on anchor objects we
  193. automatically allow internal transclusion.
  194. =cut
  195. *OldPermanentAnchorsGetPageContent = \&GetPageContent;
  196. *GetPageContent = \&NewPermanentAnchorsGetPageContent;
  197. sub NewPermanentAnchorsGetPageContent {
  198. my $id = shift;
  199. my $result = OldPermanentAnchorsGetPageContent($id);
  200. if (not $result and $PermanentAnchors{$id}) {
  201. $result = OldPermanentAnchorsGetPageContent($PermanentAnchors{$id});
  202. $result =~ s/^(.*\n)*.*\[::$id\]// or return '';
  203. $result =~ s/(\n=|\n----|\[::$FreeLinkPattern\])(.*\n)*.*$//;
  204. }
  205. return $result;
  206. }
  207. =head2 User Interface Changes
  208. Some user interface changes are required as well.
  209. =over
  210. =item *
  211. Allow the page index to list permanent anchors or not by setting
  212. C<@IndexOptions>.
  213. =cut
  214. push(@IndexOptions, ['permanentanchors', T('Include permanent anchors'),
  215. 1, sub { keys %PermanentAnchors }]);
  216. =item *
  217. Make sure that you can view old revisions of pages that have a
  218. permanent anchor of the same name. This requires link munging for all
  219. browse links from C<GetHistoryLine>.
  220. =back
  221. =cut
  222. *OldPermanentAnchorsGetHistoryLine = \&GetHistoryLine;
  223. *GetHistoryLine = \&NewPermanentAnchorsGetHistoryLine;
  224. sub NewPermanentAnchorsGetHistoryLine {
  225. my $id = shift;
  226. my $html = OldPermanentAnchorsGetHistoryLine($id, @_);
  227. if ($PermanentAnchors{$id}) {
  228. my $encoded_id = UrlEncode($id);
  229. # link to the current revision; ignore dependence on $UsePathInfo
  230. $html =~ s!$ScriptName[/?]$encoded_id!$ScriptName?action=browse;anchor=0;id=$encoded_id!;
  231. # link to old revisions
  232. $html =~ s!action=browse;id=$encoded_id!action=browse;anchor=0;id=$encoded_id!g;
  233. }
  234. return $html;
  235. }