permanent-anchors.pl 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300
  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. return '';
  72. }
  73. return;
  74. }
  75. sub GetPermanentAnchor {
  76. my $id = FreeToNormal(shift);
  77. my $text = NormalToFree($id);
  78. my ($class, $resolved, $title, $exists) = ResolveId($id);
  79. if ($class eq 'alias' and $title ne $OpenPageName) {
  80. return '[' . Ts('anchor first defined here: %s',
  81. ScriptLink(UrlEncode($resolved), $text, 'alias')) . ']';
  82. } elsif ($PermanentAnchors{$id} ne $OpenPageName
  83. # 10 tries, 3 second wait, die on error
  84. and RequestLockDir('permanentanchors', 10, 3, 1)) {
  85. # Somebody may have added a permanent anchor in the mean time.
  86. # Comparing $LastUpdate to the $IndexFile mtime does not work for
  87. # subsecond changes and updates are rare, so just reread the file!
  88. PermanentAnchorsInit();
  89. $PermanentAnchors{$id} = $OpenPageName;
  90. WritePermanentAnchors();
  91. ReleaseLockDir('permanentanchors');
  92. }
  93. $PagePermanentAnchors{$id} = 1; # add to the list of anchors in page
  94. my $html = GetSearchLink($id, 'definition', $id,
  95. T('Click to search for references to this permanent anchor'));
  96. $html .= ' [' . Ts('the page %s also exists',
  97. ScriptLink("action=browse;anchor=0;id="
  98. . UrlEncode($id), NormalToFree($id), 'local'))
  99. . ']' if $exists;
  100. return $html;
  101. }
  102. =head2 Storage
  103. Permanent anchor definitions need to be stored in a separate file.
  104. Otherwise linking to a permanent anchor would require a search of the
  105. entire page database. The permanent anchors are stored in a file
  106. called C<permanentanchors> in the data directory. The location can be
  107. changed by setting C<$PermanentAnchorsFile>.
  108. The format of the file is simple: permanent anchor names and the name
  109. of the page they are defined on follow each other, separated by
  110. whitespace. Spaces within permanent anchor names and page names are
  111. replaced with underlines, as always. Thus, the keys of
  112. C<%PermanentAnchors> is the name of the permanent anchor, and
  113. C<$PermanentAnchors{$name}> is the name of the page it is defined on.
  114. =cut
  115. push(@MyInitVariables, \&PermanentAnchorsInit);
  116. sub PermanentAnchorsInit {
  117. %PagePermanentAnchors = %PermanentAnchors = ();
  118. my ($status, $data) = ReadFile($PermanentAnchorsFile);
  119. return unless $status; # not fatal
  120. # $FS was used in 1.417 and earlier!
  121. %PermanentAnchors = split(/\n| |$FS/,$data);
  122. }
  123. sub WritePermanentAnchors {
  124. my $data = '';
  125. foreach my $name (keys %PermanentAnchors) {
  126. $data .= $name . ' ' . $PermanentAnchors{$name} ."\n";
  127. }
  128. WriteStringToFile($PermanentAnchorsFile, $data);
  129. }
  130. =head2 Deleting Anchors
  131. When deleting a page Oddmuse needs to delete the corresponding
  132. permanent anchors from its file. This is why the
  133. C<DeletePermanentAnchors> function is called from C<DeletePage>.
  134. When a page is edited, we want to make sure that Oddmuse deletes the
  135. permanent anchors no longer needed from its file. The safest way to do
  136. this is to delete all permanent anchors defined on the page being
  137. edited and redefine them when it is rendered for the first time. This
  138. is achieved by calling C<DeletePermanentAnchors> from C<Save>. After
  139. hitting the save button, the user is automatically redirected to the
  140. new page. This will render the page, and redefine all permanent
  141. anchors.
  142. =cut
  143. *OldPermanentAnchorsDeletePage = \&DeletePage;
  144. *DeletePage = \&NewPermanentAnchorsDeletePage;
  145. sub NewPermanentAnchorsDeletePage {
  146. my $status = OldPermanentAnchorsDeletePage(@_);
  147. return $status if $status; # this would be the error message
  148. DeletePermanentAnchors(@_); # the only parameter is $id
  149. return ''; # no errors
  150. }
  151. *OldPermanentAnchorsSave = \&Save;
  152. *Save = \&NewPermanentAnchorsSave;
  153. sub NewPermanentAnchorsSave {
  154. OldPermanentAnchorsSave(@_);
  155. DeletePermanentAnchors(@_); # the first parameter is $id
  156. }
  157. sub DeletePermanentAnchors {
  158. my $id = shift;
  159. # 10 tries, 3 second wait, die on error
  160. RequestLockDir('permanentanchors', 10, 3, 1);
  161. foreach (keys %PermanentAnchors) {
  162. if ($PermanentAnchors{$_} eq $id and !$PagePermanentAnchors{$_}) {
  163. delete($PermanentAnchors{$_}) ;
  164. }
  165. }
  166. WritePermanentAnchors();
  167. ReleaseLockDir('permanentanchors');
  168. }
  169. =head2 Name Resolution
  170. Name resolution is done by C<ResolveId>. This function returns a list
  171. of several items: The CSS class to use, the resolved id, the title
  172. (eg. for popups), and a boolean saying whether the page actually
  173. exists or not. When resolving a permanent anchor, the CSS class used
  174. will be “alias”, the resolved id will be the C<pagename#anchorname>,
  175. the title will be the page name.
  176. You can override this behaviour by providing the parameter
  177. C<anchor=0>. This is used for the link in the warning message “the
  178. page foo also exists.”
  179. =cut
  180. *OldPermanentAnchorsResolveId = \&ResolveId;
  181. *ResolveId = \&NewPermanentAnchorsResolveId;
  182. sub NewPermanentAnchorsResolveId {
  183. my $id = shift;
  184. my $page = $PermanentAnchors{$id};
  185. if (GetParam('anchor', 1) and $page and $page ne $id) {
  186. return ('alias', $page . '#' . $id, $page, $IndexHash{$id})
  187. } else {
  188. return OldPermanentAnchorsResolveId($id, @_);
  189. }
  190. }
  191. =head2 Anchor Objects
  192. An anchor object is the text that starts after the anchor definition
  193. and goes up to the next heading, horizontal line, or the end of the
  194. page. By redefining C<GetPageContent> to work on anchor objects we
  195. automatically allow internal transclusion.
  196. =cut
  197. *OldPermanentAnchorsGetPageContent = \&GetPageContent;
  198. *GetPageContent = \&NewPermanentAnchorsGetPageContent;
  199. sub NewPermanentAnchorsGetPageContent {
  200. my $id = shift;
  201. my $result = OldPermanentAnchorsGetPageContent($id);
  202. if (not $result and $PermanentAnchors{$id}) {
  203. $result = OldPermanentAnchorsGetPageContent($PermanentAnchors{$id});
  204. $result =~ s/^(.*\n)*.*\[::$id\]// or return '';
  205. $result =~ s/(\n=|\n----|\[::$FreeLinkPattern\])(.*\n)*.*$//;
  206. }
  207. return $result;
  208. }
  209. =head2 User Interface Changes
  210. Some user interface changes are required as well.
  211. =over
  212. =item *
  213. Allow the page index to list permanent anchors or not by setting
  214. C<@IndexOptions>. Note that we need to delay setting this option until we're
  215. sure that translations have loaded correctly, which is why we're setting
  216. C<@IndexOptions> as part of running C<@MyInitVariables>.
  217. =cut
  218. push(@MyInitVariables, sub {
  219. push(@IndexOptions, ['permanentanchors', T('Include permanent anchors'),
  220. 1, sub { keys %PermanentAnchors }])});
  221. =item *
  222. Make sure that you can view old revisions of pages that have a
  223. permanent anchor of the same name. This requires link munging for all
  224. browse links from C<GetHistoryLine>.
  225. =back
  226. =cut
  227. *OldPermanentAnchorsGetHistoryLine = \&GetHistoryLine;
  228. *GetHistoryLine = \&NewPermanentAnchorsGetHistoryLine;
  229. sub NewPermanentAnchorsGetHistoryLine {
  230. my $id = shift;
  231. my $html = OldPermanentAnchorsGetHistoryLine($id, @_);
  232. if ($PermanentAnchors{$id}) {
  233. my $encoded_id = UrlEncode($id);
  234. # link to the current revision; ignore dependence on $UsePathInfo
  235. $html =~ s!$ScriptName[/?]$encoded_id!$ScriptName?action=browse;anchor=0;id=$encoded_id!;
  236. # link to old revisions
  237. $html =~ s!action=browse;id=$encoded_id!action=browse;anchor=0;id=$encoded_id!g;
  238. }
  239. return $html;
  240. }