Skip to content
Permalink

Comparing changes

Choose two branches to see what’s changed or to start a new pull request. If you need to, you can also or learn more about diff comparisons.

Open a pull request

Create a new pull request by comparing changes across two branches. If you need to, you can also . Learn more about diff comparisons here.
base repository: desolat/DokuWiki-Pagemove-Plugin
Failed to load repositories. Confirm that selected base ref is valid, then try again.
Loading
base: master
Choose a base ref
...
head repository: michitux/dokuwiki-plugin-move
Failed to load repositories. Confirm that selected head ref is valid, then try again.
Loading
compare: master
Choose a head ref
Able to merge. These branches can be automatically merged.

Commits on Nov 1, 2012

  1. Add new link rewriting code, some code cleanup

    This includes:
    - test cases adapted for PHPUnit
    - use metadata search index for finding backlinks
    - new code for rewriting links, media references and plugin syntax
    - backlinks aren't corrected immediately anymore but instead a queue is
      filled with all moves that need to be considered
    michitux committed Nov 1, 2012
    Copy the full SHA
    4e15b85 View commit details

Commits on Dec 29, 2012

  1. Only rewrite pages that are not currently edited

    Pages that are currently edited will be rewritten after they were saved.
    michitux committed Dec 29, 2012
    Copy the full SHA
    45ec917 View commit details
  2. Use plugin.info.txt information in getInfo()

    There is still a getInfo()-function as it provides a localized
    description text.
    michitux committed Dec 29, 2012
    Copy the full SHA
    ab183b6 View commit details
  3. Copy the full SHA
    7b5f66e View commit details
  4. Copy the full SHA
    46e6e72 View commit details
  5. Copy the full SHA
    cb47e1e View commit details
  6. New user interface and handler for changed backend

    This updates the (English) user interface because of the changed
    functionality of the backend.
    michitux committed Dec 29, 2012
    Copy the full SHA
    77e946e View commit details
  7. Copy the full SHA
    c17ae02 View commit details
  8. Move page/namespace move functions into the helper

    This separates the UI (admin.php) and the functionality (helper.php) so
    the main functions of the pagemove plugin can also be used by other
    plugins or a possible ajax backend (for moving big namespaces).
    michitux committed Dec 29, 2012
    Copy the full SHA
    f970892 View commit details

Commits on Dec 30, 2012

  1. Cleanup: rename function, remove useless code, update documentation

    Namespace cleanup isn't needed, DokuWiki tries to clean the namespace
    when the page is deleted.
    michitux committed Dec 30, 2012
    Copy the full SHA
    38d04ae View commit details
  2. Copy the full SHA
    1eefdb1 View commit details
  3. Remove unused defines

    michitux committed Dec 30, 2012
    Copy the full SHA
    89da326 View commit details
  4. Copy the full SHA
    7363e4a View commit details
  5. Copy the full SHA
    493bca2 View commit details
  6. Copy the full SHA
    947ab47 View commit details
  7. Copy the full SHA
    45dd5ba View commit details
  8. Copy the full SHA
    c6239d0 View commit details
  9. Copy the full SHA
    b4ce499 View commit details
  10. Copy the full SHA
    542edaa View commit details

Commits on Jan 1, 2013

  1. Update the search index while moving pages, assume the index is correct

    This simplifies the search for links because now it can be assumed that
    the search index is always correct and contains even link updates that
    haven't been executed yet. The problem before this change was that the
    search index became unusable after a namespace move.
    michitux committed Jan 1, 2013
    Copy the full SHA
    1666890 View commit details
  2. Reconstruct missing id in IO_WIKIPAGE_READ and PARSER_CACHE_USE events

    When p_cached_instructions() is called without the $id parameter the id
    is also missing in the IO_WIKIPAGE_READ and PARSER_CACHE_USE events.
    However in both events the file path is supplied and using pathID() the
    path can be used for getting the id.
    michitux committed Jan 1, 2013
    Copy the full SHA
    63f4c24 View commit details
  3. Copy the full SHA
    259aea9 View commit details
  4. Copy the full SHA
    2167aea View commit details

Commits on Jan 19, 2013

  1. Copy the full SHA
    51892c2 View commit details

Commits on Jan 25, 2013

  1. Updated translations

    Removed no longer used language strings, corrected the names of some
    language files and updated the language strings where possible. Some
    lines were replaced by the English version or commented out. The Chinese
    pagemove.txt help file was unreadable, I didn't find the correct
    encoding options in order to fix it so I deleted it.
    michitux committed Jan 25, 2013
    Copy the full SHA
    e940890 View commit details
  2. Update indexer code, use core code if possible

    The indexer page/meta key rename code will be merged in DokuWiki and
    thus part of the spring 2013 release. If possible this code is used by
    the plugin. If this is not possible a copy of exactly this code is used.
    michitux committed Jan 25, 2013
    Copy the full SHA
    50d5c59 View commit details

Commits on Mar 29, 2013

  1. Copy the full SHA
    b4f6d8f View commit details
  2. Copy the full SHA
    38b4334 View commit details

Commits on Mar 30, 2013

  1. Copy the full SHA
    654addc View commit details
  2. Implement single media file moving (currently only in the helper)

    The media link correction code is still missing
    michitux committed Mar 30, 2013
    Copy the full SHA
    197c480 View commit details
  3. Copy the full SHA
    e1bff2e View commit details
  4. Add new namespace move functionality including ajax

    Namespace moves are now split into junks of at maximum 10 pages
    michitux committed Mar 30, 2013
    Copy the full SHA
    dbd7319 View commit details
  5. Copy the full SHA
    f4e4c17 View commit details
  6. Copy the full SHA
    f102d50 View commit details
  7. Copy the full SHA
    34b1e38 View commit details

Commits on Mar 31, 2013

  1. Copy the full SHA
    e1d9483 View commit details
  2. Copy the full SHA
    68f1b01 View commit details
  3. Copy the full SHA
    e4d6c6a View commit details
  4. Copy the full SHA
    b57a8d4 View commit details
  5. Avoid reading and writing the whole media/page list for each move

    This makes sure that only the necessary parts of the media/page list are
    read and the file is efficiently truncated. In order to calculate the
    number of remaining items this number is stored and updated in the
    options.
    michitux committed Mar 31, 2013
    Copy the full SHA
    cb27415 View commit details
  6. New release 2013-04-01

    michitux committed Mar 31, 2013
    Copy the full SHA
    229822d View commit details

Commits on Oct 29, 2013

  1. Copy the full SHA
    4f5f2d3 View commit details
  2. Fix the prevention of automatic edits for pages that are currently lo…

    …cked or edited
    
    This makes the lock check more strict by reporting pages as locked that
    are locked by the current user in order to prevent accidently moving or
    modifying pages that are currently edited.
    michitux committed Oct 29, 2013
    Copy the full SHA
    ec7eb75 View commit details
  3. Use the new media usage index from DokuWiki core if available

    Note that you need to rebuild the search index after this change in
    order to get correct link updates after media moves, this should happen
    automatically for visited pages. The index that is created in the
    pagemove plugin is now identical to the one that is created in DokuWiki
    core from the Autumn 2013 release on so you won't need to manually
    rebuild the index after updating DokuWiki.
    michitux committed Oct 29, 2013
    Copy the full SHA
    fa5ce2c View commit details
  4. New version 2013-10-29 released

    Changes:
      * Added option to select between media and page namespace moves
      * Use the media usage index that was introduced in DokuWiki itself
      * Better lock/editing detection avoids changing pages while they are
        edited
    michitux committed Oct 29, 2013
    Copy the full SHA
    6b35682 View commit details

Commits on Nov 20, 2013

  1. Copy the full SHA
    96457cc View commit details
  2. Copy the full SHA
    689122a View commit details
  3. Copy the full SHA
    6d7bd5c View commit details
  4. Renamed the pagemove plugin to "move"

    Note that all class and event names that contained the name "pagemove"
    have been renamed to the same name without "page".
    michitux committed Nov 20, 2013
    Copy the full SHA
    bcaa37b View commit details

Commits on Nov 24, 2013

  1. Remove the "pm_" prefix from all language string identifiers

    The prefix was still a leftover from the old pagemove plugin and isn't
    needed at all.
    michitux committed Nov 24, 2013
    Copy the full SHA
    d778d3f View commit details
Showing with 8,746 additions and 1,622 deletions.
  1. +3 −0 .gitattributes
  2. +11 −0 .github/workflows/dokuwiki.yml
  3. +24 −0 .github/workflows/release.yml
  4. +339 −0 LICENSE
  5. +35 −0 MenuItem.php
  6. +53 −1 README
  7. +84 −0 _test/GeneralTest.php
  8. +58 −0 _test/affectedPagesNs.test.php
  9. +47 −0 _test/cache_handling.test.php
  10. +113 −0 _test/findMissingDocuments.test.php
  11. +75 −0 _test/handler.test.php
  12. +46 −0 _test/log.test.php
  13. +124 −0 _test/mediamove.test.php
  14. +478 −0 _test/namespace_move.test.php
  15. +0 −10 _test/pagemove.group.php
  16. +210 −128 _test/pagemove.test.php
  17. +110 −0 _test/plan.test.php
  18. +274 −0 _test/stepThroughDocuments.test.php
  19. +49 −0 _test/tpl.test.php
  20. +66 −0 action/progress.php
  21. +221 −0 action/rename.php
  22. +112 −0 action/rewrite.php
  23. +63 −0 action/tree.php
  24. +0 −965 admin.php
  25. +1 −0 admin.svg
  26. +270 −0 admin/main.php
  27. +189 −0 admin/tree.php
  28. +7 −0 conf/default.php
  29. +7 −0 conf/metadata.php
  30. +34 −0 deleted.files
  31. +156 −0 helper/file.php
  32. +387 −0 helper/handler.php
  33. +323 −0 helper/op.php
  34. +953 −0 helper/plan.php
  35. +297 −0 helper/rewrite.php
  36. BIN images/disk.png
  37. BIN images/folder_add.png
  38. BIN images/page.png
  39. BIN images/page_link.png
  40. BIN images/rename.png
  41. +1 −0 images/rename.svg
  42. BIN images/sprite.png
  43. +10 −0 lang/ar/lang.php
  44. +67 −0 lang/cs/lang.php
  45. +0 −41 lang/cs/lang.php.txt
  46. +4 −5 lang/cs/{pagemove.txt.txt → move.txt}
  47. +3 −0 lang/cs/progress.txt
  48. +13 −0 lang/cs/settings.php
  49. +3 −0 lang/cs/tree.txt
  50. +65 −0 lang/da/lang.php
  51. +10 −0 lang/da/move.txt
  52. +3 −0 lang/da/progress.txt
  53. +12 −0 lang/da/settings.php
  54. +7 −0 lang/da/tree.txt
  55. +71 −0 lang/de-informal/lang.php
  56. +11 −0 lang/de-informal/move.txt
  57. +3 −0 lang/de-informal/progress.txt
  58. +12 −0 lang/de-informal/settings.php
  59. +3 −0 lang/de-informal/tree.txt
  60. +68 −43 lang/de/lang.php
  61. +11 −0 lang/de/move.txt
  62. +0 −14 lang/de/pagemove.txt
  63. +3 −0 lang/de/progress.txt
  64. +13 −0 lang/de/settings.php
  65. +3 −0 lang/de/tree.txt
  66. +65 −0 lang/el/lang.php
  67. +9 −0 lang/el/move.txt
  68. +2 −0 lang/el/progress.txt
  69. +12 −0 lang/el/settings.php
  70. +2 −0 lang/el/tree.txt
  71. +81 −41 lang/en/lang.php
  72. +10 −0 lang/en/move.txt
  73. +0 −13 lang/en/pagemove.txt
  74. +3 −0 lang/en/progress.txt
  75. +7 −0 lang/en/settings.php
  76. +7 −0 lang/en/tree.txt
  77. +66 −0 lang/es/lang.php
  78. +0 −42 lang/es/lang.php.txt
  79. +10 −0 lang/es/move.txt
  80. +0 −13 lang/es/pagemove.txt.txt
  81. +3 −0 lang/es/progress.txt
  82. +12 −0 lang/es/settings.php
  83. +3 −0 lang/es/tree.txt
  84. +67 −36 lang/fr/lang.php
  85. +10 −0 lang/fr/move.txt
  86. +0 −13 lang/fr/pagemove.txt
  87. +3 −0 lang/fr/progress.txt
  88. +12 −0 lang/fr/settings.php
  89. +3 −0 lang/fr/tree.txt
  90. +65 −0 lang/hr/lang.php
  91. +10 −0 lang/hr/move.txt
  92. +3 −0 lang/hr/progress.txt
  93. +13 −0 lang/hr/settings.php
  94. +4 −0 lang/hr/tree.txt
  95. +62 −0 lang/id/lang.php
  96. +3 −0 lang/id/progress.txt
  97. +12 −0 lang/id/settings.php
  98. +7 −0 lang/id/tree.txt
  99. +38 −0 lang/it/lang.php
  100. +10 −0 lang/it/move.txt
  101. +65 −0 lang/ja/lang.php
  102. +10 −0 lang/ja/move.txt
  103. +3 −0 lang/ja/progress.txt
  104. +12 −0 lang/ja/settings.php
  105. +3 −0 lang/ja/tree.txt
  106. +67 −0 lang/ko/lang.php
  107. +10 −0 lang/ko/move.txt
  108. +3 −0 lang/ko/progress.txt
  109. +13 −0 lang/ko/settings.php
  110. +3 −0 lang/ko/tree.txt
  111. +16 −24 lang/lv/lang.php
  112. +3 −6 lang/lv/{pagemove.txt → move.txt}
  113. +64 −35 lang/nl/lang.php
  114. +10 −0 lang/nl/move.txt
  115. +0 −9 lang/nl/pagemove.txt
  116. +3 −0 lang/nl/progress.txt
  117. +13 −0 lang/nl/settings.php
  118. +3 −0 lang/nl/tree.txt
  119. +69 −0 lang/no/lang.php
  120. +10 −0 lang/no/move.txt
  121. +3 −0 lang/no/progress.txt
  122. +13 −0 lang/no/settings.php
  123. +3 −0 lang/no/tree.txt
  124. +39 −0 lang/pl/lang.php
  125. +0 −43 lang/pl/lang.php.txt
  126. +3 −6 lang/pl/{pagemove.txt.txt → move.txt}
  127. +66 −0 lang/pt-br/lang.php
  128. +9 −0 lang/pt-br/move.txt
  129. +2 −0 lang/pt-br/progress.txt
  130. +12 −0 lang/pt-br/settings.php
  131. +6 −0 lang/pt-br/tree.txt
  132. +64 −34 lang/ru/lang.php
  133. +10 −0 lang/ru/move.txt
  134. +0 −13 lang/ru/pagemove.txt
  135. +3 −0 lang/ru/progress.txt
  136. +12 −0 lang/ru/settings.php
  137. +3 −0 lang/ru/tree.txt
  138. +72 −0 lang/sk/lang.php
  139. +11 −0 lang/sk/move.txt
  140. +3 −0 lang/sk/progress.txt
  141. +11 −0 lang/sk/settings.php
  142. +8 −0 lang/sk/tree.txt
  143. +17 −25 lang/sl/lang.php
  144. +3 −6 lang/sl/{pagemove.txt → move.txt}
  145. +51 −0 lang/sv/lang.php
  146. +10 −0 lang/sv/move.txt
  147. +3 −0 lang/sv/progress.txt
  148. +9 −0 lang/sv/settings.php
  149. +36 −0 lang/tr/lang.php
  150. +3 −0 lang/tr/progress.txt
  151. +46 −0 lang/uk/lang.php
  152. +65 −0 lang/vi/lang.php
  153. +10 −0 lang/vi/move.txt
  154. +3 −0 lang/vi/progress.txt
  155. +12 −0 lang/vi/settings.php
  156. +7 −0 lang/vi/tree.txt
  157. +66 −0 lang/zh-tw/lang.php
  158. +11 −0 lang/zh-tw/move.txt
  159. +3 −0 lang/zh-tw/progress.txt
  160. +13 −0 lang/zh-tw/settings.php
  161. +3 −0 lang/zh-tw/tree.txt
  162. +69 −35 lang/zh/lang.php
  163. +8 −0 lang/zh/move.txt
  164. +0 −14 lang/zh/pagemove.txt
  165. +3 −0 lang/zh/progress.txt
  166. +13 −0 lang/zh/settings.php
  167. +3 −0 lang/zh/tree.txt
  168. +7 −7 plugin.info.txt
  169. +15 −0 script.js
  170. +174 −0 script/MoveMediaManager.js
  171. +14 −0 script/form.js
  172. +486 −0 script/json2.js
  173. +98 −0 script/progress.js
  174. +129 −0 script/rename.js
  175. +260 −0 script/tree.js
  176. +134 −0 style.less
3 changes: 3 additions & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
/.gitignore export-ignore
/.gitattributes export-ignore
/.travis.yml export-ignore
11 changes: 11 additions & 0 deletions .github/workflows/dokuwiki.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
name: DokuWiki Default Tasks
on:
push:
pull_request:
schedule:
- cron: '1 18 5 * *'


jobs:
all:
uses: dokuwiki/github-action/.github/workflows/all.yml@main
24 changes: 24 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# Create release on change to plugin.info.txt version line
# https://github.com/dokuwiki/dokuwiki/issues/3951
#
# Requires DOKUWIKI_USER and DOKUWIKI_PASS secrets be set in GitHub Actions

name: Release

on:
push:
branches:
- master
paths:
- "*.info.txt"

jobs:
release:
name: Release
# https://github.com/dokuwiki/dokuwiki/pull/3966
uses: glensc/dokuwiki/.github/workflows/plugin-release.yml@39431875f734bddc35cc6b4a899bbfdec97e8aba
secrets:
DOKUWIKI_USER: ${{ secrets.DOKUWIKI_USER }}
DOKUWIKI_PASS: ${{ secrets.DOKUWIKI_PASS }}

# vim:ts=2:sw=2:et
339 changes: 339 additions & 0 deletions LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,339 @@
GNU GENERAL PUBLIC LICENSE
Version 2, June 1991

Copyright (C) 1989, 1991 Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.

Preamble

The licenses for most software are designed to take away your
freedom to share and change it. By contrast, the GNU General Public
License is intended to guarantee your freedom to share and change free
software--to make sure the software is free for all its users. This
General Public License applies to most of the Free Software
Foundation's software and to any other program whose authors commit to
using it. (Some other Free Software Foundation software is covered by
the GNU Lesser General Public License instead.) You can apply it to
your programs, too.

When we speak of free software, we are referring to freedom, not
price. Our General Public Licenses are designed to make sure that you
have the freedom to distribute copies of free software (and charge for
this service if you wish), that you receive source code or can get it
if you want it, that you can change the software or use pieces of it
in new free programs; and that you know you can do these things.

To protect your rights, we need to make restrictions that forbid
anyone to deny you these rights or to ask you to surrender the rights.
These restrictions translate to certain responsibilities for you if you
distribute copies of the software, or if you modify it.

For example, if you distribute copies of such a program, whether
gratis or for a fee, you must give the recipients all the rights that
you have. You must make sure that they, too, receive or can get the
source code. And you must show them these terms so they know their
rights.

We protect your rights with two steps: (1) copyright the software, and
(2) offer you this license which gives you legal permission to copy,
distribute and/or modify the software.

Also, for each author's protection and ours, we want to make certain
that everyone understands that there is no warranty for this free
software. If the software is modified by someone else and passed on, we
want its recipients to know that what they have is not the original, so
that any problems introduced by others will not reflect on the original
authors' reputations.

Finally, any free program is threatened constantly by software
patents. We wish to avoid the danger that redistributors of a free
program will individually obtain patent licenses, in effect making the
program proprietary. To prevent this, we have made it clear that any
patent must be licensed for everyone's free use or not licensed at all.

The precise terms and conditions for copying, distribution and
modification follow.

GNU GENERAL PUBLIC LICENSE
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION

0. This License applies to any program or other work which contains
a notice placed by the copyright holder saying it may be distributed
under the terms of this General Public License. The "Program", below,
refers to any such program or work, and a "work based on the Program"
means either the Program or any derivative work under copyright law:
that is to say, a work containing the Program or a portion of it,
either verbatim or with modifications and/or translated into another
language. (Hereinafter, translation is included without limitation in
the term "modification".) Each licensee is addressed as "you".

Activities other than copying, distribution and modification are not
covered by this License; they are outside its scope. The act of
running the Program is not restricted, and the output from the Program
is covered only if its contents constitute a work based on the
Program (independent of having been made by running the Program).
Whether that is true depends on what the Program does.

1. You may copy and distribute verbatim copies of the Program's
source code as you receive it, in any medium, provided that you
conspicuously and appropriately publish on each copy an appropriate
copyright notice and disclaimer of warranty; keep intact all the
notices that refer to this License and to the absence of any warranty;
and give any other recipients of the Program a copy of this License
along with the Program.

You may charge a fee for the physical act of transferring a copy, and
you may at your option offer warranty protection in exchange for a fee.

2. You may modify your copy or copies of the Program or any portion
of it, thus forming a work based on the Program, and copy and
distribute such modifications or work under the terms of Section 1
above, provided that you also meet all of these conditions:

a) You must cause the modified files to carry prominent notices
stating that you changed the files and the date of any change.

b) You must cause any work that you distribute or publish, that in
whole or in part contains or is derived from the Program or any
part thereof, to be licensed as a whole at no charge to all third
parties under the terms of this License.

c) If the modified program normally reads commands interactively
when run, you must cause it, when started running for such
interactive use in the most ordinary way, to print or display an
announcement including an appropriate copyright notice and a
notice that there is no warranty (or else, saying that you provide
a warranty) and that users may redistribute the program under
these conditions, and telling the user how to view a copy of this
License. (Exception: if the Program itself is interactive but
does not normally print such an announcement, your work based on
the Program is not required to print an announcement.)

These requirements apply to the modified work as a whole. If
identifiable sections of that work are not derived from the Program,
and can be reasonably considered independent and separate works in
themselves, then this License, and its terms, do not apply to those
sections when you distribute them as separate works. But when you
distribute the same sections as part of a whole which is a work based
on the Program, the distribution of the whole must be on the terms of
this License, whose permissions for other licensees extend to the
entire whole, and thus to each and every part regardless of who wrote it.

Thus, it is not the intent of this section to claim rights or contest
your rights to work written entirely by you; rather, the intent is to
exercise the right to control the distribution of derivative or
collective works based on the Program.

In addition, mere aggregation of another work not based on the Program
with the Program (or with a work based on the Program) on a volume of
a storage or distribution medium does not bring the other work under
the scope of this License.

3. You may copy and distribute the Program (or a work based on it,
under Section 2) in object code or executable form under the terms of
Sections 1 and 2 above provided that you also do one of the following:

a) Accompany it with the complete corresponding machine-readable
source code, which must be distributed under the terms of Sections
1 and 2 above on a medium customarily used for software interchange; or,

b) Accompany it with a written offer, valid for at least three
years, to give any third party, for a charge no more than your
cost of physically performing source distribution, a complete
machine-readable copy of the corresponding source code, to be
distributed under the terms of Sections 1 and 2 above on a medium
customarily used for software interchange; or,

c) Accompany it with the information you received as to the offer
to distribute corresponding source code. (This alternative is
allowed only for noncommercial distribution and only if you
received the program in object code or executable form with such
an offer, in accord with Subsection b above.)

The source code for a work means the preferred form of the work for
making modifications to it. For an executable work, complete source
code means all the source code for all modules it contains, plus any
associated interface definition files, plus the scripts used to
control compilation and installation of the executable. However, as a
special exception, the source code distributed need not include
anything that is normally distributed (in either source or binary
form) with the major components (compiler, kernel, and so on) of the
operating system on which the executable runs, unless that component
itself accompanies the executable.

If distribution of executable or object code is made by offering
access to copy from a designated place, then offering equivalent
access to copy the source code from the same place counts as
distribution of the source code, even though third parties are not
compelled to copy the source along with the object code.

4. You may not copy, modify, sublicense, or distribute the Program
except as expressly provided under this License. Any attempt
otherwise to copy, modify, sublicense or distribute the Program is
void, and will automatically terminate your rights under this License.
However, parties who have received copies, or rights, from you under
this License will not have their licenses terminated so long as such
parties remain in full compliance.

5. You are not required to accept this License, since you have not
signed it. However, nothing else grants you permission to modify or
distribute the Program or its derivative works. These actions are
prohibited by law if you do not accept this License. Therefore, by
modifying or distributing the Program (or any work based on the
Program), you indicate your acceptance of this License to do so, and
all its terms and conditions for copying, distributing or modifying
the Program or works based on it.

6. Each time you redistribute the Program (or any work based on the
Program), the recipient automatically receives a license from the
original licensor to copy, distribute or modify the Program subject to
these terms and conditions. You may not impose any further
restrictions on the recipients' exercise of the rights granted herein.
You are not responsible for enforcing compliance by third parties to
this License.

7. If, as a consequence of a court judgment or allegation of patent
infringement or for any other reason (not limited to patent issues),
conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License. If you cannot
distribute so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you
may not distribute the Program at all. For example, if a patent
license would not permit royalty-free redistribution of the Program by
all those who receive copies directly or indirectly through you, then
the only way you could satisfy both it and this License would be to
refrain entirely from distribution of the Program.

If any portion of this section is held invalid or unenforceable under
any particular circumstance, the balance of the section is intended to
apply and the section as a whole is intended to apply in other
circumstances.

It is not the purpose of this section to induce you to infringe any
patents or other property right claims or to contest validity of any
such claims; this section has the sole purpose of protecting the
integrity of the free software distribution system, which is
implemented by public license practices. Many people have made
generous contributions to the wide range of software distributed
through that system in reliance on consistent application of that
system; it is up to the author/donor to decide if he or she is willing
to distribute software through any other system and a licensee cannot
impose that choice.

This section is intended to make thoroughly clear what is believed to
be a consequence of the rest of this License.

8. If the distribution and/or use of the Program is restricted in
certain countries either by patents or by copyrighted interfaces, the
original copyright holder who places the Program under this License
may add an explicit geographical distribution limitation excluding
those countries, so that distribution is permitted only in or among
countries not thus excluded. In such case, this License incorporates
the limitation as if written in the body of this License.

9. The Free Software Foundation may publish revised and/or new versions
of the General Public License from time to time. Such new versions will
be similar in spirit to the present version, but may differ in detail to
address new problems or concerns.

Each version is given a distinguishing version number. If the Program
specifies a version number of this License which applies to it and "any
later version", you have the option of following the terms and conditions
either of that version or of any later version published by the Free
Software Foundation. If the Program does not specify a version number of
this License, you may choose any version ever published by the Free Software
Foundation.

10. If you wish to incorporate parts of the Program into other free
programs whose distribution conditions are different, write to the author
to ask for permission. For software which is copyrighted by the Free
Software Foundation, write to the Free Software Foundation; we sometimes
make exceptions for this. Our decision will be guided by the two goals
of preserving the free status of all derivatives of our free software and
of promoting the sharing and reuse of software generally.

NO WARRANTY

11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN
OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS
TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE
PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
REPAIR OR CORRECTION.

12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
POSSIBILITY OF SUCH DAMAGES.

END OF TERMS AND CONDITIONS

How to Apply These Terms to Your New Programs

If you develop a new program, and you want it to be of the greatest
possible use to the public, the best way to achieve this is to make it
free software which everyone can redistribute and change under these terms.

To do so, attach the following notices to the program. It is safest
to attach them to the start of each source file to most effectively
convey the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found.

<one line to give the program's name and a brief idea of what it does.>
Copyright (C) <year> <name of author>

This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License along
with this program; if not, write to the Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.

Also add information on how to contact you by electronic and paper mail.

If the program is interactive, make it output a short notice like this
when it starts in an interactive mode:

Gnomovision version 69, Copyright (C) year name of author
Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
This is free software, and you are welcome to redistribute it
under certain conditions; type `show c' for details.

The hypothetical commands `show w' and `show c' should show the appropriate
parts of the General Public License. Of course, the commands you use may
be called something other than `show w' and `show c'; they could even be
mouse-clicks or menu items--whatever suits your program.

You should also get your employer (if you work as a programmer) or your
school, if any, to sign a "copyright disclaimer" for the program, if
necessary. Here is a sample; alter the names:

Yoyodyne, Inc., hereby disclaims all copyright interest in the program
`Gnomovision' (which makes passes at compilers) written by James Hacker.

<signature of Ty Coon>, 1 April 1989
Ty Coon, President of Vice

This General Public License does not permit incorporating your program into
proprietary programs. If your program is a subroutine library, you may
consider it more useful to permit linking proprietary applications with the
library. If this is what you want to do, use the GNU Lesser General
Public License instead of this License.
35 changes: 35 additions & 0 deletions MenuItem.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
<?php
namespace dokuwiki\plugin\move;
use dokuwiki\Menu\Item\AbstractItem;
/**
* Class MenuItem
*
* Implements the Rename button for DokuWiki's menu system
*
* @package dokuwiki\plugin\move
*/
class MenuItem extends AbstractItem {

/** @var string icon file */
protected $svg = __DIR__ . '/images/rename.svg';

protected $type = "plugin_move";

public function getLinkAttributes($classprefix = 'menuitem ') {
$attr = parent::getLinkAttributes($classprefix);
if (empty($attr['class'])) {
$attr['class'] = '';
}
$attr['class'] .= ' plugin_move_page ';
return $attr;
}
/**
* Get label from plugin language file
*
* @return string
*/
public function getLabel() {
$hlp = plugin_load('action', 'move_rename');
return $hlp->getLang('renamepage');
}
}
54 changes: 53 additions & 1 deletion README
Original file line number Diff line number Diff line change
@@ -1 +1,53 @@
A DokuWiki pagemove plugin
move Plugin for DokuWiki

Move and rename pages and media files while maintaining the links.

All documentation for this plugin can be found at
http://www.dokuwiki.org/plugin:move

If you install this plugin manually, make sure it is installed in
lib/plugins/move/ - if the folder is called different it
will not work!

Please refer to http://www.dokuwiki.org/plugins for additional info
on how to install plugins in DokuWiki.

----
Copyright (C) Michael Hamann <michael@content-space.de>,
Andreas Gohr <gohr@cosmocode.de>,
and others

This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; version 2 of the License

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.

See the COPYING file in your DokuWiki folder for details

Sources of the icons contained in images/:

disk.png
folder_add.png
page.png
page_link.png

Silk icon set 1.3

_________________________________________
Mark James
http://www.famfamfam.com/lab/icons/silk/
_________________________________________

Licensed under a
Creative Commons Attribution 2.5 License.
[ http://creativecommons.org/licenses/by/2.5/ ]


rename.png

SEMLabs Free Web Design Icon Set,
http://semlabs.co.uk/blog/free-web-design-icon-set/
84 changes: 84 additions & 0 deletions _test/GeneralTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
<?php

namespace dokuwiki\plugin\move\test;

use DokuWikiTest;

/**
* General tests for the move plugin
*
* @group plugin_move
* @group plugins
*/
class GeneralTest extends DokuWikiTest
{
/**
* Simple test to make sure the plugin.info.txt is in correct format
*/
public function testPluginInfo(): void
{
$file = __DIR__ . '/../plugin.info.txt';
$this->assertFileExists($file);

$info = confToHash($file);

$this->assertArrayHasKey('base', $info);
$this->assertArrayHasKey('author', $info);
$this->assertArrayHasKey('email', $info);
$this->assertArrayHasKey('date', $info);
$this->assertArrayHasKey('name', $info);
$this->assertArrayHasKey('desc', $info);
$this->assertArrayHasKey('url', $info);

$this->assertEquals('move', $info['base']);
$this->assertRegExp('/^https?:\/\//', $info['url']);
$this->assertTrue(mail_isvalid($info['email']));
$this->assertRegExp('/^\d\d\d\d-\d\d-\d\d$/', $info['date']);
$this->assertTrue(false !== strtotime($info['date']));
}

/**
* Test to ensure that every conf['...'] entry in conf/default.php has a corresponding meta['...'] entry in
* conf/metadata.php.
*/
public function testPluginConf(): void
{
$conf_file = __DIR__ . '/../conf/default.php';
$meta_file = __DIR__ . '/../conf/metadata.php';

if (!file_exists($conf_file) && !file_exists($meta_file)) {
self::markTestSkipped('No config files exist -> skipping test');
}

if (file_exists($conf_file)) {
include($conf_file);
}
if (file_exists($meta_file)) {
include($meta_file);
}

$this->assertEquals(
gettype($conf),
gettype($meta),
'Both ' . DOKU_PLUGIN . 'move/conf/default.php and ' . DOKU_PLUGIN . 'move/conf/metadata.php have to exist and contain the same keys.'
);

if ($conf !== null && $meta !== null) {
foreach ($conf as $key => $value) {
$this->assertArrayHasKey(
$key,
$meta,
'Key $meta[\'' . $key . '\'] missing in ' . DOKU_PLUGIN . 'move/conf/metadata.php'
);
}

foreach ($meta as $key => $value) {
$this->assertArrayHasKey(
$key,
$conf,
'Key $conf[\'' . $key . '\'] missing in ' . DOKU_PLUGIN . 'move/conf/default.php'
);
}
}
}
}
58 changes: 58 additions & 0 deletions _test/affectedPagesNs.test.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
<?php


/**
* Test cases for the move plugin
*
* @group plugin_move
* @group plugins
*/
class plugin_move_affectedPagesNS_test extends DokuWikiTest {

protected $pluginsEnabled = array('move',);

public function setUp(): void {
parent::setUp();
global $USERINFO;
global $conf;
$conf['useacl'] = 1;
$conf['superuser'] = 'john';
$_SERVER['REMOTE_USER'] = 'john'; //now it's testing as admin
$USERINFO['grps'] = array('admin','user');
}

/**
* @coversNothing
*/
public function tearDown(): void {
/** @var helper_plugin_move_plan $plan */
$plan = plugin_load('helper', 'move_plan');
$plan->abort();
parent::tearDown();
}

/**
* @covers helper_plugin_move_plan::findAffectedPages
* @uses Doku_Indexer
*/
public function test_affectedPagesNS_Media() {

saveWikiText('oldns:start', '{{oldnsimage_missing.png}}', 'setup');
idx_addPage('oldns:start');

/** @var helper_plugin_move_plan $plan */
$plan = plugin_load('helper','move_plan');

$this->assertFalse($plan->inProgress());

$plan->addMediaNamespaceMove('oldns', 'newns');

$plan->commit();

$affected_file = file(TMP_DIR . '/data/meta/__move_affected');

$this->assertSame('oldns:start',trim($affected_file[0]));

}

}
47 changes: 47 additions & 0 deletions _test/cache_handling.test.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
<?php

// must be run within Dokuwiki
if (!defined('DOKU_INC')) die();

/**
* Test cases for the move plugin
*
* @group plugin_move
* @group plugins
*/
class plugin_move_cache_handling_test extends DokuWikiTest {

function setUp(): void {
parent::setUpBeforeClass();
$this->pluginsEnabled[] = 'move';
parent::setUp();
}

/**
* @group slow
*/
function test_cache_handling() {
$testid = 'wiki:bar:test';
saveWikiText($testid,
'[[wiki:foo:]]', 'Test setup');
idx_addPage($testid);
saveWikiText('wiki:foo:start',
'bar', 'Test setup');
idx_addPage('wiki:foo:start');

sleep(1); // wait in order to make sure that conditions with < give the right result.
p_wiki_xhtml($testid); // populate cache

$cache = new cache_renderer($testid, wikiFN($testid), 'xhtml');
$this->assertTrue($cache->useCache());

/** @var helper_plugin_move_op $move */
$move = plugin_load('helper', 'move_op');
$this->assertTrue($move->movePage('wiki:foo:start', 'wiki:foo2:start'));

$cache = new cache_renderer($testid, wikiFN($testid), 'xhtml');
$this->assertFalse($cache->useCache());

}

}
113 changes: 113 additions & 0 deletions _test/findMissingDocuments.test.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
<?php


class helper_plugin_move_plan_findMissingDocuments_mock extends helper_plugin_move_plan {

public function findMissingDocuments($src, $dst, $type = self::TYPE_PAGES) {
parent::findMissingDocuments($src, $dst, $type);
}

public function getTmpstore() {
return $this->tmpstore;
}

}


/**
* Test cases for helper_plugin_move_plan::stepThroughDocuments function of the move plugin
*
* @group plugin_move
* @group plugin_move_unittests
* @group plugins
* @group unittests
* @covers helper_plugin_move_plan::findMissingDocuments
*/
class plugin_move_findMissingPages_test extends DokuWikiTest {

protected $pluginsEnabled = array('move',);
/** @var helper_plugin_move_plan_findMissingDocuments_mock $plan */
protected $plan;

/**
* @coversNothing
*/
public function setUp(): void {
parent::setUp();
$this->plan = new helper_plugin_move_plan_findMissingDocuments_mock();
}


/**
* @coversNothing
*/
public function tearDown(): void {
global $conf;

$dirs = array('indexdir','datadir','metadir', 'mediadir');
foreach ($dirs as $dir) {
io_rmdir($conf[$dir],true);
mkdir($conf[$dir]);
}
$this->plan->abort();
parent::tearDown();
}


function test_findMissingPages_empty () {
$this->plan->findMissingDocuments('oldns','newns:');
$tmpstore = $this->plan->getTmpstore();
$this->assertSame(array(),$tmpstore['miss']);
}

function test_findMissingPages_missingPage_default () {
saveWikiText('start','[[oldns:missing]]','test edit');
idx_addPage('start');
$this->plan->findMissingDocuments('oldns:','newns:');
$tmpstore = $this->plan->getTmpstore();
$this->assertSame(array('oldns:missing' => 'newns:missing',),$tmpstore['miss']);
}

function test_findMissingPages_missingPage_explicit () {
saveWikiText('start','[[oldns:missing]]','test edit');
idx_addPage('start');
$this->plan->findMissingDocuments('oldns:','newns:',helper_plugin_move_plan::TYPE_PAGES);
$tmpstore = $this->plan->getTmpstore();
$this->assertSame(array('oldns:missing' => 'newns:missing',),$tmpstore['miss']);
}

function test_findMissingPages_missingPage_integrated () {
saveWikiText('oldns:start','[[oldns:missing]] {{oldns:missing.png}}','test edit');
idx_addPage('oldns:start');
$this->plan->addPageNamespaceMove('oldns', 'newns');
$this->plan->addMediaNamespaceMove('oldns', 'newns');

$this->plan->commit();

$missing_file = file(TMP_DIR . '/data/meta/__move_missing');
$this->assertSame(array("oldns:missing\tnewns:missing\n",),$missing_file,'new configuration fails');

$missing_media_file = file(TMP_DIR . '/data/meta/__move_missing_media');
$this->assertSame(array("oldns:missing.png\tnewns:missing.png\n",),$missing_media_file,'new configuration fails');

}

function test_findMissingPages_missingMedia () {
saveWikiText('start','{{oldns:missing.png}}','test edit');
idx_addPage('start');
$this->plan->findMissingDocuments('oldns:','newns:',helper_plugin_move_plan::TYPE_MEDIA);
$tmpstore = $this->plan->getTmpstore();
$this->assertSame(array('oldns:missing.png' => 'newns:missing.png',),$tmpstore['miss_media']);
}

function test_findMissingDocuments_nonMissingMedia () {
$filepath = DOKU_TMP_DATA.'media/oldns/oldnsimage.png';
io_makeFileDir($filepath);
io_saveFile($filepath,'');
saveWikiText('start','{{oldns:oldnsimage.png}}','test edit');
idx_addPage('start');
$this->plan->findMissingDocuments('oldns:','newns:',helper_plugin_move_plan::TYPE_MEDIA);
$tmpstore = $this->plan->getTmpstore();
$this->assertSame(array(),$tmpstore['miss_media']);
}
}
75 changes: 75 additions & 0 deletions _test/handler.test.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
<?php
// must be run within Dokuwiki
if(!defined('DOKU_INC')) die();

require_once(__DIR__ . '/../helper/handler.php');

/**
* Test cases for the move plugin
*
* @group plugin_move
* @group plugins
*/
class plugin_move_handler_test extends DokuWikiTest {

public function setUp(): void {
$this->pluginsEnabled[] = 'move';
parent::setUp();
}

public function test_relativeLink() {
/** @var $handler helper_plugin_move_handler */
$handler = plugin_load('helper', 'move_handler');
$handler->init('deep:namespace:page', 'used:to:be:here', array(), array(), array());

$tests = array(
'deep:namespace:new1' => 'new1',
'deep:new2' => '..:new2',
'new3' => ':new3', // absolute is shorter than relative
'deep:namespace:deeper:new4' => '.deeper:new4',
'deep:namespace:deeper:deepest:new5' => '.deeper:deepest:new5',
'deep:foobar:new6' => '..:foobar:new6',
);

foreach($tests as $new => $rel) {
$this->assertEquals($rel, $handler->relativeLink('foo', $new, 'page'));
}

$this->assertEquals('.deeper:', $handler->relativeLink('.deeper:', 'deep:namespace:deeper:start', 'page'));
$this->assertEquals('.:', $handler->relativeLink('.:', 'deep:namespace:start', 'page'));
}

public function test_resolveMoves() {
/** @var $handler helper_plugin_move_handler */
$handler = plugin_load('helper', 'move_handler');
$handler->init(
'deep:namespace:page',
'used:to:be:here',
array(
array('used:to:be:here', 'deep:namespace:page'),
array('foo', 'bar'),
array('used:to:be:this1', 'used:to:be:that1'),
array('used:to:be:this2', 'deep:namespace:that1'),
array('used:to:be:this3', 'deep:that3'),
array('deep:that3', 'but:got:moved3'),
),
array(),
array()
);

$tests = array(
'used:to:be:here' => 'deep:namespace:page', // full link to self
':foo' => 'bar', // absolute link that moved
':bang' => 'bang', // absolute link that did not move
'foo' => 'used:to:be:foo', // relative link that did not move
'this1' => 'used:to:be:that1', // relative link that did not move but is in move list
'this2' => 'deep:namespace:that1', // relative link that moved
'this3' => 'but:got:moved3', // relative link that moved twice
);

foreach($tests as $match => $id) {
$this->assertEquals($id, $handler->resolveMoves($match, 'page'));
}
}

}
46 changes: 46 additions & 0 deletions _test/log.test.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
<?php

/**
* Test cases log functionality of the move plugin
*
* @group plugin_move
* @group plugin_move_unittests
* @group plugins
* @group unittests
*/
class plugin_move_log_test extends DokuWikiTest {

protected $pluginsEnabled = array('move',);

public function test_log_one_line_success() {
/** @var helper_plugin_move_plan $plan */
$plan = plugin_load('helper', 'move_plan');
$now = time();
$date = date('Y-m-d H:i:s', $now);

$actual_log = $plan->build_log_line('P','oldpage','newpage',true);

$expected_log = "$now\t$date\tP\toldpage\tnewpage\tsuccess\t\n";

$this->assertSame($expected_log, $actual_log);
}

public function test_log_build_line_failure() {
global $MSG;
$MSG = array();
$msg = array('msg'=>"TestMessage01",);
array_push($MSG,$msg);

/** @var helper_plugin_move_plan $plan */
$plan = plugin_load('helper', 'move_plan');
$now = time();
$date = date('Y-m-d H:i:s', $now);

$actual_log = $plan->build_log_line('P','oldpage','newpage',false);

$expected_log = "$now\t$date\tP\toldpage\tnewpage\tfailed\tTestMessage01\n";

$this->assertSame($expected_log, $actual_log);
}

}
124 changes: 124 additions & 0 deletions _test/mediamove.test.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
<?php

// must be run within Dokuwiki
if (!defined('DOKU_INC')) die();

/**
* Test cases for the move plugin
*
* @group plugin_move
* @group plugins
*/
class plugin_move_mediamove_test extends DokuWikiTest {

public function setUp(): void {
$this->pluginsEnabled[] = 'move';
parent::setUp();
}

/**
* @group slow
*/
public function test_movePageWithRelativeMedia() {
$src = 'mediareltest:foo';
saveWikiText($src,
'{{ myimage.png}} [[:start|{{ testimage.png?200x800 }}]] [[bar|{{testimage.gif?400x200}}]]
[[doku>wiki:dokuwiki|{{wiki:logo.png}}]] [[http://www.example.com|{{testimage.jpg}}]]
[[doku>wiki:foo|{{foo.gif?200x3000}}]]', 'Test setup');
idx_addPage($src);

$dst = 'foo';

/** @var helper_plugin_move_op $move */
$move = plugin_load('helper', 'move_op');
$this->assertTrue($move->movePage($src, $dst));

$this->assertEquals('{{ mediareltest:myimage.png}} [[:start|{{ mediareltest:testimage.png?200x800 }}]] [[mediareltest:bar|{{mediareltest:testimage.gif?400x200}}]]
[[doku>wiki:dokuwiki|{{wiki:logo.png}}]] [[http://www.example.com|{{mediareltest:testimage.jpg}}]]
[[doku>wiki:foo|{{mediareltest:foo.gif?200x3000}}]]', rawWiki('foo'));
}

/**
* @group slow
*/
public function test_moveSingleMedia() {
global $AUTH_ACL;
$AUTH_ACL[] = "wiki:*\t@ALL\t16";
$AUTH_ACL[] = "foobar:*\t@ALL\t8";

saveWikiText('wiki:movetest', '{{wiki:dokuwiki-128.png?200}}', 'Test initialized');
idx_addPage('wiki:movetest');

$src = 'wiki:dokuwiki-128.png';
$dst = 'foobar:logo.png';

/** @var helper_plugin_move_op $move */
$move = plugin_load('helper', 'move_op');
$this->assertTrue($move->moveMedia($src, $dst));

$this->assertTrue(@file_exists(mediaFn('foobar:logo.png')));

$this->assertEquals('{{foobar:logo.png?200}}', rawWiki('wiki:movetest'));
}

/**
* @group slow
*/
public function test_moveSingleMedia_colonstart() {
global $AUTH_ACL;
$AUTH_ACL[] = "wiki:*\t@ALL\t16";
$AUTH_ACL[] = "foobar:*\t@ALL\t16";
$AUTH_ACL[] = "*\t@ALL\t8";

$filepath = DOKU_TMP_DATA.'media/wiki/testimage.png';
io_makeFileDir($filepath);
io_saveFile($filepath,'');

saveWikiText('wiki:movetest', '{{:wiki:testimage.png?200}}', 'Test initialized');
idx_addPage('wiki:movetest');

$src = 'wiki:testimage.png';
$dst = 'foobar:logo_2.png';

/** @var helper_plugin_move_op $move */
$move = plugin_load('helper', 'move_op');
$this->assertTrue($move->moveMedia($src, $dst));

$this->assertTrue(@file_exists(mediaFn('foobar:logo_2.png')));

$this->assertEquals('{{foobar:logo_2.png?200}}', rawWiki('wiki:movetest'));

$this->assertTrue($move->moveMedia($dst, 'logo_2.png'));

$this->assertTrue(@file_exists(mediaFn('logo_2.png')));

$this->assertEquals('{{:logo_2.png?200}}', rawWiki('wiki:movetest'));
}

/**
* @group slow
*/
public function test_moveSingleMedia_space() {
global $AUTH_ACL;
$AUTH_ACL[] = "wiki:*\t@ALL\t16";
$AUTH_ACL[] = "foobar:*\t@ALL\t8";

$filepath = DOKU_TMP_DATA.'media/wiki/foo/test_image.png';
io_makeFileDir($filepath);
io_saveFile($filepath,'');

saveWikiText('wiki:movetest', '{{:wiki:foo:test image.png?200|test image}}', 'Test initialized');
idx_addPage('wiki:movetest');

$src = 'wiki:foo:test_image.png';
$dst = 'wiki:foobar:test_image.png';

/** @var helper_plugin_move_op $move */
$move = plugin_load('helper', 'move_op');
$this->assertTrue($move->moveMedia($src, $dst));

$this->assertTrue(@file_exists(mediaFn('wiki:foobar:test_image.png')));

$this->assertEquals('{{wiki:foobar:test_image.png?200|test image}}', rawWiki('wiki:movetest'));
}
}
478 changes: 478 additions & 0 deletions _test/namespace_move.test.php

Large diffs are not rendered by default.

10 changes: 0 additions & 10 deletions _test/pagemove.group.php

This file was deleted.

338 changes: 210 additions & 128 deletions _test/pagemove.test.php

Large diffs are not rendered by default.

110 changes: 110 additions & 0 deletions _test/plan.test.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
<?php
// must be run within Dokuwiki
if(!defined('DOKU_INC')) die();

require_once(__DIR__ . '/../helper/plan.php');

/**
* Test cases for the move plugin
*
* @group plugin_move
* @group plugins
*/
class plugin_move_plan_test extends DokuWikiTest {

/**
* Create some page namespace structure
*/
function setUp():void {
$pages = array(
'animals:mammals:bear:brownbear',
'animals:mammals:bear:blackbear',
'animals:mammals:cute:otter',
'animals:mammals:cute:cat',
'animals:mammals:cute:dog',
'animals:insects:butterfly:fly',
'animals:insects:butterfly:moth',
'animals:monkey',
'humans:programmers:andi',
'humans:programmers:joe',
'humans:programmers:john',
'yeti'
);
foreach($pages as $page) {
saveWikiText($page, $page, 'test setup');
}

parent::setUp();
}

/**
* Check that the plan is sorted into the right order
*/
function test_sorting() {
$plan = new test_helper_plugin_move_plan();

$plan->addPageNamespaceMove('animals:mammals:bear', 'animals:mammals:cute:bear');
$plan->addPageNamespaceMove('humans:programmers', 'animals:mammals:cute:programmers');
$plan->addPageMove('humans:programmers:andi', 'animals:insects:butterfly:andi');
$plan->addPageMove('yeti', 'humans:yeti');
$plan->addPageMove('animals:monkey', 'monkey');

$sorted = $plan->sortedPlan();

// the plan is sorted FORWARD (first things first)
$this->assertEquals(5, count($sorted));
$this->assertEquals('humans:programmers:andi', $sorted[0]['src']);
$this->assertEquals('animals:monkey', $sorted[1]['src']);
$this->assertEquals('yeti', $sorted[2]['src']);
$this->assertEquals('animals:mammals:bear', $sorted[3]['src']);
$this->assertEquals('humans:programmers', $sorted[4]['src']);
}

/**
* Move a page out of a namespace and then move the namespace elsewhere
*/
function test_pageinnamespace() {
$plan = new test_helper_plugin_move_plan();

$plan->addPageNamespaceMove('animals:mammals:cute', 'animals:mammals:funny');
$plan->addPageMove('animals:mammals:cute:otter', 'animals:mammals:otter');

$plan->commit();
$list = $plan->getList('pagelist');

// the files are sorted BACKWARDS (first things last)
$this->assertEquals(3, count($list));
$this->assertEquals("animals:mammals:cute:otter\tanimals:mammals:otter", trim($list[2]));
$this->assertEquals("animals:mammals:cute:cat\tanimals:mammals:funny:cat", trim($list[1]));
$this->assertEquals("animals:mammals:cute:dog\tanimals:mammals:funny:dog", trim($list[0]));

}
}

/**
* Class test_helper_plugin_move_plan
*
* gives access to some internal stuff of the class
*/
class test_helper_plugin_move_plan extends helper_plugin_move_plan {

/**
* Access the sorted plan
*
* @return array
*/
function sortedPlan() {
usort($this->plan, array($this, 'planSorter'));
return $this->plan;
}

/**
* Get the full saved list specified by name
*
* @param $name
* @return array
*/
function getList($name) {
return file($this->files[$name]);
}
}
274 changes: 274 additions & 0 deletions _test/stepThroughDocuments.test.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,274 @@
<?php

/**
* mock class to access the helper_plugin_move_plan::stepThroughDocuments function in tests
*/
class helper_plugin_move_plan_mock extends helper_plugin_move_plan {

public $moveLog = array();

public function __construct() {
parent::__construct();
$this->MoveOperator = new helper_plugin_move_op_mock;
}

public function stepThroughDocumentsCall($type = parent::TYPE_PAGES, $skip = false) {
return $this->stepThroughDocuments($type, $skip);
}

public function getMoveOperator() {
return $this->MoveOperator;
}

public function setMoveOperator($newMoveOPerator) {
$this->MoveOperator = $newMoveOPerator;
}

public function build_log_line($type, $from, $to, $success) {
$logEntry = array($type,$from,$to,$success);
array_push($this->moveLog,$logEntry);
return parent::build_log_line($type, $from, $to, $success);
}



}

class helper_plugin_move_op_mock extends helper_plugin_move_op {

public $movedPages = array();
public $fail = false;

public function movePage($src, $dst) {
if ($this->fail !== false && count($this->movedPages) == $this->fail) {
$this->fail=false;
// Store a msg as it is expected by the plugin
msg("Intentional failure in test case.", -1);
return false;
}
$moveOperation = array($src => $dst);
array_push($this->movedPages,$moveOperation);
return true;
}
}




/**
* Test cases for helper_plugin_move_plan::stepThroughDocuments function of the move plugin
*
* @group plugin_move
* @group plugin_move_unittests
* @group plugins
* @group unittests
*/
class plugin_move_stepThroughDocuments_test extends DokuWikiTest {

public function setUp(): void {
parent::setUp();
$opts_file = dirname(DOKU_CONF) . '/data/meta/__move_opts';
if(file_exists($opts_file)){
unlink($opts_file);
}

$file = "oldns:page01\tnewns:page01\n"
. "oldns:page02\tnewns:page02\n"
. "oldns:page03\tnewns:page03\n"
. "oldns:page04\tnewns:page04\n"
. "oldns:page05\tnewns:page05\n"
. "oldns:page06\tnewns:page06\n"
. "oldns:page07\tnewns:page07\n"
. "oldns:page08\tnewns:page08\n"
. "oldns:page09\tnewns:page09\n"
. "oldns:page10\tnewns:page10\n"
. "oldns:page11\tnewns:page11\n"
. "oldns:page12\tnewns:page12\n"
. "oldns:page13\tnewns:page13\n"
. "oldns:page14\tnewns:page14\n"
. "oldns:page15\tnewns:page15\n"
. "oldns:page16\tnewns:page16\n"
. "oldns:page17\tnewns:page17\n"
. "oldns:page18\tnewns:page18";
$file_path = dirname(DOKU_CONF) . '/data/meta/__move_pagelist';
io_saveFile($file_path,$file);
}


/**
* @covers helper_plugin_move_plan::stepThroughDocuments
*/
public function test_stepThroughPages() {

$file_path = dirname(DOKU_CONF) . '/data/meta/__move_pagelist';
$mock = new helper_plugin_move_plan_mock();
$actual_return = $mock->stepThroughDocumentsCall();
$actual_file = file_get_contents($file_path);
$expected_file = "oldns:page01\tnewns:page01\n"
. "oldns:page02\tnewns:page02\n"
. "oldns:page03\tnewns:page03\n"
. "oldns:page04\tnewns:page04\n"
. "oldns:page05\tnewns:page05\n"
. "oldns:page06\tnewns:page06\n"
. "oldns:page07\tnewns:page07\n"
. "oldns:page08\tnewns:page08";

$expected_pages_run = -10;
$this->assertSame($expected_pages_run,$actual_return,"return values differ");
$this->assertSame($expected_file,$actual_file, "files differ");
$actual_move_Operator = $mock->getMoveOperator();
$this->assertSame(array('oldns:page18' => 'newns:page18',),$actual_move_Operator->movedPages[0]);
$this->assertSame(array('oldns:page09' => 'newns:page09',),$actual_move_Operator->movedPages[9]);
$this->assertTrue(!isset($actual_move_Operator->movedPages[10]));

$expected_log = array('P','oldns:page18','newns:page18',true);
$this->assertSame($expected_log,$mock->moveLog[0]);

$expected_log = array('P','oldns:page09','newns:page09',true);
$this->assertSame($expected_log,$mock->moveLog[9]);
$this->assertTrue(!isset($mock->moveLog[10]));

$opts_file = dirname(DOKU_CONF) . '/data/meta/__move_opts';
$actual_options = unserialize(io_readFile($opts_file));
$this->assertSame($expected_pages_run,$actual_options['pages_run'],'saved options are wrong');
}

/**
* @covers helper_plugin_move_plan::stepThroughDocuments
*/
public function test_stepThroughPages_skip() {

$file_path = dirname(DOKU_CONF) . '/data/meta/__move_pagelist';
$mock = new helper_plugin_move_plan_mock();
$actual_return = $mock->stepThroughDocumentsCall(1,true);
$actual_file = file_get_contents($file_path);
$expected_file = "oldns:page01\tnewns:page01\n"
. "oldns:page02\tnewns:page02\n"
. "oldns:page03\tnewns:page03\n"
. "oldns:page04\tnewns:page04\n"
. "oldns:page05\tnewns:page05\n"
. "oldns:page06\tnewns:page06\n"
. "oldns:page07\tnewns:page07\n"
. "oldns:page08\tnewns:page08";
$expected_pages_run = -10;
$this->assertSame($expected_pages_run,$actual_return,"return values differ");
$this->assertSame($expected_file,$actual_file, "files differ");
$actual_move_Operator = $mock->getMoveOperator();
$this->assertSame(array('oldns:page17' => 'newns:page17',),$actual_move_Operator->movedPages[0]);
$this->assertSame(array('oldns:page09' => 'newns:page09',),$actual_move_Operator->movedPages[8]);
$this->assertTrue(!isset($actual_move_Operator->movedPages[9]));

$expected_log = array('P','oldns:page17','newns:page17',true);
$this->assertSame($expected_log,$mock->moveLog[0]);

$expected_log = array('P','oldns:page09','newns:page09',true);
$this->assertSame($expected_log,$mock->moveLog[8]);
$this->assertTrue(!isset($mock->moveLog[9]));

$opts_file = dirname(DOKU_CONF) . '/data/meta/__move_opts';
$actual_options = unserialize(io_readFile($opts_file));
$this->assertSame($expected_pages_run,$actual_options['pages_run'],'saved options are wrong');
}

/**
* @covers helper_plugin_move_plan::stepThroughDocuments
*/
public function test_stepThroughPages_fail() {

$file_path = dirname(DOKU_CONF) . '/data/meta/__move_pagelist';
$mock = new helper_plugin_move_plan_mock();
$fail_at_item = 5;
$actual_move_Operator = $mock->getMoveOperator();
$actual_move_Operator->fail = $fail_at_item;
$mock->setMoveOperator($actual_move_Operator);
$actual_return = $mock->stepThroughDocumentsCall();
$actual_file = file_get_contents($file_path);
$expected_file = "oldns:page01\tnewns:page01\n"
. "oldns:page02\tnewns:page02\n"
. "oldns:page03\tnewns:page03\n"
. "oldns:page04\tnewns:page04\n"
. "oldns:page05\tnewns:page05\n"
. "oldns:page06\tnewns:page06\n"
. "oldns:page07\tnewns:page07\n"
. "oldns:page08\tnewns:page08\n"
. "oldns:page09\tnewns:page09\n"
. "oldns:page10\tnewns:page10\n"
. "oldns:page11\tnewns:page11\n"
. "oldns:page12\tnewns:page12\n"
. "oldns:page13\tnewns:page13";

$expected_pages_run = false;
$this->assertSame($expected_pages_run,$actual_return,"return values differ");
$this->assertSame($expected_file,$actual_file, "files differ");
$actual_move_Operator = $mock->getMoveOperator();
$this->assertSame(array('oldns:page18' => 'newns:page18',),$actual_move_Operator->movedPages[0]);
$lastIndex = 4;
$this->assertSame(array('oldns:page14' => 'newns:page14',),$actual_move_Operator->movedPages[$lastIndex]);
$this->assertTrue(!isset($actual_move_Operator->movedPages[$lastIndex + 1]));

$expected_log = array('P','oldns:page13','newns:page13',false);
$this->assertSame($expected_log,$mock->moveLog[5]);
$this->assertTrue(!isset($mock->moveLog[6]));

$opts_file = dirname(DOKU_CONF) . '/data/meta/__move_opts';
$actual_options = unserialize(io_readFile($opts_file));
$this->assertSame(-$fail_at_item,$actual_options['pages_run'],'saved options are wrong');
}


/**
* @covers helper_plugin_move_plan::stepThroughDocuments
*/
public function test_stepThroughPages_fail_autoskip() {
global $conf;
$conf['plugin']['move']['autoskip'] = '1';

$file_path = dirname(DOKU_CONF) . '/data/meta/__move_pagelist';
$mock = new helper_plugin_move_plan_mock();
$actual_move_Operator = $mock->getMoveOperator();
$actual_move_Operator->fail = 5;
$mock->setMoveOperator($actual_move_Operator);
$actual_return = $mock->stepThroughDocumentsCall();

$expected_pages_run = -10;
$this->assertSame($expected_pages_run,$actual_return,"return values differ");

$actual_file = file_get_contents($file_path);
$expected_file = "oldns:page01\tnewns:page01\n"
. "oldns:page02\tnewns:page02\n"
. "oldns:page03\tnewns:page03\n"
. "oldns:page04\tnewns:page04\n"
. "oldns:page05\tnewns:page05\n"
. "oldns:page06\tnewns:page06\n"
. "oldns:page07\tnewns:page07\n"
. "oldns:page08\tnewns:page08";

$this->assertSame($expected_file,$actual_file, "files differ");


$actual_move_Operator = $mock->getMoveOperator();
$this->assertSame(array('oldns:page18' => 'newns:page18',),$actual_move_Operator->movedPages[0]);

$lastIndex = 8;
$this->assertSame(array('oldns:page09' => 'newns:page09',),$actual_move_Operator->movedPages[$lastIndex]);
$this->assertTrue(!isset($actual_move_Operator->movedPages[$lastIndex + 1]), "The number of moved pages is incorrect");


$expected_log = array('P','oldns:page18','newns:page18',true);
$this->assertSame($expected_log,$mock->moveLog[0]);

$expected_log = array('P','oldns:page13','newns:page13',false);
$this->assertSame($expected_log,$mock->moveLog[5]);

$expected_log = array('P','oldns:page09','newns:page09',true);
$this->assertSame($expected_log,$mock->moveLog[9]);

$this->assertTrue(!isset($mock->moveLog[10]), "The number of logged items is incorrect");

$opts_file = dirname(DOKU_CONF) . '/data/meta/__move_opts';
$actual_options = unserialize(io_readFile($opts_file));
$this->assertSame($expected_pages_run,$actual_options['pages_run'],'saved options are wrong');
}


}
49 changes: 49 additions & 0 deletions _test/tpl.test.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
<?php

/**
* tests for the template button of the move plugin
*
* @author Michael Große <grosse@cosmocode.de>
* @group plugin_move
* @group plugins
*/
class move_tpl_test extends DokuWikiTest {

public function setUp(): void {
parent::setUp();
}

protected $pluginsEnabled = array('move');

/**
* @coversNothing
* Integration-ish kind of test testing action_plugin_move_rename::handle_pagetools
*//*
function test_tpl () {
saveWikiText('wiki:foo:start', '[[..:..:one_ns_up:]]', 'Test setup');
idx_addPage('wiki:foo:start');
$request = new TestRequest();
$response = $request->get(array(),'/doku.php?id=wiki:foo:start');
$this->assertTrue(strstr($response->getContent(),'class="plugin_move_page"') !== false);
}*/

/**
* @covers action_plugin_move_rename::renameOkay
*/
function test_renameOkay() {
global $conf;
global $USERINFO;
$conf['superuser'] = 'john';
$_SERVER['REMOTE_USER'] = 'john';
$USERINFO['grps'] = array('admin','user');

saveWikiText('wiki:foo:start', '[[..:..:one_ns_up:]]', 'Test setup');
idx_addPage('wiki:foo:start');

$move_rename = new action_plugin_move_rename();
$this->assertTrue($move_rename->renameOkay('wiki:foo:start'));

}
}
66 changes: 66 additions & 0 deletions action/progress.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
<?php
/**
* Move Plugin AJAX handler to step through a move plan
*
* @license GPL 2 (http://www.gnu.org/licenses/gpl.html)
* @author Andreas Gohr <gohr@cosmocode.de>
*/
// must be run within Dokuwiki
if(!defined('DOKU_INC')) die();

/**
* Class action_plugin_move_progress
*/
class action_plugin_move_progress extends DokuWiki_Action_Plugin {

/**
* Register event handlers.
*
* @param Doku_Event_Handler $controller The plugin controller
*/
public function register(Doku_Event_Handler $controller) {
$controller->register_hook('AJAX_CALL_UNKNOWN', 'BEFORE', $this, 'handle_ajax');
}

/**
* Step up
*
* @param Doku_Event $event
*/
public function handle_ajax(Doku_Event $event) {
if($event->data != 'plugin_move_progress') return;
$event->preventDefault();
$event->stopPropagation();

global $INPUT;
global $USERINFO;

if(!auth_ismanager($_SERVER['REMOTE_USER'], $USERINFO['grps'])) {
http_status(403);
exit;
}

$return = array(
'error' => '',
'complete' => false,
'progress' => 0
);

/** @var helper_plugin_move_plan $plan */
$plan = plugin_load('helper', 'move_plan');

if(!$plan->isCommited()) {
// There is no plan. Something went wrong
$return['complete'] = true;
} else {
$todo = $plan->nextStep($INPUT->bool('skip'));
$return['progress'] = $plan->getProgress();
$return['error'] = $plan->getLastError();
if($todo === 0) $return['complete'] = true;
}

$json = new JSON();
header('Content-Type: application/json');
echo $json->encode($return);
}
}
221 changes: 221 additions & 0 deletions action/rename.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,221 @@
<?php
/**
* Move Plugin Page Rename Functionality
*
* @license GPL 2 (http://www.gnu.org/licenses/gpl.html)
* @author Andreas Gohr <gohr@cosmocode.de>
*/
// must be run within Dokuwiki
if(!defined('DOKU_INC')) die();

/**
* Class action_plugin_move_rename
*/
class action_plugin_move_rename extends DokuWiki_Action_Plugin {

/**
* Register event handlers.
*
* @param Doku_Event_Handler $controller The plugin controller
*/
public function register(Doku_Event_Handler $controller) {
$controller->register_hook('DOKUWIKI_STARTED', 'AFTER', $this, 'handle_init');

// TODO: DEPRECATED JAN 2018
$controller->register_hook('TEMPLATE_PAGETOOLS_DISPLAY', 'BEFORE', $this, 'handle_pagetools');

$controller->register_hook('MENU_ITEMS_ASSEMBLY', 'AFTER', $this, 'addsvgbutton', array());
$controller->register_hook('AJAX_CALL_UNKNOWN', 'BEFORE', $this, 'handle_ajax');
$controller->register_hook('AJAX_CALL_UNKNOWN', 'BEFORE', $this, 'handleAjaxMediaManager');
}

/**
* set JavaScript info if renaming of current page is possible
*/
public function handle_init() {
global $JSINFO;
global $INFO;
global $INPUT;
global $USERINFO;

if (isset($INFO['id'])) {
$JSINFO['move_renameokay'] = $this->renameOkay($INFO['id']);
} else {
$JSINFO['move_renameokay'] = false;
}

$JSINFO['move_allowrename'] = auth_isMember(
$this->getConf('allowrename'),
$INPUT->server->str('REMOTE_USER'),
$USERINFO['grps'] ?? []
);
}

/**
* Adds a button to the default template
*
* TODO: DEPRECATED JAN 2018
*
* @param Doku_Event $event
*/
public function handle_pagetools(Doku_Event $event) {
if($event->data['view'] != 'main') return;
if (!$this->getConf('pagetools_integration')) {
return;
}

$newitem = '<li class="plugin_move_page"><a href=""><span>' . $this->getLang('renamepage') . '</span></a></li>';
$offset = count($event->data['items']) - 1;
$event->data['items'] =
array_slice($event->data['items'], 0, $offset, true) +
array('plugin_move' => $newitem) +
array_slice($event->data['items'], $offset, null, true);
}

/**
* Add 'rename' button to page tools, new SVG based mechanism
*
* @param Doku_Event $event
*/
public function addsvgbutton(Doku_Event $event) {
global $INFO, $JSINFO;
if(
$event->data['view'] !== 'page' ||
!$this->getConf('pagetools_integration') ||
empty($JSINFO['move_renameokay'])
) {
return;
}
if(!$INFO['exists']) {
return;
}
array_splice($event->data['items'], -1, 0, array(new \dokuwiki\plugin\move\MenuItem()));
}

/**
* Rename a single page
*/
public function handle_ajax(Doku_Event $event) {
if($event->data != 'plugin_move_rename') return;
$event->preventDefault();
$event->stopPropagation();

global $MSG;
global $INPUT;

$src = cleanID($INPUT->str('id'));
$dst = cleanID($INPUT->str('newid'));

/** @var helper_plugin_move_op $MoveOperator */
$MoveOperator = plugin_load('helper', 'move_op');

$JSON = new JSON();

header('Content-Type: application/json');

if($this->renameOkay($src) && $MoveOperator->movePage($src, $dst)) {
// all went well, redirect
echo $JSON->encode(array('redirect_url' => wl($dst, '', true, '&')));
} else {
if(isset($MSG[0])) {
$error = $MSG[0]; // first error
} else {
$error = $this->getLang('cantrename');
}
echo $JSON->encode(array('error' => $error));
}
}

/**
* Handle media renames in media manager
*
* @param Doku_Event $event
* @return void
*/
public function handleAjaxMediaManager(Doku_Event $event)
{
if ($event->data !== 'plugin_move_rename_mediamanager') return;

if (!checkSecurityToken()) {
throw new \Exception('Security token did not match');
}

$event->preventDefault();
$event->stopPropagation();

global $INPUT;
global $MSG;
global $USERINFO;

$src = cleanID($INPUT->str('src'));
$dst = cleanID($INPUT->str('dst'));

/** @var helper_plugin_move_op $moveOperator */
$moveOperator = plugin_load('helper', 'move_op');

if ($src && $dst) {
header('Content-Type: application/json');

$response = [];

// check user/group restrictions
if (
!auth_isMember($this->getConf('allowrename'), $INPUT->server->str('REMOTE_USER'), (array) $USERINFO['grps'])
) {
$response['error'] = $this->getLang('notallowed');
echo json_encode($response);
return;
}

$response['success'] = $moveOperator->moveMedia($src, $dst);

if ($response['success']) {
$ns = getNS($dst);
$response['redirect_url'] = wl($dst, ['do' => 'media', 'ns' => $ns], true, '&');
} else {
$response['error'] = sprintf($this->getLang('mediamoveerror'), $src);
if (isset($MSG)) {
foreach ($MSG as $msg) {
$response['error'] .= ' ' . $msg['msg'];
}
}
}

echo json_encode($response);
}
}

/**
* Determines if it would be okay to show a rename page button for the given page and current user
*
* @param $id
* @return bool
*/
public function renameOkay($id) {
global $conf;
global $ACT;
global $USERINFO;
if(!($ACT == 'show' || empty($ACT))) return false;
if(!page_exists($id)) return false;
if(auth_quickaclcheck($id) < AUTH_EDIT) return false;
if(checklock($id) !== false || @file_exists(wikiLockFN($id))) return false;
if(!$conf['useacl']) return true;
if(!isset($_SERVER['REMOTE_USER'])) return false;
if(!auth_isMember($this->getConf('allowrename'), $_SERVER['REMOTE_USER'], (array) $USERINFO['grps'])) return false;

return true;
}

/**
* Use this in your template to add a simple "move this page" link
*
* Alternatively give anything the class "plugin_move_page" - it will automatically be hidden and shown and
* trigger the page move dialog.
*/
public function tpl() {
echo '<a href="" class="plugin_move_page">';
echo $this->getLang('renamepage');
echo '</a>';
}

}
112 changes: 112 additions & 0 deletions action/rewrite.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
<?php
/**
* Move Plugin Page Rewrite Functionality
*
* @license GPL 2 (http://www.gnu.org/licenses/gpl.html)
* @author Andreas Gohr <gohr@cosmocode.de>
*/
// must be run within Dokuwiki
if(!defined('DOKU_INC')) die();

/**
* Class action_plugin_move_rewrite
*/
class action_plugin_move_rewrite extends DokuWiki_Action_Plugin {

/**
* Register event handlers.
*
* @param Doku_Event_Handler $controller The plugin controller
*/
public function register(Doku_Event_Handler $controller) {
$controller->register_hook('IO_WIKIPAGE_READ', 'AFTER', $this, 'handle_read', array());
$controller->register_hook('PARSER_CACHE_USE', 'BEFORE', $this, 'handle_cache', array());
}

/**
* Rewrite pages when they are read and they need to be updated.
*
* @param Doku_Event $event The event object
* @param mixed $param Optional parameters (not used)
*/
function handle_read(Doku_Event $event, $param) {
global $ACT, $conf;
static $stack = array();
// handle only reads of the current revision
if($event->data[3]) return;

// only rewrite if not in move already
$save = true;
if(helper_plugin_move_rewrite::isLocked()) {
$save = false;
}

$id = $event->data[2];
if($event->data[1]) $id = $event->data[1] . ':' . $id;

if(!$id) {
// try to reconstruct the id from the filename
$path = $event->data[0][0];
if(strpos($path, $conf['datadir']) === 0) {
$path = substr($path, strlen($conf['datadir']) + 1);
$id = pathID($path);
}
}

if(isset($stack[$id])) return;

// Don't change the page when the user is currently changing the page content or the page is locked
$forbidden_actions = array('save', 'preview', 'recover', 'revert');
if((isset($ACT) && (
in_array($ACT, $forbidden_actions) || (is_array($ACT) && in_array(key($ACT), $forbidden_actions)
)))
// checklock checks if the page lock hasn't expired and the page hasn't been locked by another user
// the file exists check checks if the page is reported unlocked if a lock exists which means that
// the page is locked by the current user
|| checklock($id) !== false || @file_exists(wikiLockFN($id))
) return;

/** @var helper_plugin_move_rewrite $helper */
$helper = plugin_load('helper', 'move_rewrite', true);
if(!is_null($helper)) {
$stack[$id] = true;
$event->result = $helper->rewritePage($id, $event->result, $save);
unset($stack[$id]);
}
}

/**
* Handle the cache events, it looks if a page needs to be rewritten so it can expire the cache of the page
*
* @param Doku_Event $event The even object
* @param mixed $param Optional parameters (not used)
*/
function handle_cache(Doku_Event $event, $param) {
global $conf;
/** @var $cache cache_parser */
$cache = $event->data;
$id = $cache->page;
if(!$id) {
// try to reconstruct the id from the filename
$path = $cache->file;
if(strpos($path, $conf['datadir']) === 0) {
$path = substr($path, strlen($conf['datadir']) + 1);
$id = pathID($path);
}
}
if($id) {
/** @var helper_plugin_move_rewrite $helper */
$helper = $this->loadHelper('move_rewrite');
if(!is_null($helper)) {
$meta = $helper->getMoveMeta($id);
if($meta && ($meta['pages'] || $meta['media'])) {
$file = wikiFN($id, '', false);
if(is_writable($file))
$cache->depends['purge'] = true;
else // FIXME: print error here or fail silently?
msg('Error: Page ' . hsc($id) . ' needs to be rewritten because of page renames but is not writable.', -1);
}
}
}
}
}
63 changes: 63 additions & 0 deletions action/tree.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
<?php
/**
* Move Plugin Tree Loading Functionality
*
* @license GPL 2 (http://www.gnu.org/licenses/gpl.html)
* @author Andreas Gohr <gohr@cosmocode.de>
*/
// must be run within Dokuwiki
if(!defined('DOKU_INC')) die();

/**
* Class action_plugin_move_rewrite
*/
class action_plugin_move_tree extends DokuWiki_Action_Plugin {

/**
* Register event handlers.
*
* @param Doku_Event_Handler $controller The plugin controller
*/
public function register(Doku_Event_Handler $controller) {
$controller->register_hook('AJAX_CALL_UNKNOWN', 'BEFORE', $this, 'handle_ajax_call');
}

/**
* Render a subtree
*
* @param Doku_Event $event
* @param $params
*/
public function handle_ajax_call(Doku_Event $event, $params) {
if($event->data != 'plugin_move_tree') return;
$event->preventDefault();
$event->stopPropagation();

global $INPUT;
global $USERINFO;

if(!auth_ismanager($_SERVER['REMOTE_USER'], $USERINFO['grps'])) {
http_status(403);
exit;
}

/** @var admin_plugin_move_tree $plugin */
$plugin = plugin_load('admin', 'move_tree');

$ns = cleanID($INPUT->str('ns'));
if($INPUT->bool('is_media')) {
$type = admin_plugin_move_tree::TYPE_MEDIA;
} else {
$type = admin_plugin_move_tree::TYPE_PAGES;
}

$data = $plugin->tree($type, $ns, $ns);

echo html_buildlist(
$data, 'tree_list',
array($plugin, 'html_list'),
array($plugin, 'html_li')
);
}

}
965 changes: 0 additions & 965 deletions admin.php

This file was deleted.

1 change: 1 addition & 0 deletions admin.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
270 changes: 270 additions & 0 deletions admin/main.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,270 @@
<?php
/**
* Plugin : Move
*
* @license GPL 2 (http://www.gnu.org/licenses/gpl.html)
* @author Michael Hamann <michael@content-space.de>
* @author Gary Owen,
*/

// must be run within Dokuwiki
if(!defined('DOKU_INC')) die();

/**
* Admin component of the move plugin. Provides the user interface.
*/
class admin_plugin_move_main extends DokuWiki_Admin_Plugin {

/** @var helper_plugin_move_plan $plan */
protected $plan;

public function __construct() {
$this->plan = plugin_load('helper', 'move_plan');
}

/**
* @param $language
* @return string
*/
public function getMenuText($language) {
$label = $this->getLang('menu');
if($this->plan->isCommited()) $label .= ' '.$this->getLang('inprogress');
return $label;
}


/**
* Get the sort number that defines the position in the admin menu.
*
* @return int The sort number
*/
function getMenuSort() {
return 1011;
}

/**
* If this admin plugin is for admins only
*
* @return bool false
*/
function forAdminOnly() {
return false;
}

/**
* Handle the input
*/
function handle() {
global $INPUT;

// create a new plan if possible and sufficient data was given
$this->createPlanFromInput();

// handle workflow button presses
if($this->plan->isCommited()) {
helper_plugin_move_rewrite::addLock(); //todo: right place?
switch($INPUT->str('ctl')) {
case 'continue':
$this->plan->nextStep();
break;
case 'skip':
$this->plan->nextStep(true);
break;
case 'abort':
$this->plan->abort();
break;
}
}
}

/**
* Display the interface
*/
function html() {
// decide what to do based on the plan's state
if($this->plan->isCommited()) {
$this->GUI_progress();
} else {
// display form
$this->GUI_simpleForm();
}
}

/**
* Get input variables and create a move plan from them
*
* @return bool
*/
protected function createPlanFromInput() {
global $INPUT;
global $ID;

if($this->plan->isCommited()) return false;

$this->plan->setOption('autoskip', $INPUT->bool('autoskip'));
$this->plan->setOption('autorewrite', $INPUT->bool('autorewrite'));

if($ID && $INPUT->has('dst')) {
$dst = trim($INPUT->str('dst'));
if($dst == '') {
msg($this->getLang('nodst'), -1);
return false;
}

// input came from form
if($INPUT->str('class') == 'namespace') {
$src = getNS($ID);

if($INPUT->str('type') == 'both') {
$this->plan->addPageNamespaceMove($src, $dst);
$this->plan->addMediaNamespaceMove($src, $dst);
} else if($INPUT->str('type') == 'page') {
$this->plan->addPageNamespaceMove($src, $dst);
} else if($INPUT->str('type') == 'media') {
$this->plan->addMediaNamespaceMove($src, $dst);
}
} else {
$this->plan->addPageMove($ID, $INPUT->str('dst'));
}
$this->plan->commit();
return true;
} elseif($INPUT->has('json')) {
// input came via JSON from tree manager
$json = new JSON(JSON_LOOSE_TYPE);
$data = $json->decode($INPUT->str('json'));

foreach((array) $data as $entry) {
if($entry['class'] == 'ns') {
if($entry['type'] == 'page') {
$this->plan->addPageNamespaceMove($entry['src'], $entry['dst']);
} elseif($entry['type'] == 'media') {
$this->plan->addMediaNamespaceMove($entry['src'], $entry['dst']);
}
} elseif($entry['class'] == 'doc') {
if($entry['type'] == 'page') {
$this->plan->addPageMove($entry['src'], $entry['dst']);
} elseif($entry['type'] == 'media') {
$this->plan->addMediaMove($entry['src'], $entry['dst']);
}
}
}

$this->plan->commit();
return true;
}

return false;
}

/**
* Display the simple move form
*/
protected function GUI_simpleForm() {
global $ID;

echo $this->locale_xhtml('move');

$treelink = wl($ID, array('do'=>'admin', 'page'=>'move_tree'));
echo '<p id="plugin_move__treelink">';
printf($this->getLang('treelink'), $treelink);
echo '</p>';

$form = new Doku_Form(array('action' => wl($ID), 'method' => 'post', 'class' => 'plugin_move_form'));
$form->addHidden('page', 'move_main');
$form->addHidden('id', $ID);

$form->startFieldset($this->getLang('legend'));

$form->addElement(form_makeRadioField('class', 'page', $this->getLang('movepage') . ' <code>' . $ID . '</code>', '', 'block radio click-page', array('checked' => 'checked')));
$form->addElement(form_makeRadioField('class', 'namespace', $this->getLang('movens') . ' <code>' . getNS($ID) . '</code>', '', 'block radio click-ns'));

$form->addElement(form_makeTextField('dst', $ID, $this->getLang('dst'), '', 'block indent'));
$form->addElement(form_makeMenuField('type', array('pages' => $this->getLang('move_pages'), 'media' => $this->getLang('move_media'), 'both' => $this->getLang('move_media_and_pages')), 'both', $this->getLang('content_to_move'), '', 'block indent select'));

$form->addElement(form_makeCheckboxField('autoskip', '1', $this->getLang('autoskip'), '', 'block', ($this->getConf('autoskip') ? array('checked' => 'checked') : array())));
$form->addElement(form_makeCheckboxField('autorewrite', '1', $this->getLang('autorewrite'), '', 'block', ($this->getConf('autorewrite') ? array('checked' => 'checked') : array())));

$form->addElement(form_makeButton('submit', 'admin', $this->getLang('btn_start')));
$form->endFieldset();
$form->printForm();
}

/**
* Display the GUI while the move progresses
*/
protected function GUI_progress() {
echo '<div id="plugin_move__progress">';

echo $this->locale_xhtml('progress');

$progress = $this->plan->getProgress();

if(!$this->plan->inProgress()) {
echo '<div id="plugin_move__preview">';
echo '<p>';
echo '<strong>' . $this->getLang('intro') . '</strong> ';
echo '<span>' . $this->getLang('preview') . '</span>';
echo '</p>';
echo $this->plan->previewHTML();
echo '</div>';

}

echo '<div class="progress" data-progress="' . $progress . '">' . $progress . '%</div>';

echo '<div class="output">';
if($this->plan->getLastError()) {
echo '<p><div class="error">' . $this->plan->getLastError() . '</div></p>';
} elseif ($this->plan->inProgress()) {
echo '<p><div class="info">' . $this->getLang('inexecution') . '</div></p>';
}
echo '</div>';

// display all buttons but toggle visibility according to state
echo '<p></p>';
echo '<div class="controls">';
echo '<img src="' . DOKU_BASE . 'lib/images/throbber.gif" class="hide" />';
$this->btn('start', !$this->plan->inProgress());
$this->btn('retry', $this->plan->getLastError());
$this->btn('skip', $this->plan->getLastError());
$this->btn('continue', $this->plan->inProgress() && !$this->plan->getLastError());
$this->btn('abort');
echo '</div>';

echo '</div>';
}

/**
* Display a move workflow button
*
* continue, start, retry - continue next steps
* abort - abort the whole move
* skip - skip error and continue
*
* @param string $control
* @param bool $show should this control be visible?
*/
protected function btn($control, $show = true) {
global $ID;

$skip = 0;
$label = $this->getLang('btn_' . $control);
$id = $control;
if($control == 'start') $control = 'continue';
if($control == 'retry') {
$control = 'continue';
$skip = 0;
}

$class = 'move__control ctlfrm-' . $id;
if(!$show) $class .= ' hide';

$form = new Doku_Form(array('action' => wl($ID), 'method' => 'post', 'class' => $class));
$form->addHidden('page', 'move_main');
$form->addHidden('id', $ID);
$form->addHidden('ctl', $control);
$form->addHidden('skip', $skip);
$form->addElement(form_makeButton('submit', 'admin', $label, array('class' => 'btn ctl-' . $control)));
$form->printForm();
}
}
189 changes: 189 additions & 0 deletions admin/tree.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,189 @@
<?php

class admin_plugin_move_tree extends DokuWiki_Admin_Plugin {

const TYPE_PAGES = 1;
const TYPE_MEDIA = 2;

/**
* @param $language
* @return bool
*/
public function getMenuText($language) {
return false; // do not show in Admin menu
}


/**
* If this admin plugin is for admins only
*
* @return bool false
*/
function forAdminOnly() {
return false;
}

/**
* no-op
*/
public function handle() {
}

public function html() {
global $ID;

echo $this->locale_xhtml('tree');

echo '<noscript><div class="error">' . $this->getLang('noscript') . '</div></noscript>';

echo '<div id="plugin_move__tree">';

echo '<div class="tree_root tree_pages">';
echo '<h3>' . $this->getLang('move_pages') . '</h3>';
$this->htmlTree(self::TYPE_PAGES);
echo '</div>';

echo '<div class="tree_root tree_media">';
echo '<h3>' . $this->getLang('move_media') . '</h3>';
$this->htmlTree(self::TYPE_MEDIA);
echo '</div>';

/** @var helper_plugin_move_plan $plan */
$plan = plugin_load('helper', 'move_plan');
echo '<div class="controls">';
if($plan->isCommited()) {
echo '<div class="error">' . $this->getLang('moveinprogress') . '</div>';
} else {
$form = new Doku_Form(array('action' => wl($ID), 'id' => 'plugin_move__tree_execute'));
$form->addHidden('id', $ID);
$form->addHidden('page', 'move_main');
$form->addHidden('json', '');
$form->addElement(form_makeCheckboxField('autoskip', '1', $this->getLang('autoskip'), '', '', ($this->getConf('autoskip') ? array('checked' => 'checked') : array())));
$form->addElement('<br />');
$form->addElement(form_makeCheckboxField('autorewrite', '1', $this->getLang('autorewrite'), '', '', ($this->getConf('autorewrite') ? array('checked' => 'checked') : array())));
$form->addElement('<br />');
$form->addElement('<br />');
$form->addElement(form_makeButton('submit', 'admin', $this->getLang('btn_start')));
$form->printForm();
}
echo '</div>';

echo '</div>';
}

/**
* print the HTML tree structure
*
* @param int $type
*/
protected function htmlTree($type = self::TYPE_PAGES) {
$data = $this->tree($type);

// wrap a list with the root level around the other namespaces
array_unshift(
$data, array(
'level' => 0, 'id' => '*', 'type' => 'd',
'open' => 'true', 'label' => $this->getLang('root')
)
);
echo html_buildlist(
$data, 'tree_list idx',
array($this, 'html_list'),
array($this, 'html_li')
);
}

/**
* Build a tree info structure from media or page directories
*
* @param int $type
* @param string $open The hierarchy to open FIXME not supported yet
* @param string $base The namespace to start from
* @return array
*/
public function tree($type = self::TYPE_PAGES, $open = '', $base = '') {
global $conf;

$opendir = utf8_encodeFN(str_replace(':', '/', $open));
$basedir = utf8_encodeFN(str_replace(':', '/', $base));

$opts = array(
'pagesonly' => ($type == self::TYPE_PAGES),
'listdirs' => true,
'listfiles' => true,
'sneakyacl' => $conf['sneaky_index'],
'showmsg' => false,
'depth' => 1,
'showhidden' => true
);

$data = array();
if($type == self::TYPE_PAGES) {
search($data, $conf['datadir'], 'search_universal', $opts, $basedir);
} elseif($type == self::TYPE_MEDIA) {
search($data, $conf['mediadir'], 'search_universal', $opts, $basedir);
}

return $data;
}

/**
* Item formatter for the tree view
*
* User function for html_buildlist()
*
* @author Andreas Gohr <andi@splitbrain.org>
*/
function html_list($item) {
$ret = '';
// what to display
if(!empty($item['label'])) {
$base = $item['label'];
} else {
$base = ':' . $item['id'];
$base = substr($base, strrpos($base, ':') + 1);
}

if($item['id'] == '*') $item['id'] = '';

if ($item['id']) {
$ret .= '<input type="checkbox" /> ';
}

// namespace or page?
if($item['type'] == 'd') {
$ret .= '<a href="' . $item['id'] . '" class="idx_dir">';
$ret .= $base;
$ret .= '</a>';
} else {
$ret .= '<a class="wikilink1">';
$ret .= noNS($item['id']);
$ret .= '</a>';
}

if($item['id']) $ret .= '<img class="rename" src="'. DOKU_BASE .'lib/plugins/move/images/rename.png" />';
else $ret .= '<img class="add" src="' . DOKU_BASE . 'lib/plugins/move/images/folder_add.png" />';

return $ret;
}

/**
* print the opening LI for a list item
*
* @param array $item
* @return string
*/
function html_li($item) {
if($item['id'] == '*') $item['id'] = '';

$params = array();
$params['class'] = ' type-' . $item['type'];
if($item['type'] == 'd') $params['class'] .= ' ' . ($item['open'] ? 'open' : 'closed');
$params['data-name'] = noNS($item['id']);
$params['data-id'] = $item['id'];
$attr = buildAttributes($params);

return "<li $attr>";
}

}
7 changes: 7 additions & 0 deletions conf/default.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<?php

$conf['allowrename'] = '@user';
$conf['minor'] = 1;
$conf['autoskip'] = 0;
$conf['autorewrite'] = 1;
$conf['pagetools_integration'] = 1;
7 changes: 7 additions & 0 deletions conf/metadata.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<?php

$meta['allowrename'] = array('string');
$meta['minor'] = array('onoff');
$meta['autoskip'] = array('onoff');
$meta['autorewrite'] = array('onoff');
$meta['pagetools_integration'] = array('onoff');
34 changes: 34 additions & 0 deletions deleted.files
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# This is a list of files that were present in previous plugin releases
# but were removed later. An up to date plugin should not have any of
# the files installed
_action.php
action.php
admin.php
helper.php
rename.png
sprite.png

_test/plugin_move_cache_handling.test.php
_test/mediaindex.test.php
helper/general.php
admin/simple.php
script/move.js

lang/cs/pagemove.txt
lang/de/pagemove.txt
lang/en/pagemove.txt
lang/es/pagemove.txt
lang/fr/pagemove.txt
lang/lv/pagemove.txt
lang/nl/pagemove.txt
lang/pl/pagemove.txt
lang/ru/pagemove.txt
lang/sl/pagemove.txt
lang/cs/lang.php.txt
lang/cs/pagemove.txt.txt
lang/es/lang.php.txt
lang/es/pagemove.txt.txt
lang/pl/lang.php.txt
lang/pl/pagemove.txt.txt
lang/zh/pagemove.txt
.travis.yml
156 changes: 156 additions & 0 deletions helper/file.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
<?php
/**
* Move Plugin File Mover
*
* @license GPL 2 (http://www.gnu.org/licenses/gpl.html)
* @author Michael Hamann <michael@content-space.de>
* @author Andreas Gohr <gohr@cosmocode.de>
*/
// must be run within Dokuwiki
if(!defined('DOKU_INC')) die();

/**
* Class helper_plugin_move_file
*
* This helps with moving files from one folder to another. It simply matches files and moves them
* arround. No fancy rewriting happens here.
*/
class helper_plugin_move_file extends DokuWiki_Plugin {

/**
* Move the meta files of a page
*
* @param string $src_ns The original namespace
* @param string $src_name The original basename of the moved doc (empty for namespace moves)
* @param string $dst_ns The namespace after the move
* @param string $dst_name The basename after the move (empty for namespace moves)
* @return bool If the meta files were moved successfully
*/
public function movePageMeta($src_ns, $src_name, $dst_ns, $dst_name) {
global $conf;

$regex = '\.[^.]+';
return $this->execute($conf['metadir'], $src_ns, $src_name, $dst_ns, $dst_name, $regex);
}

/**
* Move the old revisions of a page
*
* @param string $src_ns The original namespace
* @param string $src_name The original basename of the moved doc (empty for namespace moves)
* @param string $dst_ns The namespace after the move
* @param string $dst_name The basename after the move (empty for namespace moves)
* @return bool If the attic files were moved successfully
*/
public function movePageAttic($src_ns, $src_name, $dst_ns, $dst_name) {
global $conf;

$regex = '\.\d+\.txt(?:\.gz|\.bz2)?';
return $this->execute($conf['olddir'], $src_ns, $src_name, $dst_ns, $dst_name, $regex);
}

/**
* Move the meta files of the page that is specified in the options.
*
* @param string $src_ns The original namespace
* @param string $src_name The original basename of the moved doc (empty for namespace moves)
* @param string $dst_ns The namespace after the move
* @param string $dst_name The basename after the move (empty for namespace moves)
* @return bool If the meta files were moved successfully
*/
public function moveMediaMeta($src_ns, $src_name, $dst_ns, $dst_name) {
global $conf;

$regex = '\.[^.]+';
return $this->execute($conf['mediametadir'], $src_ns, $src_name, $dst_ns, $dst_name, $regex);
}

/**
* Move the old revisions of the media file that is specified in the options
*
* @param string $src_ns The original namespace
* @param string $src_name The original basename of the moved doc (empty for namespace moves)
* @param string $dst_ns The namespace after the move
* @param string $dst_name The basename after the move (empty for namespace moves)
* @return bool If the attic files were moved successfully
*/
public function moveMediaAttic($src_ns, $src_name, $dst_ns, $dst_name) {
global $conf;

$ext = mimetype($src_name);
if($ext[0] !== false) {
$name = substr($src_name, 0, -1 * strlen($ext[0]) - 1);
} else {
$name = $src_name;
}
$newext = mimetype($dst_name);
if($newext[0] !== false) {
$newname = substr($dst_name, 0, -1 * strlen($newext[0]) - 1);
} else {
$newname = $dst_name;
}
$regex = '\.\d+\.' . preg_quote((string) $ext[0], '/');

return $this->execute($conf['mediaolddir'], $src_ns, $name, $dst_ns, $newname, $regex);
}

/**
* Moves the subscription file for a namespace
*
* @param string $src_ns
* @param string $dst_ns
* @return bool
*/
public function moveNamespaceSubscription($src_ns, $dst_ns){
global $conf;

$regex = '\.mlist';
return $this->execute($conf['metadir'], $src_ns, '', $dst_ns, '', $regex);
}

/**
* Executes the move op
*
* @param string $dir The root path of the files (e.g. $conf['metadir'] or $conf['olddir']
* @param string $src_ns The original namespace
* @param string $src_name The original basename of the moved doc (empty for namespace moves)
* @param string $dst_ns The namespace after the move
* @param string $dst_name The basename after the move (empty for namespace moves)
* @param string $extregex Regular expression for matching the extension of the file that shall be moved
* @return bool If the files were moved successfully
*/
protected function execute($dir, $src_ns, $src_name, $dst_ns, $dst_name, $extregex) {
$old_path = $dir;
if($src_ns != '') $old_path .= '/' . utf8_encodeFN(str_replace(':', '/', $src_ns));
$new_path = $dir;
if($dst_ns != '') $new_path .= '/' . utf8_encodeFN(str_replace(':', '/', $dst_ns));
$regex = '/^' . preg_quote(utf8_encodeFN($src_name)) . '(' . $extregex . ')$/u';

if(!is_dir($old_path)) return true; // no media files found

$dh = @opendir($old_path);
if($dh) {
while(($file = readdir($dh)) !== false) {
if($file == '.' || $file == '..') continue;
$match = array();
if(is_file($old_path . '/' . $file) && preg_match($regex, $file, $match)) {
if(!is_dir($new_path)) {
if(!io_mkdir_p($new_path)) {
msg('Creating directory ' . hsc($new_path) . ' failed.', -1);
return false;
}
}
if(!io_rename($old_path . '/' . $file, $new_path . '/' . utf8_encodeFN($dst_name . $match[1]))) {
msg('Moving ' . hsc($old_path . '/' . $file) . ' to ' . hsc($new_path . '/' . utf8_encodeFN($dst_name . $match[1])) . ' failed.', -1);
return false;
}
}
}
closedir($dh);
} else {
msg('Directory ' . hsc($old_path) . ' couldn\'t be opened.', -1);
return false;
}
return true;
}
}
387 changes: 387 additions & 0 deletions helper/handler.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,387 @@
<?php
/**
* Move Plugin Rewriting Handler
*
* @license GPL 2 (http://www.gnu.org/licenses/gpl.html)
* @author Michael Hamann <michael@content-space.de>
*/

// must be run within Dokuwiki
if(!defined('DOKU_INC')) die();

/**
* Handler class for move. It does the actual rewriting of the content.
*
* Note: This is not actually a valid DokuWiki Helper plugin and can not be loaded via plugin_load()
*/
class helper_plugin_move_handler extends DokuWiki_Plugin {
public $calls = '';

protected $id;
protected $ns;
protected $origID;
protected $origNS;
protected $page_moves;
protected $media_moves;
protected $handlers;

/**
* Do not allow re-using instances.
*
* @return bool false - the handler must not be re-used.
*/
public function isSingleton() {
return false;
}

/**
* Initialize the move handler.
*
* @param string $id The id of the text that is passed to the handler
* @param string $original The name of the original ID of this page. Same as $id if this page wasn't moved
* @param array $page_moves Moves that shall be considered in the form [[$old,$new],...] ($old can be $original)
* @param array $media_moves Moves of media files that shall be considered in the form $old => $new
* @param array $handlers Handlers for plugin content in the form $plugin_name => $callback
*/
public function init($id, $original, $page_moves, $media_moves, $handlers) {
$this->id = $id;
$this->ns = getNS($id);
$this->origID = $original;
$this->origNS = getNS($original);
$this->page_moves = $page_moves;
$this->media_moves = $media_moves;
$this->handlers = $handlers;
}

/**
* Go through the list of moves and find the new value for the given old ID
*
* @param string $old the old, full qualified ID
* @param string $type 'media' or 'page'
* @throws Exception on bad argument
* @return string the new full qualified ID
*/
public function resolveMoves($old, $type) {
global $conf;

if($type != 'media' && $type != 'page') throw new Exception('Not a valid type');

$old = resolve_id($this->origNS, $old, false);

if($type == 'page') {
// FIXME this simply assumes that the link pointed to :$conf['start'], but it could also point to another page
// resolve_pageid does a lot more here, but we can't really assume this as the original pages might have been
// deleted already
if(substr($old, -1) === ':' || $old === '') $old .= $conf['start'];

$moves = $this->page_moves;
} else {
$moves = $this->media_moves;
}

$old = cleanID($old);

foreach($moves as $move) {
if($move[0] == $old) {
$old = $move[1];
}
}

return $old; // this is now new
}

/**
* if the old link ended with a colon and the new one is a start page, adjust
*
* @param $relold string the old, possibly relative ID
* @param $new string the new, full qualified ID
* @param $type 'media' or 'page'
* @return string
*/
protected function _nsStartCheck($relold, $new, $type) {
global $conf;
if($type == 'page' && substr($relold, -1) == ':') {
$len = strlen($conf['start']);
if($new == $conf['start']) {
$new = '.:';
} else if(substr($new, -1 * ($len + 1)) == ':' . $conf['start']) {
$new = substr($new, 0, -1 * $len);
}
}
return $new;
}

/**
* Construct a new ID relative to the current page's location
*
* Uses a relative link only if the original was relative, too. This function is for
* pages and media files.
*
* @param string $relold the old, possibly relative ID
* @param string $new the new, full qualified ID
* @param string $type 'media' or 'page'
* @throws Exception on bad argument
* @return string
*/
public function relativeLink($relold, $new, $type) {
global $conf;
if($type != 'media' && $type != 'page') throw new Exception('Not a valid type');

// first check if the old link still resolves
$exists = false;
$old = $relold;
if($type == 'page') {
resolve_pageid($this->ns, $old, $exists);
// Work around bug in DokuWiki 2020-07-29 where resolve_pageid doesn't append the start page to a link to
// the root.
if ($old === '') {
$old = $conf['start'];
}
} else {
resolve_mediaid($this->ns, $old, $exists);
}
if($old == $new) {
return $relold; // old link still resolves, keep as is
}

if($conf['useslash']) $relold = str_replace('/', ':', $relold);

// check if the link was relative
if(strpos($relold, ':') === false ||$relold[0] == '.') {
$wasrel = true;
} else {
$wasrel = false;
}

// if it wasn't relative then, leave it absolute now, too
if(!$wasrel) {
if($this->ns && !getNS($new)) $new = ':' . $new;
$new = $this->_nsStartCheck($relold, $new, $type);
return $new;
}

// split the paths and see how much common parts there are
$selfpath = explode(':', $this->ns);
$goalpath = explode(':', getNS($new));
$min = min(count($selfpath), count($goalpath));
for($common = 0; $common < $min; $common++) {
if($selfpath[$common] != $goalpath[$common]) break;
}

// we now have the non-common part and a number of uppers
$ups = max(count($selfpath) - $common, 0);
$remainder = array_slice($goalpath, $common);
$upper = $ups ? array_fill(0, $ups, '..:') : array();

// build the new relative path
$newrel = join(':', $upper);
if($remainder) $newrel .= join(':', $remainder) . ':';
$newrel .= noNS($new);
$newrel = str_replace('::', ':', trim($newrel, ':'));
if($newrel[0] != '.' && $this->ns && getNS($newrel)) $newrel = '.' . $newrel;

// if the old link ended with a colon and the new one is a start page, adjust
$newrel = $this->_nsStartCheck($relold,$newrel,$type);

// don't use relative paths if it is ridicoulus:
if(strlen($newrel) > strlen($new)) {
$newrel = $new;
if($this->ns && !getNS($new)) $newrel = ':' . $newrel;
$newrel = $this->_nsStartCheck($relold,$newrel,$type);
}

return $newrel;
}

/**
* Handle camelcase links
*
* @param string $match The text match
* @param string $state The starte of the parser
* @param int $pos The position in the input
* @return bool If parsing should be continued
*/
public function camelcaselink($match, $state, $pos) {
$oldID = cleanID($this->origNS . ':' . $match);
$newID = $this->resolveMoves($oldID, 'page');
$newNS = getNS($newID);

if($oldID == $newID || $this->origNS == $newNS) {
// link is still valid as is
$this->calls .= $match;
} else {
if(noNS($oldID) == noNS($newID)) {
// only namespace changed, keep CamelCase in link
$this->calls .= "[[$newNS:$match]]";
} else {
// all new, keep CamelCase in title
$this->calls .= "[[$newID|$match]]";
}
}
return true;
}

/**
* Handle rewriting of internal links
*
* @param string $match The text match
* @param string $state The starte of the parser
* @param int $pos The position in the input
* @return bool If parsing should be continued
*/
public function internallink($match, $state, $pos) {
// Strip the opening and closing markup
$link = preg_replace(array('/^\[\[/', '/\]\]$/u'), '', $match);

// Split title from URL
$link = explode('|', $link, 2);
if(!isset($link[1])) {
$link[1] = null;
} else if(preg_match('/^\{\{[^\}]+\}\}$/', $link[1])) {
// If the title is an image, rewrite it
$old_title = $link[1];
$link[1] = $this->rewrite_media($link[1]);
// do a simple replace of the first match so really only the id is changed and not e.g. the alignment
$oldpos = strpos($match, $old_title);
$oldlen = strlen($old_title);
$match = substr_replace($match, $link[1], $oldpos, $oldlen);
}
$link[0] = trim($link[0]);

//decide which kind of link it is

if(preg_match('/^[a-zA-Z0-9\.]+>{1}.*$/u', $link[0])) {
// Interwiki
$this->calls .= $match;
} elseif(preg_match('/^\\\\\\\\[^\\\\]+?\\\\/u', $link[0])) {
// Windows Share
$this->calls .= $match;
} elseif(preg_match('#^([a-z0-9\-\.+]+?)://#i', $link[0])) {
// external link (accepts all protocols)
$this->calls .= $match;
} elseif(preg_match('<' . PREG_PATTERN_VALID_EMAIL . '>', $link[0])) {
// E-Mail (pattern above is defined in inc/mail.php)
$this->calls .= $match;
} elseif(preg_match('!^#.+!', $link[0])) {
// local hash link
$this->calls .= $match;
} else {
$id = $link[0];

$hash = '';
$parts = explode('#', $id, 2);
if(count($parts) === 2) {
$id = $parts[0];
$hash = $parts[1];
}

$params = '';
$parts = explode('?', $id, 2);
if(count($parts) === 2) {
$id = $parts[0];
$params = $parts[1];
}

$new_id = $this->resolveMoves($id, 'page');
$new_id = $this->relativeLink($id, $new_id, 'page');

if($id == $new_id) {
$this->calls .= $match;
} else {
if($params !== '') {
$new_id .= '?' . $params;
}

if($hash !== '') {
$new_id .= '#' . $hash;
}

if($link[1] != null) {
$new_id .= '|' . $link[1];
}

$this->calls .= '[[' . $new_id . ']]';
}

}

return true;
}

/**
* Handle rewriting of media links
*
* @param string $match The text match
* @param string $state The starte of the parser
* @param int $pos The position in the input
* @return bool If parsing should be continued
*/
public function media($match, $state, $pos) {
$this->calls .= $this->rewrite_media($match);
return true;
}

/**
* Rewrite a media syntax
*
* @param string $match The text match of the media syntax
* @return string The rewritten syntax
*/
protected function rewrite_media($match) {
$p = Doku_Handler_Parse_Media($match);
if($p['type'] == 'internalmedia') { // else: external media

$new_src = $this->resolveMoves($p['src'], 'media');
$new_src = $this->relativeLink($p['src'], $new_src, 'media');

if($new_src !== $p['src']) {
// do a simple replace of the first match so really only the id is changed and not e.g. the alignment
$srcpos = strpos($match, $p['src']);
$srclen = strlen($p['src']);
return substr_replace($match, $new_src, $srcpos, $srclen);
}
}
return $match;
}

/**
* Handle rewriting of plugin syntax, calls the registered handlers
*
* @param string $match The text match
* @param string $state The starte of the parser
* @param int $pos The position in the input
* @param string $pluginname The name of the plugin
* @return bool If parsing should be continued
*/
public function plugin($match, $state, $pos, $pluginname) {
if(isset($this->handlers[$pluginname])) {
$this->calls .= call_user_func($this->handlers[$pluginname], $match, $state, $pos, $pluginname, $this);
} else {
$this->calls .= $match;
}
return true;
}

/**
* Catchall handler for the remaining syntax
*
* @param string $name Function name that was called
* @param array $params Original parameters
* @return bool If parsing should be continue
*/
public function __call($name, $params) {
if(count($params) == 3) {
$this->calls .= $params[0];
return true;
} else {
trigger_error('Error, handler function ' . hsc($name) . ' with ' . count($params) . ' parameters called which isn\'t implemented', E_USER_ERROR);
return false;
}
}

public function _finalize() {
// remove padding that is added by the parser in parse()
$this->calls = substr($this->calls, 1, -1);
}

}
323 changes: 323 additions & 0 deletions helper/op.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,323 @@
<?php
/**
* Move Plugin Operation Execution
*
* @license GPL 2 (http://www.gnu.org/licenses/gpl.html)
* @author Michael Hamann <michael@content-space.de>
* @author Gary Owen <gary@isection.co.uk>
* @author Andreas Gohr <gohr@cosmocode.de>
*/
// must be run within Dokuwiki
if(!defined('DOKU_INC')) die();

class helper_plugin_move_op extends DokuWiki_Plugin {

/**
* @var string symbol to make move operations easily recognizable in change log
*/
public $symbol = '';

/**
* @var array stores the affected pages of the last operation
*/
protected $affectedPages = array();

/**
* Check if the given page can be moved to the given destination
*
* @param $src
* @param $dst
* @return bool
*/
public function checkPage($src, $dst) {
// Check we have rights to move this document
if(!page_exists($src)) {
msg(sprintf($this->getLang('notexist'), $src), -1);
return false;
}
if(auth_quickaclcheck($src) < AUTH_EDIT) {
msg(sprintf($this->getLang('norights'), $src), -1);
return false;
}

// Check file is not locked
// checklock checks if the page lock hasn't expired and the page hasn't been locked by another user
// the file exists check checks if the page is reported unlocked if a lock exists which means that
// the page is locked by the current user
if(checklock($src) !== false || @file_exists(wikiLockFN($src))) {
msg(sprintf($this->getLang('filelocked'), $src), -1);
return false;
}

// Has the document name and/or namespace changed?
if($src == $dst) {
msg(sprintf($this->getLang('notchanged'), $src), -1);
return false;
}

// Check the page does not already exist
if(page_exists($dst)) {
msg(sprintf($this->getLang('exists'), $src, $dst), -1);
return false;
}

// Check if the current user can create the new page
if(auth_quickaclcheck($dst) < AUTH_CREATE) {
msg(sprintf($this->getLang('notargetperms'), $dst), -1);
return false;
}

return true;
}

/**
* Check if the given media file can be moved to the given destination
*
* @param $src
* @param $dst
* @return bool
*/
public function checkMedia($src, $dst) {
// Check we have rights to move this document
if(!file_exists(mediaFN($src))) {
msg(sprintf($this->getLang('medianotexist'), $src), -1);
return false;
}
if(auth_quickaclcheck($src) < AUTH_DELETE) {
msg(sprintf($this->getLang('nomediarights'), $src), -1);
return false;
}

// Has the document name and/or namespace changed?
if($src == $dst) {
msg(sprintf($this->getLang('medianotchanged'), $src), -1);
return false;
}

// Check the page does not already exist
if(@file_exists(mediaFN($dst))) {
msg(sprintf($this->getLang('mediaexists'), $src, $dst), -1);
return false;
}

// Check if the current user can create the new page
if(auth_quickaclcheck($dst) < AUTH_UPLOAD) {
msg(sprintf($this->getLang('nomediatargetperms'), $dst), -1);
return false;
}

// check if the file extension is unchanged
if (pathinfo(mediaFN($src), PATHINFO_EXTENSION) !== pathinfo(mediaFN($dst), PATHINFO_EXTENSION)) {
msg($this->getLang('extensionchange'), -1);
return false;
}

return true;
}

/**
* Execute a page move/rename
*
* @param string $src original ID
* @param string $dst new ID
* @return bool
*/
public function movePage($src, $dst) {
if(!$this->checkPage($src, $dst)) return false;

// lock rewrites
helper_plugin_move_rewrite::addLock();

/** @var helper_plugin_move_rewrite $Rewriter */
$Rewriter = plugin_load('helper', 'move_rewrite');

// remember what this page was called before the move in meta data
$Rewriter->setSelfMoveMeta($src);

// ft_backlinks() is not used here, as it does a hidden page and acl check but we really need all pages
$affected_pages = idx_get_indexer()->lookupKey('relation_references', $src);
$affected_pages[] = $dst; // the current page is always affected, because all relative links may have changed
$affected_pages = array_unique($affected_pages);

$src_ns = getNS($src);
$src_name = noNS($src);
$dst_ns = getNS($dst);
$dst_name = noNS($dst);

// pass this info on to other plugins
$eventdata = array(
// this is for compatibility to old plugin
'opts' => array(
'ns' => $src_ns,
'name' => $src_name,
'newns' => $dst_ns,
'newname' => $dst_name,
),
'affected_pages' => &$affected_pages,
'src_id' => $src,
'dst_id' => $dst,
);

// give plugins the option to add their own meta files to the list of files that need to be moved
// to the oldfiles/newfiles array or to adjust their own metadata, database, ...
// and to add other pages to the affected pages
$event = new Doku_Event('PLUGIN_MOVE_PAGE_RENAME', $eventdata);
if($event->advise_before()) {
lock($src);

/** @var helper_plugin_move_file $FileMover */
$FileMover = plugin_load('helper', 'move_file');

// Move the Subscriptions & Indexes (new feature since Spring 2013 release)
$Indexer = idx_get_indexer();
if(($idx_msg = $Indexer->renamePage($src, $dst)) !== true
|| ($idx_msg = $Indexer->renameMetaValue('relation_references', $src, $dst)) !== true
) {
msg(sprintf($this->getLang('indexerror'), $idx_msg), -1);
return false;
}
if(!$FileMover->movePageMeta($src_ns, $src_name, $dst_ns, $dst_name)) {
msg(sprintf($this->getLang('metamoveerror'), $src), -1);
return false;
}

// prepare the summary for the changelog entry
if($src_ns == $dst_ns) {
$lang_key = 'renamed';
} elseif($src_name == $dst_name) {
$lang_key = 'moved';
} else {
$lang_key = 'move_rename';
}
$summary = $this->symbol . ' ' . sprintf($this->getLang($lang_key), $src, $dst);

// Wait a second when the page has just been rewritten
$oldRev = filemtime(wikiFN($src));
if($oldRev == time()) sleep(1);

// Save the updated document in its new location
$text = rawWiki($src);
saveWikiText($dst, $text, $summary);

// Delete the orginal file
if(@file_exists(wikiFN($dst))) {
saveWikiText($src, '', $summary);
}

// Move the old revisions
if(!$FileMover->movePageAttic($src_ns, $src_name, $dst_ns, $dst_name)) {
// it's too late to stop the move, so just display a message.
msg(sprintf($this->getLang('atticmoveerror'), $src ), -1);
}

// Add meta data to all affected pages, so links get updated later
foreach($affected_pages as $id) {
$Rewriter->setMoveMeta($id, $src, $dst, 'pages');
}

unlock($src);
}
$event->advise_after();

// store this for later use
$this->affectedPages = $affected_pages;

// unlock rewrites
helper_plugin_move_rewrite::removeLock();

return true;
}

/**
* Execute a media file move/rename
*
* @param string $src original ID
* @param string $dst new ID
* @return bool true if the move was successfully executed
*/
public function moveMedia($src, $dst) {
if(!$this->checkMedia($src, $dst)) return false;

// get all pages using this media
$affected_pages = idx_get_indexer()->lookupKey('relation_media', $src);

$src_ns = getNS($src);
$src_name = noNS($src);
$dst_ns = getNS($dst);
$dst_name = noNS($dst);

// pass this info on to other plugins
$eventdata = array(
// this is for compatibility to old plugin
'opts' => array(
'ns' => $src_ns,
'name' => $src_name,
'newns' => $dst_ns,
'newname' => $dst_name,
),
'affected_pages' => &$affected_pages,
'src_id' => $src,
'dst_id' => $dst,
);

// give plugins the option to add their own meta files to the list of files that need to be moved
// to the oldfiles/newfiles array or to adjust their own metadata, database, ...
// and to add other pages to the affected pages
$event = new Doku_Event('PLUGIN_MOVE_MEDIA_RENAME', $eventdata);
if($event->advise_before()) {
/** @var helper_plugin_move_file $FileMover */
$FileMover = plugin_load('helper', 'move_file');
/** @var helper_plugin_move_rewrite $Rewriter */
$Rewriter = plugin_load('helper', 'move_rewrite');

// Move the Subscriptions & Indexes (new feature since Spring 2013 release)
$Indexer = idx_get_indexer();
if(($idx_msg = $Indexer->renameMetaValue('relation_media', $src, $dst)) !== true) {
msg(sprintf($this->getLang('indexerror'), $idx_msg), -1);
return false;
}
if(!$FileMover->moveMediaMeta($src_ns, $src_name, $dst_ns, $dst_name)) {
msg(sprintf($this->getLang('mediametamoveerror'), $src), -1);
return false;
}

// prepare directory
io_createNamespace($dst, 'media');

// move it FIXME this does not create a changelog entry!
if(!io_rename(mediaFN($src), mediaFN($dst))) {
msg(sprintf($this->getLang('mediamoveerror'), $src), -1);
return false;
}

// clean up old ns
io_sweepNS($src, 'mediadir');

// Move the old revisions
if(!$FileMover->moveMediaAttic($src_ns, $src_name, $dst_ns, $dst_name)) {
// it's too late to stop the move, so just display a message.
msg(sprintf($this->getLang('mediaatticmoveerror'), $src), -1);
}

// Add meta data to all affected pages, so links get updated later
foreach($affected_pages as $id) {
$Rewriter->setMoveMeta($id, $src, $dst, 'media');
}
}
$event->advise_after();

// store this for later use
$this->affectedPages = $affected_pages;

return true;
}

/**
* Get a list of pages that where affected by the last successful move operation
*
* @return array
*/
public function getAffectedPages() {
return $this->affectedPages;
}
}
Loading